mddoco 2.0.1__tar.gz → 2.0.3__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 (35) hide show
  1. {mddoco-2.0.1/src/mddoco.egg-info → mddoco-2.0.3}/PKG-INFO +14 -4
  2. {mddoco-2.0.1 → mddoco-2.0.3}/README.md +9 -3
  3. {mddoco-2.0.1 → mddoco-2.0.3}/pyproject.toml +15 -1
  4. mddoco-2.0.3/src/mddoco/__init__.py +1 -0
  5. {mddoco-2.0.1 → mddoco-2.0.3}/src/mddoco/cli.py +16 -7
  6. {mddoco-2.0.1 → mddoco-2.0.3}/src/mddoco/graph.py +58 -37
  7. mddoco-2.0.3/src/mddoco/pdf.py +78 -0
  8. {mddoco-2.0.1 → mddoco-2.0.3}/src/mddoco/preprocessor.py +9 -9
  9. {mddoco-2.0.1 → mddoco-2.0.3/src/mddoco.egg-info}/PKG-INFO +14 -4
  10. {mddoco-2.0.1 → mddoco-2.0.3}/src/mddoco.egg-info/requires.txt +5 -0
  11. {mddoco-2.0.1 → mddoco-2.0.3}/tests/test_preprocessor.py +24 -4
  12. mddoco-2.0.1/src/mddoco/__init__.py +0 -1
  13. mddoco-2.0.1/src/mddoco/pdf.py +0 -39
  14. {mddoco-2.0.1 → mddoco-2.0.3}/LICENSE +0 -0
  15. {mddoco-2.0.1 → mddoco-2.0.3}/setup.cfg +0 -0
  16. {mddoco-2.0.1 → mddoco-2.0.3}/src/mddoco/converter.py +0 -0
  17. {mddoco-2.0.1 → mddoco-2.0.3}/src/mddoco/mermaid.py +0 -0
  18. {mddoco-2.0.1 → mddoco-2.0.3}/src/mddoco/renderer.py +0 -0
  19. {mddoco-2.0.1 → mddoco-2.0.3}/src/mddoco/scanner.py +0 -0
  20. {mddoco-2.0.1 → mddoco-2.0.3}/src/mddoco/themes/academic-wide.html +0 -0
  21. {mddoco-2.0.1 → mddoco-2.0.3}/src/mddoco/themes/academic.html +0 -0
  22. {mddoco-2.0.1 → mddoco-2.0.3}/src/mddoco/themes/dark-wide.html +0 -0
  23. {mddoco-2.0.1 → mddoco-2.0.3}/src/mddoco/themes/dark.html +0 -0
  24. {mddoco-2.0.1 → mddoco-2.0.3}/src/mddoco/themes/default-wide.html +0 -0
  25. {mddoco-2.0.1 → mddoco-2.0.3}/src/mddoco/themes/default.html +0 -0
  26. {mddoco-2.0.1 → mddoco-2.0.3}/src/mddoco/themes/paged-professional.html +0 -0
  27. {mddoco-2.0.1 → mddoco-2.0.3}/src/mddoco/themes/professional-wide.html +0 -0
  28. {mddoco-2.0.1 → mddoco-2.0.3}/src/mddoco/themes/professional.html +0 -0
  29. {mddoco-2.0.1 → mddoco-2.0.3}/src/mddoco/themes/vanilla.html +0 -0
  30. {mddoco-2.0.1 → mddoco-2.0.3}/src/mddoco/toc.py +0 -0
  31. {mddoco-2.0.1 → mddoco-2.0.3}/src/mddoco/writer.py +0 -0
  32. {mddoco-2.0.1 → mddoco-2.0.3}/src/mddoco.egg-info/SOURCES.txt +0 -0
  33. {mddoco-2.0.1 → mddoco-2.0.3}/src/mddoco.egg-info/dependency_links.txt +0 -0
  34. {mddoco-2.0.1 → mddoco-2.0.3}/src/mddoco.egg-info/entry_points.txt +0 -0
  35. {mddoco-2.0.1 → mddoco-2.0.3}/src/mddoco.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mddoco
