text-compositor 0.2.0__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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 tokudiro
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,161 @@
1
+ Metadata-Version: 2.4
2
+ Name: text-compositor
3
+ Version: 0.2.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
+ # text-compositor
20
+
21
+ *[日本語版 (Japanese version)](README-ja.md)*
22
+
23
+ A tool that treats multiple text files as independent fragments and deterministically composes them into a single human-readable PDF document. Works equally well for AI-generated drafts refined by humans and for existing, hand-authored documents you want to organize and consolidate.
24
+
25
+ This README covers only the essentials needed to get started quickly. For a full walkthrough of `config.yaml` and how to write your source documents, see the [usage guide](doc/usage/) (which can also be built into a PDF with `cd doc/usage && python ../../build.py`). For detailed design rationale and implementation status, see [doc/spec.md](doc/spec.md). Differences from general-purpose tools like Quarto are summarized in [doc/diff.md](doc/diff.md).
26
+
27
+ ## Features
28
+
29
+ - Combines multiple text files (chapters) into a single PDF
30
+ - Markdown → AST via [markdown-it-py](https://github.com/executablebooks/markdown-it-py) → deterministic conversion to [Typst](https://typst.app/) syntax → PDF output
31
+ - Outputs other text files to the PDF unchanged
32
+ - Separates the tool itself from the documentation (source files), which can live anywhere outside the repository
33
+ - Python-centric with minimal downloads, no dependency on external servers or SaaS (runs the same way on GitHub Actions and locally on Windows/Linux/macOS)
34
+
35
+ Files listed in `chapters` are handled differently depending on their extension:
36
+
37
+ - `.md`/`.markdown`: converted as Markdown
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)
40
+ - `.csv`: rendered as a structured Typst table
41
+ - everything else (plain text, code files, etc.): rendered as plain monospaced text
42
+
43
+ See the [usage guide](doc/usage/) for details.
44
+
45
+ ## Requirements
46
+
47
+ - Python 3.10+
48
+
49
+ > **Windows note — Microsoft Store Python is not supported, even inside `pipx`/`venv`:** If Python was installed from the Microsoft Store, this tool cannot be used with it, full stop — `pipx` or a `venv` does **not** work around this.
50
+ >
51
+ > Windows redirects that Python's writes under `%LOCALAPPDATA%` into a package-private folder, and this redirection follows the Store installation into any `venv`/`pipx` environment created from it (verified by testing), breaking every subprocess-based feature (PlantUML, Mermaid, D2) even though the files appear to exist to Python itself.
52
+ >
53
+ > Before installing this tool, install Python from [python.org](https://www.python.org/downloads/) (or another non-Store distribution such as `winget install Python.Python.3.12`) and use that Python for the steps below. Run `text-compositor --check-env` afterward to confirm the environment is set up correctly.
54
+
55
+ There are two ways to install and run this tool.
56
+
57
+ ### Option A: `pip install` (recommended)
58
+
59
+ ```bash
60
+ pipx install text-compositor
61
+ ```
62
+
63
+ `pipx` installs the tool into an isolated environment and exposes the `text-compositor` command — recommended for most users. If you're developing this tool itself, use a `venv` instead so an editable install (`pip install -e .`) picks up code changes immediately:
64
+
65
+ ```bash
66
+ python -m venv .venv
67
+ .venv/bin/pip install -e . # Windows: .venv\Scripts\pip install -e .
68
+ ```
69
+
70
+ ### Option B: Clone and run directly (no installation)
71
+
72
+ ```bash
73
+ pip install -r requirements.txt
74
+ ```
75
+
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
+
78
+ ### Using Mermaid diagrams (optional)
79
+
80
+ 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:
81
+
82
+ ```bash
83
+ pipx install "text-compositor[mermaid]"
84
+ # or, if already installed: pipx inject text-compositor playwright==1.62.0
85
+ ```
86
+
87
+ With Option B (clone and run directly):
88
+
89
+ ```bash
90
+ pip install playwright==1.62.0
91
+ ```
92
+
93
+ - **A Google Chrome or Microsoft Edge installation already present on your system** (no new download by default; it's auto-detected and reused at build time)
94
+ - The `playwright` package above (used only to connect to the existing browser via CDP; Playwright's own browser-download feature is not used by default)
95
+
96
+ Node.js/npm is not required. At build time, the official single-file Mermaid bundle (`mermaid.min.js`, ~3.4MB) is fetched and loaded into a headless browser to convert diagrams to SVG (the bundle JS itself is cached under an OS-standard user cache directory — e.g. `%LOCALAPPDATA%\text-compositor\Cache` on Windows, `~/.cache/text-compositor` on Linux — and conversion results are cached under `.text-compositor/cache/`; neither is re-fetched afterward). None of this is needed for documents that don't use Mermaid.
97
+
98
+ If no Chrome/Edge is found on the system, the build fails by default. Setting `plugins: { mermaid_auto_download: true }` instead auto-fetches Playwright's own Chromium, but **this download is about 700MB** (this setting exists as a last resort for when no pre-installed browser is available; downloading that much by default is intentionally avoided).
99
+
100
+ ### Using PlantUML diagrams (optional)
101
+
102
+ No extra `config.yaml` settings are needed to use ` ```plantuml ` fences in your source documents (or to specify a `.puml` file directly in `chapters`) — `plugins.plantuml` defaults to `true` and no additional `pip install` is required.
103
+
104
+ - If Java (11+) is available locally, it's reused as-is
105
+ - Otherwise, Eclipse Temurin JRE (Adoptium distribution, ~49.7MB) is auto-fetched and cached by default under the same user cache directory as above. Setting `plugins: { plantuml_auto_download: false }` disables the auto-fetch and fails the build instead
106
+ - GitHub Actions' `ubuntu-latest` ships with Java by default, so no extra download happens on CI
107
+
108
+ The layout engine is the pure-Java Smetana implementation, so no external binary like Graphviz (`dot`) is required. PlantUML itself (MIT edition, ~17.6MB) is cached under the same user cache directory, and conversion results are cached under `.text-compositor/cache/`, same as Mermaid.
109
+
110
+ ### Using D2 diagrams (optional)
111
+
112
+ No extra `config.yaml` settings are needed to use ` ```d2 ` fences in your source documents (or to specify a `.d2` file directly in `chapters`) — `plugins.d2` defaults to `true` and no additional `pip install` is required.
113
+
114
+ - If the `d2` command is available locally, it's reused as-is
115
+ - Otherwise, the official D2 CLI binary (a single Go executable, ~13MB) is auto-fetched and cached by default under the same user cache directory as above. Setting `plugins: { d2_auto_download: false }` disables the auto-fetch and fails the build instead
116
+
117
+ Unlike Java (used by PlantUML) or a browser (used by Mermaid), GitHub Actions' `ubuntu-latest` does not ship with D2 preinstalled, so a `d2` diagram triggers this ~13MB download on every CI run (this project's workflows don't persist the cache directory across separate runs).
118
+
119
+ ## Usage
120
+
121
+ If installed via `pip`/`pipx` (Option A):
122
+
123
+ ```bash
124
+ text-compositor --config <path/to/text-compositor.config.yaml>
125
+ ```
126
+
127
+ If cloned and run directly (Option B):
128
+
129
+ ```bash
130
+ python build.py --config <path/to/text-compositor.config.yaml>
131
+ ```
132
+
133
+ If `--config` is omitted, `text-compositor.config.yaml` (or `.json`) located directly in the current directory is auto-detected.
134
+
135
+ ```bash
136
+ cd my-project/
137
+ python /path/to/text-compositor/build.py
138
+ ```
139
+
140
+ Run `text-compositor --check-env` (or `python build.py --check-env`) to check your environment (dependencies, Typst version, cached assets, Mermaid/PlantUML/D2 prerequisites) without running a build.
141
+
142
+ Other flags control runtime behavior only (not document content, which stays entirely in `config.yaml`): `-q`/`--quiet` suppresses `[Info]`-level logging, `-v`/`--verbose` adds extra detail (which chapter is being processed, cache reuse), and `--keep-temp` keeps the intermediate `temp_build.typ` around after a successful build instead of deleting it (useful for debugging; it's always kept after a failed build).
143
+
144
+ 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.
145
+
146
+ ## Trying the sample
147
+
148
+ ```bash
149
+ cd sample/
150
+ python ../build.py
151
+ ```
152
+
153
+ This generates `sample/System_Specification.pdf`.
154
+
155
+ ## Implementation status
156
+
157
+ The core feature implemented so far is combining multiple files into a single PDF according to the config file specified via `--config` (or auto-detected). Expanding CLI options (e.g., overriding the output path) is still in the planning stage. See [GitHub Issues](https://github.com/tokudiro/text-compositor/issues) for known issues and upcoming plans.
158
+
159
+ ## License
160
+
161
+ [MIT License](LICENSE)
@@ -0,0 +1,143 @@
1
+ # text-compositor
2
+
3
+ *[日本語版 (Japanese version)](README-ja.md)*
4
+
5
+ A tool that treats multiple text files as independent fragments and deterministically composes them into a single human-readable PDF document. Works equally well for AI-generated drafts refined by humans and for existing, hand-authored documents you want to organize and consolidate.
6
+
7
+ This README covers only the essentials needed to get started quickly. For a full walkthrough of `config.yaml` and how to write your source documents, see the [usage guide](doc/usage/) (which can also be built into a PDF with `cd doc/usage && python ../../build.py`). For detailed design rationale and implementation status, see [doc/spec.md](doc/spec.md). Differences from general-purpose tools like Quarto are summarized in [doc/diff.md](doc/diff.md).
8
+
9
+ ## Features
10
+
11
+ - Combines multiple text files (chapters) into a single PDF
12
+ - Markdown → AST via [markdown-it-py](https://github.com/executablebooks/markdown-it-py) → deterministic conversion to [Typst](https://typst.app/) syntax → PDF output
13
+ - Outputs other text files to the PDF unchanged
14
+ - Separates the tool itself from the documentation (source files), which can live anywhere outside the repository
15
+ - Python-centric with minimal downloads, no dependency on external servers or SaaS (runs the same way on GitHub Actions and locally on Windows/Linux/macOS)
16
+
17
+ Files listed in `chapters` are handled differently depending on their extension:
18
+
19
+ - `.md`/`.markdown`: converted as Markdown
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)
22
+ - `.csv`: rendered as a structured Typst table
23
+ - everything else (plain text, code files, etc.): rendered as plain monospaced text
24
+
25
+ See the [usage guide](doc/usage/) for details.
26
+
27
+ ## Requirements
28
+
29
+ - Python 3.10+
30
+
31
+ > **Windows note — Microsoft Store Python is not supported, even inside `pipx`/`venv`:** If Python was installed from the Microsoft Store, this tool cannot be used with it, full stop — `pipx` or a `venv` does **not** work around this.
32
+ >
33
+ > Windows redirects that Python's writes under `%LOCALAPPDATA%` into a package-private folder, and this redirection follows the Store installation into any `venv`/`pipx` environment created from it (verified by testing), breaking every subprocess-based feature (PlantUML, Mermaid, D2) even though the files appear to exist to Python itself.
34
+ >
35
+ > Before installing this tool, install Python from [python.org](https://www.python.org/downloads/) (or another non-Store distribution such as `winget install Python.Python.3.12`) and use that Python for the steps below. Run `text-compositor --check-env` afterward to confirm the environment is set up correctly.
36
+
37
+ There are two ways to install and run this tool.
38
+
39
+ ### Option A: `pip install` (recommended)
40
+
41
+ ```bash
42
+ pipx install text-compositor
43
+ ```
44
+
45
+ `pipx` installs the tool into an isolated environment and exposes the `text-compositor` command — recommended for most users. If you're developing this tool itself, use a `venv` instead so an editable install (`pip install -e .`) picks up code changes immediately:
46
+
47
+ ```bash
48
+ python -m venv .venv
49
+ .venv/bin/pip install -e . # Windows: .venv\Scripts\pip install -e .
50
+ ```
51
+
52
+ ### Option B: Clone and run directly (no installation)
53
+
54
+ ```bash
55
+ pip install -r requirements.txt
56
+ ```
57
+
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.
59
+
60
+ ### Using Mermaid diagrams (optional)
61
+
62
+ 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:
63
+
64
+ ```bash
65
+ pipx install "text-compositor[mermaid]"
66
+ # or, if already installed: pipx inject text-compositor playwright==1.62.0
67
+ ```
68
+
69
+ With Option B (clone and run directly):
70
+
71
+ ```bash
72
+ pip install playwright==1.62.0
73
+ ```
74
+
75
+ - **A Google Chrome or Microsoft Edge installation already present on your system** (no new download by default; it's auto-detected and reused at build time)
76
+ - The `playwright` package above (used only to connect to the existing browser via CDP; Playwright's own browser-download feature is not used by default)
77
+
78
+ Node.js/npm is not required. At build time, the official single-file Mermaid bundle (`mermaid.min.js`, ~3.4MB) is fetched and loaded into a headless browser to convert diagrams to SVG (the bundle JS itself is cached under an OS-standard user cache directory — e.g. `%LOCALAPPDATA%\text-compositor\Cache` on Windows, `~/.cache/text-compositor` on Linux — and conversion results are cached under `.text-compositor/cache/`; neither is re-fetched afterward). None of this is needed for documents that don't use Mermaid.
79
+
80
+ If no Chrome/Edge is found on the system, the build fails by default. Setting `plugins: { mermaid_auto_download: true }` instead auto-fetches Playwright's own Chromium, but **this download is about 700MB** (this setting exists as a last resort for when no pre-installed browser is available; downloading that much by default is intentionally avoided).
81
+
82
+ ### Using PlantUML diagrams (optional)
83
+
84
+ No extra `config.yaml` settings are needed to use ` ```plantuml ` fences in your source documents (or to specify a `.puml` file directly in `chapters`) — `plugins.plantuml` defaults to `true` and no additional `pip install` is required.
85
+
86
+ - If Java (11+) is available locally, it's reused as-is
87
+ - Otherwise, Eclipse Temurin JRE (Adoptium distribution, ~49.7MB) is auto-fetched and cached by default under the same user cache directory as above. Setting `plugins: { plantuml_auto_download: false }` disables the auto-fetch and fails the build instead
88
+ - GitHub Actions' `ubuntu-latest` ships with Java by default, so no extra download happens on CI
89
+
90
+ The layout engine is the pure-Java Smetana implementation, so no external binary like Graphviz (`dot`) is required. PlantUML itself (MIT edition, ~17.6MB) is cached under the same user cache directory, and conversion results are cached under `.text-compositor/cache/`, same as Mermaid.
91
+
92
+ ### Using D2 diagrams (optional)
93
+
94
+ No extra `config.yaml` settings are needed to use ` ```d2 ` fences in your source documents (or to specify a `.d2` file directly in `chapters`) — `plugins.d2` defaults to `true` and no additional `pip install` is required.
95
+
96
+ - If the `d2` command is available locally, it's reused as-is
97
+ - Otherwise, the official D2 CLI binary (a single Go executable, ~13MB) is auto-fetched and cached by default under the same user cache directory as above. Setting `plugins: { d2_auto_download: false }` disables the auto-fetch and fails the build instead
98
+
99
+ Unlike Java (used by PlantUML) or a browser (used by Mermaid), GitHub Actions' `ubuntu-latest` does not ship with D2 preinstalled, so a `d2` diagram triggers this ~13MB download on every CI run (this project's workflows don't persist the cache directory across separate runs).
100
+
101
+ ## Usage
102
+
103
+ If installed via `pip`/`pipx` (Option A):
104
+
105
+ ```bash
106
+ text-compositor --config <path/to/text-compositor.config.yaml>
107
+ ```
108
+
109
+ If cloned and run directly (Option B):
110
+
111
+ ```bash
112
+ python build.py --config <path/to/text-compositor.config.yaml>
113
+ ```
114
+
115
+ If `--config` is omitted, `text-compositor.config.yaml` (or `.json`) located directly in the current directory is auto-detected.
116
+
117
+ ```bash
118
+ cd my-project/
119
+ python /path/to/text-compositor/build.py
120
+ ```
121
+
122
+ Run `text-compositor --check-env` (or `python build.py --check-env`) to check your environment (dependencies, Typst version, cached assets, Mermaid/PlantUML/D2 prerequisites) without running a build.
123
+
124
+ Other flags control runtime behavior only (not document content, which stays entirely in `config.yaml`): `-q`/`--quiet` suppresses `[Info]`-level logging, `-v`/`--verbose` adds extra detail (which chapter is being processed, cache reuse), and `--keep-temp` keeps the intermediate `temp_build.typ` around after a successful build instead of deleting it (useful for debugging; it's always kept after a failed build).
125
+
126
+ 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.
127
+
128
+ ## Trying the sample
129
+
130
+ ```bash
131
+ cd sample/
132
+ python ../build.py
133
+ ```
134
+
135
+ This generates `sample/System_Specification.pdf`.
136
+
137
+ ## Implementation status
138
+
139
+ The core feature implemented so far is combining multiple files into a single PDF according to the config file specified via `--config` (or auto-detected). Expanding CLI options (e.g., overriding the output path) is still in the planning stage. See [GitHub Issues](https://github.com/tokudiro/text-compositor/issues) for known issues and upcoming plans.
140
+
141
+ ## License
142
+
143
+ [MIT License](LICENSE)
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "text-compositor"
7
+ version = "0.2.0"
8
+ description = "A tool for composing multiple text files into a PDF document."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ dependencies = [
13
+ "typst==0.15.0",
14
+ "markdown-it-py==3.0.0",
15
+ "mdit-py-plugins==0.6.1",
16
+ "PyYAML==6.0.2",
17
+ "platformdirs==4.3.6",
18
+ ]
19
+
20
+ # plugins.mermaid: true(既定)を使う場合のみ必要(11章)。
21
+ # `pip install text-compositor[mermaid]` でまとめて導入できる。
22
+ [project.optional-dependencies]
23
+ mermaid = ["playwright==1.62.0"]
24
+
25
+ [project.urls]
26
+ Repository = "https://github.com/tokudiro/text-compositor"
27
+
28
+ # `text-compositor --config ...` コマンドを提供する(#111)。
29
+ # 「クローンして直接叩く」既存の使い方(`python build.py --config ...`)も
30
+ # トップレベルの build.py ラッパー経由で引き続き使える。
31
+ [project.scripts]
32
+ text-compositor = "text_compositor.build:build"
33
+
34
+ [tool.setuptools]
35
+ packages = ["text_compositor", "text_compositor.templates"]
36
+
37
+ [tool.setuptools.package-data]
38
+ text_compositor = ["templates/*.typ"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,73 @@
1
+ """画像パス解決(_resolve_asset、_resolve_project_image_path)のリグレッションテスト(#96)。
2
+
3
+ いずれも仕様9章のFail-fast方針どおり、画像欠損時はフォールバックせず即エラー終了する。
4
+ """
5
+ import os
6
+
7
+ import pytest
8
+
9
+ import text_compositor.build as build
10
+
11
+
12
+ class TestResolveAsset:
13
+ """TypstRenderer._resolve_asset(): Markdownファイル基準の相対パスを、
14
+ temp_build.typの置き場所に依存しないtypst_root起点のルート絶対パスへ変換する。"""
15
+
16
+ def _renderer(self, tmp_path):
17
+ renderer = build.TypstRenderer(line_mapping="off", base_dir=str(tmp_path), typst_root=str(tmp_path))
18
+ renderer.current_dir = str(tmp_path)
19
+ renderer.current_file = str(tmp_path / "doc.md")
20
+ return renderer
21
+
22
+ def test_relative_path_resolves_to_root_absolute_path(self, tmp_path):
23
+ (tmp_path / "images").mkdir()
24
+ (tmp_path / "images" / "a.png").write_bytes(b"\x89PNG")
25
+ renderer = self._renderer(tmp_path)
26
+ assert renderer._resolve_asset("images/a.png") == "/images/a.png"
27
+
28
+ def test_root_absolute_path_is_passed_through_unresolved(self, tmp_path):
29
+ renderer = self._renderer(tmp_path)
30
+ assert renderer._resolve_asset("/already/absolute.png") == "/already/absolute.png"
31
+
32
+ def test_url_is_passed_through_unresolved(self, tmp_path):
33
+ renderer = self._renderer(tmp_path)
34
+ assert renderer._resolve_asset("https://example.com/a.png") == "https://example.com/a.png"
35
+
36
+ def test_missing_file_exits(self, tmp_path):
37
+ renderer = self._renderer(tmp_path)
38
+ with pytest.raises(SystemExit):
39
+ renderer._resolve_asset("missing.png")
40
+
41
+ def test_resolves_relative_to_typst_root_not_base_dir(self, tmp_path):
42
+ """base_dir(原稿側)とtypst_root(--rootに相当)が異なる場合、ルート絶対パスは
43
+ typst_root基準で組み立てる(5章)。"""
44
+ project_dir = tmp_path / "project"
45
+ project_dir.mkdir()
46
+ (project_dir / "a.png").write_bytes(b"\x89PNG")
47
+ renderer = build.TypstRenderer(line_mapping="off", base_dir=str(project_dir), typst_root=str(tmp_path))
48
+ renderer.current_dir = str(project_dir)
49
+ renderer.current_file = str(project_dir / "doc.md")
50
+ assert renderer._resolve_asset("a.png") == "/project/a.png"
51
+
52
+
53
+ class TestResolveProjectImagePath:
54
+ """document.background/logo、chapters[].background/logo(project_dir基準)の解決。"""
55
+
56
+ def test_none_path_returns_none(self, tmp_path):
57
+ assert build._resolve_project_image_path(None, str(tmp_path), str(tmp_path), "Logo") is None
58
+
59
+ def test_existing_file_resolves_to_root_absolute_path(self, tmp_path):
60
+ (tmp_path / "logo.png").write_bytes(b"\x89PNG")
61
+ result = build._resolve_project_image_path("logo.png", str(tmp_path), str(tmp_path), "Logo")
62
+ assert result == "/logo.png"
63
+
64
+ def test_missing_file_exits(self, tmp_path):
65
+ with pytest.raises(SystemExit):
66
+ build._resolve_project_image_path("missing.png", str(tmp_path), str(tmp_path), "Logo")
67
+
68
+ def test_resolves_relative_to_typst_root_not_base_dir(self, tmp_path):
69
+ project_dir = tmp_path / "project"
70
+ project_dir.mkdir()
71
+ (project_dir / "logo.png").write_bytes(b"\x89PNG")
72
+ result = build._resolve_project_image_path("logo.png", str(project_dir), str(tmp_path), "Logo")
73
+ assert result == "/project/logo.png"
@@ -0,0 +1,65 @@
1
+ """config.yamlの読み込み・デフォルト値マージ・chaptersエントリ解析のリグレッションテスト(#96)。"""
2
+ import pytest
3
+
4
+ import text_compositor.build as build
5
+
6
+
7
+ class TestDeepUpdate:
8
+ def test_merges_flat_keys(self):
9
+ d = {"a": 1, "b": 2}
10
+ result = build.deep_update(d, {"b": 20, "c": 3})
11
+ assert result == {"a": 1, "b": 20, "c": 3}
12
+
13
+ def test_merges_nested_dict_without_dropping_untouched_keys(self):
14
+ d = {"document": {"title": "t", "author": "a"}}
15
+ result = build.deep_update(d, {"document": {"title": "new"}})
16
+ assert result == {"document": {"title": "new", "author": "a"}}
17
+
18
+ def test_overwriting_nested_dict_with_non_dict_replaces_it(self):
19
+ d = {"a": {"b": 1}}
20
+ result = build.deep_update(d, {"a": "scalar"})
21
+ assert result == {"a": "scalar"}
22
+
23
+ def test_mutates_and_returns_same_object(self):
24
+ d = {"a": 1}
25
+ assert build.deep_update(d, {"b": 2}) is d
26
+
27
+
28
+ class TestDefaultConfig:
29
+ def test_has_expected_top_level_sections(self):
30
+ config = build.default_config()
31
+ assert set(config.keys()) == {"document", "output", "template", "inputs"}
32
+
33
+ def test_default_line_mapping_is_block(self):
34
+ assert build.default_config()["document"]["diagnostics"]["line_mapping"] == "block"
35
+
36
+ def test_default_template_is_named_template(self):
37
+ assert build.default_config()["template"]["path"] == "template"
38
+
39
+ def test_returns_a_fresh_dict_each_call(self):
40
+ """既定値を呼び出し元が書き換えても、他の呼び出しに影響しないこと(共有可変状態のバグ防止)。"""
41
+ a = build.default_config()
42
+ a["document"]["title"] = "changed"
43
+ b = build.default_config()
44
+ assert b["document"]["title"] != "changed"
45
+
46
+
47
+ class TestParseChapterEntry:
48
+ def test_string_entry_is_a_plain_file_chapter(self):
49
+ assert build._parse_chapter_entry("a.md") == ("a.md", {}, "file")
50
+
51
+ def test_dict_with_file_key(self):
52
+ ch = {"file": "a.md", "landscape": True}
53
+ assert build._parse_chapter_entry(ch) == ("a.md", ch, "file")
54
+
55
+ def test_dict_with_aggregate_key(self):
56
+ ch = {"aggregate": "data.yaml"}
57
+ assert build._parse_chapter_entry(ch) == ("data.yaml", ch, "aggregate")
58
+
59
+ def test_dict_without_file_or_aggregate_exits(self):
60
+ with pytest.raises(SystemExit):
61
+ build._parse_chapter_entry({"landscape": True})
62
+
63
+ def test_non_string_non_dict_entry_exits(self):
64
+ with pytest.raises(SystemExit):
65
+ build._parse_chapter_entry(123)
@@ -0,0 +1,78 @@
1
+ """CSVファイルをTypstテーブルへ変換する機能(#36)のリグレッションテスト。"""
2
+ import pytest
3
+
4
+ import text_compositor.build as build
5
+
6
+
7
+ def render_csv(csv_text, filepath="data.csv", table_header_style=None):
8
+ renderer = build.TypstRenderer(line_mapping="off")
9
+ if table_header_style is not None:
10
+ renderer.table_header_style = table_header_style
11
+ return renderer.render_chapter(csv_text, filepath=filepath)
12
+
13
+
14
+ class TestBasicConversion:
15
+ def test_header_and_rows(self):
16
+ out = render_csv("name,age\nAlice,30\nBob,25\n")
17
+ assert out == (
18
+ "#table(\n"
19
+ " columns: 2,\n"
20
+ " table.header(\n"
21
+ " [name], [age], \n"
22
+ " ),\n"
23
+ " [Alice], [30], \n"
24
+ " [Bob], [25], \n"
25
+ " \n"
26
+ ")\n\n"
27
+ )
28
+
29
+ def test_header_only_no_data_rows(self):
30
+ out = render_csv("a,b\n")
31
+ assert out == "#table(\n columns: 2,\n table.header(\n [a], [b], \n ),\n \n)\n\n"
32
+
33
+
34
+ class TestRfc4180Quoting:
35
+ def test_quoted_comma_in_cell(self):
36
+ out = render_csv('a,b\n"1, 2",3\n')
37
+ assert "[1, 2]" in out
38
+
39
+ def test_quoted_newline_in_cell(self):
40
+ out = render_csv('a\n"line1\nline2"\n')
41
+ assert "[line1\nline2]" in out
42
+
43
+ def test_escaped_quote_char(self):
44
+ out = render_csv('a\n"she said ""hi"""\n')
45
+ assert '[she said "hi"]' in out
46
+
47
+
48
+ class TestEscaping:
49
+ def test_typst_special_characters_are_escaped(self):
50
+ out = render_csv("a\n#tag $x@y\n")
51
+ assert "[\\#tag \\$x\\@y]" in out
52
+
53
+ def test_leading_block_marker_is_escaped(self):
54
+ out = render_csv("a\n= heading like\n- list like\n")
55
+ assert "[\\= heading like]" in out
56
+ assert "[\\- list like]" in out
57
+
58
+ def test_content_is_not_parsed_as_markdown(self):
59
+ out = render_csv("a\n**not bold**\n")
60
+ assert "[\\*\\*not bold\\*\\*]" in out
61
+
62
+
63
+ class TestFailFast:
64
+ def test_empty_file_exits(self):
65
+ with pytest.raises(SystemExit):
66
+ render_csv("")
67
+
68
+ def test_uneven_row_exits(self):
69
+ with pytest.raises(SystemExit):
70
+ render_csv("a,b\n1,2\n3\n")
71
+
72
+
73
+ class TestHeaderStyle:
74
+ def test_table_header_style_applies(self):
75
+ out = render_csv("a,b\n1,2\n", table_header_style={"bold": True, "background": "red"})
76
+ assert "fill: (col, row) => if row == 0 { red } else { none }" in out
77
+ assert "[#strong[a]]" in out
78
+ assert "[1], [2]" in out