markdown_convert 1.2.54__tar.gz → 1.2.56__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.54 → markdown_convert-1.2.56}/PKG-INFO +20 -4
- {markdown_convert-1.2.54 → markdown_convert-1.2.56}/README.md +19 -3
- {markdown_convert-1.2.54 → markdown_convert-1.2.56}/markdown_convert/__main__.py +20 -3
- {markdown_convert-1.2.54 → markdown_convert-1.2.56}/markdown_convert/default.css +4 -0
- {markdown_convert-1.2.54 → markdown_convert-1.2.56}/markdown_convert/modules/autoinstall.py +1 -1
- markdown_convert-1.2.56/markdown_convert/modules/constants.py +152 -0
- {markdown_convert-1.2.54 → markdown_convert-1.2.56}/markdown_convert/modules/convert.py +126 -115
- {markdown_convert-1.2.54 → markdown_convert-1.2.56}/markdown_convert/modules/extras.py +14 -9
- {markdown_convert-1.2.54 → markdown_convert-1.2.56}/markdown_convert/modules/resources.py +32 -1
- {markdown_convert-1.2.54 → markdown_convert-1.2.56}/markdown_convert/modules/transform.py +3 -19
- {markdown_convert-1.2.54 → markdown_convert-1.2.56}/markdown_convert/modules/utils.py +0 -13
- {markdown_convert-1.2.54 → markdown_convert-1.2.56}/pyproject.toml +1 -1
- markdown_convert-1.2.54/markdown_convert/modules/constants.py +0 -57
- {markdown_convert-1.2.54 → markdown_convert-1.2.56}/.gitignore +0 -0
- {markdown_convert-1.2.54 → markdown_convert-1.2.56}/LICENSE +0 -0
- {markdown_convert-1.2.54 → markdown_convert-1.2.56}/markdown_convert/__init__.py +0 -0
- {markdown_convert-1.2.54 → markdown_convert-1.2.56}/markdown_convert/code.css +0 -0
- {markdown_convert-1.2.54 → markdown_convert-1.2.56}/markdown_convert/modules/__init__.py +0 -0
- {markdown_convert-1.2.54 → markdown_convert-1.2.56}/markdown_convert/modules/overrides.py +0 -0
- {markdown_convert-1.2.54 → markdown_convert-1.2.56}/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.56
|
|
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>
|
|
@@ -80,9 +80,25 @@ Simply run `markdown-convert file.md` to convert `file.md` to `file.pdf`.
|
|
|
80
80
|
|
|
81
81
|
You can specify the following options:
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
-
|
|
83
|
+
```text
|
|
84
|
+
Usage:
|
|
85
|
+
markdown-convert [markdown_file_path] [options]
|
|
86
|
+
|
|
87
|
+
Options:
|
|
88
|
+
--mode=once|live|debug
|
|
89
|
+
Convert the markdown file once (default) or live.
|
|
90
|
+
Use debug to preserve the intermediate html file.
|
|
91
|
+
--css=[css_file_path]
|
|
92
|
+
Use a custom CSS file.
|
|
93
|
+
--out=[output_file_path]
|
|
94
|
+
Specify the output file path.
|
|
95
|
+
--extras=[extra1,extra2,...]
|
|
96
|
+
Specify the extras to use. Uses all extras if not specified.
|
|
97
|
+
Supported extras:
|
|
98
|
+
fenced-code-blocks,header-ids,breaks,tables,latex,mermaid,
|
|
99
|
+
strike,admonitions,checkboxes,custom-spans,highlights,toc,
|
|
100
|
+
vega-lite
|
|
101
|
+
```
|
|
86
102
|
|
|
87
103
|
For example: `markdown-convert README.md --mode=live --css=style.css --out=output.pdf` will convert `README.md` to `output.pdf` using `style.css` and update the PDF live as you edit the Markdown file.
|
|
88
104
|
|
|
@@ -57,9 +57,25 @@ Simply run `markdown-convert file.md` to convert `file.md` to `file.pdf`.
|
|
|
57
57
|
|
|
58
58
|
You can specify the following options:
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
-
|
|
60
|
+
```text
|
|
61
|
+
Usage:
|
|
62
|
+
markdown-convert [markdown_file_path] [options]
|
|
63
|
+
|
|
64
|
+
Options:
|
|
65
|
+
--mode=once|live|debug
|
|
66
|
+
Convert the markdown file once (default) or live.
|
|
67
|
+
Use debug to preserve the intermediate html file.
|
|
68
|
+
--css=[css_file_path]
|
|
69
|
+
Use a custom CSS file.
|
|
70
|
+
--out=[output_file_path]
|
|
71
|
+
Specify the output file path.
|
|
72
|
+
--extras=[extra1,extra2,...]
|
|
73
|
+
Specify the extras to use. Uses all extras if not specified.
|
|
74
|
+
Supported extras:
|
|
75
|
+
fenced-code-blocks,header-ids,breaks,tables,latex,mermaid,
|
|
76
|
+
strike,admonitions,checkboxes,custom-spans,highlights,toc,
|
|
77
|
+
vega-lite
|
|
78
|
+
```
|
|
63
79
|
|
|
64
80
|
For example: `markdown-convert README.md --mode=live --css=style.css --out=output.pdf` will convert `README.md` to `output.pdf` using `style.css` and update the PDF live as you edit the Markdown file.
|
|
65
81
|
|
|
@@ -9,7 +9,7 @@ from sys import exit as sys_exit
|
|
|
9
9
|
|
|
10
10
|
from argsdict import args
|
|
11
11
|
|
|
12
|
-
from .modules.constants import OPTIONS, OPTIONS_MODES, RED
|
|
12
|
+
from .modules.constants import EXTRAS, OPTIONS, OPTIONS_MODES, RED
|
|
13
13
|
from .modules.convert import convert, live_convert
|
|
14
14
|
from .modules.resources import get_css_path, get_output_path, get_usage
|
|
15
15
|
from .modules.utils import color
|
|
@@ -67,12 +67,29 @@ def main():
|
|
|
67
67
|
except Exception as exc:
|
|
68
68
|
raise IndexError(f"Invalid 'output_path' argument: {exc}") from exc
|
|
69
69
|
|
|
70
|
+
# Get the extras
|
|
71
|
+
extras = None
|
|
72
|
+
try:
|
|
73
|
+
if "--extras" in arg:
|
|
74
|
+
extras = arg["--extras"].split(",")
|
|
75
|
+
for extra in extras:
|
|
76
|
+
if extra not in EXTRAS:
|
|
77
|
+
raise ValueError(extra)
|
|
78
|
+
except Exception as exc:
|
|
79
|
+
raise IndexError(f"Invalid 'extras' argument: {exc}") from exc
|
|
80
|
+
|
|
70
81
|
# Compile the markdown file
|
|
71
82
|
print(f"\nGenerating PDF file from '{markdown_path}'...\n")
|
|
72
83
|
if mode in ("once", "debug"):
|
|
73
|
-
convert(
|
|
84
|
+
convert(
|
|
85
|
+
markdown_path,
|
|
86
|
+
css_path,
|
|
87
|
+
output_path,
|
|
88
|
+
dump_html=mode == "debug",
|
|
89
|
+
extras=extras,
|
|
90
|
+
)
|
|
74
91
|
else:
|
|
75
|
-
live_convert(markdown_path, css_path, output_path)
|
|
92
|
+
live_convert(markdown_path, css_path, output_path, extras=extras)
|
|
76
93
|
|
|
77
94
|
sys_exit(0)
|
|
78
95
|
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains the constants used in the markdown_convert package.
|
|
3
|
+
Author: @julynx
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .extras import (
|
|
7
|
+
CheckboxExtra,
|
|
8
|
+
CustomSpanExtra,
|
|
9
|
+
HighlightExtra,
|
|
10
|
+
TocExtra,
|
|
11
|
+
VegaExtra,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
RED = "31"
|
|
15
|
+
GREEN = "32"
|
|
16
|
+
YELLOW = "33"
|
|
17
|
+
BLUE = "34"
|
|
18
|
+
MAGENTA = "35"
|
|
19
|
+
CYAN = "36"
|
|
20
|
+
|
|
21
|
+
OPTIONS = (
|
|
22
|
+
"markdown_file_path",
|
|
23
|
+
"--mode",
|
|
24
|
+
"--css",
|
|
25
|
+
"--out",
|
|
26
|
+
"--extras",
|
|
27
|
+
"-h",
|
|
28
|
+
"--help",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
OPTIONS_MODES = ("once", "live", "debug")
|
|
32
|
+
|
|
33
|
+
EXTRAS = {
|
|
34
|
+
"fenced-code-blocks": {
|
|
35
|
+
"provided-by": "markdown2",
|
|
36
|
+
"args": None,
|
|
37
|
+
},
|
|
38
|
+
"header-ids": {
|
|
39
|
+
"provided-by": "markdown2",
|
|
40
|
+
"args": True,
|
|
41
|
+
},
|
|
42
|
+
"breaks": {
|
|
43
|
+
"provided-by": "markdown2",
|
|
44
|
+
"args": {"on_newline": True},
|
|
45
|
+
},
|
|
46
|
+
"tables": {
|
|
47
|
+
"provided-by": "markdown2",
|
|
48
|
+
"args": True,
|
|
49
|
+
},
|
|
50
|
+
"latex": {
|
|
51
|
+
"provided-by": "markdown2",
|
|
52
|
+
"args": True,
|
|
53
|
+
},
|
|
54
|
+
"mermaid": {
|
|
55
|
+
"provided-by": "markdown2",
|
|
56
|
+
"args": None,
|
|
57
|
+
},
|
|
58
|
+
"strike": {
|
|
59
|
+
"provided-by": "markdown2",
|
|
60
|
+
"args": None,
|
|
61
|
+
},
|
|
62
|
+
"admonitions": {
|
|
63
|
+
"provided-by": "markdown2",
|
|
64
|
+
"args": None,
|
|
65
|
+
},
|
|
66
|
+
"checkboxes": {
|
|
67
|
+
"provided-by": "markdown-convert",
|
|
68
|
+
"args": CheckboxExtra,
|
|
69
|
+
},
|
|
70
|
+
"custom-spans": {
|
|
71
|
+
"provided-by": "markdown-convert",
|
|
72
|
+
"args": CustomSpanExtra,
|
|
73
|
+
},
|
|
74
|
+
"highlights": {
|
|
75
|
+
"provided-by": "markdown-convert",
|
|
76
|
+
"args": HighlightExtra,
|
|
77
|
+
},
|
|
78
|
+
"toc": {
|
|
79
|
+
"provided-by": "markdown-convert",
|
|
80
|
+
"args": TocExtra,
|
|
81
|
+
},
|
|
82
|
+
"vega-lite": {
|
|
83
|
+
"provided-by": "markdown-convert",
|
|
84
|
+
"args": VegaExtra,
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
BROWSER_ARGS = [
|
|
89
|
+
"--disable-dev-shm-usage",
|
|
90
|
+
"--disable-extensions",
|
|
91
|
+
"--disable-plugins",
|
|
92
|
+
"--disable-gpu",
|
|
93
|
+
"--no-first-run",
|
|
94
|
+
"--no-default-browser-check",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
CSP_TEMPLATE = (
|
|
98
|
+
"default-src 'none'; "
|
|
99
|
+
"script-src 'nonce-{nonce}' https://cdn.jsdelivr.net; " # <- Script for Mermaid diagrams
|
|
100
|
+
"script-src-elem 'nonce-{nonce}' https://cdn.jsdelivr.net; "
|
|
101
|
+
"style-src 'unsafe-inline'; "
|
|
102
|
+
"img-src data: https: file:; "
|
|
103
|
+
"font-src data: https:; "
|
|
104
|
+
"connect-src https://cdn.jsdelivr.net;"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
PDF_PARAMS = {
|
|
108
|
+
"format": "A4",
|
|
109
|
+
"print_background": True,
|
|
110
|
+
"margin": {
|
|
111
|
+
"top": "20mm",
|
|
112
|
+
"bottom": "20mm",
|
|
113
|
+
"left": "20mm",
|
|
114
|
+
"right": "20mm",
|
|
115
|
+
},
|
|
116
|
+
"path": None, # <- Replace with actual output path when used
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def resolve_extras(extras_list=None):
|
|
121
|
+
"""
|
|
122
|
+
Resolve the extras to be used in the conversion.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
extras_list (list=None): List of extras to use. If None, all extras are used.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
dict: A dictionary containing the keys "markdown2_extras" and "markdown_convert_extras"
|
|
129
|
+
"""
|
|
130
|
+
if extras_list is None:
|
|
131
|
+
selected_extras = EXTRAS
|
|
132
|
+
else:
|
|
133
|
+
selected_extras = {
|
|
134
|
+
key: value for key, value in EXTRAS.items() if key in extras_list
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
markdown2_extras = {
|
|
138
|
+
extra: config["args"]
|
|
139
|
+
for extra, config in selected_extras.items()
|
|
140
|
+
if config["provided-by"] == "markdown2"
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
markdown_convert_extras = {
|
|
144
|
+
config["args"]
|
|
145
|
+
for _, config in selected_extras.items()
|
|
146
|
+
if config["provided-by"] == "markdown-convert"
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
"markdown2_extras": markdown2_extras,
|
|
151
|
+
"markdown_convert_extras": markdown_convert_extras,
|
|
152
|
+
}
|
|
@@ -15,8 +15,8 @@ from .autoinstall import ensure_chromium
|
|
|
15
15
|
from .constants import (
|
|
16
16
|
BROWSER_ARGS,
|
|
17
17
|
CSP_TEMPLATE,
|
|
18
|
-
MARKDOWN_EXTENSIONS,
|
|
19
18
|
PDF_PARAMS,
|
|
19
|
+
resolve_extras,
|
|
20
20
|
)
|
|
21
21
|
from .overrides import markdown2
|
|
22
22
|
from .resources import get_code_css_path, get_css_path, get_output_path
|
|
@@ -26,92 +26,16 @@ from .transform import (
|
|
|
26
26
|
render_extra_features,
|
|
27
27
|
render_mermaid_diagrams,
|
|
28
28
|
)
|
|
29
|
-
from .utils import drop_duplicates
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def _generate_pdf_with_playwright(
|
|
33
|
-
html_content,
|
|
34
|
-
output_path,
|
|
35
|
-
*,
|
|
36
|
-
css_content=None,
|
|
37
|
-
base_dir=None,
|
|
38
|
-
dump_html=False,
|
|
39
|
-
nonce=None,
|
|
40
|
-
):
|
|
41
|
-
"""
|
|
42
|
-
Generate a PDF from HTML content using Playwright.
|
|
43
|
-
|
|
44
|
-
Args:
|
|
45
|
-
html_content (str): HTML content to convert.
|
|
46
|
-
output_path (str): Path to save the PDF file.
|
|
47
|
-
css_content (str, optional): CSS content to inject.
|
|
48
|
-
base_dir (Path, optional): Base directory for resolving relative paths in HTML.
|
|
49
|
-
dump_html (bool, optional): Whether to dump the HTML content to a file.
|
|
50
|
-
nonce (str, optional): Nonce for Content Security Policy.
|
|
51
|
-
"""
|
|
52
|
-
if nonce is None:
|
|
53
|
-
raise ValueError("A nonce must be provided for CSP generation.")
|
|
54
|
-
|
|
55
|
-
# This prevents arbitrary JavaScript injection while allowing Mermaid to work
|
|
56
|
-
csp = CSP_TEMPLATE.format(nonce=nonce)
|
|
57
|
-
full_html = create_html_document(html_content, css_content, csp)
|
|
58
|
-
|
|
59
|
-
ensure_chromium()
|
|
60
|
-
with sync_playwright() as playwright:
|
|
61
|
-
browser = playwright.chromium.launch(headless=True, args=BROWSER_ARGS)
|
|
62
|
-
context = browser.new_context(
|
|
63
|
-
java_script_enabled=True,
|
|
64
|
-
permissions=[],
|
|
65
|
-
geolocation=None,
|
|
66
|
-
accept_downloads=False,
|
|
67
|
-
)
|
|
68
|
-
page = context.new_page()
|
|
69
|
-
|
|
70
|
-
temp_html = None
|
|
71
|
-
try:
|
|
72
|
-
if base_dir:
|
|
73
|
-
temp_html = base_dir / f".temp_{os.getpid()}.html"
|
|
74
|
-
temp_html.write_text(full_html, encoding="utf-8")
|
|
75
|
-
page.goto(temp_html.as_uri(), wait_until="networkidle", timeout=30000)
|
|
76
|
-
else:
|
|
77
|
-
page.set_content(full_html, wait_until="networkidle", timeout=30000)
|
|
78
|
-
|
|
79
|
-
pdf_params = {
|
|
80
|
-
**PDF_PARAMS,
|
|
81
|
-
"path": output_path,
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
pdf_bytes = page.pdf(**pdf_params)
|
|
85
|
-
return None if output_path else pdf_bytes
|
|
86
|
-
|
|
87
|
-
finally:
|
|
88
|
-
browser.close()
|
|
89
|
-
if temp_html and temp_html.exists() and not dump_html:
|
|
90
|
-
temp_html.unlink()
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def _get_css_content(css_sources):
|
|
94
|
-
"""
|
|
95
|
-
Get the CSS content from a list of CSS file paths.
|
|
96
|
-
|
|
97
|
-
Args:
|
|
98
|
-
css_sources (list): List of CSS file paths.
|
|
99
|
-
Returns:
|
|
100
|
-
str: Combined CSS content.
|
|
101
|
-
"""
|
|
102
|
-
css_buffer = ""
|
|
103
|
-
for css_file in css_sources:
|
|
104
|
-
css_buffer += Path(css_file).read_text(encoding="utf-8") + "\n"
|
|
105
|
-
return css_buffer
|
|
106
29
|
|
|
107
30
|
|
|
108
31
|
def convert(
|
|
109
|
-
markdown_path,
|
|
110
|
-
css_path=None,
|
|
32
|
+
markdown_path: Path,
|
|
33
|
+
css_path: Path = None,
|
|
111
34
|
output_path=None,
|
|
112
35
|
*,
|
|
113
36
|
extend_default_css=True,
|
|
114
37
|
dump_html=False,
|
|
38
|
+
extras=None,
|
|
115
39
|
):
|
|
116
40
|
"""
|
|
117
41
|
Convert a markdown file to a pdf file.
|
|
@@ -122,42 +46,40 @@ def convert(
|
|
|
122
46
|
output_path (str=None): Path to the output file.
|
|
123
47
|
extend_default_css (bool=True): Extend the default CSS file.
|
|
124
48
|
dump_html (bool=False): Dump the intermediate HTML to a file.
|
|
49
|
+
extras (list=None): List of extras to use.
|
|
125
50
|
"""
|
|
126
|
-
|
|
127
|
-
|
|
51
|
+
markdown_text = (
|
|
52
|
+
Path(markdown_path).read_text(encoding="utf-8")
|
|
53
|
+
if markdown_path and Path(markdown_path).exists()
|
|
54
|
+
else None
|
|
55
|
+
)
|
|
56
|
+
css_text = (
|
|
57
|
+
Path(css_path).read_text(encoding="utf-8")
|
|
58
|
+
if css_path and Path(css_path).exists()
|
|
59
|
+
else None
|
|
60
|
+
)
|
|
128
61
|
|
|
129
62
|
if output_path is None:
|
|
130
63
|
output_path = get_output_path(markdown_path, None)
|
|
131
64
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
html = markdown2.markdown_path(markdown_path, extras=MARKDOWN_EXTENSIONS)
|
|
142
|
-
html = create_sections(html)
|
|
143
|
-
html = render_mermaid_diagrams(html, nonce=nonce)
|
|
144
|
-
html = render_extra_features(html)
|
|
145
|
-
|
|
146
|
-
_generate_pdf_with_playwright(
|
|
147
|
-
html,
|
|
148
|
-
output_path,
|
|
149
|
-
css_content=_get_css_content(css_sources),
|
|
150
|
-
base_dir=Path(markdown_path).resolve().parent,
|
|
151
|
-
dump_html=dump_html,
|
|
152
|
-
nonce=nonce,
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
except Exception as exc:
|
|
156
|
-
raise RuntimeError(exc) from exc
|
|
65
|
+
convert_text(
|
|
66
|
+
markdown_text,
|
|
67
|
+
css_text=css_text,
|
|
68
|
+
output_path=output_path,
|
|
69
|
+
base_dir=Path(markdown_path).resolve().parent,
|
|
70
|
+
extend_default_css=extend_default_css,
|
|
71
|
+
dump_html=dump_html,
|
|
72
|
+
extras=extras,
|
|
73
|
+
)
|
|
157
74
|
|
|
158
75
|
|
|
159
76
|
def live_convert(
|
|
160
|
-
markdown_path,
|
|
77
|
+
markdown_path,
|
|
78
|
+
css_path=None,
|
|
79
|
+
output_path=None,
|
|
80
|
+
*,
|
|
81
|
+
extend_default_css=True,
|
|
82
|
+
extras=None,
|
|
161
83
|
):
|
|
162
84
|
"""
|
|
163
85
|
Convert a markdown file to a pdf file and watch for changes.
|
|
@@ -167,6 +89,7 @@ def live_convert(
|
|
|
167
89
|
css_path (str=None): Path to the CSS file.
|
|
168
90
|
output_path (str=None): Path to the output file.
|
|
169
91
|
extend_default_css (bool=True): Extend the default CSS file.
|
|
92
|
+
extras (list=None): List of extras to use.
|
|
170
93
|
"""
|
|
171
94
|
if css_path is None:
|
|
172
95
|
css_path = get_css_path()
|
|
@@ -180,27 +103,41 @@ def live_convert(
|
|
|
180
103
|
output_path,
|
|
181
104
|
extend_default_css=extend_default_css,
|
|
182
105
|
loud=True,
|
|
106
|
+
extras=extras,
|
|
183
107
|
)
|
|
184
108
|
live_converter.observe()
|
|
185
109
|
|
|
186
110
|
|
|
187
|
-
def convert_text(
|
|
111
|
+
def convert_text(
|
|
112
|
+
markdown_text,
|
|
113
|
+
css_text=None,
|
|
114
|
+
output_path=None,
|
|
115
|
+
*,
|
|
116
|
+
base_dir=None,
|
|
117
|
+
extend_default_css=True,
|
|
118
|
+
dump_html=False,
|
|
119
|
+
extras=None,
|
|
120
|
+
):
|
|
188
121
|
"""
|
|
189
122
|
Convert markdown text to a pdf file.
|
|
190
123
|
|
|
191
124
|
Args:
|
|
192
125
|
markdown_text (str): Markdown text.
|
|
193
126
|
css_text (str=None): CSS text.
|
|
127
|
+
output_path (str=None): Path to the output file.
|
|
128
|
+
base_dir (Path=None): Base directory for resolving relative paths.
|
|
194
129
|
extend_default_css (bool=True): Extend the default CSS file.
|
|
130
|
+
dump_html (bool=False): Dump the intermediate HTML to a file.
|
|
131
|
+
extras (list=None): List of extras to use.
|
|
195
132
|
|
|
196
133
|
Returns:
|
|
197
|
-
PDF file as bytes.
|
|
134
|
+
PDF file as bytes if output_path is None, else None.
|
|
198
135
|
"""
|
|
199
136
|
default_css = Path(get_css_path()).read_text(encoding="utf-8")
|
|
200
137
|
code_css = Path(get_code_css_path()).read_text(encoding="utf-8")
|
|
201
138
|
|
|
202
139
|
if css_text is None:
|
|
203
|
-
css_text =
|
|
140
|
+
css_text = ""
|
|
204
141
|
|
|
205
142
|
if extend_default_css:
|
|
206
143
|
css_sources = [code_css, default_css, css_text]
|
|
@@ -209,15 +146,19 @@ def convert_text(markdown_text, css_text=None, *, extend_default_css=True):
|
|
|
209
146
|
|
|
210
147
|
try:
|
|
211
148
|
nonce = secrets.token_urlsafe(16)
|
|
212
|
-
|
|
149
|
+
extras = resolve_extras(extras)
|
|
150
|
+
html = markdown2.markdown(markdown_text, extras=extras["markdown2_extras"])
|
|
213
151
|
html = create_sections(html)
|
|
214
|
-
|
|
215
|
-
|
|
152
|
+
if "mermaid" in extras["markdown2_extras"]:
|
|
153
|
+
html = render_mermaid_diagrams(html, nonce=nonce)
|
|
154
|
+
html = render_extra_features(html, extras=extras["markdown_convert_extras"])
|
|
216
155
|
|
|
217
156
|
return _generate_pdf_with_playwright(
|
|
218
157
|
html,
|
|
219
|
-
|
|
158
|
+
output_path,
|
|
220
159
|
css_content="\n".join(css_sources),
|
|
160
|
+
base_dir=base_dir,
|
|
161
|
+
dump_html=dump_html,
|
|
221
162
|
nonce=nonce,
|
|
222
163
|
)
|
|
223
164
|
|
|
@@ -238,6 +179,7 @@ class LiveConverter:
|
|
|
238
179
|
*,
|
|
239
180
|
extend_default_css=True,
|
|
240
181
|
loud=False,
|
|
182
|
+
extras=None,
|
|
241
183
|
):
|
|
242
184
|
"""
|
|
243
185
|
Initialize the LiveConverter class.
|
|
@@ -247,12 +189,14 @@ class LiveConverter:
|
|
|
247
189
|
css_path (str): Path to the CSS file.
|
|
248
190
|
output_path (str): Path to the output file.
|
|
249
191
|
extend_default_css (bool): Extend the default CSS file.
|
|
192
|
+
extras (list=None): List of extras to use.
|
|
250
193
|
"""
|
|
251
194
|
self.md_path = Path(markdown_path).absolute()
|
|
252
195
|
self.css_path = Path(css_path).absolute()
|
|
253
196
|
self.output_path = output_path
|
|
254
197
|
self.extend_default_css = extend_default_css
|
|
255
198
|
self.loud = loud
|
|
199
|
+
self.extras = extras
|
|
256
200
|
|
|
257
201
|
self.md_last_modified = None
|
|
258
202
|
self.css_last_modified = None
|
|
@@ -278,6 +222,7 @@ class LiveConverter:
|
|
|
278
222
|
self.css_path,
|
|
279
223
|
self.output_path,
|
|
280
224
|
extend_default_css=self.extend_default_css,
|
|
225
|
+
extras=self.extras,
|
|
281
226
|
)
|
|
282
227
|
if self.loud:
|
|
283
228
|
print(f"- PDF file updated: {datetime.now()}", flush=True)
|
|
@@ -311,3 +256,69 @@ class LiveConverter:
|
|
|
311
256
|
except KeyboardInterrupt:
|
|
312
257
|
if self.loud:
|
|
313
258
|
print("\nInterrupted by user.\n", flush=True)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _generate_pdf_with_playwright(
|
|
262
|
+
html_content,
|
|
263
|
+
output_path,
|
|
264
|
+
*,
|
|
265
|
+
css_content=None,
|
|
266
|
+
base_dir=None,
|
|
267
|
+
dump_html=False,
|
|
268
|
+
nonce=None,
|
|
269
|
+
):
|
|
270
|
+
"""
|
|
271
|
+
Generate a PDF from HTML content using Playwright.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
html_content (str): HTML content to convert.
|
|
275
|
+
output_path (str): Path to save the PDF file.
|
|
276
|
+
css_content (str, optional): CSS content to inject.
|
|
277
|
+
base_dir (Path, optional): Base directory for resolving relative paths in HTML.
|
|
278
|
+
dump_html (bool, optional): Whether to dump the HTML content to a file.
|
|
279
|
+
nonce (str, optional): Nonce for Content Security Policy.
|
|
280
|
+
"""
|
|
281
|
+
if nonce is None:
|
|
282
|
+
raise ValueError("A nonce must be provided for CSP generation.")
|
|
283
|
+
|
|
284
|
+
# This prevents arbitrary JavaScript injection while allowing Mermaid to work
|
|
285
|
+
csp = CSP_TEMPLATE.format(nonce=nonce)
|
|
286
|
+
full_html = create_html_document(html_content, css_content, csp)
|
|
287
|
+
|
|
288
|
+
ensure_chromium()
|
|
289
|
+
with sync_playwright() as playwright:
|
|
290
|
+
browser = playwright.chromium.launch(headless=True, args=BROWSER_ARGS)
|
|
291
|
+
context = browser.new_context(
|
|
292
|
+
java_script_enabled=True,
|
|
293
|
+
permissions=[],
|
|
294
|
+
geolocation=None,
|
|
295
|
+
accept_downloads=False,
|
|
296
|
+
)
|
|
297
|
+
page = context.new_page()
|
|
298
|
+
|
|
299
|
+
temp_html = None
|
|
300
|
+
try:
|
|
301
|
+
if base_dir:
|
|
302
|
+
temp_file = (
|
|
303
|
+
f"{Path(output_path).stem}.html"
|
|
304
|
+
if output_path
|
|
305
|
+
else f".temp_{os.getpid()}.html"
|
|
306
|
+
)
|
|
307
|
+
temp_html = base_dir / temp_file
|
|
308
|
+
temp_html.write_text(full_html, encoding="utf-8")
|
|
309
|
+
page.goto(temp_html.as_uri(), wait_until="networkidle", timeout=30000)
|
|
310
|
+
else:
|
|
311
|
+
page.set_content(full_html, wait_until="networkidle", timeout=30000)
|
|
312
|
+
|
|
313
|
+
pdf_params = {
|
|
314
|
+
**PDF_PARAMS,
|
|
315
|
+
"path": output_path,
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
pdf_bytes = page.pdf(**pdf_params)
|
|
319
|
+
return None if output_path else pdf_bytes
|
|
320
|
+
|
|
321
|
+
finally:
|
|
322
|
+
browser.close()
|
|
323
|
+
if temp_html and temp_html.exists() and not dump_html:
|
|
324
|
+
temp_html.unlink()
|
|
@@ -6,7 +6,7 @@ render_extra_features from transform.py
|
|
|
6
6
|
import json
|
|
7
7
|
import re
|
|
8
8
|
|
|
9
|
-
import vl_convert
|
|
9
|
+
import vl_convert
|
|
10
10
|
from bs4 import BeautifulSoup, Tag
|
|
11
11
|
from ruamel.yaml import YAML
|
|
12
12
|
|
|
@@ -23,7 +23,8 @@ class ExtraFeature:
|
|
|
23
23
|
pattern = r""
|
|
24
24
|
run_before_stash = False
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
@staticmethod
|
|
27
|
+
def replace(match, html):
|
|
27
28
|
"""
|
|
28
29
|
Replaces the matched pattern with the rendered extra feature.
|
|
29
30
|
|
|
@@ -47,6 +48,7 @@ class CheckboxExtra(ExtraFeature):
|
|
|
47
48
|
|
|
48
49
|
pattern = r"(?P<checkbox>\[\s\]|\[x\])"
|
|
49
50
|
|
|
51
|
+
@staticmethod
|
|
50
52
|
def replace(match, html):
|
|
51
53
|
"""
|
|
52
54
|
Render a tag for a checkbox.
|
|
@@ -67,6 +69,7 @@ class HighlightExtra(ExtraFeature):
|
|
|
67
69
|
|
|
68
70
|
pattern = r"==(?P<content>.*?)=="
|
|
69
71
|
|
|
72
|
+
@staticmethod
|
|
70
73
|
def replace(match, html):
|
|
71
74
|
"""
|
|
72
75
|
Render a tag for a highlight.
|
|
@@ -87,6 +90,7 @@ class CustomSpanExtra(ExtraFeature):
|
|
|
87
90
|
|
|
88
91
|
pattern = r"(?P<cls>[a-zA-Z0-9_-]+)\{\{\s*(?P<content>.*?)\s*\}\}"
|
|
89
92
|
|
|
93
|
+
@staticmethod
|
|
90
94
|
def replace(match, html):
|
|
91
95
|
"""
|
|
92
96
|
Render a tag for a custom span.
|
|
@@ -108,6 +112,7 @@ class TocExtra(ExtraFeature):
|
|
|
108
112
|
|
|
109
113
|
pattern = r"\[TOC(?:\s+depth=(?P<depth>\d+))?\]"
|
|
110
114
|
|
|
115
|
+
@staticmethod
|
|
111
116
|
def replace(match, html):
|
|
112
117
|
"""
|
|
113
118
|
Render a tag for a table of contents
|
|
@@ -176,16 +181,17 @@ class VegaExtra(ExtraFeature):
|
|
|
176
181
|
)
|
|
177
182
|
run_before_stash = True
|
|
178
183
|
|
|
184
|
+
@staticmethod
|
|
179
185
|
def replace(match, html):
|
|
180
186
|
"""
|
|
181
|
-
Render a tag for a vega
|
|
187
|
+
Render a tag for a vega-lite diagram from JSON or YAML.
|
|
182
188
|
|
|
183
189
|
Args:
|
|
184
|
-
match (re.Match): Element identified as a vega
|
|
190
|
+
match (re.Match): Element identified as a vega-lite diagram.
|
|
185
191
|
html (str): The full HTML content.
|
|
186
192
|
|
|
187
193
|
Returns:
|
|
188
|
-
str: SVG tag representing the vega
|
|
194
|
+
str: SVG tag representing the vega-lite diagram.
|
|
189
195
|
"""
|
|
190
196
|
content = match.group("content")
|
|
191
197
|
spec = None
|
|
@@ -204,7 +210,7 @@ class VegaExtra(ExtraFeature):
|
|
|
204
210
|
return match.group(0)
|
|
205
211
|
|
|
206
212
|
try:
|
|
207
|
-
tag =
|
|
213
|
+
tag = vl_convert.vegalite_to_svg(spec)
|
|
208
214
|
return f"<div class='vega-lite'>{tag}</div>"
|
|
209
215
|
except Exception as exc:
|
|
210
216
|
print(f"WARNING: Failed to convert Vega-Lite spec to SVG: {exc}")
|
|
@@ -225,12 +231,12 @@ def apply_extras(extras: set[ExtraFeature], html, before_stash=False):
|
|
|
225
231
|
continue
|
|
226
232
|
|
|
227
233
|
# Loop until the pattern no longer matches
|
|
234
|
+
new_html = html
|
|
228
235
|
while re.search(extra.pattern, html, flags=re.DOTALL):
|
|
229
|
-
new_html = html
|
|
230
236
|
try:
|
|
231
237
|
new_html = re.sub(
|
|
232
238
|
extra.pattern,
|
|
233
|
-
lambda match:
|
|
239
|
+
lambda match, ext=extra: ext.replace(match, html=html),
|
|
234
240
|
html,
|
|
235
241
|
flags=re.DOTALL,
|
|
236
242
|
)
|
|
@@ -238,7 +244,6 @@ def apply_extras(extras: set[ExtraFeature], html, before_stash=False):
|
|
|
238
244
|
print(
|
|
239
245
|
f"WARNING: An exception occurred while trying to apply an extra:\n{exc}"
|
|
240
246
|
)
|
|
241
|
-
pass
|
|
242
247
|
|
|
243
248
|
# Safety break:
|
|
244
249
|
if new_html == html:
|
|
@@ -13,7 +13,7 @@ except ImportError:
|
|
|
13
13
|
# Fallback for older Python versions
|
|
14
14
|
from importlib_resources import files
|
|
15
15
|
|
|
16
|
-
from .constants import BLUE, CYAN, GREEN, OPTIONS, OPTIONS_MODES, YELLOW
|
|
16
|
+
from .constants import BLUE, CYAN, EXTRAS, GREEN, OPTIONS, OPTIONS_MODES, YELLOW
|
|
17
17
|
from .utils import color
|
|
18
18
|
|
|
19
19
|
|
|
@@ -72,6 +72,31 @@ def get_usage():
|
|
|
72
72
|
Returns:
|
|
73
73
|
str: The usage message.
|
|
74
74
|
"""
|
|
75
|
+
|
|
76
|
+
def _wrap_by_length(items: list, max_len=20, indent=" "):
|
|
77
|
+
if not items:
|
|
78
|
+
return ""
|
|
79
|
+
|
|
80
|
+
lines = []
|
|
81
|
+
current_line = []
|
|
82
|
+
current_length = 0
|
|
83
|
+
|
|
84
|
+
for item in items:
|
|
85
|
+
item_len = len(item) + (1 if current_line else 0)
|
|
86
|
+
|
|
87
|
+
if current_length + item_len > max_len and current_line:
|
|
88
|
+
lines.append(",".join(current_line) + ",")
|
|
89
|
+
current_line = [item]
|
|
90
|
+
current_length = len(item)
|
|
91
|
+
else:
|
|
92
|
+
current_line.append(item)
|
|
93
|
+
current_length += item_len
|
|
94
|
+
|
|
95
|
+
if current_line:
|
|
96
|
+
lines.append(",".join(current_line))
|
|
97
|
+
|
|
98
|
+
return indent + f"\n{indent}".join(lines)
|
|
99
|
+
|
|
75
100
|
commd = (
|
|
76
101
|
f"{color(GREEN, 'markdown-convert')} "
|
|
77
102
|
f"[{color(YELLOW, OPTIONS[0])}] [{color(BLUE, 'options')}]"
|
|
@@ -84,6 +109,8 @@ def get_usage():
|
|
|
84
109
|
f"{color(BLUE, OPTIONS[2])}{color(CYAN, '=')}[{color(CYAN, 'css_file_path')}]"
|
|
85
110
|
)
|
|
86
111
|
option_three = f"{color(BLUE, OPTIONS[3])}{color(CYAN, '=')}[{color(CYAN, 'output_file_path')}]"
|
|
112
|
+
option_four = f"{color(BLUE, OPTIONS[4])}{color(CYAN, '=')}[{color(CYAN, 'extra1,extra2,...')}]"
|
|
113
|
+
extras_str = _wrap_by_length(list(EXTRAS.keys()), max_len=60, indent=" ")
|
|
87
114
|
|
|
88
115
|
usage = (
|
|
89
116
|
"\n"
|
|
@@ -93,9 +120,13 @@ def get_usage():
|
|
|
93
120
|
"Options:\n"
|
|
94
121
|
f" {option_one}\n"
|
|
95
122
|
" Convert the markdown file once (default) or live.\n"
|
|
123
|
+
" Use debug to preserve the intermediate html file.\n"
|
|
96
124
|
f" {option_two}\n"
|
|
97
125
|
" Use a custom CSS file.\n"
|
|
98
126
|
f" {option_three}\n"
|
|
99
127
|
" Specify the output file path.\n"
|
|
128
|
+
f" {option_four}\n"
|
|
129
|
+
" Specify the extras to use. Uses all extras if not specified.\n"
|
|
130
|
+
f" Supported extras:\n{extras_str}\n"
|
|
100
131
|
)
|
|
101
132
|
return usage
|
|
@@ -6,15 +6,7 @@ import re
|
|
|
6
6
|
|
|
7
7
|
from bs4 import BeautifulSoup
|
|
8
8
|
|
|
9
|
-
from .extras import
|
|
10
|
-
apply_extras,
|
|
11
|
-
ExtraFeature,
|
|
12
|
-
CheckboxExtra,
|
|
13
|
-
CustomSpanExtra,
|
|
14
|
-
HighlightExtra,
|
|
15
|
-
TocExtra,
|
|
16
|
-
VegaExtra,
|
|
17
|
-
)
|
|
9
|
+
from .extras import ExtraFeature, apply_extras
|
|
18
10
|
|
|
19
11
|
|
|
20
12
|
def create_html_document(html_content, css_content, csp):
|
|
@@ -82,6 +74,7 @@ def render_mermaid_diagrams(html, *, nonce):
|
|
|
82
74
|
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
|
83
75
|
mermaid.initialize({{
|
|
84
76
|
startOnLoad: true,
|
|
77
|
+
securityLevel: 'strict',
|
|
85
78
|
theme: 'default',
|
|
86
79
|
themeVariables: {{}},
|
|
87
80
|
fontFamily: 'arial, verdana, sans-serif'
|
|
@@ -95,16 +88,7 @@ def render_mermaid_diagrams(html, *, nonce):
|
|
|
95
88
|
return html
|
|
96
89
|
|
|
97
90
|
|
|
98
|
-
def render_extra_features(
|
|
99
|
-
html,
|
|
100
|
-
extras: set[ExtraFeature] = (
|
|
101
|
-
CheckboxExtra,
|
|
102
|
-
CustomSpanExtra,
|
|
103
|
-
HighlightExtra,
|
|
104
|
-
TocExtra,
|
|
105
|
-
VegaExtra,
|
|
106
|
-
),
|
|
107
|
-
):
|
|
91
|
+
def render_extra_features(html, extras: set[ExtraFeature]):
|
|
108
92
|
"""
|
|
109
93
|
Renders extra features by protecting specific tags, applying regex
|
|
110
94
|
transformations, and restoring the protected content.
|
|
@@ -23,16 +23,3 @@ def color(color_code, text):
|
|
|
23
23
|
return text
|
|
24
24
|
|
|
25
25
|
return f"\033[{color_code}m{text}\033[0m"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def drop_duplicates(lst):
|
|
29
|
-
"""
|
|
30
|
-
Drops duplicates from the given list.
|
|
31
|
-
|
|
32
|
-
Args:
|
|
33
|
-
lst: List to remove duplicates from.
|
|
34
|
-
|
|
35
|
-
Returns:
|
|
36
|
-
List without duplicates.
|
|
37
|
-
"""
|
|
38
|
-
return list(dict.fromkeys(lst))
|
|
@@ -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.56"
|
|
8
8
|
description = "Convert Markdown files to PDF from your command line."
|
|
9
9
|
authors = [
|
|
10
10
|
{ name = "Julio Cabria", email = "juliocabria@tutanota.com" },
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
This module contains the constants used in the markdown_convert package.
|
|
3
|
-
Author: @julynx
|
|
4
|
-
"""
|
|
5
|
-
|
|
6
|
-
RED = "31"
|
|
7
|
-
GREEN = "32"
|
|
8
|
-
YELLOW = "33"
|
|
9
|
-
BLUE = "34"
|
|
10
|
-
MAGENTA = "35"
|
|
11
|
-
CYAN = "36"
|
|
12
|
-
|
|
13
|
-
OPTIONS = ("markdown_file_path", "--mode", "--css", "--out", "-h", "--help")
|
|
14
|
-
|
|
15
|
-
OPTIONS_MODES = ("once", "live", "debug")
|
|
16
|
-
|
|
17
|
-
MARKDOWN_EXTENSIONS = {
|
|
18
|
-
"fenced-code-blocks": None,
|
|
19
|
-
"header-ids": True,
|
|
20
|
-
"breaks": {"on_newline": True},
|
|
21
|
-
"tables": True,
|
|
22
|
-
"latex": True,
|
|
23
|
-
"mermaid": None,
|
|
24
|
-
"strike": None,
|
|
25
|
-
"admonitions": None,
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
BROWSER_ARGS = [
|
|
29
|
-
"--disable-dev-shm-usage",
|
|
30
|
-
"--disable-extensions",
|
|
31
|
-
"--disable-plugins",
|
|
32
|
-
"--disable-gpu",
|
|
33
|
-
"--no-first-run",
|
|
34
|
-
"--no-default-browser-check",
|
|
35
|
-
]
|
|
36
|
-
|
|
37
|
-
CSP_TEMPLATE = (
|
|
38
|
-
"default-src 'none'; "
|
|
39
|
-
"script-src 'nonce-{nonce}' https://cdn.jsdelivr.net; " # <- Script for Mermaid diagrams
|
|
40
|
-
"script-src-elem 'nonce-{nonce}' https://cdn.jsdelivr.net; "
|
|
41
|
-
"style-src 'unsafe-inline'; "
|
|
42
|
-
"img-src data: https: file:; "
|
|
43
|
-
"font-src data: https:; "
|
|
44
|
-
"connect-src https://cdn.jsdelivr.net;"
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
PDF_PARAMS = {
|
|
48
|
-
"format": "A4",
|
|
49
|
-
"print_background": True,
|
|
50
|
-
"margin": {
|
|
51
|
-
"top": "20mm",
|
|
52
|
-
"bottom": "20mm",
|
|
53
|
-
"left": "20mm",
|
|
54
|
-
"right": "20mm",
|
|
55
|
-
},
|
|
56
|
-
"path": None, # <- Replace with actual output path when used
|
|
57
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|