|
| 1 | +--- |
| 2 | +main_commit: ee0f68be1 |
| 3 | +analyzed_date: 2026-02-27 |
| 4 | +key_files: |
| 5 | + - src/format/html/format-html.ts |
| 6 | + - src/format/html/format-html-axe.ts |
| 7 | + - src/resources/formats/html/axe/axe-check.js |
| 8 | + - src/resources/formats/html/axe/axe-check.scss |
| 9 | + - src/resources/formats/revealjs/axe/axe-check.scss |
| 10 | + - src/resources/formats/dashboard/axe/axe-check.scss |
| 11 | +--- |
| 12 | + |
| 13 | +# Axe Accessibility Checking Architecture |
| 14 | + |
| 15 | +Quarto's axe-core integration spans three layers: build-time TypeScript, compile-time SCSS, and runtime JavaScript. Each layer operates at a fundamentally different stage, which is why they detect formats differently. |
| 16 | + |
| 17 | +## Three-Layer Overview |
| 18 | + |
| 19 | +### Layer 1: Build-time TypeScript (format detection + dependency injection) |
| 20 | + |
| 21 | +**File:** `src/format/html/format-html.ts` |
| 22 | + |
| 23 | +TypeScript code detects the output format and conditionally injects `axe-check.js` + `axe-check.css` as a `FormatDependency`. The format detection uses the Quarto format system (`isHtmlOutput`, `isRevealJsOutput`, `isDashboardOutput`). |
| 24 | + |
| 25 | +Key responsibilities: |
| 26 | +- Read `axe` option from document metadata |
| 27 | +- Inject axe files as `FormatDependency` (copies JS/CSS into output) |
| 28 | +- Encode options as base64 JSON into a `<script>` tag |
| 29 | + |
| 30 | +The base64 encoding is a defensive pattern that prevents JSON containing `</script>` from breaking the HTML parser. |
| 31 | + |
| 32 | +### Layer 2: Compile-time SCSS (format-specific styling) |
| 33 | + |
| 34 | +**Files:** |
| 35 | +- `src/resources/formats/html/axe/axe-check.scss` — HTML/dashboard styles |
| 36 | +- `src/resources/formats/revealjs/axe/axe-check.scss` — RevealJS-specific styles |
| 37 | + |
| 38 | +SCSS files provide format-specific visual styling. RevealJS compiles its sass-bundles separately from the theme, so theme variables aren't in scope. The CSS custom property bridge (`--quarto-axe-*` variables set in HTML themes, consumed in RevealJS via `var()` with `!default` fallbacks) works around this architectural constraint. |
| 39 | + |
| 40 | +### Layer 3: Runtime JavaScript (scanning + reporting) |
| 41 | + |
| 42 | +**File:** `src/resources/formats/html/axe/axe-check.js` |
| 43 | + |
| 44 | +Single JS file handles all formats at runtime. Format detection uses DOM inspection: |
| 45 | +- `typeof Reveal !== "undefined"` → RevealJS |
| 46 | +- `document.body.classList.contains("quarto-dashboard")` → Dashboard |
| 47 | +- Otherwise → standard HTML |
| 48 | + |
| 49 | +Key classes: |
| 50 | +- `QuartoAxeChecker` — Orchestrates scanning. Loads axe-core from CDN, runs scans, creates reporters. |
| 51 | +- `QuartoAxeDocumentReporter` — Format-specific DOM report (overlay, slide, or offcanvas) |
| 52 | +- `QuartoAxeConsoleReporter` — Logs violations to browser console |
| 53 | +- `QuartoAxeJsonReporter` — Dumps full axe result as JSON to console |
| 54 | + |
| 55 | +## Format-Specific Behavior |
| 56 | + |
| 57 | +### HTML (standard) |
| 58 | +- Report: overlay appended to `<main>` (or `<body>`) |
| 59 | +- Interaction: hover highlights violation elements |
| 60 | +- Rescan: none (static page) |
| 61 | + |
| 62 | +### RevealJS |
| 63 | +- Report: dedicated `<section>` slide appended to `.reveal .slides` |
| 64 | +- Interaction: click navigates to the slide containing the violation |
| 65 | +- Scan prep: temporarily removes `hidden`/`aria-hidden` from all slides so axe can inspect them |
| 66 | +- Rescan: none (slides are static) |
| 67 | + |
| 68 | +### Dashboard |
| 69 | +- Report: Bootstrap offcanvas sidebar with toggle button |
| 70 | +- Interaction: hover highlights violation elements |
| 71 | +- Rescan: triggered by `shown.bs.tab` (page/card tabs), `popstate` (back/forward), `bslib.sidebar` (sidebar toggle) |
| 72 | +- Generation counter prevents stale scan results from overwriting newer ones |
| 73 | + |
| 74 | +## Adding a New HTML Format |
| 75 | + |
| 76 | +If a new HTML-based format is added that needs axe support: |
| 77 | + |
| 78 | +1. **TypeScript** (`format-html.ts`): Add format detection to the axe dependency injection logic |
| 79 | +2. **SCSS**: Create format-specific styles if the report placement differs. If the format uses Bootstrap themes, the CSS custom property bridge handles colors automatically. |
| 80 | +3. **JavaScript** (`axe-check.js`): |
| 81 | + - Add format detection in `QuartoAxeDocumentReporter.report()` — this determines which `createReport*()` method is called |
| 82 | + - Implement a `createReport*()` method for the format's DOM structure |
| 83 | + - If the format has dynamic content changes, add rescan triggers in `setupDashboardRescan()` (or create a format-specific equivalent) |
| 84 | +4. **Tests**: Add Playwright test fixtures (`.qmd` files in `tests/docs/playwright/<format>/`) and parameterized test cases in `axe-accessibility.spec.ts` |
| 85 | + |
| 86 | +## Key Design Decisions |
| 87 | + |
| 88 | +### CDN loading |
| 89 | +axe-core (~600KB) is loaded from `cdn.skypack.dev` at runtime rather than bundled. This keeps the Quarto distribution small since axe is a dev-only feature. Tradeoff: requires internet, fails silently in offline/CSP environments. |
| 90 | + |
| 91 | +### Options encoding |
| 92 | +Options are base64-encoded in a `<script id="quarto-axe-checker-options">` tag. This prevents JSON containing `</script>` from breaking HTML parsing. The runtime decodes with `atob()`. |
| 93 | + |
| 94 | +### Generation counter (dashboard rescan) |
| 95 | +Rapid tab switches can queue multiple `axe.run()` calls. The `scanGeneration` counter ensures only the latest scan's results are displayed. Earlier scans complete but their results are discarded if a newer scan was started. |
| 96 | + |
| 97 | +## Known Limitations |
| 98 | + |
| 99 | +- **SCSS compilation**: RevealJS sass-bundles compile separately from themes. CSS custom properties bridge this gap, but direct SCSS variable sharing isn't possible without pipeline changes. |
| 100 | +- **CDN dependency**: No offline fallback. See `quarto-cli-2u4f` for documentation task. |
| 101 | +- **Popstate delay**: 50ms `setTimeout` for dashboard back/forward navigation. See `quarto-cli-1fdf` for improvement task. |
| 102 | +- **Multi-main elements**: `createReportOverlay()` uses `document.querySelector('main')` which returns the first match. Multiple `<main>` elements is invalid HTML, so this is acceptable. |
0 commit comments