Support brand.yml font weight ranges; make invalid weights a Q-14-8 error (bd-5fseopxy) - #663
Merged
Merged
Conversation
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
Contributor
✅ Snyk checks have passed. No issues have been found so far.
💻 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
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
This was referenced Sep 8, 2026
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:enumerate_weightsdidweight_name_to_number(s).unwrap_or(400)— the@importrequested only weight 400, bold fell back to synthetic bold, no diagnostic.files[].weight: 300..800): the string was written verbatim into@font-faceasfont-weight: 300..800;, which grass rejects — the whole theme silently fell back to the 7 KBDEFAULT_CSS.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 staysNameso it can be reported verbatim). NewBrand::validate()(validate.rs) checks everyweight:undertypographyand returnsBrandError::InvalidFontWeight { path, value, reason }with aBrandPathsuch astypography.fonts[0].files[1].weight. Rules: numbers in100..=900; keywords from the closed Q1 table (weight_name_to_numbernow lives here); ranges numeric only,min <= max, allowed onfonts[]entries but not on slots.quarto-sassemission: google emits thewght@400..700axis (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 for400..700, checked against fonts.bunny.net);@font-faceemitsfont-weight: 300 800. Unknown keywords and slot ranges areErr, never a fallback.quarto-sasslocation:load_split_brandvalidates after parsing and, on failure, re-parses the brand text withquarto_yaml::parse_fileand walks the path to the offending scalar. Inlinebrand:blocks are validated at extraction time while theConfigValuetree (with per-node spans) is in scope. NewSassError::InvalidBrandFontWeight { path, value, reason, location, brand_file }.quarto-core: newQ-14-8"Invalid brand font weight" intheme_diagnostic; the error'sbrand_fileis added as a bind candidate so the snippet renders from_brand.yml. The twostage_error("brand resolution: …")sites now route throughtheme_diagnostic, so brand-shape errors render withQ-14-1too.docs/errors/theme/Q-14-8.qmd, sidebar entry, and theweightsection ofdocs/guides/authoring/brand.qmd.What the user sees now
and for the valid forms the compiled CSS contains
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, defensiveErrs), grass compile of the range brand, and the location tests (path form: span keyed byfile_id_for_filename, covering exactly the scalar; inline form: theConfigValuenode's span).quarto-coretheme_diagnostic—Q-14-8renders with code/path/value/reason and a snippet from_brand.ymlwith no caller-supplied candidates; span-less form; catalog registration.quarto/tests/integration/brand_font_weight.rs— five CLI end-to-end tests through theq2binary.Notes for review
claude-notes/plans/2026-09-08-brand-font-weight-ranges.md; repro fixtures underclaude-notes/plans/brand-font-weight-ranges-investigation/.Q-14-6/Q-14-7and editstheme_diagnostic.rsandquarto-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: fileis touched only on thefont-weightline of the@font-faceblock, deliberately; the suspected deeper file-handling bugs are being planned separately.weight:is absent stay at400;700(Q1 parity); the brand.yml reference requests all nine — split off as bd-b1rrnzp1.cargo xtask verify --skip-hub-testsgreen 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 26localStorageenvironment failure (bd-lh30hlvd). The preview-renderer integration suite failed once on a stale localnode_modules(KaTeX 0.17 installed vs 0.18.4 pinned) and passes afternpm install.🤖 Generated with Claude Code
https://claude.ai/code/session_01NE1UEmUUagZwZ5APMZYfra