@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,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scan-scope assertions — "a gate that read nothing did not pass".
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS (audit 2026-07-29, `agents/settings/contexts/gates-that-cannot-fail.md`):
|
|
5
|
+
* ADR-051 moved the source container and a later commit deleted `packages/`.
|
|
6
|
+
* Fourteen `lint_*`/`check_*` gates kept a hardcoded literal path, and every
|
|
7
|
+
* one of them treats a missing directory as "nothing to check" — so they exit 0
|
|
8
|
+
* and print a green checkmark while scanning zero files. Their own output said
|
|
9
|
+
* so (`0 file(s) scanned`, `0 name(s) checked`, `0 declarer(s)`); nothing ever
|
|
10
|
+
* asserted on the number.
|
|
11
|
+
*
|
|
12
|
+
* Two shapes cover the whole class:
|
|
13
|
+
*
|
|
14
|
+
* - **Corpus gates** walk a root and check N units → {@link assertScanned}.
|
|
15
|
+
* Zero units is a failure unless the gate declares a justified `allowEmpty`.
|
|
16
|
+
* - **Watch-list gates** (diff-based, no corpus to count) name specific files
|
|
17
|
+
* they guard → {@link assertWatchlistResolves}. A watch list that resolves to
|
|
18
|
+
* nothing on disk means the guard is watching phantoms.
|
|
19
|
+
*
|
|
20
|
+
* Both throw {@link DeadScopeError}; callers map it to a non-zero exit. The
|
|
21
|
+
* point is that the failure is LOUD and names the root, so the next path
|
|
22
|
+
* migration cannot silently disarm a gate again.
|
|
23
|
+
*/
|
|
24
|
+
import * as fs from 'node:fs';
|
|
25
|
+
import * as path from 'node:path';
|
|
26
|
+
import process from 'node:process';
|
|
27
|
+
/** Raised when a gate's scope is empty — it cannot have checked anything. */
|
|
28
|
+
export class DeadScopeError extends Error {
|
|
29
|
+
gate;
|
|
30
|
+
constructor(gate, message) {
|
|
31
|
+
super(message);
|
|
32
|
+
this.name = 'DeadScopeError';
|
|
33
|
+
this.gate = gate;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Assert a corpus gate examined at least one unit.
|
|
38
|
+
*
|
|
39
|
+
* @throws {DeadScopeError} when `scanned === 0` and no `allowEmpty` reason is given.
|
|
40
|
+
*/
|
|
41
|
+
export function assertScanned(opts) {
|
|
42
|
+
if (opts.scanned > 0) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (opts.allowEmpty !== undefined && opts.allowEmpty.trim() !== '') {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const roots = opts.roots.length > 0 ? opts.roots.join(', ') : '(no root declared)';
|
|
49
|
+
throw new DeadScopeError(opts.gate, `${opts.gate}: scanned 0 ${opts.units} under ${roots} — the scan scope is ` +
|
|
50
|
+
'dead or the root moved. A gate that read nothing has not passed. ' +
|
|
51
|
+
'Repoint the root (prefer the shared resolver in _lib/agent_src.ts), ' +
|
|
52
|
+
'or declare a justified `allowEmpty` reason if an empty corpus is genuinely expected.');
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Assert a corpus gate's scope AND publish the count it just validated.
|
|
56
|
+
*
|
|
57
|
+
* The two halves of scope hardening drifted apart in practice: 14 gates emit
|
|
58
|
+
* the machine-readable `scanned:` line the coverage guard reads, 12 assert
|
|
59
|
+
* their scope, and only 8 do both. A gate that publishes a number it never
|
|
60
|
+
* asserted can print `scanned: 0` and exit green; a gate that asserts without
|
|
61
|
+
* publishing is invisible to `check_gate_coverage`. Splitting the two also
|
|
62
|
+
* lets the published number drift from the number that was validated — the
|
|
63
|
+
* invented-count failure that is mechanically undetectable downstream.
|
|
64
|
+
*
|
|
65
|
+
* One call closes both: the emitted number is, by construction, the number the
|
|
66
|
+
* assertion just accepted. Use this for any gate that should also carry a floor
|
|
67
|
+
* in `src/config/gate-coverage.yml`; plain {@link assertScanned} stays correct
|
|
68
|
+
* for a gate that only needs its scope guarded.
|
|
69
|
+
*
|
|
70
|
+
* The line goes to stdout unconditionally — a count only visible without
|
|
71
|
+
* `--quiet` is not a count, since CI passes `--quiet` (the `lint_handoffs`
|
|
72
|
+
* lesson, preserved here so no caller has to rediscover it).
|
|
73
|
+
*/
|
|
74
|
+
export function reportScanned(opts, write = process.stdout.write.bind(process.stdout)) {
|
|
75
|
+
assertScanned(opts);
|
|
76
|
+
write(`scanned: ${String(opts.scanned)}\n`);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Assert a watch-list gate's targets exist on disk, and return the ones that do.
|
|
80
|
+
*
|
|
81
|
+
* A diff-based guard has no corpus to count: it compares changed paths against
|
|
82
|
+
* a fixed list. If that list resolves to nothing, the guard reports "clean"
|
|
83
|
+
* forever — the exact shape of the `check_safety_floor_untouched` defect, which
|
|
84
|
+
* announced `4 rules guarded` while guarding none.
|
|
85
|
+
*
|
|
86
|
+
* @returns the subset of `candidates` that exist.
|
|
87
|
+
* @throws {DeadScopeError} when none of them exist.
|
|
88
|
+
*/
|
|
89
|
+
export function assertWatchlistResolves(opts) {
|
|
90
|
+
const resolved = opts.candidates.filter((rel) => {
|
|
91
|
+
try {
|
|
92
|
+
return fs.statSync(path.join(opts.repoRoot, rel)).isFile();
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
if (resolved.length === 0) {
|
|
99
|
+
throw new DeadScopeError(opts.gate, `${opts.gate}: none of its ${opts.candidates.length} guarded path(s) exist ` +
|
|
100
|
+
`under ${opts.repoRoot} — the guard is watching phantoms and can never fire. ` +
|
|
101
|
+
`Checked: ${opts.candidates.join(', ')}`);
|
|
102
|
+
}
|
|
103
|
+
return resolved;
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=scan_scope.js.map
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* svg_dom — a dependency-free XML well-formedness parser, and the tree it
|
|
3
|
+
* builds.
|
|
4
|
+
*
|
|
5
|
+
* Step 3.1 of `road-to-svg-review-followups`, amending step 1.1 of
|
|
6
|
+
* `road-to-svg-with-a-witness`. Review claim C11: a regex cannot prove nesting
|
|
7
|
+
* or resolve a reference, and the alternatives were all refused for reasons
|
|
8
|
+
* that still hold — `package.json`'s runtime dependencies carry no XML parser,
|
|
9
|
+
* `happy-dom` is dev-only, the three vendored tree-sitter grammars are php,
|
|
10
|
+
* typescript and javascript, and Playwright is absent in a consumer, so the
|
|
11
|
+
* browser cannot carry a static layer. The remaining option is to write one,
|
|
12
|
+
* which is what `lint_design_quality` already does for its own surface.
|
|
13
|
+
*
|
|
14
|
+
* WHAT IT IS. One scanner over the bytes, producing every node kind an SVG
|
|
15
|
+
* carries — elements with their attributes, comments, CDATA sections,
|
|
16
|
+
* processing instructions, DOCTYPEs and text — each with a line and a column,
|
|
17
|
+
* assembled into a tree with parent links, plus a list of well-formedness
|
|
18
|
+
* errors.
|
|
19
|
+
*
|
|
20
|
+
* TWO SCANNERS, NOT ONE, and this header used to claim otherwise.
|
|
21
|
+
* `parseElements` in `svg_rules.ts` is an independent scanner over the same
|
|
22
|
+
* bytes, so the two CAN disagree about what a file contains — finding 3 of the
|
|
23
|
+
* third completion review of this change found a case where they did, and the
|
|
24
|
+
* disagreement produced an error-severity false positive. That one is fixed
|
|
25
|
+
* where it was (the prolog), but the general claim was false and is not
|
|
26
|
+
* restored: a reader deciding whether a fix here also lands there must check,
|
|
27
|
+
* not assume. Merging them is a larger change than the finding asked for and
|
|
28
|
+
* is not attempted under a remediation.
|
|
29
|
+
*
|
|
30
|
+
* WHAT IT IS NOT. Not a validating parser, not namespace-aware beyond keeping
|
|
31
|
+
* the prefix in the name, and it does not expand entities. It reports that a
|
|
32
|
+
* file is not well-formed; it does not report which schema it violates.
|
|
33
|
+
*
|
|
34
|
+
* WHY A FILE THAT IS NOT WELL-FORMED IS A FINDING RATHER THAN A SKIPPED FILE.
|
|
35
|
+
* A skipped file reads as a clean one in every count that matters, and the
|
|
36
|
+
* shape this exists to catch — `<g><path></g></path>` — is exactly the shape a
|
|
37
|
+
* flat element list cannot see: both tags are present, both are spelled
|
|
38
|
+
* correctly, and the document is nonetheless not a tree.
|
|
39
|
+
*/
|
|
40
|
+
/** 1-based line and column of an offset. Built once per parse. */
|
|
41
|
+
function positions(content) {
|
|
42
|
+
const starts = [0];
|
|
43
|
+
for (let i = 0; i < content.length; i += 1) {
|
|
44
|
+
if (content[i] === '\n')
|
|
45
|
+
starts.push(i + 1);
|
|
46
|
+
}
|
|
47
|
+
return (offset) => {
|
|
48
|
+
// Binary search for the last line start at or before `offset`.
|
|
49
|
+
let lo = 0;
|
|
50
|
+
let hi = starts.length - 1;
|
|
51
|
+
while (lo < hi) {
|
|
52
|
+
const mid = (lo + hi + 1) >> 1;
|
|
53
|
+
if (starts[mid] <= offset)
|
|
54
|
+
lo = mid;
|
|
55
|
+
else
|
|
56
|
+
hi = mid - 1;
|
|
57
|
+
}
|
|
58
|
+
return { line: lo + 1, column: offset - starts[lo] + 1 };
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* End of the tag, honouring quoted attribute values that may contain `>`.
|
|
63
|
+
*
|
|
64
|
+
* Exported because `svg_rules.parseElements` scans the same bytes and had its
|
|
65
|
+
* own byte-identical copy — finding 9 of the third completion review, which
|
|
66
|
+
* also noted that BOTH copies ship in the per-`.svg`-write hook chunk. Two
|
|
67
|
+
* scanners is a fact this file's header now states plainly; two copies of
|
|
68
|
+
* their shared primitives is not a consequence of it.
|
|
69
|
+
*/
|
|
70
|
+
export function tagEnd(content, from) {
|
|
71
|
+
let quote = null;
|
|
72
|
+
for (let i = from + 1; i < content.length; i += 1) {
|
|
73
|
+
const ch = content[i];
|
|
74
|
+
if (quote !== null) {
|
|
75
|
+
if (ch === quote)
|
|
76
|
+
quote = null;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (ch === '"' || ch === "'") {
|
|
80
|
+
quote = ch;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (ch === '>')
|
|
84
|
+
return i;
|
|
85
|
+
}
|
|
86
|
+
return -1;
|
|
87
|
+
}
|
|
88
|
+
/** Attributes of one tag, lowercased keys. Shared with `svg_rules` — see `tagEnd`. */
|
|
89
|
+
export function attrsOf(raw) {
|
|
90
|
+
const attrs = {};
|
|
91
|
+
const re = /([A-Za-z_:][\w.:-]*)\s*=\s*("([^"]*)"|'([^']*)')/g;
|
|
92
|
+
let m;
|
|
93
|
+
while ((m = re.exec(raw)) !== null) {
|
|
94
|
+
const key = (m[1] ?? '').toLowerCase();
|
|
95
|
+
const value = m[3] ?? m[4] ?? '';
|
|
96
|
+
attrs[key] = value.replace(/&/g, '&');
|
|
97
|
+
}
|
|
98
|
+
return attrs;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Parse one document.
|
|
102
|
+
*
|
|
103
|
+
* Never throws: a malformed file produces whatever tree could be built plus
|
|
104
|
+
* the errors that explain it. A parser that threw would make an unparseable
|
|
105
|
+
* file indistinguishable from a crash, and the caller's job is to REPORT the
|
|
106
|
+
* file, not to fall over on it.
|
|
107
|
+
*/
|
|
108
|
+
export function parseDocument(content) {
|
|
109
|
+
const at = positions(content);
|
|
110
|
+
const roots = [];
|
|
111
|
+
const elements = [];
|
|
112
|
+
const errors = [];
|
|
113
|
+
const stack = [];
|
|
114
|
+
const make = (kind, start, name = '', value = '', attrs = {}) => {
|
|
115
|
+
const { line, column } = at(start);
|
|
116
|
+
return { kind, name, attrs, line, column, start, value, children: [], parent: null };
|
|
117
|
+
};
|
|
118
|
+
const append = (node) => {
|
|
119
|
+
const parent = stack[stack.length - 1];
|
|
120
|
+
if (parent === undefined) {
|
|
121
|
+
roots.push(node);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
node.parent = parent;
|
|
125
|
+
parent.children.push(node);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
let i = 0;
|
|
129
|
+
while (i < content.length) {
|
|
130
|
+
const lt = content.indexOf('<', i);
|
|
131
|
+
if (lt === -1) {
|
|
132
|
+
const rest = content.slice(i);
|
|
133
|
+
if (rest.trim() !== '')
|
|
134
|
+
append(make('text', i, '', rest));
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
if (lt > i) {
|
|
138
|
+
const text = content.slice(i, lt);
|
|
139
|
+
if (text.trim() !== '')
|
|
140
|
+
append(make('text', i, '', text));
|
|
141
|
+
}
|
|
142
|
+
if (content.startsWith('<!--', lt)) {
|
|
143
|
+
const end = content.indexOf('-->', lt + 4);
|
|
144
|
+
if (end === -1) {
|
|
145
|
+
const p = at(lt);
|
|
146
|
+
errors.push({ message: 'unterminated comment', line: p.line, column: p.column });
|
|
147
|
+
append(make('comment', lt, '', content.slice(lt + 4)));
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
append(make('comment', lt, '', content.slice(lt + 4, end)));
|
|
151
|
+
i = end + 3;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (content.startsWith('<![CDATA[', lt)) {
|
|
155
|
+
const end = content.indexOf(']]>', lt + 9);
|
|
156
|
+
if (end === -1) {
|
|
157
|
+
const p = at(lt);
|
|
158
|
+
errors.push({ message: 'unterminated CDATA section', line: p.line, column: p.column });
|
|
159
|
+
append(make('cdata', lt, '', content.slice(lt + 9)));
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
append(make('cdata', lt, '', content.slice(lt + 9, end)));
|
|
163
|
+
i = end + 3;
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
if (content.startsWith('<?', lt)) {
|
|
167
|
+
const end = content.indexOf('?>', lt + 2);
|
|
168
|
+
if (end === -1) {
|
|
169
|
+
// Finding 11 of this change's completion review: this and the
|
|
170
|
+
// declaration below swallowed the rest of the document into one
|
|
171
|
+
// node and returned cleanly, so a file ending in
|
|
172
|
+
// `<?xml version="1.0"` read as WELL-FORMED — a skip, under a
|
|
173
|
+
// contract whose whole point is that a file which does not
|
|
174
|
+
// parse is a finding rather than a skipped file.
|
|
175
|
+
const p = at(lt);
|
|
176
|
+
errors.push({ message: 'unterminated processing instruction', line: p.line, column: p.column });
|
|
177
|
+
}
|
|
178
|
+
const stop = end === -1 ? content.length : end + 2;
|
|
179
|
+
append(make('pi', lt, '', content.slice(lt + 2, end === -1 ? content.length : end)));
|
|
180
|
+
i = stop;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (content.startsWith('<!', lt)) {
|
|
184
|
+
// A DOCTYPE may carry a bracketed internal subset; it ends after it.
|
|
185
|
+
const bracket = content.indexOf('[', lt);
|
|
186
|
+
const close = content.indexOf('>', lt);
|
|
187
|
+
let stop;
|
|
188
|
+
if (bracket !== -1 && close !== -1 && bracket < close) {
|
|
189
|
+
const endBracket = content.indexOf(']', bracket);
|
|
190
|
+
const after = endBracket === -1 ? close : content.indexOf('>', endBracket);
|
|
191
|
+
stop = after === -1 ? content.length : after + 1;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
stop = close === -1 ? content.length : close + 1;
|
|
195
|
+
}
|
|
196
|
+
if (close === -1) {
|
|
197
|
+
const p = at(lt);
|
|
198
|
+
errors.push({ message: 'unterminated declaration', line: p.line, column: p.column });
|
|
199
|
+
}
|
|
200
|
+
append(make('doctype', lt, '', content.slice(lt + 2, Math.max(lt + 2, stop - 1))));
|
|
201
|
+
i = stop;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const gt = tagEnd(content, lt);
|
|
205
|
+
if (gt === -1) {
|
|
206
|
+
const p = at(lt);
|
|
207
|
+
errors.push({ message: 'unterminated tag', line: p.line, column: p.column });
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
const raw = content.slice(lt, gt + 1);
|
|
211
|
+
const nameMatch = /^<\/?\s*([A-Za-z_][\w.:-]*)/.exec(raw);
|
|
212
|
+
if (!nameMatch) {
|
|
213
|
+
// Finding 11, third case: a `<` that opens something with no legal
|
|
214
|
+
// element name is malformed markup, not whitespace. Skipping it
|
|
215
|
+
// silently is the same skip the other two were.
|
|
216
|
+
const p = at(lt);
|
|
217
|
+
errors.push({ message: `"${raw.slice(0, 24)}" is not a tag`, line: p.line, column: p.column });
|
|
218
|
+
i = gt + 1;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
const name = (nameMatch[1] ?? '').toLowerCase();
|
|
222
|
+
const closing = raw.startsWith('</');
|
|
223
|
+
const selfClosing = raw.endsWith('/>');
|
|
224
|
+
if (closing) {
|
|
225
|
+
const open = stack[stack.length - 1];
|
|
226
|
+
const p = at(lt);
|
|
227
|
+
if (open === undefined) {
|
|
228
|
+
errors.push({ message: `closing tag </${name}> with no open element`, line: p.line, column: p.column });
|
|
229
|
+
}
|
|
230
|
+
else if (open.name !== name) {
|
|
231
|
+
// The shape a flat element list cannot see. Reported against
|
|
232
|
+
// the CLOSING tag, which is where a reader looking at the file
|
|
233
|
+
// sees the problem.
|
|
234
|
+
errors.push({
|
|
235
|
+
message: `closing tag </${name}> does not match the open <${open.name}> (line ${String(open.line)})`,
|
|
236
|
+
line: p.line,
|
|
237
|
+
column: p.column,
|
|
238
|
+
});
|
|
239
|
+
// Recover by popping to the matching open element WHEN THERE
|
|
240
|
+
// IS ONE, so a single mistake does not cascade into an error
|
|
241
|
+
// per remaining tag.
|
|
242
|
+
//
|
|
243
|
+
// When there is none, pop NOTHING. Finding 2 of the fourth
|
|
244
|
+
// completion review: the `else stack.pop()` detached a
|
|
245
|
+
// perfectly good ancestor to pay for a stray tag that never
|
|
246
|
+
// opened anything — a file whose `</g>` has no `<g>` lost its
|
|
247
|
+
// `<svg>` from the stack, so the real `</svg>` at the end then
|
|
248
|
+
// had no open element and the file collected a SECOND error it
|
|
249
|
+
// had not earned. Cascading is exactly what this branch exists
|
|
250
|
+
// to prevent, and the unmatched case was the one where it did
|
|
251
|
+
// the cascading itself. A closing tag with no match is a stray
|
|
252
|
+
// tag: report it and carry on.
|
|
253
|
+
const idx = stack.map((n) => n.name).lastIndexOf(name);
|
|
254
|
+
if (idx !== -1)
|
|
255
|
+
stack.length = idx;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
stack.pop();
|
|
259
|
+
}
|
|
260
|
+
i = gt + 1;
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
const node = make('element', lt, name, '', attrsOf(raw));
|
|
264
|
+
append(node);
|
|
265
|
+
elements.push(node);
|
|
266
|
+
if (!selfClosing)
|
|
267
|
+
stack.push(node);
|
|
268
|
+
i = gt + 1;
|
|
269
|
+
}
|
|
270
|
+
for (const unclosed of stack) {
|
|
271
|
+
errors.push({
|
|
272
|
+
message: `<${unclosed.name}> is never closed`,
|
|
273
|
+
line: unclosed.line,
|
|
274
|
+
column: unclosed.column,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return { roots, elements, errors };
|
|
278
|
+
}
|
|
279
|
+
// `ancestors` and `documentElement` used to live here. Finding 11 of the third
|
|
280
|
+
// completion review: both were exported, called by nothing, and bundled into
|
|
281
|
+
// the per-`.svg`-write dispatch chunk — own-orphans of this same diff, which
|
|
282
|
+
// `minimal-safe-diff` says the diff that made them removes. `XmlNode.parent`
|
|
283
|
+
// is the primitive either would have been built on and it is still here.
|
|
284
|
+
//# sourceMappingURL=svg_dom.js.map
|