Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions crates/n0_cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<line>`, `<polygon>`
and `<polyline>` (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
`<g>` (and `<a>`, the same container semantics) with the whole `transform`
grammar, under the outer `<svg>`.
On `<circle>` and `<ellipse>`, the `cx`/`cy` presentation attributes default
Expand Down Expand Up @@ -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`,
Expand Down
101 changes: 16 additions & 85 deletions crates/websem/src/svg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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()));
Expand All @@ -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(),
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -10177,10 +10133,9 @@ enum PointsClosure {
/// geometry kind of its own, exactly as `<line>` 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,
Expand Down Expand Up @@ -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);
};
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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
Expand Down
35 changes: 24 additions & 11 deletions crates/websem/src/svg_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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<Vec<(f32, f32)>, 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
Expand Down
9 changes: 5 additions & 4 deletions crates/websem/tests/context_paint_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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##"<defs><linearGradient id="g"><stop offset="0" stop-color="red"/><stop offset="1" stop-color="blue"/></linearGradient><rect id="leaf" x="8" y="8" width="24" height="24" fill="context-fill"/><g id="outer"><polygon points="8,8 20"/><use href="#leaf" fill="url(#g)"/></g></defs><use href="#outer"/>"##,
r##"<defs><linearGradient id="g"><stop offset="0" stop-color="red"/><stop offset="1" stop-color="blue"/></linearGradient><rect id="leaf" x="8" y="8" width="24" height="24" fill="context-fill"/><g id="outer"><rect x="8px" y="8" width="1" height="1"/><use href="#leaf" fill="url(#g)"/></g></defs><use href="#outer"/>"##,
);
let best = SvgFrameSource::from_standalone_svg_best_effort(source.as_str(), viewport())
.expect("best effort names both holes");
Expand Down
Loading
Loading