markpublish 2.3.0__tar.gz → 2.3.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.
Files changed (103) hide show
  1. {markpublish-2.3.0/src/markpublish.egg-info → markpublish-2.3.2}/PKG-INFO +1 -1
  2. {markpublish-2.3.0 → markpublish-2.3.2}/pyproject.toml +1 -1
  3. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/__init__.py +1 -1
  4. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/config/loader.py +75 -6
  5. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/cheatsheet/de/01_config.md +5 -0
  6. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/cheatsheet/de/markpublish.yaml +1 -1
  7. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/cheatsheet/en/01_config.md +5 -0
  8. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/cheatsheet/en/markpublish.yaml +1 -1
  9. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/manual/de/chapters/04_configuration_guide.md +71 -21
  10. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/manual/de/chapters/07_appendix_yaml_spec.md +1 -1
  11. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/manual/de/markpublish.yaml +1 -1
  12. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/manual/en/chapters/04_configuration_guide.md +71 -20
  13. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/manual/en/chapters/07_appendix_yaml_spec.md +1 -1
  14. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/manual/en/markpublish.yaml +1 -1
  15. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/locale/de.yaml +10 -0
  16. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/locale/en.yaml +10 -0
  17. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/markdown/engine.py +7 -1
  18. markpublish-2.3.2/src/markpublish/markdown/variables.py +408 -0
  19. {markpublish-2.3.0 → markpublish-2.3.2/src/markpublish.egg-info}/PKG-INFO +1 -1
  20. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish.egg-info/SOURCES.txt +1 -0
  21. markpublish-2.3.2/tests/test_file_ref.py +128 -0
  22. markpublish-2.3.2/tests/test_variables.py +243 -0
  23. markpublish-2.3.0/src/markpublish/markdown/variables.py +0 -122
  24. markpublish-2.3.0/tests/test_variables.py +0 -103
  25. {markpublish-2.3.0 → markpublish-2.3.2}/LICENSE +0 -0
  26. {markpublish-2.3.0 → markpublish-2.3.2}/README.md +0 -0
  27. {markpublish-2.3.0 → markpublish-2.3.2}/setup.cfg +0 -0
  28. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/cli.py +0 -0
  29. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/config/models.py +0 -0
  30. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/cheatsheet/de/02_templates.md +0 -0
  31. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/cheatsheet/en/02_templates.md +0 -0
  32. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/init/de/kapitel_1.md +0 -0
  33. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/init/de/kapitel_2.md +0 -0
  34. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/init/de/markpublish.yaml +0 -0
  35. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/init/en/chapter_1.md +0 -0
  36. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/init/en/chapter_2.md +0 -0
  37. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/init/en/markpublish.yaml +0 -0
  38. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/manual/de/chapters/01_introduction.md +0 -0
  39. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/manual/de/chapters/02_quickstart.md +0 -0
  40. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/manual/de/chapters/03_cli_reference.md +0 -0
  41. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/manual/de/chapters/05_template_system.md +0 -0
  42. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/manual/de/chapters/06_markdown_features.md +0 -0
  43. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/manual/de/chapters/08_appendix_troubleshooting.md +0 -0
  44. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/manual/en/chapters/01_introduction.md +0 -0
  45. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/manual/en/chapters/02_quickstart.md +0 -0
  46. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/manual/en/chapters/03_cli_reference.md +0 -0
  47. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/manual/en/chapters/05_template_system.md +0 -0
  48. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/manual/en/chapters/06_markdown_features.md +0 -0
  49. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/manual/en/chapters/08_appendix_troubleshooting.md +0 -0
  50. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/docs/manual/img/markpublish_logo.png +0 -0
  51. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/entry.py +0 -0
  52. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/i18n.py +0 -0
  53. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/i18n.yaml +0 -0
  54. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/markdown/__init__.py +0 -0
  55. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/markdown/alerts.py +0 -0
  56. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/markdown/pattern.py +0 -0
  57. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/markdown/toc.py +0 -0
  58. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/markdown/typst_serializer.py +0 -0
  59. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/renderers/__init__.py +0 -0
  60. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/renderers/base.py +0 -0
  61. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/renderers/pdf.py +0 -0
  62. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/syslang.py +0 -0
  63. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/templates/__init__.py +0 -0
  64. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/templates/contract.py +0 -0
  65. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/templates/default/i18n.yaml +0 -0
  66. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/templates/default/pdf/assets/icons/caution.svg +0 -0
  67. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/templates/default/pdf/assets/icons/checkbox-check.svg +0 -0
  68. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/templates/default/pdf/assets/icons/important.svg +0 -0
  69. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/templates/default/pdf/assets/icons/note.svg +0 -0
  70. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/templates/default/pdf/assets/icons/tip.svg +0 -0
  71. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/templates/default/pdf/assets/icons/warning.svg +0 -0
  72. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/templates/default/pdf/fonts/NotoSansMono-VariableFont_wdth,wght.ttf +0 -0
  73. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/templates/default/pdf/fonts/OFL-NotoSansMono.txt +0 -0
  74. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/templates/default/pdf/fonts/OFL.txt +0 -0
  75. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/templates/default/pdf/fonts/OpenSans-Italic-VariableFont_wdth,wght.ttf +0 -0
  76. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/templates/default/pdf/fonts/OpenSans-VariableFont_wdth,wght.ttf +0 -0
  77. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/templates/default/pdf/i18n.yaml +0 -0
  78. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/templates/default/pdf/template.typ +0 -0
  79. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/templates/resolver.py +0 -0
  80. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish/ui.py +0 -0
  81. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish.egg-info/dependency_links.txt +0 -0
  82. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish.egg-info/entry_points.txt +0 -0
  83. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish.egg-info/requires.txt +0 -0
  84. {markpublish-2.3.0 → markpublish-2.3.2}/src/markpublish.egg-info/top_level.txt +0 -0
  85. {markpublish-2.3.0 → markpublish-2.3.2}/tests/test_cli.py +0 -0
  86. {markpublish-2.3.0 → markpublish-2.3.2}/tests/test_config.py +0 -0
  87. {markpublish-2.3.0 → markpublish-2.3.2}/tests/test_defensive_review.py +0 -0
  88. {markpublish-2.3.0 → markpublish-2.3.2}/tests/test_figures.py +0 -0
  89. {markpublish-2.3.0 → markpublish-2.3.2}/tests/test_header_footer.py +0 -0
  90. {markpublish-2.3.0 → markpublish-2.3.2}/tests/test_i18n.py +0 -0
  91. {markpublish-2.3.0 → markpublish-2.3.2}/tests/test_i18n_cascade.py +0 -0
  92. {markpublish-2.3.0 → markpublish-2.3.2}/tests/test_markdown.py +0 -0
  93. {markpublish-2.3.0 → markpublish-2.3.2}/tests/test_packaging.py +0 -0
  94. {markpublish-2.3.0 → markpublish-2.3.2}/tests/test_pattern.py +0 -0
  95. {markpublish-2.3.0 → markpublish-2.3.2}/tests/test_regressions.py +0 -0
  96. {markpublish-2.3.0 → markpublish-2.3.2}/tests/test_renderers.py +0 -0
  97. {markpublish-2.3.0 → markpublish-2.3.2}/tests/test_templates.py +0 -0
  98. {markpublish-2.3.0 → markpublish-2.3.2}/tests/test_theme_contract.py +0 -0
  99. {markpublish-2.3.0 → markpublish-2.3.2}/tests/test_theme_layout.py +0 -0
  100. {markpublish-2.3.0 → markpublish-2.3.2}/tests/test_toc.py +0 -0
  101. {markpublish-2.3.0 → markpublish-2.3.2}/tests/test_typst_compile_cases.py +0 -0
  102. {markpublish-2.3.0 → markpublish-2.3.2}/tests/test_typst_serializer.py +0 -0
  103. {markpublish-2.3.0 → markpublish-2.3.2}/tests/test_ui.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: markpublish
