text-compositor 0.3.6__tar.gz → 0.3.8__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 (78) hide show
  1. {text_compositor-0.3.6/text_compositor.egg-info → text_compositor-0.3.8}/PKG-INFO +4 -4
  2. {text_compositor-0.3.6 → text_compositor-0.3.8}/README.md +3 -3
  3. {text_compositor-0.3.6 → text_compositor-0.3.8}/pyproject.toml +1 -1
  4. text_compositor-0.3.8/tests/test_bundled_typst_assets.py +92 -0
  5. text_compositor-0.3.8/tests/test_cetz_render.py +219 -0
  6. {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_check_pins.py +1 -2
  7. {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_diagram_cache_key.py +5 -5
  8. {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_distribution.py +20 -5
  9. {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_download.py +1 -27
  10. text_compositor-0.3.8/tests/test_emphasis_lint.py +111 -0
  11. text_compositor-0.3.8/tests/test_graphviz_render.py +88 -0
  12. {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_html_output.py +113 -36
  13. text_compositor-0.3.8/tests/test_pikchr_render.py +111 -0
  14. text_compositor-0.3.8/tests/test_renderer_structure.py +53 -0
  15. {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_worker.py +24 -108
  16. {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/api.py +16 -7
  17. text_compositor-0.3.8/text_compositor/cetz_render.py +207 -0
  18. {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/compiler.py +4 -21
  19. {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/deps.py +15 -37
  20. text_compositor-0.3.8/text_compositor/emphasis_lint.py +51 -0
  21. text_compositor-0.3.8/text_compositor/graphviz_render.py +192 -0
  22. {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/host_renderers.py +2 -13
  23. {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/html_output.py +40 -19
  24. text_compositor-0.3.8/text_compositor/pikchr_render.py +69 -0
  25. {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/project.py +5 -1
  26. text_compositor-0.3.8/text_compositor/renderer.py +408 -0
  27. text_compositor-0.3.8/text_compositor/renderer_diagrams.py +457 -0
  28. text_compositor-0.3.8/text_compositor/renderer_inline.py +217 -0
  29. text_compositor-0.3.8/text_compositor/renderer_layout.py +212 -0
  30. text_compositor-0.3.8/text_compositor/renderer_tables.py +138 -0
  31. text_compositor-0.3.8/text_compositor/renderer_tokens.py +286 -0
  32. text_compositor-0.3.8/text_compositor/typst_runtime.py +33 -0
  33. {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/worker.py +9 -10
  34. {text_compositor-0.3.6 → text_compositor-0.3.8/text_compositor.egg-info}/PKG-INFO +4 -4
  35. {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor.egg-info/SOURCES.txt +16 -0
  36. text_compositor-0.3.6/text_compositor/renderer.py +0 -1515
  37. {text_compositor-0.3.6 → text_compositor-0.3.8}/LICENSE +0 -0
  38. {text_compositor-0.3.6 → text_compositor-0.3.8}/setup.cfg +0 -0
  39. {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_api.py +0 -0
  40. {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_asset_resolution.py +0 -0
  41. {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_common_template.py +0 -0
  42. {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_config.py +0 -0
  43. {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_csv_table.py +0 -0
  44. {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_diagnostics.py +0 -0
  45. {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_doc_badges.py +0 -0
  46. {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_front_matter.py +0 -0
  47. {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_incremental.py +0 -0
  48. {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_layout_blocks.py +0 -0
  49. {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_line_mapping.py +0 -0
  50. {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_markdown_basics.py +0 -0
  51. {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_mermaid_resident.py +0 -0
  52. {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_paper_template.py +0 -0
  53. {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_revision_history.py +0 -0
  54. {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_script_references.py +0 -0
  55. {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_sections.py +0 -0
  56. {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_table_cell_style.py +0 -0
  57. {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_variables.py +0 -0
  58. {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_viewer_version.py +0 -0
  59. {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_watch.py +0 -0
  60. {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/__init__.py +0 -0
  61. {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/build.py +0 -0
  62. {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/changes.py +0 -0
  63. {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/chapters.py +0 -0
  64. {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/config.py +0 -0
  65. {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/diagnostics.py +0 -0
  66. {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/document.py +0 -0
  67. {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/env_check.py +0 -0
  68. {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/log.py +0 -0
  69. {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/mermaid.py +0 -0
  70. {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/templates/_common.typ +0 -0
  71. {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/templates/paper.typ +0 -0
  72. {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/templates/slide.typ +0 -0
  73. {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/templates/template.typ +0 -0
  74. {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/typst_literal.py +0 -0
  75. {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor.egg-info/dependency_links.txt +0 -0
  76. {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor.egg-info/entry_points.txt +0 -0
  77. {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor.egg-info/requires.txt +0 -0
  78. {text_compositor-0.3.6 → text_compositor-0.3.8}/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.3.6
3
+ Version: 0.3.8
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
@@ -36,7 +36,7 @@ Files listed in `chapters` are handled differently depending on their extension:
36
36
 
37
37
  - `.md`/`.markdown`: converted as Markdown
38
38
  - `.yaml`/`.yml`/`.json`: rendered as monospaced text with syntax highlighting
39
- - `.dot`/`.gv`, `.mmd`, `.puml`/`.plantuml`/`.pu`, `.d2`: each rendered as a one-chapter diagram (Graphviz, Mermaid, PlantUML, D2 respectively)
39
+ - `.dot`/`.gv`, `.mmd`, `.puml`/`.plantuml`/`.pu`, `.d2`, `.pikchr`: each rendered as a one-chapter diagram (Graphviz, Mermaid, PlantUML, D2, Pikchr respectively)
40
40
  - `.csv`: rendered as a structured Typst table
41
41
  - everything else (plain text, code files, etc.): rendered as plain monospaced text
42
42
 
@@ -77,7 +77,7 @@ No Typst compiler binary is bundled; it's obtained from the `typst` package (PyP
77
77
 
78
78
  ### Keeping up to date
79
79
 
80
- Security and bug fixes ship as new releases. There is no automatic update, so use the latest release: `pipx upgrade text-compositor` (or `pip install -U text-compositor`). For the [Obunzu Viewer](viewer/README.md), download the latest ZIP from the [Releases page](https://github.com/tokudiro/text-compositor/releases/latest). The Viewer bundles its own Python, so a new ZIP also updates that Python and the other bundled parts.
80
+ Security and bug fixes ship as new releases. There is no automatic update, so use the latest release: `pipx upgrade text-compositor` (or `pip install -U text-compositor`). For the [Obunzu Viewer](viewer/README.md), download the latest ZIP from the [Releases page](https://github.com/tokudiro/text-compositor/releases/latest). The Viewer bundles its own Python, so a new ZIP also updates that Python and the other bundled parts. A guide to using Obunzu (in Japanese) is in [doc/obunzu-guide/](doc/obunzu-guide/); its PDF is attached to each release.
81
81
 
82
82
  ### Using Mermaid diagrams (optional)
83
83
 
@@ -163,7 +163,7 @@ with Session() as session: # reuses the Mermaid browser and the Typst compiler
163
163
  print(result.ok, result.pdf_path, [d.message for d in result.diagnostics])
164
164
  ```
165
165
 
166
- `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, `svg`) become images next to it. `typst-exec` and raw HTML are not supported yet and are shown as code with a warning. Graphviz is drawn only in the Obunzu Viewer; elsewhere it is shown as code with a warning.
166
+ `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, 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).
167
167
 
168
168
  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.
169
169
 
@@ -18,7 +18,7 @@ Files listed in `chapters` are handled differently depending on their extension:
18
18
 
19
19
  - `.md`/`.markdown`: converted as Markdown
20
20
  - `.yaml`/`.yml`/`.json`: rendered as monospaced text with syntax highlighting
21
- - `.dot`/`.gv`, `.mmd`, `.puml`/`.plantuml`/`.pu`, `.d2`: each rendered as a one-chapter diagram (Graphviz, Mermaid, PlantUML, D2 respectively)
21
+ - `.dot`/`.gv`, `.mmd`, `.puml`/`.plantuml`/`.pu`, `.d2`, `.pikchr`: each rendered as a one-chapter diagram (Graphviz, Mermaid, PlantUML, D2, Pikchr respectively)
22
22
  - `.csv`: rendered as a structured Typst table
23
23
  - everything else (plain text, code files, etc.): rendered as plain monospaced text
24
24
 
@@ -59,7 +59,7 @@ No Typst compiler binary is bundled; it's obtained from the `typst` package (PyP
59
59
 
60
60
  ### Keeping up to date
61
61
 
62
- Security and bug fixes ship as new releases. There is no automatic update, so use the latest release: `pipx upgrade text-compositor` (or `pip install -U text-compositor`). For the [Obunzu Viewer](viewer/README.md), download the latest ZIP from the [Releases page](https://github.com/tokudiro/text-compositor/releases/latest). The Viewer bundles its own Python, so a new ZIP also updates that Python and the other bundled parts.
62
+ Security and bug fixes ship as new releases. There is no automatic update, so use the latest release: `pipx upgrade text-compositor` (or `pip install -U text-compositor`). For the [Obunzu Viewer](viewer/README.md), download the latest ZIP from the [Releases page](https://github.com/tokudiro/text-compositor/releases/latest). The Viewer bundles its own Python, so a new ZIP also updates that Python and the other bundled parts. A guide to using Obunzu (in Japanese) is in [doc/obunzu-guide/](doc/obunzu-guide/); its PDF is attached to each release.
63
63
 
64
64
  ### Using Mermaid diagrams (optional)
65
65
 
@@ -145,7 +145,7 @@ with Session() as session: # reuses the Mermaid browser and the Typst compiler
145
145
  print(result.ok, result.pdf_path, [d.message for d in result.diagnostics])
146
146
  ```
147
147
 
148
- `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, `svg`) become images next to it. `typst-exec` and raw HTML are not supported yet and are shown as code with a warning. Graphviz is drawn only in the Obunzu Viewer; elsewhere it is shown as code with a warning.
148
+ `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, 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).
149
149
 
150
150
  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.
151
151
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "text-compositor"
7
- version = "0.3.6"
7
+ version = "0.3.8"
8
8
  description = "A tool for composing multiple text files into a PDF document."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -0,0 +1,92 @@
1
+ """ViewerのZIPに同梱した、フォントとTypstのパッケージを使う仕組み(#263)のテスト。ネットワークは使わない。"""
2
+ import os
3
+ import re
4
+ import urllib.request
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ import text_compositor.deps as deps_mod
10
+ from text_compositor import cetz_render, pikchr_render
11
+ from text_compositor.compiler import TYPST_PACKAGES_ENV, typst_package_options
12
+ from text_compositor.deps import FONT_DIR_ENV, NOTO_SANS_JP_FILES, ensure_fonts
13
+
14
+ ROOT = Path(__file__).resolve().parent.parent
15
+
16
+
17
+ @pytest.fixture
18
+ def no_download(monkeypatch, tmp_path):
19
+ """取得を試みたら、失敗させる。ユーザーのキャッシュも、使い捨てにする。"""
20
+ monkeypatch.setattr(deps_mod, "_user_cache_dir", lambda: str(tmp_path / "cache"))
21
+ monkeypatch.setattr(urllib.request, "urlretrieve", lambda *a, **k: pytest.fail("downloaded"))
22
+
23
+
24
+ class TestBundledFonts:
25
+ def test_a_complete_bundled_folder_is_used_without_downloading(self, tmp_path, monkeypatch, no_download):
26
+ bundled = tmp_path / "fonts"
27
+ bundled.mkdir()
28
+ for name in NOTO_SANS_JP_FILES:
29
+ (bundled / name).write_bytes(b"font")
30
+ monkeypatch.setenv(FONT_DIR_ENV, str(bundled))
31
+ assert ensure_fonts() == str(bundled)
32
+ assert not (tmp_path / "cache").exists() # ユーザーのキャッシュにも、何も作らない
33
+
34
+ def test_an_incomplete_folder_warns_and_falls_back_to_the_download(self, tmp_path, monkeypatch, no_download, capsys):
35
+ bundled = tmp_path / "fonts"
36
+ bundled.mkdir()
37
+ (bundled / "NotoSansJP-Regular.otf").write_bytes(b"font") # Boldが無い
38
+ monkeypatch.setenv(FONT_DIR_ENV, str(bundled))
39
+ with pytest.raises(BaseException, match="downloaded"):
40
+ ensure_fonts() # 通常の取得へ進む(このテストでは、取得を失敗させてある)
41
+ captured = capsys.readouterr()
42
+ assert FONT_DIR_ENV in captured.out + captured.err # 警告が出る(出力先は、ログの実装に任せる)
43
+
44
+ def test_without_the_variable_the_behavior_is_unchanged(self, tmp_path, monkeypatch, no_download):
45
+ monkeypatch.delenv(FONT_DIR_ENV, raising=False)
46
+ with pytest.raises(BaseException, match="downloaded"):
47
+ ensure_fonts()
48
+
49
+
50
+ class TestBundledTypstPackages:
51
+ def test_no_variable_means_no_option(self, monkeypatch):
52
+ monkeypatch.delenv(TYPST_PACKAGES_ENV, raising=False)
53
+ assert typst_package_options() == {}
54
+
55
+ def test_an_existing_folder_becomes_package_cache_path(self, tmp_path, monkeypatch):
56
+ monkeypatch.setenv(TYPST_PACKAGES_ENV, str(tmp_path))
57
+ assert typst_package_options() == {"package_cache_path": str(tmp_path)}
58
+
59
+ def test_a_missing_folder_is_ignored(self, tmp_path, monkeypatch):
60
+ monkeypatch.setenv(TYPST_PACKAGES_ENV, str(tmp_path / "nope"))
61
+ assert typst_package_options() == {}
62
+
63
+ def test_a_bundled_package_folder_is_enough_to_compile_offline(self, tmp_path, monkeypatch):
64
+ """置いたパッケージだけで、`@preview/...`が解決される(ダウンロードには、頼らない)。"""
65
+ typst = pytest.importorskip("typst")
66
+ package = tmp_path / "packages" / "preview" / "mini" / "0.1.0"
67
+ package.mkdir(parents=True)
68
+ (package / "typst.toml").write_text('[package]\nname = "mini"\nversion = "0.1.0"\nentrypoint = "lib.typ"\n', encoding="utf-8")
69
+ (package / "lib.typ").write_text('#let hello() = [bundled]\n', encoding="utf-8")
70
+ source = tmp_path / "doc.typ"
71
+ source.write_text('#import "@preview/mini:0.1.0": hello\n#hello()\n', encoding="utf-8")
72
+ monkeypatch.setenv(TYPST_PACKAGES_ENV, str(tmp_path / "packages"))
73
+ pdf = typst.compile(str(source), root=str(tmp_path), ignore_system_fonts=True, **typst_package_options())
74
+ assert pdf.startswith(b"%PDF")
75
+
76
+
77
+ def test_build_dist_pins_the_same_fonts_and_packages_as_the_tool():
78
+ """配布物(build-dist.js)が同梱するフォントと、Typstのパッケージの版・SHA256が、ツール本体(deps.py・テンプレート)と、
79
+ ずれていないこと。ずれると、配布物で、CLIと違うフォント・パッケージが使われる。"""
80
+ script = (ROOT / "viewer" / "scripts" / "build-dist.js").read_text(encoding="utf-8")
81
+ for name, sha256 in NOTO_SANS_JP_FILES.items():
82
+ assert f"'{name}': '{sha256}'" in script, name
83
+ assert deps_mod.NOTO_SANS_JP_RELEASE_URL in script
84
+ common = (ROOT / "text_compositor" / "templates" / "_common.typ").read_text(encoding="utf-8")
85
+ used = set(re.findall(r"@preview/([a-z0-9-]+):([\d.]+)", common))
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の、オフラインでのコンパイルが検出する)
89
+ 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
+ bundled = set(re.findall(r"name: '([a-z0-9-]+)', version: '([\d.]+)'", script))
92
+ assert used and used == bundled, (used, bundled)
@@ -0,0 +1,219 @@
1
+ """CeTZ・Fletcher(Typstのパッケージ。cetz_render.py・図のフェンスの変換、#236)のテスト。"""
2
+ import os
3
+ import re
4
+
5
+ import pytest
6
+
7
+ from text_compositor import cetz_render, diagnostics
8
+ from text_compositor.api import build_markdown, render_html
9
+ from text_compositor.cetz_render import FigureCodeError, FigureRenderError, check_code, render_svg
10
+ from text_compositor.renderer import TypstRenderer
11
+
12
+ REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
13
+ PLAIN = {"mermaid": False, "plantuml": False, "d2": False, "graphviz": False}
14
+
15
+ CETZ = 'circle((0, 0), radius: 1)\nline((0, 0), (2, 1))\ncontent((1, 0.5), [日本語])'
16
+ FLETCHER = 'node((0, 0), [開始]), edge("->"), node((1, 0), [終了])'
17
+
18
+
19
+ def _read(relative):
20
+ with open(os.path.join(REPO_ROOT, relative), encoding="utf-8") as f:
21
+ return f.read()
22
+
23
+
24
+ def test_the_versions_are_the_same_in_the_source_and_the_bundle():
25
+ """生成コードが読み込む版と、ZIPに同梱するパッケージの版が、ずれると、配布物で、取得(ネットワーク)が起きる。"""
26
+ script = _read("viewer/scripts/build-dist.js")
27
+ bundled = set(re.findall(r"name: '([a-z0-9-]+)', version: '([\d.]+)'", script))
28
+ assert ("cetz", cetz_render.CETZ_VERSION) in bundled
29
+ assert ("fletcher", cetz_render.FLETCHER_VERSION) in bundled
30
+
31
+
32
+ class TestCheckCode:
33
+ @pytest.mark.parametrize("code", [
34
+ 'import "x.typ"',
35
+ '#import "x.typ"',
36
+ 'circle((0, 0))\ninclude "a.typ"',
37
+ 'content((0, 0), [#f({ import "/s.typ" })])',
38
+ 'let x = {\n import "a"\n}',
39
+ 'content((0, 0), [#let x = { import "a" }])',
40
+ 'f(a, (import "a"))',
41
+ '/* c */ import "x"',
42
+ '// c\nimport "x"',
43
+ 'content((0, 0), [a #include "x"])',
44
+ ])
45
+ def test_import_and_include_are_rejected(self, code):
46
+ with pytest.raises(FigureCodeError):
47
+ check_code(code)
48
+
49
+ @pytest.mark.parametrize("code", [
50
+ 'content((0, 0), [import])',
51
+ 'content((0, 0), "import and include")',
52
+ '// import x\ncircle((0, 0))',
53
+ 'content((0, 0), [Data (import) and include])',
54
+ 'my-import(1)',
55
+ 'line((0, 0), (1, 1)) /* import */',
56
+ 'content((0, 0), `import`)',
57
+ 'content((0, 0), [#strong[import]])',
58
+ 'content((0, 0), "a\\"import")',
59
+ ])
60
+ def test_the_same_words_in_a_string_a_comment_or_text_are_fine(self, code):
61
+ check_code(code)
62
+
63
+ def test_the_error_carries_the_line_in_the_code(self):
64
+ with pytest.raises(FigureCodeError) as e:
65
+ check_code('circle((0, 0))\n\nline((0, 0), (1, 1))\nimport "x"')
66
+ assert e.value.code_line == 4
67
+
68
+
69
+ class TestRender:
70
+ def test_cetz_becomes_an_svg_without_a_page_background(self):
71
+ svg = render_svg("cetz", CETZ)
72
+ assert svg.startswith("<svg") and 'fill="#ffffff"' not in svg
73
+
74
+ def test_fletcher_becomes_an_svg(self):
75
+ assert render_svg("fletcher", FLETCHER).startswith("<svg")
76
+
77
+ def test_the_compiler_is_reused_and_a_later_figure_is_not_a_leftover(self):
78
+ first = render_svg("cetz", "circle((0, 0))")
79
+ assert render_svg("cetz", "circle((0, 0))\nline((0, 0), (3, 3))") != first
80
+ assert render_svg("cetz", "circle((0, 0))") == first
81
+
82
+ def test_the_cetz_scope_has_the_drawing_functions_and_the_cetz_module(self):
83
+ assert render_svg("cetz", "let f = cetz.vector.add((1, 0, 0), (0, 1, 0))\nline((0, 0), f)").startswith("<svg")
84
+
85
+ def test_a_typst_error_is_reported_with_its_message(self):
86
+ with pytest.raises(FigureRenderError) as e:
87
+ render_svg("fletcher", 'node((0, 0), [A]), nope(1)')
88
+ assert "unknown variable: nope" in str(e.value)
89
+ assert render_svg("cetz", "circle((0, 0))").startswith("<svg") # エラーの後でも、同じCompilerで、次の図が描ける
90
+
91
+ @pytest.mark.parametrize("kind, code", [
92
+ ("cetz", 'content((0, 0), read("/etc/passwd"))'),
93
+ ("cetz", 'content((0, 0), std.read("x"))'),
94
+ ("cetz", 'content((0, 0), eval("1"))'),
95
+ ("cetz", 'content((0, 0), str(json("x.json")))'),
96
+ ("cetz", 'let f = read\ncontent((0, 0), f("x"))'),
97
+ ("cetz", 'pdf.embed("x")'),
98
+ ("fletcher", 'node((0, 0), read("x"))'),
99
+ ("fletcher", 'node((0, 0), std.plugin("x"))'),
100
+ ])
101
+ def test_functions_that_read_files_are_denied_with_a_clear_message(self, kind, code):
102
+ with pytest.raises(FigureRenderError) as e:
103
+ render_svg(kind, code)
104
+ assert "is not available" in str(e.value) or "no key" in str(e.value) or "dictionary" in str(e.value), str(e.value)
105
+
106
+ def test_import_is_rejected_before_typst_runs(self):
107
+ with pytest.raises(FigureCodeError):
108
+ render_svg("cetz", 'import "/secret.typ"')
109
+
110
+ def test_the_code_is_passed_as_data_so_quotes_and_typst_syntax_are_harmless(self):
111
+ assert render_svg("cetz", 'content((0, 0), "#panic(\\"x\\") $ \\\\ ")').startswith("<svg")
112
+
113
+
114
+ def _html(tmp_path, text, name="doc.md", plugins=PLAIN):
115
+ md = tmp_path / name
116
+ md.write_text(text, encoding="utf-8")
117
+ result = render_html(str(md), plugins=plugins)
118
+ html = open(result.html_path, encoding="utf-8").read() if result.ok else ""
119
+ return result, html
120
+
121
+
122
+ class TestHtmlOutput:
123
+ @pytest.mark.parametrize("lang, code", [("cetz", CETZ), ("fletcher", FLETCHER)])
124
+ def test_a_fence_becomes_a_cached_svg_img(self, tmp_path, lang, code):
125
+ result, html = _html(tmp_path, f'a\n\n```{lang} {{width=300pt}}\n{code}\n```\n')
126
+ assert result.ok and not result.warnings
127
+ m = re.search(rf'<img src="([^"]+)" alt="{lang} diagram" style="width:300pt">', html)
128
+ assert m, html
129
+ assert (tmp_path / ".text-compositor" / m.group(1)).read_text(encoding="utf-8").startswith("<svg")
130
+
131
+ def test_a_typst_error_is_reported_at_the_line_of_the_fence(self, tmp_path):
132
+ result, _ = _html(tmp_path, 'a\n\n```fletcher\nnode((0, 0), [A]\n```\n')
133
+ assert not result.ok
134
+ error = result.errors[0]
135
+ assert error.message == "Fletcher diagram failed to render"
136
+ assert error.line == 3 and "unclosed delimiter" in error.detail
137
+
138
+ def test_import_is_reported_at_the_line_in_the_manuscript(self, tmp_path):
139
+ # フェンスは3行目。`import`は、コードの3行目で、原稿の6行目
140
+ result, _ = _html(tmp_path, 'a\n\n```cetz\ncircle((0, 0))\nline((0, 0), (1, 1))\nimport "/secret.typ"\n```\n')
141
+ assert not result.ok
142
+ assert result.errors[0].line == 6 and "'import' cannot be used" in result.errors[0].detail
143
+
144
+ def test_a_denied_function_is_an_error_not_a_file_read(self, tmp_path):
145
+ (tmp_path / "secret.txt").write_text("TOP-SECRET", encoding="utf-8")
146
+ result, html = _html(tmp_path, '```cetz\ncontent((0, 0), read("/secret.txt"))\n```\n')
147
+ assert not result.ok and "is not available" in result.errors[0].detail
148
+ assert "TOP-SECRET" not in html
149
+
150
+ @pytest.mark.parametrize("lang", ["cetz", "fletcher"])
151
+ def test_the_plugin_setting_falls_back_to_a_code_block_without_a_warning(self, tmp_path, lang):
152
+ result, html = _html(tmp_path, f'```{lang}\nnode((0, 0), [x])\n```\n', plugins={**PLAIN, lang: False})
153
+ assert result.ok and f'class="language-{lang}"' in html and not result.warnings
154
+
155
+ def test_one_plugin_setting_does_not_disable_the_other(self, tmp_path):
156
+ result, html = _html(tmp_path, f'```cetz\n{CETZ}\n```\n\n```fletcher\n{FLETCHER}\n```\n', plugins={**PLAIN, "cetz": False})
157
+ assert result.ok and 'class="language-cetz"' in html and 'alt="fletcher diagram"' in html
158
+
159
+
160
+ class TestPdfCode:
161
+ def _render(self, text, **kwargs):
162
+ renderer = TypstRenderer(line_mapping="off", **kwargs)
163
+ with diagnostics.collect():
164
+ return renderer.render(text, filepath="doc.md")
165
+
166
+ def test_the_fence_becomes_code_that_imports_the_pinned_package_and_evals_the_code_as_a_string(self):
167
+ code = self._render(f'```cetz\n{CETZ}\n```\n')
168
+ assert f'import "@preview/cetz:{cetz_render.CETZ_VERSION}"' in code
169
+ assert 'eval("circle((0, 0), radius: 1)\\nline' in code and 'mode: "code"' in code
170
+ assert "measure(fig)" in code # 自動縮小
171
+
172
+ def test_fletcher_wraps_the_code_in_diagram(self):
173
+ code = self._render(f'```fletcher\n{FLETCHER}\n```\n')
174
+ assert f'import "@preview/fletcher:{cetz_render.FLETCHER_VERSION}"' in code
175
+ assert 'eval("diagram(\\n" + "node((0, 0)' in code
176
+
177
+ def test_the_file_reading_functions_are_denied_in_the_generated_code(self):
178
+ code = self._render('```cetz\ncircle((0, 0))\n```\n')
179
+ assert '"read", "json"' in code and "std: denied" in code
180
+
181
+ def test_a_size_attribute_scales_to_that_size(self):
182
+ code = self._render('```cetz {width=5cm}\ncircle((0, 0))\n```\n')
183
+ assert "(5cm)" in code and "scale(s * 100%" in code and "calc.min" in code # 縦横比を保つ(引き伸ばさない)
184
+ code = self._render('```fletcher {width=50%}\nnode((0, 0), [A])\n```\n')
185
+ assert "50 * 1% * size.width" in code
186
+
187
+ def test_it_is_generated_without_touching_the_templates_public_names(self):
188
+ """カスタムテンプレートが、新しい公開名を、持たなくても、動く(生成コードが、パッケージを直接importする)。"""
189
+ code = self._render('```cetz\ncircle((0, 0))\n```\n')
190
+ assert "render-cetz" not in code and "render-fletcher" not in code
191
+
192
+ def test_the_plugin_setting_leaves_a_plain_code_block(self):
193
+ code = self._render('```cetz\ncircle((0, 0))\n```\n', cetz_enabled=False)
194
+ assert "@preview/cetz" not in code and "circle" in code
195
+
196
+ def test_import_stops_the_build_at_the_line_in_the_manuscript(self):
197
+ renderer = TypstRenderer(line_mapping="off")
198
+ with diagnostics.collect() as sink:
199
+ with pytest.raises(SystemExit):
200
+ renderer.render('a\n\n```cetz\ncircle((0, 0))\nimport "x"\n```\n', filepath="doc.md")
201
+ errors = sink.errors
202
+ assert errors and errors[0].line == 5 and "'import' cannot be used" in errors[0].detail
203
+
204
+
205
+ def test_a_pdf_with_both_figures_is_built(tmp_path):
206
+ md = tmp_path / "doc.md"
207
+ md.write_text(f'# 図\n\n```cetz\n{CETZ}\n```\n\n```fletcher\n{FLETCHER}\n```\n\n'
208
+ '```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")
210
+ result = build_markdown(str(md), str(tmp_path / "doc.pdf"))
211
+ assert result.ok, [(e.message, e.detail) for e in result.errors]
212
+ assert (tmp_path / "doc.pdf").read_bytes().startswith(b"%PDF")
213
+
214
+
215
+ def test_a_denied_function_in_the_pdf_is_a_compile_error(tmp_path):
216
+ md = tmp_path / "doc.md"
217
+ md.write_text('```cetz\ncontent((0, 0), read("/secret.txt"))\n```\n', encoding="utf-8")
218
+ result = build_markdown(str(md), str(tmp_path / "doc.pdf"))
219
+ assert not result.ok and "is not available" in result.errors[0].message
@@ -20,7 +20,7 @@ def test_read_pins_finds_every_pin_in_the_sources(check_pins):
20
20
  pins = check_pins.read_pins()
21
21
  missing = [name for name, version in pins.items() if version is None]
22
22
  assert missing == []
23
- for name in ("Mermaid", "Viz.js", "PlantUML", "D2", "Temurin JRE 21", "Noto Sans JP", "組込版Python"):
23
+ for name in ("Mermaid", "PlantUML", "D2", "Temurin JRE 21", "Noto Sans JP", "組込版Python"):
24
24
  assert name in pins
25
25
  assert any(name.startswith("Typstパッケージ ") for name in pins)
26
26
 
@@ -28,7 +28,6 @@ def test_read_pins_finds_every_pin_in_the_sources(check_pins):
28
28
  def test_read_pins_matches_the_source_constants(check_pins):
29
29
  from text_compositor import deps
30
30
  pins = check_pins.read_pins()
31
- assert pins["Viz.js"] == deps.VIZ_JS_VERSION
32
31
  assert f"v{pins['D2']}" == deps.D2_RELEASE
33
32
  assert f"jdk-{pins['Temurin JRE 21']}" == deps.TEMURIN_JRE_RELEASE
34
33
 
@@ -1,7 +1,7 @@
1
1
  """図のキャッシュキーを複合ハッシュにする設計(#26)のリグレッションテスト。"""
2
2
  import os
3
3
 
4
- import text_compositor.renderer as renderer_mod
4
+ import text_compositor.renderer_diagrams as diagrams_mod
5
5
  from text_compositor.deps import D2_RELEASE
6
6
  from text_compositor.renderer import TypstRenderer, _diagram_cache_key
7
7
 
@@ -33,13 +33,13 @@ class TestDiagramCachePath:
33
33
  assert os.path.isdir(os.path.dirname(path))
34
34
 
35
35
  def test_d2_version_uses_system_d2_output(self, tmp_path, monkeypatch):
36
- monkeypatch.setattr(renderer_mod, "find_system_d2", lambda: "/usr/bin/d2")
37
- monkeypatch.setattr(renderer_mod, "_system_d2_version", lambda _bin: "v9.9.9")
36
+ monkeypatch.setattr(diagrams_mod, "find_system_d2", lambda: "/usr/bin/d2")
37
+ monkeypatch.setattr(diagrams_mod, "_system_d2_version", lambda _bin: "v9.9.9")
38
38
  renderer = TypstRenderer(base_dir=str(tmp_path), line_mapping="off")
39
39
  assert renderer._d2_version() == "v9.9.9"
40
40
 
41
41
  def test_d2_version_falls_back_to_pinned_release_without_downloading(self, tmp_path, monkeypatch):
42
- monkeypatch.setattr(renderer_mod, "find_system_d2", lambda: None)
43
- monkeypatch.setattr(renderer_mod, "ensure_d2_binary", lambda: (_ for _ in ()).throw(AssertionError("must not download")))
42
+ monkeypatch.setattr(diagrams_mod, "find_system_d2", lambda: None)
43
+ monkeypatch.setattr(diagrams_mod, "ensure_d2_binary", lambda: (_ for _ in ()).throw(AssertionError("must not download")))
44
44
  renderer = TypstRenderer(base_dir=str(tmp_path), line_mapping="off")
45
45
  assert renderer._d2_version() == D2_RELEASE
@@ -1,6 +1,7 @@
1
1
  """Viewerの配布物(#168)の前提のテスト。
2
2
 
3
- - PDF専用の依存(typst)と、Mermaid用のplaywrightが、なくても、HTML出力が動くこと(配布物に、同梱しない)。
3
+ - typstとMermaid用のplaywrightが、なくても、Graphviz以外のHTML出力が動くこと(pipで入れる場合の、最小の構成。
4
+ Graphvizは、Typstのdiagraphで描くため(#264)、typstが要る。配布物は、typstを同梱する。#263)。
4
5
  - 配布物のPython依存(viewer/dist-requirements.txt)が、pyproject.tomlの依存と、ずれていないこと。"""
5
6
  import json
6
7
  import re
@@ -41,7 +42,19 @@ class TestWithoutTypstAndPlaywright:
41
42
  "from text_compositor.compiler import typst_lib\n"
42
43
  "try:\n typst_lib.compile\nexcept ImportError as e:\n print('IMPORTERROR', e)")
43
44
  result = run_python(code, tmp_path)
44
- assert "IMPORTERROR" in result.stdout and "not needed for HTML output" in result.stdout
45
+ assert "IMPORTERROR" in result.stdout and "pip install typst" in result.stdout
46
+
47
+ def test_graphviz_in_html_output_without_typst_is_an_error_that_says_what_is_missing(self, tmp_path):
48
+ md = tmp_path / "doc.md"
49
+ md.write_text("```dot\ndigraph { a -> b }\n```\n", encoding="utf-8")
50
+ code = self.BLOCK + (
51
+ "import json\nfrom text_compositor.api import render_html\n"
52
+ f"r = render_html({str(md)!r}, plugins={{'mermaid': False, 'plantuml': False, 'd2': False}})\n"
53
+ "print(json.dumps({'ok': r.ok, 'detail': [d.detail for d in r.errors]}))")
54
+ result = run_python(code, tmp_path)
55
+ assert result.returncode == 0, result.stderr
56
+ reply = json.loads(result.stdout.strip().splitlines()[-1])
57
+ assert reply["ok"] is False and "pip install typst" in reply["detail"][0]
45
58
 
46
59
 
47
60
  def _pins(lines):
@@ -60,9 +73,11 @@ def test_the_distribution_requirements_follow_pyproject():
60
73
  project = _pins(dependencies.splitlines())
61
74
  dist = _pins((ROOT / "viewer" / "dist-requirements.txt").read_text(encoding="utf-8").splitlines())
62
75
 
63
- assert "typst" in project and "typst" not in dist # PDF専用のため、同梱しない
64
76
  assert dist, "no pinned requirements found"
65
77
  for name, version in dist.items():
66
78
  assert project.get(name) == version, f"{name}: dist {version} != pyproject {project.get(name)}"
67
- # HTML出力に必要なもの(typst以外)は、すべて同梱する
68
- assert set(project) - {"typst"} == set(dist)
79
+ # typstは、Typstを通す処理のために、同梱する(#263。#237で決めた)。Mermaid用のplaywrightは、pyprojectの必須の依存ではなく、
80
+ # ElectronのChromiumで描画するため、同梱しない
81
+ assert "typst" in project and "typst" in dist
82
+ assert "playwright" not in dist
83
+ assert set(project) == set(dist)
@@ -9,7 +9,7 @@ import zipfile
9
9
 
10
10
  import pytest
11
11
 
12
- from text_compositor.deps import DOWNLOAD_ATTEMPTS, VIZ_JS_URL, VIZ_JS_VERSION, _download, ensure_fonts, ensure_mermaid_js, ensure_viz_js
12
+ from text_compositor.deps import DOWNLOAD_ATTEMPTS, _download, ensure_fonts, ensure_mermaid_js
13
13
  import text_compositor.deps as deps_mod
14
14
  import time
15
15
  import urllib.request
@@ -186,32 +186,6 @@ class TestOtherDownloads:
186
186
  path = ensure_mermaid_js()
187
187
  assert open(path, "rb").read() == content
188
188
 
189
- def test_viz_js_is_verified_by_checksum_and_only_a_verified_file_is_kept(self, tmp_path, monkeypatch):
190
- monkeypatch.setattr(deps_mod, "_user_cache_dir", lambda: str(tmp_path / "cache"))
191
- monkeypatch.setattr(time, "sleep", lambda seconds: None)
192
-
193
- def fetch(content):
194
- def retrieve(url, filename):
195
- with open(filename, "wb") as f:
196
- f.write(content)
197
- return retrieve
198
-
199
- monkeypatch.setattr(deps_mod, "VIZ_JS_SHA256", hashlib.sha256(b"viz-bundle").hexdigest())
200
- monkeypatch.setattr(urllib.request, "urlretrieve", fetch(b"tampered"))
201
- with pytest.raises(SystemExit):
202
- ensure_viz_js()
203
- assert list((tmp_path / "cache" / "viz").iterdir()) == [] # 検証に通らなければ、何も残らない
204
-
205
- monkeypatch.setattr(urllib.request, "urlretrieve", fetch(b"viz-bundle"))
206
- path = ensure_viz_js()
207
- assert open(path, "rb").read() == b"viz-bundle"
208
- # 取得済みなら、ネットワークを使わない
209
- monkeypatch.setattr(urllib.request, "urlretrieve", lambda *a: pytest.fail("downloaded again"))
210
- assert ensure_viz_js() == path
211
-
212
- def test_the_pinned_viz_js_url_names_the_pinned_version(self):
213
- assert f"@viz-js/viz@{VIZ_JS_VERSION}/" in VIZ_JS_URL
214
-
215
189
  def test_plantuml_jar_cached_by_an_older_version_is_not_reused(self, tmp_path, monkeypatch):
216
190
  """版を上げたとき、取得済みの古いjar(固定名plantuml-mit.jar)が使われ続けると、修正が届かない(#240)。"""
217
191
  cache = tmp_path / "cache"
@@ -0,0 +1,111 @@
1
+ """効かずに、文字として残った太字(`**`)の警告(emphasis_lint.py、#215)のテスト。"""
2
+ import os
3
+
4
+ import pytest
5
+
6
+ from text_compositor import diagnostics
7
+ from text_compositor.api import render_html
8
+ from text_compositor.emphasis_lint import find_unapplied_bold
9
+ from text_compositor.renderer import TypstRenderer
10
+
11
+ PLAIN = {"mermaid": False, "plantuml": False, "d2": False, "graphviz": False}
12
+
13
+
14
+ def _inline(text):
15
+ md = TypstRenderer(line_mapping="off").md
16
+ return [t for t in md.parse(text) if t.type == "inline"][0]
17
+
18
+
19
+ def _inline_children(text):
20
+ return _inline(text).children
21
+
22
+
23
+ def _find(text):
24
+ inline = _inline(text)
25
+ return find_unapplied_bold(inline.children, inline.content)
26
+
27
+
28
+ class TestFindUnappliedBold:
29
+ @pytest.mark.parametrize("text, snippet", [
30
+ ("これは**「重要」**です。", "**「重要」**"),
31
+ ("結果は**100%**です", "**100%**"),
32
+ ("**注意:**この点に注意", "**注意:**"),
33
+ ("値は**(仮)**とする", "**(仮)**"),
34
+ ])
35
+ def test_finds_bold_that_commonmark_did_not_apply(self, text, snippet):
36
+ assert _find(text)[0] == (0, snippet)
37
+
38
+ @pytest.mark.parametrize("text", [
39
+ "これは**重要**です。", # 日本語の文字に挟まれても、効く
40
+ "これは **重要** です。",
41
+ "「**重要**」です",
42
+ "**注意:** この点",
43
+ "`**code**`", # コードスパン
44
+ r"\*\*not bold\*\*", # エスケープ
45
+ "a ** b ** c", # 空白で挟まれた`**`は、太字のつもりではない
46
+ "def f(*args, **kwargs)", # `**`が1つだけ
47
+ "2**3 and 4**5", # 数字の間は、太字として効く
48
+ "__太字__です", # `__`は、対象外
49
+ "**a**b**c**",
50
+ "普通の文章です。",
51
+ ])
52
+ def test_does_not_flag_what_is_fine(self, text):
53
+ assert _find(text) == []
54
+
55
+ def test_reports_the_line_offset_inside_a_multi_line_paragraph(self):
56
+ found = _find("1行目です。\n2行目は**「重要」**です。\n3行目。")
57
+ assert found == [(1, "**「重要」**")]
58
+
59
+ def test_bold_around_a_code_span_is_found_with_a_placeholder(self):
60
+ assert _find("式は**`x`**です") == [(0, "**…**")]
61
+
62
+ def test_a_long_match_is_shortened(self):
63
+ found = _find("あ**「" + "長" * 80 + "」**い")
64
+ assert len(found[0][1]) == 40 and found[0][1].endswith("…")
65
+
66
+ def test_no_children_is_fine(self):
67
+ assert find_unapplied_bold(None) == []
68
+
69
+
70
+ def _html_warnings(tmp_path, text):
71
+ md = tmp_path / "doc.md"
72
+ md.write_text(text, encoding="utf-8")
73
+ result = render_html(str(md), plugins=PLAIN)
74
+ return result, [w for w in result.warnings if "Bold markup" in w.message]
75
+
76
+
77
+ class TestHtmlOutput:
78
+ def test_a_failed_bold_is_a_warning_with_the_manuscript_line(self, tmp_path):
79
+ result, warnings = _html_warnings(tmp_path, "# 見出し\n\n本文です。\n\nこれは**「重要」**です。\n")
80
+ assert result.ok
81
+ assert len(warnings) == 1 and warnings[0].line == 5
82
+ assert "**「重要」**" in warnings[0].message and "space" in warnings[0].message
83
+
84
+ def test_the_line_counts_the_lines_inside_a_paragraph_and_after_a_layout_block(self, tmp_path):
85
+ text = "a\n\n::: layout-right\n```mermaid\ngraph TD\n A-->B\n```\n本文\n:::\n\n1行目\n2行目は**「重要」**です\n"
86
+ _, warnings = _html_warnings(tmp_path, text)
87
+ assert [w.line for w in warnings] == [12]
88
+
89
+ def test_a_working_bold_and_code_are_not_flagged(self, tmp_path):
90
+ _, warnings = _html_warnings(tmp_path, "これは**重要**です。\n\n`**code**`\n\n```\n**fence**\n```\n")
91
+ assert warnings == []
92
+
93
+ def test_the_text_is_still_shown(self, tmp_path):
94
+ result, _ = _html_warnings(tmp_path, "これは**「重要」**です。\n")
95
+ with open(result.html_path, encoding="utf-8") as f:
96
+ assert "<p>これは**「重要」**です。</p>" in f.read()
97
+
98
+ def test_a_table_cell_and_a_list_item_are_checked(self, tmp_path):
99
+ text = "| a | b |\n| - | - |\n| **「x」**です | y |\n\n- 項目**(仮)**です\n"
100
+ _, warnings = _html_warnings(tmp_path, text)
101
+ assert [w.line for w in warnings] == [3, 5]
102
+
103
+
104
+ class TestPdfPath:
105
+ def test_the_pdf_renderer_warns_with_the_line(self, tmp_path):
106
+ renderer = TypstRenderer(line_mapping="off")
107
+ with diagnostics.collect() as collected:
108
+ renderer.render("# 見出し\n\n本文\n\nこれは**「重要」**です。\n", filepath=str(tmp_path / "doc.md"))
109
+ warnings = [w for w in collected.warnings if "Bold markup" in w.message]
110
+ assert len(warnings) == 1 and warnings[0].line == 5
111
+ assert os.path.basename(warnings[0].file) == "doc.md"