@reticular/speakable 1.0.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/README.md +31 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +2862 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +348 -0
- package/dist/index.js +512 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
// src/model/types.ts
|
|
2
|
+
var SUPPORTED_ROLES = [
|
|
3
|
+
// Original roles (22)
|
|
4
|
+
"button",
|
|
5
|
+
"link",
|
|
6
|
+
"heading",
|
|
7
|
+
"textbox",
|
|
8
|
+
"checkbox",
|
|
9
|
+
"radio",
|
|
10
|
+
"combobox",
|
|
11
|
+
"listbox",
|
|
12
|
+
"option",
|
|
13
|
+
"list",
|
|
14
|
+
"listitem",
|
|
15
|
+
"navigation",
|
|
16
|
+
"main",
|
|
17
|
+
"banner",
|
|
18
|
+
"contentinfo",
|
|
19
|
+
"region",
|
|
20
|
+
"img",
|
|
21
|
+
"article",
|
|
22
|
+
"complementary",
|
|
23
|
+
"form",
|
|
24
|
+
"search",
|
|
25
|
+
"generic",
|
|
26
|
+
// New roles (21)
|
|
27
|
+
"paragraph",
|
|
28
|
+
"blockquote",
|
|
29
|
+
"code",
|
|
30
|
+
"staticText",
|
|
31
|
+
"table",
|
|
32
|
+
"row",
|
|
33
|
+
"cell",
|
|
34
|
+
"columnheader",
|
|
35
|
+
"rowheader",
|
|
36
|
+
"term",
|
|
37
|
+
"definition",
|
|
38
|
+
"figure",
|
|
39
|
+
"caption",
|
|
40
|
+
"group",
|
|
41
|
+
"dialog",
|
|
42
|
+
"meter",
|
|
43
|
+
"progressbar",
|
|
44
|
+
"status",
|
|
45
|
+
"document",
|
|
46
|
+
"application",
|
|
47
|
+
"separator"
|
|
48
|
+
];
|
|
49
|
+
var CURRENT_MODEL_VERSION = {
|
|
50
|
+
major: 1,
|
|
51
|
+
minor: 0
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// src/model/validation.ts
|
|
55
|
+
var ValidationError = class extends Error {
|
|
56
|
+
constructor(message) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.name = "ValidationError";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
function validateRole(role) {
|
|
62
|
+
if (!SUPPORTED_ROLES.includes(role)) {
|
|
63
|
+
throw new ValidationError(
|
|
64
|
+
`Invalid role: "${role}". Supported roles: ${SUPPORTED_ROLES.join(", ")}`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
function validateState(state) {
|
|
70
|
+
if (state.level !== void 0) {
|
|
71
|
+
if (!Number.isInteger(state.level) || state.level < 1 || state.level > 6) {
|
|
72
|
+
throw new ValidationError(
|
|
73
|
+
`Invalid heading level: ${state.level}. Must be an integer between 1 and 6.`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (state.posinset !== void 0) {
|
|
78
|
+
if (!Number.isInteger(state.posinset) || state.posinset < 1) {
|
|
79
|
+
throw new ValidationError(
|
|
80
|
+
`Invalid posinset: ${state.posinset}. Must be a positive integer.`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (state.setsize !== void 0) {
|
|
85
|
+
if (!Number.isInteger(state.setsize) || state.setsize < 1) {
|
|
86
|
+
throw new ValidationError(
|
|
87
|
+
`Invalid setsize: ${state.setsize}. Must be a positive integer.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (state.posinset !== void 0 && state.setsize !== void 0 && state.posinset > state.setsize) {
|
|
92
|
+
throw new ValidationError(
|
|
93
|
+
`Invalid set position: posinset (${state.posinset}) cannot exceed setsize (${state.setsize}).`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
if (state.checked !== void 0) {
|
|
97
|
+
if (typeof state.checked !== "boolean" && state.checked !== "mixed") {
|
|
98
|
+
throw new ValidationError(
|
|
99
|
+
`Invalid checked value: ${state.checked}. Must be boolean or 'mixed'.`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (state.pressed !== void 0) {
|
|
104
|
+
if (typeof state.pressed !== "boolean" && state.pressed !== "mixed") {
|
|
105
|
+
throw new ValidationError(
|
|
106
|
+
`Invalid pressed value: ${state.pressed}. Must be boolean or 'mixed'.`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (state.current !== void 0) {
|
|
111
|
+
const validCurrentValues = ["page", "step", "location", "date", "time", "true", false];
|
|
112
|
+
if (!validCurrentValues.includes(state.current)) {
|
|
113
|
+
throw new ValidationError(
|
|
114
|
+
`Invalid current value: ${state.current}. Must be one of: ${validCurrentValues.join(", ")}`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function validateTreeStructure(node, visited = /* @__PURE__ */ new Set()) {
|
|
120
|
+
if (visited.has(node)) {
|
|
121
|
+
throw new ValidationError(
|
|
122
|
+
"Circular reference detected in accessibility tree. Tree must be acyclic."
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
visited.add(node);
|
|
126
|
+
validateRole(node.role);
|
|
127
|
+
validateState(node.state);
|
|
128
|
+
for (const child of node.children) {
|
|
129
|
+
validateTreeStructure(child, new Set(visited));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function validateModel(model) {
|
|
133
|
+
if (!model.version || typeof model.version.major !== "number" || typeof model.version.minor !== "number") {
|
|
134
|
+
throw new ValidationError("Invalid model version. Must have major and minor number fields.");
|
|
135
|
+
}
|
|
136
|
+
if (!model.metadata || !model.metadata.extractedAt) {
|
|
137
|
+
throw new ValidationError("Invalid metadata. Must have extractedAt timestamp.");
|
|
138
|
+
}
|
|
139
|
+
const timestamp = new Date(model.metadata.extractedAt);
|
|
140
|
+
if (isNaN(timestamp.getTime())) {
|
|
141
|
+
throw new ValidationError(
|
|
142
|
+
`Invalid extractedAt timestamp: ${model.metadata.extractedAt}. Must be ISO 8601 format.`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
if (!model.root) {
|
|
146
|
+
throw new ValidationError("Model must have a root node.");
|
|
147
|
+
}
|
|
148
|
+
validateTreeStructure(model.root);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/model/serialization.ts
|
|
152
|
+
function sortObjectKeys(obj) {
|
|
153
|
+
if (obj === null || typeof obj !== "object") {
|
|
154
|
+
return obj;
|
|
155
|
+
}
|
|
156
|
+
if (Array.isArray(obj)) {
|
|
157
|
+
return obj.map(sortObjectKeys);
|
|
158
|
+
}
|
|
159
|
+
const sorted = {};
|
|
160
|
+
const keys = Object.keys(obj).sort();
|
|
161
|
+
for (const key of keys) {
|
|
162
|
+
const value = obj[key];
|
|
163
|
+
sorted[key] = typeof value === "object" && value !== null ? sortObjectKeys(value) : value;
|
|
164
|
+
}
|
|
165
|
+
return sorted;
|
|
166
|
+
}
|
|
167
|
+
function serializeModel(model, options = {}) {
|
|
168
|
+
const { pretty = false, validate = true } = options;
|
|
169
|
+
if (validate) {
|
|
170
|
+
validateModel(model);
|
|
171
|
+
}
|
|
172
|
+
const sorted = sortObjectKeys(model);
|
|
173
|
+
return pretty ? JSON.stringify(sorted, null, 2) : JSON.stringify(sorted);
|
|
174
|
+
}
|
|
175
|
+
function deserializeModel(json, options = {}) {
|
|
176
|
+
const { validate = true } = options;
|
|
177
|
+
const model = JSON.parse(json);
|
|
178
|
+
if (validate) {
|
|
179
|
+
validateModel(model);
|
|
180
|
+
}
|
|
181
|
+
return model;
|
|
182
|
+
}
|
|
183
|
+
function createModel(root, sourceHash) {
|
|
184
|
+
return {
|
|
185
|
+
version: { ...CURRENT_MODEL_VERSION },
|
|
186
|
+
root,
|
|
187
|
+
metadata: {
|
|
188
|
+
extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
189
|
+
...sourceHash && { sourceHash }
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function modelsEqual(a, b) {
|
|
194
|
+
const jsonA = serializeModel(a, { validate: false });
|
|
195
|
+
const jsonB = serializeModel(b, { validate: false });
|
|
196
|
+
return jsonA === jsonB;
|
|
197
|
+
}
|
|
198
|
+
function cloneModel(model) {
|
|
199
|
+
const json = serializeModel(model, { validate: false });
|
|
200
|
+
return deserializeModel(json, { validate: false });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/diff/diff-algorithm.ts
|
|
204
|
+
function diffAccessibilityTrees(oldTree, newTree) {
|
|
205
|
+
const changes = [];
|
|
206
|
+
const oldNodes = buildNodeMap(oldTree, "root");
|
|
207
|
+
const newNodes = buildNodeMap(newTree, "root");
|
|
208
|
+
for (const [path, node] of oldNodes) {
|
|
209
|
+
if (!newNodes.has(path)) {
|
|
210
|
+
changes.push({
|
|
211
|
+
type: "removed",
|
|
212
|
+
path,
|
|
213
|
+
node
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
for (const [path, node] of newNodes) {
|
|
218
|
+
if (!oldNodes.has(path)) {
|
|
219
|
+
changes.push({
|
|
220
|
+
type: "added",
|
|
221
|
+
path,
|
|
222
|
+
node
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
for (const [path, oldNode] of oldNodes) {
|
|
227
|
+
const newNode = newNodes.get(path);
|
|
228
|
+
if (newNode) {
|
|
229
|
+
const propertyChanges = compareNodes(oldNode, newNode);
|
|
230
|
+
if (propertyChanges.length > 0) {
|
|
231
|
+
changes.push({
|
|
232
|
+
type: "changed",
|
|
233
|
+
path,
|
|
234
|
+
changes: propertyChanges
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
changes.sort((a, b) => a.path.localeCompare(b.path));
|
|
240
|
+
const summary = {
|
|
241
|
+
added: changes.filter((c) => c.type === "added").length,
|
|
242
|
+
removed: changes.filter((c) => c.type === "removed").length,
|
|
243
|
+
changed: changes.filter((c) => c.type === "changed").length,
|
|
244
|
+
total: changes.length
|
|
245
|
+
};
|
|
246
|
+
return { changes, summary };
|
|
247
|
+
}
|
|
248
|
+
function buildNodeMap(node, path) {
|
|
249
|
+
const map = /* @__PURE__ */ new Map();
|
|
250
|
+
map.set(path, node);
|
|
251
|
+
node.children.forEach((child, index) => {
|
|
252
|
+
const childPath = `${path}.children[${index}]`;
|
|
253
|
+
const childMap = buildNodeMap(child, childPath);
|
|
254
|
+
for (const [childPath2, childNode] of childMap) {
|
|
255
|
+
map.set(childPath2, childNode);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
return map;
|
|
259
|
+
}
|
|
260
|
+
function compareNodes(oldNode, newNode) {
|
|
261
|
+
const changes = [];
|
|
262
|
+
if (oldNode.role !== newNode.role) {
|
|
263
|
+
changes.push({
|
|
264
|
+
property: "role",
|
|
265
|
+
oldValue: oldNode.role,
|
|
266
|
+
newValue: newNode.role
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
if (oldNode.name !== newNode.name) {
|
|
270
|
+
changes.push({
|
|
271
|
+
property: "name",
|
|
272
|
+
oldValue: oldNode.name,
|
|
273
|
+
newValue: newNode.name
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
if (oldNode.description !== newNode.description) {
|
|
277
|
+
changes.push({
|
|
278
|
+
property: "description",
|
|
279
|
+
oldValue: oldNode.description,
|
|
280
|
+
newValue: newNode.description
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
if (!valuesEqual(oldNode.value, newNode.value)) {
|
|
284
|
+
changes.push({
|
|
285
|
+
property: "value",
|
|
286
|
+
oldValue: oldNode.value,
|
|
287
|
+
newValue: newNode.value
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
if (!statesEqual(oldNode.state, newNode.state)) {
|
|
291
|
+
changes.push({
|
|
292
|
+
property: "state",
|
|
293
|
+
oldValue: oldNode.state,
|
|
294
|
+
newValue: newNode.state
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
if (!focusEqual(oldNode.focus, newNode.focus)) {
|
|
298
|
+
changes.push({
|
|
299
|
+
property: "focus",
|
|
300
|
+
oldValue: oldNode.focus,
|
|
301
|
+
newValue: newNode.focus
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
return changes;
|
|
305
|
+
}
|
|
306
|
+
function valuesEqual(a, b) {
|
|
307
|
+
if (a === void 0 && b === void 0) return true;
|
|
308
|
+
if (a === void 0 || b === void 0) return false;
|
|
309
|
+
return a.current === b.current && a.min === b.min && a.max === b.max && a.text === b.text;
|
|
310
|
+
}
|
|
311
|
+
function statesEqual(a, b) {
|
|
312
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
313
|
+
for (const key of keys) {
|
|
314
|
+
const aValue = a[key];
|
|
315
|
+
const bValue = b[key];
|
|
316
|
+
if (aValue !== bValue) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
function focusEqual(a, b) {
|
|
323
|
+
return a.focusable === b.focusable && a.tabindex === b.tabindex;
|
|
324
|
+
}
|
|
325
|
+
function describeChange(change) {
|
|
326
|
+
switch (change.type) {
|
|
327
|
+
case "added":
|
|
328
|
+
return `Added ${change.node?.role} "${change.node?.name}" at ${change.path}`;
|
|
329
|
+
case "removed":
|
|
330
|
+
return `Removed ${change.node?.role} "${change.node?.name}" from ${change.path}`;
|
|
331
|
+
case "changed": {
|
|
332
|
+
const descriptions = change.changes?.map((pc) => {
|
|
333
|
+
const oldVal = formatValue(pc.oldValue);
|
|
334
|
+
const newVal = formatValue(pc.newValue);
|
|
335
|
+
return `${pc.property}: ${oldVal} \u2192 ${newVal}`;
|
|
336
|
+
}) || [];
|
|
337
|
+
return `Changed at ${change.path}: ${descriptions.join(", ")}`;
|
|
338
|
+
}
|
|
339
|
+
default:
|
|
340
|
+
return `Unknown change at ${change.path}`;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function formatValue(value) {
|
|
344
|
+
if (value === void 0) return "undefined";
|
|
345
|
+
if (value === null) return "null";
|
|
346
|
+
if (typeof value === "string") return `"${value}"`;
|
|
347
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
348
|
+
return String(value);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/diff/formatter.ts
|
|
352
|
+
function formatDiffAsJSON(diff) {
|
|
353
|
+
return JSON.stringify(diff, null, 2);
|
|
354
|
+
}
|
|
355
|
+
function formatDiffAsText(diff) {
|
|
356
|
+
const lines = [];
|
|
357
|
+
lines.push("=== Accessibility Tree Diff ===");
|
|
358
|
+
lines.push("");
|
|
359
|
+
lines.push(`Total changes: ${diff.summary.total}`);
|
|
360
|
+
lines.push(` Added: ${diff.summary.added}`);
|
|
361
|
+
lines.push(` Removed: ${diff.summary.removed}`);
|
|
362
|
+
lines.push(` Changed: ${diff.summary.changed}`);
|
|
363
|
+
lines.push("");
|
|
364
|
+
if (diff.changes.length === 0) {
|
|
365
|
+
lines.push("No changes detected.");
|
|
366
|
+
return lines.join("\n");
|
|
367
|
+
}
|
|
368
|
+
const added = diff.changes.filter((c) => c.type === "added");
|
|
369
|
+
const removed = diff.changes.filter((c) => c.type === "removed");
|
|
370
|
+
const changed = diff.changes.filter((c) => c.type === "changed");
|
|
371
|
+
if (added.length > 0) {
|
|
372
|
+
lines.push("--- Added Nodes ---");
|
|
373
|
+
for (const change of added) {
|
|
374
|
+
lines.push(`+ ${change.path}`);
|
|
375
|
+
lines.push(` Role: ${change.node?.role}`);
|
|
376
|
+
lines.push(` Name: "${change.node?.name}"`);
|
|
377
|
+
if (change.node?.description) {
|
|
378
|
+
lines.push(` Description: "${change.node.description}"`);
|
|
379
|
+
}
|
|
380
|
+
lines.push("");
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
if (removed.length > 0) {
|
|
384
|
+
lines.push("--- Removed Nodes ---");
|
|
385
|
+
for (const change of removed) {
|
|
386
|
+
lines.push(`- ${change.path}`);
|
|
387
|
+
lines.push(` Role: ${change.node?.role}`);
|
|
388
|
+
lines.push(` Name: "${change.node?.name}"`);
|
|
389
|
+
if (change.node?.description) {
|
|
390
|
+
lines.push(` Description: "${change.node.description}"`);
|
|
391
|
+
}
|
|
392
|
+
lines.push("");
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (changed.length > 0) {
|
|
396
|
+
lines.push("--- Changed Nodes ---");
|
|
397
|
+
for (const change of changed) {
|
|
398
|
+
lines.push(`~ ${change.path}`);
|
|
399
|
+
if (change.changes) {
|
|
400
|
+
for (const propChange of change.changes) {
|
|
401
|
+
lines.push(` ${formatPropertyChange(propChange)}`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
lines.push("");
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return lines.join("\n");
|
|
408
|
+
}
|
|
409
|
+
function formatPropertyChange(change) {
|
|
410
|
+
const oldVal = formatValue2(change.oldValue);
|
|
411
|
+
const newVal = formatValue2(change.newValue);
|
|
412
|
+
return `${change.property}: ${oldVal} \u2192 ${newVal}`;
|
|
413
|
+
}
|
|
414
|
+
function formatValue2(value) {
|
|
415
|
+
if (value === void 0) return "undefined";
|
|
416
|
+
if (value === null) return "null";
|
|
417
|
+
if (typeof value === "string") return `"${value}"`;
|
|
418
|
+
if (typeof value === "object") {
|
|
419
|
+
const json = JSON.stringify(value);
|
|
420
|
+
if (json.length > 50) {
|
|
421
|
+
return json.substring(0, 47) + "...";
|
|
422
|
+
}
|
|
423
|
+
return json;
|
|
424
|
+
}
|
|
425
|
+
return String(value);
|
|
426
|
+
}
|
|
427
|
+
function formatDiffForCI(diff) {
|
|
428
|
+
const lines = [];
|
|
429
|
+
lines.push(`DIFF_SUMMARY: added=${diff.summary.added} removed=${diff.summary.removed} changed=${diff.summary.changed} total=${diff.summary.total}`);
|
|
430
|
+
lines.push("");
|
|
431
|
+
for (const change of diff.changes) {
|
|
432
|
+
switch (change.type) {
|
|
433
|
+
case "added":
|
|
434
|
+
lines.push(`ADDED: ${change.path} | role=${change.node?.role} | name="${change.node?.name}"`);
|
|
435
|
+
break;
|
|
436
|
+
case "removed":
|
|
437
|
+
lines.push(`REMOVED: ${change.path} | role=${change.node?.role} | name="${change.node?.name}"`);
|
|
438
|
+
break;
|
|
439
|
+
case "changed":
|
|
440
|
+
const props = change.changes?.map(
|
|
441
|
+
(pc) => `${pc.property}:${formatValue2(pc.oldValue)}->${formatValue2(pc.newValue)}`
|
|
442
|
+
).join(" | ") || "";
|
|
443
|
+
lines.push(`CHANGED: ${change.path} | ${props}`);
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return lines.join("\n");
|
|
448
|
+
}
|
|
449
|
+
function hasAccessibilityRegression(diff) {
|
|
450
|
+
const importantRoles = /* @__PURE__ */ new Set([
|
|
451
|
+
"button",
|
|
452
|
+
"link",
|
|
453
|
+
"heading",
|
|
454
|
+
"textbox",
|
|
455
|
+
"checkbox",
|
|
456
|
+
"radio",
|
|
457
|
+
"navigation",
|
|
458
|
+
"main",
|
|
459
|
+
"banner",
|
|
460
|
+
"contentinfo",
|
|
461
|
+
"form",
|
|
462
|
+
"search"
|
|
463
|
+
]);
|
|
464
|
+
for (const change of diff.changes) {
|
|
465
|
+
if (change.type === "removed" && change.node?.role && importantRoles.has(change.node.role)) {
|
|
466
|
+
return true;
|
|
467
|
+
}
|
|
468
|
+
if (change.type === "changed" && change.changes) {
|
|
469
|
+
for (const propChange of change.changes) {
|
|
470
|
+
if (propChange.property === "name") {
|
|
471
|
+
const oldName = propChange.oldValue;
|
|
472
|
+
const newName = propChange.newValue;
|
|
473
|
+
if (oldName && !newName) {
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (propChange.property === "state") {
|
|
478
|
+
const oldState = propChange.oldValue;
|
|
479
|
+
const newState = propChange.newValue;
|
|
480
|
+
if (!oldState?.hidden && newState?.hidden) {
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
if (!oldState?.disabled && newState?.disabled) {
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
export {
|
|
493
|
+
CURRENT_MODEL_VERSION,
|
|
494
|
+
SUPPORTED_ROLES,
|
|
495
|
+
ValidationError,
|
|
496
|
+
cloneModel,
|
|
497
|
+
createModel,
|
|
498
|
+
describeChange,
|
|
499
|
+
deserializeModel,
|
|
500
|
+
diffAccessibilityTrees,
|
|
501
|
+
formatDiffAsJSON,
|
|
502
|
+
formatDiffAsText,
|
|
503
|
+
formatDiffForCI,
|
|
504
|
+
hasAccessibilityRegression,
|
|
505
|
+
modelsEqual,
|
|
506
|
+
serializeModel,
|
|
507
|
+
validateModel,
|
|
508
|
+
validateRole,
|
|
509
|
+
validateState,
|
|
510
|
+
validateTreeStructure
|
|
511
|
+
};
|
|
512
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/model/types.ts","../src/model/validation.ts","../src/model/serialization.ts","../src/diff/diff-algorithm.ts","../src/diff/formatter.ts"],"sourcesContent":["/**\n * Canonical Announcement Model - Version 1\n * \n * Platform-agnostic representation of accessibility semantics.\n * Deterministic, serializable, suitable for snapshot testing.\n */\n\n/**\n * Model version for forward compatibility.\n * Breaking changes increment major version.\n */\nexport interface ModelVersion {\n major: number;\n minor: number;\n}\n\n/**\n * Accessible role following ARIA specification.\n * V1 supports common interactive and structural roles.\n */\nexport type AccessibleRole =\n // Interactive elements\n | 'button'\n | 'link'\n | 'heading'\n | 'textbox'\n | 'checkbox'\n | 'radio'\n | 'combobox'\n | 'listbox'\n | 'option'\n // Structural\n | 'list'\n | 'listitem'\n | 'article'\n | 'generic'\n // Landmarks\n | 'navigation'\n | 'main'\n | 'banner'\n | 'contentinfo'\n | 'region'\n | 'img'\n | 'complementary'\n | 'form'\n | 'search'\n // Static content (new)\n | 'paragraph'\n | 'blockquote'\n | 'code'\n | 'staticText'\n // Tables (new)\n | 'table'\n | 'row'\n | 'cell'\n | 'columnheader'\n | 'rowheader'\n // Definition lists (new)\n | 'term'\n | 'definition'\n // Figures (new)\n | 'figure'\n | 'caption'\n // Grouping & disclosure (new)\n | 'group'\n | 'dialog'\n // Widgets (new)\n | 'meter'\n | 'progressbar'\n | 'status'\n // Embedded content (new)\n | 'document'\n | 'application'\n | 'separator';\n\n/**\n * Accessible states and properties.\n * Only includes properties relevant to the element's role.\n */\nexport interface AccessibleState {\n /** Whether the element is expanded (e.g., accordion, dropdown) */\n expanded?: boolean;\n \n /** Whether the element is checked (checkbox, radio, or mixed state) */\n checked?: boolean | 'mixed';\n \n /** Whether the element is pressed (toggle button) */\n pressed?: boolean | 'mixed';\n \n /** Whether the element is selected (option, tab, etc.) */\n selected?: boolean;\n \n /** Whether the element is disabled */\n disabled?: boolean;\n \n /** Whether the element has invalid input */\n invalid?: boolean;\n \n /** Whether the element is required */\n required?: boolean;\n \n /** Whether the element is read-only */\n readonly?: boolean;\n \n /** Whether the element is busy loading */\n busy?: boolean;\n \n /** Current page/step/location indicator */\n current?: 'page' | 'step' | 'location' | 'date' | 'time' | 'true' | false;\n \n /** Whether the element is grabbed for drag-and-drop */\n grabbed?: boolean;\n \n /** Whether the element is hidden from accessibility tree */\n hidden?: boolean;\n \n /** Heading level (1-6) */\n level?: number;\n \n /** Position in set (1-indexed) for lists, tabs, etc. */\n posinset?: number;\n \n /** Total size of set */\n setsize?: number;\n}\n\n/**\n * Value for form controls and range widgets.\n */\nexport interface AccessibleValue {\n /** Current value */\n current: string | number;\n \n /** Minimum value (for range widgets) */\n min?: number;\n \n /** Maximum value (for range widgets) */\n max?: number;\n \n /** Textual representation (e.g., \"50%\") */\n text?: string;\n}\n\n/**\n * Focusability information.\n */\nexport interface FocusInfo {\n /** Whether the element can receive focus */\n focusable: boolean;\n \n /** Explicit tabindex value (only present if explicitly set) */\n tabindex?: number;\n}\n\n/**\n * Single node in the accessibility tree.\n */\nexport interface AccessibleNode {\n /** ARIA role of the element */\n role: AccessibleRole;\n \n /** Accessible name (computed via ARIA name algorithm) */\n name: string;\n \n /** Accessible description (aria-describedby, title, etc.) */\n description?: string;\n \n /** Value for form controls */\n value?: AccessibleValue;\n \n /** State properties */\n state: AccessibleState;\n \n /** Focus information */\n focus: FocusInfo;\n \n /** Child nodes in the accessibility tree */\n children: AccessibleNode[];\n}\n\n/**\n * Root of the Canonical Announcement Model.\n */\nexport interface AnnouncementModel {\n /** Model version for forward compatibility */\n version: ModelVersion;\n \n /** Root node of the accessibility tree */\n root: AccessibleNode;\n \n /** Metadata about the extraction */\n metadata: {\n /** ISO 8601 timestamp of extraction */\n extractedAt: string;\n \n /** Optional hash of source HTML for change detection */\n sourceHash?: string;\n };\n}\n\n/**\n * Supported roles as a constant array for validation.\n */\nexport const SUPPORTED_ROLES: readonly AccessibleRole[] = [\n // Original roles (22)\n 'button',\n 'link',\n 'heading',\n 'textbox',\n 'checkbox',\n 'radio',\n 'combobox',\n 'listbox',\n 'option',\n 'list',\n 'listitem',\n 'navigation',\n 'main',\n 'banner',\n 'contentinfo',\n 'region',\n 'img',\n 'article',\n 'complementary',\n 'form',\n 'search',\n 'generic',\n // New roles (21)\n 'paragraph',\n 'blockquote',\n 'code',\n 'staticText',\n 'table',\n 'row',\n 'cell',\n 'columnheader',\n 'rowheader',\n 'term',\n 'definition',\n 'figure',\n 'caption',\n 'group',\n 'dialog',\n 'meter',\n 'progressbar',\n 'status',\n 'document',\n 'application',\n 'separator',\n] as const;\n\n/**\n * Current model version constant.\n */\nexport const CURRENT_MODEL_VERSION: ModelVersion = {\n major: 1,\n minor: 0,\n};\n","/**\n * Validation functions for the Canonical Announcement Model.\n */\n\nimport type {\n AccessibleNode,\n AccessibleRole,\n AccessibleState,\n AnnouncementModel,\n} from './types.js';\nimport { SUPPORTED_ROLES } from './types.js';\n\n/**\n * Validation error class.\n */\nexport class ValidationError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'ValidationError';\n }\n}\n\n/**\n * Validates that a role is supported in V1.\n * \n * @param role - The role to validate\n * @returns true if valid\n * @throws ValidationError if invalid\n */\nexport function validateRole(role: string): role is AccessibleRole {\n if (!SUPPORTED_ROLES.includes(role as AccessibleRole)) {\n throw new ValidationError(\n `Invalid role: \"${role}\". Supported roles: ${SUPPORTED_ROLES.join(', ')}`\n );\n }\n return true;\n}\n\n/**\n * Validates accessible state constraints.\n * \n * @param state - The state object to validate\n * @throws ValidationError if invalid\n */\nexport function validateState(state: AccessibleState): void {\n // Validate level is in range 1-6 for headings\n if (state.level !== undefined) {\n if (!Number.isInteger(state.level) || state.level < 1 || state.level > 6) {\n throw new ValidationError(\n `Invalid heading level: ${state.level}. Must be an integer between 1 and 6.`\n );\n }\n }\n\n // Validate posinset is positive\n if (state.posinset !== undefined) {\n if (!Number.isInteger(state.posinset) || state.posinset < 1) {\n throw new ValidationError(\n `Invalid posinset: ${state.posinset}. Must be a positive integer.`\n );\n }\n }\n\n // Validate setsize is positive\n if (state.setsize !== undefined) {\n if (!Number.isInteger(state.setsize) || state.setsize < 1) {\n throw new ValidationError(\n `Invalid setsize: ${state.setsize}. Must be a positive integer.`\n );\n }\n }\n\n // Validate posinset <= setsize if both present\n if (\n state.posinset !== undefined &&\n state.setsize !== undefined &&\n state.posinset > state.setsize\n ) {\n throw new ValidationError(\n `Invalid set position: posinset (${state.posinset}) cannot exceed setsize (${state.setsize}).`\n );\n }\n\n // Validate checked is boolean or 'mixed'\n if (state.checked !== undefined) {\n if (typeof state.checked !== 'boolean' && state.checked !== 'mixed') {\n throw new ValidationError(\n `Invalid checked value: ${state.checked}. Must be boolean or 'mixed'.`\n );\n }\n }\n\n // Validate pressed is boolean or 'mixed'\n if (state.pressed !== undefined) {\n if (typeof state.pressed !== 'boolean' && state.pressed !== 'mixed') {\n throw new ValidationError(\n `Invalid pressed value: ${state.pressed}. Must be boolean or 'mixed'.`\n );\n }\n }\n\n // Validate current is valid enum value\n if (state.current !== undefined) {\n const validCurrentValues = ['page', 'step', 'location', 'date', 'time', 'true', false];\n if (!validCurrentValues.includes(state.current as any)) {\n throw new ValidationError(\n `Invalid current value: ${state.current}. Must be one of: ${validCurrentValues.join(', ')}`\n );\n }\n }\n}\n\n/**\n * Validates tree structure integrity (no cycles).\n * \n * @param node - The root node to validate\n * @param visited - Set of visited nodes (for cycle detection)\n * @throws ValidationError if cycles detected\n */\nexport function validateTreeStructure(\n node: AccessibleNode,\n visited: Set<AccessibleNode> = new Set()\n): void {\n // Check for cycles\n if (visited.has(node)) {\n throw new ValidationError(\n 'Circular reference detected in accessibility tree. Tree must be acyclic.'\n );\n }\n\n visited.add(node);\n\n // Validate role\n validateRole(node.role);\n\n // Validate state\n validateState(node.state);\n\n // Recursively validate children\n for (const child of node.children) {\n validateTreeStructure(child, new Set(visited));\n }\n}\n\n/**\n * Validates an entire AnnouncementModel.\n * \n * @param model - The model to validate\n * @throws ValidationError if invalid\n */\nexport function validateModel(model: AnnouncementModel): void {\n // Validate version\n if (!model.version || typeof model.version.major !== 'number' || typeof model.version.minor !== 'number') {\n throw new ValidationError('Invalid model version. Must have major and minor number fields.');\n }\n\n // Validate metadata\n if (!model.metadata || !model.metadata.extractedAt) {\n throw new ValidationError('Invalid metadata. Must have extractedAt timestamp.');\n }\n\n // Validate timestamp is ISO 8601\n const timestamp = new Date(model.metadata.extractedAt);\n if (isNaN(timestamp.getTime())) {\n throw new ValidationError(\n `Invalid extractedAt timestamp: ${model.metadata.extractedAt}. Must be ISO 8601 format.`\n );\n }\n\n // Validate root node and tree structure\n if (!model.root) {\n throw new ValidationError('Model must have a root node.');\n }\n\n validateTreeStructure(model.root);\n}\n","/**\n * JSON serialization and deserialization for the Canonical Announcement Model.\n * \n * Ensures deterministic output with consistent property ordering.\n */\n\nimport type { AnnouncementModel, AccessibleNode } from './types.js';\nimport { CURRENT_MODEL_VERSION } from './types.js';\nimport { validateModel } from './validation.js';\n\n/**\n * Serialization options.\n */\nexport interface SerializationOptions {\n /** Whether to pretty-print the JSON output */\n pretty?: boolean;\n \n /** Whether to validate the model before serialization */\n validate?: boolean;\n}\n\n/**\n * Deserialization options.\n */\nexport interface DeserializationOptions {\n /** Whether to validate the model after deserialization */\n validate?: boolean;\n}\n\n/**\n * Sorts object keys to ensure deterministic JSON output.\n * \n * @param obj - Object to sort\n * @returns New object with sorted keys\n */\nfunction sortObjectKeys<T extends Record<string, any>>(obj: T): T {\n if (obj === null || typeof obj !== 'object') {\n return obj;\n }\n\n if (Array.isArray(obj)) {\n return obj.map(sortObjectKeys) as any as T;\n }\n\n const sorted: Record<string, any> = {};\n const keys = Object.keys(obj).sort();\n\n for (const key of keys) {\n const value = obj[key];\n sorted[key] = typeof value === 'object' && value !== null \n ? sortObjectKeys(value) \n : value;\n }\n\n return sorted as T;\n}\n\n/**\n * Serializes an AnnouncementModel to JSON string.\n * \n * Produces deterministic output with consistent property ordering.\n * \n * @param model - The model to serialize\n * @param options - Serialization options\n * @returns JSON string representation\n * @throws ValidationError if validation is enabled and model is invalid\n */\nexport function serializeModel(\n model: AnnouncementModel,\n options: SerializationOptions = {}\n): string {\n const { pretty = false, validate = true } = options;\n\n // Validate model if requested\n if (validate) {\n validateModel(model);\n }\n\n // Sort keys for deterministic output\n const sorted = sortObjectKeys(model);\n\n // Serialize with optional pretty printing\n return pretty \n ? JSON.stringify(sorted, null, 2)\n : JSON.stringify(sorted);\n}\n\n/**\n * Deserializes a JSON string to an AnnouncementModel.\n * \n * @param json - JSON string to deserialize\n * @param options - Deserialization options\n * @returns Parsed AnnouncementModel\n * @throws SyntaxError if JSON is invalid\n * @throws ValidationError if validation is enabled and model is invalid\n */\nexport function deserializeModel(\n json: string,\n options: DeserializationOptions = {}\n): AnnouncementModel {\n const { validate = true } = options;\n\n // Parse JSON\n const model = JSON.parse(json) as AnnouncementModel;\n\n // Validate model if requested\n if (validate) {\n validateModel(model);\n }\n\n return model;\n}\n\n/**\n * Creates a new AnnouncementModel with current version and timestamp.\n * \n * @param root - Root accessible node\n * @param sourceHash - Optional hash of source HTML\n * @returns New AnnouncementModel\n */\nexport function createModel(\n root: AccessibleNode,\n sourceHash?: string\n): AnnouncementModel {\n return {\n version: { ...CURRENT_MODEL_VERSION },\n root,\n metadata: {\n extractedAt: new Date().toISOString(),\n ...(sourceHash && { sourceHash }),\n },\n };\n}\n\n/**\n * Compares two models for equality (deep comparison).\n * \n * @param a - First model\n * @param b - Second model\n * @returns true if models are equivalent\n */\nexport function modelsEqual(a: AnnouncementModel, b: AnnouncementModel): boolean {\n // Simple approach: serialize both and compare strings\n // This works because serialization is deterministic\n const jsonA = serializeModel(a, { validate: false });\n const jsonB = serializeModel(b, { validate: false });\n return jsonA === jsonB;\n}\n\n/**\n * Clones a model (deep copy).\n * \n * @param model - Model to clone\n * @returns Deep copy of the model\n */\nexport function cloneModel(model: AnnouncementModel): AnnouncementModel {\n const json = serializeModel(model, { validate: false });\n return deserializeModel(json, { validate: false });\n}\n","/**\n * Semantic diff algorithm for accessibility trees.\n * \n * Compares two accessibility trees and identifies:\n * - Added nodes (present in new tree, not in old)\n * - Removed nodes (present in old tree, not in new)\n * - Changed nodes (same path, different properties)\n */\n\nimport type { AccessibleNode, AccessibleState, AccessibleValue, FocusInfo } from '../model/types.js';\nimport type { SemanticDiff, NodeChange, PropertyChange, NodePath } from './types.js';\n\n/**\n * Compare two accessibility trees and generate a semantic diff.\n * \n * @param oldTree - The original accessibility tree\n * @param newTree - The updated accessibility tree\n * @returns Semantic diff with all detected changes\n */\nexport function diffAccessibilityTrees(\n oldTree: AccessibleNode,\n newTree: AccessibleNode\n): SemanticDiff {\n const changes: NodeChange[] = [];\n \n // Build maps of path -> node for both trees\n const oldNodes = buildNodeMap(oldTree, 'root');\n const newNodes = buildNodeMap(newTree, 'root');\n \n // Find removed nodes (in old but not in new)\n for (const [path, node] of oldNodes) {\n if (!newNodes.has(path)) {\n changes.push({\n type: 'removed',\n path,\n node,\n });\n }\n }\n \n // Find added nodes (in new but not in old)\n for (const [path, node] of newNodes) {\n if (!oldNodes.has(path)) {\n changes.push({\n type: 'added',\n path,\n node,\n });\n }\n }\n \n // Find changed nodes (in both but with different properties)\n for (const [path, oldNode] of oldNodes) {\n const newNode = newNodes.get(path);\n if (newNode) {\n const propertyChanges = compareNodes(oldNode, newNode);\n if (propertyChanges.length > 0) {\n changes.push({\n type: 'changed',\n path,\n changes: propertyChanges,\n });\n }\n }\n }\n \n // Sort changes by path for deterministic output\n changes.sort((a, b) => a.path.localeCompare(b.path));\n \n // Calculate summary statistics\n const summary = {\n added: changes.filter(c => c.type === 'added').length,\n removed: changes.filter(c => c.type === 'removed').length,\n changed: changes.filter(c => c.type === 'changed').length,\n total: changes.length,\n };\n \n return { changes, summary };\n}\n\n/**\n * Build a map of path -> node for a tree.\n * \n * @param node - Root node of the tree\n * @param path - Current path (starts with 'root')\n * @returns Map of paths to nodes\n */\nfunction buildNodeMap(\n node: AccessibleNode,\n path: NodePath\n): Map<NodePath, AccessibleNode> {\n const map = new Map<NodePath, AccessibleNode>();\n \n // Add current node\n map.set(path, node);\n \n // Recursively add children\n node.children.forEach((child, index) => {\n const childPath = `${path}.children[${index}]`;\n const childMap = buildNodeMap(child, childPath);\n for (const [childPath, childNode] of childMap) {\n map.set(childPath, childNode);\n }\n });\n \n return map;\n}\n\n/**\n * Compare two nodes and identify property changes.\n * \n * @param oldNode - Original node\n * @param newNode - Updated node\n * @returns Array of property changes\n */\nfunction compareNodes(\n oldNode: AccessibleNode,\n newNode: AccessibleNode\n): PropertyChange[] {\n const changes: PropertyChange[] = [];\n \n // Compare role\n if (oldNode.role !== newNode.role) {\n changes.push({\n property: 'role',\n oldValue: oldNode.role,\n newValue: newNode.role,\n });\n }\n \n // Compare name\n if (oldNode.name !== newNode.name) {\n changes.push({\n property: 'name',\n oldValue: oldNode.name,\n newValue: newNode.name,\n });\n }\n \n // Compare description\n if (oldNode.description !== newNode.description) {\n changes.push({\n property: 'description',\n oldValue: oldNode.description,\n newValue: newNode.description,\n });\n }\n \n // Compare value\n if (!valuesEqual(oldNode.value, newNode.value)) {\n changes.push({\n property: 'value',\n oldValue: oldNode.value,\n newValue: newNode.value,\n });\n }\n \n // Compare state\n if (!statesEqual(oldNode.state, newNode.state)) {\n changes.push({\n property: 'state',\n oldValue: oldNode.state,\n newValue: newNode.state,\n });\n }\n \n // Compare focus\n if (!focusEqual(oldNode.focus, newNode.focus)) {\n changes.push({\n property: 'focus',\n oldValue: oldNode.focus,\n newValue: newNode.focus,\n });\n }\n \n return changes;\n}\n\n/**\n * Compare two AccessibleValue objects for equality.\n */\nfunction valuesEqual(\n a: AccessibleValue | undefined,\n b: AccessibleValue | undefined\n): boolean {\n if (a === undefined && b === undefined) return true;\n if (a === undefined || b === undefined) return false;\n \n return (\n a.current === b.current &&\n a.min === b.min &&\n a.max === b.max &&\n a.text === b.text\n );\n}\n\n/**\n * Compare two AccessibleState objects for equality.\n */\nfunction statesEqual(\n a: AccessibleState,\n b: AccessibleState\n): boolean {\n // Get all unique keys from both states\n const keys = new Set([...Object.keys(a), ...Object.keys(b)]);\n \n for (const key of keys) {\n const aValue = a[key as keyof AccessibleState];\n const bValue = b[key as keyof AccessibleState];\n \n if (aValue !== bValue) {\n return false;\n }\n }\n \n return true;\n}\n\n/**\n * Compare two FocusInfo objects for equality.\n */\nfunction focusEqual(\n a: FocusInfo,\n b: FocusInfo\n): boolean {\n return (\n a.focusable === b.focusable &&\n a.tabindex === b.tabindex\n );\n}\n\n/**\n * Generate a human-readable description of a change.\n * \n * @param change - The change to describe\n * @returns Human-readable description\n */\nexport function describeChange(change: NodeChange): string {\n switch (change.type) {\n case 'added':\n return `Added ${change.node?.role} \"${change.node?.name}\" at ${change.path}`;\n \n case 'removed':\n return `Removed ${change.node?.role} \"${change.node?.name}\" from ${change.path}`;\n \n case 'changed': {\n const descriptions = change.changes?.map(pc => {\n const oldVal = formatValue(pc.oldValue);\n const newVal = formatValue(pc.newValue);\n return `${pc.property}: ${oldVal} → ${newVal}`;\n }) || [];\n return `Changed at ${change.path}: ${descriptions.join(', ')}`;\n }\n \n default:\n return `Unknown change at ${change.path}`;\n }\n}\n\n/**\n * Format a value for display in change descriptions.\n */\nfunction formatValue(value: unknown): string {\n if (value === undefined) return 'undefined';\n if (value === null) return 'null';\n if (typeof value === 'string') return `\"${value}\"`;\n if (typeof value === 'object') return JSON.stringify(value);\n return String(value);\n}\n","/**\n * Formatting utilities for semantic diff output.\n * \n * Provides JSON and human-readable text formatting for diffs.\n */\n\nimport type { SemanticDiff, PropertyChange } from './types.js';\n\n/**\n * Format a semantic diff as pretty-printed JSON.\n * \n * @param diff - The semantic diff to format\n * @returns JSON string with 2-space indentation\n */\nexport function formatDiffAsJSON(diff: SemanticDiff): string {\n return JSON.stringify(diff, null, 2);\n}\n\n/**\n * Format a semantic diff as human-readable text.\n * \n * @param diff - The semantic diff to format\n * @returns Multi-line text description of changes\n */\nexport function formatDiffAsText(diff: SemanticDiff): string {\n const lines: string[] = [];\n \n // Summary header\n lines.push('=== Accessibility Tree Diff ===');\n lines.push('');\n lines.push(`Total changes: ${diff.summary.total}`);\n lines.push(` Added: ${diff.summary.added}`);\n lines.push(` Removed: ${diff.summary.removed}`);\n lines.push(` Changed: ${diff.summary.changed}`);\n lines.push('');\n \n if (diff.changes.length === 0) {\n lines.push('No changes detected.');\n return lines.join('\\n');\n }\n \n // Group changes by type\n const added = diff.changes.filter(c => c.type === 'added');\n const removed = diff.changes.filter(c => c.type === 'removed');\n const changed = diff.changes.filter(c => c.type === 'changed');\n \n // Added nodes\n if (added.length > 0) {\n lines.push('--- Added Nodes ---');\n for (const change of added) {\n lines.push(`+ ${change.path}`);\n lines.push(` Role: ${change.node?.role}`);\n lines.push(` Name: \"${change.node?.name}\"`);\n if (change.node?.description) {\n lines.push(` Description: \"${change.node.description}\"`);\n }\n lines.push('');\n }\n }\n \n // Removed nodes\n if (removed.length > 0) {\n lines.push('--- Removed Nodes ---');\n for (const change of removed) {\n lines.push(`- ${change.path}`);\n lines.push(` Role: ${change.node?.role}`);\n lines.push(` Name: \"${change.node?.name}\"`);\n if (change.node?.description) {\n lines.push(` Description: \"${change.node.description}\"`);\n }\n lines.push('');\n }\n }\n \n // Changed nodes\n if (changed.length > 0) {\n lines.push('--- Changed Nodes ---');\n for (const change of changed) {\n lines.push(`~ ${change.path}`);\n if (change.changes) {\n for (const propChange of change.changes) {\n lines.push(` ${formatPropertyChange(propChange)}`);\n }\n }\n lines.push('');\n }\n }\n \n return lines.join('\\n');\n}\n\n/**\n * Format a single property change as text.\n */\nfunction formatPropertyChange(change: PropertyChange): string {\n const oldVal = formatValue(change.oldValue);\n const newVal = formatValue(change.newValue);\n return `${change.property}: ${oldVal} → ${newVal}`;\n}\n\n/**\n * Format a value for display.\n */\nfunction formatValue(value: unknown): string {\n if (value === undefined) return 'undefined';\n if (value === null) return 'null';\n if (typeof value === 'string') return `\"${value}\"`;\n if (typeof value === 'object') {\n // For objects, show a compact representation\n const json = JSON.stringify(value);\n if (json.length > 50) {\n return json.substring(0, 47) + '...';\n }\n return json;\n }\n return String(value);\n}\n\n/**\n * Format a semantic diff for CI/CD tools (GitHub Actions, GitLab CI, etc.).\n * \n * Uses a format that's easy to parse and diff-friendly.\n * \n * @param diff - The semantic diff to format\n * @returns CI-friendly text output\n */\nexport function formatDiffForCI(diff: SemanticDiff): string {\n const lines: string[] = [];\n \n // Machine-readable summary line\n lines.push(`DIFF_SUMMARY: added=${diff.summary.added} removed=${diff.summary.removed} changed=${diff.summary.changed} total=${diff.summary.total}`);\n lines.push('');\n \n // One line per change for easy parsing\n for (const change of diff.changes) {\n switch (change.type) {\n case 'added':\n lines.push(`ADDED: ${change.path} | role=${change.node?.role} | name=\"${change.node?.name}\"`);\n break;\n \n case 'removed':\n lines.push(`REMOVED: ${change.path} | role=${change.node?.role} | name=\"${change.node?.name}\"`);\n break;\n \n case 'changed':\n const props = change.changes?.map(pc => \n `${pc.property}:${formatValue(pc.oldValue)}->${formatValue(pc.newValue)}`\n ).join(' | ') || '';\n lines.push(`CHANGED: ${change.path} | ${props}`);\n break;\n }\n }\n \n return lines.join('\\n');\n}\n\n/**\n * Check if a diff represents a regression (accessibility got worse).\n * \n * Heuristics:\n * - Removed nodes with important roles (button, link, heading, etc.)\n * - Changed nodes that lost accessible names\n * - Changed nodes that became disabled or hidden\n * \n * @param diff - The semantic diff to analyze\n * @returns True if potential regression detected\n */\nexport function hasAccessibilityRegression(diff: SemanticDiff): boolean {\n const importantRoles = new Set([\n 'button', 'link', 'heading', 'textbox', 'checkbox', 'radio',\n 'navigation', 'main', 'banner', 'contentinfo', 'form', 'search'\n ]);\n \n // Check for removed important nodes\n for (const change of diff.changes) {\n if (change.type === 'removed' && change.node?.role && importantRoles.has(change.node.role)) {\n return true;\n }\n \n // Check for nodes that lost their accessible name\n if (change.type === 'changed' && change.changes) {\n for (const propChange of change.changes) {\n if (propChange.property === 'name') {\n const oldName = propChange.oldValue as string;\n const newName = propChange.newValue as string;\n if (oldName && !newName) {\n return true;\n }\n }\n \n // Check for nodes that became hidden or disabled\n if (propChange.property === 'state') {\n const oldState = propChange.oldValue as any;\n const newState = propChange.newValue as any;\n if (!oldState?.hidden && newState?.hidden) {\n return true;\n }\n if (!oldState?.disabled && newState?.disabled) {\n return true;\n }\n }\n }\n }\n }\n \n return false;\n}\n"],"mappings":"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","names":["childPath","formatValue"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@reticular/speakable",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI tool for analyzing HTML accessibility announcements",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": ["dist"],
|
|
9
|
+
"bin": {
|
|
10
|
+
"speakable": "dist/cli.js"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsup",
|
|
14
|
+
"dev": "tsup --watch",
|
|
15
|
+
"test": "vitest run",
|
|
16
|
+
"test:watch": "vitest",
|
|
17
|
+
"test:coverage": "vitest run --coverage"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"accessibility",
|
|
21
|
+
"a11y",
|
|
22
|
+
"screen-reader",
|
|
23
|
+
"aria",
|
|
24
|
+
"cli"
|
|
25
|
+
],
|
|
26
|
+
"author": "",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/jsdom": "^28.0.1",
|
|
30
|
+
"@types/node": "^20.0.0",
|
|
31
|
+
"@vitest/coverage-v8": "^1.0.0",
|
|
32
|
+
"fast-check": "^3.15.0",
|
|
33
|
+
"tsup": "^8.0.0",
|
|
34
|
+
"typescript": "^5.3.0",
|
|
35
|
+
"vitest": "^1.0.0"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"commander": "^11.1.0",
|
|
39
|
+
"jsdom": "^23.0.0",
|
|
40
|
+
"picocolors": "^1.1.1"
|
|
41
|
+
}
|
|
42
|
+
}
|