@emasoft/svg-matrix 1.0.26 → 1.0.28

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.
@@ -0,0 +1,641 @@
1
+ /**
2
+ * Inkscape/Sodipodi Support Module
3
+ *
4
+ * Provides utilities for preserving and manipulating Inkscape-specific SVG features
5
+ * including layers, guides, document settings, and arc parameters.
6
+ *
7
+ * @module inkscape-support
8
+ */
9
+
10
+ import { SVGElement } from './svg-parser.js';
11
+
12
+ // Inkscape namespace URIs
13
+ export const INKSCAPE_NS = 'http://www.inkscape.org/namespaces/inkscape';
14
+ export const SODIPODI_NS = 'http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd';
15
+
16
+ // Inkscape-specific element and attribute prefixes
17
+ export const INKSCAPE_PREFIXES = ['inkscape', 'sodipodi'];
18
+
19
+ /**
20
+ * Check if an element is an Inkscape layer.
21
+ * Inkscape uses `<g inkscape:groupmode="layer">` for layers.
22
+ *
23
+ * @param {Object} element - SVG element to check
24
+ * @returns {boolean} True if the element is an Inkscape layer
25
+ */
26
+ export function isInkscapeLayer(element) {
27
+ if (!element || element.tagName !== 'g') return false;
28
+ return element.getAttribute('inkscape:groupmode') === 'layer';
29
+ }
30
+
31
+ /**
32
+ * Get the label of an Inkscape layer.
33
+ *
34
+ * @param {Object} element - Inkscape layer element
35
+ * @returns {string|null} Layer label or null if not set
36
+ */
37
+ export function getLayerLabel(element) {
38
+ return element?.getAttribute('inkscape:label') || null;
39
+ }
40
+
41
+ /**
42
+ * Find all Inkscape layers in a document.
43
+ *
44
+ * @param {Object} doc - Parsed SVG document
45
+ * @returns {Array<{element: Object, label: string|null, id: string|null}>} Array of layer info objects
46
+ */
47
+ export function findLayers(doc) {
48
+ const layers = [];
49
+
50
+ const walk = (el) => {
51
+ if (!el || !el.children) return;
52
+ if (isInkscapeLayer(el)) {
53
+ layers.push({
54
+ element: el,
55
+ label: getLayerLabel(el),
56
+ id: el.getAttribute('id')
57
+ });
58
+ }
59
+ for (const child of el.children) {
60
+ walk(child);
61
+ }
62
+ };
63
+
64
+ walk(doc);
65
+ return layers;
66
+ }
67
+
68
+ /**
69
+ * Get sodipodi:namedview document settings.
70
+ * Contains Inkscape document settings like page color, grid, guides.
71
+ *
72
+ * @param {Object} doc - Parsed SVG document
73
+ * @returns {Object|null} Named view settings or null if not found
74
+ */
75
+ export function getNamedViewSettings(doc) {
76
+ // Find namedview element - may be direct child or nested
77
+ let namedview = null;
78
+
79
+ const findNamedview = (el) => {
80
+ if (!el) return;
81
+ if (el.tagName === 'sodipodi:namedview') {
82
+ namedview = el;
83
+ return;
84
+ }
85
+ if (el.children) {
86
+ for (const child of el.children) {
87
+ findNamedview(child);
88
+ if (namedview) return;
89
+ }
90
+ }
91
+ };
92
+
93
+ findNamedview(doc);
94
+ if (!namedview) return null;
95
+
96
+ return {
97
+ pagecolor: namedview.getAttribute('pagecolor'),
98
+ bordercolor: namedview.getAttribute('bordercolor'),
99
+ borderopacity: namedview.getAttribute('borderopacity'),
100
+ showgrid: namedview.getAttribute('showgrid'),
101
+ showguides: namedview.getAttribute('showguides'),
102
+ guidetolerance: namedview.getAttribute('guidetolerance'),
103
+ inkscapeZoom: namedview.getAttribute('inkscape:zoom'),
104
+ inkscapeCx: namedview.getAttribute('inkscape:cx'),
105
+ inkscapeCy: namedview.getAttribute('inkscape:cy'),
106
+ inkscapeWindowWidth: namedview.getAttribute('inkscape:window-width'),
107
+ inkscapeWindowHeight: namedview.getAttribute('inkscape:window-height'),
108
+ inkscapeCurrentLayer: namedview.getAttribute('inkscape:current-layer')
109
+ };
110
+ }
111
+
112
+ /**
113
+ * Find all sodipodi:guide elements (guidelines).
114
+ *
115
+ * @param {Object} doc - Parsed SVG document
116
+ * @returns {Array<{position: string, orientation: string, id: string|null}>} Array of guide info
117
+ */
118
+ export function findGuides(doc) {
119
+ const guides = [];
120
+
121
+ const walk = (el) => {
122
+ if (!el || !el.children) return;
123
+ if (el.tagName === 'sodipodi:guide') {
124
+ guides.push({
125
+ position: el.getAttribute('position'),
126
+ orientation: el.getAttribute('orientation'),
127
+ id: el.getAttribute('id'),
128
+ inkscapeColor: el.getAttribute('inkscape:color'),
129
+ inkscapeLabel: el.getAttribute('inkscape:label')
130
+ });
131
+ }
132
+ for (const child of el.children) {
133
+ walk(child);
134
+ }
135
+ };
136
+
137
+ walk(doc);
138
+ return guides;
139
+ }
140
+
141
+ /**
142
+ * Get sodipodi arc parameters from a path element.
143
+ * Inkscape stores original arc parameters for shapes converted from ellipses.
144
+ *
145
+ * @param {Object} element - SVG element (typically a path)
146
+ * @returns {Object|null} Arc parameters or null if not an arc
147
+ */
148
+ export function getArcParameters(element) {
149
+ if (!element) return null;
150
+
151
+ const type = element.getAttribute('sodipodi:type');
152
+ if (type !== 'arc') return null;
153
+
154
+ return {
155
+ type: 'arc',
156
+ cx: element.getAttribute('sodipodi:cx'),
157
+ cy: element.getAttribute('sodipodi:cy'),
158
+ rx: element.getAttribute('sodipodi:rx'),
159
+ ry: element.getAttribute('sodipodi:ry'),
160
+ start: element.getAttribute('sodipodi:start'),
161
+ end: element.getAttribute('sodipodi:end'),
162
+ open: element.getAttribute('sodipodi:open')
163
+ };
164
+ }
165
+
166
+ /**
167
+ * Get node types from a path element.
168
+ * Inkscape stores node types (corner, smooth, symmetric, auto) for path editing.
169
+ *
170
+ * @param {Object} element - SVG path element
171
+ * @returns {string|null} Node types string (c=corner, s=smooth, z=symmetric, a=auto)
172
+ */
173
+ export function getNodeTypes(element) {
174
+ return element?.getAttribute('sodipodi:nodetypes') || null;
175
+ }
176
+
177
+ /**
178
+ * Get export settings from an element.
179
+ *
180
+ * @param {Object} element - SVG element
181
+ * @returns {Object|null} Export settings or null if not set
182
+ */
183
+ export function getExportSettings(element) {
184
+ if (!element) return null;
185
+
186
+ const filename = element.getAttribute('inkscape:export-filename');
187
+ const xdpi = element.getAttribute('inkscape:export-xdpi');
188
+ const ydpi = element.getAttribute('inkscape:export-ydpi');
189
+
190
+ if (!filename && !xdpi && !ydpi) return null;
191
+
192
+ return {
193
+ filename,
194
+ xdpi: xdpi ? parseFloat(xdpi) : null,
195
+ ydpi: ydpi ? parseFloat(ydpi) : null
196
+ };
197
+ }
198
+
199
+ /**
200
+ * Check if element is part of a tiled clone.
201
+ *
202
+ * @param {Object} element - SVG element
203
+ * @returns {boolean} True if element is a tiled clone
204
+ */
205
+ export function isTiledClone(element) {
206
+ return element?.hasAttribute('inkscape:tiled-clone-of') || false;
207
+ }
208
+
209
+ /**
210
+ * Get tiled clone source ID.
211
+ *
212
+ * @param {Object} element - SVG element
213
+ * @returns {string|null} Source element ID or null
214
+ */
215
+ export function getTiledCloneSource(element) {
216
+ return element?.getAttribute('inkscape:tiled-clone-of') || null;
217
+ }
218
+
219
+ /**
220
+ * Check if document has Inkscape namespaces declared.
221
+ *
222
+ * @param {Object} doc - Parsed SVG document
223
+ * @returns {boolean} True if Inkscape namespaces are present
224
+ */
225
+ export function hasInkscapeNamespaces(doc) {
226
+ const svg = doc.documentElement || doc;
227
+ const hasInkscape = svg.getAttribute('xmlns:inkscape') === INKSCAPE_NS;
228
+ const hasSodipodi = svg.getAttribute('xmlns:sodipodi') === SODIPODI_NS;
229
+ return hasInkscape || hasSodipodi;
230
+ }
231
+
232
+ /**
233
+ * Ensure Inkscape namespace declarations are present.
234
+ * Adds xmlns:inkscape and xmlns:sodipodi if missing.
235
+ *
236
+ * @param {Object} doc - Parsed SVG document
237
+ * @returns {Object} The document (modified in place)
238
+ */
239
+ export function ensureInkscapeNamespaces(doc) {
240
+ const svg = doc.documentElement || doc;
241
+
242
+ if (!svg.getAttribute('xmlns:inkscape')) {
243
+ svg.setAttribute('xmlns:inkscape', INKSCAPE_NS);
244
+ }
245
+ if (!svg.getAttribute('xmlns:sodipodi')) {
246
+ svg.setAttribute('xmlns:sodipodi', SODIPODI_NS);
247
+ }
248
+
249
+ return doc;
250
+ }
251
+
252
+ // ============================================================================
253
+ // LAYER EXTRACTION
254
+ // ============================================================================
255
+
256
+ /**
257
+ * Find all IDs referenced by an element and its descendants.
258
+ * Looks for url(#id) references in fill, stroke, clip-path, mask, marker-*, filter, etc.
259
+ * Also checks xlink:href and href attributes for #id references.
260
+ *
261
+ * @param {Object} element - SVG element to scan
262
+ * @returns {Set<string>} Set of referenced IDs
263
+ */
264
+ export function findReferencedIds(element) {
265
+ const ids = new Set();
266
+
267
+ // Attributes that can contain url(#id) references
268
+ const urlRefAttrs = [
269
+ 'fill', 'stroke', 'clip-path', 'mask', 'filter',
270
+ 'marker-start', 'marker-mid', 'marker-end',
271
+ 'fill-opacity', 'stroke-opacity' // Sometimes reference paint servers
272
+ ];
273
+
274
+ // Attributes that can contain #id or url(#id) references
275
+ const hrefAttrs = ['href', 'xlink:href'];
276
+
277
+ const extractUrlId = (value) => {
278
+ if (!value) return null;
279
+ // Match url(#id) or url("#id")
280
+ const match = value.match(/url\(["']?#([^"')]+)["']?\)/);
281
+ return match ? match[1] : null;
282
+ };
283
+
284
+ const extractHrefId = (value) => {
285
+ if (!value) return null;
286
+ // Match #id references
287
+ if (value.startsWith('#')) {
288
+ return value.slice(1);
289
+ }
290
+ return null;
291
+ };
292
+
293
+ const walk = (el) => {
294
+ if (!el) return;
295
+
296
+ // Check url() references
297
+ for (const attr of urlRefAttrs) {
298
+ const id = extractUrlId(el.getAttribute?.(attr));
299
+ if (id) ids.add(id);
300
+ }
301
+
302
+ // Check href references
303
+ for (const attr of hrefAttrs) {
304
+ const id = extractHrefId(el.getAttribute?.(attr));
305
+ if (id) ids.add(id);
306
+ }
307
+
308
+ // Check style attribute for url() references
309
+ const style = el.getAttribute?.('style');
310
+ if (style) {
311
+ const urlMatches = style.matchAll(/url\(["']?#([^"')]+)["']?\)/g);
312
+ for (const match of urlMatches) {
313
+ ids.add(match[1]);
314
+ }
315
+ }
316
+
317
+ // Recurse into children
318
+ if (el.children) {
319
+ for (const child of el.children) {
320
+ walk(child);
321
+ }
322
+ }
323
+ };
324
+
325
+ walk(element);
326
+ return ids;
327
+ }
328
+
329
+ /**
330
+ * Build a map of all defs elements by their ID.
331
+ *
332
+ * @param {Object} doc - Parsed SVG document
333
+ * @returns {Map<string, Object>} Map of ID to element
334
+ */
335
+ export function buildDefsMap(doc) {
336
+ const defsMap = new Map();
337
+
338
+ const walk = (el) => {
339
+ if (!el) return;
340
+
341
+ // If element has an ID, add to map
342
+ const id = el.getAttribute?.('id');
343
+ if (id) {
344
+ defsMap.set(id, el);
345
+ }
346
+
347
+ // Recurse
348
+ if (el.children) {
349
+ for (const child of el.children) {
350
+ walk(child);
351
+ }
352
+ }
353
+ };
354
+
355
+ // Only scan defs elements for efficiency
356
+ const findDefs = (el) => {
357
+ if (!el) return;
358
+ if (el.tagName === 'defs') {
359
+ walk(el);
360
+ }
361
+ if (el.children) {
362
+ for (const child of el.children) {
363
+ findDefs(child);
364
+ }
365
+ }
366
+ };
367
+
368
+ findDefs(doc);
369
+ return defsMap;
370
+ }
371
+
372
+ /**
373
+ * Recursively resolve all dependencies for a set of IDs.
374
+ * Defs elements can reference other defs (e.g., gradient with xlink:href to another gradient).
375
+ *
376
+ * @param {Set<string>} initialIds - Initial set of IDs to resolve
377
+ * @param {Map<string, Object>} defsMap - Map of all defs elements
378
+ * @returns {Set<string>} Complete set of IDs including all nested dependencies
379
+ */
380
+ export function resolveDefsDependencies(initialIds, defsMap) {
381
+ const resolved = new Set();
382
+ const toProcess = [...initialIds];
383
+
384
+ while (toProcess.length > 0) {
385
+ const id = toProcess.pop();
386
+ if (resolved.has(id)) continue;
387
+
388
+ const element = defsMap.get(id);
389
+ if (!element) continue;
390
+
391
+ resolved.add(id);
392
+
393
+ // Find references within this def element
394
+ const nestedRefs = findReferencedIds(element);
395
+ for (const nestedId of nestedRefs) {
396
+ if (!resolved.has(nestedId) && defsMap.has(nestedId)) {
397
+ toProcess.push(nestedId);
398
+ }
399
+ }
400
+ }
401
+
402
+ return resolved;
403
+ }
404
+
405
+ /**
406
+ * Deep clone an SVG element and all its children using proper SVGElement class.
407
+ *
408
+ * @param {Object} element - Element to clone
409
+ * @returns {SVGElement} Cloned element with serialize() method
410
+ */
411
+ export function cloneElement(element) {
412
+ if (!element) return null;
413
+
414
+ // Get attributes as plain object
415
+ const attrs = {};
416
+ if (element._attributes) {
417
+ Object.assign(attrs, element._attributes);
418
+ } else if (element.getAttributeNames) {
419
+ for (const name of element.getAttributeNames()) {
420
+ attrs[name] = element.getAttribute(name);
421
+ }
422
+ }
423
+
424
+ // Clone children recursively
425
+ const clonedChildren = [];
426
+ if (element.children) {
427
+ for (const child of element.children) {
428
+ const clonedChild = cloneElement(child);
429
+ if (clonedChild) {
430
+ clonedChildren.push(clonedChild);
431
+ }
432
+ }
433
+ }
434
+
435
+ // Create proper SVGElement with serialize() method
436
+ const clone = new SVGElement(
437
+ element.tagName,
438
+ attrs,
439
+ clonedChildren,
440
+ element.textContent || null
441
+ );
442
+
443
+ return clone;
444
+ }
445
+
446
+ /**
447
+ * Extract a single layer as a standalone SVG document.
448
+ * Includes only the defs elements that are referenced by the layer.
449
+ *
450
+ * @param {Object} doc - Source parsed SVG document
451
+ * @param {Object|string} layerOrId - Layer element or layer ID to extract
452
+ * @param {Object} [options] - Options
453
+ * @param {boolean} [options.includeHiddenLayers=false] - Include hidden layers in output
454
+ * @param {boolean} [options.preserveTransform=true] - Preserve layer transform attribute
455
+ * @returns {{svg: SVGElement, layerInfo: {id: string, label: string}}} Extracted SVG and layer info
456
+ */
457
+ export function extractLayer(doc, layerOrId, options = {}) {
458
+ const { preserveTransform = true } = options;
459
+
460
+ // Find the layer element
461
+ let layer;
462
+ if (typeof layerOrId === 'string') {
463
+ const layers = findLayers(doc);
464
+ const found = layers.find(l => l.id === layerOrId || l.label === layerOrId);
465
+ if (!found) {
466
+ throw new Error(`Layer not found: ${layerOrId}`);
467
+ }
468
+ layer = found.element;
469
+ } else {
470
+ layer = layerOrId;
471
+ }
472
+
473
+ if (!isInkscapeLayer(layer)) {
474
+ throw new Error('Element is not an Inkscape layer');
475
+ }
476
+
477
+ // Get SVG root element
478
+ const svgRoot = doc.documentElement || doc;
479
+
480
+ // Build defs map from source document
481
+ const defsMap = buildDefsMap(doc);
482
+
483
+ // Find all IDs referenced by this layer
484
+ const referencedIds = findReferencedIds(layer);
485
+
486
+ // Resolve all nested dependencies
487
+ const requiredDefIds = resolveDefsDependencies(referencedIds, defsMap);
488
+
489
+ // Get SVG root attributes
490
+ const svgAttrs = {};
491
+ if (svgRoot._attributes) {
492
+ Object.assign(svgAttrs, svgRoot._attributes);
493
+ } else if (svgRoot.getAttributeNames) {
494
+ for (const name of svgRoot.getAttributeNames()) {
495
+ svgAttrs[name] = svgRoot.getAttribute(name);
496
+ }
497
+ }
498
+
499
+ // Build children array for new SVG
500
+ const svgChildren = [];
501
+
502
+ // Create defs element with required definitions
503
+ if (requiredDefIds.size > 0) {
504
+ const defsChildren = [];
505
+ for (const id of requiredDefIds) {
506
+ const defElement = defsMap.get(id);
507
+ if (defElement) {
508
+ defsChildren.push(cloneElement(defElement));
509
+ }
510
+ }
511
+ const newDefs = new SVGElement('defs', {}, defsChildren, null);
512
+ svgChildren.push(newDefs);
513
+ }
514
+
515
+ // Clone the layer
516
+ const clonedLayer = cloneElement(layer);
517
+
518
+ // Optionally remove the transform
519
+ if (!preserveTransform && clonedLayer._attributes) {
520
+ delete clonedLayer._attributes.transform;
521
+ }
522
+
523
+ svgChildren.push(clonedLayer);
524
+
525
+ // Create new SVG document using SVGElement
526
+ const newSvg = new SVGElement('svg', svgAttrs, svgChildren, null);
527
+
528
+ // Get layer info
529
+ const layerInfo = {
530
+ id: layer.getAttribute('id'),
531
+ label: getLayerLabel(layer)
532
+ };
533
+
534
+ return { svg: newSvg, layerInfo };
535
+ }
536
+
537
+ /**
538
+ * Extract all layers from an Inkscape SVG as separate documents.
539
+ *
540
+ * @param {Object} doc - Source parsed SVG document
541
+ * @param {Object} [options] - Options
542
+ * @param {boolean} [options.includeHidden=false] - Include hidden layers (display:none or visibility:hidden)
543
+ * @param {boolean} [options.preserveTransform=true] - Preserve layer transform attributes
544
+ * @returns {Array<{svg: Object, layerInfo: {id: string, label: string}}>} Array of extracted SVGs
545
+ */
546
+ export function extractAllLayers(doc, options = {}) {
547
+ const { includeHidden = false } = options;
548
+ const layers = findLayers(doc);
549
+ const results = [];
550
+
551
+ for (const layerData of layers) {
552
+ const layer = layerData.element;
553
+
554
+ // Skip hidden layers unless requested
555
+ if (!includeHidden) {
556
+ const style = layer.getAttribute('style') || '';
557
+ const display = layer.getAttribute('display');
558
+ const visibility = layer.getAttribute('visibility');
559
+
560
+ if (display === 'none' ||
561
+ visibility === 'hidden' ||
562
+ style.includes('display:none') ||
563
+ style.includes('visibility:hidden')) {
564
+ continue;
565
+ }
566
+ }
567
+
568
+ try {
569
+ const extracted = extractLayer(doc, layer, options);
570
+ results.push(extracted);
571
+ } catch (e) {
572
+ // Skip layers that fail to extract
573
+ console.warn(`Failed to extract layer ${layerData.id || layerData.label}: ${e.message}`);
574
+ }
575
+ }
576
+
577
+ return results;
578
+ }
579
+
580
+ /**
581
+ * Get a summary of shared resources between layers.
582
+ * Useful for understanding what defs are shared across layers.
583
+ *
584
+ * @param {Object} doc - Parsed SVG document
585
+ * @returns {Object} Summary of shared resources
586
+ */
587
+ export function analyzeLayerDependencies(doc) {
588
+ const layers = findLayers(doc);
589
+ const defsMap = buildDefsMap(doc);
590
+ const layerRefs = new Map(); // layer ID -> Set of referenced def IDs
591
+ const defUsage = new Map(); // def ID -> Set of layer IDs that use it
592
+
593
+ for (const layerData of layers) {
594
+ const layer = layerData.element;
595
+ const layerId = layerData.id || layerData.label || 'unnamed';
596
+
597
+ // Find refs for this layer
598
+ const refs = findReferencedIds(layer);
599
+ const resolved = resolveDefsDependencies(refs, defsMap);
600
+
601
+ layerRefs.set(layerId, resolved);
602
+
603
+ // Track which defs are used by which layers
604
+ for (const defId of resolved) {
605
+ if (!defUsage.has(defId)) {
606
+ defUsage.set(defId, new Set());
607
+ }
608
+ defUsage.get(defId).add(layerId);
609
+ }
610
+ }
611
+
612
+ // Find shared defs (used by more than one layer)
613
+ const sharedDefs = [];
614
+ const exclusiveDefs = new Map(); // layer ID -> defs only used by that layer
615
+
616
+ for (const [defId, layerSet] of defUsage) {
617
+ if (layerSet.size > 1) {
618
+ sharedDefs.push({
619
+ id: defId,
620
+ usedBy: [...layerSet]
621
+ });
622
+ } else {
623
+ const layerId = [...layerSet][0];
624
+ if (!exclusiveDefs.has(layerId)) {
625
+ exclusiveDefs.set(layerId, []);
626
+ }
627
+ exclusiveDefs.get(layerId).push(defId);
628
+ }
629
+ }
630
+
631
+ return {
632
+ layers: layers.map(l => ({
633
+ id: l.id,
634
+ label: l.label,
635
+ referencedDefs: [...(layerRefs.get(l.id || l.label || 'unnamed') || [])]
636
+ })),
637
+ sharedDefs,
638
+ exclusiveDefs: Object.fromEntries(exclusiveDefs),
639
+ totalDefs: defsMap.size
640
+ };
641
+ }
@@ -718,7 +718,9 @@ export const allowedChildrenPerElement = {
718
718
  'circle', 'clipPath', 'defs', 'desc', 'ellipse', 'filter', 'font', 'font-face',
719
719
  'foreignObject', 'g', 'image', 'line', 'linearGradient', 'marker', 'mask', 'metadata',
720
720
  'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialGradient', 'rect', 'script',
721
- 'set', 'style', 'svg', 'switch', 'symbol', 'text', 'title', 'use', 'view'
721
+ 'set', 'style', 'svg', 'switch', 'symbol', 'text', 'title', 'use', 'view',
722
+ // SVG 2.0 elements
723
+ 'meshgradient', 'meshGradient', 'hatch', 'solidcolor', 'solidColor'
722
724
  ]),