3
- Version: 2.3.0
3
+ Version: 2.3.2
4
4
  Summary: Modern, modular, and extensible Markdown-to-PDF publishing tool powered by Typst
5
5
  Author-email: Frank Winter <studio@frankwinter.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "markpublish"
7
- version = "2.3.0"
7
+ version = "2.3.2"
8
8
  description = "Modern, modular, and extensible Markdown-to-PDF publishing tool powered by Typst"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -2,5 +2,5 @@
2
2
  markpublish - Modern, modular Markdown-to-PDF publishing tool powered by Typst.
3
3
  """
4
4
 
5
- __version__ = "2.3.0"
5
+ __version__ = "2.3.2"
6
6
 
@@ -6,8 +6,11 @@ from __future__ import annotations
6
6
 
7
7
  import copy
8
8
  import datetime
9
+ import urllib.error
10
+ import urllib.request
9
11
  from pathlib import Path
10
- from typing import Any, Dict, Optional, Union
12
+ from typing import Any, Dict, FrozenSet, Optional, Union
13
+ from urllib.parse import urljoin, urlparse
11
14
 
12
15
  import yaml
13
16
 
@@ -25,6 +28,70 @@ def format_current_date(language: Optional[str] = None) -> str:
25
28
  return now.strftime("%Y-%m-%d")
26
29
 
27
30
 
31
+ #: Woher eine Konfiguration stammt: Verzeichnis einer Datei oder eine URL.
32
+ #: Relative Verweise mit `!file` loesen sich dagegen auf.
33
+ Base = Union[Path, str]
34
+
35
+ FILE_TAG = "!file"
36
+ YAML_SUFFIXES = (".yaml", ".yml")
37
+ URL_TIMEOUT = 10
38
+
39
+
40
+ def _is_url(src: str) -> bool:
41
+ return src.startswith(("http://", "https://"))
42
+
43
+
44
+ def _resolve_ref(src: str, base: Base) -> Union[Path, str]:
45
+ """Absoluter Ort eines `!file`-Verweises, relativ zu der Datei, in der er steht."""
46
+ if _is_url(src):
47
+ return src
48
+ if isinstance(base, str):
49
+ return urljoin(base, src)
50
+ return (base / src).resolve()
51
+
52
+
53
+ def _read_ref(target: Union[Path, str], src: str) -> str:
54
+ if isinstance(target, str):
55
+ try:
56
+ with urllib.request.urlopen(target, timeout=URL_TIMEOUT) as response:
57
+ return response.read().decode("utf-8")
58
+ except (urllib.error.URLError, OSError, UnicodeDecodeError) as exc:
59
+ raise ValueError(t("err.config.file_ref_url", src=src, reason=exc)) from exc
60
+ if not target.is_file():
61
+ raise FileNotFoundError(t("err.config.file_ref_missing", src=src, path=target))
62
+ return target.read_text(encoding="utf-8")
63
+
64
+
65
+ def _load_yaml(text: Any, base: Base, seen: FrozenSet[Union[Path, str]] = frozenset()) -> Any:
66
+ """
67
+ Wie `yaml.safe_load`, zusaetzlich mit `!file "pfad-oder-url"`.
68
+
69
+ Ein Verweis auf .yaml/.yml setzt deren Inhalt an seine Stelle, jede
70
+ andere Datei ihren Text. Nachgeladene YAML-Dateien duerfen selbst wieder
71
+ `!file` enthalten; deren Pfade gelten relativ zu ihnen. `seen` haelt die
72
+ Kette der offenen Dateien -- dieselbe Datei in zwei Zweigen ist erlaubt,
73
+ nur ein Verweis zurueck in die eigene Kette nicht.
74
+ """
75
+
76
+ def construct(loader: yaml.SafeLoader, node: yaml.Node) -> Any:
77
+ src = loader.construct_scalar(node)
78
+ target = _resolve_ref(src, base)
79
+ if target in seen:
80
+ raise ValueError(t("err.config.file_ref_cycle", src=src, path=target))
81
+ content = _read_ref(target, src)
82
+ name = urlparse(target).path if isinstance(target, str) else target.name
83
+ if not name.lower().endswith(YAML_SUFFIXES):
84
+ return content.rstrip("\n")
85
+ child_base = target if isinstance(target, str) else target.parent
86
+ return _load_yaml(content, child_base, seen | {target})
87
+
88
+ # Eigene Loader-Klasse je Aufruf: der Konstruktor haengt an `base` und
89
+ # `seen`, und SafeLoader selbst soll unveraendert bleiben.
90
+ loader_cls = type("_FileLoader", (yaml.SafeLoader,), {})
91
+ loader_cls.add_constructor(FILE_TAG, construct)
92
+ return yaml.load(text, Loader=loader_cls) # noqa: S506 - SafeLoader-Unterklasse
93
+
94
+
28
95
  def load_config(config_path_or_str: Union[str, Path, Dict[str, Any]]) -> MarkpublishConfig:
29
96
  """
30
97
  Loads and validates a markpublish configuration.
