@github/accessibility-scanner-alt-text-plugin 0.0.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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +258 -2
  3. package/dist/index.d.ts +4 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +40 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/src/config.d.ts +5 -0
  8. package/dist/src/config.d.ts.map +1 -0
  9. package/dist/src/config.js +45 -0
  10. package/dist/src/config.js.map +1 -0
  11. package/dist/src/extract.d.ts +4 -0
  12. package/dist/src/extract.d.ts.map +1 -0
  13. package/dist/src/extract.js +22 -0
  14. package/dist/src/extract.js.map +1 -0
  15. package/dist/src/findings.d.ts +3 -0
  16. package/dist/src/findings.d.ts.map +1 -0
  17. package/dist/src/findings.js +16 -0
  18. package/dist/src/findings.js.map +1 -0
  19. package/dist/src/rules/filename-alt-text.d.ts +3 -0
  20. package/dist/src/rules/filename-alt-text.d.ts.map +1 -0
  21. package/dist/src/rules/filename-alt-text.js +21 -0
  22. package/dist/src/rules/filename-alt-text.js.map +1 -0
  23. package/dist/src/rules/index.d.ts +3 -0
  24. package/dist/src/rules/index.d.ts.map +1 -0
  25. package/dist/src/rules/index.js +8 -0
  26. package/dist/src/rules/index.js.map +1 -0
  27. package/dist/src/rules/missing-alt-text.d.ts +3 -0
  28. package/dist/src/rules/missing-alt-text.d.ts.map +1 -0
  29. package/dist/src/rules/missing-alt-text.js +22 -0
  30. package/dist/src/rules/missing-alt-text.js.map +1 -0
  31. package/dist/src/rules/placeholder-alt-text.d.ts +3 -0
  32. package/dist/src/rules/placeholder-alt-text.d.ts.map +1 -0
  33. package/dist/src/rules/placeholder-alt-text.js +31 -0
  34. package/dist/src/rules/placeholder-alt-text.js.map +1 -0
  35. package/dist/src/rules/repeated-alt-text.d.ts +3 -0
  36. package/dist/src/rules/repeated-alt-text.d.ts.map +1 -0
  37. package/dist/src/rules/repeated-alt-text.js +67 -0
  38. package/dist/src/rules/repeated-alt-text.js.map +1 -0
  39. package/dist/src/rules/vague-alt-text.d.ts +3 -0
  40. package/dist/src/rules/vague-alt-text.d.ts.map +1 -0
  41. package/dist/src/rules/vague-alt-text.js +133 -0
  42. package/dist/src/rules/vague-alt-text.js.map +1 -0
  43. package/dist/src/types.d.ts +49 -0
  44. package/dist/src/types.d.ts.map +1 -0
  45. package/dist/src/types.js +3 -0
  46. package/dist/src/types.js.map +1 -0
  47. package/dist/src/utils/normalize-alt-text.d.ts +7 -0
  48. package/dist/src/utils/normalize-alt-text.d.ts.map +1 -0
  49. package/dist/src/utils/normalize-alt-text.js +15 -0
  50. package/dist/src/utils/normalize-alt-text.js.map +1 -0
  51. package/package.json +43 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright GitHub, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,259 @@
1
- # @github/accessibility-scanner-alt-text-plugin (stub)
1
+ # Alt-Text Plugin for the Accessibility Scanner
2
2
 
