Files
999/scripts/stamp_iching_intersections.py

231 lines
8.0 KiB
Python

#!/usr/bin/env python3
"""Stamp I Ching station intersections onto the 1000-word catalog — like runes.
Does not add words to the 1000. Hexagrams stay atlas stations; each intersecting
lexicon word gets an [И Цзин: …] tag. Station clusters pick up extra YAML names
that already exist in the lexicon.
"""
from __future__ import annotations
import json
import re
from collections import defaultdict
from pathlib import Path
import yaml
ROOT = Path(__file__).resolve().parents[1]
CANON = ROOT / "oracle_enricher" / "data" / "traditions_canon.json"
ASSET = ROOT / "assets" / "data" / "traditions_canon.json"
YAML_PATH = ROOT / "oracle_enricher" / "data" / "sources" / "iching.yaml"
DART = ROOT / "lib" / "data" / "words_data.dart"
HTML = ROOT / "lexicon_viewer.html"
MD = ROOT / "words_alphabetical.md"
LEX = ROOT / "oracle_enricher" / "data" / "lexicon.json"
ICHING_TAG_RE = re.compile(r"\[И Цзин:[^\]]*\]\s*")
# Ba Gua images. A hexagram that merely *contains* ☲ does not own «огонь».
# Keep the trigram itself and the doubled hexagram (same gua twice).
ELEMENT_OWNERS = {
"огонь": {"li", "hex_30_li_hex"},
"вода": {"kan", "hex_29_kan_hex"},
"земля": {"kun", "hex_02_kun"},
"воздух": {"xun", "hex_57_xun_hex"},
"ветер": {"xun", "hex_57_xun_hex"},
"озеро": {"dui", "hex_58_dui_hex"},
"небо": {"qian", "hex_01_qian"},
"гора": {"gen", "hex_52_gen_hex"},
"гром": {"zhen", "hex_51_zhen_hex"},
}
def allowed_element(word: str, station_id: str) -> bool:
owners = ELEMENT_OWNERS.get(word)
if owners is None:
return True
return station_id in owners
def tag_for(station: dict) -> str:
name = station["name"]
if station["id"].startswith("hex_"):
# "Гексаграмма 01 ䷀ Цянь (Творчество)"
inner = name.replace("Гексаграмма ", "", 1)
return f"[И Цзин: Гексаграмма {inner}]"
return f"[И Цзин: {name}]"
def main() -> None:
canon = json.loads(CANON.read_text(encoding="utf-8"))
lex = json.loads(LEX.read_text(encoding="utf-8"))
by_word = {e["word"]: e for e in lex}
yaml_cards = {e["id"]: e for e in yaml.safe_load(YAML_PATH.read_text(encoding="utf-8"))}
dart_text = DART.read_text(encoding="utf-8")
dart_by_word = {
w: int(i)
for i, w in re.findall(r'WordItem\(\s*id:\s*(\d+),\s*word:\s*"([^"]+)"', dart_text)
}
def payload(word: str) -> dict:
entry = by_word.get(word) or {}
return {
"id": dart_by_word.get(word, entry.get("id", 0)),
"word": word,
"emoji": entry.get("emoji", ""),
"pos": entry.get("pos", ""),
"category": entry.get("category", ""),
"desc": "",
}
word_to_tags: dict[str, list[str]] = defaultdict(list)
extra_cluster = 0
stripped = 0
for st in canon["iching"]["stations"]:
sid = st["id"]
tag = tag_for(st)
prim = (st.get("primary") or {}).get("word")
if prim and not allowed_element(prim, sid):
prim = None
keep: list[str] = []
seen = {prim} if prim else set()
def consider(word: str, from_yaml: bool = False) -> None:
nonlocal extra_cluster
if not word or word in seen:
return
if word not in by_word and word not in dart_by_word:
return
if not allowed_element(word, sid):
return
seen.add(word)
keep.append(word)
if from_yaml:
extra_cluster += 1
for c in st.get("cluster") or []:
consider(c.get("word") or "")
y = yaml_cards.get(sid, {})
for name in y.get("names_ru") or []:
consider(name, from_yaml=True)
stripped += max(0, len(st.get("cluster") or []) - len(keep))
if not prim and keep:
prim = keep.pop(0)
seen = {prim, *keep}
if prim:
st["primary"] = payload(prim)
st["cluster"] = [payload(w) for w in keep]
linked = []
if prim:
linked.append(prim)
linked.extend(keep)
for w in linked:
if tag not in word_to_tags[w]:
word_to_tags[w].append(tag)
text = json.dumps(canon, ensure_ascii=False, indent=2) + "\n"
CANON.write_text(text, encoding="utf-8")
ASSET.write_text(text, encoding="utf-8")
dart_lines = dart_text.splitlines(keepends=True)
n = 0
i = 0
while i < len(dart_lines):
wm = re.match(r'(\s*word: ")([^"]+)(",\s*)$', dart_lines[i])
if wm:
word = wm.group(2)
for j in range(i + 1, min(i + 12, len(dart_lines))):
raw = dart_lines[j].rstrip("\n")
dm = re.match(r'^(\s*description: ")(.*)(",)\s*$', raw)
if dm:
body = ICHING_TAG_RE.sub("", dm.group(2)).strip()
tags = word_to_tags.get(word)
if tags:
dart_lines[j] = f"{dm.group(1)}{' '.join(tags)} {body}{dm.group(3)}\n"
n += 1
else:
dart_lines[j] = f"{dm.group(1)}{body}{dm.group(3)}\n"
break
if dart_lines[j].strip().startswith("WordItem("):
break
i += 1
dart2 = "".join(dart_lines)
DART.write_text(dart2, encoding="utf-8")
# HTML WORDS_DATA: update description by word
html = HTML.read_text(encoding="utf-8")
m = re.search(r"const WORDS_DATA = (\[.*?\]);", html, flags=re.DOTALL)
if not m:
raise SystemExit("WORDS_DATA not found")
words = json.loads(m.group(1))
updated_html_words = 0
dart_descs = dict(
re.findall(r'word: "([^"]+)"[\s\S]*?description: "([^"]*)"', dart2)
)
for w in words:
wd = w.get("word")
if wd in dart_descs and w.get("description") != dart_descs[wd]:
w["description"] = dart_descs[wd]
updated_html_words += 1
html = html[: m.start(1)] + json.dumps(words, ensure_ascii=False) + html[m.end(1) :]
# HTML TRADITIONS_CANON
start = html.find(" const TRADITIONS_CANON = ")
end = html.find(" const WORDS_DATA = ")
if start > 0 and end > start:
html = (
html[:start]
+ " const TRADITIONS_CANON = "
+ json.dumps(canon, ensure_ascii=False, indent=2)
+ ";\n\n"
+ html[end:]
)
HTML.write_text(html, encoding="utf-8")
if MD.exists():
md = MD.read_text(encoding="utf-8")
def repl_md(mm: re.Match) -> str:
word = mm.group(1)
rest_head = mm.group(2)
old = mm.group(3)
if word not in dart_descs:
return mm.group(0)
return f"### {word} `#{mm.group(0).split('`#')[1].split('`')[0] if False else ''}"
# simpler: replace "* **Толкование:** ..." after ### word
def repl_block(mm: re.Match) -> str:
word = mm.group(1)
body = mm.group(2)
if word not in dart_descs:
return mm.group(0)
body2 = re.sub(
r"\* \*\*Толкование:\*\* .+",
f"* **Толкование:** {dart_descs[word]}",
body,
count=1,
)
return f"### {word} `{mm.group(0).split('`')[1]}`\n{body2}"
md2 = re.sub(
r"### ([^\s`]+) `(#[^`]+)`\n([\s\S]*?)(?=\n### |\n## |\Z)",
repl_block,
md,
)
MD.write_text(md2, encoding="utf-8")
print(
f"stations={len(canon['iching']['stations'])} "
f"words_stamped={len(word_to_tags)} extra_cluster={extra_cluster} "
f"stripped_generic={stripped} dart_blocks={n} html_words={updated_html_words}"
)
for w in ("огонь", "вода", "земля", "воздух", "озеро", "небо", "ветер", "гора", "гром"):
print(f" {w:8} {word_to_tags.get(w)}")
if __name__ == "__main__":
main()