diff --git a/docs/build-manual.py b/docs/build-manual.py index b383eea..c9a0224 100644 --- a/docs/build-manual.py +++ b/docs/build-manual.py @@ -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"(? 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 diff --git a/docs/fish-config.md b/docs/fish-config.md index 3d9d63c..1475582 100644 --- a/docs/fish-config.md +++ b/docs/fish-config.md @@ -3465,9 +3465,9 @@ category variable. C5 Logging and Capture — Session logs, command duration C6 Greeting & First-Run UI — Custom startup banner -Each category further sub-divides into two to six sub-categories (25 in -total) with their own `__fish_config_op__` toggles --- see that category's page for its sub-category list. +Each category further sub-divides into several sub-categories, each with +its own `__fish_config_op__` toggle -- see that +category's page for its sub-category list. ## Per-function overrides: `C0`/`always` @@ -4317,7 +4317,7 @@ Re-enable everything: set -Ue __fish_config_opinionated -Each category also has two to six sub-categories (e.g. +Each category can also have several sub-categories (e.g. `__fish_config_op_aliases_filesystem`) that can be checked, disabled, or reset the same way — `set -U __fish_config_op__ off` and `set -Ue __fish_config_op__` work identically to diff --git a/docs/manual/08-components-reference/index.md b/docs/manual/08-components-reference/index.md index a2bc942..e34ac1c 100644 --- a/docs/manual/08-components-reference/index.md +++ b/docs/manual/08-components-reference/index.md @@ -19,9 +19,9 @@ category variable. C5 [Logging and Capture](/08-components-reference/05-c5-logging-and-capture/) — Session logs, command duration C6 [Greeting & First-Run UI](/08-components-reference/06-c6-greeting-and-first-run-ui/) — Custom startup banner -Each category further sub-divides into two to six sub-categories (25 in -total) with their own `__fish_config_op__` toggles --- see that category's page for its sub-category list. +Each category further sub-divides into several sub-categories, each with +its own `__fish_config_op__` toggle -- see that +category's page for its sub-category list. ## Per-function overrides: `C0`/`always` diff --git a/docs/manual/12-troubleshooting.md b/docs/manual/12-troubleshooting.md index 5439136..3f97ed5 100644 --- a/docs/manual/12-troubleshooting.md +++ b/docs/manual/12-troubleshooting.md @@ -237,7 +237,7 @@ Re-enable everything: set -Ue __fish_config_opinionated -Each category also has two to six sub-categories (e.g. +Each category can also have several sub-categories (e.g. `__fish_config_op_aliases_filesystem`) that can be checked, disabled, or reset the same way — `set -U __fish_config_op__ off` and `set -Ue __fish_config_op__` work identically to diff --git a/docs/verify-manual.py b/docs/verify-manual.py index f89adb9..bbb140b 100644 --- a/docs/verify-manual.py +++ b/docs/verify-manual.py @@ -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