markdown_convert 1.2.50__tar.gz → 1.2.52__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {markdown_convert-1.2.50 → markdown_convert-1.2.52}/PKG-INFO +3 -1
- {markdown_convert-1.2.50 → markdown_convert-1.2.52}/markdown_convert/default.css +92 -0
- {markdown_convert-1.2.50 → markdown_convert-1.2.52}/markdown_convert/modules/constants.py +1 -0
- markdown_convert-1.2.52/markdown_convert/modules/extras.py +223 -0
- markdown_convert-1.2.52/markdown_convert/modules/transform.py +135 -0
- {markdown_convert-1.2.50 → markdown_convert-1.2.52}/pyproject.toml +3 -1
- markdown_convert-1.2.50/markdown_convert/modules/extras.py +0 -100
- markdown_convert-1.2.50/markdown_convert/modules/transform.py +0 -164
- {markdown_convert-1.2.50 → markdown_convert-1.2.52}/.gitignore +0 -0
- {markdown_convert-1.2.50 → markdown_convert-1.2.52}/LICENSE +0 -0
- {markdown_convert-1.2.50 → markdown_convert-1.2.52}/README.md +0 -0
- {markdown_convert-1.2.50 → markdown_convert-1.2.52}/markdown_convert/__init__.py +0 -0
- {markdown_convert-1.2.50 → markdown_convert-1.2.52}/markdown_convert/__main__.py +0 -0
- {markdown_convert-1.2.50 → markdown_convert-1.2.52}/markdown_convert/code.css +0 -0
- {markdown_convert-1.2.50 → markdown_convert-1.2.52}/markdown_convert/modules/__init__.py +0 -0
- {markdown_convert-1.2.50 → markdown_convert-1.2.52}/markdown_convert/modules/autoinstall.py +0 -0
- {markdown_convert-1.2.50 → markdown_convert-1.2.52}/markdown_convert/modules/convert.py +0 -0
- {markdown_convert-1.2.50 → markdown_convert-1.2.52}/markdown_convert/modules/resources.py +0 -0
- {markdown_convert-1.2.50 → markdown_convert-1.2.52}/markdown_convert/modules/utils.py +0 -0
- {markdown_convert-1.2.50 → markdown_convert-1.2.52}/markdown_convert/modules/validate.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: markdown_convert
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.52
|
|
4
4
|
Summary: Convert Markdown files to PDF from your command line.
|
|
5
5
|
Project-URL: homepage, https://github.com/Julynx/markdown_convert
|
|
6
6
|
Author-email: Julio Cabria <juliocabria@tutanota.com>
|
|
@@ -17,6 +17,8 @@ Requires-Dist: latex2mathml>=3.78.1
|
|
|
17
17
|
Requires-Dist: markdown2<3,>=2.4.13
|
|
18
18
|
Requires-Dist: playwright>=1.57.0
|
|
19
19
|
Requires-Dist: pygments<3,>=2.17.2
|
|
20
|
+
Requires-Dist: ruamel-yaml>=0.19.1
|
|
21
|
+
Requires-Dist: vl-convert-python>=1.9.0.post1
|
|
20
22
|
Description-Content-Type: text/markdown
|
|
21
23
|
|
|
22
24
|
# markdown-convert
|
|
@@ -24,6 +24,17 @@
|
|
|
24
24
|
--color-border-light: #ccc;
|
|
25
25
|
/* Hyperlinks */
|
|
26
26
|
--color-links: #09f;
|
|
27
|
+
|
|
28
|
+
/* Note */
|
|
29
|
+
--color-ad-note: oklch(70% 0.1 238.77);
|
|
30
|
+
/* Tip */
|
|
31
|
+
--color-ad-tip: oklch(70% 0.1 142.605);
|
|
32
|
+
/* Important */
|
|
33
|
+
--color-ad-important: oklch(70% 0.1 284.67);
|
|
34
|
+
/* Warning */
|
|
35
|
+
--color-ad-warning: oklch(70% 0.1 74.25);
|
|
36
|
+
/* Caution */
|
|
37
|
+
--color-ad-caution: oklch(70% 0.1 14.4);
|
|
27
38
|
}
|
|
28
39
|
|
|
29
40
|
/* Document */
|
|
@@ -432,4 +443,85 @@ math {
|
|
|
432
443
|
.justify {
|
|
433
444
|
display: block;
|
|
434
445
|
text-align: justify;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/* Admonitions */
|
|
449
|
+
.admonition {
|
|
450
|
+
padding: 0.5rem 1rem;
|
|
451
|
+
margin-bottom: 1rem;
|
|
452
|
+
border-left: 0.25rem solid;
|
|
453
|
+
border-radius: 0.3rem;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
.admonition strong {
|
|
457
|
+
text-transform: capitalize;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
.admonition em:not(:empty)::before {
|
|
461
|
+
content: ": ";
|
|
462
|
+
font-weight: bold;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.admonition em {
|
|
466
|
+
font-weight: 600;
|
|
467
|
+
font-style: normal;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.admonition p {
|
|
471
|
+
margin: 0;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
.admonition.note {
|
|
475
|
+
border-left-color: var(--color-ad-note);
|
|
476
|
+
background-color: oklch(from var(--color-ad-note) 0.97 0.015 h);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.admonition.note strong {
|
|
480
|
+
color: var(--color-ad-note);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.admonition.tip {
|
|
484
|
+
border-left-color: var(--color-ad-tip);
|
|
485
|
+
background-color: oklch(from var(--color-ad-tip) 0.97 0.015 h);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.admonition.tip strong {
|
|
489
|
+
color: var(--color-ad-tip);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.admonition.important {
|
|
493
|
+
border-left-color: var(--color-ad-important);
|
|
494
|
+
background-color: oklch(from var(--color-ad-important) 0.97 0.015 h);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.admonition.important strong {
|
|
498
|
+
color: var(--color-ad-important);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.admonition.warning {
|
|
502
|
+
border-left-color: var(--color-ad-warning);
|
|
503
|
+
background-color: oklch(from var(--color-ad-warning) 0.97 0.015 h);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.admonition.warning strong {
|
|
507
|
+
color: var(--color-ad-warning);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.admonition.caution {
|
|
511
|
+
border-left-color: var(--color-ad-caution);
|
|
512
|
+
background-color: oklch(from var(--color-ad-caution) 0.97 0.015 h);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.admonition.caution strong {
|
|
516
|
+
color: var(--color-ad-caution);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/* Vega-Lite charts*/
|
|
520
|
+
.vega-lite {
|
|
521
|
+
display: flex;
|
|
522
|
+
justify-content: center;
|
|
523
|
+
align-items: center;
|
|
524
|
+
width: 100%;
|
|
525
|
+
margin-top: 1em;
|
|
526
|
+
margin-bottom: 1em;
|
|
435
527
|
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Extras are defined as helper functions called by
|
|
3
|
+
render_extra_features from transform.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import vl_convert as vlc
|
|
7
|
+
from ruamel.yaml import YAML
|
|
8
|
+
from bs4 import Tag, BeautifulSoup
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ExtraFeature:
|
|
13
|
+
"""
|
|
14
|
+
Base class for extra features that can be applied to HTML.
|
|
15
|
+
|
|
16
|
+
Attributes:
|
|
17
|
+
pattern (str): Regex pattern to match the extra feature in the HTML.
|
|
18
|
+
run_before_stash (bool): Whether to run this extra before stashing code blocks.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
pattern = r""
|
|
22
|
+
run_before_stash = False
|
|
23
|
+
|
|
24
|
+
def replace(self, match, html):
|
|
25
|
+
"""
|
|
26
|
+
Replaces the matched pattern with the rendered extra feature.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
match (re.Match): The regex match object.
|
|
30
|
+
html (str): The full HTML content.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
str: The replacement string.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
NotImplementedError: If the subclass does not implement this method.
|
|
37
|
+
"""
|
|
38
|
+
raise NotImplementedError("Subclasses must implement the replace method.")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class CheckboxExtra(ExtraFeature):
|
|
42
|
+
"""
|
|
43
|
+
Extra feature for rendering checkboxes.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
pattern = r"(?P<checkbox>\[\s\]|\[x\])"
|
|
47
|
+
|
|
48
|
+
def replace(match, html):
|
|
49
|
+
"""
|
|
50
|
+
Render a tag for a checkbox.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
match: Element identified as a checkbox
|
|
54
|
+
Returns:
|
|
55
|
+
str: tag representing the checkbox
|
|
56
|
+
"""
|
|
57
|
+
status = "checked" if "[x]" in match.group("checkbox") else ""
|
|
58
|
+
return f'<input type="checkbox" {status}>'
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class HighlightExtra(ExtraFeature):
|
|
62
|
+
"""
|
|
63
|
+
Extra feature for rendering highlighted text.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
pattern = r"==(?P<content>.*?)=="
|
|
67
|
+
|
|
68
|
+
def replace(match, html):
|
|
69
|
+
"""
|
|
70
|
+
Render a tag for a highlight.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
match: Element identified as a highlight
|
|
74
|
+
Returns:
|
|
75
|
+
str: tag representing the highlight
|
|
76
|
+
"""
|
|
77
|
+
content = match.group("content")
|
|
78
|
+
return f'<span class="highlight">{content}</span>'
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class CustomSpanExtra(ExtraFeature):
|
|
82
|
+
"""
|
|
83
|
+
Extra feature for rendering custom spans with specific classes.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
pattern = r"(?P<cls>[a-zA-Z0-9_-]+)\{\{\s*(?P<content>.*?)\s*\}\}"
|
|
87
|
+
|
|
88
|
+
def replace(match, html):
|
|
89
|
+
"""
|
|
90
|
+
Render a tag for a custom span.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
match: Element identified as a custom span
|
|
94
|
+
Returns:
|
|
95
|
+
str: tag representing the custom span
|
|
96
|
+
"""
|
|
97
|
+
cls = match.group("cls")
|
|
98
|
+
content = match.group("content")
|
|
99
|
+
return f'<span class="{cls}">{content}</span>'
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class TocExtra(ExtraFeature):
|
|
103
|
+
"""
|
|
104
|
+
Extra feature for rendering a Table of Contents.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
pattern = r"\[TOC(?:\s+depth=(?P<depth>\d+))?\]"
|
|
108
|
+
|
|
109
|
+
def replace(match, html):
|
|
110
|
+
"""
|
|
111
|
+
Render a tag for a table of contents
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
match: Element identified as a table of contents
|
|
115
|
+
Returns:
|
|
116
|
+
str: tag representing the table of contents
|
|
117
|
+
"""
|
|
118
|
+
soup = BeautifulSoup(html, "html.parser")
|
|
119
|
+
max_level = match.group("depth")
|
|
120
|
+
max_level = 3 if max_level is None else int(max_level)
|
|
121
|
+
|
|
122
|
+
headers = [
|
|
123
|
+
header
|
|
124
|
+
for header in soup.find_all(
|
|
125
|
+
[f"h{index}" for index in range(1, max_level + 1)]
|
|
126
|
+
)
|
|
127
|
+
if header.get("id")
|
|
128
|
+
]
|
|
129
|
+
if not headers:
|
|
130
|
+
return ""
|
|
131
|
+
|
|
132
|
+
tag: Tag = soup.new_tag("ul", attrs={"class": "toc"})
|
|
133
|
+
active_list = {0: tag}
|
|
134
|
+
last_list_element = {}
|
|
135
|
+
|
|
136
|
+
for header in headers:
|
|
137
|
+
level = int(header.name[1])
|
|
138
|
+
|
|
139
|
+
if level not in active_list:
|
|
140
|
+
parent_lvl = max(key for key in active_list if key < level)
|
|
141
|
+
if last_list_element.get(parent_lvl):
|
|
142
|
+
sub_list = soup.new_tag("ul")
|
|
143
|
+
last_list_element[parent_lvl].append(sub_list)
|
|
144
|
+
active_list[level] = sub_list
|
|
145
|
+
else:
|
|
146
|
+
active_list[level] = active_list[parent_lvl]
|
|
147
|
+
|
|
148
|
+
active_list = {
|
|
149
|
+
key: value for key, value in active_list.items() if key <= level
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
list_item = soup.new_tag("li")
|
|
153
|
+
link = soup.new_tag("a", href=f"#{header['id']}")
|
|
154
|
+
link.string = header.get_text(strip=True)
|
|
155
|
+
list_item.append(link)
|
|
156
|
+
|
|
157
|
+
active_list[level].append(list_item)
|
|
158
|
+
last_list_element[level] = list_item
|
|
159
|
+
|
|
160
|
+
return tag.prettify()
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class VegaExtra(ExtraFeature):
|
|
164
|
+
"""
|
|
165
|
+
Extra feature for rendering Vega-Lite diagrams from YAML.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
pattern = r"(?s)<pre><code>\$schema: https://vega\.github\.io(?P<content>.*?)</code></pre>"
|
|
169
|
+
run_before_stash = True
|
|
170
|
+
|
|
171
|
+
def replace(match, html):
|
|
172
|
+
"""
|
|
173
|
+
Render a tag for a vega lite diagram YAML.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
match (re.Match): Element identified as a vega lite diagram YAML.
|
|
177
|
+
html (str): The full HTML content.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
str: SVG tag representing the vega lite diagram.
|
|
181
|
+
"""
|
|
182
|
+
schema_line = "$schema: https://vega.github.io"
|
|
183
|
+
yaml = YAML()
|
|
184
|
+
spec = yaml.load(schema_line + match.group("content"))
|
|
185
|
+
tag = vlc.vegalite_to_svg(spec)
|
|
186
|
+
return f"<div class='vega-lite'>{tag}</div>"
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def apply_extras(extras: set[ExtraFeature], html, before_stash=False):
|
|
190
|
+
"""
|
|
191
|
+
Applies extra features to an html string.
|
|
192
|
+
Args:
|
|
193
|
+
extras: set[ExtraFeature] Extra features to apply
|
|
194
|
+
html: complete html text, used by some extras like TOC.
|
|
195
|
+
Returns:
|
|
196
|
+
str: The updated html.
|
|
197
|
+
"""
|
|
198
|
+
for extra in extras:
|
|
199
|
+
if not extra.run_before_stash == before_stash:
|
|
200
|
+
continue
|
|
201
|
+
|
|
202
|
+
# Loop until the pattern no longer matches
|
|
203
|
+
while re.search(extra.pattern, html, flags=re.DOTALL):
|
|
204
|
+
new_html = html
|
|
205
|
+
try:
|
|
206
|
+
new_html = re.sub(
|
|
207
|
+
extra.pattern,
|
|
208
|
+
lambda match: extra.replace(match, html=html),
|
|
209
|
+
html,
|
|
210
|
+
flags=re.DOTALL,
|
|
211
|
+
)
|
|
212
|
+
except Exception as exc:
|
|
213
|
+
print(
|
|
214
|
+
f"WARNING: An exception occurred while trying to apply an extra:\n{exc}"
|
|
215
|
+
)
|
|
216
|
+
pass
|
|
217
|
+
|
|
218
|
+
# Safety break:
|
|
219
|
+
if new_html == html:
|
|
220
|
+
break
|
|
221
|
+
html = new_html
|
|
222
|
+
|
|
223
|
+
return html
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module for transforming HTML content.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
from bs4 import BeautifulSoup
|
|
8
|
+
|
|
9
|
+
from .extras import (
|
|
10
|
+
apply_extras,
|
|
11
|
+
ExtraFeature,
|
|
12
|
+
CheckboxExtra,
|
|
13
|
+
CustomSpanExtra,
|
|
14
|
+
HighlightExtra,
|
|
15
|
+
TocExtra,
|
|
16
|
+
VegaExtra,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def create_html_document(html_content, css_content, csp):
|
|
21
|
+
"""
|
|
22
|
+
Creates a complete HTML document with the given content, CSS, and Content Security Policy.
|
|
23
|
+
Args:
|
|
24
|
+
html_content (str): The HTML content to include in the body.
|
|
25
|
+
css_content (str): The CSS styles to include in the head.
|
|
26
|
+
csp (str): The Content Security Policy string.
|
|
27
|
+
Returns:
|
|
28
|
+
str: A complete HTML document as a string.
|
|
29
|
+
"""
|
|
30
|
+
return f"""<!DOCTYPE html>
|
|
31
|
+
<html>
|
|
32
|
+
<head>
|
|
33
|
+
<meta charset="UTF-8">
|
|
34
|
+
<meta http-equiv="Content-Security-Policy" content="{csp or ""}">
|
|
35
|
+
<style>
|
|
36
|
+
{css_content or ""}
|
|
37
|
+
</style>
|
|
38
|
+
</head>
|
|
39
|
+
<body>
|
|
40
|
+
{html_content or ""}
|
|
41
|
+
</body>
|
|
42
|
+
</html>"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def create_sections(html_string):
|
|
46
|
+
"""
|
|
47
|
+
Wraps each h2 and its following content in a <section> tag.
|
|
48
|
+
The section ends when the next h2 is encountered, or the parent ends.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
html_string (str): The input HTML string.
|
|
52
|
+
Returns:
|
|
53
|
+
str: The modified HTML string with sections wrapped.
|
|
54
|
+
"""
|
|
55
|
+
soup = BeautifulSoup(html_string, "html.parser")
|
|
56
|
+
|
|
57
|
+
for header in soup.find_all("h2"):
|
|
58
|
+
new_section = soup.new_tag("section")
|
|
59
|
+
header.insert_before(new_section)
|
|
60
|
+
|
|
61
|
+
current = header
|
|
62
|
+
while current is not None and (current == header or current.name != "h2"):
|
|
63
|
+
next_sibling = current.next_sibling
|
|
64
|
+
new_section.append(current)
|
|
65
|
+
current = next_sibling
|
|
66
|
+
|
|
67
|
+
return str(soup)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def render_mermaid_diagrams(html, *, nonce):
|
|
71
|
+
"""
|
|
72
|
+
Renders Mermaid diagrams in the HTML content.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
html (str): HTML content.
|
|
76
|
+
nonce (str): Cryptographic nonce for CSP.
|
|
77
|
+
Returns:
|
|
78
|
+
str: HTML content with rendered Mermaid diagrams.
|
|
79
|
+
"""
|
|
80
|
+
mermaid_script = f"""
|
|
81
|
+
<script type="module" nonce="{nonce}">
|
|
82
|
+
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
|
83
|
+
mermaid.initialize({{
|
|
84
|
+
startOnLoad: true,
|
|
85
|
+
theme: 'default',
|
|
86
|
+
themeVariables: {{}},
|
|
87
|
+
fontFamily: 'arial, verdana, sans-serif'
|
|
88
|
+
}});
|
|
89
|
+
</script>
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
if '<div class="mermaid">' in html:
|
|
93
|
+
html = mermaid_script + html
|
|
94
|
+
|
|
95
|
+
return html
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def render_extra_features(
|
|
99
|
+
html,
|
|
100
|
+
extras: set[ExtraFeature] = (
|
|
101
|
+
CheckboxExtra,
|
|
102
|
+
CustomSpanExtra,
|
|
103
|
+
HighlightExtra,
|
|
104
|
+
TocExtra,
|
|
105
|
+
VegaExtra,
|
|
106
|
+
),
|
|
107
|
+
):
|
|
108
|
+
"""
|
|
109
|
+
Renders extra features by protecting specific tags, applying regex
|
|
110
|
+
transformations, and restoring the protected content.
|
|
111
|
+
"""
|
|
112
|
+
placeholders = {}
|
|
113
|
+
|
|
114
|
+
def stash(match):
|
|
115
|
+
key = f"__PROTECTED_BLOCK_{len(placeholders)}__"
|
|
116
|
+
placeholders[key] = match.group(0)
|
|
117
|
+
return key
|
|
118
|
+
|
|
119
|
+
# 0. Pre protection extras
|
|
120
|
+
html = apply_extras(extras, html, before_stash=True)
|
|
121
|
+
|
|
122
|
+
# 1. Protection: Replace ignored tags with unique hashes
|
|
123
|
+
ignored_pattern = re.compile(
|
|
124
|
+
r"<(code|pre|script|style)\b[^>]*>.*?</\1>", re.DOTALL | re.IGNORECASE
|
|
125
|
+
)
|
|
126
|
+
html = ignored_pattern.sub(stash, html)
|
|
127
|
+
|
|
128
|
+
# 2. Transformations: Define patterns and their replacements
|
|
129
|
+
html = apply_extras(extras, html, before_stash=False)
|
|
130
|
+
|
|
131
|
+
# 3. Restoration: Replace hashes back with original content
|
|
132
|
+
for key, original_content in placeholders.items():
|
|
133
|
+
html = html.replace(key, original_content)
|
|
134
|
+
|
|
135
|
+
return html
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "markdown_convert"
|
|
7
|
-
version = "1.2.
|
|
7
|
+
version = "1.2.52"
|
|
8
8
|
description = "Convert Markdown files to PDF from your command line."
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Julio Cabria", email = "juliocabria@tutanota.com" },
|
|
@@ -25,6 +25,8 @@ dependencies = [
|
|
|
25
25
|
"playwright>=1.57.0",
|
|
26
26
|
"beautifulsoup4>=4.14.3",
|
|
27
27
|
"install-playwright>=1.0.0",
|
|
28
|
+
"vl-convert-python>=1.9.0.post1",
|
|
29
|
+
"ruamel-yaml>=0.19.1",
|
|
28
30
|
]
|
|
29
31
|
|
|
30
32
|
[project.urls]
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Extras are defined as helper functions called by
|
|
3
|
-
render_extra_features from transform.py
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def create_checkbox(soup, match):
|
|
8
|
-
"""
|
|
9
|
-
Render a tag for a checkbox.
|
|
10
|
-
|
|
11
|
-
Args:
|
|
12
|
-
soup: HTML beautifulsoup
|
|
13
|
-
match: Element identified as a checkbox
|
|
14
|
-
Returns:
|
|
15
|
-
tag: Beautifulsoup tag representing the checkbox
|
|
16
|
-
"""
|
|
17
|
-
tag = soup.new_tag("input", type="checkbox")
|
|
18
|
-
if "[x]" in match.group("checkbox"):
|
|
19
|
-
tag["checked"] = ""
|
|
20
|
-
return tag
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def create_highlight(soup, match):
|
|
24
|
-
"""
|
|
25
|
-
Render a tag for a highlight.
|
|
26
|
-
|
|
27
|
-
Args:
|
|
28
|
-
soup: HTML beautifulsoup
|
|
29
|
-
match: Element identified as a highlight
|
|
30
|
-
Returns:
|
|
31
|
-
tag: Beautifulsoup tag representing the highlight
|
|
32
|
-
"""
|
|
33
|
-
tag = soup.new_tag("span", attrs={"class": "highlight"})
|
|
34
|
-
tag.string = match.group("hl_content")
|
|
35
|
-
return tag
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def create_custom_span(soup, match):
|
|
39
|
-
"""
|
|
40
|
-
Render a tag for a custom span.
|
|
41
|
-
|
|
42
|
-
Args:
|
|
43
|
-
soup: HTML beautifulsoup
|
|
44
|
-
match: Element identified as a custom span
|
|
45
|
-
Returns:
|
|
46
|
-
tag: Beautifulsoup tag representing the custom span
|
|
47
|
-
"""
|
|
48
|
-
tag = soup.new_tag("span", attrs={"class": match.group("cls")})
|
|
49
|
-
tag.string = match.group("sp_content")
|
|
50
|
-
return tag
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def create_toc(soup, match):
|
|
54
|
-
"""
|
|
55
|
-
Render a tag for a table of contents
|
|
56
|
-
|
|
57
|
-
Args:
|
|
58
|
-
soup: HTML beautifulsoup
|
|
59
|
-
match: Element identified as a table of contents
|
|
60
|
-
Returns:
|
|
61
|
-
tag: Beautifulsoup tag representing the table of contents
|
|
62
|
-
"""
|
|
63
|
-
max_level = match.group("depth")
|
|
64
|
-
max_level = 3 if max_level is None else int(max_level)
|
|
65
|
-
|
|
66
|
-
headers = [
|
|
67
|
-
header
|
|
68
|
-
for header in soup.find_all([f"h{index}" for index in range(1, max_level + 1)])
|
|
69
|
-
if header.get("id")
|
|
70
|
-
]
|
|
71
|
-
if not headers:
|
|
72
|
-
return ""
|
|
73
|
-
|
|
74
|
-
tag = soup.new_tag("ul", attrs={"class": "toc"})
|
|
75
|
-
active_list = {0: tag}
|
|
76
|
-
last_list_element = {}
|
|
77
|
-
|
|
78
|
-
for header in headers:
|
|
79
|
-
level = int(header.name[1])
|
|
80
|
-
|
|
81
|
-
if level not in active_list:
|
|
82
|
-
parent_lvl = max(key for key in active_list if key < level)
|
|
83
|
-
if last_list_element.get(parent_lvl):
|
|
84
|
-
sub_list = soup.new_tag("ul")
|
|
85
|
-
last_list_element[parent_lvl].append(sub_list)
|
|
86
|
-
active_list[level] = sub_list
|
|
87
|
-
else:
|
|
88
|
-
active_list[level] = active_list[parent_lvl]
|
|
89
|
-
|
|
90
|
-
active_list = {key: value for key, value in active_list.items() if key <= level}
|
|
91
|
-
|
|
92
|
-
list_item = soup.new_tag("li")
|
|
93
|
-
link = soup.new_tag("a", href=f"#{header['id']}")
|
|
94
|
-
link.string = header.get_text(strip=True)
|
|
95
|
-
list_item.append(link)
|
|
96
|
-
|
|
97
|
-
active_list[level].append(list_item)
|
|
98
|
-
last_list_element[level] = list_item
|
|
99
|
-
|
|
100
|
-
return tag
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Module for transforming HTML content.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import re
|
|
6
|
-
|
|
7
|
-
from bs4 import BeautifulSoup
|
|
8
|
-
|
|
9
|
-
from .constants import YELLOW
|
|
10
|
-
from .extras import create_checkbox, create_custom_span, create_highlight, create_toc
|
|
11
|
-
from .utils import color
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def create_html_document(html_content, css_content, csp):
|
|
15
|
-
"""
|
|
16
|
-
Creates a complete HTML document with the given content, CSS, and Content Security Policy.
|
|
17
|
-
Args:
|
|
18
|
-
html_content (str): The HTML content to include in the body.
|
|
19
|
-
css_content (str): The CSS styles to include in the head.
|
|
20
|
-
csp (str): The Content Security Policy string.
|
|
21
|
-
Returns:
|
|
22
|
-
str: A complete HTML document as a string.
|
|
23
|
-
"""
|
|
24
|
-
return f"""<!DOCTYPE html>
|
|
25
|
-
<html>
|
|
26
|
-
<head>
|
|
27
|
-
<meta charset="UTF-8">
|
|
28
|
-
<meta http-equiv="Content-Security-Policy" content="{csp or ""}">
|
|
29
|
-
<style>
|
|
30
|
-
{css_content or ""}
|
|
31
|
-
</style>
|
|
32
|
-
</head>
|
|
33
|
-
<body>
|
|
34
|
-
{html_content or ""}
|
|
35
|
-
</body>
|
|
36
|
-
</html>"""
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def create_sections(html_string):
|
|
40
|
-
"""
|
|
41
|
-
Wraps each h2 and its following content in a <section> tag.
|
|
42
|
-
The section ends when the next h2 is encountered, or the parent ends.
|
|
43
|
-
|
|
44
|
-
Args:
|
|
45
|
-
html_string (str): The input HTML string.
|
|
46
|
-
Returns:
|
|
47
|
-
str: The modified HTML string with sections wrapped.
|
|
48
|
-
"""
|
|
49
|
-
soup = BeautifulSoup(html_string, "html.parser")
|
|
50
|
-
|
|
51
|
-
for header in soup.find_all("h2"):
|
|
52
|
-
new_section = soup.new_tag("section")
|
|
53
|
-
header.insert_before(new_section)
|
|
54
|
-
|
|
55
|
-
current = header
|
|
56
|
-
while current is not None and (current == header or current.name != "h2"):
|
|
57
|
-
next_sibling = current.next_sibling
|
|
58
|
-
new_section.append(current)
|
|
59
|
-
current = next_sibling
|
|
60
|
-
|
|
61
|
-
return str(soup)
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
def render_mermaid_diagrams(html, *, nonce):
|
|
65
|
-
"""
|
|
66
|
-
Renders Mermaid diagrams in the HTML content.
|
|
67
|
-
|
|
68
|
-
Args:
|
|
69
|
-
html (str): HTML content.
|
|
70
|
-
nonce (str): Cryptographic nonce for CSP.
|
|
71
|
-
Returns:
|
|
72
|
-
str: HTML content with rendered Mermaid diagrams.
|
|
73
|
-
"""
|
|
74
|
-
mermaid_script = f"""
|
|
75
|
-
<script type="module" nonce="{nonce}">
|
|
76
|
-
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
|
77
|
-
mermaid.initialize({{
|
|
78
|
-
startOnLoad: true,
|
|
79
|
-
theme: 'default',
|
|
80
|
-
themeVariables: {{}},
|
|
81
|
-
fontFamily: 'arial, verdana, sans-serif'
|
|
82
|
-
}});
|
|
83
|
-
</script>
|
|
84
|
-
"""
|
|
85
|
-
|
|
86
|
-
if '<div class="mermaid">' in html:
|
|
87
|
-
html = mermaid_script + html
|
|
88
|
-
|
|
89
|
-
return html
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def render_extra_features(html):
|
|
93
|
-
"""
|
|
94
|
-
Renders extra features like checkboxes, highlights, and custom spans in the HTML content.
|
|
95
|
-
|
|
96
|
-
Args:
|
|
97
|
-
html (str): HTML content.
|
|
98
|
-
Returns:
|
|
99
|
-
str: HTML content with extra features rendered.
|
|
100
|
-
"""
|
|
101
|
-
|
|
102
|
-
handlers = {
|
|
103
|
-
"checkbox": create_checkbox,
|
|
104
|
-
"highlight": create_highlight,
|
|
105
|
-
"span": create_custom_span,
|
|
106
|
-
"toc": create_toc,
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
master_pattern = re.compile(
|
|
110
|
-
r"(?P<checkbox>\[\s\]|\[x\])|"
|
|
111
|
-
r"(?P<highlight>==(?P<hl_content>.*?)==)|"
|
|
112
|
-
r"(?P<span>(?P<cls>[a-zA-Z0-9_-]+)\{\{\s*(?P<sp_content>.*?)\s*\}\})|"
|
|
113
|
-
r"(?P<toc>\[TOC(?:\s+depth=(?P<depth>\d+))?\])"
|
|
114
|
-
)
|
|
115
|
-
|
|
116
|
-
ignored_tags = {"code", "pre", "script", "style"}
|
|
117
|
-
|
|
118
|
-
soup = BeautifulSoup(html, "html.parser")
|
|
119
|
-
for text_node in soup.find_all(string=True):
|
|
120
|
-
# Ignore text nodes within certain tags
|
|
121
|
-
if text_node.parent.name in ignored_tags:
|
|
122
|
-
continue
|
|
123
|
-
|
|
124
|
-
# If no match, skip processing
|
|
125
|
-
content = text_node.string
|
|
126
|
-
if not master_pattern.search(content):
|
|
127
|
-
continue
|
|
128
|
-
|
|
129
|
-
new_nodes = []
|
|
130
|
-
last_end = 0
|
|
131
|
-
for match in master_pattern.finditer(content):
|
|
132
|
-
start, end = match.span()
|
|
133
|
-
|
|
134
|
-
# Append text before the match
|
|
135
|
-
if start > last_end:
|
|
136
|
-
new_nodes.append(content[last_end:start])
|
|
137
|
-
|
|
138
|
-
kind = match.lastgroup
|
|
139
|
-
|
|
140
|
-
# Call the appropriate handler
|
|
141
|
-
handler = handlers.get(kind)
|
|
142
|
-
if handler:
|
|
143
|
-
try:
|
|
144
|
-
tag = handler(soup, match)
|
|
145
|
-
new_nodes.append(tag)
|
|
146
|
-
except Exception as exc:
|
|
147
|
-
print(
|
|
148
|
-
color(
|
|
149
|
-
YELLOW,
|
|
150
|
-
f"WARNING: Handler for '{kind}' failed with exception: {exc}",
|
|
151
|
-
)
|
|
152
|
-
)
|
|
153
|
-
new_nodes.append(match.group(0))
|
|
154
|
-
|
|
155
|
-
last_end = end
|
|
156
|
-
|
|
157
|
-
# Append any remaining text after the last match
|
|
158
|
-
if new_nodes:
|
|
159
|
-
if last_end < len(content):
|
|
160
|
-
new_nodes.append(content[last_end:])
|
|
161
|
-
|
|
162
|
-
text_node.replace_with(*new_nodes)
|
|
163
|
-
|
|
164
|
-
return str(soup)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|