texsmith 0.0.2.dev0__py3-none-any.whl
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.
- texsmith/__init__.py +107 -0
- texsmith/_alias.py +59 -0
- texsmith/adapters/__init__.py +6 -0
- texsmith/adapters/docker.py +258 -0
- texsmith/adapters/handlers/__init__.py +3 -0
- texsmith/adapters/handlers/_assets.py +363 -0
- texsmith/adapters/handlers/_helpers.py +62 -0
- texsmith/adapters/handlers/_mermaid.py +122 -0
- texsmith/adapters/handlers/admonitions.py +267 -0
- texsmith/adapters/handlers/basic.py +228 -0
- texsmith/adapters/handlers/blocks.py +851 -0
- texsmith/adapters/handlers/code.py +309 -0
- texsmith/adapters/handlers/inline.py +937 -0
- texsmith/adapters/handlers/links.py +239 -0
- texsmith/adapters/handlers/media.py +380 -0
- texsmith/adapters/latex/__init__.py +10 -0
- texsmith/adapters/latex/engines/__init__.py +726 -0
- texsmith/adapters/latex/engines/latex/__init__.py +26 -0
- texsmith/adapters/latex/engines/latex/log.py +720 -0
- texsmith/adapters/latex/engines/latex/runner.py +28 -0
- texsmith/adapters/latex/engines/tectonic/__init__.py +38 -0
- texsmith/adapters/latex/formatter.py +297 -0
- texsmith/adapters/latex/latexmk.py +148 -0
- texsmith/adapters/latex/partials/acronym.tex +1 -0
- texsmith/adapters/latex/partials/add.tex +1 -0
- texsmith/adapters/latex/partials/addition.tex +1 -0
- texsmith/adapters/latex/partials/blockquote.tex +3 -0
- texsmith/adapters/latex/partials/callout.tex +3 -0
- texsmith/adapters/latex/partials/choices.tex +5 -0
- texsmith/adapters/latex/partials/citation.tex +1 -0
- texsmith/adapters/latex/partials/codeblock.tex +7 -0
- texsmith/adapters/latex/partials/codeblock_listings.tex +5 -0
- texsmith/adapters/latex/partials/codeblock_pygments.tex +5 -0
- texsmith/adapters/latex/partials/codeblock_verbatim.tex +12 -0
- texsmith/adapters/latex/partials/codeinline.tex +1 -0
- texsmith/adapters/latex/partials/codeinlinett.tex +1 -0
- texsmith/adapters/latex/partials/comment.tex +1 -0
- texsmith/adapters/latex/partials/del.tex +1 -0
- texsmith/adapters/latex/partials/deletion.tex +1 -0
- texsmith/adapters/latex/partials/description_list.tex +5 -0
- texsmith/adapters/latex/partials/enquote.tex +1 -0
- texsmith/adapters/latex/partials/epigraph.tex +1 -0
- texsmith/adapters/latex/partials/exercises_solutions.tex +7 -0
- texsmith/adapters/latex/partials/figure.tex +33 -0
- texsmith/adapters/latex/partials/figure_tcolorbox.tex +15 -0
- texsmith/adapters/latex/partials/footnote.tex +3 -0
- texsmith/adapters/latex/partials/glossary.tex +1 -0
- texsmith/adapters/latex/partials/heading.tex +14 -0
- texsmith/adapters/latex/partials/highlight.tex +25 -0
- texsmith/adapters/latex/partials/horizontal_rule.tex +1 -0
- texsmith/adapters/latex/partials/href.tex +1 -0
- texsmith/adapters/latex/partials/icon.tex +1 -0
- texsmith/adapters/latex/partials/include.tex +1 -0
- texsmith/adapters/latex/partials/index.tex +1 -0
- texsmith/adapters/latex/partials/italic.tex +1 -0
- texsmith/adapters/latex/partials/keystroke.tex +46 -0
- texsmith/adapters/latex/partials/label.tex +1 -0
- texsmith/adapters/latex/partials/list_acronyms.tex +5 -0
- texsmith/adapters/latex/partials/list_glossary.tex +8 -0
- texsmith/adapters/latex/partials/multicolumn.tex +3 -0
- texsmith/adapters/latex/partials/ordered_list.tex +5 -0
- texsmith/adapters/latex/partials/pagestyle.tex +1 -0
- texsmith/adapters/latex/partials/ref.tex +1 -0
- texsmith/adapters/latex/partials/regex.tex +1 -0
- texsmith/adapters/latex/partials/smallcaps.tex +1 -0
- texsmith/adapters/latex/partials/strikethrough.tex +1 -0
- texsmith/adapters/latex/partials/strong.tex +1 -0
- texsmith/adapters/latex/partials/subscript.tex +1 -0
- texsmith/adapters/latex/partials/substitution.tex +1 -0
- texsmith/adapters/latex/partials/superscript.tex +1 -0
- texsmith/adapters/latex/partials/tabbed.tex +3 -0
- texsmith/adapters/latex/partials/table.tex +48 -0
- texsmith/adapters/latex/partials/underline.tex +1 -0
- texsmith/adapters/latex/partials/unordered_list.tex +5 -0
- texsmith/adapters/latex/partials/url.tex +1 -0
- texsmith/adapters/latex/pygments.py +98 -0
- texsmith/adapters/latex/pyxindy.py +64 -0
- texsmith/adapters/latex/renderer.py +220 -0
- texsmith/adapters/latex/tectonic.py +366 -0
- texsmith/adapters/latex/utils.py +86 -0
- texsmith/adapters/markdown/__init__.py +342 -0
- texsmith/adapters/plugins/__init__.py +8 -0
- texsmith/adapters/plugins/material.py +174 -0
- texsmith/adapters/plugins/snippet.py +1734 -0
- texsmith/adapters/transformers/__init__.py +122 -0
- texsmith/adapters/transformers/base.py +140 -0
- texsmith/adapters/transformers/strategies.py +1456 -0
- texsmith/adapters/transformers/utils.py +59 -0
- texsmith/api/__init__.py +80 -0
- texsmith/api/_utils.py +56 -0
- texsmith/api/document.py +645 -0
- texsmith/api/pipeline.py +229 -0
- texsmith/api/service.py +648 -0
- texsmith/api/templates.py +287 -0
- texsmith/core/__init__.py +6 -0
- texsmith/core/bibliography/__init__.py +57 -0
- texsmith/core/bibliography/collection.py +354 -0
- texsmith/core/bibliography/doi.py +194 -0
- texsmith/core/bibliography/issues.py +15 -0
- texsmith/core/bibliography/parsing.py +45 -0
- texsmith/core/callouts.py +99 -0
- texsmith/core/config.py +191 -0
- texsmith/core/context.py +242 -0
- texsmith/core/conversion/__init__.py +103 -0
- texsmith/core/conversion/core.py +780 -0
- texsmith/core/conversion/debug.py +87 -0
- texsmith/core/conversion/inputs.py +474 -0
- texsmith/core/conversion/renderer.py +578 -0
- texsmith/core/conversion/templates.py +646 -0
- texsmith/core/conversion_contexts.py +95 -0
- texsmith/core/diagnostics.py +118 -0
- texsmith/core/exceptions.py +41 -0
- texsmith/core/fonts/__init__.py +3 -0
- texsmith/core/fragments/__init__.py +716 -0
- texsmith/core/fragments/base.py +117 -0
- texsmith/core/metadata.py +280 -0
- texsmith/core/mustache.py +86 -0
- texsmith/core/partials.py +20 -0
- texsmith/core/rules.py +394 -0
- texsmith/core/templates/__init__.py +58 -0
- texsmith/core/templates/base.py +343 -0
- texsmith/core/templates/builtins.py +68 -0
- texsmith/core/templates/context_usage.py +137 -0
- texsmith/core/templates/loader.py +303 -0
- texsmith/core/templates/manifest.py +861 -0
- texsmith/core/templates/runtime.py +347 -0
- texsmith/core/templates/text.py +14 -0
- texsmith/core/templates/wrapper.py +340 -0
- texsmith/core/user_dir.py +179 -0
- texsmith/devtools.py +28 -0
- texsmith/extensions/__init__.py +162 -0
- texsmith/extensions/index/__init__.py +21 -0
- texsmith/extensions/index/markdown.py +94 -0
- texsmith/extensions/index/mkdocs_plugin.py +136 -0
- texsmith/extensions/index/registry.py +57 -0
- texsmith/extensions/index/renderer.py +183 -0
- texsmith/extensions/index/templates/index.tex +1 -0
- texsmith/extensions/latex_raw.py +115 -0
- texsmith/extensions/latex_text.py +117 -0
- texsmith/extensions/mermaid.py +267 -0
- texsmith/extensions/missing_footnotes.py +125 -0
- texsmith/extensions/multi_citations.py +54 -0
- texsmith/extensions/progressbar/__init__.py +9 -0
- texsmith/extensions/progressbar/markdown.py +212 -0
- texsmith/extensions/progressbar/renderer.py +117 -0
- texsmith/extensions/smallcaps.py +48 -0
- texsmith/extensions/texlogos/__init__.py +10 -0
- texsmith/extensions/texlogos/markdown.py +236 -0
- texsmith/extensions/texlogos/renderer.py +81 -0
- texsmith/extensions/texlogos/specs.py +66 -0
- texsmith/fonts/__init__.py +57 -0
- texsmith/fonts/cache.py +46 -0
- texsmith/fonts/constants.py +60 -0
- texsmith/fonts/coverage.py +275 -0
- texsmith/fonts/downloader.py +103 -0
- texsmith/fonts/fallback.py +438 -0
- texsmith/fonts/html_scripts.py +168 -0
- texsmith/fonts/logging.py +127 -0
- texsmith/fonts/pipeline.py +338 -0
- texsmith/fonts/scripts.py +493 -0
- texsmith/fonts/ucharclasses.py +164 -0
- texsmith/fragments/__init__.py +11 -0
- texsmith/fragments/bibliography/__init__.py +65 -0
- texsmith/fragments/bibliography/fragment.toml +3 -0
- texsmith/fragments/bibliography/ts-bibliography-backmatter.jinja.tex +35 -0
- texsmith/fragments/bibliography/ts-bibliography.jinja.tex +10 -0
- texsmith/fragments/callouts/__init__.py +88 -0
- texsmith/fragments/callouts/fragment.toml +3 -0
- texsmith/fragments/callouts/ts-callouts.jinja.sty +129 -0
- texsmith/fragments/code/__init__.py +82 -0
- texsmith/fragments/code/fragment.toml +3 -0
- texsmith/fragments/code/ts-code.jinja.sty +140 -0
- texsmith/fragments/extra/__init__.py +180 -0
- texsmith/fragments/extra/fragment.toml +3 -0
- texsmith/fragments/extra/ts-extra.jinja.tex +13 -0
- texsmith/fragments/fonts/__init__.py +880 -0
- texsmith/fragments/fonts/fragment.toml +3 -0
- texsmith/fragments/fonts/ts-fonts.jinja.sty +292 -0
- texsmith/fragments/frame/__init__.py +170 -0
- texsmith/fragments/frame/fragment.toml +3 -0
- texsmith/fragments/frame/ts-frame.tex.jinja +49 -0
- texsmith/fragments/geometry/__init__.py +169 -0
- texsmith/fragments/geometry/fragment.toml +3 -0
- texsmith/fragments/geometry/paper.py +526 -0
- texsmith/fragments/geometry/ts_geometry.tex.jinja +53 -0
- texsmith/fragments/glossary/__init__.py +67 -0
- texsmith/fragments/glossary/fragment.toml +3 -0
- texsmith/fragments/glossary/ts-glossary-backmatter.jinja.tex +5 -0
- texsmith/fragments/glossary/ts-glossary.jinja.sty +28 -0
- texsmith/fragments/index/__init__.py +66 -0
- texsmith/fragments/index/fragment.toml +3 -0
- texsmith/fragments/index/ts-index-backmatter.jinja.tex +3 -0
- texsmith/fragments/index/ts-index.jinja.sty +12 -0
- texsmith/fragments/keystrokes/__init__.py +69 -0
- texsmith/fragments/keystrokes/fragment.toml +3 -0
- texsmith/fragments/keystrokes/ts-keystrokes.jinja.sty +20 -0
- texsmith/fragments/todolist/__init__.py +71 -0
- texsmith/fragments/todolist/fragment.toml +3 -0
- texsmith/fragments/todolist/ts-todolist.jinja.sty +21 -0
- texsmith/fragments/typesetting/__init__.py +214 -0
- texsmith/fragments/typesetting/fragment.toml +3 -0
- texsmith/fragments/typesetting/ts-typesetting.tex.jinja +81 -0
- texsmith/index.py +26 -0
- texsmith/plugins/__init__.py +14 -0
- texsmith/progressbar.py +8 -0
- texsmith/quotes.py +40 -0
- texsmith/smart_dashes.py +66 -0
- texsmith/templates/__init__.py +3 -0
- texsmith/templates/article/README.md +33 -0
- texsmith/templates/article/__init__.py +281 -0
- texsmith/templates/article/template/manifest.toml +125 -0
- texsmith/templates/article/template/mermaid-config.json +18 -0
- texsmith/templates/article/template/template.tex +74 -0
- texsmith/templates/book/README.md +26 -0
- texsmith/templates/book/__init__.py +75 -0
- texsmith/templates/book/overrides/codeblock.tex +7 -0
- texsmith/templates/book/overrides/codeinline.tex +1 -0
- texsmith/templates/book/template/fixtoc.sty +81 -0
- texsmith/templates/book/template/manifest.toml +198 -0
- texsmith/templates/book/template/template.tex +314 -0
- texsmith/templates/common/__init__.py +1 -0
- texsmith/templates/common/latexmkrc +46 -0
- texsmith/templates/letter/README.md +59 -0
- texsmith/templates/letter/__init__.py +495 -0
- texsmith/templates/letter/demo.md +31 -0
- texsmith/templates/letter/fonts/modernline bold.otf +0 -0
- texsmith/templates/letter/fonts/modernline.otf +0 -0
- texsmith/templates/letter/manifest.toml +197 -0
- texsmith/templates/letter/template/callouts.jinja.sty +290 -0
- texsmith/templates/letter/template/template.tex +123 -0
- texsmith/templates/snippet/README.md +21 -0
- texsmith/templates/snippet/__init__.py +80 -0
- texsmith/templates/snippet/template/manifest.toml +66 -0
- texsmith/templates/snippet/template/template.tex +47 -0
- texsmith/texlogos.py +15 -0
- texsmith/ui/__init__.py +6 -0
- texsmith/ui/cli/__init__.py +22 -0
- texsmith/ui/cli/_options.py +338 -0
- texsmith/ui/cli/app.py +65 -0
- texsmith/ui/cli/bibliography.py +300 -0
- texsmith/ui/cli/commands/__init__.py +14 -0
- texsmith/ui/cli/commands/render.py +1128 -0
- texsmith/ui/cli/commands/templates.py +397 -0
- texsmith/ui/cli/diagnostics.py +36 -0
- texsmith/ui/cli/presenter.py +663 -0
- texsmith/ui/cli/state.py +263 -0
- texsmith/ui/cli/utils.py +235 -0
- texsmith-0.0.2.dev0.dist-info/METADATA +187 -0
- texsmith-0.0.2.dev0.dist-info/RECORD +252 -0
- texsmith-0.0.2.dev0.dist-info/WHEEL +4 -0
- texsmith-0.0.2.dev0.dist-info/entry_points.txt +22 -0
- texsmith-0.0.2.dev0.dist-info/licenses/LICENSE.md +21 -0
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
"""Template orchestration helpers powering the conversion pipeline."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable, Mapping
|
|
6
|
+
import contextlib
|
|
7
|
+
import copy
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import re
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
from bs4 import BeautifulSoup, FeatureNotFound
|
|
14
|
+
from bs4.element import NavigableString, Tag
|
|
15
|
+
from pybtex.exceptions import PybtexError
|
|
16
|
+
from slugify import slugify
|
|
17
|
+
import yaml
|
|
18
|
+
|
|
19
|
+
from ..bibliography.collection import BibliographyCollection
|
|
20
|
+
from ..bibliography.parsing import (
|
|
21
|
+
bibliography_data_from_inline_entry,
|
|
22
|
+
bibliography_data_from_string,
|
|
23
|
+
)
|
|
24
|
+
from ..config import BookConfig
|
|
25
|
+
from ..conversion_contexts import BinderContext, DocumentContext, GenerationStrategy
|
|
26
|
+
from ..diagnostics import DiagnosticEmitter
|
|
27
|
+
from ..mustache import replace_mustaches, replace_mustaches_in_structure
|
|
28
|
+
from ..templates import (
|
|
29
|
+
TemplateBinding,
|
|
30
|
+
TemplateError,
|
|
31
|
+
TemplateRuntime,
|
|
32
|
+
TemplateSlot,
|
|
33
|
+
build_template_overrides,
|
|
34
|
+
resolve_template_binding,
|
|
35
|
+
resolve_template_language,
|
|
36
|
+
)
|
|
37
|
+
from .debug import debug_enabled, ensure_emitter, raise_conversion_error, record_event
|
|
38
|
+
from .inputs import (
|
|
39
|
+
DOCUMENT_SELECTOR_SENTINEL,
|
|
40
|
+
InlineBibliographyEntry,
|
|
41
|
+
InlineBibliographyValidationError,
|
|
42
|
+
extract_front_matter_bibliography,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
47
|
+
from ..bibliography.doi import DoiBibliographyFetcher
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
_DOI_SUPPORT: dict[str, Any] | None = None
|
|
51
|
+
# Exported for compatibility with existing monkeypatch patterns in tests.
|
|
52
|
+
DoiBibliographyFetcher: type[Any] | None = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(slots=True)
|
|
56
|
+
class SlotFragment:
|
|
57
|
+
"""HTML fragment mapped to a template slot with position metadata."""
|
|
58
|
+
|
|
59
|
+
name: str
|
|
60
|
+
html: str
|
|
61
|
+
position: int
|
|
62
|
+
heading_levels: list[int] = field(default_factory=list)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def build_binder_context(
|
|
66
|
+
*,
|
|
67
|
+
document_context: DocumentContext,
|
|
68
|
+
template: str | None,
|
|
69
|
+
template_runtime: TemplateRuntime | None,
|
|
70
|
+
requested_language: str | None,
|
|
71
|
+
bibliography_files: list[Path],
|
|
72
|
+
slot_overrides: Mapping[str, str] | None,
|
|
73
|
+
output_dir: Path,
|
|
74
|
+
strategy: GenerationStrategy,
|
|
75
|
+
emitter: DiagnosticEmitter | None,
|
|
76
|
+
legacy_latex_accents: bool,
|
|
77
|
+
session_overrides: Mapping[str, Any] | None = None,
|
|
78
|
+
preloaded_bibliography: BibliographyCollection | None = None,
|
|
79
|
+
seen_bibliography_issues: set[tuple[str, str | None, str | None]] | None = None,
|
|
80
|
+
) -> BinderContext:
|
|
81
|
+
"""Prepare template bindings, bibliography data, and slot mappings."""
|
|
82
|
+
emitter = ensure_emitter(emitter)
|
|
83
|
+
resolved_language = resolve_template_language(requested_language, document_context.front_matter)
|
|
84
|
+
document_context.language = resolved_language
|
|
85
|
+
|
|
86
|
+
config = BookConfig(
|
|
87
|
+
project_dir=document_context.source_path.parent,
|
|
88
|
+
language=resolved_language,
|
|
89
|
+
legacy_latex_accents=legacy_latex_accents,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
inline_bibliography = extract_front_matter_bibliography(document_context.front_matter)
|
|
94
|
+
except InlineBibliographyValidationError as exc:
|
|
95
|
+
raise_conversion_error(emitter, str(exc), exc)
|
|
96
|
+
|
|
97
|
+
issue_signatures = seen_bibliography_issues if seen_bibliography_issues is not None else set()
|
|
98
|
+
bibliography_collection: BibliographyCollection | None = (
|
|
99
|
+
preloaded_bibliography.clone() if preloaded_bibliography is not None else None
|
|
100
|
+
)
|
|
101
|
+
bibliography_map: dict[str, dict[str, Any]] = {}
|
|
102
|
+
|
|
103
|
+
if bibliography_collection is None:
|
|
104
|
+
bibliography_collection = BibliographyCollection()
|
|
105
|
+
if bibliography_files:
|
|
106
|
+
bibliography_collection.load_files(bibliography_files)
|
|
107
|
+
|
|
108
|
+
if inline_bibliography:
|
|
109
|
+
_load_inline_bibliography(
|
|
110
|
+
bibliography_collection,
|
|
111
|
+
inline_bibliography,
|
|
112
|
+
source_label=document_context.source_path.stem,
|
|
113
|
+
output_dir=output_dir,
|
|
114
|
+
emitter=emitter,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
bibliography_map = bibliography_collection.to_dict()
|
|
118
|
+
for issue in bibliography_collection.issues:
|
|
119
|
+
signature = (issue.message, issue.key, str(issue.source) if issue.source else None)
|
|
120
|
+
if signature in issue_signatures:
|
|
121
|
+
continue
|
|
122
|
+
prefix = f"[{issue.key}] " if issue.key else ""
|
|
123
|
+
source_hint = f" ({issue.source})" if issue.source else ""
|
|
124
|
+
emitter.warning(f"{prefix}{issue.message}{source_hint}")
|
|
125
|
+
issue_signatures.add(signature)
|
|
126
|
+
|
|
127
|
+
document_context.bibliography = bibliography_map
|
|
128
|
+
|
|
129
|
+
slot_requests = dict(document_context.slot_requests)
|
|
130
|
+
if slot_overrides:
|
|
131
|
+
slot_requests.update(dict(slot_overrides))
|
|
132
|
+
|
|
133
|
+
template_overrides = build_template_overrides(document_context.front_matter)
|
|
134
|
+
if session_overrides:
|
|
135
|
+
template_overrides = _merge_template_overrides(template_overrides, session_overrides)
|
|
136
|
+
|
|
137
|
+
press_section = template_overrides.get("press")
|
|
138
|
+
if not isinstance(press_section, dict):
|
|
139
|
+
press_section = None
|
|
140
|
+
|
|
141
|
+
def _ensure_press_section() -> dict[str, Any]:
|
|
142
|
+
nonlocal press_section
|
|
143
|
+
if press_section is None:
|
|
144
|
+
press_section = {}
|
|
145
|
+
template_overrides["press"] = press_section
|
|
146
|
+
return press_section
|
|
147
|
+
|
|
148
|
+
if document_context.extracted_title:
|
|
149
|
+
template_overrides.setdefault("title", document_context.extracted_title)
|
|
150
|
+
_ensure_press_section().setdefault("title", document_context.extracted_title)
|
|
151
|
+
|
|
152
|
+
template_overrides.setdefault("language", resolved_language)
|
|
153
|
+
if press_section is not None or "press" in template_overrides:
|
|
154
|
+
_ensure_press_section().setdefault("language", resolved_language)
|
|
155
|
+
|
|
156
|
+
template_overrides.setdefault("_source_dir", str(document_context.source_path.parent))
|
|
157
|
+
template_overrides.setdefault("_source_path", str(document_context.source_path))
|
|
158
|
+
template_overrides.setdefault("source_dir", str(document_context.source_path.parent))
|
|
159
|
+
template_overrides["output_dir"] = str(output_dir)
|
|
160
|
+
|
|
161
|
+
raw_contexts = (template_overrides, document_context.front_matter)
|
|
162
|
+
template_overrides = replace_mustaches_in_structure(
|
|
163
|
+
template_overrides, raw_contexts, emitter=emitter, source="template attributes"
|
|
164
|
+
)
|
|
165
|
+
document_context.front_matter = replace_mustaches_in_structure(
|
|
166
|
+
document_context.front_matter,
|
|
167
|
+
raw_contexts,
|
|
168
|
+
emitter=emitter,
|
|
169
|
+
source=str(document_context.source_path),
|
|
170
|
+
)
|
|
171
|
+
merged_contexts = (template_overrides, document_context.front_matter)
|
|
172
|
+
if isinstance(document_context.html, str):
|
|
173
|
+
document_context.html = replace_mustaches(
|
|
174
|
+
document_context.html,
|
|
175
|
+
merged_contexts,
|
|
176
|
+
emitter=emitter,
|
|
177
|
+
source=str(document_context.source_path),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
active_slot_requests: dict[str, str] = {}
|
|
181
|
+
binding: TemplateBinding | None = None
|
|
182
|
+
try:
|
|
183
|
+
binding, active_slot_requests = resolve_template_binding(
|
|
184
|
+
template=template,
|
|
185
|
+
template_runtime=template_runtime,
|
|
186
|
+
template_overrides=template_overrides,
|
|
187
|
+
slot_requests=slot_requests,
|
|
188
|
+
warn=lambda message: emitter.warning(message),
|
|
189
|
+
)
|
|
190
|
+
except TemplateError as exc:
|
|
191
|
+
if debug_enabled(emitter):
|
|
192
|
+
raise
|
|
193
|
+
raise_conversion_error(emitter, str(exc), exc)
|
|
194
|
+
if binding is None: # pragma: no cover - defensive
|
|
195
|
+
raise RuntimeError("Failed to resolve template binding.")
|
|
196
|
+
|
|
197
|
+
if binding.runtime and binding.runtime.extras:
|
|
198
|
+
template_mermaid = binding.runtime.extras.get("mermaid_config")
|
|
199
|
+
if template_mermaid and not config.mermaid_config:
|
|
200
|
+
config.mermaid_config = Path(template_mermaid)
|
|
201
|
+
|
|
202
|
+
binder_context = BinderContext(
|
|
203
|
+
output_dir=output_dir,
|
|
204
|
+
config=config,
|
|
205
|
+
strategy=strategy,
|
|
206
|
+
language=resolved_language,
|
|
207
|
+
slot_requests=active_slot_requests,
|
|
208
|
+
template_overrides=dict(template_overrides),
|
|
209
|
+
bibliography_map=bibliography_map,
|
|
210
|
+
bibliography_collection=bibliography_collection,
|
|
211
|
+
template_binding=binding,
|
|
212
|
+
)
|
|
213
|
+
binder_context.documents.append(document_context)
|
|
214
|
+
|
|
215
|
+
return binder_context
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _merge_template_overrides(
|
|
219
|
+
base: Mapping[str, Any], overrides: Mapping[str, Any]
|
|
220
|
+
) -> dict[str, Any]:
|
|
221
|
+
merged: dict[str, Any] = copy.deepcopy(dict(base))
|
|
222
|
+
|
|
223
|
+
def _merge(target: dict[str, Any], source: Mapping[str, Any]) -> None:
|
|
224
|
+
for key, value in source.items():
|
|
225
|
+
if isinstance(value, Mapping):
|
|
226
|
+
existing = target.get(key)
|
|
227
|
+
nested: dict[str, Any] = dict(existing) if isinstance(existing, Mapping) else {}
|
|
228
|
+
_merge(nested, value)
|
|
229
|
+
target[key] = nested
|
|
230
|
+
else:
|
|
231
|
+
target[key] = copy.deepcopy(value)
|
|
232
|
+
|
|
233
|
+
_merge(merged, overrides)
|
|
234
|
+
return merged
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def extract_slot_fragments(
|
|
238
|
+
html: str,
|
|
239
|
+
requests: Mapping[str, str],
|
|
240
|
+
default_slot: str,
|
|
241
|
+
*,
|
|
242
|
+
slot_definitions: Mapping[str, TemplateSlot],
|
|
243
|
+
parser_backend: str,
|
|
244
|
+
) -> tuple[list[SlotFragment], list[str]]:
|
|
245
|
+
"""Split the HTML document into fragments mapped to template slots."""
|
|
246
|
+
try:
|
|
247
|
+
soup = BeautifulSoup(html, parser_backend)
|
|
248
|
+
except FeatureNotFound:
|
|
249
|
+
soup = BeautifulSoup(html, "html.parser")
|
|
250
|
+
|
|
251
|
+
container = soup.body or soup
|
|
252
|
+
document_html = "".join(str(node) for node in container.contents)
|
|
253
|
+
|
|
254
|
+
wildcard_values = {
|
|
255
|
+
DOCUMENT_SELECTOR_SENTINEL,
|
|
256
|
+
DOCUMENT_SELECTOR_SENTINEL.lower(),
|
|
257
|
+
"*",
|
|
258
|
+
}
|
|
259
|
+
full_document_slots: list[str] = []
|
|
260
|
+
filtered_requests: dict[str, str] = {}
|
|
261
|
+
for slot_name, selector in requests.items():
|
|
262
|
+
if not selector:
|
|
263
|
+
continue
|
|
264
|
+
token = selector.strip()
|
|
265
|
+
if token.lower() in wildcard_values:
|
|
266
|
+
full_document_slots.append(slot_name)
|
|
267
|
+
continue
|
|
268
|
+
filtered_requests[slot_name] = selector
|
|
269
|
+
|
|
270
|
+
headings: list[tuple[int, Tag]] = []
|
|
271
|
+
for index, heading in enumerate(container.find_all(re.compile(r"^h[1-6]$"), recursive=True)):
|
|
272
|
+
headings.append((index, heading))
|
|
273
|
+
|
|
274
|
+
matched: dict[str, tuple[int, Tag]] = {}
|
|
275
|
+
missing: list[str] = []
|
|
276
|
+
occupied_nodes: set[int] = set()
|
|
277
|
+
document_nodes = list(container.contents)
|
|
278
|
+
|
|
279
|
+
for slot_name, selector in filtered_requests.items():
|
|
280
|
+
if not selector:
|
|
281
|
+
continue
|
|
282
|
+
target_label = selector.lstrip("#")
|
|
283
|
+
matched_heading: tuple[int, Tag] | None = None
|
|
284
|
+
for index, heading in headings:
|
|
285
|
+
if id(heading) in occupied_nodes:
|
|
286
|
+
continue
|
|
287
|
+
node_id = heading.get("id")
|
|
288
|
+
if isinstance(node_id, str) and node_id == target_label:
|
|
289
|
+
matched_heading = (index, heading)
|
|
290
|
+
break
|
|
291
|
+
if matched_heading is None:
|
|
292
|
+
for index, heading in headings:
|
|
293
|
+
if id(heading) in occupied_nodes:
|
|
294
|
+
continue
|
|
295
|
+
if heading.get_text(strip=True) == selector:
|
|
296
|
+
matched_heading = (index, heading)
|
|
297
|
+
break
|
|
298
|
+
if matched_heading is None:
|
|
299
|
+
missing.append(f"unable to locate section '{selector}' for slot '{slot_name}'")
|
|
300
|
+
continue
|
|
301
|
+
matched_index, heading = matched_heading
|
|
302
|
+
occupied_nodes.add(id(heading))
|
|
303
|
+
matched[slot_name] = (matched_index, heading)
|
|
304
|
+
|
|
305
|
+
fragments: list[SlotFragment] = []
|
|
306
|
+
|
|
307
|
+
for offset, slot_name in enumerate(full_document_slots):
|
|
308
|
+
fragments.append(
|
|
309
|
+
SlotFragment(
|
|
310
|
+
name=slot_name,
|
|
311
|
+
html=document_html,
|
|
312
|
+
position=-(len(full_document_slots) - offset),
|
|
313
|
+
heading_levels=_heading_levels_for_nodes(document_nodes),
|
|
314
|
+
)
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
for slot_name, (order, heading) in sorted(matched.items(), key=lambda item: item[1][0]):
|
|
318
|
+
section_nodes = collect_section_nodes(heading)
|
|
319
|
+
slot_config = slot_definitions.get(slot_name)
|
|
320
|
+
strip_heading = bool(slot_config.strip_heading) if slot_config else False
|
|
321
|
+
render_nodes = list(section_nodes)
|
|
322
|
+
if strip_heading and render_nodes:
|
|
323
|
+
render_nodes = render_nodes[1:]
|
|
324
|
+
while render_nodes and isinstance(render_nodes[0], NavigableString):
|
|
325
|
+
if str(render_nodes[0]).strip():
|
|
326
|
+
break
|
|
327
|
+
render_nodes.pop(0)
|
|
328
|
+
html_fragment = "".join(str(node) for node in render_nodes)
|
|
329
|
+
fragments.append(
|
|
330
|
+
SlotFragment(
|
|
331
|
+
name=slot_name,
|
|
332
|
+
html=html_fragment,
|
|
333
|
+
position=order,
|
|
334
|
+
heading_levels=_heading_levels_for_nodes(render_nodes),
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
for node in section_nodes:
|
|
338
|
+
if hasattr(node, "extract"):
|
|
339
|
+
node.extract()
|
|
340
|
+
|
|
341
|
+
container = soup.body or soup
|
|
342
|
+
if full_document_slots:
|
|
343
|
+
remainder_html = ""
|
|
344
|
+
else:
|
|
345
|
+
remainder_html = "".join(str(node) for node in container.contents)
|
|
346
|
+
|
|
347
|
+
remainder_position = max(fragment.position for fragment in fragments) + 1 if fragments else 0
|
|
348
|
+
|
|
349
|
+
fragments.append(
|
|
350
|
+
SlotFragment(
|
|
351
|
+
name=default_slot,
|
|
352
|
+
html=remainder_html,
|
|
353
|
+
position=remainder_position,
|
|
354
|
+
heading_levels=_heading_levels_for_nodes(container.contents),
|
|
355
|
+
)
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
fragments.sort(key=lambda fragment: fragment.position)
|
|
359
|
+
return fragments, missing
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _heading_levels_for_nodes(nodes: Iterable[Any]) -> list[int]:
|
|
363
|
+
"""Return heading levels discovered in the given node sequence (document order)."""
|
|
364
|
+
levels: list[int] = []
|
|
365
|
+
|
|
366
|
+
def _walk(node: Any) -> None:
|
|
367
|
+
if isinstance(node, Tag):
|
|
368
|
+
if re.fullmatch(r"h[1-6]", node.name or ""):
|
|
369
|
+
with contextlib.suppress(ValueError):
|
|
370
|
+
levels.append(heading_level_for(node))
|
|
371
|
+
for child in node.children:
|
|
372
|
+
_walk(child)
|
|
373
|
+
|
|
374
|
+
for node in nodes:
|
|
375
|
+
_walk(node)
|
|
376
|
+
|
|
377
|
+
return levels
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def collect_section_nodes(heading: Tag) -> list[Any]:
|
|
381
|
+
"""Collect a heading node and its associated section content."""
|
|
382
|
+
nodes: list[Any] = [heading]
|
|
383
|
+
heading_level = heading_level_for(heading)
|
|
384
|
+
for sibling in heading.next_siblings:
|
|
385
|
+
if isinstance(sibling, NavigableString):
|
|
386
|
+
nodes.append(sibling)
|
|
387
|
+
continue
|
|
388
|
+
if isinstance(sibling, Tag):
|
|
389
|
+
if re.fullmatch(r"h[1-6]", sibling.name or ""):
|
|
390
|
+
sibling_level = heading_level_for(sibling)
|
|
391
|
+
if sibling_level <= heading_level:
|
|
392
|
+
break
|
|
393
|
+
nodes.append(sibling)
|
|
394
|
+
return nodes
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def heading_level_for(node: Tag) -> int:
|
|
398
|
+
"""Return the numeric level of a heading element."""
|
|
399
|
+
name = node.name or ""
|
|
400
|
+
if not re.fullmatch(r"h[1-6]", name):
|
|
401
|
+
raise ValueError(f"Expected heading element, got '{name}'.")
|
|
402
|
+
return int(name[1])
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def compute_heading_offset(
|
|
406
|
+
html: str,
|
|
407
|
+
*,
|
|
408
|
+
drop_first_heading: bool = False,
|
|
409
|
+
parser_backend: str = "html.parser",
|
|
410
|
+
) -> int:
|
|
411
|
+
"""Return the offset required to align the top heading to level 1.
|
|
412
|
+
|
|
413
|
+
The shallowest heading in the fragment counts as offset ``0``; headings
|
|
414
|
+
starting at ``<h2>`` therefore yield ``-1``. When ``drop_first_heading`` is
|
|
415
|
+
true the first heading is ignored to mirror title promotion.
|
|
416
|
+
"""
|
|
417
|
+
try:
|
|
418
|
+
soup = BeautifulSoup(html, parser_backend)
|
|
419
|
+
except FeatureNotFound:
|
|
420
|
+
soup = BeautifulSoup(html, "html.parser")
|
|
421
|
+
|
|
422
|
+
headings = soup.find_all(re.compile(r"^h[1-6]$"), recursive=True)
|
|
423
|
+
if drop_first_heading and headings:
|
|
424
|
+
headings = headings[1:]
|
|
425
|
+
|
|
426
|
+
minimum: int | None = None
|
|
427
|
+
for heading in headings:
|
|
428
|
+
try:
|
|
429
|
+
level = heading_level_for(heading)
|
|
430
|
+
except ValueError:
|
|
431
|
+
continue
|
|
432
|
+
if minimum is None or level < minimum:
|
|
433
|
+
minimum = level
|
|
434
|
+
|
|
435
|
+
if minimum is None:
|
|
436
|
+
return 0
|
|
437
|
+
return 1 - minimum
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _load_inline_bibliography(
|
|
441
|
+
collection: BibliographyCollection,
|
|
442
|
+
entries: Mapping[str, InlineBibliographyEntry],
|
|
443
|
+
*,
|
|
444
|
+
source_label: str,
|
|
445
|
+
output_dir: Path | None = None,
|
|
446
|
+
emitter: DiagnosticEmitter,
|
|
447
|
+
fetcher: DoiBibliographyFetcher | None = None,
|
|
448
|
+
) -> None:
|
|
449
|
+
if not entries:
|
|
450
|
+
return
|
|
451
|
+
|
|
452
|
+
resolver = fetcher
|
|
453
|
+
source_path = _inline_bibliography_source_path(source_label)
|
|
454
|
+
cache_entries, cache_path = _initialise_doi_cache(output_dir)
|
|
455
|
+
cache_dirty = False
|
|
456
|
+
doi_support: tuple[type[Exception], Any] | None = None
|
|
457
|
+
|
|
458
|
+
for key, entry in entries.items():
|
|
459
|
+
if entry.doi:
|
|
460
|
+
if doi_support is None:
|
|
461
|
+
_, lookup_error_cls, normalise_doi_fn = _ensure_doi_support()
|
|
462
|
+
doi_support = (lookup_error_cls, normalise_doi_fn)
|
|
463
|
+
lookup_error_cls, normalise_doi_fn = doi_support
|
|
464
|
+
doi_value = entry.doi
|
|
465
|
+
try:
|
|
466
|
+
doi_key = normalise_doi_fn(doi_value)
|
|
467
|
+
except lookup_error_cls as exc:
|
|
468
|
+
emitter.warning(f"Failed to resolve DOI '{doi_value}' for '{key}': {exc}")
|
|
469
|
+
continue
|
|
470
|
+
|
|
471
|
+
payload = cache_entries.get(doi_key)
|
|
472
|
+
cache_mode = "doi_cache" if payload is not None else "doi"
|
|
473
|
+
|
|
474
|
+
if payload is None:
|
|
475
|
+
if resolver is None:
|
|
476
|
+
resolver = _resolve_bibliography_fetcher()
|
|
477
|
+
try:
|
|
478
|
+
payload = resolver.fetch(doi_value)
|
|
479
|
+
except lookup_error_cls as exc:
|
|
480
|
+
emitter.warning(f"Failed to resolve DOI '{doi_value}' for '{key}': {exc}")
|
|
481
|
+
continue
|
|
482
|
+
cache_entries[doi_key] = payload
|
|
483
|
+
cache_dirty = True
|
|
484
|
+
|
|
485
|
+
try:
|
|
486
|
+
data = bibliography_data_from_string(payload, key)
|
|
487
|
+
except PybtexError as exc:
|
|
488
|
+
emitter.warning(f"Failed to parse bibliography entry '{key}': {exc}")
|
|
489
|
+
if cache_mode == "doi":
|
|
490
|
+
cache_entries.pop(doi_key, None)
|
|
491
|
+
continue
|
|
492
|
+
doi_source = source_path.with_stem(f"{source_path.stem}-doi")
|
|
493
|
+
collection.load_data(data, source=doi_source)
|
|
494
|
+
record_event(
|
|
495
|
+
emitter,
|
|
496
|
+
"doi_fetch",
|
|
497
|
+
{
|
|
498
|
+
"key": key,
|
|
499
|
+
"value": doi_value,
|
|
500
|
+
"mode": cache_mode,
|
|
501
|
+
"source": source_label,
|
|
502
|
+
"resolved_source": str(source_path),
|
|
503
|
+
},
|
|
504
|
+
)
|
|
505
|
+
continue
|
|
506
|
+
|
|
507
|
+
if entry.is_manual:
|
|
508
|
+
try:
|
|
509
|
+
data = bibliography_data_from_inline_entry(key, entry)
|
|
510
|
+
except (ValueError, PybtexError) as exc:
|
|
511
|
+
emitter.warning(f"Failed to materialise bibliography entry '{key}': {exc}")
|
|
512
|
+
continue
|
|
513
|
+
collection.load_data(data, source=source_path)
|
|
514
|
+
record_event(
|
|
515
|
+
emitter,
|
|
516
|
+
"inline_bibliography",
|
|
517
|
+
{
|
|
518
|
+
"key": key,
|
|
519
|
+
"mode": "manual",
|
|
520
|
+
"source": source_label,
|
|
521
|
+
"resolved_source": str(source_path),
|
|
522
|
+
},
|
|
523
|
+
)
|
|
524
|
+
continue
|
|
525
|
+
|
|
526
|
+
emitter.warning(
|
|
527
|
+
f"Bibliography entry '{key}' does not provide a DOI or manual fields; skipping."
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
if cache_dirty and cache_path is not None:
|
|
531
|
+
_write_doi_cache(cache_path, cache_entries)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _inline_bibliography_source_path(label: str) -> Path:
|
|
535
|
+
slug = slugify(label, separator="-")
|
|
536
|
+
if not slug:
|
|
537
|
+
slug = "frontmatter"
|
|
538
|
+
return Path(f"frontmatter-{slug}.bib")
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
def _resolve_bibliography_fetcher() -> DoiBibliographyFetcher:
|
|
542
|
+
fetcher_cls, _, _ = _ensure_doi_support()
|
|
543
|
+
return fetcher_cls()
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
_DOI_CACHE_FILENAME = "texsmith-doi-cache.yaml"
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def _initialise_doi_cache(output_dir: Path | None) -> tuple[dict[str, str], Path | None]:
|
|
550
|
+
if output_dir is None:
|
|
551
|
+
return {}, None
|
|
552
|
+
|
|
553
|
+
cache_path = output_dir / _DOI_CACHE_FILENAME
|
|
554
|
+
try:
|
|
555
|
+
raw_text = cache_path.read_text(encoding="utf-8")
|
|
556
|
+
except FileNotFoundError:
|
|
557
|
+
return {}, cache_path
|
|
558
|
+
except OSError:
|
|
559
|
+
return {}, cache_path
|
|
560
|
+
|
|
561
|
+
try:
|
|
562
|
+
payload = yaml.safe_load(raw_text) or {}
|
|
563
|
+
except yaml.YAMLError:
|
|
564
|
+
return {}, cache_path
|
|
565
|
+
|
|
566
|
+
entries_payload: Any
|
|
567
|
+
if isinstance(payload, dict) and isinstance(payload.get("entries"), dict):
|
|
568
|
+
entries_payload = payload["entries"]
|
|
569
|
+
elif isinstance(payload, dict):
|
|
570
|
+
entries_payload = payload
|
|
571
|
+
else:
|
|
572
|
+
entries_payload = {}
|
|
573
|
+
|
|
574
|
+
entries: dict[str, str] = {}
|
|
575
|
+
normalise_fn: Any | None = None
|
|
576
|
+
lookup_error_cls: type[Exception] | None = None
|
|
577
|
+
if isinstance(entries_payload, dict):
|
|
578
|
+
for key, value in entries_payload.items():
|
|
579
|
+
if not isinstance(key, str) or not isinstance(value, str):
|
|
580
|
+
continue
|
|
581
|
+
try:
|
|
582
|
+
if normalise_fn is None:
|
|
583
|
+
_, lookup_error_cls, normalise_fn = _ensure_doi_support()
|
|
584
|
+
normalised = normalise_fn(key)
|
|
585
|
+
except Exception as exc:
|
|
586
|
+
if lookup_error_cls is not None and isinstance(exc, lookup_error_cls):
|
|
587
|
+
continue
|
|
588
|
+
raise
|
|
589
|
+
entries[normalised] = value
|
|
590
|
+
|
|
591
|
+
return entries, cache_path
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _write_doi_cache(path: Path, entries: dict[str, str]) -> None:
|
|
595
|
+
document = {
|
|
596
|
+
"version": 1,
|
|
597
|
+
"entries": {key: entries[key] for key in sorted(entries)},
|
|
598
|
+
}
|
|
599
|
+
try:
|
|
600
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
601
|
+
path.write_text(
|
|
602
|
+
yaml.safe_dump(
|
|
603
|
+
document,
|
|
604
|
+
sort_keys=True,
|
|
605
|
+
default_flow_style=False,
|
|
606
|
+
encoding=None,
|
|
607
|
+
),
|
|
608
|
+
encoding="utf-8",
|
|
609
|
+
)
|
|
610
|
+
except OSError:
|
|
611
|
+
return
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def _ensure_doi_support() -> tuple[type[Any], type[Exception], Any]:
|
|
615
|
+
"""Lazily import DOI helpers to avoid pulling in requests unless required."""
|
|
616
|
+
global _DOI_SUPPORT, DoiBibliographyFetcher
|
|
617
|
+
|
|
618
|
+
if _DOI_SUPPORT is None:
|
|
619
|
+
from ..bibliography.doi import DoiLookupError, normalise_doi
|
|
620
|
+
|
|
621
|
+
_DOI_SUPPORT = {
|
|
622
|
+
"lookup_error": DoiLookupError,
|
|
623
|
+
"normalise": normalise_doi,
|
|
624
|
+
}
|
|
625
|
+
if DoiBibliographyFetcher is None:
|
|
626
|
+
from ..bibliography.doi import DoiBibliographyFetcher as _Fetcher
|
|
627
|
+
|
|
628
|
+
DoiBibliographyFetcher = _Fetcher
|
|
629
|
+
|
|
630
|
+
assert _DOI_SUPPORT is not None
|
|
631
|
+
assert DoiBibliographyFetcher is not None
|
|
632
|
+
return (
|
|
633
|
+
DoiBibliographyFetcher,
|
|
634
|
+
_DOI_SUPPORT["lookup_error"],
|
|
635
|
+
_DOI_SUPPORT["normalise"],
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
__all__ = [
|
|
640
|
+
"SlotFragment",
|
|
641
|
+
"build_binder_context",
|
|
642
|
+
"collect_section_nodes",
|
|
643
|
+
"compute_heading_offset",
|
|
644
|
+
"extract_slot_fragments",
|
|
645
|
+
"heading_level_for",
|
|
646
|
+
]
|