3
- Version: 2.0.1
3
+ Version: 2.0.3
4
4
  Summary: Markdown to HTML/PDF document converter
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -10,6 +10,10 @@ Requires-Dist: markdown>=3.5
10
10
  Requires-Dist: jinja2>=3.1
11
11
  Requires-Dist: playwright>=1.40
12
12
  Requires-Dist: matplotlib>=3.7
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest>=8.0; extra == "dev"
15
+ Requires-Dist: black>=24.0; extra == "dev"
16
+ Requires-Dist: ruff<0.17,>=0.16; extra == "dev"
13
17
  Dynamic: license-file
14
18
 
15
19
  # mddoco
@@ -20,17 +24,23 @@ A CLI tool that converts markdown files to a single HTML (or future PDF) documen
20
24
 
21
25
  ```bash
22
26
  pip install mddoco
23
- playwright install chromium
24
27
  ```
25
28
 
26
29
  Or for development:
27
30
 
28
31
  ```bash
29
32
  pip install -e .
30
- playwright install chromium
31
33
  ```
32
34
 
33
- > Playwright (Chromium) is required for PDF output only. HTML output works without it.
35
+ > **Playwright (Chromium) is required for PDF output only.** HTML output works without it.
36
+ > The `playwright` Python package is installed automatically, but the Chromium
37
+ > browser it drives is not. The first time you render a PDF, mddoco downloads
38
+ > Chromium for you (~150 MB, one time). To do it ahead of time, or if the
39
+ > automatic download fails, run it yourself:
40
+ >
41
+ > ```bash
42
+ > playwright install chromium
43
+ > ```
34
44
 
35
45
  ## Usage
36
46
 
@@ -6,17 +6,23 @@ A CLI tool that converts markdown files to a single HTML (or future PDF) documen
6
6
 
7
7
  ```bash
8
8
  pip install mddoco
9
- playwright install chromium
10
9
  ```
11
10
 
12
11
  Or for development:
13
12
 
14
13
  ```bash
15
14
  pip install -e .
16
- playwright install chromium
17
15
  ```
18
16
 
19
- > Playwright (Chromium) is required for PDF output only. HTML output works without it.
17
+ > **Playwright (Chromium) is required for PDF output only.** HTML output works without it.
18
+ > The `playwright` Python package is installed automatically, but the Chromium
19
+ > browser it drives is not. The first time you render a PDF, mddoco downloads
20
+ > Chromium for you (~150 MB, one time). To do it ahead of time, or if the
21
+ > automatic download fails, run it yourself:
22
+ >
23
+ > ```bash
24
+ > playwright install chromium
25
+ > ```
20
26
 
21
27
  ## Usage
22
28
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mddoco"
7
- version = "2.0.1"
7
+ version = "2.0.3"
8
8
  description = "Markdown to HTML/PDF document converter"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -16,6 +16,13 @@ dependencies = [
16
16
  "matplotlib>=3.7",
17
17
  ]
18
18
 
19
+ [project.optional-dependencies]
20
+ dev = [
21
+ "pytest>=8.0",
22
+ "black>=24.0",
23
+ "ruff>=0.16,<0.17",
24
+ ]
25
+
19
26
  [project.scripts]
20
27
  mddoco = "mddoco.cli:main"
21
28
 
@@ -25,9 +32,16 @@ where = ["src"]
25
32
  [tool.setuptools.package-data]
26
33
  mddoco = ["themes/*.html"]
27
34
 
35
+ [tool.black]
36
+ line-length = 88
37
+ target-version = ["py310"]
38
+
28
39
  [tool.ruff]
29
40
  src = ["src"]
30
41
  line-length = 88
31
42
 
32
43
  [tool.ruff.lint]
33
44
  select = ["E", "F", "I", "UP"]
45
+
46
+ [tool.pytest.ini_options]
47
+ testpaths = ["tests"]
@@ -0,0 +1 @@
1
+ __version__ = "2.0.3"
@@ -15,31 +15,40 @@ from mddoco.writer import write_output
15
15
  @click.command()
16
16
  @click.argument("input_path", type=click.Path(exists=True, path_type=Path))
