Skip to content

Support brand.yml font weight ranges; make invalid weights a Q-14-8 error (bd-5fseopxy) - #663

Merged
cscheid merged 5 commits into
mainfrom
feature/bd-5fseopxy-brand-font-weight-ranges
Sep 8, 2026
Merged

Support brand.yml font weight ranges; make invalid weights a Q-14-8 error (bd-5fseopxy)#663
cscheid merged 5 commits into
mainfrom
feature/bd-5fseopxy-brand-font-weight-ranges

Conversation

@cscheid

@cscheid cscheid commented Sep 8, 2026

Copy link
Copy Markdown
Member

Fixes bd-5fseopxy.

Problem

brand.yml allows a variable-font weight range written as a string (weight: 400..700). Quarto 2 parsed it as an unknown keyword and then:

  • google: enumerate_weights did weight_name_to_number(s).unwrap_or(400) — the @import requested only weight 400, bold fell back to synthetic bold, no diagnostic.
  • file (files[].weight: 300..800): the string was written verbatim into @font-face as font-weight: 300..800;, which grass rejects — the whole theme silently fell back to the 7 KB DEFAULT_CSS.
  • slot (headings: {weight: 500..700}): same verbatim passthrough into $headings-font-weight: 500..700 !default;, same silent fallback.

Any other unrecognised weight string (Bold, semibold, bolder) also became a silent 400. Quarto 1 fails hard here (Unknown font weight).

Change

  • quarto-brand: BrandFontWeight::Range(BrandFontWeightRange { min, max }), deserialised from exactly <digits>..<digits> (anything else stays Name so it can be reported verbatim). New Brand::validate() (validate.rs) checks every weight: under typography and returns BrandError::InvalidFontWeight { path, value, reason } with a BrandPath such as typography.fonts[0].files[1].weight. Rules: numbers in 100..=900; keywords from the closed Q1 table (weight_name_to_number now lives here); ranges numeric only, min <= max, allowed on fonts[] entries but not on slots.
  • quarto-sass emission: google emits the wght@400..700 axis (with italics: ital,wght@0,400..700;1,400..700); bunny expands a range to discrete weights (400,500,600,700 — Bunny has no range syntax and silently serves 400 for 400..700, checked against fonts.bunny.net); @font-face emits font-weight: 300 800. Unknown keywords and slot ranges are Err, never a fallback.
  • quarto-sass location: load_split_brand validates after parsing and, on failure, re-parses the brand text with quarto_yaml::parse_file and walks the path to the offending scalar. Inline brand: blocks are validated at extraction time while the ConfigValue tree (with per-node spans) is in scope. New SassError::InvalidBrandFontWeight { path, value, reason, location, brand_file }.
  • quarto-core: new Q-14-8 "Invalid brand font weight" in theme_diagnostic; the error's brand_file is added as a bind candidate so the snippet renders from _brand.yml. The two stage_error("brand resolution: …") sites now route through theme_diagnostic, so brand-shape errors render with Q-14-1 too.
  • Catalog entry, docs/errors/theme/Q-14-8.qmd, sidebar entry, and the weight section of docs/guides/authoring/brand.qmd.

What the user sees now

$ q2 render repro-slot-only
Error: [Q-14-8] Invalid brand font weight
   ╭─[ …/_brand.yml:9:13 ]
 9 │     weight: 500..700
   │             ────┬───
   │                 ╰───── `500..700` at `typography.headings.weight` is not a font weight Quarto
                            understands: a typography slot takes a single weight, not a range; …
ℹ Use a number from 100 to 900, a keyword such as `bold` or `semi-bold`, a list of those, or —
  on `typography.fonts` entries — a numeric range such as `400..700`?

and for the valid forms the compiled CSS contains

@import url("https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400..700;1,400..700&display=swap")
@font-face{font-family:"Local Var";src:url("LocalVar-VariableFont_wght.woff2");font-weight:300 800;font-style:normal}

