Plain is headed towards 1.0! Subscribe for development updates →

 1from html.parser import HTMLParser
 2
 3import mistune
 4from pygments import highlight
 5from pygments.formatters import html
 6from pygments.lexers import get_lexer_by_name
 7
 8from plain.utils.text import slugify
 9
10
11class PagesRenderer(mistune.HTMLRenderer):
12    def heading(self, text, level, **attrs):
13        """Automatically add an ID to headings if one is not provided."""
14
15        if "id" not in attrs:
16            inner_text = get_inner_text(text)
17            inner_text = inner_text.replace(
18                ".", "-"
19            )  # Replace dots with hyphens (slugify won't)
20            attrs["id"] = slugify(inner_text)
21
22        return super().heading(text, level, **attrs)
23
24    def block_code(self, code, info=None):
25        """Highlight code blocks using Pygments."""
26
27        if info:
28            lexer = get_lexer_by_name(info, stripall=True)
29            formatter = html.HtmlFormatter(wrapcode=True)
30            return highlight(code, lexer, formatter)
31
32        return "<pre><code>" + mistune.escape(code) + "</code></pre>"
33
34
35def render_markdown(content):
36    renderer = PagesRenderer(escape=False)
37    markdown = mistune.create_markdown(
38        renderer=renderer, plugins=["strikethrough", "table"]
39    )
40    return markdown(content)
41
42
43class InnerTextParser(HTMLParser):
44    def __init__(self):
45        super().__init__()
46        self.text_content = []
47
48    def handle_data(self, data):
49        # Collect all text data
50        self.text_content.append(data.strip())
51
52
53def get_inner_text(html_content):
54    parser = InnerTextParser()
55    parser.feed(html_content)
56    return " ".join([text for text in parser.text_content if text])