"Fossies" - the Fresh Open Source Software Archive

Member "mutt-2.2.10/contrib/markdown2html" (12 Dec 2022, 9750 Bytes) of package /linux/misc/mutt-2.2.10.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file.

    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 nxt is not None and re.match(r'^\s*[^>].+:\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 is not None and re.match(r'^\s*[^>].+:\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 is not None 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('{.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()