Tests (written first, observed failing)

  • quarto-brand/tests/integration/font_weight_test.rs — 24 tests: parsing of the range variant, the keyword table, every accepted shape, and each rejection (reversed, out of range, keyword ends, half-open, unknown keyword, list index, file-entry path, slot range, every slot name).
  • quarto-sass — emission (google axis, italics, bunny expansion incl. non-round ends, file pair, keyword list regression guard, defensive Errs), grass compile of the range brand, and the location tests (path form: span keyed by file_id_for_filename, covering exactly the scalar; inline form: the ConfigValue node's span).
  • quarto-core theme_diagnosticQ-14-8 renders with code/path/value/reason and a snippet from _brand.yml with no caller-supplied candidates; span-less form; catalog registration.
  • quarto/tests/integration/brand_font_weight.rs — five CLI end-to-end tests through the q2 binary.

Notes for review

  • Design decisions and the investigation record: claude-notes/plans/2026-09-08-brand-font-weight-ranges.md; repro fixtures under claude-notes/plans/brand-font-weight-ranges-investigation/.
  • Coordination with bd-jsvetdea (theme compile errors, in flight): that work claims Q-14-6/Q-14-7 and edits theme_diagnostic.rs and quarto-sass/src/error.rs; this PR adds one arm and one variant to each. Small, but expect a trivial merge in whichever lands second.
  • source: file is touched only on the font-weight line of the @font-face block, deliberately; the suspected deeper file-handling bugs are being planned separately.
  • Default weights when weight: is absent stay at 400;700 (Q1 parity); the brand.yml reference requests all nine — split off as bd-b1rrnzp1.
  • Verification: cargo xtask verify --skip-hub-tests green through Rust build, clippy, workspace tests, ts-packages, and the hub-client WASM build. The hub-client vitest leg is skipped for the known Node 26 localStorage environment failure (bd-lh30hlvd). The preview-renderer integration suite failed once on a stale local node_modules (KaTeX 0.17 installed vs 0.18.4 pinned) and passes after npm install.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NE1UEmUUagZwZ5APMZYfra

cscheid and others added 4 commits September 8, 2026 12:34
Plan skeleton + four repro fixtures (google-only, fonts-only, slot-only,
combined). Verdict: ready to design. Findings beyond the strand: the
`source: file` per-file range also breaks the SCSS compile (not just
unsupported), Q1 has no range support either (schema enum), the brand.yml
reference implementation supports ranges on file fonts too, and Bunny
silently ignores the `N..M` form so it must be expanded to discrete
weights. Q-14-6/7 are claimed by bd-jsvetdea (room-3); any new code here
starts at Q-14-8.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NE1UEmUUagZwZ5APMZYfra
Decisions: ranges on google/bunny/file (numeric N..M only, no keyword
ends); unknown weight strings and slot ranges are hard errors; new
Q-14-8 with a real _brand.yml span via quarto-yaml path lookup +
bind_config_source; default weights stay 400;700 (bd-b1rrnzp1).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NE1UEmUUagZwZ5APMZYfra
…rror (bd-5fseopxy)

A `weight: 400..700` on a `typography.fonts[]` entry parsed as an unknown
keyword and silently became weight 400 for google fonts; on a file entry
or a typography slot it was written verbatim into SCSS/CSS, failed the
theme compile, and shipped the 7 KB DEFAULT_CSS with exit 0. Any other
unrecognised weight string (`Bold`, `semibold`, `bolder`) was also a
silent 400. Quarto 1 fails hard here.

quarto-brand: `BrandFontWeight::Range(BrandFontWeightRange)` parsed from
exactly `<digits>..<digits>`; `Brand::validate()` walks every `weight:`
under `typography` and reports the first invalid one as
`BrandError::InvalidFontWeight { path, value, reason }` with a
`BrandPath` (`typography.fonts[0].files[1].weight`). Numbers must be in
100..=900, keywords come from the closed Q1 table (now
`quarto_brand::weight_name_to_number`), ranges are numeric only with
min <= max and are allowed on fonts[] entries but not on slots.

quarto-sass: google emits the `wght@400..700` axis (italic:
`ital,wght@0,400..700;1,400..700`); bunny expands a range to discrete
weights (`400,500,600,700`) because fonts.bunny.net silently serves 400
for `400..700`; `@font-face` emits `font-weight: 300 800`. Unknown
keywords and slot ranges are `Err`, never a fallback. `load_split_brand`
validates after parsing and re-parses the brand text with quarto-yaml to
find the offending scalar's span (keyed by `file_id_for_filename`);
inline `brand:` blocks are validated at extraction time against the
ConfigValue tree. New `SassError::InvalidBrandFontWeight { path, value,
reason, location, brand_file }`. The duplicate `brand_err` in config.rs
is gone; brand_layer's is `pub(crate)` and maps the new variant.

quarto-core: `Q-14-8` "Invalid brand font weight" in theme_diagnostic,
registering the error's `brand_file` as a bind candidate so the snippet
renders from `_brand.yml`; the two `stage_error("brand resolution: …")`
sites in compile_theme_css now route through theme_diagnostic (so brand
shape errors render as Q-14-1 as well).

Catalog entry, docs/errors/theme/Q-14-8.qmd, sidebar entry, and the
`weight` section of docs/guides/authoring/brand.qmd. Tests written first
and observed failing: 24 quarto-brand parse/validate tests, quarto-sass
emission/compile/location tests, theme_diagnostic Q-14-8 tests, and five
CLI end-to-end tests through the q2 binary
(crates/quarto/tests/integration/brand_font_weight.rs).

Verified end to end: repro fixtures under
claude-notes/plans/brand-font-weight-ranges-investigation/ now emit the
axis import and `font-weight:300 800`, and the slot-range fixture fails
with Q-14-8 pointing at `_brand.yml:9:13`. `cargo xtask verify
--skip-hub-tests` green (hub vitest skipped for the Node 26 localStorage
environment issue, bd-lh30hlvd).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NE1UEmUUagZwZ5APMZYfra
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NE1UEmUUagZwZ5APMZYfra
@posit-snyk-bot

posit-snyk-bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Snyk checks have passed. No issues have been found so far.

Status Scan Engine Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

Resolves the expected overlap with the theme-compile-error work:
theme_diagnostic keeps both the Q-14-7 (InvalidScssFile) and Q-14-8
(InvalidBrandFontWeight) arms and the Q-14-6 catch-all replaces this
branch's old code-less fallback; SassError::with_location and
sass_error_location cover both new variants; the errors sidebar lists
Q-14-6, Q-14-7, Q-14-8 in code order. The brand-file bind candidate and
main's fallback_location coexist in sass_error_to_parse_error_at.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NE1UEmUUagZwZ5APMZYfra
@cscheid
cscheid merged commit 9a49ba4 into main Sep 8, 2026
10 checks passed
@cscheid
cscheid deleted the feature/bd-5fseopxy-brand-font-weight-ranges branch September 8, 2026 21:25
cscheid added a commit that referenced this pull request Sep 8, 2026
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NE1UEmUUagZwZ5APMZYfra
cscheid added a commit that referenced this pull request Sep 9, 2026
…bering

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CoQm2W4rtvPQNDtkYKdb6D
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants