feat(docs): split fish-config.md into docs/manual SSOT tree
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (C) 2026 Rootiest
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""One-shot migration: docs/fish-config.md -> docs/manual/**.
|
||||
|
||||
Run once, verify with verify-manual.py, then delete this script.
|
||||
"""
|
||||
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import manualtools as mt
|
||||
|
||||
DOCS = Path(__file__).parent
|
||||
SRC = DOCS / "fish-config.md"
|
||||
OUT = DOCS / "manual"
|
||||
INDEX = DOCS / "fish-config.index"
|
||||
|
||||
MAN_ONLY = {"NAME", "SYNOPSIS", "TABLE OF CONTENTS"}
|
||||
LANDING = "DESCRIPTION"
|
||||
FUNCTIONS_TITLE_RE = re.compile(r"^\d+\.\s+FUNCTIONS REFERENCE$", re.I)
|
||||
NUM_PREFIX_RE = re.compile(r"^\d+(\.\d+)*\.?\s+")
|
||||
|
||||
|
||||
def slugify(title: str) -> str:
|
||||
s = NUM_PREFIX_RE.sub("", title).lower()
|
||||
s = re.sub(r"[^\w\s-]", "", s)
|
||||
return re.sub(r"[\s_]+", "-", s).strip("-")
|
||||
|
||||
|
||||
def display_title(heading: str) -> str:
|
||||
"""Strip leading numbering and normalise SHOUTING to Title Case."""
|
||||
t = NUM_PREFIX_RE.sub("", heading).strip()
|
||||
return t.title() if t.isupper() else t
|
||||
|
||||
|
||||
def load_keywords() -> dict[str, list[str]]:
|
||||
"""Reverse fish-config.index into {heading text: [keywords]}."""
|
||||
mapping: dict[str, list[str]] = {}
|
||||
if not INDEX.exists():
|
||||
return mapping
|
||||
for line in INDEX.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
keyword, heading = line.split("=", 1)
|
||||
mapping.setdefault(heading.strip().lstrip("# ").strip(), []).append(keyword.strip())
|
||||
return mapping
|
||||
|
||||
|
||||
def split_h1(text: str) -> list[tuple[str, str]]:
|
||||
parts = re.split(r"^# (.+)$", text, flags=re.MULTILINE)
|
||||
return [(parts[i].strip(), parts[i + 1].strip()) for i in range(1, len(parts), 2)]
|
||||
|
||||
|
||||
def split_h2(body: str) -> tuple[str, list[tuple[str, str]]]:
|
||||
parts = re.split(r"^## (.+)$", body, flags=re.MULTILINE)
|
||||
intro = parts[0].strip()
|
||||
subs = [(parts[i].strip(), parts[i + 1].strip()) for i in range(1, len(parts), 2)]
|
||||
return intro, subs
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not SRC.exists():
|
||||
print(f"error: {SRC} not found", file=sys.stderr)
|
||||
return 1
|
||||
if OUT.exists():
|
||||
shutil.rmtree(OUT)
|
||||
OUT.mkdir(parents=True)
|
||||
|
||||
keywords = load_keywords()
|
||||
sections = split_h1(SRC.read_text())
|
||||
order = 0
|
||||
|
||||
for heading, body in sections:
|
||||
kw = keywords.get(heading, [])
|
||||
if heading in MAN_ONLY:
|
||||
path = OUT / f"00-{slugify(heading)}.md"
|
||||
fm = {
|
||||
"title": display_title(heading),
|
||||
"manTitle": heading,
|
||||
"man": True,
|
||||
"site": False,
|
||||
}
|
||||
path.write_text(mt.serialize(fm, body))
|
||||
continue
|
||||
|
||||
if heading == LANDING:
|
||||
fm = {
|
||||
"title": "Fish Shell Configuration",
|
||||
"description": "Reference manual for the rootiest fish configuration.",
|
||||
"manTitle": heading,
|
||||
"sidebar": {"order": 0},
|
||||
}
|
||||
if kw:
|
||||
fm["helpKeywords"] = kw
|
||||
(OUT / "index.md").write_text(mt.serialize(fm, body))
|
||||
continue
|
||||
|
||||
order += 1
|
||||
if FUNCTIONS_TITLE_RE.match(heading):
|
||||
# Section 5 explodes into a directory of category files.
|
||||
d = OUT / f"{order:02d}-functions"
|
||||
d.mkdir()
|
||||
intro, subs = split_h2(body)
|
||||
fm = {
|
||||
"title": display_title(heading),
|
||||
"manTitle": heading,
|
||||
"sidebar": {"order": order},
|
||||
}
|
||||
if kw:
|
||||
fm["helpKeywords"] = kw
|
||||
(d / "index.md").write_text(mt.serialize(fm, intro))
|
||||
|
||||
for i, (sub_heading, sub_body) in enumerate(subs, start=1):
|
||||
sub_fm = {
|
||||
"title": display_title(sub_heading),
|
||||
"manTitle": sub_heading,
|
||||
"sidebar": {"order": i},
|
||||
}
|
||||
sub_kw = keywords.get(sub_heading, [])
|
||||
if sub_kw:
|
||||
sub_fm["helpKeywords"] = sub_kw
|
||||
# H3 function entries -> H2 so --site can split on them.
|
||||
promoted = mt.shift_headings(sub_body, -1)
|
||||
(d / f"{i:02d}-{slugify(sub_heading)}.md").write_text(
|
||||
mt.serialize(sub_fm, promoted)
|
||||
)
|
||||
continue
|
||||
|
||||
fm = {
|
||||
"title": display_title(heading),
|
||||
"manTitle": heading,
|
||||
"sidebar": {"order": order},
|
||||
}
|
||||
if kw:
|
||||
fm["helpKeywords"] = kw
|
||||
(OUT / f"{order:02d}-{slugify(heading)}.md").write_text(mt.serialize(fm, body))
|
||||
|
||||
count = sum(1 for _ in OUT.rglob("*.md"))
|
||||
print(f"wrote {count} files to {OUT}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user