3
- Stub package published to reserve the name.
3
+ The Alt-Text Plugin is a [plugin](https://github.com/github/accessibility-scanner/blob/main/PLUGINS.md) for the [AI-powered Accessibility Scanner](https://github.com/github/accessibility-scanner) that flags low-quality `alt` text on images. It complements axe-core's built-in `image-alt` rule, helping teams ship images with descriptive, screen-reader-friendly alternative text.
4
+
5
+ The plugin helps teams:
6
+
7
+ - 🖼️ Catch vague, generic, or filler `alt` text (`"image"`, `"photo"`, `"icon"`)
8
+ - 📛 Catch raw filenames used as `alt` text (`"IMG_1234.png"`)
9
+ - 🔁 Catch runs of adjacent images that share the same `alt` text
10
+ - 🚧 Catch boilerplate placeholder `alt` text (`"todo"`, `"tbd"`, `"fixme"`)
11
+ - ♿ Catch images missing an `alt` attribute entirely (without flagging intentional decorative `alt=""`)
12
+
13
+ > ⚠️ **Note:** This plugin is in active development alongside the a11y scanner's public preview. New rules are still being added and end-to-end integration with the scanner's issue-filing workflow is still maturing. Always review filed issues before acting on them.
14
+
15
+ ---
16
+
17
+ ## [Frequently-Asked Questions (FAQ)](https://github.com/github/accessibility-scanner/blob/main/FAQ.md)
18
+
19
+ The plugin inherits the a11y scanner's general FAQ — see the link above for questions about scanning, caching, GitHub Enterprise, and more. Plugin-specific questions are answered inline below.
20
+
21
+ ---
22
+
23
+ ## Background
24
+
25
+ This plugin catches low-quality `alt` text that axe-core's built-in [`image-alt`](https://dequeuniversity.com/rules/axe/4.10/image-alt) rule cannot — vague single-word `alt`, raw filenames, runs of duplicate `alt`, and never-filled-in placeholders. The scope is intentionally narrow: deterministic, heuristic checks on `<img>` elements only. Non-`<img>` `role="img"` elements, decorative `alt=""`, and hidden subtrees are filtered out before any rule runs.
26
+
27
+ The project is under active development alongside the scanner's public preview. Roadmap and open work live in this repo's [Issues](https://github.com/github/accessibility-scanner-alt-text-plugin/issues). See [CONTRIBUTING.md](./CONTRIBUTING.md) for how to contribute, including local setup, expected checks, and PR conventions.
28
+
29
+ ---
30
+
31
+ ## Requirements
32
+
33
+ To use the Alt-Text Plugin, you'll need:
34
+
35
+ - **The [AI-powered Accessibility Scanner](https://github.com/github/accessibility-scanner)** (v3 or later) wired into a GitHub Actions workflow in your repository
36
+ - **Everything required to run the scanner itself** (Actions enabled, Issues enabled, a `GH_TOKEN` PAT — see the [scanner README](https://github.com/github/accessibility-scanner#requirements) for the full list)
37
+
38
+ The plugin is published to npm as [`@github/accessibility-scanner-alt-text-plugin`](https://www.npmjs.com/package/@github/accessibility-scanner-alt-text-plugin). The scanner installs it for you when running the `Find` sub-action — you don't need to copy any source into your repository or run `npm install` yourself.
39
+
40
+ To develop the plugin locally, you'll also need:
41
+
42
+ - **Node.js** matching the `engines` field in [`package.json`](./package.json) — currently `^22.13.0 || ^24 || ^26`
43
+ - **npm** (ships with Node)
44
+
45
+ ---
46
+
47
+ ## Getting started
48
+
49
+ ### 1. Enable the plugin in your workflow
50
+
51
+ The plugin is loaded from its npm package — there's nothing to copy into your repo. Add it to the scanner action's `scans` input as an **object** with `name`, `package`, and (recommended) a pinned `version`. Keep `"axe"` in the list too, since the scanner only runs Axe by default:
52
+
53
+ ```yaml
54
+ name: Accessibility Scanner
55
+ on: workflow_dispatch
56
+
57
+ jobs:
58
+ accessibility_scanner:
59
+ runs-on: ubuntu-latest
60
+ steps:
61
+ - uses: github/accessibility-scanner@v3
62
+ with:
63
+ urls: |
64
+ https://example.com
65
+ repository: REPLACE_THIS/REPLACE_THIS
66
+ token: ${{ secrets.GH_TOKEN }}
67
+ cache_key: REPLACE_THIS
68
+ scans: |
69
+ ["axe", {"name": "alt-text-scan", "package": "@github/accessibility-scanner-alt-text-plugin", "version": "1.0.0"}]
70
+ ```
71
+
72
+ > 👉 Update all `REPLACE_THIS` placeholders with your actual values. See the [scanner's Action inputs](https://github.com/github/accessibility-scanner#action-inputs) for the full list, and the scanner's [PLUGINS.md](https://github.com/github/accessibility-scanner/blob/main/PLUGINS.md) for how NPM plugins are loaded.
73
+
74
+ 📚 Learn more
75
+
76
+ - [Plugin docs in the scanner repository](https://github.com/github/accessibility-scanner/blob/main/PLUGINS.md)
77
+ - [Scanner getting-started guide](https://github.com/github/accessibility-scanner#getting-started)
78
+ - [Writing workflows](https://docs.github.com/en/actions/how-tos/write-workflows)
79
+
80
+ ---
81
+
82
+ ### 2. Run your first scan
83
+
84
+ Trigger your scanner workflow manually or on its configured schedule. The plugin runs on every URL the scanner visits, extracts each image exposed to assistive technology, and emits a finding for every rule violation. The scanner then turns those findings into GitHub issues.
85
+
86
+ 📚 Learn more
87
+
88
+ - [View workflow run history](https://docs.github.com/en/actions/how-tos/monitor-workflows/view-workflow-run-history)
89
+ - [Running a workflow manually](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/manually-run-a-workflow#running-a-workflow)
90
+
91
+ ---
92
+
93
+ ## Rules
94
+
95
+ The plugin runs every extracted image through an append-only registry of rules. Each rule returns a finding when an image fails its criteria, and the scanner turns each finding into an issue.
96
+
97
+ | Rule | ID | Fires when | Example (flagged) |
98
+ | ------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
99
+ | **Missing alt** | `missing-alt-text` | The `alt` attribute is absent (`null`) or whitespace-only (`" "`). `alt=""` is treated as intentional decorative use and is **not** flagged. | `<img src="cat.png">`<br>`<img src="cat.png" alt=" ">` |
100
+ | **Vague alt** | `vague-alt-text` | The alt text is one of a curated set of generic single words (`image`, `photo`, `icon`, `logo`, `screenshot`, `chart`, `untitled`, etc.) or short filler phrases (`an image of`, `a photo of`). Normalization is applied before matching: case-insensitive, whitespace-collapsed, surrounding punctuation stripped. | `<img alt="image">`<br>`<img alt="An image of">`<br>`<img alt="PHOTO.">` |
101
+ | **Filename as alt** | `filename-alt-text` | The alt text ends in a common image file extension (`.png`, `.jpg`, `.jpeg`, `.gif`, `.svg`, `.webp`, `.bmp`, `.ico`). | `<img alt="IMG_1234.png">`<br>`<img alt="Screenshot 2024-04-28.jpg">` |
102
+ | **Repeated alt** | `repeated-alt-text` | Two or more adjacent images on the rendered page share the same normalized alt text. Useful for patterns like five star icons all labeled `"3/5 stars"`. | Five consecutive `<img alt="3/5 stars">` elements |
103
+ | **Placeholder alt** | `placeholder-alt-text` | The alt text matches a known boilerplate string that signals it was never written (`todo`, `tbd`, `fixme`, `placeholder`, `alt text`, `insert alt text`, `image alt`). Normalization is applied before matching. | `<img alt="TODO">`<br>`<img alt="insert alt text">` |
104
+
105
+ ### Image extraction
106
+
107
+ Before rules run, the plugin extracts images from the page through Playwright's accessibility tree (`page.getByRole('img')`) and narrows the result to actual `<img>` elements. The following are filtered out automatically and never reach the rules:
108
+
109
+ - Non-`<img>` elements with `role="img"` (e.g. `<svg role="img">`, `<div role="img">`) — this plugin's rules only apply to HTML `<img>` tags
110
+ - Images inside `aria-hidden="true"` subtrees
111
+ - Images inside `display: none` or `visibility: hidden` subtrees
112
+ - Decorative images with `alt=""` (implicit `role="presentation"`)
113
+
114
+ ### Overlap with Axe
115
+
116
+ The scanner's built-in Axe scan includes a rule called [`image-alt`](https://dequeuniversity.com/rules/axe/4.10/image-alt) that catches missing and whitespace-only `alt` attributes. If you have both `"axe"` and `"alt-text-scan"` enabled, the same image may be flagged by both. The other four rules in this plugin (`vague-alt-text`, `filename-alt-text`, `repeated-alt-text`, `placeholder-alt-text`) are unique to the plugin and don't overlap with Axe.
117
+
118
+ ---
119
+
120
+ ## Output
121
+
122
+ When a rule fires, the plugin emits a finding with the following shape (matching the scanner's [`Finding` type](https://github.com/github/accessibility-scanner/blob/main/.github/actions/find/src/types.d.ts)):
123
+
124
+ - `scannerType` — always `'alt-text-scan'`; identifies which plugin produced the finding
125
+ - `ruleId` — the ID of the rule that fired (e.g. `'vague-alt-text'`)
126
+ - `url` — the page URL where the image was found
127
+ - `html` — the offending `<img>` element's outer HTML
128
+ - `problemShort` — one-sentence description of what's wrong, including the offending alt text where applicable
129
+ - `problemUrl` — link to the relevant WCAG technique or W3C tutorial
130
+ - `solutionShort` — one-sentence description of how to fix it
131
+ - `solutionLong` — optional longer explanation when one sentence isn't enough
132
+
133
+ The scanner uses these fields to file or update a GitHub issue.
134
+
135
+ ---
136
+
137
+ ## Configuration
138
+
139
+ To override the default enabled state of one or more rules, add a `config.json` file in your scanner repository at `.github/scanner-plugins/alt-text-scan/config.json`:
140
+
141
+ ```text
142
+ .github/scanner-plugins/alt-text-scan/
143
+ └── config.json ← optional
144
+ ```
145
+
146
+ ```json
147
+ {
148
+ "rules": {
149
+ "repeated-alt-text": false,
150
+ "placeholder-alt-text": false
151
+ }
152
+ }
153
+ ```
154
+
155
+ - Each key under `rules` is a rule ID from the [Rules](#rules) table above; the value is `true` (run the rule) or `false` (skip it).
156
+ - Rules you don't list keep their default behavior. Today every rule defaults to enabled.
157
+ - Unknown rule IDs and non-boolean values are logged as warnings and ignored (typo guard).
158
+ - A missing or malformed `config.json` causes the plugin to run with all defaults.
159
+ - The plugin reads the config once at startup, not per URL.
160
+
161
+ A JSON Schema is published at [`schema/config.schema.json`](./schema/config.schema.json). Add a `$schema` line at the top of your `config.json` to get autocomplete, hover docs, and inline validation in editors that support JSON Schema (VS Code, JetBrains IDEs, etc.):
162
+
163
+ ```json
164
+ {
165
+ "$schema": "https://raw.githubusercontent.com/github/accessibility-scanner-alt-text-plugin/main/schema/config.schema.json",
166
+ "rules": {
167
+ "repeated-alt-text": false
168
+ }
169
+ }
170
+ ```
171
+
172
+ The `$schema` line is optional and is ignored by the plugin at runtime.
173
+
174
+ ---
175
+
176
+ ## Development
177
+
178
+ ### Local setup
179
+
180
+ ```sh
181
+ git clone https://github.com/github/accessibility-scanner-alt-text-plugin.git
182
+ cd accessibility-scanner-alt-text-plugin
183
+ npm ci
184
+ ```
185
+
186
+ ### Common scripts
187
+
188
+ | Script | What it does |
189
+ | ---------------------- | --------------------------------------------- |
190
+ | `npm run test` | Runs the Vitest unit suite once |
191
+ | `npm run test:watch` | Runs Vitest in watch mode |
192
+ | `npm run typecheck` | Runs `tsc --noEmit` against the whole project |
193
+ | `npm run lint` | Runs ESLint |
194
+ | `npm run format` | Rewrites files with Prettier |
195
+ | `npm run format:check` | Reports formatting violations without writing |
196
+
197
+ Pull requests trigger two CI workflows: [`lint.yml`](./.github/workflows/lint.yml) runs `lint` and `format:check` on Node 24, and [`test.yml`](./.github/workflows/test.yml) runs `typecheck` and `test` across Node 22, 24, and 26.
198
+
199
+ ### Project layout
200
+
201
+ ```text
202
+ index.ts # Plugin entry point: exports `name` and the default scan function
203
+ src/
204
+ config.ts # Loads & validates .github/scanner-plugins/alt-text-scan/config.json
205
+ extract.ts # Pulls visible <img> records from a Playwright page
206
+ findings.ts # Translates each RuleResult into the scanner's Finding shape
207
+ rules/
208
+ index.ts # Append-only rule registry
209
+ missing-alt-text.ts
210
+ vague-alt-text.ts
211
+ filename-alt-text.ts
212
+ placeholder-alt-text.ts
213
+ repeated-alt-text.ts
214
+ utils/
215
+ normalize-alt-text.ts # Lowercase, trim, collapse whitespace, strip punctuation
216
+ types.ts # Rule, RuleContext, RuleResult, ImageRecord, Finding
217
+ tests/
218
+ extract.test.ts # Playwright-driven tests for the image extractor
219
+ example-site.test.ts # Runs the plugin against the example/site-with-errors fixture
220
+ fixtures/ # Static HTML fixtures used by the extractor tests
221
+ unit/
222
+ *.test.ts # One file per rule, plus config.test.ts for the loader
223
+ utils/
224
+ helpers.ts # makeImage() and evaluateAlts() — shared across rule tests
225
+ ```
226
+
227
+ ### Adding a new rule
228
+
229
+ 1. Create `src/rules/<rule-name>.ts` exporting a `Rule` (see [`src/types.ts`](./src/types.ts) for the shape — `id`, `problemUrl`, and `evaluate(context)`). Filenames under `src/` and `tests/` must be kebab-case; ESLint's `check-file/filename-naming-convention` rule enforces this.
230
+ 2. Import the rule in [`src/rules/index.ts`](./src/rules/index.ts) and append it to `allRules`. The registry is append-only — don't reorder existing rules.
231
+ 3. Add a `tests/unit/<rule-name>.test.ts` file. Use `evaluateAlts(alts, rule)` and `makeImage(overrides)` from [`tests/utils/helpers.ts`](./tests/utils/helpers.ts) for the common cases; construct an explicit `RuleContext` only when you need control over `src` or other per-image fields.
232
+ 4. Run `npm run test && npm run typecheck && npm run lint` locally before opening a PR. CI re-runs them.
233
+
234
+ > [!IMPORTANT]
235
+ > Image extraction happens once per page, before any rule runs, so every rule sees the same filtered list regardless of which rules are enabled. Don't reach into the DOM from a rule — work from the `ImageRecord[]` the rule's context provides.
236
+
237
+ ---
238
+
239
+ ## Feedback
240
+
241
+ 💬 We welcome your feedback! To submit feedback or report issues, please open an issue in this repository. For broader feedback on the a11y scanner itself, file it in the [scanner repository](https://github.com/github/accessibility-scanner/issues).
242
+
243
+ ---
244
+
245
+ ## License
246
+
247
+ 📄 This project is licensed under the terms of the MIT open source license. See the [LICENSE](./LICENSE) file for the full terms.
248
+
249
+ ## Maintainers
250
+
251
+ 🔧 Maintained alongside the [AI-powered Accessibility Scanner](https://github.com/github/accessibility-scanner). See [CODEOWNERS](./.github/CODEOWNERS) for the responsible team.
252
+
253
+ ## Support
254
+
255
+ ❓ For support, please open an issue in this repository. See [SUPPORT.md](./SUPPORT.md) for support expectations, or the scanner's [SUPPORT](https://github.com/github/accessibility-scanner/blob/main/SUPPORT.md) document for guidance that applies across the project.
256
+
257
+ ## Acknowledgement
258
+
259
+ ✨ Built on top of [Playwright](https://playwright.dev/), [Vitest](https://vitest.dev/), and the broader open-source accessibility tooling ecosystem. Thank you to everyone contributing rules, tests, and review.
@@ -0,0 +1,4 @@
1
+ import type { PluginArgs } from './src/types.js';
2
+ export declare const name = "alt-text-scan";
3
+ export default function altTextScan({ page, addFinding }: PluginArgs): Promise<void>;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,gBAAgB,CAAA;AAE9C,eAAO,MAAM,IAAI,kBAAkB,CAAA;AAOnC,wBAA8B,WAAW,CAAC,EAAC,IAAI,EAAE,UAAU,EAAC,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BvF"}
package/dist/index.js ADDED
@@ -0,0 +1,40 @@
1
+ import { join } from 'node:path';
2
+ import { extractImages } from './src/extract.js';
3
+ import { allRules } from './src/rules/index.js';
4
+ import { emitFindings } from './src/findings.js';
5
+ import { loadConfig } from './src/config.js';
6
+ export const name = 'alt-text-scan';
7
+ // Config lives in the consumer's repo at `.github/scanner-plugins/<name>/config.json`.
8
+ const configPath = join(process.cwd(), '.github', 'scanner-plugins', name, 'config.json');
9
+ const knownRuleIds = new Set(allRules.map(r => r.id));
10
+ const configPromise = loadConfig(configPath, knownRuleIds);
11
+ export default async function altTextScan({ page, addFinding }) {
12
+ const url = page.url();
13
+ const { ruleOverrides } = await configPromise;
14
+ const enabledRules = allRules.filter(rule => ruleOverrides.get(rule.id) ?? rule.defaultEnabled ?? true);
15
+ // Extract images from the page.
16
+ let images;
17
+ try {
18
+ images = await extractImages(page);
19
+ }
20
+ catch (err) {
21
+ console.error(`[alt-text-scan] failed to extract images from ${url}:`, err);
22
+ return;
23
+ }
24
+ if (images.length === 0)
25
+ return;
26
+ const ctx = { url, images };
27
+ // Enforce checks on each image against each rule.
28
+ for (const rule of enabledRules) {
29
+ let results;
30
+ try {
31
+ results = rule.evaluate(ctx);
32
+ }
33
+ catch (err) {
34
+ console.error(`[alt-text-scan] rule "${rule.id}" threw on ${url}:`, err);
35
+ continue;
36
+ }
37
+ await emitFindings(rule, results, url, addFinding);
38
+ }
39
+ }
40
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,IAAI,EAAC,MAAM,WAAW,CAAA;AAC9B,OAAO,EAAC,aAAa,EAAC,MAAM,kBAAkB,CAAA;AAC9C,OAAO,EAAC,QAAQ,EAAC,MAAM,sBAAsB,CAAA;AAC7C,OAAO,EAAC,YAAY,EAAC,MAAM,mBAAmB,CAAA;AAC9C,OAAO,EAAC,UAAU,EAAC,MAAM,iBAAiB,CAAA;AAG1C,MAAM,CAAC,MAAM,IAAI,GAAG,eAAe,CAAA;AAEnC,uFAAuF;AACvF,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,iBAAiB,EAAE,IAAI,EAAE,aAAa,CAAC,CAAA;AACzF,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;AACrD,MAAM,aAAa,GAAG,UAAU,CAAC,UAAU,EAAE,YAAY,CAAC,CAAA;AAE1D,MAAM,CAAC,OAAO,CAAC,KAAK,UAAU,WAAW,CAAC,EAAC,IAAI,EAAE,UAAU,EAAa;IACtE,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAA;IAEtB,MAAM,EAAC,aAAa,EAAC,GAAG,MAAM,aAAa,CAAA;IAC3C,MAAM,YAAY,GAAG,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,IAAI,CAAC,cAAc,IAAI,IAAI,CAAC,CAAA;IAEvG,gCAAgC;IAChC,IAAI,MAAM,CAAA;IACV,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,aAAa,CAAC,IAAI,CAAC,CAAA;IACpC,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,iDAAiD,GAAG,GAAG,EAAE,GAAG,CAAC,CAAA;QAC3E,OAAM;IACR,CAAC;IAED,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAM;IAE/B,MAAM,GAAG,GAAG,EAAC,GAAG,EAAE,MAAM,EAAC,CAAA;IAEzB,kDAAkD;IAClD,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;QAChC,IAAI,OAAO,CAAA;QACX,IAAI,CAAC;YACH,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAA;QAC9B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,yBAAyB,IAAI,CAAC,EAAE,cAAc,GAAG,GAAG,EAAE,GAAG,CAAC,CAAA;YACxE,SAAQ;QACV,CAAC;QACD,MAAM,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,CAAC,CAAA;IACpD,CAAC;AACH,CAAC"}
@@ -0,0 +1,5 @@
1
+ export type PluginConfig = {
2
+ ruleOverrides: ReadonlyMap<string, boolean>;
3
+ };
4
+ export declare function loadConfig(configPath: string, knownRuleIds: ReadonlySet<string>): Promise<PluginConfig>;
5
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,YAAY,GAAG;IACzB,aAAa,EAAE,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC5C,CAAA;AAID,wBAAsB,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,YAAY,CAAC,CAqB7G"}
@@ -0,0 +1,45 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ const emptyConfig = () => ({ ruleOverrides: new Map() });
3
+ export async function loadConfig(configPath, knownRuleIds) {
4
+ let raw;
5
+ try {
6
+ raw = await readFile(configPath, 'utf8');
7
+ }
8
+ catch (err) {
9
+ if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
10
+ return emptyConfig();
11
+ }
12
+ console.warn(`[alt-text-scan] failed to read ${configPath}; running with default rule settings:`, err);
13
+ return emptyConfig();
14
+ }
15
+ let parsed;
16
+ try {
17
+ parsed = JSON.parse(raw);
18
+ }
19
+ catch (err) {
20
+ console.error(`[alt-text-scan] failed to parse ${configPath}; running with default rule settings:`, err);
21
+ return emptyConfig();
22
+ }
23
+ return { ruleOverrides: collectRuleOverrides(parsed, knownRuleIds) };
24
+ }
25
+ function collectRuleOverrides(parsed, knownRuleIds) {
26
+ const result = new Map();
27
+ if (!parsed || typeof parsed !== 'object')
28
+ return result;
29
+ const rules = parsed.rules;
30
+ if (!rules || typeof rules !== 'object' || Array.isArray(rules))
31
+ return result;
32
+ for (const [id, value] of Object.entries(rules)) {
33
+ if (!knownRuleIds.has(id)) {
34
+ console.warn(`[alt-text-scan] unknown rule id "${id}" in config; ignoring. Known ids: ${[...knownRuleIds].join(', ')}`);
35
+ continue;
36
+ }
37
+ if (typeof value !== 'boolean') {
38
+ console.warn(`[alt-text-scan] non-boolean value for rule "${id}" in config (got ${typeof value}); ignoring.`);
39
+ continue;
40
+ }
41
+ result.set(id, value);
42
+ }
43
+ return result;
44
+ }
45
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,QAAQ,EAAC,MAAM,kBAAkB,CAAA;AAMzC,MAAM,WAAW,GAAG,GAAiB,EAAE,CAAC,CAAC,EAAC,aAAa,EAAE,IAAI,GAAG,EAAE,EAAC,CAAC,CAAA;AAEpE,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,UAAkB,EAAE,YAAiC;IACpF,IAAI,GAAW,CAAA;IACf,IAAI,CAAC;QACH,GAAG,GAAG,MAAM,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,CAAA;IAC1C,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,KAAK,IAAI,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;YACnE,OAAO,WAAW,EAAE,CAAA;QACtB,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,kCAAkC,UAAU,uCAAuC,EAAE,GAAG,CAAC,CAAA;QACtG,OAAO,WAAW,EAAE,CAAA;IACtB,CAAC;IAED,IAAI,MAAe,CAAA;IACnB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAC1B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,mCAAmC,UAAU,uCAAuC,EAAE,GAAG,CAAC,CAAA;QACxG,OAAO,WAAW,EAAE,CAAA;IACtB,CAAC;IAED,OAAO,EAAC,aAAa,EAAE,oBAAoB,CAAC,MAAM,EAAE,YAAY,CAAC,EAAC,CAAA;AACpE,CAAC;AAED,SAAS,oBAAoB,CAAC,MAAe,EAAE,YAAiC;IAC9E,MAAM,MAAM,GAAG,IAAI,GAAG,EAAmB,CAAA;IACzC,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAA;IAExD,MAAM,KAAK,GAAI,MAA4B,CAAC,KAAK,CAAA;IACjD,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,MAAM,CAAA;IAE9E,KAAK,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC,EAAE,CAAC;QAC3E,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;YAC1B,OAAO,CAAC,IAAI,CACV,oCAAoC,EAAE,qCAAqC,CAAC,GAAG,YAAY,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAC1G,CAAA;YACD,SAAQ;QACV,CAAC;QACD,IAAI,OAAO,KAAK,KAAK,SAAS,EAAE,CAAC;YAC/B,OAAO,CAAC,IAAI,CAAC,+CAA+C,EAAE,oBAAoB,OAAO,KAAK,cAAc,CAAC,CAAA;YAC7G,SAAQ;QACV,CAAC;QACD,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,KAAK,CAAC,CAAA;IACvB,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { Page } from 'playwright';
2
+ import type { ImageRecord } from './types.js';
3
+ export declare function extractImages(page: Page): Promise<ImageRecord[]>;
4
+ //# sourceMappingURL=extract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract.d.ts","sourceRoot":"","sources":["../../src/extract.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,YAAY,CAAA;AACpC,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,YAAY,CAAA;AAI3C,wBAAsB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC,CAqBtE"}
@@ -0,0 +1,22 @@
1
+ // Returns one ImageRecord per HTML <img> element that is exposed in the accessibility tree.
2
+ // Using getByRole('img') filters out elements that assistive tech cannot perceive.
3
+ export async function extractImages(page) {
4
+ return page.getByRole('img').evaluateAll(els => els
5
+ // getByRole('img') also matches SVG/div with role="img", so filter those out.
6
+ .filter(el => el.tagName === 'IMG')
7
+ .map(el => {
8
+ const rect = el.getBoundingClientRect();
9
+ const boundingBox = rect.width === 0 && rect.height === 0 ? null : { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
10
+ return {
11
+ src: el.getAttribute('src'),
12
+ alt: el.getAttribute('alt'),
13
+ role: el.getAttribute('role'),
14
+ ariaHidden: el.getAttribute('aria-hidden') === 'true',
15
+ ariaLabel: el.getAttribute('aria-label'),
16
+ ariaLabelledBy: el.getAttribute('aria-labelledby'),
17
+ outerHTML: el.outerHTML,
18
+ boundingBox,
19
+ };
20
+ }));
21
+ }
22
+ //# sourceMappingURL=extract.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract.js","sourceRoot":"","sources":["../../src/extract.ts"],"names":[],"mappings":"AAGA,4FAA4F;AAC5F,mFAAmF;AACnF,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAAU;IAC5C,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,CAC7C,GAAG;QACD,8EAA8E;SAC7E,MAAM,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,OAAO,KAAK,KAAK,CAAC;SAClC,GAAG,CAAC,EAAE,CAAC,EAAE;QACR,MAAM,IAAI,GAAG,EAAE,CAAC,qBAAqB,EAAE,CAAA;QACvC,MAAM,WAAW,GACf,IAAI,CAAC,KAAK,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAC,CAAC,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAC,CAAA;QAC/G,OAAO;YACL,GAAG,EAAE,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC;YAC3B,GAAG,EAAE,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC;YAC3B,IAAI,EAAE,EAAE,CAAC,YAAY,CAAC,MAAM,CAAC;YAC7B,UAAU,EAAE,EAAE,CAAC,YAAY,CAAC,aAAa,CAAC,KAAK,MAAM;YACrD,SAAS,EAAE,EAAE,CAAC,YAAY,CAAC,YAAY,CAAC;YACxC,cAAc,EAAE,EAAE,CAAC,YAAY,CAAC,iBAAiB,CAAC;YAClD,SAAS,EAAE,EAAE,CAAC,SAAS;YACvB,WAAW;SACZ,CAAA;IACH,CAAC,CAAC,CACL,CAAA;AACH,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { Finding, Rule, RuleResult } from './types.js';
2
+ export declare function emitFindings(rule: Rule, results: RuleResult[], url: string, addFinding: (finding: Finding) => Promise<void>): Promise<void>;
3
+ //# sourceMappingURL=findings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"findings.d.ts","sourceRoot":"","sources":["../../src/findings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,OAAO,EAAE,IAAI,EAAE,UAAU,EAAC,MAAM,YAAY,CAAA;AAGzD,wBAAsB,YAAY,CAChC,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,UAAU,EAAE,EACrB,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GAC9C,OAAO,CAAC,IAAI,CAAC,CAaf"}
@@ -0,0 +1,16 @@
1
+ // Translates each RuleResult into the scanner's Finding shape and records it.
2
+ export async function emitFindings(rule, results, url, addFinding) {
3
+ for (const result of results) {
4
+ await addFinding({
5
+ scannerType: 'alt-text-scan',
6
+ ruleId: rule.id,
7
+ url,
8
+ html: result.image.outerHTML,
9
+ problemShort: result.problemShort,
10
+ problemUrl: rule.problemUrl,
11
+ solutionShort: result.solutionShort,
12
+ solutionLong: result.solutionLong,
13
+ });
14
+ }
15
+ }
16
+ //# sourceMappingURL=findings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"findings.js","sourceRoot":"","sources":["../../src/findings.ts"],"names":[],"mappings":"AAEA,8EAA8E;AAC9E,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,IAAU,EACV,OAAqB,EACrB,GAAW,EACX,UAA+C;IAE/C,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,UAAU,CAAC;YACf,WAAW,EAAE,eAAe;YAC5B,MAAM,EAAE,IAAI,CAAC,EAAE;YACf,GAAG;YACH,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,SAAS;YAC5B,YAAY,EAAE,MAAM,CAAC,YAAY;YACjC,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,aAAa,EAAE,MAAM,CAAC,aAAa;YACnC,YAAY,EAAE,MAAM,CAAC,YAAY;SAClC,CAAC,CAAA;IACJ,CAAC;AACH,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { Rule } from '../types.js';
2
+ export declare const filenameAltText: Rule;
3
+ //# sourceMappingURL=filename-alt-text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filename-alt-text.d.ts","sourceRoot":"","sources":["../../../src/rules/filename-alt-text.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,IAAI,EAA0B,MAAM,aAAa,CAAA;AAI9D,eAAO,MAAM,eAAe,EAAE,IAmB7B,CAAA"}
@@ -0,0 +1,21 @@
1
+ const FILENAME_PATTERN = /\.(png|jpg|jpeg|gif|svg|webp|bmp|ico)$/i;
2
+ export const filenameAltText = {
3
+ id: 'filename-alt-text',
4
+ problemUrl: 'https://www.w3.org/WAI/tutorials/images/decision-tree/',
5
+ evaluate(context) {
6
+ const results = [];
7
+ for (const image of context.images) {
8
+ if (image.alt === null || image.alt === '')
9
+ continue;
10
+ if (!FILENAME_PATTERN.test(image.alt.trim()))
11
+ continue;
12
+ results.push({
13
+ image,
14
+ problemShort: `Image alt text appears to be a filename: "${image.alt}"`,
15
+ solutionShort: 'Replace the filename with a meaningful description of the image content.',
16
+ });
17
+ }
18
+ return results;
19
+ },
20
+ };
21
+ //# sourceMappingURL=filename-alt-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filename-alt-text.js","sourceRoot":"","sources":["../../../src/rules/filename-alt-text.ts"],"names":[],"mappings":"AAEA,MAAM,gBAAgB,GAAG,yCAAyC,CAAA;AAElE,MAAM,CAAC,MAAM,eAAe,GAAS;IACnC,EAAE,EAAE,mBAAmB;IACvB,UAAU,EAAE,wDAAwD;IACpE,QAAQ,CAAC,OAAoB;QAC3B,MAAM,OAAO,GAAiB,EAAE,CAAA;QAEhC,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YACnC,IAAI,KAAK,CAAC,GAAG,KAAK,IAAI,IAAI,KAAK,CAAC,GAAG,KAAK,EAAE;gBAAE,SAAQ;YACpD,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC;gBAAE,SAAQ;YAEtD,OAAO,CAAC,IAAI,CAAC;gBACX,KAAK;gBACL,YAAY,EAAE,6CAA6C,KAAK,CAAC,GAAG,GAAG;gBACvE,aAAa,EAAE,0EAA0E;aAC1F,CAAC,CAAA;QACJ,CAAC;QAED,OAAO,OAAO,CAAA;IAChB,CAAC;CACF,CAAA"}
@@ -0,0 +1,3 @@
1
+ import type { Rule } from '../types.js';
2
+ export declare const allRules: Rule[];
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/rules/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,aAAa,CAAA;AAQrC,eAAO,MAAM,QAAQ,EAAE,IAAI,EAAyF,CAAA"}
@@ -0,0 +1,8 @@
1
+ import { filenameAltText } from './filename-alt-text.js';
2
+ import { vagueAltText } from './vague-alt-text.js';
3
+ import { missingAltText } from './missing-alt-text.js';
4
+ import { placeholderAltText } from './placeholder-alt-text.js';
5
+ import { repeatedAltText } from './repeated-alt-text.js';
6
+ // Append-only registry. Add a rule by importing it here and pushing it onto the array.
7
+ export const allRules = [filenameAltText, vagueAltText, missingAltText, placeholderAltText, repeatedAltText];
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/rules/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,eAAe,EAAC,MAAM,wBAAwB,CAAA;AACtD,OAAO,EAAC,YAAY,EAAC,MAAM,qBAAqB,CAAA;AAChD,OAAO,EAAC,cAAc,EAAC,MAAM,uBAAuB,CAAA;AACpD,OAAO,EAAC,kBAAkB,EAAC,MAAM,2BAA2B,CAAA;AAC5D,OAAO,EAAC,eAAe,EAAC,MAAM,wBAAwB,CAAA;AAEtD,uFAAuF;AACvF,MAAM,CAAC,MAAM,QAAQ,GAAW,CAAC,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,kBAAkB,EAAE,eAAe,CAAC,CAAA"}
@@ -0,0 +1,3 @@
1
+ import type { Rule } from '../types.js';
2
+ export declare const missingAltText: Rule;
3
+ //# sourceMappingURL=missing-alt-text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"missing-alt-text.d.ts","sourceRoot":"","sources":["../../../src/rules/missing-alt-text.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,IAAI,EAA0B,MAAM,aAAa,CAAA;AAE9D,eAAO,MAAM,cAAc,EAAE,IAqB5B,CAAA"}
@@ -0,0 +1,22 @@
1
+ export const missingAltText = {
2
+ id: 'missing-alt-text',
3
+ problemUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/non-text-content.html',
4
+ evaluate(context) {
5
+ const results = [];
6
+ for (const image of context.images) {
7
+ // alt === '' is intentional to mark an image as decorative
8
+ if (image.alt === '')
9
+ continue;
10
+ // alt text has real content, so skip
11
+ if (image.alt !== null && image.alt.trim() !== '')
12
+ continue;
13
+ results.push({
14
+ image,
15
+ problemShort: 'Image has no usable alt text (attribute is missing or whitespace-only).',
16
+ solutionShort: 'Add an alt attribute describing the image, or alt="" if it is purely decorative.',
17
+ });
18
+ }
19
+ return results;
20
+ },
21
+ };
22
+ //# sourceMappingURL=missing-alt-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"missing-alt-text.js","sourceRoot":"","sources":["../../../src/rules/missing-alt-text.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,cAAc,GAAS;IAClC,EAAE,EAAE,kBAAkB;IACtB,UAAU,EAAE,mEAAmE;IAC/E,QAAQ,CAAC,OAAoB;QAC3B,MAAM,OAAO,GAAiB,EAAE,CAAA;QAEhC,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YACnC,2DAA2D;YAC3D,IAAI,KAAK,CAAC,GAAG,KAAK,EAAE;gBAAE,SAAQ;YAC9B,qCAAqC;YACrC,IAAI,KAAK,CAAC,GAAG,KAAK,IAAI,IAAI,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE;gBAAE,SAAQ;YAE3D,OAAO,CAAC,IAAI,CAAC;gBACX,KAAK;gBACL,YAAY,EAAE,yEAAyE;gBACvF,aAAa,EAAE,kFAAkF;aAClG,CAAC,CAAA;QACJ,CAAC;QAED,OAAO,OAAO,CAAA;IAChB,CAAC;CACF,CAAA"}
@@ -0,0 +1,3 @@
1
+ import type { Rule } from '../types.js';
2
+ export declare const placeholderAltText: Rule;
3
+ //# sourceMappingURL=placeholder-alt-text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"placeholder-alt-text.d.ts","sourceRoot":"","sources":["../../../src/rules/placeholder-alt-text.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,IAAI,EAA0B,MAAM,aAAa,CAAA;AAc9D,eAAO,MAAM,kBAAkB,EAAE,IAmBhC,CAAA"}
@@ -0,0 +1,31 @@
1
+ import { normalizeAltText } from '../utils/normalize-alt-text.js';
2
+ // Known placeholder/boilerplate strings that signal the alt text was never written.
3
+ const PLACEHOLDER_ALT_TEXT = new Set([
4
+ 'todo',
5
+ 'tbd',
6
+ 'placeholder',
7
+ 'alt text',
8
+ 'insert alt text',
9
+ 'image alt',
10
+ 'fixme',
11
+ ]);
12
+ export const placeholderAltText = {
13
+ id: 'placeholder-alt-text',
14
+ problemUrl: 'https://www.w3.org/WAI/tutorials/images/decision-tree/',
15
+ evaluate(context) {
16
+ const results = [];
17
+ for (const image of context.images) {
18
+ if (image.alt === null || image.alt === '')
19
+ continue;
20
+ if (!PLACEHOLDER_ALT_TEXT.has(normalizeAltText(image.alt)))
21
+ continue;
22
+ results.push({
23
+ image,
24
+ problemShort: `Image alt text is placeholder text: "${image.alt}"`,
25
+ solutionShort: 'Replace the placeholder with a meaningful description of the image content.',
26
+ });
27
+ }
28
+ return results;
29
+ },
30
+ };
31
+ //# sourceMappingURL=placeholder-alt-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"placeholder-alt-text.js","sourceRoot":"","sources":["../../../src/rules/placeholder-alt-text.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,gBAAgB,EAAC,MAAM,gCAAgC,CAAA;AAE/D,oFAAoF;AACpF,MAAM,oBAAoB,GAAG,IAAI,GAAG,CAAC;IACnC,MAAM;IACN,KAAK;IACL,aAAa;IACb,UAAU;IACV,iBAAiB;IACjB,WAAW;IACX,OAAO;CACR,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAS;IACtC,EAAE,EAAE,sBAAsB;IAC1B,UAAU,EAAE,wDAAwD;IACpE,QAAQ,CAAC,OAAoB;QAC3B,MAAM,OAAO,GAAiB,EAAE,CAAA;QAEhC,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YACnC,IAAI,KAAK,CAAC,GAAG,KAAK,IAAI,IAAI,KAAK,CAAC,GAAG,KAAK,EAAE;gBAAE,SAAQ;YACpD,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC,gBAAgB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;gBAAE,SAAQ;YAEpE,OAAO,CAAC,IAAI,CAAC;gBACX,KAAK;gBACL,YAAY,EAAE,wCAAwC,KAAK,CAAC,GAAG,GAAG;gBAClE,aAAa,EAAE,6EAA6E;aAC7F,CAAC,CAAA;QACJ,CAAC;QAED,OAAO,OAAO,CAAA;IAChB,CAAC;CACF,CAAA"}
@@ -0,0 +1,3 @@
1
+ import type { Rule } from '../types.js';
2
+ export declare const repeatedAltText: Rule;
3
+ //# sourceMappingURL=repeated-alt-text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repeated-alt-text.d.ts","sourceRoot":"","sources":["../../../src/rules/repeated-alt-text.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAc,IAAI,EAAa,MAAM,aAAa,CAAA;AA+B9D,eAAO,MAAM,eAAe,EAAE,IA4C7B,CAAA"}
@@ -0,0 +1,67 @@
1
+ import { normalizeAltText } from '../utils/normalize-alt-text.js';
2
+ // Minimum number of consecutive images sharing the same alt before the run is flagged.
3
+ const MIN_RUN_LENGTH = 2;
4
+ // Two adjacent images extend a "run" only if the gap between their bounding boxes is at
5
+ // most this multiple of the larger box's longest dimension.
6
+ const GAP_MULTIPLIER = 3;
7
+ // Returns true when two images are far enough apart on screen that a user
8
+ // would not perceive them as part of the same repeating group. Fails open
9
+ // when either image lacks a measurable layout box: with no spatial data we
10
+ // fall back to the pre-spatial behavior rather than silently dropping a run.
11
+ function tooFarApart(a, b) {
12
+ if (!a.boundingBox || !b.boundingBox)
13
+ return false;
14
+ const aRight = a.boundingBox.x + a.boundingBox.width;
15
+ const aBottom = a.boundingBox.y + a.boundingBox.height;
16
+ const bRight = b.boundingBox.x + b.boundingBox.width;
17
+ const bBottom = b.boundingBox.y + b.boundingBox.height;
18
+ const horizontalGap = Math.max(0, Math.max(a.boundingBox.x, b.boundingBox.x) - Math.min(aRight, bRight));
19
+ const verticalGap = Math.max(0, Math.max(a.boundingBox.y, b.boundingBox.y) - Math.min(aBottom, bBottom));
20
+ const gap = Math.max(horizontalGap, verticalGap);
21
+ const largerDim = Math.max(a.boundingBox.width, a.boundingBox.height, b.boundingBox.width, b.boundingBox.height);
22
+ return gap > GAP_MULTIPLIER * largerDim;
23
+ }
24
+ export const repeatedAltText = {
25
+ id: 'repeated-alt-text',
26
+ problemUrl: 'https://www.w3.org/WAI/tutorials/images/groups/',
27
+ evaluate(context) {
28
+ const findings = [];
29
+ const { images } = context;
30
+ let i = 0;
31
+ while (i < images.length) {
32
+ // Normalize once and don't consider empty alt texts in run length
33
+ const start = images[i].alt;
34
+ const currAlt = start === null ? '' : normalizeAltText(start);
35
+ if (currAlt === '') {
36
+ i++;
37
+ continue;
38
+ }
39
+ // Detect length of consecutive alt texts
40
+ let j = i + 1;
41
+ while (j < images.length) {
42
+ const next = images[j].alt;
43
+ const nextAlt = next === null ? '' : normalizeAltText(next);
44
+ if (nextAlt !== currAlt)
45
+ break;
46
+ if (tooFarApart(images[j - 1], images[j]))
47
+ break;
48
+ j++;
49
+ }
50
+ const runLength = j - i;
51
+ if (runLength >= MIN_RUN_LENGTH) {
52
+ // Flag every image in the run except the first
53
+ for (let k = i + 1; k < j; k++) {
54
+ const img = images[k];
55
+ findings.push({
56
+ image: img,
57
+ problemShort: `Alt text is repeated across ${runLength} consecutive images:\n"${img.alt}"`,
58
+ solutionShort: 'If these images form one visual group, describe the group once and mark repeated/decorative images with `alt=""`. Otherwise, give each image unique alt text.',
59
+ });
60
+ }
61
+ }
62
+ i = j;
63
+ }
64
+ return findings;
65
+ },
66
+ };
67
+ //# sourceMappingURL=repeated-alt-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repeated-alt-text.js","sourceRoot":"","sources":["../../../src/rules/repeated-alt-text.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { Rule } from '../types.js';
2
+ export declare const vagueAltText: Rule;
3
+ //# sourceMappingURL=vague-alt-text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vague-alt-text.d.ts","sourceRoot":"","sources":["../../../src/rules/vague-alt-text.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,IAAI,EAAa,MAAM,aAAa,CAAA;AA0HjD,eAAO,MAAM,YAAY,EAAE,IAsB1B,CAAA"}
@@ -0,0 +1,133 @@
1
+ import { normalizeAltText } from '../utils/normalize-alt-text.js';
2
+ // Set of words that by themselves, are too vague to be useful alt text
3
+ const VAGUE_WORDS = new Set([
4
+ // Generic media terms
5
+ 'image',
6
+ 'images',
7
+ 'img',
8
+ 'photo',
9
+ 'photos',
10
+ 'photograph',
11
+ 'photographs',
12
+ 'picture',
13
+ 'pictures',
14
+ 'pic',
15
+ 'pics',
16
+ 'graphic',
17
+ 'graphics',
18
+ 'visual',
19
+ 'visuals',
20
+ 'media',
21
+ 'clipart',
22
+ 'gif',
23
+ 'gifs',
24
+ // Common UI/image labels
25
+ 'icon',
26
+ 'icons',
27
+ 'illustration',
28
+ 'illustrations',
29
+ 'banner',
30
+ 'banners',
31
+ 'thumbnail',
32
+ 'thumbnails',
33
+ 'logo',
34
+ 'logos',
35
+ 'avatar',
36
+ 'avatars',
37
+ // Very generic descriptors
38
+ 'art',
39
+ 'artwork',
40
+ 'drawing',
41
+ 'drawings',
42
+ 'diagram',
43
+ 'diagrams',
44
+ 'chart',
45
+ 'charts',
46
+ 'graph',
47
+ 'graphs',
48
+ 'screenshot',
49
+ 'screenshots',
50
+ 'figure',
51
+ 'figures',
52
+ 'painting',
53
+ 'paintings',
54
+ 'map',
55
+ 'maps',
56
+ // Placeholders
57
+ // Note: todo, tbd, fixme, and placeholder live in placeholder-alt-text.ts.
58
+ 'sample',
59
+ 'example',
60
+ 'test',
61
+ 'demo',
62
+ 'default',
63
+ 'untitled',
64
+ 'null',
65
+ 'undefined',
66
+ 'none',
67
+ // File format / extension names
68
+ 'jpg',
69
+ 'jpeg',
70
+ 'png',
71
+ 'svg',
72
+ 'webp',
73
+ // Contextless terms
74
+ 'this',
75
+ 'that',
76
+ 'here',
77
+ 'there',
78
+ 'above',
79
+ 'below',
80
+ // Weak file/asset labels
81
+ 'asset',
82
+ 'assets',
83
+ 'file',
84
+ 'attachment',
85
+ 'upload',
86
+ 'uploaded',
87
+ ]);
88
+ // Set of multi-word phrases that by themselves, are too vague to be useful alt text
89
+ const VAGUE_PHRASES = new Set([
90
+ 'an image',
91
+ 'an image of',
92
+ 'a photo',
93
+ 'a photo of',
94
+ 'a picture',
95
+ 'a picture of',
96
+ 'an icon',
97
+ 'an illustration',
98
+ 'a graphic',
99
+ 'a screenshot',
100
+ 'image of',
101
+ 'photo of',
102
+ 'picture of',
103
+ 'graphic of',
104
+ 'screenshot of',
105
+ 'image goes here',
106
+ 'photo goes here',
107
+ 'picture goes here',
108
+ 'your image here',
109
+ 'your photo here',
110
+ 'insert image',
111
+ 'insert photo',
112
+ ]);
113
+ export const vagueAltText = {
114
+ id: 'vague-alt-text',
115
+ problemUrl: 'https://www.w3.org/WAI/WCAG21/Understanding/non-text-content.html',
116
+ evaluate(context) {
117
+ return (context.images
118
+ // Find images whose alt text is too vague.
119
+ .filter(img => {
120
+ if (img.alt === null || img.alt === '')
121
+ return false;
122
+ const normalizedAltText = normalizeAltText(img.alt);
123
+ return VAGUE_WORDS.has(normalizedAltText) || VAGUE_PHRASES.has(normalizedAltText);
124
+ })
125
+ // Report each one with offending alt text.
126
+ .map(image => ({
127
+ image,
128
+ problemShort: `Alt text is too vague to describe the image:\n"${image.alt}"`,
129
+ solutionShort: 'replace with descriptive alt text, or use `alt=""` if the image is decorative',
130
+ })));
131
+ },
132
+ };
133
+ //# sourceMappingURL=vague-alt-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vague-alt-text.js","sourceRoot":"","sources":["../../../src/rules/vague-alt-text.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,gBAAgB,EAAC,MAAM,gCAAgC,CAAA;AAE/D,uEAAuE;AACvE,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC;IAC1B,sBAAsB;IACtB,OAAO;IACP,QAAQ;IACR,KAAK;IACL,OAAO;IACP,QAAQ;IACR,YAAY;IACZ,aAAa;IACb,SAAS;IACT,UAAU;IACV,KAAK;IACL,MAAM;IACN,SAAS;IACT,UAAU;IACV,QAAQ;IACR,SAAS;IACT,OAAO;IACP,SAAS;IACT,KAAK;IACL,MAAM;IAEN,yBAAyB;IACzB,MAAM;IACN,OAAO;IACP,cAAc;IACd,eAAe;IACf,QAAQ;IACR,SAAS;IACT,WAAW;IACX,YAAY;IACZ,MAAM;IACN,OAAO;IACP,QAAQ;IACR,SAAS;IAET,2BAA2B;IAC3B,KAAK;IACL,SAAS;IACT,SAAS;IACT,UAAU;IACV,SAAS;IACT,UAAU;IACV,OAAO;IACP,QAAQ;IACR,OAAO;IACP,QAAQ;IACR,YAAY;IACZ,aAAa;IACb,QAAQ;IACR,SAAS;IACT,UAAU;IACV,WAAW;IACX,KAAK;IACL,MAAM;IAEN,eAAe;IACf,2EAA2E;IAC3E,QAAQ;IACR,SAAS;IACT,MAAM;IACN,MAAM;IACN,SAAS;IACT,UAAU;IACV,MAAM;IACN,WAAW;IACX,MAAM;IAEN,gCAAgC;IAChC,KAAK;IACL,MAAM;IACN,KAAK;IACL,KAAK;IACL,MAAM;IAEN,oBAAoB;IACpB,MAAM;IACN,MAAM;IACN,MAAM;IACN,OAAO;IACP,OAAO;IACP,OAAO;IAEP,yBAAyB;IACzB,OAAO;IACP,QAAQ;IACR,MAAM;IACN,YAAY;IACZ,QAAQ;IACR,UAAU;CACX,CAAC,CAAA;AAEF,oFAAoF;AACpF,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC;IAC5B,UAAU;IACV,aAAa;IACb,SAAS;IACT,YAAY;IACZ,WAAW;IACX,cAAc;IACd,SAAS;IACT,iBAAiB;IACjB,WAAW;IACX,cAAc;IACd,UAAU;IACV,UAAU;IACV,YAAY;IACZ,YAAY;IACZ,eAAe;IACf,iBAAiB;IACjB,iBAAiB;IACjB,mBAAmB;IACnB,iBAAiB;IACjB,iBAAiB;IACjB,cAAc;IACd,cAAc;CACf,CAAC,CAAA;AAEF,MAAM,CAAC,MAAM,YAAY,GAAS;IAChC,EAAE,EAAE,gBAAgB;IACpB,UAAU,EAAE,mEAAmE;IAC/E,QAAQ,CAAC,OAAO;QACd,OAAO,CACL,OAAO,CAAC,MAAM;YAEZ,2CAA2C;aAC1C,MAAM,CAAC,GAAG,CAAC,EAAE;YACZ,IAAI,GAAG,CAAC,GAAG,KAAK,IAAI,IAAI,GAAG,CAAC,GAAG,KAAK,EAAE;gBAAE,OAAO,KAAK,CAAA;YACpD,MAAM,iBAAiB,GAAG,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;YACnD,OAAO,WAAW,CAAC,GAAG,CAAC,iBAAiB,CAAC,IAAI,aAAa,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAA;QACnF,CAAC,CAAC;YAEF,2CAA2C;aAC1C,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YACb,KAAK;YACL,YAAY,EAAE,kDAAkD,KAAK,CAAC,GAAG,GAAG;YAC5E,aAAa,EAAE,+EAA+E;SAC/F,CAAC,CAAC,CACN,CAAA;IACH,CAAC;CACF,CAAA"}
@@ -0,0 +1,49 @@
1
+ import type { Page } from 'playwright';
2
+ export type Finding = {
3
+ scannerType: string;
4
+ ruleId?: string;
5
+ url: string;
6
+ html?: string;
7
+ problemShort: string;
8
+ problemUrl: string;
9
+ solutionShort: string;
10
+ solutionLong?: string;
11
+ screenshotId?: string;
12
+ };
13
+ export type PluginArgs = {
14
+ page: Page;
15
+ addFinding: (finding: Finding) => Promise<void>;
16
+ };
17
+ export type ImageRecord = {
18
+ src: string | null;
19
+ alt: string | null;
20
+ role: string | null;
21
+ ariaHidden: boolean;
22
+ ariaLabel: string | null;
23
+ ariaLabelledBy: string | null;
24
+ outerHTML: string;
25
+ boundingBox: BoundingBox | null;
26
+ };
27
+ export type BoundingBox = {
28
+ x: number;
29
+ y: number;
30
+ width: number;
31
+ height: number;
32
+ };
33
+ export type RuleContext = {
34
+ url: string;
35
+ images: ImageRecord[];
36
+ };
37
+ export type RuleResult = {
38
+ image: ImageRecord;
39
+ problemShort: string;
40
+ solutionShort: string;
41
+ solutionLong?: string;
42
+ };
43
+ export type Rule = {
44
+ id: string;
45
+ problemUrl: string;
46
+ defaultEnabled?: boolean;
47
+ evaluate(ctx: RuleContext): RuleResult[];
48
+ };
49
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,YAAY,CAAA;AAIpC,MAAM,MAAM,OAAO,GAAG;IACpB,WAAW,EAAE,MAAM,CAAA;IACnB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAGD,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,IAAI,CAAA;IACV,UAAU,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAChD,CAAA;AAGD,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,UAAU,EAAE,OAAO,CAAA;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,WAAW,GAAG,IAAI,CAAA;CAChC,CAAA;AAGD,MAAM,MAAM,WAAW,GAAG;IACxB,CAAC,EAAE,MAAM,CAAA;IACT,CAAC,EAAE,MAAM,CAAA;IACT,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAGD,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,WAAW,EAAE,CAAA;CACtB,CAAA;AAGD,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,WAAW,CAAA;IAClB,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAGD,MAAM,MAAM,IAAI,GAAG;IACjB,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,MAAM,CAAA;IAGlB,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,QAAQ,CAAC,GAAG,EAAE,WAAW,GAAG,UAAU,EAAE,CAAA;CACzC,CAAA"}
@@ -0,0 +1,3 @@
1
+ // Shared contract for the alt-text-scan plugin.
2
+ export {};
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA,gDAAgD"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Normalizes alt text for comparison by trimming, lowercasing, collapsing internal
3
+ * whitespace, and stripping leading and trailing punctuation. Used by rules that
4
+ * need to compare alt text against a set.
5
+ */
6
+ export declare function normalizeAltText(alt: string): string;
7
+ //# sourceMappingURL=normalize-alt-text.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize-alt-text.d.ts","sourceRoot":"","sources":["../../../src/utils/normalize-alt-text.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQpD"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Normalizes alt text for comparison by trimming, lowercasing, collapsing internal
3
+ * whitespace, and stripping leading and trailing punctuation. Used by rules that
4
+ * need to compare alt text against a set.
5
+ */
6
+ export function normalizeAltText(alt) {
7
+ return alt
8
+ .trim()
9
+ .toLowerCase()
10
+ .replace(/\s+/g, ' ')
11
+ .replace(/^[.,!?;:()[\]{}'"“”‘’]+/, '')
12
+ .replace(/[.,!?;:()[\]{}'"“”‘’]+$/, '')
13
+ .trim();
14
+ }
15
+ //# sourceMappingURL=normalize-alt-text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize-alt-text.js","sourceRoot":"","sources":["../../../src/utils/normalize-alt-text.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,OAAO,GAAG;SACP,IAAI,EAAE;SACN,WAAW,EAAE;SACb,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,OAAO,CAAC,yBAAyB,EAAE,EAAE,CAAC;SACtC,OAAO,CAAC,yBAAyB,EAAE,EAAE,CAAC;SACtC,IAAI,EAAE,CAAA;AACX,CAAC"}
package/package.json CHANGED
@@ -1,12 +1,51 @@
1
1
  {
2
2
  "name": "@github/accessibility-scanner-alt-text-plugin",
3
- "version": "0.0.1",
4
- "description": "Stub package for @github/accessibility-scanner-alt-text-plugin",
5
- "keywords": [],
6
- "author": "",
3
+ "version": "1.0.0",
4
+ "description": "Alt-text validation plugin for github/accessibility-scanner",
7
5
  "license": "MIT",
8
6
  "repository": {
9
7
  "type": "git",
10
8
  "url": "git+https://github.com/github/accessibility-scanner-alt-text-plugin.git"
9
+ },
10
+ "type": "module",
11
+ "main": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.build.json",
27
+ "prepack": "npm run build",
28
+ "test": "vitest run --passWithNoTests",
29
+ "test:watch": "vitest",
30
+ "typecheck": "tsc --noEmit",
31
+ "lint": "eslint .",
32
+ "format": "prettier --write .",
33
+ "format:check": "prettier --check ."
34
+ },
35
+ "prettier": "@github/prettier-config",
36
+ "engines": {
37
+ "node": "^22.13.0 || ^24 || ^26"
38
+ },
39
+ "devDependencies": {
40
+ "@github/prettier-config": "^0.0.6",
41
+ "@types/node": "^26.1.0",
42
+ "eslint": "^10.6.0",
43
+ "eslint-config-prettier": "^10.1.8",
44
+ "eslint-plugin-check-file": "^3.3.1",
45
+ "playwright": "^1.61.1",
46
+ "prettier": "^3.9.4",
47
+ "typescript": "^6.0.3",
48
+ "typescript-eslint": "^8.62.1",
49
+ "vitest": "^4.1.9"
11
50
  }
12
51
  }