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
37 changes: 25 additions & 12 deletions crates/n0_cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -539,8 +539,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 874 Chromium-baked cells plus 10 sampled
frames, with 170 named
cells. The complete corpus contains 936 Chromium-baked cells plus 10 sampled
frames, with 169 named
refusal rows. `feFlood`, `feComposite`,
`feMerge`, `feMergeNode`, `feDropShadow`, `feColorMatrix`,
`feComponentTransfer`, `feBlend`, `feMorphology`, `feConvolveMatrix`,
Expand Down Expand Up @@ -787,22 +787,35 @@ cargo run -p n0_cli --bin n0 -- \
`patternTransform` is the transform property's presentation hint: CSS beats
it and a plain `transform` attribute is inert. Translation, axis scale,
reflection, and exact quarter turns are admitted. The repeating source can
contain admitted rectangles, gradients, `<use>`, masks, and a pattern nested
alone; pattern paint covers admitted rect, ellipse, and path fills and
strokes, and target opacity, clip, mask, and filter scopes retain their
established order. Sixty-two Chromium-baked cells cover that profile,
contain admitted rectangles, gradients, `<use>`, masks, filters, and a
pattern nested alone; a filter may wrap that sole nested-pattern draw.
Pattern paint covers admitted rect, ellipse, and path fills and strokes, and
target opacity, clip, mask, and filter scopes retain their established
order. A pattern may be selected through all four destination
fill/stroke × context-fill/context-stroke crossings, including CSS ingress,
recursive `<use>` owners, object-box coordinates, transforms, opacity,
fallback/nothing semantics, masks, and filters. It may also supply a
source-derived filter's input when the filtered source is one direct
sharp-cornered rectangle with exactly one pattern-painted fill or simple
stroke channel. That filter profile includes transparent and alpha-bearing
tiles, gradients, masks, strokes, and nested patterns. One hundred
twenty-four Chromium-baked cells cover the core and composition profiles,
including independent object-box clients and one inline-HTML SVG entry; all
are exact without a new tolerance.
What refuses by stable name: a pattern selected through
`context-fill`/`context-stroke`; an external template dependency; a non-`px`
What refuses by stable name: an external template dependency; a non-`px`
unit, CSS math, `var()`, CSS-wide tile value, or CSS comments around an
otherwise valid tile length; a source child outside the admitted element
slice; filter composition inside the source program; curved source
coverage; isolated multi-draw source
slice; curved source coverage; isolated multi-draw source
opacity or a geometric source clip; another source draw mixed with a nested
pattern; a fractional final tile extent; and a final tile map carrying a
general rotation or shear. Those last five are measured picture-shader
precision boundaries, not guessed omissions. Before its patrol, the valid
general rotation or shear. A source-derived filter over curved, rounded,
multi-draw, or wider pattern-painted target geometry has its own
filtered-pattern coverage refusal. The context-aware source classifier also
keeps eventual gradients behind the existing color-matrix, component-
transfer, convolution, morphology, native-shadow, and translucent-source
precision names instead of letting a context keyword hide them. Those
picture-shader and source-layer boundaries are measured, not guessed
omissions. Before its patrol, the valid
comment spelling silently selected fallback in both admissions and changed
all 2,304 target pixels at maximum delta 202. A derived template whose
author stylesheet may contribute `transform:none` also refuses because the
Expand Down
383 changes: 297 additions & 86 deletions crates/websem/src/svg.rs

Large diffs are not rendered by default.

19 changes: 14 additions & 5 deletions crates/websem/tests/context_paint_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -325,13 +325,22 @@ fn nonstandard_context_fallback_refuses_from_every_ingress() {
}

