diff --git a/config/profiles/metanorma.yml b/config/profiles/metanorma.yml index 14732ce..4f03aee 100644 --- a/config/profiles/metanorma.yml +++ b/config/profiles/metanorma.yml @@ -21,7 +21,7 @@ requirements: element_configs: # Direct encoding from svgcheck word_properties.py elements dictionary - tag: "svg" - attributes: ["version", "baseProfile", "width", "viewBox", "preserveAspectRatio", "snapshotTime", "height", "id", "role", "break", "color-rendering", "fill-rule", "overflow", "data-name"] + attributes: ["version", "baseProfile", "width", "viewBox", "preserveAspectRatio", "snapshotTime", "height", "id", "role", "break", "color-rendering", "fill-rule", "overflow", "data-name", "clip-path", "mask"] required_attributes: ["version", "baseProfile"] attribute_values: version: ["1.2"] @@ -34,37 +34,37 @@ requirements: attributes: ["id", "role", "shape-rendering", "text-rendering", "buffered-rendering", "visibility", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space", "fill-rule"] allowed_children: ["text"] - tag: "path" - attributes: ["d", "pathLength", "stroke-miterlimit", "id", "role", "fill", "style", "transform", "font-size", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space"] + attributes: ["d", "pathLength", "stroke-miterlimit", "id", "role", "fill", "style", "transform", "font-size", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space", "clip-path", "mask"] allowed_children: ["title", "desc"] - tag: "rect" - attributes: ["x", "y", "width", "height", "rx", "ry", "stroke-miterlimit", "id", "role", "fill", "style", "transform", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space"] + attributes: ["x", "y", "width", "height", "rx", "ry", "stroke-miterlimit", "id", "role", "fill", "style", "transform", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space", "clip-path", "mask"] allowed_children: ["title", "desc"] - tag: "circle" - attributes: ["cx", "cy", "r", "id", "role", "fill", "style", "transform", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space"] + attributes: ["cx", "cy", "r", "id", "role", "fill", "style", "transform", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space", "clip-path", "mask"] allowed_children: ["title", "desc"] - tag: "line" - attributes: ["x1", "y1", "x2", "y2", "id", "role", "fill", "transform", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space"] + attributes: ["x1", "y1", "x2", "y2", "id", "role", "fill", "transform", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space", "clip-path", "mask"] allowed_children: ["title", "desc"] - tag: "ellipse" - attributes: ["cx", "cy", "rx", "ry", "id", "role", "fill", "style", "transform", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space"] + attributes: ["cx", "cy", "rx", "ry", "id", "role", "fill", "style", "transform", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space", "clip-path", "mask"] allowed_children: ["title", "desc"] - tag: "polyline" - attributes: ["points", "id", "role", "fill", "transform", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space"] + attributes: ["points", "id", "role", "fill", "transform", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space", "clip-path", "mask"] allowed_children: ["title", "desc"] - tag: "polygon" - attributes: ["points", "id", "role", "fill", "style", "transform", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space"] + attributes: ["points", "id", "role", "fill", "style", "transform", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space", "clip-path", "mask"] allowed_children: ["title", "desc"] - tag: "solidColor" - attributes: ["id", "role", "fill", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space"] + attributes: ["id", "role", "fill", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space", "clip-path", "mask"] allowed_children: ["title", "desc"] - tag: "textArea" - attributes: ["x", "y", "width", "height", "auto", "id", "role", "fill", "transform", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space"] + attributes: ["x", "y", "width", "height", "auto", "id", "role", "fill", "transform", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space", "clip-path", "mask"] allowed_children: ["desc", "title", "tspan", "text", "a"] - tag: "text" - attributes: ["x", "y", "rotate", "id", "role", "fill", "style", "transform", "font-size", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space", "font-stretch", "writing-mode", "text-decoration"] + attributes: ["x", "y", "rotate", "id", "role", "fill", "style", "transform", "font-size", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space", "font-stretch", "writing-mode", "text-decoration", "clip-path", "mask"] allowed_children: ["desc", "title", "tspan", "text", "a"] - tag: "g" - attributes: ["label", "class", "id", "role", "fill", "style", "transform", "fill-rule", "visibility", "base", "lang", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space", "data-name"] + attributes: ["label", "class", "id", "role", "fill", "style", "transform", "fill-rule", "visibility", "base", "lang", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space", "data-name", "clip-path", "mask"] allowed_children: ["title", "path", "rect", "circle", "line", "ellipse", "polyline", "polygon", "solidColor", "textArea", "text", "g", "defs", "use", "a", "tspan", "desc", "image"] - tag: "defs" attributes: ["id", "role", "fill", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space"] @@ -97,7 +97,7 @@ requirements: attributes: ["type", "media", "title", "id", "class"] allowed_children: [] - tag: "use" - attributes: ["x", "y", "href", "xlink:href", "id", "role", "fill", "transform", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space"] + attributes: ["x", "y", "href", "xlink:href", "id", "role", "fill", "transform", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space", "clip-path", "mask"] allowed_children: ["title", "desc"] - tag: "a" attributes: ["id", "role", "fill", "transform", "fill-rule", "target", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space"] @@ -108,7 +108,7 @@ requirements: - tag: "tbreak" attributes: ["id", "role", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space"] - tag: "image" - attributes: ["x", "y", "width", "height", "href", "xlink:href", "preserveAspectRatio", "id", "class", "style", "transform"] + attributes: ["x", "y", "width", "height", "href", "xlink:href", "preserveAspectRatio", "id", "class", "style", "transform", "clip-path", "mask"] allowed_children: ["title", "desc"] check_attributes: true check_invalid_attributes: true diff --git a/docs/profiles.adoc b/docs/profiles.adoc index a99c802..7b27b05 100644 --- a/docs/profiles.adoc +++ b/docs/profiles.adoc @@ -269,6 +269,7 @@ See link:remediation.adoc#namespace-attribute-remediation[NamespaceAttributeReme * **Flexible fonts**: Any font families allowed (unlike RFC 7996 generic-only restriction) * **Flexible styles**: Any CSS styles allowed * **Event handlers allowed**: Supports event attributes (`on*`) for interactivity +* **Clipping and masking**: `clip-path` and `mask` presentation attributes allowed * **Self-contained resources**: All CSS and fonts must be embedded * **Structural compliance**: Proper namespaces and viewBox required * **No external dependencies**: External CSS and fonts strictly prohibited @@ -286,6 +287,31 @@ requirements: - "on*" # Allow all event handler attributes ---- +**Clipping and Masking Support**: + +The metanorma profile allows `clip-path` and `mask` presentation attributes on all SVG elements. These are fundamental features for technical documentation: + +* **`clip-path`**: Restricts rendering to a defined region (e.g., gaps around wire annotations) +* **`mask`**: Applies opacity/transparency effects (e.g., hatching patterns, image compositing) + +These are allowed via the `GLOBAL_PROPERTIES` constant in `AllowedElementsRequirement`: + +[source,yaml] +---- +# clip-path and mask are global properties - allowed on any element +# No explicit configuration needed - included by default +---- + +**Use cases**: + +* **Wire annotations**: Draw unbroken traces, then clip around text labels +* **Hatching**: Apply cross-hatching via `` elements for technical drawings +* **Compositing**: Build complex graphics from multiple source images with masking +* **Layer gaps**: Create precise gaps between elements using clipping paths + +This support was added in response to issue #80 (https://github.com/claricle/svg_conform/issues/80) +to address real-world technical documentation workflows. + **Requirements**: [source,yaml] ---- @@ -334,6 +360,10 @@ See link:remediation.adoc#viewbox-remediation[ViewboxRemediation], link:remediat |Restricted property set |Any styles allowed +|Clipping and Masking +|Not allowed (rejected as invalid attributes) +|`clip-path` and `mask` allowed on all elements + |External CSS |Implicitly prohibited |Explicitly prohibited with requirement diff --git a/docs/requirements.adoc b/docs/requirements.adoc index ddfe2be..0c1de18 100644 --- a/docs/requirements.adoc +++ b/docs/requirements.adoc @@ -308,6 +308,61 @@ hardcoding * Structural invalidity tracking: Invalid elements marked to skip descendant validation * Zero hardcoding: All element and namespace handling configuration-driven +* Clipping and masking support: `clip-path` and `mask` presentation attributes +allowed globally on all elements + +==== Clipping and Masking Support + +The `AllowedElementsRequirement` allows `clip-path` and `mask` attributes as global +presentation attributes on any SVG element. These are defined in `GLOBAL_PROPERTIES` +and are allowed regardless of element-specific attribute configurations. + +**What are `clip-path` and `mask`?** + +* **`clip-path`**: References a `` element to restrict rendering to a + defined region. Used for creating gaps around annotations, masking out areas, + or compositing shapes. + +* **`mask`**: References a `` element to apply opacity/transparency effects. + Used for hatching patterns, fading, or advanced compositing. + +These attributes use URL references (e.g., `clip-path="url(#myClip)"`) to point to +`` and `` elements defined in the document. + +**Example usage**: + +[source,xml] +---- + + + + + + + + + + + + + +---- + +**ID reference validation**: While `clip-path` and `mask` attributes are allowed, +the `IdReferenceRequirement` still validates that referenced IDs exist. Broken +references (e.g., `clip-path="url(#nonexistent)"`) will be flagged as errors. + +**Why allow these attributes?** + +Clipping and masking are fundamental SVG features used in technical documentation: + +* **Wire annotations**: Draw traces without breaks, then clip around text +* **Hatching patterns**: Apply hatching via mask elements for cross-hatching +* **Image compositing**: Build complex graphics from multiple source images +* **Layer management**: Control visibility with precision + +These were not supported in the original svgcheck reference implementation but are +part of the SVG specification and commonly used in professional technical diagrams. ==== Related remediation diff --git a/spec/svg_conform/requirements/allowed_elements_requirement_spec.rb b/spec/svg_conform/requirements/allowed_elements_requirement_spec.rb index db84246..b6c1d52 100644 --- a/spec/svg_conform/requirements/allowed_elements_requirement_spec.rb +++ b/spec/svg_conform/requirements/allowed_elements_requirement_spec.rb @@ -94,6 +94,126 @@ end end + describe "clip-path and mask global properties" do + let(:base_requirement) do + described_class.new( + id: "test_global_props", + description: "Test global properties", + check_attributes: true, + ) + end + + it "allows clip-path attribute on any element" do + svg = <<~SVG + + + + + + + + + + + SVG + + document = SvgConform::Document.from_content(svg) + context = SvgConform::ValidationContext.new(document, nil) + base_requirement.validate_document(document, context) + + # clip-path should be allowed - no attribute errors + clip_path_errors = context.errors.select { |e| e.message.include?("clip-path") } + expect(clip_path_errors).to be_empty + end + + it "allows mask attribute on any element" do + svg = <<~SVG + + + + + + + + + + + SVG + + document = SvgConform::Document.from_content(svg) + context = SvgConform::ValidationContext.new(document, nil) + base_requirement.validate_document(document, context) + + # mask should be allowed - no attribute errors + mask_errors = context.errors.select { |e| e.message.include?("mask") } + expect(mask_errors).to be_empty + end + + it "allows both clip-path and mask on same element" do + svg = <<~SVG + + + + + + + + + + + + + + SVG + + document = SvgConform::Document.from_content(svg) + context = SvgConform::ValidationContext.new(document, nil) + base_requirement.validate_document(document, context) + + expect(context.errors).to be_empty + end + + it "allows clip-path on svg root element" do + svg = <<~SVG + + + + + + + + + SVG + + document = SvgConform::Document.from_content(svg) + context = SvgConform::ValidationContext.new(document, nil) + base_requirement.validate_document(document, context) + + clip_path_errors = context.errors.select { |e| e.message.include?("clip-path") } + expect(clip_path_errors).to be_empty + end + + it "allows mask on rect element" do + svg = <<~SVG + + + + + + + + + SVG + + document = SvgConform::Document.from_content(svg) + context = SvgConform::ValidationContext.new(document, nil) + base_requirement.validate_document(document, context) + + mask_errors = context.errors.select { |e| e.message.include?("mask") } + expect(mask_errors).to be_empty + end + end + describe "configuration" do it "accepts custom allowed elements list" do requirement = described_class.new(