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.
This commit is contained in:
2026-07-26 15:55:48 -04:00
parent ab030d7ec0
commit 05ba0a4f10
4 changed files with 125 additions and 9 deletions
+48 -1
View File
@@ -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 <session> 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)
+4 -4
View File
@@ -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
+4 -4
View File
@@ -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
+69
View File
@@ -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():
"""<placeholder> / 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_<session>-w<win>.log",
"Autopair auto-close to (), [], {}",
]
)
assert out is not None, "angle brackets/braces caused the table to be rejected"
assert "`saved to tmux_<session>-w<win>.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