@@ -44,8 +111,9 @@ def load_config(config_path_or_str: Union[str, Path, Dict[str, Any]]) -> Markpub
44
111
  elif isinstance(config_path_or_str, Path):
45
112
  if not config_path_or_str.is_file():
46
113
  raise FileNotFoundError(t("err.config.not_found_file", path=config_path_or_str))
47
- with open(config_path_or_str, "r", encoding="utf-8") as f:
48
- loaded = yaml.safe_load(f)
114
+ config_path = config_path_or_str.resolve()
115
+ with open(config_path, "r", encoding="utf-8") as f:
116
+ loaded = _load_yaml(f, config_path.parent, frozenset({config_path}))
49
117
  if loaded is None:
50
118
  raise ValueError(t("err.config.expected_mapping", type="empty"))
51
119
  if not isinstance(loaded, dict):
@@ -56,8 +124,9 @@ def load_config(config_path_or_str: Union[str, Path, Dict[str, Any]]) -> Markpub
56
124
  # YAML-Text mit Zeilenumbruch ist nie ein Pfad; ein langer liesse is_file()
57
125
  # unter Linux/macOS vor Python 3.14 mit ENAMETOOLONG scheitern.
58
126
  if "\n" not in config_path_or_str and candidate_path.is_file():
59
- with open(candidate_path, "r", encoding="utf-8") as f:
60
- loaded = yaml.safe_load(f)
127
+ config_path = candidate_path.resolve()
128
+ with open(config_path, "r", encoding="utf-8") as f:
129
+ loaded = _load_yaml(f, config_path.parent, frozenset({config_path}))
61
130
  if loaded is None:
62
131
  raise ValueError(t("err.config.expected_mapping", type="empty"))
63
132
  if not isinstance(loaded, dict):
@@ -78,7 +147,7 @@ def load_config(config_path_or_str: Union[str, Path, Dict[str, Any]]) -> Markpub
78
147
 
79
148
  # Versuche als YAML-String zu parsen
80
149
  try:
81
- loaded = yaml.safe_load(config_path_or_str)
150
+ loaded = _load_yaml(config_path_or_str, Path.cwd())
82
151
  except yaml.YAMLError as exc:
83
152
  raise ValueError(str(exc)) from exc
84
153
 
@@ -34,6 +34,7 @@ Metadaten und globale Layout-Schalter. Pflichtangabe ist allein `title`.
34
34
  | `status` | – | Dokumentstatus (z. B. `Entwurf`, `Freigegeben`) |
35
35
  | `copyright` | – | Copyright-Hinweis auf dem Deckblatt |
36
36
  | `language` | Systemsprache | ISO-Sprachcode für statische Beschriftungen (`de`, `en`) |
37
+ | `custom` | – | Eigene Werte für Platzhalter im Text (`{{custom.a.b}}`), auch verschachtelt |
37
38
  | `cover` | `false` | Deckblatt erzeugen |
38
39
  | `header` / `footer` | `true` | Kopf- und Fußzeilen aktivieren |
39
40
  | `document_toc` | `full` | Haupt-Inhaltsverzeichnis: `none`, `full` oder Tiefe als Zahl |
@@ -49,6 +50,10 @@ Besteht das Dokument aus **genau einem Kapitel**, lässt das Haupt-Inhaltsverzei
49
50
 
50
51
  Eigene Zusatzfelder unter `document:` (`abteilung: "F&E"`) erreichen das Theme über das Wörterbuch `meta` (`meta.at("abteilung").value`). Statische Beschriftungen gehören in die `i18n.yaml`, nicht hierher.
51
52
 
53
+ Im Markdown setzt `{{name}}` eine Angabe aus `document:` ein (`{{author}}`, `{{custom.verfahren.name}}`). Werte unter `custom` erreichen das Theme nicht. Ein Platzhalter ohne Wert bricht den Bau ab; `\{{author}}` bleibt wörtlich stehen.
54
+
55
+ `schlüssel: !file "pfad-oder-url"` lädt einen Wert aus einer anderen Datei, überall in der Konfiguration. YAML-Dateien liefern einen verschachtelten Wert, jede andere Datei ihren Text. Relative Pfade gelten ab der verweisenden Datei.
56
+
52
57
  ## parts
53
58
 
54
59
  Ein Part klammert Kapitel und gibt Einstellungen an sie weiter. Ohne `break_before` zeigt er sich selbst nicht.
@@ -11,7 +11,7 @@ document:
11
11
  summary: "Nachschlagewerk zu allen Schlüsseln der markpublish.yaml und den Bausteinen eines Themes."
12
12
  author: "Frank Winter"
13
13
  copyright: "© 2026 Frank Winter"
14
- version: "2.3.0"
14
+ version: "2.3.2"
15
15
  date: "auto"
16
16
  language: "de"
17
17
 
@@ -34,6 +34,7 @@ Metadata and global layout toggles. Only `title` is required.
34
34
  | `status` | – | Document status (e.g. `Draft`, `Approved`) |
35
35
  | `copyright` | – | Copyright notice on the cover page |
36
36
  | `language` | system language | ISO code for static labels (`de`, `en`) |
37
+ | `custom` | – | Own values for placeholders in the text (`{{custom.a.b}}`), may be nested |
37
38
  | `cover` | `false` | Generate a cover page |
38
39
  | `header` / `footer` | `true` | Running headers and footers |
39
40
  | `document_toc` | `full` | Main table of contents: `none`, `full` or a depth |
@@ -49,6 +50,10 @@ If the document consists of **exactly one chapter**, the main table of contents
49
50
 
50
51
  Custom fields under `document:` (`department: "R&D"`) reach the theme through the `meta` dictionary (`meta.at("department").value`). Static labels belong in `i18n.yaml`, not here.
51
52
 
53
+ In Markdown, `{{name}}` inserts a value from `document:` (`{{author}}`, `{{custom.procedure.name}}`). Values under `custom` do not reach the theme. A placeholder without a value stops the build; `\{{author}}` stays as written.
54
+
55
+ `key: !file "path-or-url"` loads a value from another file, anywhere in the configuration. YAML files supply a nested value, any other file its text. Relative paths resolve against the referencing file.
56
+
52
57
  ## parts
53
58
 
54
59
  A part brackets chapters and passes settings down to them. Without `break_before` it does not show itself.
@@ -11,7 +11,7 @@ document:
11
11
  summary: "Reference for every key of markpublish.yaml and the building blocks of a theme."