17
17
  @click.option(
18
- "--output", "-o", "output_path",
19
- default=".", show_default=True,
18
+ "--output",
19
+ "-o",
20
+ "output_path",
21
+ default=".",
22
+ show_default=True,
20
23
  type=click.Path(path_type=Path),
21
24
  help="Output directory, or explicit output file path.",
22
25
  )
23
26
  @click.option(
24
- "--format", "-f", "fmt",
25
- default="html", show_default=True,
27
+ "--format",
28
+ "-f",
29
+ "fmt",
30
+ default="html",
31
+ show_default=True,
26
32
  type=click.Choice(["html", "pdf"], case_sensitive=False),
27
33
  help="Output format.",
28
34
  )
29
35
  @click.option("--title", "-t", default=None, help="Document title.")
30
36
  @click.option(
31
37
  "--theme",
32
- default="default", show_default=True,
38
+ default="default",
39
+ show_default=True,
33
40
  help="Theme name to use for rendering.",
34
41
  )
35
42
  @click.option(
36
43
  "--toc/--no-toc",
37
- default=False, show_default=True,
44
+ default=False,
45
+ show_default=True,
38
46
  help="Generate a table of contents.",
39
47
  )
40
48
  @click.option(
41
49
  "--toc-depth",
42
- default=3, show_default=True,
50
+ default=3,
51
+ show_default=True,
43
52
  type=click.IntRange(1, 6),
44
53
  help="Maximum heading depth included in the TOC.",
45
54
  )
@@ -6,84 +6,105 @@ import re
6
6
  from html import unescape
7
7
 
8
8
  import matplotlib
9
- matplotlib.use('Agg') # non-interactive backend — must be set before importing pyplot
9
+
10
+ matplotlib.use("Agg") # non-interactive backend — must be set before importing pyplot
10
11
  import matplotlib.pyplot as plt
11
12
 
12
13
  log = logging.getLogger(__name__)
13
14
 
14
- _JSON_TO_PY = re.compile(r'\b(true|false|null)\b')
15
- _JSON_TO_PY_MAP = {'true': 'True', 'false': 'False', 'null': 'None'}
15
+ _JSON_TO_PY = re.compile(r"\b(true|false|null)\b")
16
+ _JSON_TO_PY_MAP = {"true": "True", "false": "False", "null": "None"}
16
17
 
17
18
  _DEFAULT_COLOURS = [
18
- '#2d6cbe', '#e74c3c', '#27ae60', '#e67e22',
19
- '#8e44ad', '#16a085', '#2c3e50', '#d35400',
19
+ "#2d6cbe",
20
+ "#e74c3c",
21
+ "#27ae60",
22
+ "#e67e22",
23
+ "#8e44ad",
24
+ "#16a085",
25
+ "#2c3e50",
26
+ "#d35400",
20
27
  ]
21
28
 
22
29
 
23
30
  def graph_it(data, output=None):
24
31
  dpi = 100
25
- width_px = data.get('width_px', 640)
26
- height_px = data.get('height_px', 480)
32
+ width_px = data.get("width_px", 640)
33
+ height_px = data.get("height_px", 480)
27
34
  fig, ax = plt.subplots(figsize=(width_px / dpi, height_px / dpi), dpi=dpi)
28
35
 
29
- orientation = data.get('orientation', 'vertical').lower()
30
- if orientation not in ('horizontal', 'vertical'):
36
+ orientation = data.get("orientation", "vertical").lower()
37
+ if orientation not in ("horizontal", "vertical"):
31
38
  raise ValueError(
32
39
  f"Invalid orientation '{orientation}' — must be 'horizontal' or 'vertical'"
33
40
  )
34
- horizontal = orientation == 'horizontal'
41
+ horizontal = orientation == "horizontal"
35
42
 
36
- x = data['data']['x']
43
+ x = data["data"]["x"]
37
44
 
38
45
  # Auto-generate series from data keys (everything except 'x') if not provided
39
- series_list = data.get('series') or [
40
- {'label': k} for k in data['data'] if k != 'x'
41
- ]
46
+ series_list = data.get("series") or [{"label": k} for k in data["data"] if k != "x"]
42
47
 
43
48
  for i, series in enumerate(series_list):