723
725
  'symbol': new Set([
724
726
  'a', 'animate', 'animateColor', 'animateMotion', 'animateTransform', 'circle',
@@ -781,6 +783,29 @@ export const allowedChildrenPerElement = {
781
783
  'radialGradient': new Set([
782
784
  'animate', 'animateTransform', 'desc', 'metadata', 'set', 'stop', 'title'
783
785
  ]),
786
+ // SVG 2.0 mesh gradient element hierarchy
787
+ 'meshgradient': new Set([
788
+ 'animate', 'animateTransform', 'desc', 'metadata', 'meshrow', 'set', 'title'
789
+ ]),
790
+ 'meshGradient': new Set([
791
+ 'animate', 'animateTransform', 'desc', 'metadata', 'meshrow', 'set', 'title'
792
+ ]),
793
+ 'meshrow': new Set([
794
+ 'meshpatch'
795
+ ]),
796
+ 'meshpatch': new Set([
797
+ 'stop'
798
+ ]),
799
+ // SVG 2.0 hatch element
800
+ 'hatch': new Set([
801
+ 'animate', 'animateTransform', 'desc', 'hatchpath', 'hatchPath', 'metadata', 'script', 'set', 'style', 'title'
802
+ ]),
803
+ 'hatchpath': new Set([
804
+ 'animate', 'animateTransform', 'desc', 'metadata', 'script', 'set', 'title'
805
+ ]),
806
+ 'hatchPath': new Set([
807
+ 'animate', 'animateTransform', 'desc', 'metadata', 'script', 'set', 'title'
808
+ ]),
784
809
  'filter': new Set([
785
810
  'animate', 'animateTransform', 'desc', 'feBlend', 'feColorMatrix', 'feComponentTransfer',
786
811
  'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap',