From 7dd699dab529084fc3cfbf53bf5c26e20ae24b97 Mon Sep 17 00:00:00 2001 From: mihailkudravcev Date: Wed, 29 Jul 2026 11:12:38 +0300 Subject: [PATCH] Release 0.3.8: translit wiki slugs and relative md links Co-authored-by: Cursor --- CHANGELOG.md | 8 ++++ VERSION | 2 +- export1c_help/convert.py | 18 ++++++--- export1c_help/discover.py | 15 ++++--- export1c_help/export.py | 27 ++++++++----- export1c_help/naming.py | 83 +++++++++++++++++++++++++++------------ 6 files changed, 105 insertions(+), 48 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0235635..82562c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ 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 ### Fixed diff --git a/VERSION b/VERSION index 0f82685..6678432 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.7 +0.3.8 diff --git a/export1c_help/convert.py b/export1c_help/convert.py index 4bc0408..0b282a1 100644 --- a/export1c_help/convert.py +++ b/export1c_help/convert.py @@ -346,7 +346,11 @@ def _toc_paragraphs_to_list(md: str) -> str: 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.""" href = href.strip() 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: meta_key = m.group(1) - title = meta_to_title.get(meta_key) - if title: - if text == title or text == meta_key: - return f"[[{title}]]" - return f"[[{text}|{title}]]" + page = meta_to_page.get(meta_key) + if page: + title, filename = page + label = text + if text == meta_key: + label = title + return f"[{label}]({filename})" return f"{text} (`{meta_key}`)" if re.search(r"\.(png|jpe?g|gif|webp|bmp)$", href, re.I): diff --git a/export1c_help/discover.py b/export1c_help/discover.py index af454bd..047aa2b 100644 --- a/export1c_help/discover.py +++ b/export1c_help/discover.py @@ -216,19 +216,24 @@ def discover_help_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. """ - result: dict[str, str] = {} + result: dict[str, tuple[str, str]] = {} alias_owners: dict[str, list[str]] = {} 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: alias_owners.setdefault(alias, []).append(p.meta_key) for alias, owners in alias_owners.items(): if len(owners) == 1: - result[alias] = titles[owners[0]] + key = owners[0] + result[alias] = (titles[key], files[key]) return result diff --git a/export1c_help/export.py b/export1c_help/export.py index a766653..13aa3d3 100644 --- a/export1c_help/export.py +++ b/export1c_help/export.py @@ -11,7 +11,7 @@ from pathlib import Path from export1c_help.__version__ import __version__ 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.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.lifecycle import ( LifecycleResult, @@ -70,6 +70,7 @@ def _page_footer(rec: PageRecord, *, identity: ConfigIdentity) -> str: def build_home( pages: list[HelpPage], titles: dict[str, str], + files: dict[str, str], *, source_label: str, identity: ConfigIdentity, @@ -104,20 +105,24 @@ def build_home( lines.append("") for p in group: title = titles[p.meta_key] - lines.append(f"- [[{title}]]") + lines.append(f"- [{title}]({files[p.meta_key]})") lines.append("") 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]] = {} for p in pages: by_type.setdefault(p.type_dir, []).append(p) lines = [ "# Справка", "", - "[[Home|Оглавление]]", - "[[History|История выгрузок]]", + "[Оглавление](Home.md)", + "[История выгрузок](History.md)", "", ] 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.sort(key=lambda p: titles[p.meta_key].casefold()) 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: - lines.append(f"- … ещё {len(objs) - 80} (см. [[Home]])") + lines.append(f"- … ещё {len(objs) - 80} (см. [Оглавление](Home.md))") lines.append("") return "\n".join(lines).rstrip() + "\n" @@ -244,7 +249,7 @@ def export_help( h1_by_meta[p.meta_key] = extract_h1(html) 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(): import shutil @@ -268,7 +273,8 @@ def export_help( for p in pages: html = html_cache[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): stats.pages_skipped_empty += 1 continue @@ -350,12 +356,13 @@ def export_help( build_home( written_pages, titles, + files, source_label=label, identity=identity, 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( out_dir / "History.md", build_history( diff --git a/export1c_help/naming.py b/export1c_help/naming.py index eac7153..cf404f1 100644 --- a/export1c_help/naming.py +++ b/export1c_help/naming.py @@ -1,18 +1,53 @@ -"""Wiki page titles and Gitea filename encoding.""" +"""Wiki page titles and transliterated wiki filenames.""" 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 +_NON_SLUG = re.compile(r"[^a-z0-9._-]+") +_DASH_RUNS = re.compile(r"-+") +_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: @@ -26,35 +61,31 @@ 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 _translit_slug(title: str) -> str: + 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: """ - 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. + Build ASCII transliterated ``slug.md`` with bounded slug length. """ - slug = sanitize_title(title).replace(" ", "-") - encoded = _encoded_filename(slug) - if len(encoded) <= _MAX_ENCODED_FILENAME: - return encoded - + slug = _translit_slug(sanitize_title(title)) + if len(slug) <= _MAX_SLUG: + return f"{slug}.md" 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" + keep = max(8, _MAX_SLUG - len(suffix)) + slug = slug[:keep].rstrip("-._") or "page" + return f"{slug}{suffix}.md" def decode_wiki_filename(name: str) -> str: - base = name[:-3] if name.endswith(".md") else name - return unquote(base) + return name[:-3] if name.endswith(".md") else name def assign_titles( @@ -92,7 +123,7 @@ def assign_titles( ) guard = 0 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 used_title.add(title.casefold())