text-compositor 0.4.2__tar.gz → 0.4.3__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 (96) hide show
  1. {text_compositor-0.4.2 → text_compositor-0.4.3}/PKG-INFO +11 -1
  2. {text_compositor-0.4.2 → text_compositor-0.4.3}/README.md +10 -0
  3. {text_compositor-0.4.2 → text_compositor-0.4.3}/pyproject.toml +1 -1
  4. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_html_output.py +6 -1
  5. text_compositor-0.4.3/tests/test_pdf_fonts.py +100 -0
  6. text_compositor-0.4.3/tests/test_pdf_metadata.py +81 -0
  7. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_worker.py +1 -1
  8. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/html_output.py +11 -0
  9. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/templates/_common.typ +9 -0
  10. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/templates/paper.typ +9 -1
  11. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/templates/slide.typ +9 -1
  12. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/templates/template.typ +9 -1
  13. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/worker.py +1 -1
  14. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor.egg-info/PKG-INFO +11 -1
  15. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor.egg-info/SOURCES.txt +2 -0
  16. {text_compositor-0.4.2 → text_compositor-0.4.3}/LICENSE +0 -0
  17. {text_compositor-0.4.2 → text_compositor-0.4.3}/setup.cfg +0 -0
  18. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_aggregate_table.py +0 -0
  19. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_api.py +0 -0
  20. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_asset_resolution.py +0 -0
  21. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_bundled_diagram_tools.py +0 -0
  22. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_bundled_typst_assets.py +0 -0
  23. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_cetz_render.py +0 -0
  24. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_check_pins.py +0 -0
  25. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_cli_single_markdown.py +0 -0
  26. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_common_template.py +0 -0
  27. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_config.py +0 -0
  28. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_config_key_validation.py +0 -0
  29. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_csv_table.py +0 -0
  30. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_diagnostics.py +0 -0
  31. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_diagram_cache_key.py +0 -0
  32. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_diagram_trim.py +0 -0
  33. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_distribution.py +0 -0
  34. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_doc_badges.py +0 -0
  35. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_download.py +0 -0
  36. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_emphasis_lint.py +0 -0
  37. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_front_matter.py +0 -0
  38. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_graphviz_render.py +0 -0
  39. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_html_line_numbers.py +0 -0
  40. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_incremental.py +0 -0
  41. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_java_encoding.py +0 -0
  42. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_layout_blocks.py +0 -0
  43. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_line_mapping.py +0 -0
  44. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_markdown_basics.py +0 -0
  45. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_math_render.py +0 -0
  46. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_mermaid_resident.py +0 -0
  47. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_pandoc_compat.py +0 -0
  48. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_paper_template.py +0 -0
  49. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_pikchr_render.py +0 -0
  50. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_plantuml_batch.py +0 -0
  51. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_renderer_structure.py +0 -0
  52. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_revision_history.py +0 -0
  53. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_script_references.py +0 -0
  54. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_sections.py +0 -0
  55. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_soft_break_hints.py +0 -0
  56. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_structurizr_render.py +0 -0
  57. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_svg_trim.py +0 -0
  58. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_table_cell_style.py +0 -0
  59. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_variables.py +0 -0
  60. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_vega_render.py +0 -0
  61. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_viewer_version.py +0 -0
  62. {text_compositor-0.4.2 → text_compositor-0.4.3}/tests/test_watch.py +0 -0
  63. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/__init__.py +0 -0
  64. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/api.py +0 -0
  65. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/build.py +0 -0
  66. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/cetz_render.py +0 -0
  67. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/changes.py +0 -0
  68. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/chapters.py +0 -0
  69. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/compiler.py +0 -0
  70. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/config.py +0 -0
  71. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/deps.py +0 -0
  72. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/diagnostics.py +0 -0
  73. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/document.py +0 -0
  74. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/emphasis_lint.py +0 -0
  75. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/env_check.py +0 -0
  76. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/graphviz_render.py +0 -0
  77. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/host_renderers.py +0 -0
  78. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/log.py +0 -0
  79. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/math_render.py +0 -0
  80. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/mermaid.py +0 -0
  81. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/pikchr_render.py +0 -0
  82. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/project.py +0 -0
  83. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/renderer.py +0 -0
  84. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/renderer_diagrams.py +0 -0
  85. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/renderer_inline.py +0 -0
  86. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/renderer_layout.py +0 -0
  87. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/renderer_tables.py +0 -0
  88. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/renderer_tokens.py +0 -0
  89. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/svg_trim.py +0 -0
  90. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/typst_literal.py +0 -0
  91. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/typst_runtime.py +0 -0
  92. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor/vega_render.py +0 -0
  93. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor.egg-info/dependency_links.txt +0 -0
  94. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor.egg-info/entry_points.txt +0 -0
  95. {text_compositor-0.4.2 → text_compositor-0.4.3}/text_compositor.egg-info/requires.txt +0 -0
  96. {text_compositor-0.4.2 → text_compositor-0.4.3}/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.2
