231 lines
8.0 KiB
Python
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()
|