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

1"""This module contains a Markdown extension allowing to integrate generated headings into the ToC.""" 

2 

3from __future__ import annotations 

4 

5import copy 

6import re 

7from typing import TYPE_CHECKING 

8from xml.etree.ElementTree import Element 

9 

10from markdown.treeprocessors import Treeprocessor 

11from markdown.util import HTML_PLACEHOLDER_RE 

12 

13if TYPE_CHECKING: 

14 from markdown import Markdown 

15 from markupsafe import Markup 

16 

17 

18# code taken from mkdocstrings, credits to @oprypin 

19class IdPrependingTreeprocessor(Treeprocessor): 

20 """Prepend the configured prefix to IDs of all HTML elements.""" 

21 

22 name = "markdown_exec_ids" 

23 

24 def __init__(self, md: Markdown, id_prefix: str) -> None: # noqa: D107 

25 super().__init__(md) 

26 self.id_prefix = id_prefix 

27 

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) 

35 

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:]) 

39 

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) 

43 

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) 

48 

49 

50# code taken from mkdocstrings, credits to @oprypin 

51class HeadingReportingTreeprocessor(Treeprocessor): 

52 """Records the heading elements encountered in the document.""" 

53 

54 name = "markdown_exec_record_headings" 

55 regex = re.compile("[Hh][1-6]") 

56 

57 def __init__(self, md: Markdown, headings: list[Element]): # noqa: D107 

58 super().__init__(md) 

59 self.headings = headings 

60 

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) 

70 

71 

72class InsertHeadings(Treeprocessor): 

73 """Our headings insertor.""" 

74 

75 name = "markdown_exec_insert_headings" 

76 

77 def __init__(self, md: Markdown): 

78 """Initialize the object. 

79 

80 Arguments: 

81 md: A `markdown.Markdown` instance. 

82 """ 

83 super().__init__(md) 

84 self.headings: dict[Markup, list[Element]] = {} 

85 

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 

89 

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) 

99 

100 

101class RemoveHeadings(Treeprocessor): 

102 """Our headings remover.""" 

103 

104 name = "markdown_exec_remove_headings" 

105 

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