3
+ Version: 0.4.3
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
@@ -192,6 +192,16 @@ This generates `sample/SampleDocument.pdf`. Two more samples show other template
192
192
 
193
193
  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
194
 
195
+ ## Policy for adding diagram notations
196
+
197
+ 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:
198
+
199
+ - **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.
200
+ - **A heavy runtime.** It needs Docker or a similar container, or a large runtime that most users do not have.
201
+ - **An external service.** It sends the manuscript to a server.
202
+
203
+ Candidates that fail these checks are closed as "not planned" on [GitHub Issues](https://github.com/tokudiro/text-compositor/issues), with the reason recorded.
204
+
195
205
  ## License
196
206
 
197
207
  [MIT License](LICENSE)
@@ -174,6 +174,16 @@ This generates `sample/SampleDocument.pdf`. Two more samples show other template
174
174
 
175
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.
176
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
+
177
187
  ## License
178
188
 
179
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.2"
7
+ version = "0.4.3"
8
8
  description = "A tool for composing multiple text files into a PDF document."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -69,6 +69,11 @@ def messages(result, severity):
69
69
 
70
70
 
71
71
  class TestApi:
72
+ def test_headings_get_github_style_ids_for_in_document_links(self, tmp_path):
73
+ # 文書内リンク[…](#見出し)の飛び先(#361)。重複には-1・-2を付ける
74
+ _, html = convert(tmp_path, "# Hello, World!\n\n## 日本語 見出し\n\n## Hello, World!\n")
75
+ assert 'id="hello-world"' in html and 'id="日本語-見出し"' in html and 'id="hello-world-1"' in html
76
+
72
77
  def test_writes_a_standalone_document_next_to_the_markdown(self, tmp_path):
73
78
  result, html = convert(tmp_path, "# 見出し\n\n本文。\n")
74
79
  assert isinstance(result, HtmlResult)
@@ -76,7 +81,7 @@ class TestApi:
76
81
  assert result.html_path == str(tmp_path / ".text-compositor" / "preview.html")
77
82
  assert html.startswith("<!DOCTYPE html>")
78
83
  assert "<title>見出し</title>" in html
79
- assert '<h1 data-line="1">見出し</h1>' in html
84
+ assert '<h1 data-line="1" id="見出し">見出し</h1>' in html
80
85
  assert "<script" not in html
81
86
  # スクリプトは、ブラウザ側でも禁止する(Viewerは、JavaScriptを有効にしたビューで開く)
82
87
  assert "script-src 'none'" in html and "object-src 'none'" in html
@@ -0,0 +1,100 @@
1
+ """PDFのフォント埋め込みの検証(#320)。
2
+
3
+ 生成したPDFの、すべてのフォントが、PDFの中に埋め込まれている(サブセット化されている)ことを確かめる。
4
+ 埋め込まれていないフォントがあると、閲覧する環境に、そのフォントが無いとき、見た目が変わる(文字化け・字体の置き換え)。
5
+
6
+ TypstのPDFは、フォントの辞書を、平文(圧縮しない)で書くため、追加のツール(pdffonts等)なしで、読める。
7
+ フォントは、システムのものを使わず(`ignore_system_fonts=True`、#71)、同梱のNoto Sans JPと、Typst内蔵のフォントだけを使う。
8
+ """
9
+ import re
10
+ import sys
11
+
12
+ import pytest
13
+
14
+ import text_compositor.build as build_mod
15
+ from text_compositor import log
16
+
17
+ @pytest.fixture(autouse=True)
18
+ def _restore_verbosity():
19
+ """`build()`は、-q/-vを、プロセスグローバルへ反映する。他のテストへ、残さない(残すと、後の警告の検査が落ちる)。"""
20
+ yield
21
+ log.set_verbosity(False, False)
22
+
23
+
24
+ # 本文・見出し・太字・斜体・コード(等幅)・数式・表・Graphviz・SVGの文字を含む、1つの文書。
25
+ # Mermaid・PlantUML・D2は、ブラウザ・Java・外部の実行ファイルが要るため、含めない(PDFに入る文字は、同じ経路のため)。
26
+ DOCUMENT = """# フォント埋め込みの検証
27
+
28
+ ## 本文と装飾
29
+
30
+ 通常の本文です。**太字**、*斜体*、***太字の斜体***、`インラインコード(日本語)`。
31
+
32
+ 記号: → ✓ ★ ① ≠ ∞ ± × ÷。ラテン: café。ギリシャ・キリル: αβγ Привет。
33
+
34
+ ```python
35
+ # 日本語のコメント
36
+ def greet(name):
37
+ return f"こんにちは、{name}"
38
+ ```
39
+
40
+ 数式: $E = mc^2$
41
+
42
+ | 名前 | 説明 |
43
+ | --- | --- |
44
+ | りんご | 赤い果物 |
45
+
46
+ ```dot
47
+ digraph G { 開始 -> 処理 -> 終了; }
48
+ ```
49
+
50
+ ```svg
51
+ <svg xmlns="http://www.w3.org/2000/svg" width="240" height="60"><text x="10" y="35" font-family="Arial" font-size="20">SVG text 日本語</text></svg>
52
+ ```
53
+ """
54
+
55
+ # フォントの辞書: `N 0 obj <</Type/FontDescriptor/FontName/ABCDEF+名前/.../FontFile3 N 0 R>>`
56
+ DESCRIPTOR = re.compile(rb"<<(?:(?!>>).)*?/Type\s*/FontDescriptor(?:(?!>>).)*?>>", re.S)
57
+ FONT_NAME = re.compile(rb"/FontName\s*/([^/\s>]+)")
58
+ FONT_FILE = re.compile(rb"/FontFile[23]?\s+\d+\s+\d+\s+R")
59
+ SUBSET_TAG = re.compile(rb"^[A-Z]{6}\+")
60
+
61
+
62
+ def font_descriptors(pdf: bytes):
63
+ """PDFの、フォントの辞書ごとに、(フォント名, 埋め込まれているか)を返す。"""
64
+ result = []
65
+ for match in DESCRIPTOR.finditer(pdf):
66
+ body = match.group(0)
67
+ name = FONT_NAME.search(body)
68
+ result.append((name.group(1) if name else b"(no name)", FONT_FILE.search(body) is not None))
69
+ return result
70
+
71
+
72
+ def test_font_descriptors_reads_the_embedded_flag():
73
+ """読み取りの部品そのもの: 埋め込みのあるものと、無いもの(標準14フォントの参照など)を、区別できる。"""
74
+ embedded = b"5 0 obj\n<</Type/FontDescriptor/FontName/ABCDEF+NotoSansJP-Regular/Flags 4/FontFile3 9 0 R>>\nendobj\n"
75
+ missing = b"6 0 obj\n<</Type/FontDescriptor/FontName/Helvetica/Flags 32>>\nendobj\n"
76
+ other = b"7 0 obj\n<</Type/Font/Subtype/Type0/BaseFont/ABCDEF+X>>\nendobj\n"
77
+ assert font_descriptors(embedded + missing + other) == [(b"ABCDEF+NotoSansJP-Regular", True), (b"Helvetica", False)]
78
+
79
+
80
+ def test_every_font_in_a_real_pdf_is_embedded_and_subset(tmp_path, monkeypatch):
81
+ from text_compositor.deps import ensure_fonts
82
+ try:
83
+ ensure_fonts()
84
+ except SystemExit:
85
+ pytest.skip("fonts are not available")
86
+ md = tmp_path / "fonts.md"
87
+ md.write_text(DOCUMENT, encoding="utf-8")
88
+ monkeypatch.setattr(sys, "argv", ["text-compositor", str(md), "-q"])
89
+ build_mod.build()
90
+ pdf = (tmp_path / "fonts.pdf").read_bytes()
91
+
92
+ fonts = font_descriptors(pdf)
93
+ assert fonts, "no font is found in the PDF"
94
+ names = [name.decode("latin1") for name, _ in fonts]
95
+ not_embedded = [name for name, embedded in fonts if not embedded]
96
+ assert not not_embedded, f"fonts that are not embedded: {not_embedded} (all: {names})"
97
+ not_subset = [name for name, _ in fonts if not SUBSET_TAG.match(name)]
98
+ assert not not_subset, f"fonts that are not subset (no ABCDEF+ tag): {not_subset} (all: {names})"
99
+ # 日本語の本文・見出しは、同梱のNoto Sans JPで出る(システムのフォントに、置き換わらない)
100
+ assert any(b"NotoSansJP" in name for name, _ in fonts), names
@@ -0,0 +1,81 @@
1
+ """PDFのメタデータ(プロパティ)の検証(#317)。
2
+
3
+ `document:`のtitle・subtitle・author・dateが、PDFのTitle・Subject・Author・作成日に入ることを、3つのテンプレート
4
+ (template・paper・slide)で確かめる。TypstのPDFは、情報の辞書を平文で書くため、追加のツールなしで読める
5
+ (フォントの検証(test_pdf_fonts.py)と同じ方法)。
6
+ """
7
+ import re
8
+ import sys
9
+
10
+ import pytest
11
+
12
+ import text_compositor.build as build_mod
13
+ from text_compositor import log
14
+
15
+ TEMPLATES = ["template", "paper", "slide"]
16
+
17
+
18
+ @pytest.fixture(autouse=True)
19
+ def _restore_verbosity():
20
+ """`build()`は、-q/-vを、プロセスグローバルへ反映する。他のテストへ、残さない。"""
21
+ yield
22
+ log.set_verbosity(False, False)
23
+
24
+
25
+ def pdf_info(pdf: bytes) -> dict:
26
+ """PDFの情報の辞書(`/Creator(Typst ...)`を持つ辞書)から、Title・Author・Subject・CreationDateを読む。
27
+ 文字列は、UTF-16BEの16進(`<FEFF...>`)か、括弧で囲んだ文字列。ブックマークの`/Title`とは、別の辞書のため、混ざらない。"""
28
+ match = re.search(rb"<<(?:(?!>>).)*?/Creator\(Typst(?:(?!>>).)*?>>", pdf, re.S)
29
+ assert match, "the document information dictionary is not found"
30
+ body = match.group(0)
31
+ info = {}
32
+ for key in (b"Title", b"Author", b"Subject", b"CreationDate"):
33
+ found = re.search(rb"/" + key + rb"(?:<FEFF([0-9A-Fa-f]*)>|\(([^)]*)\))", body)
34
+ if found:
35
+ info[key.decode()] = (bytes.fromhex(found.group(1).decode()).decode("utf-16-be") if found.group(1) is not None
36
+ else found.group(2).decode("latin1"))
37
+ return info
38
+
39
+
40
+ def build_pdf(tmp_path, monkeypatch, template, document):
41
+ from text_compositor.deps import ensure_fonts
42
+ try:
43
+ ensure_fonts()
44
+ except SystemExit:
45
+ pytest.skip("fonts are not available")
46
+ (tmp_path / "a.md").write_text("# 本文\n\nこんにちは。\n", encoding="utf-8")
47
+ lines = ["document:"] + [f' {key}: "{value}"' for key, value in document.items()]
48
+ lines += ["output:", ' filename: "out.pdf"', ' dir: "."', "template:", f' path: "{template}"',
49
+ "inputs:", ' dir: "."', "chapters:", ' - "a.md"']
50
+ config = tmp_path / "config.yaml"
51
+ config.write_text("\n".join(lines) + "\n", encoding="utf-8")
52
+ monkeypatch.setattr(sys, "argv", ["text-compositor", "--config", str(config), "-q"])
53
+ build_mod.build()
54
+ return (tmp_path / "out.pdf").read_bytes()
55
+
56
+
57
+ def test_pdf_info_reads_hex_and_literal_strings():
58
+ """読み取りの部品そのもの: UTF-16の16進の文字列と、括弧の文字列を読み、ブックマークの/Titleは、読まない。"""
59
+ pdf = (b"2 0 obj\n<</Parent 3 0 R/Title<FEFF672C6587>/Dest 9 0 R>>\nendobj\n"
60
+ b"20 0 obj\n<</Title(Plain)/Author<FEFF5C717530>/Creator(Typst 0.15.0)/CreationDate(D:20260814000000Z)>>\nendobj\n")
61
+ assert pdf_info(pdf) == {"Title": "Plain", "Author": "山田", "CreationDate": "D:20260814000000Z"}
62
+
63
+
64
+ @pytest.mark.parametrize("template", TEMPLATES)
65
+ def test_document_values_are_written_to_the_pdf_properties(tmp_path, monkeypatch, template):
66
+ pdf = build_pdf(tmp_path, monkeypatch, template,
67
+ {"title": "テスト文書", "subtitle": "副題です", "author": "山田 太郎", "date": "2026-08-14"})
68
+ info = pdf_info(pdf)
69
+ assert info["Title"] == "テスト文書"
70
+ assert info["Subject"] == "副題です"
71
+ assert info["Author"] == "山田 太郎"
72
+ assert info["CreationDate"].startswith("D:20260814"), info
73
+
74
+
75
+ def test_a_free_form_date_keeps_the_build_time(tmp_path, monkeypatch):
76
+ """dateが、YYYY-MM-DDでない(自由な文字列。例: 「2026年8月版」)とき、作成日は、従来どおり、ビルドの日時になる(表紙には、文字列のまま出る)。
77
+ title・author・subtitleを書かないときは、設定の既定値(config.pyのDEFAULT_CONFIG。表紙と同じ値)が、そのまま入る。"""
78
+ pdf = build_pdf(tmp_path, monkeypatch, "template", {"date": "2026年8月版"})
79
+ info = pdf_info(pdf)
80
+ assert "CreationDate" in info and not info["CreationDate"].startswith("D:20260814"), info
81
+ assert info["Title"] == "System_Specification", info
@@ -279,7 +279,7 @@ class TestProcess:
279
279
  "path": str(md), "output": str(out), "plugins": plugins}})
