Path:
strictdoc/backend/markdown/markdown_to_html_fragment_writer.py
Lines:
159
Non-empty lines:
129
Non-empty lines covered with requirements:
129 / 129 (100.0%)
Functions:
11
Functions covered by requirements:
11 / 11 (100.0%)
1
"""2
@relation(SDOC-SRS-24, scope=file)3
"""4
5
import re
6
from html import escape
7
from typing import Callable, MutableMapping, Optional, Sequence, Tuple, cast
8
9
from markdown_it import MarkdownIt
10
from markdown_it.common.utils import escapeHtml, unescapeAll
11
from markdown_it.renderer import RendererHTML
12
from markdown_it.rules_inline import StateInline
13
from markdown_it.token import Token
14
from markdown_it.utils import EnvType, OptionsDict
15
from markupsafe import Markup
16
17
_MARKDOWN_PARSER = MarkdownIt("default", {"html": True})
18
_MARKDOWN_RENDERER = cast(RendererHTML, _MARKDOWN_PARSER.renderer)
19
_FenceRenderer = Callable[[Sequence[Token], int, OptionsDict, EnvType], str]
20
_MARKDOWN_RENDERER_RULES = cast(
21
MutableMapping[str, _FenceRenderer], _MARKDOWN_RENDERER.rules
22
)23
_DEFAULT_FENCE_RENDERER: _FenceRenderer = _MARKDOWN_RENDERER_RULES["fence"]
24
25
26
def _render_fence(
27
tokens: Sequence[Token],
28
idx: int,
29
options: OptionsDict,
30
env: EnvType,
31
) -> str:
32
token = tokens[idx]
33
info = unescapeAll(token.info).strip() if token.info else ""
34
lang_name = info.split(maxsplit=1)[0] if info else ""
35
36
if lang_name.lower() != "mermaid":
37
return _DEFAULT_FENCE_RENDERER(tokens, idx, options, env)
38
39
return f'<pre class="mermaid">{escapeHtml(token.content)}</pre>\n'
40
41
42
def _math_inline_rule(state: StateInline, silent: bool) -> bool:
43
pos = state.pos
44
max_ = state.posMax
45
src = state.src
46
47
if pos >= max_ or src[pos] != "$":
48
return False
49
50
is_display = pos + 1 <= max_ and src[pos + 1] == "$"
51
marker_len = 2 if is_display else 1
52
start = pos + marker_len
53
54
if start >= max_:
55
return False
56
57
end = -1
58
i = start
59
while i <= max_:
60
dollar_idx = src.find("$", i)
61
if dollar_idx == -1:
62
break63
if is_display:
64
if dollar_idx + 1 <= max_ and src[dollar_idx + 1] == "$":
65
end = dollar_idx
66
break67
i = dollar_idx + 1
68
else:
69
if dollar_idx + 1 <= max_ and src[dollar_idx + 1] == "$":
70
i = dollar_idx + 2
71
else:
72
end = dollar_idx
73
break74
75
if end == -1:
76
return False
77
78
content = src[start:end]
79
if not content.strip():
80
return False
81
82
if not silent:
83
token_type = "math_inline_double" if is_display else "math_inline"
84
token = state.push(token_type, "math", 0)
85
token.markup = "$$" if is_display else "$"
86
token.content = content
87
88
state.pos = end + marker_len
89
return True
90
91
92
def _render_math_inline(
93
tokens: Sequence[Token],
94
idx: int,
95
_options: OptionsDict,
96
_env: EnvType,
97
) -> str:
98
return f'<span class="math notranslate nohighlight">\\( {tokens[idx].content} \\)</span>'
99
100
101
def _render_math_inline_double(
102
tokens: Sequence[Token],
103
idx: int,
104
_options: OptionsDict,
105
_env: EnvType,
106
) -> str:
107
return f'<div class="math notranslate nohighlight">\\[ {tokens[idx].content} \\]</div>'
108
109
110
def _strip_dotdot_from_img_src(html: str) -> str:
111
def _rebase(m: "re.Match[str]") -> str:
112
src = m.group(1)
113
while src.startswith("../"):
114
src = src[3:]
115
return f'src="{src}"'
116
117
return re.sub(r'src="(\.\./[^"]*)"', _rebase, html)
118
119
120
class MarkdownToHtmlFragmentWriter:
121
# Use the default preset to support common Markdown extensions such as122
# pipe tables when rendering HTML fragments.123
markdown_parser = _MARKDOWN_PARSER
124
_MARKDOWN_RENDERER_RULES["fence"] = _render_fence
125
_MARKDOWN_PARSER.inline.ruler.before(
126
"backticks", "math_inline", _math_inline_rule
127
)128
_MARKDOWN_RENDERER_RULES["math_inline"] = _render_math_inline
129
_MARKDOWN_RENDERER_RULES["math_inline_double"] = _render_math_inline_double
130
131
def __init__(self, flat_assets: bool = False) -> None:
132
self.flat_assets: bool = flat_assets
133
134
def write(self, markdown_fragment: str) -> Markup:
135
assert isinstance(markdown_fragment, str), markdown_fragment
136
html = MarkdownToHtmlFragmentWriter.markdown_parser.render(
137
markdown_fragment138
)139
if self.flat_assets:
140
html = _strip_dotdot_from_img_src(html)
141
return Markup(html)
142
143
@staticmethod144
def write_with_validation(
145
markdown_fragment: str,
146
) -> Tuple[Optional[str], Optional[str]]:
147
assert isinstance(markdown_fragment, str), markdown_fragment
148
return (
149
MarkdownToHtmlFragmentWriter.markdown_parser.render(
150
markdown_fragment151
),152
None,
153
)154
155
@staticmethod156
def write_anchor_link(title: str, href: str) -> str:
157
return (
158
f'<a href="{escape(href, quote=True)}">🔗 {escape(title)}</a>'
159
)