#[test]
fn pattern_and_external_urls_keep_their_own_refusal_through_context() {
let pattern = refusal(&document(
r##"<defs><pattern id="p" width="8" height="8"><rect width="4" height="4" fill="red"/></pattern><rect id="r" width="20" height="20" fill="context-fill"/></defs><use href="#r" fill="url(#p)"/>"##,
fn pattern_resolves_through_context_while_external_urls_keep_their_refusal() {
let pattern = admit_both(&document(
r##"<defs><pattern id="p" width=".25" height=".5"><rect width="5" height="10" fill="red"/></pattern><rect id="r" width="20" height="20" fill="context-fill"/></defs><use href="#r" fill="url(#p)"/>"##,
));
let resolved = pattern.nodes()[0]
.paints
.pattern()
.expect("the context owner selects one source-neutral pattern");
assert!(
matches!(pattern, CompileError::UnsupportedFill(ref reason) if reason.contains("pattern paint selected through context-fill/context-stroke")),
"{pattern}"
resolved.items().nodes().next().is_some(),
"the selected pattern retains its resolved source program"
);
assert_eq!(
(resolved.width(), resolved.height()),
(5.0, 10.0),
"object-box tile extents use the 20x20 context owner's reference space"
);

let external = refusal(&document(
Expand Down
180 changes: 175 additions & 5 deletions crates/websem/tests/filter_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,24 +29,33 @@ fn document(body: &str) -> String {
)
}

fn admit_both(source: &str) -> Frame {
let strict = SvgFrameSource::from_standalone_svg(source, viewport()).expect("strict admits");
fn admit_both_named(source: &str, context: &str) -> Frame {
let strict = SvgFrameSource::from_standalone_svg(source, viewport())
.unwrap_or_else(|error| panic!("{context}: strict must admit: {error}"));
let best = SvgFrameSource::from_standalone_svg_best_effort(source, viewport())
.expect("best effort admits");
.unwrap_or_else(|error| panic!("{context}: best effort must admit: {error}"));
let declared: Vec<_> = best
.degradations()
.iter()
.filter(|degradation| degradation.action() != DegradationAction::SamplesAsBase)
.collect();
assert!(
declared.is_empty(),
"an admitted filter declares nothing: {declared:?}"
"{context}: an admitted filter declares nothing: {declared:?}"
);
let frame = strict.base_frame();
assert_eq!(frame, best.base_frame(), "admissions are frame-identical");
assert_eq!(
frame,
best.base_frame(),
"{context}: admissions are frame-identical"
);
frame
}

fn admit_both(source: &str) -> Frame {
admit_both_named(source, "filter source")
}

fn assert_target_skip(source: &str, reason: &str) {
let strict =
SvgFrameSource::from_standalone_svg(source, viewport()).expect_err("strict must refuse");
Expand Down Expand Up @@ -1032,6 +1041,167 @@ fn component_transfer_precision_patrols_name_paint_server_and_transform_boundari
));
}

#[test]
fn direct_rect_patterns_feed_every_admitted_source_dependent_filter_family() {
let primitives = [
r##"<feGaussianBlur stdDeviation="2"/>"##,
r##"<feOffset dx="3" dy="2"/>"##,
r##"<feFlood flood-color="#e11d48" result="f"/><feBlend in="SourceGraphic" in2="f" mode="multiply"/>"##,
r##"<feOffset dx="3" dy="2" result="o"/><feMerge><feMergeNode in="o"/><feMergeNode in="SourceGraphic"/></feMerge>"##,
r##"<feComponentTransfer><feFuncR type="linear" slope=".4" intercept=".1"/><feFuncA type="linear" slope=".7"/></feComponentTransfer>"##,
r##"<feMorphology operator="dilate" radius="2"/>"##,
r##"<feConvolveMatrix order="3" kernelMatrix="0 -1 0 -1 5 -1 0 -1 0"/>"##,
r##"<feDropShadow dx="3" dy="2" stdDeviation="2" flood-color="#0f172a"/>"##,
r##"<feColorMatrix type="saturate" values=".2"/>"##,
r##"<feFlood flood-color="#e11d48" flood-opacity=".7" result="f"/><feComposite in="SourceGraphic" in2="f" operator="arithmetic" k1=".5" k2=".5" k3=".25" k4=".1"/>"##,
r##"<feTurbulence baseFrequency=".08" numOctaves="1" seed="5" result="n"/><feDisplacementMap in="SourceGraphic" in2="n" scale="7"/>"##,
r##"<feDiffuseLighting surfaceScale="2" diffuseConstant="1.2" lighting-color="#f8fafc"><feDistantLight azimuth="225" elevation="45"/></feDiffuseLighting>"##,
];
for primitive in primitives {
admit_both_named(
&document(&format!(
r##" <rect width="64" height="64" fill="white"/>
<defs>
<pattern id="p" patternUnits="userSpaceOnUse" width="8" height="8"><rect width="4" height="8" fill="#16a34a"/><rect x="4" width="4" height="8" fill="#2563eb"/></pattern>
<filter id="f" filterUnits="userSpaceOnUse" primitiveUnits="userSpaceOnUse" x="0" y="0" width="64" height="64" color-interpolation-filters="sRGB">{primitive}</filter>
</defs>
<rect x="10" y="10" width="44" height="44" fill="url(#p)" fill-opacity=".57" filter="url(#f)"/>"##
)),
primitive,
);
}

admit_both(&document(
r##" <rect width="64" height="64" fill="white"/>
<defs>
<pattern id="p" patternUnits="userSpaceOnUse" width="8" height="8"><rect width="4" height="8" fill="#16a34a"/></pattern>
<filter id="f" filterUnits="userSpaceOnUse" x="0" y="0" width="64" height="64" color-interpolation-filters="sRGB"><feComponentTransfer><feFuncR type="linear" slope=".4"/></feComponentTransfer></filter>
<rect id="leaf" x="10" y="10" width="44" height="44" fill="context-fill" filter="url(#f)"/>
</defs>
<use href="#leaf" fill="url(#p)"/>"##,
));
}

/// A stylesheet geometry declaration that could round the filtered target is
/// already the `rx`/`ry` property's own named departure. Strict refuses at the
/// sheet; best effort records that declaration as ignored before compiling the
/// sharp rectangle it actually represents. The filter profile must not claim
/// to consume the missing geometry property or duplicate its refusal.
#[test]
fn stylesheet_rect_radius_keeps_its_own_departure_before_pattern_filtering() {
for property in ["rx", "ry"] {
let source = document(&format!(
r##" <style>.rounded {{ {property}: 12px; }}</style>
<rect width="64" height="64" fill="white"/>
<defs>
<pattern id="p" patternUnits="userSpaceOnUse" width="8" height="8"><rect width="4" height="8" fill="#16a34a"/></pattern>
<filter id="f" filterUnits="userSpaceOnUse" x="0" y="0" width="64" height="64"><feGaussianBlur stdDeviation="2"/></filter>
</defs>
<rect class="rounded" x="10" y="10" width="44" height="44" fill="url(#p)" filter="url(#f)"/>"##
));
let strict = SvgFrameSource::from_standalone_svg(source.as_str(), viewport())
.expect_err("strict refuses the unrepresented geometry property");
assert!(
strict
.to_string()
.contains(&format!("stylesheet declares {property}")),
"{property}: {strict}"
);

let best = SvgFrameSource::from_standalone_svg_best_effort(source.as_str(), viewport())
.expect("best effort declares the property departure");
let declared: Vec<_> = best
.degradations()
.iter()
.filter(|degradation| degradation.action() == DegradationAction::DeclarationIgnored)
.collect();
assert_eq!(declared.len(), 1, "{property}: {declared:?}");
assert!(
declared[0]
.reason()
.contains(&format!("stylesheet declares {property}")),
"{property}: {}",
declared[0].reason()
);
assert!(
best.degradations()
.iter()
.all(|degradation| degradation.action() != DegradationAction::Skipped),
"{property}: the own-row declaration, not a duplicate filter skip, owns the gap"
);
resolved_filter(&best.base_frame());
}
}

#[test]
fn context_selected_gradients_reach_the_existing_filter_precision_patrols() {
for (primitive, reason) in [
(
r##"<feDropShadow dx="3" dy="2" stdDeviation="2"/>"##,
"native-shadow source-layer precision boundary",
),
(
r##"<feConvolveMatrix order="3" kernelMatrix="0 -1 0 -1 5 -1 0 -1 0"/>"##,
"convolution-filter paint-server precision boundary",
),
(
r##"<feMorphology operator="dilate" radius="2"/>"##,
"morphology paint-server precision boundary",
),
(
r##"<feComponentTransfer><feFuncR type="linear" slope=".4"/></feComponentTransfer>"##,
"table-filter paint-server precision boundary",
),
(
r##"<feColorMatrix type="saturate" values=".2"/>"##,
"color-matrix source-layer precision boundary",
),
(
r##"<feFlood flood-color="#e11d48" result="f"/><feComposite in="SourceGraphic" in2="f" operator="arithmetic" k1=".5" k2=".5"/>"##,
"translucent-source composition precision boundary",
),
] {
assert_target_skip(
&document(&format!(
r##" <rect width="64" height="64" fill="white"/>
<defs>
<linearGradient id="g"><stop stop-color="#e11d48"/><stop offset="1" stop-color="#2563eb"/></linearGradient>
<filter id="f" filterUnits="userSpaceOnUse" primitiveUnits="userSpaceOnUse" x="0" y="0" width="64" height="64" color-interpolation-filters="sRGB">{primitive}</filter>
<rect id="leaf" x="10" y="10" width="44" height="44" fill="context-fill" filter="url(#f)"/>
</defs>
<use href="#leaf" fill="url(#g)"/>"##
)),
reason,
);
}
}

#[test]
fn filtered_patterns_refuse_curved_and_rounded_source_profiles() {
for (primitive, target) in [
(
r##"<feOffset dx="3" dy="2"/>"##,
r##"<path d="M8 49 C13 5 51 5 56 49 C45 37 19 37 8 49 Z" fill="url(#p)" filter="url(#f)"/>"##,
),
(
r##"<feGaussianBlur stdDeviation="2"/>"##,
r##"<rect x="8" y="10" width="48" height="44" rx="13" fill="url(#p)" filter="url(#f)"/>"##,
),
] {
assert_target_skip(
&document(&format!(
r##" <rect width="64" height="64" fill="white"/>
<defs>
<pattern id="p" patternUnits="userSpaceOnUse" width="8" height="8"><rect width="4" height="8" fill="#16a34a"/></pattern>
<filter id="f" filterUnits="userSpaceOnUse" x="0" y="0" width="64" height="64" color-interpolation-filters="sRGB">{primitive}</filter>
</defs>
{target}"##
)),
"filtered-pattern coverage precision boundary",
);
}
}

#[test]
fn filtered_descendants_refuse_same_scope_clip_and_partial_opacity() {
let defs = r##" <defs>
Expand Down
68 changes: 63 additions & 5 deletions crates/websem/tests/pattern_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
mod support;

use cg::{CGColor, Paint};
use rframe::{Frame, PatternPaint};
use rframe::{Frame, FrameItem, PatternPaint, ScopeEffect};
use support::render_through_n0;
use websem::{DegradationAction, InitialViewport, SvgFrameSource};

Expand Down Expand Up @@ -275,6 +275,68 @@ fn href_beats_xlink_and_the_first_local_content_owner_wins() {
assert_eq!(solid_of(paint), CGColor::from_rgb(255, 0, 0));
}

#[test]
fn filters_inside_pattern_content_keep_the_measured_source_programs() {
let frame = admit_both(&document(
r##" <defs>
<filter id="f" filterUnits="userSpaceOnUse" x="0" y="0" width="16" height="16"
color-interpolation-filters="sRGB"><feGaussianBlur stdDeviation="2"/></filter>
<pattern id="p" patternUnits="userSpaceOnUse" width="16" height="16">
<rect x="4" y="4" width="8" height="8" fill="#16a34a" filter="url(#f)"/>
</pattern>
</defs>
<rect width="64" height="64" fill="url(#p)"/>"##,
));
let source = pattern_of(&frame, 0);
assert!(source.items().iter().any(|item| {
matches!(
item,
FrameItem::ScopeBegin(scope) if matches!(scope.effect, ScopeEffect::Filter(_))
)
}));

let nested = admit_both(&document(
r##" <defs>
<filter id="f" filterUnits="userSpaceOnUse" x="0" y="0" width="16" height="16"
color-interpolation-filters="sRGB"><feGaussianBlur stdDeviation="2"/></filter>
<pattern id="q" patternUnits="userSpaceOnUse" width="4" height="4"><rect width="2" height="4" fill="#e11d48"/></pattern>
<pattern id="p" patternUnits="userSpaceOnUse" width="16" height="16">
<rect width="16" height="16" fill="url(#q)" filter="url(#f)"/>
</pattern>
</defs>
<rect width="64" height="64" fill="url(#p)"/>"##,
));
assert!(pattern_of(&nested, 0).items().iter().any(|item| {
matches!(
item,
FrameItem::ScopeBegin(scope) if matches!(scope.effect, ScopeEffect::Filter(_))
)
}));

let multi_draw = admit_both(&document(
r##" <defs>
<filter id="f" filterUnits="userSpaceOnUse" x="0" y="0" width="16" height="16"
color-interpolation-filters="sRGB"><feGaussianBlur stdDeviation="2"/></filter>
<pattern id="p" patternUnits="userSpaceOnUse" width="16" height="16">
<g filter="url(#f)"><rect width="8" height="16" fill="#e11d48"/><rect x="8" width="8" height="16" fill="#2563eb"/></g>
</pattern>
</defs>
<rect width="64" height="64" fill="url(#p)"/>"##,
));
let source = pattern_of(&multi_draw, 0);
assert_eq!(
source.items().nodes().count(),
2,
"the P2 group-filter profile intentionally carries both source draws"
);
assert!(source.items().iter().any(|item| {
matches!(
item,
FrameItem::ScopeBegin(scope) if matches!(scope.effect, ScopeEffect::Filter(_))
)
}));
}

#[test]
fn every_measured_picture_shader_boundary_refuses_in_both_admissions() {
let cases = [
Expand All @@ -290,10 +352,6 @@ fn every_measured_picture_shader_boundary_refuses_in_both_admissions() {
r##"<pattern id="p" patternUnits="userSpaceOnUse" width="16" height="16"><g opacity=".5"><rect width="8" height="16" fill="red"/><rect x="8" width="8" height="16" fill="blue"/></g></pattern>"##,
"source-effect precision boundary",
),
(
r##"<filter id="f"><feGaussianBlur stdDeviation="2"/></filter><pattern id="p" patternUnits="userSpaceOnUse" width="16" height="16"><rect x="4" y="4" width="8" height="8" fill="red" filter="url(#f)"/></pattern>"##,
"filter composition outside the admitted pattern source slice",
),
(
r##"<pattern id="q" patternUnits="userSpaceOnUse" width="8" height="8"><rect width="8" height="8" fill="red"/></pattern><pattern id="p" patternUnits="userSpaceOnUse" width="16" height="16"><rect width="16" height="16" fill="url(#q)"/><rect width="4" height="4" fill="white"/></pattern>"##,
"composition precision boundary",
Expand Down
Loading
Loading