text-compositor 0.4.0__tar.gz → 0.4.2__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.0 → text_compositor-0.4.2}/PKG-INFO +5 -5
- {text_compositor-0.4.0 → text_compositor-0.4.2}/README.md +3 -3
- {text_compositor-0.4.0 → text_compositor-0.4.2}/pyproject.toml +2 -2
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_asset_resolution.py +42 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_bundled_diagram_tools.py +6 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_bundled_typst_assets.py +6 -3
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_cetz_render.py +27 -5
- text_compositor-0.4.2/tests/test_cli_single_markdown.py +129 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_emphasis_lint.py +1 -1
- text_compositor-0.4.2/tests/test_html_line_numbers.py +94 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_html_output.py +42 -5
- text_compositor-0.4.2/tests/test_math_render.py +164 -0
- text_compositor-0.4.2/tests/test_vega_render.py +366 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_worker.py +81 -6
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/api.py +10 -3
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/build.py +67 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/cetz_render.py +55 -7
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/config.py +1 -1
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/deps.py +46 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/document.py +2 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/env_check.py +1 -1
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/host_renderers.py +13 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/html_output.py +127 -13
- text_compositor-0.4.2/text_compositor/math_render.py +78 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/mermaid.py +61 -24
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/project.py +4 -1
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/renderer.py +19 -6
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/renderer_diagrams.py +181 -9
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/renderer_inline.py +3 -1
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/renderer_layout.py +7 -7
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/renderer_tokens.py +16 -1
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/svg_trim.py +5 -3
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/templates/_common.typ +3 -0
- text_compositor-0.4.2/text_compositor/vega_render.py +140 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/worker.py +21 -6
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor.egg-info/PKG-INFO +5 -5
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor.egg-info/SOURCES.txt +6 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor.egg-info/requires.txt +1 -1
- {text_compositor-0.4.0 → text_compositor-0.4.2}/LICENSE +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/setup.cfg +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_aggregate_table.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_api.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_check_pins.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_common_template.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_config.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_config_key_validation.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_csv_table.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_diagnostics.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_diagram_cache_key.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_diagram_trim.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_distribution.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_doc_badges.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_download.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_front_matter.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_graphviz_render.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_incremental.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_java_encoding.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_layout_blocks.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_line_mapping.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_markdown_basics.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_mermaid_resident.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_pandoc_compat.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_paper_template.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_pikchr_render.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_plantuml_batch.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_renderer_structure.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_revision_history.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_script_references.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_sections.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_soft_break_hints.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_structurizr_render.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_svg_trim.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_table_cell_style.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_variables.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_viewer_version.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/tests/test_watch.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/__init__.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/changes.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/chapters.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/compiler.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/diagnostics.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/emphasis_lint.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/graphviz_render.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/log.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/pikchr_render.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/renderer_tables.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/templates/paper.typ +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/templates/slide.typ +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/templates/template.typ +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/typst_literal.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor/typst_runtime.py +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor.egg-info/dependency_links.txt +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/text_compositor.egg-info/entry_points.txt +0 -0
- {text_compositor-0.4.0 → text_compositor-0.4.2}/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.2
|
|
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
|
|
@@ -11,7 +11,7 @@ Requires-Dist: typst==0.15.0
|
|
|
11
11
|
Requires-Dist: markdown-it-py==4.2.0
|
|
12
12
|
Requires-Dist: mdit-py-plugins==0.6.1
|
|
13
13
|
Requires-Dist: PyYAML==6.0.3
|
|
14
|
-
Requires-Dist: platformdirs==4.11.
|
|
14
|
+
Requires-Dist: platformdirs==4.11.14
|
|
15
15
|
Provides-Extra: mermaid
|
|
16
16
|
Requires-Dist: playwright==1.63.0; extra == "mermaid"
|
|
17
17
|
Dynamic: license-file
|
|
@@ -87,13 +87,13 @@ This is only needed if your source documents use ` ```mermaid ` fences (or speci
|
|
|
87
87
|
|
|
88
88
|
```bash
|
|
89
89
|
pipx install "text-compositor[mermaid]"
|
|
90
|
-
# or, if already installed: pipx inject text-compositor playwright==1.
|
|
90
|
+
# or, if already installed: pipx inject text-compositor playwright==1.63.0
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
With Option B (clone and run directly):
|
|
94
94
|
|
|
95
95
|
```bash
|
|
96
|
-
pip install playwright==1.
|
|
96
|
+
pip install playwright==1.63.0
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
- **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)
|
|
@@ -175,7 +175,7 @@ with Session() as session: # reuses the Mermaid browser and the Typst compiler
|
|
|
175
175
|
print(result.ok, result.pdf_path, [d.message for d in result.diagnostics])
|
|
176
176
|
```
|
|
177
177
|
|
|
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, `svg`) become images next to it. `typst-exec` and raw HTML are not supported yet and are shown as code with a warning. Graphviz, Pikchr, CeTZ and
|
|
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).
|
|
179
179
|
|
|
180
180
|
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
181
|
|
|
@@ -69,13 +69,13 @@ This is only needed if your source documents use ` ```mermaid ` fences (or speci
|
|
|
69
69
|
|
|
70
70
|
```bash
|
|
71
71
|
pipx install "text-compositor[mermaid]"
|
|
72
|
-
# or, if already installed: pipx inject text-compositor playwright==1.
|
|
72
|
+
# or, if already installed: pipx inject text-compositor playwright==1.63.0
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
With Option B (clone and run directly):
|
|
76
76
|
|
|
77
77
|
```bash
|
|
78
|
-
pip install playwright==1.
|
|
78
|
+
pip install playwright==1.63.0
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
- **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)
|
|
@@ -157,7 +157,7 @@ with Session() as session: # reuses the Mermaid browser and the Typst compiler
|
|
|
157
157
|
print(result.ok, result.pdf_path, [d.message for d in result.diagnostics])
|
|
158
158
|
```
|
|
159
159
|
|
|
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, `svg`) become images next to it. `typst-exec` and raw HTML are not supported yet and are shown as code with a warning. Graphviz, Pikchr, CeTZ and
|
|
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, 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).
|
|
161
161
|
|
|
162
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.
|
|
163
163
|
|
|
@@ -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.2"
|
|
8
8
|
description = "A tool for composing multiple text files into a PDF document."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -14,7 +14,7 @@ dependencies = [
|
|
|
14
14
|
"markdown-it-py==4.2.0",
|
|
15
15
|
"mdit-py-plugins==0.6.1",
|
|
16
16
|
"PyYAML==6.0.3",
|
|
17
|
-
"platformdirs==4.11.
|
|
17
|
+
"platformdirs==4.11.14",
|
|
18
18
|
]
|
|
19
19
|
|
|
20
20
|
# plugins.mermaid: true(既定)を使う場合のみ必要(11章)。
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
いずれも仕様9章のFail-fast方針どおり、画像欠損時はフォールバックせず即エラー終了する。
|
|
4
4
|
"""
|
|
5
|
+
import base64
|
|
5
6
|
import os
|
|
6
7
|
|
|
7
8
|
import pytest
|
|
@@ -51,6 +52,47 @@ class TestResolveAsset:
|
|
|
51
52
|
assert renderer._resolve_asset("a.png") == "/project/a.png"
|
|
52
53
|
|
|
53
54
|
|
|
55
|
+
class TestResolveDataUriAsset:
|
|
56
|
+
"""`data:`のURI画像は、キャッシュへ書き出してから解決する。PDF(Typstの#image())は、data:のURIを
|
|
57
|
+
直接受け取れないため(HTMLと挙動をそろえ、`Image not found`のエラーにしない。#238)。"""
|
|
58
|
+
|
|
59
|
+
PNG_BASE64 = base64.b64encode(b"\x89PNG\r\n\x1a\n" + b"\x00" * 10).decode()
|
|
60
|
+
|
|
61
|
+
def _renderer(self, tmp_path):
|
|
62
|
+
renderer = TypstRenderer(line_mapping="off", base_dir=str(tmp_path), typst_root=str(tmp_path))
|
|
63
|
+
renderer.current_dir = str(tmp_path)
|
|
64
|
+
renderer.current_file = str(tmp_path / "doc.md")
|
|
65
|
+
return renderer
|
|
66
|
+
|
|
67
|
+
def test_data_uri_is_written_to_the_cache_and_resolved(self, tmp_path):
|
|
68
|
+
renderer = self._renderer(tmp_path)
|
|
69
|
+
result = renderer._resolve_asset(f"data:image/png;base64,{self.PNG_BASE64}")
|
|
70
|
+
assert result.startswith("/.text-compositor/cache/datauri_") and result.endswith(".png")
|
|
71
|
+
cache_path = tmp_path / result.lstrip("/")
|
|
72
|
+
assert cache_path.is_file()
|
|
73
|
+
assert cache_path.read_bytes() == base64.b64decode(self.PNG_BASE64)
|
|
74
|
+
|
|
75
|
+
def test_the_same_data_uri_reuses_the_cached_file(self, tmp_path):
|
|
76
|
+
renderer = self._renderer(tmp_path)
|
|
77
|
+
src = f"data:image/png;base64,{self.PNG_BASE64}"
|
|
78
|
+
first = renderer._resolve_asset(src)
|
|
79
|
+
cache_path = tmp_path / first.lstrip("/")
|
|
80
|
+
mtime = cache_path.stat().st_mtime_ns
|
|
81
|
+
second = renderer._resolve_asset(src)
|
|
82
|
+
assert second == first
|
|
83
|
+
assert cache_path.stat().st_mtime_ns == mtime
|
|
84
|
+
|
|
85
|
+
def test_unsupported_mime_type_exits(self, tmp_path):
|
|
86
|
+
renderer = self._renderer(tmp_path)
|
|
87
|
+
with pytest.raises(SystemExit):
|
|
88
|
+
renderer._resolve_asset("data:application/pdf;base64,AAAA")
|
|
89
|
+
|
|
90
|
+
def test_non_base64_data_uri_exits(self, tmp_path):
|
|
91
|
+
renderer = self._renderer(tmp_path)
|
|
92
|
+
with pytest.raises(SystemExit):
|
|
93
|
+
renderer._resolve_asset("data:image/png,not-base64-encoded")
|
|
94
|
+
|
|
95
|
+
|
|
54
96
|
class TestResolveProjectImagePath:
|
|
55
97
|
"""document.background/logo、chapters[].background/logo(project_dir基準)の解決。"""
|
|
56
98
|
|
|
@@ -143,3 +143,9 @@ def test_build_dist_pins_the_same_java_plantuml_d2_structurizr_and_mermaid_as_th
|
|
|
143
143
|
|
|
144
144
|
assert deps_mod.MERMAID_JS_URL in script
|
|
145
145
|
assert deps_mod.MERMAID_JS_SHA256 in script
|
|
146
|
+
|
|
147
|
+
# Vega・Vega-Lite(#351)
|
|
148
|
+
assert deps_mod.VEGA_JS_URL in script
|
|
149
|
+
assert deps_mod.VEGA_JS_SHA256 in script
|
|
150
|
+
assert deps_mod.VEGA_LITE_JS_URL in script
|
|
151
|
+
assert deps_mod.VEGA_LITE_JS_SHA256 in script
|
|
@@ -84,9 +84,12 @@ def test_build_dist_pins_the_same_fonts_and_packages_as_the_tool():
|
|
|
84
84
|
common = (ROOT / "text_compositor" / "templates" / "_common.typ").read_text(encoding="utf-8")
|
|
85
85
|
used = set(re.findall(r"@preview/([a-z0-9-]+):([\d.]+)", common))
|
|
86
86
|
used.add(("kip", pikchr_render.KIP_VERSION)) # Pikchr(#213)は、テンプレートではなく、生成コードが読み込む
|
|
87
|
-
# CeTZ・Fletcher(#236)も、生成コードが読み込む。Fletcher 0.5.8が内部で使うcetz 0.3.4・oxifmt
|
|
88
|
-
#
|
|
87
|
+
# CeTZ・Fletcher(#236)・timeliney(#294)も、生成コードが読み込む。Fletcher 0.5.8が内部で使うcetz 0.3.4・oxifmt、
|
|
88
|
+
# cetz 0.5.2が使うoxifmt 1.0.0、timeliney 0.4.0が使うcetz 0.4.1は、ネットワークなしで動くように、推移的な依存として、
|
|
89
|
+
# 同梱する(同梱の不足は、viewer/scripts/check-dist.jsの、オフラインでのコンパイルが検出する)
|
|
89
90
|
used |= {("cetz", cetz_render.CETZ_VERSION), ("fletcher", cetz_render.FLETCHER_VERSION),
|
|
90
|
-
("
|
|
91
|
+
("timeliney", cetz_render.TIMELINEY_VERSION),
|
|
92
|
+
("cetz", "0.3.4"), ("cetz", cetz_render.TIMELINEY_CETZ_VERSION),
|
|
93
|
+
("oxifmt", "0.2.1"), ("oxifmt", "1.0.0")}
|
|
91
94
|
bundled = set(re.findall(r"name: '([a-z0-9-]+)', version: '([\d.]+)'", script))
|
|
92
95
|
assert used and used == bundled, (used, bundled)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""CeTZ・Fletcher(Typstのパッケージ。cetz_render.py・図のフェンスの変換、#236)のテスト。"""
|
|
1
|
+
"""CeTZ・Fletcher・timeliney(Typstのパッケージ。cetz_render.py・図のフェンスの変換、#236・#294)のテスト。"""
|
|
2
2
|
import os
|
|
3
3
|
import re
|
|
4
4
|
|
|
@@ -14,6 +14,7 @@ PLAIN = {"mermaid": False, "plantuml": False, "d2": False, "graphviz": False}
|
|
|
14
14
|
|
|
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
|
+
TIMELINEY = 'headerline(group(([*2024*], 2)))\ntaskgroup(title: [日本語], {\n task("A", (0, 1))\n})'
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
def _read(relative):
|
|
@@ -27,6 +28,8 @@ def test_the_versions_are_the_same_in_the_source_and_the_bundle():
|
|
|
27
28
|
bundled = set(re.findall(r"name: '([a-z0-9-]+)', version: '([\d.]+)'", script))
|
|
28
29
|
assert ("cetz", cetz_render.CETZ_VERSION) in bundled
|
|
29
30
|
assert ("fletcher", cetz_render.FLETCHER_VERSION) in bundled
|
|
31
|
+
assert ("timeliney", cetz_render.TIMELINEY_VERSION) in bundled
|
|
32
|
+
assert ("cetz", cetz_render.TIMELINEY_CETZ_VERSION) in bundled
|
|
30
33
|
|
|
31
34
|
|
|
32
35
|
class TestCheckCode:
|
|
@@ -74,6 +77,9 @@ class TestRender:
|
|
|
74
77
|
def test_fletcher_becomes_an_svg(self):
|
|
75
78
|
assert render_svg("fletcher", FLETCHER).startswith("<svg")
|
|
76
79
|
|
|
80
|
+
def test_timeliney_becomes_an_svg(self):
|
|
81
|
+
assert render_svg("timeliney", TIMELINEY).startswith("<svg")
|
|
82
|
+
|
|
77
83
|
def test_the_compiler_is_reused_and_a_later_figure_is_not_a_leftover(self):
|
|
78
84
|
first = render_svg("cetz", "circle((0, 0))")
|
|
79
85
|
assert render_svg("cetz", "circle((0, 0))\nline((0, 0), (3, 3))") != first
|
|
@@ -97,6 +103,7 @@ class TestRender:
|
|
|
97
103
|
("cetz", 'pdf.embed("x")'),
|
|
98
104
|
("fletcher", 'node((0, 0), read("x"))'),
|
|
99
105
|
("fletcher", 'node((0, 0), std.plugin("x"))'),
|
|
106
|
+
("timeliney", 'task(str(read("x")), (0, 1))'),
|
|
100
107
|
])
|
|
101
108
|
def test_functions_that_read_files_are_denied_with_a_clear_message(self, kind, code):
|
|
102
109
|
with pytest.raises(FigureRenderError) as e:
|
|
@@ -120,7 +127,7 @@ def _html(tmp_path, text, name="doc.md", plugins=PLAIN):
|
|
|
120
127
|
|
|
121
128
|
|
|
122
129
|
class TestHtmlOutput:
|
|
123
|
-
@pytest.mark.parametrize("lang, code", [("cetz", CETZ), ("fletcher", FLETCHER)])
|
|
130
|
+
@pytest.mark.parametrize("lang, code", [("cetz", CETZ), ("fletcher", FLETCHER), ("timeliney", TIMELINEY)])
|
|
124
131
|
def test_a_fence_becomes_a_cached_svg_img(self, tmp_path, lang, code):
|
|
125
132
|
result, html = _html(tmp_path, f'a\n\n```{lang} {{width=300pt}}\n{code}\n```\n')
|
|
126
133
|
assert result.ok and not result.warnings
|
|
@@ -147,7 +154,7 @@ class TestHtmlOutput:
|
|
|
147
154
|
assert not result.ok and "is not available" in result.errors[0].detail
|
|
148
155
|
assert "TOP-SECRET" not in html
|
|
149
156
|
|
|
150
|
-
@pytest.mark.parametrize("lang", ["cetz", "fletcher"])
|
|
157
|
+
@pytest.mark.parametrize("lang", ["cetz", "fletcher", "timeliney"])
|
|
151
158
|
def test_the_plugin_setting_falls_back_to_a_code_block_without_a_warning(self, tmp_path, lang):
|
|
152
159
|
result, html = _html(tmp_path, f'```{lang}\nnode((0, 0), [x])\n```\n', plugins={**PLAIN, lang: False})
|
|
153
160
|
assert result.ok and f'class="language-{lang}"' in html and not result.warnings
|
|
@@ -174,6 +181,12 @@ class TestPdfCode:
|
|
|
174
181
|
assert f'import "@preview/fletcher:{cetz_render.FLETCHER_VERSION}"' in code
|
|
175
182
|
assert 'eval("diagram(\\n" + "node((0, 0)' in code
|
|
176
183
|
|
|
184
|
+
def test_timeliney_wraps_the_code_in_timeline(self):
|
|
185
|
+
code = self._render(f'```timeliney\n{TIMELINEY}\n```\n')
|
|
186
|
+
assert f'import "@preview/timeliney:{cetz_render.TIMELINEY_VERSION}"' in code
|
|
187
|
+
assert 'timeliney.timeline(show-grid: true, { eval(' in code
|
|
188
|
+
assert 'eval("headerline(group' in code
|
|
189
|
+
|
|
177
190
|
def test_the_file_reading_functions_are_denied_in_the_generated_code(self):
|
|
178
191
|
code = self._render('```cetz\ncircle((0, 0))\n```\n')
|
|
179
192
|
assert '"read", "json"' in code and "std: denied" in code
|
|
@@ -184,6 +197,14 @@ class TestPdfCode:
|
|
|
184
197
|
code = self._render('```fletcher {width=50%}\nnode((0, 0), [A])\n```\n')
|
|
185
198
|
assert "50 * 1% * size.width" in code
|
|
186
199
|
|
|
200
|
+
def test_a_size_attribute_on_timeliney_sets_the_container_width_instead_of_scaling(self):
|
|
201
|
+
"""timelineyは、measure()+scale()ではなく、box/blockのwidthへそのまま渡す(cetz_render.pdf_sourceのdocstring参照)。"""
|
|
202
|
+
code = self._render(f'```timeliney {{width=10cm}}\n{TIMELINEY}\n```\n')
|
|
203
|
+
assert "block(width: 10cm, height: auto)" in code
|
|
204
|
+
assert "measure(fig)" not in code and "scale(" not in code
|
|
205
|
+
code = self._render(f'```timeliney\n{TIMELINEY}\n```\n') # 指定なしは、行の幅いっぱい
|
|
206
|
+
assert "block(width: 100%, height: auto)" in code
|
|
207
|
+
|
|
187
208
|
def test_it_is_generated_without_touching_the_templates_public_names(self):
|
|
188
209
|
"""カスタムテンプレートが、新しい公開名を、持たなくても、動く(生成コードが、パッケージを直接importする)。"""
|
|
189
210
|
code = self._render('```cetz\ncircle((0, 0))\n```\n')
|
|
@@ -204,9 +225,10 @@ class TestPdfCode:
|
|
|
204
225
|
|
|
205
226
|
def test_a_pdf_with_both_figures_is_built(tmp_path):
|
|
206
227
|
md = tmp_path / "doc.md"
|
|
207
|
-
md.write_text(f'# 図\n\n```cetz\n{CETZ}\n```\n\n```fletcher\n{FLETCHER}\n```\n\n'
|
|
228
|
+
md.write_text(f'# 図\n\n```cetz\n{CETZ}\n```\n\n```fletcher\n{FLETCHER}\n```\n\n```timeliney\n{TIMELINEY}\n```\n\n'
|
|
208
229
|
'```cetz {width=5cm}\ncircle((0, 0))\n```\n\n```fletcher {width=50% height=3cm}\n'
|
|
209
|
-
'node((0, 0), [A]), edge("->"), node((1, 0), [B])\n```\n
|
|
230
|
+
'node((0, 0), [A]), edge("->"), node((1, 0), [B])\n```\n\n```timeliney {width=10cm}\n'
|
|
231
|
+
f'{TIMELINEY}\n```\n', encoding="utf-8")
|
|
210
232
|
result = build_markdown(str(md), str(tmp_path / "doc.pdf"))
|
|
211
233
|
assert result.ok, [(e.message, e.detail) for e in result.errors]
|
|
212
234
|
assert (tmp_path / "doc.pdf").read_bytes().startswith(b"%PDF")
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""CLI: 単一のMarkdownファイルを直接指定してPDFにする(`text-compositor FILE.md`、#179)のテスト。"""
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
import text_compositor.build as build_mod
|
|
8
|
+
from text_compositor import log
|
|
9
|
+
import text_compositor.project as project_mod
|
|
10
|
+
from text_compositor.build import _single_markdown_output, parse_args
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@pytest.fixture(autouse=True)
|
|
14
|
+
def _restore_verbosity():
|
|
15
|
+
"""`build()`は、-q/-vを、プロセスグローバルへ反映する。他のテストへ、残さない。"""
|
|
16
|
+
yield
|
|
17
|
+
log.set_verbosity(False, False)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def parse(monkeypatch, *argv):
|
|
21
|
+
monkeypatch.setattr(sys, "argv", ["text-compositor", *argv])
|
|
22
|
+
return parse_args()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TestParseArgs:
|
|
26
|
+
def test_a_single_file_with_output_and_template(self, monkeypatch):
|
|
27
|
+
args = parse(monkeypatch, "doc.md", "-o", "out.pdf", "-t", "slide", "-q", "--keep-temp")
|
|
28
|
+
assert (args.markdown, args.output, args.template, args.quiet, args.keep_temp) == ("doc.md", "out.pdf", "slide", True, True)
|
|
29
|
+
|
|
30
|
+
def test_existing_usage_without_a_file_is_unchanged(self, monkeypatch):
|
|
31
|
+
args = parse(monkeypatch, "--config", "c.yaml", "--if-changed")
|
|
32
|
+
assert args.markdown is None and args.config == "c.yaml" and args.if_changed
|
|
33
|
+
|
|
34
|
+
@pytest.mark.parametrize("flags", [
|
|
35
|
+
["--config", "c.yaml"], ["--config-list", "l.txt"], ["--check-env"], ["--watch"], ["--if-changed"],
|
|
36
|
+
["--clean"], ["--clean-cache"],
|
|
37
|
+
])
|
|
38
|
+
def test_options_that_need_a_config_cannot_be_combined_with_a_file(self, monkeypatch, capsys, flags):
|
|
39
|
+
with pytest.raises(SystemExit) as e:
|
|
40
|
+
parse(monkeypatch, "doc.md", *flags)
|
|
41
|
+
assert e.value.code == 2
|
|
42
|
+
assert "同時に指定できません" in capsys.readouterr().err
|
|
43
|
+
|
|
44
|
+
@pytest.mark.parametrize("flags", [["-o", "out.pdf"], ["-t", "slide"]])
|
|
45
|
+
def test_output_and_template_need_a_file(self, monkeypatch, capsys, flags):
|
|
46
|
+
with pytest.raises(SystemExit) as e:
|
|
47
|
+
parse(monkeypatch, *flags)
|
|
48
|
+
assert e.value.code == 2 and "FILE.md" in capsys.readouterr().err
|
|
49
|
+
|
|
50
|
+
def test_only_markdown_files_are_accepted(self, monkeypatch, capsys):
|
|
51
|
+
with pytest.raises(SystemExit) as e:
|
|
52
|
+
parse(monkeypatch, "notes.txt")
|
|
53
|
+
assert e.value.code == 2
|
|
54
|
+
assert parse(monkeypatch, "A.MD").markdown == "A.MD" and parse(monkeypatch, "a.markdown").markdown == "a.markdown"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TestOutputPath:
|
|
58
|
+
def test_the_default_is_next_to_the_manuscript(self, tmp_path):
|
|
59
|
+
md = str(tmp_path / "sub" / "note.md")
|
|
60
|
+
assert _single_markdown_output(md, None) == str(tmp_path / "sub" / "note.pdf")
|
|
61
|
+
|
|
62
|
+
def test_an_explicit_file_and_a_folder(self, tmp_path, monkeypatch):
|
|
63
|
+
monkeypatch.chdir(tmp_path)
|
|
64
|
+
md = str(tmp_path / "note.md")
|
|
65
|
+
assert _single_markdown_output(md, "out.pdf") == str(tmp_path / "out.pdf")
|
|
66
|
+
(tmp_path / "existing").mkdir()
|
|
67
|
+
assert _single_markdown_output(md, "existing") == str(tmp_path / "existing" / "note.pdf")
|
|
68
|
+
assert _single_markdown_output(md, "new" + os.sep) == str(tmp_path / "new" / "note.pdf")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class TestBuildSingleMarkdown:
|
|
72
|
+
@pytest.fixture
|
|
73
|
+
def calls(self, monkeypatch):
|
|
74
|
+
recorded = []
|
|
75
|
+
monkeypatch.setattr(project_mod, "_build_project", lambda *a, **k: recorded.append((a, k)))
|
|
76
|
+
return recorded
|
|
77
|
+
|
|
78
|
+
def run(self, tmp_path, *argv, monkeypatch):
|
|
79
|
+
monkeypatch.setattr(build_mod, "check_typst_version", lambda *a: None)
|
|
80
|
+
monkeypatch.setattr(build_mod, "ensure_fonts", lambda: "fonts")
|
|
81
|
+
monkeypatch.setattr(sys, "argv", ["text-compositor", *argv])
|
|
82
|
+
build_mod.build()
|
|
83
|
+
|
|
84
|
+
def test_it_builds_the_api_config_and_writes_next_to_the_manuscript(self, tmp_path, calls, monkeypatch):
|
|
85
|
+
md = tmp_path / "note.md"
|
|
86
|
+
md.write_text("# T\n", encoding="utf-8")
|
|
87
|
+
self.run(tmp_path, str(md), monkeypatch=monkeypatch)
|
|
88
|
+
(args, kwargs), = calls
|
|
89
|
+
project_dir, config, chapters = args[3], args[4], args[5]
|
|
90
|
+
assert project_dir == str(tmp_path) and chapters == ["note.md"]
|
|
91
|
+
assert config["document"]["cover"] == "markdown" and config["document"]["title"] == "note" # APIと同じ既定
|
|
92
|
+
assert config["template"]["path"] == "template"
|
|
93
|
+
assert kwargs["out_pdf"] == str(tmp_path / "note.pdf")
|
|
94
|
+
|
|
95
|
+
def test_the_template_name_and_a_typ_path_relative_to_the_cwd(self, tmp_path, calls, monkeypatch):
|
|
96
|
+
md = tmp_path / "note.md"
|
|
97
|
+
md.write_text("# T\n", encoding="utf-8")
|
|
98
|
+
monkeypatch.chdir(tmp_path)
|
|
99
|
+
self.run(tmp_path, "note.md", "-t", "paper", monkeypatch=monkeypatch)
|
|
100
|
+
self.run(tmp_path, "note.md", "-t", "my/own.typ", "-o", "build/o.pdf", monkeypatch=monkeypatch)
|
|
101
|
+
assert calls[0][0][4]["template"]["path"] == "paper"
|
|
102
|
+
assert calls[1][0][4]["template"]["path"] == str(tmp_path / "my" / "own.typ")
|
|
103
|
+
assert calls[1][1]["out_pdf"] == str(tmp_path / "build" / "o.pdf") and (tmp_path / "build").is_dir()
|
|
104
|
+
|
|
105
|
+
def test_a_missing_file_and_an_unknown_template_fail_with_exit_code_1(self, tmp_path, calls, monkeypatch, capsys):
|
|
106
|
+
with pytest.raises(SystemExit) as e:
|
|
107
|
+
self.run(tmp_path, str(tmp_path / "nope.md"), monkeypatch=monkeypatch)
|
|
108
|
+
assert e.value.code == 1 and "Markdown file not found" in capsys.readouterr().out
|
|
109
|
+
md = tmp_path / "note.md"
|
|
110
|
+
md.write_text("# T\n", encoding="utf-8")
|
|
111
|
+
with pytest.raises(SystemExit) as e:
|
|
112
|
+
self.run(tmp_path, str(md), "-t", "nosuch", monkeypatch=monkeypatch)
|
|
113
|
+
assert e.value.code == 1 and "Unknown template" in capsys.readouterr().out
|
|
114
|
+
assert calls == []
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def test_a_real_pdf_is_built_without_a_config(tmp_path, monkeypatch):
|
|
118
|
+
"""実際に、configなしで、PDFができる(Typstとフォントが、ある環境)。先頭の見出しは、表紙に落とされず、本文に残る。"""
|
|
119
|
+
from text_compositor.deps import ensure_fonts
|
|
120
|
+
try:
|
|
121
|
+
ensure_fonts()
|
|
122
|
+
except SystemExit:
|
|
123
|
+
pytest.skip("fonts are not available")
|
|
124
|
+
md = tmp_path / "note.md"
|
|
125
|
+
md.write_text("# 見出し\n\n本文です。\n", encoding="utf-8")
|
|
126
|
+
monkeypatch.setattr(sys, "argv", ["text-compositor", str(md), "-q"])
|
|
127
|
+
build_mod.build()
|
|
128
|
+
pdf = tmp_path / "note.pdf"
|
|
129
|
+
assert pdf.is_file() and pdf.read_bytes().startswith(b"%PDF")
|
|
@@ -93,7 +93,7 @@ class TestHtmlOutput:
|
|
|
93
93
|
def test_the_text_is_still_shown(self, tmp_path):
|
|
94
94
|
result, _ = _html_warnings(tmp_path, "これは**「重要」**です。\n")
|
|
95
95
|
with open(result.html_path, encoding="utf-8") as f:
|
|
96
|
-
assert
|
|
96
|
+
assert '<p data-line="1">これは**「重要」**です。</p>' in f.read()
|
|
97
97
|
|
|
98
98
|
def test_a_table_cell_and_a_list_item_are_checked(self, tmp_path):
|
|
99
99
|
text = "| a | b |\n| - | - |\n| **「x」**です | y |\n\n- 項目**(仮)**です\n"
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""HTML出力の、ブロック要素の`data-line`(原稿の行番号。Viewerが、ツールバーに出す。#328・#357)のテスト。"""
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from text_compositor.api import render_html
|
|
7
|
+
from text_compositor.html_output import DOCUMENT_CSS, _with_data_line
|
|
8
|
+
|
|
9
|
+
PLAIN = {"mermaid": False, "plantuml": False, "d2": False}
|
|
10
|
+
|
|
11
|
+
DOC = """# 見出し
|
|
12
|
+
|
|
13
|
+
段落その1です。
|
|
14
|
+
続きの行。
|
|
15
|
+
|
|
16
|
+
- 項目A
|
|
17
|
+
- 項目B
|
|
18
|
+
- 入れ子B1
|
|
19
|
+
|
|
20
|
+
1. 番号1
|
|
21
|
+
|
|
22
|
+
ゆるい段落
|
|
23
|
+
|
|
24
|
+
> 引用の段落
|
|
25
|
+
|
|
26
|
+
| a | b |
|
|
27
|
+
| - | - |
|
|
28
|
+
| 1 | 2 |
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
print(1)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
$$
|
|
35
|
+
a=b
|
|
36
|
+
$$
|
|
37
|
+
|
|
38
|
+
最後の段落。
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def lines_by_tag(tmp_path, markdown):
|
|
43
|
+
md = tmp_path / "doc.md"
|
|
44
|
+
md.write_text(markdown, encoding="utf-8")
|
|
45
|
+
result = render_html(str(md), plugins=PLAIN)
|
|
46
|
+
assert result.ok, result.diagnostics
|
|
47
|
+
html = open(result.html_path, encoding="utf-8").read()
|
|
48
|
+
body = html[html.index("<body"):]
|
|
49
|
+
return [(m.group(1), int(m.group(2))) for m in re.finditer(r'<(\w+)[^>]*\sdata-line="(\d+)"', body)], body
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_block_elements_carry_their_manuscript_line(tmp_path):
|
|
53
|
+
found, _ = lines_by_tag(tmp_path, DOC)
|
|
54
|
+
assert found == [
|
|
55
|
+
("h1", 1), ("p", 3),
|
|
56
|
+
("li", 6), ("li", 7), ("li", 8), # 緊密なリスト: 項目が持つ(中の段落は、隠れている)
|
|
57
|
+
("p", 10), ("p", 12), # ゆるいリスト: 項目ではなく、中の段落が持つ
|
|
58
|
+
("p", 14), # 引用: 中の段落が持つ
|
|
59
|
+
("table", 16), ("pre", 20), ("div", 24), # 表・フェンス・数式
|
|
60
|
+
("p", 28),
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_a_diagram_fence_carries_the_line_on_its_wrapper(tmp_path):
|
|
65
|
+
svg = tmp_path / "x.svg"
|
|
66
|
+
svg.write_text('<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10"></svg>', encoding="utf-8")
|
|
67
|
+
found, body = lines_by_tag(tmp_path, "# T\n\n```svg\n" + svg.read_text(encoding="utf-8") + "\n```\n")
|
|
68
|
+
assert ("div", 3) in found and '<div data-line="3" class="diagram diagram-svg">' in body
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_a_line_is_not_added_twice_or_to_the_wrong_element(tmp_path):
|
|
72
|
+
_, body = lines_by_tag(tmp_path, DOC)
|
|
73
|
+
assert len(re.findall(r'data-line="10"', body)) == 1 and len(re.findall(r'data-line="6"', body)) == 1
|
|
74
|
+
assert "<hr" not in body or 'hr data-line' not in body
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class TestWithDataLine:
|
|
78
|
+
def test_it_adds_the_line_to_the_first_tag_only(self):
|
|
79
|
+
assert _with_data_line('<div class="a"><p>x</p></div>\n', 7) == '<div data-line="7" class="a"><p>x</p></div>\n'
|
|
80
|
+
|
|
81
|
+
@pytest.mark.parametrize("html,line", [('<pre>x</pre>', None), ('<pre>x</pre>', 0), ('text', 3), ('', 3)])
|
|
82
|
+
def test_it_leaves_html_without_a_line_or_a_leading_tag_alone(self, html, line):
|
|
83
|
+
assert _with_data_line(html, line) == html
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class TestNoBadgeCss:
|
|
87
|
+
"""行番号は、ツールバーに出す(#357)。文書のCSSには、バッジ(疑似要素)を、持たせない。"""
|
|
88
|
+
|
|
89
|
+
def test_the_document_css_has_no_badge(self):
|
|
90
|
+
assert "data-line-badge" not in DOCUMENT_CSS and "attr(data-line)" not in DOCUMENT_CSS
|
|
91
|
+
|
|
92
|
+
def test_the_document_still_has_no_script(self, tmp_path):
|
|
93
|
+
_, body = lines_by_tag(tmp_path, DOC)
|
|
94
|
+
assert "<script" not in body
|
|
@@ -76,10 +76,12 @@ class TestApi:
|
|
|
76
76
|
assert result.html_path == str(tmp_path / ".text-compositor" / "preview.html")
|
|
77
77
|
assert html.startswith("<!DOCTYPE html>")
|
|
78
78
|
assert "<title>見出し</title>" in html
|
|
79
|
-
assert
|
|
79
|
+
assert '<h1 data-line="1">見出し</h1>' in html
|
|
80
80
|
assert "<script" not in html
|
|
81
81
|
# スクリプトは、ブラウザ側でも禁止する(Viewerは、JavaScriptを有効にしたビューで開く)
|
|
82
82
|
assert "script-src 'none'" in html and "object-src 'none'" in html
|
|
83
|
+
# 外部の画像は、二重の防御として、ブラウザ側でも禁止する(1章の方針・#238)
|
|
84
|
+
assert "img-src 'self' data:" in html
|
|
83
85
|
assert set(result.timings_ms) >= {"render", "total"}
|
|
84
86
|
|
|
85
87
|
def test_the_title_falls_back_to_the_file_name(self, tmp_path):
|
|
@@ -140,7 +142,7 @@ class TestMarkdown:
|
|
|
140
142
|
assert "<strong>b</strong>" in b and "<em>i</em>" in b and "<s>s</s>" in b and "<code>c</code>" in b
|
|
141
143
|
assert '<a href="https://example.com">l</a>' in b
|
|
142
144
|
assert "<ul>" in b and "<ol>" in b
|
|
143
|
-
assert '<pre><code class="language-python">print(1)\n</code></pre>'
|
|
145
|
+
assert re.search(r'<pre data-line="\d+"><code class="language-python">print\(1\)\n</code></pre>', b)
|
|
144
146
|
|
|
145
147
|
def test_line_breaks_are_kept_like_the_pdf(self, tmp_path):
|
|
146
148
|
_, html = convert(tmp_path, "one\ntwo\n")
|
|
@@ -257,9 +259,42 @@ class TestImages:
|
|
|
257
259
|
warning = result.warnings[0]
|
|
258
260
|
assert "Image not found" in warning.message and warning.line == 3
|
|
259
261
|
|
|
260
|
-
def
|
|
262
|
+
def test_external_urls_are_blocked_by_default(self, tmp_path):
|
|
263
|
+
"""方針2章「ローカルに閉じる」: 既定では、外部の画像は読み込まず、プレースホルダと警告にする(#238)。"""
|
|
261
264
|
result, html = convert(tmp_path, "\n")
|
|
265
|
+
assert result.ok
|
|
266
|
+
assert "<img" not in html
|
|
267
|
+
assert 'class="blocked-image"' in html and "https://example.com/a.png" in html
|
|
268
|
+
assert "外部の画像は、既定で読み込みません" in html
|
|
269
|
+
warning = result.warnings[0]
|
|
270
|
+
assert "External image blocked" in warning.message and "https://example.com/a.png" in warning.message
|
|
271
|
+
assert warning.line == 1
|
|
272
|
+
|
|
273
|
+
def test_external_urls_are_loaded_when_explicitly_allowed(self, tmp_path):
|
|
274
|
+
result, html = convert(tmp_path, "\n", allow_external_images=True)
|
|
262
275
|
assert 'src="https://example.com/a.png"' in html and not result.warnings
|
|
276
|
+
assert 'class="blocked-image"' not in html
|
|
277
|
+
|
|
278
|
+
def test_data_uri_images_are_always_kept(self, tmp_path):
|
|
279
|
+
"""`data:`のURIは、ローカルで完結するため、allow_external_imagesによらず、常に読み込む(#238)。"""
|
|
280
|
+
src = "data:image/png;base64,eA=="
|
|
281
|
+
result, html = convert(tmp_path, f"\n")
|
|
282
|
+
assert result.ok and not result.warnings
|
|
283
|
+
assert f'src="{src}"' in html
|
|
284
|
+
assert 'class="blocked-image"' not in html
|
|
285
|
+
|
|
286
|
+
def test_windows_absolute_paths_are_blocked_by_default(self, tmp_path):
|
|
287
|
+
"""Windows絶対パス(`C:\\...`)は、通信は起きないが、原稿の外側の任意のローカルファイルへアクセス
|
|
288
|
+
できてしまうため、外部の画像と同じ扱いにする(既定で読み込まず、設定で許可。`file:`のURIと同じ理由、#238。
|
|
289
|
+
`file:`のURI自体は、markdown-it-py側が既に危険なスキームとして拒否し、リンクにならない)。"""
|
|
290
|
+
result, html = convert(tmp_path, "\n")
|
|
291
|
+
assert result.ok
|
|
292
|
+
assert "<img" not in html
|
|
293
|
+
assert 'class="blocked-image"' in html
|
|
294
|
+
assert result.warnings and "External image blocked" in result.warnings[0].message
|
|
295
|
+
|
|
296
|
+
result, html = convert(tmp_path, "\n", allow_external_images=True)
|
|
297
|
+
assert 'src="C:/Windows/win.ini"' in html and not result.warnings
|
|
263
298
|
|
|
264
299
|
def test_invalid_size_units_are_ignored_with_a_warning(self, tmp_path):
|
|
265
300
|
write(tmp_path / "p.png", "x")
|
|
@@ -282,6 +317,8 @@ class TestDependencies:
|
|
|
282
317
|
def test_the_markdown_itself_and_external_urls_are_not_included(self, tmp_path):
|
|
283
318
|
result, _ = convert(tmp_path, "# a\n\n\n")
|
|
284
319
|
assert result.dependencies == []
|
|
320
|
+
result, _ = convert(tmp_path, "# a\n\n\n", allow_external_images=True)
|
|
321
|
+
assert result.dependencies == []
|
|
285
322
|
|
|
286
323
|
def test_a_missing_image_is_included_so_that_creating_it_triggers_an_update(self, tmp_path):
|
|
287
324
|
result, _ = convert(tmp_path, "\n")
|
|
@@ -430,12 +467,12 @@ class TestFences:
|
|
|
430
467
|
|
|
431
468
|
def test_a_disabled_plugin_falls_back_to_code_without_a_warning(self, tmp_path):
|
|
432
469
|
result, html = convert(tmp_path, "```mermaid\ngraph TD\n A-->B\n```\n\n```plantuml\n@startuml\n@enduml\n```\n\n```d2\na -> b\n```\n")
|
|
433
|
-
assert
|
|
470
|
+
assert len(re.findall(r"<pre[ >]", body(html))) == 3 and "<img" not in html
|
|
434
471
|
assert not result.warnings
|
|
435
472
|
|
|
436
473
|
def test_other_languages_are_plain_code_blocks(self, tmp_path):
|
|
437
474
|
_, html = convert(tmp_path, "```\n<b>&\n```\n")
|
|
438
|
-
assert
|
|
475
|
+
assert '<pre data-line="1"><code><b>&\n</code></pre>' in html
|
|
439
476
|
|
|
440
477
|
def test_a_diagram_source_file_is_a_page_with_that_diagram(self, tmp_path):
|
|
441
478
|
result, html = convert(tmp_path, "graph TD\n A-->B\n", name="flow.mmd")
|