280
280
  response = self.receive(proc)
281
281
  assert response["ok"] is True, response
282
- assert response["html"] == str(out) and '<h1 data-line="1">見出し</h1>' in out.read_text(encoding="utf-8")
282
+ assert response["html"] == str(out) and '<h1 data-line="1" id="見出し">見出し</h1>' in out.read_text(encoding="utf-8")
283
283
  # 失敗(存在しないファイル)でも、ワーカーは、同じまま動き続ける
284
284
  self.send(proc, {"id": 2, "method": "render_html", "params": {"path": str(tmp_path / "none.md")}})
285
285
  failed = self.receive(proc)
@@ -193,6 +193,8 @@ class _TokenRenderer(RendererHTML):
193
193
  def heading_open(self, tokens, idx, options, env):
194
194
  if self.owner._title is None and idx + 1 < len(tokens) and tokens[idx + 1].type == "inline":
195
195
  self.owner._title = tokens[idx + 1].content
196
+ if idx + 1 < len(tokens) and tokens[idx + 1].type == "inline":
197
+ tokens[idx].attrSet("id", self.owner._heading_id(tokens[idx + 1].content))
196
198
  return self.renderToken(tokens, idx, options, env)
197
199
 
198
200
  def blockquote_open(self, tokens, idx, options, env):