12
12
  author: "Frank Winter"
13
13
  copyright: "© 2026 Frank Winter"
14
- version: "2.3.0"
14
+ version: "2.3.2"
15
15
  date: "auto"
16
16
  language: "en"
17
17
 
@@ -41,7 +41,35 @@ Globale Verzeichnisvorgaben
41
41
  Nummerierungsregeln
42
42
  : Vorgaben zur automatischen Nummerierung (`autonum_pattern`, `autonum_reset`).
43
43
 
44
- #### Eigene Metadatenfelder
44
+ ### Parts (Abschnitte)
45
+
46
+ Ein Dokument wird durch `parts:` in übergeordnete Abschnitte gegliedert (z. B. „Hauptteil“, „Fallstudien“, „Anhänge“):
47
+
48
+ * Ein Dokument muss immer über `parts:` aufgebaut sein; ein flaches `chapters:` direkt auf oberster Ebene wird abgewiesen.
49
+
50
+ * Ein Abschnitt fasst logisch zusammengehörige Kapitel zusammen.
51
+
52
+ * Ohne Angabe tritt ein Abschnitt selbst nicht in Erscheinung (`break_before: "none"`): er klammert seine Kapitel und vererbt seine Einstellungen, belegt aber keine Seite und erscheint nicht im Inhaltsverzeichnis. Mit `break_before: "divider"` erhält er eine gestaltete Trennseite, mit `break_before: "page"` eine Überschrift auf einer neuen Seite.
53
+
54
+ * Auf Abschnittsebene gesetzte Einstellungen (z. B. Verzeichnistiefe oder ein `autonum_pattern`) vererben sich automatisch auf alle darin enthaltenen Kapitel.
55
+
56
+ * Auf `parts:` sind ausschließlich die deklarierten Konfigurationsschlüssel erlaubt. Unbekannte Schlüssel (wie `break_befor`) werden mit Ähnlichkeitsvorschlägen abgelehnt, um unbemerkte Fehlkonfigurationen auszuschließen.
57
+
58
+ ### Chapters (Kapitel)
59
+
60
+ Die Liste `chapters:` innerhalb eines Abschnitts verweist auf die eigentlichen Markdown-Inhaltsdateien:
61
+
62
+ * Jedes Kapitel verweist auf seine Markdown-Datei (`file:`). Autoren können in ihren Markdown-Dateien ganz natürlich mit einer `#`-Überschrift beginnen.
63
+
64
+ * Über **`show_title:`** (`true` oder `false`, Standard: `true`) lässt sich steuern, ob die `#`-Überschrift der Markdown-Datei auf der Inhaltsseite gerendert werden soll. Wird vor dem Kapitel eine Trennseite erzeugt (`break_before: "divider"`), kann mit `show_title: false` verhindert werden, dass der Titel auf der Folgeseite doppelt erscheint – die Inhaltsseite beginnt dann direkt mit dem Fließtext oder der ersten Zwischenüberschrift.
65
+
66
+ * Über die optionalen Schlüssel **`toc_title:`** (für Inhaltsverzeichnisse und Kopfzeilen) und **`divider_title:`** (für Trennseiten) können gezielt abweichende Titel vergeben werden (z. B. eine prägnante Kurzform im Inhaltsverzeichnis gegenüber einer ausführlichen Überschrift auf der Textseite). Fehlen die Schlüssel, erben beide automatisch die `#`-Überschrift der Datei.
67
+
68
+ * Soll eine Trennseite erzeugt werden (`break_before: "divider"`) oder das Kapitel im Inhaltsverzeichnis gelistet werden, muss mindestens eine `#`-Überschrift oder der entsprechende Titelschlüssel (`divider_title` / `toc_title`) vorhanden sein, andernfalls bricht der Build mit einer klaren Fehlermeldung ab.
69
+
70
+ * Kapitel können über das Feld `break_before` steuern, ob vor ihnen eine Trennseite erzeugt wird, ein einfacher Seitenwechsel erfolgt oder der Text nahtlos anschließt.
71
+
72
+ ## Eigene Metadatenfelder
45
73
 
46
74
  Unter `document:` sind über die bekannten Schlüssel hinaus **beliebige eigene Felder** erlaubt. Sie erreichen das Theme, werden aber nicht von selbst gedruckt:
47
75
 
@@ -76,7 +104,7 @@ Drei Dinge sind dabei zu wissen:
76
104
 
77
105
  * **Der Typ bleibt erhalten.** Ein `true` ist ein Wahrheitswert und kein Text: Das Deckblatt setzt `Ja` beziehungsweise `Yes`, je nach Dokumentsprache. Die beiden Wörter stehen als `bool_true` und `bool_false` in der i18n-Kaskade.
78
106
 
79
- #### Platzhalter im Text
107
+ ## Platzhalter im Text
80
108
 
81
109
  Im Markdown setzt `{{name}}` eine Angabe aus `document:` ein, etwa `{{author}}`, `{{version}}` oder `{{date}}` (bei `"auto"` das errechnete Datum). Das gilt auch für eigene Metadatenfelder. Werte, die nur im Text gebraucht werden, gehören unter `custom:`. Dort dürfen sie verschachtelt sein und werden mit Punkten angesprochen:
82
110
 
@@ -94,36 +122,58 @@ Ansprechpartner: {{author}}, {{custom.support_mail}}.
94
122
  Dieses Dokument beschreibt {{custom.verfahren.name}}.
