@octaviaflow/accessibility-checker 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/LICENSE +0 -0
- package/README.md +94 -0
- package/bin/achecker.js +18 -0
- package/cjs/checker/index.js +130 -0
- package/mjs/checker/index.js +94 -0
- package/package.json +73 -0
- package/types/checker/index.d.ts +18 -0
package/LICENSE
ADDED
|
File without changes
|
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Accessibility Checker
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Accessibility Checker is a JavaScript project designed to help developers ensure that their web content meets accessibility standards. This tool provides functionalities to check and validate the accessibility of web pages.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Core functionality for accessibility checking.
|
|
10
|
+
- Utility functions to support main functionalities.
|
|
11
|
+
- Unit tests to ensure reliability and correctness.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
To install the project, clone the repository and run the following command in the project directory:
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
npm install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
To use the accessibility checker, you can run the main entry point:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
node src/index.js
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Running Tests
|
|
30
|
+
|
|
31
|
+
To run the unit tests, use the following command:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
npm test
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Contributing
|
|
38
|
+
|
|
39
|
+
Contributions are welcome! Please feel free to submit a pull request or open an issue for any enhancements or bug fixes.
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
This project is licensed under the MIT License. See the LICENSE file for more details.
|
|
44
|
+
|
|
45
|
+
### octaviaflow - Local Accessibility Checker
|
|
46
|
+
|
|
47
|
+
Usage:
|
|
48
|
+
|
|
49
|
+
- node src/index.js --input path/to/file.html
|
|
50
|
+
- node src/index.js --sample
|
|
51
|
+
- Optional: --output path/to/results.json (defaults to ./data/results.json)
|
|
52
|
+
|
|
53
|
+
Behavior:
|
|
54
|
+
|
|
55
|
+
- Performs lightweight accessibility heuristics (images missing alt, empty links, empty headings).
|
|
56
|
+
- Stores results as an array in the output JSON file (suitable for future import to MongoDB).
|
|
57
|
+
- No IBM telemetry or IBM/carbon dependencies included.
|
|
58
|
+
|
|
59
|
+
### octaviaflow - Local Accessibility Checker (TypeScript)
|
|
60
|
+
|
|
61
|
+
Quick start:
|
|
62
|
+
|
|
63
|
+
1. Install deps:
|
|
64
|
+
|
|
65
|
+
cd accessibility-checker
|
|
66
|
+
npm install
|
|
67
|
+
|
|
68
|
+
2. Build:
|
|
69
|
+
|
|
70
|
+
npm run build
|
|
71
|
+
|
|
72
|
+
3. Run:
|
|
73
|
+
|
|
74
|
+
node ./cjs/index.js --sample
|
|
75
|
+
or
|
|
76
|
+
octaviaflow --input path/to/file.html --output ./data/results.json
|
|
77
|
+
|
|
78
|
+
What changed:
|
|
79
|
+
|
|
80
|
+
- Converted to TypeScript.
|
|
81
|
+
- Uses axe-core + heuristics for more complete checks.
|
|
82
|
+
- Stores results locally as an array in JSON (ready for importing to MongoDB).
|
|
83
|
+
- No IBM telemetry or Carbon packages included.
|
|
84
|
+
|
|
85
|
+
Testing:
|
|
86
|
+
|
|
87
|
+
- npm test (uses vitest)
|
|
88
|
+
|
|
89
|
+
Build outputs:
|
|
90
|
+
|
|
91
|
+
- cjs/ CommonJS build (cjs/index.js)
|
|
92
|
+
- mjs/ ES module build (mjs/index.js)
|
|
93
|
+
- types/ TypeScript declaration files
|
|
94
|
+
- bin/ CLI shim (bin/octaviaflow.js)
|
package/bin/achecker.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// simple shim that delegates to the CommonJS build
|
|
3
|
+
try {
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
5
|
+
const mod = require('../cjs/index.js');
|
|
6
|
+
if (mod && typeof mod.main === 'function') {
|
|
7
|
+
mod.main(process.argv.slice(2)).catch((e) => {
|
|
8
|
+
console.error('Error:', e);
|
|
9
|
+
process.exit(2);
|
|
10
|
+
});
|
|
11
|
+
} else {
|
|
12
|
+
console.error('octaviaflow entrypoint missing');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
} catch (e) {
|
|
16
|
+
console.error('Failed to start octaviaflow:', e);
|
|
17
|
+
process.exit(2);
|
|
18
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.analyze = analyze;
|
|
37
|
+
const jsdom_1 = require("jsdom");
|
|
38
|
+
const axe = __importStar(require("axe-core"));
|
|
39
|
+
function heuristicAnalyze(html) {
|
|
40
|
+
const issues = [];
|
|
41
|
+
// images without alt
|
|
42
|
+
const imgRegex = /<img\b[^>]*>/gi;
|
|
43
|
+
let m;
|
|
44
|
+
while ((m = imgRegex.exec(html))) {
|
|
45
|
+
const tag = m[0];
|
|
46
|
+
if (!/alt\s*=\s*["'][^"']*["']/i.test(tag)) {
|
|
47
|
+
issues.push({
|
|
48
|
+
type: 'image-missing-alt',
|
|
49
|
+
message: 'Image tag missing alt attribute',
|
|
50
|
+
snippet: tag,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// links without descriptive text (naive)
|
|
55
|
+
const aRegex = /<a\b[^>]*>([\s\S]*?)<\/a>/gi;
|
|
56
|
+
while ((m = aRegex.exec(html))) {
|
|
57
|
+
const inner = (m[1] || '').trim();
|
|
58
|
+
if (!inner || /^<img\b/i.test(inner) || inner.length === 0) {
|
|
59
|
+
issues.push({
|
|
60
|
+
type: 'link-empty',
|
|
61
|
+
message: 'Link has no accessible text',
|
|
62
|
+
snippet: m[0],
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// empty headings
|
|
67
|
+
const hRegex = /<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi;
|
|
68
|
+
while ((m = hRegex.exec(html))) {
|
|
69
|
+
const content = (m[1] || '').replace(/<[^>]*>/g, '').trim();
|
|
70
|
+
if (!content) {
|
|
71
|
+
issues.push({
|
|
72
|
+
type: 'heading-empty',
|
|
73
|
+
message: 'Heading tag is empty',
|
|
74
|
+
snippet: m[0],
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return issues;
|
|
79
|
+
}
|
|
80
|
+
async function runAxe(html) {
|
|
81
|
+
const dom = new jsdom_1.JSDOM(html, {
|
|
82
|
+
runScripts: 'dangerously',
|
|
83
|
+
resources: 'usable',
|
|
84
|
+
});
|
|
85
|
+
const { window } = dom;
|
|
86
|
+
// axe.run expects document; pass document
|
|
87
|
+
const result = await axe.run(window.document, {
|
|
88
|
+
// default rules; you can customize here
|
|
89
|
+
});
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
async function analyze(html) {
|
|
93
|
+
const heuristics = heuristicAnalyze(html);
|
|
94
|
+
let axeRes = null;
|
|
95
|
+
try {
|
|
96
|
+
axeRes = await runAxe(html);
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
// If axe fails for some HTML, continue with heuristics
|
|
100
|
+
axeRes = { error: String(e) };
|
|
101
|
+
}
|
|
102
|
+
const axeIssues = [];
|
|
103
|
+
if (axeRes && Array.isArray(axeRes.violations)) {
|
|
104
|
+
for (const v of axeRes.violations) {
|
|
105
|
+
for (const node of v.nodes || []) {
|
|
106
|
+
axeIssues.push({
|
|
107
|
+
type: `axe:${v.id}`,
|
|
108
|
+
message: v.description || v.help,
|
|
109
|
+
snippet: node.html,
|
|
110
|
+
helpUrl: v.helpUrl,
|
|
111
|
+
source: 'axe',
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const combined = heuristics.concat(axeIssues);
|
|
117
|
+
const byType = {};
|
|
118
|
+
for (const it of combined) {
|
|
119
|
+
byType[it.type] = (byType[it.type] || 0) + 1;
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
summary: {
|
|
123
|
+
totalIssues: combined.length,
|
|
124
|
+
byType,
|
|
125
|
+
axeViolations: axeIssues.length,
|
|
126
|
+
},
|
|
127
|
+
issues: combined,
|
|
128
|
+
axe: axeRes,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { JSDOM } from 'jsdom';
|
|
2
|
+
import * as axe from 'axe-core';
|
|
3
|
+
function heuristicAnalyze(html) {
|
|
4
|
+
const issues = [];
|
|
5
|
+
// images without alt
|
|
6
|
+
const imgRegex = /<img\b[^>]*>/gi;
|
|
7
|
+
let m;
|
|
8
|
+
while ((m = imgRegex.exec(html))) {
|
|
9
|
+
const tag = m[0];
|
|
10
|
+
if (!/alt\s*=\s*["'][^"']*["']/i.test(tag)) {
|
|
11
|
+
issues.push({
|
|
12
|
+
type: 'image-missing-alt',
|
|
13
|
+
message: 'Image tag missing alt attribute',
|
|
14
|
+
snippet: tag,
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// links without descriptive text (naive)
|
|
19
|
+
const aRegex = /<a\b[^>]*>([\s\S]*?)<\/a>/gi;
|
|
20
|
+
while ((m = aRegex.exec(html))) {
|
|
21
|
+
const inner = (m[1] || '').trim();
|
|
22
|
+
if (!inner || /^<img\b/i.test(inner) || inner.length === 0) {
|
|
23
|
+
issues.push({
|
|
24
|
+
type: 'link-empty',
|
|
25
|
+
message: 'Link has no accessible text',
|
|
26
|
+
snippet: m[0],
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// empty headings
|
|
31
|
+
const hRegex = /<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi;
|
|
32
|
+
while ((m = hRegex.exec(html))) {
|
|
33
|
+
const content = (m[1] || '').replace(/<[^>]*>/g, '').trim();
|
|
34
|
+
if (!content) {
|
|
35
|
+
issues.push({
|
|
36
|
+
type: 'heading-empty',
|
|
37
|
+
message: 'Heading tag is empty',
|
|
38
|
+
snippet: m[0],
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return issues;
|
|
43
|
+
}
|
|
44
|
+
async function runAxe(html) {
|
|
45
|
+
const dom = new JSDOM(html, {
|
|
46
|
+
runScripts: 'dangerously',
|
|
47
|
+
resources: 'usable',
|
|
48
|
+
});
|
|
49
|
+
const { window } = dom;
|
|
50
|
+
// axe.run expects document; pass document
|
|
51
|
+
const result = await axe.run(window.document, {
|
|
52
|
+
// default rules; you can customize here
|
|
53
|
+
});
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
export async function analyze(html) {
|
|
57
|
+
const heuristics = heuristicAnalyze(html);
|
|
58
|
+
let axeRes = null;
|
|
59
|
+
try {
|
|
60
|
+
axeRes = await runAxe(html);
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
// If axe fails for some HTML, continue with heuristics
|
|
64
|
+
axeRes = { error: String(e) };
|
|
65
|
+
}
|
|
66
|
+
const axeIssues = [];
|
|
67
|
+
if (axeRes && Array.isArray(axeRes.violations)) {
|
|
68
|
+
for (const v of axeRes.violations) {
|
|
69
|
+
for (const node of v.nodes || []) {
|
|
70
|
+
axeIssues.push({
|
|
71
|
+
type: `axe:${v.id}`,
|
|
72
|
+
message: v.description || v.help,
|
|
73
|
+
snippet: node.html,
|
|
74
|
+
helpUrl: v.helpUrl,
|
|
75
|
+
source: 'axe',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const combined = heuristics.concat(axeIssues);
|
|
81
|
+
const byType = {};
|
|
82
|
+
for (const it of combined) {
|
|
83
|
+
byType[it.type] = (byType[it.type] || 0) + 1;
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
summary: {
|
|
87
|
+
totalIssues: combined.length,
|
|
88
|
+
byType,
|
|
89
|
+
axeViolations: axeIssues.length,
|
|
90
|
+
},
|
|
91
|
+
issues: combined,
|
|
92
|
+
axe: axeRes,
|
|
93
|
+
};
|
|
94
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@octaviaflow/accessibility-checker",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"description": "Accessibility checker for OctaviaFlow Design System - A custom implementation",
|
|
6
|
+
"main": "cjs/index.js",
|
|
7
|
+
"module": "mjs/index.js",
|
|
8
|
+
"types": "types/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./mjs/index.js",
|
|
12
|
+
"require": "./cjs/index.js",
|
|
13
|
+
"types": "./types/index.d.ts"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"cjs",
|
|
18
|
+
"mjs",
|
|
19
|
+
"types",
|
|
20
|
+
"bin"
|
|
21
|
+
],
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/OctaviaFlow/OctaviaFlow-Design-System.git",
|
|
25
|
+
"directory": "packages/accessibility-checker"
|
|
26
|
+
},
|
|
27
|
+
"bugs": "https://github.com/OctaviaFlow/OctaviaFlow-Design-System/issues",
|
|
28
|
+
"scripts": {
|
|
29
|
+
"clean": "rimraf cjs mjs types",
|
|
30
|
+
"build:cjs": "tsc -p tsconfig.cjs.json",
|
|
31
|
+
"build:mjs": "tsc -p tsconfig.mjs.json",
|
|
32
|
+
"build:types": "tsc -p tsconfig.types.json",
|
|
33
|
+
"build": "yarn clean && yarn build:cjs && yarn build:mjs && yarn build:types",
|
|
34
|
+
"test": "vitest",
|
|
35
|
+
"ci-check": "yarn build && yarn test",
|
|
36
|
+
"start": "node ./cjs/index.js --sample"
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"octaviaflow",
|
|
40
|
+
"octaviaflow-design-system",
|
|
41
|
+
"accessibility",
|
|
42
|
+
"checker",
|
|
43
|
+
"web",
|
|
44
|
+
"a11y"
|
|
45
|
+
],
|
|
46
|
+
"author": "OctaviaFlow",
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"axe-core": "^4.11.0",
|
|
49
|
+
"chromedriver": "*",
|
|
50
|
+
"deep-diff": "^1.0.2",
|
|
51
|
+
"exceljs": "^4.3.0",
|
|
52
|
+
"js-yaml": "^4.1.0",
|
|
53
|
+
"jsdom": "^27.1.0",
|
|
54
|
+
"puppeteer": "^24.29.1",
|
|
55
|
+
"string-hash": "^1.1.3"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@types/jsdom": "^27.0.0",
|
|
59
|
+
"@types/node": "^24.10.0",
|
|
60
|
+
"eslint": "^9.31.1",
|
|
61
|
+
"jest": "^30.2.0",
|
|
62
|
+
"rimraf": "^6.0.1",
|
|
63
|
+
"ts-node": "^10.9.2",
|
|
64
|
+
"typescript": "^5.9.3",
|
|
65
|
+
"vitest": "^4.0.8"
|
|
66
|
+
},
|
|
67
|
+
"bin": {
|
|
68
|
+
"octaviaflow-achecker": "bin/achecker.js"
|
|
69
|
+
},
|
|
70
|
+
"publishConfig": {
|
|
71
|
+
"access": "public"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type Issue = {
|
|
2
|
+
type: string;
|
|
3
|
+
message: string;
|
|
4
|
+
snippet?: string;
|
|
5
|
+
helpUrl?: string;
|
|
6
|
+
source?: string;
|
|
7
|
+
};
|
|
8
|
+
export type Result = {
|
|
9
|
+
summary: {
|
|
10
|
+
totalIssues: number;
|
|
11
|
+
byType: Record<string, number>;
|
|
12
|
+
axeViolations?: number;
|
|
13
|
+
};
|
|
14
|
+
issues: Issue[];
|
|
15
|
+
axe?: any;
|
|
16
|
+
meta?: any;
|
|
17
|
+
};
|
|
18
|
+
export declare function analyze(html: string): Promise<Result>;
|