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:
@@ -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.11] - 2026-07-29
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Internal wiki links omit the `.md` suffix in Markdown targets (Gitea resolves `/wiki/page`, not `/wiki/page.md`).
|
||||||
|
- Folder `INDEX.md`: merge source body into auto-generated index; do not list INDEX as a self-link under «Статьи».
|
||||||
|
- Relative links to files in the git repo but outside the wiki export (e.g. `ws-rhana/...`) rewrite to Gitea `src/branch/...` URLs.
|
||||||
|
|
||||||
## [0.3.10] - 2026-07-29
|
## [0.3.10] - 2026-07-29
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -29,6 +29,30 @@ _EXPORT_DOCS_MARKER_END = "<!-- export_docs:end -->"
|
|||||||
_RESERVED_ROOT_PAGES = {"Home.md", "History.md", "_Sidebar.md"}
|
_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)
|
@dataclass(frozen=True)
|
||||||
class DocItem:
|
class DocItem:
|
||||||
src: Path
|
src: Path
|
||||||
@@ -135,6 +159,7 @@ def _rewrite_links(
|
|||||||
src_to_md_dst: dict[Path, Path],
|
src_to_md_dst: dict[Path, Path],
|
||||||
map_src_abs_to_dst_rel,
|
map_src_abs_to_dst_rel,
|
||||||
attachments: set[Path],
|
attachments: set[Path],
|
||||||
|
repo: RepoLinkContext | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
def repl(m: re.Match[str]) -> str:
|
def repl(m: re.Match[str]) -> str:
|
||||||
left, raw_target, right = m.group(1), m.group(2), m.group(3)
|
left, raw_target, right = m.group(1), m.group(2), m.group(3)
|
||||||
@@ -162,6 +187,10 @@ def _rewrite_links(
|
|||||||
else:
|
else:
|
||||||
if not cand.is_file():
|
if not cand.is_file():
|
||||||
return m.group(0)
|
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)
|
dst_target_rel = map_src_abs_to_dst_rel(cand)
|
||||||
if dst_target_rel is None:
|
if dst_target_rel is None:
|
||||||
return m.group(0)
|
return m.group(0)
|
||||||
@@ -206,6 +235,10 @@ def _rewrite_links(
|
|||||||
else:
|
else:
|
||||||
if not cand.is_file():
|
if not cand.is_file():
|
||||||
return m.group(0)
|
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)
|
dst_target_rel = map_src_abs_to_dst_rel(cand)
|
||||||
if dst_target_rel is None:
|
if dst_target_rel is None:
|
||||||
return m.group(0)
|
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:
|
def _encode_md_target(target: str) -> str:
|
||||||
# Keep slash + fragment markers, encode spaces/parentheses/unicode safely.
|
# Gitea wiki page URLs omit the `.md` suffix (file `foo.md` → page `/wiki/foo`).
|
||||||
return quote(target, safe="/#._-~")
|
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:
|
def _folder_label(folder_rel: Path, folder_clean: str) -> str:
|
||||||
@@ -415,6 +459,7 @@ def build_docs_output(
|
|||||||
folder: str,
|
folder: str,
|
||||||
flat_root: bool = True,
|
flat_root: bool = True,
|
||||||
clean: bool = True,
|
clean: bool = True,
|
||||||
|
repo: RepoLinkContext | None = None,
|
||||||
) -> tuple[DocsStats, str]:
|
) -> tuple[DocsStats, str]:
|
||||||
out_dir = out_dir.resolve()
|
out_dir = out_dir.resolve()
|
||||||
if clean and out_dir.exists():
|
if clean and out_dir.exists():
|
||||||
@@ -471,10 +516,17 @@ def build_docs_output(
|
|||||||
attachments: set[Path] = set()
|
attachments: set[Path] = set()
|
||||||
managed_rels: set[Path] = set(src_to_md_dst.values())
|
managed_rels: set[Path] = set(src_to_md_dst.values())
|
||||||
folder_docs: dict[Path, list[tuple[str, Path]]] = {}
|
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:
|
for item in items_ok:
|
||||||
raw = item.src.read_text(encoding="utf-8-sig", errors="ignore")
|
raw = item.src.read_text(encoding="utf-8-sig", errors="ignore")
|
||||||
title_by_src[item.src.resolve()] = _extract_doc_title(raw, item.src.stem)
|
title_by_src[item.src.resolve()] = _extract_doc_title(raw, item.src.stem)
|
||||||
dst_rel = src_to_md_dst[item.src.resolve()]
|
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(
|
rendered = _rewrite_links(
|
||||||
raw,
|
raw,
|
||||||
src=item.src,
|
src=item.src,
|
||||||
@@ -482,13 +534,12 @@ def build_docs_output(
|
|||||||
src_to_md_dst=src_to_md_dst,
|
src_to_md_dst=src_to_md_dst,
|
||||||
map_src_abs_to_dst_rel=map_src_abs_to_dst_rel,
|
map_src_abs_to_dst_rel=map_src_abs_to_dst_rel,
|
||||||
attachments=attachments,
|
attachments=attachments,
|
||||||
|
repo=repo,
|
||||||
)
|
)
|
||||||
src_folder = item.dst_rel.parent
|
|
||||||
folder_index_rel = _target_rel_for(
|
folder_index_rel = _target_rel_for(
|
||||||
(src_folder / "INDEX.md") if src_folder != Path(".") else Path("INDEX.md"),
|
(src_folder / "INDEX.md") if src_folder != Path(".") else Path("INDEX.md"),
|
||||||
flat_root=flat_root,
|
flat_root=flat_root,
|
||||||
)
|
)
|
||||||
folder_title = _folder_label(src_folder, folder_clean)
|
|
||||||
short = _folder_short_name(src_folder, folder_clean)
|
short = _folder_short_name(src_folder, folder_clean)
|
||||||
nav = "\n".join(
|
nav = "\n".join(
|
||||||
[
|
[
|
||||||
@@ -514,7 +565,10 @@ def build_docs_output(
|
|||||||
copy_file(abs_src, out_dir / dst_rel)
|
copy_file(abs_src, out_dir / dst_rel)
|
||||||
|
|
||||||
# Build per-folder index pages with links to articles and child folders.
|
# 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]] = {}
|
child_folders: dict[Path, set[Path]] = {}
|
||||||
folder_set = set(all_folders)
|
folder_set = set(all_folders)
|
||||||
for f in all_folders:
|
for f in all_folders:
|
||||||
@@ -562,15 +616,34 @@ def build_docs_output(
|
|||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
docs = sorted(folder_docs.get(f, []), key=lambda x: x[0].casefold())
|
docs = sorted(folder_docs.get(f, []), key=lambda x: x[0].casefold())
|
||||||
|
idx_target = index_target_by_folder[f]
|
||||||
if docs:
|
if docs:
|
||||||
lines.append("## Статьи")
|
lines.append("## Статьи")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
for title, target_rel in docs:
|
for title, target_rel in docs:
|
||||||
|
if target_rel == idx_target:
|
||||||
|
continue
|
||||||
rel_link = _rel_link(index_target_by_folder[f], target_rel)
|
rel_link = _rel_link(index_target_by_folder[f], target_rel)
|
||||||
lines.append(f"- [{title}]({rel_link})")
|
lines.append(f"- [{title}]({rel_link})")
|
||||||
lines.append("")
|
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")
|
write_text(out_dir / idx_target, "\n".join(lines).rstrip() + "\n")
|
||||||
managed_rels.add(idx_target)
|
managed_rels.add(idx_target)
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ if str(TOOL_ROOT) not in sys.path:
|
|||||||
|
|
||||||
from export1c_help import __status__, __version__ # noqa: E402
|
from export1c_help import __status__, __version__ # noqa: E402
|
||||||
from export1c_help.docs_export import ( # noqa: E402
|
from export1c_help.docs_export import ( # noqa: E402
|
||||||
|
RepoLinkContext,
|
||||||
|
_git_web_base,
|
||||||
build_docs_output,
|
build_docs_output,
|
||||||
clean_docs_wiki,
|
clean_docs_wiki,
|
||||||
collect_docs,
|
collect_docs,
|
||||||
@@ -119,6 +121,15 @@ def cmd_push(args: argparse.Namespace) -> int:
|
|||||||
src = top
|
src = top
|
||||||
slug = re.sub(r"[^\w.\-]+", "-", top.name, flags=re.UNICODE).strip("-") or "repo"
|
slug = re.sub(r"[^\w.\-]+", "-", top.name, flags=re.UNICODE).strip("-") or "repo"
|
||||||
folder = validate_folder(args.folder)
|
folder = validate_folder(args.folder)
|
||||||
|
repo_remote = git_remote_url(src if src_input is None else git_toplevel(src) or src, remote=args.remote)
|
||||||
|
repo_root = git_toplevel(src) or src
|
||||||
|
repo_ctx: RepoLinkContext | None = None
|
||||||
|
if repo_remote:
|
||||||
|
repo_ctx = RepoLinkContext(
|
||||||
|
root=repo_root.resolve(),
|
||||||
|
web_base=_git_web_base(repo_remote),
|
||||||
|
branch=args.branch,
|
||||||
|
)
|
||||||
except (ResolveError, ValueError) as exc:
|
except (ResolveError, ValueError) as exc:
|
||||||
print(f"error: {exc}", file=sys.stderr)
|
print(f"error: {exc}", file=sys.stderr)
|
||||||
return 1
|
return 1
|
||||||
@@ -150,6 +161,7 @@ def cmd_push(args: argparse.Namespace) -> int:
|
|||||||
folder=folder,
|
folder=folder,
|
||||||
flat_root=not args.no_flat_root,
|
flat_root=not args.no_flat_root,
|
||||||
clean=True,
|
clean=True,
|
||||||
|
repo=repo_ctx,
|
||||||
)
|
)
|
||||||
print(
|
print(
|
||||||
f"collect: md={build_stats.markdown_files}, attachments={build_stats.attachment_files}, "
|
f"collect: md={build_stats.markdown_files}, attachments={build_stats.attachment_files}, "
|
||||||
|
|||||||
Reference in New Issue
Block a user