diff --git a/crates/n0_cli/README.md b/crates/n0_cli/README.md index 4f24da62..83a25442 100644 --- a/crates/n0_cli/README.md +++ b/crates/n0_cli/README.md @@ -105,7 +105,14 @@ cargo run -p n0_cli --bin n0 -- \ `calc()`/`min()`, precedence, and fallback claims are Chromium-baked cells; the remaining absolute units are pinned by the strokes contract against the same cascade constants (`6pt ≡ 8px` measured). Its - cap, join and miter limit come from the one cascade. + cap, join and miter limit come from the one cascade. Pure fixed widths clamp + to Chromium's Web used-length ceiling (33,554,429 authored, 33,554,428 as + the resolved f32 fact). Both source spellings are Chromium-baked in one + large-user-space repair cell. An extreme pure percentage or percentage-only + `calc()` remains a named refusal: under a discriminating transform, + Chromium's accepted used value paints a butt-capped round or bevel join but + not the default miter or round/square-cap variants, so it cannot be normalized + to one universal no-stroke result. A width whose basis this cascade lacks (viewport-, container-, and font-metric-relative units, root-relative twins included), a `calc()` mixing lengths and percentages, a font-size that would poison the `em` @@ -122,14 +129,18 @@ cargo run -p n0_cli --bin n0 -- \ geometry. `none`, an all-zero list, and an invalid negative list retain Chromium's solid fallback; zero painted intervals remain meaningful under round/square caps, including on closed contours. These claims are covered by - 25 Chromium-baked cells. The named remainder is exact: `stroke-dashoffset` + 27 Chromium-baked cells. Pure fixed dash members clamp individually to the + same Web used-length ceiling before odd-list doubling. Extreme percentages + do not take that fixed ceiling: if their resolution makes the cycle + non-finite, Chromium drops the dash effect, leaving a solid stroke with the + authored cap. Byte-identical attribute/CSS cells pin the clamp, doubling, + per-contour restart, and percentage result on discriminating large geometry. + The named remainder is exact: `stroke-dashoffset` (both spellings) and `pathLength` calibration remain refused because the frame contract is zero-phase and uncalibrated; dash lengths with the same - untrustworthy basis/provenance classes as width refuse; and a list of finite - intervals whose f32 cycle sum overflows refuses before the frame boundary. - Chromium honors that last in-grammar magnitude class (measured, not celled), - so the checklist twins remain an explicit split even though the capability - slice renders the committed grammar cells. + untrustworthy basis/provenance classes as width refuse by their own registered + names. Those separate rows do not leave a standard-track dasharray grammar + remainder. The stroke's `` grammar is celled: hex and named colours, `currentColor` against the `color` hint, `none` (the initial — an invalid paint drops to it), and the full `url() [none | ]?` diff --git a/crates/websem/src/svg.rs b/crates/websem/src/svg.rs index b67b9704..b2664552 100644 --- a/crates/websem/src/svg.rs +++ b/crates/websem/src/svg.rs @@ -4407,6 +4407,43 @@ fn patrol_stroke_dasharray_units( patrol_stroke_length_units(el, element_name, "stroke-dasharray") } +/// The upper used-value bound for a non-negative Web `` consumed by +/// SVG stroke properties. +/// +/// Blink mixes CSS lengths with SVG user-unit lengths by clamping the former +/// to its fixed-point layout range: `INT_MAX / 64 - 2`, or 33,554,429. That +/// integer rounds to 33,554,428 when represented by the `f32` facts this +/// compiler and frame contract carry. +const WEB_USED_LENGTH_MAX: f32 = (i32::MAX / 64 - 2) as f32; + +fn clamp_web_used_length(length: Length) -> f32 { + let px = length.px(); + if px > WEB_USED_LENGTH_MAX { + WEB_USED_LENGTH_MAX + } else { + px + } +} + +/// Resolve a pure percentage stroke width with Blink's float operation order. +/// +/// Stylo stores `N%` as the fraction `N / 100`. Blink's SVG length path keeps +/// `N`, multiplies that by the viewport dimension, and only then divides by +/// 100. The intermediate multiplication is observable at the top of the f32 +/// range: overflow saturates to `f32::MAX` rather than recovering the finite +/// mathematical product. The boolean names that saturation so the caller can +/// refuse the cap- and join-dependent renderer result without hiding an +/// unrelated stroke construction error. +fn resolve_web_percentage_length(percentage: f32, basis: f32) -> (f32, bool) { + let authored_percentage = percentage * 100.0; + let resolved = basis * authored_percentage / 100.0; + if resolved == f32::INFINITY { + (f32::MAX, true) + } else { + (resolved, false) + } +} + /// Resolve the SVG stroke from the one cascade, as typed values — the same /// ingress discipline as [`resolve_fill`], so presentation attributes, /// stylesheet rules, inheritance through containers, unit-bearing lengths @@ -4506,7 +4543,7 @@ fn resolve_stroke( // the same basis chain the shape geometry percentages refuse on. let destination_data = el.borrow_data().ok_or(CompileError::MissingComputedStyle)?; let destination_style: &ComputedValues = destination_data.styles.primary(); - let width = match destination_style.clone_stroke_width() { + let (width, percentage_width_saturated) = match destination_style.clone_stroke_width() { SVGLength::ContextValue => { return Err(CompileError::UnsupportedStroke( "stroke-width: context-value".to_string(), @@ -4517,14 +4554,14 @@ fn resolve_stroke( // The unit is gone from a computed length, so the authored text // is what says whether its basis was one this build has. patrol_stroke_width_units(el, element_name)?; - length.px() + (clamp_web_used_length(length), false) } // A pure percentage resolves against the viewport's normalized // diagonal (SVG2 §7.10; measured — `10%` of 64x64 paints 6.4 // units). A calc() mixing lengths and percentages has neither a // computed length nor a pure percentage and stays refused. None => match width.0.to_percentage() { - Some(percentage) => percentage.0 * bases.diagonal(), + Some(percentage) => resolve_web_percentage_length(percentage.0, bases.diagonal()), None => { return Err(CompileError::UnsupportedStroke( "a calc() stroke-width mixing lengths and percentages is not consumed" @@ -4534,6 +4571,20 @@ fn resolve_stroke( }, }, }; + if percentage_width_saturated { + // Chromium's result after this saturation is cap- and join-dependent: + // a butt-capped round/bevel stroke paints while the default miter and + // round/square caps paint nothing. This frame/consumer path does not + // admit that renderer-level branch across the full cap/join grammar + // (the default miter's conservative reach is not representable). + // Refuse the typed arithmetic event rather than normalizing it to an + // absence that silently erases the painted cases, and do not catch + // unrelated reach errors here. + return Err(CompileError::UnsupportedStroke( + "stroke-width percentage saturation has cap- and join-dependent paint semantics" + .to_string(), + )); + } if width == 0.0 { return Ok(None); } @@ -4554,20 +4605,31 @@ fn resolve_stroke( SVGStrokeDashArray::Values(values) => { let mut intervals = Vec::with_capacity(values.len() * 2); for value in values.iter() { - intervals.push(value.0.resolve(Length::new(bases.diagonal())).px()); + // Blink applies the Web used-length ceiling to a pure resolved + // `` (including an SVG number and a calc() simplified + // to one length), but not to a percentage-bearing value. Keep + // that typed distinction through resolution: clamping the + // resolved result wholesale would change percentage cycles. + let interval = match value.0.to_length() { + Some(length) => clamp_web_used_length(length), + None => value.0.resolve(Length::new(bases.diagonal())).px(), + }; + intervals.push(interval); } if !intervals.len().is_multiple_of(2) { intervals.extend_from_within(..); } match StrokeDashIntervals::new(intervals) { Ok(intervals) => intervals, - Err(StrokeDashIntervalsError::UnrepresentableCycleLength) => { - return Err(CompileError::UnsupportedStroke( - "a stroke-dasharray cycle has a finite authored grammar but its \ - resolved total is not representable by this frame contract" - .to_string(), - )); - } + // Chromium retains the declaration but drops the dash path + // effect when percentage resolution produces a non-finite + // member or repeated-cycle sum. Dash absence is therefore a + // solid stroke with the authored cap, not a refusal and not an + // invisible stroke. + Err( + StrokeDashIntervalsError::NonFiniteInterval { .. } + | StrokeDashIntervalsError::UnrepresentableCycleLength, + ) => None, Err(error) => { return Err(CompileError::UnsupportedStroke(format!( "stroke-dasharray did not resolve to one checked cycle: {error}" diff --git a/crates/websem/tests/strokes_contract.rs b/crates/websem/tests/strokes_contract.rs index c10814da..ce28d2a5 100644 --- a/crates/websem/tests/strokes_contract.rs +++ b/crates/websem/tests/strokes_contract.rs @@ -10,7 +10,7 @@ //! resolved facts plus the refusals that keep the unconsumed half honest. //! //! Every pixel claim here was measured in Chromium 149 first; the corpus bakes -//! them (`fixtures/web-first/svg-stroke-*.svg`, 69 of 70 byte-exact — only +//! them (`fixtures/web-first/svg-stroke-*.svg`, 97 of 98 byte-exact — only //! `svg-stroke-path-closed` carries the declared conic tolerance). // This binary consumes only the n0 render half of the shared plumbing. @@ -165,6 +165,124 @@ fn the_stroke_width_is_a_cascaded_length() { assert_eq!(stroke_of(&ruled, 0).width(), 8.0); } +/// Blink clamps pure resolved Web lengths to its fixed-point layout ceiling +/// before stroke construction. The integer ceiling (33,554,429) rounds to +/// 33,554,428 in the frame's f32 vocabulary; both authored spellings must +/// produce that exact fact instead of reaching the stroke-reach refusal. +#[test] +fn a_huge_pure_stroke_width_clamps_to_the_web_used_length_ceiling() { + for extra in [ + r##"stroke-width="3.4e38""##, + r##"style="stroke-width: 3.4e38px""##, + ] { + let frame = admit_both(&stroked_rect(extra)); + assert_eq!( + stroke_of(&frame, 0).width(), + 33_554_428.0, + "extra={extra:?}" + ); + } +} + +/// Percentages take a different used-value path from fixed lengths. Blink +/// multiplies the authored percentage by the viewport basis before dividing by +/// 100; overflow saturates to f32::MAX. On the discriminating line probe, +/// Chromium then paints a butt-capped round/bevel stroke but drops the default +/// miter and round/square caps. That cap/join-dependent renderer result is not +/// an admitted percentage-width capability, so every cascade ingress refuses +/// the typed saturation event instead of silently normalizing it to no stroke. +#[test] +fn an_overflowing_percentage_stroke_width_refuses_by_name() { + let mut sources = vec![ + ( + "presentation attribute", + stroked_rect(r##"stroke-width="3.4e38%""##), + ), + ( + "style attribute", + stroked_rect(r##"style="stroke-width: 3.4e38%""##), + ), + ( + "calc presentation attribute", + stroked_rect(r##"stroke-width="calc(3.4e38%)""##), + ), + ( + "calc style attribute", + stroked_rect(r##"style="stroke-width: calc(3.4e38%)""##), + ), + ( + "stylesheet", + document( + r##" + "##, + ), + ), + ( + "inheritance", + document( + r##" + + "##, + ), + ), + ]; + + for (name, extra) in [ + ("default miter and butt cap", ""), + ("round join and butt cap", r##"stroke-linejoin="round""##), + ("bevel join and butt cap", r##"stroke-linejoin="bevel""##), + ( + "miter-one and butt cap", + r##"stroke-linejoin="miter" stroke-miterlimit="1""##, + ), + ( + "round join and round cap", + r##"stroke-linejoin="round" stroke-linecap="round""##, + ), + ( + "round join and square cap", + r##"stroke-linejoin="round" stroke-linecap="square""##, + ), + ] { + sources.push(( + name, + stroked_rect(&format!(r##"stroke-width="3.4e38%" {extra}"##)), + )); + } + + for (name, source) in sources { + let error = refusal(&source); + assert!( + matches!(error, CompileError::UnsupportedStroke(ref reason) + if reason.contains("stroke-width percentage saturation") + && reason.contains("cap- and join-dependent")), + "{name}: got {error}" + ); + } + + // A large percentage whose intermediate product is still finite remains a + // stroke. 1e9% of the normalized 64x64 diagonal is 640,000,000 — well + // beyond the fixed-length ceiling, proving the refusal is the percentage + // operation's saturation event rather than a coarse magnitude patrol. + let finite = admit_both(&stroked_rect(r##"stroke-width="1e9%""##)); + assert_eq!(stroke_of(&finite, 0).width(), 640_000_000.0); + + // The saturation flag belongs to percentage resolution, not to every + // construction failure involving a percentage. A finite 100% width under + // an enormous miter limit still refuses its unrepresentable reach. + let error = refusal(&stroked_rect( + r##"stroke-width="100%" stroke-miterlimit="3.4e38""##, + )); + assert!( + matches!(error, CompileError::UnsupportedStroke(ref reason) if reason.contains("representable")), + "got {error}" + ); + assert!( + !error.to_string().contains("percentage saturation"), + "the miter reach is a distinct refusal: {error}" + ); +} + /// A stroke that would paint nothing is `None`, not an empty stroke — so no /// consumer has to re-derive "is this visible". #[test] @@ -548,6 +666,40 @@ fn a_dash_array_resolves_to_one_even_local_space_cycle() { } } +/// The same pure-length used-value ceiling applies member-by-member before an +/// odd dash list is doubled. SVG unitless numbers, CSS lengths, and a calc() +/// simplified to a pure length therefore cross the frame boundary as the same +/// exact finite interval. +#[test] +fn huge_pure_dash_lengths_clamp_before_the_cycle_is_formed() { + for extra in [ + r##"stroke-dasharray="3.4e38 3.4e38""##, + r##"style="stroke-dasharray: 3.4e38px 3.4e38px""##, + r##"style="stroke-dasharray: calc(3.4e38px) calc(3.4e38px)""##, + ] { + let frame = admit_both(&stroked_rect(&format!(r##"stroke-width="8" {extra}"##))); + assert_eq!( + stroke_of(&frame, 0) + .dash_intervals() + .expect("active finite cycle") + .as_slice(), + [33_554_428.0, 33_554_428.0], + "extra={extra:?}" + ); + } + + let odd = admit_both(&stroked_rect( + r##"stroke-width="8" stroke-dasharray="3.4e38 0 0""##, + )); + assert_eq!( + stroke_of(&odd, 0) + .dash_intervals() + .expect("active doubled cycle") + .as_slice(), + [33_554_428.0, 0.0, 0.0, 33_554_428.0, 0.0, 0.0] + ); +} + /// The cascade owns precedence and inheritance for the property: a rule beats /// its presentation hint, an inline declaration beats the rule, an invalid CSS /// declaration exposes the valid hint, and a container passes the computed @@ -656,21 +808,49 @@ fn neutral_and_zero_painted_dash_cycles_keep_their_measured_meaning() { ); } -/// Chromium accepts individually finite intervals whose repeated f32 cycle -/// overflows, but this frame boundary cannot state their period. The gap is a -/// named refusal rather than a backend-rejected path effect that erases paint. +/// Percentages do not take the pure-length ceiling. When resolving them makes +/// the repeated f32 cycle non-finite, Chromium drops the dash path effect: the +/// result is a solid stroke, with the authored cap still intact. Attribute and +/// CSS declarations produce the same normalized dash absence. #[test] -fn an_unrepresentable_dash_cycle_refuses_before_the_frame_boundary() { +fn a_nonfinite_percentage_dash_cycle_normalizes_to_solid() { for extra in [ - r##"stroke-dasharray="3.4e38 3.4e38""##, - r##"style="stroke-dasharray: 3.4e38 3.4e38""##, + r##"stroke-dasharray="3.4e38% 3.4e38%""##, + r##"style="stroke-dasharray: 3.4e38% 3.4e38%""##, ] { - let error = refusal(&stroked_rect(&format!(r##"stroke-width="8" {extra}"##))); + let frame = admit_both(&stroked_rect(&format!(r##"stroke-width="8" {extra}"##))); assert!( - matches!(error, CompileError::UnsupportedStroke(ref reason) if reason.contains("resolved total is not representable")), - "{extra}: got {error}" + stroke_of(&frame, 0).dash_intervals().is_none(), + "extra={extra:?}" ); } + + for extra in [ + r##"stroke-linecap="round" stroke-dasharray="0 3.4e38% 0 3.4e38%""##, + r##"style="stroke-linecap: round; stroke-dasharray: 0 3.4e38% 0 3.4e38%""##, + ] { + let frame = admit_both(&stroked_rect(&format!(r##"stroke-width="8" {extra}"##))); + let stroke = stroke_of(&frame, 0); + assert_eq!(stroke.cap(), StrokeCap::Round, "extra={extra:?}"); + assert!( + stroke.dash_intervals().is_none(), + "zero-first overflow is solid, not an initial cap dot: extra={extra:?}" + ); + } + + // A negative CSS member remains invalid grammar. It drops at the cascade + // and exposes the valid presentation hint; overflow normalization must not + // turn invalid declarations into an accepted solid value. + let fallback = admit_both(&stroked_rect( + r##"stroke-width="8" stroke-dasharray="8 4" style="stroke-dasharray: -1 2""##, + )); + assert_eq!( + stroke_of(&fallback, 0) + .dash_intervals() + .expect("valid presentation fallback") + .as_slice(), + [8.0, 4.0] + ); } /// A percentage `stroke-width` resolves against the viewport's normalized diff --git a/crates/websem/tests/unsupported_corpus.rs b/crates/websem/tests/unsupported_corpus.rs index ebcf88f1..d2018d93 100644 --- a/crates/websem/tests/unsupported_corpus.rs +++ b/crates/websem/tests/unsupported_corpus.rs @@ -174,11 +174,6 @@ const CORPUS: &[(&str, Departure, &str)] = &[ DeclaredByBestEffort, "animation element ", ), - ( - "svg-stroke-dasharray-cycle-overflow", - DeclaredByBestEffort, - "resolved total is not representable", - ), ( "svg-stroke-dasharray-escape", DeclaredByBestEffort, @@ -225,6 +220,11 @@ const CORPUS: &[(&str, Departure, &str)] = &[ DeclaredByBestEffort, "font-size", ), + ( + "svg-stroke-width-percentage-overflow", + DeclaredByBestEffort, + "stroke-width percentage saturation", + ), ("svg-stroke-width-var", DeclaredByBestEffort, "var()"), ("svg-switch", DeclaredByBestEffort, ""), // The text rung's residue: `` itself graduated (its fixtures are diff --git a/docs/wg/consolidation/svg-engine-of-record.md b/docs/wg/consolidation/svg-engine-of-record.md index a7148d95..04104c28 100644 --- a/docs/wg/consolidation/svg-engine-of-record.md +++ b/docs/wg/consolidation/svg-engine-of-record.md @@ -56,7 +56,7 @@ from the dated addenda below: the full `preserveAspectRatio` grammar; and one exact-time `` on a top-level ``. `crates/n0_cli/README.md` is the statement of record. -- **The corpus** is 277 Chromium-baked primitive cells plus 10 sampled frames. +- **The corpus** is 280 Chromium-baked primitive cells plus 10 sampled frames. All byte-exact except six curved cells carrying a declared, geometrically confined tolerance (the weighted rational conic) and three gradient cells carrying a declared one-code-value ramp-quantization tolerance (one pixel @@ -1410,6 +1410,11 @@ widen this contract. ### Dasharray rung verdict — cells landed, rows split +This subsection records the gridaco/nothing#83 verdict as it landed. Its +cycle-sum premise is superseded by the 2026-08-14 ratified correction below: +the apparent authored-magnitude remainder was Chromium's fixed-length used +clamp, not an unrepresentable resolved-frame fact. + The amendment is exercised by 25 Chromium-149-baked cells. Together they cover the attribute and CSS spellings; number, length, percentage, exponent, comma and whitespace forms; CSS math and an authored font-size basis; odd-list @@ -1532,7 +1537,9 @@ The standard-track boundary stays sharp. Chromium drops a fallback following after a URL; the pinned source parser accepts the extension, so one new named registered refusal and guards cover attribute, inline-style, and stylesheet ingresses. The former load-bearing context-paint refusal graduates, leaving the -register at 56 rows. Context-selected patterns and external paint resources +register at 56 rows at that rung (and still 56 at tip after the replacement +below). +Context-selected patterns and external paint resources were also measured to propagate in Chromium, but remain refused by their own resource names. Marker context and author stylesheets across a use-shadow boundary likewise retain their own rows. Under the own-row precedent of @@ -1543,3 +1550,58 @@ is outside their grammar bar. The CSS SVG-presentation `fill` and `stroke` rows and their SVG presentation-attribute twins therefore tick together. This is a capability verdict only. It produces no conformance score and takes no FLIP action. + +## Ratified correction — stroke used-value range (2026-08-14) + +The browser's used-value range, not the resolved render contract, closes the +dasharray remainder recorded by gridaco/nothing#83. Chromium clamps each pure +fixed stroke length before using it. The authored ceiling is 33,554,429 +in its fixed-point CSS length range, represented in the resolved scalar +vocabulary as 33,554,428. For a dash list, that clamp happens member by member +before an odd list is repeated. The existing contract's finite, non-negative, +positive-sum cycle therefore already states the browser's resolved fact; no +contract or consumer amendment is warranted. + +Percentages follow a distinct used-value rule. A moderate resolved percentage +may exceed the fixed-length ceiling and remains unclamped. At the extreme, an +overflow during percentage resolution causes Chromium to drop the dash effect, +so the stroke becomes solid while retaining its authored cap. The same extreme +percentage used as stroke width is not governed by that dash-effect result: +under a discriminating transform, Chromium paints a butt-capped round or bevel +join but not the default miter or round/square-cap variants. It remains a named +capability instead of being normalized to one universal no-stroke result. +These are property-specific outcomes, not a shared normalization of every +large length. + +The earlier short-path probe did not discriminate authored `3.4e38` from the +browser's used clamp: either interval stayed longer than the entire path, so +both painted the same apparent solid or initial cap dot. The corrected matrix +put the transition inside large user-space geometry and located the fixed +ceiling directly: 33,000,000 differs, while 34,000,000, 40,000,000, and +`3.4e38` converge. It separately measured percentages above that ceiling and +the extreme percentage outcomes for dasharray and width. + +Two byte-identical Chromium cells now cover the presentation-attribute and CSS +dasharray spellings. Together they discriminate the fixed used clamp, odd-list +repetition, restart at each subpath, and the cap-preserving solid result for an +extreme percentage. A third cell repairs the fixed-length evidence of the +previously closed width family by covering the clamp in both spellings. The +extreme percentage width class is measured, not celled, and its +presentation-attribute, CSS, and +percentage-only arithmetic ingresses are guarded by one named refusal. +The corpus moves from 277 to 280 cells; 97 of its 98 `svg-stroke-*` cells are +byte-exact, with only the previously declared closed-path tolerance. The +graduated cycle-overflow refusal leaves as the percentage-overflow width refusal +enters, so the register remains at 56 rows. Dashoffset, `pathLength`, unit-basis, +variable-indirection, escape, and font-basis patrols remain unchanged. + +The earned precedents apply without revision: gridaco/nothing#77 supplies the +standard-track grammar bar, gridaco/nothing#80 assigns independent unit and +spelling gaps to their own guarded rows, and gridaco/nothing#81 remains the rule +for a genuine measured split. Here the corrected evidence removes that split's +premise for dasharray, so both `stroke-dasharray` checklist rows tick. It also +corrects gridaco/nothing#80's width evidence: the valid extreme-percentage +class has no independent row and Chromium paints some of it, so both +`stroke-width` rows reopen under gridaco/nothing#81 until that named capability +lands. This is a capability verdict only; it produces no conformance score and +takes no FLIP action. diff --git a/docs/wg/consolidation/web-checklist.md b/docs/wg/consolidation/web-checklist.md index 153d2e35..cefd13cf 100644 --- a/docs/wg/consolidation/web-checklist.md +++ b/docs/wg/consolidation/web-checklist.md @@ -964,9 +964,9 @@ excluded. - [x] `fill-opacity` - [x] `fill-rule` - [x] `stroke` -- [x] `stroke-width` +- [ ] `stroke-width` - [x] `stroke-opacity` -- [ ] `stroke-dasharray` +- [x] `stroke-dasharray` - [ ] `stroke-dashoffset` - [x] `stroke-linecap` - [x] `stroke-linejoin` @@ -1472,13 +1472,13 @@ for attributes the platform ships ahead of the SVG 2 indexes. - [ ] `stop-color` - [ ] `stop-opacity` - [x] `stroke` -- [ ] `stroke-dasharray` +- [x] `stroke-dasharray` - [ ] `stroke-dashoffset` - [x] `stroke-linecap` - [x] `stroke-linejoin` - [x] `stroke-miterlimit` - [x] `stroke-opacity` -- [x] `stroke-width` +- [ ] `stroke-width` - [ ] `text-anchor` - [ ] `text-decoration` - [ ] `text-overflow` diff --git a/fixtures/web-first/README.md b/fixtures/web-first/README.md index b4a86195..c756c8f3 100644 --- a/fixtures/web-first/README.md +++ b/fixtures/web-first/README.md @@ -49,13 +49,16 @@ is exactly what the engine renders pixel-for-pixel. | `svg-stroke-width-em-font-size.svg` | The em basis is the *authored* font-size, not just the default: `font-size: 8px` under `stroke-width="1em"` bakes the same 8-wide stroke as `svg-stroke-rect-centred` (shared hash) — a render still reading the default 16px basis paints double and fails the raster. A font-size this cascade cannot vouch for (`2vw`, `var()`, an escape) poisons that basis and refuses by name instead of painting the measured ~20× error. | | `svg-stroke-width-css.svg` · `svg-stroke-width-css-unitless.svg` · `svg-stroke-width-css-calc.svg` · `svg-stroke-width-css-min.svg` · `svg-stroke-width-css-percent.svg` | The width's CSS twin: `8px`, the unitless `8` (the SVG quirk the cascade parses in CSS spelling too), `calc(4px + 4px)`, and `min(8px, 12px)` all share the attribute cell's exact hash, and `stroke-width:10%` shares `svg-percent-stroke-width`'s — one normalized-diagonal basis, both spellings. A percentage *inside* the math (`calc(10% + 0.8px)`, `min(10%, 12px)`) resolves in Chromium (measured: ≡ authored `7.2` / `6.4`) and refuses here by name — the register's `svg-stroke-width-calc-mixed`. | | `svg-stroke-width-css-over-attr.svg` · `svg-stroke-width-css-invalid-falls-back.svg` | The precedence pair: an author `stroke-width:8px` beats the `2` presentation attribute, and an invalid negative declaration ceases to exist at parse so the `8` attribute survives. Alone, the same negative declaration leaves the initial 1, and `context-value` — no part of the standard-track grammar — drops the same way (both measured, neither celled). | +| `svg-stroke-width-used-range.svg` | The width-side repair for the shared Web used-value range: a huge fixed value in either the attribute or CSS spelling clamps to Chromium's fixed-length ceiling. The discriminating large-user-space cell is byte-exact. The probe fixed the authored ceiling at `INT_MAX / 64 - 2` = 33,554,429, represented by the resolved f32 fact as 33,554,428; moderate percentages above that fixed ceiling remain unclamped (measured, not separately celled). | +| *(measured, not celled)* | An extreme pure percentage or percentage-only `calc()` takes a different width path: Chromium accepts it, but overflow during used-value multiplication produces join- and cap-dependent paint — under a discriminating transform, a butt-capped round or bevel join paints, while the default miter and round/square-cap variants do not. The named `svg-stroke-width-percentage-overflow` refusal guards that valid grammar class. It has no independent checklist row, so this corrects gridaco/nothing#80's evidence and reopens both `stroke-width` twins under the gridaco/nothing#81 split law; the fixed huge-length cell remains admitted. | | `svg-stroke-dasharray.svg` · `svg-stroke-dasharray-css.svg` · `svg-stroke-dasharray-comma.svg` · `svg-stroke-dasharray-odd.svg` · `svg-stroke-dasharray-exponent.svg` | The dash list's two source spellings enter one typed cascade value: numbers and px lengths, whitespace/comma separators, exponent notation, and odd-list repetition all bake. The CSS and attribute base cells are byte-identical; the odd cell forces producer-side doubling before the even resolved cycle crosses rframe. | | `svg-stroke-dasharray-percent.svg` · `svg-stroke-dasharray-viewbox-percent.svg` · `svg-stroke-dasharray-calc.svg` · `svg-stroke-dasharray-css-math.svg` · `svg-stroke-dasharray-em-font-size.svg` | The admitted length-percentage grammar: percentages resolve against the viewport's normalized diagonal in user units, including through a scaling `viewBox`; the percent cell uses a 64×32 user space specifically so width, height, and normalized-diagonal bases cannot share its oracle. Mixed calc resolves on that same basis, CSS `min()`/`max()` reach the resolved list, and `em` uses the authored font-size. `clamp()` was also Chromium-probed successfully (measured, not celled). | | `svg-stroke-dasharray-inherited.svg` · `svg-stroke-dasharray-use-inherited.svg` · `svg-stroke-dasharray-css-over-attr.svg` · `svg-stroke-dasharray-css-invalid-falls-back.svg` | Cascade and instance semantics: a group and a use site pass their computed cycle to the descendant/clone; an author declaration beats the presentation hint; an invalid declaration ceases to exist so the valid hint survives. The ordinary inherited value plus CSS-wide `initial` and `unset` were separately Chromium-probed with their standard cascade meanings (measured, not celled). | | `svg-stroke-dasharray-none.svg` · `svg-stroke-dasharray-all-zero.svg` · `svg-stroke-dasharray-negative.svg` · `svg-stroke-dasharray-zero-round.svg` | The neutral/zero edges discriminate: `none`, an all-zero list, and a negative invalid list are solid strokes, while round caps turn `0 8`'s zero-length painted entries into dots. The same cycle under butt paints nothing (measured, not celled). The checked contract normalizes only the all-zero cycle to absence and preserves cap-bearing zeros. | | `svg-stroke-dasharray-closed-ellipse-round.svg` · `svg-stroke-dasharray-closed-path-square.svg` · `svg-stroke-dasharray-mixed-contours-round.svg` · `svg-stroke-dasharray-subpath-restart.svg` · `svg-stroke-dasharray-scaled-group.svg` · `svg-stroke-dasharray-geometries.svg` | Consumer and applicability cells: dashed closed contours retain authored round/square caps; a mixed open/closed path is no longer caught by the solid-only cap patrol; each subpath restarts at phase zero; intervals stay in local space under transforms; and every admitted geometry kind receives the same cycle. All are byte-exact. | | `svg-stroke-dasharray-backend-saturation.svg` | The pinned painter's dense-cycle realization is Chromium's: a `0.00001 0.00001` cycle exceeds Skia's bounded dash expansion, with route- and cap-shaped saturation across lines, a cubic, a rect, and an ellipse. One cell combines a disappearing butt line, saturated solid closed/curved routes, and a solid round-capped line; its Chromium oracle differs from an all-solid control by 96 pixels and from an all-absent control by 891, so neither coarse fallback can pass. This is measured renderer behavior, not a source normalization or a resolved-contract limit; any backend drift must fail the byte-exact cell. | -| *(measured, not celled)* | `stroke-dashoffset` shifts the cycle for positive/negative unitless and px values plus normalized-diagonal percentages. `pathLength` calibrates dash values on path, rect, circle, and ellipse, and calibrates dash offset on path. Both remain named guarded refusals because this frame amendment is zero-phase and uncalibrated. Viewport/container/font-metric/root-font-metric unit classes, `var()` indirection, CSS escapes, and a poisoned em basis also remain registered refusals following the stroke-width/own-unit-row precedent. One additional in-grammar remainder is engine-refused: individually finite intervals whose f32 cycle sum overflows (`3.4e38 3.4e38`; the zero-painted round-cap variant proves the class cannot be normalized to solid). The cells land, but the attribute and CSS checklist twins stay unchecked as an explicit gridaco/nothing#81-style SPLIT until that representability rung closes. | +| `svg-stroke-dasharray-used-clamp-attr.svg` · `svg-stroke-dasharray-used-clamp-css.svg` | The former cycle-overflow remainder was a used-value question, not a wider frame fact. In both spellings, pure fixed members clamp individually to 33,554,429 (33,554,428 as f32) before odd-list doubling; the two-contour specimen restarts the doubled cycle. Extreme percentage members take a different path: when their resolution overflows, Chromium drops the dash effect and preserves the authored round cap on the resulting solid stroke. The two cells are byte-identical and discriminate the fixed clamp on geometry long enough to cross it; the old short-path probe could not distinguish a raw huge interval from this browser used clamp. | +| *(measured, not celled)* | `stroke-dashoffset` shifts the cycle for positive/negative unitless and px values plus normalized-diagonal percentages. `pathLength` calibrates dash values on path, rect, circle, and ellipse, and calibrates dash offset on path. Both remain named guarded refusals because this frame amendment is zero-phase and uncalibrated. Viewport/container/font-metric/root-font-metric unit classes, `var()` indirection, CSS escapes, and a poisoned em basis also remain registered refusals. Under the earned gridaco/nothing#77 and gridaco/nothing#80 standard-track and guarded-gap precedents, these do not hold `stroke-dasharray` open. The gridaco/nothing#81 split precedent remains valid, but its premise is absent here: the measured used-value behavior now has committed twin cells, so both dasharray rows tick. | | `svg-stroke-paint-hex.svg` · `svg-stroke-paint-named.svg` · `svg-stroke-paint-css.svg` · `svg-stroke-paint-currentcolor.svg` | One stroke paint, four spellings: `#4682b4`, its name `steelblue`, the CSS declaration, and `currentColor` against the element's `color` hint — four cells, one shared oracle hash. The CSS-spelled name, an ancestor's `color` through the cascade, keyword case-insensitivity, `transparent`, and a color through `var()` (colors carry no device basis) all measure identical too (measured, not celled). The CSS-wide keywords split by their own semantics, engine and Chromium agreeing on each (measured, not celled): `inherit` takes the ancestor's paint, `initial` and `revert` land on `none` past the presentation hint, `unset` inherits (measured with no ancestor stroke: the same `none`), and `revert-layer` rolls back only one layer so the hint *survives*. An *escaped* paint spelling refuses under the stroke patrol's coarse whole-attribute scan where Chromium paints it (measured, declared: over-refusal, never wrong pixels). | | `svg-stroke-paint-css-over-attr.svg` · `svg-stroke-paint-css-invalid-falls-back.svg` | The paint's precedence pair, on the same shared hash: an author declaration beats the presentation attribute, and an invalid declaration ceases to exist so the attribute survives. | | `svg-stroke-paint-none.svg` · `svg-stroke-paint-invalid-none.svg` | `stroke="none"` paints no stroke, and an invalid attribute drops to that same initial `none` — one shared oracle, byte-identical to the fill alone (measured). The initial value of `stroke` *is* `none`, baked rather than asserted. | @@ -179,7 +182,7 @@ The numbers are the measured values, not headroom. No single cell is worst on both axes: the largest differing-pixel count is 6 (`svg-circle-viewbox-scaled`, at delta 3) and the largest per-channel delta is 8 (`svg-ellipse-fill`, in 1 pixel), so the corpus admits at most 6 pixels and at most delta 8, never both at -once. Strokes land in the same class and mostly below it: 94 of the 95 +once. Strokes land in the same class and mostly below it: 97 of the 98 `svg-stroke-*` cells are byte-exact, and the one that is not (`svg-stroke-path-closed`) differs in 4 pixels at delta 3 along the round join diff --git a/fixtures/web-first/STATUS.md b/fixtures/web-first/STATUS.md index 02f410e2..d5f6306d 100644 --- a/fixtures/web-first/STATUS.md +++ b/fixtures/web-first/STATUS.md @@ -19,7 +19,7 @@ Not a conformance claim: no score is computed or implied (FLIP is unratified), and the corpus enumerates constructs, not the SVG surface. -## Chromium-baked cells (277) +## Chromium-baked cells (280) Each renders byte-exact against its committed Chromium oracle (six curved cells and three gradient ramps carry a declared, bounded @@ -215,6 +215,8 @@ to its fixture source. No new image is committed for this view. svg-stroke-dasharray-scaled-group svg-stroke-dasharray-subpath-restart svg-stroke-dasharray-use-inherited +svg-stroke-dasharray-used-clamp-attr +svg-stroke-dasharray-used-clamp-css svg-stroke-dasharray-viewbox-percent svg-stroke-dasharray-zero-round svg-stroke-default-width @@ -269,6 +271,7 @@ to its fixture source. No new image is committed for this view. svg-stroke-width-em-font-size svg-stroke-width-px svg-stroke-width-rem +svg-stroke-width-used-range svg-stroke-zero-extent-rect svg-stroke-zero-length-dot svg-stroke-zero-width @@ -348,7 +351,6 @@ its row into the cells above. | `svg-smil-animate-transform` | declared | skipped svg/g[1]: its authored state is overridden at document load by the unsupported animation at svg/g[1]/animateTransform[1]: animation element is outside the rect-x proving slice | | `svg-smil-retarget-href` | **both refuse** | SVG animation at svg/rect[2]/set[1] is unsupported: animation element is outside the rect-x proving slice; it carries href, so its target cannot be attributed to one element without id resolution; it is active at document load, so the authored state it overrides cannot render as the Base view | | `svg-smil-set-load-active` | declared | skipped svg/rect[2]: its authored state is overridden at document load by the unsupported animation at svg/rect[2]/set[1]: animation element is outside the rect-x proving slice | -| `svg-stroke-dasharray-cycle-overflow` | declared | skipped svg/path[1]: unsupported stroke value "a stroke-dasharray cycle has a finite authored grammar but its resolved total is not representable by this frame contract" | | `svg-stroke-dasharray-escape` | declared | skipped svg/path[1]: unsupported stroke value "a stroke-dasharray on carries a CSS escape this patrol cannot read" | | `svg-stroke-dasharray-font-basis` | declared | skipped svg/path[1]: unsupported stroke value "a stroke-dasharray in em on under an authored font-size carrying vw needs a basis this cascade does not have" | | `svg-stroke-dasharray-sheet-unit` | declared | declaration ignored at svg/style[1]: a stylesheet declares a stroke-dasharray in ex, which needs a basis this cascade does not have; elements it matches render the wrong dash cycle | @@ -359,6 +361,7 @@ its row into the cells above. | `svg-stroke-vector-effect` | declared | skipped svg/g[1]/rect[1]: unsupported rendering attribute vector-effect on (not yet consumed) | | `svg-stroke-width-calc-mixed` | declared | skipped svg/rect[1]: unsupported stroke value "a calc() stroke-width mixing lengths and percentages is not consumed" | | `svg-stroke-width-font-basis` | declared | skipped svg/rect[1]: unsupported stroke value "a stroke-width in em on under an authored font-size carrying vw needs a basis this cascade does not have" | +| `svg-stroke-width-percentage-overflow` | declared | skipped svg/path[1]: unsupported stroke value "stroke-width percentage saturation has cap- and join-dependent paint semantics" | | `svg-stroke-width-var` | declared | declaration ignored at svg/style[1]: a stylesheet declares a stroke-width through var(), an indirection this patrol cannot follow; elements it matches may render at the wrong width | | `svg-switch` | declared | skipped svg/switch[1]: unsupported element | | `svg-text-tspan` | declared | skipped svg/text[1]: unsupported element | diff --git a/fixtures/web-first/chromium/svg-stroke-dasharray-used-clamp-attr.png b/fixtures/web-first/chromium/svg-stroke-dasharray-used-clamp-attr.png new file mode 100644 index 00000000..e4f52e91 Binary files /dev/null and b/fixtures/web-first/chromium/svg-stroke-dasharray-used-clamp-attr.png differ diff --git a/fixtures/web-first/chromium/svg-stroke-dasharray-used-clamp-css.png b/fixtures/web-first/chromium/svg-stroke-dasharray-used-clamp-css.png new file mode 100644 index 00000000..e4f52e91 Binary files /dev/null and b/fixtures/web-first/chromium/svg-stroke-dasharray-used-clamp-css.png differ diff --git a/fixtures/web-first/chromium/svg-stroke-width-used-range.png b/fixtures/web-first/chromium/svg-stroke-width-used-range.png new file mode 100644 index 00000000..c8253fa9 Binary files /dev/null and b/fixtures/web-first/chromium/svg-stroke-width-used-range.png differ diff --git a/fixtures/web-first/oracle-bake.json b/fixtures/web-first/oracle-bake.json index 51e76e14..1d351c06 100644 --- a/fixtures/web-first/oracle-bake.json +++ b/fixtures/web-first/oracle-bake.json @@ -5,7 +5,7 @@ "bake_script_sha256": "2bdb5f933d072a1e87c9a675c3342fcf506c0955c0f5c26e2988f4e8fa37c4f2", "capture_module_sha256": "15ba5c3156f3ed0bfd0f32b6ad773e1d228c0f678396db08c8de1d972cf5bced", "suite": "primitives.json", - "suite_sha256": "8d179aba7340b9fb8f0a38c27754631ff03a8328f0f0aa7c7c0e228224c4d50c", + "suite_sha256": "9a03381e68df2cffe57901c1f88d965f4f7f6430ebb63bed2eaa4f1da4284c61", "capture": { "device_scale_factor": 1, "omit_background": true, @@ -1702,6 +1702,24 @@ "width": 64, "height": 64 }, + { + "id": "svg-stroke-dasharray-used-clamp-attr", + "source": "svg-stroke-dasharray-used-clamp-attr.svg", + "source_sha256": "b04fbbbeac5f7f45930df9ae0a4b4064a9ad17761e1c1d3ae6349bbf10096634", + "oracle": "chromium/svg-stroke-dasharray-used-clamp-attr.png", + "oracle_sha256": "531a1e875dd5214228feb72a259cc76be924369de35e584bb5559103ebb3ca11", + "width": 64, + "height": 64 + }, + { + "id": "svg-stroke-dasharray-used-clamp-css", + "source": "svg-stroke-dasharray-used-clamp-css.svg", + "source_sha256": "ccee4cf26327cc6dd14bd5fe8e3b6b27c89663226ab89d7b7c012f74fc604942", + "oracle": "chromium/svg-stroke-dasharray-used-clamp-css.png", + "oracle_sha256": "531a1e875dd5214228feb72a259cc76be924369de35e584bb5559103ebb3ca11", + "width": 64, + "height": 64 + }, { "id": "svg-stroke-dasharray-viewbox-percent", "source": "svg-stroke-dasharray-viewbox-percent.svg", @@ -2188,6 +2206,15 @@ "width": 64, "height": 64 }, + { + "id": "svg-stroke-width-used-range", + "source": "svg-stroke-width-used-range.svg", + "source_sha256": "1e3df85af1207a60afa6a064dcd5981f26a60e6a469263391d232de7f84fabb8", + "oracle": "chromium/svg-stroke-width-used-range.png", + "oracle_sha256": "70bc59da3e8b8c2fc6bcc8a27a5e656db3122c488e635bf5dfe7a06677b0647a", + "width": 64, + "height": 64 + }, { "id": "svg-stroke-zero-extent-rect", "source": "svg-stroke-zero-extent-rect.svg", diff --git a/fixtures/web-first/primitives.json b/fixtures/web-first/primitives.json index 9eb5097c..c9157f01 100644 --- a/fixtures/web-first/primitives.json +++ b/fixtures/web-first/primitives.json @@ -1577,6 +1577,22 @@ "width": 64, "height": 64 }, + { + "id": "svg-stroke-dasharray-used-clamp-attr", + "source": "svg-stroke-dasharray-used-clamp-attr.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-stroke-dasharray-used-clamp-attr.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-stroke-dasharray-used-clamp-css", + "source": "svg-stroke-dasharray-used-clamp-css.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-stroke-dasharray-used-clamp-css.png", + "width": 64, + "height": 64 + }, { "id": "svg-stroke-dasharray-viewbox-percent", "source": "svg-stroke-dasharray-viewbox-percent.svg", @@ -2022,6 +2038,14 @@ "width": 64, "height": 64 }, + { + "id": "svg-stroke-width-used-range", + "source": "svg-stroke-width-used-range.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-stroke-width-used-range.png", + "width": 64, + "height": 64 + }, { "id": "svg-stroke-zero-extent-rect", "source": "svg-stroke-zero-extent-rect.svg", diff --git a/fixtures/web-first/svg-stroke-dasharray-used-clamp-attr.svg b/fixtures/web-first/svg-stroke-dasharray-used-clamp-attr.svg new file mode 100644 index 00000000..8e5eec22 --- /dev/null +++ b/fixtures/web-first/svg-stroke-dasharray-used-clamp-attr.svg @@ -0,0 +1,11 @@ + + + + + diff --git a/fixtures/web-first/svg-stroke-dasharray-used-clamp-css.svg b/fixtures/web-first/svg-stroke-dasharray-used-clamp-css.svg new file mode 100644 index 00000000..5f21fb92 --- /dev/null +++ b/fixtures/web-first/svg-stroke-dasharray-used-clamp-css.svg @@ -0,0 +1,11 @@ + + + + + diff --git a/fixtures/web-first/svg-stroke-width-used-range.svg b/fixtures/web-first/svg-stroke-width-used-range.svg new file mode 100644 index 00000000..a529fa3b --- /dev/null +++ b/fixtures/web-first/svg-stroke-width-used-range.svg @@ -0,0 +1,4 @@ + + + + diff --git a/fixtures/web-first/unsupported/README.md b/fixtures/web-first/unsupported/README.md index abac02d6..0f7d11a2 100644 --- a/fixtures/web-first/unsupported/README.md +++ b/fixtures/web-first/unsupported/README.md @@ -34,7 +34,6 @@ The scannable, generated view of this register (beside the baked cells) is | `svg-path-css-d-property.svg` | Declare a stylesheet's `d: path(…)`: Chromium honors it in place of the attribute, and the pinned Stylo build drops the declaration entirely. | | `svg-path-pathlength.svg` | Refuse by name — **load-bearing now**. Chromium scales dash intervals through `pathLength` on path, rect, circle, and ellipse, and scales dash offset on path (measured); the zero-calibration frame contract carries no such fact. The same patrol covers all seven admitted geometry elements so a dash cycle never paints in the wrong distance space. | | `svg-path-marker-end.svg` | Refuse by name — **load-bearing**. Nothing else reads a marker property: the property *is* the paint trigger, so this refusal is what keeps Chromium's arrowhead from becoming a silent hole. | -| `svg-stroke-dasharray-cycle-overflow.svg` | Refuse by name — each authored interval is finite, but the repeated cycle's f32 sum is not. Chromium honors the grammar (measured: `3.4e38 3.4e38` covers the finite path solid; `0 3.4e38 0 3.4e38` under round caps paints its initial dots), while the checked frame contract requires one finite positive cycle length. The valid-but-unrepresentable remainder is measured, not celled. | | `svg-stroke-dasharray-escape.svg` | Refuse by name — a CSS escape can hide a basis-less unit from the authored-text patrol (`1\76 w` tokenizes as `1vw`), so the presentation attribute, style attribute, and stylesheet spellings all refuse rather than silently use the pinned device basis. | | `svg-stroke-dasharray-font-basis.svg` | Refuse by name — `em`/`rem` are admitted only while their cascaded `font-size` basis is trustworthy. A basis-less unit, `var()`, or escape in that basis would resolve a different cycle from Chromium; the exact poison classes and all ancestor/sheet ingresses share the stroke-width rung's guarded patrol. | | `svg-stroke-dasharray-sheet-unit.svg` | Refuse by name — the stylesheet spelling of a dash interval in a unit whose basis this build lacks. Viewport-, container-, font-metric-, and root-font-metric-relative unit classes are all guarded; the units' own checklist rows carry those gaps, following the stroke-width precedent. | @@ -45,6 +44,7 @@ The scannable, generated view of this register (beside the baked cells) is | `svg-stroke-width-calc-mixed.svg` | Refuse by name — a `calc()` stroke-width mixing lengths and percentages. Chromium resolves the sum with the percentage against the normalized diagonal (measured: `calc(10% + 0.8px)` ≡ an authored `7.2`); the resolve here reads pure lengths and pure percentages only, so the mixed computed value refuses rather than dropping either term. Unlike the basis-less units, this refusal sits at resolve, where the element is known — so every spelling, the `