@blamejs/core 0.7.105 → 0.7.107
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/CHANGELOG.md +4 -0
- package/index.js +1 -0
- package/lib/auth/sd-jwt-vc-disclosure.js +95 -0
- package/lib/auth/sd-jwt-vc-holder.js +203 -0
- package/lib/auth/sd-jwt-vc-issuer.js +197 -0
- package/lib/auth/sd-jwt-vc.js +526 -0
- package/lib/guard-html-wcag-aria.js +164 -0
- package/lib/guard-html-wcag-forms.js +144 -0
- package/lib/guard-html-wcag-tables.js +154 -0
- package/lib/guard-html-wcag-tagwalk.js +44 -0
- package/lib/guard-html-wcag.js +470 -0
- package/lib/guard-html.js +4 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* WCAG 2.2 audit-only scanner for HTML.
|
|
4
|
+
*
|
|
5
|
+
* Scans operator-supplied HTML for accessibility violations against
|
|
6
|
+
* the WCAG 2.2 Recommendation (https://www.w3.org/TR/WCAG22/) without
|
|
7
|
+
* modifying the document. Returns a structured report of findings the
|
|
8
|
+
* operator wires into a CI gate, audit log, or development warning.
|
|
9
|
+
*
|
|
10
|
+
* var audit = b.guardHtml.wcag.audit(htmlString, {
|
|
11
|
+
* level: "AA", // | "A" | "AAA"
|
|
12
|
+
* ignore: ["1.4.3"], // SCs the operator's deployment opts out of
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* Design constraints:
|
|
16
|
+
* - PURE static analysis — no rendering, no JS execution.
|
|
17
|
+
* - The scanner is conservative: it flags clear violations (missing
|
|
18
|
+
* alt text, empty heading text) and high-confidence patterns
|
|
19
|
+
* (out-of-order headings, form fields without label / aria-label /
|
|
20
|
+
* aria-labelledby). It does NOT attempt color-contrast checks,
|
|
21
|
+
* focus-indicator checks, or text-spacing checks (require rendered
|
|
22
|
+
* styles or runtime CSS).
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
var validateOpts = require("./validate-opts");
|
|
26
|
+
var lazyRequire = require("./lazy-require");
|
|
27
|
+
var { defineClass } = require("./framework-error");
|
|
28
|
+
|
|
29
|
+
var GuardHtmlWcagError = defineClass("GuardHtmlWcagError", { alwaysPermanent: true });
|
|
30
|
+
|
|
31
|
+
var observability = lazyRequire(function () { return require("./observability"); });
|
|
32
|
+
var aria = require("./guard-html-wcag-aria");
|
|
33
|
+
var tables = require("./guard-html-wcag-tables");
|
|
34
|
+
var forms = require("./guard-html-wcag-forms");
|
|
35
|
+
var tagwalk = require("./guard-html-wcag-tagwalk");
|
|
36
|
+
|
|
37
|
+
var SC_REGISTRY = Object.freeze({
|
|
38
|
+
"1.1.1": { level: "A", name: "Non-text Content" },
|
|
39
|
+
"1.3.1": { level: "A", name: "Info and Relationships" },
|
|
40
|
+
"2.4.1": { level: "A", name: "Bypass Blocks" },
|
|
41
|
+
"2.4.2": { level: "A", name: "Page Titled" },
|
|
42
|
+
"2.4.4": { level: "A", name: "Link Purpose (In Context)" },
|
|
43
|
+
"3.1.1": { level: "A", name: "Language of Page" },
|
|
44
|
+
"3.3.2": { level: "A", name: "Labels or Instructions" },
|
|
45
|
+
"4.1.1": { level: "A", name: "Parsing (deprecated in 2.2; retained for back-compat)" },
|
|
46
|
+
"4.1.2": { level: "A", name: "Name, Role, Value" },
|
|
47
|
+
"2.4.11": { level: "AA", name: "Focus Not Obscured (Minimum)" },
|
|
48
|
+
"2.4.13": { level: "AAA", name: "Focus Appearance" },
|
|
49
|
+
"2.5.7": { level: "AA", name: "Dragging Movements" },
|
|
50
|
+
"2.5.8": { level: "AA", name: "Target Size (Minimum)" },
|
|
51
|
+
"3.2.6": { level: "A", name: "Consistent Help" },
|
|
52
|
+
"3.3.7": { level: "A", name: "Redundant Entry" },
|
|
53
|
+
"3.3.8": { level: "AA", name: "Accessible Authentication (Minimum)" },
|
|
54
|
+
"3.3.9": { level: "AAA", name: "Accessible Authentication (Enhanced)" },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
var VALID_LEVELS = Object.freeze(["A", "AA", "AAA"]);
|
|
58
|
+
|
|
59
|
+
function _meetsLevel(scLevel, requestedLevel) {
|
|
60
|
+
if (requestedLevel === "AAA") return true;
|
|
61
|
+
if (requestedLevel === "AA") return scLevel === "A" || scLevel === "AA";
|
|
62
|
+
return scLevel === "A";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function _newReport(scopeUrl) {
|
|
66
|
+
return {
|
|
67
|
+
findings: [],
|
|
68
|
+
summary: { error: 0, warning: 0, info: 0 },
|
|
69
|
+
totalFindings: 0,
|
|
70
|
+
scopeUrl: scopeUrl || null,
|
|
71
|
+
scannedAt: Date.now(),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function _addFinding(report, finding) {
|
|
76
|
+
report.findings.push(finding);
|
|
77
|
+
report.totalFindings += 1;
|
|
78
|
+
report.summary[finding.severity] = (report.summary[finding.severity] || 0) + 1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
var _TAG_RE = tagwalk.TAG_RE;
|
|
82
|
+
var _parseAttrs = tagwalk.parseAttrs;
|
|
83
|
+
var _lineColAt = tagwalk.lineColAt;
|
|
84
|
+
|
|
85
|
+
function _innerText(html, tagOpenEnd, tagName) {
|
|
86
|
+
var lower = tagName.toLowerCase();
|
|
87
|
+
var closeRe = new RegExp("</\\s*" + lower + "\\s*>", "i"); // allow:dynamic-regex — `lower` is a tag name from the framework's static SC registry, not operator input; `\\s*` and tag name are RegExp-safe (no special chars)
|
|
88
|
+
closeRe.lastIndex = tagOpenEnd;
|
|
89
|
+
var m = closeRe.exec(html);
|
|
90
|
+
if (!m) return "";
|
|
91
|
+
var raw = html.slice(tagOpenEnd, m.index);
|
|
92
|
+
return raw.replace(/<[^>]+>/g, "").replace(/&[a-z]+;|&#\d+;/gi, "").trim();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---- Per-element checks ----
|
|
96
|
+
|
|
97
|
+
function _checkImgAlt(html, attrs, tagName, offset, report, opts) {
|
|
98
|
+
if (tagName !== "img") return;
|
|
99
|
+
if (opts.ignore.indexOf("1.1.1") !== -1) return;
|
|
100
|
+
if ("alt" in attrs) return;
|
|
101
|
+
var pos = _lineColAt(html, offset);
|
|
102
|
+
_addFinding(report, {
|
|
103
|
+
sc: "1.1.1",
|
|
104
|
+
level: "A",
|
|
105
|
+
severity: "error",
|
|
106
|
+
element: "img",
|
|
107
|
+
line: pos.line,
|
|
108
|
+
column: pos.column,
|
|
109
|
+
message: "img element missing alt attribute (use alt=\"\" for purely decorative images)",
|
|
110
|
+
remediation: "Add alt=\"<descriptive text>\" or alt=\"\" if purely decorative",
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function _checkInputLabel(html, attrs, tagName, offset, report, opts, ctx) {
|
|
115
|
+
if (tagName !== "input") return;
|
|
116
|
+
if (opts.ignore.indexOf("3.3.2") !== -1) return;
|
|
117
|
+
var inputType = (attrs.type || "text").toLowerCase();
|
|
118
|
+
if (["hidden", "submit", "button", "image", "reset"].indexOf(inputType) !== -1) return;
|
|
119
|
+
var hasLabel = "aria-label" in attrs ||
|
|
120
|
+
"aria-labelledby" in attrs ||
|
|
121
|
+
"title" in attrs ||
|
|
122
|
+
(attrs.id && ctx.labelledIds[attrs.id]);
|
|
123
|
+
if (!hasLabel) {
|
|
124
|
+
var pos = _lineColAt(html, offset);
|
|
125
|
+
_addFinding(report, {
|
|
126
|
+
sc: "3.3.2",
|
|
127
|
+
level: "A",
|
|
128
|
+
severity: "error",
|
|
129
|
+
element: "input",
|
|
130
|
+
line: pos.line,
|
|
131
|
+
column: pos.column,
|
|
132
|
+
message: "Form input has no associated label (no aria-label, aria-labelledby, title, or matching <label for=...>)",
|
|
133
|
+
remediation: "Add <label for=\"<id>\">...</label> or aria-label=\"<text>\" attribute",
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (!("name" in attrs) && opts.ignore.indexOf("4.1.2") === -1) {
|
|
137
|
+
var pos2 = _lineColAt(html, offset);
|
|
138
|
+
_addFinding(report, {
|
|
139
|
+
sc: "4.1.2",
|
|
140
|
+
level: "A",
|
|
141
|
+
severity: "warning",
|
|
142
|
+
element: "input",
|
|
143
|
+
line: pos2.line,
|
|
144
|
+
column: pos2.column,
|
|
145
|
+
message: "input element has no name attribute (required for form submission + assistive-tech identification)",
|
|
146
|
+
remediation: "Add name=\"<field-name>\" attribute",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function _checkAnchorScheduled(html, attrs, tagName, offset, report, opts) {
|
|
152
|
+
if (tagName !== "a") return null;
|
|
153
|
+
if (opts.ignore.indexOf("2.4.4") !== -1) return null;
|
|
154
|
+
if (!("href" in attrs)) return null;
|
|
155
|
+
var hasAccessibleName = "aria-label" in attrs ||
|
|
156
|
+
"aria-labelledby" in attrs ||
|
|
157
|
+
"title" in attrs;
|
|
158
|
+
return { offset: offset, attrs: attrs, hasAccessibleName: hasAccessibleName };
|
|
159
|
+
// void the unused vars so the linter is happy
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function _checkButtonText(html, tagOpenEnd, attrs, offset, report, opts) {
|
|
163
|
+
if (opts.ignore.indexOf("4.1.2") !== -1) return;
|
|
164
|
+
var inner = _innerText(html, tagOpenEnd, "button");
|
|
165
|
+
if (inner.length > 0) return;
|
|
166
|
+
if ("aria-label" in attrs || "aria-labelledby" in attrs ||
|
|
167
|
+
"title" in attrs) return;
|
|
168
|
+
var pos = _lineColAt(html, offset);
|
|
169
|
+
_addFinding(report, {
|
|
170
|
+
sc: "4.1.2",
|
|
171
|
+
level: "A",
|
|
172
|
+
severity: "error",
|
|
173
|
+
element: "button",
|
|
174
|
+
line: pos.line,
|
|
175
|
+
column: pos.column,
|
|
176
|
+
message: "button has no visible text and no aria-label / title (assistive tech reads it as \"button\" with no purpose)",
|
|
177
|
+
remediation: "Add visible text content, aria-label=\"<purpose>\", or aria-labelledby=\"<idref>\"",
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function _checkHeadingOrder(html, attrs, tagName, offset, report, opts, ctx) {
|
|
182
|
+
if (!/^h[1-6]$/.test(tagName)) return;
|
|
183
|
+
if (opts.ignore.indexOf("1.3.1") !== -1) return;
|
|
184
|
+
var level = parseInt(tagName.charAt(1), 10); // allow:raw-byte-literal — base-10 parse radix
|
|
185
|
+
if (ctx.headingLevels.length === 0) {
|
|
186
|
+
if (level !== 1) {
|
|
187
|
+
var pos = _lineColAt(html, offset);
|
|
188
|
+
_addFinding(report, {
|
|
189
|
+
sc: "1.3.1",
|
|
190
|
+
level: "A",
|
|
191
|
+
severity: "warning",
|
|
192
|
+
element: tagName,
|
|
193
|
+
line: pos.line,
|
|
194
|
+
column: pos.column,
|
|
195
|
+
message: "First heading on the page is " + tagName + " (expected h1; missing/late h1 hurts navigation for screen readers)",
|
|
196
|
+
remediation: "Promote the first heading to h1, or insert an h1 above it",
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
var lastLevel = ctx.headingLevels[ctx.headingLevels.length - 1];
|
|
201
|
+
if (level > lastLevel + 1) {
|
|
202
|
+
var pos2 = _lineColAt(html, offset);
|
|
203
|
+
_addFinding(report, {
|
|
204
|
+
sc: "1.3.1",
|
|
205
|
+
level: "A",
|
|
206
|
+
severity: "warning",
|
|
207
|
+
element: tagName,
|
|
208
|
+
line: pos2.line,
|
|
209
|
+
column: pos2.column,
|
|
210
|
+
message: "Heading skips levels (" + tagName + " follows h" + lastLevel +
|
|
211
|
+
"; intermediate level skipped)",
|
|
212
|
+
remediation: "Insert intermediate heading or demote " + tagName + " to h" + (lastLevel + 1),
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
var inner = _innerText(html, ctx.lastTagEndOffset, tagName);
|
|
217
|
+
if (inner.length === 0) {
|
|
218
|
+
var pos3 = _lineColAt(html, offset);
|
|
219
|
+
_addFinding(report, {
|
|
220
|
+
sc: "1.3.1",
|
|
221
|
+
level: "A",
|
|
222
|
+
severity: "error",
|
|
223
|
+
element: tagName,
|
|
224
|
+
line: pos3.line,
|
|
225
|
+
column: pos3.column,
|
|
226
|
+
message: "Empty heading element (" + tagName + " has no text content)",
|
|
227
|
+
remediation: "Add the heading text or remove the empty heading element",
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
ctx.headingLevels.push(level);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ---- Page-level checks ----
|
|
234
|
+
|
|
235
|
+
function _checkHtmlLang(html, report, opts) {
|
|
236
|
+
if (opts.ignore.indexOf("3.1.1") !== -1) return;
|
|
237
|
+
var m = /<html\b([^>]*)>/i.exec(html);
|
|
238
|
+
if (!m) return;
|
|
239
|
+
var attrs = _parseAttrs(m[1]);
|
|
240
|
+
if (!attrs.lang || !attrs.lang.trim()) {
|
|
241
|
+
var pos = _lineColAt(html, m.index);
|
|
242
|
+
_addFinding(report, {
|
|
243
|
+
sc: "3.1.1",
|
|
244
|
+
level: "A",
|
|
245
|
+
severity: "error",
|
|
246
|
+
element: "html",
|
|
247
|
+
line: pos.line,
|
|
248
|
+
column: pos.column,
|
|
249
|
+
message: "html element missing lang attribute (assistive tech can't pick the right voice / pronunciation)",
|
|
250
|
+
remediation: "Add lang=\"<BCP47-tag>\" e.g. lang=\"en\" or lang=\"en-US\"",
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function _checkPageTitle(html, report, opts) {
|
|
256
|
+
if (opts.ignore.indexOf("2.4.2") !== -1) return;
|
|
257
|
+
if (!/<head\b/i.test(html)) return;
|
|
258
|
+
var m = /<title\b[^>]*>([^]*?)<\/title>/i.exec(html);
|
|
259
|
+
if (!m) {
|
|
260
|
+
var pos = _lineColAt(html, html.search(/<head\b/i));
|
|
261
|
+
_addFinding(report, {
|
|
262
|
+
sc: "2.4.2",
|
|
263
|
+
level: "A",
|
|
264
|
+
severity: "error",
|
|
265
|
+
element: "title",
|
|
266
|
+
line: pos.line,
|
|
267
|
+
column: pos.column,
|
|
268
|
+
message: "Page has no <title> element",
|
|
269
|
+
remediation: "Add <title>Descriptive page title</title> inside <head>",
|
|
270
|
+
});
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
var title = m[1].replace(/<[^>]+>/g, "").trim();
|
|
274
|
+
if (title.length === 0) {
|
|
275
|
+
var pos2 = _lineColAt(html, m.index);
|
|
276
|
+
_addFinding(report, {
|
|
277
|
+
sc: "2.4.2",
|
|
278
|
+
level: "A",
|
|
279
|
+
severity: "error",
|
|
280
|
+
element: "title",
|
|
281
|
+
line: pos2.line,
|
|
282
|
+
column: pos2.column,
|
|
283
|
+
message: "<title> element is empty",
|
|
284
|
+
remediation: "Add descriptive text inside <title>",
|
|
285
|
+
});
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (title.length < 4 || /^untitled/i.test(title)) {
|
|
289
|
+
var pos3 = _lineColAt(html, m.index);
|
|
290
|
+
_addFinding(report, {
|
|
291
|
+
sc: "2.4.2",
|
|
292
|
+
level: "A",
|
|
293
|
+
severity: "warning",
|
|
294
|
+
element: "title",
|
|
295
|
+
line: pos3.line,
|
|
296
|
+
column: pos3.column,
|
|
297
|
+
message: "<title> is too short or generic (\"" + title + "\")",
|
|
298
|
+
remediation: "Use a descriptive page title that distinguishes the page from siblings",
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function _checkSkipLink(html, report, opts) {
|
|
304
|
+
if (opts.ignore.indexOf("2.4.1") !== -1) return;
|
|
305
|
+
if (!/<body\b/i.test(html)) return;
|
|
306
|
+
if (/<a[^>]+href=["']#[a-zA-Z][^"']*["'][^>]*>\s*(skip|jump)\b/i.test(html)) return;
|
|
307
|
+
var bodyMatch = /<body\b[^>]*>/i.exec(html);
|
|
308
|
+
var pos = _lineColAt(html, bodyMatch ? bodyMatch.index : 0);
|
|
309
|
+
_addFinding(report, {
|
|
310
|
+
sc: "2.4.1",
|
|
311
|
+
level: "A",
|
|
312
|
+
severity: "info",
|
|
313
|
+
element: "body",
|
|
314
|
+
line: pos.line,
|
|
315
|
+
column: pos.column,
|
|
316
|
+
message: "No \"skip to content\" link detected at the start of the page",
|
|
317
|
+
remediation: "Add <a href=\"#main\" class=\"sr-only\">Skip to content</a> as the first focusable element in <body>",
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function _checkAnchors(html, scheduled, report) {
|
|
322
|
+
for (var i = 0; i < scheduled.length; i++) {
|
|
323
|
+
var s = scheduled[i];
|
|
324
|
+
var tagOpen = html.indexOf(">", s.offset);
|
|
325
|
+
if (tagOpen === -1) continue;
|
|
326
|
+
var inner = _innerText(html, tagOpen + 1, "a");
|
|
327
|
+
var visibleText = inner.length > 0;
|
|
328
|
+
if (visibleText || s.hasAccessibleName) continue;
|
|
329
|
+
var pos = _lineColAt(html, s.offset);
|
|
330
|
+
_addFinding(report, {
|
|
331
|
+
sc: "2.4.4",
|
|
332
|
+
level: "A",
|
|
333
|
+
severity: "error",
|
|
334
|
+
element: "a",
|
|
335
|
+
line: pos.line,
|
|
336
|
+
column: pos.column,
|
|
337
|
+
message: "Link has no accessible name (no visible text, aria-label, aria-labelledby, or title)",
|
|
338
|
+
remediation: "Add link text, aria-label=\"<purpose>\", or aria-labelledby=\"<idref>\"",
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ---- Main audit ----
|
|
344
|
+
|
|
345
|
+
function audit(html, opts) {
|
|
346
|
+
opts = opts || {};
|
|
347
|
+
validateOpts(opts, [
|
|
348
|
+
"level", "ignore", "checkAll", "scopeUrl",
|
|
349
|
+
"skipAria", "allowedRoles", "skipTables",
|
|
350
|
+
"skipForms", "allowedAutocomplete",
|
|
351
|
+
], "guardHtml.wcag.audit");
|
|
352
|
+
if (typeof html !== "string") {
|
|
353
|
+
throw new GuardHtmlWcagError("guard-html-wcag/bad-input",
|
|
354
|
+
"audit: html must be a string");
|
|
355
|
+
}
|
|
356
|
+
var level = opts.level || "AA";
|
|
357
|
+
if (VALID_LEVELS.indexOf(level) === -1) {
|
|
358
|
+
throw new GuardHtmlWcagError("guard-html-wcag/bad-level",
|
|
359
|
+
"audit: level must be one of " + VALID_LEVELS.join(", "));
|
|
360
|
+
}
|
|
361
|
+
var ignore = Array.isArray(opts.ignore) ? opts.ignore : [];
|
|
362
|
+
for (var i = 0; i < ignore.length; i++) {
|
|
363
|
+
if (typeof ignore[i] !== "string") {
|
|
364
|
+
throw new GuardHtmlWcagError("guard-html-wcag/bad-ignore",
|
|
365
|
+
"audit: ignore[" + i + "] must be a string SC like \"1.4.3\"");
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
var report = _newReport(opts.scopeUrl);
|
|
369
|
+
var scanOpts = { level: level, ignore: ignore };
|
|
370
|
+
|
|
371
|
+
// Page-level checks
|
|
372
|
+
_checkHtmlLang(html, report, scanOpts);
|
|
373
|
+
_checkPageTitle(html, report, scanOpts);
|
|
374
|
+
_checkSkipLink(html, report, scanOpts);
|
|
375
|
+
|
|
376
|
+
// Per-element walker
|
|
377
|
+
var ctx = {
|
|
378
|
+
headingLevels: [],
|
|
379
|
+
labelledIds: Object.create(null),
|
|
380
|
+
lastTagEndOffset: 0,
|
|
381
|
+
scheduledAnchors: [],
|
|
382
|
+
};
|
|
383
|
+
var labelRe = /<label\b[^>]*\bfor\s*=\s*["']([^"']+)["'][^>]*>/gi;
|
|
384
|
+
var lm;
|
|
385
|
+
while ((lm = labelRe.exec(html))) {
|
|
386
|
+
ctx.labelledIds[lm[1]] = true;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function _scLevelEligible(sc) {
|
|
390
|
+
var sce = SC_REGISTRY[sc];
|
|
391
|
+
if (!sce) return true;
|
|
392
|
+
return _meetsLevel(sce.level, level);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
_TAG_RE.lastIndex = 0;
|
|
396
|
+
var m;
|
|
397
|
+
while ((m = _TAG_RE.exec(html))) {
|
|
398
|
+
// Skip closing tags (m[0] starts with "</"); only run per-element
|
|
399
|
+
// checks on opening tags so we don't double-fire on each pair.
|
|
400
|
+
if (m[0].charAt(1) === "/") continue;
|
|
401
|
+
var tagName = m[1].toLowerCase();
|
|
402
|
+
var attrs = _parseAttrs(m[2]);
|
|
403
|
+
var offset = m.index;
|
|
404
|
+
var endOffset = m.index + m[0].length;
|
|
405
|
+
ctx.lastTagEndOffset = endOffset;
|
|
406
|
+
|
|
407
|
+
if (_scLevelEligible("1.1.1")) _checkImgAlt(html, attrs, tagName, offset, report, scanOpts);
|
|
408
|
+
if (_scLevelEligible("3.3.2") || _scLevelEligible("4.1.2")) _checkInputLabel(html, attrs, tagName, offset, report, scanOpts, ctx);
|
|
409
|
+
if (_scLevelEligible("2.4.4")) {
|
|
410
|
+
var sched = _checkAnchorScheduled(html, attrs, tagName, offset, report, scanOpts);
|
|
411
|
+
if (sched) ctx.scheduledAnchors.push(sched);
|
|
412
|
+
}
|
|
413
|
+
if (_scLevelEligible("4.1.2") && tagName === "button") {
|
|
414
|
+
_checkButtonText(html, endOffset, attrs, offset, report, scanOpts);
|
|
415
|
+
}
|
|
416
|
+
if (_scLevelEligible("1.3.1")) _checkHeadingOrder(html, attrs, tagName, offset, report, scanOpts, ctx);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
_checkAnchors(html, ctx.scheduledAnchors, report);
|
|
420
|
+
|
|
421
|
+
// ARIA validation pass — fold its findings into the report.
|
|
422
|
+
if (opts.skipAria !== true) {
|
|
423
|
+
var ariaFindings = aria.audit(html, {
|
|
424
|
+
allowedRoles: opts.allowedRoles,
|
|
425
|
+
scopeUrl: opts.scopeUrl,
|
|
426
|
+
});
|
|
427
|
+
for (var ai = 0; ai < ariaFindings.length; ai++) {
|
|
428
|
+
_addFinding(report, ariaFindings[ai]);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// Table-semantics pass — caption / scope / layout-table heuristics.
|
|
432
|
+
if (opts.skipTables !== true) {
|
|
433
|
+
var tableFindings = tables.audit(html, { scopeUrl: opts.scopeUrl });
|
|
434
|
+
for (var ti = 0; ti < tableFindings.length; ti++) {
|
|
435
|
+
_addFinding(report, tableFindings[ti]);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
// Forms pass — fieldset/legend / autocomplete / textarea labelling.
|
|
439
|
+
if (opts.skipForms !== true) {
|
|
440
|
+
var formFindings = forms.audit(html, {
|
|
441
|
+
allowedAutocomplete: opts.allowedAutocomplete,
|
|
442
|
+
scopeUrl: opts.scopeUrl,
|
|
443
|
+
});
|
|
444
|
+
for (var fi = 0; fi < formFindings.length; fi++) {
|
|
445
|
+
_addFinding(report, formFindings[fi]);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Heuristic score: 1 - weighted-violations / heuristic-max
|
|
450
|
+
var weighted = report.summary.error * 3 + report.summary.warning * 1.5 + // allow:raw-byte-literal — severity weights for heuristic score
|
|
451
|
+
report.summary.info * 0.5; // allow:raw-byte-literal — severity weights for heuristic score
|
|
452
|
+
var maxFor = Math.max(50, weighted * 2); // allow:raw-byte-literal — heuristic-score floor
|
|
453
|
+
report.score = Math.max(0, 1 - weighted / maxFor);
|
|
454
|
+
|
|
455
|
+
try { observability().safeEvent("guard-html.wcag.audited", 1, {
|
|
456
|
+
level: level, errors: String(report.summary.error),
|
|
457
|
+
}); } catch (_e) { /* drop-silent */ }
|
|
458
|
+
|
|
459
|
+
return report;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
module.exports = {
|
|
463
|
+
audit: audit,
|
|
464
|
+
aria: aria,
|
|
465
|
+
tables: tables,
|
|
466
|
+
forms: forms,
|
|
467
|
+
SC_REGISTRY: SC_REGISTRY,
|
|
468
|
+
VALID_LEVELS: VALID_LEVELS,
|
|
469
|
+
GuardHtmlWcagError: GuardHtmlWcagError,
|
|
470
|
+
};
|
package/lib/guard-html.js
CHANGED
|
@@ -118,6 +118,7 @@
|
|
|
118
118
|
*/
|
|
119
119
|
|
|
120
120
|
var codepointClass = require("./codepoint-class");
|
|
121
|
+
var guardHtmlWcag = require("./guard-html-wcag");
|
|
121
122
|
var lazyRequire = require("./lazy-require");
|
|
122
123
|
var gateContract = require("./gate-contract");
|
|
123
124
|
var C = require("./constants");
|
|
@@ -972,5 +973,8 @@ module.exports = {
|
|
|
972
973
|
DANGEROUS_SCHEMES: DANGEROUS_SCHEMES,
|
|
973
974
|
CLOBBER_GLOBALS: CLOBBER_GLOBALS,
|
|
974
975
|
CLOBBER_PRONE_TAGS: CLOBBER_PRONE_TAGS,
|
|
976
|
+
// WCAG 2.2 audit-only mode (b.guardHtml.wcag.audit) — accessibility
|
|
977
|
+
// scanner that emits violations without modifying HTML.
|
|
978
|
+
wcag: guardHtmlWcag,
|
|
975
979
|
GuardHtmlError: GuardHtmlError,
|
|
976
980
|
};
|
package/package.json
CHANGED
package/sbom.cyclonedx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:31469bcd-ab0f-4150-bfe2-c7d79a72a1c7",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-06T12:31:16.570Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.7.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.7.107",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.7.
|
|
25
|
+
"version": "0.7.107",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.7.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.7.107",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.7.
|
|
57
|
+
"ref": "@blamejs/core@0.7.107",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|