Coverage for src/markdown_exec/processors.py: 81.98%
73 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-04-18 18:20 +0200
« prev ^ index » next coverage.py v7.2.3, created at 2023-04-18 18:20 +0200
1"""This module contains a Markdown extension allowing to integrate generated headings into the ToC."""
3from __future__ import annotations
5import copy
6import re
7from typing import TYPE_CHECKING
8from xml.etree.ElementTree import Element
10from markdown.treeprocessors import Treeprocessor
11from markdown.util import HTML_PLACEHOLDER_RE
13if TYPE_CHECKING:
14 from markdown import Markdown
15 from markupsafe import Markup
18# code taken from mkdocstrings, credits to @oprypin
19class IdPrependingTreeprocessor(Treeprocessor):
20 """Prepend the configured prefix to IDs of all HTML elements."""
22 name = "markdown_exec_ids"
24 def __init__(self, md: Markdown, id_prefix: str) -> None: # noqa: D107
25 super().__init__(md)
26 self.id_prefix = id_prefix
28 def run(self, root: Element) -> None: # noqa: D102
29 if not self.id_prefix:
30 return
31 for el in root.iter():
32 id_attr = el.get("id")
33 if id_attr:
34 el.set("id", self.id_prefix + id_attr)
36 href_attr = el.get("href")
37 if href_attr and href_attr.startswith("#"): 37 ↛ 38line 37 didn't jump to line 38, because the condition on line 37 was never true
38 el.set("href", "#" + self.id_prefix + href_attr[1:])
40 name_attr = el.get("name")
41 if name_attr: 41 ↛ 42line 41 didn't jump to line 42, because the condition on line 41 was never true
42 el.set("name", self.id_prefix + name_attr)
44 if el.tag == "label": 44 ↛ 45line 44 didn't jump to line 45, because the condition on line 44 was never true
45 for_attr = el.get("for")
46 if for_attr:
47 el.set("for", self.id_prefix + for_attr)
50# code taken from mkdocstrings, credits to @oprypin
51class HeadingReportingTreeprocessor(Treeprocessor):
52 """Records the heading elements encountered in the document."""
54 name = "markdown_exec_record_headings"
55 regex = re.compile("[Hh][1-6]")
57 def __init__(self, md: Markdown, headings: list[Element]): # noqa: D107
58 super().__init__(md)
59 self.headings = headings
61 def run(self, root: Element) -> None: # noqa: D102
62 for el in root.iter():
63 if self.regex.fullmatch(el.tag):
64 el = copy.copy(el) # noqa: PLW2901
65 # 'toc' extension's first pass (which we require to build heading stubs/ids) also edits the HTML.
66 # Undo the permalink edit so we can pass this heading to the outer pass of the 'toc' extension.
67 if len(el) > 0 and el[-1].get("class") == self.md.treeprocessors["toc"].permalink_class: 67 ↛ 68line 67 didn't jump to line 68, because the condition on line 67 was never true
68 del el[-1]
69 self.headings.append(el)
72class InsertHeadings(Treeprocessor):
73 """Our headings insertor."""
75 name = "markdown_exec_insert_headings"
77 def __init__(self, md: Markdown):
78 """Initialize the object.
80 Arguments:
81 md: A `markdown.Markdown` instance.
82 """
83 super().__init__(md)
84 self.headings: dict[Markup, list[Element]] = {}
86 def run(self, root: Element) -> None: # noqa: D102 (ignore missing docstring)
87 if not self.headings: 87 ↛ 88line 87 didn't jump to line 88, because the condition on line 87 was never true
88 return
90 for el in root.iter():
91 match = HTML_PLACEHOLDER_RE.match(el.text or "")
92 if match:
93 counter = int(match.group(1))
94 markup: Markup = self.md.htmlStash.rawHtmlBlocks[counter] # type: ignore[assignment]
95 if markup in self.headings:
96 div = Element("div", {"class": "markdown-exec"})
97 div.extend(self.headings[markup])
98 el.append(div)
101class RemoveHeadings(Treeprocessor):
102 """Our headings remover."""
104 name = "markdown_exec_remove_headings"
106 def run(self, root: Element) -> None: # noqa: D102
107 carry_text = ""
108 for el in reversed(root): # Reversed mainly for the ability to mutate during iteration.
109 for subel in reversed(el):
110 if subel.tag == "div" and subel.get("class") == "markdown-exec": 110 ↛ 114line 110 didn't jump to line 114, because the condition on line 110 was never false
111 # Delete the duplicated headings along with their container, but keep the text (i.e. the actual HTML).
112 carry_text = (subel.text or "") + carry_text
113 el.remove(subel)
114 elif carry_text:
115 subel.tail = (subel.tail or "") + carry_text
116 carry_text = ""
117 if carry_text:
118 el.text = (el.text or "") + carry_text