Skip to content

Commit

Permalink
refactor: Setup a more robust Markdown converter
Browse files Browse the repository at this point in the history
  • Loading branch information
pawamoy committed May 1, 2022
1 parent 9f0f5f4 commit 395f4c4
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 43 deletions.
37 changes: 0 additions & 37 deletions src/markdown_exec/markdown_helpers.py

This file was deleted.

10 changes: 4 additions & 6 deletions src/markdown_exec/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from markdown.core import Markdown
from markupsafe import Markup

from markdown_exec.markdown_helpers import code_block, tabbed
from markdown_exec.rendering import code_block, markdown, tabbed

md_copy = None

Expand Down Expand Up @@ -63,10 +63,8 @@ def exec_python( # noqa: WPS231
Returns:
HTML contents.
"""
global md_copy # noqa: WPS420
if md_copy is None:
md_copy = Markdown() # noqa: WPS442
md_copy.registerExtensions(md.registeredExtensions, {})
markdown.mimic(md)

if isolate:
exec_source = f"def _function():\n{indent(source, prefix=' ' * 4)}\n_function()\n"
else:
Expand All @@ -90,4 +88,4 @@ def exec_python( # noqa: WPS231
output = tabbed(("Source", source_block), ("Result", output))
elif show_source == "tabbed-right":
output = tabbed(("Result", output), ("Source", source_block))
return Markup(md_copy.convert(output))
return markdown.convert(output)
119 changes: 119 additions & 0 deletions src/markdown_exec/rendering.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
"""Markdown extensions and helpers."""

from __future__ import annotations

from textwrap import indent
from xml.etree.ElementTree import Element

from markdown import Markdown
from markdown.treeprocessors import Treeprocessor
from markupsafe import Markup


def code_block(language: str, code: str, **options: str) -> str:
"""Format code as a code block.
Parameters:
language: The code block language.
code: The source code to format.
**options: Additional options passed from the source, to add back to the generated code block.
Returns:
The formatted code block.
"""
opts = " ".join(f'{opt_name}="{opt_value}"' for opt_name, opt_value in options.items())
return f"```{language} {opts}\n{code}\n```"


def tabbed(*tabs: tuple[str, str]) -> str:
"""Format tabs using `pymdownx.tabbed` extension.
Parameters:
*tabs: Tuples of strings: title and text.
Returns:
The formatted tabs.
"""
parts = []
for title, text in tabs:
title = title.replace("\\|", "|").strip()
parts.append(f'=== "{title}"')
parts.append(indent(text, prefix=" " * 4))
parts.append("")
return "\n".join(parts)


# code taken from mkdocstrings, credits to @oprypin
class _IdPrependingTreeprocessor(Treeprocessor):
"""Prepend the configured prefix to IDs of all HTML elements."""

name = "markdown_exec_ids"

def __init__(self, md: Markdown, id_prefix: str): # noqa: D107
super().__init__(md)
self.id_prefix = id_prefix

def run(self, root: Element): # noqa: D102,WPS231
if not self.id_prefix:
return
for el in root.iter():
id_attr = el.get("id")
if id_attr:
el.set("id", self.id_prefix + id_attr)

href_attr = el.get("href")
if href_attr and href_attr.startswith("#"):
el.set("href", "#" + self.id_prefix + href_attr[1:])

name_attr = el.get("name")
if name_attr:
el.set("name", self.id_prefix + name_attr)

if el.tag == "label":
for_attr = el.get("for")
if for_attr:
el.set("for", self.id_prefix + for_attr)


class _MarkdownConverter:
"""Helper class to avoid breaking the original Markdown instance state."""

def __init__(self) -> None: # noqa: D107
self.md: Markdown = None
self.counter: int = 0

def mimic(self, md: Markdown) -> None:
"""Mimic the passed Markdown instance by registering the same extensions.
Parameters:
md: A Markdown instance.
"""
if self.md is None:
self.md = Markdown() # noqa: WPS442
self.md.registerExtensions(md.registeredExtensions + ["pymdownx.extra"], {})
self.md.treeprocessors.register(
_IdPrependingTreeprocessor(md, ""),
_IdPrependingTreeprocessor.name,
priority=4, # right after 'toc' (needed because that extension adds ids to headers)
)

def convert(self, text: str) -> Markup:
"""Convert Markdown text to safe HTML.
Parameters:
text: Markdown text.
Returns:
Safe HTML.
"""
self.md.treeprocessors[_IdPrependingTreeprocessor.name].id_prefix = f"exec-{self.counter}--"
self.counter += 1

try: # noqa: WPS501
return Markup(self.md.convert(text))
finally:
self.md.treeprocessors[_IdPrependingTreeprocessor.name].id_prefix = ""


# provide a singleton
markdown = _MarkdownConverter()

0 comments on commit 395f4c4

Please sign in to comment.