@emasoft/svg-matrix 1.0.18 → 1.0.20
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.
- package/README.md +256 -759
- package/bin/svg-matrix.js +171 -2
- package/bin/svglinter.cjs +1162 -0
- package/package.json +8 -2
- package/scripts/postinstall.js +6 -9
- package/src/animation-optimization.js +394 -0
- package/src/animation-references.js +440 -0
- package/src/arc-length.js +940 -0
- package/src/bezier-analysis.js +1626 -0
- package/src/bezier-intersections.js +1369 -0
- package/src/clip-path-resolver.js +110 -2
- package/src/convert-path-data.js +583 -0
- package/src/css-specificity.js +443 -0
- package/src/douglas-peucker.js +356 -0
- package/src/flatten-pipeline.js +109 -4
- package/src/geometry-to-path.js +126 -16
- package/src/gjk-collision.js +840 -0
- package/src/index.js +175 -2
- package/src/off-canvas-detection.js +1222 -0
- package/src/path-analysis.js +1241 -0
- package/src/path-data-plugins.js +928 -0
- package/src/path-optimization.js +825 -0
- package/src/path-simplification.js +1140 -0
- package/src/polygon-clip.js +376 -99
- package/src/svg-boolean-ops.js +898 -0
- package/src/svg-collections.js +910 -0
- package/src/svg-parser.js +175 -16
- package/src/svg-rendering-context.js +627 -0
- package/src/svg-toolbox.js +7495 -0
- package/src/svg-validation-data.js +944 -0
- package/src/transform-decomposition.js +810 -0
- package/src/transform-optimization.js +936 -0
- package/src/use-symbol-resolver.js +75 -7
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Animation-Aware Reference Tracking for SVG
|
|
3
|
+
*
|
|
4
|
+
* Unlike SVGO (which destroys animations by removing "unused" defs),
|
|
5
|
+
* this module tracks ALL references including:
|
|
6
|
+
*
|
|
7
|
+
* 1. STATIC REFERENCES:
|
|
8
|
+
* - href="#id", xlink:href="#id"
|
|
9
|
+
* - url(#id) in fill, stroke, clip-path, mask, filter, marker-*
|
|
10
|
+
* - CSS url() references in style attributes and <style> blocks
|
|
11
|
+
*
|
|
12
|
+
* 2. SMIL ANIMATION REFERENCES:
|
|
13
|
+
* - <animate values="#id1;#id2;#id3"> (frame-by-frame animation)
|
|
14
|
+
* - <animate from="#id1" to="#id2">
|
|
15
|
+
* - <set to="#id">
|
|
16
|
+
* - begin="id.click", begin="id.end", begin="id.begin+1s"
|
|
17
|
+
* - end="id.click", end="id.end"
|
|
18
|
+
* - <animateMotion><mpath xlink:href="#path"/></animateMotion>
|
|
19
|
+
*
|
|
20
|
+
* 3. CSS ANIMATION REFERENCES:
|
|
21
|
+
* - @keyframes names referenced by animation-name
|
|
22
|
+
* - url(#id) in @keyframes rules
|
|
23
|
+
*
|
|
24
|
+
* 4. JAVASCRIPT PATTERN DETECTION (best-effort):
|
|
25
|
+
* - getElementById('id') patterns
|
|
26
|
+
* - querySelector('#id') patterns
|
|
27
|
+
*
|
|
28
|
+
* @module animation-references
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { SVGElement } from './svg-parser.js';
|
|
32
|
+
|
|
33
|
+
// Animation elements that can reference other elements
|
|
34
|
+
const ANIMATION_ELEMENTS = ['animate', 'animateTransform', 'animateMotion', 'animateColor', 'set'];
|
|
35
|
+
|
|
36
|
+
// Attributes that can contain ID references
|
|
37
|
+
const HREF_ATTRIBUTES = ['href', 'xlink:href'];
|
|
38
|
+
|
|
39
|
+
// Attributes that use url(#id) syntax
|
|
40
|
+
const URL_ATTRIBUTES = [
|
|
41
|
+
'fill', 'stroke', 'clip-path', 'mask', 'filter',
|
|
42
|
+
'marker-start', 'marker-mid', 'marker-end',
|
|
43
|
+
'cursor', 'color-profile'
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// Animation timing attributes that can reference other elements by ID
|
|
47
|
+
const TIMING_ATTRIBUTES = ['begin', 'end'];
|
|
48
|
+
|
|
49
|
+
// Animation value attributes that can contain ID references
|
|
50
|
+
const VALUE_ATTRIBUTES = ['values', 'from', 'to', 'by'];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Parse ID from url(#id) or url("#id") syntax
|
|
54
|
+
* @param {string} value - Attribute value
|
|
55
|
+
* @returns {string|null} Parsed ID or null
|
|
56
|
+
*/
|
|
57
|
+
export function parseUrlId(value) {
|
|
58
|
+
if (!value || typeof value !== 'string') return null;
|
|
59
|
+
|
|
60
|
+
// Match url(#id) or url("#id") or url('#id') with optional whitespace
|
|
61
|
+
const match = value.match(/url\(\s*["']?#([^"')\s]+)\s*["']?\s*\)/);
|
|
62
|
+
return match ? match[1] : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Parse ID from href="#id" or xlink:href="#id" syntax
|
|
67
|
+
* @param {string} value - Attribute value
|
|
68
|
+
* @returns {string|null} Parsed ID or null
|
|
69
|
+
*/
|
|
70
|
+
export function parseHrefId(value) {
|
|
71
|
+
if (!value || typeof value !== 'string') return null;
|
|
72
|
+
if (value.startsWith('#')) {
|
|
73
|
+
return value.substring(1);
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Parse IDs from animation values attribute (semicolon-separated)
|
|
80
|
+
* Example: values="#FRAME001;#FRAME002;#FRAME003"
|
|
81
|
+
* @param {string} value - Values attribute
|
|
82
|
+
* @returns {string[]} Array of parsed IDs
|
|
83
|
+
*/
|
|
84
|
+
export function parseAnimationValueIds(value) {
|
|
85
|
+
if (!value || typeof value !== 'string') return [];
|
|
86
|
+
|
|
87
|
+
const ids = [];
|
|
88
|
+
// Split by semicolon and find #id references
|
|
89
|
+
const parts = value.split(';');
|
|
90
|
+
for (const part of parts) {
|
|
91
|
+
const trimmed = part.trim();
|
|
92
|
+
if (trimmed.startsWith('#')) {
|
|
93
|
+
ids.push(trimmed.substring(1));
|
|
94
|
+
}
|
|
95
|
+
// Also check for url(#id) within values
|
|
96
|
+
const urlId = parseUrlId(trimmed);
|
|
97
|
+
if (urlId) ids.push(urlId);
|
|
98
|
+
}
|
|
99
|
+
return ids;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Parse IDs from timing attributes (begin, end)
|
|
104
|
+
* Examples:
|
|
105
|
+
* - "button.click" -> "button"
|
|
106
|
+
* - "anim1.end" -> "anim1"
|
|
107
|
+
* - "anim1.begin+1s" -> "anim1"
|
|
108
|
+
* - "click" -> null (no ID reference)
|
|
109
|
+
* - "3s;button.click" -> "button"
|
|
110
|
+
* @param {string} value - Timing attribute value
|
|
111
|
+
* @returns {string[]} Array of parsed IDs
|
|
112
|
+
*/
|
|
113
|
+
export function parseTimingIds(value) {
|
|
114
|
+
if (!value || typeof value !== 'string') return [];
|
|
115
|
+
|
|
116
|
+
const ids = [];
|
|
117
|
+
// Split by semicolon for multiple timing values
|
|
118
|
+
const parts = value.split(';');
|
|
119
|
+
|
|
120
|
+
for (const part of parts) {
|
|
121
|
+
const trimmed = part.trim();
|
|
122
|
+
// Match patterns like "id.event" or "id.begin" or "id.end"
|
|
123
|
+
// Events: click, mousedown, mouseup, mouseover, mouseout, focusin, focusout, etc.
|
|
124
|
+
const match = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_-]*)\.(begin|end|click|mousedown|mouseup|mouseover|mouseout|mousemove|mouseenter|mouseleave|focusin|focusout|activate|repeat)/);
|
|
125
|
+
if (match) {
|
|
126
|
+
ids.push(match[1]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return ids;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Parse IDs from CSS content (style attribute or <style> element)
|
|
134
|
+
* @param {string} css - CSS content
|
|
135
|
+
* @returns {string[]} Array of parsed IDs
|
|
136
|
+
*/
|
|
137
|
+
export function parseCSSIds(css) {
|
|
138
|
+
if (!css || typeof css !== 'string') return [];
|
|
139
|
+
|
|
140
|
+
const ids = [];
|
|
141
|
+
|
|
142
|
+
// Find url(#id) references
|
|
143
|
+
const urlRegex = /url\(\s*["']?#([^"')]+)["']?\s*\)/g;
|
|
144
|
+
let match;
|
|
145
|
+
while ((match = urlRegex.exec(css)) !== null) {
|
|
146
|
+
ids.push(match[1]);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return ids;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Parse IDs from JavaScript content (best-effort)
|
|
154
|
+
* @param {string} js - JavaScript content
|
|
155
|
+
* @returns {string[]} Array of parsed IDs
|
|
156
|
+
*/
|
|
157
|
+
export function parseJavaScriptIds(js) {
|
|
158
|
+
if (!js || typeof js !== 'string') return [];
|
|
159
|
+
|
|
160
|
+
const ids = [];
|
|
161
|
+
|
|
162
|
+
// getElementById('id') or getElementById("id")
|
|
163
|
+
const getByIdRegex = /getElementById\(\s*["']([^"']+)["']\s*\)/g;
|
|
164
|
+
let match;
|
|
165
|
+
while ((match = getByIdRegex.exec(js)) !== null) {
|
|
166
|
+
ids.push(match[1]);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// querySelector('#id') or querySelector("#id")
|
|
170
|
+
const querySelectorRegex = /querySelector(?:All)?\(\s*["']#([^"'#\s]+)["']\s*\)/g;
|
|
171
|
+
while ((match = querySelectorRegex.exec(js)) !== null) {
|
|
172
|
+
ids.push(match[1]);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return ids;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Collect all references from a single element
|
|
180
|
+
* @param {SVGElement} el - SVG element to scan
|
|
181
|
+
* @returns {{static: Set<string>, animation: Set<string>, css: Set<string>, js: Set<string>}}
|
|
182
|
+
*/
|
|
183
|
+
export function collectElementReferences(el) {
|
|
184
|
+
const refs = {
|
|
185
|
+
static: new Set(),
|
|
186
|
+
animation: new Set(),
|
|
187
|
+
css: new Set(),
|
|
188
|
+
js: new Set()
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const tagName = el.tagName?.toLowerCase() || '';
|
|
192
|
+
const isAnimationElement = ANIMATION_ELEMENTS.includes(tagName);
|
|
193
|
+
|
|
194
|
+
for (const attrName of el.getAttributeNames()) {
|
|
195
|
+
const value = el.getAttribute(attrName);
|
|
196
|
+
if (!value) continue;
|
|
197
|
+
|
|
198
|
+
// Check href attributes
|
|
199
|
+
if (HREF_ATTRIBUTES.includes(attrName)) {
|
|
200
|
+
const id = parseHrefId(value);
|
|
201
|
+
if (id) {
|
|
202
|
+
if (isAnimationElement) {
|
|
203
|
+
refs.animation.add(id);
|
|
204
|
+
} else {
|
|
205
|
+
refs.static.add(id);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Check url() attributes
|
|
211
|
+
if (URL_ATTRIBUTES.includes(attrName) || attrName === 'style') {
|
|
212
|
+
const id = parseUrlId(value);
|
|
213
|
+
if (id) refs.static.add(id);
|
|
214
|
+
|
|
215
|
+
// For style attribute, also parse CSS IDs
|
|
216
|
+
if (attrName === 'style') {
|
|
217
|
+
parseCSSIds(value).forEach(id => refs.css.add(id));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Check animation timing attributes (begin, end)
|
|
222
|
+
if (TIMING_ATTRIBUTES.includes(attrName)) {
|
|
223
|
+
parseTimingIds(value).forEach(id => refs.animation.add(id));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Check animation value attributes (values, from, to, by)
|
|
227
|
+
if (VALUE_ATTRIBUTES.includes(attrName)) {
|
|
228
|
+
parseAnimationValueIds(value).forEach(id => refs.animation.add(id));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Also check for url() in any attribute (some custom attributes may use it)
|
|
232
|
+
if (!URL_ATTRIBUTES.includes(attrName) && value.includes('url(')) {
|
|
233
|
+
const id = parseUrlId(value);
|
|
234
|
+
if (id) refs.static.add(id);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Check <mpath> element inside <animateMotion>
|
|
239
|
+
if (tagName === 'animatemotion') {
|
|
240
|
+
const mpaths = el.getElementsByTagName('mpath');
|
|
241
|
+
for (const mpath of mpaths) {
|
|
242
|
+
for (const attr of HREF_ATTRIBUTES) {
|
|
243
|
+
const value = mpath.getAttribute(attr);
|
|
244
|
+
const id = parseHrefId(value);
|
|
245
|
+
if (id) refs.animation.add(id);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return refs;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Collect all ID references from an SVG document tree
|
|
255
|
+
* This is the main function that ensures we never remove animated elements
|
|
256
|
+
*
|
|
257
|
+
* @param {SVGElement} root - Root SVG element
|
|
258
|
+
* @returns {{
|
|
259
|
+
* all: Set<string>,
|
|
260
|
+
* static: Set<string>,
|
|
261
|
+
* animation: Set<string>,
|
|
262
|
+
* css: Set<string>,
|
|
263
|
+
* js: Set<string>,
|
|
264
|
+
* details: Map<string, {sources: string[], type: string}>
|
|
265
|
+
* }}
|
|
266
|
+
*/
|
|
267
|
+
export function collectAllReferences(root) {
|
|
268
|
+
const result = {
|
|
269
|
+
all: new Set(),
|
|
270
|
+
static: new Set(),
|
|
271
|
+
animation: new Set(),
|
|
272
|
+
css: new Set(),
|
|
273
|
+
js: new Set(),
|
|
274
|
+
details: new Map() // ID -> { sources: [], type: 'static'|'animation'|'css'|'js' }
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// Type priority: animation > js > css > static
|
|
278
|
+
// Animation is highest priority because SVGO destroys it
|
|
279
|
+
const TYPE_PRIORITY = { animation: 4, js: 3, css: 2, static: 1 };
|
|
280
|
+
|
|
281
|
+
const addRef = (id, type, source) => {
|
|
282
|
+
result.all.add(id);
|
|
283
|
+
result[type].add(id);
|
|
284
|
+
|
|
285
|
+
if (!result.details.has(id)) {
|
|
286
|
+
result.details.set(id, { sources: [], type });
|
|
287
|
+
} else {
|
|
288
|
+
// Update type if new type has higher priority
|
|
289
|
+
const existing = result.details.get(id);
|
|
290
|
+
if (TYPE_PRIORITY[type] > TYPE_PRIORITY[existing.type]) {
|
|
291
|
+
existing.type = type;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
result.details.get(id).sources.push(source);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const processElement = (el, path = '') => {
|
|
298
|
+
const tagName = el.tagName?.toLowerCase() || '';
|
|
299
|
+
const currentPath = path ? `${path}>${tagName}` : tagName;
|
|
300
|
+
const elId = el.getAttribute('id');
|
|
301
|
+
const elPath = elId ? `${tagName}#${elId}` : currentPath;
|
|
302
|
+
|
|
303
|
+
// Collect element references
|
|
304
|
+
const refs = collectElementReferences(el);
|
|
305
|
+
|
|
306
|
+
refs.static.forEach(id => addRef(id, 'static', elPath));
|
|
307
|
+
refs.animation.forEach(id => addRef(id, 'animation', elPath));
|
|
308
|
+
refs.css.forEach(id => addRef(id, 'css', elPath));
|
|
309
|
+
refs.js.forEach(id => addRef(id, 'js', elPath));
|
|
310
|
+
|
|
311
|
+
// Process <style> elements
|
|
312
|
+
if (tagName === 'style') {
|
|
313
|
+
const cssContent = el.textContent || '';
|
|
314
|
+
parseCSSIds(cssContent).forEach(id => addRef(id, 'css', `<style>`));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Process <script> elements
|
|
318
|
+
if (tagName === 'script') {
|
|
319
|
+
const jsContent = el.textContent || '';
|
|
320
|
+
parseJavaScriptIds(jsContent).forEach(id => addRef(id, 'js', `<script>`));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Recurse to children
|
|
324
|
+
for (const child of el.children) {
|
|
325
|
+
if (child instanceof SVGElement) {
|
|
326
|
+
processElement(child, currentPath);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
processElement(root);
|
|
332
|
+
|
|
333
|
+
return result;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Check if an ID is referenced anywhere (static, animation, CSS, or JS)
|
|
338
|
+
* @param {SVGElement} root - Root SVG element
|
|
339
|
+
* @param {string} id - ID to check
|
|
340
|
+
* @returns {boolean} True if the ID is referenced
|
|
341
|
+
*/
|
|
342
|
+
export function isIdReferenced(root, id) {
|
|
343
|
+
const refs = collectAllReferences(root);
|
|
344
|
+
return refs.all.has(id);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Get detailed reference information for an ID
|
|
349
|
+
* @param {SVGElement} root - Root SVG element
|
|
350
|
+
* @param {string} id - ID to check
|
|
351
|
+
* @returns {{referenced: boolean, type: string|null, sources: string[]}}
|
|
352
|
+
*/
|
|
353
|
+
export function getIdReferenceInfo(root, id) {
|
|
354
|
+
const refs = collectAllReferences(root);
|
|
355
|
+
const details = refs.details.get(id);
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
referenced: refs.all.has(id),
|
|
359
|
+
type: details?.type || null,
|
|
360
|
+
sources: details?.sources || []
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Find unreferenced IDs in defs that are SAFE to remove
|
|
366
|
+
* Unlike SVGO, this properly skips animation-referenced elements
|
|
367
|
+
*
|
|
368
|
+
* @param {SVGElement} root - Root SVG element
|
|
369
|
+
* @returns {{safeToRemove: string[], referenced: string[], animationReferenced: string[]}}
|
|
370
|
+
*/
|
|
371
|
+
export function findUnreferencedDefs(root) {
|
|
372
|
+
const refs = collectAllReferences(root);
|
|
373
|
+
|
|
374
|
+
const safeToRemove = [];
|
|
375
|
+
const referenced = [];
|
|
376
|
+
const animationReferenced = [];
|
|
377
|
+
|
|
378
|
+
// Scan all defs
|
|
379
|
+
const defsElements = root.getElementsByTagName('defs');
|
|
380
|
+
for (const defs of defsElements) {
|
|
381
|
+
for (const child of defs.children) {
|
|
382
|
+
if (child instanceof SVGElement) {
|
|
383
|
+
const id = child.getAttribute('id');
|
|
384
|
+
if (!id) continue;
|
|
385
|
+
|
|
386
|
+
if (refs.animation.has(id)) {
|
|
387
|
+
animationReferenced.push(id);
|
|
388
|
+
} else if (refs.all.has(id)) {
|
|
389
|
+
referenced.push(id);
|
|
390
|
+
} else {
|
|
391
|
+
safeToRemove.push(id);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return { safeToRemove, referenced, animationReferenced };
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Remove only truly unreferenced defs (animation-safe version)
|
|
402
|
+
* This is the SAFE alternative to SVGO's aggressive removal
|
|
403
|
+
*
|
|
404
|
+
* @param {SVGElement} root - Root SVG element
|
|
405
|
+
* @returns {{removed: string[], kept: string[], keptForAnimation: string[]}}
|
|
406
|
+
*/
|
|
407
|
+
export function removeUnreferencedDefsSafe(root) {
|
|
408
|
+
const { safeToRemove, referenced, animationReferenced } = findUnreferencedDefs(root);
|
|
409
|
+
|
|
410
|
+
const removed = [];
|
|
411
|
+
|
|
412
|
+
// Only remove elements that are truly unreferenced
|
|
413
|
+
const defsElements = root.getElementsByTagName('defs');
|
|
414
|
+
for (const defs of defsElements) {
|
|
415
|
+
for (const child of [...defs.children]) {
|
|
416
|
+
if (child instanceof SVGElement) {
|
|
417
|
+
const id = child.getAttribute('id');
|
|
418
|
+
if (id && safeToRemove.includes(id)) {
|
|
419
|
+
defs.removeChild(child);
|
|
420
|
+
removed.push(id);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return {
|
|
427
|
+
removed,
|
|
428
|
+
kept: referenced,
|
|
429
|
+
keptForAnimation: animationReferenced
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Named exports for all functions
|
|
434
|
+
export {
|
|
435
|
+
ANIMATION_ELEMENTS,
|
|
436
|
+
HREF_ATTRIBUTES,
|
|
437
|
+
URL_ATTRIBUTES,
|
|
438
|
+
TIMING_ATTRIBUTES,
|
|
439
|
+
VALUE_ATTRIBUTES
|
|
440
|
+
};
|