@@ -274,6 +276,14 @@ class HtmlRenderer(TypstRenderer):
274
276
  # 使う(#170)。存在しないファイルも含める(あとから作られたときに、更新できるように)。
275
277
  self.dependencies: set = set()
276
278
 
279
+ def _heading_id(self, text: str) -> str:
280
+ """見出しの`id`(文書内リンク`#見出し`の飛び先)。GitHubと同じ規則: 小文字にし、文字・数字・`_`・`-`・空白だけを残し、
281
+ 空白を`-`にする。同じ見出しが続くときは、`-1`・`-2`を付ける。Obunzuで、`[…](#見出し)`が飛べるようにするため(#361)。"""
282
+ base = re.sub(r"\s", "-", re.sub(r"[^\w\s-]", "", text.strip().lower())) or "section"
283
+ count = self._heading_ids.get(base, 0)
284
+ self._heading_ids[base] = count + 1
285
+ return base if count == 0 else f"{base}-{count}"
286
+
277
287
  def _abs_line(self, token) -> Optional[int]:
278
288
  """トークンの、原稿での行番号(1始まり)。行を持たないトークンは、None。"""
279
289
  return token.map[0] + 1 + self._line_base if token.map else None
@@ -288,6 +298,7 @@ class HtmlRenderer(TypstRenderer):
288
298
  self.current_dir = os.path.dirname(md_path)
