""" Import UpNote I: (inventar) notes into 03 Bereiche/Inventar/. Title: '# I: Itemname 🗄️' Extract standard property table rows into YAML frontmatter. Preserve content below property table (e.g. '## weitere Details') as body. Skip @@TITEL@@ templates. Tag: inventar, upnote-import. Use --trash to pull from UpNote trash. """ from __future__ import annotations import argparse import re import sys from dataclasses import dataclass, field from pathlib import Path try: sys.stdout.reconfigure(encoding="utf-8") # type: ignore[attr-defined] except Exception: pass UPNOTE_ROOT = Path( r"C:\Users\d-chrka\AppData\Roaming\UpNote\UpNote Backup" r"\HtgSdi2hYyUfnYq3OZkBwx13H5q2\Markdown\General Space" ) VAULT = Path(r"D:\projects\chrka\brain") INVENTAR = VAULT / "03 Bereiche" / "Inventar" TITLE_RE = re.compile(r"^#{1,3}\s*I:\s*(.+?)(?:\s*🗄️?)?\s*$", re.MULTILINE) # first tiny meta table (#dtInventar) META_TABLE_RE = re.compile(r"^\|.*\|\s*\n\|[\s\-:|]+\|\s*\n(?:\|.*\|\s*\n)+", re.MULTILINE) HR_RE = re.compile(r"^\*\s*\*\s*\*\s*$", re.MULTILINE) BR_LINE_RE = re.compile(r"^\s*\s*$", re.MULTILINE) # Standard inventory property keys (order preserved in frontmatter output) PROP_KEYS = [ "Typ", "Ort", "Details", "Firma", "Zuordnung", "Modell", "Seriennummer", "Hersteller", "Preis", "Anzahl", "Kaufdatum", "Rechnung", "Handbuch", "Tests / Shop", "Garantie", ] # normalized -> canonical PROP_KEY_NORM = {k.lower().replace(" ", "").replace("/", ""): k for k in PROP_KEYS} # YAML-safe key transform YAML_KEY = { "Typ": "typ", "Ort": "ort", "Details": "details", "Firma": "firma", "Zuordnung": "zuordnung", "Modell": "modell", "Seriennummer": "seriennummer", "Hersteller": "hersteller", "Preis": "preis", "Anzahl": "anzahl", "Kaufdatum": "kaufdatum", "Rechnung": "rechnung", "Handbuch": "handbuch", "Tests / Shop": "tests_shop", "Garantie": "garantie", } @dataclass class Item: uuid: str title: str props: dict[str, str] = field(default_factory=dict) body: str = "" def clean_cell(v: str) -> str: v = v.replace("
", "").replace("
", "").replace("
", "") return v.strip() def extract_props(table: str) -> dict[str, str]: """Parse 2-col property table. Returns dict of canonical key -> value (non-empty).""" out: dict[str, str] = {} for line in table.splitlines(): if not line.startswith("|"): continue cells = [c.strip() for c in line.strip().strip("|").split("|")] if len(cells) < 2: continue # skip header and separator rows if set(cells[0]) <= set("-: ") and set(cells[1]) <= set("-: "): continue key_norm = cells[0].lower().replace(" ", "").replace("/", "") if key_norm not in PROP_KEY_NORM: continue canonical = PROP_KEY_NORM[key_norm] value = clean_cell(cells[1]) if value: out[canonical] = value return out def yaml_escape(v: str) -> str: """Quote if value has YAML-special chars.""" if re.search(r'[:\[\]{}#&*!|>\'"%@`,]', v) or v.startswith("-") or "\n" in v: # use double-quoted, escape backslash + double quote return '"' + v.replace("\\", "\\\\").replace('"', '\\"') + '"' return v def parse(src: Path) -> Item | None: text = src.read_text(encoding="utf-8") m = TITLE_RE.search(text) if not m: return None title = m.group(1).strip() if "@@" in title or "{{" in title: return None # normalize filesystem-hostile chars safe_title = re.sub(r"[\\/:*?\"<>|]", "-", title) safe_title = re.sub(r"\s+", " ", safe_title).strip() rest = text[m.end():] # drop #dtInventar marker (plain line or inside small 1-row table) # first variant: small table containing only
+ #dtInventar def _is_meta_marker(tbl: str) -> bool: return "#dtInventar" in tbl and "Typ" not in tbl and "Modell" not in tbl # consume up to one meta-marker table tm = META_TABLE_RE.search(rest) if tm and _is_meta_marker(tm.group(0)): rest = rest[:tm.start()] + rest[tm.end():] # also drop bare '#dtInventar' lines rest = re.sub(r"^\s*#dtInventar\s*$", "", rest, flags=re.MULTILINE) # drop first HR rest = HR_RE.sub("", rest, count=1) # find main property table props: dict[str, str] = {} prop_match = META_TABLE_RE.search(rest) if prop_match: props = extract_props(prop_match.group(0)) rest = rest[:prop_match.start()] + rest[prop_match.end():] # clean body rest = BR_LINE_RE.sub("", rest) rest = re.sub(r"\n{3,}", "\n\n", rest).strip() return Item(uuid=src.stem, title=safe_title, props=props, body=rest) def render(item: Item) -> str: fm = ["---", "tags:", " - inventar", " - upnote-import"] for k in PROP_KEYS: if k in item.props: fm.append(f"{YAML_KEY[k]}: {yaml_escape(item.props[k])}") fm.append("---") header = "\n".join(fm) body = item.body if item.body else "" return f"{header}\n\n# {item.title}\n\n{body}\n".rstrip() + "\n" def append_block(existing: str, item: Item) -> str: lines = ["\n\n## UpNote-Import\n"] if item.props: for k in PROP_KEYS: if k in item.props: lines.append(f"- **{k}:** {item.props[k]}") lines.append("") if item.body: lines.append(item.body) return existing.rstrip() + "\n".join(lines) + "\n" def collect(trash: bool) -> list[Path]: d = UPNOTE_ROOT / "trash" if trash else UPNOTE_ROOT out = [] for p in d.glob("*.md"): try: head = p.read_text(encoding="utf-8", errors="ignore")[:200] except Exception: continue if re.match(r"^#{1,3}\s*I:\s", head): out.append(p) return out def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--dry-run", action="store_true") ap.add_argument("--trash", action="store_true") args = ap.parse_args() INVENTAR.mkdir(parents=True, exist_ok=True) items: list[Item] = [] skipped = 0 for src in collect(trash=args.trash): it = parse(src) if it is None: skipped += 1 continue items.append(it) items.sort(key=lambda x: x.title.lower()) print(f"Parsed: {len(items)} inventar items (skipped template/empty: {skipped})") if args.dry_run: for it in items: pk = ", ".join(k for k in PROP_KEYS if k in it.props) or "(no props)" print(f" {it.title} [{pk}]") return 0 written = merged = 0 for it in items: target = INVENTAR / f"{it.title}.md" if target.exists(): target.write_text( append_block(target.read_text(encoding="utf-8"), it), encoding="utf-8", ) merged += 1 else: target.write_text(render(it), encoding="utf-8") written += 1 print(f"Written: {written}, merged: {merged}") return 0 if __name__ == "__main__": sys.exit(main())