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,179 @@
|
|
|
1
|
+
"""Centralised resolution of the TeXSmith user and cache directories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import shutil
|
|
11
|
+
from threading import RLock
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"TexsmithUserDir",
|
|
16
|
+
"configure_user_dir",
|
|
17
|
+
"get_user_dir",
|
|
18
|
+
"set_user_dir",
|
|
19
|
+
"user_dir_context",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
_USER_DIR: TexsmithUserDir | None = None
|
|
23
|
+
_LOCK: RLock = RLock()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _resolve_root(root: str | Path | None) -> tuple[Path, bool]:
|
|
27
|
+
if root is not None:
|
|
28
|
+
return Path(root).expanduser(), True
|
|
29
|
+
env_root = os.environ.get("TEXSMITH_HOME")
|
|
30
|
+
if env_root:
|
|
31
|
+
return Path(env_root).expanduser(), True
|
|
32
|
+
return Path.home() / ".texsmith", False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _resolve_cache_root(
|
|
36
|
+
cache_root: str | Path | None,
|
|
37
|
+
*,
|
|
38
|
+
user_root: Path,
|
|
39
|
+
root_was_explicit: bool,
|
|
40
|
+
) -> tuple[Path, bool]:
|
|
41
|
+
if cache_root is not None:
|
|
42
|
+
return Path(cache_root).expanduser(), True
|
|
43
|
+
env_cache = os.environ.get("TEXSMITH_CACHE_DIR")
|
|
44
|
+
if env_cache:
|
|
45
|
+
return Path(env_cache).expanduser(), True
|
|
46
|
+
xdg_cache = os.environ.get("XDG_CACHE_HOME")
|
|
47
|
+
if xdg_cache:
|
|
48
|
+
return Path(xdg_cache).expanduser() / "texsmith", True
|
|
49
|
+
if root_was_explicit:
|
|
50
|
+
return user_root / "cache", True
|
|
51
|
+
return Path.home() / ".cache" / "texsmith", False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass(slots=True)
|
|
55
|
+
class TexsmithUserDir:
|
|
56
|
+
"""Resolved user and cache roots plus helpers to manage them."""
|
|
57
|
+
|
|
58
|
+
root: Path
|
|
59
|
+
cache_root: Path
|
|
60
|
+
root_is_explicit: bool = False
|
|
61
|
+
cache_is_explicit: bool = False
|
|
62
|
+
|
|
63
|
+
def data_dir(self, *parts: str | Path, create: bool = True) -> Path:
|
|
64
|
+
"""Return a directory under the user root, creating it when requested."""
|
|
65
|
+
target = self.root.joinpath(*parts)
|
|
66
|
+
if create:
|
|
67
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
return target
|
|
69
|
+
|
|
70
|
+
def cache_dir(self, *parts: str | Path, create: bool = True) -> Path:
|
|
71
|
+
"""Return a directory under the cache root, creating it when requested."""
|
|
72
|
+
target = self.cache_root.joinpath(*parts)
|
|
73
|
+
if create:
|
|
74
|
+
target.mkdir(parents=True, exist_ok=True)
|
|
75
|
+
return target
|
|
76
|
+
|
|
77
|
+
def data_path(self, *parts: str | Path, create: bool = True) -> Path:
|
|
78
|
+
"""Return a path under the user root, creating parent directories if needed."""
|
|
79
|
+
target = self.root.joinpath(*parts)
|
|
80
|
+
if create:
|
|
81
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
82
|
+
return target
|
|
83
|
+
|
|
84
|
+
def cache_path(self, *parts: str | Path, create: bool = True) -> Path:
|
|
85
|
+
"""Return a path under the cache root, creating parent directories if needed."""
|
|
86
|
+
target = self.cache_root.joinpath(*parts)
|
|
87
|
+
if create:
|
|
88
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
return target
|
|
90
|
+
|
|
91
|
+
def clear_cache(self, namespaces: Iterable[str] | None = None) -> list[Path]:
|
|
92
|
+
"""Clear cached namespaces and return the list of removed paths."""
|
|
93
|
+
targets: list[Path] = []
|
|
94
|
+
if namespaces is None:
|
|
95
|
+
targets.append(self.cache_root)
|
|
96
|
+
else:
|
|
97
|
+
for name in namespaces:
|
|
98
|
+
targets.append(self.cache_root / name)
|
|
99
|
+
targets.append(self.root / name)
|
|
100
|
+
|
|
101
|
+
cleared: list[Path] = []
|
|
102
|
+
for path in targets:
|
|
103
|
+
if not path.exists():
|
|
104
|
+
continue
|
|
105
|
+
try:
|
|
106
|
+
shutil.rmtree(path)
|
|
107
|
+
cleared.append(path)
|
|
108
|
+
except OSError:
|
|
109
|
+
continue
|
|
110
|
+
return cleared
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def configure_user_dir(
|
|
114
|
+
*,
|
|
115
|
+
root: str | Path | None = None,
|
|
116
|
+
cache_root: str | Path | None = None,
|
|
117
|
+
) -> TexsmithUserDir:
|
|
118
|
+
"""Replace the global user dir singleton with a freshly resolved instance."""
|
|
119
|
+
user_root, root_was_explicit = _resolve_root(root)
|
|
120
|
+
resolved_cache_root, cache_was_explicit = _resolve_cache_root(
|
|
121
|
+
cache_root, user_root=user_root, root_was_explicit=root_was_explicit
|
|
122
|
+
)
|
|
123
|
+
return set_user_dir(
|
|
124
|
+
TexsmithUserDir(
|
|
125
|
+
root=user_root,
|
|
126
|
+
cache_root=resolved_cache_root,
|
|
127
|
+
root_is_explicit=root_was_explicit,
|
|
128
|
+
cache_is_explicit=cache_was_explicit,
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def get_user_dir() -> TexsmithUserDir:
|
|
134
|
+
"""Return the lazily created user dir singleton."""
|
|
135
|
+
global _USER_DIR
|
|
136
|
+
with _LOCK:
|
|
137
|
+
if _USER_DIR is None:
|
|
138
|
+
_USER_DIR = configure_user_dir()
|
|
139
|
+
return _USER_DIR
|
|
140
|
+
current_root, root_was_explicit = _resolve_root(None)
|
|
141
|
+
current_cache_root, cache_was_explicit = _resolve_cache_root(
|
|
142
|
+
None, user_root=current_root, root_was_explicit=root_was_explicit
|
|
143
|
+
)
|
|
144
|
+
if (not _USER_DIR.root_is_explicit and _USER_DIR.root != current_root) or (
|
|
145
|
+
not _USER_DIR.cache_is_explicit and _USER_DIR.cache_root != current_cache_root
|
|
146
|
+
):
|
|
147
|
+
_USER_DIR = TexsmithUserDir(
|
|
148
|
+
root=current_root,
|
|
149
|
+
cache_root=current_cache_root,
|
|
150
|
+
root_is_explicit=root_was_explicit,
|
|
151
|
+
cache_is_explicit=cache_was_explicit,
|
|
152
|
+
)
|
|
153
|
+
return _USER_DIR
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def set_user_dir(user_dir: TexsmithUserDir) -> TexsmithUserDir:
|
|
157
|
+
"""Replace the current user dir singleton and return it."""
|
|
158
|
+
global _USER_DIR
|
|
159
|
+
with _LOCK:
|
|
160
|
+
_USER_DIR = user_dir
|
|
161
|
+
return _USER_DIR
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@contextmanager
|
|
165
|
+
def user_dir_context(
|
|
166
|
+
*,
|
|
167
|
+
root: str | Path | None = None,
|
|
168
|
+
cache_root: str | Path | None = None,
|
|
169
|
+
) -> Iterable[TexsmithUserDir]:
|
|
170
|
+
"""Temporarily override the global user dir singleton."""
|
|
171
|
+
global _USER_DIR
|
|
172
|
+
with _LOCK:
|
|
173
|
+
previous = _USER_DIR
|
|
174
|
+
current = configure_user_dir(root=root, cache_root=cache_root)
|
|
175
|
+
try:
|
|
176
|
+
yield current
|
|
177
|
+
finally:
|
|
178
|
+
with _LOCK:
|
|
179
|
+
_USER_DIR = previous
|
texsmith/devtools.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Developer utilities for local workflows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
11
|
+
"""Run repository checks (currently proxies to ruff)."""
|
|
12
|
+
args = list(argv) if argv is not None else sys.argv[1:]
|
|
13
|
+
try:
|
|
14
|
+
pass
|
|
15
|
+
except Exception as exc: # pragma: no cover - defensive message
|
|
16
|
+
sys.stderr.write("ruff is required to run checks; install dev dependencies.\n")
|
|
17
|
+
sys.stderr.write(f"Details: {exc}\n")
|
|
18
|
+
return 1
|
|
19
|
+
|
|
20
|
+
result = subprocess.run(
|
|
21
|
+
["ruff", "check", *args],
|
|
22
|
+
check=False,
|
|
23
|
+
)
|
|
24
|
+
return result.returncode
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__": # pragma: no cover
|
|
28
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Central registry for TeXSmith's bundled Markdown extensions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Iterable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from importlib import import_module
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ExtensionSpec",
|
|
13
|
+
"available_extensions",
|
|
14
|
+
"get_extension_spec",
|
|
15
|
+
"load_markdown_extension",
|
|
16
|
+
"load_mkdocs_plugin",
|
|
17
|
+
"register_all_renderers",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _load_attribute(path: str) -> Any:
|
|
22
|
+
module_name, _, attribute = path.partition(":")
|
|
23
|
+
if not module_name or not attribute:
|
|
24
|
+
msg = f"Extension entry point '{path}' must use the 'module:attribute' format."
|
|
25
|
+
raise ValueError(msg)
|
|
26
|
+
module = import_module(module_name)
|
|
27
|
+
target: Any = module
|
|
28
|
+
for chunk in attribute.split("."):
|
|
29
|
+
target = getattr(target, chunk)
|
|
30
|
+
return target
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _normalise_slug(value: str) -> str:
|
|
34
|
+
slug = value.split(":", 1)[0].lower()
|
|
35
|
+
if slug.startswith("texsmith."):
|
|
36
|
+
return slug.removeprefix("texsmith.")
|
|
37
|
+
return slug
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class ExtensionSpec:
|
|
42
|
+
"""Describe how to import Markdown and renderer hooks for an extension."""
|
|
43
|
+
|
|
44
|
+
slug: str
|
|
45
|
+
markdown_entry: str
|
|
46
|
+
renderer_entry: str | None = None
|
|
47
|
+
mkdocs_entry: str | None = None
|
|
48
|
+
description: str | None = None
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def package_name(self) -> str:
|
|
52
|
+
return f"texsmith.{self.slug}"
|
|
53
|
+
|
|
54
|
+
def iter_entry_points(self) -> Iterable[str]:
|
|
55
|
+
"""Yield configured entry points for documentation/debugging."""
|
|
56
|
+
yield self.markdown_entry
|
|
57
|
+
if self.renderer_entry:
|
|
58
|
+
yield self.renderer_entry
|
|
59
|
+
if self.mkdocs_entry:
|
|
60
|
+
yield self.mkdocs_entry
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
_EXTENSIONS: dict[str, ExtensionSpec] = {
|
|
64
|
+
"index": ExtensionSpec(
|
|
65
|
+
slug="index",
|
|
66
|
+
markdown_entry="texsmith.index:TexsmithIndexExtension",
|
|
67
|
+
renderer_entry="texsmith.index:register_renderer",
|
|
68
|
+
mkdocs_entry="texsmith.index:IndexPlugin",
|
|
69
|
+
description="Inline hashtags converted into LaTeX index entries.",
|
|
70
|
+
),
|
|
71
|
+
"texlogos": ExtensionSpec(
|
|
72
|
+
slug="texlogos",
|
|
73
|
+
markdown_entry="texsmith.texlogos:TexLogosExtension",
|
|
74
|
+
renderer_entry="texsmith.texlogos:register_renderer",
|
|
75
|
+
description="Semantic spans for TeX logos rendered in Markdown and LaTeX.",
|
|
76
|
+
),
|
|
77
|
+
"smallcaps": ExtensionSpec(
|
|
78
|
+
slug="smallcaps",
|
|
79
|
+
markdown_entry="texsmith.smallcaps:SmallCapsExtension",
|
|
80
|
+
description="Maps '__text__' syntax to small caps spans.",
|
|
81
|
+
),
|
|
82
|
+
"latex_raw": ExtensionSpec(
|
|
83
|
+
slug="latex_raw",
|
|
84
|
+
markdown_entry="texsmith.latex_raw:LatexRawExtension",
|
|
85
|
+
description="Fence supporting inline raw LaTeX payloads.",
|
|
86
|
+
),
|
|
87
|
+
"rawlatex": ExtensionSpec(
|
|
88
|
+
slug="rawlatex",
|
|
89
|
+
markdown_entry="texsmith.rawlatex:RawLatexExtension",
|
|
90
|
+
description="Inline {latex}[...] markers and /// latex fences.",
|
|
91
|
+
),
|
|
92
|
+
"latex_text": ExtensionSpec(
|
|
93
|
+
slug="latex_text",
|
|
94
|
+
markdown_entry="texsmith.latex_text:LatexTextExtension",
|
|
95
|
+
description="Styles the literal 'LaTeX' token with a dedicated span.",
|
|
96
|
+
),
|
|
97
|
+
"smart_dashes": ExtensionSpec(
|
|
98
|
+
slug="smart_dashes",
|
|
99
|
+
markdown_entry="texsmith.smart_dashes:TexsmithSmartDashesExtension",
|
|
100
|
+
description="Converts '--' and '---' into typographic dashes outside code.",
|
|
101
|
+
),
|
|
102
|
+
"missing_footnotes": ExtensionSpec(
|
|
103
|
+
slug="missing_footnotes",
|
|
104
|
+
markdown_entry="texsmith.missing_footnotes:MissingFootnotesExtension",
|
|
105
|
+
description="Warns about references to undefined footnotes.",
|
|
106
|
+
),
|
|
107
|
+
"multi_citations": ExtensionSpec(
|
|
108
|
+
slug="multi_citations",
|
|
109
|
+
markdown_entry="texsmith.multi_citations:MultiCitationExtension",
|
|
110
|
+
description="Normalises '^[a,b]' inline citations before footnotes run.",
|
|
111
|
+
),
|
|
112
|
+
"mermaid": ExtensionSpec(
|
|
113
|
+
slug="mermaid",
|
|
114
|
+
markdown_entry="texsmith.mermaid:MermaidExtension",
|
|
115
|
+
description="Inlines Mermaid diagrams referenced via Markdown images.",
|
|
116
|
+
),
|
|
117
|
+
"progressbar": ExtensionSpec(
|
|
118
|
+
slug="progressbar",
|
|
119
|
+
markdown_entry="texsmith.progressbar:ProgressBarExtension",
|
|
120
|
+
renderer_entry="texsmith.progressbar:register_renderer",
|
|
121
|
+
description="Renders `[=50%]` progress blocks via the LaTeX progressbar package.",
|
|
122
|
+
),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def available_extensions() -> list[ExtensionSpec]:
|
|
127
|
+
"""Return the registered extension specs sorted by slug."""
|
|
128
|
+
return [_EXTENSIONS[key] for key in sorted(_EXTENSIONS)]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def get_extension_spec(name: str) -> ExtensionSpec:
|
|
132
|
+
"""Look up the runtime spec for a given extension slug or qualified name."""
|
|
133
|
+
slug = _normalise_slug(name)
|
|
134
|
+
try:
|
|
135
|
+
return _EXTENSIONS[slug]
|
|
136
|
+
except KeyError as exc: # pragma: no cover - defensive
|
|
137
|
+
raise KeyError(f"No TeXSmith extension named '{name}'.") from exc
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def load_markdown_extension(name: str, **config: Any) -> Any:
|
|
141
|
+
"""Instantiate a Python-Markdown extension by slug or qualified name."""
|
|
142
|
+
spec = get_extension_spec(name)
|
|
143
|
+
factory: Callable[..., Any] = _load_attribute(spec.markdown_entry)
|
|
144
|
+
return factory(**config)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def register_all_renderers(renderer: object) -> None:
|
|
148
|
+
"""Register every available renderer hook on the provided renderer."""
|
|
149
|
+
for spec in _EXTENSIONS.values():
|
|
150
|
+
if not spec.renderer_entry:
|
|
151
|
+
continue
|
|
152
|
+
register: Callable[[object], None] = _load_attribute(spec.renderer_entry)
|
|
153
|
+
register(renderer)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def load_mkdocs_plugin(name: str) -> type[Any] | None:
|
|
157
|
+
"""Return the MkDocs plugin class for an extension, if any."""
|
|
158
|
+
spec = get_extension_spec(name)
|
|
159
|
+
if not spec.mkdocs_entry:
|
|
160
|
+
return None
|
|
161
|
+
plugin_cls: type[Any] = _load_attribute(spec.mkdocs_entry)
|
|
162
|
+
return plugin_cls
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Public entry points for TeXSmith's hashtag index extension."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .markdown import TexsmithIndexExtension, makeExtension
|
|
6
|
+
from .registry import (
|
|
7
|
+
IndexEntry,
|
|
8
|
+
IndexRegistry,
|
|
9
|
+
clear_registry,
|
|
10
|
+
get_registry,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"IndexEntry",
|
|
16
|
+
"IndexRegistry",
|
|
17
|
+
"TexsmithIndexExtension",
|
|
18
|
+
"clear_registry",
|
|
19
|
+
"get_registry",
|
|
20
|
+
"makeExtension",
|
|
21
|
+
]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Markdown extension providing the ``#[term]`` hashtag syntax."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from xml.etree import ElementTree
|
|
7
|
+
|
|
8
|
+
from markdown import Markdown
|
|
9
|
+
from markdown.extensions import Extension
|
|
10
|
+
from markdown.inlinepatterns import InlineProcessor
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
TAG_PATTERN = re.compile(r"\[([^\]]+)\]")
|
|
14
|
+
STYLE_PATTERN = re.compile(r"\{([^}]+)\}")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _IndexInlineProcessor(InlineProcessor):
|
|
18
|
+
"""Replace index syntax with tagged spans."""
|
|
19
|
+
|
|
20
|
+
def handleMatch( # noqa: N802 - Markdown inline API requires camelCase
|
|
21
|
+
self,
|
|
22
|
+
match: re.Match[str],
|
|
23
|
+
data: str,
|
|
24
|
+
) -> tuple[ElementTree.Element, int, int]: # type: ignore[override]
|
|
25
|
+
del data
|
|
26
|
+
payload = match.group("payload")
|
|
27
|
+
registry = match.group("registry1")
|
|
28
|
+
style_token = match.group("style")
|
|
29
|
+
|
|
30
|
+
tags = _extract_tags(payload)
|
|
31
|
+
if not tags:
|
|
32
|
+
return None, match.start(0), match.end(0)
|
|
33
|
+
|
|
34
|
+
element = ElementTree.Element("span")
|
|
35
|
+
element.set("class", "ts-hashtag")
|
|
36
|
+
for index, tag in enumerate(tags):
|
|
37
|
+
key = "data-tag" if index == 0 else f"data-tag{index}"
|
|
38
|
+
element.set(key, tag)
|
|
39
|
+
|
|
40
|
+
normalised_style = _normalise_style(style_token)
|
|
41
|
+
if normalised_style:
|
|
42
|
+
element.set("data-style", normalised_style)
|
|
43
|
+
|
|
44
|
+
if registry:
|
|
45
|
+
element.set("data-registry", registry)
|
|
46
|
+
|
|
47
|
+
# The index marker is invisible in the output text
|
|
48
|
+
element.text = ""
|
|
49
|
+
return element, match.start(0), match.end(0)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _extract_tags(payload: str | None) -> list[str]:
|
|
53
|
+
if not payload:
|
|
54
|
+
return []
|
|
55
|
+
values = [part.strip() for part in TAG_PATTERN.findall(payload)]
|
|
56
|
+
return [value for value in values if value]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _normalise_style(style_token: str | None) -> str | None:
|
|
60
|
+
if not style_token:
|
|
61
|
+
return None
|
|
62
|
+
match = STYLE_PATTERN.search(style_token)
|
|
63
|
+
if not match:
|
|
64
|
+
return None
|
|
65
|
+
style = match.group(1).strip().lower()
|
|
66
|
+
if not style:
|
|
67
|
+
return None
|
|
68
|
+
if style == "ib":
|
|
69
|
+
style = "bi"
|
|
70
|
+
if style not in {"b", "i", "bi"}:
|
|
71
|
+
return None
|
|
72
|
+
return style
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class TexsmithIndexExtension(Extension):
|
|
76
|
+
"""Register the inline index processor with Python-Markdown."""
|
|
77
|
+
|
|
78
|
+
def extendMarkdown(self, md: Markdown) -> None: # noqa: N802
|
|
79
|
+
"""Match modern and legacy index syntaxes and register the inline processor."""
|
|
80
|
+
pattern = (
|
|
81
|
+
r"(?<!\\)"
|
|
82
|
+
r"(?P<prefix>#|\{index(?::(?P<registry1>[^\]\{\(\s]+))?\}|%)"
|
|
83
|
+
r"(?P<payload>(?:\[[^\]]+\])+)"
|
|
84
|
+
r"(?P<style>\{[^}]+\})?"
|
|
85
|
+
)
|
|
86
|
+
processor = _IndexInlineProcessor(pattern, md)
|
|
87
|
+
md.inlinePatterns.register(processor, "texsmith_index", 180)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def makeExtension(**kwargs: object) -> TexsmithIndexExtension: # noqa: N802 - Markdown API hook; pragma: no cover
|
|
91
|
+
return TexsmithIndexExtension(**kwargs)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
__all__ = ["TexsmithIndexExtension", "makeExtension"]
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""MkDocs plugin injecting TeXSmith hashtag spans into the lunr search index."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from mkdocs import plugins
|
|
13
|
+
from mkdocs.config import config_options
|
|
14
|
+
from mkdocs.config.defaults import MkDocsConfig
|
|
15
|
+
from mkdocs.plugins import BasePlugin
|
|
16
|
+
from mkdocs.structure.files import Files
|
|
17
|
+
from mkdocs.structure.pages import Page
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
RE_HEADERLINK = re.compile(r'<a\s+[^>]*headerlink[^>]*href="(#[^"]+)"[^>]*>')
|
|
21
|
+
RE_HASHTAG = re.compile(r"<span\s+[^>]*class=\"[^\"]*ts-(?:hashtag|index)[^\"]*\"[^>]*>")
|
|
22
|
+
RE_DATA_TAG = re.compile(r"data-tag\d*=\"([^\"]+)\"")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _expand_search_terms(tags: Iterable[str]) -> list[str]:
|
|
26
|
+
"""Return a list of search tokens derived from the hierarchy of tags."""
|
|
27
|
+
tokens: list[str] = []
|
|
28
|
+
collected: set[str] = set()
|
|
29
|
+
hierarchy: list[str] = []
|
|
30
|
+
for tag in tags:
|
|
31
|
+
hierarchy.append(tag)
|
|
32
|
+
direct = tag.strip()
|
|
33
|
+
if direct and direct not in collected:
|
|
34
|
+
collected.add(direct)
|
|
35
|
+
tokens.append(direct)
|
|
36
|
+
composite = "::".join(hierarchy)
|
|
37
|
+
if composite not in collected:
|
|
38
|
+
collected.add(composite)
|
|
39
|
+
tokens.append(composite)
|
|
40
|
+
return tokens
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _extract_tags(fragment: str) -> list[str]:
|
|
44
|
+
return [value.strip() for value in RE_DATA_TAG.findall(fragment) if value.strip()]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class IndexPlugin(BasePlugin):
|
|
48
|
+
"""Collect hashtag spans and inject their tags into MkDocs search."""
|
|
49
|
+
|
|
50
|
+
config_scheme = (("inject_markdown_extension", config_options.Type(bool, default=True)),)
|
|
51
|
+
|
|
52
|
+
def __init__(self) -> None:
|
|
53
|
+
self._collected: dict[str, set[tuple[str, ...]]] = defaultdict(set)
|
|
54
|
+
|
|
55
|
+
def on_config(self, config: MkDocsConfig) -> MkDocsConfig:
|
|
56
|
+
"""Optionally enable the markdown extension automatically."""
|
|
57
|
+
self._collected.clear()
|
|
58
|
+
if self.config.get("inject_markdown_extension", True):
|
|
59
|
+
extension = "texsmith.index:TexsmithIndexExtension"
|
|
60
|
+
extensions = list(config.markdown_extensions or [])
|
|
61
|
+
if extension not in extensions:
|
|
62
|
+
extensions.append(extension)
|
|
63
|
+
config.markdown_extensions = extensions
|
|
64
|
+
return config
|
|
65
|
+
|
|
66
|
+
def on_page_content(
|
|
67
|
+
self,
|
|
68
|
+
html: str,
|
|
69
|
+
page: Page,
|
|
70
|
+
config: MkDocsConfig,
|
|
71
|
+
files: Files,
|
|
72
|
+
) -> str:
|
|
73
|
+
"""Collect tags per page (and optionally per section heading)."""
|
|
74
|
+
del config, files
|
|
75
|
+
base = page.url or ""
|
|
76
|
+
anchor = ""
|
|
77
|
+
heading_count = 0
|
|
78
|
+
|
|
79
|
+
for line in html.split("\n"):
|
|
80
|
+
if header := RE_HEADERLINK.search(line):
|
|
81
|
+
anchor = header.group(1)
|
|
82
|
+
heading_count += 1
|
|
83
|
+
|
|
84
|
+
for match in RE_HASHTAG.findall(line):
|
|
85
|
+
tags = _extract_tags(match)
|
|
86
|
+
if not tags:
|
|
87
|
+
continue
|
|
88
|
+
location = f"{base}{anchor}" if anchor and heading_count > 1 else base
|
|
89
|
+
self._collected[location].add(tuple(tags))
|
|
90
|
+
|
|
91
|
+
return html
|
|
92
|
+
|
|
93
|
+
@plugins.event_priority(-100)
|
|
94
|
+
def on_post_build(self, config: MkDocsConfig) -> None:
|
|
95
|
+
"""Inject gathered tags into the lunr search index."""
|
|
96
|
+
if not self._collected:
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
search_dir = Path(config.site_dir) / "search"
|
|
100
|
+
index_path = search_dir / "search_index.json"
|
|
101
|
+
if not index_path.exists():
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
data: dict[str, Any]
|
|
105
|
+
with index_path.open("r", encoding="utf-8") as handle:
|
|
106
|
+
data = json.load(handle)
|
|
107
|
+
|
|
108
|
+
docs = data.get("docs")
|
|
109
|
+
if not isinstance(docs, list):
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
for entry in docs:
|
|
113
|
+
location = entry.get("location")
|
|
114
|
+
if not isinstance(location, str):
|
|
115
|
+
continue
|
|
116
|
+
tag_sets = self._collected.get(location)
|
|
117
|
+
if not tag_sets:
|
|
118
|
+
continue
|
|
119
|
+
existing = entry.setdefault("tags", [])
|
|
120
|
+
if not isinstance(existing, list):
|
|
121
|
+
continue
|
|
122
|
+
payload: list[str] = []
|
|
123
|
+
seen: set[str] = set(map(str, existing))
|
|
124
|
+
for tags in sorted(tag_sets):
|
|
125
|
+
for token in _expand_search_terms(tags):
|
|
126
|
+
if token not in seen:
|
|
127
|
+
seen.add(token)
|
|
128
|
+
payload.append(token)
|
|
129
|
+
if payload:
|
|
130
|
+
existing.extend(payload)
|
|
131
|
+
|
|
132
|
+
with index_path.open("w", encoding="utf-8") as handle:
|
|
133
|
+
json.dump(data, handle)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
__all__ = ["IndexPlugin"]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Global registry tracking hashtag-index entries across documents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterable, Iterator
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from threading import Lock
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
IndexEntry = tuple[str, ...]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(slots=True)
|
|
14
|
+
class IndexRegistry:
|
|
15
|
+
"""Thread-safe container gathering index entries encountered during rendering."""
|
|
16
|
+
|
|
17
|
+
_entries: set[IndexEntry] = field(default_factory=set)
|
|
18
|
+
_lock: Lock = field(default_factory=Lock)
|
|
19
|
+
|
|
20
|
+
def add(self, entry: Iterable[str]) -> None:
|
|
21
|
+
"""Record a tuple describing an index entry."""
|
|
22
|
+
values = tuple(part for part in entry if part)
|
|
23
|
+
if not values:
|
|
24
|
+
return
|
|
25
|
+
with self._lock:
|
|
26
|
+
self._entries.add(values)
|
|
27
|
+
|
|
28
|
+
def clear(self) -> None:
|
|
29
|
+
"""Reset the registry to its initial empty state."""
|
|
30
|
+
with self._lock:
|
|
31
|
+
self._entries.clear()
|
|
32
|
+
|
|
33
|
+
def __len__(self) -> int: # pragma: no cover - trivial
|
|
34
|
+
with self._lock:
|
|
35
|
+
return len(self._entries)
|
|
36
|
+
|
|
37
|
+
def __iter__(self) -> Iterator[IndexEntry]: # pragma: no cover - simple proxy
|
|
38
|
+
with self._lock:
|
|
39
|
+
yield from sorted(self._entries)
|
|
40
|
+
|
|
41
|
+
def snapshot(self) -> set[IndexEntry]:
|
|
42
|
+
"""Return a shallow copy of the registered entries."""
|
|
43
|
+
with self._lock:
|
|
44
|
+
return set(self._entries)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
_REGISTRY = IndexRegistry()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_registry() -> IndexRegistry:
|
|
51
|
+
"""Return the global index registry."""
|
|
52
|
+
return _REGISTRY
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def clear_registry() -> None:
|
|
56
|
+
"""Convenience helper to wipe the global registry."""
|
|
57
|
+
_REGISTRY.clear()
|