3
\$\begingroup\$

I'm working on a markdown-to-html parser for my C++ proposals. Example usage is turning markdown like this into an html page like this. The part that caused me the most frustration was having (a) tables that (b) themselves can internally use markdown. I ended up with a hacky solution that happens to work right now (basically, recursively re-invoking markdown while doing my own HTML... "parsing"), but I'm very unsatisfied with it and would love to hear feedback on it. There's also other things I'm doing with markdown that can certainly be improved upon. The code can be found here.

toc is basically a copy of markdown.extensions.toc with some minor changes - I wanted an ordered list and the section number to be included in the name (which I couldn't do without... copying it all).

import argparse
import json
import os
import sys

import markdown
from markdown.extensions import Extension
from markdown.extensions.codehilite import CodeHiliteExtension
from markdown.inlinepatterns import LinkPattern, LINK_RE, BacktickPattern, BACKTICK_RE
from markdown.treeprocessors import Treeprocessor
from markdown.postprocessors import Postprocessor
from markdown.preprocessors import Preprocessor
from markdown.util import etree, string_type, isBlockLevel, AtomicString
from toc import TocExtension

def local_path(filename):
    return os.path.join(sys.path[0], filename)

class LinkSaver(LinkPattern):
    def __init__(self, *args, **kwargs):
        super(LinkSaver, self).__init__(*args, **kwargs)
        self.markdown.saved_links = []

    def handleMatch(self, m):
        self.markdown.saved_links.append(m.group(9))
        return super(LinkSaver, self).handleMatch(m)

class RefProcessor(Treeprocessor):
    def __init__(self, md):
        super(RefProcessor, self).__init__(md)

        self.wg21 = json.load(open(local_path('index.json')))
        for key in sorted(self.wg21, reverse=True):
            if key.startswith('P') and key[:5] not in self.wg21:
                self.wg21[key[:5]] = self.wg21[key]

    def run(self, doc):
        wg21_links = []
        for link in self.markdown.saved_links:
            if 'wg21.link' in link:
                wg21_links.append(link.split('/')[-1].upper())

        if sorted(wg21_links):
            refs = etree.SubElement(doc, 'h1')
            refs.text = 'References'

            ul = etree.SubElement(doc, 'ul')
            for link in sorted(wg21_links):
                info = self.wg21[link]
                li = etree.SubElement(ul, 'li')
                a = etree.SubElement(li, 'a')
                a.attrib["href"] = info['long_link']
                a.text = '[{}]'.format(link)

                desc = etree.SubElement(li, 'span')
                desc.attrib['style'] = "margin-left: 5px;"
                desc.text = '"{}" by {}, {}'.format(
                    info['title'],
                    info['author'],
                    info['date'])

class TableCodeBlockProcessor(Preprocessor):
    def run(self, lines):
        new_lines = []
        to_markup = []
        target = new_lines

        for line in lines:
            if line.startswith('<th') or line.startswith('<td'):
                new_lines.append(line)
                target = to_markup
            elif line.startswith('</th') or line.startswith('</td'):
                target = new_lines
                new_lines.extend(
                    self.markdown.convert(u'\n'.join(to_markup))
                        .replace('<pre class="codehilite">',
                            '<pre style="background:transparent;border:0px">')
                        .split('\n'))
                to_markup = []
                new_lines.append(line)
            else:
                target.append(line)
        return new_lines

class CppBacktickPattern(BacktickPattern):
    def handleMatch(self, m):
        if m.group(4):
            text = m.group(4).strip()
            el = etree.Element('code')
            if text.startswith('#!'):
                el.text = AtomicString(text[2:].strip())
            else:
                el.attrib["class"] = "language-cpp"
                el.text = AtomicString(text.strip())
            return el
        else:
            return super(CppBacktickPattern, self).handleMatch(m)

class CppBacktickExtension(Extension):
    def extendMarkdown(self, md, md_globals):
        md.inlinePatterns[u'backtick'] = CppBacktickPattern(BACKTICK_RE)

class RefExtension(Extension):
    def extendMarkdown(self, md, md_globals):
        md.inlinePatterns[u'link'] = LinkSaver(LINK_RE, md)
        md.treeprocessors.add("refs", RefProcessor(md), '<toc')

class TableCodeBlockExtension(Extension):
    def extendMarkdown(self, md, md_globals):
        md.preprocessors.add("tbl_codeblock",
            TableCodeBlockProcessor(
                markdown.Markdown(extensions=[CodeHiliteExtension(use_pygments=False),
                    CppBacktickExtension()
                    ])
            ),
            '_begin')

def main(argv=None):
    parser = argparse.ArgumentParser(
        description='Markdown formatter for papers')
    parser.add_argument('-i', '--input', dest='input', type=argparse.FileType('r'))
    parser.add_argument('-o', '--output', dest='output', type=argparse.FileType('w'),
        default='-')
    parser.add_argument('--style', default=local_path('style.html'),
        type=argparse.FileType('r'))
    parser.add_argument('--references', action='store_true')

    args = parser.parse_args(argv)

    write = args.output.write

    extensions = [CodeHiliteExtension(use_pygments=False),
        TocExtension(baselevel=2, anchorlink=False, title=None, marker=''),
        'markdown.extensions.meta',
        CppBacktickExtension(),
        TableCodeBlockExtension()]
    if args.references:
        extensions.append(RefExtension())

    md = markdown.Markdown(extensions=extensions)
    in_md = args.input.read().decode('utf-8')
    html = md.convert(in_md).replace('language-c++', 'language-cpp')
    write('<html>\n')
    write('<head>\n')
    write('<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">\n')
    write('<title>{}</title>\n'.format(md.Meta['title'][0]))
    write(args.style.read())
    write('\n</head>\n')
    write('<body>\n')
    write('<address align=right>\n')    
    write('Document Number: {} <br />\n'.format(md.Meta['document-number'][0]))
    write('Date: {} <br />\n'.format(md.Meta['date'][0]))
    write('Audience: {} <br />\n'.format(md.Meta['audience'][0]))
    write('Reply-To: {} <br />\n'.format('<br />'.join(md.Meta['authors'])))
    write('</address>\n')
    write('<hr /><h1 align=center>{}</h1>\n'.format(md.Meta['title'][0]))
    write('<h2>Contents</h2>\n')
    write('{}\n{}\n</html>'.format(md.toc, html.encode('utf-8')))

if __name__ == '__main__':
    main()

Also, yes I know, this really needs a lot of comments.

\$\endgroup\$
  • \$\begingroup\$ BTW, you do know that programs like pandoc already have this functionality, right? \$\endgroup\$ – Graipher May 6 '18 at 7:12
  • 1
    \$\begingroup\$ @Graipher No, I didn't. Thanks for the tip, will look into it! \$\endgroup\$ – Barry May 6 '18 at 16:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.