#!/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()