From 05ba0a4f103116818e937185353733c1b714d93b Mon Sep 17 00:00:00 2001 From: rootiest Date: Sun, 26 Jul 2026 15:55:48 -0400 Subject: [PATCH] fix(docs): render Component Reference tables as real Markdown tables _as_table() only detects tables whose data rows are indented deeper than a ":"-terminated label. The "Component Reference" tables use a different convention (header, dashed rule, rows all at the same indent), so they never matched and fell through to a plain indented code block on the Starlight site. Add _as_ruled_table() to recognize that header+rule+rows shape. It supports N columns, folds word-wrapped continuation lines into the previous row, and backtick-escapes cells containing "<" or "{" instead of rejecting the table outright (unlike _as_table's stricter guard, which those tables would otherwise trip on for angle-bracket placeholders and brace globs). Also fixes four ambiguous rows in 07-customization.md where two columns had collapsed to a single space, making them indistinguishable from a word-wrapped continuation. --- docs/build-manual.py | 49 ++++++++++++++++++++++- docs/fish-config.md | 8 ++-- docs/manual/07-customization.md | 8 ++-- docs/verify-manual.py | 69 +++++++++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 9 deletions(-) diff --git a/docs/build-manual.py b/docs/build-manual.py index d249d7e..83d1168 100644 --- a/docs/build-manual.py +++ b/docs/build-manual.py @@ -214,6 +214,12 @@ FILENAME_COMMENT_RE = re.compile(r"^#\s*(?:in\s+)?([\w-]+\.\w+)\s*$") CELL_SPLIT = re.compile(r"\s{2,}") +# A solid rule line under a header row — the "Component Reference" tables' +# authoring convention (header, dashes, data rows all at the same indent, +# no ":"-terminated label). Distinct enough from CELL_SPLIT-based prose that +# it needs its own check rather than overloading _as_table's indent rule. +RULE_RE = re.compile(r"^[─\-]{10,}$") + def _cell(text: str, code: bool) -> str: """Render one table cell. `|` must be escaped even inside a code span.""" @@ -264,6 +270,47 @@ def _as_table(para: list[str]) -> str | None: return "\n".join(out) +def _as_ruled_table(para: list[str]) -> str | None: + """Render a header + solid-rule + rows block as an N-column table, else None. + + This is the "Component Reference" tables' convention: header row, a + dashed rule, then data rows at the same indent (no ":"-label, no extra + nesting — the two things _as_table looks for). A row that splits into + just one cell is a word-wrapped continuation of the row above; anything + else that doesn't match the header's column count is a source alignment + bug, so bail out to the code-block fallback rather than guess. + """ + if len(para) < 4 or not RULE_RE.match(para[1].strip()): + return None + header = CELL_SPLIT.split(para[0].strip()) + n = len(header) + if n < 2: + return None + rows: list[list[str]] = [] + for line in para[2:]: + parts = CELL_SPLIT.split(line.strip(), n - 1) + if len(parts) == n: + rows.append(parts) + elif len(parts) == 1 and rows: + rows[-1][-1] += " " + parts[0].strip() + else: + return None + if len(rows) < 2: + return None + + # Unlike _as_table's prose column, these tables legitimately contain + # placeholders like or brace globs — code-span protects them + # instead of rejecting the whole table. + def cell(text: str, code: bool) -> str: + return _cell(text, code or "<" in text or "{" in text) + + out = [f"| {' | '.join(header)} |", "|" + "|".join(["---"] * n) + "|"] + for row in rows: + cells = [cell(row[0], True)] + [cell(c, False) for c in row[1:]] + out.append(f"| {' | '.join(cells)} |") + return "\n".join(out) + + def _render_para(para: list[str], entry_name: str | None, deeper: bool) -> str: """Render one paragraph of a former indented block. @@ -285,7 +332,7 @@ def _render_para(para: list[str], entry_name: str | None, deeper: bool) -> str: title, body = m.group(1), para[1:] info = f'fish title="{title}"' if title else "fish" return f"```{info}\n" + "\n".join(body) + "\n```" - table = _as_table(para) + table = _as_ruled_table(para) or _as_table(para) if table is not None: return table return "\n".join(INDENT + line for line in para) diff --git a/docs/fish-config.md b/docs/fish-config.md index cd4afc3..9b3e3c6 100644 --- a/docs/fish-config.md +++ b/docs/fish-config.md @@ -2613,7 +2613,7 @@ all of these commands. cd zoxide frecency-based navigation fish builtin cd rm moves files to trash (recoverable) command rm (permanent) less $PAGER → ov → less → more → cat system less - du duf (disk overview) or dust (dir tree) system du + du duf (disk overview) or dust (dir tree) system du top btop resource monitor system top ping prettyping --nolegend animation system ping ssh kitten ssh in Kitty terminal system ssh @@ -2625,8 +2625,8 @@ all of these commands. wget forced --continue (resume downloads) system wget grep/fgrep/egrep forced --color=auto system grep variants dir / vdir forced --color=auto system dir / vdir - help config intercepts "help config" → config-help fish builtin help - claude auto-links AGENTS.md as CLAUDE.md before launch command claude + help config intercepts "help config" → config-help fish builtin help + claude auto-links AGENTS.md as CLAUDE.md before launch command claude edit multi-editor launcher (GUI/term + fallbacks) $EDITOR/nvim/nano/vi When C1 is disabled, `rm` uses bare `command rm` with no wrapper — files @@ -2714,7 +2714,7 @@ __fish_config_op_integrations disables all of them. ─────────────────────────────────────────────────────────────────────────── ~60 Kitty/WezTerm abbrs Active Kitty or WezTerm session (:w, :wv, :wh, :t, etc.) - Done desktop notifications Graphical desktop with a notification daemon + Done desktop notifications Graphical desktop with a notification daemon spwin Kitty or WezTerm tab Kitty, WezTerm, or Konsole split Kitty or WezTerm diff --git a/docs/manual/07-customization.md b/docs/manual/07-customization.md index ceece81..95f7575 100644 --- a/docs/manual/07-customization.md +++ b/docs/manual/07-customization.md @@ -153,7 +153,7 @@ all of these commands. cd zoxide frecency-based navigation fish builtin cd rm moves files to trash (recoverable) command rm (permanent) less $PAGER → ov → less → more → cat system less - du duf (disk overview) or dust (dir tree) system du + du duf (disk overview) or dust (dir tree) system du top btop resource monitor system top ping prettyping --nolegend animation system ping ssh kitten ssh in Kitty terminal system ssh @@ -165,8 +165,8 @@ all of these commands. wget forced --continue (resume downloads) system wget grep/fgrep/egrep forced --color=auto system grep variants dir / vdir forced --color=auto system dir / vdir - help config intercepts "help config" → config-help fish builtin help - claude auto-links AGENTS.md as CLAUDE.md before launch command claude + help config intercepts "help config" → config-help fish builtin help + claude auto-links AGENTS.md as CLAUDE.md before launch command claude edit multi-editor launcher (GUI/term + fallbacks) $EDITOR/nvim/nano/vi When C1 is disabled, `rm` uses bare `command rm` with no wrapper — files @@ -254,7 +254,7 @@ __fish_config_op_integrations disables all of them. ─────────────────────────────────────────────────────────────────────────── ~60 Kitty/WezTerm abbrs Active Kitty or WezTerm session (:w, :wv, :wh, :t, etc.) - Done desktop notifications Graphical desktop with a notification daemon + Done desktop notifications Graphical desktop with a notification daemon spwin Kitty or WezTerm tab Kitty, WezTerm, or Konsole split Kitty or WezTerm diff --git a/docs/verify-manual.py b/docs/verify-manual.py index 7cb1133..58cde59 100644 --- a/docs/verify-manual.py +++ b/docs/verify-manual.py @@ -420,6 +420,75 @@ def test_as_table_rejects_non_tables(): assert build_manual._as_table(para) is None, f"{label} was wrongly tabled" +def test_as_ruled_table_converts_header_rule_rows(): + """A header + dashed-rule + rows block (Component Reference style) tables.""" + import build_manual + + out = build_manual._as_ruled_table( + [ + "Component Requires", + "─────────────────────────────────────────", + "spwin Kitty or WezTerm", + "hist fzf + wl-copy (Wayland clipboard)", + ] + ) + assert out is not None, "a header+rule+rows block was rejected" + assert out.splitlines()[0] == "| Component | Requires |" + assert "| `spwin` | Kitty or WezTerm |" in out + + +def test_as_ruled_table_folds_wrapped_continuations(): + """A row that splits into one cell continues the previous row's last column.""" + import build_manual + + out = build_manual._as_ruled_table( + [ + "Component What it captures", + "─────────────────────────────────────────", + "Scrollback capture Terminal output saved to:", + " ~/.terminal_history/scrollback.log", + "tmux pane capture Streamed via pipe-pane", + ] + ) + assert out is not None + assert ( + "| `Scrollback capture` | Terminal output saved to: ~/.terminal_history/scrollback.log |" + in out + ), "a wrapped continuation line did not fold into the row above" + + +def test_as_ruled_table_code_spans_angle_brackets_and_braces(): + """ / brace-glob cells get backtick-protected, not rejected.""" + import build_manual + + out = build_manual._as_ruled_table( + [ + "Component What it captures", + "─────────────────────────────────────────", + "tmux pane capture saved to tmux_-w.log", + "Autopair auto-close to (), [], {}", + ] + ) + assert out is not None, "angle brackets/braces caused the table to be rejected" + assert "`saved to tmux_-w.log`" in out + assert "`auto-close to (), [], {}`" in out + + +def test_as_ruled_table_rejects_ambiguous_columns(): + """A row with fewer delimited columns than the header is a source bug, not a guess.""" + import build_manual + + out = build_manual._as_ruled_table( + [ + "Command Active behavior Disabled fallback", + "─────────────────────────────────────────────────────────", + "ls eza -l -a --icons system ls", + "du duf (disk overview) system du", + ] + ) + assert out is None, "an under-delimited row should fall back to a code block" + + def test_prettify_leaves_reference_tables_alone(): """Column-aligned blocks are data, not shell, and must not be fenced.""" import build_manual