===== run.js ===== /* Runs axe-core (Deque Systems, MPL-2.0, loaded from axe.min.js beside this file) on the page this frame holds, for the WCAG A/AA rules the aiwebpageseo Accessibility check does not cover itself, and posts the result to the tool page. The page in this frame is a copy of the audited page with all of its own scripts taken out. */ (function () { try { parent.postMessage({ a11yAxe: 2 }, '*'); } catch (e) {} // started: the tool page stops waiting if this never arrives var RULES = ['input-image-alt', 'nested-interactive', 'list', 'listitem', 'definition-list', 'dlitem', 'meta-viewport', 'aria-required-parent', 'aria-required-children', 'aria-valid-attr', 'aria-valid-attr-value', 'aria-allowed-attr', 'aria-prohibited-attr', 'aria-required-attr', 'aria-roles', 'aria-hidden-body', 'aria-input-field-name', 'aria-toggle-field-name', 'aria-command-name', 'aria-meter-name', 'aria-progressbar-name', 'aria-tooltip-name', 'aria-braille-equivalent', 'aria-conditional-attr', 'aria-deprecated-role', 'scrollable-region-focusable', 'link-in-text-block', 'svg-img-alt', 'role-img-alt', 'object-alt', 'area-alt', 'input-button-name', 'summary-name', 'document-title', 'avoid-inline-spacing', 'server-side-image-map', 'blink', 'marquee', 'td-headers-attr', 'th-has-data-cells', 'valid-lang', 'html-xml-lang-mismatch', 'autocomplete-valid', 'target-size']; // Best-practice rules (axe-core's own "best-practice" tag): not WCAG requirements, so the tool page reports any that // fail as unscored notes. Left out: heading-order, page-has-heading-one and skip-link (the tool's own checks cover // them) and frame-tested (it only reports frames axe could not enter). var BEST = ['region', 'landmark-one-main', 'landmark-unique', 'landmark-no-duplicate-banner', 'landmark-no-duplicate-contentinfo', 'landmark-no-duplicate-main', 'landmark-banner-is-top-level', 'landmark-contentinfo-is-top-level', 'landmark-main-is-top-level', 'landmark-complementary-is-top-level', 'empty-heading', 'empty-table-header', 'tabindex', 'accesskeys', 'image-redundant-alt', 'label-title-only', 'aria-allowed-role', 'aria-dialog-name', 'aria-treeitem-name', 'aria-text', 'presentation-role-conflict', 'scope-attr-valid', 'table-duplicate-name', 'meta-viewport-large']; var sent = false, started = false; function send(m) { if (sent) return; sent = true; m.a11yAxe = 1; try { parent.postMessage(m, '*'); } catch (e) {} } function go() { if (sent || started) return; started = true; if (!window.axe) return send({ ok: false, error: 'the checker did not load' }); var known = axe.getRules().map(function (r) { return r.ruleId; }); var rules = RULES.concat(BEST).filter(function (r) { return known.indexOf(r) >= 0; }); axe.run(document, { runOnly: { type: 'rule', values: rules }, resultTypes: ['violations'] }).then(function (r) { var violations = r.violations.map(function (v) { return { id: v.id, impact: v.impact, help: v.help, description: v.description, tags: v.tags, n: v.nodes.length, bp: BEST.indexOf(v.id) >= 0, ex: v.nodes.slice(0, 3).map(function (n) { return { html: String(n.html || '').slice(0, 200), fix: String(n.failureSummary || '').slice(0, 300) }; }) }; }); var actResults = []; var actRules = [ { id: '9e45ec', property: 'word-spacing', threshold: '0.16em', help: 'Word spacing restricted below 0.16em' }, { id: '24afc2', property: 'letter-spacing', threshold: '0.12em', help: 'Letter spacing restricted below 0.12em' } ]; actRules.forEach(function (rule) { try { if (!window.a11yTextSpacing || typeof window.a11yTextSpacing.validateTextSpacing !== 'function') { throw Error('ACT text-spacing validator not loaded'); } var result = window.a11yTextSpacing.validateTextSpacing(rule.id); actResults.push({ id: rule.id, outcome: result.outcome, count: result.findings.length }); if (result.outcome !== 'failed') return; violations.push({ id: 'act-' + rule.id, impact: 'serious', help: rule.help, description: 'W3C ACT rule ' + rule.id + ': restrictive CSS text-spacing declaration detected.', tags: ['wcag1412'], n: result.findings.length, bp: false, ex: result.findings.slice(0, 3).map(function (f) { return { html: String(f.selector || '').slice(0, 200), fix: String(f.message || '').slice(0, 300) }; }) }); } catch (error) { actResults.push({ id: rule.id, outcome: 'error', error: String(error && error.message || error).slice(0, 200) }); } }); send({ ok: true, version: axe.version, violations: violations, actResults: actResults }); }, function (e) { send({ ok: false, error: String((e && e.message) || e).slice(0, 200) }); }); } if (document.readyState === 'complete') go(); else { window.addEventListener('load', go); setTimeout(go, 8000); } })(); ===== text-spacing.js ===== 'use strict'; const RULES = { '9e45ec': { property: 'word-spacing', threshold: 0.16 }, '24afc2': { property: 'letter-spacing', threshold: 0.12 } }; function validateTextSpacing(ruleId) { const rule = RULES[ruleId]; if (!rule) throw new Error('Unsupported ACT rule: ' + ruleId); // ACT applicability is restricted to HTML documents. if (document.documentElement.namespaceURI !== 'http://www.w3.org/1999/xhtml') { return { outcome: 'inapplicable', findings: [] }; } const findings = []; let applicable = false; const walker = document.createTreeWalker( document.body || document.documentElement, NodeFilter.SHOW_TEXT ); while (walker.nextNode()) { const node = walker.currentNode; if (!node.textContent || !node.textContent.trim()) continue; const element = node.parentElement; if (!element) continue; const style = getComputedStyle(element); if (style.display === 'none' || style.visibility === 'hidden') continue; const rect = document.createRange(); rect.selectNodeContents(node); const boxes = [...rect.getClientRects()]; if (!boxes.length) continue; // Distinguish scrollable text from fully hidden text. let hidden = false; let visibilityAncestor = element; while (visibilityAncestor) { const cs = getComputedStyle(visibilityAncestor); if (cs.display === 'none' || cs.visibility === 'hidden' || cs.visibility === 'collapse' || parseFloat(cs.opacity) === 0) { hidden = true; break; } if (cs.overflow === 'hidden' || cs.overflowX === 'hidden' || cs.overflowY === 'hidden') { const ar = visibilityAncestor.getBoundingClientRect(); if (!boxes.some(box => box.right > ar.left && box.left < ar.right && box.bottom > ar.top && box.top < ar.bottom )) { hidden = true; break; } } visibilityAncestor = visibilityAncestor.parentElement; } if (hidden) continue; const hasRenderedBox = boxes.some(box => box.width > 0 && box.height > 0 ); if (!hasRenderedBox) continue; // Preserve the official fixture exclusion for text positioned // far outside the document's usable scroll area. const withinDocument = boxes.some(box => box.right > 0 && box.bottom > 0 && box.left < Math.max( document.documentElement.scrollWidth, window.innerWidth ) && box.top < Math.max( document.documentElement.scrollHeight, window.innerHeight ) ); if (!withinDocument) continue; let ancestor = element; let relevant = false; while (ancestor) { const inline = ancestor.style; if (inline) { const value = inline.getPropertyValue(rule.property); const priority = inline.getPropertyPriority(rule.property); if (value && priority === 'important') { const keyword = value.trim().toLowerCase(); if (!['inherit', 'unset', 'revert', 'revert-layer'] .includes(keyword)) { relevant = true; } } } ancestor = ancestor.parentElement; } if (!relevant) continue; applicable = true; const fontSize = parseFloat(style.fontSize) || 16; const actualSpacing = parseFloat( style.getPropertyValue(rule.property) ) || 0; const minimum = fontSize * rule.threshold; if (actualSpacing < minimum - 0.01) { findings.push({ ruleId, property: rule.property, selector: element.tagName.toLowerCase(), actualPx: actualSpacing, minimumPx: minimum, message: rule.property + ' is restricted below the required ACT threshold' }); } } return { outcome: findings.length ? 'failed' : applicable ? 'passed' : 'inapplicable', findings }; } module.exports = { validateTextSpacing };