markdown_convert 1.2.54__tar.gz → 1.2.55__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.
Files changed (20) hide show
  1. {markdown_convert-1.2.54 → markdown_convert-1.2.55}/PKG-INFO +20 -4
  2. {markdown_convert-1.2.54 → markdown_convert-1.2.55}/README.md +19 -3
  3. {markdown_convert-1.2.54 → markdown_convert-1.2.55}/markdown_convert/__main__.py +20 -3
  4. {markdown_convert-1.2.54 → markdown_convert-1.2.55}/markdown_convert/modules/autoinstall.py +1 -1
  5. markdown_convert-1.2.55/markdown_convert/modules/constants.py +152 -0
  6. {markdown_convert-1.2.54 → markdown_convert-1.2.55}/markdown_convert/modules/convert.py +126 -115
  7. {markdown_convert-1.2.54 → markdown_convert-1.2.55}/markdown_convert/modules/extras.py +14 -9
  8. {markdown_convert-1.2.54 → markdown_convert-1.2.55}/markdown_convert/modules/resources.py +32 -1
  9. {markdown_convert-1.2.54 → markdown_convert-1.2.55}/markdown_convert/modules/transform.py +3 -19
  10. {markdown_convert-1.2.54 → markdown_convert-1.2.55}/markdown_convert/modules/utils.py +0 -13
  11. {markdown_convert-1.2.54 → markdown_convert-1.2.55}/pyproject.toml +1 -1
  12. markdown_convert-1.2.54/markdown_convert/modules/constants.py +0 -57
  13. {markdown_convert-1.2.54 → markdown_convert-1.2.55}/.gitignore +0 -0
  14. {markdown_convert-1.2.54 → markdown_convert-1.2.55}/LICENSE +0 -0
  15. {markdown_convert-1.2.54 → markdown_convert-1.2.55}/markdown_convert/__init__.py +0 -0
  16. {markdown_convert-1.2.54 → markdown_convert-1.2.55}/markdown_convert/code.css +0 -0
  17. {markdown_convert-1.2.54 → markdown_convert-1.2.55}/markdown_convert/default.css +0 -0
  18. {markdown_convert-1.2.54 → markdown_convert-1.2.55}/markdown_convert/modules/__init__.py +0 -0
  19. {markdown_convert-1.2.54 → markdown_convert-1.2.55}/markdown_convert/modules/overrides.py +0 -0
  20. {markdown_convert-1.2.54 → markdown_convert-1.2.55}/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.54
3
+ Version: 1.2.55
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
- - `--mode=once|live`: Convert the markdown file once (default) or live.
84
- - `--css=[css_file_path]`: Use a custom CSS file.
85
- - `--out=[output_file_path]`: Specify the output file path.
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
- - `--mode=once|live`: Convert the markdown file once (default) or live.
61
- - `--css=[css_file_path]`: Use a custom CSS file.
62
- - `--out=[output_file_path]`: Specify the output file path.
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(markdown_path, css_path, output_path, dump_html=mode == "debug")
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
 
@@ -18,7 +18,7 @@ def ensure_chromium(loud=True):
18
18
  """
19
19
  with sync_playwright() as playwright:
20
20
  if is_browser_installed(playwright.chromium):
21
- return
21
+ return True
22
22
 
23
23
  if loud:
24
24
  print(
@@ -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
- if css_path is None:
127
- css_path = get_css_path()
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
- if extend_default_css:
133
- css_sources = [get_code_css_path(), get_css_path(), css_path]
134
- else:
135
- css_sources = [get_code_css_path(), css_path]
136
-
137
- css_sources = drop_duplicates(css_sources)
138
-
139
- try:
140
- nonce = secrets.token_urlsafe(16)
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, css_path=None, output_path=None, *, extend_default_css=True
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(markdown_text, css_text=None, *, extend_default_css=True):
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 = default_css
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
- html = markdown2.markdown(markdown_text, extras=MARKDOWN_EXTENSIONS)
149
+ extras = resolve_extras(extras)
150
+ html = markdown2.markdown(markdown_text, extras=extras["markdown2_extras"])
213
151
  html = create_sections(html)
214
- html = render_mermaid_diagrams(html, nonce=nonce)
215
- html = render_extra_features(html)
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
- None,
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 as vlc
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
- def replace(self, match, html):
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 lite diagram from JSON or YAML.
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 lite diagram.
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 lite diagram.
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 = vlc.vegalite_to_svg(spec)
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: extra.replace(match, html=html),
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.54"
7
+ version = "1.2.55"
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
- }