text-compositor 0.4.3__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.
Files changed (106) hide show
  1. {text_compositor-0.4.3 → text_compositor-0.4.4}/PKG-INFO +5 -3
  2. {text_compositor-0.4.3 → text_compositor-0.4.4}/README.md +2 -2
  3. {text_compositor-0.4.3 → text_compositor-0.4.4}/pyproject.toml +8 -1
  4. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_bundled_diagram_tools.py +8 -0
  5. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_bundled_typst_assets.py +5 -3
  6. text_compositor-0.4.4/tests/test_bytefield_render.py +164 -0
  7. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_cetz_render.py +26 -4
  8. text_compositor-0.4.4/tests/test_diagram_alt.py +105 -0
  9. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_distribution.py +6 -2
  10. text_compositor-0.4.4/tests/test_fonts.py +228 -0
  11. text_compositor-0.4.4/tests/test_highlight.py +139 -0
  12. text_compositor-0.4.4/tests/test_html_lang.py +43 -0
  13. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_html_output.py +5 -10
  14. text_compositor-0.4.4/tests/test_wavedrom_render.py +163 -0
  15. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_worker.py +104 -3
  16. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/api.py +3 -0
  17. text_compositor-0.4.4/text_compositor/bytefield_render.py +39 -0
  18. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/cetz_render.py +18 -5
  19. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/chapters.py +2 -0
  20. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/compiler.py +4 -3
  21. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/config.py +7 -2
  22. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/deps.py +42 -0
  23. text_compositor-0.4.4/text_compositor/fonts.py +225 -0
  24. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/graphviz_render.py +5 -3
  25. text_compositor-0.4.4/text_compositor/highlight.py +78 -0
  26. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/host_renderers.py +24 -0
  27. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/html_output.py +66 -19
  28. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/math_render.py +2 -1
  29. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/mermaid.py +45 -2
  30. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/pikchr_render.py +2 -1
  31. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/project.py +8 -2
  32. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/renderer.py +14 -3
  33. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/renderer_diagrams.py +163 -14
  34. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/renderer_layout.py +9 -8
  35. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/renderer_tokens.py +3 -2
  36. text_compositor-0.4.4/text_compositor/wavedrom_render.py +49 -0
  37. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/worker.py +23 -2
  38. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor.egg-info/PKG-INFO +5 -3
  39. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor.egg-info/SOURCES.txt +10 -0
  40. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor.egg-info/requires.txt +3 -0
  41. {text_compositor-0.4.3 → text_compositor-0.4.4}/LICENSE +0 -0
  42. {text_compositor-0.4.3 → text_compositor-0.4.4}/setup.cfg +0 -0
  43. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_aggregate_table.py +0 -0
  44. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_api.py +0 -0
  45. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_asset_resolution.py +0 -0
  46. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_check_pins.py +0 -0
  47. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_cli_single_markdown.py +0 -0
  48. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_common_template.py +0 -0
  49. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_config.py +0 -0
  50. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_config_key_validation.py +0 -0
  51. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_csv_table.py +0 -0
  52. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_diagnostics.py +0 -0
  53. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_diagram_cache_key.py +0 -0
  54. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_diagram_trim.py +0 -0
  55. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_doc_badges.py +0 -0
  56. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_download.py +0 -0
  57. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_emphasis_lint.py +0 -0
  58. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_front_matter.py +0 -0
  59. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_graphviz_render.py +0 -0
  60. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_html_line_numbers.py +0 -0
  61. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_incremental.py +0 -0
  62. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_java_encoding.py +0 -0
  63. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_layout_blocks.py +0 -0
  64. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_line_mapping.py +0 -0
  65. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_markdown_basics.py +0 -0
  66. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_math_render.py +0 -0
  67. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_mermaid_resident.py +0 -0
  68. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_pandoc_compat.py +0 -0
  69. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_paper_template.py +0 -0
  70. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_pdf_fonts.py +0 -0
  71. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_pdf_metadata.py +0 -0
  72. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_pikchr_render.py +0 -0
  73. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_plantuml_batch.py +0 -0
  74. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_renderer_structure.py +0 -0
  75. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_revision_history.py +0 -0
  76. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_script_references.py +0 -0
  77. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_sections.py +0 -0
  78. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_soft_break_hints.py +0 -0
  79. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_structurizr_render.py +0 -0
  80. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_svg_trim.py +0 -0
  81. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_table_cell_style.py +0 -0
  82. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_variables.py +0 -0
  83. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_vega_render.py +0 -0
  84. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_viewer_version.py +0 -0
  85. {text_compositor-0.4.3 → text_compositor-0.4.4}/tests/test_watch.py +0 -0
  86. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/__init__.py +0 -0
  87. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/build.py +0 -0
  88. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/changes.py +0 -0
  89. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/diagnostics.py +0 -0
  90. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/document.py +0 -0
  91. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/emphasis_lint.py +0 -0
  92. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/env_check.py +0 -0
  93. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/log.py +0 -0
  94. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/renderer_inline.py +0 -0
  95. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/renderer_tables.py +0 -0
  96. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/svg_trim.py +0 -0
  97. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/templates/_common.typ +0 -0
  98. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/templates/paper.typ +0 -0
  99. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/templates/slide.typ +0 -0
  100. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/templates/template.typ +0 -0
  101. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/typst_literal.py +0 -0
  102. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/typst_runtime.py +0 -0
  103. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor/vega_render.py +0 -0
  104. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor.egg-info/dependency_links.txt +0 -0
  105. {text_compositor-0.4.3 → text_compositor-0.4.4}/text_compositor.egg-info/entry_points.txt +0 -0
  106. {text_compositor-0.4.3 → 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
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
 
@@ -61,7 +61,7 @@ No Typst compiler binary is bundled; it's obtained from the `typst` package (PyP
61
61
 
62
62
  ### Keeping up to date
63
63
 
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 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.
65
65
 
66
66
  ### Using Mermaid diagrams (optional)
67
67
 
@@ -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, 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).
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.3"
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 &lt;b&gt; &amp; &quot;q&quot;"' 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 project.get(name) == version, f"{name}: dist {version} != pyproject {project.get(name)}"
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(project) == set(dist)
87
+ assert set(expected) == set(dist)