"""Wiki page titles and Gitea filename encoding.""" from __future__ import annotations import hashlib import re from urllib.parse import quote, unquote from export1c_help.discover import HelpPage _INVALID = re.compile(r'[\\/:*?"<>|#\[\]]+') _SPACES = re.compile(r"\s+") # Keep slug short so percent-encoded form fits in 255-byte FS names _MAX_SLUG_BYTES = 72 def sanitize_title(title: str) -> str: title = _SPACES.sub(" ", title).strip() title = _INVALID.sub(" ", title) title = _SPACES.sub(" ", title).strip() return title or "untitled" def _truncate_utf8(s: str, max_bytes: int) -> str: raw = s.encode("utf-8") if len(raw) <= max_bytes: return s raw = raw[:max_bytes] while raw: try: return raw.decode("utf-8").rstrip("- ") except UnicodeDecodeError: raw = raw[:-1] return "page" def _short_hash(s: str) -> str: return hashlib.sha1(s.encode("utf-8")).hexdigest()[:8] def wiki_filename(title: str, *, meta_key: str | None = None) -> str: """ Gitea wiki stores non-ASCII page names as percent-encoded «Slug.md». Slug = title with spaces → «-», truncated so encoded length ≤ 255. """ slug = sanitize_title(title).replace(" ", "-") slug = _truncate_utf8(slug, _MAX_SLUG_BYTES) if meta_key: # stable disambiguator when truncated pass encoded = quote(slug, safe="-_.") + ".md" if len(encoded) > 255 and meta_key: slug = _truncate_utf8(slug, 50) + "-" + _short_hash(meta_key) encoded = quote(slug, safe="-_.") + ".md" return encoded def decode_wiki_filename(name: str) -> str: base = name[:-3] if name.endswith(".md") else name return unquote(base) def assign_titles( pages: list[HelpPage], h1_by_meta: dict[str, str | None], ) -> tuple[dict[str, str], dict[str, str]]: """Returns (meta_key → title, meta_key → filename).""" used_title: set[str] = set() used_file: set[str] = set() titles: dict[str, str] = {} files: dict[str, str] = {} for p in pages: h1 = h1_by_meta.get(p.meta_key) base = sanitize_title(h1 or p.synonym or p.meta_key) title = base n = 2 while title.casefold() in used_title: if p.is_form and p.form_name and n == 2: title = sanitize_title(f"{base} — {p.form_name}") elif n == 2: title = sanitize_title(f"{base} — {p.object_name}") else: title = sanitize_title(f"{base} ({n})") n += 1 if n > 5000: title = sanitize_title(p.meta_key) break fname = wiki_filename(title, meta_key=p.meta_key) if fname.casefold() in used_file: slug = _truncate_utf8(sanitize_title(title).replace(" ", "-"), 50) fname = quote(f"{slug}-{_short_hash(p.meta_key)}", safe="-_.") + ".md" guard = 0 while fname.casefold() in used_file: fname = quote(f"page-{_short_hash(p.meta_key + str(guard))}", safe="-_.") + ".md" guard += 1 used_title.add(title.casefold()) used_file.add(fname.casefold()) titles[p.meta_key] = title files[p.meta_key] = fname return titles, files