Release 0.3.8: translit wiki slugs and relative md links

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
mihailkudravcev
2026-07-29 11:12:38 +03:00
parent 98cbb88461
commit 7dd699dab5
6 changed files with 105 additions and 48 deletions
+8
View File
@@ -2,6 +2,14 @@
All notable changes to **export_1c_help** are documented in this file. All notable changes to **export_1c_help** are documented in this file.
## [0.3.8] - 2026-07-29
### Changed
- Wiki page filenames switched to short ASCII transliteration slugs (with stable hash suffix on truncation), while visible page titles remain full Russian.
- Internal links now use Markdown format with human-readable Russian labels and relative `.md` targets (no `wiki/` prefix), matching Gitea wiki routing.
- Generated article body now always starts with `# <Полное русское наименование>`.
## [0.3.7] - 2026-07-24 ## [0.3.7] - 2026-07-24
### Fixed ### Fixed
+1 -1
View File
@@ -1 +1 @@
0.3.7 0.3.8
+12 -6
View File
@@ -346,7 +346,11 @@ def _toc_paragraphs_to_list(md: str) -> str:
return "\n".join(out) return "\n".join(out)
def default_link_rewrite(href: str, text: str, meta_to_title: dict[str, str]) -> str: def default_link_rewrite(
href: str,
text: str,
meta_to_page: dict[str, tuple[str, str]],
) -> str:
"""Rewrite href into markdown / wiki link.""" """Rewrite href into markdown / wiki link."""
href = href.strip() href = href.strip()
text = text.strip() or href text = text.strip() or href
@@ -368,11 +372,13 @@ def default_link_rewrite(href: str, text: str, meta_to_title: dict[str, str]) ->
) )
if m: if m:
meta_key = m.group(1) meta_key = m.group(1)
title = meta_to_title.get(meta_key) page = meta_to_page.get(meta_key)
if title: if page:
if text == title or text == meta_key: title, filename = page
return f"[[{title}]]" label = text
return f"[[{text}|{title}]]" if text == meta_key:
label = title
return f"[{label}]({filename})"
return f"{text} (`{meta_key}`)" return f"{text} (`{meta_key}`)"
if re.search(r"\.(png|jpe?g|gif|webp|bmp)$", href, re.I): if re.search(r"\.(png|jpe?g|gif|webp|bmp)$", href, re.I):
+10 -5
View File
@@ -216,19 +216,24 @@ def discover_help_pages(
return pages return pages
def build_link_title_map(pages: list[HelpPage], titles: dict[str, str]) -> dict[str, str]: def build_link_map(
pages: list[HelpPage],
titles: dict[str, str],
files: dict[str, str],
) -> dict[str, tuple[str, str]]:
""" """
meta_key / alias → wiki title for href rewrite. meta_key / alias → (wiki title, filename) for href rewrite.
Leaf aliases (Subsystem.X) are registered only when unique. Leaf aliases (Subsystem.X) are registered only when unique.
""" """
result: dict[str, str] = {} result: dict[str, tuple[str, str]] = {}
alias_owners: dict[str, list[str]] = {} alias_owners: dict[str, list[str]] = {}
for p in pages: for p in pages:
result[p.meta_key] = titles[p.meta_key] result[p.meta_key] = (titles[p.meta_key], files[p.meta_key])
for alias in p.link_aliases: for alias in p.link_aliases:
alias_owners.setdefault(alias, []).append(p.meta_key) alias_owners.setdefault(alias, []).append(p.meta_key)
for alias, owners in alias_owners.items(): for alias, owners in alias_owners.items():
if len(owners) == 1: if len(owners) == 1:
result[alias] = titles[owners[0]] key = owners[0]
result[alias] = (titles[key], files[key])
return result return result
+17 -10
View File
@@ -11,7 +11,7 @@ from pathlib import Path
from export1c_help.__version__ import __version__ from export1c_help.__version__ import __version__
from export1c_help.config_version import ConfigIdentity, read_config_identity from export1c_help.config_version import ConfigIdentity, read_config_identity
from export1c_help.convert import default_link_rewrite, extract_h1, html_to_markdown from export1c_help.convert import default_link_rewrite, extract_h1, html_to_markdown
from export1c_help.discover import HelpPage, build_link_title_map, discover_help_pages from export1c_help.discover import HelpPage, build_link_map, discover_help_pages
from export1c_help.fscompat import copy_file, ensure_dir, remove_file, write_text from export1c_help.fscompat import copy_file, ensure_dir, remove_file, write_text
from export1c_help.lifecycle import ( from export1c_help.lifecycle import (
LifecycleResult, LifecycleResult,
@@ -70,6 +70,7 @@ def _page_footer(rec: PageRecord, *, identity: ConfigIdentity) -> str:
def build_home( def build_home(
pages: list[HelpPage], pages: list[HelpPage],
titles: dict[str, str], titles: dict[str, str],
files: dict[str, str],
*, *,
source_label: str, source_label: str,
identity: ConfigIdentity, identity: ConfigIdentity,
@@ -104,20 +105,24 @@ def build_home(
lines.append("") lines.append("")
for p in group: for p in group:
title = titles[p.meta_key] title = titles[p.meta_key]
lines.append(f"- [[{title}]]") lines.append(f"- [{title}]({files[p.meta_key]})")
lines.append("") lines.append("")
return "\n".join(lines).rstrip() + "\n" return "\n".join(lines).rstrip() + "\n"
def build_sidebar(pages: list[HelpPage], titles: dict[str, str]) -> str: def build_sidebar(
pages: list[HelpPage],
titles: dict[str, str],
files: dict[str, str],
) -> str:
by_type: dict[str, list[HelpPage]] = {} by_type: dict[str, list[HelpPage]] = {}
for p in pages: for p in pages:
by_type.setdefault(p.type_dir, []).append(p) by_type.setdefault(p.type_dir, []).append(p)
lines = [ lines = [
"# Справка", "# Справка",
"", "",
"[[Home|Оглавление]]", "[Оглавление](Home.md)",
"[[History|История выгрузок]]", "[История выгрузок](History.md)",
"", "",
] ]
for type_dir in sorted(by_type, key=lambda d: DIR_TO_RU.get(d, d)): for type_dir in sorted(by_type, key=lambda d: DIR_TO_RU.get(d, d)):
@@ -127,9 +132,9 @@ def build_sidebar(pages: list[HelpPage], titles: dict[str, str]) -> str:
objs = [p for p in by_type[type_dir] if not p.is_form] objs = [p for p in by_type[type_dir] if not p.is_form]
objs.sort(key=lambda p: titles[p.meta_key].casefold()) objs.sort(key=lambda p: titles[p.meta_key].casefold())
for p in objs[:80]: for p in objs[:80]:
lines.append(f"- [[{titles[p.meta_key]}]]") lines.append(f"- [{titles[p.meta_key]}]({files[p.meta_key]})")
if len(objs) > 80: if len(objs) > 80:
lines.append(f"- … ещё {len(objs) - 80} (см. [[Home]])") lines.append(f"- … ещё {len(objs) - 80} (см. [Оглавление](Home.md))")
lines.append("") lines.append("")
return "\n".join(lines).rstrip() + "\n" return "\n".join(lines).rstrip() + "\n"
@@ -244,7 +249,7 @@ def export_help(
h1_by_meta[p.meta_key] = extract_h1(html) h1_by_meta[p.meta_key] = extract_h1(html)
titles, files = assign_titles(pages, h1_by_meta) titles, files = assign_titles(pages, h1_by_meta)
link_map = build_link_title_map(pages, titles) link_map = build_link_map(pages, titles, files)
if clean and out_dir.exists(): if clean and out_dir.exists():
import shutil import shutil
@@ -268,7 +273,8 @@ def export_help(
for p in pages: for p in pages:
html = html_cache[p.meta_key] html = html_cache[p.meta_key]
title = titles[p.meta_key] title = titles[p.meta_key]
md = html_to_markdown(html, rewrite_link=rewrite, strip_first_h1=title) md_body = html_to_markdown(html, rewrite_link=rewrite, strip_first_h1=title)
md = f"# {title}\n\n{md_body.lstrip()}"
if skip_empty and _is_effectively_empty(md): if skip_empty and _is_effectively_empty(md):
stats.pages_skipped_empty += 1 stats.pages_skipped_empty += 1
continue continue
@@ -350,12 +356,13 @@ def export_help(
build_home( build_home(
written_pages, written_pages,
titles, titles,
files,
source_label=label, source_label=label,
identity=identity, identity=identity,
life=life, life=life,
), ),
) )
write_text(out_dir / "_Sidebar.md", build_sidebar(written_pages, titles)) write_text(out_dir / "_Sidebar.md", build_sidebar(written_pages, titles, files))
write_text( write_text(
out_dir / "History.md", out_dir / "History.md",
build_history( build_history(
+57 -26
View File
@@ -1,18 +1,53 @@
"""Wiki page titles and Gitea filename encoding.""" """Wiki page titles and transliterated wiki filenames."""
from __future__ import annotations from __future__ import annotations
import hashlib import hashlib
import re import re
from urllib.parse import quote, unquote
from export1c_help.discover import HelpPage from export1c_help.discover import HelpPage
_INVALID = re.compile(r'[\\/:*?"<>|#\[\]]+') _INVALID = re.compile(r'[\\/:*?"<>|#\[\]]+')
_SPACES = re.compile(r"\s+") _SPACES = re.compile(r"\s+")
# Encoded «Slug.md» must leave room under Windows MAX_PATH (~260) for deep _NON_SLUG = re.compile(r"[^a-z0-9._-]+")
# project roots (e.g. D:\SynologyDrive\projects_syn\…\out\). _DASH_RUNS = re.compile(r"-+")
_MAX_ENCODED_FILENAME = 100 _MAX_SLUG = 48
_TRANSLIT = {
"а": "a",
"б": "b",
"в": "v",
"г": "g",
"д": "d",
"е": "e",
"ё": "e",
"ж": "zh",
"з": "z",
"и": "i",
"й": "y",
"к": "k",
"л": "l",
"м": "m",
"н": "n",
"о": "o",
"п": "p",
"р": "r",
"с": "s",
"т": "t",
"у": "u",
"ф": "f",
"х": "h",
"ц": "ts",
"ч": "ch",
"ш": "sh",
"щ": "sch",
"ъ": "",
"ы": "y",
"ь": "",
"э": "e",
"ю": "yu",
"я": "ya",
}
def sanitize_title(title: str) -> str: def sanitize_title(title: str) -> str:
@@ -26,35 +61,31 @@ def _short_hash(s: str) -> str:
return hashlib.sha1(s.encode("utf-8")).hexdigest()[:8] return hashlib.sha1(s.encode("utf-8")).hexdigest()[:8]
def _encoded_filename(slug: str) -> str: def _translit_slug(title: str) -> str:
return quote(slug, safe="-_.") + ".md" chars: list[str] = []
for ch in title.lower():
chars.append(_TRANSLIT.get(ch, ch))
raw = "".join(chars).replace(" ", "-")
raw = _NON_SLUG.sub("-", raw)
raw = _DASH_RUNS.sub("-", raw).strip("-._")
return raw or "page"
def wiki_filename(title: str, *, meta_key: str | None = None) -> str: def wiki_filename(title: str, *, meta_key: str | None = None) -> str:
""" """
Gitea wiki stores non-ASCII page names as percent-encoded «Slug.md». Build ASCII transliterated ``slug.md`` with bounded slug length.
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(" ", "-") slug = _translit_slug(sanitize_title(title))
encoded = _encoded_filename(slug) if len(slug) <= _MAX_SLUG:
if len(encoded) <= _MAX_ENCODED_FILENAME: return f"{slug}.md"
return encoded
suffix = "-" + _short_hash(meta_key or title) suffix = "-" + _short_hash(meta_key or title)
# ASCII suffix: encoded length == visible length keep = max(8, _MAX_SLUG - len(suffix))
budget = _MAX_ENCODED_FILENAME - len(suffix) - 3 # ".md" slug = slug[:keep].rstrip("-._") or "page"
while slug and len(quote(slug, safe="-_.")) > budget: return f"{slug}{suffix}.md"
slug = slug[:-1]
slug = slug.rstrip("-") or "page"
return quote(slug, safe="-_.") + suffix + ".md"
def decode_wiki_filename(name: str) -> str: def decode_wiki_filename(name: str) -> str:
base = name[:-3] if name.endswith(".md") else name return name[:-3] if name.endswith(".md") else name
return unquote(base)
def assign_titles( def assign_titles(
@@ -92,7 +123,7 @@ def assign_titles(
) )
guard = 0 guard = 0
while fname.casefold() in used_file: while fname.casefold() in used_file:
fname = _encoded_filename(f"page-{_short_hash(p.meta_key + str(guard))}") fname = f"page-{_short_hash(p.meta_key + str(guard))}.md"
guard += 1 guard += 1
used_title.add(title.casefold()) used_title.add(title.casefold())