289
299
  self.front_matter = {}
290
300
  self._title = None
301
+ self._heading_ids = {}
291
302
  self._pagebreaks = 0
292
303
  self._line_base = 0
293
304
  self.dependencies = set()
@@ -107,3 +107,12 @@
107
107
  } else {
108
108
  none
109
109
  }
110
+
111
+ // PDFのメタデータ(#317)用: 日付(document.date)の文字列を、datetimeにする。YYYY-MM-DD(例: 2026-08-14)だけを解釈し、
112
+ // それ以外(自由な文字列・未指定)は、autoにする(Typstが、ビルドの日時を入れる。従来どおり)。
113
+ #let meta-date(d) = if type(d) == str {
114
+ let m = d.trim().match(regex("^(\d{4})-(\d{2})-(\d{2})$"))
115
+ if m != none {
116
+ datetime(year: int(m.captures.at(0)), month: int(m.captures.at(1)), day: int(m.captures.at(2)))
117
+ } else { auto }
118
+ } else { auto }
@@ -5,7 +5,7 @@
5
5
  //
6
6
  // 補助関数(fit-image・render-graph・callout・render-header・render-footer・render-background)は
7
7
  // template.typと同一実装のため_common.typから読み込む(#63)。
8
- #import "_common.typ": fit-image, render-graph, callout, render-header, render-footer, render-background
8
+ #import "_common.typ": fit-image, render-graph, callout, render-header, render-footer, render-background, meta-date
9
9
 