95
123
  ```
96
124
 
97
- Werte unter `custom` erreichen das Theme nicht. Ein Platzhalter ohne Wert, auch ein vertippter, bricht den Bau mit Angabe der Datei ab. In Code (`` `{{author}}` `` oder Codeblock) bleibt der Platzhalter wörtlich stehen, im Fließtext mit vorangestelltem Backslash: `\{{author}}` ergibt `{{author}}`.
98
-
99
- ### Parts (Abschnitte)
100
-
101
- Ein Dokument wird durch `parts:` in übergeordnete Abschnitte gegliedert (z. B. „Hauptteil“, „Fallstudien“, „Anhänge“):
102
-
103
- * Ein Dokument muss immer über `parts:` aufgebaut sein; ein flaches `chapters:` direkt auf oberster Ebene wird abgewiesen.
125
+ Ist ein eingesetzter Wert eine E-Mail-Adresse oder eine URL, wird er wie direkt geschriebener Text ein Link; steht der Platzhalter schon in einem Link, bleibt es bei diesem. Werte unter `custom` erreichen das Theme nicht. Ein Platzhalter ohne Wert, auch ein vertippter, bricht den Bau mit Angabe der Datei ab. In Code (`` `{{author}}` `` oder Codeblock) bleibt der Platzhalter wörtlich stehen, im Fließtext mit vorangestelltem Backslash: `\{{author}}` ergibt `{{author}}`.
104
126
 
105
- * Ein Abschnitt fasst logisch zusammengehörige Kapitel zusammen.
127
+ ### Schleifen und Kurznamen
106
128
 
107
- * Ohne Angabe tritt ein Abschnitt selbst nicht in Erscheinung (`break_before: "none"`): er klammert seine Kapitel und vererbt seine Einstellungen, belegt aber keine Seite und erscheint nicht im Inhaltsverzeichnis. Mit `break_before: "divider"` erhält er eine gestaltete Trennseite, mit `break_before: "page"` eine Überschrift auf einer neuen Seite.
129
+ `{% for %}` wiederholt die Zeilen bis `{% endfor %}` für jeden Eintrag einer Gruppe unter `custom`, in der Reihenfolge der YAML-Datei. So entsteht etwa eine Tabelle mit einer Zeile je Rolle:
108
130
 
109
- * Auf Abschnittsebene gesetzte Einstellungen (z. B. Verzeichnistiefe oder ein `autonum_pattern`) vererben sich automatisch auf alle darin enthaltenen Kapitel.
110
-
111
- * Auf `parts:` sind ausschließlich die deklarierten Konfigurationsschlüssel erlaubt. Unbekannte Schlüssel (wie `break_befor`) werden mit Ähnlichkeitsvorschlägen abgelehnt, um unbemerkte Fehlkonfigurationen auszuschließen.
131
+ ```yaml
132
+ document:
133
+ custom:
134
+ app:
135
+ roles:
136
+ application_owner: {label: "Verfahrensverantwortliche/r", name: "A. Muster"}
137
+ technical_administrator: {label: "Technische Administration", name: "B. Beispiel"}
138
+ ```
112
139
 
113
- ### Chapters (Kapitel)
140
+ ```markdown
141
+ | Rolle | Name | Schlüssel |
142
+ | :--- | :--- | :--- |
143
+ {% for (role, key) in custom.app.roles %}
144
+ | {{role.label}} | {{role.name}} | {{key}} |
145
+ {% endfor %}
146
+ ```
114
147
 
115
- Die Liste `chapters:` innerhalb eines Abschnitts verweist auf die eigentlichen Markdown-Inhaltsdateien:
148
+ `{% set %}` gibt einem langen Pfad einen Kurznamen, der bis zum Ende der Datei gilt (innerhalb einer Schleife bis zu deren Ende):
116
149
 
117
- * Jedes Kapitel verweist auf seine Markdown-Datei (`file:`). Autoren können in ihren Markdown-Dateien ganz natürlich mit einer `#`-Überschrift beginnen.
150
+ ```markdown
151
+ {% set owner = custom.app.roles.application_owner %}
152
+ Verantwortlich ist {{owner.name}}.
153
+ ```
118
154
 
119
- * Über **`show_title:`** (`true` oder `false`, Standard: `true`) lässt sich steuern, ob die `#`-Überschrift der Markdown-Datei auf der Inhaltsseite gerendert werden soll. Wird vor dem Kapitel eine Trennseite erzeugt (`break_before: "divider"`), kann mit `show_title: false` verhindert werden, dass der Titel auf der Folgeseite doppelt erscheint – die Inhaltsseite beginnt dann direkt mit dem Fließtext oder der ersten Zwischenüberschrift.
155
+ * **Anweisungen stehen allein in ihrer Zeile**; die Zeile verschwindet beim Bau vollständig, eine Tabelle bleibt also zusammenhängend. In einem Zitat oder einer Hinweisbox darf `>` davorstehen.
156
+ * **`(role, key)`** liefert zusätzlich den Namen des Eintrags (`application_owner`); `for role in …` genügt, wenn er nicht gebraucht wird.
157
+ * **Schleifen lassen sich schachteln.** Der Name einer Schleife gilt nur in ihr; ein Name darf keine Angabe unter `document` verdecken (`author`, `custom` …).
158
+ * In Code und Codeblöcken bleiben Anweisungen wörtlich stehen, im Fließtext mit vorangestelltem Backslash: `\{% … %}`. Fehler wie ein fehlendes `{% endfor %}` brechen den Bau mit Datei und Zeile ab.
120
159
 
121
- * Über die optionalen Schlüssel **`toc_title:`** (für Inhaltsverzeichnisse und Kopfzeilen) und **`divider_title:`** (für Trennseiten) können gezielt abweichende Titel vergeben werden (z. B. eine prägnante Kurzform im Inhaltsverzeichnis gegenüber einer ausführlichen Überschrift auf der Textseite). Fehlen die Schlüssel, erben beide automatisch die `#`-Überschrift der Datei.
160
+ ## Werte aus anderen Dateien (`!file`)
122
161
 
123
- * Soll eine Trennseite erzeugt werden (`break_before: "divider"`) oder das Kapitel im Inhaltsverzeichnis gelistet werden, muss mindestens eine `#`-Überschrift oder der entsprechende Titelschlüssel (`divider_title` / `toc_title`) vorhanden sein, andernfalls bricht der Build mit einer klaren Fehlermeldung ab.
162
+ Statt einen Wert direkt hinzuschreiben, lädt `!file` ihn aus einer Datei oder von einer URL nach. Beides lässt sich beliebig mischen:
124
163
 
125
- * Kapitel können über das Feld `break_before` steuern, ob vor ihnen eine Trennseite erzeugt wird, ein einfacher Seitenwechsel erfolgt oder der Text nahtlos anschließt.
164
+ ```yaml
165
+ document:
166
+ custom:
167
+ a: "xxx"
168
+ verfahren: !file "../gemeinsam/verfahren.yaml"
169
+ zweck: !file "zweck.txt"
170
+ betreiber: !file "https://intranet.example/betreiber.yaml"
171
+ ```
126
172
 
