Skip to content

Commit b903070

Browse files
committed
feature: render configuration per doc-type added
1 parent 8ec3381 commit b903070

13 files changed

Lines changed: 381 additions & 6 deletions

.github/scripts/build/apply_cached_intros.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
SCRIPTS_ROOT = SCRIPT_DIR.parent
1616
sys.path.insert(0, str(SCRIPTS_ROOT))
1717

18-
from helpers.qmd_utils import find_qmd_files # noqa: E402
18+
from helpers.qmd_utils import find_qmd_files, read_qmd_frontmatter # noqa: E402
1919
from helpers.file_updater import apply_all_updates # noqa: E402
20+
from helpers.doc_types import element_off # noqa: E402
2021

2122
ROOT_DIR = (SCRIPT_DIR / "../../..").resolve()
2223
CACHE_DIR = (ROOT_DIR / ".llm_cache").resolve()
@@ -52,6 +53,20 @@ def main() -> int:
5253
print(f"[apply_cached_intros] no .qmd files under {input_dir}")
5354
return 0
5455

56+
# Skip types that take neither intro nor keywords (dashboard) so nothing is
57+
# injected, not even into the origin_DOCS copy. They come as one bundle,
58+
# hence the "both off" check — split it if a type ever wants only one.
59+
def _wants_intro(qmd: Path) -> bool:
60+
fm, _ = read_qmd_frontmatter(qmd)
61+
dtype = fm.get("type")
62+
return not (element_off(dtype, "keywords") and element_off(dtype, "description"))
63+
64+
kept = {q for q in qmd_files if _wants_intro(q)}
65+
skipped = len(qmd_files) - len(kept)
66+
if skipped:
67+
print(f"[apply_cached_intros] skipping {skipped} file(s) whose type omits intros/keywords")
68+
qmd_files = kept
69+
5570
stats = apply_all_updates(
5671
qmd_files,
5772
CACHE_DIR,

.github/scripts/build/build-docs.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,10 @@ cp _site/sitemap.xml.bkp _site/sitemap.xml
127127
rm -f _site/sitemap.xml.bkp
128128
mv _site/llms.txt.bkp _site/llms.txt
129129

130+
# Drop .llms.md for llms-off types (dashboard), before the llm sitemap so it
131+
# falls back to the HTML URL.
132+
python3 ../.github/scripts/build/strip_llms_sidecars.py . _site
133+
130134
# Remove non-browsable links from sitemap.xml and llms.txt
131135
python3 ../.github/scripts/build/remove_non_browsable.py _site/sitemap.xml
132136
python3 ../.github/scripts/build/remove_non_browsable.py --format llms _site/llms.txt

.github/scripts/build/fill_version.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
sys.path.insert(0, str(Path(__file__).resolve().parents[1])) # .github/scripts
2323
from helpers.json_io import load_json_or_empty
24+
from helpers.doc_types import element_off
2425

2526
VERSIONS_FILE = ".llm_cache/versions.json"
2627

@@ -83,6 +84,10 @@ def main():
8384
continue
8485
_, end = bounds
8586

87+
# version-off types keep their own `version:` header, if any; don't touch.
88+
if element_off(fm_value(lines, end, "type"), "version"):
89+
continue
90+
8691
src = fm_value(lines, end, "original-filename")
8792
mj = major_from_name(src) if src else None
8893
if mj is None:

.github/scripts/build/inject_changelog.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
pf.elements.RAW_FORMATS.add("typst")
1212
pf.elements.RAW_FORMATS.add("pdf")
1313

14+
# honour `changelog: false` from doc-types.yml
15+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
16+
from helpers.doc_types import element_off
17+
1418
CHANGE_LOG_PATH = os.path.abspath(
1519
os.path.join(os.path.dirname(__file__), "../../../.llm_cache/change_logs.json")
1620
)
@@ -166,6 +170,10 @@ def get_input_file(doc):
166170

167171
def add_changelog(doc):
168172
"""Main filter function that adds changelog to the document"""
173+
# changelog-off types (dashboard) get no Change Log
174+
if element_off(doc.get_metadata("type"), "changelog"):
175+
return doc
176+
169177
current_file = get_input_file(doc)
170178
if not current_file:
171179
return doc
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env python3
2+
"""Delete the .llms.md sidecar for docs whose type has `llms` off (dashboards).
3+
4+
`llms-txt: true` drops an <name>.llms.md next to every page and has no per-doc
5+
opt-out, so we delete it after render. DOCS/<rel>.qmd -> _site/<rel>.llms.md.
6+
7+
Run before generate_llm_sitemap.py (it falls back to the HTML URL when the file
8+
is gone). Non-browsable docs already lose their llms.txt/sitemap entry via
9+
remove_non_browsable.py; this just clears the orphan file. A browsable llms-off
10+
type would also need its llms.txt entry stripped — deal with that if it happens.
11+
12+
Usage: strip_llms_sidecars.py <docs_dir> <site_dir> # e.g. . _site
13+
"""
14+
15+
from __future__ import annotations
16+
17+
import sys
18+
from pathlib import Path
19+
20+
SCRIPT_DIR = Path(__file__).resolve().parent
21+
sys.path.insert(0, str(SCRIPT_DIR.parent)) # .github/scripts
22+
23+
from helpers.doc_types import element_off # noqa: E402
24+
from helpers.qmd_utils import read_qmd_frontmatter, find_qmd_files # noqa: E402
25+
26+
SKIP_DIRS = {"_site", ".quarto", "_meta", "templates", "theme", "includes"}
27+
28+
29+
def main() -> int:
30+
if len(sys.argv) != 3:
31+
print("usage: strip_llms_sidecars.py <docs_dir> <site_dir>", file=sys.stderr)
32+
return 2
33+
docs_dir = Path(sys.argv[1]).resolve()
34+
site_dir = Path(sys.argv[2]).resolve()
35+
36+
removed = 0
37+
for qmd in find_qmd_files(docs_dir, SKIP_DIRS):
38+
fm, _ = read_qmd_frontmatter(qmd)
39+
if not element_off(fm.get("type"), "llms"):
40+
continue
41+
sidecar = site_dir / qmd.relative_to(docs_dir).with_suffix(".llms.md")
42+
if sidecar.exists():
43+
sidecar.unlink()
44+
removed += 1
45+
print(f" • removed {sidecar.relative_to(site_dir)}")
46+
47+
print(f"[strip_llms_sidecars] removed {removed} .llms.md sidecar(s)")
48+
return 0
49+
50+
51+
if __name__ == "__main__":
52+
sys.exit(main())

.github/scripts/build/strip_unknown_frontmatter.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
sys.path.insert(0, str(SCRIPTS_ROOT))
2525

2626
from helpers.qmd_utils import read_qmd_frontmatter, write_qmd_frontmatter # noqa: E402
27+
from helpers.doc_types import config_for, DEFAULT_FORMATS # noqa: E402
2728

2829

2930
ALLOWLIST = {
@@ -36,6 +37,9 @@
3637
"version",
3738
"keywords",
3839
"original-filename",
40+
# Kept (not stripped) so fill_version / inject_changelog / strip_llms can
41+
# read it downstream.
42+
"type",
3943
}
4044

4145
EXCLUDED_DIRS = {"templates", "theme", "includes", "_meta", "_site", ".quarto"}
@@ -46,17 +50,61 @@ def is_excluded(qmd_path: Path, source_root: Path) -> bool:
4650
return any(part in EXCLUDED_DIRS for part in rel_parts)
4751

4852

53+
def apply_doc_type(fm: dict) -> None:
54+
"""Apply the type config to `fm` in place.
55+
56+
Only OFF toggles do anything (ON == the default), so an untyped doc is
57+
untouched. The frontmatter toggles are done here; version/changelog/llms are
58+
handled later by fill_version / inject_changelog / strip_llms_sidecars, which
59+
read `type` themselves.
60+
"""
61+
cfg = config_for(fm.get("type"))
62+
els = cfg["elements"]
63+
64+
# OFF actions only (ON == existing default == leave frontmatter untouched).
65+
if els["toc"] is False:
66+
fm["toc"] = False
67+
if els["number-sections"] is False:
68+
fm["number-sections"] = False
69+
if els["code"] is False:
70+
fm["echo"] = False # hides code source; Quarto tags the cell .hidden
71+
if els["contact"] is False:
72+
fm["contact"] = False # honoured by filters/inject_contact_info.lua
73+
if els["keywords"] is False:
74+
fm.pop("keywords", None) # no project-level keywords default to leak now
75+
if els["description"] is False:
76+
fm.pop("description", None)
77+
78+
# style keys + code-fold off when code is off (project defaults it on, which
79+
# would leave fold triangles on hidden cells).
80+
html_opts = dict(cfg["style"])
81+
if els["code"] is False:
82+
html_opts["code-fold"] = False
83+
84+
# A doc `format:` block replaces the project's whole format list (Quarto's
85+
# one non-merging key), so only write it when we're restricting formats or
86+
# setting html options — never for the default.
87+
formats = cfg["formats"]
88+
if formats != DEFAULT_FORMATS or html_opts:
89+
fm["format"] = {
90+
f: (dict(html_opts) if f == "html" and html_opts else "default")
91+
for f in formats
92+
}
93+
94+
4995
def strip_one(qmd_path: Path) -> tuple[bool, list[str]]:
5096
"""Return (changed, dropped_field_names)."""
5197
yaml_data, lines = read_qmd_frontmatter(qmd_path)
5298
if not yaml_data or not lines:
5399
return False, []
54100

55101
dropped = sorted(k for k in yaml_data.keys() if k not in ALLOWLIST)
56-
if not dropped:
102+
cleaned = {k: v for k, v in yaml_data.items() if k in ALLOWLIST}
103+
apply_doc_type(cleaned)
104+
105+
if cleaned == yaml_data: # nothing dropped and no type deviations -> no-op
57106
return False, []
58107

59-
cleaned = {k: v for k, v in yaml_data.items() if k in ALLOWLIST}
60108
write_qmd_frontmatter(qmd_path, cleaned, lines)
61109
return True, dropped
62110

.github/scripts/doc-types.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Single source of truth for DOCS document *types*.
2+
# A document's `type:` frontmatter field selects one of these; the
3+
# build then keeps/omits elements and picks output formats accordingly.
4+
# Read via helpers/doc_types.py by:
5+
# - validate_qmd_files.py (allowed values for `type:`)
6+
# - build/strip_unknown_frontmatter.py (applies element toggles + formats)
7+
# - build/fill_version.py (skips versioning when version is off)
8+
# - build/strip_llms_sidecars.py (deletes .llms.md when llms is off)
9+
#
10+
# Add / edit types ONLY here. Fields per entry:
11+
# name (required) value authors put in `type:`; lowercase kebab-case.
12+
# elements (optional) on/off toggles; any omitted toggle defaults to TRUE (on).
13+
# Known toggles: toc, keywords, description, version, changelog,
14+
# contact, code, number-sections, llms.
15+
# formats (optional, default [html, typst, gfm]) output formats to render.
16+
# style (optional) extra keys merged into the HTML format block (e.g. page-layout, css).
17+
#
18+
# `document` is the library default and MUST exist. Because every toggle's ON
19+
# state equals the pipeline's existing default, the `document` type is a no-op:
20+
# a doc with no `type:` renders exactly as before this feature existed.
21+
types:
22+
- name: document # the library default — restates every default explicitly
23+
elements:
24+
toc: true
25+
keywords: true
26+
description: true
27+
version: true
28+
changelog: true
29+
contact: true
30+
code: true
31+
number-sections: true
32+
llms: true
33+
formats: [html, typst, gfm]
34+
35+
- name: dashboard # interactive OJS pages (e.g. CDSE migration dashboard)
36+
elements:
37+
toc: false
38+
keywords: false
39+
description: false
40+
version: false
41+
changelog: false
42+
contact: false
43+
code: false
44+
number-sections: false
45+
llms: false
46+
formats: [html]
47+
style:
48+
page-layout: full

0 commit comments

Comments
 (0)