equall-cli 0.1.3 → 0.1.5

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/dist/cli.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  __toESM,
8
8
  globby,
9
9
  runScan
10
- } from "./chunk-5YPLWCOT.js";
10
+ } from "./chunk-OQXPOPP6.js";
11
11
 
12
12
  // node_modules/commander/lib/error.js
13
13
  var require_error = __commonJS({
@@ -3011,6 +3011,8 @@ var require_commander = __commonJS({
3011
3011
 
3012
3012
  // src/cli.ts
3013
3013
  import { resolve as resolve2, basename } from "path";
3014
+ import { readFileSync, existsSync, statSync } from "fs";
3015
+ import { fileURLToPath } from "url";
3014
3016
 
3015
3017
  // node_modules/commander/esm.mjs
3016
3018
  var import_index = __toESM(require_commander(), 1);
@@ -6410,6 +6412,107 @@ function ora(options) {
6410
6412
  return new Ora(options);
6411
6413
  }
6412
6414
 
6415
+ // src/wcag-catalog.ts
6416
+ var WCAG_CATALOG = [
6417
+ // Principle 1 — Perceivable
6418
+ { id: "1.1.1", name: "Non-text Content", level: "A", pour: "perceivable" },
6419
+ { id: "1.2.1", name: "Audio-only and Video-only (Prerecorded)", level: "A", pour: "perceivable" },
6420
+ { id: "1.2.2", name: "Captions (Prerecorded)", level: "A", pour: "perceivable" },
6421
+ { id: "1.2.3", name: "Audio Description or Media Alternative", level: "A", pour: "perceivable" },
6422
+ { id: "1.2.4", name: "Captions (Live)", level: "AA", pour: "perceivable" },
6423
+ { id: "1.2.5", name: "Audio Description (Prerecorded)", level: "AA", pour: "perceivable" },
6424
+ { id: "1.2.6", name: "Sign Language (Prerecorded)", level: "AAA", pour: "perceivable" },
6425
+ { id: "1.2.7", name: "Extended Audio Description (Prerecorded)", level: "AAA", pour: "perceivable" },
6426
+ { id: "1.2.8", name: "Media Alternative (Prerecorded)", level: "AAA", pour: "perceivable" },
6427
+ { id: "1.2.9", name: "Audio-only (Live)", level: "AAA", pour: "perceivable" },
6428
+ { id: "1.3.1", name: "Info and Relationships", level: "A", pour: "perceivable" },
6429
+ { id: "1.3.2", name: "Meaningful Sequence", level: "A", pour: "perceivable" },
6430
+ { id: "1.3.3", name: "Sensory Characteristics", level: "A", pour: "perceivable" },
6431
+ { id: "1.3.4", name: "Orientation", level: "AA", pour: "perceivable" },
6432
+ { id: "1.3.5", name: "Identify Input Purpose", level: "AA", pour: "perceivable" },
6433
+ { id: "1.3.6", name: "Identify Purpose", level: "AAA", pour: "perceivable" },
6434
+ { id: "1.4.1", name: "Use of Color", level: "A", pour: "perceivable" },
6435
+ { id: "1.4.2", name: "Audio Control", level: "A", pour: "perceivable" },
6436
+ { id: "1.4.3", name: "Contrast (Minimum)", level: "AA", pour: "perceivable" },
6437
+ { id: "1.4.4", name: "Resize Text", level: "AA", pour: "perceivable" },
6438
+ { id: "1.4.5", name: "Images of Text", level: "AA", pour: "perceivable" },
6439
+ { id: "1.4.6", name: "Contrast (Enhanced)", level: "AAA", pour: "perceivable" },
6440
+ { id: "1.4.7", name: "Low or No Background Audio", level: "AAA", pour: "perceivable" },
6441
+ { id: "1.4.8", name: "Visual Presentation", level: "AAA", pour: "perceivable" },
6442
+ { id: "1.4.9", name: "Images of Text (No Exception)", level: "AAA", pour: "perceivable" },
6443
+ { id: "1.4.10", name: "Reflow", level: "AA", pour: "perceivable" },
6444
+ { id: "1.4.11", name: "Non-text Contrast", level: "AA", pour: "perceivable" },
6445
+ { id: "1.4.12", name: "Text Spacing", level: "AA", pour: "perceivable" },
6446
+ { id: "1.4.13", name: "Content on Hover or Focus", level: "AA", pour: "perceivable" },
6447
+ // Principle 2 — Operable
6448
+ { id: "2.1.1", name: "Keyboard", level: "A", pour: "operable" },
6449
+ { id: "2.1.2", name: "No Keyboard Trap", level: "A", pour: "operable" },
6450
+ { id: "2.1.3", name: "Keyboard (No Exception)", level: "AAA", pour: "operable" },
6451
+ { id: "2.1.4", name: "Character Key Shortcuts", level: "A", pour: "operable" },
6452
+ { id: "2.2.1", name: "Timing Adjustable", level: "A", pour: "operable" },
6453
+ { id: "2.2.2", name: "Pause, Stop, Hide", level: "A", pour: "operable" },
6454
+ { id: "2.2.3", name: "No Timing", level: "AAA", pour: "operable" },
6455
+ { id: "2.2.4", name: "Interruptions", level: "AAA", pour: "operable" },
6456
+ { id: "2.2.5", name: "Re-authenticating", level: "AAA", pour: "operable" },
6457
+ { id: "2.2.6", name: "Timeouts", level: "AAA", pour: "operable" },
6458
+ { id: "2.3.1", name: "Three Flashes or Below Threshold", level: "A", pour: "operable" },
6459
+ { id: "2.3.2", name: "Three Flashes", level: "AAA", pour: "operable" },
6460
+ { id: "2.3.3", name: "Animation from Interactions", level: "AAA", pour: "operable" },
6461
+ { id: "2.4.1", name: "Bypass Blocks", level: "A", pour: "operable" },
6462
+ { id: "2.4.2", name: "Page Titled", level: "A", pour: "operable" },
6463
+ { id: "2.4.3", name: "Focus Order", level: "A", pour: "operable" },
6464
+ { id: "2.4.4", name: "Link Purpose (In Context)", level: "A", pour: "operable" },
6465
+ { id: "2.4.5", name: "Multiple Ways", level: "AA", pour: "operable" },
6466
+ { id: "2.4.6", name: "Headings and Labels", level: "AA", pour: "operable" },
6467
+ { id: "2.4.7", name: "Focus Visible", level: "AA", pour: "operable" },
6468
+ { id: "2.4.8", name: "Location", level: "AAA", pour: "operable" },
6469
+ { id: "2.4.9", name: "Link Purpose (Link Only)", level: "AAA", pour: "operable" },
6470
+ { id: "2.4.10", name: "Section Headings", level: "AAA", pour: "operable" },
6471
+ { id: "2.4.11", name: "Focus Not Obscured (Minimum)", level: "AA", pour: "operable" },
6472
+ { id: "2.4.12", name: "Focus Not Obscured (Enhanced)", level: "AAA", pour: "operable" },
6473
+ { id: "2.4.13", name: "Focus Appearance", level: "AAA", pour: "operable" },
6474
+ { id: "2.5.1", name: "Pointer Gestures", level: "A", pour: "operable" },
6475
+ { id: "2.5.2", name: "Pointer Cancellation", level: "A", pour: "operable" },
6476
+ { id: "2.5.3", name: "Label in Name", level: "A", pour: "operable" },
6477
+ { id: "2.5.4", name: "Motion Actuation", level: "A", pour: "operable" },
6478
+ { id: "2.5.5", name: "Target Size (Enhanced)", level: "AAA", pour: "operable" },
6479
+ { id: "2.5.6", name: "Concurrent Input Mechanisms", level: "A", pour: "operable" },
6480
+ { id: "2.5.7", name: "Dragging Movements", level: "AA", pour: "operable" },
6481
+ { id: "2.5.8", name: "Target Size (Minimum)", level: "AA", pour: "operable" },
6482
+ // Principle 3 — Understandable
6483
+ { id: "3.1.1", name: "Language of Page", level: "A", pour: "understandable" },
6484
+ { id: "3.1.2", name: "Language of Parts", level: "AA", pour: "understandable" },
6485
+ { id: "3.1.3", name: "Unusual Words", level: "AAA", pour: "understandable" },
6486
+ { id: "3.1.4", name: "Abbreviations", level: "AAA", pour: "understandable" },
6487
+ { id: "3.1.5", name: "Reading Level", level: "AAA", pour: "understandable" },
6488
+ { id: "3.1.6", name: "Pronunciation", level: "AAA", pour: "understandable" },
6489
+ { id: "3.2.1", name: "On Focus", level: "A", pour: "understandable" },
6490
+ { id: "3.2.2", name: "On Input", level: "A", pour: "understandable" },
6491
+ { id: "3.2.3", name: "Consistent Navigation", level: "AA", pour: "understandable" },
6492
+ { id: "3.2.4", name: "Consistent Identification", level: "AA", pour: "understandable" },
6493
+ { id: "3.2.5", name: "Change on Request", level: "AAA", pour: "understandable" },
6494
+ { id: "3.2.6", name: "Consistent Help", level: "A", pour: "understandable" },
6495
+ { id: "3.3.1", name: "Error Identification", level: "A", pour: "understandable" },
6496
+ { id: "3.3.2", name: "Labels or Instructions", level: "A", pour: "understandable" },
6497
+ { id: "3.3.3", name: "Error Suggestion", level: "AA", pour: "understandable" },
6498
+ { id: "3.3.4", name: "Error Prevention (Legal, Financial, Data)", level: "AA", pour: "understandable" },
6499
+ { id: "3.3.5", name: "Help", level: "AAA", pour: "understandable" },
6500
+ { id: "3.3.6", name: "Error Prevention (All)", level: "AAA", pour: "understandable" },
6501
+ { id: "3.3.7", name: "Redundant Entry", level: "A", pour: "understandable" },
6502
+ { id: "3.3.8", name: "Accessible Authentication (Minimum)", level: "AA", pour: "understandable" },
6503
+ { id: "3.3.9", name: "Accessible Authentication (Enhanced)", level: "AAA", pour: "understandable" },
6504
+ // Principle 4 — Robust
6505
+ { id: "4.1.2", name: "Name, Role, Value", level: "A", pour: "robust" },
6506
+ { id: "4.1.3", name: "Status Messages", level: "AA", pour: "robust" }
6507
+ ];
6508
+ var LEVEL_RANK = { A: 1, AA: 2, AAA: 3 };
6509
+ var catalogMap = /* @__PURE__ */ new Map();
6510
+ for (const c of WCAG_CATALOG) catalogMap.set(c.id, c);
6511
+ function getCriteriaForLevel(level) {
6512
+ const maxRank = LEVEL_RANK[level];
6513
+ return WCAG_CATALOG.filter((c) => LEVEL_RANK[c.level] <= maxRank);
6514
+ }
6515
+
6413
6516
  // src/output/terminal.ts
6414
6517
  var WCAG_A_CRITERIA = /* @__PURE__ */ new Set([
6415
6518
  "1.1.1",
@@ -6435,15 +6538,17 @@ var WCAG_A_CRITERIA = /* @__PURE__ */ new Set([
6435
6538
  "2.5.2",
6436
6539
  "2.5.3",
6437
6540
  "2.5.4",
6541
+ "2.5.6",
6438
6542
  "3.1.1",
6439
6543
  "3.2.1",
6440
6544
  "3.2.2",
6441
6545
  "3.2.6",
6442
6546
  "3.3.1",
6547
+ "3.3.2",
6443
6548
  "3.3.7",
6444
6549
  "4.1.2"
6445
6550
  ]);
6446
- var WCAG_A_TOTAL = 30;
6551
+ var WCAG_A_TOTAL = 32;
6447
6552
  var BP_HINTS = {
6448
6553
  "region": "Landmarks help screen reader users navigate page sections",
6449
6554
  "landmark-main-is-top-level": "Nested landmarks confuse assistive technology",
@@ -6599,6 +6704,22 @@ function printResult(result, options = {}) {
6599
6704
  const hint = BP_HINTS[criterion];
6600
6705
  const hintSuffix = hint ? ` ${DIM}${hint}${RESET}` : "";
6601
6706
  console.log(` ${severityIcon(topSeverity)} ${BOLD}${criterion}${RESET} ${DIM}\u2014 ${group.issues.length} issue${group.issues.length > 1 ? "s" : ""}${RESET}${hintSuffix}`);
6707
+ const maxFiles = options.verbose ? group.issues.length : 2;
6708
+ const seen = /* @__PURE__ */ new Set();
6709
+ const uniqueIssues = [];
6710
+ for (const issue of group.issues) {
6711
+ if (!seen.has(issue.file_path)) {
6712
+ seen.add(issue.file_path);
6713
+ uniqueIssues.push(issue);
6714
+ }
6715
+ }
6716
+ for (const issue of uniqueIssues.slice(0, maxFiles)) {
6717
+ const location = issue.line ? `:${issue.line}` : "";
6718
+ console.log(` ${DIM}${issue.file_path}${location}${RESET}`);
6719
+ }
6720
+ if (!options.verbose && uniqueIssues.length > 2) {
6721
+ console.log(` ${DIM}... and ${uniqueIssues.length - 2} more (use --verbose to see all)${RESET}`);
6722
+ }
6602
6723
  }
6603
6724
  console.log();
6604
6725
  }
@@ -6613,6 +6734,20 @@ function printResult(result, options = {}) {
6613
6734
  console.log();
6614
6735
  }
6615
6736
  }
6737
+ if (options.showManual) {
6738
+ const level = options.targetLevel ?? "AA";
6739
+ const allForLevel = getCriteriaForLevel(level);
6740
+ const coveredSet = new Set(result.criteria_covered);
6741
+ const untested = allForLevel.filter((c) => !coveredSet.has(c.id));
6742
+ if (untested.length > 0) {
6743
+ console.log(` ${BOLD}Manual review needed${RESET} ${DIM}(${untested.length} criteria)${RESET}`);
6744
+ for (const c of untested) {
6745
+ const principle = c.pour.charAt(0).toUpperCase() + c.pour.slice(1);
6746
+ console.log(` ${DIM}${c.id}${RESET} ${c.name} ${DIM}\u2014 ${principle}${RESET}`);
6747
+ }
6748
+ console.log();
6749
+ }
6750
+ }
6616
6751
  console.log(` ${DIM}Scanners: ${scanners_used.map((s) => `${s.name}@${s.version} (${s.issues_found} issues)`).join(", ")}${RESET}`);
6617
6752
  console.log(` ${DIM}Completed in ${(duration_ms / 1e3).toFixed(1)}s${RESET}`);
6618
6753
  console.log();
@@ -6716,6 +6851,28 @@ async function findIgnores(rootPath) {
6716
6851
  }
6717
6852
  return entries.sort((a, b) => a.file_path.localeCompare(b.file_path) || a.line - b.line);
6718
6853
  }
6854
+ async function addIgnoreFile(rootPath, filePath) {
6855
+ const absolutePath = resolve(rootPath, filePath);
6856
+ let content;
6857
+ try {
6858
+ content = await readFile(absolutePath, "utf-8");
6859
+ } catch {
6860
+ return null;
6861
+ }
6862
+ const lines = content.split("\n");
6863
+ if (lines.slice(0, 5).some((l) => l.includes("equall-ignore-file"))) {
6864
+ return null;
6865
+ }
6866
+ const ext = filePath.split(".").pop()?.toLowerCase();
6867
+ let comment;
6868
+ if (ext === "html" || ext === "htm") {
6869
+ comment = "<!-- equall-ignore-file -->";
6870
+ } else {
6871
+ comment = "// equall-ignore-file";
6872
+ }
6873
+ await writeFile(absolutePath, comment + "\n" + content, "utf-8");
6874
+ return { file: filePath, comment };
6875
+ }
6719
6876
  async function removeIgnore(rootPath, target) {
6720
6877
  const ignores = await findIgnores(rootPath);
6721
6878
  const colonIdx = target.lastIndexOf(":");
@@ -6809,9 +6966,11 @@ async function clearAllIgnores(rootPath) {
6809
6966
  }
6810
6967
 
6811
6968
  // src/cli.ts
6969
+ var __dir = resolve2(fileURLToPath(import.meta.url), "..");
6970
+ var pkg = JSON.parse(readFileSync(resolve2(__dir, "..", "package.json"), "utf-8"));
6812
6971
  var program2 = new Command();
6813
- program2.name("equall").description("Open-source accessibility scoring \u2014 aggregates axe-core, eslint-plugin-jsx-a11y, and more.").version("0.1.0");
6814
- program2.command("scan").description("Scan a project for accessibility issues").argument("[path]", "Path to project root", ".").option("-l, --level <level>", "WCAG conformance target: A, AA, or AAA", "AA").option("--include <patterns...>", "Glob patterns to include").option("--exclude <patterns...>", "Glob patterns to exclude").option("--json", "Output results as JSON").option("-i, --show-ignored", "Show ignored issues in output").option("--no-color", "Disable colored output").action(async (path, opts) => {
6972
+ program2.name("equall").description("Open-source accessibility scoring \u2014 aggregates axe-core, eslint-plugin-jsx-a11y, and more.").version(pkg.version);
6973
+ program2.command("scan").description("Scan a project for accessibility issues").argument("[path]", "Path to project root", ".").option("-l, --level <level>", "WCAG conformance target: A, AA, or AAA", "AA").option("--include <patterns...>", "Glob patterns to include").option("--exclude <patterns...>", "Glob patterns to exclude").option("--json", "Output results as JSON").option("-i, --show-ignored", "Show ignored issues in output").option("-v, --verbose", "Show all occurrences for best-practice issues").option("-m, --show-manual", "List WCAG criteria that require manual review").option("--no-color", "Disable colored output").action(async (path, opts) => {
6815
6974
  const level = opts.level.toUpperCase();
6816
6975
  if (!["A", "AA", "AAA"].includes(level)) {
6817
6976
  console.error(`Invalid level "${opts.level}". Use A, AA, or AAA.`);
@@ -6839,7 +6998,7 @@ program2.command("scan").description("Scan a project for accessibility issues").
6839
6998
  if (opts.json) {
6840
6999
  printJson(result);
6841
7000
  } else {
6842
- printResult(result, { showIgnored: opts.showIgnored });
7001
+ printResult(result, { showIgnored: opts.showIgnored, verbose: opts.verbose, showManual: opts.showManual, targetLevel: level });
6843
7002
  }
6844
7003
  if (result.score < 50) process.exit(1);
6845
7004
  } catch (error2) {
@@ -6895,6 +7054,21 @@ program2.command("ignore").description("Add, list, or remove equall-ignore comme
6895
7054
  console.log(`
6896
7055
  ${GREEN2}Added${RESET2} ${result.file}:${result.line}`);
6897
7056
  console.log(` ${DIM2}${result.comment.trim()}${RESET2}
7057
+ `);
7058
+ return;
7059
+ }
7060
+ const isDirectory = target && existsSync(resolve2(rootPath, target)) && statSync(resolve2(rootPath, target)).isDirectory();
7061
+ if (target && !isDirectory && (target.includes("/") || target.match(/\.\w+$/))) {
7062
+ const result = await addIgnoreFile(rootPath, target);
7063
+ if (!result) {
7064
+ console.error(`
7065
+ Could not ignore ${target}. File not found or already ignored.
7066
+ `);
7067
+ process.exit(1);
7068
+ }
7069
+ console.log(`
7070
+ ${GREEN2}Added${RESET2} ${result.file}`);
7071
+ console.log(` ${DIM2}${result.comment}${RESET2}
6898
7072
  `);
6899
7073
  return;
6900
7074
  }
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import { createRequire as __createRequire } from 'module'; import { fileURLToPat
2
2
  import {
3
3
  computeScanResult,
4
4
  runScan
5
- } from "./chunk-5YPLWCOT.js";
5
+ } from "./chunk-OQXPOPP6.js";
6
6
  export {
7
7
  computeScanResult,
8
8
  runScan
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "equall-cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Open-source accessibility scoring CLI — aggregates axe-core, eslint-plugin-jsx-a11y, and more into a unified score.",
5
5
  "keywords": [
6
6
  "accessibility",
@@ -51,12 +51,14 @@
51
51
  "@types/jsdom": "^21.1.7",
52
52
  "@types/node": "^22.0.0",
53
53
  "axe-core": "^4.10.0",
54
+ "cheerio": "^1.2.0",
54
55
  "commander": "^12.1.0",
55
56
  "eslint": "^9.0.0",
56
57
  "eslint-plugin-jsx-a11y": "^6.10.0",
57
58
  "globby": "^14.0.0",
58
59
  "jsdom": "^25.0.0",
59
60
  "ora": "^9.3.0",
61
+ "text-readability": "^1.1.1",
60
62
  "tsup": "^8.0.0",
61
63
  "tsx": "^4.0.0",
62
64
  "typescript": "^5.9.3",