svg_conform 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2dd3f8b58d869e1375f4ec0e5c0c212d36dc5ae64ab91873ccc896acb9bd42b7
4
- data.tar.gz: 184b07a501eb1bae6fc1691849df9f2fbb0693c340149822b35a0129b47ae2c0
3
+ metadata.gz: 136c672fbb6005495a240d2d8b7f30000fae008cd16fff6bfe51a74e7dbe2262
4
+ data.tar.gz: 679452859b709b2e9ef78916918ea9e58040ebfb382291b8c6543b9062a5ee09
5
5
  SHA512:
6
- metadata.gz: 9db79546df082674dbfbe29bd010e7a676765bd08366e26af6da279d9b56d01a907e259178ef1b3009340aa22eb785098790b49aa1cb81525831d9b60ac8e962
7
- data.tar.gz: '091d729bffcc2716bcce077e5509afbce8971329c1479e86bc8c2968ac540742c161f3a3f369c78fa0f76bdf6f5de84439b6c773349656f1466da4d3175e5ca5'
6
+ metadata.gz: 26a32728202834b61e892d78ef87bb6bae0030e768414985cfe0bfa208da9fa21828035fe6f247225c6cd37a299dff6fe91863c70137c528d7e24a9aade3250e
7
+ data.tar.gz: 47b4ba1d348244438817269403a2b5b09019687024849e691ff6fe4dcbb9d4fed1a322f132758af38f8ba1310fb0666cc5d9dbaeef93192a8edaf19df66bb1c9
@@ -21,7 +21,7 @@ requirements:
21
21
  element_configs:
22
22
  # Direct encoding from svgcheck word_properties.py elements dictionary
23
23
  - tag: "svg"
24
- attributes: ["version", "baseProfile", "width", "viewBox", "preserveAspectRatio", "snapshotTime", "height", "id", "role", "break", "color-rendering", "fill-rule", "overflow", "data-name"]
24
+ attributes: ["version", "baseProfile", "width", "viewBox", "preserveAspectRatio", "snapshotTime", "height", "id", "role", "break", "color-rendering", "fill-rule", "overflow", "data-name", "clip-path", "mask"]
25
25
  required_attributes: ["version", "baseProfile"]
26
26
  attribute_values:
27
27
  version: ["1.2"]
