diff --git a/CHANGELOG.md b/CHANGELOG.md index c7be0af..7ed3f1a 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.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 ### Fixed diff --git a/VERSION b/VERSION index 5503126..2080591 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.3.10 +0.3.11 diff --git a/export1c_help/docs_export.py b/export1c_help/docs_export.py index 4b8b566..2efbe72 100644 --- a/export1c_help/docs_export.py +++ b/export1c_help/docs_export.py @@ -29,6 +29,30 @@ _EXPORT_DOCS_MARKER_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) diff --git a/export_docs.py b/export_docs.py index 2d0ae41..8061350 100644 --- a/export_docs.py +++ b/export_docs.py @@ -14,6 +14,8 @@ if str(TOOL_ROOT) not in sys.path: from export1c_help import __status__, __version__ # noqa: E402 from export1c_help.docs_export import ( # noqa: E402 + RepoLinkContext, + _git_web_base, build_docs_output, clean_docs_wiki, collect_docs, @@ -119,6 +121,15 @@ def cmd_push(args: argparse.Namespace) -> int: src = top slug = re.sub(r"[^\w.\-]+", "-", top.name, flags=re.UNICODE).strip("-") or "repo" 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: print(f"error: {exc}", file=sys.stderr) return 1 @@ -150,6 +161,7 @@ def cmd_push(args: argparse.Namespace) -> int: folder=folder, flat_root=not args.no_flat_root, clean=True, + repo=repo_ctx, ) print( f"collect: md={build_stats.markdown_files}, attachments={build_stats.attachment_files}, "