44
- label = series['label']
45
- values = data['data'][label]
46
- series_type = series.get('type', 'line')
47
- colour = series.get('colour', _DEFAULT_COLOURS[i % len(_DEFAULT_COLOURS)])
49
+ label = series["label"]
50
+ values = data["data"][label]
51
+ series_type = series.get("type", "line")
52
+ colour = series.get("colour", _DEFAULT_COLOURS[i % len(_DEFAULT_COLOURS)])
48
53
 
49
- if series_type == 'bar':
54
+ if series_type == "bar":
50
55
  colour_val = colour if isinstance(colour, list) else [colour] * len(values)
51
56
  if horizontal:
52
57
  ax.barh(x, values, color=colour_val, label=label)
53
58
  else:
54
59
  ax.bar(x, values, color=colour_val, label=label)
55
- elif series_type in ('line', 'line2'):
56
- marker = 'o' if series.get('marker', False) else 'None'
57
- linestyle = ':' if series_type == 'line2' else '-'
60
+ elif series_type in ("line", "line2"):
61
+ marker = "o" if series.get("marker", False) else "None"
62
+ linestyle = ":" if series_type == "line2" else "-"
58
63
  if horizontal:
59
- ax.plot(values, x, color=colour, marker=marker, linewidth=2, linestyle=linestyle, label=label)
64
+ ax.plot(
65
+ values,
66
+ x,
67
+ color=colour,
68
+ marker=marker,
69
+ linewidth=2,
70
+ linestyle=linestyle,
71
+ label=label,
72
+ )
60
73
  else:
61
- ax.plot(x, values, color=colour, marker=marker, linewidth=2, linestyle=linestyle, label=label)
62
-
63
- if 'min' in data or 'max' in data:
64
- lo = data.get('min', None)
65
- hi = data.get('max', None)
74
+ ax.plot(
75
+ x,
76
+ values,
77
+ color=colour,
78
+ marker=marker,
79
+ linewidth=2,
80
+ linestyle=linestyle,
81
+ label=label,
82
+ )
83
+
84
+ if "min" in data or "max" in data:
85
+ lo = data.get("min", None)
86
+ hi = data.get("max", None)
66
87
  if horizontal:
67
88
  ax.set_xlim(lo, hi)
68
89
  else:
69
90
  ax.set_ylim(lo, hi)
70
91
 
71
- title = data.get('title', '')
92
+ title = data.get("title", "")
72
93
  if title:
73
94
  ax.set_title(title)
74
95
 
75
- if data.get('show_legend', True):
76
- ax.legend(loc='upper left')
96
+ if data.get("show_legend", True):
97
+ ax.legend(loc="upper left")
77
98
 
78
- ax.spines['top'].set_visible(False)
79
- ax.spines['right'].set_visible(False)
99
+ ax.spines["top"].set_visible(False)
100
+ ax.spines["right"].set_visible(False)
80
101
 
81
102
  if output:
82
- plt.savefig(output, format="svg", bbox_inches='tight')
103
+ plt.savefig(output, format="svg", bbox_inches="tight")
83
104
  plt.close(fig)
84
105
  else:
85
106
  buf = io.StringIO()
86
- plt.savefig(buf, format="svg", bbox_inches='tight')
107
+ plt.savefig(buf, format="svg", bbox_inches="tight")
87
108
  plt.close(fig)
88
109
  return buf.getvalue()
89
110
 
@@ -93,7 +114,7 @@ _GRAPH_BLOCK = re.compile(
93
114
  re.DOTALL,
94
115
  )
95
116
 
96
- _SVG_HEADER = re.compile(r'^.*?(?=<svg)', re.DOTALL)
117
+ _SVG_HEADER = re.compile(r"^.*?(?=<svg)", re.DOTALL)
97
118
 
98
119
 
99
120
  def _parse_source(source: str) -> dict:
@@ -113,7 +134,7 @@ def _render_graph(source: str) -> str:
113
134
  """Parse source, render via graph_it, and return an inline SVG element."""
114
135
  data = _parse_source(source)
115
136
  svg = graph_it(data)
116
- svg = _SVG_HEADER.sub('', svg) # strip XML declaration and DOCTYPE
137
+ svg = _SVG_HEADER.sub("", svg) # strip XML declaration and DOCTYPE
117
138
  return f'<div class="graph">{svg}</div>'