173
+ * **YAML-Dateien** (`.yaml`, `.yml`) setzen ihren Inhalt an die Stelle des Verweises, `{{custom.verfahren.name}}` greift also in die nachgeladene Datei. **Jede andere Datei** liefert ihren Text als einfachen Wert.
174
+ * **Relative Pfade** gelten relativ zu der Datei, in der das `!file` steht, bei einer URL relativ zu dieser URL.
175
+ * **Verschachtelt:** Eine nachgeladene YAML-Datei darf selbst wieder `!file` enthalten. Dieselbe Datei an mehreren Stellen ist erlaubt; ein Verweis, der über die Kette zu sich selbst zurückführt, bricht den Bau ab.
176
+ * `!file` wirkt überall in der Konfiguration, nicht nur unter `custom`. Ein Abruf per URL wartet höchstens 10 Sekunden.
127
177
 
128
178
  ---
129
179
 
@@ -19,7 +19,7 @@ Die Sektion `document:` legt globale Metadaten, Layoutschalter, Verzeichnisvorga
19
19
  | `date` | String | `"auto"` | Datum des Dokuments. Bei `"auto"` oder `"today"` wird das aktuelle Tagesdatum eingesetzt. |
20
20
  | `version` | String | `None` | Versionskennung (z. B. `"2.0.0"`). Wird nur gedruckt, wenn explizit gesetzt. |
21
21
  | `language` | String | *(Systemsprache)* | ISO-Sprachcode (z. B. `"de"` oder `"en"`). Steuert Silbentrennung und UI-Texte. |
22
- | `custom` | Mapping | `{}` | Eigene Werte für Platzhalter im Text (`{{custom.a.b}}`), auch verschachtelt. Siehe *Platzhalter im Text*. |
22
+ | `custom` | Mapping | `{}` | Eigene Werte für Platzhalter im Text (`{{custom.a.b}}`), auch verschachtelt. Siehe *Platzhalter im Text*. Einzelne Werte oder Gruppen lassen sich mit `!file` nachladen. |
23
23
  | `cover` | Boolean | `false` | Steuert, ob eine gestaltete Deckblattseite erzeugt wird. Ohne Angabe beginnt das Dokument mit dem ersten Inhalt. |
24
24
  | `header` | Boolean | `true` | Aktiviert oder deaktiviert die laufende Kopfzeile im Dokument. |
25
25
  | `footer` | Boolean | `true` | Aktiviert oder deaktiviert die laufende Fußzeile (inkl. Seitennummerierung). |
@@ -6,7 +6,7 @@ document:
6
6
  summary: "Praxisorientierte Anleitung und Referenz zur Konfiguration, CLI-Bedienung, Theme-Gestaltung und Dokumentenerstellung mit markpublish."
7
7
  author: "Frank Winter"
8
8
  copyright: "© 2026 Frank Winter"
9
- version: "2.3.0"
9
+ version: "2.3.2"
10
10
  date: "auto"
11
11
  language: "de"
12
12
 
@@ -41,7 +41,35 @@ Global Table of Contents Settings
41
41
  Numbering Rules
42
42
  : Settings for automatic numbering (`autonum_pattern`, `autonum_reset`).
43
43
 
44
- #### Custom Metadata Fields
44
+ ### Parts (Sections)
45
+
46
+ A document is organized into high-level sections via `parts:` (e.g., "Main Part", "Case Studies", "Appendices"):
47
+
48
+ * A document must always be structured using `parts:`; a flat `chapters:` list at the top level is rejected.
49
+
50
+ * A part groups logically related chapters together.
51
+
52
+ * By default, a part produces no separate page (`break_before: "none"`): it groups its chapters and passes down its settings, but occupies no page and does not appear in the table of contents. With `break_before: "divider"`, it receives a styled divider page; with `break_before: "page"`, it receives a heading on a fresh page.
53
+
54
+ * Settings defined at the part level (such as TOC depth or an `autonum_pattern`) are automatically inherited by all chapters contained within.
55
+
56
+ * Under `parts:`, only declared configuration keys are allowed. Unknown keys (such as `break_befor`) are rejected with fuzzy suggestions to prevent unnoticed misconfigurations.
57
+
58
+ ### Chapters (Chapters)
59
+
60
+ The `chapters:` list within a part references the actual Markdown content files:
61
+
62
+ * Each chapter points to its Markdown file (`file:`). Authors can write their Markdown files naturally starting with a `#` heading.
63
+
64
+ * **`show_title:`** (`true` or `false`, default: `true`) controls whether the file's `#` heading should be rendered on the content page. If a divider page precedes the chapter (`break_before: "divider"`), setting `show_title: false` prevents the title from repeating on the next page – the content page then starts directly with the introductory text or the first subheading.
65
+
66
+ * Optional keys **`toc_title:`** (for tables of contents and headers) and **`divider_title:`** (for divider pages) allow defining distinct alternative titles (e.g., a concise short form in the table of contents versus an extensive title on the chapter page). When omitted, both inherit the file's `#` heading.
67
+
68
+ * When generating a divider page (`break_before: "divider"`) or listing the chapter in the table of contents, at least one `#` heading or the corresponding title key (`divider_title` / `toc_title`) must exist; otherwise, the build aborts with an informative error message.
69
+
70
+ * Chapters can control via `break_before` whether a divider page is generated, a simple page break occurs, or content flows seamlessly.
71
+
72
+ ## Custom Metadata Fields
45
73
 
46
74
  Under `document:`, **arbitrary custom fields** are permitted beyond the standard keys. They are forwarded to the theme, but are not printed automatically:
47
75
 
@@ -76,7 +104,7 @@ Three essential things to keep in mind:
76
104
 
77
105
  * **Types are preserved.** A `true` is a boolean value, not raw text: the cover page typesets `Yes` or `No` (or `Ja` / `Nein`) depending on the document language. These strings are looked up as `bool_true` and `bool_false` in the i18n cascade.
78
106
 
79
- #### Placeholders in the Text
107
+ ## Placeholders in the Text
80
108
 
81
109
  In Markdown, `{{name}}` inserts a value from `document:`, e.g. `{{author}}`, `{{version}}` or `{{date}}` (the computed date for `"auto"`). This includes custom metadata fields. Values needed only in the text belong under `custom:`. They may be nested there and are addressed with dots:
82
110
 
@@ -94,35 +122,58 @@ Contact: {{author}}, {{custom.support_mail}}.
94
122
  This document describes {{custom.procedure.name}}.
