text-compositor 0.4.2__tar.gz → 0.4.4__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.4.2/text_compositor.egg-info → text_compositor-0.4.4}/PKG-INFO +15 -3
- text_compositor-0.4.2/PKG-INFO → text_compositor-0.4.4/README.md +12 -20
- {text_compositor-0.4.2 → text_compositor-0.4.4}/pyproject.toml +8 -1
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_bundled_diagram_tools.py +8 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_bundled_typst_assets.py +5 -3
- text_compositor-0.4.4/tests/test_bytefield_render.py +164 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_cetz_render.py +26 -4
- text_compositor-0.4.4/tests/test_diagram_alt.py +105 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_distribution.py +6 -2
- text_compositor-0.4.4/tests/test_fonts.py +228 -0
- text_compositor-0.4.4/tests/test_highlight.py +139 -0
- text_compositor-0.4.4/tests/test_html_lang.py +43 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_html_output.py +11 -11
- text_compositor-0.4.4/tests/test_pdf_fonts.py +100 -0
- text_compositor-0.4.4/tests/test_pdf_metadata.py +81 -0
- text_compositor-0.4.4/tests/test_wavedrom_render.py +163 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_worker.py +105 -4
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/api.py +3 -0
- text_compositor-0.4.4/text_compositor/bytefield_render.py +39 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/cetz_render.py +18 -5
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/chapters.py +2 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/compiler.py +4 -3
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/config.py +7 -2
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/deps.py +42 -0
- text_compositor-0.4.4/text_compositor/fonts.py +225 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/graphviz_render.py +5 -3
- text_compositor-0.4.4/text_compositor/highlight.py +78 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/host_renderers.py +24 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/html_output.py +77 -19
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/math_render.py +2 -1
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/mermaid.py +45 -2
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/pikchr_render.py +2 -1
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/project.py +8 -2
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/renderer.py +14 -3
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/renderer_diagrams.py +163 -14
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/renderer_layout.py +9 -8
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/renderer_tokens.py +3 -2
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/templates/_common.typ +9 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/templates/paper.typ +9 -1
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/templates/slide.typ +9 -1
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/templates/template.typ +9 -1
- text_compositor-0.4.4/text_compositor/wavedrom_render.py +49 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/worker.py +23 -2
- text_compositor-0.4.2/README.md → text_compositor-0.4.4/text_compositor.egg-info/PKG-INFO +32 -2
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor.egg-info/SOURCES.txt +12 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor.egg-info/requires.txt +3 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/LICENSE +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/setup.cfg +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_aggregate_table.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_api.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_asset_resolution.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_check_pins.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_cli_single_markdown.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_common_template.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_config.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_config_key_validation.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_csv_table.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_diagnostics.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_diagram_cache_key.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_diagram_trim.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_doc_badges.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_download.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_emphasis_lint.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_front_matter.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_graphviz_render.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_html_line_numbers.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_incremental.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_java_encoding.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_layout_blocks.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_line_mapping.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_markdown_basics.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_math_render.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_mermaid_resident.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_pandoc_compat.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_paper_template.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_pikchr_render.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_plantuml_batch.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_renderer_structure.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_revision_history.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_script_references.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_sections.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_soft_break_hints.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_structurizr_render.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_svg_trim.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_table_cell_style.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_variables.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_vega_render.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_viewer_version.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/tests/test_watch.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/__init__.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/build.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/changes.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/diagnostics.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/document.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/emphasis_lint.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/env_check.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/log.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/renderer_inline.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/renderer_tables.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/svg_trim.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/typst_literal.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/typst_runtime.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor/vega_render.py +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor.egg-info/dependency_links.txt +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/text_compositor.egg-info/entry_points.txt +0 -0
- {text_compositor-0.4.2 → text_compositor-0.4.4}/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.4.
|
|
3
|
+
Version: 0.4.4
|
|
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
|
|
@@ -14,6 +14,8 @@ Requires-Dist: PyYAML==6.0.3
|
|
|
14
14
|
Requires-Dist: platformdirs==4.11.14
|
|
15
15
|
Provides-Extra: mermaid
|
|
16
16
|
Requires-Dist: playwright==1.63.0; extra == "mermaid"
|
|
17
|
+
Provides-Extra: highlight
|
|
18
|
+
Requires-Dist: Pygments==2.21.0; extra == "highlight"
|
|
17
19
|
Dynamic: license-file
|
|
18
20
|
|
|
19
21
|
# text-compositor
|
|
@@ -79,7 +81,7 @@ No Typst compiler binary is bundled; it's obtained from the `typst` package (PyP
|
|
|
79
81
|
|
|
80
82
|
### Keeping up to date
|
|
81
83
|
|
|
82
|
-
Security and bug fixes ship as new releases. There is no automatic update, so use the latest release: `pipx upgrade text-compositor` (or `pip install -U text-compositor`). For the [Obunzu Viewer](viewer/README.md), download the latest ZIP from the [Releases page](https://github.com/tokudiro/text-compositor/releases/latest). The Viewer bundles its own Python, so a new ZIP also updates that Python and the other bundled parts. A guide to using Obunzu (in Japanese) is in [doc/obunzu-guide/](doc/obunzu-guide/); its PDF is attached to each release.
|
|
84
|
+
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 Markdown Viewer](viewer/README.md), download the latest ZIP from the [Releases page](https://github.com/tokudiro/text-compositor/releases/latest). The Viewer bundles its own Python, so a new ZIP also updates that Python and the other bundled parts. A guide to using Obunzu (in Japanese) is in [doc/obunzu-guide/](doc/obunzu-guide/); its PDF is attached to each release.
|
|
83
85
|
|
|
84
86
|
### Using Mermaid diagrams (optional)
|
|
85
87
|
|
|
@@ -175,7 +177,7 @@ with Session() as session: # reuses the Mermaid browser and the Typst compiler
|
|
|
175
177
|
print(result.ok, result.pdf_path, [d.message for d in result.diagnostics])
|
|
176
178
|
```
|
|
177
179
|
|
|
178
|
-
`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, Structurizr, Graphviz, Pikchr, CeTZ, Fletcher, timeliney, Vega-Lite, Vega, `svg`) become images next to it. `typst-exec` and raw HTML are not supported yet and are shown as code with a warning. Graphviz, Pikchr, CeTZ, Fletcher and timeliney are drawn with the Typst packages `diagraph`, `kip`, `cetz`, `fletcher` and `timeliney`, the same as in the PDF (Graphviz's `shape=record` and a graph-level `label` cannot be drawn and produce a warning).
|
|
180
|
+
`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, Structurizr, Graphviz, Pikchr, CeTZ, Fletcher, timeliney, finite, Vega-Lite, Vega, WaveDrom, Bytefield, `svg`) become images next to it. `typst-exec` and raw HTML are not supported yet and are shown as code with a warning. Graphviz, Pikchr, CeTZ, Fletcher and timeliney are drawn with the Typst packages `diagraph`, `kip`, `cetz`, `fletcher` and `timeliney`, the same as in the PDF (Graphviz's `shape=record` and a graph-level `label` cannot be drawn and produce a warning).
|
|
179
181
|
|
|
180
182
|
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.
|
|
181
183
|
|
|
@@ -192,6 +194,16 @@ This generates `sample/SampleDocument.pdf`. Two more samples show other template
|
|
|
192
194
|
|
|
193
195
|
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.
|
|
194
196
|
|
|
197
|
+
## Policy for adding diagram notations
|
|
198
|
+
|
|
199
|
+
This tool is free for commercial use, runs locally, and puts no restrictions on the people who use it (see chapter 2 of [doc/spec.md](doc/spec.md)). A diagram tool or notation is therefore **not** added when it has one of these:
|
|
200
|
+
|
|
201
|
+
- **A strong license constraint.** Strong copyleft such as GPL or AGPL, a watermark or credit that must stay in the output, a fee, or a usage limit.
|
|
202
|
+
- **A heavy runtime.** It needs Docker or a similar container, or a large runtime that most users do not have.
|
|
203
|
+
- **An external service.** It sends the manuscript to a server.
|
|
204
|
+
|
|
205
|
+
Candidates that fail these checks are closed as "not planned" on [GitHub Issues](https://github.com/tokudiro/text-compositor/issues), with the reason recorded.
|
|
206
|
+
|
|
195
207
|
## License
|
|
196
208
|
|
|
197
209
|
[MIT License](LICENSE)
|
|
@@ -1,21 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: text-compositor
|
|
3
|
-
Version: 0.4.2
|
|
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==4.2.0
|
|
12
|
-
Requires-Dist: mdit-py-plugins==0.6.1
|
|
13
|
-
Requires-Dist: PyYAML==6.0.3
|
|
14
|
-
Requires-Dist: platformdirs==4.11.14
|
|
15
|
-
Provides-Extra: mermaid
|
|
16
|
-
Requires-Dist: playwright==1.63.0; extra == "mermaid"
|
|
17
|
-
Dynamic: license-file
|
|
18
|
-
|
|
19
1
|
# text-compositor
|
|
20
2
|
|
|
21
3
|
*[日本語版 (Japanese version)](README-ja.md)*
|
|
@@ -79,7 +61,7 @@ No Typst compiler binary is bundled; it's obtained from the `typst` package (PyP
|
|
|
79
61
|
|
|
80
62
|
### Keeping up to date
|
|
81
63
|
|
|
82
|
-
Security and bug fixes ship as new releases. There is no automatic update, so use the latest release: `pipx upgrade text-compositor` (or `pip install -U text-compositor`). For the [Obunzu Viewer](viewer/README.md), download the latest ZIP from the [Releases page](https://github.com/tokudiro/text-compositor/releases/latest). The Viewer bundles its own Python, so a new ZIP also updates that Python and the other bundled parts. A guide to using Obunzu (in Japanese) is in [doc/obunzu-guide/](doc/obunzu-guide/); its PDF is attached to each release.
|
|
64
|
+
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 Markdown Viewer](viewer/README.md), download the latest ZIP from the [Releases page](https://github.com/tokudiro/text-compositor/releases/latest). The Viewer bundles its own Python, so a new ZIP also updates that Python and the other bundled parts. A guide to using Obunzu (in Japanese) is in [doc/obunzu-guide/](doc/obunzu-guide/); its PDF is attached to each release.
|
|
83
65
|
|
|
84
66
|
### Using Mermaid diagrams (optional)
|
|
85
67
|
|
|
@@ -175,7 +157,7 @@ with Session() as session: # reuses the Mermaid browser and the Typst compiler
|
|
|
175
157
|
print(result.ok, result.pdf_path, [d.message for d in result.diagnostics])
|
|
176
158
|
```
|
|
177
159
|
|
|
178
|
-
`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, Structurizr, Graphviz, Pikchr, CeTZ, Fletcher, timeliney, Vega-Lite, Vega, `svg`) become images next to it. `typst-exec` and raw HTML are not supported yet and are shown as code with a warning. Graphviz, Pikchr, CeTZ, Fletcher and timeliney are drawn with the Typst packages `diagraph`, `kip`, `cetz`, `fletcher` and `timeliney`, the same as in the PDF (Graphviz's `shape=record` and a graph-level `label` cannot be drawn and produce a warning).
|
|
160
|
+
`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, Structurizr, Graphviz, Pikchr, CeTZ, Fletcher, timeliney, finite, Vega-Lite, Vega, WaveDrom, Bytefield, `svg`) become images next to it. `typst-exec` and raw HTML are not supported yet and are shown as code with a warning. Graphviz, Pikchr, CeTZ, Fletcher and timeliney are drawn with the Typst packages `diagraph`, `kip`, `cetz`, `fletcher` and `timeliney`, the same as in the PDF (Graphviz's `shape=record` and a graph-level `label` cannot be drawn and produce a warning).
|
|
179
161
|
|
|
180
162
|
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.
|
|
181
163
|
|
|
@@ -192,6 +174,16 @@ This generates `sample/SampleDocument.pdf`. Two more samples show other template
|
|
|
192
174
|
|
|
193
175
|
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.
|
|
194
176
|
|
|
177
|
+
## Policy for adding diagram notations
|
|
178
|
+
|
|
179
|
+
This tool is free for commercial use, runs locally, and puts no restrictions on the people who use it (see chapter 2 of [doc/spec.md](doc/spec.md)). A diagram tool or notation is therefore **not** added when it has one of these:
|
|
180
|
+
|
|
181
|
+
- **A strong license constraint.** Strong copyleft such as GPL or AGPL, a watermark or credit that must stay in the output, a fee, or a usage limit.
|
|
182
|
+
- **A heavy runtime.** It needs Docker or a similar container, or a large runtime that most users do not have.
|
|
183
|
+
- **An external service.** It sends the manuscript to a server.
|
|
184
|
+
|
|
185
|
+
Candidates that fail these checks are closed as "not planned" on [GitHub Issues](https://github.com/tokudiro/text-compositor/issues), with the reason recorded.
|
|
186
|
+
|
|
195
187
|
## License
|
|
196
188
|
|
|
197
189
|
[MIT License](LICENSE)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "text-compositor"
|
|
7
|
-
version = "0.4.
|
|
7
|
+
version = "0.4.4"
|
|
8
8
|
description = "A tool for composing multiple text files into a PDF document."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -21,6 +21,8 @@ dependencies = [
|
|
|
21
21
|
# `pip install text-compositor[mermaid]` でまとめて導入できる。
|
|
22
22
|
[project.optional-dependencies]
|
|
23
23
|
mermaid = ["playwright==1.63.0"]
|
|
24
|
+
# HTML出力のシンタックスハイライト(#218)に使う。無くても動く(等幅の素の表示になる)。
|
|
25
|
+
highlight = ["Pygments==2.21.0"]
|
|
24
26
|
|
|
25
27
|
[project.urls]
|
|
26
28
|
Repository = "https://github.com/tokudiro/text-compositor"
|
|
@@ -36,3 +38,8 @@ packages = ["text_compositor", "text_compositor.templates"]
|
|
|
36
38
|
|
|
37
39
|
[tool.setuptools.package-data]
|
|
38
40
|
text_compositor = ["templates/*.typ"]
|
|
41
|
+
|
|
42
|
+
# 引数なしのpytestが、tests/以外(git管理外のtemp/に置いた他パッケージのテストなど)を
|
|
43
|
+
# 収集して失敗しないようにする(#385)。CIは`pytest tests/`と明示している。
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
testpaths = ["tests"]
|
|
@@ -149,3 +149,11 @@ def test_build_dist_pins_the_same_java_plantuml_d2_structurizr_and_mermaid_as_th
|
|
|
149
149
|
assert deps_mod.VEGA_JS_SHA256 in script
|
|
150
150
|
assert deps_mod.VEGA_LITE_JS_URL in script
|
|
151
151
|
assert deps_mod.VEGA_LITE_JS_SHA256 in script
|
|
152
|
+
# WaveDrom(#392)
|
|
153
|
+
assert deps_mod.WAVEDROM_JS_URL in script
|
|
154
|
+
assert deps_mod.WAVEDROM_JS_SHA256 in script
|
|
155
|
+
assert deps_mod.WAVEDROM_SKIN_URL in script
|
|
156
|
+
assert deps_mod.WAVEDROM_SKIN_SHA256 in script
|
|
157
|
+
# Bytefield-svg(#300)
|
|
158
|
+
assert deps_mod.BYTEFIELD_JS_URL in script
|
|
159
|
+
assert deps_mod.BYTEFIELD_JS_SHA256 in script
|
|
@@ -86,10 +86,12 @@ def test_build_dist_pins_the_same_fonts_and_packages_as_the_tool():
|
|
|
86
86
|
used.add(("kip", pikchr_render.KIP_VERSION)) # Pikchr(#213)は、テンプレートではなく、生成コードが読み込む
|
|
87
87
|
# CeTZ・Fletcher(#236)・timeliney(#294)も、生成コードが読み込む。Fletcher 0.5.8が内部で使うcetz 0.3.4・oxifmt、
|
|
88
88
|
# cetz 0.5.2が使うoxifmt 1.0.0、timeliney 0.4.0が使うcetz 0.4.1は、ネットワークなしで動くように、推移的な依存として、
|
|
89
|
-
# 同梱する(同梱の不足は、viewer/scripts/check-dist.js
|
|
89
|
+
# 同梱する(同梱の不足は、viewer/scripts/check-dist.jsの、オフラインでのコンパイルが検出する)。
|
|
90
|
+
# finite(#292)も、生成コードが読み込む。finite 0.5.1が使うcetz 0.4.2・diagraph-layout 0.0.1・t4t 0.4.3も、同梱する
|
|
90
91
|
used |= {("cetz", cetz_render.CETZ_VERSION), ("fletcher", cetz_render.FLETCHER_VERSION),
|
|
91
|
-
("timeliney", cetz_render.TIMELINEY_VERSION),
|
|
92
|
-
("cetz", "0.3.4"), ("cetz", cetz_render.TIMELINEY_CETZ_VERSION),
|
|
92
|
+
("timeliney", cetz_render.TIMELINEY_VERSION), ("finite", cetz_render.FINITE_VERSION),
|
|
93
|
+
("cetz", "0.3.4"), ("cetz", cetz_render.TIMELINEY_CETZ_VERSION), ("cetz", cetz_render.FINITE_CETZ_VERSION),
|
|
94
|
+
("diagraph-layout", "0.0.1"), ("t4t", "0.4.3"),
|
|
93
95
|
("oxifmt", "0.2.1"), ("oxifmt", "1.0.0")}
|
|
94
96
|
bundled = set(re.findall(r"name: '([a-z0-9-]+)', version: '([\d.]+)'", script))
|
|
95
97
|
assert used and used == bundled, (used, bundled)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""```bytefieldフェンス(#300)のテスト。
|
|
2
|
+
|
|
3
|
+
入力の検査(bytefield_render.parse_spec)は、ブラウザなしで確かめる。描画は、偽のページで、キャッシュ・無効化・エラーの扱いを確かめ、
|
|
4
|
+
実ブラウザでの描画だけは、ブラウザがある環境でだけ実行する。"""
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from text_compositor import bytefield_render, host_renderers
|
|
11
|
+
from text_compositor.api import render_html
|
|
12
|
+
from text_compositor.deps import find_system_browser
|
|
13
|
+
from text_compositor.renderer import TypstRenderer
|
|
14
|
+
|
|
15
|
+
SOURCE = '(draw-column-headers)\n(draw-box "Source Port" {:span 16})\n(draw-box "Destination Port" {:span 16})'
|
|
16
|
+
FENCE = "```bytefield\n" + SOURCE + "\n```\n"
|
|
17
|
+
FAKE_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"></svg>'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestParseSpec:
|
|
21
|
+
def test_a_source_is_returned_as_is(self):
|
|
22
|
+
assert bytefield_render.parse_spec(SOURCE) == SOURCE
|
|
23
|
+
|
|
24
|
+
@pytest.mark.parametrize("code", ["", " \n "])
|
|
25
|
+
def test_an_empty_source_is_rejected(self, code):
|
|
26
|
+
# Bytefield-svgは、空の入力を、エラーにせず、空の図にする。そのため、描く前に、止める。
|
|
27
|
+
with pytest.raises(bytefield_render.SpecError, match="empty"):
|
|
28
|
+
bytefield_render.parse_spec(code)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def convert_html(tmp_path, markdown, plugins=None, name="doc.md"):
|
|
32
|
+
md = tmp_path / name
|
|
33
|
+
md.write_text(markdown, encoding="utf-8")
|
|
34
|
+
result = render_html(str(md), plugins={"mermaid": False, "plantuml": False, "d2": False, **(plugins or {})})
|
|
35
|
+
html = open(result.html_path, encoding="utf-8").read() if result.ok else ""
|
|
36
|
+
return result, html
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class FakeBytefieldPage:
|
|
40
|
+
def __init__(self, svg=FAKE_SVG, error=None):
|
|
41
|
+
self.svg, self.error, self.calls = svg, error, []
|
|
42
|
+
|
|
43
|
+
def evaluate(self, script, args):
|
|
44
|
+
self.calls.append((script, args))
|
|
45
|
+
if self.error:
|
|
46
|
+
raise self.error
|
|
47
|
+
return self.svg
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.fixture
|
|
51
|
+
def fake_page(monkeypatch):
|
|
52
|
+
page = FakeBytefieldPage()
|
|
53
|
+
monkeypatch.setattr("text_compositor.mermaid.MermaidBrowser.ensure_bytefield_page", lambda self, *a: page)
|
|
54
|
+
return page
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TestRenderWithFakePage:
|
|
58
|
+
def test_a_fence_becomes_an_svg_image_and_is_cached(self, tmp_path, fake_page):
|
|
59
|
+
result, html = convert_html(tmp_path, "# T\n\n" + FENCE)
|
|
60
|
+
assert result.ok, result.diagnostics
|
|
61
|
+
m = re.search(r'<img src="([^"]+\.svg)" alt="bytefield diagram">', html)
|
|
62
|
+
assert m
|
|
63
|
+
assert os.path.basename(m.group(1)).startswith("bytefield_")
|
|
64
|
+
assert len(fake_page.calls) == 1
|
|
65
|
+
assert fake_page.calls[0] == (bytefield_render.RENDER_SCRIPT, [SOURCE + chr(10)]) # フェンスの中身は、末尾の改行つき
|
|
66
|
+
second, _ = convert_html(tmp_path, "# T\n\n" + FENCE)
|
|
67
|
+
assert second.ok and len(fake_page.calls) == 1 # キャッシュを使い、再描画しない
|
|
68
|
+
|
|
69
|
+
def test_the_pdf_side_embeds_the_svg_with_the_requested_width(self, tmp_path, fake_page):
|
|
70
|
+
renderer = TypstRenderer(str(tmp_path), typst_root=str(tmp_path), line_mapping="off")
|
|
71
|
+
code = renderer._render_diagram_fence("bytefield", SOURCE, width="50%")
|
|
72
|
+
assert re.search(r'#image\("/.text-compositor/cache/bytefield_[0-9a-f]+\.svg", width: 50%\)', code)
|
|
73
|
+
|
|
74
|
+
def test_a_disabled_plugin_leaves_the_source_as_code(self, tmp_path, fake_page):
|
|
75
|
+
result, html = convert_html(tmp_path, "# T\n\n" + FENCE, plugins={"bytefield": False})
|
|
76
|
+
assert result.ok and "language-bytefield" in html and "<img" not in html
|
|
77
|
+
assert fake_page.calls == []
|
|
78
|
+
|
|
79
|
+
def test_an_empty_fence_fails_before_touching_the_browser(self, tmp_path, fake_page):
|
|
80
|
+
result, _ = convert_html(tmp_path, "# T\n\n```bytefield\n \n```\n")
|
|
81
|
+
assert not result.ok
|
|
82
|
+
error = [d for d in result.diagnostics if d.severity == "error"][0]
|
|
83
|
+
assert error.message == "Bytefield diagram failed to render" and error.line == 3
|
|
84
|
+
assert "empty" in error.detail
|
|
85
|
+
assert fake_page.calls == []
|
|
86
|
+
|
|
87
|
+
def test_a_rendering_failure_keeps_the_message_with_the_position(self, tmp_path, monkeypatch):
|
|
88
|
+
page = FakeBytefieldPage(error=RuntimeError(
|
|
89
|
+
"Page.evaluate: Error: draw-box called with span larger than remaining columns in row [at line 2, column 1]\n at eval (<anonymous>:1:1)"))
|
|
90
|
+
monkeypatch.setattr("text_compositor.mermaid.MermaidBrowser.ensure_bytefield_page", lambda self, *a: page)
|
|
91
|
+
result, _ = convert_html(tmp_path, "# T\n\n" + FENCE)
|
|
92
|
+
error = [d for d in result.diagnostics if d.severity == "error"][0]
|
|
93
|
+
assert error.detail == "draw-box called with span larger than remaining columns in row [at line 2, column 1]"
|
|
94
|
+
|
|
95
|
+
def test_the_bundled_js_dir_env_is_used_without_downloading(self, tmp_path, monkeypatch):
|
|
96
|
+
from text_compositor import deps
|
|
97
|
+
(tmp_path / "lib.js").write_text("/* b */", encoding="utf-8")
|
|
98
|
+
monkeypatch.setenv(deps.BYTEFIELD_JS_DIR_ENV, str(tmp_path))
|
|
99
|
+
monkeypatch.setattr(deps, "_download", lambda *a, **k: pytest.fail("must not download"))
|
|
100
|
+
assert deps.ensure_bytefield_js() == str(tmp_path / "lib.js")
|
|
101
|
+
|
|
102
|
+
def test_a_host_renderer_is_used_instead_of_the_browser(self, tmp_path, monkeypatch):
|
|
103
|
+
calls = []
|
|
104
|
+
|
|
105
|
+
def host(diagram_id, source, script, js):
|
|
106
|
+
calls.append((diagram_id, source, script, js))
|
|
107
|
+
return FAKE_SVG
|
|
108
|
+
monkeypatch.setattr(host_renderers, "_bytefield_host_renderer", host)
|
|
109
|
+
monkeypatch.setattr("text_compositor.renderer_diagrams.ensure_bytefield_js", lambda: "lib.js")
|
|
110
|
+
result, _ = convert_html(tmp_path, "# T\n\n" + FENCE) # ensure_bytefield_pageは、呼ばれない
|
|
111
|
+
assert result.ok, result.diagnostics
|
|
112
|
+
assert len(calls) == 1 and calls[0][1:] == (SOURCE + chr(10), bytefield_render.RENDER_SCRIPT, {"bytefield": "lib.js"})
|
|
113
|
+
assert calls[0][0].startswith("bytefield-")
|
|
114
|
+
|
|
115
|
+
def test_plugins_bytefield_is_an_allowed_config_key(self):
|
|
116
|
+
from text_compositor.config import _ALLOWED_PLUGINS_KEYS
|
|
117
|
+
assert "bytefield" in _ALLOWED_PLUGINS_KEYS
|
|
118
|
+
|
|
119
|
+
def test_a_bytefield_fence_works_inside_a_layout_block(self, tmp_path, fake_page):
|
|
120
|
+
md = "# T\n\n::: layout-right\n本文。\n\n" + FENCE + "\n:::\n"
|
|
121
|
+
result, html = convert_html(tmp_path, md)
|
|
122
|
+
assert result.ok, result.diagnostics
|
|
123
|
+
assert 'alt="bytefield diagram"' in html
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _has_browser():
|
|
127
|
+
try:
|
|
128
|
+
import playwright # noqa: F401
|
|
129
|
+
except ImportError:
|
|
130
|
+
return False
|
|
131
|
+
return find_system_browser() is not None
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@pytest.mark.skipif(not _has_browser(), reason="needs playwright and a system Chrome/Edge")
|
|
135
|
+
class TestRealBrowser:
|
|
136
|
+
"""実ブラウザで描画する(初回は、Bytefield-svgのJSを取得するため、ネットワークが要る)。"""
|
|
137
|
+
|
|
138
|
+
def test_a_packet_diagram_renders_to_svg(self, tmp_path):
|
|
139
|
+
md = tmp_path / "doc.md"
|
|
140
|
+
md.write_text("# T\n\n```bytefield\n(def boxes-per-row 32)\n(draw-box \"Source Port\" {:span 16})\n"
|
|
141
|
+
"(draw-box \"Destination Port\" {:span 16})\n(draw-gap \"Data\")\n(draw-bottom)\n```\n", encoding="utf-8")
|
|
142
|
+
result = render_html(str(md), plugins={"mermaid": False, "plantuml": False, "d2": False})
|
|
143
|
+
assert result.ok, result.diagnostics
|
|
144
|
+
html = open(result.html_path, encoding="utf-8").read()
|
|
145
|
+
src = re.search(r'<img src="([^"]+\.svg)"', html).group(1)
|
|
146
|
+
svg = open(os.path.join(os.path.dirname(result.html_path), src), encoding="utf-8").read()
|
|
147
|
+
assert "<svg" in svg and "Source Port" in svg and "Destination Port" in svg
|
|
148
|
+
|
|
149
|
+
def test_a_drawing_error_is_reported_with_the_position(self, tmp_path):
|
|
150
|
+
md = tmp_path / "doc.md"
|
|
151
|
+
md.write_text("# T\n\n```bytefield\n(draw-box \"A\" {:span 99})\n```\n", encoding="utf-8")
|
|
152
|
+
result = render_html(str(md), plugins={"mermaid": False, "plantuml": False, "d2": False})
|
|
153
|
+
assert not result.ok
|
|
154
|
+
error = [d for d in result.diagnostics if d.severity == "error"][0]
|
|
155
|
+
assert "span larger than remaining columns" in error.detail and "[at line 1, column 1]" in error.detail
|
|
156
|
+
|
|
157
|
+
def test_javascript_interop_is_not_available_to_the_source(self, tmp_path):
|
|
158
|
+
"""記述から、ブラウザの機能(通信など)には触れない。インタプリタが、`js/`の記号を解決しない。"""
|
|
159
|
+
md = tmp_path / "doc.md"
|
|
160
|
+
md.write_text("# T\n\n```bytefield\n(js/fetch \"http://example.invalid/\")\n```\n", encoding="utf-8")
|
|
161
|
+
result = render_html(str(md), plugins={"mermaid": False, "plantuml": False, "d2": False})
|
|
162
|
+
assert not result.ok
|
|
163
|
+
error = [d for d in result.diagnostics if d.severity == "error"][0]
|
|
164
|
+
assert "Could not resolve symbol: js/fetch" in error.detail
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""CeTZ・Fletcher・timeliney(Typstのパッケージ。cetz_render.py・図のフェンスの変換、#236・#294)のテスト。"""
|
|
1
|
+
"""CeTZ・Fletcher・timeliney・finite(Typstのパッケージ。cetz_render.py・図のフェンスの変換、#236・#294・#292)のテスト。"""
|
|
2
2
|
import os
|
|
3
3
|
import re
|
|
4
4
|
|
|
@@ -15,6 +15,7 @@ PLAIN = {"mermaid": False, "plantuml": False, "d2": False, "graphviz": False}
|
|
|
15
15
|
CETZ = 'circle((0, 0), radius: 1)\nline((0, 0), (2, 1))\ncontent((1, 0.5), [日本語])'
|
|
16
16
|
FLETCHER = 'node((0, 0), [開始]), edge("->"), node((1, 0), [終了])'
|
|
17
17
|
TIMELINEY = 'headerline(group(([*2024*], 2)))\ntaskgroup(title: [日本語], {\n task("A", (0, 1))\n})'
|
|
18
|
+
FINITE = '(\n q0: (q0: "0", q1: "1"),\n q1: (q0: "1"),\n),\ninitial: "q0",\nfinal: ("q1",)'
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
def _read(relative):
|
|
@@ -30,6 +31,8 @@ def test_the_versions_are_the_same_in_the_source_and_the_bundle():
|
|
|
30
31
|
assert ("fletcher", cetz_render.FLETCHER_VERSION) in bundled
|
|
31
32
|
assert ("timeliney", cetz_render.TIMELINEY_VERSION) in bundled
|
|
32
33
|
assert ("cetz", cetz_render.TIMELINEY_CETZ_VERSION) in bundled
|
|
34
|
+
assert ("finite", cetz_render.FINITE_VERSION) in bundled
|
|
35
|
+
assert ("cetz", cetz_render.FINITE_CETZ_VERSION) in bundled
|
|
33
36
|
|
|
34
37
|
|
|
35
38
|
class TestCheckCode:
|
|
@@ -80,6 +83,13 @@ class TestRender:
|
|
|
80
83
|
def test_timeliney_becomes_an_svg(self):
|
|
81
84
|
assert render_svg("timeliney", TIMELINEY).startswith("<svg")
|
|
82
85
|
|
|
86
|
+
def test_finite_becomes_an_svg(self):
|
|
87
|
+
assert render_svg("finite", FINITE).startswith("<svg")
|
|
88
|
+
|
|
89
|
+
def test_finite_scope_has_the_layouts(self):
|
|
90
|
+
"""`layout.circular`など、finiteの配置を使える(Typstの`layout`関数は、eval内でだけ隠される)。"""
|
|
91
|
+
assert render_svg("finite", FINITE + ",\nlayout: layout.circular").startswith("<svg")
|
|
92
|
+
|
|
83
93
|
def test_the_compiler_is_reused_and_a_later_figure_is_not_a_leftover(self):
|
|
84
94
|
first = render_svg("cetz", "circle((0, 0))")
|
|
85
95
|
assert render_svg("cetz", "circle((0, 0))\nline((0, 0), (3, 3))") != first
|
|
@@ -104,6 +114,7 @@ class TestRender:
|
|
|
104
114
|
("fletcher", 'node((0, 0), read("x"))'),
|
|
105
115
|
("fletcher", 'node((0, 0), std.plugin("x"))'),
|
|
106
116
|
("timeliney", 'task(str(read("x")), (0, 1))'),
|
|
117
|
+
("finite", '(q0: (q1: str(read("x")))),\ninitial: "q0"'),
|
|
107
118
|
])
|
|
108
119
|
def test_functions_that_read_files_are_denied_with_a_clear_message(self, kind, code):
|
|
109
120
|
with pytest.raises(FigureRenderError) as e:
|
|
@@ -127,7 +138,7 @@ def _html(tmp_path, text, name="doc.md", plugins=PLAIN):
|
|
|
127
138
|
|
|
128
139
|
|
|
129
140
|
class TestHtmlOutput:
|
|
130
|
-
@pytest.mark.parametrize("lang, code", [("cetz", CETZ), ("fletcher", FLETCHER), ("timeliney", TIMELINEY)])
|
|
141
|
+
@pytest.mark.parametrize("lang, code", [("cetz", CETZ), ("fletcher", FLETCHER), ("timeliney", TIMELINEY), ("finite", FINITE)])
|
|
131
142
|
def test_a_fence_becomes_a_cached_svg_img(self, tmp_path, lang, code):
|
|
132
143
|
result, html = _html(tmp_path, f'a\n\n```{lang} {{width=300pt}}\n{code}\n```\n')
|
|
133
144
|
assert result.ok and not result.warnings
|
|
@@ -154,7 +165,7 @@ class TestHtmlOutput:
|
|
|
154
165
|
assert not result.ok and "is not available" in result.errors[0].detail
|
|
155
166
|
assert "TOP-SECRET" not in html
|
|
156
167
|
|
|
157
|
-
@pytest.mark.parametrize("lang", ["cetz", "fletcher", "timeliney"])
|
|
168
|
+
@pytest.mark.parametrize("lang", ["cetz", "fletcher", "timeliney", "finite"])
|
|
158
169
|
def test_the_plugin_setting_falls_back_to_a_code_block_without_a_warning(self, tmp_path, lang):
|
|
159
170
|
result, html = _html(tmp_path, f'```{lang}\nnode((0, 0), [x])\n```\n', plugins={**PLAIN, lang: False})
|
|
160
171
|
assert result.ok and f'class="language-{lang}"' in html and not result.warnings
|
|
@@ -187,6 +198,17 @@ class TestPdfCode:
|
|
|
187
198
|
assert 'timeliney.timeline(show-grid: true, { eval(' in code
|
|
188
199
|
assert 'eval("headerline(group' in code
|
|
189
200
|
|
|
201
|
+
def test_finite_wraps_the_code_in_automaton(self):
|
|
202
|
+
code = self._render(f'```finite\n{FINITE}\n```\n')
|
|
203
|
+
assert f'import "@preview/finite:{cetz_render.FINITE_VERSION}"' in code
|
|
204
|
+
assert 'eval("automaton(\\n" + ' in code
|
|
205
|
+
assert "layout: finite.layout" in code
|
|
206
|
+
assert "measure(fig)" in code # cetz・fletcherと同じ、自動縮小の経路
|
|
207
|
+
|
|
208
|
+
def test_a_size_attribute_on_finite_scales_to_that_size(self):
|
|
209
|
+
code = self._render(f'```finite {{width=50%}}\n{FINITE}\n```\n')
|
|
210
|
+
assert "50 * 1% * size.width" in code
|
|
211
|
+
|
|
190
212
|
def test_the_file_reading_functions_are_denied_in_the_generated_code(self):
|
|
191
213
|
code = self._render('```cetz\ncircle((0, 0))\n```\n')
|
|
192
214
|
assert '"read", "json"' in code and "std: denied" in code
|
|
@@ -228,7 +250,7 @@ def test_a_pdf_with_both_figures_is_built(tmp_path):
|
|
|
228
250
|
md.write_text(f'# 図\n\n```cetz\n{CETZ}\n```\n\n```fletcher\n{FLETCHER}\n```\n\n```timeliney\n{TIMELINEY}\n```\n\n'
|
|
229
251
|
'```cetz {width=5cm}\ncircle((0, 0))\n```\n\n```fletcher {width=50% height=3cm}\n'
|
|
230
252
|
'node((0, 0), [A]), edge("->"), node((1, 0), [B])\n```\n\n```timeliney {width=10cm}\n'
|
|
231
|
-
f'{TIMELINEY}\n```\n', encoding="utf-8")
|
|
253
|
+
f'{TIMELINEY}\n```\n\n```finite\n{FINITE}\n```\n\n```finite {{width=6cm}}\n{FINITE}\n```\n', encoding="utf-8")
|
|
232
254
|
result = build_markdown(str(md), str(tmp_path / "doc.pdf"))
|
|
233
255
|
assert result.ok, [(e.message, e.detail) for e in result.errors]
|
|
234
256
|
assert (tmp_path / "doc.pdf").read_bytes().startswith(b"%PDF")
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""図の代替テキスト(フェンスの`alt="..."`、#398)のテスト。
|
|
2
|
+
|
|
3
|
+
ブラウザの要らない`svg`フェンスで、HTML・PDF(Typstのコード)の両方を確かめる。"""
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from text_compositor.api import render_html
|
|
9
|
+
from text_compositor.renderer import TypstRenderer
|
|
10
|
+
|
|
11
|
+
SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"><rect width="10" height="10"/></svg>'
|
|
12
|
+
PLAIN = {"mermaid": False, "plantuml": False, "d2": False}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def renderer(tmp_path):
|
|
17
|
+
return TypstRenderer(str(tmp_path), typst_root=str(tmp_path), line_mapping="off")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestParse:
|
|
21
|
+
@pytest.mark.parametrize("attrs,expected", [
|
|
22
|
+
('{alt="ログインの流れ"}', "ログインの流れ"),
|
|
23
|
+
("{alt='single quoted'}", "single quoted"),
|
|
24
|
+
("{alt=word}", "word"),
|
|
25
|
+
('{alt=""}', ""),
|
|
26
|
+
('{width=50% alt="a b c" trim=true}', "a b c"),
|
|
27
|
+
('{width=50%}', None),
|
|
28
|
+
('', None),
|
|
29
|
+
(None, None),
|
|
30
|
+
])
|
|
31
|
+
def test_the_alt_value_is_read(self, renderer, attrs, expected):
|
|
32
|
+
assert renderer._parse_alt_attr(attrs) == expected
|
|
33
|
+
|
|
34
|
+
def test_a_word_alt_inside_the_value_is_not_taken_for_another_attribute(self, renderer):
|
|
35
|
+
attrs = '{alt="width=3 and height=4 trim=true" width=50%}'
|
|
36
|
+
assert renderer._parse_alt_attr(attrs) == "width=3 and height=4 trim=true"
|
|
37
|
+
assert renderer._parse_size_attrs(attrs) == ("50%", None)
|
|
38
|
+
assert renderer._parse_trim_attr(attrs) is None
|
|
39
|
+
|
|
40
|
+
def test_sizes_and_trim_still_work_next_to_alt(self, renderer):
|
|
41
|
+
attrs = '{alt="x" width=40% height=8cm trim=false}'
|
|
42
|
+
assert renderer._parse_size_attrs(attrs) == ("40%", "8cm")
|
|
43
|
+
assert renderer._parse_trim_attr(attrs) is False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class TestHtml:
|
|
47
|
+
def convert(self, tmp_path, attrs):
|
|
48
|
+
md = tmp_path / "doc.md"
|
|
49
|
+
md.write_text(f"# T\n\n```svg {attrs}\n{SVG}\n```\n", encoding="utf-8")
|
|
50
|
+
result = render_html(str(md), plugins=PLAIN)
|
|
51
|
+
assert result.ok, result.diagnostics
|
|
52
|
+
return open(result.html_path, encoding="utf-8").read()
|
|
53
|
+
|
|
54
|
+
def test_the_alt_becomes_the_img_alt(self, tmp_path):
|
|
55
|
+
html = self.convert(tmp_path, '{alt="ログインの流れ"}')
|
|
56
|
+
assert 'alt="ログインの流れ"' in html and 'alt="svg diagram"' not in html
|
|
57
|
+
|
|
58
|
+
def test_without_alt_the_kind_is_kept(self, tmp_path):
|
|
59
|
+
assert 'alt="svg diagram"' in self.convert(tmp_path, "")
|
|
60
|
+
|
|
61
|
+
def test_an_empty_alt_marks_the_figure_as_decorative(self, tmp_path):
|
|
62
|
+
assert 'alt=""' in self.convert(tmp_path, '{alt=""}')
|
|
63
|
+
|
|
64
|
+
def test_the_alt_is_escaped(self, tmp_path):
|
|
65
|
+
html = self.convert(tmp_path, "{alt='a <b> & \"q\"'}")
|
|
66
|
+
assert 'alt="a <b> & "q""' in html and "<b>" not in html
|
|
67
|
+
|
|
68
|
+
def test_alt_works_in_a_layout_block(self, tmp_path):
|
|
69
|
+
md = tmp_path / "doc.md"
|
|
70
|
+
md.write_text(f"# T\n\n::: layout-right\n本文。\n\n```svg {{alt=\"横並びの図\"}}\n{SVG}\n```\n\n:::\n", encoding="utf-8")
|
|
71
|
+
result = render_html(str(md), plugins=PLAIN)
|
|
72
|
+
assert result.ok, result.diagnostics
|
|
73
|
+
assert 'alt="横並びの図"' in open(result.html_path, encoding="utf-8").read()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TestPdfCode:
|
|
77
|
+
def test_the_alt_is_set_on_the_image_with_fit_image(self, renderer):
|
|
78
|
+
code = renderer._render_diagram_fence("svg", SVG, alt="流れ図")
|
|
79
|
+
assert re.search(r'#align\(center\)\[#\{ set image\(alt: "流れ図"\); fit-image\("[^"]+"\) \}\]', code)
|
|
80
|
+
|
|
81
|
+
def test_the_alt_is_set_on_the_image_with_an_explicit_size(self, renderer):
|
|
82
|
+
code = renderer._render_diagram_fence("svg", SVG, width="50%", alt="x")
|
|
83
|
+
assert re.search(r'set image\(alt: "x"\); image\("[^"]+", width: 50%\)', code)
|
|
84
|
+
|
|
85
|
+
def test_quotes_and_backslashes_are_escaped_for_typst(self, renderer):
|
|
86
|
+
code = renderer._render_diagram_fence("svg", SVG, alt='a "q" \\ b')
|
|
87
|
+
assert 'alt: "a \\"q\\" \\\\ b"' in code
|
|
88
|
+
|
|
89
|
+
def test_an_empty_alt_is_passed_as_an_empty_string(self, renderer):
|
|
90
|
+
assert 'set image(alt: "")' in renderer._render_diagram_fence("svg", SVG, alt="")
|
|
91
|
+
|
|
92
|
+
def test_without_alt_the_output_is_unchanged(self, renderer):
|
|
93
|
+
code = renderer._render_diagram_fence("svg", SVG)
|
|
94
|
+
assert "alt" not in code and re.search(r'#align\(center\)\[#fit-image\("[^"]+"\)\]', code)
|
|
95
|
+
|
|
96
|
+
def test_the_alt_does_not_leak_into_the_next_diagram(self, renderer):
|
|
97
|
+
renderer._render_diagram_fence("svg", SVG, alt="first")
|
|
98
|
+
assert renderer._pending_alt is None
|
|
99
|
+
assert "alt" not in renderer._render_diagram_fence("svg", SVG)
|
|
100
|
+
|
|
101
|
+
def test_the_alt_is_cleared_even_when_rendering_fails(self, renderer, monkeypatch):
|
|
102
|
+
monkeypatch.setattr(TypstRenderer, "_render_svg", lambda *a, **k: (_ for _ in ()).throw(RuntimeError("boom")))
|
|
103
|
+
with pytest.raises(RuntimeError):
|
|
104
|
+
renderer._render_diagram_fence("svg", SVG, alt="x")
|
|
105
|
+
assert renderer._pending_alt is None
|
|
@@ -72,12 +72,16 @@ def test_the_distribution_requirements_follow_pyproject():
|
|
|
72
72
|
dependencies = re.search(r"^dependencies = \[(.*?)^\]", pyproject, re.S | re.M).group(1)
|
|
73
73
|
project = _pins(dependencies.splitlines())
|
|
74
74
|
dist = _pins((ROOT / "viewer" / "dist-requirements.txt").read_text(encoding="utf-8").splitlines())
|
|
75
|
+
# 配布物には、必須の依存に加えて、シンタックスハイライト用のPygments(`highlight`エクストラ。#218)も入れる
|
|
76
|
+
highlight = _pins(re.search(r"^highlight = \[(.*?)\]", pyproject, re.S | re.M).group(1).split(","))
|
|
77
|
+
assert highlight == {"pygments": highlight["pygments"]}
|
|
78
|
+
expected = {**project, **highlight}
|
|
75
79
|
|
|
76
80
|
assert dist, "no pinned requirements found"
|
|
77
81
|
for name, version in dist.items():
|
|
78
|
-
assert
|
|
82
|
+
assert expected.get(name) == version, f"{name}: dist {version} != pyproject {expected.get(name)}"
|
|
79
83
|
# typstは、Typstを通す処理のために、同梱する(#263。#237で決めた)。Mermaid用のplaywrightは、pyprojectの必須の依存ではなく、
|
|
80
84
|
# ElectronのChromiumで描画するため、同梱しない
|
|
81
85
|
assert "typst" in project and "typst" in dist
|
|
82
86
|
assert "playwright" not in dist
|
|
83
|
-
assert set(
|
|
87
|
+
assert set(expected) == set(dist)
|