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
28 changes: 14 additions & 14 deletions config/profiles/metanorma.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -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"]
Expand Down Expand Up @@ -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"]
Expand All @@ -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
Expand Down
30 changes: 30 additions & 0 deletions docs/profiles.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 `<mask>` 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]
----
Expand Down Expand Up @@ -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
Expand Down
55 changes: 55 additions & 0 deletions docs/requirements.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<clipPath>` element to restrict rendering to a
defined region. Used for creating gaps around annotations, masking out areas,
or compositing shapes.

* **`mask`**: References a `<mask>` 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
`<clipPath>` and `<mask>` elements defined in the document.

**Example usage**:

[source,xml]
----
<svg viewBox="0 0 100 100">
<defs>
<clipPath id="clip1">
<rect x="0" y="0" width="50" height="50"/>
</clipPath>
<mask id="mask1">
<rect x="0" y="0" width="50" height="50" fill="white"/>
</mask>
</defs>
<g clip-path="url(#clip1)" mask="url(#mask1)">
<rect x="10" y="10" width="30" height="30" fill="red"/>
</g>
</svg>
----

**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

Expand Down
120 changes: 120 additions & 0 deletions spec/svg_conform/requirements/allowed_elements_requirement_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<defs>
<clipPath id="clip1">
<rect x="0" y="0" width="50" height="50"/>
</clipPath>
</defs>
<g clip-path="url(#clip1)">
<rect x="10" y="10" width="30" height="30" fill="red"/>
</g>
</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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<defs>
<mask id="mask1">
<rect x="0" y="0" width="50" height="50" fill="white"/>
</mask>
</defs>
<g mask="url(#mask1)">
<rect x="10" y="10" width="30" height="30" fill="red"/>
</g>
</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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<defs>
<clipPath id="clip1">
<rect x="0" y="0" width="50" height="50"/>
</clipPath>
<mask id="mask1">
<rect x="0" y="0" width="50" height="50" fill="white"/>
</mask>
</defs>
<g clip-path="url(#clip1)" mask="url(#mask1)">
<rect x="10" y="10" width="30" height="30" fill="red"/>
</g>
</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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" clip-path="url(#rootClip)">
<defs>
<clipPath id="rootClip">
<rect x="0" y="0" width="100" height="100"/>
</clipPath>
</defs>
<rect x="10" y="10" width="80" height="80" fill="red"/>
</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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<defs>
<mask id="mask1">
<rect x="0" y="0" width="100" height="100" fill="white"/>
</mask>
</defs>
<rect x="10" y="10" width="80" height="80" fill="red" mask="url(#mask1)"/>
</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(
Expand Down
Loading