95
123
  ```
96
124
 
97
- Values under `custom` do not reach the theme. A placeholder without a value, including a misspelled one, stops the build and names the file. In code (`` `{{author}}` `` or a code block) the placeholder stays as written; in running text, put a backslash in front: `\{{author}}` gives `{{author}}`.
125
+ If an inserted value is an email address or a URL, it becomes a link, just like text written directly; a placeholder already inside a link keeps that link. Values under `custom` do not reach the theme. A placeholder without a value, including a misspelled one, stops the build and names the file. In code (`` `{{author}}` `` or a code block) the placeholder stays as written; in running text, put a backslash in front: `\{{author}}` gives `{{author}}`.
98
126
 
99
- ### Parts (Sections)
127
+ ### Loops and Short Names
100
128
 
101
- A document is organized into high-level sections via `parts:` (e.g., "Main Part", "Case Studies", "Appendices"):
102
-
103
- * A document must always be structured using `parts:`; a flat `chapters:` list at the top level is rejected.
104
-
105
- * A part groups logically related chapters together.
106
-
107
- * By default, a part produces no separate page (`break_before: "none"`): it groups its chapters and passes down its settings, but occupies no page and does not appear in the table of contents. With `break_before: "divider"`, it receives a styled divider page; with `break_before: "page"`, it receives a heading on a fresh page.
129
+ `{% for %}` repeats the lines up to `{% endfor %}` for each entry of a group under `custom`, in the order of the YAML file. This gives, for example, a table with one row per role:
108
130
 
109
- * Settings defined at the part level (such as TOC depth or an `autonum_pattern`) are automatically inherited by all chapters contained within.
131
+ ```yaml
132
+ document:
133
+ custom:
134
+ app:
135
+ roles:
136
+ application_owner: {label: "Application owner", name: "A. Sample"}
137
+ technical_administrator: {label: "Technical administration", name: "B. Example"}
138
+ ```
110
139
 
111
- * Under `parts:`, only declared configuration keys are allowed. Unknown keys (such as `break_befor`) are rejected with fuzzy suggestions to prevent unnoticed misconfigurations.
140
+ ```markdown
141
+ | Role | Name | Key |
142
+ | :--- | :--- | :--- |
143
+ {% for (role, key) in custom.app.roles %}
144
+ | {{role.label}} | {{role.name}} | {{key}} |
145
+ {% endfor %}
146
+ ```
112
147
 
113
- ### Chapters (Chapters)
148
+ `{% set %}` gives a long path a short name, valid up to the end of the file (inside a loop, up to the end of that loop):
114
149
 
115
- The `chapters:` list within a part references the actual Markdown content files:
150
+ ```markdown
151
+ {% set owner = custom.app.roles.application_owner %}
152
+ Responsible: {{owner.name}}.
153
+ ```
116
154
 
117
- * Each chapter points to its Markdown file (`file:`). Authors can write their Markdown files naturally starting with a `#` heading.
155
+ * **Statements stand alone on their line**; the line disappears completely in the build, so a table stays in one piece. Inside a quote or callout, `>` may precede it.
156
+ * **`(role, key)`** also provides the entry's name (`application_owner`); `for role in …` is enough when it is not needed.
157
+ * **Loops can be nested.** A loop's name is valid only inside it; a name must not hide an entry under `document` (`author`, `custom` …).
158
+ * In code and code blocks statements stay as written; in running text, put a backslash in front: `\{% … %}`. Errors such as a missing `{% endfor %}` stop the build and name file and line.
118
159
 
119
- * **`show_title:`** (`true` or `false`, default: `true`) controls whether the file's `#` heading should be rendered on the content page. If a divider page precedes the chapter (`break_before: "divider"`), setting `show_title: false` prevents the title from repeating on the next page – the content page then starts directly with the introductory text or the first subheading.
160
+ ## Values from Other Files (`!file`)
120
161
 
121
- * Optional keys **`toc_title:`** (for tables of contents and headers) and **`divider_title:`** (for divider pages) allow defining distinct alternative titles (e.g., a concise short form in the table of contents versus an extensive title on the chapter page). When omitted, both inherit the file's `#` heading.
162
+ Instead of writing a value directly, `!file` loads it from a file or a URL. Both can be mixed freely:
122
163
 
123
- * When generating a divider page (`break_before: "divider"`) or listing the chapter in the table of contents, at least one `#` heading or the corresponding title key (`divider_title` / `toc_title`) must exist; otherwise, the build aborts with an informative error message.
164
+ ```yaml
165
+ document:
166
+ custom:
167
+ a: "xxx"
168
+ procedure: !file "../shared/procedure.yaml"
169
+ purpose: !file "purpose.txt"
170
+ operator: !file "https://intranet.example/operator.yaml"
171
+ ```
124
172
 
125
- * Chapters can control via `break_before` whether a divider page is generated, a simple page break occurs, or content flows seamlessly.
173
+ * **YAML files** (`.yaml`, `.yml`) put their content in place of the reference, so `{{custom.procedure.name}}` reaches into the loaded file. **Any other file** supplies its text as a simple value.
174
+ * **Relative paths** are relative to the file containing the `!file`, or to the URL it was loaded from.
175
+ * **Nested:** a loaded YAML file may itself contain `!file`. The same file may appear in several places; a reference leading back to itself through the chain stops the build.
176
+ * `!file` works anywhere in the configuration, not only under `custom`. A URL request waits at most 10 seconds.
126
177
 
127
178
  ---
128
179
 
@@ -19,7 +19,7 @@ The `document:` section defines global metadata, layout switches, table of conte
19
19
  | `date` | String | `"auto"` | Document date. Set to `"auto"` or `"today"` to use current date. |
20
20
  | `version` | String | `None` | Version identifier (e.g., `"2.0.0"`). Only printed when explicitly set. |
21
21
  | `language` | String | *(System language)* | ISO language code (e.g., `"en"` or `"de"`). Governs hyphenation and UI strings. |
22
- | `custom` | Mapping | `{}` | Own values for placeholders in the text (`{{custom.a.b}}`), may be nested. See *Placeholders in the Text*. |
22
+ | `custom` | Mapping | `{}` | Own values for placeholders in the text (`{{custom.a.b}}`), may be nested. See *Placeholders in the Text*. Single values or groups can be loaded with `!file`. |
23
23
  | `cover` | Boolean | `false` | Controls whether a styled cover page is generated. |
24
24
  | `header` | Boolean | `true` | Enables or disables running headers throughout the document. |
25
25
  | `footer` | Boolean | `true` | Enables or disables running footers (including page numbering). |
@@ -6,7 +6,7 @@ document:
6
6
  summary: "Practical guide and reference for configuration, CLI usage, theme design, and document publishing with markpublish."
7
7
  author: "Frank Winter"
8
8
  copyright: "© 2026 Frank Winter"
9
- version: "2.3.0"
9
+ version: "2.3.2"
10
10
  date: "auto"
