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.
- text_compositor-0.2.0/LICENSE +21 -0
- text_compositor-0.2.0/PKG-INFO +161 -0
- text_compositor-0.2.0/README.md +143 -0
- text_compositor-0.2.0/pyproject.toml +38 -0
- text_compositor-0.2.0/setup.cfg +4 -0
- text_compositor-0.2.0/tests/test_asset_resolution.py +73 -0
- text_compositor-0.2.0/tests/test_config.py +65 -0
- text_compositor-0.2.0/tests/test_csv_table.py +78 -0
- text_compositor-0.2.0/tests/test_front_matter.py +119 -0
- text_compositor-0.2.0/tests/test_layout_blocks.py +142 -0
- text_compositor-0.2.0/tests/test_line_mapping.py +164 -0
- text_compositor-0.2.0/tests/test_markdown_basics.py +190 -0
- text_compositor-0.2.0/text_compositor/__init__.py +0 -0
- text_compositor-0.2.0/text_compositor/build.py +2680 -0
- text_compositor-0.2.0/text_compositor/templates/slide.typ +173 -0
- text_compositor-0.2.0/text_compositor/templates/template.typ +187 -0
- text_compositor-0.2.0/text_compositor.egg-info/PKG-INFO +161 -0
- text_compositor-0.2.0/text_compositor.egg-info/SOURCES.txt +20 -0
- text_compositor-0.2.0/text_compositor.egg-info/dependency_links.txt +1 -0
- text_compositor-0.2.0/text_compositor.egg-info/entry_points.txt +2 -0
- text_compositor-0.2.0/text_compositor.egg-info/requires.txt +8 -0
- text_compositor-0.2.0/text_compositor.egg-info/top_level.txt +1 -0
|
@@ -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,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
|