diff --git a/.github/wiki/generate.py b/.github/wiki/generate.py index f915bec1..2db1e801 100755 --- a/.github/wiki/generate.py +++ b/.github/wiki/generate.py @@ -35,9 +35,74 @@ def sh(cmd, stdin=None): ).stdout +DEF_START = re.compile(r"^\s*\*(.+?)\*:\s*(?:\+\+)?\s*$") +INDENT = re.compile(r"^(?:\t| {2,})") +FIELD = re.compile(r"^(typeof|default)\s*:\s*(.*)$", re.IGNORECASE) + + +def normalize_option_blocks(text): + """Rewrite definition-style option blocks into scdoc table syntax. + + Some man pages document options as: + + *name*: ++ + typeof: string ++ + default: foo ++ + Description... + + which pandoc renders as a flat wall of text. Convert consecutive such + blocks into the same scdoc table syntax the table-style pages already use + (Option / Typeof / Default / Description), so every page renders as a + clean table. Table-style pages are left untouched (they never match). + Nested (indented) sub-option blocks become their own table. Fenced code + blocks are passed through verbatim. + """ + lines = text.split("\n") + out, i, n, in_code = [], 0, len(lines), False + while i < n: + if lines[i].lstrip().startswith("```"): + in_code = not in_code + out.append(lines[i]) + i += 1 + continue + # A block starts with `*name*:` and its first indented line is `typeof:`. + m = DEF_START.match(lines[i]) + nxt = lines[i + 1] if i + 1 < n else "" + if in_code or not (m and INDENT.match(nxt) + and nxt.strip().lower().startswith("typeof")): + out.append(lines[i]) + i += 1 + continue + rows = [] + while i < n and DEF_START.match(lines[i]): + name = DEF_START.match(lines[i]).group(1) + i += 1 + typ = default = "" + desc = [] + while i < n and INDENT.match(lines[i]): + cell = re.sub(r"\s*\+\+\s*$", "", lines[i].strip()) + f = FIELD.match(cell) + if f and f.group(1).lower() == "typeof": + typ = f.group(2).strip() + elif f and f.group(1).lower() == "default": + default = f.group(2).strip() + elif cell: + desc.append(cell) + i += 1 + rows.append((name, typ, default, " ".join(desc))) + if i < n and lines[i].strip() == "" and i + 1 < n and DEF_START.match(lines[i + 1]): + i += 1 # swallow blank line between two blocks, continue the run + out += ["[- *Option*", ":- *Typeof*", ":- *Default*", ":- *Description*"] + for name, typ, default, desc in rows: + out += [f"|[ *{name}*", f":[ {typ}", f":[ {default}", f":[ {desc}"] + out.append("") + return "\n".join(out) + + def man_to_gfm(scd_path): """scdoc -> roff -> pandoc gfm, as raw markdown text.""" - roff = sh(["scdoc"], stdin=open(scd_path, "rb").read()) + src = normalize_option_blocks(open(scd_path, encoding="utf-8").read()) + roff = sh(["scdoc"], stdin=src.encode("utf-8")) gfm = sh(["pandoc", "-f", "man", "-t", "gfm", "--wrap=none"], stdin=roff) return gfm.decode("utf-8") @@ -63,21 +128,52 @@ def demote(md, levels=1): return "\n".join(out) +EMPTY_ROW = re.compile(r"^\|(?:\s*\|)+\s*$") +SEP_ROW = re.compile(r"^\|[\s:\-|]+\|\s*$") + + +def fix_tables(md): + """Drop pandoc's empty leading header row so the real first row is the header. + + scdoc's `[-` header markers make pandoc emit an empty header row followed by + the actual header as the first body row. Detect `| | |` + separator and + remove them, promoting the next row to the header with the same alignment. + """ + lines = md.splitlines() + out, i = [], 0 + while i < len(lines): + if (EMPTY_ROW.match(lines[i]) and i + 2 < len(lines) + and SEP_ROW.match(lines[i + 1]) and lines[i + 2].startswith("|")): + out.append(lines[i + 2]) # real header + out.append(lines[i + 1]) # reuse the separator (keeps alignment) + i += 3 + else: + out.append(lines[i]) + i += 1 + return "\n".join(out) + + def pretty(basename): """waybar-sway-mode -> sway/mode (submodule heading for aggregated pages).""" return basename.removeprefix("waybar-").replace("-", "/", 1) +REPO = "https://github.com/Alexays/Waybar" + + def build_page(man_dir, sources, page, extras_dir): parts = [] - banner = ("") - parts.append(banner) + srclinks = ", ".join(f"[`man/{s}.5.scd`]({REPO}/blob/master/man/{s}.5.scd)" + for s in sources) + note = ("> [!NOTE]\n" + f"> This page is **auto-generated from {srclinks}** on the `master` branch.\n" + "> Do not edit it here — changes will be overwritten on the next sync.\n" + "> To update it, edit the man page(s) and open a PR.") + parts.append(note) aggregated = len(sources) > 1 for src in sources: scd = os.path.join(man_dir, src + ".5.scd") - body = strip_sections(man_to_gfm(scd)) + body = fix_tables(strip_sections(man_to_gfm(scd))) if aggregated: parts.append(f"\n# {pretty(src)}\n\n" + demote(body)) else: diff --git a/.github/workflows/wiki.yml b/.github/workflows/wiki.yml index 8875ca81..74320027 100644 --- a/.github/workflows/wiki.yml +++ b/.github/workflows/wiki.yml @@ -31,7 +31,16 @@ jobs: uses: actions/checkout@v4 - name: Install scdoc and pandoc - run: sudo apt-get update && sudo apt-get install -y scdoc pandoc + # pandoc is pinned: the distro package can be an old 2.x whose man reader + # mangles lists into blockquotes. Keep this in sync with local tooling. + env: + PANDOC_VERSION: "3.5" + run: | + sudo apt-get update && sudo apt-get install -y scdoc + curl -fsSL -o /tmp/pandoc.deb \ + "https://github.com/jgm/pandoc/releases/download/${PANDOC_VERSION}/pandoc-${PANDOC_VERSION}-1-amd64.deb" + sudo dpkg -i /tmp/pandoc.deb + pandoc --version | head -1 - name: Clone the wiki run: |