73 lines
2.6 KiB
Python
73 lines
2.6 KiB
Python
"""Парсер карточки знаний Александры (frontmatter + тело) и сборка справки по дереву."""
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
import re
|
|
|
|
|
|
@dataclass
|
|
class Card:
|
|
nisha: str
|
|
body: str
|
|
path: str
|
|
roditel: str | None = None
|
|
sinonimy: list[str] = field(default_factory=list)
|
|
istochnik: str | None = None
|
|
obnovleno: str | None = None
|
|
trendy_obnovleno: str | None = None
|
|
|
|
|
|
def _parse_frontmatter(text: str):
|
|
"""Вернуть (dict метаданных, тело). Поддержка скаляров и списков [a, b]."""
|
|
m = re.match(r"^---\s*\n(.*?)\n---\s*\n?(.*)$", text, re.S)
|
|
if not m:
|
|
if text.lstrip().startswith("---"):
|
|
raise ValueError("битый frontmatter: есть открывающий '---', но нет закрывающего")
|
|
return {}, text.strip()
|
|
meta_raw, body = m.group(1), m.group(2)
|
|
meta = {}
|
|
for line in meta_raw.splitlines():
|
|
if not line.strip() or ":" not in line:
|
|
continue
|
|
key, _, val = line.partition(":")
|
|
key, val = key.strip(), val.strip()
|
|
if val.startswith("[") and val.endswith("]"):
|
|
items = [x.strip() for x in val[1:-1].split(",") if x.strip()]
|
|
meta[key] = items
|
|
else:
|
|
meta[key] = val
|
|
return meta, body.strip()
|
|
|
|
|
|
def parse_card(path) -> Card:
|
|
p = Path(path)
|
|
meta, body = _parse_frontmatter(p.read_text(encoding="utf-8"))
|
|
roditel = meta.get("roditel")
|
|
if roditel in ("", "—", "-"):
|
|
roditel = None
|
|
return Card(
|
|
nisha=meta.get("nisha", p.stem),
|
|
body=body,
|
|
path=str(p),
|
|
roditel=roditel,
|
|
sinonimy=meta.get("sinonimy", []) or [],
|
|
istochnik=meta.get("istochnik"),
|
|
obnovleno=meta.get("obnovleno"),
|
|
trendy_obnovleno=meta.get("trendy_obnovleno"),
|
|
)
|
|
|
|
|
|
def assemble_brief(card: Card, cards_by_name: dict[str, Card]) -> str:
|
|
"""Склейка справки: общее родителя (вверх по цепочке) + тело ниши.
|
|
Цепочку ограничиваем 4 уровнями от зацикливания."""
|
|
chain = []
|
|
cur, seen, depth = card, set(), 0
|
|
while cur is not None and depth < 4:
|
|
chain.append(cur)
|
|
if cur.roditel in seen or cur.roditel is None:
|
|
break
|
|
seen.add(cur.roditel)
|
|
cur = cards_by_name.get(cur.roditel)
|
|
depth += 1
|
|
# от корня (домен) к листу (ниша)
|
|
return "\n\n".join(c.body for c in reversed(chain)).strip()
|