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.
- {text_compositor-0.3.6/text_compositor.egg-info → text_compositor-0.3.8}/PKG-INFO +4 -4
- {text_compositor-0.3.6 → text_compositor-0.3.8}/README.md +3 -3
- {text_compositor-0.3.6 → text_compositor-0.3.8}/pyproject.toml +1 -1
- text_compositor-0.3.8/tests/test_bundled_typst_assets.py +92 -0
- text_compositor-0.3.8/tests/test_cetz_render.py +219 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_check_pins.py +1 -2
- {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_diagram_cache_key.py +5 -5
- {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_distribution.py +20 -5
- {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_download.py +1 -27
- text_compositor-0.3.8/tests/test_emphasis_lint.py +111 -0
- text_compositor-0.3.8/tests/test_graphviz_render.py +88 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_html_output.py +113 -36
- text_compositor-0.3.8/tests/test_pikchr_render.py +111 -0
- text_compositor-0.3.8/tests/test_renderer_structure.py +53 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_worker.py +24 -108
- {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/api.py +16 -7
- text_compositor-0.3.8/text_compositor/cetz_render.py +207 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/compiler.py +4 -21
- {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/deps.py +15 -37
- text_compositor-0.3.8/text_compositor/emphasis_lint.py +51 -0
- text_compositor-0.3.8/text_compositor/graphviz_render.py +192 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/host_renderers.py +2 -13
- {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/html_output.py +40 -19
- text_compositor-0.3.8/text_compositor/pikchr_render.py +69 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/project.py +5 -1
- text_compositor-0.3.8/text_compositor/renderer.py +408 -0
- text_compositor-0.3.8/text_compositor/renderer_diagrams.py +457 -0
- text_compositor-0.3.8/text_compositor/renderer_inline.py +217 -0
- text_compositor-0.3.8/text_compositor/renderer_layout.py +212 -0
- text_compositor-0.3.8/text_compositor/renderer_tables.py +138 -0
- text_compositor-0.3.8/text_compositor/renderer_tokens.py +286 -0
- text_compositor-0.3.8/text_compositor/typst_runtime.py +33 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/worker.py +9 -10
- {text_compositor-0.3.6 → text_compositor-0.3.8/text_compositor.egg-info}/PKG-INFO +4 -4
- {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor.egg-info/SOURCES.txt +16 -0
- text_compositor-0.3.6/text_compositor/renderer.py +0 -1515
- {text_compositor-0.3.6 → text_compositor-0.3.8}/LICENSE +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/setup.cfg +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_api.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_asset_resolution.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_common_template.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_config.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_csv_table.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_diagnostics.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_doc_badges.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_front_matter.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_incremental.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_layout_blocks.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_line_mapping.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_markdown_basics.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_mermaid_resident.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_paper_template.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_revision_history.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_script_references.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_sections.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_table_cell_style.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_variables.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_viewer_version.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/tests/test_watch.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/__init__.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/build.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/changes.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/chapters.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/config.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/diagnostics.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/document.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/env_check.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/log.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/mermaid.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/templates/_common.typ +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/templates/paper.typ +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/templates/slide.typ +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/templates/template.typ +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor/typst_literal.py +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor.egg-info/dependency_links.txt +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor.egg-info/entry_points.txt +0 -0
- {text_compositor-0.3.6 → text_compositor-0.3.8}/text_compositor.egg-info/requires.txt +0 -0
- {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.
|
|
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
|
|
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
|
|
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
|
|
|
@@ -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", "
|
|
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.
|
|
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(
|
|
37
|
-
monkeypatch.setattr(
|
|
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(
|
|
43
|
-
monkeypatch.setattr(
|
|
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
|
-
-
|
|
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 "
|
|
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
|
-
#
|
|
68
|
-
|
|
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,
|
|
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"
|