@event4u/agent-config 16.4.0 → 16.5.0
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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +40 -191
- package/README.md +1 -1
- package/dist/agent-src/commands/design.md +1 -1
- package/dist/agent-src/guidelines/design-antipatterns.md +1 -1
- package/dist/agent-src/rules/token-budget-discipline.md +2 -2
- package/dist/agent-src/skills/accessibility-auditor/data/aria-patterns.csv +2 -0
- package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
- package/dist/agent-src/skills/canvas-design/SKILL.md +4 -2
- package/dist/agent-src/skills/design-intelligence/references/integration-mapping.md +13 -10
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +49 -2
- package/dist/agent-src/skills/frontend-render-security/SKILL.md +9 -0
- package/dist/agent-src/skills/iconography/SKILL.md +4 -2
- package/dist/agent-src/skills/image-provider-routing/SKILL.md +16 -5
- package/dist/agent-src/skills/logo-generation/SKILL.md +12 -8
- package/dist/agent-src/skills/svg-authoring/SKILL.md +318 -0
- package/dist/agent-src/skills/wireframe/SKILL.md +5 -2
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/commands/uiRender.js +201 -4
- package/dist/cli/commands/uiRenderAsset.js +1308 -0
- package/dist/cli/commands/uiRenderAssetCompare.js +752 -0
- package/dist/cli/commands/uiRenderAssetGrid.js +265 -0
- package/dist/cli/commands/uiRenderAssetGrounds.js +323 -0
- package/dist/cli/commands/uiRenderAssetIsolation.js +85 -0
- package/dist/cli/commands/uiRenderAssetTypes.js +145 -0
- package/dist/cli/main.js +47 -1
- package/dist/cli/registry.js +1 -1
- package/dist/cli-delegate/{chunk-MSUL5NYP.js → chunk-4DBG5K7M.js} +1 -1
- package/dist/cli-delegate/{chunk-AED2VQGM.js → chunk-TMWYUOK4.js} +10 -1
- package/dist/cli-delegate/cmd_conformance.js +395 -214
- package/dist/cli-delegate/cmd_handoff.js +2 -2
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +47 -22
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +6 -6
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +19 -15
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +13 -9
- package/dist/hooks/dispatch.js +1073 -808
- package/dist/hooks/dispatch.sha256 +1 -1
- package/dist/hooks/svg_rules.chunk.cjs +1626 -0
- package/dist/hooks/svg_rules.chunk.cjs.sha256 +1 -0
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +931 -735
- package/dist/scripts/_lib/scan_scope.js +105 -0
- package/dist/scripts/_lib/svg_dom.js +284 -0
- package/dist/scripts/_lib/svg_geometry.js +477 -0
- package/dist/scripts/_lib/svg_motion.js +179 -0
- package/dist/scripts/_lib/svg_rules.js +1337 -0
- package/dist/scripts/_lib/svg_urls.js +67 -0
- package/dist/scripts/lint_svg_asset.js +324 -0
- package/docs/guidelines/design-antipatterns.md +1 -1
- package/package.json +4 -3
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/routing-coverage-seed.json +9 -2
- package/src/scripts/_lib/svg_dom.ts +318 -0
- package/src/scripts/_lib/svg_geometry.ts +480 -0
- package/src/scripts/_lib/svg_motion.ts +208 -0
- package/src/scripts/_lib/svg_rules.ts +1453 -0
- package/src/scripts/_lib/svg_urls.ts +72 -0
- package/src/scripts/_lib/verification_command.ts +59 -0
- package/src/scripts/_lib/verify_clause.ts +27 -17
- package/src/scripts/hooks/block_speaking_inbox_dir.ts +16 -1
- package/src/scripts/hooks/design_pass_hook.ts +157 -3
- package/src/scripts/hooks/svg_obligation.ts +497 -0
- package/src/scripts/hooks/svg_rules_loader.ts +133 -0
- package/src/scripts/hooks/turn_end_gate_hook.ts +5 -3
- package/src/scripts/lint_consolidation_lineage.ts +6 -3
- package/src/scripts/lint_design_quality.ts +60 -12
- package/src/scripts/lint_motion_authority_drift.ts +27 -0
- package/src/scripts/lint_svg_asset.ts +365 -0
|
@@ -0,0 +1,1626 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/scripts/_lib/svg_rules.ts
|
|
21
|
+
var svg_rules_exports = {};
|
|
22
|
+
__export(svg_rules_exports, {
|
|
23
|
+
DELIVERY_MODES: () => DELIVERY_MODES,
|
|
24
|
+
GENERIC_IDS: () => GENERIC_IDS,
|
|
25
|
+
VECTOR_GEOMETRY: () => VECTOR_GEOMETRY,
|
|
26
|
+
directChildrenOfRoot: () => directChildrenOfRoot,
|
|
27
|
+
elementExtent: () => elementExtent,
|
|
28
|
+
maskComments: () => maskComments,
|
|
29
|
+
parseElements: () => parseElements,
|
|
30
|
+
parseIgnores: () => parseIgnores,
|
|
31
|
+
pathDataError: () => pathDataError,
|
|
32
|
+
pathExtent: () => pathExtent,
|
|
33
|
+
pointsExtent: () => pointsExtent,
|
|
34
|
+
readViewBox: () => readViewBox,
|
|
35
|
+
referencedIds: () => referencedIds,
|
|
36
|
+
safetySeverity: () => safetySeverity,
|
|
37
|
+
scanHostHtml: () => scanHostHtml,
|
|
38
|
+
scanSvg: () => scanSvg,
|
|
39
|
+
staticBox: () => staticBox,
|
|
40
|
+
tokenisePath: () => tokenisePath,
|
|
41
|
+
vectorVerdict: () => vectorVerdict
|
|
42
|
+
});
|
|
43
|
+
module.exports = __toCommonJS(svg_rules_exports);
|
|
44
|
+
|
|
45
|
+
// src/scripts/_lib/svg_dom.ts
|
|
46
|
+
function positions(content) {
|
|
47
|
+
const starts = [0];
|
|
48
|
+
for (let i = 0; i < content.length; i += 1) {
|
|
49
|
+
if (content[i] === "\n") starts.push(i + 1);
|
|
50
|
+
}
|
|
51
|
+
return (offset) => {
|
|
52
|
+
let lo = 0;
|
|
53
|
+
let hi = starts.length - 1;
|
|
54
|
+
while (lo < hi) {
|
|
55
|
+
const mid = lo + hi + 1 >> 1;
|
|
56
|
+
if (starts[mid] <= offset) lo = mid;
|
|
57
|
+
else hi = mid - 1;
|
|
58
|
+
}
|
|
59
|
+
return { line: lo + 1, column: offset - starts[lo] + 1 };
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function tagEnd(content, from) {
|
|
63
|
+
let quote = null;
|
|
64
|
+
for (let i = from + 1; i < content.length; i += 1) {
|
|
65
|
+
const ch = content[i];
|
|
66
|
+
if (quote !== null) {
|
|
67
|
+
if (ch === quote) quote = null;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (ch === '"' || ch === "'") {
|
|
71
|
+
quote = ch;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (ch === ">") return i;
|
|
75
|
+
}
|
|
76
|
+
return -1;
|
|
77
|
+
}
|
|
78
|
+
function attrsOf(raw) {
|
|
79
|
+
const attrs = {};
|
|
80
|
+
const re = /([A-Za-z_:][\w.:-]*)\s*=\s*("([^"]*)"|'([^']*)')/g;
|
|
81
|
+
let m;
|
|
82
|
+
while ((m = re.exec(raw)) !== null) {
|
|
83
|
+
const key = (m[1] ?? "").toLowerCase();
|
|
84
|
+
const value = m[3] ?? m[4] ?? "";
|
|
85
|
+
attrs[key] = value.replace(/&/g, "&");
|
|
86
|
+
}
|
|
87
|
+
return attrs;
|
|
88
|
+
}
|
|
89
|
+
function parseDocument(content) {
|
|
90
|
+
const at = positions(content);
|
|
91
|
+
const roots = [];
|
|
92
|
+
const elements = [];
|
|
93
|
+
const errors = [];
|
|
94
|
+
const stack = [];
|
|
95
|
+
const make = (kind, start, name = "", value = "", attrs = {}) => {
|
|
96
|
+
const { line, column } = at(start);
|
|
97
|
+
return { kind, name, attrs, line, column, start, value, children: [], parent: null };
|
|
98
|
+
};
|
|
99
|
+
const append = (node) => {
|
|
100
|
+
const parent = stack[stack.length - 1];
|
|
101
|
+
if (parent === void 0) {
|
|
102
|
+
roots.push(node);
|
|
103
|
+
} else {
|
|
104
|
+
node.parent = parent;
|
|
105
|
+
parent.children.push(node);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
let i = 0;
|
|
109
|
+
while (i < content.length) {
|
|
110
|
+
const lt = content.indexOf("<", i);
|
|
111
|
+
if (lt === -1) {
|
|
112
|
+
const rest = content.slice(i);
|
|
113
|
+
if (rest.trim() !== "") append(make("text", i, "", rest));
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
if (lt > i) {
|
|
117
|
+
const text = content.slice(i, lt);
|
|
118
|
+
if (text.trim() !== "") append(make("text", i, "", text));
|
|
119
|
+
}
|
|
120
|
+
if (content.startsWith("<!--", lt)) {
|
|
121
|
+
const end = content.indexOf("-->", lt + 4);
|
|
122
|
+
if (end === -1) {
|
|
123
|
+
const p = at(lt);
|
|
124
|
+
errors.push({ message: "unterminated comment", line: p.line, column: p.column });
|
|
125
|
+
append(make("comment", lt, "", content.slice(lt + 4)));
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
append(make("comment", lt, "", content.slice(lt + 4, end)));
|
|
129
|
+
i = end + 3;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (content.startsWith("<![CDATA[", lt)) {
|
|
133
|
+
const end = content.indexOf("]]>", lt + 9);
|
|
134
|
+
if (end === -1) {
|
|
135
|
+
const p = at(lt);
|
|
136
|
+
errors.push({ message: "unterminated CDATA section", line: p.line, column: p.column });
|
|
137
|
+
append(make("cdata", lt, "", content.slice(lt + 9)));
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
append(make("cdata", lt, "", content.slice(lt + 9, end)));
|
|
141
|
+
i = end + 3;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (content.startsWith("<?", lt)) {
|
|
145
|
+
const end = content.indexOf("?>", lt + 2);
|
|
146
|
+
if (end === -1) {
|
|
147
|
+
const p = at(lt);
|
|
148
|
+
errors.push({ message: "unterminated processing instruction", line: p.line, column: p.column });
|
|
149
|
+
}
|
|
150
|
+
const stop = end === -1 ? content.length : end + 2;
|
|
151
|
+
append(make("pi", lt, "", content.slice(lt + 2, end === -1 ? content.length : end)));
|
|
152
|
+
i = stop;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (content.startsWith("<!", lt)) {
|
|
156
|
+
const bracket = content.indexOf("[", lt);
|
|
157
|
+
const close = content.indexOf(">", lt);
|
|
158
|
+
let stop;
|
|
159
|
+
if (bracket !== -1 && close !== -1 && bracket < close) {
|
|
160
|
+
const endBracket = content.indexOf("]", bracket);
|
|
161
|
+
const after = endBracket === -1 ? close : content.indexOf(">", endBracket);
|
|
162
|
+
stop = after === -1 ? content.length : after + 1;
|
|
163
|
+
} else {
|
|
164
|
+
stop = close === -1 ? content.length : close + 1;
|
|
165
|
+
}
|
|
166
|
+
if (close === -1) {
|
|
167
|
+
const p = at(lt);
|
|
168
|
+
errors.push({ message: "unterminated declaration", line: p.line, column: p.column });
|
|
169
|
+
}
|
|
170
|
+
append(make("doctype", lt, "", content.slice(lt + 2, Math.max(lt + 2, stop - 1))));
|
|
171
|
+
i = stop;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
const gt = tagEnd(content, lt);
|
|
175
|
+
if (gt === -1) {
|
|
176
|
+
const p = at(lt);
|
|
177
|
+
errors.push({ message: "unterminated tag", line: p.line, column: p.column });
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
const raw = content.slice(lt, gt + 1);
|
|
181
|
+
const nameMatch = /^<\/?\s*([A-Za-z_][\w.:-]*)/.exec(raw);
|
|
182
|
+
if (!nameMatch) {
|
|
183
|
+
const p = at(lt);
|
|
184
|
+
errors.push({ message: `"${raw.slice(0, 24)}" is not a tag`, line: p.line, column: p.column });
|
|
185
|
+
i = gt + 1;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
const name = (nameMatch[1] ?? "").toLowerCase();
|
|
189
|
+
const closing = raw.startsWith("</");
|
|
190
|
+
const selfClosing = raw.endsWith("/>");
|
|
191
|
+
if (closing) {
|
|
192
|
+
const open = stack[stack.length - 1];
|
|
193
|
+
const p = at(lt);
|
|
194
|
+
if (open === void 0) {
|
|
195
|
+
errors.push({ message: `closing tag </${name}> with no open element`, line: p.line, column: p.column });
|
|
196
|
+
} else if (open.name !== name) {
|
|
197
|
+
errors.push({
|
|
198
|
+
message: `closing tag </${name}> does not match the open <${open.name}> (line ${String(open.line)})`,
|
|
199
|
+
line: p.line,
|
|
200
|
+
column: p.column
|
|
201
|
+
});
|
|
202
|
+
const idx = stack.map((n) => n.name).lastIndexOf(name);
|
|
203
|
+
if (idx !== -1) stack.length = idx;
|
|
204
|
+
} else {
|
|
205
|
+
stack.pop();
|
|
206
|
+
}
|
|
207
|
+
i = gt + 1;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const node = make("element", lt, name, "", attrsOf(raw));
|
|
211
|
+
append(node);
|
|
212
|
+
elements.push(node);
|
|
213
|
+
if (!selfClosing) stack.push(node);
|
|
214
|
+
i = gt + 1;
|
|
215
|
+
}
|
|
216
|
+
for (const unclosed of stack) {
|
|
217
|
+
errors.push({
|
|
218
|
+
message: `<${unclosed.name}> is never closed`,
|
|
219
|
+
line: unclosed.line,
|
|
220
|
+
column: unclosed.column
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return { roots, elements, errors };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/scripts/_lib/svg_urls.ts
|
|
227
|
+
function svgPrefixes(content) {
|
|
228
|
+
const out = /* @__PURE__ */ new Set();
|
|
229
|
+
const re = /xmlns:([\w.-]+)\s*=\s*["']http:\/\/www\.w3\.org\/2000\/svg["']/gi;
|
|
230
|
+
let m;
|
|
231
|
+
while ((m = re.exec(content)) !== null) out.add((m[1] ?? "").toLowerCase());
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
function localName(name, prefixes) {
|
|
235
|
+
const colon = name.indexOf(":");
|
|
236
|
+
return colon !== -1 && prefixes.has(name.slice(0, colon)) ? name.slice(colon + 1) : name;
|
|
237
|
+
}
|
|
238
|
+
function rawAttrs(raw) {
|
|
239
|
+
const out = {};
|
|
240
|
+
const re = /([\w:.-]+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>"'`=]+))/g;
|
|
241
|
+
let m;
|
|
242
|
+
while ((m = re.exec(raw)) !== null) out[(m[1] ?? "").toLowerCase()] = m[2] ?? m[3] ?? m[4] ?? "";
|
|
243
|
+
return out;
|
|
244
|
+
}
|
|
245
|
+
var URL_ATTRS = /* @__PURE__ */ new Set(["href", "xlink:href", "src"]);
|
|
246
|
+
var isActiveUrl = (raw) => /^(javascript:|data:text\/html)/i.test(resolvedUrl(raw));
|
|
247
|
+
var NAMED_URL_ENTITIES = {
|
|
248
|
+
colon: ":",
|
|
249
|
+
sol: "/",
|
|
250
|
+
tab: " ",
|
|
251
|
+
newline: "\n",
|
|
252
|
+
period: ".",
|
|
253
|
+
lpar: "(",
|
|
254
|
+
rpar: ")",
|
|
255
|
+
amp: "&"
|
|
256
|
+
};
|
|
257
|
+
function resolvedUrl(raw) {
|
|
258
|
+
const decoded = raw.replace(/&(?:#x([0-9a-f]+)|#(\d+)|([a-z]+));?/gi, (m, hex, dec, name) => {
|
|
259
|
+
const cp = hex ? parseInt(hex, 16) : dec ? parseInt(dec, 10) : NaN;
|
|
260
|
+
if (!Number.isNaN(cp)) return cp <= 1114111 ? String.fromCodePoint(cp) : m;
|
|
261
|
+
return NAMED_URL_ENTITIES[name.toLowerCase()] ?? m;
|
|
262
|
+
});
|
|
263
|
+
return decoded.replace(/^[\u0000- ]+|[\u0000- ]+$/g, "").replace(/[\t\n\r]/g, "");
|
|
264
|
+
}
|
|
265
|
+
function cssUnescape(raw) {
|
|
266
|
+
return raw.replace(/\\([0-9a-f]{1,6})\s?|\\(.)/gi, (m, hex, ch) => {
|
|
267
|
+
if (hex) {
|
|
268
|
+
const cp = parseInt(hex, 16);
|
|
269
|
+
return cp <= 1114111 ? String.fromCodePoint(cp) : m;
|
|
270
|
+
}
|
|
271
|
+
return ch;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// src/scripts/_lib/svg_geometry.ts
|
|
276
|
+
function staticBox(el) {
|
|
277
|
+
if (el.attrs["transform"] || el.attrs["style"]?.includes("transform")) return null;
|
|
278
|
+
const num = (k) => {
|
|
279
|
+
const v = el.attrs[k];
|
|
280
|
+
if (v === void 0) return null;
|
|
281
|
+
if (!/^-?\d+(\.\d+)?$/.test(v.trim())) return null;
|
|
282
|
+
return Number(v);
|
|
283
|
+
};
|
|
284
|
+
const originOr0 = (k) => el.attrs[k] === void 0 ? 0 : num(k);
|
|
285
|
+
switch (el.name) {
|
|
286
|
+
case "rect": {
|
|
287
|
+
const x = originOr0("x");
|
|
288
|
+
const y = originOr0("y");
|
|
289
|
+
const w = num("width");
|
|
290
|
+
const h = num("height");
|
|
291
|
+
if (x === null || y === null || w === null || h === null) return null;
|
|
292
|
+
return { minX: x, minY: y, maxX: x + w, maxY: y + h };
|
|
293
|
+
}
|
|
294
|
+
case "circle": {
|
|
295
|
+
const cx = originOr0("cx");
|
|
296
|
+
const cy = originOr0("cy");
|
|
297
|
+
const r = num("r");
|
|
298
|
+
if (cx === null || cy === null || r === null) return null;
|
|
299
|
+
return { minX: cx - r, minY: cy - r, maxX: cx + r, maxY: cy + r };
|
|
300
|
+
}
|
|
301
|
+
case "ellipse": {
|
|
302
|
+
const cx = originOr0("cx");
|
|
303
|
+
const cy = originOr0("cy");
|
|
304
|
+
const rx = num("rx");
|
|
305
|
+
const ry = num("ry");
|
|
306
|
+
if (cx === null || cy === null || rx === null || ry === null) return null;
|
|
307
|
+
return { minX: cx - rx, minY: cy - ry, maxX: cx + rx, maxY: cy + ry };
|
|
308
|
+
}
|
|
309
|
+
case "line": {
|
|
310
|
+
const x1 = num("x1");
|
|
311
|
+
const y1 = num("y1");
|
|
312
|
+
const x2 = num("x2");
|
|
313
|
+
const y2 = num("y2");
|
|
314
|
+
if (x1 === null || y1 === null || x2 === null || y2 === null) return null;
|
|
315
|
+
return {
|
|
316
|
+
minX: Math.min(x1, x2),
|
|
317
|
+
minY: Math.min(y1, y2),
|
|
318
|
+
maxX: Math.max(x1, x2),
|
|
319
|
+
maxY: Math.max(y1, y2)
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
default:
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function pathExtent(d) {
|
|
327
|
+
const tokens = tokenisePath(d);
|
|
328
|
+
if (!tokens) return null;
|
|
329
|
+
const argCount = { m: 2, l: 2, t: 2, h: 1, v: 1, c: 6, s: 4, q: 4, a: 7, z: 0 };
|
|
330
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
331
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
332
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
333
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
334
|
+
const see = (x, y) => {
|
|
335
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return;
|
|
336
|
+
minX = Math.min(minX, x);
|
|
337
|
+
maxX = Math.max(maxX, x);
|
|
338
|
+
minY = Math.min(minY, y);
|
|
339
|
+
maxY = Math.max(maxY, y);
|
|
340
|
+
};
|
|
341
|
+
let cx = 0;
|
|
342
|
+
let cy = 0;
|
|
343
|
+
let sx = 0;
|
|
344
|
+
let sy = 0;
|
|
345
|
+
let i = 0;
|
|
346
|
+
let cmd = "";
|
|
347
|
+
let rel = false;
|
|
348
|
+
while (i < tokens.length) {
|
|
349
|
+
const tok = tokens[i];
|
|
350
|
+
if (/^[a-zA-Z]$/.test(tok)) {
|
|
351
|
+
cmd = tok.toLowerCase();
|
|
352
|
+
rel = tok === tok.toLowerCase();
|
|
353
|
+
i += 1;
|
|
354
|
+
if (cmd === "z") {
|
|
355
|
+
cx = sx;
|
|
356
|
+
cy = sy;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (!(cmd in argCount)) return null;
|
|
360
|
+
} else if (cmd === "" || cmd === "z") {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
const need = argCount[cmd];
|
|
364
|
+
if (need === 0) continue;
|
|
365
|
+
const args = [];
|
|
366
|
+
while (args.length < need && i < tokens.length && !/^[a-zA-Z]$/.test(tokens[i])) {
|
|
367
|
+
args.push(Number(tokens[i]));
|
|
368
|
+
i += 1;
|
|
369
|
+
}
|
|
370
|
+
if (args.length < need) return null;
|
|
371
|
+
switch (cmd) {
|
|
372
|
+
case "h": {
|
|
373
|
+
cx = rel ? cx + args[0] : args[0];
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
case "v": {
|
|
377
|
+
cy = rel ? cy + args[0] : args[0];
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
case "a": {
|
|
381
|
+
const ex = rel ? cx + args[5] : args[5];
|
|
382
|
+
const ey = rel ? cy + args[6] : args[6];
|
|
383
|
+
for (const [px, py] of arcExtremes(
|
|
384
|
+
cx,
|
|
385
|
+
cy,
|
|
386
|
+
ex,
|
|
387
|
+
ey,
|
|
388
|
+
args[0],
|
|
389
|
+
args[1],
|
|
390
|
+
args[2],
|
|
391
|
+
args[3] !== 0,
|
|
392
|
+
args[4] !== 0
|
|
393
|
+
)) {
|
|
394
|
+
see(px, py);
|
|
395
|
+
}
|
|
396
|
+
cx = ex;
|
|
397
|
+
cy = ey;
|
|
398
|
+
break;
|
|
399
|
+
}
|
|
400
|
+
default: {
|
|
401
|
+
for (let k = 0; k + 1 < args.length; k += 2) {
|
|
402
|
+
const px = rel ? cx + args[k] : args[k];
|
|
403
|
+
const py = rel ? cy + args[k + 1] : args[k + 1];
|
|
404
|
+
see(px, py);
|
|
405
|
+
if (k + 2 >= args.length) {
|
|
406
|
+
cx = px;
|
|
407
|
+
cy = py;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
see(cx, cy);
|
|
414
|
+
if (cmd === "m") {
|
|
415
|
+
sx = cx;
|
|
416
|
+
sy = cy;
|
|
417
|
+
cmd = "l";
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (!Number.isFinite(minX)) return null;
|
|
421
|
+
return { minX, minY, maxX, maxY };
|
|
422
|
+
}
|
|
423
|
+
function tokenisePath(d) {
|
|
424
|
+
const NUM = /[+-]?(?:\d*\.\d+|\d+\.?)(?:[eE][+-]?\d+)?/y;
|
|
425
|
+
const FLAG = /[01]/y;
|
|
426
|
+
const SEP = /[\s,]*/y;
|
|
427
|
+
const out = [];
|
|
428
|
+
const text = d.trim();
|
|
429
|
+
let i = 0;
|
|
430
|
+
let cmd = "";
|
|
431
|
+
const eat = (re) => {
|
|
432
|
+
re.lastIndex = i;
|
|
433
|
+
const m = re.exec(text);
|
|
434
|
+
if (m === null) return null;
|
|
435
|
+
i = re.lastIndex;
|
|
436
|
+
return m[0];
|
|
437
|
+
};
|
|
438
|
+
const skip = () => {
|
|
439
|
+
void eat(SEP);
|
|
440
|
+
};
|
|
441
|
+
while (i < text.length) {
|
|
442
|
+
skip();
|
|
443
|
+
if (i >= text.length) break;
|
|
444
|
+
const ch = text[i];
|
|
445
|
+
if (/[a-zA-Z]/.test(ch)) {
|
|
446
|
+
cmd = ch;
|
|
447
|
+
out.push(ch);
|
|
448
|
+
i += 1;
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (cmd.toLowerCase() === "a") {
|
|
452
|
+
for (let k = 0; k < 3; k += 1) {
|
|
453
|
+
skip();
|
|
454
|
+
const n2 = eat(NUM);
|
|
455
|
+
if (n2 === null) return null;
|
|
456
|
+
out.push(n2);
|
|
457
|
+
}
|
|
458
|
+
for (let k = 0; k < 2; k += 1) {
|
|
459
|
+
skip();
|
|
460
|
+
const f = eat(FLAG);
|
|
461
|
+
if (f === null) return null;
|
|
462
|
+
out.push(f);
|
|
463
|
+
}
|
|
464
|
+
for (let k = 0; k < 2; k += 1) {
|
|
465
|
+
skip();
|
|
466
|
+
const n2 = eat(NUM);
|
|
467
|
+
if (n2 === null) return null;
|
|
468
|
+
out.push(n2);
|
|
469
|
+
}
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const n = eat(NUM);
|
|
473
|
+
if (n === null) return null;
|
|
474
|
+
out.push(n);
|
|
475
|
+
}
|
|
476
|
+
return out.length > 0 ? out : null;
|
|
477
|
+
}
|
|
478
|
+
function arcExtremes(x1, y1, x2, y2, rxIn, ryIn, rotationDeg, largeArc, sweep) {
|
|
479
|
+
const ends = [
|
|
480
|
+
[x1, y1],
|
|
481
|
+
[x2, y2]
|
|
482
|
+
];
|
|
483
|
+
let rx = Math.abs(rxIn);
|
|
484
|
+
let ry = Math.abs(ryIn);
|
|
485
|
+
if (rx === 0 || ry === 0 || x1 === x2 && y1 === y2) return ends;
|
|
486
|
+
const phi = rotationDeg * Math.PI / 180;
|
|
487
|
+
const cosP = Math.cos(phi);
|
|
488
|
+
const sinP = Math.sin(phi);
|
|
489
|
+
const dx2 = (x1 - x2) / 2;
|
|
490
|
+
const dy2 = (y1 - y2) / 2;
|
|
491
|
+
const x1p = cosP * dx2 + sinP * dy2;
|
|
492
|
+
const y1p = -sinP * dx2 + cosP * dy2;
|
|
493
|
+
const lambda = x1p * x1p / (rx * rx) + y1p * y1p / (ry * ry);
|
|
494
|
+
if (lambda > 1) {
|
|
495
|
+
const k = Math.sqrt(lambda);
|
|
496
|
+
rx *= k;
|
|
497
|
+
ry *= k;
|
|
498
|
+
}
|
|
499
|
+
const num = rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p;
|
|
500
|
+
const den = rx * rx * y1p * y1p + ry * ry * x1p * x1p;
|
|
501
|
+
if (den === 0) return ends;
|
|
502
|
+
const coef = (largeArc === sweep ? -1 : 1) * Math.sqrt(Math.max(0, num / den));
|
|
503
|
+
const cxp = coef * rx * y1p / ry;
|
|
504
|
+
const cyp = -coef * ry * x1p / rx;
|
|
505
|
+
const ccx = cosP * cxp - sinP * cyp + (x1 + x2) / 2;
|
|
506
|
+
const ccy = sinP * cxp + cosP * cyp + (y1 + y2) / 2;
|
|
507
|
+
const angleOf = (px, py) => {
|
|
508
|
+
const ux = (cosP * (px - ccx) + sinP * (py - ccy)) / rx;
|
|
509
|
+
const uy = (-sinP * (px - ccx) + cosP * (py - ccy)) / ry;
|
|
510
|
+
return Math.atan2(uy, ux);
|
|
511
|
+
};
|
|
512
|
+
const start = angleOf(x1, y1);
|
|
513
|
+
const end = angleOf(x2, y2);
|
|
514
|
+
const TAU = Math.PI * 2;
|
|
515
|
+
let delta = end - start;
|
|
516
|
+
if (sweep && delta < 0) delta += TAU;
|
|
517
|
+
if (!sweep && delta > 0) delta -= TAU;
|
|
518
|
+
const within = (theta) => {
|
|
519
|
+
let t = theta - start;
|
|
520
|
+
if (sweep) {
|
|
521
|
+
while (t < 0) t += TAU;
|
|
522
|
+
return t <= delta + 1e-9;
|
|
523
|
+
}
|
|
524
|
+
while (t > 0) t -= TAU;
|
|
525
|
+
return t >= delta - 1e-9;
|
|
526
|
+
};
|
|
527
|
+
const at = (theta) => [
|
|
528
|
+
ccx + rx * Math.cos(theta) * cosP - ry * Math.sin(theta) * sinP,
|
|
529
|
+
ccy + rx * Math.cos(theta) * sinP + ry * Math.sin(theta) * cosP
|
|
530
|
+
];
|
|
531
|
+
const out = [...ends];
|
|
532
|
+
const tx = Math.atan2(-ry * sinP, rx * cosP);
|
|
533
|
+
const ty = Math.atan2(ry * cosP, rx * sinP);
|
|
534
|
+
for (const base of [tx, ty]) {
|
|
535
|
+
for (const theta of [base, base + Math.PI]) {
|
|
536
|
+
if (within(theta)) out.push(at(theta));
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return out;
|
|
540
|
+
}
|
|
541
|
+
function pointsExtent(points) {
|
|
542
|
+
const nums = points.trim().match(/-?\d*\.?\d+(?:[eE][-+]?\d+)?/g);
|
|
543
|
+
if (!nums || nums.length < 2) return null;
|
|
544
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
545
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
546
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
547
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
548
|
+
for (let i = 0; i + 1 < nums.length; i += 2) {
|
|
549
|
+
const x = Number(nums[i]);
|
|
550
|
+
const y = Number(nums[i + 1]);
|
|
551
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) continue;
|
|
552
|
+
minX = Math.min(minX, x);
|
|
553
|
+
maxX = Math.max(maxX, x);
|
|
554
|
+
minY = Math.min(minY, y);
|
|
555
|
+
maxY = Math.max(maxY, y);
|
|
556
|
+
}
|
|
557
|
+
return Number.isFinite(minX) ? { minX, minY, maxX, maxY } : null;
|
|
558
|
+
}
|
|
559
|
+
function elementExtent(el) {
|
|
560
|
+
if (el.closing) return null;
|
|
561
|
+
if (el.attrs["transform"] || el.attrs["style"]?.includes("transform")) return null;
|
|
562
|
+
if (el.name === "path") return pathExtent(el.attrs["d"] ?? "");
|
|
563
|
+
if (el.name === "polygon" || el.name === "polyline") return pointsExtent(el.attrs["points"] ?? "");
|
|
564
|
+
return staticBox(el);
|
|
565
|
+
}
|
|
566
|
+
function pathDataError(d) {
|
|
567
|
+
const trimmed = d.trim();
|
|
568
|
+
if (!trimmed) return "empty d attribute";
|
|
569
|
+
if (!/^[mM]/.test(trimmed)) return "path data does not start with a moveto command";
|
|
570
|
+
const argCount = {
|
|
571
|
+
m: 2,
|
|
572
|
+
l: 2,
|
|
573
|
+
t: 2,
|
|
574
|
+
h: 1,
|
|
575
|
+
v: 1,
|
|
576
|
+
c: 6,
|
|
577
|
+
s: 4,
|
|
578
|
+
q: 4,
|
|
579
|
+
a: 7,
|
|
580
|
+
z: 0
|
|
581
|
+
};
|
|
582
|
+
const tokens = tokenisePath(trimmed);
|
|
583
|
+
if (!tokens) {
|
|
584
|
+
return "path data could not be read \u2014 a coordinate, an arc flag or a command letter is malformed";
|
|
585
|
+
}
|
|
586
|
+
let i = 0;
|
|
587
|
+
while (i < tokens.length) {
|
|
588
|
+
const tok = tokens[i];
|
|
589
|
+
if (!/^[a-zA-Z]$/.test(tok)) return `unexpected token "${tok}" where a command letter was required`;
|
|
590
|
+
const cmd = tok.toLowerCase();
|
|
591
|
+
if (!(cmd in argCount)) return `unknown path command "${tok}"`;
|
|
592
|
+
const need = argCount[cmd];
|
|
593
|
+
i += 1;
|
|
594
|
+
if (need === 0) continue;
|
|
595
|
+
let consumed = 0;
|
|
596
|
+
while (i < tokens.length && !/^[a-zA-Z]$/.test(tokens[i])) {
|
|
597
|
+
i += 1;
|
|
598
|
+
consumed += 1;
|
|
599
|
+
}
|
|
600
|
+
if (consumed === 0 || consumed % need !== 0) {
|
|
601
|
+
return `command "${tok}" takes ${need} coordinate(s) per repetition, got ${consumed}`;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/scripts/_lib/svg_motion.ts
|
|
608
|
+
var SMIL_TAGS = /* @__PURE__ */ new Set([
|
|
609
|
+
"animate",
|
|
610
|
+
"animatetransform",
|
|
611
|
+
"animatemotion",
|
|
612
|
+
"animatecolor",
|
|
613
|
+
"set"
|
|
614
|
+
]);
|
|
615
|
+
var SMIL_MARKUP_RE = /<(animate|animateTransform|animateMotion|animateColor|set)\b/i;
|
|
616
|
+
var REDUCED_MOTION_RE = /@media[^{]*prefers-reduced-motion/i;
|
|
617
|
+
function hasSmilMarkup(content) {
|
|
618
|
+
return SMIL_MARKUP_RE.test(content);
|
|
619
|
+
}
|
|
620
|
+
function hasCssAnimation(content) {
|
|
621
|
+
return /@keyframes\s+[\w-]+/i.test(content) || /\banimation(?:-name)?\s*:/i.test(content);
|
|
622
|
+
}
|
|
623
|
+
function hasReducedMotionQuery(content) {
|
|
624
|
+
return REDUCED_MOTION_RE.test(content);
|
|
625
|
+
}
|
|
626
|
+
function lineIndexer(content) {
|
|
627
|
+
const starts = [0];
|
|
628
|
+
for (let i = 0; i < content.length; i++) if (content[i] === "\n") starts.push(i + 1);
|
|
629
|
+
return (offset) => {
|
|
630
|
+
let lo = 0;
|
|
631
|
+
let hi = starts.length - 1;
|
|
632
|
+
while (lo < hi) {
|
|
633
|
+
const mid = lo + hi + 1 >> 1;
|
|
634
|
+
if (starts[mid] <= offset) lo = mid;
|
|
635
|
+
else hi = mid - 1;
|
|
636
|
+
}
|
|
637
|
+
return lo + 1;
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
function matchBrace(css, open) {
|
|
641
|
+
let depth = 0;
|
|
642
|
+
for (let i = open; i < css.length; i++) {
|
|
643
|
+
if (css[i] === "{") depth += 1;
|
|
644
|
+
else if (css[i] === "}") {
|
|
645
|
+
depth -= 1;
|
|
646
|
+
if (depth === 0) return i;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
return -1;
|
|
650
|
+
}
|
|
651
|
+
function collectRules(css, base, atRule, lineOf, out) {
|
|
652
|
+
let i = 0;
|
|
653
|
+
while (i < css.length) {
|
|
654
|
+
const brace = css.indexOf("{", i);
|
|
655
|
+
if (brace === -1) break;
|
|
656
|
+
const prelude = css.slice(i, brace).trim();
|
|
657
|
+
const end = matchBrace(css, brace);
|
|
658
|
+
if (end === -1) break;
|
|
659
|
+
const body = css.slice(brace + 1, end);
|
|
660
|
+
if (prelude.startsWith("@")) {
|
|
661
|
+
collectRules(body, base + brace + 1, prelude, lineOf, out);
|
|
662
|
+
} else if (prelude) {
|
|
663
|
+
out.push({ selector: prelude, body, line: lineOf(base + i), atRule });
|
|
664
|
+
}
|
|
665
|
+
i = end + 1;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
function readCssRules(content) {
|
|
669
|
+
const rules = [];
|
|
670
|
+
const lineOf = lineIndexer(content);
|
|
671
|
+
const styleRe = /<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi;
|
|
672
|
+
let sm;
|
|
673
|
+
while ((sm = styleRe.exec(content)) !== null) {
|
|
674
|
+
const css = (sm[1] ?? "").replace(/\/\*[\s\S]*?\*\//g, (c) => c.replace(/[^\n]/g, " "));
|
|
675
|
+
const base = sm.index + (sm[0] ?? "").indexOf(sm[1] ?? "");
|
|
676
|
+
collectRules(css, base, "", lineOf, rules);
|
|
677
|
+
}
|
|
678
|
+
return rules;
|
|
679
|
+
}
|
|
680
|
+
function declarationsOf(body) {
|
|
681
|
+
const out = {};
|
|
682
|
+
for (const decl of body.split(";")) {
|
|
683
|
+
const idx = decl.indexOf(":");
|
|
684
|
+
if (idx === -1) continue;
|
|
685
|
+
const prop = decl.slice(0, idx).trim().toLowerCase();
|
|
686
|
+
if (!prop || prop.startsWith("@") || prop.includes("{")) continue;
|
|
687
|
+
out[prop] = decl.slice(idx + 1).trim();
|
|
688
|
+
}
|
|
689
|
+
return out;
|
|
690
|
+
}
|
|
691
|
+
function reducedMotionReachesSmil(content) {
|
|
692
|
+
const scoped = readCssRules(content).filter((r) => /prefers-reduced-motion/i.test(r.atRule));
|
|
693
|
+
for (const rule of scoped) {
|
|
694
|
+
for (const prop of Object.keys(declarationsOf(rule.body))) {
|
|
695
|
+
if (prop === "animation" || prop.startsWith("animation-")) continue;
|
|
696
|
+
if (prop === "transition" || prop.startsWith("transition-")) continue;
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if (/begin\s*=\s*["']indefinite["']/i.test(content)) return true;
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
703
|
+
function smilVerdict(content) {
|
|
704
|
+
if (!hasSmilMarkup(content)) return "no-smil";
|
|
705
|
+
if (!hasReducedMotionQuery(content)) return "no-route";
|
|
706
|
+
return reducedMotionReachesSmil(content) ? "routed" : "css-only";
|
|
707
|
+
}
|
|
708
|
+
function firstSmilLine(content) {
|
|
709
|
+
const lines = content.split("\n");
|
|
710
|
+
for (let i = 0; i < lines.length; i++) {
|
|
711
|
+
if (SMIL_MARKUP_RE.test(lines[i] ?? "")) return i + 1;
|
|
712
|
+
}
|
|
713
|
+
return 1;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// src/scripts/_lib/svg_rules.ts
|
|
717
|
+
var DELIVERY_MODES = ["inline", "document", "img", "css-bg"];
|
|
718
|
+
function parseElements(content) {
|
|
719
|
+
const out = [];
|
|
720
|
+
const lineOf = lineIndexer2(content);
|
|
721
|
+
let i = 0;
|
|
722
|
+
while (i < content.length) {
|
|
723
|
+
const lt = content.indexOf("<", i);
|
|
724
|
+
if (lt === -1) break;
|
|
725
|
+
if (content.startsWith("<!--", lt)) {
|
|
726
|
+
const end = content.indexOf("-->", lt + 4);
|
|
727
|
+
i = end === -1 ? content.length : end + 3;
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
if (content.startsWith("<![CDATA[", lt)) {
|
|
731
|
+
const end = content.indexOf("]]>", lt + 9);
|
|
732
|
+
i = end === -1 ? content.length : end + 3;
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
if (content.startsWith("<?", lt)) {
|
|
736
|
+
const end = content.indexOf("?>", lt + 2);
|
|
737
|
+
i = end === -1 ? content.length : end + 2;
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
if (content.startsWith("<!", lt)) {
|
|
741
|
+
const bracket = content.indexOf("[", lt);
|
|
742
|
+
const close = content.indexOf(">", lt);
|
|
743
|
+
if (bracket !== -1 && close !== -1 && bracket < close) {
|
|
744
|
+
const endBracket = content.indexOf("]", bracket);
|
|
745
|
+
const after = endBracket === -1 ? close : content.indexOf(">", endBracket);
|
|
746
|
+
i = after === -1 ? content.length : after + 1;
|
|
747
|
+
} else {
|
|
748
|
+
i = close === -1 ? content.length : close + 1;
|
|
749
|
+
}
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
const gt = tagEnd(content, lt);
|
|
753
|
+
if (gt === -1) break;
|
|
754
|
+
const raw = content.slice(lt, gt + 1);
|
|
755
|
+
const closing = raw.startsWith("</");
|
|
756
|
+
const selfClosing = raw.endsWith("/>");
|
|
757
|
+
const nameMatch = /^<\/?\s*([A-Za-z_][\w.:-]*)/.exec(raw);
|
|
758
|
+
if (!nameMatch) {
|
|
759
|
+
i = gt + 1;
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
out.push({
|
|
763
|
+
name: (nameMatch[1] ?? "").toLowerCase(),
|
|
764
|
+
attrs: closing ? {} : attrsOf(raw),
|
|
765
|
+
line: lineOf(lt),
|
|
766
|
+
start: lt,
|
|
767
|
+
selfClosing,
|
|
768
|
+
closing,
|
|
769
|
+
raw
|
|
770
|
+
});
|
|
771
|
+
i = gt + 1;
|
|
772
|
+
}
|
|
773
|
+
return out;
|
|
774
|
+
}
|
|
775
|
+
function maskComments(content) {
|
|
776
|
+
if (!content.includes("<!--")) return content;
|
|
777
|
+
const out = content.split("");
|
|
778
|
+
let i = 0;
|
|
779
|
+
while (i < out.length) {
|
|
780
|
+
const start = content.indexOf("<!--", i);
|
|
781
|
+
if (start === -1) break;
|
|
782
|
+
const close = content.indexOf("-->", start + 4);
|
|
783
|
+
const end = close === -1 ? content.length : close + 3;
|
|
784
|
+
for (let j = start; j < end; j += 1) {
|
|
785
|
+
if (out[j] !== "\n" && out[j] !== "\r") out[j] = " ";
|
|
786
|
+
}
|
|
787
|
+
i = end;
|
|
788
|
+
}
|
|
789
|
+
return out.join("");
|
|
790
|
+
}
|
|
791
|
+
function lineIndexer2(content) {
|
|
792
|
+
const starts = [0];
|
|
793
|
+
for (let i = 0; i < content.length; i++) if (content[i] === "\n") starts.push(i + 1);
|
|
794
|
+
return (offset) => {
|
|
795
|
+
let lo = 0;
|
|
796
|
+
let hi = starts.length - 1;
|
|
797
|
+
while (lo < hi) {
|
|
798
|
+
const mid = lo + hi + 1 >> 1;
|
|
799
|
+
if (starts[mid] <= offset) lo = mid;
|
|
800
|
+
else hi = mid - 1;
|
|
801
|
+
}
|
|
802
|
+
return lo + 1;
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
function textOf(content, elements, index) {
|
|
806
|
+
const el = elements[index];
|
|
807
|
+
if (!el || el.selfClosing) return "";
|
|
808
|
+
const openEnd = content.indexOf(">", el.start);
|
|
809
|
+
const closeTag = `</${el.name}`;
|
|
810
|
+
const closeAt = content.toLowerCase().indexOf(closeTag, openEnd);
|
|
811
|
+
if (openEnd === -1 || closeAt === -1) return "";
|
|
812
|
+
return content.slice(openEnd + 1, closeAt);
|
|
813
|
+
}
|
|
814
|
+
function readViewBox(el) {
|
|
815
|
+
const raw = el?.attrs["viewbox"];
|
|
816
|
+
if (!raw) return null;
|
|
817
|
+
const parts = raw.trim().split(/[\s,]+/).map(Number);
|
|
818
|
+
if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n))) return null;
|
|
819
|
+
const [x, y, w, h] = parts;
|
|
820
|
+
if (w <= 0 || h <= 0) return null;
|
|
821
|
+
return { x, y, w, h };
|
|
822
|
+
}
|
|
823
|
+
function referencedIds(content, elements) {
|
|
824
|
+
const ids = /* @__PURE__ */ new Set();
|
|
825
|
+
for (const el of elements) {
|
|
826
|
+
for (const key of ["href", "xlink:href"]) {
|
|
827
|
+
const v = el.attrs[key];
|
|
828
|
+
if (v && v.startsWith("#")) ids.add(v.slice(1));
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
const urlRe = /url\(\s*['"]?#([^)'"\s]+)/g;
|
|
832
|
+
let m;
|
|
833
|
+
while ((m = urlRe.exec(content)) !== null) ids.add(m[1]);
|
|
834
|
+
const beginRe = /\b(?:begin|end)\s*=\s*"([A-Za-z_][\w.:-]*)\./g;
|
|
835
|
+
while ((m = beginRe.exec(content)) !== null) ids.add(m[1]);
|
|
836
|
+
return ids;
|
|
837
|
+
}
|
|
838
|
+
function ruleMissingXmlns(elements, file) {
|
|
839
|
+
const root = elements.find((el) => el.name === "svg" && !el.closing);
|
|
840
|
+
if (!root) return [];
|
|
841
|
+
if (root.attrs["xmlns"]) return [];
|
|
842
|
+
return [
|
|
843
|
+
{
|
|
844
|
+
rule: "svg-structure-missing-xmlns",
|
|
845
|
+
file,
|
|
846
|
+
line: root.line,
|
|
847
|
+
snippet: "root <svg> has no xmlns \u2014 the file does not render as a standalone document or through <img>",
|
|
848
|
+
severity: "error"
|
|
849
|
+
}
|
|
850
|
+
];
|
|
851
|
+
}
|
|
852
|
+
function buildContainerDepth(elements) {
|
|
853
|
+
const stack = [];
|
|
854
|
+
const out = /* @__PURE__ */ new Map();
|
|
855
|
+
elements.forEach((el, idx) => {
|
|
856
|
+
if (el.closing) {
|
|
857
|
+
if (stack[stack.length - 1] === el.name) stack.pop();
|
|
858
|
+
out.set(idx, [...stack]);
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
out.set(idx, [...stack]);
|
|
862
|
+
if (!el.selfClosing) stack.push(el.name);
|
|
863
|
+
});
|
|
864
|
+
return out;
|
|
865
|
+
}
|
|
866
|
+
var NON_RENDERED_CONTAINERS = /* @__PURE__ */ new Set(["defs", "symbol", "clippath", "mask", "marker", "pattern"]);
|
|
867
|
+
function ruleOutsideViewBox(elements, file) {
|
|
868
|
+
const root = elements.find((el) => el.name === "svg" && !el.closing);
|
|
869
|
+
const vb = readViewBox(root);
|
|
870
|
+
if (!vb) return [];
|
|
871
|
+
const ancestry = buildContainerDepth(elements);
|
|
872
|
+
const findings = [];
|
|
873
|
+
elements.forEach((el, idx) => {
|
|
874
|
+
if (el.closing) return;
|
|
875
|
+
const chain = ancestry.get(idx) ?? [];
|
|
876
|
+
if (chain.some((c) => NON_RENDERED_CONTAINERS.has(c))) return;
|
|
877
|
+
if (chain.length && hasTransformedAncestor(elements, idx)) return;
|
|
878
|
+
const box = staticBox(el);
|
|
879
|
+
if (!box) return;
|
|
880
|
+
const outside = box.maxX <= vb.x || box.minX >= vb.x + vb.w || box.maxY <= vb.y || box.minY >= vb.y + vb.h;
|
|
881
|
+
if (!outside) return;
|
|
882
|
+
findings.push({
|
|
883
|
+
rule: "svg-structure-outside-viewbox",
|
|
884
|
+
file,
|
|
885
|
+
line: el.line,
|
|
886
|
+
snippet: `<${el.name}> box (${box.minX},${box.minY})-(${box.maxX},${box.maxY}) lies entirely outside viewBox ${vb.x} ${vb.y} ${vb.w} ${vb.h} \u2014 it will never paint`,
|
|
887
|
+
severity: "error"
|
|
888
|
+
});
|
|
889
|
+
});
|
|
890
|
+
return findings;
|
|
891
|
+
}
|
|
892
|
+
function hasTransformedAncestor(elements, index) {
|
|
893
|
+
const stack = [];
|
|
894
|
+
for (let i = 0; i < index; i++) {
|
|
895
|
+
const el = elements[i];
|
|
896
|
+
if (el.closing) {
|
|
897
|
+
if (stack[stack.length - 1]?.name === el.name) stack.pop();
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
if (!el.selfClosing) stack.push(el);
|
|
901
|
+
}
|
|
902
|
+
return stack.some((el) => Boolean(el.attrs["transform"]));
|
|
903
|
+
}
|
|
904
|
+
function ruleUnparseablePath(elements, file) {
|
|
905
|
+
const findings = [];
|
|
906
|
+
for (const el of elements) {
|
|
907
|
+
if (el.closing) continue;
|
|
908
|
+
const d = el.attrs["d"];
|
|
909
|
+
if (d === void 0) continue;
|
|
910
|
+
const err = pathDataError(d);
|
|
911
|
+
if (err) {
|
|
912
|
+
findings.push({
|
|
913
|
+
rule: "svg-structure-unparseable-path",
|
|
914
|
+
file,
|
|
915
|
+
line: el.line,
|
|
916
|
+
snippet: `<${el.name} d="\u2026"> ${err}`,
|
|
917
|
+
severity: "error"
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return findings;
|
|
922
|
+
}
|
|
923
|
+
function ruleUnusedDefs(content, elements, file) {
|
|
924
|
+
const used = referencedIds(content, elements);
|
|
925
|
+
const ancestry = buildContainerDepth(elements);
|
|
926
|
+
const findings = [];
|
|
927
|
+
elements.forEach((el, idx) => {
|
|
928
|
+
if (el.closing) return;
|
|
929
|
+
const id = el.attrs["id"];
|
|
930
|
+
if (!id) return;
|
|
931
|
+
const chain = ancestry.get(idx) ?? [];
|
|
932
|
+
if (!chain.includes("defs")) return;
|
|
933
|
+
if (el.name === "symbol") return;
|
|
934
|
+
if (used.has(id)) return;
|
|
935
|
+
findings.push({
|
|
936
|
+
rule: "svg-structure-unused-defs",
|
|
937
|
+
file,
|
|
938
|
+
line: el.line,
|
|
939
|
+
snippet: `<${el.name} id="${id}"> sits in <defs> and nothing in this file references it`,
|
|
940
|
+
severity: "warning"
|
|
941
|
+
});
|
|
942
|
+
});
|
|
943
|
+
return findings;
|
|
944
|
+
}
|
|
945
|
+
function ruleDanglingReference(content, elements, file) {
|
|
946
|
+
const defined = /* @__PURE__ */ new Set();
|
|
947
|
+
for (const el of elements) if (!el.closing && el.attrs["id"]) defined.add(el.attrs["id"]);
|
|
948
|
+
const findings = [];
|
|
949
|
+
const lineOf = lineIndexer2(content);
|
|
950
|
+
for (const el of elements) {
|
|
951
|
+
if (el.closing) continue;
|
|
952
|
+
for (const key of ["href", "xlink:href"]) {
|
|
953
|
+
const v = el.attrs[key];
|
|
954
|
+
if (!v || !v.startsWith("#")) continue;
|
|
955
|
+
const id = v.slice(1);
|
|
956
|
+
if (defined.has(id)) continue;
|
|
957
|
+
findings.push({
|
|
958
|
+
rule: "svg-structure-dangling-reference",
|
|
959
|
+
file,
|
|
960
|
+
line: el.line,
|
|
961
|
+
snippet: `<${el.name} ${key}="${v}"> points at an id this file does not define`,
|
|
962
|
+
severity: "error"
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
const urlRe = /url\(\s*['"]?#([^)'"\s]+)/g;
|
|
967
|
+
let m;
|
|
968
|
+
while ((m = urlRe.exec(content)) !== null) {
|
|
969
|
+
const id = m[1];
|
|
970
|
+
if (defined.has(id)) continue;
|
|
971
|
+
findings.push({
|
|
972
|
+
rule: "svg-structure-dangling-reference",
|
|
973
|
+
file,
|
|
974
|
+
line: lineOf(m.index),
|
|
975
|
+
snippet: `url(#${id}) points at an id this file does not define`,
|
|
976
|
+
severity: "error"
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
return dedupe(findings);
|
|
980
|
+
}
|
|
981
|
+
function safetySeverity(mode) {
|
|
982
|
+
return mode === "img" || mode === "css-bg" ? "inert" : "error";
|
|
983
|
+
}
|
|
984
|
+
var ACTIVE_ATTR_RE = /\son[a-z]+\s*=\s*(["']|[^\s>"'])/i;
|
|
985
|
+
var ACTIVE_ELEMENTS = /* @__PURE__ */ new Set(["script", "foreignobject", "iframe", "embed", "object"]);
|
|
986
|
+
function ruleActiveContent(content, elements, file, mode) {
|
|
987
|
+
const severity = safetySeverity(mode);
|
|
988
|
+
const findings = [];
|
|
989
|
+
const lineOf = lineIndexer2(content);
|
|
990
|
+
const prefixes = svgPrefixes(content);
|
|
991
|
+
for (const el of elements) {
|
|
992
|
+
if (el.closing) continue;
|
|
993
|
+
const local = localName(el.name, prefixes);
|
|
994
|
+
const smil = local === "set" || local === "animate" ? rawAttrs(el.raw) : null;
|
|
995
|
+
const animatesHref = smil !== null && /^(xlink:)?href$/i.test(resolvedUrl(smil["attributename"] ?? ""));
|
|
996
|
+
if (animatesHref && ["to", "from", "by", "values"].some((k) => (smil[k] ?? "").split(";").some(isActiveUrl))) {
|
|
997
|
+
findings.push({
|
|
998
|
+
rule: "svg-safety-active-content",
|
|
999
|
+
file,
|
|
1000
|
+
line: el.line,
|
|
1001
|
+
snippet: `<${el.name} attributeName="href"> animates in an active URL; delivery mode "${mode}" grades it ${severity}`,
|
|
1002
|
+
severity
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
if (ACTIVE_ELEMENTS.has(local)) {
|
|
1006
|
+
findings.push({
|
|
1007
|
+
rule: "svg-safety-active-content",
|
|
1008
|
+
file,
|
|
1009
|
+
line: el.line,
|
|
1010
|
+
snippet: `<${el.name}> is active content; delivery mode "${mode}" grades it ${severity}`,
|
|
1011
|
+
severity
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
for (const [key, value] of Object.entries(el.attrs)) {
|
|
1015
|
+
if (/^on[a-z]+$/.test(key)) {
|
|
1016
|
+
findings.push({
|
|
1017
|
+
rule: "svg-safety-active-content",
|
|
1018
|
+
file,
|
|
1019
|
+
line: el.line,
|
|
1020
|
+
snippet: `<${el.name} ${key}="\u2026"> is an event handler; delivery mode "${mode}" grades it ${severity}`,
|
|
1021
|
+
severity
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
if (/^javascript:/i.test(resolvedUrl(value)) || URL_ATTRS.has(key) && isActiveUrl(value)) {
|
|
1025
|
+
findings.push({
|
|
1026
|
+
rule: "svg-safety-active-content",
|
|
1027
|
+
file,
|
|
1028
|
+
line: el.line,
|
|
1029
|
+
snippet: `<${el.name} ${key}="${resolvedUrl(value).slice(0, 16)}\u2026"> is an active URL; delivery mode "${mode}" grades it ${severity}`,
|
|
1030
|
+
severity
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
if (!findings.length && ACTIVE_ATTR_RE.test(content)) {
|
|
1036
|
+
const idx = content.search(ACTIVE_ATTR_RE);
|
|
1037
|
+
findings.push({
|
|
1038
|
+
rule: "svg-safety-active-content",
|
|
1039
|
+
file,
|
|
1040
|
+
line: lineOf(idx),
|
|
1041
|
+
snippet: `an on* event handler attribute is present; delivery mode "${mode}" grades it ${severity}`,
|
|
1042
|
+
severity
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
return dedupe(findings);
|
|
1046
|
+
}
|
|
1047
|
+
function ruleInternalSubset(content, file) {
|
|
1048
|
+
const lineOf = lineIndexer2(content);
|
|
1049
|
+
const findings = [];
|
|
1050
|
+
const entity = /<!ENTITY\b/i.exec(content);
|
|
1051
|
+
if (entity) {
|
|
1052
|
+
findings.push({
|
|
1053
|
+
rule: "svg-safety-internal-subset",
|
|
1054
|
+
file,
|
|
1055
|
+
line: lineOf(entity.index),
|
|
1056
|
+
snippet: "<!ENTITY declaration \u2014 an entity-expansion surface; a bare public DOCTYPE is clean, this is not",
|
|
1057
|
+
severity: "error"
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
const doctype = /<!DOCTYPE[^>[]*\[/i.exec(content);
|
|
1061
|
+
if (doctype) {
|
|
1062
|
+
findings.push({
|
|
1063
|
+
rule: "svg-safety-internal-subset",
|
|
1064
|
+
file,
|
|
1065
|
+
line: lineOf(doctype.index),
|
|
1066
|
+
snippet: 'DOCTYPE carries an internal subset "[\u2026]" \u2014 strip it; a bare public DOCTYPE is clean',
|
|
1067
|
+
severity: "error"
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
return dedupe(findings);
|
|
1071
|
+
}
|
|
1072
|
+
var EXTERNAL_URL_RE = /^(?:https?:|ftp:|\/\/)/i;
|
|
1073
|
+
function ruleExternalReference(content, elements, file, mode) {
|
|
1074
|
+
const severity = safetySeverity(mode);
|
|
1075
|
+
const findings = [];
|
|
1076
|
+
const lineOf = lineIndexer2(content);
|
|
1077
|
+
for (const el of elements) {
|
|
1078
|
+
if (el.closing) continue;
|
|
1079
|
+
for (const key of ["href", "xlink:href", "src"]) {
|
|
1080
|
+
const v = el.attrs[key];
|
|
1081
|
+
if (v && EXTERNAL_URL_RE.test(resolvedUrl(v))) {
|
|
1082
|
+
findings.push({
|
|
1083
|
+
rule: "svg-safety-external-reference",
|
|
1084
|
+
file,
|
|
1085
|
+
line: el.line,
|
|
1086
|
+
snippet: `<${el.name} ${key}="${v.slice(0, 60)}"> loads from outside the file; mode "${mode}" grades it ${severity}`,
|
|
1087
|
+
severity
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
const urlRe = /url\(\s*['"]?((?:https?:|ftp:|\/\/)[^)'"\s]+)/gi;
|
|
1093
|
+
let m;
|
|
1094
|
+
while ((m = urlRe.exec(content)) !== null) {
|
|
1095
|
+
findings.push({
|
|
1096
|
+
rule: "svg-safety-external-reference",
|
|
1097
|
+
file,
|
|
1098
|
+
line: lineOf(m.index),
|
|
1099
|
+
snippet: `url(${(m[1] ?? "").slice(0, 60)}) loads from outside the file; mode "${mode}" grades it ${severity}`,
|
|
1100
|
+
severity
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
const importRe = /@import\s+(?:url\(\s*)?(['"])([^'"]*)\1/gi;
|
|
1104
|
+
while ((m = importRe.exec(content)) !== null) {
|
|
1105
|
+
const target = resolvedUrl(cssUnescape(resolvedUrl(m[2] ?? "")));
|
|
1106
|
+
if (!EXTERNAL_URL_RE.test(target)) continue;
|
|
1107
|
+
findings.push({
|
|
1108
|
+
rule: "svg-safety-external-reference",
|
|
1109
|
+
file,
|
|
1110
|
+
line: lineOf(m.index),
|
|
1111
|
+
snippet: `@import "${target.slice(0, 60)}" loads from outside the file; mode "${mode}" grades it ${severity}`,
|
|
1112
|
+
severity
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
return dedupe(findings);
|
|
1116
|
+
}
|
|
1117
|
+
var VECTOR_GEOMETRY = /* @__PURE__ */ new Set([
|
|
1118
|
+
"path",
|
|
1119
|
+
"rect",
|
|
1120
|
+
"circle",
|
|
1121
|
+
"ellipse",
|
|
1122
|
+
"line",
|
|
1123
|
+
"polyline",
|
|
1124
|
+
"polygon",
|
|
1125
|
+
"text",
|
|
1126
|
+
"textpath",
|
|
1127
|
+
"use"
|
|
1128
|
+
]);
|
|
1129
|
+
function vectorVerdict(elements) {
|
|
1130
|
+
let raster = 0;
|
|
1131
|
+
let vector = 0;
|
|
1132
|
+
for (const el of elements) {
|
|
1133
|
+
if (el.closing) continue;
|
|
1134
|
+
if (el.name === "image") {
|
|
1135
|
+
raster += 1;
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
1138
|
+
if (el.name === "foreignobject") {
|
|
1139
|
+
raster += 1;
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
if (VECTOR_GEOMETRY.has(el.name)) vector += 1;
|
|
1143
|
+
}
|
|
1144
|
+
if (raster > 0) {
|
|
1145
|
+
return {
|
|
1146
|
+
trueVector: false,
|
|
1147
|
+
rasterElements: raster,
|
|
1148
|
+
vectorElements: vector,
|
|
1149
|
+
reason: `carries ${raster} raster/foreignObject element(s) \u2014 not a true vector whatever else it contains`
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
return {
|
|
1153
|
+
trueVector: vector > 0,
|
|
1154
|
+
rasterElements: 0,
|
|
1155
|
+
vectorElements: vector,
|
|
1156
|
+
reason: vector > 0 ? `${vector} vector geometry element(s), no embedded raster` : "no painted geometry at all"
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
function ruleEmbeddedRaster(elements, file) {
|
|
1160
|
+
const verdict = vectorVerdict(elements);
|
|
1161
|
+
if (verdict.rasterElements === 0) return [];
|
|
1162
|
+
const first = elements.find((el) => !el.closing && (el.name === "image" || el.name === "foreignobject"));
|
|
1163
|
+
return [
|
|
1164
|
+
{
|
|
1165
|
+
rule: "svg-vector-embedded-raster",
|
|
1166
|
+
file,
|
|
1167
|
+
line: first?.line ?? 1,
|
|
1168
|
+
snippet: `${verdict.reason}; ${verdict.vectorElements} vector element(s) beside it does not make it one`,
|
|
1169
|
+
severity: "error"
|
|
1170
|
+
}
|
|
1171
|
+
];
|
|
1172
|
+
}
|
|
1173
|
+
var GENERIC_IDS = /* @__PURE__ */ new Set([
|
|
1174
|
+
"title",
|
|
1175
|
+
"desc",
|
|
1176
|
+
"a",
|
|
1177
|
+
"b",
|
|
1178
|
+
"c",
|
|
1179
|
+
"g",
|
|
1180
|
+
"gradient",
|
|
1181
|
+
"grad",
|
|
1182
|
+
"mask",
|
|
1183
|
+
"clip",
|
|
1184
|
+
"clippath",
|
|
1185
|
+
"shadow",
|
|
1186
|
+
"icon",
|
|
1187
|
+
"path",
|
|
1188
|
+
"path1",
|
|
1189
|
+
"layer1",
|
|
1190
|
+
"layer_1",
|
|
1191
|
+
"svg",
|
|
1192
|
+
"defs",
|
|
1193
|
+
"filter",
|
|
1194
|
+
"glow"
|
|
1195
|
+
]);
|
|
1196
|
+
function ruleGenericId(elements, file) {
|
|
1197
|
+
const findings = [];
|
|
1198
|
+
for (const el of elements) {
|
|
1199
|
+
if (el.closing) continue;
|
|
1200
|
+
const id = el.attrs["id"];
|
|
1201
|
+
if (!id) continue;
|
|
1202
|
+
if (!GENERIC_IDS.has(id.toLowerCase())) continue;
|
|
1203
|
+
findings.push({
|
|
1204
|
+
rule: "svg-a11y-generic-id",
|
|
1205
|
+
file,
|
|
1206
|
+
line: el.line,
|
|
1207
|
+
snippet: `id="${id}" collides with any second graphic inlined on the same page \u2014 namespace it`,
|
|
1208
|
+
severity: "warning"
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
return findings;
|
|
1212
|
+
}
|
|
1213
|
+
function isDirectChildOfRoot(elements, index) {
|
|
1214
|
+
let depth = 0;
|
|
1215
|
+
for (let i = 0; i < index; i++) {
|
|
1216
|
+
const el = elements[i];
|
|
1217
|
+
if (el.closing) depth -= 1;
|
|
1218
|
+
else if (!el.selfClosing) depth += 1;
|
|
1219
|
+
}
|
|
1220
|
+
return depth === 1;
|
|
1221
|
+
}
|
|
1222
|
+
function directChildrenOfRoot(doc) {
|
|
1223
|
+
const root = doc.elements.find((el) => el.name === "svg");
|
|
1224
|
+
if (root === void 0) return [];
|
|
1225
|
+
return root.children.filter((n) => n.kind === "element");
|
|
1226
|
+
}
|
|
1227
|
+
function ruleDegenerateGradient(elements, file) {
|
|
1228
|
+
const findings = [];
|
|
1229
|
+
const open = [];
|
|
1230
|
+
for (const el of elements) {
|
|
1231
|
+
if (el.name === "lineargradient" || el.name === "radialgradient") {
|
|
1232
|
+
if (el.closing) {
|
|
1233
|
+
const g = open.pop();
|
|
1234
|
+
if (!g || g.stops.length < 2) continue;
|
|
1235
|
+
const first = g.stops[0];
|
|
1236
|
+
const same = g.stops.every((s) => s.color === first.color && s.opacity === first.opacity);
|
|
1237
|
+
if (!same) continue;
|
|
1238
|
+
findings.push({
|
|
1239
|
+
rule: "svg-structure-degenerate-gradient",
|
|
1240
|
+
file,
|
|
1241
|
+
line: g.el.line,
|
|
1242
|
+
snippet: `<${g.el.name} id="${g.el.attrs["id"] ?? ""}"> has ${String(g.stops.length)} stops, all ${first.color || "(inherited)"} at opacity ${first.opacity} \u2014 a flat fill in a gradient's clothes, which costs bytes and makes the file harder to edit`,
|
|
1243
|
+
severity: "warning"
|
|
1244
|
+
});
|
|
1245
|
+
} else if (!el.selfClosing) {
|
|
1246
|
+
open.push({ el, stops: [] });
|
|
1247
|
+
}
|
|
1248
|
+
continue;
|
|
1249
|
+
}
|
|
1250
|
+
if (el.name === "stop" && !el.closing) {
|
|
1251
|
+
const host = open[open.length - 1];
|
|
1252
|
+
if (!host) continue;
|
|
1253
|
+
const style = el.attrs["style"] ?? "";
|
|
1254
|
+
const fromStyle = /stop-color\s*:\s*([^;]+)/i.exec(style)?.[1];
|
|
1255
|
+
const opacityFromStyle = /stop-opacity\s*:\s*([^;]+)/i.exec(style)?.[1];
|
|
1256
|
+
host.stops.push({
|
|
1257
|
+
color: (fromStyle ?? el.attrs["stop-color"] ?? "").trim().toLowerCase(),
|
|
1258
|
+
opacity: (opacityFromStyle ?? el.attrs["stop-opacity"] ?? "1").trim()
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
return findings;
|
|
1263
|
+
}
|
|
1264
|
+
function ruleWellFormed(doc, file) {
|
|
1265
|
+
if (doc.errors.length === 0) return [];
|
|
1266
|
+
const shown = doc.errors.slice(0, 5);
|
|
1267
|
+
const more = doc.errors.length - shown.length;
|
|
1268
|
+
return shown.map((e, i) => ({
|
|
1269
|
+
rule: "svg-structure-not-well-formed",
|
|
1270
|
+
file,
|
|
1271
|
+
line: e.line,
|
|
1272
|
+
snippet: `${e.message} (column ${String(e.column)})` + (i === shown.length - 1 && more > 0 ? ` \u2014 and ${String(more)} further error(s)` : ""),
|
|
1273
|
+
severity: "error"
|
|
1274
|
+
}));
|
|
1275
|
+
}
|
|
1276
|
+
function rootNameState(content, doc, elements) {
|
|
1277
|
+
const rootIdx = elements.findIndex((el) => el.name === "svg" && !el.closing);
|
|
1278
|
+
const root = rootIdx === -1 ? void 0 : elements[rootIdx];
|
|
1279
|
+
const titleIdx = elements.findIndex(
|
|
1280
|
+
(el, i) => el.name === "title" && !el.closing && rootIdx !== -1 && isDirectChildOfRoot(elements, i)
|
|
1281
|
+
);
|
|
1282
|
+
const titleIsDirectChild = directChildrenOfRoot(doc).some((n) => n.name === "title");
|
|
1283
|
+
const hasTitle = titleIsDirectChild && titleIdx !== -1 && textOf(content, elements, titleIdx).trim().length > 0;
|
|
1284
|
+
const titleIds = new Set(
|
|
1285
|
+
elements.filter((el) => el.name === "title" && !el.closing && el.attrs["id"]).map((el) => el.attrs["id"])
|
|
1286
|
+
);
|
|
1287
|
+
const labelledBy = (root?.attrs["aria-labelledby"] ?? "").trim();
|
|
1288
|
+
const labelledByTargets = labelledBy.split(/\s+/).filter(Boolean);
|
|
1289
|
+
const labelledByOwnTitle = labelledByTargets.length > 0 && labelledByTargets.every((id) => titleIds.has(id));
|
|
1290
|
+
return {
|
|
1291
|
+
root,
|
|
1292
|
+
hidden: (root?.attrs["aria-hidden"] ?? "").toLowerCase() === "true",
|
|
1293
|
+
hasTitle,
|
|
1294
|
+
hasLabel: Boolean(root?.attrs["aria-label"]?.trim()),
|
|
1295
|
+
hasLabelledBy: labelledBy.length > 0,
|
|
1296
|
+
labelledByOwnTitle,
|
|
1297
|
+
hasRoleImg: (root?.attrs["role"] ?? "").toLowerCase() === "img"
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
function ruleAccessibleName(s, file) {
|
|
1301
|
+
if (!s.root) return [];
|
|
1302
|
+
if (s.hidden) return [];
|
|
1303
|
+
if (s.hasTitle || s.hasLabel || s.hasLabelledBy) return [];
|
|
1304
|
+
return [
|
|
1305
|
+
{
|
|
1306
|
+
rule: "svg-a11y-no-accessible-name",
|
|
1307
|
+
file,
|
|
1308
|
+
line: s.root.line,
|
|
1309
|
+
snippet: 'graphic has no accessible name and is not aria-hidden="true" \u2014 add role="img" with a <title>, or declare it decorative',
|
|
1310
|
+
severity: "error"
|
|
1311
|
+
}
|
|
1312
|
+
];
|
|
1313
|
+
}
|
|
1314
|
+
function ruleDoubleName(s, file) {
|
|
1315
|
+
if (!s.root) return [];
|
|
1316
|
+
const labelledByIsSeparate = s.hasLabelledBy && !s.labelledByOwnTitle;
|
|
1317
|
+
const names = [s.hasLabel, labelledByIsSeparate, s.hasTitle].filter(Boolean).length;
|
|
1318
|
+
if (names < 2) return [];
|
|
1319
|
+
return [
|
|
1320
|
+
{
|
|
1321
|
+
rule: "svg-a11y-double-name",
|
|
1322
|
+
file,
|
|
1323
|
+
line: s.root.line,
|
|
1324
|
+
snippet: "graphic declares more than one accessible name (aria-label / aria-labelledby / <title>) \u2014 only one wins and which one is renderer-dependent",
|
|
1325
|
+
severity: "error"
|
|
1326
|
+
}
|
|
1327
|
+
];
|
|
1328
|
+
}
|
|
1329
|
+
function ruleKeySplinesRange(elements, file) {
|
|
1330
|
+
const findings = [];
|
|
1331
|
+
for (const el of elements) {
|
|
1332
|
+
if (el.closing) continue;
|
|
1333
|
+
const raw = el.attrs["keysplines"];
|
|
1334
|
+
if (!raw) continue;
|
|
1335
|
+
const nums = raw.split(/[;\s,]+/).filter(Boolean).map(Number);
|
|
1336
|
+
const bad = nums.filter((n) => Number.isFinite(n) && (n < 0 || n > 1));
|
|
1337
|
+
if (!bad.length) continue;
|
|
1338
|
+
findings.push({
|
|
1339
|
+
rule: "svg-motion-keysplines-range",
|
|
1340
|
+
file,
|
|
1341
|
+
line: el.line,
|
|
1342
|
+
snippet: `<${el.name} keySplines="${raw}"> holds ${bad.join(", ")} outside [0,1] \u2014 SMIL rejects the whole animation rather than clamping it, so the element never moves`,
|
|
1343
|
+
severity: "error"
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
return findings;
|
|
1347
|
+
}
|
|
1348
|
+
function ruleTransformBox(content, elements, file) {
|
|
1349
|
+
const findings = [];
|
|
1350
|
+
for (const el of elements) {
|
|
1351
|
+
if (el.closing) continue;
|
|
1352
|
+
const style = el.attrs["style"] ?? "";
|
|
1353
|
+
const decls = declarationsOf(style);
|
|
1354
|
+
const origin = decls["transform-origin"] ?? el.attrs["transform-origin"];
|
|
1355
|
+
if (!origin) continue;
|
|
1356
|
+
if (isOriginDefault(origin)) continue;
|
|
1357
|
+
const box = decls["transform-box"] ?? el.attrs["transform-box"];
|
|
1358
|
+
if (box) continue;
|
|
1359
|
+
findings.push({
|
|
1360
|
+
rule: "svg-motion-transform-box",
|
|
1361
|
+
file,
|
|
1362
|
+
line: el.line,
|
|
1363
|
+
snippet: `<${el.name}> sets transform-origin: ${origin} with no transform-box \u2014 in SVG the origin resolves against the view box, not the element, so the pivot lands somewhere else`,
|
|
1364
|
+
severity: "error"
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
for (const rule of readCssRules(content)) {
|
|
1368
|
+
const decls = declarationsOf(rule.body);
|
|
1369
|
+
const origin = decls["transform-origin"];
|
|
1370
|
+
if (!origin || isOriginDefault(origin)) continue;
|
|
1371
|
+
if (decls["transform-box"]) continue;
|
|
1372
|
+
findings.push({
|
|
1373
|
+
rule: "svg-motion-transform-box",
|
|
1374
|
+
file,
|
|
1375
|
+
line: rule.line,
|
|
1376
|
+
snippet: `selector "${rule.selector}" sets transform-origin: ${origin} with no transform-box \u2014 the pivot resolves against the view box`,
|
|
1377
|
+
severity: "error"
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
return dedupe(findings);
|
|
1381
|
+
}
|
|
1382
|
+
function isOriginDefault(value) {
|
|
1383
|
+
const v = value.trim().toLowerCase();
|
|
1384
|
+
return v === "0 0" || v === "0px 0px" || v === "0" || v === "left top" || v === "0% 0%";
|
|
1385
|
+
}
|
|
1386
|
+
function ruleNoReducedMotion(content, elements, file) {
|
|
1387
|
+
if (!hasSmilMarkup(content) && !hasCssAnimation(content)) return [];
|
|
1388
|
+
if (hasReducedMotionQuery(content)) return [];
|
|
1389
|
+
const line = hasSmilMarkup(content) ? firstSmilLine(content) : elements.find((el) => !el.closing)?.line ?? 1;
|
|
1390
|
+
return [
|
|
1391
|
+
{
|
|
1392
|
+
rule: "svg-motion-no-reduced-motion",
|
|
1393
|
+
file,
|
|
1394
|
+
line,
|
|
1395
|
+
snippet: "the file animates and carries no prefers-reduced-motion route at all \u2014 a viewer who asked the OS to stop motion still gets it",
|
|
1396
|
+
severity: "error"
|
|
1397
|
+
}
|
|
1398
|
+
];
|
|
1399
|
+
}
|
|
1400
|
+
function ruleSmilCssOnlyBranch(content, file) {
|
|
1401
|
+
if (smilVerdict(content) !== "css-only") return [];
|
|
1402
|
+
return [
|
|
1403
|
+
{
|
|
1404
|
+
rule: "svg-motion-smil-css-only",
|
|
1405
|
+
file,
|
|
1406
|
+
line: firstSmilLine(content),
|
|
1407
|
+
snippet: "this file animates with SMIL and its only reduced-motion route is a CSS declaration \u2014 CSS animation/transition properties do not reach SMIL, so the animation keeps running",
|
|
1408
|
+
severity: "error"
|
|
1409
|
+
}
|
|
1410
|
+
];
|
|
1411
|
+
}
|
|
1412
|
+
var PATH_COMMAND_RE = /[a-zA-Z]/g;
|
|
1413
|
+
function ruleDMorphMismatch(elements, file) {
|
|
1414
|
+
const findings = [];
|
|
1415
|
+
for (const el of elements) {
|
|
1416
|
+
if (el.closing) continue;
|
|
1417
|
+
if (!SMIL_TAGS.has(el.name)) continue;
|
|
1418
|
+
if ((el.attrs["attributename"] ?? "").toLowerCase() !== "d") continue;
|
|
1419
|
+
const raw = el.attrs["values"];
|
|
1420
|
+
if (!raw) continue;
|
|
1421
|
+
const entries = raw.split(";").map((s) => s.trim()).filter(Boolean);
|
|
1422
|
+
if (entries.length < 2) continue;
|
|
1423
|
+
const shapes = entries.map((e) => (e.match(PATH_COMMAND_RE) ?? []).join("").toUpperCase());
|
|
1424
|
+
const first = shapes[0];
|
|
1425
|
+
const differing = shapes.findIndex((s) => s !== first);
|
|
1426
|
+
if (differing === -1) continue;
|
|
1427
|
+
findings.push({
|
|
1428
|
+
rule: "svg-motion-d-morph-mismatch",
|
|
1429
|
+
file,
|
|
1430
|
+
line: el.line,
|
|
1431
|
+
snippet: `<${el.name} attributeName="d"> value ${differing + 1} has command sequence "${shapes[differing]}" against "${first}" in value 1 \u2014 a path morph interpolates coordinates, not commands`,
|
|
1432
|
+
severity: "error"
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
return findings;
|
|
1436
|
+
}
|
|
1437
|
+
function dedupe(findings) {
|
|
1438
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1439
|
+
const out = [];
|
|
1440
|
+
for (const f of findings) {
|
|
1441
|
+
const key = `${f.rule}::${f.file}::${f.line}::${f.snippet}`;
|
|
1442
|
+
if (seen.has(key)) continue;
|
|
1443
|
+
seen.add(key);
|
|
1444
|
+
out.push(f);
|
|
1445
|
+
}
|
|
1446
|
+
return out;
|
|
1447
|
+
}
|
|
1448
|
+
function scanSvg(content, relPath, opts = {}) {
|
|
1449
|
+
const mode = opts.delivery ?? "inline";
|
|
1450
|
+
const elements = parseElements(content);
|
|
1451
|
+
const doc = parseDocument(content);
|
|
1452
|
+
const nameState = rootNameState(content, doc, elements);
|
|
1453
|
+
const text = maskComments(content);
|
|
1454
|
+
const raw = [
|
|
1455
|
+
...ruleWellFormed(doc, relPath),
|
|
1456
|
+
// No `text` argument: the rule reads elements only, and comment
|
|
1457
|
+
// skipping already happened inside `parseElements`. Passing the masked
|
|
1458
|
+
// text told a reader the parameter was load-bearing (finding 12);
|
|
1459
|
+
// `no-unused-vars` could not catch it because two later parameters were
|
|
1460
|
+
// used.
|
|
1461
|
+
...ruleDegenerateGradient(elements, relPath),
|
|
1462
|
+
...ruleMissingXmlns(elements, relPath),
|
|
1463
|
+
...ruleOutsideViewBox(elements, relPath),
|
|
1464
|
+
...ruleUnparseablePath(elements, relPath),
|
|
1465
|
+
...ruleUnusedDefs(text, elements, relPath),
|
|
1466
|
+
...ruleDanglingReference(text, elements, relPath),
|
|
1467
|
+
...ruleActiveContent(text, elements, relPath, mode),
|
|
1468
|
+
...ruleInternalSubset(text, relPath),
|
|
1469
|
+
...ruleExternalReference(text, elements, relPath, mode),
|
|
1470
|
+
...ruleEmbeddedRaster(elements, relPath),
|
|
1471
|
+
...ruleGenericId(elements, relPath),
|
|
1472
|
+
...ruleAccessibleName(nameState, relPath),
|
|
1473
|
+
...ruleDoubleName(nameState, relPath),
|
|
1474
|
+
...ruleKeySplinesRange(elements, relPath),
|
|
1475
|
+
...ruleTransformBox(text, elements, relPath),
|
|
1476
|
+
...ruleNoReducedMotion(text, elements, relPath),
|
|
1477
|
+
...ruleSmilCssOnlyBranch(text, relPath),
|
|
1478
|
+
...ruleDMorphMismatch(elements, relPath)
|
|
1479
|
+
];
|
|
1480
|
+
const ignores = parseIgnores(content.split("\n"));
|
|
1481
|
+
return dedupe(raw).filter((f) => !isIgnored(ignores, f));
|
|
1482
|
+
}
|
|
1483
|
+
function maskScriptBodies(content) {
|
|
1484
|
+
const out = content.split("");
|
|
1485
|
+
const re = /<(script|style)\b[^>]*>/gi;
|
|
1486
|
+
let m;
|
|
1487
|
+
while ((m = re.exec(content)) !== null) {
|
|
1488
|
+
const bodyStart = m.index + m[0].length;
|
|
1489
|
+
const close = content.toLowerCase().indexOf(`</${m[1].toLowerCase()}`, bodyStart);
|
|
1490
|
+
const bodyEnd = close === -1 ? content.length : close;
|
|
1491
|
+
for (let i = bodyStart; i < bodyEnd; i += 1) {
|
|
1492
|
+
if (out[i] !== "\n" && out[i] !== "\r") out[i] = " ";
|
|
1493
|
+
}
|
|
1494
|
+
re.lastIndex = bodyEnd;
|
|
1495
|
+
}
|
|
1496
|
+
return out.join("");
|
|
1497
|
+
}
|
|
1498
|
+
function topLevelGraphics(text) {
|
|
1499
|
+
const out = [];
|
|
1500
|
+
const tagRe = /<(\/?)svg\b[^>]*?(\/?)>/gi;
|
|
1501
|
+
let depth = 0;
|
|
1502
|
+
let start = -1;
|
|
1503
|
+
let t;
|
|
1504
|
+
while ((t = tagRe.exec(text)) !== null) {
|
|
1505
|
+
const closing = t[1] === "/";
|
|
1506
|
+
const selfClosing = t[2] === "/";
|
|
1507
|
+
if (closing) {
|
|
1508
|
+
if (depth === 0) continue;
|
|
1509
|
+
depth -= 1;
|
|
1510
|
+
if (depth === 0 && start !== -1) {
|
|
1511
|
+
out.push({ start, end: t.index + t[0].length });
|
|
1512
|
+
start = -1;
|
|
1513
|
+
}
|
|
1514
|
+
continue;
|
|
1515
|
+
}
|
|
1516
|
+
if (selfClosing) {
|
|
1517
|
+
if (depth === 0) out.push({ start: t.index, end: t.index + t[0].length });
|
|
1518
|
+
continue;
|
|
1519
|
+
}
|
|
1520
|
+
if (depth === 0) start = t.index;
|
|
1521
|
+
depth += 1;
|
|
1522
|
+
}
|
|
1523
|
+
if (depth > 0 && start !== -1) out.push({ start, end: text.length });
|
|
1524
|
+
return out;
|
|
1525
|
+
}
|
|
1526
|
+
function scanHostHtml(content, relPath, opts = {}) {
|
|
1527
|
+
const mode = opts.delivery ?? "inline";
|
|
1528
|
+
const findings = [];
|
|
1529
|
+
const lineOf = lineIndexer2(content);
|
|
1530
|
+
const graphics = [];
|
|
1531
|
+
const hostText = maskScriptBodies(maskComments(content));
|
|
1532
|
+
for (const m of topLevelGraphics(hostText)) {
|
|
1533
|
+
graphics.push({ body: content.slice(m.start, m.end), offset: m.start });
|
|
1534
|
+
}
|
|
1535
|
+
for (const g of graphics) {
|
|
1536
|
+
const baseLine = lineOf(g.offset) - 1;
|
|
1537
|
+
for (const f of scanSvg(g.body, relPath, { delivery: mode })) {
|
|
1538
|
+
if (f.rule === "svg-structure-missing-xmlns") continue;
|
|
1539
|
+
if (f.rule === "svg-motion-no-reduced-motion") continue;
|
|
1540
|
+
if (f.rule === "svg-motion-smil-css-only") continue;
|
|
1541
|
+
findings.push({ ...f, line: f.line + baseLine });
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1545
|
+
for (const g of graphics) {
|
|
1546
|
+
const baseLine = lineOf(g.offset) - 1;
|
|
1547
|
+
for (const el of parseElements(g.body)) {
|
|
1548
|
+
if (el.closing) continue;
|
|
1549
|
+
const id = el.attrs["id"];
|
|
1550
|
+
if (!id) continue;
|
|
1551
|
+
const key = id.toLowerCase();
|
|
1552
|
+
if (seen.has(key)) {
|
|
1553
|
+
findings.push({
|
|
1554
|
+
// ITS OWN RULE ID. Finding 4 of the fifth completion
|
|
1555
|
+
// review: this error shared `svg-a11y-generic-id` with the
|
|
1556
|
+
// single-graphic WARNING about a generic name, so one
|
|
1557
|
+
// `svg-asset-disable svg-a11y-generic-id` — a reasonable
|
|
1558
|
+
// thing to write when you have deliberately named
|
|
1559
|
+
// something `icon` — silenced a real collision between two
|
|
1560
|
+
// graphics as well. Two findings, two severities, two
|
|
1561
|
+
// remedies; one id was one too few.
|
|
1562
|
+
rule: "svg-a11y-duplicate-id",
|
|
1563
|
+
file: relPath,
|
|
1564
|
+
line: el.line + baseLine,
|
|
1565
|
+
snippet: `id="${id}" is already used by another inline graphic on this page (line ${seen.get(key)}) \u2014 the accessibility tree names both after the first`,
|
|
1566
|
+
severity: "error"
|
|
1567
|
+
});
|
|
1568
|
+
} else {
|
|
1569
|
+
seen.set(key, el.line + baseLine);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
const ignores = parseIgnores(content.split("\n"));
|
|
1574
|
+
return dedupe(findings).filter((f) => !isIgnored(ignores, f));
|
|
1575
|
+
}
|
|
1576
|
+
function parseIgnores(lines) {
|
|
1577
|
+
const ignores = /* @__PURE__ */ new Map();
|
|
1578
|
+
const slot = (rule) => {
|
|
1579
|
+
const found = ignores.get(rule) ?? { lines: /* @__PURE__ */ new Set(), wholeFile: false };
|
|
1580
|
+
ignores.set(rule, found);
|
|
1581
|
+
return found;
|
|
1582
|
+
};
|
|
1583
|
+
const add = (rule, line) => {
|
|
1584
|
+
slot(rule).lines.add(line);
|
|
1585
|
+
};
|
|
1586
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1587
|
+
const line = lines[i] ?? "";
|
|
1588
|
+
const next = /svg-asset-disable-next-line\s+([\w-]+)/i.exec(line);
|
|
1589
|
+
if (next?.[1]) add(next[1].toLowerCase(), i + 2);
|
|
1590
|
+
const same = /svg-asset-disable-line\s+([\w-]+)/i.exec(line);
|
|
1591
|
+
if (same?.[1]) add(same[1].toLowerCase(), i + 1);
|
|
1592
|
+
const whole = /svg-asset-disable\s+([\w-]+)/i.exec(line);
|
|
1593
|
+
if (whole?.[1] && !/-next-line|-line\s/.test(line)) {
|
|
1594
|
+
slot(whole[1].toLowerCase()).wholeFile = true;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
return ignores;
|
|
1598
|
+
}
|
|
1599
|
+
function isIgnored(ignores, finding) {
|
|
1600
|
+
if (finding.rule.startsWith("svg-safety-")) return false;
|
|
1601
|
+
const hit = ignores.get(finding.rule.toLowerCase());
|
|
1602
|
+
if (hit === void 0) return false;
|
|
1603
|
+
return hit.wholeFile || hit.lines.has(finding.line);
|
|
1604
|
+
}
|
|
1605
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1606
|
+
0 && (module.exports = {
|
|
1607
|
+
DELIVERY_MODES,
|
|
1608
|
+
GENERIC_IDS,
|
|
1609
|
+
VECTOR_GEOMETRY,
|
|
1610
|
+
directChildrenOfRoot,
|
|
1611
|
+
elementExtent,
|
|
1612
|
+
maskComments,
|
|
1613
|
+
parseElements,
|
|
1614
|
+
parseIgnores,
|
|
1615
|
+
pathDataError,
|
|
1616
|
+
pathExtent,
|
|
1617
|
+
pointsExtent,
|
|
1618
|
+
readViewBox,
|
|
1619
|
+
referencedIds,
|
|
1620
|
+
safetySeverity,
|
|
1621
|
+
scanHostHtml,
|
|
1622
|
+
scanSvg,
|
|
1623
|
+
staticBox,
|
|
1624
|
+
tokenisePath,
|
|
1625
|
+
vectorVerdict
|
|
1626
|
+
});
|