mutt stable branch with some hacks
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add markdown2html contrib script.

This was contributed by martin f. kraft, to give an example script for
the new $send_multipart_alternative_filter funtionality.

+310 -1
+1 -1
contrib/Makefile.am
··· 6 6 sample.mailcap sample.muttrc sample.muttrc-sidebar sample.muttrc-tlr \ 7 7 sample.muttrc-compress sample.muttrc-starter \ 8 8 sample.vimrc-sidebar colors.default colors.linux smime.rc \ 9 - ca-bundle.crt smime_keys_test.pl mutt_xtitle 9 + ca-bundle.crt smime_keys_test.pl mutt_xtitle markdown2html 10 10 11 11 EXTRA_DIST = language.txt language50.txt \ 12 12 patch.slang-1.2.2.keypad.1 \
+309
contrib/markdown2html
··· 1 + #!/usr/bin/python3 2 + # 3 + # markdown2html.py — simple Markdown-to-HTML converter for use with Mutt 4 + # 5 + # Mutt recently learnt [how to compose `multipart/alternative` 6 + # emails][1]. This script assumes a message has been composed using Markdown 7 + # (with a lot of pandoc extensions enabled), and translates it to `text/html` 8 + # for Mutt to tie into such a `multipart/alternative` message. 9 + # 10 + # [1]: https://gitlab.com/muttmua/mutt/commit/0e566a03725b4ad789aa6ac1d17cdf7bf4e7e354) 11 + # 12 + # Configuration: 13 + # muttrc: 14 + # set send_multipart_alternative=yes 15 + # set send_multipart_alternative_filter=/path/to/markdown2html.py 16 + # 17 + # Optionally, Custom CSS styles will be read from `~/.mutt/markdown2html.css`, 18 + # if present. 19 + # 20 + # Requirements: 21 + # - python3 22 + # - PyPandoc (and pandoc installed, or downloaded) 23 + # - Pynliner 24 + # 25 + # Optional: 26 + # - Pygments, if installed, then syntax highlighting is enabled 27 + # 28 + # Latest version: 29 + # https://git.madduck.net/etc/mutt.git/blob_plain/HEAD:/.mutt/markdown2html 30 + # 31 + # Copyright © 2019 martin f. krafft <madduck@madduck.net> 32 + # Released under the GPL-2+ licence, just like Mutt itself. 33 + # 34 + 35 + import pypandoc 36 + import pynliner 37 + import re 38 + import os 39 + import sys 40 + 41 + try: 42 + from pygments.formatters import get_formatter_by_name 43 + formatter = get_formatter_by_name('html', style='default') 44 + DEFAULT_CSS = formatter.get_style_defs('.sourceCode') 45 + 46 + except ImportError: 47 + DEFAULT_CSS = "" 48 + 49 + 50 + DEFAULT_CSS += ''' 51 + .quote { 52 + padding: 0 0.5em; 53 + margin: 0; 54 + font-style: italic; 55 + border-left: 2px solid #ccc; 56 + color: #999; 57 + font-size: 80%; 58 + } 59 + .quotelead { 60 + font-style: italic; 61 + margin-bottom: -1em; 62 + color: #999; 63 + font-size: 80%; 64 + } 65 + .quotechar { display: none; } 66 + .footnote-ref, .footnote-back { text-decoration: none;} 67 + .signature { 68 + color: #999; 69 + font-family: monospace; 70 + white-space: pre; 71 + margin: 1em 0 0 0; 72 + font-size: 80%; 73 + } 74 + table, th, td { 75 + border-collapse: collapse; 76 + border: 1px solid #999; 77 + } 78 + th, td { padding: 0.5em; } 79 + .header { 80 + background: #eee; 81 + } 82 + .even { background: #eee; } 83 + ''' 84 + 85 + STYLESHEET = os.path.join(os.path.expanduser('~/.mutt'), 86 + 'markdown2html.css') 87 + if os.path.exists(STYLESHEET): 88 + DEFAULT_CSS += open(STYLESHEET).read() 89 + 90 + HTML_DOCUMENT = '''<!DOCTYPE html> 91 + <html><head> 92 + <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> 93 + <meta charset="utf-8"/> 94 + <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes"/> 95 + <title>HTML E-Mail</title> 96 + </head><body class="email"> 97 + {htmlbody} 98 + </body></html>''' 99 + 100 + 101 + SIGNATURE_HTML = \ 102 + '<div class="signature"><span class="leader">-- </span>{sig}</div>' 103 + 104 + 105 + def _preprocess_markdown(mdwn): 106 + ''' 107 + Preprocess Markdown for handling by the converter. 108 + ''' 109 + # convert hard line breaks within paragraphs to 2 trailing spaces, which 110 + # is the markdown way of representing hard line breaks. Note how the 111 + # regexp will not match between paragraphs. 112 + ret = re.sub(r'(\S)\n(\s*\S)', r'\g<1> \n\g<2>', mdwn, flags=re.MULTILINE) 113 + 114 + # Clients like Thunderbird need the leading '>' to be able to properly 115 + # create nested quotes, so we duplicate the symbol, the first instance 116 + # will tell pandoc to create a blockquote, while the second instance will 117 + # be a <span> containing the character, along with a class that causes CSS 118 + # to actually hide it from display. However, this does not work with the 119 + # text-mode HTML2text converters, and so it's left commented for now. 120 + #ret = re.sub(r'\n>', r' \n>[>]{.quotechar}', ret, flags=re.MULTILINE) 121 + 122 + return ret 123 + 124 + 125 + def _identify_quotes_for_later(mdwn): 126 + ''' 127 + Email quoting such as: 128 + 129 + ``` 130 + On 1970-01-01, you said: 131 + > The Flat Earth Society has members all around the globe. 132 + ``` 133 + 134 + isn't really properly handled by Markdown, so let's do our best to 135 + identify the individual elements, and mark them, using a syntax similar to 136 + what pandoc uses already in some cases. As pandoc won't actually use these 137 + data (yet?), we call `self._reformat_quotes` later to use these markers 138 + to slap the appropriate classes on the HTML tags. 139 + ''' 140 + 141 + def generate_lines_with_context(mdwn): 142 + ''' 143 + Iterates the input string line-wise, returning a triplet of 144 + previous, current, and next line, the first and last of which 145 + will be None on the first and last line of the input data 146 + respectively. 147 + ''' 148 + prev = cur = nxt = None 149 + lines = iter(mdwn.splitlines()) 150 + cur = next(lines) 151 + for nxt in lines: 152 + yield prev, cur, nxt 153 + prev = cur 154 + cur = nxt 155 + yield prev, cur, None 156 + 157 + ret = [] 158 + for prev, cur, nxt in generate_lines_with_context(mdwn): 159 + 160 + # The lead-in to a quote is a single line immediately preceding the 161 + # quote, and ending with ':'. Note that there could be multiple of 162 + # these: 163 + if re.match(r'^.+:\s*$', cur) and nxt.startswith('>'): 164 + ret.append(f'{{.quotelead}}{cur.strip()}') 165 + # pandoc needs an empty line before the blockquote, so 166 + # we enter one for the purpose of HTML rendition: 167 + ret.append('') 168 + continue 169 + 170 + # The first blockquote after such a lead-in gets marked as the 171 + # "initial" quote: 172 + elif prev and re.match(r'^.+:\s*$', prev) and cur.startswith('>'): 173 + ret.append(re.sub(r'^(\s*>\s*)+(.+)', 174 + r'\g<1>{.quoteinitial}\g<2>', 175 + cur, flags=re.MULTILINE)) 176 + 177 + # All other occurrences of blockquotes get the "subsequent" marker: 178 + elif cur.startswith('>') and prev and not prev.startswith('>'): 179 + ret.append(re.sub(r'^((?:\s*>\s*)+)(.+)', 180 + r'\g<1>{.quotesubsequent}\g<2>', 181 + cur, flags=re.MULTILINE)) 182 + 183 + else: # pass through everything else. 184 + ret.append(cur) 185 + 186 + return '\n'.join(ret) 187 + 188 + 189 + def _reformat_quotes(html): 190 + ''' 191 + Earlier in the pipeline, we marked email quoting, using markers, which we 192 + now need to turn into HTML classes, so that we can use CSS to style them. 193 + ''' 194 + ret = html.replace('<p>{.quotelead}', '<p class="quotelead">') 195 + ret = re.sub(r'<blockquote>\n((?:<blockquote>\n)*)<p>(?:\{\.quote(\w+)\})', 196 + r'<blockquote class="quote \g<2>">\n\g<1><p>', ret, flags=re.MULTILINE) 197 + return ret 198 + 199 + 200 + 201 + def _convert_with_pandoc(mdwn, inputfmt='markdown', outputfmt='html5', 202 + ext_enabled=None, ext_disabled=None, 203 + standalone=True, title="HTML E-Mail"): 204 + ''' 205 + Invoke pandoc to do the actual conversion of Markdown to HTML5. 206 + ''' 207 + if not ext_enabled: 208 + ext_enabled = [ 'backtick_code_blocks', 209 + 'line_blocks', 210 + 'fancy_lists', 211 + 'startnum', 212 + 'definition_lists', 213 + 'example_lists', 214 + 'table_captions', 215 + 'simple_tables', 216 + 'multiline_tables', 217 + 'grid_tables', 218 + 'pipe_tables', 219 + 'all_symbols_escapable', 220 + 'intraword_underscores', 221 + 'strikeout', 222 + 'superscript', 223 + 'subscript', 224 + 'fenced_divs', 225 + 'bracketed_spans', 226 + 'footnotes', 227 + 'inline_notes', 228 + 'emoji', 229 + 'tex_math_double_backslash', 230 + 'autolink_bare_uris' 231 + ] 232 + if not ext_disabled: 233 + ext_disabled = [ 'tex_math_single_backslash', 234 + 'tex_math_dollars', 235 + 'smart', 236 + 'raw_html' 237 + ] 238 + 239 + enabled = '+'.join(ext_enabled) 240 + disabled = '-'.join(ext_disabled) 241 + inputfmt = f'{inputfmt}+{enabled}-{disabled}' 242 + 243 + args = [] 244 + if standalone: 245 + args.append('--standalone') 246 + if title: 247 + args.append(f'--metadata=pagetitle:"{title}"') 248 + 249 + return pypandoc.convert_text(mdwn, format=inputfmt, to=outputfmt, 250 + extra_args=args) 251 + 252 + 253 + def _apply_styling(html): 254 + ''' 255 + Inline all styles defined and used into the individual HTML tags. 256 + ''' 257 + return pynliner.Pynliner().from_string(html).with_cssString(DEFAULT_CSS).run() 258 + 259 + 260 + def _postprocess_html(html): 261 + ''' 262 + Postprocess the generated and styled HTML. 263 + ''' 264 + return html 265 + 266 + 267 + def convert_markdown_to_html(mdwn): 268 + ''' 269 + Converts the input Markdown to HTML, handling separately the body, as well 270 + as an optional signature. 271 + ''' 272 + parts = re.split(r'^-- $', mdwn, 1, flags=re.MULTILINE) 273 + body = parts[0] 274 + if len(parts) == 2: 275 + sig = parts[1] 276 + else: 277 + sig = None 278 + 279 + html='' 280 + if body: 281 + body = _preprocess_markdown(body) 282 + body = _identify_quotes_for_later(body) 283 + html = _convert_with_pandoc(body, standalone=False) 284 + html = _reformat_quotes(html) 285 + 286 + if sig: 287 + sig = _preprocess_markdown(sig) 288 + html += SIGNATURE_HTML.format(sig='<br/>'.join(sig.splitlines())) 289 + 290 + html = HTML_DOCUMENT.format(htmlbody=html) 291 + html = _apply_styling(html) 292 + html = _postprocess_html(html) 293 + 294 + return html 295 + 296 + 297 + def main(): 298 + ''' 299 + Convert text on stdin to HTML, and print it to stdout, like mutt would 300 + expect. 301 + ''' 302 + html = convert_markdown_to_html(sys.stdin.read()) 303 + if html: 304 + # mutt expects the content type in the first line, so: 305 + print(f'text/html\n\n{html}') 306 + 307 + 308 + if __name__ == '__main__': 309 + main()