fix(docs): title distro code blocks and fix missed shell highlighting

Troubleshooting's "Upgrading Fish by distribution" examples carried a
leading "# Arch / AUR"-style comment instead of a Starlight fence title.
Generalize the existing filename-comment title signal to any short
label comment (excluded when it reads as a sentence, e.g. ends in
punctuation) so it titles the block and is stripped from the body.

Also fixes two cases where shell examples fell back to an unhighlighted
```text block instead of ```fish:
- `_is_shell` only recognized a hardcoded command list, so any repo-only
  command (fish-deps, config-settings, ...) was invisible to it even
  though the same vocabulary already exists for codespans wrapping.
  Now `_is_shell` reuses that one vocabulary instead of a second,
  drifting one.
- A shell paragraph with its own nested indentation (a for/if/while
  body) was routed past shell detection entirely by the `deeper` guard,
  which exists for nested option tables. Shell detection now runs
  regardless of `deeper`.
This commit is contained in:
2026-09-09 00:02:06 -04:00
parent 8f48120ba9
commit 47074821c0
2 changed files with 88 additions and 10 deletions
+51 -10
View File
@@ -334,7 +334,16 @@ def _is_prose(para: list[str]) -> bool:
def _is_shell(para: list[str], entry_name: str | None) -> bool: def _is_shell(para: list[str], entry_name: str | None) -> bool:
"""True when every line of a paragraph looks like a shell command.""" """True when every line of a paragraph looks like a shell command.
Recognised command names are `SHELL_HEADS` plus the same code
vocabulary `codespans` wraps in backticks (repo function names and
`fish-deps` catalog entries included) — one shared list instead of a
second hand-maintained one that silently drifts, which is how
`fish-deps`/`config-settings`-style custom commands used to fall
through to an unhighlighted block.
"""
vocab = SHELL_HEADS | _code_vocabulary().full
name_re = ( name_re = (
re.compile(rf"(?<![\w-]){re.escape(entry_name)}(?![\w-])") re.compile(rf"(?<![\w-]){re.escape(entry_name)}(?![\w-])")
if entry_name if entry_name
@@ -346,7 +355,7 @@ def _is_shell(para: list[str], entry_name: str | None) -> bool:
continue continue
if name_re and name_re.search(stripped): if name_re and name_re.search(stripped):
continue continue
if stripped.split()[0].lstrip("$").rstrip(";") not in SHELL_HEADS: if stripped.split()[0].lstrip("$").rstrip(";") not in vocab:
return False return False
return True return True
@@ -361,6 +370,30 @@ PATH_LINE_RE = re.compile(r"^[~$][\w./{}-]*\.\w+$")
# as a literal comment inside the code. # as a literal comment inside the code.
FILENAME_COMMENT_RE = re.compile(r"^#\s*(?:in\s+)?([$~\w./-]+\.\w+)\s*$") FILENAME_COMMENT_RE = re.compile(r"^#\s*(?:in\s+)?([$~\w./-]+\.\w+)\s*$")
# A leading comment that isn't a filename can still name what the block is
# about (e.g. "# Arch / AUR" heading a distro's install command) rather
# than explain a step ("# Turn it off:") — the trailing-punctuation and
# length checks in `_label_title` are what tell the two apart.
LABEL_COMMENT_RE = re.compile(r"^#\s*(.+)$")
def _label_title(line: str) -> str | None:
"""A short, label-shaped leading comment, promoted to a fence title.
Anything that reads as a sentence — trailing `.`/`!`/`?`/`;`/`:`/`,`,
or just long — is left as a literal comment instead: it's explaining a
step, not naming the block.
"""
m = LABEL_COMMENT_RE.match(line)
if not m:
return None
text = m.group(1).strip()
if not text or text[-1] in ".!?;:,":
return None
if len(text) > 48 or len(text.split()) > 8:
return None
return text
CELL_SPLIT = re.compile(r"\s{2,}") CELL_SPLIT = re.compile(r"\s{2,}")
# A rule line under a header row — the "Component Reference" tables' # A rule line under a header row — the "Component Reference" tables'
@@ -505,14 +538,22 @@ def _render_para(para: list[str], entry_name: str | None, deeper: bool) -> str:
path = para[0].strip() path = para[0].strip()
name = path.rsplit("/", 1)[-1] name = path.rsplit("/", 1)[-1]
return f'```fish title="{name}"\n{path}\n```' return f'```fish title="{name}"\n{path}\n```'
if _is_shell(para, entry_name): # Checked even when `deeper`: a shell paragraph's own nested indentation
body = para # (a for/if/while body) must not be mistaken for a table's alignment —
title = None # `_is_shell` only looks at each line's first word, so it stays safe to
m = FILENAME_COMMENT_RE.match(para[0].strip()) # try before falling through to the table/text fallbacks below.
if m: if _is_shell(para, entry_name):
title, body = m.group(1), para[1:] body = para
info = f'fish title="{title}"' if title else "fish" title = None
return f"```{info}\n" + "\n".join(body) + "\n```" m = FILENAME_COMMENT_RE.match(para[0].strip())
if m:
title, body = m.group(1), para[1:]
else:
label = _label_title(para[0].strip())
if label:
title, body = label, para[1:]
info = f'fish title="{title}"' if title else "fish"
return f"```{info}\n" + "\n".join(body) + "\n```"
table = _as_ruled_table(para) or _as_table(para) or _as_file_tree(para) table = _as_ruled_table(para) or _as_table(para) or _as_file_tree(para)
if table is not None: if table is not None:
return table return table
+37
View File
@@ -681,6 +681,43 @@ def test_prettify_titles_paths_and_commented_examples():
) )
def test_prettify_titles_label_comments_but_not_explanations():
"""A short leading comment titles a shell block; a sentence stays a comment."""
import build_manual
distro = "\n".join([" # Arch / AUR", " pacman -S fish"])
out = build_manual.prettify(distro)
assert '```fish title="Arch / AUR"\npacman -S fish\n```' in out, (
f"a distro label comment was not promoted to the fence title:\n{out}"
)
explanation = "\n".join(
[" # Turn it off:", " set -U __fish_config_op_logging off"]
)
out = build_manual.prettify(explanation)
assert 'title=' not in out and "# Turn it off:" in out, (
f"a sentence-shaped comment was wrongly promoted to a title:\n{out}"
)
def test_prettify_highlights_nested_shell_and_custom_commands():
"""A for-loop's indented body and a repo-only command still get shell highlighting."""
import build_manual
loop = "\n".join(
[" for v in (set -Un | string match 'x*')", " set -Ue $v", " end"]
)
out = build_manual.prettify(loop)
assert out.startswith("```fish\n") and "```text" not in out, (
f"a nested for-loop body lost shell highlighting:\n{out}"
)
out = build_manual.prettify(" fish-deps sync")
assert out == '```fish\nfish-deps sync\n```', (
f"a repo function name was not recognised as a shell command:\n{out}"
)
def test_as_aside_converts_a_single_line_label(): def test_as_aside_converts_a_single_line_label():
"""A `LABEL: text` line becomes a titled <Aside> with the label's type.""" """A `LABEL: text` line becomes a titled <Aside> with the label's type."""
import build_manual import build_manual