@@ -34,37 +34,37 @@ requirements:
34
34
  attributes: ["id", "role", "shape-rendering", "text-rendering", "buffered-rendering", "visibility", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space", "fill-rule"]
35
35
  allowed_children: ["text"]
36
36
  - tag: "path"
37
- 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"]
37
+ 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"]
38
38
  allowed_children: ["title", "desc"]
39
39
  - tag: "rect"
40
- 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"]
40
+ 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"]
41
41
  allowed_children: ["title", "desc"]
42
42
  - tag: "circle"
43
- attributes: ["cx", "cy", "r", "id", "role", "fill", "style", "transform", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space"]
43
+ 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"]
44
44
  allowed_children: ["title", "desc"]
45
45
  - tag: "line"
46
- attributes: ["x1", "y1", "x2", "y2", "id", "role", "fill", "transform", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space"]
46
+ 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"]
47
47
  allowed_children: ["title", "desc"]
48
48
  - tag: "ellipse"
49
- attributes: ["cx", "cy", "rx", "ry", "id", "role", "fill", "style", "transform", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space"]
49
+ 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"]
50
50
  allowed_children: ["title", "desc"]
51
51
  - tag: "polyline"
52
- attributes: ["points", "id", "role", "fill", "transform", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space"]
52
+ attributes: ["points", "id", "role", "fill", "transform", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space", "clip-path", "mask"]
53
53
  allowed_children: ["title", "desc"]
54
54
  - tag: "polygon"
55
- attributes: ["points", "id", "role", "fill", "style", "transform", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space"]
55
+ attributes: ["points", "id", "role", "fill", "style", "transform", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space", "clip-path", "mask"]
56
56
  allowed_children: ["title", "desc"]
57
57
  - tag: "solidColor"
58
- attributes: ["id", "role", "fill", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space"]
58
+ attributes: ["id", "role", "fill", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space", "clip-path", "mask"]
59
59
  allowed_children: ["title", "desc"]
60
60
  - tag: "textArea"
61
- attributes: ["x", "y", "width", "height", "auto", "id", "role", "fill", "transform", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space"]
61
+ 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"]
62
62
  allowed_children: ["desc", "title", "tspan", "text", "a"]
63
63
  - tag: "text"
64
- 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"]
64
+ 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"]
65
65
  allowed_children: ["desc", "title", "tspan", "text", "a"]
66
66
  - tag: "g"
67
- attributes: ["label", "class", "id", "role", "fill", "style", "transform", "fill-rule", "visibility", "base", "lang", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space", "data-name"]
67
+ 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"]
68
68
  allowed_children: ["title", "path", "rect", "circle", "line", "ellipse", "polyline", "polygon", "solidColor", "textArea", "text", "g", "defs", "use", "a", "tspan", "desc", "image"]
69
69
  - tag: "defs"
70
70
  attributes: ["id", "role", "fill", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space"]
@@ -97,7 +97,7 @@ requirements:
97
97
  attributes: ["type", "media", "title", "id", "class"]
98
98
  allowed_children: []
99
99
  - tag: "use"
100
- attributes: ["x", "y", "href", "xlink:href", "id", "role", "fill", "transform", "fill-rule", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space"]
100
+ 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"]
101
101
  allowed_children: ["title", "desc"]
102
102
  - tag: "a"
103
103
  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:
108
108
  - tag: "tbreak"
109
109
  attributes: ["id", "role", "base", "lang", "class", "rel", "rev", "typeof", "content", "datatype", "resource", "about", "property", "space"]
110
110
  - tag: "image"
111
- attributes: ["x", "y", "width", "height", "href", "xlink:href", "preserveAspectRatio", "id", "class", "style", "transform"]
111
+ attributes: ["x", "y", "width", "height", "href", "xlink:href", "preserveAspectRatio", "id", "class", "style", "transform", "clip-path", "mask"]
112
112
  allowed_children: ["title", "desc"]
113
113
  check_attributes: true
114
114
  check_invalid_attributes: true
data/docs/profiles.adoc CHANGED
@@ -269,6 +269,7 @@ See link:remediation.adoc#namespace-attribute-remediation[NamespaceAttributeReme
269
269
  * **Flexible fonts**: Any font families allowed (unlike RFC 7996 generic-only restriction)
270
270
  * **Flexible styles**: Any CSS styles allowed
271
271
  * **Event handlers allowed**: Supports event attributes (`on*`) for interactivity
272
+ * **Clipping and masking**: `clip-path` and `mask` presentation attributes allowed
272
273
  * **Self-contained resources**: All CSS and fonts must be embedded
273
274
  * **Structural compliance**: Proper namespaces and viewBox required
274
275
  * **No external dependencies**: External CSS and fonts strictly prohibited
@@ -286,6 +287,31 @@ requirements:
286
287
  - "on*" # Allow all event handler attributes
287
288
  ----
288
289
 
290
+ **Clipping and Masking Support**:
291
+
292
+ The metanorma profile allows `clip-path` and `mask` presentation attributes on all SVG elements. These are fundamental features for technical documentation:
293
+
294
+ * **`clip-path`**: Restricts rendering to a defined region (e.g., gaps around wire annotations)
295
+ * **`mask`**: Applies opacity/transparency effects (e.g., hatching patterns, image compositing)
296
+
297
+ These are allowed via the `GLOBAL_PROPERTIES` constant in `AllowedElementsRequirement`:
298
+
299
+ [source,yaml]
300
+ ----
301
+ # clip-path and mask are global properties - allowed on any element
302
+ # No explicit configuration needed - included by default
303
+ ----
304
+
305
+ **Use cases**:
306
+
307
+ * **Wire annotations**: Draw unbroken traces, then clip around text labels
308
+ * **Hatching**: Apply cross-hatching via `<mask>` elements for technical drawings
309
+ * **Compositing**: Build complex graphics from multiple source images with masking
310
+ * **Layer gaps**: Create precise gaps between elements using clipping paths
311
+
312
+ This support was added in response to issue #80 (https://github.com/claricle/svg_conform/issues/80)
313
+ to address real-world technical documentation workflows.
314
+
289
315
  **Requirements**:
290
316
  [source,yaml]
291
317
  ----
@@ -334,6 +360,10 @@ See link:remediation.adoc#viewbox-remediation[ViewboxRemediation], link:remediat
334
360
  |Restricted property set
335
361
  |Any styles allowed
336
362
 
363
+ |Clipping and Masking
364
+ |Not allowed (rejected as invalid attributes)
365
+ |`clip-path` and `mask` allowed on all elements
366
+
337
367
  |External CSS
338
368
  |Implicitly prohibited
339
369
  |Explicitly prohibited with requirement
@@ -308,6 +308,61 @@ hardcoding
308
308
  * Structural invalidity tracking: Invalid elements marked to skip descendant
309
309
  validation
310
310
  * Zero hardcoding: All element and namespace handling configuration-driven
311
+ * Clipping and masking support: `clip-path` and `mask` presentation attributes
312
+ allowed globally on all elements
313
+
314
+ ==== Clipping and Masking Support
315
+
316
+ The `AllowedElementsRequirement` allows `clip-path` and `mask` attributes as global
317
+ presentation attributes on any SVG element. These are defined in `GLOBAL_PROPERTIES`
318
+ and are allowed regardless of element-specific attribute configurations.
319
+
320
+ **What are `clip-path` and `mask`?**
321
+
322
+ * **`clip-path`**: References a `<clipPath>` element to restrict rendering to a
323
+ defined region. Used for creating gaps around annotations, masking out areas,
324
+ or compositing shapes.
325
+
326
+ * **`mask`**: References a `<mask>` element to apply opacity/transparency effects.
327
+ Used for hatching patterns, fading, or advanced compositing.
328
+
329
+ These attributes use URL references (e.g., `clip-path="url(#myClip)"`) to point to
330
+ `<clipPath>` and `<mask>` elements defined in the document.
331
+
332
+ **Example usage**:
333
+
334
+ [source,xml]
335
+ ----
336
+ <svg viewBox="0 0 100 100">
337
+ <defs>
338
+ <clipPath id="clip1">
339
+ <rect x="0" y="0" width="50" height="50"/>
340
+ </clipPath>
341
+ <mask id="mask1">
342
+ <rect x="0" y="0" width="50" height="50" fill="white"/>
343
+ </mask>
344
+ </defs>
345
+ <g clip-path="url(#clip1)" mask="url(#mask1)">
346
+ <rect x="10" y="10" width="30" height="30" fill="red"/>
347
+ </g>
348
+ </svg>
349
+ ----
350
+
351
+ **ID reference validation**: While `clip-path` and `mask` attributes are allowed,
352
+ the `IdReferenceRequirement` still validates that referenced IDs exist. Broken
353
+ references (e.g., `clip-path="url(#nonexistent)"`) will be flagged as errors.
354
+
355
+ **Why allow these attributes?**
356
+
357
+ Clipping and masking are fundamental SVG features used in technical documentation:
358
+
359
+ * **Wire annotations**: Draw traces without breaks, then clip around text
360
+ * **Hatching patterns**: Apply hatching via mask elements for cross-hatching
361
+ * **Image compositing**: Build complex graphics from multiple source images
362
+ * **Layer management**: Control visibility with precision
363
+
364
+ These were not supported in the original svgcheck reference implementation but are
365
+ part of the SVG specification and commonly used in professional technical diagrams.
311
366
 
312
367
  ==== Related remediation
313
368
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SvgConform
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.1"
5
5
  end
@@ -94,6 +94,126 @@ RSpec.describe SvgConform::Requirements::AllowedElementsRequirement do
94
94
  end
95
95
  end
96
96
 
97
+ describe "clip-path and mask global properties" do
98
+ let(:base_requirement) do
99
+ described_class.new(
100
+ id: "test_global_props",
101
+ description: "Test global properties",
102
+ check_attributes: true,
103
+ )
104
+ end
105
+
106
+ it "allows clip-path attribute on any element" do
107
+ svg = <<~SVG
108
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
109
+ <defs>
110
+ <clipPath id="clip1">
111
+ <rect x="0" y="0" width="50" height="50"/>
112
+ </clipPath>
113
+ </defs>
114
+ <g clip-path="url(#clip1)">
115
+ <rect x="10" y="10" width="30" height="30" fill="red"/>
116
+ </g>
117
+ </svg>
118
+ SVG
119
+
120
+ document = SvgConform::Document.from_content(svg)
121
+ context = SvgConform::ValidationContext.new(document, nil)
122
+ base_requirement.validate_document(document, context)
123
+
124
+ # clip-path should be allowed - no attribute errors
125
+ clip_path_errors = context.errors.select { |e| e.message.include?("clip-path") }
126
+ expect(clip_path_errors).to be_empty
127
+ end
128
+
129
+ it "allows mask attribute on any element" do
130
+ svg = <<~SVG
131
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
132
+ <defs>
133
+ <mask id="mask1">
134
+ <rect x="0" y="0" width="50" height="50" fill="white"/>
135
+ </mask>
136
+ </defs>
137
+ <g mask="url(#mask1)">
138
+ <rect x="10" y="10" width="30" height="30" fill="red"/>
139
+ </g>
140
+ </svg>
141
+ SVG
142
+
143
+ document = SvgConform::Document.from_content(svg)
144
+ context = SvgConform::ValidationContext.new(document, nil)
145
+ base_requirement.validate_document(document, context)
146
+
147
+ # mask should be allowed - no attribute errors
148
+ mask_errors = context.errors.select { |e| e.message.include?("mask") }
149
+ expect(mask_errors).to be_empty
150
+ end
151
+
152
+ it "allows both clip-path and mask on same element" do
153
+ svg = <<~SVG
154
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
155
+ <defs>
156
+ <clipPath id="clip1">
157
+ <rect x="0" y="0" width="50" height="50"/>
158
+ </clipPath>
159
+ <mask id="mask1">
160
+ <rect x="0" y="0" width="50" height="50" fill="white"/>
161
+ </mask>
162
+ </defs>
163
+ <g clip-path="url(#clip1)" mask="url(#mask1)">
164
+ <rect x="10" y="10" width="30" height="30" fill="red"/>
165
+ </g>
166
+ </svg>
167
+ SVG
168
+
169
+ document = SvgConform::Document.from_content(svg)
170
+ context = SvgConform::ValidationContext.new(document, nil)
171
+ base_requirement.validate_document(document, context)
172
+
173
+ expect(context.errors).to be_empty
174
+ end
175
+
176
+ it "allows clip-path on svg root element" do
177
+ svg = <<~SVG
178
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" clip-path="url(#rootClip)">
179
+ <defs>
180
+ <clipPath id="rootClip">
181
+ <rect x="0" y="0" width="100" height="100"/>
182
+ </clipPath>
183
+ </defs>
184
+ <rect x="10" y="10" width="80" height="80" fill="red"/>
185
+ </svg>
186
+ SVG
187
+
188
+ document = SvgConform::Document.from_content(svg)
189
+ context = SvgConform::ValidationContext.new(document, nil)
190
+ base_requirement.validate_document(document, context)
191
+
192
+ clip_path_errors = context.errors.select { |e| e.message.include?("clip-path") }
193
+ expect(clip_path_errors).to be_empty
194
+ end
195
+
196
+ it "allows mask on rect element" do
197
+ svg = <<~SVG
198
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
199
+ <defs>
200
+ <mask id="mask1">
201
+ <rect x="0" y="0" width="100" height="100" fill="white"/>
202
+ </mask>
203
+ </defs>
204
+ <rect x="10" y="10" width="80" height="80" fill="red" mask="url(#mask1)"/>
205
+ </svg>
206
+ SVG
207
+
208
+ document = SvgConform::Document.from_content(svg)
209
+ context = SvgConform::ValidationContext.new(document, nil)
210
+ base_requirement.validate_document(document, context)
211
+
212
+ mask_errors = context.errors.select { |e| e.message.include?("mask") }
213
+ expect(mask_errors).to be_empty
214
+ end
215
+ end
216
+
97
217
  describe "configuration" do
98
218
  it "accepts custom allowed elements list" do
99
219
  requirement = described_class.new(
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: svg_conform
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-04-10 00:00:00.000000000 Z
11
+ date: 2026-04-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: lutaml-model