text-compositor 0.4.0__tar.gz → 0.4.2__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 (94) hide show
  1. {text_compositor-0.4.0 → text_compositor-0.4.2}/PKG-INFO +5 -5
  2. {text_compositor-0.4.0 → text_compositor-0.4.2}/README.md +3 -3
  3. {text_compositor-0.4.0 → text_compositor-0.4.2}/pyproject.toml +2 -2
  4. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_asset_resolution.py +42 -0
  5. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_bundled_diagram_tools.py +6 -0
  6. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_bundled_typst_assets.py +6 -3
  7. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_cetz_render.py +27 -5
  8. text_compositor-0.4.2/tests/test_cli_single_markdown.py +129 -0
  9. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_emphasis_lint.py +1 -1
  10. text_compositor-0.4.2/tests/test_html_line_numbers.py +94 -0
  11. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_html_output.py +42 -5
  12. text_compositor-0.4.2/tests/test_math_render.py +164 -0
  13. text_compositor-0.4.2/tests/test_vega_render.py +366 -0
  14. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_worker.py +81 -6
  15. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/api.py +10 -3
  16. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/build.py +67 -0
  17. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/cetz_render.py +55 -7
  18. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/config.py +1 -1
  19. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/deps.py +46 -0
  20. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/document.py +2 -0
  21. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/env_check.py +1 -1
  22. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/host_renderers.py +13 -0
  23. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/html_output.py +127 -13
  24. text_compositor-0.4.2/text_compositor/math_render.py +78 -0
  25. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/mermaid.py +61 -24
  26. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/project.py +4 -1
  27. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/renderer.py +19 -6
  28. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/renderer_diagrams.py +181 -9
  29. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/renderer_inline.py +3 -1
  30. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/renderer_layout.py +7 -7
  31. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/renderer_tokens.py +16 -1
  32. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/svg_trim.py +5 -3
  33. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/templates/_common.typ +3 -0
  34. text_compositor-0.4.2/text_compositor/vega_render.py +140 -0
  35. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/worker.py +21 -6
  36. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor.egg-info/PKG-INFO +5 -5
  37. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor.egg-info/SOURCES.txt +6 -0
  38. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor.egg-info/requires.txt +1 -1
  39. {text_compositor-0.4.0 → text_compositor-0.4.2}/LICENSE +0 -0
  40. {text_compositor-0.4.0 → text_compositor-0.4.2}/setup.cfg +0 -0
  41. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_aggregate_table.py +0 -0
  42. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_api.py +0 -0
  43. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_check_pins.py +0 -0
  44. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_common_template.py +0 -0
  45. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_config.py +0 -0
  46. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_config_key_validation.py +0 -0
  47. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_csv_table.py +0 -0
  48. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_diagnostics.py +0 -0
  49. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_diagram_cache_key.py +0 -0
  50. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_diagram_trim.py +0 -0
  51. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_distribution.py +0 -0
  52. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_doc_badges.py +0 -0
  53. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_download.py +0 -0
  54. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_front_matter.py +0 -0
  55. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_graphviz_render.py +0 -0
  56. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_incremental.py +0 -0
  57. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_java_encoding.py +0 -0
  58. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_layout_blocks.py +0 -0
  59. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_line_mapping.py +0 -0
  60. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_markdown_basics.py +0 -0
  61. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_mermaid_resident.py +0 -0
  62. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_pandoc_compat.py +0 -0
  63. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_paper_template.py +0 -0
  64. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_pikchr_render.py +0 -0
  65. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_plantuml_batch.py +0 -0
  66. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_renderer_structure.py +0 -0
  67. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_revision_history.py +0 -0
  68. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_script_references.py +0 -0
  69. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_sections.py +0 -0
  70. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_soft_break_hints.py +0 -0
  71. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_structurizr_render.py +0 -0
  72. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_svg_trim.py +0 -0
  73. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_table_cell_style.py +0 -0
  74. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_variables.py +0 -0
  75. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_viewer_version.py +0 -0
  76. {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_watch.py +0 -0
  77. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/__init__.py +0 -0
  78. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/changes.py +0 -0
  79. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/chapters.py +0 -0
  80. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/compiler.py +0 -0
  81. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/diagnostics.py +0 -0
  82. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/emphasis_lint.py +0 -0
  83. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/graphviz_render.py +0 -0
  84. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/log.py +0 -0
  85. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/pikchr_render.py +0 -0
  86. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/renderer_tables.py +0 -0
  87. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/templates/paper.typ +0 -0
  88. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/templates/slide.typ +0 -0
  89. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/templates/template.typ +0 -0
  90. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/typst_literal.py +0 -0
  91. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/typst_runtime.py +0 -0
  92. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor.egg-info/dependency_links.txt +0 -0
  93. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor.egg-info/entry_points.txt +0 -0
  94. {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: text-compositor
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: A tool for composing multiple text files into a PDF document.
5
5
  License-Expression: MIT
6
6
  Project-URL: Repository, https://github.com/tokudiro/text-compositor
@@ -11,7 +11,7 @@ Requires-Dist: typst==0.15.0
11
11
  Requires-Dist: markdown-it-py==4.2.0
12
12
  Requires-Dist: mdit-py-plugins==0.6.1
13
13
  Requires-Dist: PyYAML==6.0.3
14
- Requires-Dist: platformdirs==4.11.10
14
+ Requires-Dist: platformdirs==4.11.14
15
15
  Provides-Extra: mermaid
16
16
  Requires-Dist: playwright==1.63.0; extra == "mermaid"
17
17
  Dynamic: license-file
@@ -87,13 +87,13 @@ This is only needed if your source documents use ` ```mermaid ` fences (or speci
87
87
 
88
88
  ```bash
89
89
  pipx install "text-compositor[mermaid]"
90
- # or, if already installed: pipx inject text-compositor playwright==1.62.0
90
+ # or, if already installed: pipx inject text-compositor playwright==1.63.0
91
91
  ```
92
92
 
93
93
  With Option B (clone and run directly):
94
94
 
95
95
  ```bash
96
- pip install playwright==1.62.0
96
+ pip install playwright==1.63.0
97
97
  ```
98
98
 
99
99
  - **A Google Chrome or Microsoft Edge installation already present on your system** (no new download by default; it's auto-detected and reused at build time)
@@ -175,7 +175,7 @@ with Session() as session: # reuses the Mermaid browser and the Typst compiler
175
175
  print(result.ok, result.pdf_path, [d.message for d in result.diagnostics])
176
176
  ```
177
177
 
178
- `session.render_html("doc.md")` (experimental, [#161](https://github.com/tokudiro/text-compositor/issues/161)) writes a self-contained HTML file instead. Diagrams (Mermaid, PlantUML, D2, Structurizr, Graphviz, Pikchr, CeTZ, Fletcher, `svg`) become images next to it. `typst-exec` and raw HTML are not supported yet and are shown as code with a warning. Graphviz, Pikchr, CeTZ and Fletcher are drawn with the Typst packages `diagraph`, `kip`, `cetz` and `fletcher`, the same as in the PDF (Graphviz's `shape=record` and a graph-level `label` cannot be drawn and produce a warning).
178
+ `session.render_html("doc.md")` (experimental, [#161](https://github.com/tokudiro/text-compositor/issues/161)) writes a self-contained HTML file instead. Diagrams (Mermaid, PlantUML, D2, Structurizr, Graphviz, Pikchr, CeTZ, Fletcher, timeliney, Vega-Lite, Vega, `svg`) become images next to it. `typst-exec` and raw HTML are not supported yet and are shown as code with a warning. Graphviz, Pikchr, CeTZ, Fletcher and timeliney are drawn with the Typst packages `diagraph`, `kip`, `cetz`, `fletcher` and `timeliney`, the same as in the PDF (Graphviz's `shape=record` and a graph-level `label` cannot be drawn and produce a warning).
179
179
 
180
180
  See [sample/text-compositor.config.yaml](sample/text-compositor.config.yaml) for how to write the config file, and the [usage guide](doc/usage/) for details on `document:`/`plugins:`, front matter, Marp directives, and more. The Markdown files listed in `chapters` are concatenated in order to produce the PDF.
181
181
 
@@ -69,13 +69,13 @@ This is only needed if your source documents use ` ```mermaid ` fences (or speci
69
69
 
70
70
  ```bash
71
71
  pipx install "text-compositor[mermaid]"
72
- # or, if already installed: pipx inject text-compositor playwright==1.62.0
72
+ # or, if already installed: pipx inject text-compositor playwright==1.63.0
73
73
  ```
74
74
 
75
75
  With Option B (clone and run directly):
76
76
 
77
77
  ```bash
78
- pip install playwright==1.62.0
78
+ pip install playwright==1.63.0
79
79
  ```
80
80
 
81
81
  - **A Google Chrome or Microsoft Edge installation already present on your system** (no new download by default; it's auto-detected and reused at build time)
@@ -157,7 +157,7 @@ with Session() as session: # reuses the Mermaid browser and the Typst compiler
157
157
  print(result.ok, result.pdf_path, [d.message for d in result.diagnostics])
158
158
  ```
159
159
 
160
- `session.render_html("doc.md")` (experimental, [#161](https://github.com/tokudiro/text-compositor/issues/161)) writes a self-contained HTML file instead. Diagrams (Mermaid, PlantUML, D2, Structurizr, Graphviz, Pikchr, CeTZ, Fletcher, `svg`) become images next to it. `typst-exec` and raw HTML are not supported yet and are shown as code with a warning. Graphviz, Pikchr, CeTZ and Fletcher are drawn with the Typst packages `diagraph`, `kip`, `cetz` and `fletcher`, the same as in the PDF (Graphviz's `shape=record` and a graph-level `label` cannot be drawn and produce a warning).
160
+ `session.render_html("doc.md")` (experimental, [#161](https://github.com/tokudiro/text-compositor/issues/161)) writes a self-contained HTML file instead. Diagrams (Mermaid, PlantUML, D2, Structurizr, Graphviz, Pikchr, CeTZ, Fletcher, timeliney, Vega-Lite, Vega, `svg`) become images next to it. `typst-exec` and raw HTML are not supported yet and are shown as code with a warning. Graphviz, Pikchr, CeTZ, Fletcher and timeliney are drawn with the Typst packages `diagraph`, `kip`, `cetz`, `fletcher` and `timeliney`, the same as in the PDF (Graphviz's `shape=record` and a graph-level `label` cannot be drawn and produce a warning).
161
161
 
162
162
  See [sample/text-compositor.config.yaml](sample/text-compositor.config.yaml) for how to write the config file, and the [usage guide](doc/usage/) for details on `document:`/`plugins:`, front matter, Marp directives, and more. The Markdown files listed in `chapters` are concatenated in order to produce the PDF.
163
163
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "text-compositor"
7
- version = "0.4.0"
7
+ version = "0.4.2"
8
8
  description = "A tool for composing multiple text files into a PDF document."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -14,7 +14,7 @@ dependencies = [
14
14
  "markdown-it-py==4.2.0",
15
15
  "mdit-py-plugins==0.6.1",
16
16
  "PyYAML==6.0.3",
17
- "platformdirs==4.11.10",
17
+ "platformdirs==4.11.14",
18
18
  ]
19
19
 
20
20
  # plugins.mermaid: true(既定)を使う場合のみ必要(11章)。
@@ -2,6 +2,7 @@
2
2
 
3
3
  いずれも仕様9章のFail-fast方針どおり、画像欠損時はフォールバックせず即エラー終了する。
4
4
  """
5
+ import base64
5
6
  import os
6
7
 
7
8
  import pytest
@@ -51,6 +52,47 @@ class TestResolveAsset:
51
52
  assert renderer._resolve_asset("a.png") == "/project/a.png"
52
53
 
53
54
 
55
+ class TestResolveDataUriAsset:
56
+ """`data:`のURI画像は、キャッシュへ書き出してから解決する。PDF(Typstの#image())は、data:のURIを
57
+ 直接受け取れないため(HTMLと挙動をそろえ、`Image not found`のエラーにしない。#238)。"""
58
+
59
+ PNG_BASE64 = base64.b64encode(b"\x89PNG\r\n\x1a\n" + b"\x00" * 10).decode()
60
+
61
+ def _renderer(self, tmp_path):
62
+ renderer = TypstRenderer(line_mapping="off", base_dir=str(tmp_path), typst_root=str(tmp_path))
63
+ renderer.current_dir = str(tmp_path)
64
+ renderer.current_file = str(tmp_path / "doc.md")
65
+ return renderer
66
+
67
+ def test_data_uri_is_written_to_the_cache_and_resolved(self, tmp_path):
68
+ renderer = self._renderer(tmp_path)
69
+ result = renderer._resolve_asset(f"data:image/png;base64,{self.PNG_BASE64}")
70
+ assert result.startswith("/.text-compositor/cache/datauri_") and result.endswith(".png")
71
+ cache_path = tmp_path / result.lstrip("/")
72
+ assert cache_path.is_file()
73
+ assert cache_path.read_bytes() == base64.b64decode(self.PNG_BASE64)
74
+
75
+ def test_the_same_data_uri_reuses_the_cached_file(self, tmp_path):
76
+ renderer = self._renderer(tmp_path)
77
+ src = f"data:image/png;base64,{self.PNG_BASE64}"
78
+ first = renderer._resolve_asset(src)
79
+ cache_path = tmp_path / first.lstrip("/")
80
+ mtime = cache_path.stat().st_mtime_ns
81
+ second = renderer._resolve_asset(src)
82
+ assert second == first
83
+ assert cache_path.stat().st_mtime_ns == mtime
84
+
85
+ def test_unsupported_mime_type_exits(self, tmp_path):
86
+ renderer = self._renderer(tmp_path)
87
+ with pytest.raises(SystemExit):
88
+ renderer._resolve_asset("data:application/pdf;base64,AAAA")
89
+
90
+ def test_non_base64_data_uri_exits(self, tmp_path):
91
+ renderer = self._renderer(tmp_path)
92
+ with pytest.raises(SystemExit):
93
+ renderer._resolve_asset("data:image/png,not-base64-encoded")
94
+
95
+
54
96
  class TestResolveProjectImagePath:
55
97
  """document.background/logo、chapters[].background/logo(project_dir基準)の解決。"""
56
98
 
@@ -143,3 +143,9 @@ def test_build_dist_pins_the_same_java_plantuml_d2_structurizr_and_mermaid_as_th
143
143
 
144
144
  assert deps_mod.MERMAID_JS_URL in script
145
145
  assert deps_mod.MERMAID_JS_SHA256 in script
146
+
147
+ # Vega・Vega-Lite(#351)
148
+ assert deps_mod.VEGA_JS_URL in script
149
+ assert deps_mod.VEGA_JS_SHA256 in script
150
+ assert deps_mod.VEGA_LITE_JS_URL in script
151
+ assert deps_mod.VEGA_LITE_JS_SHA256 in script
@@ -84,9 +84,12 @@ def test_build_dist_pins_the_same_fonts_and_packages_as_the_tool():
84
84
  common = (ROOT / "text_compositor" / "templates" / "_common.typ").read_text(encoding="utf-8")
85
85
  used = set(re.findall(r"@preview/([a-z0-9-]+):([\d.]+)", common))
86
86
  used.add(("kip", pikchr_render.KIP_VERSION)) # Pikchr(#213)は、テンプレートではなく、生成コードが読み込む
87
- # CeTZ・Fletcher(#236)も、生成コードが読み込む。Fletcher 0.5.8が内部で使うcetz 0.3.4・oxifmtと、cetz 0.5.2が使うoxifmt 1.0.0は、
88
- # ネットワークなしで動くように、推移的な依存として、同梱する(同梱の不足は、viewer/scripts/check-dist.jsの、オフラインでのコンパイルが検出する)
87
+ # CeTZ・Fletcher(#236)・timeliney(#294)も、生成コードが読み込む。Fletcher 0.5.8が内部で使うcetz 0.3.4・oxifmt、
88
+ # cetz 0.5.2が使うoxifmt 1.0.0、timeliney 0.4.0が使うcetz 0.4.1は、ネットワークなしで動くように、推移的な依存として、
89
+ # 同梱する(同梱の不足は、viewer/scripts/check-dist.jsの、オフラインでのコンパイルが検出する)
89
90
  used |= {("cetz", cetz_render.CETZ_VERSION), ("fletcher", cetz_render.FLETCHER_VERSION),
90
- ("cetz", "0.3.4"), ("oxifmt", "0.2.1"), ("oxifmt", "1.0.0")}
91
+ ("timeliney", cetz_render.TIMELINEY_VERSION),
92
+ ("cetz", "0.3.4"), ("cetz", cetz_render.TIMELINEY_CETZ_VERSION),
93
+ ("oxifmt", "0.2.1"), ("oxifmt", "1.0.0")}
91
94
  bundled = set(re.findall(r"name: '([a-z0-9-]+)', version: '([\d.]+)'", script))
92
95
  assert used and used == bundled, (used, bundled)
@@ -1,4 +1,4 @@
1
- """CeTZ・Fletcher(Typstのパッケージ。cetz_render.py・図のフェンスの変換、#236)のテスト。"""
1
+ """CeTZ・Fletcher・timeliney(Typstのパッケージ。cetz_render.py・図のフェンスの変換、#236・#294)のテスト。"""
2
2
  import os
3
3
  import re
4
4
 
@@ -14,6 +14,7 @@ PLAIN = {"mermaid": False, "plantuml": False, "d2": False, "graphviz": False}
14
14
 
15
15
  CETZ = 'circle((0, 0), radius: 1)\nline((0, 0), (2, 1))\ncontent((1, 0.5), [日本語])'
16
16
  FLETCHER = 'node((0, 0), [開始]), edge("->"), node((1, 0), [終了])'
17
+ TIMELINEY = 'headerline(group(([*2024*], 2)))\ntaskgroup(title: [日本語], {\n task("A", (0, 1))\n})'
17
18
 
18
19
 
19
20
  def _read(relative):
@@ -27,6 +28,8 @@ def test_the_versions_are_the_same_in_the_source_and_the_bundle():
27
28
  bundled = set(re.findall(r"name: '([a-z0-9-]+)', version: '([\d.]+)'", script))
28
29
  assert ("cetz", cetz_render.CETZ_VERSION) in bundled
29
30
  assert ("fletcher", cetz_render.FLETCHER_VERSION) in bundled
31
+ assert ("timeliney", cetz_render.TIMELINEY_VERSION) in bundled
32
+ assert ("cetz", cetz_render.TIMELINEY_CETZ_VERSION) in bundled
30
33
 
31
34
 
32
35
  class TestCheckCode:
@@ -74,6 +77,9 @@ class TestRender:
74
77
  def test_fletcher_becomes_an_svg(self):
75
78
  assert render_svg("fletcher", FLETCHER).startswith("<svg")
76
79
 
80
+ def test_timeliney_becomes_an_svg(self):
81
+ assert render_svg("timeliney", TIMELINEY).startswith("<svg")
82
+
77
83
  def test_the_compiler_is_reused_and_a_later_figure_is_not_a_leftover(self):
78
84
  first = render_svg("cetz", "circle((0, 0))")
79
85
  assert render_svg("cetz", "circle((0, 0))\nline((0, 0), (3, 3))") != first
@@ -97,6 +103,7 @@ class TestRender:
97
103
  ("cetz", 'pdf.embed("x")'),
98
104
  ("fletcher", 'node((0, 0), read("x"))'),
99
105
  ("fletcher", 'node((0, 0), std.plugin("x"))'),
106
+ ("timeliney", 'task(str(read("x")), (0, 1))'),
100
107
  ])
101
108
  def test_functions_that_read_files_are_denied_with_a_clear_message(self, kind, code):
102
109
  with pytest.raises(FigureRenderError) as e:
@@ -120,7 +127,7 @@ def _html(tmp_path, text, name="doc.md", plugins=PLAIN):
120
127
 
121
128
 
122
129
  class TestHtmlOutput:
123
- @pytest.mark.parametrize("lang, code", [("cetz", CETZ), ("fletcher", FLETCHER)])
130
+ @pytest.mark.parametrize("lang, code", [("cetz", CETZ), ("fletcher", FLETCHER), ("timeliney", TIMELINEY)])
124
131
  def test_a_fence_becomes_a_cached_svg_img(self, tmp_path, lang, code):
125
132
  result, html = _html(tmp_path, f'a\n\n```{lang} {{width=300pt}}\n{code}\n```\n')
126
133
  assert result.ok and not result.warnings
@@ -147,7 +154,7 @@ class TestHtmlOutput:
147
154
  assert not result.ok and "is not available" in result.errors[0].detail
148
155
  assert "TOP-SECRET" not in html
149
156
 
150
- @pytest.mark.parametrize("lang", ["cetz", "fletcher"])
157
+ @pytest.mark.parametrize("lang", ["cetz", "fletcher", "timeliney"])
151
158
  def test_the_plugin_setting_falls_back_to_a_code_block_without_a_warning(self, tmp_path, lang):
152
159
  result, html = _html(tmp_path, f'```{lang}\nnode((0, 0), [x])\n```\n', plugins={**PLAIN, lang: False})
153
160
  assert result.ok and f'class="language-{lang}"' in html and not result.warnings
@@ -174,6 +181,12 @@ class TestPdfCode:
174
181
  assert f'import "@preview/fletcher:{cetz_render.FLETCHER_VERSION}"' in code
175
182
  assert 'eval("diagram(\\n" + "node((0, 0)' in code
176
183
 
184
+ def test_timeliney_wraps_the_code_in_timeline(self):
185
+ code = self._render(f'```timeliney\n{TIMELINEY}\n```\n')
186
+ assert f'import "@preview/timeliney:{cetz_render.TIMELINEY_VERSION}"' in code
187
+ assert 'timeliney.timeline(show-grid: true, { eval(' in code
188
+ assert 'eval("headerline(group' in code
189
+
177
190
  def test_the_file_reading_functions_are_denied_in_the_generated_code(self):
178
191
  code = self._render('```cetz\ncircle((0, 0))\n```\n')
179
192
  assert '"read", "json"' in code and "std: denied" in code
@@ -184,6 +197,14 @@ class TestPdfCode:
184
197
  code = self._render('```fletcher {width=50%}\nnode((0, 0), [A])\n```\n')
185
198
  assert "50 * 1% * size.width" in code
186
199
 
200
+ def test_a_size_attribute_on_timeliney_sets_the_container_width_instead_of_scaling(self):
201
+ """timelineyは、measure()+scale()ではなく、box/blockのwidthへそのまま渡す(cetz_render.pdf_sourceのdocstring参照)。"""
202
+ code = self._render(f'```timeliney {{width=10cm}}\n{TIMELINEY}\n```\n')
203
+ assert "block(width: 10cm, height: auto)" in code
204
+ assert "measure(fig)" not in code and "scale(" not in code
205
+ code = self._render(f'```timeliney\n{TIMELINEY}\n```\n') # 指定なしは、行の幅いっぱい
206
+ assert "block(width: 100%, height: auto)" in code
207
+
187
208
  def test_it_is_generated_without_touching_the_templates_public_names(self):
188
209
  """カスタムテンプレートが、新しい公開名を、持たなくても、動く(生成コードが、パッケージを直接importする)。"""
189
210
  code = self._render('```cetz\ncircle((0, 0))\n```\n')
@@ -204,9 +225,10 @@ class TestPdfCode:
204
225
 
205
226
  def test_a_pdf_with_both_figures_is_built(tmp_path):
206
227
  md = tmp_path / "doc.md"
207
- md.write_text(f'# 図\n\n```cetz\n{CETZ}\n```\n\n```fletcher\n{FLETCHER}\n```\n\n'
228
+ md.write_text(f'# 図\n\n```cetz\n{CETZ}\n```\n\n```fletcher\n{FLETCHER}\n```\n\n```timeliney\n{TIMELINEY}\n```\n\n'
208
229
  '```cetz {width=5cm}\ncircle((0, 0))\n```\n\n```fletcher {width=50% height=3cm}\n'
209
- 'node((0, 0), [A]), edge("->"), node((1, 0), [B])\n```\n', encoding="utf-8")
230
+ 'node((0, 0), [A]), edge("->"), node((1, 0), [B])\n```\n\n```timeliney {width=10cm}\n'
231
+ f'{TIMELINEY}\n```\n', encoding="utf-8")
210
232
  result = build_markdown(str(md), str(tmp_path / "doc.pdf"))
211
233
  assert result.ok, [(e.message, e.detail) for e in result.errors]
212
234
  assert (tmp_path / "doc.pdf").read_bytes().startswith(b"%PDF")
@@ -0,0 +1,129 @@
1
+ """CLI: 単一のMarkdownファイルを直接指定してPDFにする(`text-compositor FILE.md`、#179)のテスト。"""
2
+ import os
3
+ import sys
4
+
5
+ import pytest
6
+
7
+ import text_compositor.build as build_mod
8
+ from text_compositor import log
9
+ import text_compositor.project as project_mod
10
+ from text_compositor.build import _single_markdown_output, parse_args
11
+
12
+
13
+ @pytest.fixture(autouse=True)
14
+ def _restore_verbosity():
15
+ """`build()`は、-q/-vを、プロセスグローバルへ反映する。他のテストへ、残さない。"""
16
+ yield
17
+ log.set_verbosity(False, False)
18
+
19
+
20
+ def parse(monkeypatch, *argv):
21
+ monkeypatch.setattr(sys, "argv", ["text-compositor", *argv])
22
+ return parse_args()
23
+
24
+
25
+ class TestParseArgs:
26
+ def test_a_single_file_with_output_and_template(self, monkeypatch):
27
+ args = parse(monkeypatch, "doc.md", "-o", "out.pdf", "-t", "slide", "-q", "--keep-temp")
28
+ assert (args.markdown, args.output, args.template, args.quiet, args.keep_temp) == ("doc.md", "out.pdf", "slide", True, True)
29
+
30
+ def test_existing_usage_without_a_file_is_unchanged(self, monkeypatch):
31
+ args = parse(monkeypatch, "--config", "c.yaml", "--if-changed")
32
+ assert args.markdown is None and args.config == "c.yaml" and args.if_changed
33
+
34
+ @pytest.mark.parametrize("flags", [
35
+ ["--config", "c.yaml"], ["--config-list", "l.txt"], ["--check-env"], ["--watch"], ["--if-changed"],
36
+ ["--clean"], ["--clean-cache"],
37
+ ])
38
+ def test_options_that_need_a_config_cannot_be_combined_with_a_file(self, monkeypatch, capsys, flags):
39
+ with pytest.raises(SystemExit) as e:
40
+ parse(monkeypatch, "doc.md", *flags)
41
+ assert e.value.code == 2
42
+ assert "同時に指定できません" in capsys.readouterr().err
43
+
44
+ @pytest.mark.parametrize("flags", [["-o", "out.pdf"], ["-t", "slide"]])
45
+ def test_output_and_template_need_a_file(self, monkeypatch, capsys, flags):
46
+ with pytest.raises(SystemExit) as e:
47
+ parse(monkeypatch, *flags)
48
+ assert e.value.code == 2 and "FILE.md" in capsys.readouterr().err
49
+
50
+ def test_only_markdown_files_are_accepted(self, monkeypatch, capsys):
51
+ with pytest.raises(SystemExit) as e:
52
+ parse(monkeypatch, "notes.txt")
53
+ assert e.value.code == 2
54
+ assert parse(monkeypatch, "A.MD").markdown == "A.MD" and parse(monkeypatch, "a.markdown").markdown == "a.markdown"
55
+
56
+
57
+ class TestOutputPath:
58
+ def test_the_default_is_next_to_the_manuscript(self, tmp_path):
59
+ md = str(tmp_path / "sub" / "note.md")
60
+ assert _single_markdown_output(md, None) == str(tmp_path / "sub" / "note.pdf")
61
+
62
+ def test_an_explicit_file_and_a_folder(self, tmp_path, monkeypatch):
63
+ monkeypatch.chdir(tmp_path)
64
+ md = str(tmp_path / "note.md")
65
+ assert _single_markdown_output(md, "out.pdf") == str(tmp_path / "out.pdf")
66
+ (tmp_path / "existing").mkdir()
67
+ assert _single_markdown_output(md, "existing") == str(tmp_path / "existing" / "note.pdf")
68
+ assert _single_markdown_output(md, "new" + os.sep) == str(tmp_path / "new" / "note.pdf")
69
+
70
+
71
+ class TestBuildSingleMarkdown:
72
+ @pytest.fixture
73
+ def calls(self, monkeypatch):
74
+ recorded = []
75
+ monkeypatch.setattr(project_mod, "_build_project", lambda *a, **k: recorded.append((a, k)))
76
+ return recorded
77
+
78
+ def run(self, tmp_path, *argv, monkeypatch):
79
+ monkeypatch.setattr(build_mod, "check_typst_version", lambda *a: None)
80
+ monkeypatch.setattr(build_mod, "ensure_fonts", lambda: "fonts")
81
+ monkeypatch.setattr(sys, "argv", ["text-compositor", *argv])
82
+ build_mod.build()
83
+
84
+ def test_it_builds_the_api_config_and_writes_next_to_the_manuscript(self, tmp_path, calls, monkeypatch):
85
+ md = tmp_path / "note.md"
86
+ md.write_text("# T\n", encoding="utf-8")
87
+ self.run(tmp_path, str(md), monkeypatch=monkeypatch)
88
+ (args, kwargs), = calls
89
+ project_dir, config, chapters = args[3], args[4], args[5]
90
+ assert project_dir == str(tmp_path) and chapters == ["note.md"]
91
+ assert config["document"]["cover"] == "markdown" and config["document"]["title"] == "note" # APIと同じ既定
92
+ assert config["template"]["path"] == "template"
93
+ assert kwargs["out_pdf"] == str(tmp_path / "note.pdf")
94
+
95
+ def test_the_template_name_and_a_typ_path_relative_to_the_cwd(self, tmp_path, calls, monkeypatch):
96
+ md = tmp_path / "note.md"
97
+ md.write_text("# T\n", encoding="utf-8")
98
+ monkeypatch.chdir(tmp_path)
99
+ self.run(tmp_path, "note.md", "-t", "paper", monkeypatch=monkeypatch)
100
+ self.run(tmp_path, "note.md", "-t", "my/own.typ", "-o", "build/o.pdf", monkeypatch=monkeypatch)
101
+ assert calls[0][0][4]["template"]["path"] == "paper"
102
+ assert calls[1][0][4]["template"]["path"] == str(tmp_path / "my" / "own.typ")
103
+ assert calls[1][1]["out_pdf"] == str(tmp_path / "build" / "o.pdf") and (tmp_path / "build").is_dir()
104
+
105
+ def test_a_missing_file_and_an_unknown_template_fail_with_exit_code_1(self, tmp_path, calls, monkeypatch, capsys):
106
+ with pytest.raises(SystemExit) as e:
107
+ self.run(tmp_path, str(tmp_path / "nope.md"), monkeypatch=monkeypatch)
108
+ assert e.value.code == 1 and "Markdown file not found" in capsys.readouterr().out
109
+ md = tmp_path / "note.md"
110
+ md.write_text("# T\n", encoding="utf-8")
111
+ with pytest.raises(SystemExit) as e:
112
+ self.run(tmp_path, str(md), "-t", "nosuch", monkeypatch=monkeypatch)
113
+ assert e.value.code == 1 and "Unknown template" in capsys.readouterr().out
114
+ assert calls == []
115
+
116
+
117
+ def test_a_real_pdf_is_built_without_a_config(tmp_path, monkeypatch):
118
+ """実際に、configなしで、PDFができる(Typstとフォントが、ある環境)。先頭の見出しは、表紙に落とされず、本文に残る。"""
119
+ from text_compositor.deps import ensure_fonts
120
+ try:
121
+ ensure_fonts()
122
+ except SystemExit:
123
+ pytest.skip("fonts are not available")
124
+ md = tmp_path / "note.md"
125
+ md.write_text("# 見出し\n\n本文です。\n", encoding="utf-8")
126
+ monkeypatch.setattr(sys, "argv", ["text-compositor", str(md), "-q"])
127
+ build_mod.build()
128
+ pdf = tmp_path / "note.pdf"
129
+ assert pdf.is_file() and pdf.read_bytes().startswith(b"%PDF")
@@ -93,7 +93,7 @@ class TestHtmlOutput:
93
93
  def test_the_text_is_still_shown(self, tmp_path):
94
94
  result, _ = _html_warnings(tmp_path, "これは**「重要」**です。\n")
95
95
  with open(result.html_path, encoding="utf-8") as f:
96
- assert "<p>これは**「重要」**です。</p>" in f.read()
96
+ assert '<p data-line="1">これは**「重要」**です。</p>' in f.read()
97
97
 
98
98
  def test_a_table_cell_and_a_list_item_are_checked(self, tmp_path):
99
99
  text = "| a | b |\n| - | - |\n| **「x」**です | y |\n\n- 項目**(仮)**です\n"
@@ -0,0 +1,94 @@
1
+ """HTML出力の、ブロック要素の`data-line`(原稿の行番号。Viewerが、ツールバーに出す。#328・#357)のテスト。"""
2
+ import re
3
+
4
+ import pytest
5
+
6
+ from text_compositor.api import render_html
7
+ from text_compositor.html_output import DOCUMENT_CSS, _with_data_line
8
+
9
+ PLAIN = {"mermaid": False, "plantuml": False, "d2": False}
10
+
11
+ DOC = """# 見出し
12
+
13
+ 段落その1です。
14
+ 続きの行。
15
+
16
+ - 項目A
17
+ - 項目B
18
+ - 入れ子B1
19
+
20
+ 1. 番号1
21
+
22
+ ゆるい段落
23
+
24
+ > 引用の段落
25
+
26
+ | a | b |
27
+ | - | - |
28
+ | 1 | 2 |
29
+
30
+ ```python
31
+ print(1)
32
+ ```
33
+
34
+ $$
35
+ a=b
36
+ $$
37
+
38
+ 最後の段落。
39
+ """
40
+
41
+
42
+ def lines_by_tag(tmp_path, markdown):
43
+ md = tmp_path / "doc.md"
44
+ md.write_text(markdown, encoding="utf-8")
45
+ result = render_html(str(md), plugins=PLAIN)
46
+ assert result.ok, result.diagnostics
47
+ html = open(result.html_path, encoding="utf-8").read()
48
+ body = html[html.index("<body"):]
49
+ return [(m.group(1), int(m.group(2))) for m in re.finditer(r'<(\w+)[^>]*\sdata-line="(\d+)"', body)], body
50
+
51
+
52
+ def test_block_elements_carry_their_manuscript_line(tmp_path):
53
+ found, _ = lines_by_tag(tmp_path, DOC)
54
+ assert found == [
55
+ ("h1", 1), ("p", 3),
56
+ ("li", 6), ("li", 7), ("li", 8), # 緊密なリスト: 項目が持つ(中の段落は、隠れている)
57
+ ("p", 10), ("p", 12), # ゆるいリスト: 項目ではなく、中の段落が持つ
58
+ ("p", 14), # 引用: 中の段落が持つ
59
+ ("table", 16), ("pre", 20), ("div", 24), # 表・フェンス・数式
60
+ ("p", 28),
61
+ ]
62
+
63
+
64
+ def test_a_diagram_fence_carries_the_line_on_its_wrapper(tmp_path):
65
+ svg = tmp_path / "x.svg"
66
+ svg.write_text('<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"></svg>', encoding="utf-8")
67
+ found, body = lines_by_tag(tmp_path, "# T\n\n```svg\n" + svg.read_text(encoding="utf-8") + "\n```\n")
68
+ assert ("div", 3) in found and '<div data-line="3" class="diagram diagram-svg">' in body
69
+
70
+
71
+ def test_a_line_is_not_added_twice_or_to_the_wrong_element(tmp_path):
72
+ _, body = lines_by_tag(tmp_path, DOC)
73
+ assert len(re.findall(r'data-line="10"', body)) == 1 and len(re.findall(r'data-line="6"', body)) == 1
74
+ assert "<hr" not in body or 'hr data-line' not in body
75
+
76
+
77
+ class TestWithDataLine:
78
+ def test_it_adds_the_line_to_the_first_tag_only(self):
79
+ assert _with_data_line('<div class="a"><p>x</p></div>\n', 7) == '<div data-line="7" class="a"><p>x</p></div>\n'
80
+
81
+ @pytest.mark.parametrize("html,line", [('<pre>x</pre>', None), ('<pre>x</pre>', 0), ('text', 3), ('', 3)])
82
+ def test_it_leaves_html_without_a_line_or_a_leading_tag_alone(self, html, line):
83
+ assert _with_data_line(html, line) == html
84
+
85
+
86
+ class TestNoBadgeCss:
87
+ """行番号は、ツールバーに出す(#357)。文書のCSSには、バッジ(疑似要素)を、持たせない。"""
88
+
89
+ def test_the_document_css_has_no_badge(self):
90
+ assert "data-line-badge" not in DOCUMENT_CSS and "attr(data-line)" not in DOCUMENT_CSS
91
+
92
+ def test_the_document_still_has_no_script(self, tmp_path):
93
+ _, body = lines_by_tag(tmp_path, DOC)
94
+ assert "<script" not in body
@@ -76,10 +76,12 @@ class TestApi:
76
76
  assert result.html_path == str(tmp_path / ".text-compositor" / "preview.html")
77
77
  assert html.startswith("<!DOCTYPE html>")
78
78
  assert "<title>見出し</title>" in html
79
- assert "<h1>見出し</h1>" in html
79
+ assert '<h1 data-line="1">見出し</h1>' in html
80
80
  assert "<script" not in html
81
81
  # スクリプトは、ブラウザ側でも禁止する(Viewerは、JavaScriptを有効にしたビューで開く)
82
82
  assert "script-src 'none'" in html and "object-src 'none'" in html
83
+ # 外部の画像は、二重の防御として、ブラウザ側でも禁止する(1章の方針・#238)
84
+ assert "img-src 'self' data:" in html
83
85
  assert set(result.timings_ms) >= {"render", "total"}
84
86
 
85
87
  def test_the_title_falls_back_to_the_file_name(self, tmp_path):
@@ -140,7 +142,7 @@ class TestMarkdown:
140
142
  assert "<strong>b</strong>" in b and "<em>i</em>" in b and "<s>s</s>" in b and "<code>c</code>" in b
141
143
  assert '<a href="https://example.com">l</a>' in b
142
144
  assert "<ul>" in b and "<ol>" in b
143
- assert '<pre><code class="language-python">print(1)\n</code></pre>' in b
145
+ assert re.search(r'<pre data-line="\d+"><code class="language-python">print\(1\)\n</code></pre>', b)
144
146
 
145
147
  def test_line_breaks_are_kept_like_the_pdf(self, tmp_path):
146
148
  _, html = convert(tmp_path, "one\ntwo\n")
@@ -257,9 +259,42 @@ class TestImages:
257
259
  warning = result.warnings[0]
258
260
  assert "Image not found" in warning.message and warning.line == 3
259
261
 
260
- def test_external_urls_are_kept(self, tmp_path):
262
+ def test_external_urls_are_blocked_by_default(self, tmp_path):
263
+ """方針2章「ローカルに閉じる」: 既定では、外部の画像は読み込まず、プレースホルダと警告にする(#238)。"""
261
264
  result, html = convert(tmp_path, "![x](https://example.com/a.png)\n")
265
+ assert result.ok
266
+ assert "<img" not in html
267
+ assert 'class="blocked-image"' in html and "https://example.com/a.png" in html
268
+ assert "外部の画像は、既定で読み込みません" in html
269
+ warning = result.warnings[0]
270
+ assert "External image blocked" in warning.message and "https://example.com/a.png" in warning.message
271
+ assert warning.line == 1
272
+
273
+ def test_external_urls_are_loaded_when_explicitly_allowed(self, tmp_path):
274
+ result, html = convert(tmp_path, "![x](https://example.com/a.png)\n", allow_external_images=True)
262
275
  assert 'src="https://example.com/a.png"' in html and not result.warnings
276
+ assert 'class="blocked-image"' not in html
277
+
278
+ def test_data_uri_images_are_always_kept(self, tmp_path):
279
+ """`data:`のURIは、ローカルで完結するため、allow_external_imagesによらず、常に読み込む(#238)。"""
280
+ src = "data:image/png;base64,eA=="
281
+ result, html = convert(tmp_path, f"![x]({src})\n")
282
+ assert result.ok and not result.warnings
283
+ assert f'src="{src}"' in html
284
+ assert 'class="blocked-image"' not in html
285
+
286
+ def test_windows_absolute_paths_are_blocked_by_default(self, tmp_path):
287
+ """Windows絶対パス(`C:\\...`)は、通信は起きないが、原稿の外側の任意のローカルファイルへアクセス
288
+ できてしまうため、外部の画像と同じ扱いにする(既定で読み込まず、設定で許可。`file:`のURIと同じ理由、#238。
289
+ `file:`のURI自体は、markdown-it-py側が既に危険なスキームとして拒否し、リンクにならない)。"""
290
+ result, html = convert(tmp_path, "![x](C:/Windows/win.ini)\n")
291
+ assert result.ok
292
+ assert "<img" not in html
293
+ assert 'class="blocked-image"' in html
294
+ assert result.warnings and "External image blocked" in result.warnings[0].message
295
+
296
+ result, html = convert(tmp_path, "![x](C:/Windows/win.ini)\n", allow_external_images=True)
297
+ assert 'src="C:/Windows/win.ini"' in html and not result.warnings
263
298
 
264
299
  def test_invalid_size_units_are_ignored_with_a_warning(self, tmp_path):
265
300
  write(tmp_path / "p.png", "x")
@@ -282,6 +317,8 @@ class TestDependencies:
282
317
  def test_the_markdown_itself_and_external_urls_are_not_included(self, tmp_path):
283
318
  result, _ = convert(tmp_path, "# a\n\n![x](https://example.com/a.png)\n")
284
319
  assert result.dependencies == []
320
+ result, _ = convert(tmp_path, "# a\n\n![x](https://example.com/a.png)\n", allow_external_images=True)
321
+ assert result.dependencies == []
285
322
 
286
323
  def test_a_missing_image_is_included_so_that_creating_it_triggers_an_update(self, tmp_path):
287
324
  result, _ = convert(tmp_path, "![x](missing.png)\n")
@@ -430,12 +467,12 @@ class TestFences:
430
467
 
431
468
  def test_a_disabled_plugin_falls_back_to_code_without_a_warning(self, tmp_path):
432
469
  result, html = convert(tmp_path, "```mermaid\ngraph TD\n A-->B\n```\n\n```plantuml\n@startuml\n@enduml\n```\n\n```d2\na -> b\n```\n")
433
- assert body(html).count("<pre>") == 3 and "<img" not in html
470
+ assert len(re.findall(r"<pre[ >]", body(html))) == 3 and "<img" not in html
434
471
  assert not result.warnings
435
472
 
436
473
  def test_other_languages_are_plain_code_blocks(self, tmp_path):
437
474
  _, html = convert(tmp_path, "```\n<b>&\n```\n")
438
- assert "<pre><code>&lt;b&gt;&amp;\n</code></pre>" in html
475
+ assert '<pre data-line="1"><code>&lt;b&gt;&amp;\n</code></pre>' in html
439
476
 
440
477
  def test_a_diagram_source_file_is_a_page_with_that_diagram(self, tmp_path):
441
478
  result, html = convert(tmp_path, "graph TD\n A-->B\n", name="flow.mmd")