diff --git a/crates/n0_cli/README.md b/crates/n0_cli/README.md index 1f14d9af..0f7cbc4f 100644 --- a/crates/n0_cli/README.md +++ b/crates/n0_cli/README.md @@ -74,8 +74,9 @@ cargo run -p n0_cli --bin n0 -- \ remains a named refusal because the pinned cascade has no corresponding longhand. Also admitted are ``, `` and `` (the `points` grammar through the same number scanner as - path data; an erroneous list refuses the whole element by name where - Chromium renders its valid pair prefix — a declared divergence), nested in + path data; a final unmatched x coordinate is dropped after all complete + pairs, while every lexical or numeric parse failure resolves to the empty + list), nested in `` (and ``, the same container semantics) with the whole `transform` grammar, under the outer ``. On `` and ``, the `cx`/`cy` presentation attributes default @@ -539,8 +540,8 @@ cargo run -p n0_cli --bin n0 -- \ The filter estate contains 26 chassis/blur cells, 60 shadow-graph, 28 native drop-shadow, 27 color-matrix, 32 component-transfer, 38 blend, 37 morphology, 91 turbulence/displacement, 41 convolution-rung, and 71 diffuse-lighting - cells. The complete corpus contains 1,026 Chromium-baked cells plus 16 sampled - frames, with 189 named + cells. The complete corpus contains 1,035 Chromium-baked cells plus 16 sampled + frames, with 188 named refusal rows. `feFlood`, `feComposite`, `feMerge`, `feMergeNode`, `feDropShadow`, `feColorMatrix`, `feComponentTransfer`, `feBlend`, `feMorphology`, `feConvolveMatrix`, diff --git a/crates/websem/src/svg.rs b/crates/websem/src/svg.rs index e18c46df..bd1da94f 100644 --- a/crates/websem/src/svg.rs +++ b/crates/websem/src/svg.rs @@ -362,20 +362,10 @@ pub enum CompileError { /// Chromium silently falls back to the default `xMidYMid meet` for /// these; the slice refuses by name instead of silently defaulting. BadPreserveAspectRatio(String), - /// A `points` list outside the SVG2 §10.4 grammar. Chromium renders the - /// valid coordinate-pair prefix and drops the rest; this slice refuses - /// the whole element by name instead, so an odd trailing coordinate is - /// one named hole, never a silently different shape. - BadPoints { - element: String, - /// Byte offset where the value stopped being a valid points list. - offset: usize, - excerpt: String, - }, /// A producer-normalized path stream the resolved contract rejected. - /// Source `d` syntax errors are finalized at the last complete segment; - /// this variant therefore names a compiler arithmetic/contract failure, - /// not authored malformed path data. + /// Source `d` errors and authored point-list errors are finalized before + /// this boundary; this variant therefore names a compiler + /// arithmetic/contract failure, not malformed source data. BadPathData { element: String, /// Reserved source offset; normalized-stream failures use zero. @@ -904,14 +894,6 @@ impl std::fmt::Display for CompileError { CompileError::BadPreserveAspectRatio(v) => { write!(f, "preserveAspectRatio {v:?} is invalid") } - CompileError::BadPoints { - element, - offset, - excerpt, - } => write!( - f, - "points on <{element}> is invalid at byte {offset} (near {excerpt:?})" - ), CompileError::BadPathData { element, offset, @@ -2224,13 +2206,7 @@ fn measure_leaf_geometry( let Some(value) = get_attr(el, "points") else { return Ok(MeasuredGeometry::Empty); }; - let points = crate::svg_path::parse_points(&value).map_err( - |crate::svg_path::SourceSyntaxError::Syntax { offset }| CompileError::BadPoints { - element: el.local_name_string(), - offset, - excerpt: excerpt_at(&value, offset), - }, - )?; + let points = crate::svg_path::parse_points(&value); if points.is_empty() { return Ok(MeasuredGeometry::Empty); } @@ -5874,18 +5850,7 @@ mod clip_path { } "polygon" | "polyline" => { let points = get_attr(element, "points") - .map(|value| { - crate::svg_path::parse_points(&value).map_err( - |crate::svg_path::SourceSyntaxError::Syntax { offset }| { - CompileError::BadPoints { - element: tag.clone(), - offset, - excerpt: excerpt_at(&value, offset), - } - }, - ) - }) - .transpose()? + .map(|value| crate::svg_path::parse_points(&value)) .unwrap_or_default(); let Some(((first_x, first_y), rest)) = points.split_first() else { return Ok(Geometry::Rect(Rectangle::empty())); @@ -5902,7 +5867,7 @@ mod clip_path { commands.push(rframe::PathCommand::Close); } Geometry::Path(Arc::new(PathData::new(commands, fill_rule).map_err( - |error| CompileError::BadPoints { + |error| CompileError::BadPathData { element: tag, offset: 0, excerpt: error.to_string(), @@ -9452,18 +9417,9 @@ fn prepare_marker_projection( }) } "polygon" | "polyline" if projects => { - let points = match get_attr(el, "points") { - None => Vec::new(), - Some(value) => crate::svg_path::parse_points(&value).map_err( - |crate::svg_path::SourceSyntaxError::Syntax { offset }| { - CompileError::BadPoints { - element: tag.to_string(), - offset, - excerpt: excerpt_at(&value, offset), - } - }, - )?, - }; + let points = get_attr(el, "points") + .map(|value| crate::svg_path::parse_points(&value)) + .unwrap_or_default(); Ok(MarkerProjection { parsed_path: None, positions: crate::svg_path::points_marker_positions(&points, tag == "polygon"), @@ -10177,10 +10133,9 @@ enum PointsClosure { /// geometry kind of its own, exactly as `` lowers. /// /// The `points` list maps to `MoveTo` + `LineTo`* (+ `Close` for a -/// polygon). Chromium renders the valid coordinate-pair prefix of an -/// erroneous list; this slice refuses the whole element by name instead -/// (see [`CompileError::BadPoints`]). A missing or empty list is valid and -/// renders nothing, like an empty `d`. +/// polygon). A final unmatched x coordinate is dropped after retaining every +/// complete pair; any lexical or numeric parse error clears the list. A +/// missing or empty list is valid and renders nothing, like an empty `d`. fn compile_points_shape( el: HtmlElement<'_>, viewport: AffineTransform, @@ -10213,16 +10168,9 @@ fn compile_points_shape( if patrol.opacity == 0.0 { return Ok(None); } - let points = match get_attr(el, "points") { - None => Vec::new(), - Some(value) => crate::svg_path::parse_points(&value).map_err( - |crate::svg_path::SourceSyntaxError::Syntax { offset }| CompileError::BadPoints { - element: element.to_string(), - offset, - excerpt: excerpt_at(&value, offset), - }, - )?, - }; + let points = get_attr(el, "points") + .map(|value| crate::svg_path::parse_points(&value)) + .unwrap_or_default(); let Some(((first_x, first_y), rest)) = points.split_first() else { return Ok(None); }; @@ -10256,7 +10204,7 @@ fn compile_points_shape( let path = PathData::new(commands, resolve_fill_rule(el)?).map_err(|error| { // The producer normalizes into the contract's canonical form, so a // rejection here is this compiler's bug, not the document's. - CompileError::BadPoints { + CompileError::BadPathData { element: element.to_string(), offset: 0, excerpt: error.to_string(), @@ -10285,23 +10233,6 @@ fn compile_points_shape( .map(Some) } -/// The authored text at an error offset, clipped to a readable excerpt on a -/// character boundary. -fn excerpt_at(value: &str, offset: usize) -> String { - /// Long enough to show the offending token, short enough that a - /// kilobyte-long `d` does not reach a terminal. - const WIDTH: usize = 24; - let start = (0..=offset.min(value.len())) - .rev() - .find(|index| value.is_char_boundary(*index)) - .unwrap_or(0); - let end = (start..=(start + WIDTH).min(value.len())) - .rev() - .find(|index| value.is_char_boundary(*index)) - .unwrap_or(value.len()); - value[start..end].to_string() -} - /// The cascaded `fill-rule` — which regions of a self-overlapping path the /// fill covers. Read as a typed computed value like `fill`, so the SVG2 /// precedence (presentation attribute below author rules), inheritance diff --git a/crates/websem/src/svg_path.rs b/crates/websem/src/svg_path.rs index ff6f804c..76ee8b6d 100644 --- a/crates/websem/src/svg_path.rs +++ b/crates/websem/src/svg_path.rs @@ -121,8 +121,8 @@ pub(crate) enum MarkerPathElement { /// Why one SVG numeric grammar stopped parsing. /// -/// Path data itself consumes its valid prefix when this occurs. `points` -/// retains the error because its valid-pair-prefix rule is a separate rung. +/// Path data itself consumes its valid prefix when this occurs. Point lists +/// use the same scanner but finalize their own list-level error semantics. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum SourceSyntaxError { /// The source value stopped being valid at this byte offset. @@ -145,28 +145,41 @@ pub(crate) fn parse_path(d: &str) -> ParsedPath { Parser::new(d).parse_path_prefix() } -/// Parse one `points` list (SVG2 §10.4) into coordinate pairs, through the -/// same number scanner as path data so the two grammars cannot drift. +/// Parse one `points` list (SVG2 §10.4) into Chromium's used coordinate pairs, +/// through the same number scanner as path data so the two grammars cannot +/// drift. /// /// The separator rules are Blink's, measured against Chromium 149: a /// trailing separator after the last complete pair is accepted (unlike the /// `viewBox` grammar), a leading or doubled comma is an error, a trailing /// dot needs a digit, and a sign starts a new number (`32-56` is two). -/// Chromium renders the valid *pair prefix* of an erroneous list; this -/// slice refuses the whole element by name instead — the same declared -/// divergence as path data, so an odd trailing coordinate is one named -/// hole, never a silently different shape. +/// A final unmatched x coordinate, with or without one trailing comma, is the +/// one recoverable error: it is dropped and all complete pairs remain. Every +/// lexical or numeric failure clears the whole list, including one after an +/// unmatched x; no malformed-list prefix survives. /// /// An empty (or whitespace-only) value is valid and resolves to no points, /// which renders nothing. -pub(crate) fn parse_points(value: &str) -> Result, SourceSyntaxError> { +pub(crate) fn parse_points(value: &str) -> Vec<(f32, f32)> { let mut parser = Parser::new(value); parser.skip_wsp(); let mut points = Vec::new(); while parser.at < parser.bytes.len() { - points.push(parser.coordinate_pair()?); + let Ok(x) = parser.number() else { + return Vec::new(); + }; + // Blink emits only complete pairs. Reaching the end after x is the + // SVGPointList odd-count exception, including when x consumed one + // trailing comma. + if parser.at == parser.bytes.len() { + return points; + } + let Ok(y) = parser.number() else { + return Vec::new(); + }; + points.push((x, y)); } - Ok(points) + points } /// The five ASCII characters Blink's SVG parsers treat as whitespace diff --git a/crates/websem/tests/context_paint_contract.rs b/crates/websem/tests/context_paint_contract.rs index c48f2722..60081a16 100644 --- a/crates/websem/tests/context_paint_contract.rs +++ b/crates/websem/tests/context_paint_contract.rs @@ -428,11 +428,12 @@ fn unknown_geometry_never_becomes_a_partial_context_box() { #[test] fn an_unindexed_nested_use_is_unknown_not_empty() { - // The odd `points` coordinate is a registered whole-element departure. - // Its measurement error deliberately prevents the outer group's prepass - // entry; the nested use must not reinterpret that absence as an empty box. + // The unit-bearing `x` is a registered whole-element `BadNumber` + // over-refusal. Its measurement error deliberately prevents the outer + // group's prepass entry; the nested use must not reinterpret that absence + // as an empty box. let source = document( - r##""##, + r##""##, ); let best = SvgFrameSource::from_standalone_svg_best_effort(source.as_str(), viewport()) .expect("best effort names both holes"); diff --git a/crates/websem/tests/points_contract.rs b/crates/websem/tests/points_contract.rs index 0abc02b7..1d34bdf1 100644 --- a/crates/websem/tests/points_contract.rs +++ b/crates/websem/tests/points_contract.rs @@ -6,9 +6,9 @@ //! same number scanner as path data so the two cannot drift, and its //! separator rules are Blink's, measured against Chromium: a trailing //! separator after the last complete pair is accepted (unlike `viewBox`), -//! a leading or doubled comma is an error, and Chromium renders the valid -//! pair prefix of an erroneous list where this slice refuses the whole -//! element by name — the paths rung's declared divergence, restated here. +//! a leading or doubled comma is an error, and a final unmatched x coordinate +//! is dropped while retaining all complete pairs. Every lexical or numeric +//! parse error clears the whole list, including one after an unmatched x. // This binary consumes only the n0 render half of the shared plumbing. #[allow(dead_code)] @@ -16,7 +16,7 @@ mod support; use rframe::{Geometry, PathCommand}; use support::render_through_n0; -use websem::{CompileError, DegradationAction, InitialViewport, SvgFrameSource}; +use websem::{InitialViewport, SvgFrameSource}; fn viewport() -> InitialViewport { InitialViewport::new(64.0, 64.0) @@ -228,53 +228,66 @@ fn a_trailing_separator_after_the_last_pair_is_admitted() { assert_eq!(trailing.nodes()[0].geometry, clean.nodes()[0].geometry); } -/// An erroneous list refuses the whole element by name, with the byte -/// offset — Chromium renders the valid pair prefix instead, and that -/// divergence is declared, never silent. +/// A final unmatched x coordinate is the one recoverable list error: it is +/// dropped, with or without one trailing comma, and every complete pair +/// remains. #[test] -fn an_erroneous_points_list_refuses_the_whole_element_by_name() { +fn an_odd_coordinate_count_keeps_only_complete_pairs() { + for (label, odd, clean) in [ + ( + "three-point polygon", + r##""##, + r##""##, + ), + ( + "one-point polygon", + r##""##, + r##""##, + ), + ( + "two-point polyline", + r##""##, + r##""##, + ), + ( + "odd x with trailing comma", + r##""##, + r##""##, + ), + ] { + let odd = admit_both(&document(odd)); + let clean = admit_both(&document(clean)); + assert_eq!(odd, clean, "{label}: only complete pairs survive"); + } +} + +/// Every other parse error invalidates the whole list. In particular, a +/// lexical error after an unmatched x is not allowed to inherit the odd-count +/// exception. +#[test] +fn a_non_odd_parse_error_clears_the_whole_list() { for (label, points) in [ ("leading comma", ",8,8 56,8 32,56"), ("doubled comma", "8,8,,56,8 32,56"), + ("garbage after complete pairs", "8,8 56,8 32,56 X"), + ("garbage after unmatched x", "8,8 56,8 32 X"), ("trailing dot", "8,8 56,8 32.,56"), - ("odd coordinate count", "8,8 56,8 32,56 40"), + ("malformed exponent", "8,8 56,8 32e,56"), + ("overflowing x", "8,8 56,8 1e40,56"), + ("overflowing y", "8,8 56,8 32,1e40"), + ("unit", "8,8 56,8 32px,56"), ("percentage", "8,8 56,8 50%,56"), + ("calculation", "8,8 56,8 calc(32),56"), + ("variable", "8,8 56,8 var(--x),56"), + ("CSS-wide text", "8,8 56,8 initial"), + ("CSS comment", "8,8 56,8/**/32,56"), + ("non-ASCII whitespace", "8,8 56,8\u{a0}32,56"), + ("second trailing comma", "8,8 56,8 32,56,,"), ] { - let source = document(&format!( + let frame = admit_both(&document(&format!( r##" "## - )); - let strict = SvgFrameSource::from_standalone_svg(source.as_str(), viewport()) - .expect_err(&format!("{label}: strict refuses")); - assert!( - matches!(strict, CompileError::BadPoints { .. }), - "{label}: expected BadPoints, got {strict}" - ); - assert!( - strict.to_string().contains("points on ") - && strict.to_string().contains("invalid at byte"), - "{label}: the refusal names the construct and the offset; got {strict}" - ); - - let best = SvgFrameSource::from_standalone_svg_best_effort(source.as_str(), viewport()) - .unwrap_or_else(|error| panic!("{label}: best-effort compiles: {error}")); - assert_eq!( - best.base_frame().nodes().len(), - 0, - "{label}: a declared hole" - ); - assert_eq!(best.degradations().len(), 1, "{label}"); - assert!( - best.degradations()[0] - .reason() - .contains("points on "), - "{label}: the skip names the construct; got {}", - best.degradations()[0].reason() - ); - assert_eq!( - best.degradations()[0].action(), - DegradationAction::Skipped, - "{label}" - ); + ))); + assert_eq!(frame.nodes().len(), 0, "{label}: the used list is empty"); } } @@ -350,7 +363,7 @@ fn points_shapes_keep_the_marker_css_patrol() { /// (the equivalence law extended to the rung). #[test] fn inline_and_standalone_points_resolve_to_the_same_frame() { - let svg_body = r##""##; + let svg_body = r##""##; let html = format!("{svg_body}"); let inline = websem::compile_html_inline_svg(&html).expect("compile inline entry"); let standalone = diff --git a/crates/websem/tests/unsupported_corpus.rs b/crates/websem/tests/unsupported_corpus.rs index d8ad8c4e..f2d671d9 100644 --- a/crates/websem/tests/unsupported_corpus.rs +++ b/crates/websem/tests/unsupported_corpus.rs @@ -738,11 +738,6 @@ const CORPUS: &[(&str, Departure, &str)] = &[ DeclaredByBestEffort, "transform:none on a derived pattern", ), - ( - "svg-points-odd-coordinate", - DeclaredByBestEffort, - "points on ", - ), ( "svg-preserve-aspect-ratio-case-folded", BothRefuse, diff --git a/docs/wg/consolidation/svg-engine-of-record.md b/docs/wg/consolidation/svg-engine-of-record.md index f60386b0..f632dbcd 100644 --- a/docs/wg/consolidation/svg-engine-of-record.md +++ b/docs/wg/consolidation/svg-engine-of-record.md @@ -90,14 +90,14 @@ from the dated addenda below: carrying admitted repeating-pattern paint and admitted source/target filter composition. `crates/n0_cli/README.md` is the statement of record. -- **The corpus** is 1,026 Chromium-baked primitive cells plus 16 sampled frames. +- **The corpus** is 1,035 Chromium-baked primitive cells plus 16 sampled frames. All byte-exact except seven curved cells carrying a declared, geometrically confined tolerance (the native-oval/conic boundary) and four gradient cells carrying a declared one-code-value ramp-quantization tolerance (one pixel against Chromium's Skia; 18 knife-edge pixels between this engine's own macOS and Linux Skia builds; 336 ramp pixels under an isolated layer's restore; 576 after a masked ramp becomes luminance alpha). The named refusal - register has 189 rows. + register has 188 rows. - **Not claimed:** no conformance score exists or may be computed — FLIP is unratified. The FLIP record and identity-changing review are prepared, but only the owner act on gridaco/nothing#49 may authorize them and the first @@ -922,21 +922,21 @@ Closure is the one semantic difference between the two elements, and the `points` grammar runs through the same number scanner as path data, so the two grammars cannot drift. -**Measured before written.** The grammar's edges were probed against -Chromium 149 before the parser existed, and the probe moved the design in -one place: a trailing separator after the last complete pair is *accepted* -in `points` (unlike the `viewBox` grammar, whose trailing comma stays a -refusal), so the slice admits it, Chromium-baked. The rest confirmed the -plan: a leading or doubled comma, a trailing dot, and a percent are errors -whose valid *pair prefix* Chromium renders — this slice refuses the whole -element by name instead, the paths rung's declared divergence restated -(`svg-points-odd-coordinate` is its refusal-corpus row); a filled polyline -paints as if closed; and a single point splits by closure — the polygon is -the zero-length **closed** contour whose cap paints a dot, resolved into -the contract's canonical `M x y L x y Z` spelling (the cap-normalization -exception from the cap-defect addendum fires for it unchanged), while the -polyline is a neutral move-only contour that paints nothing under any cap -and is admitted as not-a-node. +**Measured before written, corrected 2026-08-30.** The original probe correctly +found that a trailing separator after the last complete pair is accepted in +`points` (unlike the `viewBox` grammar), but incorrectly generalized the +odd-coordinate recovery rule to lexical errors. Current Chromium, its parser, +and the upstream point-list test agree on the narrower rule: a final unmatched +x coordinate is dropped and complete pairs remain; every lexical or numeric +failure clears the whole list. The original slice refused both classes. That +made malformed lists declared over-refusals with the correct empty pixels, +while `svg-points-odd-coordinate` was the actual missing-pixel row. The other +findings stand: a filled polyline paints as if closed; and a single point +splits by closure — the polygon is the zero-length **closed** contour whose cap +paints a dot, resolved into the contract's canonical `M x y L x y Z` spelling +(the cap-normalization exception from the cap-defect addendum fires for it +unchanged), while the polyline is a neutral move-only contour that paints +nothing under any cap and is admitted as not-a-node. **Eight cells, byte-exact.** Fill with mixed separators, the trailing separator, an evenodd self-intersecting star (the cascaded `fill-rule` @@ -4242,3 +4242,68 @@ focused source, resource, cascade, and value rows replace it, moving the named register from 172 to 189. The sixteen exact-time frames and 451-cell filter estate are unchanged. No checklist row closes, no conformance score was produced, and no FLIP record, rule, or baseline changed. + +## Rung: SVG point-list attribute closure (2026-08-30) + +The verdict is CLOSE. The `points` attribute now carries its complete listed +grammar on both `` and ``, so its checklist row closes. The +two applicable element rows were already closed, and there is no CSS-property +twin implied by this attribute result. + +The crux corrected the earlier points-rung record. SVG's odd-coordinate rule +is not a general valid-prefix rule. Chromium 149.0.7827.55, Blink's current +point-list parser, and the upstream point-list test all agree: parsing appends +each completed coordinate pair; reaching the end after an unmatched x leaves +those pairs in place, including when one trailing comma follows that x; any +lexical or numeric parse failure clears the whole list. A malformed token after +an unmatched x therefore overrides the odd-count recovery and clears even the +earlier complete pairs. This also explains the former asymmetry: malformed +lists already produced Chromium's empty pixels under best effort, but were +unnecessarily declared and rejected by strict admission, while an odd list +lost visible pixels in best effort and was the registered refusal. + +The standard grammar and browser implementation support the same boundary: +[SVG 2 defines the unmatched-coordinate recovery](https://svgwg.org/svg2-draft/shapes.html#DataTypePoints), +[Blink clears a list after any parser error](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/core/svg/svg_point_list.cc), +and the [upstream browser test](https://wpt.live/svg/types/scripted/SVGList-parse-invalid-clears-items.html) +separately checks malformed-list clearing and odd-x truncation. Measurement, +not the earlier prose, selected the corrected rule. + +Nine Chromium-baked cells carry the result. Four direct contour cells cover an +odd coordinate after three, one, and two complete pairs and after a trailing +comma. One sixteen-case cell covers leading and doubled separators, complete- +pair and odd-x lexical errors, malformed dot and exponent forms, x/y overflow, +units, percentages, calculations, variables, CSS-wide text, comments, +non-ASCII whitespace, and a second trailing comma; all resolve to an empty +list with no declaration. Four consumer cells carry the recovered list through +marker topology, geometric clip geometry, object-box bounds, and same-document +instancing. Existing cells continue to cover empty and missing lists, mixed +separators, sign adjacency, exponents, ordered source-number evaluation, +ordinary trailing commas, closure, fill rules, and single-point contours. +Strict and best-effort admissions are frame-identical for every new cell, and +all nine are byte-exact to Chromium without a tolerance. + +The wider scratch matrix measured the same split before admission. Odd-prefix +controls were exact through direct geometry, markers, clips, and instances; +depending on the route, clearing the odd list lost 64 to 1,200 pixels at +maximum channel deltas from 218 to 233. Leading commas, doubled commas, +garbage, overflow, unit and percentage spellings, calculations, variables, +CSS-wide text, comments, non-ASCII whitespace, trailing-dot and malformed- +exponent forms, and a second trailing comma were each pixel-identical to an +empty list. A lexical error after an unmatched x also cleared the list, while +one trailing comma after that x preserved the earlier pairs. Every candidate +rendered through both actual admissions as well as Chromium (measured, not +celled except where the nine cells named above carry the same branch). + +Gate sensitivity was proved against both halves at once. Temporarily restoring +the stale behavior—clearing odd lists while retaining malformed complete-pair +prefixes—made `just gate` fail loudly on all eight odd-coordinate route cells +and the malformed-list matrix. The failures ranged from 64 to 1,200 pixels; +the malformed matrix moved 660 pixels at maximum channel delta 238. Restoring +the corrected rule returned the full gate to green. + +The primitive corpus moves from 1,026 to 1,035 cells. The former +`svg-points-odd-coordinate` refusal graduates, moving the named register from +189 to 188 rows. The sixteen sampled frames and 451-cell filter estate are +unchanged. No conformance score was produced, and no FLIP record, rule, or +baseline changed. diff --git a/docs/wg/consolidation/web-checklist.md b/docs/wg/consolidation/web-checklist.md index e2093498..5e87de4b 100644 --- a/docs/wg/consolidation/web-checklist.md +++ b/docs/wg/consolidation/web-checklist.md @@ -2267,7 +2267,20 @@ for attributes the platform ships ahead of the SVG 2 indexes. - [ ] `ping` - [ ] `playbackorder` -- [ ] `points` +- [x] `points` + +> **2026-08-30 close:** the complete point-list grammar is Chromium-baked for +> both applicable elements. Empty and missing lists paint nothing; mixed +> comma/whitespace separators, sign-separated numbers, exponents, ordered +> source-number evaluation, and one trailing comma are carried. A final +> unmatched x coordinate, including one followed by a trailing comma, is +> dropped while all complete coordinate pairs remain. Every lexical or +> numeric parse failure instead clears the whole list; the committed matrix +> includes failures after complete pairs and after an unmatched x. Direct +> polygon/polyline geometry, one-point and two-point contours, marker +> topology, geometric clips, object-box bounds, and same-document use +> instances share the result. Nine new cells are exact through strict and +> best-effort admission with no declaration or tolerance. - [x] `pointsAtX` - [x] `pointsAtY` - [x] `pointsAtZ` diff --git a/fixtures/web-first/README.md b/fixtures/web-first/README.md index f04661ba..124ab57d 100644 --- a/fixtures/web-first/README.md +++ b/fixtures/web-first/README.md @@ -121,6 +121,9 @@ is exactly what the engine renders pixel-for-pixel. | `svg-polygon-stroke-closed.svg` · `svg-polyline-stroke-open.svg` | The closure split, stroked: the same three points as a polygon paint the closing segment and its joins; as a polyline they end in caps with no closing edge. | | `svg-polyline-fill-implicit-close.svg` | A filled polyline paints as if closed — identical ink to the same polygon's fill (measured), because filling an open contour closes it. | | `svg-polygon-single-point-square-cap.svg` · `svg-polyline-single-point-square-cap.svg` | A single point splits by closure: the polygon is the zero-length **closed** contour whose square cap paints a dot, the polyline is a move-only open contour that paints nothing — the cap laws from the strokes rung, restated through the points grammar. | +| `svg-points-odd-coordinate.svg` · `svg-points-odd-coordinate-single-point.svg` · `svg-points-odd-coordinate-polyline.svg` · `svg-points-odd-coordinate-trailing-comma.svg` | The point-list recovery rule at four discriminating contour sizes. A final unmatched x is dropped while completed pairs survive; one trailing comma after that x does not change the result. The retained prefixes paint a filled triangle, a 64-pixel square-cap dot, and two 288-pixel stroked segments. | +| `svg-points-invalid-clears-list.svg` | The opposite branch: leading/doubled separators, garbage after a complete prefix or unmatched x, malformed dot/exponent forms, x/y overflow, units, percentages, `calc()`, `var()`, CSS-wide text, comments, non-ASCII whitespace, and a second trailing comma all clear the entire point list. Preserving completed prefixes changes 660 pixels at maximum channel delta 238 under the sensitivity mutation. | +| `svg-points-odd-coordinate-marker.svg` · `svg-points-odd-coordinate-clip.svg` · `svg-points-odd-coordinate-object-box.svg` · `svg-points-odd-coordinate-use.svg` | Consumer routing for the same recovered prefix: marker topology, geometric clip geometry, object-box bound measurement, and same-document instancing. Each odd source is exact to its clean-list control in Chromium. The object-box clip remains discriminating: removing it changes 963 pixels at maximum channel delta 233 (controls measured, not celled). These four complete a nine-cell close, taking the corpus to 1,035 cells plus 16 sampled frames; graduating the odd-coordinate refusal leaves 188 named rows. | | `svg-display-none-shape.svg` · `svg-display-none-group.svg` | `display: none` generates no box: the shape disappears (its sibling paints), and a container prunes its whole subtree — a `visibility: visible` descendant stays gone. | | `svg-display-none-root.svg` | The entry split the oracle itself caught: a **standalone** document's outermost `` ignores `display: none` and paints normally, where an embedded root generates no box. Baked as proof after an embedded-context probe suggested otherwise. | | `svg-visibility-hidden-shape.svg` · `svg-visibility-collapse-shape.svg` | `visibility: hidden` and `collapse` are identical for shapes: the element's own paint turns off, siblings render. | diff --git a/fixtures/web-first/STATUS.md b/fixtures/web-first/STATUS.md index 98fe6394..99e9ac5c 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 (1026) +## Chromium-baked cells (1035) Each renders byte-exact against its committed Chromium oracle (seven curved cells and four gradient ramps carry a declared, bounded @@ -883,7 +883,16 @@ to its fixture source. No new image is committed for this view. svg-percent-rect-in-viewbox svg-percent-rect-root-units svg-percent-stroke-width +svg-points-invalid-clears-list svg-points-number-accumulation +svg-points-odd-coordinate +svg-points-odd-coordinate-clip +svg-points-odd-coordinate-marker +svg-points-odd-coordinate-object-box +svg-points-odd-coordinate-polyline +svg-points-odd-coordinate-single-point +svg-points-odd-coordinate-trailing-comma +svg-points-odd-coordinate-use svg-points-trailing-comma svg-polygon-fill svg-polygon-fill-rule-evenodd @@ -1055,7 +1064,7 @@ to its fixture source. No new image is committed for this view. svg-visibility-rule-beats-attribute svg-visibility-unhide -## The refusal register (189) +## The refusal register (188) What the slice refuses, by name, in the compiler's own words — **both refuse** is a document-level contract; **declared** renders @@ -1217,7 +1226,6 @@ its row into the cells above. | `svg-pattern-tile-phase-precision` | declared | skipped svg/rect[2]: unsupported fill value "url(#p): pattern #p tile phase resolves to a fractional coordinate at the pinned-backend picture-shader phase precision boundary" | | `svg-pattern-tile-sampling-precision` | declared | skipped svg/rect[2]: unsupported fill value "tile has a fractional final device extent at the pinned-backend picture-shader sampling precision boundary" | | `svg-pattern-transform-none-provenance` | declared | skipped svg/rect[2]: unsupported fill value "url(#p): an author stylesheet may set transform:none on a derived pattern; the empty computed value loses the provenance needed to decide template inheritance" | -| `svg-points-odd-coordinate` | declared | skipped svg/polygon[1]: points on is invalid at byte 17 (near "") | | `svg-preserve-aspect-ratio-case-folded` | **both refuse** | preserveAspectRatio "xmidymid meet" is invalid | | `svg-preserve-aspect-ratio-defer` | **both refuse** | preserveAspectRatio "defer xMidYMid meet" is invalid | | `svg-preserve-aspect-ratio-invalid-align` | **both refuse** | preserveAspectRatio "xMidYMiddle meet" is invalid | diff --git a/fixtures/web-first/chromium/svg-points-invalid-clears-list.png b/fixtures/web-first/chromium/svg-points-invalid-clears-list.png new file mode 100644 index 00000000..d9b4b216 Binary files /dev/null and b/fixtures/web-first/chromium/svg-points-invalid-clears-list.png differ diff --git a/fixtures/web-first/chromium/svg-points-odd-coordinate-clip.png b/fixtures/web-first/chromium/svg-points-odd-coordinate-clip.png new file mode 100644 index 00000000..6a42ffaf Binary files /dev/null and b/fixtures/web-first/chromium/svg-points-odd-coordinate-clip.png differ diff --git a/fixtures/web-first/chromium/svg-points-odd-coordinate-marker.png b/fixtures/web-first/chromium/svg-points-odd-coordinate-marker.png new file mode 100644 index 00000000..c3894571 Binary files /dev/null and b/fixtures/web-first/chromium/svg-points-odd-coordinate-marker.png differ diff --git a/fixtures/web-first/chromium/svg-points-odd-coordinate-object-box.png b/fixtures/web-first/chromium/svg-points-odd-coordinate-object-box.png new file mode 100644 index 00000000..148fba27 Binary files /dev/null and b/fixtures/web-first/chromium/svg-points-odd-coordinate-object-box.png differ diff --git a/fixtures/web-first/chromium/svg-points-odd-coordinate-polyline.png b/fixtures/web-first/chromium/svg-points-odd-coordinate-polyline.png new file mode 100644 index 00000000..24b1a086 Binary files /dev/null and b/fixtures/web-first/chromium/svg-points-odd-coordinate-polyline.png differ diff --git a/fixtures/web-first/chromium/svg-points-odd-coordinate-single-point.png b/fixtures/web-first/chromium/svg-points-odd-coordinate-single-point.png new file mode 100644 index 00000000..ae7c96be Binary files /dev/null and b/fixtures/web-first/chromium/svg-points-odd-coordinate-single-point.png differ diff --git a/fixtures/web-first/chromium/svg-points-odd-coordinate-trailing-comma.png b/fixtures/web-first/chromium/svg-points-odd-coordinate-trailing-comma.png new file mode 100644 index 00000000..24b1a086 Binary files /dev/null and b/fixtures/web-first/chromium/svg-points-odd-coordinate-trailing-comma.png differ diff --git a/fixtures/web-first/chromium/svg-points-odd-coordinate-use.png b/fixtures/web-first/chromium/svg-points-odd-coordinate-use.png new file mode 100644 index 00000000..6a42ffaf Binary files /dev/null and b/fixtures/web-first/chromium/svg-points-odd-coordinate-use.png differ diff --git a/fixtures/web-first/chromium/svg-points-odd-coordinate.png b/fixtures/web-first/chromium/svg-points-odd-coordinate.png new file mode 100644 index 00000000..6a42ffaf Binary files /dev/null and b/fixtures/web-first/chromium/svg-points-odd-coordinate.png differ diff --git a/fixtures/web-first/oracle-bake.json b/fixtures/web-first/oracle-bake.json index 4f0b6cdf..4cce2f50 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": "11f9e757b180ffe9244dcd1d2f1a03a2eaec89d17bb649b5987a67c92ed3b4b9", + "suite_sha256": "98c22bb109eb631920b3da76b0657f1d39a222d130272140b9ca311a79df0b0f", "capture": { "device_scale_factor": 1, "omit_background": true, @@ -7714,6 +7714,15 @@ "width": 64, "height": 64 }, + { + "id": "svg-points-invalid-clears-list", + "source": "svg-points-invalid-clears-list.svg", + "source_sha256": "52cb8bc1b66079ac02f6dffc05dad8657483984392e2f81eb559e525188be8c9", + "oracle": "chromium/svg-points-invalid-clears-list.png", + "oracle_sha256": "4f852cfe622067e2872424c8add5d3977db6b7d6ad84a6329f288f6d7da566fa", + "width": 64, + "height": 64 + }, { "id": "svg-points-number-accumulation", "source": "svg-points-number-accumulation.svg", @@ -7723,6 +7732,78 @@ "width": 64, "height": 64 }, + { + "id": "svg-points-odd-coordinate", + "source": "svg-points-odd-coordinate.svg", + "source_sha256": "c2914f73e07c7120a5455997f6da548b00ab71f380634a0ce3a153e74aaaab54", + "oracle": "chromium/svg-points-odd-coordinate.png", + "oracle_sha256": "5b5132bf617615a31e67d39a29adbc44c04527f054148708a2411b4da408f37c", + "width": 64, + "height": 64 + }, + { + "id": "svg-points-odd-coordinate-clip", + "source": "svg-points-odd-coordinate-clip.svg", + "source_sha256": "5df5e55ca3b0d04b918ea72419bc4649d720a4ec7c074c115307ec709174598a", + "oracle": "chromium/svg-points-odd-coordinate-clip.png", + "oracle_sha256": "5b5132bf617615a31e67d39a29adbc44c04527f054148708a2411b4da408f37c", + "width": 64, + "height": 64 + }, + { + "id": "svg-points-odd-coordinate-marker", + "source": "svg-points-odd-coordinate-marker.svg", + "source_sha256": "bc3199129216310eeca7649b57e26f03442f9103271f8f22b61d642358a3e7ce", + "oracle": "chromium/svg-points-odd-coordinate-marker.png", + "oracle_sha256": "39b87cf93ec8a0e3f3dc69e21d22d7d0e1c16f2d1dfb41d4360d27071a72f5e5", + "width": 64, + "height": 64 + }, + { + "id": "svg-points-odd-coordinate-object-box", + "source": "svg-points-odd-coordinate-object-box.svg", + "source_sha256": "23a3261e86de4dea236fb8fd3eb02de2c2c67d05031fb7bc8c5eb09abafd065a", + "oracle": "chromium/svg-points-odd-coordinate-object-box.png", + "oracle_sha256": "b69f6643d7a5dc5ec52989e375c55c7d0f2a304515bce064a4c8aa48b7364d69", + "width": 64, + "height": 64 + }, + { + "id": "svg-points-odd-coordinate-polyline", + "source": "svg-points-odd-coordinate-polyline.svg", + "source_sha256": "a7eb7897ef51c68b3f39588c015415346786d9c8c8518c728f1e83def4ed9444", + "oracle": "chromium/svg-points-odd-coordinate-polyline.png", + "oracle_sha256": "5ec41f8f346a3e8faba07ea8518a7abae40a5c0872574ff16d1d686822e2172b", + "width": 64, + "height": 64 + }, + { + "id": "svg-points-odd-coordinate-single-point", + "source": "svg-points-odd-coordinate-single-point.svg", + "source_sha256": "bcd1c73603402b5e0d4107b865a2c66e488a5375a4e22df0d4e5b8fae7d16438", + "oracle": "chromium/svg-points-odd-coordinate-single-point.png", + "oracle_sha256": "7d83da8993a38624518785357f2c582a6bd648b62997beda4f12cc5bdf7dca3f", + "width": 64, + "height": 64 + }, + { + "id": "svg-points-odd-coordinate-trailing-comma", + "source": "svg-points-odd-coordinate-trailing-comma.svg", + "source_sha256": "b3efe479d124616cd7432924f3974617922024a1c9c19217da1622e0c9f9e8da", + "oracle": "chromium/svg-points-odd-coordinate-trailing-comma.png", + "oracle_sha256": "5ec41f8f346a3e8faba07ea8518a7abae40a5c0872574ff16d1d686822e2172b", + "width": 64, + "height": 64 + }, + { + "id": "svg-points-odd-coordinate-use", + "source": "svg-points-odd-coordinate-use.svg", + "source_sha256": "40e3bd0d434d435ca7e40f92b8b0c6a2dd0c5d497ced1960c6887d01817f1f84", + "oracle": "chromium/svg-points-odd-coordinate-use.png", + "oracle_sha256": "5b5132bf617615a31e67d39a29adbc44c04527f054148708a2411b4da408f37c", + "width": 64, + "height": 64 + }, { "id": "svg-points-trailing-comma", "source": "svg-points-trailing-comma.svg", diff --git a/fixtures/web-first/primitives.json b/fixtures/web-first/primitives.json index 6ff1026b..1120dc7b 100644 --- a/fixtures/web-first/primitives.json +++ b/fixtures/web-first/primitives.json @@ -6939,6 +6939,14 @@ "width": 64, "height": 64 }, + { + "id": "svg-points-invalid-clears-list", + "source": "svg-points-invalid-clears-list.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-points-invalid-clears-list.png", + "width": 64, + "height": 64 + }, { "id": "svg-points-number-accumulation", "source": "svg-points-number-accumulation.svg", @@ -6947,6 +6955,70 @@ "width": 64, "height": 64 }, + { + "id": "svg-points-odd-coordinate", + "source": "svg-points-odd-coordinate.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-points-odd-coordinate.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-points-odd-coordinate-clip", + "source": "svg-points-odd-coordinate-clip.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-points-odd-coordinate-clip.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-points-odd-coordinate-marker", + "source": "svg-points-odd-coordinate-marker.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-points-odd-coordinate-marker.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-points-odd-coordinate-object-box", + "source": "svg-points-odd-coordinate-object-box.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-points-odd-coordinate-object-box.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-points-odd-coordinate-polyline", + "source": "svg-points-odd-coordinate-polyline.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-points-odd-coordinate-polyline.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-points-odd-coordinate-single-point", + "source": "svg-points-odd-coordinate-single-point.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-points-odd-coordinate-single-point.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-points-odd-coordinate-trailing-comma", + "source": "svg-points-odd-coordinate-trailing-comma.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-points-odd-coordinate-trailing-comma.png", + "width": 64, + "height": 64 + }, + { + "id": "svg-points-odd-coordinate-use", + "source": "svg-points-odd-coordinate-use.svg", + "entry": "standalone-svg", + "oracle": "chromium/svg-points-odd-coordinate-use.png", + "width": 64, + "height": 64 + }, { "id": "svg-points-trailing-comma", "source": "svg-points-trailing-comma.svg", diff --git a/fixtures/web-first/svg-points-invalid-clears-list.svg b/fixtures/web-first/svg-points-invalid-clears-list.svg new file mode 100644 index 00000000..8dc97970 --- /dev/null +++ b/fixtures/web-first/svg-points-invalid-clears-list.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/fixtures/web-first/svg-points-odd-coordinate-clip.svg b/fixtures/web-first/svg-points-odd-coordinate-clip.svg new file mode 100644 index 00000000..fc609838 --- /dev/null +++ b/fixtures/web-first/svg-points-odd-coordinate-clip.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-points-odd-coordinate-marker.svg b/fixtures/web-first/svg-points-odd-coordinate-marker.svg new file mode 100644 index 00000000..5363b771 --- /dev/null +++ b/fixtures/web-first/svg-points-odd-coordinate-marker.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-points-odd-coordinate-object-box.svg b/fixtures/web-first/svg-points-odd-coordinate-object-box.svg new file mode 100644 index 00000000..d8a17793 --- /dev/null +++ b/fixtures/web-first/svg-points-odd-coordinate-object-box.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/fixtures/web-first/svg-points-odd-coordinate-polyline.svg b/fixtures/web-first/svg-points-odd-coordinate-polyline.svg new file mode 100644 index 00000000..291e9ec6 --- /dev/null +++ b/fixtures/web-first/svg-points-odd-coordinate-polyline.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-points-odd-coordinate-single-point.svg b/fixtures/web-first/svg-points-odd-coordinate-single-point.svg new file mode 100644 index 00000000..36ffe047 --- /dev/null +++ b/fixtures/web-first/svg-points-odd-coordinate-single-point.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-points-odd-coordinate-trailing-comma.svg b/fixtures/web-first/svg-points-odd-coordinate-trailing-comma.svg new file mode 100644 index 00000000..048a2ea2 --- /dev/null +++ b/fixtures/web-first/svg-points-odd-coordinate-trailing-comma.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/svg-points-odd-coordinate-use.svg b/fixtures/web-first/svg-points-odd-coordinate-use.svg new file mode 100644 index 00000000..1f8009f1 --- /dev/null +++ b/fixtures/web-first/svg-points-odd-coordinate-use.svg @@ -0,0 +1 @@ + diff --git a/fixtures/web-first/unsupported/svg-points-odd-coordinate.svg b/fixtures/web-first/svg-points-odd-coordinate.svg similarity index 100% rename from fixtures/web-first/unsupported/svg-points-odd-coordinate.svg rename to fixtures/web-first/svg-points-odd-coordinate.svg diff --git a/fixtures/web-first/unsupported/README.md b/fixtures/web-first/unsupported/README.md index e75c8537..1c79668a 100644 --- a/fixtures/web-first/unsupported/README.md +++ b/fixtures/web-first/unsupported/README.md @@ -55,7 +55,6 @@ The scannable, generated view of this register (beside the baked cells) is | `svg-smil-number-precision-alias.svg` | Refuse a valid endpoint decimal whose direct binary32 parse selects the other neighbour from Chromium's CSS-number route. Chromium samples the witness as `1`; the former route sampled the next binary32 value, and an exact transform amplifier changed 48 pixels at maximum channel delta 238. The f64 shadow is only a one-way classifier and never supplies a rendered value (measured, not celled). | | `svg-smil-number-source-syntax.svg` | Refuse endpoint spellings outside the SVG number grammar before Rust's broader float syntax can admit them. Chromium leaves the authored target at `x=2` for `1.` and `1.e0`, while the former route animated it to `1`; Unicode NBSP and ideographic-space padding exposed the same broad-whitespace bug. Each changed 3,072 pixels at maximum channel delta 238. ASCII SVG whitespace, leading plus, and a leading fractional dot remain admitted controls (measured, not celled). | | `svg-smil-retarget-href.svg` | A `` retarget. href resolves by id, which this slice does not own, so the override cannot be attributed to one skippable element — document-level, both admissions refuse, exactly as `