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,366 @@
|
|
|
1
|
+
"""Bundled Tectonic (and helper) acquisition and selection helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import platform
|
|
8
|
+
import shutil
|
|
9
|
+
import stat
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import tarfile
|
|
13
|
+
import tempfile
|
|
14
|
+
from urllib.error import URLError
|
|
15
|
+
from urllib.request import urlopen
|
|
16
|
+
import zipfile
|
|
17
|
+
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
|
|
20
|
+
from texsmith.core.user_dir import get_user_dir
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
TECTONIC_VERSION = "0.15.0"
|
|
24
|
+
BIBER_VERSION = "2.17"
|
|
25
|
+
MAKEGLOSSARIES_URL = "https://mirrors.ctan.org/macros/latex/contrib/glossaries.zip"
|
|
26
|
+
_BASE_URL = (
|
|
27
|
+
"https://github.com/tectonic-typesetting/tectonic/releases/download/"
|
|
28
|
+
f"tectonic%40{TECTONIC_VERSION}/tectonic-{TECTONIC_VERSION}-"
|
|
29
|
+
)
|
|
30
|
+
_BIBER_BASE_URL = (
|
|
31
|
+
"https://sourceforge.net/projects/biblatex-biber/files/biblatex-biber/"
|
|
32
|
+
f"{BIBER_VERSION}/binaries/"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class BundledToolError(RuntimeError):
|
|
37
|
+
"""Raised when a bundled tool cannot be prepared."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TectonicAcquisitionError(BundledToolError):
|
|
41
|
+
"""Raised when the bundled Tectonic binary cannot be prepared."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class BiberAcquisitionError(BundledToolError):
|
|
45
|
+
"""Raised when the bundled Biber binary cannot be prepared."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class MakeglossariesAcquisitionError(BundledToolError):
|
|
49
|
+
"""Raised when the makeglossaries helper cannot be prepared."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass(slots=True)
|
|
53
|
+
class TectonicSelection:
|
|
54
|
+
"""Resolved Tectonic binary and its origin."""
|
|
55
|
+
|
|
56
|
+
path: Path
|
|
57
|
+
source: str # "bundled" or "system"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(slots=True)
|
|
61
|
+
class HelperSelection:
|
|
62
|
+
"""Resolved auxiliary tool selection (system or bundled)."""
|
|
63
|
+
|
|
64
|
+
path: Path
|
|
65
|
+
source: str # "bundled" or "system"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def select_tectonic_binary(
|
|
69
|
+
use_system: bool = False, *, console: Console | None = None
|
|
70
|
+
) -> TectonicSelection:
|
|
71
|
+
"""Return the Tectonic binary to invoke, downloading when needed."""
|
|
72
|
+
if use_system:
|
|
73
|
+
system_path = shutil.which("tectonic")
|
|
74
|
+
if system_path is None:
|
|
75
|
+
raise TectonicAcquisitionError(
|
|
76
|
+
"Tectonic is not available on PATH (use --engine lualatex/xelatex "
|
|
77
|
+
"or omit --system to download the bundled binary)."
|
|
78
|
+
)
|
|
79
|
+
return TectonicSelection(path=Path(system_path), source="system")
|
|
80
|
+
|
|
81
|
+
bundled_path = _ensure_bundled_binary(console=console)
|
|
82
|
+
return TectonicSelection(path=bundled_path, source="bundled")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _ensure_bundled_binary(*, console: Console | None) -> Path:
|
|
86
|
+
install_dir = _install_dir()
|
|
87
|
+
binary_name = "tectonic.exe" if _is_windows() else "tectonic"
|
|
88
|
+
target = install_dir / binary_name
|
|
89
|
+
|
|
90
|
+
if target.exists() and target.is_file():
|
|
91
|
+
if _binary_matches_version(target):
|
|
92
|
+
_log(console, f"Using bundled Tectonic at {target}")
|
|
93
|
+
return target
|
|
94
|
+
_log(console, f"Refreshing bundled Tectonic in {install_dir}")
|
|
95
|
+
target.unlink(missing_ok=True)
|
|
96
|
+
|
|
97
|
+
arch, archive_ext = _detect_architecture()
|
|
98
|
+
url = f"{_BASE_URL}{arch}{archive_ext}"
|
|
99
|
+
_log(console, f"Downloading Tectonic {TECTONIC_VERSION} for {arch}…")
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
with tempfile.TemporaryDirectory(prefix="texsmith-tectonic-") as tmpdir:
|
|
103
|
+
archive_path = Path(tmpdir) / f"tectonic{archive_ext}"
|
|
104
|
+
_download_file(url, archive_path)
|
|
105
|
+
extracted_root = Path(tmpdir) / "extracted"
|
|
106
|
+
extracted_root.mkdir(parents=True, exist_ok=True)
|
|
107
|
+
_extract_archive(archive_path, extracted_root)
|
|
108
|
+
candidate = _find_binary(extracted_root, binary_name)
|
|
109
|
+
_log(console, f"Installing bundled Tectonic into {install_dir}")
|
|
110
|
+
install_dir.mkdir(parents=True, exist_ok=True)
|
|
111
|
+
source_dir = candidate.parent
|
|
112
|
+
if _is_windows():
|
|
113
|
+
# Windows builds ship DLL sidecars; copy them alongside the binary.
|
|
114
|
+
for item in source_dir.iterdir():
|
|
115
|
+
destination = install_dir / item.name
|
|
116
|
+
if destination.exists():
|
|
117
|
+
if destination.is_dir():
|
|
118
|
+
shutil.rmtree(destination)
|
|
119
|
+
else:
|
|
120
|
+
destination.unlink()
|
|
121
|
+
if item.is_dir():
|
|
122
|
+
shutil.copytree(item, destination)
|
|
123
|
+
else:
|
|
124
|
+
shutil.copy2(item, destination)
|
|
125
|
+
target = install_dir / binary_name
|
|
126
|
+
target.chmod(target.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
127
|
+
else:
|
|
128
|
+
shutil.move(str(candidate), target)
|
|
129
|
+
target.chmod(target.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
130
|
+
except (OSError, URLError, zipfile.BadZipFile, tarfile.TarError) as exc:
|
|
131
|
+
raise TectonicAcquisitionError(f"Unable to download bundled Tectonic: {exc}") from exc
|
|
132
|
+
|
|
133
|
+
return target
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def select_biber_binary(*, console: Console | None = None) -> Path:
|
|
137
|
+
"""Return the bundled Biber binary, downloading when needed."""
|
|
138
|
+
install_dir = _install_dir()
|
|
139
|
+
binary_name = "biber.exe" if _is_windows() else "biber"
|
|
140
|
+
target = install_dir / binary_name
|
|
141
|
+
|
|
142
|
+
if target.exists() and target.is_file():
|
|
143
|
+
if _binary_matches_version(target, expected=BIBER_VERSION):
|
|
144
|
+
_log(console, f"Using bundled Biber at {target}", tool="biber")
|
|
145
|
+
return target
|
|
146
|
+
_log(console, f"Refreshing bundled Biber in {install_dir}", tool="biber")
|
|
147
|
+
target.unlink(missing_ok=True)
|
|
148
|
+
|
|
149
|
+
archive_url, archive_ext = _detect_biber_archive()
|
|
150
|
+
_log(console, f"Downloading Biber {BIBER_VERSION} ({archive_url})…", tool="biber")
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
with tempfile.TemporaryDirectory(prefix="texsmith-biber-") as tmpdir:
|
|
154
|
+
archive_path = Path(tmpdir) / f"biber{archive_ext}"
|
|
155
|
+
_download_file(archive_url, archive_path)
|
|
156
|
+
extracted_root = Path(tmpdir) / "extracted"
|
|
157
|
+
extracted_root.mkdir(parents=True, exist_ok=True)
|
|
158
|
+
_extract_archive(archive_path, extracted_root)
|
|
159
|
+
candidate = _find_binary(extracted_root, binary_name, error_cls=BiberAcquisitionError)
|
|
160
|
+
_log(console, f"Installing bundled Biber into {install_dir}", tool="biber")
|
|
161
|
+
shutil.move(str(candidate), target)
|
|
162
|
+
target.chmod(target.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
163
|
+
except (OSError, URLError, zipfile.BadZipFile, tarfile.TarError) as exc:
|
|
164
|
+
raise BiberAcquisitionError(f"Unable to download bundled Biber: {exc}") from exc
|
|
165
|
+
|
|
166
|
+
return target
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def select_makeglossaries(*, console: Console | None = None) -> HelperSelection:
|
|
170
|
+
"""Return a makeglossaries command, downloading the Perl helper if absent."""
|
|
171
|
+
existing = shutil.which("makeglossaries")
|
|
172
|
+
if existing:
|
|
173
|
+
return HelperSelection(path=Path(existing), source="system")
|
|
174
|
+
bundled = _ensure_makeglossaries(console=console)
|
|
175
|
+
return HelperSelection(path=bundled, source="bundled")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _ensure_makeglossaries(*, console: Console | None) -> Path:
|
|
179
|
+
install_dir = _install_dir()
|
|
180
|
+
target = install_dir / "makeglossaries"
|
|
181
|
+
|
|
182
|
+
if target.exists() and target.is_file():
|
|
183
|
+
_log(console, f"Using bundled makeglossaries at {target}", tool="makeglossaries")
|
|
184
|
+
return target
|
|
185
|
+
|
|
186
|
+
_log(console, f"Downloading makeglossaries helper into {install_dir}", tool="makeglossaries")
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
with tempfile.TemporaryDirectory(prefix="texsmith-makeglossaries-") as tmpdir:
|
|
190
|
+
archive_path = Path(tmpdir) / "glossaries.zip"
|
|
191
|
+
_download_file(MAKEGLOSSARIES_URL, archive_path)
|
|
192
|
+
extracted_root = Path(tmpdir) / "extracted"
|
|
193
|
+
extracted_root.mkdir(parents=True, exist_ok=True)
|
|
194
|
+
_extract_archive(archive_path, extracted_root)
|
|
195
|
+
candidate = _find_binary(
|
|
196
|
+
extracted_root, "makeglossaries", error_cls=MakeglossariesAcquisitionError
|
|
197
|
+
)
|
|
198
|
+
shutil.move(str(candidate), target)
|
|
199
|
+
target.chmod(target.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
200
|
+
except (OSError, URLError, zipfile.BadZipFile, tarfile.TarError) as exc:
|
|
201
|
+
raise MakeglossariesAcquisitionError(
|
|
202
|
+
f"Unable to download makeglossaries helper: {exc}"
|
|
203
|
+
) from exc
|
|
204
|
+
|
|
205
|
+
return target
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _install_dir() -> Path:
|
|
209
|
+
return get_user_dir().data_dir("bin")
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _download_file(url: str, destination: Path) -> None:
|
|
213
|
+
with urlopen(url) as response, destination.open("wb") as handle:
|
|
214
|
+
shutil.copyfileobj(response, handle)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _extract_archive(archive_path: Path, destination: Path) -> None:
|
|
218
|
+
suffixes = archive_path.suffixes
|
|
219
|
+
if suffixes and suffixes[-1] == ".zip":
|
|
220
|
+
with zipfile.ZipFile(archive_path) as archive:
|
|
221
|
+
archive.extractall(destination)
|
|
222
|
+
return
|
|
223
|
+
mode = "r:gz" if suffixes[-2:] == [".tar", ".gz"] or suffixes[-1:] == [".tgz"] else "r:*"
|
|
224
|
+
with tarfile.open(archive_path, mode) as archive:
|
|
225
|
+
try:
|
|
226
|
+
archive.extractall(destination, filter="data")
|
|
227
|
+
except TypeError: # Python < 3.12 compatibility (no filter kw)
|
|
228
|
+
archive.extractall(destination)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _find_binary(
|
|
232
|
+
root: Path, name: str, *, error_cls: type[BundledToolError] = TectonicAcquisitionError
|
|
233
|
+
) -> Path:
|
|
234
|
+
for candidate in root.rglob(name):
|
|
235
|
+
if candidate.is_file():
|
|
236
|
+
return candidate
|
|
237
|
+
raise error_cls(f"{name} binary not found in archive contents for {root}")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _binary_matches_version(binary: Path, *, expected: str = TECTONIC_VERSION) -> bool:
|
|
241
|
+
return _probe_version(binary, expected=expected)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _probe_version(binary: Path, *, expected: str) -> bool:
|
|
245
|
+
try:
|
|
246
|
+
process = subprocess.run(
|
|
247
|
+
[str(binary), "--version"],
|
|
248
|
+
check=False,
|
|
249
|
+
capture_output=True,
|
|
250
|
+
text=True,
|
|
251
|
+
timeout=10,
|
|
252
|
+
)
|
|
253
|
+
except OSError:
|
|
254
|
+
return False
|
|
255
|
+
if process.returncode != 0:
|
|
256
|
+
return False
|
|
257
|
+
output = (process.stdout or "") + (process.stderr or "")
|
|
258
|
+
return expected in output
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _detect_architecture() -> tuple[str, str]:
|
|
262
|
+
"""Return (triple, archive_ext) compatible with official Tectonic releases."""
|
|
263
|
+
system, machine, is_musl = _platform_details()
|
|
264
|
+
|
|
265
|
+
if system == "Darwin":
|
|
266
|
+
ostype = "apple-darwin"
|
|
267
|
+
elif system == "Windows":
|
|
268
|
+
ostype = "pc-windows-msvc"
|
|
269
|
+
elif system == "Linux":
|
|
270
|
+
libc = "musl" if is_musl else "gnu"
|
|
271
|
+
ostype = f"unknown-linux-{libc}"
|
|
272
|
+
else:
|
|
273
|
+
raise TectonicAcquisitionError(f"Unsupported platform: {system}")
|
|
274
|
+
|
|
275
|
+
if machine in {"x86_64", "x86-64", "amd64", "x64"}:
|
|
276
|
+
cputype = "x86_64"
|
|
277
|
+
elif machine in {"i386", "i486", "i686", "i786", "x86"}:
|
|
278
|
+
cputype = "i686"
|
|
279
|
+
elif machine in {"aarch64", "arm64"}:
|
|
280
|
+
cputype = "aarch64"
|
|
281
|
+
elif machine in {"armv7l", "armv7"}:
|
|
282
|
+
cputype = "armv7"
|
|
283
|
+
if system == "Linux" and not is_musl:
|
|
284
|
+
ostype = f"{ostype}eabihf"
|
|
285
|
+
else:
|
|
286
|
+
raise TectonicAcquisitionError(f"Unsupported CPU architecture: {machine}")
|
|
287
|
+
|
|
288
|
+
ext = ".zip" if system == "Windows" else ".tar.gz"
|
|
289
|
+
return f"{cputype}-{ostype}", ext
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _detect_biber_archive() -> tuple[str, str]:
|
|
293
|
+
"""Return (url, archive_ext) for the appropriate Biber release."""
|
|
294
|
+
system, machine, is_musl = _platform_details()
|
|
295
|
+
|
|
296
|
+
if system == "Linux":
|
|
297
|
+
platform_dir = "Linux-musl" if is_musl else "Linux"
|
|
298
|
+
libc_suffix = "-musl" if is_musl else ""
|
|
299
|
+
if machine in {"x86_64", "x86-64", "amd64", "x64"}:
|
|
300
|
+
filename = f"biber-linux_x86_64{libc_suffix}.tar.gz"
|
|
301
|
+
elif machine in {"aarch64", "arm64"}:
|
|
302
|
+
filename = f"biber-linux_aarch64{libc_suffix}.tar.gz"
|
|
303
|
+
elif machine in {"i386", "i486", "i686", "i786", "x86"}:
|
|
304
|
+
filename = f"biber-linux_i386{libc_suffix}.tar.gz"
|
|
305
|
+
else:
|
|
306
|
+
raise BiberAcquisitionError(f"Unsupported CPU architecture for Biber: {machine}")
|
|
307
|
+
return f"{_BIBER_BASE_URL}{platform_dir}/{filename}", ".tar.gz"
|
|
308
|
+
|
|
309
|
+
if system == "Darwin":
|
|
310
|
+
if machine in {"aarch64", "arm64"}:
|
|
311
|
+
filename = "biber-darwin_arm64.tar.gz"
|
|
312
|
+
elif machine in {"x86_64", "x86-64", "amd64", "x64"}:
|
|
313
|
+
filename = "biber-darwin_x86_64.tar.gz"
|
|
314
|
+
else:
|
|
315
|
+
raise BiberAcquisitionError(f"Unsupported CPU architecture for Biber: {machine}")
|
|
316
|
+
return f"{_BIBER_BASE_URL}Darwin/{filename}", ".tar.gz"
|
|
317
|
+
|
|
318
|
+
if system == "Windows":
|
|
319
|
+
filename = (
|
|
320
|
+
"biber-MSWIN64.zip" if machine in {"x86_64", "amd64", "x64"} else "biber-MSWIN32.zip"
|
|
321
|
+
)
|
|
322
|
+
return f"{_BIBER_BASE_URL}Windows/{filename}", ".zip"
|
|
323
|
+
|
|
324
|
+
raise BiberAcquisitionError(f"Unsupported platform for bundled Biber: {system}")
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _platform_details() -> tuple[str, str, bool]:
|
|
328
|
+
system = platform.system()
|
|
329
|
+
machine = platform.machine().lower()
|
|
330
|
+
|
|
331
|
+
is_musl = False
|
|
332
|
+
if system == "Linux":
|
|
333
|
+
try:
|
|
334
|
+
output = subprocess.check_output(
|
|
335
|
+
["ldd", "--version"], stderr=subprocess.STDOUT, text=True
|
|
336
|
+
)
|
|
337
|
+
is_musl = "musl" in output.lower()
|
|
338
|
+
except (OSError, subprocess.CalledProcessError):
|
|
339
|
+
is_musl = False
|
|
340
|
+
|
|
341
|
+
return system, machine, is_musl
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _is_windows() -> bool:
|
|
345
|
+
return sys.platform.startswith("win")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _log(console: Console | None, message: str, *, tool: str = "tectonic") -> None:
|
|
349
|
+
if console is None:
|
|
350
|
+
return
|
|
351
|
+
console.log(f"[cyan]{tool}[/]: {message}")
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
__all__ = [
|
|
355
|
+
"BIBER_VERSION",
|
|
356
|
+
"TECTONIC_VERSION",
|
|
357
|
+
"BiberAcquisitionError",
|
|
358
|
+
"BundledToolError",
|
|
359
|
+
"HelperSelection",
|
|
360
|
+
"MakeglossariesAcquisitionError",
|
|
361
|
+
"TectonicAcquisitionError",
|
|
362
|
+
"TectonicSelection",
|
|
363
|
+
"select_biber_binary",
|
|
364
|
+
"select_makeglossaries",
|
|
365
|
+
"select_tectonic_binary",
|
|
366
|
+
]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Utility helpers specific to LaTeX rendering."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import unicodedata
|
|
7
|
+
|
|
8
|
+
from pylatexenc.latexencode import unicode_to_latex
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
_BASIC_LATEX_ESCAPE_MAP = {
|
|
12
|
+
"&": r"\&",
|
|
13
|
+
"%": r"\%",
|
|
14
|
+
"#": r"\#",
|
|
15
|
+
"$": r"\$",
|
|
16
|
+
"_": r"\_",
|
|
17
|
+
"^": r"\^",
|
|
18
|
+
"{": r"\{",
|
|
19
|
+
"}": r"\}",
|
|
20
|
+
"~": r"\textasciitilde{}",
|
|
21
|
+
"\\": r"\textbackslash{}",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
_ACCENT_NEEDS_BRACES_PATTERN = re.compile(
|
|
25
|
+
r"\\([" + re.escape("`'^\"~=\\.Hrvuck") + r"])\s*([A-Za-z])(?!\{)"
|
|
26
|
+
)
|
|
27
|
+
_ACCENT_CONTROL_TARGET_PATTERN = re.compile(
|
|
28
|
+
r"\\([" + re.escape("`'^\"~=\\.Hrvuck") + r"])\s*(\\[ij])"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _wrap_latex_output(payload: str) -> str:
|
|
33
|
+
"""Ensure accent macros wrap their payload in braces."""
|
|
34
|
+
|
|
35
|
+
def _repl(match: re.Match[str]) -> str:
|
|
36
|
+
command, char = match.groups()
|
|
37
|
+
return f"\\{command}{{{char}}}"
|
|
38
|
+
|
|
39
|
+
payload = _ACCENT_NEEDS_BRACES_PATTERN.sub(_repl, payload)
|
|
40
|
+
|
|
41
|
+
def _repl_control(match: re.Match[str]) -> str:
|
|
42
|
+
command, control = match.groups()
|
|
43
|
+
return f"\\{command}{{{control}}}"
|
|
44
|
+
|
|
45
|
+
return _ACCENT_CONTROL_TARGET_PATTERN.sub(_repl_control, payload)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def escape_latex_chars(text: str, *, legacy_accents: bool = False) -> str:
|
|
49
|
+
"""Escape LaTeX special characters leveraging pylatexenc."""
|
|
50
|
+
if not text:
|
|
51
|
+
return text
|
|
52
|
+
parts: list[str] = []
|
|
53
|
+
buffer: list[str] = []
|
|
54
|
+
|
|
55
|
+
def _encode_chunk(chunk: str) -> str:
|
|
56
|
+
escaped = "".join(_BASIC_LATEX_ESCAPE_MAP.get(char, char) for char in chunk)
|
|
57
|
+
if legacy_accents:
|
|
58
|
+
encoded = unicode_to_latex(escaped, non_ascii_only=True, unknown_char_warning=False)
|
|
59
|
+
return _wrap_latex_output(encoded)
|
|
60
|
+
return escaped
|
|
61
|
+
|
|
62
|
+
def _should_skip_encoding(char: str) -> bool:
|
|
63
|
+
try:
|
|
64
|
+
name = unicodedata.name(char)
|
|
65
|
+
except ValueError:
|
|
66
|
+
return False
|
|
67
|
+
if "SUPERSCRIPT" in name or "SUBSCRIPT" in name:
|
|
68
|
+
return True
|
|
69
|
+
return "MODIFIER LETTER" in name and ("SMALL" in name or "CAPITAL" in name)
|
|
70
|
+
|
|
71
|
+
for char in text:
|
|
72
|
+
if _should_skip_encoding(char):
|
|
73
|
+
if buffer:
|
|
74
|
+
parts.append(_encode_chunk("".join(buffer)))
|
|
75
|
+
buffer.clear()
|
|
76
|
+
parts.append(char)
|
|
77
|
+
else:
|
|
78
|
+
buffer.append(char)
|
|
79
|
+
|
|
80
|
+
if buffer:
|
|
81
|
+
parts.append(_encode_chunk("".join(buffer)))
|
|
82
|
+
|
|
83
|
+
return "".join(parts)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
__all__ = ["escape_latex_chars"]
|