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:
+51
-10
@@ -334,7 +334,16 @@ def _is_prose(para: list[str]) -> 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 = (
|
||||
re.compile(rf"(?<![\w-]){re.escape(entry_name)}(?![\w-])")
|
||||
if entry_name
|
||||
@@ -346,7 +355,7 @@ def _is_shell(para: list[str], entry_name: str | None) -> bool:
|
||||
continue
|
||||
if name_re and name_re.search(stripped):
|
||||
continue
|
||||
if stripped.split()[0].lstrip("$").rstrip(";") not in SHELL_HEADS:
|
||||
if stripped.split()[0].lstrip("$").rstrip(";") not in vocab:
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -361,6 +370,30 @@ PATH_LINE_RE = re.compile(r"^[~$][\w./{}-]*\.\w+$")
|
||||
# as a literal comment inside the code.
|
||||
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,}")
|
||||
|
||||
# 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()
|
||||
name = path.rsplit("/", 1)[-1]
|
||||
return f'```fish title="{name}"\n{path}\n```'
|
||||
if _is_shell(para, entry_name):
|
||||
body = para
|
||||
title = None
|
||||
m = FILENAME_COMMENT_RE.match(para[0].strip())
|
||||
if m:
|
||||
title, body = m.group(1), para[1:]
|
||||
info = f'fish title="{title}"' if title else "fish"
|
||||
return f"```{info}\n" + "\n".join(body) + "\n```"
|
||||
# Checked even when `deeper`: a shell paragraph's own nested indentation
|
||||
# (a for/if/while body) must not be mistaken for a table's alignment —
|
||||
# `_is_shell` only looks at each line's first word, so it stays safe to
|
||||
# try before falling through to the table/text fallbacks below.
|
||||
if _is_shell(para, entry_name):
|
||||
body = para
|
||||
title = None
|
||||
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)
|
||||
if table is not None:
|
||||
return table
|
||||
|
||||
@@ -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():
|
||||
"""A `LABEL: text` line becomes a titled <Aside> with the label's type."""
|
||||
import build_manual
|
||||
|
||||
Reference in New Issue
Block a user