11
11
  language: "en"
12
12
 
@@ -167,8 +167,18 @@ err.config.toc_bool: "{key_path} nimmt keine Wahrheitswerte. Schreiben Sie 'none
167
167
  err.config.toc_value: "{key_path} kennt {allowed} oder eine Tiefe als Zahl (war: {value})."
168
168
  err.config.custom_value: "{key_path} muss ein einfacher Wert (Text, Zahl) oder eine Gruppe weiterer Werte sein."
169
169
  err.config.custom_key: "{key_path}: Namen unter custom bestehen nur aus Buchstaben, Ziffern, '_' und '-'."
170
+ err.config.file_ref_missing: "!file \"{src}\": Datei nicht gefunden ({path})."
171
+ err.config.file_ref_cycle: "!file \"{src}\": Zyklischer Verweis, {path} lädt sich über diese Kette selbst nach."
172
+ err.config.file_ref_url: "!file \"{src}\": Abruf fehlgeschlagen ({reason})."
170
173
  err.variable.unknown: "Platzhalter {{{{{name}}}}} in {file} hat keinen Wert. Verfügbar sind die Angaben unter document (z. B. {{{{author}}}}) und custom.* ."
171
174
  err.variable.document_field: "Platzhalter {{{{{name}}}}} in {file} hat keinen Wert. In den Angaben unter document sind nur Werte aus custom.* erlaubt (z. B. {{{{custom.verfahren.name}}}})."
175
+ err.statement.syntax: "{file}, Zeile {line}: {{% {text} %}} ist keine bekannte Anweisung. Möglich sind {{% for name in custom.a %}}, {{% for (name, key) in custom.a %}}, {{% endfor %}} und {{% set name = custom.a.b %}}."
176
+ err.statement.inline: "{file}, Zeile {line}: Eine Anweisung {{% … %}} muss allein in ihrer Zeile stehen. Als Text schreiben Sie \\{{% … %}}."
177
+ err.statement.unclosed: "{file}, Zeile {line}: Zu diesem {{% for %}} fehlt das {{% endfor %}}."
178
+ err.statement.stray_end: "{file}, Zeile {line}: {{% endfor %}} ohne vorangehendes {{% for %}}."
179
+ err.statement.unknown_path: "{file}, Zeile {line}: {path} hat keinen Wert."
180
+ err.statement.not_group: "{file}, Zeile {line}: {path} ist ein einzelner Wert, keine Gruppe; {{% for %}} kann nicht darüber laufen."
181
+ err.statement.reserved: "{file}, Zeile {line}: Der Name {name} ist schon eine Angabe unter document. Wählen Sie einen anderen Namen."
172
182
  err.config.toc_depth: "{key_path} braucht eine Tiefe ab 1 (war: {value}). Für 'kommt nicht vor' schreiben Sie 'none'."
173
183
  err.config.toc_kind: "{key_path} kennt 'none', 'full' oder eine Tiefe als Zahl (war: {value})."
174
184
  err.config.document_unknown: |
@@ -170,8 +170,18 @@ err.config.toc_bool: "{key_path} takes no boolean. Write 'none', 'full' or a dep
170
170
  err.config.toc_value: "{key_path} takes {allowed} or a depth as a number (was: {value})."
171
171
  err.config.custom_value: "{key_path} must be a simple value (text, number) or a group of further values."
172
172
  err.config.custom_key: "{key_path}: names under custom consist only of letters, digits, '_' and '-'."
173
+ err.config.file_ref_missing: "!file \"{src}\": file not found ({path})."
174
+ err.config.file_ref_cycle: "!file \"{src}\": circular reference, {path} loads itself through this chain."
175
+ err.config.file_ref_url: "!file \"{src}\": retrieval failed ({reason})."
173
176
  err.variable.unknown: "Placeholder {{{{{name}}}}} in {file} has no value. Available are the entries under document (e.g. {{{{author}}}}) and custom.* ."
174
177
  err.variable.document_field: "Placeholder {{{{{name}}}}} in {file} has no value. The entries under document may only use values from custom.* (e.g. {{{{custom.verfahren.name}}}})."
178
+ err.statement.syntax: "{file}, line {line}: {{% {text} %}} is not a known statement. Available are {{% for name in custom.a %}}, {{% for (name, key) in custom.a %}}, {{% endfor %}} and {{% set name = custom.a.b %}}."
179
+ err.statement.inline: "{file}, line {line}: a statement {{% … %}} must stand alone on its line. To show it as text, write \\{{% … %}}."
180
+ err.statement.unclosed: "{file}, line {line}: this {{% for %}} has no {{% endfor %}}."
181
+ err.statement.stray_end: "{file}, line {line}: {{% endfor %}} without a preceding {{% for %}}."
182
+ err.statement.unknown_path: "{file}, line {line}: {path} has no value."
183
+ err.statement.not_group: "{file}, line {line}: {path} is a single value, not a group; {{% for %}} cannot loop over it."
184
+ err.statement.reserved: "{file}, line {line}: the name {name} is already an entry under document. Choose another name."
175
185
  err.config.toc_depth: "{key_path} needs a depth of at least 1 (was: {value}). For 'does not appear' write 'none'."
176
186
  err.config.toc_kind: "{key_path} takes 'none', 'full' or a depth as a number (was: {value})."
177
187
  err.config.document_unknown: |
@@ -34,7 +34,12 @@ from markpublish.markdown.typst_serializer import (
34
34
  html_to_tree,
35
35
  process_tree_headings_and_toc,
36
36
  )
37
- from markpublish.markdown.variables import build_variables, protect_escapes, substitute_tree
37
+ from markpublish.markdown.variables import (
38
+ build_variables,
39
+ expand_statements,
40
+ protect_escapes,
41
+ substitute_tree,
42
+ )
38
43
  from markpublish.ui import t
39
44
 
40
45
  #: Namen der String-Extensions. Der GitHubAlertsExtension wird pro Sprache
@@ -555,6 +560,7 @@ class MarkdownPipeline:
555
560
  # Convert markdown to HTML via python-markdown (all extensions active)
556
561
  if raw_md and doc_cfg:
557
562
  raw_md = protect_escapes(raw_md)
563
+ raw_md = expand_statements(raw_md, build_variables(doc_cfg), chapter_cfg.file or "")
558
564
  raw_html = self.engine.convert(raw_md) if raw_md else ""
559
565
 
560
566
  # Parse HTML into ElementTree and process headings / numbering directly in the AST