@surea11y/core 1.1.2 → 1.3.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/CHANGELOG.md +48 -4
- package/LICENSE +373 -21
- package/README.md +70 -1
- package/bin/core.js +240 -11
- package/docs/API_STABILITY.md +61 -0
- package/docs/BASELINE.md +66 -0
- package/docs/BINDING_AUTHORS_GUIDE.md +9 -9
- package/docs/CI_INTEGRATIONS.md +103 -0
- package/docs/CLI.md +80 -1
- package/docs/ENGINE_OPTIONS.md +4 -0
- package/docs/INTEGRATION.md +19 -1
- package/docs/OUTPUT_SCHEMA.md +2 -2
- package/docs/REPORT.md +33 -0
- package/docs/RULE_AUTHORING.md +31 -0
- package/docs/RULE_CATALOG.md +1 -1
- package/docs/SARIF.md +59 -0
- package/package.json +15 -4
- package/src/baseline.js +0 -0
- package/src/catalogs/composites.wcag.js +414 -450
- package/src/checks/automatic/area-alt-present.js +59 -25
- package/src/checks/automatic/aria-allowed-attr.js +193 -33
- package/src/checks/automatic/aria-allowed-role.js +21 -7
- package/src/checks/automatic/aria-braille-equivalent.js +32 -10
- package/src/checks/automatic/aria-conditional-attr.js +24 -7
- package/src/checks/automatic/aria-deprecated-role.js +22 -8
- package/src/checks/automatic/aria-hidden-body.js +50 -19
- package/src/checks/automatic/aria-hidden-focus.js +408 -53
- package/src/checks/automatic/aria-prohibited-attr.js +296 -22
- package/src/checks/automatic/aria-prohibited-children.js +125 -29
- package/src/checks/automatic/aria-required-attr.js +23 -8
- package/src/checks/automatic/aria-required-children.js +37 -14
- package/src/checks/automatic/aria-required-parent.js +48 -14
- package/src/checks/automatic/aria-role-name-present.js +47 -21
- package/src/checks/automatic/aria-roles-valid.js +22 -12
- package/src/checks/automatic/aria-valid-attr-value.js +28 -7
- package/src/checks/automatic/aria-valid-attr.js +17 -5
- package/src/checks/automatic/autocomplete-valid.js +74 -16
- package/src/checks/automatic/avoid-inline-spacing.js +20 -6
- package/src/checks/automatic/binary-control-name-present.js +60 -50
- package/src/checks/automatic/button-name-present.js +48 -18
- package/src/checks/automatic/bypass-blocks-present.js +50 -25
- package/src/checks/automatic/canvas-text-alternative-present.js +57 -26
- package/src/checks/automatic/combobox-name-present.js +38 -45
- package/src/checks/automatic/contrast-computable.js +361 -341
- package/src/checks/automatic/contrast-enhanced.js +487 -466
- package/src/checks/automatic/contrast-minimum.js +486 -465
- package/src/checks/automatic/css-orientation-lock.js +39 -9
- package/src/checks/automatic/definition-list-children-valid.js +40 -19
- package/src/checks/automatic/deprecated-elements-not-used.js +21 -7
- package/src/checks/automatic/dialog-name-present.js +37 -75
- package/src/checks/automatic/dlitem-parent-valid.js +23 -8
- package/src/checks/automatic/duplicate-id-aria.js +24 -6
- package/src/checks/automatic/embed-text-alternative-present.js +86 -35
- package/src/checks/automatic/form-control-programmatic-label-present.js +79 -196
- package/src/checks/automatic/form-control-single-label.js +47 -10
- package/src/checks/automatic/html-xml-lang-mismatch.js +43 -19
- package/src/checks/automatic/iframe-focusable-content.js +26 -11
- package/src/checks/automatic/iframe-name-present.js +31 -9
- package/src/checks/automatic/iframe-title-unique.js +29 -8
- package/src/checks/automatic/img-alt-present.js +47 -43
- package/src/checks/automatic/input-image-alt-present.js +143 -112
- package/src/checks/automatic/label-in-name.js +50 -22
- package/src/checks/automatic/language-page-present.js +117 -109
- package/src/checks/automatic/link-in-text-block.js +59 -19
- package/src/checks/automatic/link-name-present.js +45 -14
- package/src/checks/automatic/list-children-valid.js +26 -9
- package/src/checks/automatic/listbox-name-present.js +39 -19
- package/src/checks/automatic/listitem-parent-valid.js +18 -6
- package/src/checks/automatic/menuitem-name-present.js +39 -61
- package/src/checks/automatic/meta-refresh-no-exceptions.js +37 -8
- package/src/checks/automatic/meta-refresh-timing-absent.js +28 -6
- package/src/checks/automatic/meta-viewport-zoom-enabled.js +32 -7
- package/src/checks/automatic/meter-name-present.js +36 -33
- package/src/checks/automatic/nested-interactive-controls-absent.js +31 -10
- package/src/checks/automatic/object-text-alternative-present.js +91 -39
- package/src/checks/automatic/option-name-present.js +38 -21
- package/src/checks/automatic/page-title-present.js +26 -7
- package/src/checks/automatic/progressbar-name-present.js +41 -34
- package/src/checks/automatic/role-img-alt-present.js +209 -157
- package/src/checks/automatic/searchbox-name-present.js +39 -19
- package/src/checks/automatic/server-side-image-map-absent.js +23 -8
- package/src/checks/automatic/slider-name-present.js +40 -47
- package/src/checks/automatic/spinbutton-name-present.js +39 -19
- package/src/checks/automatic/summary-name-present.js +37 -17
- package/src/checks/automatic/svg-image-text-alternative-present.js +114 -47
- package/src/checks/automatic/svg-text-alternative-present.js +246 -226
- package/src/checks/automatic/tab-name-present.js +37 -60
- package/src/checks/automatic/table-headers-attr-valid.js +24 -8
- package/src/checks/automatic/table-th-has-data-cells.js +22 -8
- package/src/checks/automatic/target-size-minimum.js +118 -48
- package/src/checks/automatic/td-has-header.js +29 -11
- package/src/checks/automatic/textbox-name-present.js +39 -19
- package/src/checks/automatic/tooltip-name-present.js +37 -18
- package/src/checks/automatic/treeitem-name-present.js +38 -21
- package/src/checks/automatic/valid-lang.js +20 -6
- package/src/checks/automatic/video-poster-text-alternative-present.js +79 -36
- package/src/checks/manual/accesskeys-manual.js +14 -5
- package/src/checks/manual/area-alt-decorative-manual.js +192 -193
- package/src/checks/manual/area-alt-quality-manual.js +182 -141
- package/src/checks/manual/aria-checked-state-mismatch-manual.js +34 -11
- package/src/checks/manual/aria-text-manual.js +14 -6
- package/src/checks/manual/canvas-text-alternative-quality-manual.js +149 -114
- package/src/checks/manual/css-hidden-focus.js +196 -165
- package/src/checks/manual/embed-text-alternative-quality-manual.js +171 -160
- package/src/checks/manual/empty-heading-manual.js +24 -12
- package/src/checks/manual/empty-table-header-manual.js +21 -14
- package/src/checks/manual/focus-order-semantics-manual.js +45 -10
- package/src/checks/manual/form-control-programmatic-label-quality-manual.js +207 -246
- package/src/checks/manual/heading-order-manual.js +22 -12
- package/src/checks/manual/identical-links-same-purpose-manual.js +34 -12
- package/src/checks/manual/image-redundant-alt-manual.js +17 -7
- package/src/checks/manual/img-alt-decorative-manual.js +131 -96
- package/src/checks/manual/img-alt-quality-manual.js +176 -127
- package/src/checks/manual/input-image-alt-decorative-manual.js +125 -92
- package/src/checks/manual/input-image-alt-quality-manual.js +125 -92
- package/src/checks/manual/label-title-only-manual.js +14 -5
- package/src/checks/manual/landmark-banner-is-top-level-manual.js +95 -29
- package/src/checks/manual/landmark-contentinfo-is-top-level-manual.js +82 -29
- package/src/checks/manual/landmark-main-is-top-level-manual.js +64 -23
- package/src/checks/manual/landmark-no-duplicate-banner-manual.js +54 -23
- package/src/checks/manual/landmark-no-duplicate-contentinfo-manual.js +54 -23
- package/src/checks/manual/landmark-no-duplicate-main-manual.js +17 -8
- package/src/checks/manual/landmark-one-main-manual.js +35 -21
- package/src/checks/manual/landmark-unique-manual.js +73 -45
- package/src/checks/manual/link-name-quality-manual.js +43 -12
- package/src/checks/manual/media-transcript-present-manual.js +35 -22
- package/src/checks/manual/meta-viewport-large-manual.js +25 -6
- package/src/checks/manual/mouse-only-event-handlers-manual.js +38 -11
- package/src/checks/manual/no-autoplay-audio-manual.js +20 -6
- package/src/checks/manual/object-text-alternative-quality-manual.js +175 -154
- package/src/checks/manual/p-as-heading-manual.js +22 -7
- package/src/checks/manual/page-has-heading-one-manual.js +40 -23
- package/src/checks/manual/page-title-patterns-manual.js +87 -51
- package/src/checks/manual/presentation-role-conflict-manual.js +59 -19
- package/src/checks/manual/region-manual.js +275 -62
- package/src/checks/manual/scope-attr-valid-manual.js +11 -9
- package/src/checks/manual/scrollable-region-focusable-manual.js +37 -11
- package/src/checks/manual/skip-link-manual.js +35 -12
- package/src/checks/manual/svg-text-alternative-quality-manual.js +206 -165
- package/src/checks/manual/tabindex-manual.js +11 -9
- package/src/checks/manual/table-duplicate-name-manual.js +17 -7
- package/src/checks/manual/table-fake-caption-manual.js +24 -7
- package/src/checks/manual/video-caption-manual.js +15 -4
- package/src/checks/manual-review.js +56 -12
- package/src/core/aria-helpers.js +1128 -823
- package/src/core/contrast-helpers.js +1217 -1062
- package/src/core/dom-helpers.js +4176 -3886
- package/src/core/dom-runner.js +720 -596
- package/src/core/frame-messaging.js +189 -138
- package/src/core/frame-scan.js +94 -82
- package/src/core/rollup-composites.js +94 -102
- package/src/core/rule-meta.js +71 -35
- package/src/core.js +37939 -29121
- package/src/i18n/en.js +1194 -887
- package/src/i18n/fr.js +1136 -793
- package/src/policy/contracts.js +13 -13
- package/src/policy/resolvePolicy.js +48 -44
- package/src/report.js +502 -0
- package/src/sarif.js +175 -0
- package/surea11y.browser.js +36042 -0
package/src/policy/contracts.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
module.exports = {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
4
|
+
POLICY_CONTRACTS: {
|
|
5
|
+
a11y: {
|
|
6
|
+
id: 'a11y',
|
|
7
|
+
allowedOutcomes: ['fail', 'pass', 'cantTell', 'notApplicable'],
|
|
8
|
+
allowedConfidence: ['high', 'medium', 'low'],
|
|
9
|
+
coerceManualFailToCantTell: true
|
|
10
|
+
},
|
|
11
|
+
generic: {
|
|
12
|
+
id: 'generic',
|
|
13
|
+
allowedOutcomes: ['fail', 'pass', 'cantTell', 'notApplicable'],
|
|
14
|
+
allowedConfidence: ['high', 'medium', 'low'],
|
|
15
|
+
coerceManualFailToCantTell: false
|
|
17
16
|
}
|
|
17
|
+
}
|
|
18
18
|
};
|
|
@@ -1,55 +1,59 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
function resolvePolicy(POLICY_CONTRACTS, engineOptions) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return fallback;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function normalizePolicyOverrides(policy) {
|
|
34
|
-
const p = (policy && typeof policy === 'object') ? policy : {};
|
|
35
|
-
return {
|
|
36
|
-
allowedOutcomes: Array.isArray(p.allowedOutcomes) ? p.allowedOutcomes.slice() : null,
|
|
37
|
-
allowedConfidence: Array.isArray(p.allowedConfidence) ? p.allowedConfidence.slice() : null,
|
|
38
|
-
coerceManualFailToCantTell: typeof p.coerceManualFailToCantTell === 'boolean' ? p.coerceManualFailToCantTell : null
|
|
39
|
-
};
|
|
4
|
+
function normalizePolicyContract(POLICY_CONTRACTS, contract, fallbackId) {
|
|
5
|
+
const fallback = POLICY_CONTRACTS[fallbackId] || POLICY_CONTRACTS.a11y;
|
|
6
|
+
if (typeof contract === 'string') return POLICY_CONTRACTS[contract] || fallback;
|
|
7
|
+
|
|
8
|
+
if (contract && typeof contract === 'object') {
|
|
9
|
+
const allowedOutcomes = Array.isArray(contract.allowedOutcomes)
|
|
10
|
+
? contract.allowedOutcomes.slice()
|
|
11
|
+
: fallback.allowedOutcomes.slice();
|
|
12
|
+
|
|
13
|
+
const allowedConfidence = Array.isArray(contract.allowedConfidence)
|
|
14
|
+
? contract.allowedConfidence.slice()
|
|
15
|
+
: fallback.allowedConfidence.slice();
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
id:
|
|
19
|
+
typeof contract.id === 'string' && contract.id.trim()
|
|
20
|
+
? contract.id.trim()
|
|
21
|
+
: fallback.id || fallbackId || 'custom',
|
|
22
|
+
allowedOutcomes,
|
|
23
|
+
allowedConfidence,
|
|
24
|
+
coerceManualFailToCantTell:
|
|
25
|
+
typeof contract.coerceManualFailToCantTell === 'boolean'
|
|
26
|
+
? contract.coerceManualFailToCantTell
|
|
27
|
+
: !!fallback.coerceManualFailToCantTell
|
|
28
|
+
};
|
|
40
29
|
}
|
|
41
30
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const ov = normalizePolicyOverrides(opts.policy);
|
|
31
|
+
return fallback;
|
|
32
|
+
}
|
|
45
33
|
|
|
34
|
+
function normalizePolicyOverrides(policy) {
|
|
35
|
+
const p = policy && typeof policy === 'object' ? policy : {};
|
|
46
36
|
return {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
coerceManualFailToCantTell:
|
|
51
|
-
ov.coerceManualFailToCantTell !== null ? ov.coerceManualFailToCantTell : !!contract.coerceManualFailToCantTell
|
|
37
|
+
allowedOutcomes: Array.isArray(p.allowedOutcomes) ? p.allowedOutcomes.slice() : null,
|
|
38
|
+
allowedConfidence: Array.isArray(p.allowedConfidence) ? p.allowedConfidence.slice() : null,
|
|
39
|
+
coerceManualFailToCantTell:
|
|
40
|
+
typeof p.coerceManualFailToCantTell === 'boolean' ? p.coerceManualFailToCantTell : null
|
|
52
41
|
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const opts = engineOptions && typeof engineOptions === 'object' ? engineOptions : {};
|
|
45
|
+
const contract = normalizePolicyContract(POLICY_CONTRACTS, opts.policyContract, 'a11y');
|
|
46
|
+
const ov = normalizePolicyOverrides(opts.policy);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
contractId: contract.id,
|
|
50
|
+
allowedOutcomes: ov.allowedOutcomes || contract.allowedOutcomes.slice(),
|
|
51
|
+
allowedConfidence: ov.allowedConfidence || contract.allowedConfidence.slice(),
|
|
52
|
+
coerceManualFailToCantTell:
|
|
53
|
+
ov.coerceManualFailToCantTell !== null
|
|
54
|
+
? ov.coerceManualFailToCantTell
|
|
55
|
+
: !!contract.coerceManualFailToCantTell
|
|
56
|
+
};
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
module.exports = { resolvePolicy };
|
package/src/report.js
ADDED
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Renders one scan result into a single, self-contained HTML report (no
|
|
5
|
+
* external CSS/JS/fonts -- opens straight from disk, no server, no network).
|
|
6
|
+
*
|
|
7
|
+
* Self-contained single file: hero bar + legend, grouped "worth reviewing"
|
|
8
|
+
* cards with an overflow cap, collapsible "full technical data" with a
|
|
9
|
+
* scorecard + searchable/filterable/paginated table, dark-mode CSS.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Uses the dataviz skill's validated status palette, mapped 1:1 onto this
|
|
13
|
+
// engine's own 4 outcomes.
|
|
14
|
+
const STATUS = {
|
|
15
|
+
good: { color: '#0ca30c', bg: '#e9f7e9', icon: '✓' },
|
|
16
|
+
serious: { color: '#c1502e', bg: '#fdece5', icon: '⚠' },
|
|
17
|
+
warning: { color: '#8a6400', bg: '#fdf3d9', icon: 'ℹ' },
|
|
18
|
+
neutral: { color: '#5f6368', bg: '#f1f2f3', icon: '–' }
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// fail first: a QA tester scanning a report wants violations up front,
|
|
22
|
+
// not passes.
|
|
23
|
+
const OUTCOME_ORDER = ['fail', 'cantTell', 'pass', 'notApplicable'];
|
|
24
|
+
const OUTCOME_INFO = {
|
|
25
|
+
fail: { label: 'Fail', status: 'serious', defaultOn: true },
|
|
26
|
+
cantTell: { label: 'Needs review', status: 'warning', defaultOn: true },
|
|
27
|
+
pass: { label: 'Pass', status: 'good', defaultOn: false },
|
|
28
|
+
notApplicable: { label: 'Not applicable', status: 'neutral', defaultOn: false }
|
|
29
|
+
};
|
|
30
|
+
for (const code of OUTCOME_ORDER) {
|
|
31
|
+
Object.assign(OUTCOME_INFO[code], STATUS[OUTCOME_INFO[code].status]);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function esc(s) {
|
|
35
|
+
return String(s == null ? '' : s).replace(
|
|
36
|
+
/[&<>"']/g,
|
|
37
|
+
(c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Neutralizes '<' so embedded JSON can never break out of its <script> tag,
|
|
42
|
+
// even if an occurrence's own html snippet literally contains "</script>".
|
|
43
|
+
function jsonForScript(value) {
|
|
44
|
+
return JSON.stringify(value).replace(/</g, '\\u003c');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function fmtPct(n, total) {
|
|
48
|
+
return total ? `${((n / total) * 100).toFixed(1)}%` : '0.0%';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function countByOutcome(checksResults) {
|
|
52
|
+
const counts = { pass: 0, fail: 0, cantTell: 0, notApplicable: 0 };
|
|
53
|
+
for (const r of checksResults) {
|
|
54
|
+
if (Object.prototype.hasOwnProperty.call(counts, r.outcome)) counts[r.outcome] += 1;
|
|
55
|
+
}
|
|
56
|
+
return counts;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// One plain-language headline + one horizontal stacked bar + a legend with
|
|
60
|
+
// icon+label+count (status color is never the only signal) -- the first
|
|
61
|
+
// thing a reader sees; exhaustive detail lives in the collapsed
|
|
62
|
+
// "full technical data" section further down.
|
|
63
|
+
function renderHeroBar(byOutcome) {
|
|
64
|
+
const total = OUTCOME_ORDER.reduce((sum, c) => sum + byOutcome[c], 0) || 1;
|
|
65
|
+
const applicable = byOutcome.pass + byOutcome.fail + byOutcome.cantTell;
|
|
66
|
+
|
|
67
|
+
const segments = OUTCOME_ORDER.filter((c) => byOutcome[c] > 0)
|
|
68
|
+
.map((c) => {
|
|
69
|
+
const n = byOutcome[c];
|
|
70
|
+
const pct = (n / total) * 100;
|
|
71
|
+
const info = OUTCOME_INFO[c];
|
|
72
|
+
return `<div class="hero-bar-seg" style="width:${pct}%; background:${info.color}" title="${esc(info.label)}: ${n.toLocaleString()} (${pct.toFixed(1)}%)"></div>`;
|
|
73
|
+
})
|
|
74
|
+
.join('');
|
|
75
|
+
|
|
76
|
+
const legend = OUTCOME_ORDER.map((c) => {
|
|
77
|
+
const n = byOutcome[c];
|
|
78
|
+
const info = OUTCOME_INFO[c];
|
|
79
|
+
return `<div class="hero-legend-item">
|
|
80
|
+
<span class="hero-legend-swatch" style="background:${info.color}">${info.icon}</span>
|
|
81
|
+
<span class="hero-legend-label">${esc(info.label)}</span>
|
|
82
|
+
<span class="hero-legend-count">${n.toLocaleString()} (${fmtPct(n, total)})</span>
|
|
83
|
+
</div>`;
|
|
84
|
+
}).join('\n');
|
|
85
|
+
|
|
86
|
+
let headline;
|
|
87
|
+
if (byOutcome.fail > 0) {
|
|
88
|
+
headline = `<strong>${byOutcome.pass.toLocaleString()}</strong> of <strong>${applicable.toLocaleString()}</strong> applicable checks passed. <strong>${byOutcome.fail.toLocaleString()}</strong> failure${byOutcome.fail === 1 ? '' : 's'} need${byOutcome.fail === 1 ? 's' : ''} attention below.`;
|
|
89
|
+
} else if (byOutcome.cantTell > 0) {
|
|
90
|
+
headline = `<strong>${byOutcome.pass.toLocaleString()}</strong> of <strong>${applicable.toLocaleString()}</strong> applicable checks passed, with <strong>${byOutcome.cantTell.toLocaleString()}</strong> needing manual review.`;
|
|
91
|
+
} else if (applicable > 0) {
|
|
92
|
+
headline = `All <strong>${applicable.toLocaleString()}</strong> applicable checks passed.`;
|
|
93
|
+
} else {
|
|
94
|
+
headline = 'No applicable checks ran for this scan.';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return `<div class="hero">
|
|
98
|
+
<p class="hero-headline">${headline}</p>
|
|
99
|
+
<div class="hero-bar">${segments}</div>
|
|
100
|
+
<div class="hero-legend">${legend}</div>
|
|
101
|
+
</div>`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function renderScorecard(byOutcome) {
|
|
105
|
+
const total = OUTCOME_ORDER.reduce((sum, c) => sum + byOutcome[c], 0);
|
|
106
|
+
const tiles = OUTCOME_ORDER.map((c) => {
|
|
107
|
+
const n = byOutcome[c];
|
|
108
|
+
const info = OUTCOME_INFO[c];
|
|
109
|
+
return `<div class="tile" style="border-color:${info.color}; background:${info.bg}">
|
|
110
|
+
<div class="tile-num" style="color:${info.color}">${n.toLocaleString()}</div>
|
|
111
|
+
<div class="tile-pct">${fmtPct(n, total)}</div>
|
|
112
|
+
<div class="tile-label">${esc(info.label)}</div>
|
|
113
|
+
</div>`;
|
|
114
|
+
}).join('\n');
|
|
115
|
+
return `<div class="scorecard">${tiles}</div>`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// WCAG rollup -- straight from the engine's own rulesResults (one composite
|
|
119
|
+
// entry per Success Criterion, docs/WCAG_CONFORMANCE.md), not an invented
|
|
120
|
+
// grouping. Grouped by conformance level (A / AA / AAA) since that's the
|
|
121
|
+
// axis a compliance-minded reader actually cares about.
|
|
122
|
+
function renderWcagRollup(rulesResults) {
|
|
123
|
+
if (!Array.isArray(rulesResults) || !rulesResults.length) {
|
|
124
|
+
return '<p class="note">No WCAG composite rollups available for this scan (composite rules were excluded via runOnly/engineOptions).</p>';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const byLevel = { A: [], AA: [], AAA: [], other: [] };
|
|
128
|
+
for (const r of rulesResults) {
|
|
129
|
+
const mapping =
|
|
130
|
+
(r.meta && Array.isArray(r.meta.normativeMappings) && r.meta.normativeMappings[0]) || null;
|
|
131
|
+
const level = (mapping && mapping.level) || 'other';
|
|
132
|
+
(byLevel[level] || byLevel.other).push({ rule: r, mapping });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const sections = ['A', 'AA', 'AAA', 'other']
|
|
136
|
+
.filter((level) => byLevel[level].length)
|
|
137
|
+
.map((level) => {
|
|
138
|
+
const rows = byLevel[level]
|
|
139
|
+
.sort((a, b) =>
|
|
140
|
+
(a.mapping ? a.mapping.requirement : '').localeCompare(
|
|
141
|
+
b.mapping ? b.mapping.requirement : '',
|
|
142
|
+
undefined,
|
|
143
|
+
{ numeric: true }
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
.map(({ rule, mapping }) => {
|
|
147
|
+
const info = OUTCOME_INFO[rule.outcome] || OUTCOME_INFO.notApplicable;
|
|
148
|
+
const metrics = (rule.data && rule.data.details && rule.data.details.metrics) || {};
|
|
149
|
+
const checksIds = (rule.data && rule.data.details && rule.data.details.checksIds) || [];
|
|
150
|
+
const chip = `<span class="chip" style="background:${info.bg};color:${info.color}">${esc(info.label)}</span>`;
|
|
151
|
+
const scLabel = mapping ? `WCAG ${esc(mapping.requirement)}` : 'WCAG (unmapped)';
|
|
152
|
+
const metricsLabel = `${metrics.passCount || 0} pass / ${metrics.failCount || 0} fail / ${metrics.cantTellCount || 0} needs review / ${metrics.notApplicableCount || 0} n/a`;
|
|
153
|
+
return `<tr>
|
|
154
|
+
<td class="sc-cell">${scLabel}</td>
|
|
155
|
+
<td>${esc(rule.title || rule.ruleId)}</td>
|
|
156
|
+
<td>${chip}</td>
|
|
157
|
+
<td class="note">${esc(metricsLabel)}</td>
|
|
158
|
+
<td class="note">${esc(checksIds.join(', '))}</td>
|
|
159
|
+
</tr>`;
|
|
160
|
+
})
|
|
161
|
+
.join('\n');
|
|
162
|
+
|
|
163
|
+
return `<h3 class="wcag-level-heading">Level ${level === 'other' ? '(unmapped)' : level}</h3>
|
|
164
|
+
<table class="wcag-table">
|
|
165
|
+
<thead><tr><th>SC</th><th>Requirement</th><th>Outcome</th><th>Breakdown</th><th>Contributing rules</th></tr></thead>
|
|
166
|
+
<tbody>${rows}</tbody>
|
|
167
|
+
</table>`;
|
|
168
|
+
})
|
|
169
|
+
.join('\n');
|
|
170
|
+
|
|
171
|
+
return sections;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Collapse internal whitespace/newlines and cap length for card display --
|
|
175
|
+
// the findings table below shows the untruncated value.
|
|
176
|
+
const CARD_SNIPPET_MAX = 220;
|
|
177
|
+
function truncateForCard(s) {
|
|
178
|
+
const collapsed = String(s || '')
|
|
179
|
+
.replace(/\s+/g, ' ')
|
|
180
|
+
.trim();
|
|
181
|
+
return collapsed.length > CARD_SNIPPET_MAX
|
|
182
|
+
? `${collapsed.slice(0, CARD_SNIPPET_MAX)}…`
|
|
183
|
+
: collapsed;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// One card per rule (not per occurrence) -- a rule with many occurrences of
|
|
187
|
+
// the same underlying issue is one thing worth a person's attention, not N.
|
|
188
|
+
const MAX_CARDS = 24;
|
|
189
|
+
|
|
190
|
+
function renderCards(checksResults) {
|
|
191
|
+
const withIssues = checksResults.filter(
|
|
192
|
+
(r) =>
|
|
193
|
+
(r.outcome === 'fail' || r.outcome === 'cantTell') &&
|
|
194
|
+
Array.isArray(r.occurrences) &&
|
|
195
|
+
r.occurrences.length > 0
|
|
196
|
+
);
|
|
197
|
+
if (!withIssues.length) {
|
|
198
|
+
return '<p class="note">No fail/cantTell rules with occurrences on this scan.</p>';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const sorted = withIssues.slice().sort((a, b) => {
|
|
202
|
+
if (a.outcome !== b.outcome) return a.outcome === 'fail' ? -1 : 1;
|
|
203
|
+
return b.occurrences.length - a.occurrences.length;
|
|
204
|
+
});
|
|
205
|
+
const shown = sorted.slice(0, MAX_CARDS);
|
|
206
|
+
|
|
207
|
+
const cards = shown
|
|
208
|
+
.map((r) => {
|
|
209
|
+
const info = OUTCOME_INFO[r.outcome];
|
|
210
|
+
const occ = r.occurrences[0];
|
|
211
|
+
const wcagChips = ((r.meta && r.meta.normativeMappings) || [])
|
|
212
|
+
.map(
|
|
213
|
+
(m) =>
|
|
214
|
+
`<span class="chip" style="background:${STATUS.neutral.bg};color:${STATUS.neutral.color}">WCAG ${esc(m.requirement)}</span>`
|
|
215
|
+
)
|
|
216
|
+
.join('');
|
|
217
|
+
const countLabel =
|
|
218
|
+
r.occurrences.length > 1
|
|
219
|
+
? ` <span class="card-count">× ${r.occurrences.length.toLocaleString()}</span>`
|
|
220
|
+
: '';
|
|
221
|
+
|
|
222
|
+
return `<div class="card">
|
|
223
|
+
<div class="card-head">
|
|
224
|
+
<span class="hero-legend-swatch" style="background:${info.color}">${info.icon}</span>
|
|
225
|
+
<span class="card-title"><strong>${esc(r.ruleId)}</strong> (${esc(info.label)}, ${esc(r.severity)})${countLabel}</span>
|
|
226
|
+
</div>
|
|
227
|
+
<div class="card-body">
|
|
228
|
+
<div class="card-meta">${wcagChips}</div>
|
|
229
|
+
<div class="card-selector"><span class="card-selector-label">Selector:</span> <code>${esc(occ.selector || '(none)')}</code></div>
|
|
230
|
+
<div class="card-snippet">${esc(occ.summary)}${occ.hint ? ` — ${esc(occ.hint)}` : ''}</div>
|
|
231
|
+
${r.occurrences.length > 1 ? `<p class="card-note">Selector/summary above are from one representative occurrence — ${r.occurrences.length.toLocaleString()} total on this rule.</p>` : ''}
|
|
232
|
+
</div>
|
|
233
|
+
</div>`;
|
|
234
|
+
})
|
|
235
|
+
.join('\n');
|
|
236
|
+
|
|
237
|
+
const overflow =
|
|
238
|
+
sorted.length > MAX_CARDS
|
|
239
|
+
? `<p class="note">Showing the ${MAX_CARDS} highest-priority rules of ${sorted.length.toLocaleString()} with issues — see the full technical data below for the rest.</p>`
|
|
240
|
+
: '';
|
|
241
|
+
|
|
242
|
+
return `<div class="cards">${cards}</div>${overflow}`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Flatten every occurrence across every rule into one row per occurrence --
|
|
246
|
+
// the searchable/filterable/paginated detail view. Rules with zero
|
|
247
|
+
// occurrences (pass/notApplicable) have nothing to show at this granularity.
|
|
248
|
+
function flattenOccurrences(checksResults) {
|
|
249
|
+
const rows = [];
|
|
250
|
+
for (const r of checksResults) {
|
|
251
|
+
if (!Array.isArray(r.occurrences)) continue;
|
|
252
|
+
for (const occ of r.occurrences) {
|
|
253
|
+
rows.push({
|
|
254
|
+
ruleId: r.ruleId,
|
|
255
|
+
outcome: r.outcome,
|
|
256
|
+
severity: r.severity,
|
|
257
|
+
selector: occ.selector || '',
|
|
258
|
+
html: occ.html || '',
|
|
259
|
+
summary: occ.summary || '',
|
|
260
|
+
hint: occ.hint || ''
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return rows;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function renderHtmlReport(result, options = {}) {
|
|
268
|
+
const checksResults = Array.isArray(result && result.checksResults) ? result.checksResults : [];
|
|
269
|
+
const rulesResults = Array.isArray(result && result.rulesResults) ? result.rulesResults : [];
|
|
270
|
+
const byOutcome = countByOutcome(checksResults);
|
|
271
|
+
const generatedAtLabel = new Date().toLocaleString('en-US', {
|
|
272
|
+
dateStyle: 'medium',
|
|
273
|
+
timeStyle: 'medium'
|
|
274
|
+
});
|
|
275
|
+
const title = (options && options.title) || 'surea11y scan report';
|
|
276
|
+
|
|
277
|
+
const defaultOnList = OUTCOME_ORDER.filter((c) => OUTCOME_INFO[c].defaultOn);
|
|
278
|
+
const rows = flattenOccurrences(checksResults);
|
|
279
|
+
|
|
280
|
+
return `<!doctype html>
|
|
281
|
+
<html lang="en">
|
|
282
|
+
<head>
|
|
283
|
+
<meta charset="utf-8">
|
|
284
|
+
<title>${esc(title)} — ${esc(generatedAtLabel)}</title>
|
|
285
|
+
<style>
|
|
286
|
+
:root { color-scheme: light dark; }
|
|
287
|
+
* { box-sizing: border-box; }
|
|
288
|
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif; margin: 0; padding: 0 0 48px; background: #fafafa; color: #1a1a1a; }
|
|
289
|
+
@media (prefers-color-scheme: dark) {
|
|
290
|
+
body { background: #16181c; color: #e6e6e6; }
|
|
291
|
+
.tile, .wcag-table, .findings-table, .hero, .card, details { background: #1f2227 !important; border-color: #3a3d44 !important; }
|
|
292
|
+
.wcag-table th, .findings-table th { background: #262a30 !important; color: #e6e6e6 !important; }
|
|
293
|
+
a { color: #7db6ff; }
|
|
294
|
+
input, select { background: #1f2227; color: #e6e6e6; border-color: #3a3d44; }
|
|
295
|
+
.meta-bar { background: #1f2227 !important; border-color: #3a3d44 !important; }
|
|
296
|
+
.hero-bar { background: #2a2d33; }
|
|
297
|
+
summary { color: #e6e6e6 !important; }
|
|
298
|
+
}
|
|
299
|
+
header { padding: 24px 32px; background: #202531; color: #fff; }
|
|
300
|
+
header h1 { margin: 0 0 4px; font-size: 22px; }
|
|
301
|
+
header .sub { opacity: 0.8; font-size: 13px; }
|
|
302
|
+
.meta-bar { display: flex; flex-wrap: wrap; gap: 24px; padding: 12px 32px; background: #fff; border-bottom: 1px solid #ddd; font-size: 13px; }
|
|
303
|
+
.meta-bar b { display: block; font-size: 15px; }
|
|
304
|
+
main { padding: 24px 32px; max-width: 1400px; margin: 0 auto; }
|
|
305
|
+
h2 { font-size: 16px; text-transform: uppercase; letter-spacing: 0.04em; color: #555; margin: 32px 0 12px; }
|
|
306
|
+
h3.wcag-level-heading { font-size: 14px; margin: 20px 0 8px; }
|
|
307
|
+
|
|
308
|
+
.hero { background: #fff; border: 1px solid #e0e0e0; border-radius: 10px; padding: 20px 24px; margin-bottom: 28px; }
|
|
309
|
+
.hero-headline { font-size: 17px; margin: 0 0 16px; line-height: 1.5; }
|
|
310
|
+
.hero-bar { display: flex; height: 22px; border-radius: 6px; overflow: hidden; background: #eee; }
|
|
311
|
+
.hero-bar-seg { height: 100%; }
|
|
312
|
+
.hero-bar-seg:not(:last-child) { border-right: 2px solid #fff; }
|
|
313
|
+
@media (prefers-color-scheme: dark) { .hero-bar-seg:not(:last-child) { border-right-color: #16181c; } }
|
|
314
|
+
.hero-legend { display: flex; flex-wrap: wrap; gap: 14px 24px; margin-top: 16px; }
|
|
315
|
+
.hero-legend-item { display: flex; align-items: center; gap: 6px; font-size: 13px; }
|
|
316
|
+
.hero-legend-swatch { display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; border-radius: 5px; color: #fff; font-size: 11px; font-weight: 700; flex-shrink: 0; }
|
|
317
|
+
.hero-legend-count { opacity: 0.65; font-variant-numeric: tabular-nums; }
|
|
318
|
+
|
|
319
|
+
.cards { display: grid; gap: 10px; margin-bottom: 8px; }
|
|
320
|
+
.card { border: 1px solid #e0e0e0; border-radius: 8px; padding: 12px 14px; background: #fff; }
|
|
321
|
+
.card-head { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
|
322
|
+
.card-title { font-size: 13.5px; }
|
|
323
|
+
.card-count { font-weight: 700; opacity: 0.65; font-variant-numeric: tabular-nums; }
|
|
324
|
+
.card-meta { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 6px; }
|
|
325
|
+
.card-selector { font-size: 12px; margin-bottom: 6px; word-break: break-all; }
|
|
326
|
+
.card-selector-label { opacity: 0.65; margin-right: 4px; }
|
|
327
|
+
.card-selector code { font-family: ui-monospace, monospace; background: #f0f0f2; border-radius: 4px; padding: 1px 5px; user-select: all; }
|
|
328
|
+
@media (prefers-color-scheme: dark) { .card-selector code { background: #2a2d33 !important; } }
|
|
329
|
+
.card-snippet { font-family: ui-monospace, monospace; font-size: 11.5px; background: #f7f7f8; border-radius: 5px; padding: 6px 8px; overflow-x: auto; white-space: pre-wrap; word-break: break-word; }
|
|
330
|
+
@media (prefers-color-scheme: dark) { .card-snippet { background: #262a30 !important; } }
|
|
331
|
+
.card-note { font-size: 11px; opacity: 0.6; margin: 6px 0 0; }
|
|
332
|
+
|
|
333
|
+
details.tech-details { border: 1px solid #e0e0e0; border-radius: 8px; margin-top: 8px; }
|
|
334
|
+
details.tech-details summary { padding: 14px 18px; cursor: pointer; font-weight: 600; font-size: 14px; color: #333; list-style: none; }
|
|
335
|
+
details.tech-details summary::-webkit-details-marker { display: none; }
|
|
336
|
+
details.tech-details summary::before { content: '▶'; display: inline-block; margin-right: 8px; font-size: 11px; transition: transform 0.15s; }
|
|
337
|
+
details.tech-details[open] summary::before { transform: rotate(90deg); }
|
|
338
|
+
details.tech-details > .tech-body { padding: 0 18px 20px; }
|
|
339
|
+
|
|
340
|
+
.scorecard { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; }
|
|
341
|
+
.tile { border: 1.5px solid; border-radius: 8px; padding: 12px 14px; }
|
|
342
|
+
.tile-num { font-size: 24px; font-weight: 700; line-height: 1.1; }
|
|
343
|
+
.tile-pct { font-size: 12px; opacity: 0.75; }
|
|
344
|
+
.tile-label { font-size: 12.5px; margin-top: 4px; font-weight: 600; }
|
|
345
|
+
table { border-collapse: collapse; width: 100%; font-size: 12.5px; }
|
|
346
|
+
.wcag-table, .findings-table { background: #fff; border: 1px solid #e0e0e0; border-radius: 6px; overflow: hidden; margin-bottom: 8px; }
|
|
347
|
+
.wcag-table th, .wcag-table td, .findings-table th, .findings-table td { padding: 5px 8px; border-bottom: 1px solid #eee; text-align: left; }
|
|
348
|
+
.wcag-table th, .findings-table th { background: #f2f2f4; position: sticky; top: 0; font-size: 11px; }
|
|
349
|
+
.sc-cell { font-family: ui-monospace, monospace; font-size: 11.5px; white-space: nowrap; }
|
|
350
|
+
.filters { display: flex; flex-wrap: wrap; gap: 14px; align-items: center; margin-bottom: 12px; }
|
|
351
|
+
.filters label { display: inline-flex; align-items: center; gap: 4px; font-size: 12.5px; cursor: pointer; user-select: none; }
|
|
352
|
+
.chip { display: inline-block; padding: 1px 7px; border-radius: 10px; font-size: 10.5px; font-weight: 700; white-space: nowrap; }
|
|
353
|
+
.findings-table td.snippet { font-family: ui-monospace, monospace; font-size: 11px; max-width: 320px; overflow-x: auto; white-space: pre; }
|
|
354
|
+
.pager { display: flex; gap: 10px; align-items: center; margin-top: 10px; font-size: 13px; }
|
|
355
|
+
.pager button { padding: 5px 12px; border: 1px solid #ccc; background: #fff; border-radius: 5px; cursor: pointer; }
|
|
356
|
+
.pager button:disabled { opacity: 0.4; cursor: default; }
|
|
357
|
+
.note { font-size: 12.5px; opacity: 0.75; margin-top: 6px; }
|
|
358
|
+
</style>
|
|
359
|
+
</head>
|
|
360
|
+
<body>
|
|
361
|
+
|
|
362
|
+
<header>
|
|
363
|
+
<h1>${esc(title)}</h1>
|
|
364
|
+
<div class="sub">${esc(result && result.url ? result.url : '(no url)')} — generated ${esc(generatedAtLabel)}</div>
|
|
365
|
+
</header>
|
|
366
|
+
|
|
367
|
+
<div class="meta-bar">
|
|
368
|
+
<div><b>${checksResults.length}</b>rules run</div>
|
|
369
|
+
<div><b>${rows.length}</b>total occurrences</div>
|
|
370
|
+
<div><b>${esc((result && result.engine && result.engine.tag) || '?')}</b>engine</div>
|
|
371
|
+
<div><b>${esc((result && result.engine && result.engine.schemaVersion) || '?')}</b>schema version</div>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
<main>
|
|
375
|
+
${renderHeroBar(byOutcome)}
|
|
376
|
+
|
|
377
|
+
<h2>Worth reviewing</h2>
|
|
378
|
+
${renderCards(checksResults)}
|
|
379
|
+
|
|
380
|
+
<h2>WCAG rollup</h2>
|
|
381
|
+
${renderWcagRollup(rulesResults)}
|
|
382
|
+
|
|
383
|
+
<details class="tech-details">
|
|
384
|
+
<summary>Full technical data — scorecard, searchable occurrence browser</summary>
|
|
385
|
+
<div class="tech-body">
|
|
386
|
+
<h2>Scorecard</h2>
|
|
387
|
+
${renderScorecard(byOutcome)}
|
|
388
|
+
|
|
389
|
+
<h2>Occurrences</h2>
|
|
390
|
+
<div class="filters" id="filters"></div>
|
|
391
|
+
<input type="search" id="search" placeholder="Search rule id / selector / html…" style="margin-bottom:10px; padding:6px 10px; border:1px solid #ccc; border-radius:5px; font-size:13px; width:100%; max-width:480px;">
|
|
392
|
+
<div style="overflow-x:auto;">
|
|
393
|
+
<table class="findings-table" id="findings-table">
|
|
394
|
+
<thead>
|
|
395
|
+
<tr>
|
|
396
|
+
<th>Rule</th>
|
|
397
|
+
<th>Outcome</th>
|
|
398
|
+
<th>Severity</th>
|
|
399
|
+
<th>Selector</th>
|
|
400
|
+
<th>Summary</th>
|
|
401
|
+
</tr>
|
|
402
|
+
</thead>
|
|
403
|
+
<tbody id="findings-body"></tbody>
|
|
404
|
+
</table>
|
|
405
|
+
</div>
|
|
406
|
+
<div class="pager">
|
|
407
|
+
<button id="prev-page">← Prev</button>
|
|
408
|
+
<span id="page-info"></span>
|
|
409
|
+
<button id="next-page">Next →</button>
|
|
410
|
+
</div>
|
|
411
|
+
</div>
|
|
412
|
+
</details>
|
|
413
|
+
</main>
|
|
414
|
+
|
|
415
|
+
<script type="application/json" id="report-data">${jsonForScript(rows)}</script>
|
|
416
|
+
<script>
|
|
417
|
+
(function () {
|
|
418
|
+
var OUTCOME_INFO = ${jsonForScript(OUTCOME_INFO)};
|
|
419
|
+
var ORDER = ${jsonForScript(OUTCOME_ORDER)};
|
|
420
|
+
var DEFAULT_ON = ${jsonForScript(defaultOnList)};
|
|
421
|
+
var rows = JSON.parse(document.getElementById('report-data').textContent);
|
|
422
|
+
|
|
423
|
+
var PAGE_SIZE = 100;
|
|
424
|
+
var page = 0;
|
|
425
|
+
var active = {};
|
|
426
|
+
DEFAULT_ON.forEach(function (c) { active[c] = true; });
|
|
427
|
+
|
|
428
|
+
var filtersEl = document.getElementById('filters');
|
|
429
|
+
ORDER.forEach(function (c) {
|
|
430
|
+
var info = OUTCOME_INFO[c];
|
|
431
|
+
var id = 'chk-' + c;
|
|
432
|
+
var label = document.createElement('label');
|
|
433
|
+
var checked = DEFAULT_ON.indexOf(c) !== -1;
|
|
434
|
+
label.innerHTML = '<input type="checkbox" id="' + id + '"' + (checked ? ' checked' : '') + '> ' +
|
|
435
|
+
'<span class="chip" style="background:' + info.bg + ';color:' + info.color + '">' + c + '</span>';
|
|
436
|
+
filtersEl.appendChild(label);
|
|
437
|
+
label.querySelector('input').addEventListener('change', function (e) {
|
|
438
|
+
active[c] = e.target.checked;
|
|
439
|
+
page = 0;
|
|
440
|
+
render();
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
var searchEl = document.getElementById('search');
|
|
445
|
+
var bodyEl = document.getElementById('findings-body');
|
|
446
|
+
var pageInfoEl = document.getElementById('page-info');
|
|
447
|
+
var prevBtn = document.getElementById('prev-page');
|
|
448
|
+
var nextBtn = document.getElementById('next-page');
|
|
449
|
+
|
|
450
|
+
function esc(s) {
|
|
451
|
+
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
|
452
|
+
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function matches(r, q) {
|
|
457
|
+
if (!active[r.outcome]) return false;
|
|
458
|
+
if (!q) return true;
|
|
459
|
+
var hay = (r.ruleId + ' ' + r.selector + ' ' + r.html + ' ' + r.summary).toLowerCase();
|
|
460
|
+
return hay.indexOf(q) !== -1;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function render() {
|
|
464
|
+
var q = searchEl.value.trim().toLowerCase();
|
|
465
|
+
var filtered = rows.filter(function (r) { return matches(r, q); });
|
|
466
|
+
var totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
|
|
467
|
+
if (page >= totalPages) page = totalPages - 1;
|
|
468
|
+
if (page < 0) page = 0;
|
|
469
|
+
|
|
470
|
+
var slice = filtered.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE);
|
|
471
|
+
|
|
472
|
+
bodyEl.innerHTML = slice.map(function (r) {
|
|
473
|
+
var info = OUTCOME_INFO[r.outcome];
|
|
474
|
+
var chip = '<span class="chip" style="background:' + info.bg + ';color:' + info.color + '">' + r.outcome + '</span>';
|
|
475
|
+
return '<tr>' +
|
|
476
|
+
'<td class="snippet">' + esc(r.ruleId) + '</td>' +
|
|
477
|
+
'<td>' + chip + '</td>' +
|
|
478
|
+
'<td>' + esc(r.severity) + '</td>' +
|
|
479
|
+
'<td class="snippet" title="' + esc(r.html) + '">' + esc(r.selector) + '</td>' +
|
|
480
|
+
'<td class="snippet">' + esc(r.summary) + '</td>' +
|
|
481
|
+
'</tr>';
|
|
482
|
+
}).join('');
|
|
483
|
+
|
|
484
|
+
pageInfoEl.textContent = filtered.length === 0
|
|
485
|
+
? 'No matching occurrences'
|
|
486
|
+
: ('Page ' + (page + 1) + ' / ' + totalPages + ' (' + filtered.length.toLocaleString() + ' matching, ' + rows.length.toLocaleString() + ' total)');
|
|
487
|
+
prevBtn.disabled = page <= 0;
|
|
488
|
+
nextBtn.disabled = page >= totalPages - 1;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
searchEl.addEventListener('input', function () { page = 0; render(); });
|
|
492
|
+
prevBtn.addEventListener('click', function () { page--; render(); });
|
|
493
|
+
nextBtn.addEventListener('click', function () { page++; render(); });
|
|
494
|
+
|
|
495
|
+
render();
|
|
496
|
+
})();
|
|
497
|
+
</script>
|
|
498
|
+
</body>
|
|
499
|
+
</html>`;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
module.exports = { renderHtmlReport };
|