fhirsmith 0.18.1 → 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/ACCESSIBILITY.md +119 -0
- package/CHANGELOG.md +190 -9
- package/CONFIGURATION.md +305 -0
- package/LICENSE +29 -0
- package/README.md +29 -122
- package/cluster.js +1220 -0
- package/cluster.md +707 -0
- package/config-template.json +77 -8
- package/extension-tracker/extension-tracker-template.html +5 -8
- package/extension-tracker/extension-tracker.js +0 -2
- package/folder/folder.js +7 -6
- package/ips/analysis.js +560 -0
- package/ips/ips-template.html +5 -8
- package/ips/ips.js +331 -33
- package/ips/jurisdictions.js +65 -0
- package/ips/nationality.js +120 -0
- package/ips/pipeline.js +131 -17
- package/ips/presenter.js +1 -1
- package/ips/profile-editor.js +283 -4
- package/ips/readme.md +170 -20
- package/ips/render.js +769 -109
- package/ips/scripts.js +46 -0
- package/ips/store.js +123 -11
- package/ips/structure.js +247 -0
- package/ips/terminology.js +1 -1
- package/library/body-limit.js +22 -0
- package/library/cache-generation.js +15 -0
- package/library/cgroup-memory.js +133 -1
- package/library/cluster-counts.js +121 -0
- package/library/cluster-events.js +153 -0
- package/library/cluster-metrics.js +193 -0
- package/library/cluster-status.js +468 -0
- package/library/config-reader.js +96 -0
- package/library/daily-report.js +100 -2
- package/library/html-server.js +27 -1
- package/library/i18nsupport.js +23 -1
- package/library/keep-alive.js +18 -0
- package/library/languages.js +17 -3
- package/library/logger.js +31 -0
- package/library/memory-guard.js +428 -0
- package/library/native-memory.js +52 -0
- package/library/npm-audit.js +51 -25
- package/library/openapi-doc.js +7 -5
- package/library/package-manager.js +8 -0
- package/library/role.js +197 -0
- package/library/server-credentials.js +169 -0
- package/library/sqlite-utilities.js +59 -0
- package/library/ssrf-guard.js +100 -0
- package/library/static-response-stats.js +16 -0
- package/library/version-utilities.js +50 -0
- package/library/worker-count.js +192 -0
- package/nginx.md +30 -11
- package/npmprojector/indexer.js +18 -10
- package/package.json +11 -8
- package/packages/feed-check-page.js +117 -0
- package/packages/feed-check.js +279 -0
- package/packages/npm-check.js +57 -3
- package/packages/npm-signing.js +219 -0
- package/packages/openapi.yaml +207 -1
- package/packages/package-crawler.js +322 -41
- package/packages/packages-template.html +6 -8
- package/packages/packages.js +604 -57
- package/packages/readme.md +113 -1
- package/publisher/publisher-template.html +2 -5
- package/publisher/publisher.js +121 -110
- package/publisher/readme.md +75 -0
- package/registry/api.js +179 -1
- package/registry/crawler.js +86 -108
- package/registry/documentation/api.md +68 -2
- package/registry/documentation/configuration.md +34 -0
- package/registry/documentation/internals.md +4 -1
- package/registry/documentation/readme.md +3 -2
- package/registry/model.js +8 -0
- package/registry/openapi.yaml +84 -0
- package/registry/registry-template.html +5 -8
- package/registry/registry.js +58 -6
- package/root-bare-template.html +7 -4
- package/root-template.html +5 -8
- package/server.js +398 -198
- package/shl/shl.js +38 -37
- package/static/a11y/a11y-check.js +832 -0
- package/static/a11y/axe.min.js +12 -0
- package/static/assets/css/bootstrap-fhir.css +15 -15
- package/static/assets/images/flags/au.svg +18 -0
- package/static/assets/images/flags/unknown.svg +11 -0
- package/static/assets/images/flags/us.svg +63 -0
- package/static/assets/images/icon_audit.png +0 -0
- package/static/assets/images/icon_clinical.png +0 -0
- package/static/assets/images/icon_document.png +0 -0
- package/static/assets/images/icon_external.png +0 -0
- package/static/assets/images/icon_medication.png +0 -0
- package/static/assets/images/icon_organization.png +0 -0
- package/static/assets/images/icon_other.png +0 -0
- package/static/assets/images/icon_person.png +0 -0
- package/static/assets/images/icon_section.png +0 -0
- package/stats-db.js +45 -0
- package/stats.js +197 -10
- package/testing/testing-template.html +5 -8
- package/token/nginx_passport_setup.md +1 -1
- package/token/security_guide.md +1 -1
- package/token/token-template.html +2 -5
- package/token/token.js +40 -36
- package/translations/Messages.properties +19 -0
- package/tx/README.md +158 -4
- package/tx/closure/closure-store.js +126 -3
- package/tx/cm/cm-database.js +69 -57
- package/tx/cm/cm-package.js +50 -16
- package/tx/cs/cs-api.js +284 -19
- package/tx/cs/cs-api.md +41 -4
- package/tx/cs/cs-areacode.js +1 -1
- package/tx/cs/cs-country.js +17 -5
- package/tx/cs/cs-cpt.js +33 -16
- package/tx/cs/cs-cs.js +199 -24
- package/tx/cs/cs-currency.js +1 -1
- package/tx/cs/cs-icd11.js +21 -13
- package/tx/cs/cs-icf.js +22 -3
- package/tx/cs/cs-lang.js +2 -2
- package/tx/cs/cs-loinc.js +193 -67
- package/tx/cs/cs-mimetypes.js +12 -2
- package/tx/cs/cs-ndc.js +40 -25
- package/tx/cs/cs-omop.js +54 -29
- package/tx/cs/cs-rxnorm.js +300 -196
- package/tx/cs/cs-snomed.js +861 -95
- package/tx/cs/cs-ucum.js +14 -4
- package/tx/cs/cs-unii.js +18 -14
- package/tx/cs/cs-virtual.js +210 -0
- package/tx/ecosystem-resolver.js +413 -0
- package/tx/html/cluster-dash-metrics.liquid +26 -0
- package/tx/html/cluster-home-metrics.liquid +31 -0
- package/tx/html/cluster-metrics-script.liquid +251 -0
- package/tx/html/codesystem-operations.liquid +3 -3
- package/tx/html/ecl-panel.liquid +1 -1
- package/tx/html/external-metrics.liquid +17 -0
- package/tx/html/operations-form.liquid +17 -17
- package/tx/html/search-form.liquid +11 -11
- package/tx/html/status-line.liquid +58 -0
- package/tx/html/status-panel.liquid +140 -0
- package/tx/html/status-tasks.liquid +25 -0
- package/tx/html/tx-template.html +2 -5
- package/tx/importers/atc-to-fhir.js +17 -16
- package/tx/importers/export-icd11-codesystem.js +3 -2
- package/tx/importers/import-icd11.js +4 -3
- package/tx/importers/import-loinc.module.js +118 -56
- package/tx/importers/import-ndc.module.js +56 -44
- package/tx/importers/import-rxnorm.module.js +118 -79
- package/tx/importers/import-sct.module.js +125 -61
- package/tx/importers/import-unii.module.js +37 -36
- package/tx/importers/subset-loinc.module.js +49 -48
- package/tx/importers/subset-rxnorm.module.js +29 -28
- package/tx/importers/terminal.js +18 -0
- package/tx/importers/tx-import-base.js +13 -12
- package/tx/importers/tx-import-settings.js +5 -4
- package/tx/importers/tx-import.js +53 -52
- package/tx/library/canonical-resource.js +49 -0
- package/tx/library/codesystem.js +94 -5
- package/tx/library/designations.js +45 -2
- package/tx/library/extensions.js +13 -7
- package/tx/library/implicit-valuesets.js +66 -0
- package/tx/library/renderer.js +77 -21
- package/tx/library/sqlite-utilities.js +2 -0
- package/tx/library/tho-precedence.js +40 -0
- package/tx/library/ucum-parsers.js +11 -1
- package/tx/library/wait-for-cache.js +43 -0
- package/tx/library.js +321 -22
- package/tx/openapi-operations.js +6 -4
- package/tx/operation-context.js +460 -99
- package/tx/params.js +25 -0
- package/tx/problems.js +3 -5
- package/tx/provider.js +314 -67
- package/tx/resource-cache-sqlite.js +1050 -0
- package/tx/sct/documentation.md +45 -21
- package/tx/sct/ecl.js +81 -17
- package/tx/sct/editions.js +39 -0
- package/tx/sct/expressions.js +10 -1
- package/tx/sct/load-config.js +107 -0
- package/tx/sct/mmap.js +109 -0
- package/tx/sct/structures.js +175 -15
- package/tx/sct/text-index.js +5 -1
- package/tx/syndication/syndication-feed.js +2 -2
- package/tx/tx-html.js +44 -8
- package/tx/tx.fhir.org.yml +8 -8
- package/tx/tx.js +282 -80
- package/tx/usage-tracker.js +39 -11
- package/tx/vs/vs-api.js +19 -0
- package/tx/vs/vs-database.js +386 -62
- package/tx/vs/vs-package.js +44 -17
- package/tx/vs/vs-vsac.js +649 -101
- package/tx/workers/batch-validate.js +37 -10
- package/tx/workers/batch.js +6 -1
- package/tx/workers/cache-control.js +15 -4
- package/tx/workers/closure.js +156 -13
- package/tx/workers/compare.js +8 -5
- package/tx/workers/expand.js +1173 -289
- package/tx/workers/metadata.js +35 -0
- package/tx/workers/search.js +53 -1
- package/tx/workers/supplement-filters.js +145 -0
- package/tx/workers/translate.js +169 -4
- package/tx/workers/validate.js +223 -119
- package/tx/workers/worker.js +123 -31
- package/tx/xversion/xv-valueset.js +7 -4
- package/utilities/submit-test-report.js +3 -1
- package/utilities/tx-stress-test.js +482 -0
- package/vcl/readme.md +37 -0
- package/vcl/vcl-parser.js +15 -0
- package/xig/readme.md +52 -0
- package/xig/xig-template.html +5 -8
- package/xig/xig.js +238 -422
- package/changelog-template.md +0 -22
- package/registry/test-runner.js +0 -237
- package/release.sh +0 -80
- package/test-scripts/repro-re2-wasm-leak.js +0 -34
- package/tsconfig.eslint.json +0 -9
- package/tx/html/dash-metrics.liquid +0 -147
- package/tx/html/home-metrics.liquid +0 -225
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* FHIRsmith - in-page accessibility check.
|
|
3
|
+
*
|
|
4
|
+
* When the server's config turns on accessibility.checkPanel, html-server adds a small panel (id
|
|
5
|
+
* 'fhir-a11y-panel') at the bottom of every page. This started as a copy of the IG Publisher's
|
|
6
|
+
* a11y-check.js, and is maintained separately from it. Pressing the button runs axe-core (https://github.com/dequelabs/axe-core, MPL-2.0) on the page as it is now - after
|
|
7
|
+
* the page's scripts have run, with whatever tabs or sections the user has opened - restricted to WCAG 2.0 A
|
|
8
|
+
* and AA, which is what the HHS Section 508 checklist is based on. It adds a check of its own for HHS 5B
|
|
9
|
+
* (links distinguished from surrounding text by colour alone), and reports everything grouped by HHS
|
|
10
|
+
* checklist id.
|
|
11
|
+
*
|
|
12
|
+
* It also runs structural checks that axe does not do under WCAG 2.0 A/AA (ported from the IG Publisher's
|
|
13
|
+
* HTMLInspector, which does them on the server side): heading structure, labels that point at nothing,
|
|
14
|
+
* accesskeys, placeholder alt text, generic link text, and click handlers on things the keyboard cannot reach.
|
|
15
|
+
*
|
|
16
|
+
* axe-core is only loaded when the button is pressed, so this costs nothing on normal page loads.
|
|
17
|
+
*/
|
|
18
|
+
(function () {
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
var PANEL_ID = 'fhir-a11y-panel';
|
|
22
|
+
|
|
23
|
+
// HHS checklist ids (Section 508 web checklist, version 03/2020) - short descriptions for the report
|
|
24
|
+
var HHS = {
|
|
25
|
+
'2A': 'Images, image buttons and image map areas have appropriate alternative text',
|
|
26
|
+
'2B': 'Decorative images have null alt text',
|
|
27
|
+
'2D': 'Embedded multimedia is identified via accessible text',
|
|
28
|
+
'2E': 'Frames are appropriately titled',
|
|
29
|
+
'2F': 'Content hidden from all users is also hidden from assistive technology',
|
|
30
|
+
'3A': 'Time-based media has captions / transcripts',
|
|
31
|
+
'4A': 'Semantic markup is used for headings, lists and emphasis',
|
|
32
|
+
'4C': 'Data table headers are identified',
|
|
33
|
+
'4D': 'Data cells are associated with their headers',
|
|
34
|
+
'4E': 'Data table captions and summaries are used appropriately',
|
|
35
|
+
'4F': 'Layout tables do not contain structural markup',
|
|
36
|
+
'4G': 'Text labels are associated with form inputs',
|
|
37
|
+
'4H': 'Related form elements are grouped with fieldset/legend',
|
|
38
|
+
'4J': 'Reading and navigation order is logical',
|
|
39
|
+
'5A': 'Colour is not the only way of conveying information',
|
|
40
|
+
'5B': 'Colour alone does not distinguish links from surrounding text (unless 3:1 contrast with the text, plus a non-colour cue on hover and focus)',
|
|
41
|
+
'5D': 'Audio that plays automatically can be stopped',
|
|
42
|
+
'5E': 'Text has a contrast ratio of at least 4.5:1 (3:1 for large text)',
|
|
43
|
+
'5F': 'The page is usable when text is resized to 200%',
|
|
44
|
+
'6A': 'All functionality is available from the keyboard',
|
|
45
|
+
'6B': 'Shortcut keys and accesskeys do not conflict',
|
|
46
|
+
'7A': 'Time limits can be turned off, adjusted or extended',
|
|
47
|
+
'7B': 'Moving, blinking or scrolling content can be paused',
|
|
48
|
+
'9A': 'A mechanism to skip repeated blocks is provided',
|
|
49
|
+
'9D': 'The page has a descriptive title',
|
|
50
|
+
'9E': 'Focus order is logical',
|
|
51
|
+
'9G': 'The purpose of each link can be determined',
|
|
52
|
+
'9J': 'Headings and labels are informative',
|
|
53
|
+
'10A': 'The language of the page is identified',
|
|
54
|
+
'10B': 'Changes of language within the page are identified',
|
|
55
|
+
'12C': 'Labels and instructions are provided for inputs',
|
|
56
|
+
'13A': 'No significant HTML parsing errors (e.g. duplicate ids)',
|
|
57
|
+
'13B': 'Name, role and value of controls can be determined programmatically',
|
|
58
|
+
'?': 'Other WCAG 2.0 A/AA failures'
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// axe rule -> HHS id, where the rule maps to one specific checklist row
|
|
62
|
+
var RULE_TO_HHS = {
|
|
63
|
+
'image-alt': '2A', 'input-image-alt': '2A', 'area-alt': '2A', 'role-img-alt': '2A', 'svg-img-alt': '2A',
|
|
64
|
+
'image-redundant-alt': '2B',
|
|
65
|
+
'object-alt': '2D', 'video-caption': '3A', 'audio-caption': '3A', 'no-autoplay-audio': '5D',
|
|
66
|
+
'frame-title': '2E', 'frame-title-unique': '2E', 'frame-focusable-content': '6A',
|
|
67
|
+
'aria-hidden-body': '2F', 'aria-hidden-focus': '2F',
|
|
68
|
+
'list': '4A', 'listitem': '4A', 'definition-list': '4A', 'dlitem': '4A', 'empty-heading': '4A', 'p-as-heading': '4A',
|
|
69
|
+
'th-has-data-cells': '4C', 'td-has-header': '4C', 'td-headers-attr': '4D', 'scope-attr-valid': '4D',
|
|
70
|
+
'table-duplicate-name': '4E', 'table-fake-caption': '4E', 'layout-table': '4F', 'presentation-role-conflict': '4F',
|
|
71
|
+
'label': '4G', 'label-title-only': '4G', 'form-field-multiple-labels': '4G', 'select-name': '4G',
|
|
72
|
+
'color-contrast': '5E', 'link-in-text-block': '5B',
|
|
73
|
+
'meta-viewport': '5F', 'meta-viewport-large': '5F',
|
|
74
|
+
'scrollable-region-focusable': '6A', 'server-side-image-map': '6A',
|
|
75
|
+
'accesskeys': '6B',
|
|
76
|
+
'meta-refresh': '7A', 'meta-refresh-no-exceptions': '7A', 'blink': '7B', 'marquee': '7B',
|
|
77
|
+
'bypass': '9A', 'document-title': '9D', 'tabindex': '9E',
|
|
78
|
+
'link-name': '9G', 'identical-links-same-purpose': '9G',
|
|
79
|
+
'html-has-lang': '10A', 'html-lang-valid': '10A', 'html-xml-lang-mismatch': '10A', 'valid-lang': '10B',
|
|
80
|
+
'duplicate-id': '13A', 'duplicate-id-active': '13A', 'duplicate-id-aria': '13A'
|
|
81
|
+
};
|
|
82
|
+
// otherwise, WCAG success criterion (from the rule's tags) -> HHS id
|
|
83
|
+
var SC_TO_HHS = {
|
|
84
|
+
'wcag111': '2A', 'wcag121': '3A', 'wcag122': '3A', 'wcag123': '3A', 'wcag124': '3A', 'wcag125': '3A',
|
|
85
|
+
'wcag131': '4A', 'wcag132': '4J', 'wcag141': '5A', 'wcag142': '5D', 'wcag143': '5E', 'wcag144': '5F',
|
|
86
|
+
'wcag211': '6A', 'wcag212': '6A', 'wcag221': '7A', 'wcag222': '7B',
|
|
87
|
+
'wcag241': '9A', 'wcag242': '9D', 'wcag243': '9E', 'wcag244': '9G', 'wcag246': '9J',
|
|
88
|
+
'wcag311': '10A', 'wcag312': '10B', 'wcag332': '12C', 'wcag411': '13A', 'wcag412': '13B'
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
function hhsFor(rule) {
|
|
92
|
+
if (RULE_TO_HHS[rule.id]) {
|
|
93
|
+
return RULE_TO_HHS[rule.id];
|
|
94
|
+
}
|
|
95
|
+
var tags = rule.tags || [];
|
|
96
|
+
for (var i = 0; i < tags.length; i++) {
|
|
97
|
+
if (SC_TO_HHS[tags[i]]) {
|
|
98
|
+
return SC_TO_HHS[tags[i]];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return '?';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ---- colour ------------------------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
function parseColor(s) {
|
|
107
|
+
var m = /rgba?\(\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)(?:[\s,/]+([\d.]+%?))?\s*\)/.exec(s || '');
|
|
108
|
+
if (!m) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
var a = m[4] === undefined ? 1 : (m[4].indexOf('%') > 0 ? parseFloat(m[4]) / 100 : parseFloat(m[4]));
|
|
112
|
+
return { r: +m[1], g: +m[2], b: +m[3], a: a };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function luminance(c) {
|
|
116
|
+
function ch(v) {
|
|
117
|
+
v = v / 255;
|
|
118
|
+
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
|
119
|
+
}
|
|
120
|
+
return 0.2126 * ch(c.r) + 0.7152 * ch(c.g) + 0.0722 * ch(c.b);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function contrast(c1, c2) {
|
|
124
|
+
var l1 = luminance(c1), l2 = luminance(c2);
|
|
125
|
+
return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function colorText(c) {
|
|
129
|
+
function h(v) {
|
|
130
|
+
var s = Math.round(v).toString(16);
|
|
131
|
+
return s.length === 1 ? '0' + s : s;
|
|
132
|
+
}
|
|
133
|
+
return '#' + h(c.r) + h(c.g) + h(c.b);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---- HHS 5B: links distinguished from the surrounding text by colour alone ----------------------------
|
|
137
|
+
|
|
138
|
+
function hasNonColourCue(style) {
|
|
139
|
+
var deco = style.textDecorationLine || style.textDecoration || '';
|
|
140
|
+
if (deco.indexOf('underline') >= 0 || deco.indexOf('overline') >= 0) {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
if (style.borderBottomStyle && style.borderBottomStyle !== 'none' && parseFloat(style.borderBottomWidth) > 0) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function blockAncestor(el) {
|
|
150
|
+
var p = el.parentElement;
|
|
151
|
+
while (p && p !== document.body) {
|
|
152
|
+
var d = getComputedStyle(p).display;
|
|
153
|
+
if (d !== 'inline' && d !== 'contents') {
|
|
154
|
+
return p;
|
|
155
|
+
}
|
|
156
|
+
p = p.parentElement;
|
|
157
|
+
}
|
|
158
|
+
return p;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// is the link in a run of text? (a link alone in a table cell or a list item is not - there's nothing to confuse it with)
|
|
162
|
+
// the text a sighted reader sees around the link: not other links (a run of links, like 'Reference(US Core
|
|
163
|
+
// Patient)', is not a link in text), not text hidden off-screen for screen readers (e.g. the element paths
|
|
164
|
+
// in the tree tables), and not our own badges
|
|
165
|
+
function isVisuallyHidden(e) {
|
|
166
|
+
if (e.classList && (e.classList.contains('sr-only') || e.classList.contains('fhir-a11y-badge'))) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
if (e.getAttribute && e.getAttribute('aria-hidden') === 'true') {
|
|
170
|
+
return false; // hidden from screen readers, but still seen
|
|
171
|
+
}
|
|
172
|
+
var cs = getComputedStyle(e);
|
|
173
|
+
if (cs.display === 'none' || cs.visibility === 'hidden') {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
return cs.position === 'absolute' && (parseFloat(cs.width) <= 1 || parseFloat(cs.height) <= 1) && cs.overflow === 'hidden';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function surroundingText(block) {
|
|
180
|
+
var out = [];
|
|
181
|
+
(function walk(n) {
|
|
182
|
+
for (var c = n.firstChild; c; c = c.nextSibling) {
|
|
183
|
+
if (c.nodeType === 3) {
|
|
184
|
+
out.push(c.nodeValue);
|
|
185
|
+
} else if (c.nodeType === 1 && !/^(A|SCRIPT|STYLE|BUTTON|SELECT|TEXTAREA)$/.test(c.tagName) && !isVisuallyHidden(c)) {
|
|
186
|
+
walk(c);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
})(block);
|
|
190
|
+
return out.join(' ');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function inTextBlock(link, block) {
|
|
194
|
+
var other = surroundingText(block).replace(/[\s.,;:()[\]|-]+/g, '');
|
|
195
|
+
return other.length >= 3;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// does some stylesheet rule add an underline (or border) to this link on :hover / :focus?
|
|
199
|
+
// returns true / false, or null if the stylesheets can't be read (e.g. pages opened as file:)
|
|
200
|
+
function stateCue(link, state) {
|
|
201
|
+
var unreadable = false;
|
|
202
|
+
for (var i = 0; i < document.styleSheets.length; i++) {
|
|
203
|
+
var rules;
|
|
204
|
+
try {
|
|
205
|
+
rules = document.styleSheets[i].cssRules;
|
|
206
|
+
} catch (e) {
|
|
207
|
+
unreadable = true;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (rules && ruleListHasCue(rules, link, state)) {
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return unreadable ? null : false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function ruleListHasCue(rules, link, state) {
|
|
218
|
+
for (var j = 0; j < rules.length; j++) {
|
|
219
|
+
var r = rules[j];
|
|
220
|
+
if (!r.selectorText) {
|
|
221
|
+
// a grouping rule (@media, @supports, @layer): look inside it if it applies. Note that style rules
|
|
222
|
+
// also have cssRules now (css nesting), so this has to key off the selector, not cssRules
|
|
223
|
+
if (r.cssRules && (!r.media || window.matchMedia(r.media.mediaText).matches) && ruleListHasCue(r.cssRules, link, state)) {
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (r.selectorText.indexOf(':' + state) < 0) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
var deco = r.style.textDecorationLine || r.style.textDecoration || '';
|
|
232
|
+
var border = r.style.borderBottomStyle && r.style.borderBottomStyle !== 'none';
|
|
233
|
+
if (deco.indexOf('underline') < 0 && !border) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
var parts = r.selectorText.split(',');
|
|
237
|
+
for (var k = 0; k < parts.length; k++) {
|
|
238
|
+
if (parts[k].indexOf(':' + state) < 0) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
var sel = parts[k].replace(/:(hover|focus-visible|focus-within|focus|active)/g, '').trim() || '*';
|
|
242
|
+
try {
|
|
243
|
+
if (link.matches(sel)) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
} catch (e) {
|
|
247
|
+
// a selector the browser can't match against (e.g. only a pseudo element) - ignore
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function own5B(el, msg) {
|
|
255
|
+
return { el: el, msg: msg, hhs: '5B', rule: 'hhs-5b', help: 'Links must be distinguishable from the surrounding text without relying on colour' };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function check5B() {
|
|
259
|
+
var violations = [], review = [];
|
|
260
|
+
var links = document.querySelectorAll('a[href]');
|
|
261
|
+
for (var i = 0; i < links.length; i++) {
|
|
262
|
+
var a = links[i];
|
|
263
|
+
if (a.closest('#' + PANEL_ID) || !a.offsetParent || !(a.innerText || '').trim()) {
|
|
264
|
+
continue; // the panel itself, hidden links, and links with no text
|
|
265
|
+
}
|
|
266
|
+
var ls = getComputedStyle(a);
|
|
267
|
+
if (hasNonColourCue(ls)) {
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
var block = blockAncestor(a);
|
|
271
|
+
if (!block || !inTextBlock(a, block)) {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
var lc = parseColor(ls.color), tc = parseColor(getComputedStyle(block).color);
|
|
275
|
+
if (!lc || !tc) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (lc.r === tc.r && lc.g === tc.g && lc.b === tc.b) {
|
|
279
|
+
// same colour as the text and no underline: nothing distinguishes it at all
|
|
280
|
+
violations.push(own5B(a, 'The link is not distinguished from the surrounding text at all (same colour, no underline)'));
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
var ratio = contrast(lc, tc);
|
|
284
|
+
var ratioText = ratio.toFixed(2) + ':1 (link ' + colorText(lc) + ', text ' + colorText(tc) + ')';
|
|
285
|
+
if (ratio < 3) {
|
|
286
|
+
violations.push(own5B(a, 'The link is distinguished from the surrounding text by colour alone, and the contrast between the two is only ' + ratioText + '; it must be at least 3:1, or the link needs a non-colour cue such as an underline'));
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
var hover = stateCue(a, 'hover'), focus = stateCue(a, 'focus');
|
|
290
|
+
if (hover === false || focus === false) {
|
|
291
|
+
violations.push(own5B(a, 'The link is distinguished from the surrounding text by colour alone (' + ratioText + ', which is enough), but gets no underline or other non-colour cue on ' + (hover === false && focus === false ? 'hover or focus' : hover === false ? 'hover' : 'focus')));
|
|
292
|
+
} else if (hover === null || focus === null) {
|
|
293
|
+
review.push(own5B(a, 'The link is distinguished from the surrounding text by colour alone (' + ratioText + '). Check that it gets an underline or other non-colour cue on hover and focus - the stylesheets could not be read to check (open the page over http rather than as a file to check this automatically)'));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return { violations: violations, review: review };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
// ---- structural checks ------------------------------------------------------
|
|
301
|
+
//
|
|
302
|
+
// The same rules as the IG Publisher's HTMLInspector. Only the ones axe does not already do under WCAG 2.0 A/AA: axe covers the page language and title,
|
|
303
|
+
// missing alt text, unlabelled controls, nested interactive elements, focusable content in aria-hidden,
|
|
304
|
+
// empty links, layout tables, frames, captions and meta refresh, and the aria idrefs.
|
|
305
|
+
|
|
306
|
+
var PLACEHOLDER_ALT_TEXT = ['.', '-', 'icon', 'image', 'img', 'picture', 'graphic', 'photo', 'spacer', 'logo'];
|
|
307
|
+
// only phrases that are never names: words like 'link' or 'this' are also element names (Bundle.link)
|
|
308
|
+
var GENERIC_LINK_TEXT = ['here', 'click here', 'more', 'read more', 'this link', 'click this link'];
|
|
309
|
+
var WIDGET_ROLES = ['button', 'link', 'checkbox', 'radio', 'switch', 'tab', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option',
|
|
310
|
+
'textbox', 'searchbox', 'combobox', 'slider', 'spinbutton', 'treeitem', 'gridcell'];
|
|
311
|
+
|
|
312
|
+
function ours(e) {
|
|
313
|
+
return e.closest('#' + PANEL_ID) || (e.classList && e.classList.contains('fhir-a11y-badge'));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function shortText(e) {
|
|
317
|
+
var t = (e.textContent || '').replace(/\s+/g, ' ').trim();
|
|
318
|
+
return t.length > 50 ? t.slice(0, 47) + '...' : t;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function headingLevel(h) {
|
|
322
|
+
if (/^H[1-6]$/.test(h.tagName)) {
|
|
323
|
+
return +h.tagName.charAt(1);
|
|
324
|
+
}
|
|
325
|
+
var l = parseInt(h.getAttribute('aria-level'), 10);
|
|
326
|
+
return l > 0 ? l : 2; // role=heading with no aria-level is a level 2 heading
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function hdesc(h) {
|
|
330
|
+
return '<' + h.tagName.toLowerCase() + (h.getAttribute('role') === 'heading' ? ' role="heading" aria-level="' + headingLevel(h) + '"' : '') + "> '" + shortText(h) + "'";
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// A page must have exactly one top level heading, it must be the first heading on the page, and levels
|
|
334
|
+
// must not skip on the way down. As in HTMLInspector, the walk carries the level each heading SHOULD have
|
|
335
|
+
// had, so one misplaced heading isn't reported again for every heading after it.
|
|
336
|
+
function checkHeadings(out) {
|
|
337
|
+
var all = document.querySelectorAll('h1, h2, h3, h4, h5, h6, [role="heading"]');
|
|
338
|
+
var hs = [];
|
|
339
|
+
for (var i = 0; i < all.length; i++) {
|
|
340
|
+
if (!ours(all[i])) {
|
|
341
|
+
hs.push(all[i]);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (hs.length === 0) {
|
|
345
|
+
out.review.push({ el: document.body, hhs: '4A', rule: 'heading-none', help: 'Pages should have headings', msg: 'The page has no headings at all, so assistive technology users cannot navigate it by heading' });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
var first = hs[0], root = headingLevel(first), prev = root, prevNode = first;
|
|
349
|
+
for (var j = 1; j < hs.length; j++) {
|
|
350
|
+
var h = hs[j], level = headingLevel(h), should = level;
|
|
351
|
+
if (level < root) {
|
|
352
|
+
should = root;
|
|
353
|
+
out.violations.push({ el: h, hhs: '4A', rule: 'heading-above-root', help: 'The first heading on the page must be its top level heading',
|
|
354
|
+
msg: 'The heading ' + hdesc(h) + ' is at a higher level than the first heading on the page (' + hdesc(first) + ')' });
|
|
355
|
+
} else if (level === root) {
|
|
356
|
+
out.violations.push({ el: h, hhs: '4A', rule: 'heading-multiple-roots', help: 'A page must have exactly one top level heading',
|
|
357
|
+
msg: 'The heading ' + hdesc(h) + ' is at the same level as the first heading on the page (' + hdesc(first) + '); every other heading must be beneath it' });
|
|
358
|
+
} else if (level > prev + 1) {
|
|
359
|
+
should = prev + 1;
|
|
360
|
+
out.violations.push({ el: h, hhs: '4A', rule: 'heading-level-skipped', help: 'Heading levels must not skip a level going down',
|
|
361
|
+
msg: 'The heading ' + hdesc(h) + ' follows ' + hdesc(prevNode) + ', so it should be <h' + should + '>' });
|
|
362
|
+
}
|
|
363
|
+
prevNode = h;
|
|
364
|
+
prev = should;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function focusable(e) {
|
|
369
|
+
var ti = e.getAttribute('tabindex');
|
|
370
|
+
if (ti !== null) {
|
|
371
|
+
return ti.trim().charAt(0) !== '-';
|
|
372
|
+
}
|
|
373
|
+
switch (e.tagName) {
|
|
374
|
+
case 'A': return e.hasAttribute('href');
|
|
375
|
+
case 'BUTTON': case 'SELECT': case 'TEXTAREA': case 'SUMMARY': return !e.hasAttribute('disabled');
|
|
376
|
+
case 'INPUT': return !e.hasAttribute('disabled') && (e.getAttribute('type') || '').toLowerCase() !== 'hidden';
|
|
377
|
+
default: return false;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function interactive(e) {
|
|
382
|
+
return focusable(e) || WIDGET_ROLES.indexOf((e.getAttribute('role') || '').trim()) >= 0;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function isLinkToNamedTarget(text, href) {
|
|
386
|
+
if (!href || href.indexOf('#') < 0) {
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
var frag = href.slice(href.indexOf('#') + 1).toLowerCase(), t = text.toLowerCase();
|
|
390
|
+
return frag === t || frag.slice(-(t.length + 1)) === '.' + t;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function isFileNameOf(alt, src) {
|
|
394
|
+
if (!alt || !alt.trim() || !src || src.indexOf('data:') === 0) {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
var fn = src.indexOf('/') >= 0 ? src.slice(src.lastIndexOf('/') + 1) : src;
|
|
398
|
+
return alt.trim().toLowerCase() === fn.toLowerCase();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function desc(e) {
|
|
402
|
+
var attrs = ['id', 'name', 'type', 'href', 'src', 'class'];
|
|
403
|
+
for (var i = 0; i < attrs.length; i++) {
|
|
404
|
+
var v = e.getAttribute(attrs[i]);
|
|
405
|
+
if (v !== null && v.indexOf('data:') !== 0) {
|
|
406
|
+
return '<' + e.tagName.toLowerCase() + ' ' + attrs[i] + '="' + (v.length > 60 ? v.slice(0, 60) + '...' : v) + '">';
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return '<' + e.tagName.toLowerCase() + '>';
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function checkElements(out) {
|
|
413
|
+
var els = document.body.getElementsByTagName('*');
|
|
414
|
+
for (var i = 0; i < els.length; i++) {
|
|
415
|
+
var e = els[i];
|
|
416
|
+
if (ours(e) || e.closest('svg')) {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
var tag = e.tagName;
|
|
420
|
+
if (tag === 'LABEL' && e.hasAttribute('for')) {
|
|
421
|
+
var f = e.getAttribute('for');
|
|
422
|
+
if (f && !document.getElementById(f)) {
|
|
423
|
+
out.violations.push({ el: e, hhs: '13B', rule: 'label-for-broken', help: 'A label must point at a form control that exists',
|
|
424
|
+
msg: "The label '" + shortText(e) + "' says it is for the element with id '" + f + "', but nothing on the page has that id, so the label is not connected to any form control" });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (e.hasAttribute('accesskey')) {
|
|
428
|
+
out.violations.push({ el: e, hhs: '6B', rule: 'accesskey', help: 'Accesskeys can conflict with browser and screen reader shortcuts',
|
|
429
|
+
msg: 'The ' + desc(e) + ' has an accesskey' });
|
|
430
|
+
}
|
|
431
|
+
if (tag === 'IMG' || tag === 'AREA' || (tag === 'INPUT' && (e.getAttribute('type') || '').toLowerCase() === 'image')) {
|
|
432
|
+
var alt = e.getAttribute('alt');
|
|
433
|
+
if (alt !== null && (PLACEHOLDER_ALT_TEXT.indexOf(alt.trim().toLowerCase()) >= 0 || isFileNameOf(alt, e.getAttribute('src')))) {
|
|
434
|
+
out.review.push({ el: e, hhs: '2A', rule: 'alt-placeholder', help: 'Alternative text must describe the image',
|
|
435
|
+
msg: 'The ' + desc(e) + " has alt='" + alt + "', which tells a screen reader user nothing: describe the image, or use alt=\"\" if it is decorative" });
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
if (tag === 'A' && e.hasAttribute('href')) {
|
|
439
|
+
var text = (e.textContent || '').replace(/\s+/g, ' ').trim();
|
|
440
|
+
if (GENERIC_LINK_TEXT.indexOf(text.toLowerCase()) >= 0 && !(e.getAttribute('aria-label') || '').trim() && !isLinkToNamedTarget(text, e.getAttribute('href'))) {
|
|
441
|
+
out.review.push({ el: e, hhs: '9G', rule: 'link-generic-text', help: 'Link text should say where the link goes',
|
|
442
|
+
msg: "The link '" + text + "' (" + desc(e) + ') does not say where it goes when read out of context' });
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
// only handlers in the markup can be seen: ones added with addEventListener can't be
|
|
446
|
+
if (e.hasAttribute('onclick') && !interactive(e)) {
|
|
447
|
+
var oc = e.getAttribute('onclick');
|
|
448
|
+
out.violations.push({ el: e, hhs: '6A', rule: 'click-not-keyboard', help: 'Everything that can be clicked must also work from the keyboard',
|
|
449
|
+
msg: 'The ' + desc(e) + ' has a click handler (' + (oc.length > 40 ? oc.slice(0, 40) + '...' : oc) + ') but cannot be reached or used from the keyboard' });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function checkStructure() {
|
|
455
|
+
var out = { violations: [], review: [] };
|
|
456
|
+
checkHeadings(out);
|
|
457
|
+
checkElements(out);
|
|
458
|
+
return out;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ---- contrast that axe could not decide -------------------------------------------------------------
|
|
462
|
+
//
|
|
463
|
+
// axe will not guess at the background when there's a background image (the tree lines in the hierarchy
|
|
464
|
+
// tables) or when something else overlaps the text (fixed headers, tab bars), and files those under 'needs
|
|
465
|
+
// review'. That makes the same link colour look like it passes in one place and fails in another. For these,
|
|
466
|
+
// work the contrast out against the nearest solid background colour, and report it either way with the
|
|
467
|
+
// assumption stated.
|
|
468
|
+
|
|
469
|
+
var UNDECIDED = /background image|overlapped|obscured|could not be determined|pseudo|partially/i;
|
|
470
|
+
|
|
471
|
+
function solidBackground(e) {
|
|
472
|
+
var layers = [];
|
|
473
|
+
for (var n = e; n && n.nodeType === 1; n = n.parentElement) {
|
|
474
|
+
var c = parseColor(getComputedStyle(n).backgroundColor);
|
|
475
|
+
if (c && c.a > 0) {
|
|
476
|
+
layers.push(c);
|
|
477
|
+
if (c.a >= 1) {
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
var bg = { r: 255, g: 255, b: 255, a: 1 };
|
|
483
|
+
for (var i = layers.length - 1; i >= 0; i--) {
|
|
484
|
+
bg = blend(layers[i], bg);
|
|
485
|
+
}
|
|
486
|
+
return bg;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function blend(top, under) {
|
|
490
|
+
var a = top.a === undefined ? 1 : top.a;
|
|
491
|
+
return { r: top.r * a + under.r * (1 - a), g: top.g * a + under.g * (1 - a), b: top.b * a + under.b * (1 - a), a: 1 };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function effectiveOpacity(e) {
|
|
495
|
+
var o = 1;
|
|
496
|
+
for (var n = e; n && n.nodeType === 1; n = n.parentElement) {
|
|
497
|
+
o = o * parseFloat(getComputedStyle(n).opacity || '1');
|
|
498
|
+
}
|
|
499
|
+
return o;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function decideContrast(res) {
|
|
503
|
+
var decided = [];
|
|
504
|
+
res.incomplete = res.incomplete.filter(function (rule) {
|
|
505
|
+
if (rule.id !== 'color-contrast') {
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
var keep = [];
|
|
509
|
+
rule.nodes.forEach(function (n) {
|
|
510
|
+
var reason = (n.any || []).map(function (c) { return c.message || ''; }).join(' ');
|
|
511
|
+
var target = null;
|
|
512
|
+
try {
|
|
513
|
+
target = n.target && document.querySelector(n.target[0]);
|
|
514
|
+
} catch (e) {
|
|
515
|
+
target = null;
|
|
516
|
+
}
|
|
517
|
+
if (!target || !UNDECIDED.test(reason)) {
|
|
518
|
+
keep.push(n);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
var cs = getComputedStyle(target);
|
|
522
|
+
var fg = parseColor(cs.color);
|
|
523
|
+
if (!fg) {
|
|
524
|
+
keep.push(n);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
var bg = solidBackground(target);
|
|
528
|
+
fg.a = (fg.a === undefined ? 1 : fg.a) * effectiveOpacity(target);
|
|
529
|
+
var text = blend(fg, bg);
|
|
530
|
+
var ratio = contrast(text, bg);
|
|
531
|
+
var px = parseFloat(cs.fontSize), bold = parseInt(cs.fontWeight, 10) >= 700;
|
|
532
|
+
var large = px >= 24 || (bold && px >= 18.66);
|
|
533
|
+
var needed = large ? 3 : 4.5;
|
|
534
|
+
if (ratio < needed) {
|
|
535
|
+
decided.push({
|
|
536
|
+
html: n.html, target: n.target, impact: 'serious',
|
|
537
|
+
failureSummary: 'Element has insufficient color contrast of ' + ratio.toFixed(2) + ' (foreground color: ' + colorText(text) +
|
|
538
|
+
', background color: ' + colorText(bg) + ', font size: ' + px + 'px, font weight: ' + (bold ? 'bold' : 'normal') + '). Expected contrast ratio of ' + needed +
|
|
539
|
+
':1. [background assumed: the checker could not see it directly (' + reason.replace(/^Element's /, '').replace(/\.$/, '') + '), so this uses the nearest solid background colour]'
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
// and if it is enough, it passes - no need for a person to look at it
|
|
543
|
+
});
|
|
544
|
+
rule.nodes = keep;
|
|
545
|
+
if (decided.length > 0) {
|
|
546
|
+
var copy = { id: rule.id, tags: rule.tags, help: rule.help, helpUrl: rule.helpUrl, impact: 'serious', nodes: decided };
|
|
547
|
+
var existing = res.violations.filter(function (v) { return v.id === 'color-contrast'; })[0];
|
|
548
|
+
if (existing) {
|
|
549
|
+
existing.nodes = existing.nodes.concat(decided);
|
|
550
|
+
} else {
|
|
551
|
+
res.violations.push(copy);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return keep.length > 0;
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// ---- running ---------------------------------------------------------------------------------------
|
|
559
|
+
|
|
560
|
+
function loadAxe(src, done) {
|
|
561
|
+
if (window.axe) {
|
|
562
|
+
done();
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
var s = document.createElement('script');
|
|
566
|
+
s.src = src;
|
|
567
|
+
s.onload = function () { done(); };
|
|
568
|
+
s.onerror = function () { done('Could not load ' + src); };
|
|
569
|
+
document.head.appendChild(s);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function run(panel) {
|
|
573
|
+
var status = panel.querySelector('.fhir-a11y-status');
|
|
574
|
+
var out = panel.querySelector('.fhir-a11y-results');
|
|
575
|
+
status.textContent = 'Checking...';
|
|
576
|
+
out.textContent = '';
|
|
577
|
+
clearMarkers();
|
|
578
|
+
loadAxe(panel.getAttribute('data-axe'), function (err) {
|
|
579
|
+
if (err) {
|
|
580
|
+
status.textContent = err;
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
window.axe.run({ exclude: [['#' + PANEL_ID]] }, { runOnly: { type: 'tag', values: ['wcag2a', 'wcag2aa'] }, rules: { 'link-in-text-block': { enabled: false } }, resultTypes: ['violations', 'incomplete'] })
|
|
584
|
+
.then(function (res) {
|
|
585
|
+
decideContrast(res);
|
|
586
|
+
var own = check5B();
|
|
587
|
+
var st = checkStructure();
|
|
588
|
+
own.violations = own.violations.concat(st.violations);
|
|
589
|
+
own.review = own.review.concat(st.review);
|
|
590
|
+
render(panel, res, own);
|
|
591
|
+
})
|
|
592
|
+
.catch(function (e) {
|
|
593
|
+
status.textContent = 'The check failed: ' + e;
|
|
594
|
+
});
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function group(results, own, ownKey) {
|
|
599
|
+
var g = {};
|
|
600
|
+
function add(id, item) {
|
|
601
|
+
(g[id] = g[id] || []).push(item);
|
|
602
|
+
}
|
|
603
|
+
results.forEach(function (rule) {
|
|
604
|
+
rule.nodes.forEach(function (n) {
|
|
605
|
+
add(hhsFor(rule), {
|
|
606
|
+
rule: rule.id, help: rule.help, url: rule.helpUrl, impact: n.impact || rule.impact,
|
|
607
|
+
detail: (n.failureSummary || '').replace(/^Fix (any|all) of the following:\s*/i, ''),
|
|
608
|
+
html: n.html, target: n.target && n.target[0]
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
own[ownKey].forEach(function (f) {
|
|
613
|
+
add(f.hhs, { rule: f.rule, help: f.help, detail: f.msg, html: f.el === document.body ? '<body>' : f.el.outerHTML.slice(0, 200), el: f.el });
|
|
614
|
+
});
|
|
615
|
+
return g;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function sortedIds(g) {
|
|
619
|
+
var order = Object.keys(HHS);
|
|
620
|
+
return Object.keys(g).sort(function (a, b) {
|
|
621
|
+
return order.indexOf(a) - order.indexOf(b);
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function el(tag, text, attrs) {
|
|
626
|
+
var e = document.createElement(tag);
|
|
627
|
+
if (text) {
|
|
628
|
+
e.textContent = text;
|
|
629
|
+
}
|
|
630
|
+
for (var k in (attrs || {})) {
|
|
631
|
+
e.setAttribute(k, attrs[k]);
|
|
632
|
+
}
|
|
633
|
+
return e;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// ---- markers on the page ---------------------------------------------------------------------------
|
|
637
|
+
//
|
|
638
|
+
// Each finding gets a number. The element it is about gets an outline and a small numbered badge next to
|
|
639
|
+
// it (red for failures, orange for 'needs review'); the badge links back to the finding in the report,
|
|
640
|
+
// and 'Show on page' in the report scrolls to the element. The markers are removed before each run so
|
|
641
|
+
// they are never checked themselves, and 'Clear markers' takes them away.
|
|
642
|
+
|
|
643
|
+
var markers = []; // { el, outline, outlineOffset, badge }
|
|
644
|
+
|
|
645
|
+
// elements whose parent can't hold a span in front of them: the badge goes inside instead
|
|
646
|
+
var BADGE_INSIDE_PARENTS = ['TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TR', 'UL', 'OL', 'DL', 'SELECT', 'COLGROUP', 'OPTGROUP'];
|
|
647
|
+
var VOID = ['IMG', 'INPUT', 'BR', 'HR', 'AREA', 'COL', 'EMBED', 'SOURCE', 'TRACK', 'WBR', 'META', 'LINK'];
|
|
648
|
+
|
|
649
|
+
function resolve(item) {
|
|
650
|
+
if (!item.el && typeof item.target === 'string') {
|
|
651
|
+
try {
|
|
652
|
+
item.el = document.querySelector(item.target);
|
|
653
|
+
} catch (e) {
|
|
654
|
+
item.el = null;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return item.el;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function mark(item, review) {
|
|
661
|
+
var target = resolve(item);
|
|
662
|
+
if (!target || target === document.documentElement || target === document.body || target.closest('#' + PANEL_ID)) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
var colour = review ? '#b35900' : '#c00000';
|
|
666
|
+
var m = { el: target, outline: target.style.outline, outlineOffset: target.style.outlineOffset };
|
|
667
|
+
target.style.outline = '2px ' + (review ? 'dashed ' : 'solid ') + colour;
|
|
668
|
+
target.style.outlineOffset = '1px';
|
|
669
|
+
var badge = el('a', String(item.num), {
|
|
670
|
+
'class': 'fhir-a11y-badge', href: '#fhir-a11y-item-' + item.num, 'aria-hidden': 'true', tabindex: '-1',
|
|
671
|
+
title: '#' + item.num + ': ' + item.help,
|
|
672
|
+
style: 'display: inline-block; margin: 0 2px; padding: 0 4px; border-radius: 8px; background-color: ' + colour +
|
|
673
|
+
'; color: #ffffff; font: bold 11px/15px sans-serif; text-decoration: none; vertical-align: top; position: relative; z-index: 1000'
|
|
674
|
+
});
|
|
675
|
+
var parent = target.parentElement;
|
|
676
|
+
if (parent && BADGE_INSIDE_PARENTS.indexOf(parent.tagName) >= 0 && VOID.indexOf(target.tagName) < 0) {
|
|
677
|
+
target.insertBefore(badge, target.firstChild);
|
|
678
|
+
} else if (parent) {
|
|
679
|
+
parent.insertBefore(badge, target);
|
|
680
|
+
}
|
|
681
|
+
m.badge = badge;
|
|
682
|
+
markers.push(m);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function clearMarkers() {
|
|
686
|
+
markers.forEach(function (m) {
|
|
687
|
+
m.el.style.outline = m.outline;
|
|
688
|
+
m.el.style.outlineOffset = m.outlineOffset;
|
|
689
|
+
if (m.badge && m.badge.parentNode) {
|
|
690
|
+
m.badge.parentNode.removeChild(m.badge);
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
markers = [];
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function show(item) {
|
|
697
|
+
var target = resolve(item);
|
|
698
|
+
if (!target) {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
if (!target.offsetParent && target !== document.body) {
|
|
702
|
+
alertHidden(item);
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
target.scrollIntoView({ block: 'center' });
|
|
706
|
+
// flash it so it stands out from the other marked elements
|
|
707
|
+
var old = target.style.outline;
|
|
708
|
+
target.style.outline = '4px solid #0050c0';
|
|
709
|
+
setTimeout(function () {
|
|
710
|
+
target.style.outline = old;
|
|
711
|
+
}, 2500);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function alertHidden(item) {
|
|
715
|
+
var li = document.getElementById('fhir-a11y-item-' + item.num);
|
|
716
|
+
if (li && !li.querySelector('.fhir-a11y-hidden-note')) {
|
|
717
|
+
li.appendChild(el('div', 'This element is not visible at the moment - it is probably in a tab or section that is not open.', { 'class': 'fhir-a11y-hidden-note', style: 'font-style: italic' }));
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function renderGroups(container, g, counter) {
|
|
722
|
+
sortedIds(g).forEach(function (id) {
|
|
723
|
+
var items = g[id];
|
|
724
|
+
var sec = el('div', null, { 'class': 'fhir-a11y-group' });
|
|
725
|
+
sec.appendChild(el('div', 'HHS ' + id + ' - ' + (HHS[id] || '') + ' (' + items.length + ')', { 'class': 'fhir-a11y-group-title', style: 'font-weight: bold; margin-top: 8px' }));
|
|
726
|
+
var ul = el('ul');
|
|
727
|
+
items.forEach(function (item) {
|
|
728
|
+
item.num = ++counter.n;
|
|
729
|
+
if (counter.markers) {
|
|
730
|
+
mark(item, counter.review);
|
|
731
|
+
}
|
|
732
|
+
var li = el('li', null, { id: 'fhir-a11y-item-' + item.num });
|
|
733
|
+
li.appendChild(el('b', '#' + item.num + ' ', { style: 'color: ' + (counter.review ? '#b35900' : '#c00000') }));
|
|
734
|
+
li.appendChild(el('b', item.help + ' '));
|
|
735
|
+
if (item.url) {
|
|
736
|
+
li.appendChild(el('a', '[' + item.rule + ']', { href: item.url, target: '_blank', rel: 'noopener' }));
|
|
737
|
+
} else {
|
|
738
|
+
li.appendChild(el('span', '[' + item.rule + ']'));
|
|
739
|
+
}
|
|
740
|
+
if (item.detail) {
|
|
741
|
+
li.appendChild(el('div', item.detail, { style: 'white-space: pre-wrap' }));
|
|
742
|
+
}
|
|
743
|
+
li.appendChild(el('code', item.html.length > 200 ? item.html.slice(0, 200) + '...' : item.html, { style: 'display: block; color: #333' }));
|
|
744
|
+
var b = el('button', 'Show on page', { type: 'button' });
|
|
745
|
+
b.addEventListener('click', function () {
|
|
746
|
+
show(item);
|
|
747
|
+
});
|
|
748
|
+
li.appendChild(b);
|
|
749
|
+
ul.appendChild(li);
|
|
750
|
+
});
|
|
751
|
+
sec.appendChild(ul);
|
|
752
|
+
container.appendChild(sec);
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function count(g) {
|
|
757
|
+
var n = 0;
|
|
758
|
+
Object.keys(g).forEach(function (k) {
|
|
759
|
+
n += g[k].length;
|
|
760
|
+
});
|
|
761
|
+
return n;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function render(panel, res, own) {
|
|
765
|
+
var status = panel.querySelector('.fhir-a11y-status');
|
|
766
|
+
var out = panel.querySelector('.fhir-a11y-results');
|
|
767
|
+
var failed = group(res.violations, own, 'violations');
|
|
768
|
+
var review = group(res.incomplete, own, 'review');
|
|
769
|
+
var nf = count(failed), nr = count(review);
|
|
770
|
+
status.textContent = nf === 0 ? 'No automatically detectable WCAG 2.0 A/AA failures on this page as it is now.' : nf + ' failure(s) found.';
|
|
771
|
+
if (nr > 0) {
|
|
772
|
+
status.textContent += ' ' + nr + ' item(s) need a person to review.';
|
|
773
|
+
}
|
|
774
|
+
// find every element before any badge goes in: axe's selectors use positions (nth-child), which the badges change
|
|
775
|
+
[failed, review].forEach(function (g) {
|
|
776
|
+
Object.keys(g).forEach(function (k) {
|
|
777
|
+
g[k].forEach(resolve);
|
|
778
|
+
});
|
|
779
|
+
});
|
|
780
|
+
var showMarkers = panel.querySelector('.fhir-a11y-markers');
|
|
781
|
+
var counter = { n: 0, review: false, markers: !showMarkers || showMarkers.checked };
|
|
782
|
+
renderGroups(out, failed, counter);
|
|
783
|
+
if (nr > 0) {
|
|
784
|
+
var d = el('details');
|
|
785
|
+
d.appendChild(el('summary', 'Needs review (' + nr + ') - the checker could not decide these'));
|
|
786
|
+
counter.review = true;
|
|
787
|
+
var showReview = panel.querySelector('.fhir-a11y-markers-review');
|
|
788
|
+
counter.markers = counter.markers && showReview && showReview.checked;
|
|
789
|
+
renderGroups(d, review, counter);
|
|
790
|
+
out.appendChild(d);
|
|
791
|
+
}
|
|
792
|
+
if (markers.length > 0) {
|
|
793
|
+
status.textContent += ' The elements concerned are outlined and numbered on the page.';
|
|
794
|
+
}
|
|
795
|
+
out.appendChild(el('p', 'Automated checks find only some problems: whether text alternatives, headings and link text are meaningful, keyboard use, focus order and so on still need checking by hand. Content in tabs that are not showing is not checked - open the tab and check again.', { style: 'font-style: italic' }));
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function init() {
|
|
799
|
+
var panel = document.getElementById(PANEL_ID);
|
|
800
|
+
if (!panel) {
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
var b = panel.querySelector('.fhir-a11y-run');
|
|
804
|
+
if (b) {
|
|
805
|
+
b.addEventListener('click', function () {
|
|
806
|
+
run(panel);
|
|
807
|
+
});
|
|
808
|
+
var lbl = el('label', null, { style: 'margin-left: 6px' });
|
|
809
|
+
var cb = el('input', null, { type: 'checkbox', 'class': 'fhir-a11y-markers' });
|
|
810
|
+
cb.checked = true;
|
|
811
|
+
lbl.appendChild(cb);
|
|
812
|
+
lbl.appendChild(document.createTextNode(' mark failures on the page '));
|
|
813
|
+
b.parentNode.insertBefore(lbl, b.nextSibling);
|
|
814
|
+
// the 'needs review' items are mostly whole paragraphs and headings (contrast axe couldn't decide), so
|
|
815
|
+
// marking them makes a lot of noise: off unless asked for
|
|
816
|
+
var lbl2 = el('label', null, { style: 'margin-left: 6px' });
|
|
817
|
+
var cb2 = el('input', null, { type: 'checkbox', 'class': 'fhir-a11y-markers-review' });
|
|
818
|
+
lbl2.appendChild(cb2);
|
|
819
|
+
lbl2.appendChild(document.createTextNode(' and items needing review '));
|
|
820
|
+
lbl.parentNode.insertBefore(lbl2, lbl.nextSibling);
|
|
821
|
+
var clr = el('button', 'Clear markers', { type: 'button' });
|
|
822
|
+
clr.addEventListener('click', clearMarkers);
|
|
823
|
+
lbl2.parentNode.insertBefore(clr, lbl2.nextSibling);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
if (document.readyState === 'loading') {
|
|
828
|
+
document.addEventListener('DOMContentLoaded', init);
|
|
829
|
+
} else {
|
|
830
|
+
init();
|
|
831
|
+
}
|
|
832
|
+
})();
|