text-compositor 0.3.0__tar.gz → 0.3.6__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.0 → text_compositor-0.3.6}/PKG-INFO +23 -5
- text_compositor-0.3.0/text_compositor.egg-info/PKG-INFO → text_compositor-0.3.6/README.md +18 -18
- {text_compositor-0.3.0 → text_compositor-0.3.6}/pyproject.toml +5 -5
- text_compositor-0.3.6/tests/test_api.py +330 -0
- {text_compositor-0.3.0 → text_compositor-0.3.6}/tests/test_asset_resolution.py +8 -7
- text_compositor-0.3.6/tests/test_check_pins.py +70 -0
- {text_compositor-0.3.0 → text_compositor-0.3.6}/tests/test_common_template.py +9 -7
- {text_compositor-0.3.0 → text_compositor-0.3.6}/tests/test_config.py +16 -15
- text_compositor-0.3.6/tests/test_csv_table.py +149 -0
- text_compositor-0.3.6/tests/test_diagnostics.py +134 -0
- text_compositor-0.3.6/tests/test_diagram_cache_key.py +45 -0
- text_compositor-0.3.6/tests/test_distribution.py +68 -0
- text_compositor-0.3.6/tests/test_doc_badges.py +38 -0
- text_compositor-0.3.6/tests/test_download.py +231 -0
- {text_compositor-0.3.0 → text_compositor-0.3.6}/tests/test_front_matter.py +9 -8
- text_compositor-0.3.6/tests/test_html_output.py +689 -0
- {text_compositor-0.3.0 → text_compositor-0.3.6}/tests/test_incremental.py +19 -14
- {text_compositor-0.3.0 → text_compositor-0.3.6}/tests/test_layout_blocks.py +3 -3
- {text_compositor-0.3.0 → text_compositor-0.3.6}/tests/test_line_mapping.py +23 -21
- {text_compositor-0.3.0 → text_compositor-0.3.6}/tests/test_markdown_basics.py +9 -9
- text_compositor-0.3.6/tests/test_mermaid_resident.py +151 -0
- {text_compositor-0.3.0 → text_compositor-0.3.6}/tests/test_paper_template.py +12 -10
- {text_compositor-0.3.0 → text_compositor-0.3.6}/tests/test_revision_history.py +9 -9
- text_compositor-0.3.6/tests/test_script_references.py +37 -0
- {text_compositor-0.3.0 → text_compositor-0.3.6}/tests/test_sections.py +15 -11
- {text_compositor-0.3.0 → text_compositor-0.3.6}/tests/test_table_cell_style.py +2 -2
- {text_compositor-0.3.0 → text_compositor-0.3.6}/tests/test_variables.py +11 -10
- text_compositor-0.3.6/tests/test_viewer_version.py +21 -0
- {text_compositor-0.3.0 → text_compositor-0.3.6}/tests/test_watch.py +12 -10
- text_compositor-0.3.6/tests/test_worker.py +491 -0
- text_compositor-0.3.6/text_compositor/__init__.py +28 -0
- text_compositor-0.3.6/text_compositor/api.py +349 -0
- text_compositor-0.3.6/text_compositor/build.py +215 -0
- text_compositor-0.3.6/text_compositor/changes.py +93 -0
- text_compositor-0.3.6/text_compositor/chapters.py +270 -0
- text_compositor-0.3.6/text_compositor/compiler.py +187 -0
- text_compositor-0.3.6/text_compositor/config.py +205 -0
- text_compositor-0.3.6/text_compositor/deps.py +459 -0
- text_compositor-0.3.6/text_compositor/diagnostics.py +105 -0
- text_compositor-0.3.6/text_compositor/document.py +229 -0
- text_compositor-0.3.6/text_compositor/env_check.py +204 -0
- text_compositor-0.3.6/text_compositor/host_renderers.py +26 -0
- text_compositor-0.3.6/text_compositor/html_output.py +690 -0
- text_compositor-0.3.6/text_compositor/log.py +37 -0
- text_compositor-0.3.6/text_compositor/mermaid.py +150 -0
- text_compositor-0.3.6/text_compositor/project.py +155 -0
- text_compositor-0.3.6/text_compositor/renderer.py +1515 -0
- text_compositor-0.3.6/text_compositor/typst_literal.py +15 -0
- text_compositor-0.3.6/text_compositor/worker.py +197 -0
- text_compositor-0.3.0/README.md → text_compositor-0.3.6/text_compositor.egg-info/PKG-INFO +36 -0
- {text_compositor-0.3.0 → text_compositor-0.3.6}/text_compositor.egg-info/SOURCES.txt +28 -0
- text_compositor-0.3.6/text_compositor.egg-info/requires.txt +8 -0
- text_compositor-0.3.0/tests/test_csv_table.py +0 -78
- text_compositor-0.3.0/tests/test_diagram_cache_key.py +0 -43
- text_compositor-0.3.0/text_compositor/__init__.py +0 -0
- text_compositor-0.3.0/text_compositor/build.py +0 -3286
- text_compositor-0.3.0/text_compositor.egg-info/requires.txt +0 -8
- {text_compositor-0.3.0 → text_compositor-0.3.6}/LICENSE +0 -0
- {text_compositor-0.3.0 → text_compositor-0.3.6}/setup.cfg +0 -0
- {text_compositor-0.3.0 → text_compositor-0.3.6}/text_compositor/templates/_common.typ +0 -0
- {text_compositor-0.3.0 → text_compositor-0.3.6}/text_compositor/templates/paper.typ +0 -0
- {text_compositor-0.3.0 → text_compositor-0.3.6}/text_compositor/templates/slide.typ +0 -0
- {text_compositor-0.3.0 → text_compositor-0.3.6}/text_compositor/templates/template.typ +0 -0
- {text_compositor-0.3.0 → text_compositor-0.3.6}/text_compositor.egg-info/dependency_links.txt +0 -0
- {text_compositor-0.3.0 → text_compositor-0.3.6}/text_compositor.egg-info/entry_points.txt +0 -0
- {text_compositor-0.3.0 → text_compositor-0.3.6}/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.6
|
|
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
|
|
@@ -8,12 +8,12 @@ Requires-Python: >=3.10
|
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
9
9
|
License-File: LICENSE
|
|
10
10
|
Requires-Dist: typst==0.15.0
|
|
11
|
-
Requires-Dist: markdown-it-py==
|
|
11
|
+
Requires-Dist: markdown-it-py==4.2.0
|
|
12
12
|
Requires-Dist: mdit-py-plugins==0.6.1
|
|
13
|
-
Requires-Dist: PyYAML==6.0.
|
|
14
|
-
Requires-Dist: platformdirs==4.
|
|
13
|
+
Requires-Dist: PyYAML==6.0.3
|
|
14
|
+
Requires-Dist: platformdirs==4.11.9
|
|
15
15
|
Provides-Extra: mermaid
|
|
16
|
-
Requires-Dist: playwright==1.
|
|
16
|
+
Requires-Dist: playwright==1.63.0; extra == "mermaid"
|
|
17
17
|
Dynamic: license-file
|
|
18
18
|
|
|
19
19
|
# text-compositor
|
|
@@ -75,6 +75,10 @@ pip install -r requirements.txt
|
|
|
75
75
|
|
|
76
76
|
No Typst compiler binary is bundled; it's obtained from the `typst` package (PyPI wheel) installed via `pip install` above. No additional downloads or installation steps are needed.
|
|
77
77
|
|
|
78
|
+
### Keeping up to date
|
|
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.
|
|
81
|
+
|
|
78
82
|
### Using Mermaid diagrams (optional)
|
|
79
83
|
|
|
80
84
|
This is only needed if your source documents use ` ```mermaid ` fences (or specify an `.mmd` file directly in `chapters`). With Option A, install the `mermaid` extra:
|
|
@@ -147,6 +151,20 @@ Other flags control runtime behavior only (not document content, which stays ent
|
|
|
147
151
|
|
|
148
152
|
`--clean` deletes the output PDF and the intermediate files under `.text-compositor/` (`temp_build.typ`, `_template.typ`, `_common.typ`) without building. `--clean-cache` additionally deletes the diagram cache (`.text-compositor/cache/`). Input files and the config are never deleted.
|
|
149
153
|
|
|
154
|
+
### Python API
|
|
155
|
+
|
|
156
|
+
`text_compositor.Session` and `build_markdown()` turn a single Markdown file into a PDF without a `config.yaml`, and return warnings and errors as structured diagnostics (Markdown file and line included) instead of printing them. A resident worker (`python -m text_compositor.worker`) exposes the same over JSON lines on stdin/stdout, for GUI viewers. See the usage guide chapter "Using from Python" and section 14 of [doc/spec.md](doc/spec.md).
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from text_compositor import Session
|
|
160
|
+
|
|
161
|
+
with Session() as session: # reuses the Mermaid browser and the Typst compiler across builds
|
|
162
|
+
result = session.build("doc.md", "out/doc.pdf")
|
|
163
|
+
print(result.ok, result.pdf_path, [d.message for d in result.diagnostics])
|
|
164
|
+
```
|
|
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.
|
|
167
|
+
|
|
150
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.
|
|
151
169
|
|
|
152
170
|
## Trying the sample
|
|
@@ -1,21 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: text-compositor
|
|
3
|
-
Version: 0.3.0
|
|
4
|
-
Summary: A tool for composing multiple text files into a PDF document.
|
|
5
|
-
License-Expression: MIT
|
|
6
|
-
Project-URL: Repository, https://github.com/tokudiro/text-compositor
|
|
7
|
-
Requires-Python: >=3.10
|
|
8
|
-
Description-Content-Type: text/markdown
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
Requires-Dist: typst==0.15.0
|
|
11
|
-
Requires-Dist: markdown-it-py==3.0.0
|
|
12
|
-
Requires-Dist: mdit-py-plugins==0.6.1
|
|
13
|
-
Requires-Dist: PyYAML==6.0.2
|
|
14
|
-
Requires-Dist: platformdirs==4.3.6
|
|
15
|
-
Provides-Extra: mermaid
|
|
16
|
-
Requires-Dist: playwright==1.62.0; extra == "mermaid"
|
|
17
|
-
Dynamic: license-file
|
|
18
|
-
|
|
19
1
|
# text-compositor
|
|
20
2
|
|
|
21
3
|
*[日本語版 (Japanese version)](README-ja.md)*
|
|
@@ -75,6 +57,10 @@ pip install -r requirements.txt
|
|
|
75
57
|
|
|
76
58
|
No Typst compiler binary is bundled; it's obtained from the `typst` package (PyPI wheel) installed via `pip install` above. No additional downloads or installation steps are needed.
|
|
77
59
|
|
|
60
|
+
### Keeping up to date
|
|
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.
|
|
63
|
+
|
|
78
64
|
### Using Mermaid diagrams (optional)
|
|
79
65
|
|
|
80
66
|
This is only needed if your source documents use ` ```mermaid ` fences (or specify an `.mmd` file directly in `chapters`). With Option A, install the `mermaid` extra:
|
|
@@ -147,6 +133,20 @@ Other flags control runtime behavior only (not document content, which stays ent
|
|
|
147
133
|
|
|
148
134
|
`--clean` deletes the output PDF and the intermediate files under `.text-compositor/` (`temp_build.typ`, `_template.typ`, `_common.typ`) without building. `--clean-cache` additionally deletes the diagram cache (`.text-compositor/cache/`). Input files and the config are never deleted.
|
|
149
135
|
|
|
136
|
+
### Python API
|
|
137
|
+
|
|
138
|
+
`text_compositor.Session` and `build_markdown()` turn a single Markdown file into a PDF without a `config.yaml`, and return warnings and errors as structured diagnostics (Markdown file and line included) instead of printing them. A resident worker (`python -m text_compositor.worker`) exposes the same over JSON lines on stdin/stdout, for GUI viewers. See the usage guide chapter "Using from Python" and section 14 of [doc/spec.md](doc/spec.md).
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from text_compositor import Session
|
|
142
|
+
|
|
143
|
+
with Session() as session: # reuses the Mermaid browser and the Typst compiler across builds
|
|
144
|
+
result = session.build("doc.md", "out/doc.pdf")
|
|
145
|
+
print(result.ok, result.pdf_path, [d.message for d in result.diagnostics])
|
|
146
|
+
```
|
|
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.
|
|
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
|
|
|
152
152
|
## Trying the sample
|
|
@@ -4,23 +4,23 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "text-compositor"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.6"
|
|
8
8
|
description = "A tool for composing multiple text files into a PDF document."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
11
11
|
requires-python = ">=3.10"
|
|
12
12
|
dependencies = [
|
|
13
13
|
"typst==0.15.0",
|
|
14
|
-
"markdown-it-py==
|
|
14
|
+
"markdown-it-py==4.2.0",
|
|
15
15
|
"mdit-py-plugins==0.6.1",
|
|
16
|
-
"PyYAML==6.0.
|
|
17
|
-
"platformdirs==4.
|
|
16
|
+
"PyYAML==6.0.3",
|
|
17
|
+
"platformdirs==4.11.9",
|
|
18
18
|
]
|
|
19
19
|
|
|
20
20
|
# plugins.mermaid: true(既定)を使う場合のみ必要(11章)。
|
|
21
21
|
# `pip install text-compositor[mermaid]` でまとめて導入できる。
|
|
22
22
|
[project.optional-dependencies]
|
|
23
|
-
mermaid = ["playwright==1.
|
|
23
|
+
mermaid = ["playwright==1.63.0"]
|
|
24
24
|
|
|
25
25
|
[project.urls]
|
|
26
26
|
Repository = "https://github.com/tokudiro/text-compositor"
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""Python API(api.py、#167)のテスト。実際にTypstでコンパイルするため、フォント(初回のみ取得)と、
|
|
2
|
+
Typstのパッケージ(テンプレートが使う@preview/…。初回のみ取得)を使う。"""
|
|
3
|
+
import os
|
|
4
|
+
import struct
|
|
5
|
+
import sys
|
|
6
|
+
import zlib
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
import text_compositor.build as build
|
|
11
|
+
from text_compositor.deps import ensure_fonts
|
|
12
|
+
import text_compositor.project as project_mod
|
|
13
|
+
import time
|
|
14
|
+
from text_compositor import diagnostics
|
|
15
|
+
from text_compositor.api import BuildResult, Session, build_markdown
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.fixture(scope="module")
|
|
19
|
+
def font_dir():
|
|
20
|
+
return ensure_fonts()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@pytest.fixture
|
|
24
|
+
def session(font_dir):
|
|
25
|
+
with Session(font_dir=font_dir) as s:
|
|
26
|
+
yield s
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def write(path, text):
|
|
30
|
+
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
31
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
32
|
+
f.write(text)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def png_bytes(rgb):
|
|
36
|
+
"""8x8の単色PNG(標準ライブラリだけで作る)。"""
|
|
37
|
+
def chunk(kind, data):
|
|
38
|
+
return struct.pack(">I", len(data)) + kind + data + struct.pack(">I", zlib.crc32(kind + data) & 0xFFFFFFFF)
|
|
39
|
+
raw = b"".join(b"\x00" + bytes(rgb) * 8 for _ in range(8))
|
|
40
|
+
return (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", 8, 8, 8, 2, 0, 0, 0))
|
|
41
|
+
+ chunk(b"IDAT", zlib.compress(raw)) + chunk(b"IEND", b""))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def read_bytes(path):
|
|
45
|
+
with open(path, "rb") as f:
|
|
46
|
+
return f.read()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
PLAIN = {"mermaid": False, "plantuml": False, "d2": False}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class TestBuild:
|
|
53
|
+
def test_builds_a_pdf_to_the_requested_path(self, session, tmp_path):
|
|
54
|
+
md = tmp_path / "doc.md"
|
|
55
|
+
write(str(md), "# 見出し\n\n本文。\n\n- a\n- b\n")
|
|
56
|
+
out = tmp_path / "out" / "doc.pdf"
|
|
57
|
+
result = session.build(str(md), str(out), plugins=PLAIN)
|
|
58
|
+
assert isinstance(result, BuildResult)
|
|
59
|
+
assert result.ok and not result.errors
|
|
60
|
+
assert result.pdf_path == str(out)
|
|
61
|
+
assert read_bytes(str(out)).startswith(b"%PDF")
|
|
62
|
+
|
|
63
|
+
def test_does_not_create_an_outputs_directory_next_to_the_markdown(self, session, tmp_path):
|
|
64
|
+
md = tmp_path / "doc.md"
|
|
65
|
+
write(str(md), "# T\n\nbody\n")
|
|
66
|
+
session.build(str(md), str(tmp_path / "x" / "o.pdf"), plugins=PLAIN)
|
|
67
|
+
assert not (tmp_path / "outputs").exists()
|
|
68
|
+
|
|
69
|
+
def test_default_output_is_under_the_work_directory(self, session, tmp_path):
|
|
70
|
+
md = tmp_path / "doc.md"
|
|
71
|
+
write(str(md), "# T\n\nbody\n")
|
|
72
|
+
result = session.build(str(md), plugins=PLAIN)
|
|
73
|
+
assert result.ok
|
|
74
|
+
assert result.pdf_path == str(tmp_path / ".text-compositor" / "preview.pdf")
|
|
75
|
+
assert os.path.exists(result.pdf_path)
|
|
76
|
+
|
|
77
|
+
def test_leaves_no_intermediate_files_and_no_temporary_pdf(self, session, tmp_path):
|
|
78
|
+
md = tmp_path / "doc.md"
|
|
79
|
+
write(str(md), "# T\n\nbody\n")
|
|
80
|
+
out = tmp_path / "out" / "doc.pdf"
|
|
81
|
+
session.build(str(md), str(out), plugins=PLAIN)
|
|
82
|
+
assert sorted(os.listdir(str(tmp_path / ".text-compositor"))) == []
|
|
83
|
+
assert sorted(os.listdir(str(out.parent))) == ["doc.pdf"]
|
|
84
|
+
|
|
85
|
+
def test_keep_temp_keeps_the_generated_typst(self, session, tmp_path):
|
|
86
|
+
md = tmp_path / "doc.md"
|
|
87
|
+
write(str(md), "# T\n\nbody\n")
|
|
88
|
+
session.build(str(md), str(tmp_path / "o.pdf"), plugins=PLAIN, keep_temp=True)
|
|
89
|
+
assert (tmp_path / ".text-compositor" / "temp_build.typ").exists()
|
|
90
|
+
|
|
91
|
+
def test_writes_nothing_to_stdout(self, session, tmp_path, capsys):
|
|
92
|
+
md = tmp_path / "doc.md"
|
|
93
|
+
write(str(md), "# T\n\nbefore <span>x</span> after\n")
|
|
94
|
+
session.build(str(md), str(tmp_path / "o.pdf"), plugins=PLAIN)
|
|
95
|
+
write(str(md), "# T\n\n```dot {width=bogus}\ndigraph { a -> b }\n```\n")
|
|
96
|
+
session.build(str(md), str(tmp_path / "o.pdf"), plugins=PLAIN)
|
|
97
|
+
assert capsys.readouterr().out == ""
|
|
98
|
+
|
|
99
|
+
def test_timings_are_reported(self, session, tmp_path):
|
|
100
|
+
md = tmp_path / "doc.md"
|
|
101
|
+
write(str(md), "# T\n\nbody\n")
|
|
102
|
+
result = session.build(str(md), str(tmp_path / "o.pdf"), plugins=PLAIN)
|
|
103
|
+
assert set(result.timings_ms) >= {"render", "compile", "total"}
|
|
104
|
+
assert result.timings_ms["total"] >= result.timings_ms["compile"] > 0
|
|
105
|
+
|
|
106
|
+
def test_first_markdown_heading_is_kept_as_is(self, session, tmp_path):
|
|
107
|
+
"""プレビューでは、先頭のH1を落とさない(CLIの既定のcover: noneは、先頭のタイトルを落とす)。"""
|
|
108
|
+
md = tmp_path / "doc.md"
|
|
109
|
+
write(str(md), "# 先頭の見出し\n\n本文。\n")
|
|
110
|
+
session.build(str(md), str(tmp_path / "o.pdf"), plugins=PLAIN, keep_temp=True)
|
|
111
|
+
typst = (tmp_path / ".text-compositor" / "temp_build.typ").read_text(encoding="utf-8")
|
|
112
|
+
assert "= 先頭の見出し" in typst
|
|
113
|
+
assert "cover: false" in typst
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class TestOptions:
|
|
117
|
+
@pytest.mark.parametrize("template", ["template", "slide", "paper"])
|
|
118
|
+
def test_bundled_templates(self, session, tmp_path, template):
|
|
119
|
+
md = tmp_path / "doc.md"
|
|
120
|
+
write(str(md), "# T\n\n## 節\n\n本文。\n")
|
|
121
|
+
result = session.build(str(md), str(tmp_path / "o.pdf"), template=template, plugins=PLAIN)
|
|
122
|
+
assert result.ok, result.diagnostics
|
|
123
|
+
|
|
124
|
+
def test_document_and_variables_overrides(self, session, tmp_path):
|
|
125
|
+
md = tmp_path / "doc.md"
|
|
126
|
+
write(str(md), "# T\n\nバージョンは{{VERSION}}。\n")
|
|
127
|
+
result = session.build(str(md), str(tmp_path / "o.pdf"), plugins=PLAIN,
|
|
128
|
+
document={"title": "仕様書", "toc": True}, variables={"VERSION": "1.2"},
|
|
129
|
+
keep_temp=True)
|
|
130
|
+
assert result.ok
|
|
131
|
+
typst = (tmp_path / ".text-compositor" / "temp_build.typ").read_text(encoding="utf-8")
|
|
132
|
+
assert 'title: "仕様書"' in typst and "toc: true" in typst and "バージョンは1.2。" in typst
|
|
133
|
+
|
|
134
|
+
def test_config_override_applies_last(self, session, tmp_path):
|
|
135
|
+
md = tmp_path / "doc.md"
|
|
136
|
+
write(str(md), "# T\n\nbody\n")
|
|
137
|
+
result = session.build(str(md), str(tmp_path / "o.pdf"), plugins=PLAIN, document={"title": "A"},
|
|
138
|
+
config={"document": {"title": "B"}}, keep_temp=True)
|
|
139
|
+
assert result.ok
|
|
140
|
+
assert 'title: "B"' in (tmp_path / ".text-compositor" / "temp_build.typ").read_text(encoding="utf-8")
|
|
141
|
+
|
|
142
|
+
def test_custom_typ_template_by_path(self, session, tmp_path):
|
|
143
|
+
"""独自テンプレートは、Markdownの隣からの相対パスで指定できる。"""
|
|
144
|
+
with open(os.path.join(build.__file__ and os.path.dirname(build.__file__), "templates", "template.typ"),
|
|
145
|
+
encoding="utf-8") as f:
|
|
146
|
+
write(str(tmp_path / "my.typ"), f.read())
|
|
147
|
+
md = tmp_path / "doc.md"
|
|
148
|
+
write(str(md), "# T\n\nbody\n")
|
|
149
|
+
result = session.build(str(md), str(tmp_path / "o.pdf"), template="my.typ", plugins=PLAIN)
|
|
150
|
+
assert result.ok, result.diagnostics
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class TestFailures:
|
|
154
|
+
def test_missing_markdown_is_reported_not_raised(self, session, tmp_path):
|
|
155
|
+
result = session.build(str(tmp_path / "none.md"), str(tmp_path / "o.pdf"))
|
|
156
|
+
assert not result.ok and result.pdf_path is None
|
|
157
|
+
assert result.errors and "not found" in result.errors[0].message
|
|
158
|
+
|
|
159
|
+
def test_unknown_template_is_reported(self, session, tmp_path):
|
|
160
|
+
md = tmp_path / "doc.md"
|
|
161
|
+
write(str(md), "# T\n")
|
|
162
|
+
result = session.build(str(md), str(tmp_path / "o.pdf"), template="no_such_template", plugins=PLAIN)
|
|
163
|
+
assert not result.ok
|
|
164
|
+
assert any("Template not found" in d.message for d in result.errors)
|
|
165
|
+
|
|
166
|
+
def test_compile_error_points_at_the_markdown_line(self, session, tmp_path):
|
|
167
|
+
md = tmp_path / "doc.md"
|
|
168
|
+
write(str(md), "# 見出し\n\n```dot {width=bogus}\ndigraph { a -> b }\n```\n")
|
|
169
|
+
result = session.build(str(md), str(tmp_path / "o.pdf"), plugins=PLAIN)
|
|
170
|
+
assert not result.ok and result.pdf_path is None
|
|
171
|
+
error = result.errors[0]
|
|
172
|
+
assert "unknown variable: bogus" in error.message
|
|
173
|
+
assert error.file == str(md) and error.line == 3
|
|
174
|
+
assert error.detail and "temp_build.typ" in error.detail
|
|
175
|
+
|
|
176
|
+
def test_a_failed_build_keeps_the_previous_pdf(self, session, tmp_path):
|
|
177
|
+
md = tmp_path / "doc.md"
|
|
178
|
+
out = tmp_path / "o.pdf"
|
|
179
|
+
write(str(md), "# T\n\nok\n")
|
|
180
|
+
assert session.build(str(md), str(out), plugins=PLAIN).ok
|
|
181
|
+
before = read_bytes(str(out))
|
|
182
|
+
write(str(md), "# T\n\n```dot {width=bogus}\ndigraph { a -> b }\n```\n")
|
|
183
|
+
assert not session.build(str(md), str(out), plugins=PLAIN).ok
|
|
184
|
+
assert read_bytes(str(out)) == before
|
|
185
|
+
|
|
186
|
+
def test_the_session_recovers_after_a_failure(self, session, tmp_path):
|
|
187
|
+
md = tmp_path / "doc.md"
|
|
188
|
+
write(str(md), "# T\n\n```dot {width=bogus}\ndigraph { a -> b }\n```\n")
|
|
189
|
+
assert not session.build(str(md), str(tmp_path / "o.pdf"), plugins=PLAIN).ok
|
|
190
|
+
write(str(md), "# T\n\nfine\n")
|
|
191
|
+
assert session.build(str(md), str(tmp_path / "o.pdf"), plugins=PLAIN).ok
|
|
192
|
+
|
|
193
|
+
def test_security_violation_is_a_structured_error(self, session, tmp_path):
|
|
194
|
+
md = tmp_path / "doc.md"
|
|
195
|
+
write(str(md), "# T\n\n```typst-exec\n#panic(\"x\")\n```\n")
|
|
196
|
+
result = session.build(str(md), str(tmp_path / "o.pdf"), plugins=PLAIN)
|
|
197
|
+
assert not result.ok
|
|
198
|
+
assert any("typst-exec" in d.message and d.file == str(md) for d in result.errors)
|
|
199
|
+
|
|
200
|
+
def test_unexpected_exceptions_do_not_escape(self, session, tmp_path, monkeypatch):
|
|
201
|
+
md = tmp_path / "doc.md"
|
|
202
|
+
write(str(md), "# T\n")
|
|
203
|
+
|
|
204
|
+
def boom(*a, **k):
|
|
205
|
+
raise RuntimeError("kaboom")
|
|
206
|
+
monkeypatch.setattr(project_mod, "_build_project", boom)
|
|
207
|
+
result = session.build(str(md), str(tmp_path / "o.pdf"))
|
|
208
|
+
assert not result.ok
|
|
209
|
+
assert "kaboom" in result.errors[0].message and "Traceback" in result.errors[0].detail
|
|
210
|
+
|
|
211
|
+
def test_closed_session_reports_an_error(self, font_dir, tmp_path):
|
|
212
|
+
s = Session(font_dir=font_dir)
|
|
213
|
+
s.close()
|
|
214
|
+
md = tmp_path / "doc.md"
|
|
215
|
+
write(str(md), "# T\n")
|
|
216
|
+
assert not s.build(str(md), str(tmp_path / "o.pdf")).ok
|
|
217
|
+
|
|
218
|
+
def test_output_open_elsewhere_is_reported(self, session, tmp_path, monkeypatch):
|
|
219
|
+
md = tmp_path / "doc.md"
|
|
220
|
+
write(str(md), "# T\n\nbody\n")
|
|
221
|
+
|
|
222
|
+
def denied(src, dst):
|
|
223
|
+
raise PermissionError("in use")
|
|
224
|
+
monkeypatch.setattr(os, "replace", denied)
|
|
225
|
+
monkeypatch.setattr(time, "sleep", lambda s: None)
|
|
226
|
+
result = session.build(str(md), str(tmp_path / "o.pdf"), plugins=PLAIN)
|
|
227
|
+
assert not result.ok
|
|
228
|
+
assert any("Cannot write the PDF" in d.message for d in result.errors)
|
|
229
|
+
assert [n for n in os.listdir(str(tmp_path)) if n.startswith(".tmp-")] == []
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class TestWarnings:
|
|
233
|
+
def test_html_warning_carries_file_and_line(self, session, tmp_path):
|
|
234
|
+
md = tmp_path / "doc.md"
|
|
235
|
+
write(str(md), "# T\n\ntext\n\nbefore <span>x</span> after\n")
|
|
236
|
+
result = session.build(str(md), str(tmp_path / "o.pdf"), plugins=PLAIN)
|
|
237
|
+
assert result.ok
|
|
238
|
+
assert result.warnings
|
|
239
|
+
assert all(d.file == str(md) and d.line == 5 for d in result.warnings if "HTML" in d.message)
|
|
240
|
+
|
|
241
|
+
def test_warnings_do_not_leak_between_builds(self, session, tmp_path):
|
|
242
|
+
md = tmp_path / "doc.md"
|
|
243
|
+
write(str(md), "# T\n\nbefore <span>x</span>\n")
|
|
244
|
+
assert session.build(str(md), str(tmp_path / "o.pdf"), plugins=PLAIN).warnings
|
|
245
|
+
write(str(md), "# T\n\nclean\n")
|
|
246
|
+
assert not session.build(str(md), str(tmp_path / "o.pdf"), plugins=PLAIN).warnings
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class TestResidentReuse:
|
|
250
|
+
def test_compiler_is_reused_and_changes_are_reflected(self, session, tmp_path):
|
|
251
|
+
md = tmp_path / "doc.md"
|
|
252
|
+
out = tmp_path / "o.pdf"
|
|
253
|
+
write(str(md), "# T\n\nfirst\n")
|
|
254
|
+
assert session.build(str(md), str(out), plugins=PLAIN).ok
|
|
255
|
+
first = read_bytes(str(out))
|
|
256
|
+
write(str(md), "# T\n\nsecond, and a bit longer text\n")
|
|
257
|
+
assert session.build(str(md), str(out), plugins=PLAIN).ok
|
|
258
|
+
assert read_bytes(str(out)) != first
|
|
259
|
+
assert len(session._compilers) == 1
|
|
260
|
+
|
|
261
|
+
def test_a_replaced_image_is_picked_up(self, session, tmp_path):
|
|
262
|
+
"""同じパスの画像を差し替えても、使い回したコンパイラが古い画像を返さない。"""
|
|
263
|
+
md = tmp_path / "doc.md"
|
|
264
|
+
img = tmp_path / "img.png"
|
|
265
|
+
out = tmp_path / "o.pdf"
|
|
266
|
+
write(str(md), "# T\n\n\n")
|
|
267
|
+
img.write_bytes(png_bytes((255, 0, 0)))
|
|
268
|
+
assert session.build(str(md), str(out), plugins=PLAIN).ok
|
|
269
|
+
red = read_bytes(str(out))
|
|
270
|
+
img.write_bytes(png_bytes((0, 0, 255)))
|
|
271
|
+
assert session.build(str(md), str(out), plugins=PLAIN).ok
|
|
272
|
+
assert read_bytes(str(out)) != red
|
|
273
|
+
|
|
274
|
+
def test_different_documents_in_one_session(self, session, tmp_path):
|
|
275
|
+
a, b = tmp_path / "a" / "a.md", tmp_path / "b" / "b.md"
|
|
276
|
+
write(str(a), "# A\n\nalpha\n")
|
|
277
|
+
write(str(b), "# B\n\nbeta\n")
|
|
278
|
+
assert session.build(str(a), str(tmp_path / "a.pdf"), plugins=PLAIN).ok
|
|
279
|
+
assert session.build(str(b), str(tmp_path / "b.pdf"), plugins=PLAIN).ok
|
|
280
|
+
assert session.build(str(a), str(tmp_path / "a.pdf"), plugins=PLAIN).ok
|
|
281
|
+
assert read_bytes(str(tmp_path / "a.pdf")) != read_bytes(str(tmp_path / "b.pdf"))
|
|
282
|
+
|
|
283
|
+
def test_close_releases_everything_and_is_idempotent(self, font_dir):
|
|
284
|
+
closed = []
|
|
285
|
+
|
|
286
|
+
class FakeBrowser:
|
|
287
|
+
page = None
|
|
288
|
+
|
|
289
|
+
def close(self):
|
|
290
|
+
closed.append(1)
|
|
291
|
+
s = Session(font_dir=font_dir)
|
|
292
|
+
s._mermaid = FakeBrowser()
|
|
293
|
+
s._compilers["k"] = object()
|
|
294
|
+
s.close()
|
|
295
|
+
s.close()
|
|
296
|
+
assert closed == [1, 1] and s._compilers == {}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class TestModuleFunction:
|
|
300
|
+
def test_build_markdown_is_a_one_shot(self, font_dir, tmp_path):
|
|
301
|
+
md = tmp_path / "doc.md"
|
|
302
|
+
write(str(md), "# T\n\nbody\n")
|
|
303
|
+
result = build_markdown(str(md), str(tmp_path / "o.pdf"), plugins=PLAIN)
|
|
304
|
+
assert result.ok and os.path.exists(result.pdf_path)
|
|
305
|
+
|
|
306
|
+
def test_public_names_are_importable_from_the_package(self):
|
|
307
|
+
import text_compositor
|
|
308
|
+
assert text_compositor.Session is Session
|
|
309
|
+
assert text_compositor.build_markdown is build_markdown
|
|
310
|
+
assert text_compositor.BuildResult is BuildResult
|
|
311
|
+
assert text_compositor.Diagnostic is diagnostics.Diagnostic
|
|
312
|
+
with pytest.raises(AttributeError):
|
|
313
|
+
text_compositor.nope
|
|
314
|
+
|
|
315
|
+
def test_plain_import_stays_light(self):
|
|
316
|
+
import subprocess
|
|
317
|
+
code = "import sys, text_compositor; sys.exit(1 if 'text_compositor.build' in sys.modules else 0)"
|
|
318
|
+
repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
319
|
+
env = dict(os.environ, PYTHONPATH=repo_root + os.pathsep + os.environ.get("PYTHONPATH", ""))
|
|
320
|
+
assert subprocess.run([sys.executable, "-c", code], env=env, cwd=repo_root).returncode == 0
|
|
321
|
+
|
|
322
|
+
def test_result_to_dict_is_json_serializable(self, font_dir, tmp_path):
|
|
323
|
+
import json
|
|
324
|
+
md = tmp_path / "doc.md"
|
|
325
|
+
write(str(md), "# T\n\nbefore <span>x</span>\n")
|
|
326
|
+
result = build_markdown(str(md), str(tmp_path / "o.pdf"), plugins=PLAIN)
|
|
327
|
+
data = json.loads(json.dumps(result.to_dict()))
|
|
328
|
+
assert data["ok"] is True and data["pdf"] == str(tmp_path / "o.pdf")
|
|
329
|
+
assert data["diagnostics"][0]["severity"] == "warning"
|
|
330
|
+
assert set(data["timings_ms"]) >= {"render", "compile", "total"}
|
|
@@ -6,7 +6,8 @@ import os
|
|
|
6
6
|
|
|
7
7
|
import pytest
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
from text_compositor.config import _resolve_project_image_path
|
|
10
|
+
from text_compositor.renderer import TypstRenderer
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class TestResolveAsset:
|
|
@@ -14,7 +15,7 @@ class TestResolveAsset:
|
|
|
14
15
|
temp_build.typの置き場所に依存しないtypst_root起点のルート絶対パスへ変換する。"""
|
|
15
16
|
|
|
16
17
|
def _renderer(self, tmp_path):
|
|
17
|
-
renderer =
|
|
18
|
+
renderer = TypstRenderer(line_mapping="off", base_dir=str(tmp_path), typst_root=str(tmp_path))
|
|
18
19
|
renderer.current_dir = str(tmp_path)
|
|
19
20
|
renderer.current_file = str(tmp_path / "doc.md")
|
|
20
21
|
return renderer
|
|
@@ -44,7 +45,7 @@ class TestResolveAsset:
|
|
|
44
45
|
project_dir = tmp_path / "project"
|
|
45
46
|
project_dir.mkdir()
|
|
46
47
|
(project_dir / "a.png").write_bytes(b"\x89PNG")
|
|
47
|
-
renderer =
|
|
48
|
+
renderer = TypstRenderer(line_mapping="off", base_dir=str(project_dir), typst_root=str(tmp_path))
|
|
48
49
|
renderer.current_dir = str(project_dir)
|
|
49
50
|
renderer.current_file = str(project_dir / "doc.md")
|
|
50
51
|
assert renderer._resolve_asset("a.png") == "/project/a.png"
|
|
@@ -54,20 +55,20 @@ class TestResolveProjectImagePath:
|
|
|
54
55
|
"""document.background/logo、chapters[].background/logo(project_dir基準)の解決。"""
|
|
55
56
|
|
|
56
57
|
def test_none_path_returns_none(self, tmp_path):
|
|
57
|
-
assert
|
|
58
|
+
assert _resolve_project_image_path(None, str(tmp_path), str(tmp_path), "Logo") is None
|
|
58
59
|
|
|
59
60
|
def test_existing_file_resolves_to_root_absolute_path(self, tmp_path):
|
|
60
61
|
(tmp_path / "logo.png").write_bytes(b"\x89PNG")
|
|
61
|
-
result =
|
|
62
|
+
result = _resolve_project_image_path("logo.png", str(tmp_path), str(tmp_path), "Logo")
|
|
62
63
|
assert result == "/logo.png"
|
|
63
64
|
|
|
64
65
|
def test_missing_file_exits(self, tmp_path):
|
|
65
66
|
with pytest.raises(SystemExit):
|
|
66
|
-
|
|
67
|
+
_resolve_project_image_path("missing.png", str(tmp_path), str(tmp_path), "Logo")
|
|
67
68
|
|
|
68
69
|
def test_resolves_relative_to_typst_root_not_base_dir(self, tmp_path):
|
|
69
70
|
project_dir = tmp_path / "project"
|
|
70
71
|
project_dir.mkdir()
|
|
71
72
|
(project_dir / "logo.png").write_bytes(b"\x89PNG")
|
|
72
|
-
result =
|
|
73
|
+
result = _resolve_project_image_path("logo.png", str(project_dir), str(tmp_path), "Logo")
|
|
73
74
|
assert result == "/project/logo.png"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""週次の固定値確認(.github/scripts/check-pins.py。#240)のテスト。ネットワークには接続しない。"""
|
|
2
|
+
import importlib.util
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
SCRIPT = os.path.join(os.path.dirname(__file__), "..", ".github", "scripts", "check-pins.py")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture(scope="module")
|
|
11
|
+
def check_pins():
|
|
12
|
+
spec = importlib.util.spec_from_file_location("check_pins", SCRIPT)
|
|
13
|
+
module = importlib.util.module_from_spec(spec)
|
|
14
|
+
spec.loader.exec_module(module)
|
|
15
|
+
return module
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_read_pins_finds_every_pin_in_the_sources(check_pins):
|
|
19
|
+
"""deps.py・build-dist.js・_common.typの書き方を変えたときに、確認が空振りにならないための番人。"""
|
|
20
|
+
pins = check_pins.read_pins()
|
|
21
|
+
missing = [name for name, version in pins.items() if version is None]
|
|
22
|
+
assert missing == []
|
|
23
|
+
for name in ("Mermaid", "Viz.js", "PlantUML", "D2", "Temurin JRE 21", "Noto Sans JP", "組込版Python"):
|
|
24
|
+
assert name in pins
|
|
25
|
+
assert any(name.startswith("Typstパッケージ ") for name in pins)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_read_pins_matches_the_source_constants(check_pins):
|
|
29
|
+
from text_compositor import deps
|
|
30
|
+
pins = check_pins.read_pins()
|
|
31
|
+
assert pins["Viz.js"] == deps.VIZ_JS_VERSION
|
|
32
|
+
assert f"v{pins['D2']}" == deps.D2_RELEASE
|
|
33
|
+
assert f"jdk-{pins['Temurin JRE 21']}" == deps.TEMURIN_JRE_RELEASE
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.mark.parametrize("latest, current, expected", [
|
|
37
|
+
("1.2026.8", "1.2026.6", True),
|
|
38
|
+
("21.0.12.1+1", "21.0.12+8", True), # 桁が多い版は、新しい(ビルド番号の大小より先に比べる)
|
|
39
|
+
("21.0.12+8", "21.0.12.1+1", False),
|
|
40
|
+
("3.14.7", "3.14.7", False),
|
|
41
|
+
("3.14.10", "3.14.9", True), # 文字列ではなく、数として比べる
|
|
42
|
+
("v0.9.1", "0.9.0", True),
|
|
43
|
+
("2.004", "2.004", False),
|
|
44
|
+
])
|
|
45
|
+
def test_is_newer(check_pins, latest, current, expected):
|
|
46
|
+
assert check_pins.is_newer(latest, current) is expected
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_is_major_update(check_pins):
|
|
50
|
+
assert check_pins.is_major_update("12.0.0", "11.16.1")
|
|
51
|
+
assert not check_pins.is_major_update("11.17.0", "11.16.1")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_build_report_counts_updates_and_failures(check_pins):
|
|
55
|
+
pins = {"Mermaid": "11.16.1", "D2": "0.9.0", "PlantUML": "1.2026.6"}
|
|
56
|
+
latest = {
|
|
57
|
+
"Mermaid": ("12.0.0", ""),
|
|
58
|
+
"D2": ("0.9.0", ""),
|
|
59
|
+
"PlantUML": TimeoutError("timed out"),
|
|
60
|
+
}
|
|
61
|
+
report, updates, failures = check_pins.build_report(pins, latest)
|
|
62
|
+
assert (updates, failures) == (1, 1)
|
|
63
|
+
assert "**12.0.0**" in report and "メジャー更新" in report
|
|
64
|
+
assert "確認できず(TimeoutError: timed out)" in report
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_build_report_without_differences(check_pins):
|
|
68
|
+
report, updates, failures = check_pins.build_report({"D2": "0.9.0"}, {"D2": ("0.9.0", "")})
|
|
69
|
+
assert (updates, failures) == (0, 0)
|
|
70
|
+
assert "最新" in report
|
|
@@ -5,6 +5,9 @@ import re
|
|
|
5
5
|
import pytest
|
|
6
6
|
|
|
7
7
|
import text_compositor.build as build
|
|
8
|
+
from text_compositor.changes import _is_up_to_date
|
|
9
|
+
from text_compositor.config import _load_project_config
|
|
10
|
+
from text_compositor.document import _prepare_template
|
|
8
11
|
|
|
9
12
|
TOOL_DIR = os.path.dirname(os.path.abspath(build.__file__))
|
|
10
13
|
TEMPLATES = os.path.join(TOOL_DIR, "templates")
|
|
@@ -59,7 +62,7 @@ class TestPrepareTemplate:
|
|
|
59
62
|
def test_common_is_copied_next_to_template(self, env):
|
|
60
63
|
project_dir, work_dir = env
|
|
61
64
|
config = {"template": {"path": "template"}}
|
|
62
|
-
copy_path, _ =
|
|
65
|
+
copy_path, _ = _prepare_template(config, TOOL_DIR, str(project_dir), str(work_dir), str(project_dir))
|
|
63
66
|
assert os.path.dirname(copy_path) == str(work_dir)
|
|
64
67
|
assert (work_dir / "_common.typ").read_text(encoding="utf-8") == read("_common.typ")
|
|
65
68
|
|
|
@@ -68,7 +71,7 @@ class TestPrepareTemplate:
|
|
|
68
71
|
project_dir, work_dir = env
|
|
69
72
|
write(str(project_dir / "adapter.typ"), '#import "_common.typ": fit-image\n')
|
|
70
73
|
config = {"template": {"path": "adapter.typ"}}
|
|
71
|
-
|
|
74
|
+
_prepare_template(config, TOOL_DIR, str(project_dir), str(work_dir), str(project_dir))
|
|
72
75
|
assert (work_dir / "_common.typ").exists()
|
|
73
76
|
|
|
74
77
|
|
|
@@ -84,12 +87,11 @@ class TestCleanRemovesCommon:
|
|
|
84
87
|
|
|
85
88
|
|
|
86
89
|
class TestIfChangedDependsOnCommon:
|
|
87
|
-
def test_newer_common_makes_output_stale(self, tmp_path
|
|
90
|
+
def test_newer_common_makes_output_stale(self, tmp_path):
|
|
88
91
|
"""同梱の補助関数が更新(pip upgrade等)されたら、入力が同じでも再生成する。"""
|
|
89
92
|
tool = tmp_path / "tool"
|
|
90
93
|
for name in ("templates/template.typ", "templates/_common.typ", "build.py"):
|
|
91
94
|
write(str(tool / name))
|
|
92
|
-
monkeypatch.setattr(build, "__file__", str(tool / "build.py"), raising=False)
|
|
93
95
|
cfg = str(tmp_path / "proj" / "c.yaml")
|
|
94
96
|
write(cfg, "chapters: [a.md]\n")
|
|
95
97
|
pdf = str(tmp_path / "proj" / "outputs" / "System_Specification.pdf")
|
|
@@ -102,8 +104,8 @@ class TestIfChangedDependsOnCommon:
|
|
|
102
104
|
mtime(p, 1000)
|
|
103
105
|
mtime(str(tool / "templates" / "_common.typ"), 1000)
|
|
104
106
|
mtime(pdf, 2000)
|
|
105
|
-
project_dir, config, _ =
|
|
106
|
-
assert
|
|
107
|
+
project_dir, config, _ = _load_project_config(cfg)
|
|
108
|
+
assert _is_up_to_date(str(tool), cfg, project_dir, config)[0] is True
|
|
107
109
|
|
|
108
110
|
mtime(str(tool / "templates" / "_common.typ"), 3000)
|
|
109
|
-
assert
|
|
111
|
+
assert _is_up_to_date(str(tool), cfg, project_dir, config)[0] is False
|