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,59 @@
|
|
|
1
|
+
"""Utility helpers shared across transformer strategies."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from texsmith.core.exceptions import TransformerExecutionError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def points_to_mm(points: float) -> float:
|
|
11
|
+
"""Convert PDF points to millimetres."""
|
|
12
|
+
return points * 25.4 / 72
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def normalise_pdf_version(pdf_path: Path, *, target_version: str = "1.5") -> None:
|
|
16
|
+
"""Re-write a PDF so its header advertises the requested version."""
|
|
17
|
+
try:
|
|
18
|
+
import pypdf # type: ignore[import]
|
|
19
|
+
except ImportError as exc: # pragma: no cover - optional dependency
|
|
20
|
+
msg = "pypdf is required to normalise PDF artefacts."
|
|
21
|
+
raise TransformerExecutionError(msg) from exc
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
reader = pypdf.PdfReader(str(pdf_path))
|
|
25
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
26
|
+
msg = f"Failed to read generated PDF '{pdf_path}': {exc}"
|
|
27
|
+
raise TransformerExecutionError(msg) from exc
|
|
28
|
+
|
|
29
|
+
# Skip rewriting when already on the desired version.
|
|
30
|
+
header = getattr(reader, "pdf_header", b"")
|
|
31
|
+
if isinstance(header, bytes):
|
|
32
|
+
header_text = header.decode("latin-1", "ignore")
|
|
33
|
+
if header_text.startswith("%PDF-"):
|
|
34
|
+
current_version = header_text[5:].strip()
|
|
35
|
+
if current_version == target_version:
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
writer = pypdf.PdfWriter()
|
|
39
|
+
for page in reader.pages:
|
|
40
|
+
writer.add_page(page)
|
|
41
|
+
writer.pdf_header = f"%PDF-{target_version}\n"
|
|
42
|
+
|
|
43
|
+
metadata = reader.metadata or {}
|
|
44
|
+
filtered_metadata = {k: v for k, v in metadata.items() if isinstance(v, str)}
|
|
45
|
+
if filtered_metadata:
|
|
46
|
+
writer.add_metadata(filtered_metadata)
|
|
47
|
+
|
|
48
|
+
tmp_path = pdf_path.with_suffix(pdf_path.suffix + ".tmp")
|
|
49
|
+
try:
|
|
50
|
+
with tmp_path.open("wb") as handle:
|
|
51
|
+
writer.write(handle)
|
|
52
|
+
except Exception as exc: # pragma: no cover - defensive
|
|
53
|
+
msg = f"Failed to rewrite PDF '{pdf_path}' to version {target_version}: {exc}"
|
|
54
|
+
raise TransformerExecutionError(msg) from exc
|
|
55
|
+
|
|
56
|
+
tmp_path.replace(pdf_path)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
__all__ = ["normalise_pdf_version", "points_to_mm"]
|
texsmith/api/__init__.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Facade aggregating the high-level TeXSmith authoring experience.
|
|
2
|
+
|
|
3
|
+
Architecture
|
|
4
|
+
: `Document` and `DocumentRenderOptions` model the source inputs that travel
|
|
5
|
+
through the conversion pipeline. They encapsulate content, front matter, and
|
|
6
|
+
slot selections independently of the engine internals.
|
|
7
|
+
: `convert_documents` together with `RenderSettings` coordinates the conversion
|
|
8
|
+
engine and wraps results in `ConversionBundle` instances exposing convenience
|
|
9
|
+
helpers such as `combined_output`.
|
|
10
|
+
: `TemplateSession` and related classes manage higher-level template workflows,
|
|
11
|
+
combining multiple converted fragments into coherent LaTeX projects.
|
|
12
|
+
|
|
13
|
+
Implementation Rationale
|
|
14
|
+
: Providing downstream code with a single import location helps preserve
|
|
15
|
+
backward compatibility. Internal modules can evolve while this facade offers a
|
|
16
|
+
declarative list of supported entry points.
|
|
17
|
+
: Concentrating documentation and doctest coverage here guides users toward the
|
|
18
|
+
intended composition patterns when embedding TeXSmith.
|
|
19
|
+
|
|
20
|
+
Usage Example
|
|
21
|
+
:
|
|
22
|
+
>>> from pathlib import Path
|
|
23
|
+
>>> from tempfile import TemporaryDirectory
|
|
24
|
+
>>> from texsmith.api import Document, convert_documents
|
|
25
|
+
>>> with TemporaryDirectory() as tmpdir:
|
|
26
|
+
... path = Path(tmpdir) / "example.md"
|
|
27
|
+
... _ = path.write_text("# Demo\\nHello")
|
|
28
|
+
... bundle = convert_documents([Document.from_markdown(path)])
|
|
29
|
+
... [fragment.stem for fragment in bundle.fragments]
|
|
30
|
+
['example']
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from texsmith.core.user_dir import (
|
|
36
|
+
TexsmithUserDir,
|
|
37
|
+
configure_user_dir,
|
|
38
|
+
get_user_dir,
|
|
39
|
+
user_dir_context,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
from .document import Document, DocumentRenderOptions, TitleStrategy
|
|
43
|
+
from .pipeline import ConversionBundle, LaTeXFragment, RenderSettings, convert_documents
|
|
44
|
+
from .service import (
|
|
45
|
+
ConversionRequest,
|
|
46
|
+
ConversionResponse,
|
|
47
|
+
ConversionService,
|
|
48
|
+
SlotAssignment,
|
|
49
|
+
classify_input_source,
|
|
50
|
+
)
|
|
51
|
+
from .templates import (
|
|
52
|
+
TemplateOptions,
|
|
53
|
+
TemplateRenderResult,
|
|
54
|
+
TemplateSession,
|
|
55
|
+
get_template,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
__all__ = [
|
|
60
|
+
"ConversionBundle",
|
|
61
|
+
"ConversionRequest",
|
|
62
|
+
"ConversionResponse",
|
|
63
|
+
"ConversionService",
|
|
64
|
+
"Document",
|
|
65
|
+
"DocumentRenderOptions",
|
|
66
|
+
"LaTeXFragment",
|
|
67
|
+
"RenderSettings",
|
|
68
|
+
"SlotAssignment",
|
|
69
|
+
"TemplateOptions",
|
|
70
|
+
"TemplateRenderResult",
|
|
71
|
+
"TemplateSession",
|
|
72
|
+
"TexsmithUserDir",
|
|
73
|
+
"TitleStrategy",
|
|
74
|
+
"classify_input_source",
|
|
75
|
+
"configure_user_dir",
|
|
76
|
+
"convert_documents",
|
|
77
|
+
"get_template",
|
|
78
|
+
"get_user_dir",
|
|
79
|
+
"user_dir_context",
|
|
80
|
+
]
|
texsmith/api/_utils.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Supporting utilities shared by the high-level API modules.
|
|
2
|
+
|
|
3
|
+
Architecture
|
|
4
|
+
: `build_unique_stem_map` receives the original source paths used by the API and
|
|
5
|
+
produces collision-free stem identifiers. Conversion pipelines rely on these
|
|
6
|
+
stems to generate fragment file names and manifest entries without leaking
|
|
7
|
+
implementation details from the caller.
|
|
8
|
+
|
|
9
|
+
Implementation Rationale
|
|
10
|
+
: The conversion pipeline needs deterministic names to produce predictable
|
|
11
|
+
outputs across runs. Centralising the stem logic ensures that future
|
|
12
|
+
refinements, such as Unicode slug handling, propagate consistently.
|
|
13
|
+
: Encapsulating the naming strategy inside a dedicated helper keeps pipeline
|
|
14
|
+
tests focused on transformation behaviour instead of string manipulation edge
|
|
15
|
+
cases.
|
|
16
|
+
|
|
17
|
+
Usage Example
|
|
18
|
+
:
|
|
19
|
+
>>> from pathlib import Path
|
|
20
|
+
>>> paths = [Path("book/chapter.md"), Path("notes/chapter.md"), Path("notes/appendix.md")]
|
|
21
|
+
>>> mapping = build_unique_stem_map(paths)
|
|
22
|
+
>>> mapping[Path("book/chapter.md")]
|
|
23
|
+
'chapter'
|
|
24
|
+
>>> mapping[Path("notes/chapter.md")]
|
|
25
|
+
'chapter-2'
|
|
26
|
+
>>> mapping[Path("notes/appendix.md")]
|
|
27
|
+
'appendix'
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from collections.abc import Iterable
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__all__ = ["build_unique_stem_map"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def build_unique_stem_map(paths: Iterable[Path]) -> dict[Path, str]:
|
|
40
|
+
"""Generate unique stems for a sequence of paths to avoid filename collisions during rendering."""
|
|
41
|
+
counters: dict[str, int] = {}
|
|
42
|
+
used: set[str] = set()
|
|
43
|
+
mapping: dict[Path, str] = {}
|
|
44
|
+
|
|
45
|
+
for path in paths:
|
|
46
|
+
base = path.stem
|
|
47
|
+
index = counters.get(base, 0)
|
|
48
|
+
candidate = base if index == 0 else f"{base}-{index + 1}"
|
|
49
|
+
while candidate in used:
|
|
50
|
+
index += 1
|
|
51
|
+
candidate = f"{base}-{index + 1}"
|
|
52
|
+
counters[base] = index + 1
|
|
53
|
+
used.add(candidate)
|
|
54
|
+
mapping[path] = candidate
|
|
55
|
+
|
|
56
|
+
return mapping
|