Custom Link extension for marko markdown Python library

🏠 Go home
Python 2022-08-08

I generate this site using a simple Python script (currently 108 LOC) from markdown files created using obsidian. I use python-frontmatter Python library to parse the markdown with yaml header and then marko (there is a javascript implementation with the same name) to parse the markdown and generate the HTML.

In obsidian, I link the files using the usual markdown syntax and during the HTML generation I need to convert these links to HTML hyperlinks. That works out-of-box in marko but links contain addresses to markdown files (without the .md extension) and I need append there the .html extension.

Marko extensions

Marko allows to extend its behaviour using extensions. Extensions can be provided in the constructor of the Markdown class. Let's call our extension LinkExtension.

class LinkExtension:
    """TODO"""

markdown = Markdown(extensions=[LinkExtension])

The extension can specify list of Element classes that implement element parsing. Luckily, we're not introducing a new syntax and there is already existing marko.inline.Link class for that.

from marko.inline import Link

class LinkExtension:
    elements = [Link]

Actually, we can completely ignore it and not specify it at all because the only thing we're going to modify is the rendering. We implement a render mixin class. We need to implement render_link method which is going to be used instead of the one from marko.html_renderer.HTMLRenderer. It accepts a single element object that contains title and dest properties parsed using the Link parser.

I simply called the parent's render_link with an element modified so that the dest has .html extension. I decided to make a shallow copy of the element object, but it works correctly also when the element is mutated. But mutation is dangerous in general, so just in case...

from copy import copy

class LinkRendererMixin:
    def render_link(self, element):
        new_element = copy(element)
        new_element.dest = f"{new_element.dest}.html"
        return super().render_link(element)

The final extension is then a simple class specifying the renderer.

class LinkExtension:
    renderer_mixins = [LinkRendererMixin]

Final result

from copy import copy

from marko import Markdown

class LinkRendererMixin:
    def render_link(self, element):
        new_element = copy(element)
        new_element.dest = f"{new_element.dest}.html"
        return super().render_link(new_element)

class LinkExtension:
    renderer_mixins = [LinkRendererMixin]

def main():
    markdown = Markdown(extensions=[LinkExtension])
    example_text = "Hello world with [link](files/another_one)"
    print(markdown.convert(example_text))

if __name__ == "__main__":
    main()

which output is

<p>Hello world with <a href="files/another_one.html">link</a>.</p>