Kansho/backend/journal_body.py
2026-08-25 13:57:23 +02:00

71 lines
2.5 KiB
Python

"""Journal entry body as Markdown. Media stays in media_assets; the body only references it.
Canonical embed: ![caption](kansho-media:<uuid>)
Legacy [[media:<uuid>|caption]] is still read and normalised on save.
Tokens never go to the Privacy Gateway; captions may, as user wording.
"""
from __future__ import annotations
import re
UUID = r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
UUID_RE = re.compile(rf"^{UUID}$")
MD_MEDIA_RE = re.compile(rf"!\[([^\]]*)\]\(kansho-media:({UUID})\)")
LEGACY_MEDIA_RE = re.compile(rf"\[\[media:({UUID})(?:\|([^\]]*))?\]\]")
HEADING_PREFIX = re.compile(r"^#{1,6}\s+")
def clean_title(raw: str) -> str:
"""First line only, without Markdown heading marks."""
line = (raw or "").strip().splitlines()[0].strip() if raw else ""
return HEADING_PREFIX.sub("", line).strip()
def media_token(media_id: str, caption: str = "") -> str:
cap = (caption or "").replace("]", "").replace("\n", " ").strip()
return f"![{cap}](kansho-media:{media_id})"
def media_ids(body: str) -> list[str]:
seen: list[str] = []
for match in MD_MEDIA_RE.finditer(body or ""):
media_id = match.group(2)
if media_id not in seen:
seen.append(media_id)
for match in LEGACY_MEDIA_RE.finditer(body or ""):
media_id = match.group(1)
if media_id not in seen:
seen.append(media_id)
return seen
def to_markdown(body: str) -> str:
"""Legacy media tokens become Markdown image syntax. Already-Markdown is unchanged."""
def repl(match: re.Match) -> str:
return media_token(match.group(1), match.group(2) or "")
return LEGACY_MEDIA_RE.sub(repl, body or "")
def plain_text(body: str) -> str:
"""User wording only: no media URLs, no asset IDs, no Markdown markup. Captions stay."""
text = to_markdown(body)
def caption(match: re.Match) -> str:
return (match.group(1) or "").strip()
text = MD_MEDIA_RE.sub(caption, text)
text = re.sub(r"<u>(.*?)</u>", r"\1", text, flags=re.I | re.S)
text = re.sub(r"^#{1,6}\s+", "", text, flags=re.M)
text = re.sub(r"\*\*(.+?)\*\*", r"\1", text)
text = re.sub(r"__(.+?)__", r"\1", text)
text = re.sub(r"(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)", r"\1", text)
text = re.sub(r"^\s*[-*+]\s+", "", text, flags=re.M)
text = re.sub(r"^\s*\d+\.\s+", "", text, flags=re.M)
text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text)
text = re.sub(r"`([^`]+)`", r"\1", text)
text = re.sub(r"\n{3,}", "\n\n", text)
return text.strip()