fix(export_docs): Gitea wiki link targets and INDEX.md pages

Strip .md from internal links, merge folder INDEX bodies, rewrite out-of-wiki repo paths to Gitea src URLs.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
mihailkudravcev
2026-07-29 13:37:34 +03:00
parent a455cd8085
commit cd79549c27
4 changed files with 100 additions and 7 deletions
+79 -6
View File
@@ -29,6 +29,30 @@ _EXPORT_DOCS_MARKER_END = "<!-- export_docs:end -->"
_RESERVED_ROOT_PAGES = {"Home.md", "History.md", "_Sidebar.md"}
@dataclass(frozen=True)
class RepoLinkContext:
"""Rewrite links to repo files (outside wiki export) as Gitea src/ URLs."""
root: Path
web_base: str
branch: str = "main"
def _git_web_base(remote_url: str) -> str:
u = remote_url.strip().rstrip("/")
if u.lower().endswith(".git"):
return u[:-4]
return u
def _repo_src_url(ctx: RepoLinkContext, abs_path: Path) -> str | None:
try:
rel = abs_path.resolve().relative_to(ctx.root.resolve())
except ValueError:
return None
return f"{ctx.web_base.rstrip('/')}/src/branch/{ctx.branch}/{rel.as_posix()}"
@dataclass(frozen=True)
class DocItem:
src: Path
@@ -135,6 +159,7 @@ def _rewrite_links(
src_to_md_dst: dict[Path, Path],
map_src_abs_to_dst_rel,
attachments: set[Path],
repo: RepoLinkContext | None = None,
) -> str:
def repl(m: re.Match[str]) -> str:
left, raw_target, right = m.group(1), m.group(2), m.group(3)
@@ -162,6 +187,10 @@ def _rewrite_links(
else:
if not cand.is_file():
return m.group(0)
if repo is not None:
src_url = _repo_src_url(repo, cand)
if src_url:
return f"{left}{src_url}{anchor}{tail}{right}"
dst_target_rel = map_src_abs_to_dst_rel(cand)
if dst_target_rel is None:
return m.group(0)
@@ -206,6 +235,10 @@ def _rewrite_links(
else:
if not cand.is_file():
return m.group(0)
if repo is not None:
src_url = _repo_src_url(repo, cand)
if src_url:
return f"{prefix}{src_url}{anchor}{suffix}"
dst_target_rel = map_src_abs_to_dst_rel(cand)
if dst_target_rel is None:
return m.group(0)
@@ -245,8 +278,19 @@ def _target_rel_for(rel: Path, *, flat_root: bool) -> Path:
def _encode_md_target(target: str) -> str:
# Keep slash + fragment markers, encode spaces/parentheses/unicode safely.
return quote(target, safe="/#._-~")
# Gitea wiki page URLs omit the `.md` suffix (file `foo.md` → page `/wiki/foo`).
t = target.replace("\\", "/")
if t.lower().endswith(".md"):
t = t[:-3]
return quote(t, safe="/#._-~")
def _is_folder_index_src(src: Path) -> bool:
return src.name.casefold() == "index.md"
def _strip_leading_h1(text: str) -> str:
return re.sub(r"^\s*#\s+.+?\n+", "", text, count=1, flags=re.MULTILINE).lstrip()
def _folder_label(folder_rel: Path, folder_clean: str) -> str:
@@ -415,6 +459,7 @@ def build_docs_output(
folder: str,
flat_root: bool = True,
clean: bool = True,
repo: RepoLinkContext | None = None,
) -> tuple[DocsStats, str]:
out_dir = out_dir.resolve()
if clean and out_dir.exists():
@@ -471,10 +516,17 @@ def build_docs_output(
attachments: set[Path] = set()
managed_rels: set[Path] = set(src_to_md_dst.values())
folder_docs: dict[Path, list[tuple[str, Path]]] = {}
index_body_by_folder: dict[Path, str] = {}
index_src_by_folder: dict[Path, Path] = {}
for item in items_ok:
raw = item.src.read_text(encoding="utf-8-sig", errors="ignore")
title_by_src[item.src.resolve()] = _extract_doc_title(raw, item.src.stem)
dst_rel = src_to_md_dst[item.src.resolve()]
src_folder = item.dst_rel.parent
if _is_folder_index_src(item.src):
index_body_by_folder[src_folder] = raw
index_src_by_folder[src_folder] = item.src
continue
rendered = _rewrite_links(
raw,
src=item.src,
@@ -482,13 +534,12 @@ def build_docs_output(
src_to_md_dst=src_to_md_dst,
map_src_abs_to_dst_rel=map_src_abs_to_dst_rel,
attachments=attachments,
repo=repo,
)
src_folder = item.dst_rel.parent
folder_index_rel = _target_rel_for(
(src_folder / "INDEX.md") if src_folder != Path(".") else Path("INDEX.md"),
flat_root=flat_root,
)
folder_title = _folder_label(src_folder, folder_clean)
short = _folder_short_name(src_folder, folder_clean)
nav = "\n".join(
[
@@ -514,7 +565,10 @@ def build_docs_output(
copy_file(abs_src, out_dir / dst_rel)
# Build per-folder index pages with links to articles and child folders.
all_folders = sorted(folder_docs.keys(), key=lambda p: p.as_posix())
all_folders = sorted(
set(folder_docs.keys()) | set(index_body_by_folder.keys()),
key=lambda p: p.as_posix(),
)
child_folders: dict[Path, set[Path]] = {}
folder_set = set(all_folders)
for f in all_folders:
@@ -562,15 +616,34 @@ def build_docs_output(
lines.append("")
docs = sorted(folder_docs.get(f, []), key=lambda x: x[0].casefold())
idx_target = index_target_by_folder[f]
if docs:
lines.append("## Статьи")
lines.append("")
for title, target_rel in docs:
if target_rel == idx_target:
continue
rel_link = _rel_link(index_target_by_folder[f], target_rel)
lines.append(f"- [{title}]({rel_link})")
lines.append("")
idx_target = index_target_by_folder[f]
index_raw = index_body_by_folder.get(f)
if index_raw:
idx_dst = index_target_by_folder[f]
index_src = index_src_by_folder.get(f, Path("INDEX.md"))
index_rendered = _rewrite_links(
_strip_leading_h1(index_raw),
src=index_src,
dst_rel=idx_dst,
src_to_md_dst=src_to_md_dst,
map_src_abs_to_dst_rel=map_src_abs_to_dst_rel,
attachments=attachments,
repo=repo,
)
if index_rendered.strip():
lines.append(index_rendered.rstrip())
lines.append("")
write_text(out_dir / idx_target, "\n".join(lines).rstrip() + "\n")
managed_rels.add(idx_target)