csl-pyutil 0.24.1__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.
csl_pyutil/__init__.py ADDED
@@ -0,0 +1,68 @@
1
+ # -*- coding: utf-8 -*-
2
+ """csl_pyutil — generic (non-Sanskrit-specific) Python helpers shared across
3
+ the CDSL / Sanskrit-Lexicon repos.
4
+
5
+ Public API
6
+ ----------
7
+ render_review_sheet(items, config, extras=True) self-contained HTML review/
8
+ voting sheet (H925)
9
+ render_review_sheet_packset(items, config, ...) the same sheet split into
10
+ packs of 10 sharing one
11
+ sheet_id, plus an index
12
+ page (V16, H2991)
13
+ anatomy.highlight(raw, target=None, ...) colour-coded CDSL raw-markup
14
+ anatomy.legend_html(parts=None, ...) anatomy for a panel (H1808)
15
+ evidence.EvidenceManifest / evidence.preflight the V9 evidence-reuse gate a
16
+ sheet must pass before it is
17
+ written (H1889)
18
+ RU_UI_STRINGS one-line Russian chrome
19
+ preset for config["ui_strings"]
20
+ (H2854)
21
+ integrity_tripwire.check / .extract committed checksum + key-set
22
+ on human-reviewed overlay
23
+ data, red in CI when a
24
+ seeder wipes it (H2891)
25
+ """
26
+ from csl_pyutil import anatomy, evidence
27
+ from csl_pyutil.evidence import EvidenceManifest, PreflightError, PreflightWarning, preflight
28
+ from csl_pyutil.review_sheet import (render_review_sheet, render_review_sheet_packset,
29
+ esc, mark_cyrillic, RU_UI_STRINGS)
30
+
31
+ # integrity_tripwire is imported LAZILY (PEP 562), not eagerly like its
32
+ # neighbours. Its documented CI invocation is `python -m
33
+ # csl_pyutil.integrity_tripwire --check`, and runpy warns "found in sys.modules
34
+ # after import of package" whenever the package has already pulled the module
35
+ # in — which it would, on every single tripwire run in every consumer repo. A
36
+ # gate whose job is to be believed when it prints RED must not also print a
37
+ # spurious RuntimeWarning every time it prints GREEN.
38
+ _LAZY = {
39
+ "integrity_tripwire": None,
40
+ "TripwireError": "integrity_tripwire",
41
+ "project": "integrity_tripwire",
42
+ "overlay_digest": "integrity_tripwire",
43
+ "keyset_digest": "integrity_tripwire",
44
+ "is_reviewed": "integrity_tripwire",
45
+ "extract": "integrity_tripwire",
46
+ "check": "integrity_tripwire",
47
+ "redact": "integrity_tripwire",
48
+ }
49
+
50
+
51
+ def __getattr__(name):
52
+ if name in _LAZY:
53
+ import importlib
54
+
55
+ module = importlib.import_module("csl_pyutil.integrity_tripwire")
56
+ return module if _LAZY[name] is None else getattr(module, name)
57
+ raise AttributeError("module %r has no attribute %r" % (__name__, name))
58
+
59
+
60
+ def __dir__():
61
+ return sorted(list(globals()) + list(_LAZY))
62
+
63
+ __version__ = "0.24.1"
64
+ __all__ = ["render_review_sheet", "render_review_sheet_packset", "esc", "mark_cyrillic",
65
+ "RU_UI_STRINGS", "anatomy", "evidence",
66
+ "EvidenceManifest", "PreflightError", "PreflightWarning", "preflight",
67
+ "integrity_tripwire", "TripwireError", "project", "overlay_digest",
68
+ "keyset_digest", "is_reviewed", "extract", "check"]
csl_pyutil/anatomy.py ADDED
@@ -0,0 +1,237 @@
1
+ # -*- coding: utf-8 -*-
2
+ """anatomy — colour-code the anatomy of a raw CDSL dictionary record.
3
+
4
+ A CDSL record body is a dense mix of SGML-ish tags (``<s>``, ``<lex>``, ``<ls>``,
5
+ ``<ab>``) and brace markers (``{#…#}``, ``{%…%}``) whose classes differ per
6
+ dictionary. Dumped verbatim into a review card it is a wall of punctuation, and a
7
+ reviewer cannot see which clause the judgement rests on (H1646: "add dictionary
8
+ entry anatomy markup, the bright colors for different part of entry").
9
+
10
+ This module keeps the markup fully VISIBLE — the tags are the anatomy, not noise
11
+ to be stripped — but dims the delimiters and colours the payload by part class, so
12
+ the shape of the entry reads at a glance.
13
+
14
+ Provenance: written for csl-atlas's xref sheet (H1646) as
15
+ ``scripts/lib/cdsl_anatomy.py``, lifted here unchanged in behaviour under H1808
16
+ when a SECOND sheet generator (SanskritLexicography's G5 print-readiness lane)
17
+ turned out to need the same thing and had no way to reach it — MG, voting that
18
+ sheet: "why entry anatomy is missing again? It must be a hook". One canonical
19
+ copy in the shared emitter's package is that fix; csl-atlas's file is now a
20
+ re-export shim.
21
+
22
+ Prior art it deliberately reuses rather than re-derives:
23
+
24
+ * Part taxonomy and colour semantics — the ``/entry-anatomy`` skill
25
+ (``entry_anatomy.py``'s ``PARTS`` / ``DICT_MAPS``), which segments a CDSL entry
26
+ into headword · grammar · etymology · sense · citation · cross-reference.
27
+ * The raw-markup highlighting approach and dark palette —
28
+ ``SanskritLexicography/EntryAnatomy/build_entry_anatomy.py`` ``raw_highlight()``
29
+ + ``GENERIC_EXTRA_CSS``, whose colours already sit on a dark panel.
30
+
31
+ Colours are INLINE ``style=`` attributes, not a stylesheet: the output has to drop
32
+ into any panel body of any sheet, including callers that pass no ``extra_css``.
33
+ The container carries ``class="anatomy"`` so the emitter's type scale can still
34
+ reach it (the ``!important`` scale layer outranks the inline ``font`` shorthand).
35
+ """
36
+ import html
37
+ import re
38
+
39
+ #: Part class -> (colour, human label, extra CSS). Dark-panel palette, matching the
40
+ #: review sheet's own --panel2 (#1e222b). Labels drive the rendered legend.
41
+ #: Labels are Russian: the surfaces rendering this legend are review sheets, whose
42
+ #: reviewer reads Russian (H1648). Keys stay English machine identifiers.
43
+ PARTS = {
44
+ "sanskrit": ("#e6c07b", "санскритская форма", ""),
45
+ "gloss": ("#98c379", "перевод / значение", "font-style:italic"),
46
+ "citation": ("#e06c75", "ссылка на источник", ""),
47
+ "grammar": ("#d19a66", "грамматическая помета", ""),
48
+ "abbreviation": ("#c9a227", "сокращение (<ab>)", ""),
49
+ "crossref": ("#56b6c2", "маркер перекрёстной ссылки (cf. / Vgl.)", "font-weight:600"),
50
+ "etymology": ("#61afef", "этимология / когнат", ""),
51
+ "language": ("#7aa2c9", "название языка", ""),
52
+ "taxon": ("#c678dd", "ботаническое / зоологическое название", ""),
53
+ "homonym": ("#b57edc", "номер омонима", ""),
54
+ "structure": ("#7f8c9b", "разделитель значения / раздела", ""),
55
+ }
56
+
57
+ #: Paired content tags -> part class. ``<s>``/``<s1>``/``<s2>`` are MW's Sanskrit
58
+ #: spans; ``<is>`` is PWG's.
59
+ #:
60
+ #: ``<ab>`` defaults to ``abbreviation`` — in PWG it wraps EVERY abbreviation
61
+ #: (``caus.``, ``gerund.``, ``v. a.``), of which cf./Vgl. is one case. csl-atlas's
62
+ #: xref sheet judges cross-references specifically and wants the brighter
63
+ #: ``crossref`` treatment, so it passes ``tag_parts={"ab": "crossref"}``.
64
+ TAG_PARTS = {
65
+ "s": "sanskrit", "s1": "sanskrit", "s2": "sanskrit", "is": "sanskrit",
66
+ "ns": "gloss",
67
+ "ls": "citation",
68
+ "lex": "grammar",
69
+ "ab": "abbreviation",
70
+ "etym": "etymology",
71
+ "lang": "language",
72
+ "bot": "taxon", "zoo": "taxon",
73
+ "hom": "homonym",
74
+ }
75
+
76
+ #: Brace markers -> part class. ``{#…#}`` Sanskrit, ``{%…%}`` gloss (PWG/AP90),
77
+ #: ``{@…@}`` a sense/section number.
78
+ BRACE_PARTS = {"#": "sanskrit", "%": "gloss", "@": "structure"}
79
+
80
+ _DELIM = "#5c6773" # tag/brace delimiters — present but receded
81
+ _ACCENT = "#ff7b72" # Vedic accent marks inside a Sanskrit form
82
+ _PIPE = "#e06c75" # the ¦ head/body separator
83
+ _PLAIN = "#d8dce2" # untagged running text
84
+
85
+ _SCANNER = re.compile(
86
+ r"(?P<pair><(?P<tag>s1|s2|s|is|ns|ls|lex|ab|etym|lang|bot|zoo|hom)\b(?P<attrs>[^>]*)>"
87
+ r"(?P<inner>.*?)</(?P=tag)>)"
88
+ r"|(?P<brace>\{(?P<bk>[#%@])(?P<binner>.*?)(?P=bk)\})"
89
+ r"|(?P<other><[^>]+>)"
90
+ r"|(?P<pipe>¦)",
91
+ re.DOTALL,
92
+ )
93
+
94
+ #: Vedic accent / length marks CDSL writes inside SLP1 forms. Stripped only when
95
+ #: comparing a form against the highlight target.
96
+ _ACCENT_CHARS = "/\\^~"
97
+
98
+
99
+ def _strip_accents(text):
100
+ return "".join(ch for ch in text if ch not in _ACCENT_CHARS)
101
+
102
+
103
+ def _span(text, colour, extra="", title=None, escape=True):
104
+ body = html.escape(text) if escape else text
105
+ style = "color:%s" % colour
106
+ if extra:
107
+ style += ";" + extra
108
+ attrs = ' title="%s"' % html.escape(title) if title else ""
109
+ return '<span style="%s"%s>%s</span>' % (style, attrs, body)
110
+
111
+
112
+ def _sanskrit_body(inner, target_norm):
113
+ """Colour a Sanskrit payload, marking accents and the highlight target."""
114
+ colour, _label, extra = PARTS["sanskrit"]
115
+ pieces = []
116
+ for ch in inner:
117
+ if ch in _ACCENT_CHARS:
118
+ pieces.append(_span(ch, _ACCENT, "font-weight:700"))
119
+ else:
120
+ pieces.append(html.escape(ch))
121
+ body = "".join(pieces)
122
+ if target_norm and _strip_accents(inner).strip() == target_norm:
123
+ # This span IS the form the card is asking about.
124
+ return (
125
+ '<span style="background:rgba(86,182,194,.22);outline:1px solid #56b6c2;'
126
+ 'border-radius:3px;padding:0 2px" title="цель перекрёстной ссылки, о которой спрашивает эта карточка">'
127
+ + _span(body, colour, extra, escape=False)
128
+ + "</span>"
129
+ )
130
+ return _span(body, colour, extra, escape=False)
131
+
132
+
133
+ def highlight(raw, target=None, *, tag_parts=None, plain_hook=None, payload_hook=None):
134
+ """Return colour-coded HTML for one raw CDSL record body.
135
+
136
+ ``target`` is the SLP1 form under judgement; every Sanskrit span in the
137
+ record equal to it (ignoring accent marks) is outlined.
138
+
139
+ ``tag_parts`` overrides the tag -> part mapping for this call (merged over
140
+ ``TAG_PARTS``), e.g. ``{"ab": "crossref"}``.
141
+
142
+ ``plain_hook(text) -> html`` is called for every UNTAGGED run. A caller uses
143
+ it to reach content the markup does not delimit — bare citations, bracketed
144
+ diasystem tags — and is responsible for escaping what it returns. Return
145
+ ``None`` to fall through to the default plain rendering.
146
+
147
+ ``payload_hook(part, inner, attrs) -> html`` is called for each tagged
148
+ payload before it is coloured; a caller returns ready HTML (e.g. an ``<ls>``
149
+ citation rendered as a source link) or ``None`` to fall through.
150
+ """
151
+ text = str(raw or "")
152
+ target_norm = _strip_accents(str(target or "").strip()) or None
153
+ parts_map = dict(TAG_PARTS)
154
+ if tag_parts:
155
+ parts_map.update(tag_parts)
156
+
157
+ def plain(chunk):
158
+ if plain_hook is not None:
159
+ got = plain_hook(chunk)
160
+ if got is not None:
161
+ return got
162
+ return _span(chunk, _PLAIN)
163
+
164
+ out = []
165
+ pos = 0
166
+ for m in _SCANNER.finditer(text):
167
+ if m.start() > pos:
168
+ out.append(plain(text[pos:m.start()]))
169
+ if m.group("pair"):
170
+ tag, attrs, inner = m.group("tag"), m.group("attrs") or "", m.group("inner")
171
+ part = parts_map.get(tag, "structure")
172
+ colour, label, extra = PARTS[part]
173
+ out.append(_span("<%s%s>" % (tag, attrs), _DELIM, title=label))
174
+ hooked = payload_hook(part, inner, attrs) if payload_hook is not None else None
175
+ if hooked is not None:
176
+ out.append(hooked)
177
+ elif part == "sanskrit":
178
+ out.append(_sanskrit_body(inner, target_norm))
179
+ else:
180
+ out.append(_span(inner, colour, extra, title=label))
181
+ out.append(_span("</%s>" % tag, _DELIM, title=label))
182
+ elif m.group("brace"):
183
+ bk, inner = m.group("bk"), m.group("binner")
184
+ part = BRACE_PARTS.get(bk, "structure")
185
+ colour, label, extra = PARTS[part]
186
+ out.append(_span("{" + bk, _DELIM, title=label))
187
+ if part == "sanskrit":
188
+ out.append(_sanskrit_body(inner, target_norm))
189
+ else:
190
+ out.append(_span(inner, colour, extra, title=label))
191
+ out.append(_span(bk + "}", _DELIM, title=label))
192
+ elif m.group("other"):
193
+ # <div n="v">, <info lex="m"/> and friends: structural, kept visible but quiet.
194
+ out.append(_span(m.group("other"), _DELIM, title="structural markup"))
195
+ else:
196
+ out.append(_span("¦", _PIPE, "font-weight:700", title="headword / body separator"))
197
+ pos = m.end()
198
+ if pos < len(text):
199
+ out.append(plain(text[pos:]))
200
+ return (
201
+ '<div class="anatomy" style="background:#20242a;border-radius:6px;padding:12px 14px;'
202
+ 'font:12.5px/1.9 Consolas,\'Cascadia Mono\',monospace;white-space:pre-wrap;'
203
+ 'word-break:break-word">' + "".join(out) + "</div>"
204
+ )
205
+
206
+
207
+ def legend_html(parts=None, extra_chips=()):
208
+ """A compact swatch legend for the part classes, for one place on the sheet.
209
+
210
+ ``parts`` restricts (and orders) the classes shown — pass only the ones the
211
+ sheet's dictionary actually uses. ``extra_chips`` appends caller chips as
212
+ ``(colour, label)`` pairs, for conventions this module does not own.
213
+ """
214
+ keys = list(parts or PARTS)
215
+ chips = []
216
+ for key in keys:
217
+ colour, label, extra = PARTS[key]
218
+ chips.append(
219
+ '<span style="display:inline-block;margin:0 10px 4px 0;white-space:nowrap">'
220
+ '<span style="display:inline-block;width:10px;height:10px;border-radius:2px;'
221
+ 'background:%s;margin-right:5px;vertical-align:baseline"></span>'
222
+ '<span style="color:%s;%s">%s</span></span>' % (colour, colour, extra, html.escape(label))
223
+ )
224
+ chips.append(
225
+ '<span style="display:inline-block;margin:0 10px 4px 0;white-space:nowrap">'
226
+ '<span style="display:inline-block;width:10px;height:10px;border-radius:2px;'
227
+ 'background:rgba(86,182,194,.22);outline:1px solid #56b6c2;margin-right:5px"></span>'
228
+ '<span style="color:#56b6c2">цель перекрёстной ссылки</span></span>'
229
+ )
230
+ for colour, label in extra_chips:
231
+ chips.append(
232
+ '<span style="display:inline-block;margin:0 10px 4px 0;white-space:nowrap">'
233
+ '<span style="display:inline-block;width:10px;height:10px;border-radius:2px;'
234
+ 'background:%s;margin-right:5px;vertical-align:baseline"></span>'
235
+ '<span style="color:%s">%s</span></span>' % (colour, colour, html.escape(label))
236
+ )
237
+ return '<div style="font-size:12px;line-height:1.9">' + "".join(chips) + "</div>"