118
139
 
119
140
 
@@ -0,0 +1,78 @@
1
+ import logging
2
+ import subprocess
3
+ import sys
4
+ import tempfile
5
+ from pathlib import Path
6
+
7
+ log = logging.getLogger(__name__)
8
+
9
+ _INSTALL_HINT = (
10
+ "Chromium is required for PDF output and could not be installed "
11
+ "automatically. Run: playwright install chromium"
12
+ )
13
+
14
+
15
+ def html_to_pdf(html_content: str, dest: Path) -> None:
16
+ """Render an HTML string to a PDF file using a headless Chromium browser.
17
+
18
+ Navigates via a temporary file:// URL so that external resources (CDN
19
+ scripts, Mermaid.js, etc.) load correctly before the page is printed.
20
+
21
+ The Chromium binary that Playwright drives is not shipped with the Python
22
+ package. The first time a PDF is rendered on a fresh machine it is
23
+ downloaded automatically; if that download fails the user is told how to
24
+ install it by hand.
25
+ """
26
+ try:
27
+ from playwright.sync_api import Error as PlaywrightError
28
+ from playwright.sync_api import sync_playwright
29
+ except ImportError:
30
+ raise RuntimeError(
31
+ "Playwright is required for PDF output. "
32
+ "Run: pip install playwright && playwright install chromium"
33
+ )
34
+
35
+ with tempfile.NamedTemporaryFile(
36
+ suffix=".html", delete=False, mode="w", encoding="utf-8"
37
+ ) as f:
38
+ f.write(html_content)
39
+ tmp_path = Path(f.name)
40
+
41
+ try:
42
+ try:
43
+ _render(sync_playwright, tmp_path, dest)
44
+ except PlaywrightError as exc:
45
+ if "Executable doesn't exist" not in str(exc):
46
+ raise
47
+ _install_chromium()
48
+ _render(sync_playwright, tmp_path, dest)
49
+ finally:
50
+ tmp_path.unlink(missing_ok=True)
51
+
52
+
53
+ def _render(sync_playwright, source: Path, dest: Path) -> None:
54
+ """Drive headless Chromium to print ``source`` to ``dest`` as A4 PDF."""
55
+ with sync_playwright() as p:
56
+ browser = p.chromium.launch()
57
+ page = browser.new_page()
58
+ page.goto(source.as_uri(), wait_until="networkidle")
59
+ # If Paged.js is present, wait for it to finish paginating before
60
+ # capturing — networkidle fires before its JS layout pass completes.
61
+ page.wait_for_function(
62
+ "typeof window.PagedPolyfill === 'undefined'"
63
+ " || !!document.querySelector('.pagedjs_pages')"
64
+ )
65
+ page.pdf(path=str(dest), format="A4", print_background=True)
66
+ browser.close()
67
+
68
+
69
+ def _install_chromium() -> None:
70
+ """Download the Chromium binary Playwright needs, or explain how to."""
71
+ log.warning("Chromium not found — downloading it now (~150 MB, one time).")
72
+ try:
73
+ subprocess.run(
74
+ [sys.executable, "-m", "playwright", "install", "chromium"],
75
+ check=True,
76
+ )
77
+ except (subprocess.CalledProcessError, OSError) as exc:
78
+ raise RuntimeError(_INSTALL_HINT) from exc
@@ -45,29 +45,29 @@ def _parse_csv_line(line: str) -> list[tuple[str, bool]]:
45
45
  else:
46
46
  parts.append(line[i])
47
47
  i += 1
48
- value = ''.join(parts)
49
- while i < n and line[i] != ',':
48
+ value = "".join(parts)
49
+ while i < n and line[i] != ",":
50
50
  i += 1
51
51
  fields.append((value, True))
52
52
  else:
53
53
  start = i
54
- while i < n and line[i] != ',':
54
+ while i < n and line[i] != ",":
55
55
  i += 1
56
56
  fields.append((line[start:i], False))
57
57
 
58
- if i < n and line[i] == ',':
58
+ if i < n and line[i] == ",":
59
59
  i += 1
60
60
  if i >= n:
61
- fields.append(('', False))
61
+ fields.append(("", False))
62
62
 
63
63
  return fields