10
10
  #let conf(
11
11
  title: none,
@@ -31,6 +31,14 @@
31
31
  logo: none,
32
32
  doc,
33
33
  ) = {
34
+ // PDFのメタデータ(プロパティ)。document:のtitle・subtitle・author・dateを、Title・Subject・Author・作成日に入れる(#317)。
35
+ // 未指定の項目は、設定しない(Typstの既定のまま)。dateは、YYYY-MM-DDのときだけ、作成日にする。
36
+ set document(
37
+ title: if title != none { title } else { auto },
38
+ description: if subtitle != none { subtitle } else { none },
39
+ author: if author != none { author } else { () },
40
+ date: meta-date(date),
41
+ )
34
42
  set text(font: "Noto Sans JP", size: 10pt)
35
43
 
36
44
  show raw.where(lang: "dot"): it => if graphviz { align(center)[#render-graph(it.text)] } else { it }
@@ -1,6 +1,6 @@
1
1
  // fit-image・render-graph・callout・render-backgroundは、template.typと同一実装のため_common.typに
2
2
  // 置いている(#63)。render-header/render-footerはスライド固有の見た目のため、このファイルに持つ。
3
- #import "_common.typ": fit-image, render-graph, callout, render-background
3
+ #import "_common.typ": fit-image, render-graph, callout, render-background, meta-date
4
4
 
5
5
  // 本文ページのヘッダー・フッター(#42)。template.typと同じ関数名でエクスポートし、build.py側が
6
6
  // テンプレート種別を意識せず同じ呼び出し方でチャプター単位の上書きを再発行できるようにする。
@@ -56,6 +56,14 @@
56
56
  logo: none,
57
57
  doc,
58
58
  ) = {
59
+ // PDFのメタデータ(プロパティ)。document:のtitle・subtitle・author・dateを、Title・Subject・Author・作成日に入れる(#317)。
60
+ // 未指定の項目は、設定しない(Typstの既定のまま)。dateは、YYYY-MM-DDのときだけ、作成日にする。
61
+ set document(
62
+ title: if title != none { title } else { auto },
63
+ description: if subtitle != none { subtitle } else { none },
64
+ author: if author != none { author } else { () },
65
+ date: meta-date(date),
66
+ )
59
67
  // フォント設定(CJKフォントは build.py が取得・キャッシュした Noto Sans JP を --font-path 経由で渡す。
60
68
  // OSフォントは直接指定しない。Noto Sans JP に無いグリフはTypstが自動でシステムフォントにフォールバックする)
61
69
  set text(font: "Noto Sans JP", size: 18pt)
@@ -1,7 +1,7 @@
1
1
  // 補助関数(fit-image・render-graph・callout・render-header・render-footer・render-background)は
2
2
  // _common.typへ切り出した(#63)。build.pyが生成するコードはこのモジュールからも
3
3
  // 同じ名前でimportするため、ここで読み込むだけで(再エクスポートされて)そのまま使える。
4
- #import "_common.typ": fit-image, render-graph, callout, render-header, render-footer, render-background
4
+ #import "_common.typ": fit-image, render-graph, callout, render-header, render-footer, render-background, meta-date
5
5
 
6
6
  #let conf(
7
7
  title: none,
@@ -23,6 +23,14 @@
23
23
  logo: none,
24
24
  doc,
25
25
  ) = {
26
+ // PDFのメタデータ(プロパティ)。document:のtitle・subtitle・author・dateを、Title・Subject・Author・作成日に入れる(#317)。
27
+ // 未指定の項目は、設定しない(Typstの既定のまま)。dateは、YYYY-MM-DDのときだけ、作成日にする。
28
+ set document(
29
+ title: if title != none { title } else { auto },
30
+ description: if subtitle != none { subtitle } else { none },
31
+ author: if author != none { author } else { () },
32
+ date: meta-date(date),
33
+ )
26
34
  // フォント設定(CJKフォントは build.py が取得・キャッシュした Noto Sans JP を --font-path 経由で渡す。
27
35
  // OSフォントは直接指定しない。Noto Sans JP に無いグリフはTypstが自動でシステムフォントにフォールバックする)
28
36
  set text(font: "Noto Sans JP", size: 10.5pt)
@@ -8,7 +8,7 @@ GUI版Viewerが、Pythonをサブプロセスとして常駐させ、これに
8
8
  JSONオブジェクトが返る。文字コードは、UTF-8。ワーカーは、依頼を1つずつ順に処理する。
9
9
 
10
10
  起動時に、準備ができたことを示すイベントを1行出す。
11
- {"event": "ready", "protocol": 1, "version": "0.4.2"}
11
+ {"event": "ready", "protocol": 1, "version": "0.4.3"}
12
12
 
13
13
  依頼: {"id": <任意の値。応答にそのまま返る>, "method": <メソッド名>, "params": {...}}
14
14
  - `build` : Markdownをビルドする。params: `path`(必須)・`output`・`template`・`plugins`・
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: text-compositor
3
- Version: 0.4.2
3
+ Version: 0.4.3
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
@@ -192,6 +192,16 @@ This generates `sample/SampleDocument.pdf`. Two more samples show other template
192
192
 
193
193
  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
194
 
195
+ ## Policy for adding diagram notations
196
+
197
+ 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:
198
+
199
+ - **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.
200
+ - **A heavy runtime.** It needs Docker or a similar container, or a large runtime that most users do not have.
201
+ - **An external service.** It sends the manuscript to a server.
202
+
203
+ Candidates that fail these checks are closed as "not planned" on [GitHub Issues](https://github.com/tokudiro/text-compositor/issues), with the reason recorded.
204
+
195
205
  ## License
196
206
 
197
207
  [MIT License](LICENSE)
@@ -33,6 +33,8 @@ tests/test_math_render.py
33
33
  tests/test_mermaid_resident.py
34
34
  tests/test_pandoc_compat.py
35
35
  tests/test_paper_template.py
36
+ tests/test_pdf_fonts.py
37
+ tests/test_pdf_metadata.py
36
38
  tests/test_pikchr_render.py
37
39
  tests/test_plantuml_batch.py
38
40
  tests/test_renderer_structure.py
File without changes