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,}") 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: def _cell(text: str, code: bool) -> str:
"""Render one table cell. `|` must be escaped even inside a code span.""" """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) 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: def _render_para(para: list[str], entry_name: str | None, deeper: bool) -> str:
"""Render one paragraph of a former indented block. """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:] title, body = m.group(1), para[1:]
info = f'fish title="{title}"' if title else "fish" info = f'fish title="{title}"' if title else "fish"
return f"```{info}\n" + "\n".join(body) + "\n```" 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: if table is not None:
return table return table
return "\n".join(INDENT + line for line in para) return "\n".join(INDENT + line for line in para)
+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" 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(): def test_prettify_leaves_reference_tables_alone():
"""Column-aligned blocks are data, not shell, and must not be fenced.""" """Column-aligned blocks are data, not shell, and must not be fenced."""
import build_manual import build_manual