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:
+48
-1
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user