dc8be1bd1a
Cap encoded slug length and use \\?\ long-path writes (0.3.6). Co-authored-by: Cursor <cursoragent@cursor.com>
104 lines
3.2 KiB
Python
104 lines
3.2 KiB
Python
"""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+")
|
|
# Encoded «Slug.md» must leave room under Windows MAX_PATH (~260) for deep
|
|
# project roots (e.g. D:\SynologyDrive\projects_syn\…\out\).
|
|
_MAX_ENCODED_FILENAME = 100
|
|
|
|
|
|
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 _short_hash(s: str) -> str:
|
|
return hashlib.sha1(s.encode("utf-8")).hexdigest()[:8]
|
|
|
|
|
|
def _encoded_filename(slug: str) -> str:
|
|
return quote(slug, safe="-_.") + ".md"
|
|
|
|
|
|
def wiki_filename(title: str, *, meta_key: str | None = None) -> str:
|
|
"""
|
|
Gitea wiki stores non-ASCII page names as percent-encoded «Slug.md».
|
|
|
|
Encoded length is capped so typical Windows project paths still fit
|
|
under MAX_PATH; when truncated, a short hash of meta_key (or title)
|
|
is appended for stability.
|
|
"""
|
|
slug = sanitize_title(title).replace(" ", "-")
|
|
encoded = _encoded_filename(slug)
|
|
if len(encoded) <= _MAX_ENCODED_FILENAME:
|
|
return encoded
|
|
|
|
suffix = "-" + _short_hash(meta_key or title)
|
|
# ASCII suffix: encoded length == visible length
|
|
budget = _MAX_ENCODED_FILENAME - len(suffix) - 3 # ".md"
|
|
while slug and len(quote(slug, safe="-_.")) > budget:
|
|
slug = slug[:-1]
|
|
slug = slug.rstrip("-") or "page"
|
|
return quote(slug, safe="-_.") + suffix + ".md"
|
|
|
|
|
|
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:
|
|
fname = wiki_filename(
|
|
f"{title}-{_short_hash(p.meta_key)}",
|
|
meta_key=p.meta_key,
|
|
)
|
|
guard = 0
|
|
while fname.casefold() in used_file:
|
|
fname = _encoded_filename(f"page-{_short_hash(p.meta_key + str(guard))}")
|
|
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
|