64
64
 
65
65
 
66
66
  def _apply_semicolon_split(value: str, was_quoted: bool) -> str | list[str]:
67
67
  """Return value split on ';' unless the field was originally quoted."""
68
- if was_quoted or ';' not in value:
68
+ if was_quoted or ";" not in value:
69
69
  return value
70
- return [part.strip() for part in value.split(';')]
70
+ return [part.strip() for part in value.split(";")]
71
71
 
72
72
 
73
73
  def load_csv_context(input_path: Path) -> dict:
@@ -99,7 +99,7 @@ def load_csv_context(input_path: Path) -> dict:
99
99
  if idx < len(fields):
100
100
  value, was_quoted = fields[idx]
101
101
  else:
102
- value, was_quoted = '', False
102
+ value, was_quoted = "", False
103
103
  row[key] = _apply_semicolon_split(value, was_quoted)
104
104
  rows.append(row)
105
105
 
@@ -114,7 +114,7 @@ def load_context(input_path: Path) -> dict:
114
114
  csv_ctx = load_csv_context(input_path)
115
115
  conflicts = set(json_ctx) & set(csv_ctx)
116
116
  if conflicts:
117
- names = ', '.join(sorted(conflicts))
117
+ names = ", ".join(sorted(conflicts))
118
118
  raise ValueError(
119
119
  f"Context name conflict — both a .json and .csv exist for: {names}"
120
120
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mddoco
3
- Version: 2.0.1
3
+ Version: 2.0.3
4
4
  Summary: Markdown to HTML/PDF document converter
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -10,6 +10,10 @@ Requires-Dist: markdown>=3.5
10
10
  Requires-Dist: jinja2>=3.1
11
11
  Requires-Dist: playwright>=1.40
12
12
  Requires-Dist: matplotlib>=3.7
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest>=8.0; extra == "dev"
15
+ Requires-Dist: black>=24.0; extra == "dev"
16
+ Requires-Dist: ruff<0.17,>=0.16; extra == "dev"
13
17
  Dynamic: license-file
14
18
 
15
19
  # mddoco
@@ -20,17 +24,23 @@ A CLI tool that converts markdown files to a single HTML (or future PDF) documen
20
24
 
21
25
  ```bash
22
26
  pip install mddoco
23
- playwright install chromium
24
27
  ```
25
28
 
26
29
  Or for development:
27
30
 
28
31
  ```bash
29
32
  pip install -e .
30
- playwright install chromium
31
33
  ```
32
34
 
33
- > Playwright (Chromium) is required for PDF output only. HTML output works without it.
35
+ > **Playwright (Chromium) is required for PDF output only.** HTML output works without it.
36
+ > The `playwright` Python package is installed automatically, but the Chromium
37
+ > browser it drives is not. The first time you render a PDF, mddoco downloads
38
+ > Chromium for you (~150 MB, one time). To do it ahead of time, or if the
39
+ > automatic download fails, run it yourself:
40
+ >
41
+ > ```bash
42
+ > playwright install chromium
43
+ > ```
34
44
 
35
45
  ## Usage
36
46
 
@@ -3,3 +3,8 @@ markdown>=3.5
3
3
  jinja2>=3.1
4
4
  playwright>=1.40
5
5
  matplotlib>=3.7
6
+
7
+ [dev]
8
+ pytest>=8.0
9
+ black>=24.0
10
+ ruff<0.17,>=0.16
@@ -1,6 +1,8 @@
1
- import pytest
2
1
  from pathlib import Path
3
2
 
3
+ import pytest
4
+
5
+ import mddoco
4
6
  from mddoco.preprocessor import (
5
7
  _apply_semicolon_split,
6
8
  _parse_csv_line,
@@ -8,9 +10,16 @@ from mddoco.preprocessor import (
8
10
  load_csv_context,
9
11
  )
10
12
 
13
+ # --- version ---
14
+
15
+
16
+ def test_version():
17
+ assert mddoco.__version__ == "2.0.3"
18
+
11
19
 
12
20
  # --- _parse_csv_line ---
13
21
 
22
+
14
23
  def test_parse_csv_line_simple():
15
24
  assert _parse_csv_line("a,b,c") == [("a", False), ("b", False), ("c", False)]
16
25
 
@@ -37,12 +46,17 @@ def test_parse_csv_line_single_field():
37
46
 
38
47
  # --- _apply_semicolon_split ---
39
48
 
49
+
40
50
  def test_split_unquoted_with_semicolon():
41
51
  assert _apply_semicolon_split("python;flask", False) == ["python", "flask"]
42
52
 
43
53
 
44
54
  def test_split_trims_whitespace():
45
- assert _apply_semicolon_split("python ; flask ; sql", False) == ["python", "flask", "sql"]
55
+ assert _apply_semicolon_split("python ; flask ; sql", False) == [
56
+ "python",
57
+ "flask",
58
+ "sql",
59
+ ]
46
60
 
47
61
 
48
62
  def test_no_split_when_quoted():
@@ -59,8 +73,11 @@ def test_single_element_after_split():
59
73
 
60
74
  # --- load_csv_context ---
61
75
 
76
+
62
77
  def test_load_csv_basic(tmp_path: Path):
63
- (tmp_path / "people.csv").write_text("name,role\nAlice,Engineer\nBob,Manager\n", encoding="utf-8")
78
+ (tmp_path / "people.csv").write_text(
79
+ "name,role\nAlice,Engineer\nBob,Manager\n", encoding="utf-8"
80
+ )
64
81
  ctx = load_csv_context(tmp_path)
65
82
  assert ctx == {
66
83
  "people": [
@@ -71,7 +88,9 @@ def test_load_csv_basic(tmp_path: Path):
71
88
 
72
89
 
73
90
  def test_load_csv_semicolon_becomes_list(tmp_path: Path):
74
- (tmp_path / "data.csv").write_text("name,skills\nAlice,python;flask\n", encoding="utf-8")
91
+ (tmp_path / "data.csv").write_text(
92
+ "name,skills\nAlice,python;flask\n", encoding="utf-8"
93
+ )
75
94
  ctx = load_csv_context(tmp_path)
76
95
  assert ctx["data"][0]["skills"] == ["python", "flask"]
77
96
 
@@ -111,6 +130,7 @@ def test_load_csv_from_file_path(tmp_path: Path):
111
130
 
112
131
  # --- load_context ---
113
132
 
133
+
114
134
  def test_load_context_merges_json_and_csv(tmp_path: Path):
115
135
  (tmp_path / "config.json").write_text('{"env": "prod"}', encoding="utf-8")
116
136
  (tmp_path / "people.csv").write_text("name\nAlice\n", encoding="utf-8")
@@ -1 +0,0 @@
1
- __version__ = "2.0.0"
@@ -1,39 +0,0 @@
1
- import tempfile
2
- from pathlib import Path
3
-
4
-
5
- def html_to_pdf(html_content: str, dest: Path) -> None:
6
- """Render an HTML string to a PDF file using a headless Chromium browser.
7
-
8
- Navigates via a temporary file:// URL so that external resources (CDN
9
- scripts, Mermaid.js, etc.) load correctly before the page is printed.
10
- """
11
- try:
12
- from playwright.sync_api import sync_playwright
13
- except ImportError:
14
- raise RuntimeError(
15
- "Playwright is required for PDF output. "
16
- "Run: pip install playwright && playwright install chromium"
17
- )
18
-
19
- with tempfile.NamedTemporaryFile(
20
- suffix=".html", delete=False, mode="w", encoding="utf-8"
21
- ) as f:
22
- f.write(html_content)
23
- tmp_path = Path(f.name)
24
-
25
- try:
26
- with sync_playwright() as p:
27
- browser = p.chromium.launch()
28
- page = browser.new_page()
29
- page.goto(tmp_path.as_uri(), wait_until="networkidle")
30
- # If Paged.js is present, wait for it to finish paginating before
31
- # capturing — networkidle fires before its JS layout pass completes.
32
- page.wait_for_function(
33
- "typeof window.PagedPolyfill === 'undefined'"
34
- " || !!document.querySelector('.pagedjs_pages')"
35
- )
36
- page.pdf(path=str(dest), format="A4", print_background=True)
37
- browser.close()
38
- finally:
39
- tmp_path.unlink(missing_ok=True)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes