diff --git a/.babelrc b/.babelrc index 36ace527..d4da6841 100644 --- a/.babelrc +++ b/.babelrc @@ -5,7 +5,7 @@ { "targets": { "node": [ - "8" + "10" ] } } diff --git a/.gitignore b/.gitignore index 987f84b4..50cf2600 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,5 @@ website/site .DS_Store ._* Thumbs.db + +.history diff --git a/.snyk b/.snyk deleted file mode 100644 index 371e80b4..00000000 --- a/.snyk +++ /dev/null @@ -1,15 +0,0 @@ -# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. -version: v1.10.1 -ignore: {} -# patches apply the minimum changes required to fix a vulnerability -patch: - 'npm:debug:20170905': - - extract-zip > debug: - patched: '2017-10-27T13:22:41.480Z' - - puppeteer > extract-zip > debug: - patched: '2017-10-27T13:22:41.480Z' - 'npm:ms:20170412': - - extract-zip > debug > ms: - patched: '2017-10-27T13:22:41.480Z' - - puppeteer > extract-zip > debug > ms: - patched: '2017-10-27T13:22:41.480Z' diff --git a/.travis.yml b/.travis.yml index 74c4825c..5945cdcd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,17 +2,23 @@ language: node_js node_js: - node - lts/* - - 8 + - 10 +#services: xvfb addons: apt: packages: - xvfb before_install: - - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.2.1 + - curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.22.4 - export PATH="$HOME/.yarn/bin:$PATH" +before_script: + - yarn patchJestForTravis script: # - yarn snyk test - xvfb-run yarn test --runInBand + - xvfb-run yarn test-cli --runInBand +# - xvfb-run yarn test-electron --runInBand +# - xvfb-run yarn test-electron-cli --runInBand branches: only: - master diff --git a/jest.config-cli.js b/jest.config-cli.js new file mode 100644 index 00000000..94d2c909 --- /dev/null +++ b/jest.config-cli.js @@ -0,0 +1,7 @@ +const common = require('./jest.config-common'); +module.exports = { + ...common, + testMatch: [ + '/tests/__tests__/cli.test.js', + ], +}; diff --git a/jest.config-common.js b/jest.config-common.js new file mode 100644 index 00000000..28a7db42 --- /dev/null +++ b/jest.config-common.js @@ -0,0 +1,18 @@ +module.exports = { + verbose: true, + testEnvironment: 'node', + setupFilesAfterEnv: ['/tests/jest-setup.js'], + testPathIgnorePatterns: [ + '/node_modules/', + '/.history/', + '/website/', + '/scripts/', + '/resources/', + '/CompareAxeRunners/', + '/tests/data/', + ], + testMatch: [ + "/tests/__tests__/**/*.js", + "/packages/**/src/**/(*.)+test.js", + ], +}; diff --git a/jest.config-electron.js b/jest.config-electron.js new file mode 100644 index 00000000..197554be --- /dev/null +++ b/jest.config-electron.js @@ -0,0 +1,8 @@ +const common = require('./jest.config-common'); +module.exports = { + ...common, + runner: '@jest-runner/electron/main', + testPathIgnorePatterns: common.testPathIgnorePatterns.concat([ + '/tests/__tests__/cli', + ]), +}; diff --git a/jest.config-puppeteer.js b/jest.config-puppeteer.js new file mode 100644 index 00000000..1eb4fe7f --- /dev/null +++ b/jest.config-puppeteer.js @@ -0,0 +1,7 @@ +const common = require('./jest.config-common'); +module.exports = { + ...common, + testPathIgnorePatterns: common.testPathIgnorePatterns.concat([ + '/tests/__tests__/cli', + ]), +}; diff --git a/lerna.json b/lerna.json index d716402d..ba24fd28 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { - "lerna": "2.5.1", - "version": "1.1.1", + "lerna": "4.0.0", + "version": "1.2.0-beta.15", "npmClient": "yarn", "useWorkspaces": true } diff --git a/package.json b/package.json index 251d3fae..8701c73b 100644 --- a/package.json +++ b/package.json @@ -4,54 +4,65 @@ "workspaces": [ "packages/*" ], + "version": "1.2.0-beta.15", + "engines": { + "node": ">=10.0.0", + "yarn": "^1.22.5", + "npm": ">=6.14.12" + }, "devDependencies": { - "@daisy/jest-env-puppeteer": "^1.0.0", - "@daisy/jest-puppeteer": "^1.0.0", - "babel-core": "^6.0.0", - "babel-jest": "^21.2.0", - "babel-preset-env": "^1.6.0", + "@daisy/jest-env-puppeteer": "^1.2.0-beta.15", + "@daisy/jest-puppeteer": "^1.2.0-beta.15", + "@jest-runner/electron": "^3.0.1", + "babel-core": "^6.26.3", + "babel-jest": "^26.6.3", + "babel-preset-env": "^1.7.0", "babel-register": "^6.26.0", - "chalk": "^2.3.0", - "cross-env": "^5.2.0", - "cross-spawn": "^5.1.0", - "eslint": "^3.19.0", - "eslint-config-airbnb-base": "^11.2.0", - "eslint-plugin-import": "^2.3.0", - "glob": "^7.1.2", + "chalk": "^4.1.0", + "cpy-cli": "^3.1.1", + "cross-env": "^7.0.3", + "cross-spawn": "^7.0.3", + "glob": "^7.1.6", "i18next-json-sync": "^2.3.1", - "jest": "21.3.0-beta.10", - "lerna": "2.8", - "micromatch": "^3.1.4", - "mkdirp": "^0.5.1", - "rimraf": "^2.6.1", - "snyk": "^1.56.0", - "standard-version": "^4.2.0", - "strip-ansi": "^4.0.0", - "uglify-js": "^3.0.8", - "watch": "^1.0.2" + "jest": "^26.6.3", + "lerna": "^4.0.0", + "micromatch": "^4.0.2", + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2", + "strip-ansi": "^6.0.0" }, "scripts": { + "axe-dev": "cpy \"../axe-core_DAISY/axe.js\" \"node_modules/@daisy/axe-core-for-ace/\" && cpy \"../axe-core_DAISY/axe.min.js\" \"node_modules/@daisy/axe-core-for-ace/\" && cpy \"../axe-core_DAISY/axe.d.ts\" \"node_modules/@daisy/axe-core-for-ace/\" && cpy \"../axe-core_DAISY/locales/*.*\" \"node_modules/@daisy/axe-core-for-ace/locales/\" && cpy \"../axe-core_DAISY/package.json\" \"node_modules/@daisy/axe-core-for-ace/\"", "ace": "node ./packages/ace-cli/bin/ace.js", - "clean": "yarn run clean-libs", + "ace-electron": "electron ./packages/ace-axe-runner-electron/lib/cli.js", + "clean": "yarn clean-libs", "clean-libs": "rimraf packages/*/lib", "clean-node-modules": "rimraf packages/*/node_modules && rimraf node_modules", - "clean-all": "yarn run clean-libs && yarn run clean-node-modules", - "prebuild": "yarn run clean-libs", + "clean-all": "yarn clean-libs && yarn clean-node-modules", + "prebuild": "yarn clean-libs", "build": "cross-env VERBOSE=1 node ./scripts/build.js", "docs": "echo docs script not implemented", "lint": "echo lint script not implemented", - "postinstall": "yarn run build", - "test": "jest", - "watch": "yarn run build && node ./scripts/watch.js", + "postinstall": "yarn npmVersionsCheck && yarn build && yarn patchElectronJestRunner", + "patchJestForTravis": "yarn patchJestForTravis1 && yarn patchJestForTravis2", + "patchJestForTravis1": "node scripts/replace-in-file.js node_modules/jest-environment-node/build/index.js \"function _fakeTimers\\(\\) {\" \"function _fakeTimersLegacyFakeTimers() { const data = require('@jest/fake-timers/build/legacyFakeTimers'); _fakeTimersLegacyFakeTimers = function () { return data; }; return data; } function _fakeTimersModernFakeTimers() { const data = require('@jest/fake-timers/build/modernFakeTimers'); _fakeTimersModernFakeTimers = function () { return data; }; return data; } function _fakeTimers() {\"", + "patchJestForTravis2": "node scripts/replace-in-file.js node_modules/jest-environment-node/build/index.js \"_fakeTimers\\(\\).LegacyFakeTimers\" \"_fakeTimersLegacyFakeTimers().default || _fakeTimersLegacyFakeTimers()\" && node scripts/replace-in-file.js node_modules/jest-environment-node/build/index.js \"_fakeTimers\\(\\).ModernFakeTimers\" \"_fakeTimersModernFakeTimers().default || _fakeTimersModernFakeTimers()\"", + "npmVersionsCheck": "node ./scripts/npm-versions-check.js", + "patchElectronJestRunner": "yarn patchElectronJestRunner1", + "patchElectronJestRunner1": "echo \";_electron.app.allowRendererProcessReuse = true;\" >> \"./node_modules/@jest-runner/electron/build/electron_process_injected_code.js\"", + "patchElectronJestRunner2": "node scripts/replace-in-file.js \"./node_modules/jest-runner/node_modules/jest-runtime/build/index.js\" \"_defineProperty\\(this, '_hasWarnedAboutRequireCacheModification', false\\);\" \"_defineProperty(this, '_hasWarnedAboutRequireCacheModification', true);\"", + "test": "cross-env JEST_TESTS=1 jest --config=jest.config-puppeteer.js --runInBand --bail=1 --no-cache", + "test-cli": "cross-env JEST_TESTS=1 jest --config=jest.config-cli.js --runInBand --bail=1 --no-cache", + "test-electron": "cross-env JEST_TESTS=1 AXE_ELECTRON_RUNNER=true jest --config=jest.config-electron.js --runInBand --bail=1 --no-cache", + "test-electron-cli": "cross-env JEST_TESTS=1 AXE_ELECTRON_RUNNER=true jest --config=jest.config-cli.js --runInBand --bail=1 --no-cache", + "test-all": "yarn test && yarn test-cli && yarn test-electron && yarn test-electron-cli", + "watch": "yarn build && node ./scripts/watch.js", "i18n-sort": "node ./scripts/locales-sort.js", "i18n-scan-ace-report": "node ./scripts/translate-scan.js \"packages/ace-report/src\" \"packages/ace-report/src/l10n/locales/temp.json\" && sync-i18n --files 'packages/ace-report/src/l10n/locales/*.json' --primary temp --languages en fr pt_BR es da --space 4 --finalnewline --newkeysempty && rimraf \"packages/ace-report/src/l10n/locales/temp.json\"", "i18n-scan-ace-report-axe": "node ./scripts/translate-scan.js \"packages/ace-report-axe/src\" \"packages/ace-report-axe/src/l10n/locales/temp.json\" && sync-i18n --files 'packages/ace-report-axe/src/l10n/locales/*.json' --primary temp --languages en fr pt_BR es da --space 4 --finalnewline --newkeysempty && rimraf \"packages/ace-report-axe/src/l10n/locales/temp.json\"", "i18n-scan-ace-core": "node ./scripts/translate-scan.js \"packages/ace-core/src\" \"packages/ace-core/src/l10n/locales/temp.json\" && sync-i18n --files 'packages/ace-core/src/l10n/locales/*.json' --primary temp --languages en fr pt_BR es da --space 4 --finalnewline --newkeysempty && rimraf \"packages/ace-core/src/l10n/locales/temp.json\"", "i18n-scan": "npm run i18n-scan-ace-report && npm run i18n-scan-ace-report-axe && npm run i18n-scan-ace-core", - "i18n-check": "sync-i18n --files 'packages/**/src/l10n/locales/*.json' --primary en --languages fr pt_BR es da --space 4 --finalnewline --newkeysempty" - }, - "jest": { - "setupTestFrameworkScriptFile": "/tests/jest-setup.js", - "testEnvironment": "node" + "i18n-check": "sync-i18n --files 'packages/**/src/l10n/locales/*.json' --primary en --languages fr pt_BR es da --space 4 --finalnewline --newkeysempty", + "ace-app-prepare": "rm -f yarn.lock && rm -rf packages/*/lib && rm -rf packages/*/node_modules && rm -rf node_modules && yarn install && rm -rf packages/*/node_modules/@daisy && rm -rf node_modules/@daisy && yarn build && yarn upgrade && yarn patchElectronJestRunner && yarn npmVersionsCheck && git status && git --no-pager diff" } } diff --git a/packages/ace-axe-runner-electron/bin/ace.js b/packages/ace-axe-runner-electron/bin/ace.js new file mode 100644 index 00000000..3f63e88e --- /dev/null +++ b/packages/ace-axe-runner-electron/bin/ace.js @@ -0,0 +1,33 @@ +#!/usr/bin/env node + +var electron = require('electron') + +var proc = require('child_process') + +var path = require('path') + +// console.log(process.argv); +// console.log(process.cwd()); +// console.log(__dirname); +var args = [].concat(path.resolve(__dirname, "../lib/cli.js"), process.argv.slice(2)) +// console.log(args); + +var child = proc.spawn(electron, args, { stdio: 'inherit', windowsHide: false }) +child.on('close', function (code, signal) { + if (code === null) { + console.error(electron, 'exited with signal', signal) + process.exit(1) + } + process.exit(code) +}) + +const handleTerminationSignal = function (signal) { + process.on(signal, function signalHandler () { + if (!child.killed) { + child.kill(signal) + } + }) +} + +handleTerminationSignal('SIGINT') +handleTerminationSignal('SIGTERM') diff --git a/packages/ace-axe-runner-electron/package.json b/packages/ace-axe-runner-electron/package.json new file mode 100644 index 00000000..35656580 --- /dev/null +++ b/packages/ace-axe-runner-electron/package.json @@ -0,0 +1,40 @@ +{ + "name": "@daisy/ace-axe-runner-electron", + "version": "1.2.0-beta.15", + "engines": { + "node": ">=10.0.0", + "yarn": "^1.22.5", + "npm": ">=6.14.12" + }, + "description": "Electron-based Axe runner for Ace", + "author": { + "name": "DAISY developers", + "organization": "DAISY Consortium", + "url": "http://www.daisy.org/" + }, + "repository": { + "type": "git", + "url": "https://github.com/daisy/ace", + "directory": "packages/ace-axe-runner-electron" + }, + "bugs": { + "url": "https://github.com/daisy/ace/issues" + }, + "license": "MIT", + "main": "lib/index.js", + "dependencies": { + "@daisy/ace-cli-shared": "^1.2.0-beta.15", + "express": "^4.17.1", + "portfinder": "^1.0.28", + "selfsigned": "^1.10.8", + "uuid": "^8.3.2" + }, + "devDependencies": { + "electron": "^12.0.2", + "json": "^10.0.0", + "json-diff": "^0.5.4" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/ace-axe-runner-electron/src/cli.js b/packages/ace-axe-runner-electron/src/cli.js new file mode 100644 index 00000000..ef04b1af --- /dev/null +++ b/packages/ace-axe-runner-electron/src/cli.js @@ -0,0 +1,83 @@ +'use strict'; + +const electron = require('electron'); +const app = electron.app; +// const ipcMain = electron.ipcMain; +// const ipcRenderer = electron.ipcRenderer; + +// Removes the deprecation warning message in the console +// https://github.com/electron/electron/issues/18397 +app.allowRendererProcessReuse = true; + +const EventEmitter = require('events'); +class ElectronMockMainRendererEmitter extends EventEmitter {} +const eventEmmitter = new ElectronMockMainRendererEmitter(); +eventEmmitter.send = eventEmmitter.emit; +eventEmmitter.ace_notElectronIpcMainRenderer = true; + +const CONCURRENT_INSTANCES = 4; // same as the Puppeteer Axe runner + +const axeRunnerElectronFactory = require('@daisy/ace-axe-runner-electron'); +const axeRunner = axeRunnerElectronFactory.createAxeRunner(eventEmmitter, CONCURRENT_INSTANCES); + +const prepareLaunch = require('./init').prepareLaunch; +prepareLaunch(eventEmmitter, CONCURRENT_INSTANCES); + +const cli = require('@daisy/ace-cli-shared'); + +const LOG_DEBUG = false; +const ACE_LOG_PREFIX = "[ACE-AXE]"; + +if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner CLI launch...`); + +// let win; +// app.whenReady().then(() => { +app.on('ready', async () => { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner CLI app ready.`); + + // win = new BrowserWindow( + // { + // show: false, + // webPreferences: { + // allowRunningInsecureContent: false, + // contextIsolation: false, + // nodeIntegration: true, + // nodeIntegrationInWorker: false, + // sandbox: false, + // webSecurity: true, + // webviewTag: false, + // } + // } + // ); + // // win.maximize(); + // // let sz = win.getSize(); + // // const sz0 = sz[0]; + // // const sz1 = sz[1]; + // // win.unmaximize(); + // // // open a window that's not quite full screen ... makes sense on mac, anyway + // // win.setSize(Math.min(Math.round(sz0 * .75),1200), Math.min(Math.round(sz1 * .85), 800)); + // // // win.setPosition(Math.round(sz[0] * .10), Math.round(sz[1] * .10)); + // // win.setPosition(Math.round(sz0*0.5-win.getSize()[0]*0.5), Math.round(sz1*0.5-win.getSize()[1]*0.5)); + // // win.show(); + + // win.loadURL(`file://${__dirname}/index.html`); + // win.on('closed', function () { + // if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner win closed.`); + // }); + + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner run...`); + await cli.run(axeRunner, app.exit, (typeof process.env.JEST_TESTS !== "undefined" ? "ace-tests-cli-electron.log" : "ace-cli-electron.log")); // const exitCode = app.quit(); +}); + +app.on('activate', function () { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner app activate.`); +}); +app.on('window-all-closed', function () { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner app window-all-closed.`); +}); +app.on('before-quit', function() { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner app before-quit.`); +}); +app.on('quit', () => { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner app quit.`); +}); diff --git a/packages/ace-axe-runner-electron/src/index.js b/packages/ace-axe-runner-electron/src/index.js new file mode 100644 index 00000000..71631710 --- /dev/null +++ b/packages/ace-axe-runner-electron/src/index.js @@ -0,0 +1,119 @@ +'use strict'; + +const LOG_DEBUG = false; +const ACE_LOG_PREFIX = "[ACE-AXE]"; + +function createAxeRunner(eventEmmitter, CONCURRENT_INSTANCES) { + + return { + concurrency: CONCURRENT_INSTANCES, + launch: function () { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner will launch ...`); + + return new Promise((resolve, reject) => { + // ipcRenderer + const callback = (event, arg) => { + const payload = eventEmmitter.ace_notElectronIpcMainRenderer ? event : arg; + // const sender = eventEmmitter.ace_notElectronIpcMainRenderer ? eventEmmitter : event.sender; + + if (eventEmmitter.ace_notElectronIpcMainRenderer) { + eventEmmitter.removeListener('AXE_RUNNER_LAUNCH_', callback); + } + + if (payload.ok !== null && typeof payload.ok !== "undefined") { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner did launch OK.`); + resolve(payload.ok); + } else { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner did launch FAIL.`); + console.log(payload.err); + reject(payload.err); + } + }; + if (eventEmmitter.ace_notElectronIpcMainRenderer) { + eventEmmitter.on('AXE_RUNNER_LAUNCH_', callback); + } else { + eventEmmitter.once('AXE_RUNNER_LAUNCH_', callback); + } + + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner about to launch ...`); + // ipcRenderer + eventEmmitter.send('AXE_RUNNER_LAUNCH', {}); + }); + }, + close: function () { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner will close ...`); + + // // ipcRenderer + // eventEmmitter.send('AXE_RUNNER_CLOSE', {}); + // return Promise.resolve(); + + return new Promise((resolve, reject) => { + // ipcRenderer + const callback = (event, arg) => { + const payload = eventEmmitter.ace_notElectronIpcMainRenderer ? event : arg; + // const sender = eventEmmitter.ace_notElectronIpcMainRenderer ? eventEmmitter : event.sender; + + if (eventEmmitter.ace_notElectronIpcMainRenderer) { + eventEmmitter.removeListener('AXE_RUNNER_CLOSE_', callback); + } + + if (payload.ok !== null && typeof payload.ok !== "undefined") { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner did close OK.`); + resolve(payload.ok); + } else { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner did close FAIL.`); + console.log(payload.err); + reject(payload.err); + } + }; + if (eventEmmitter.ace_notElectronIpcMainRenderer) { + eventEmmitter.on('AXE_RUNNER_CLOSE_', callback); + } else { + eventEmmitter.once('AXE_RUNNER_CLOSE_', callback); + } + + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner about to close ...`); + // ipcRenderer + eventEmmitter.send('AXE_RUNNER_CLOSE', {}); + }); + }, + run: function (url, scripts, scriptContents, basedir) { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner will run ... ${url}`); + + return new Promise((resolve, reject) => { + // ipcRenderer + const callback = (event, arg) => { + const payload = eventEmmitter.ace_notElectronIpcMainRenderer ? event : arg; + // const sender = eventEmmitter.ace_notElectronIpcMainRenderer ? eventEmmitter : event.sender; + + if (payload.url === url) { + eventEmmitter.removeListener('AXE_RUNNER_RUN_', callback); + + if (payload.ok !== null && typeof payload.ok !== "undefined") { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner did run OK. ${url} ${payload.url}`); + resolve(payload.ok); + } else { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner did run FAIL. ${url} ${payload.url}`); + console.log(payload.err); + reject(payload.err); + } + } else { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner received AXE_RUNNER_RUN_ but filter out: ${url} ${payload.url}`); + } + }; + eventEmmitter.on('AXE_RUNNER_RUN_', callback); + + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner about to run ... ${url}`); + // ipcRenderer + eventEmmitter.send('AXE_RUNNER_RUN', { + url, + scripts, + scriptContents, + basedir, + }); + }); + } + }; +} + +module.exports = { createAxeRunner }; \ No newline at end of file diff --git a/packages/ace-axe-runner-electron/src/init.js b/packages/ace-axe-runner-electron/src/init.js new file mode 100644 index 00000000..b1b78f41 --- /dev/null +++ b/packages/ace-axe-runner-electron/src/init.js @@ -0,0 +1,793 @@ +'use strict'; + +const path = require('path'); +const fs = require('fs'); +const url = require('url'); + +const electron = require('electron'); +const app = electron.app; +const session = electron.session; +const BrowserWindow = electron.BrowserWindow; +// const webContents = electron.webContents; +// const ipcMain = electron.ipcMain; + +const fsOriginal = require('original-fs'); + +const express = require('express'); +const portfinder = require('portfinder'); +// const http = require('http'); +const https = require('https'); + +const generateSelfSignedData = require('./selfsigned').generateSelfSignedData; + +const isDev = process && process.env && (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true'); +const showWindow = false; + +const LOG_DEBUG_URLS = process.env.LOG_DEBUG_URLS === "1"; + +const LOG_DEBUG = false; +const ACE_LOG_PREFIX = "[ACE-AXE]"; + +const SESSION_PARTITION = "persist:axe"; + +const HTTP_QUERY_PARAM = "AXE_RUNNER"; + +let expressApp; +let httpServer; +let port; +let ip; +let proto; +let rootUrl; + +let httpServerStartWasRequested = false; +let httpServerStarted = false; + +let browserWindows = undefined; + +const jsCache = {}; + +let _firstTimeInit = true; + +let iHttpReq = 0; + +function loadUrl(browserWindow) { + browserWindow.ace__loadUrlPending = undefined; + + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner LOAD URL ... ${browserWindow.ace__currentUrlOriginal} => ${rootUrl}${browserWindow.ace__currentUrl}`); + + browserWindow.ace__TIME_loadURL = process.hrtime(); + browserWindow.ace__TIME_executeJavaScript = 0; + + if (browserWindow.ace__timeout) { + clearTimeout(browserWindow.ace__timeout); + } + browserWindow.ace__timeout = undefined; + + const options = {}; // { extraHeaders: 'pragma: no-cache\n' }; + const uareel = `${rootUrl}${browserWindow.ace__currentUrl}?${HTTP_QUERY_PARAM}=${iHttpReq++}`; + if (LOG_DEBUG_URLS) { + console.log("======>>>>>> URL TO LOAD"); + console.log(uareel); + } + browserWindow.loadURL(uareel, options); + + const MILLISECONDS_TIMEOUT_INITIAL = 10000; // 10s max to load the window's web contents + const MILLISECONDS_TIMEOUT_EXTENSION = 480000; // 480s (8mn) max to load + execute Axe checkers + const timeoutFunc = () => { + if (browserWindow.ace__replySent) { + browserWindow.ace__timeout = undefined; + browserWindow.ace__timeoutExtended = false; + return; + } + + const timeElapsed1 = process.hrtime(browserWindow.ace__TIME_loadURL); + const timeElapsed2 = browserWindow.ace__TIME_executeJavaScript ? process.hrtime(browserWindow.ace__TIME_executeJavaScript) : [0, 0]; + + if (browserWindow.ace__timeoutExtended) { + browserWindow.ace__replySent = true; + browserWindow.ace__timeout = undefined; + + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner ${MILLISECONDS_TIMEOUT_INITIAL + MILLISECONDS_TIMEOUT_EXTENSION}ms timeout [[FAIL]] (${timeElapsed1[0]} seconds + ${timeElapsed1[1]} nanoseconds) (${timeElapsed2[0]} seconds + ${timeElapsed2[1]} nanoseconds) ${browserWindow.ace__currentUrlOriginal} => ${rootUrl}${browserWindow.ace__currentUrl}`); + browserWindow.ace__eventEmmitterSender.send("AXE_RUNNER_RUN_", { + err: `Timeout :( ${MILLISECONDS_TIMEOUT_INITIAL + MILLISECONDS_TIMEOUT_EXTENSION}ms (${timeElapsed1[0]} seconds + ${timeElapsed1[1]} nanoseconds) (${timeElapsed2[0]} seconds + ${timeElapsed2[1]} nanoseconds)`, + url: browserWindow.ace__currentUrlOriginal + }); + } else { + + if (!browserWindow.ace__TIME_executeJavaScript) { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner ${MILLISECONDS_TIMEOUT_INITIAL}ms timeout [[RELOAD]] (${timeElapsed1[0]} seconds + ${timeElapsed1[1]} nanoseconds) (${timeElapsed2[0]} seconds + ${timeElapsed2[1]} nanoseconds) ${browserWindow.ace__currentUrlOriginal} => ${rootUrl}${browserWindow.ace__currentUrl}`); + + browserWindow.ace__TIME_loadURL = process.hrtime(); + browserWindow.ace__TIME_executeJavaScript = 0; + browserWindow.webContents.reload(); + browserWindow.ace__timeoutExtended = false; + browserWindow.ace__timeout = setTimeout(timeoutFunc, MILLISECONDS_TIMEOUT_INITIAL); + return; + } + + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner ${MILLISECONDS_TIMEOUT_INITIAL}ms timeout [[EXTEND]] (${timeElapsed1[0]} seconds + ${timeElapsed1[1]} nanoseconds) (${timeElapsed2[0]} seconds + ${timeElapsed2[1]} nanoseconds) ${browserWindow.ace__currentUrlOriginal} => ${rootUrl}${browserWindow.ace__currentUrl}`); + + browserWindow.ace__timeoutExtended = true; + browserWindow.ace__timeout = setTimeout(timeoutFunc, MILLISECONDS_TIMEOUT_EXTENSION); + } + }; + browserWindow.ace__timeoutExtended = false; + browserWindow.ace__timeout = setTimeout(timeoutFunc, MILLISECONDS_TIMEOUT_INITIAL); +} + +function poolCheck() { + for (const browserWindow of browserWindows) { + if (browserWindow.ace__loadUrlPending) { + loadUrl(browserWindow); + } + } +} + +function axeRunnerInit(eventEmmitter, CONCURRENT_INSTANCES) { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunnerInit ...`); + + if (!axeRunnerInit.todo) { + return; + } + axeRunnerInit.todo = false; + + const firstTimeInit = _firstTimeInit; + _firstTimeInit = false; + + browserWindows = []; + for (let i = 0; i < CONCURRENT_INSTANCES; i++) { + + let browserWindow = new BrowserWindow({ + show: showWindow, + webPreferences: { + devTools: isDev && showWindow, + title: "Axe Electron runner", + allowRunningInsecureContent: false, + contextIsolation: false, + nodeIntegration: false, + nodeIntegrationInWorker: false, + sandbox: false, + webSecurity: true, + webviewTag: false, + partition: SESSION_PARTITION + }, + }); + + browserWindow.setSize(1024, 768); + browserWindow.setPosition(0, 0); + + browserWindow.webContents.session.webRequest.onBeforeRequest({ + urls: [], + }, (details, callback) => { + if (details.url + && /^https?:\/\//.test(details.url) + && ((rootUrl && !details.url.startsWith(rootUrl)) || (!rootUrl && !/^https?:\/\/127.0.0.1/.test(details.url))) + ) { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} onBeforeRequest BLOCK: ${details.url} (${rootUrl})`); + + // causes ERR_BLOCKED_BY_CLIENT -20 did-fail-load + callback({ + cancel: true, + // redirectURL: "about:blank", + }); + return; + } + + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} onBeforeRequest OKAY: ${details.url}`); + callback({ cancel: false }); + }); + + // browserWindow.maximize(); + // let sz = browserWindow.getSize(); + // const sz0 = sz[0]; + // const sz1 = sz[1]; + // browserWindow.unmaximize(); + // browserWindow.setSize(Math.min(Math.round(sz0 * .75), 1200), Math.min(Math.round(sz1 * .85), 800)); + // // browserWindow.setPosition(Math.round(sz[0] * .10), Math.round(sz[1] * .10)); + // browserWindow.setPosition(Math.round(sz0 * 0.5 - browserWindow.getSize()[0] * 0.5), Math.round(sz1 * 0.5 - browserWindow.getSize()[1] * 0.5)); + // if (showWindow) { + // browserWindow.show(); + // } + + if (typeof browserWindow.webContents.audioMuted !== "undefined") { + browserWindow.webContents.audioMuted = true; + } else { + browserWindow.webContents.setAudioMuted(true); + } + + browserWindow.ace__poolIndex = browserWindows.length; + + browserWindows.push(browserWindow); + } + + if (!firstTimeInit) { + return; + } + + app.on("certificate-error", (event, webContents, u, error, certificate, callback) => { + if (u.indexOf(`${rootUrl}/`) === 0) { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} HTTPS cert error OKAY ${u}`); + callback(true); + return; + } + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} HTTPS cert error FAIL ${u}`); + callback(false); + }); + + // const filter = { urls: ["*", "*://*/*"] }; + + // const onHeadersReceivedCB = (details, callback) => { + // if (!details.url) { + // callback({}); + // return; + // } + + // if (details.url.indexOf(`${rootUrl}/`) === 0) { + // if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} CSP ${details.url}`); + // callback({ + // // responseHeaders: { + // // ...details.responseHeaders, + // // "Content-Security-Policy": + // // [`default-src 'self' 'unsafe-inline' 'unsafe-eval' data: http: https: ${rootUrl}`], + // // }, + // }); + // } else { + // if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} !CSP ${details.url}`); + // callback({}); + // } + // }; + + const setCertificateVerifyProcCB = (request, callback) => { + + if (request.hostname === ip) { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} HTTPS cert verify OKAY ${request.hostname}`); + callback(0); // OK + return; + } + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} HTTPS cert verify FALLBACK ${request.hostname}`); + callback(-3); // Chromium + // callback(-2); // Fail + }; + + const sess = session.fromPartition(SESSION_PARTITION, { cache: true }); // || session.defaultSession; + + if (sess) { + // sess.webRequest.onHeadersReceived(filter, onHeadersReceivedCB); + // sess.webRequest.onBeforeSendHeaders(filter, onBeforeSendHeadersCB); + sess.setCertificateVerifyProc(setCertificateVerifyProcCB); + } + + // ipcMain + eventEmmitter.on('AXE_RUNNER_CLOSE', (event, arg) => { + // const payload = eventEmmitter.ace_notElectronIpcMainRenderer ? event : arg; + const sender = eventEmmitter.ace_notElectronIpcMainRenderer ? eventEmmitter : event.sender; + + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner closing ...`); + + axeRunnerInit.todo = true; + + if (browserWindows) { + if (!(isDev && showWindow)) { + for (let i = browserWindows.length - 1; i >= 0; i--) { + try { + browserWindows[i].close(); + } catch (err) { + if (LOG_DEBUG) console.log(err); + } + browserWindows.splice(0, -1); // remove last + } + } + } + browserWindows = undefined; + + httpServerStarted = false; + httpServerStartWasRequested = false; + + if (httpServer) { + httpServer.close(); + httpServer = undefined; + } + + let _timeOutID = setTimeout(() => { + _timeOutID = undefined; + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} xxxx axeRunner timeout.`); + closed(); + }, 3000); + + let _closed = false; + function closed() { + if (_timeOutID) { + clearTimeout(_timeOutID); + _timeOutID = undefined; + } + if (_closed) { + return; + } + _closed = true; + + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner sending closed event ...`); + sender.send("AXE_RUNNER_CLOSE_", { + ok: true + }); + } + let _done = 0; + function done() { + _done++; + if (_done === 2) { + closed(); + } + } + + const sess = session.fromPartition(SESSION_PARTITION, { cache: true }); // || session.defaultSession; + if (sess) { + setTimeout(async () => { + try { + await sess.clearCache(); + } catch (err) { + if (LOG_DEBUG) console.log(err); + } + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} session cache cleared`); + done(); + + try { + await sess.clearStorageData({ + origin: "*", + quotas: [ + "temporary", + "persistent", + "syncable", + ], + storages: [ + "appcache", + "cookies", + "filesystem", + "indexdb", + // "localstorage", BLOCKS!? + "shadercache", + "websql", + "serviceworkers", + ], + }); + } catch (err) { + if (LOG_DEBUG) console.log(err); + } + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} session storage cleared`); + done(); + }, 0); + } + }); + + // ipcMain + eventEmmitter.on('AXE_RUNNER_RUN', (event, arg) => { + + const payload = eventEmmitter.ace_notElectronIpcMainRenderer ? event : arg; + const sender = eventEmmitter.ace_notElectronIpcMainRenderer ? eventEmmitter : event.sender; + + const basedir = payload.basedir; + const uarel = payload.url; + const scripts = payload.scripts; + const scriptContents = payload.scriptContents; + + if (LOG_DEBUG_URLS) { + console.log("######## URL 1"); + console.log(uarel); + } + // windows! file://C:\aa\bb\chapter.xhtml + const uarelObj = url.parse(uarel.replace(/\\/g, "/")); + const windowsDrive = uarelObj.hostname ? `${uarelObj.hostname.toUpperCase()}:` : ""; + if (LOG_DEBUG_URLS) { + console.log("######## URL 2"); + console.log(windowsDrive); + } + const bd = basedir.replace(/\\/g, "/"); + if (LOG_DEBUG_URLS) { + console.log("######## URL 3"); + console.log(uarelObj.pathname); + } + const full = (windowsDrive + decodeURI(uarelObj.pathname)); + if (LOG_DEBUG_URLS) { + console.log("######## URL 4"); + console.log(full); + } + let httpUrl = full.replace(bd, ""); + if (LOG_DEBUG_URLS) { + console.log("######## URL 5"); + console.log(httpUrl); + } + httpUrl = encodeURI(httpUrl); + if (LOG_DEBUG_URLS) { + console.log("######## URL 6"); + console.log(httpUrl); + } + + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner running ... ${basedir} --- ${uarel} => ${httpUrl}`); + + function poolPush() { + + const browserWindow = browserWindows.find((bw) => { + if (!bw.ace__loadUrlPending && + (!bw.ace__currentUrl || (bw.ace__currentUrl && bw.ace__replySent))) { + return bw; + } + return undefined; + }); + + if (!browserWindow) { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner xxxxx no free browser window in pool?! ${uarel} --- ${httpUrl}`); + setTimeout(() => { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner xxxxx trying another free browser window in pool ... ${uarel} --- ${httpUrl}`); + poolPush(); + }, 1000); + return; + } + + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner free browser window in pool: ${browserWindow.ace__poolIndex}`); + + browserWindow.ace__eventEmmitterSender = sender; + browserWindow.ace__replySent = false; + browserWindow.ace__timeout = undefined; + browserWindow.ace__previousUrl = browserWindow.ace__currentUrl; + browserWindow.ace__currentUrlOriginal = uarel; + browserWindow.ace__currentUrl = httpUrl; + + browserWindow.webContents.once("did-start-loading", () => { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner did-start-loading ${browserWindow.ace__poolIndex} ${browserWindow.ace__currentUrlOriginal} --- ${browserWindow.ace__currentUrl}`); + }); + // browserWindow.webContents.once("did-stop-loading", () => { + // if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner did-stop-loading ${browserWindow.ace__poolIndex} ${browserWindow.ace__currentUrlOriginal} --- ${browserWindow.ace__currentUrl}`); + // }); + browserWindow.webContents.once("did-fail-load", (event, errorCode, errorDescription, validatedURL, isMainFrame, frameProcessId, frameRoutingId) => { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner did-fail-load ${browserWindow.ace__poolIndex} ${browserWindow.ace__currentUrlOriginal} --- ${browserWindow.ace__currentUrl}`, "\n", `${errorCode} - ${errorDescription} - ${validatedURL} - ${isMainFrame} - ${frameProcessId} - ${frameRoutingId}`); + + // https://cs.chromium.org/chromium/src/net/base/net_error_list.h + if (errorCode == -20) { // ERR_BLOCKED_BY_CLIENT + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner ERR_BLOCKED_BY_CLIENT (ignore) ${browserWindow.ace__poolIndex} ${browserWindow.ace__currentUrlOriginal} --- ${browserWindow.ace__currentUrl}`); + return; + } + + if (browserWindow.ace__replySent) { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner WAS TIMEOUT! ${browserWindow.ace__poolIndex} ${browserWindow.ace__currentUrlOriginal} --- ${browserWindow.ace__currentUrl}`); + return; + } + + browserWindow.ace__replySent = true; + if (browserWindow.ace__timeout) { + clearTimeout(browserWindow.ace__timeout); + } + browserWindow.ace__timeout = undefined; + + browserWindow.ace__eventEmmitterSender.send("AXE_RUNNER_RUN_", { + err: `did-fail-load: ${errorCode} - ${errorDescription} - ${validatedURL} - ${isMainFrame} - ${frameProcessId} - ${frameRoutingId}`, + url: browserWindow.ace__currentUrlOriginal + }); + }); + // browserWindow.webContents.once("dom-ready", () => { // occurs early + // if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner dom-ready ${browserWindow.ace__poolIndex} ${browserWindow.ace__currentUrlOriginal} --- ${browserWindow.ace__currentUrl}`); + // }); + browserWindow.webContents.once("did-finish-load", () => { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner did-finish-load ${browserWindow.ace__poolIndex} ${browserWindow.ace__currentUrlOriginal} --- ${browserWindow.ace__currentUrl}`); + + browserWindow.ace__TIME_executeJavaScript = process.hrtime(); + + const js = ` +new Promise((resolve, reject) => { + window.daisy.ace.run((err, res) => { + if (err) { + reject(err); + return; + } + resolve(res); + }); +}).then(res => res).catch(err => { throw err; }); +`; + browserWindow.webContents.executeJavaScript(js, true) + .then((ok) => { + const timeElapsed = process.hrtime(browserWindow.ace__TIME_executeJavaScript); + + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner done. (${timeElapsed[0]} seconds + ${timeElapsed[1]} nanoseconds) ${browserWindow.ace__poolIndex} ${browserWindow.ace__currentUrlOriginal} --- ${browserWindow.ace__currentUrl}`); + // if (LOG_DEBUG) console.log(ok); + if (browserWindow.ace__replySent) { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner WAS TIMEOUT! ${browserWindow.ace__poolIndex} ${browserWindow.ace__currentUrlOriginal} --- ${browserWindow.ace__currentUrl}`); + return; + } + + browserWindow.ace__replySent = true; + if (browserWindow.ace__timeout) { + clearTimeout(browserWindow.ace__timeout); + } + browserWindow.ace__timeout = undefined; + + browserWindow.ace__eventEmmitterSender.send("AXE_RUNNER_RUN_", { + ok, + url: browserWindow.ace__currentUrlOriginal + }); + }) + .catch((err) => { + const timeElapsed = process.hrtime(browserWindow.ace__TIME_executeJavaScript); + + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner fail! (${timeElapsed[0]} seconds + ${timeElapsed[1]} nanoseconds) ${browserWindow.ace__poolIndex} ${browserWindow.ace__currentUrlOriginal} --- ${browserWindow.ace__currentUrl}`); + if (LOG_DEBUG) console.log(err); + if (browserWindow.ace__replySent) { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner WAS TIMEOUT! ${browserWindow.ace__poolIndex} ${browserWindow.ace__currentUrlOriginal} --- ${browserWindow.ace__currentUrl}`); + return; + } + + browserWindow.ace__replySent = true; + if (browserWindow.ace__timeout) { + clearTimeout(browserWindow.ace__timeout); + } + browserWindow.ace__timeout = undefined; + + browserWindow.ace__eventEmmitterSender.send("AXE_RUNNER_RUN_", { + err, + url: browserWindow.ace__currentUrlOriginal + }); + }); + }); + + if (httpServerStarted) { + loadUrl(browserWindow); + } else { + browserWindow.ace__loadUrlPending = httpUrl; + } + } + + if (!httpServerStartWasRequested) { // lazy init + httpServerStartWasRequested = true; + + poolPush(); + + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner starting server ...`); + + startAxeServer(basedir, scripts, scriptContents).then(() => { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner server started`); + httpServerStarted = true; + + poolCheck(); + }).catch((err) => { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner server error`); + console.log(err); + browserWindow.ace__eventEmmitterSender.send("AXE_RUNNER_RUN_", { + err, + url: browserWindow.ace__currentUrlOriginal + }); + }); + } else { + poolPush(); + } + }); +} +axeRunnerInit.todo = true; + +const filePathsExpressStaticNotExist = {}; +function startAxeServer(basedir, scripts, scriptContents) { + + return new Promise((resolve, reject) => { + + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner startAxeServer...`); + + let scriptsMarkup = ""; + scriptContents.forEach((scriptCode) => { + scriptsMarkup += ``; + }); + scripts.forEach((scriptPath) => { + const filename = path.basename(scriptPath); + scriptsMarkup += ``; + }); + + expressApp = express(); + // expressApp.enable('strict routing'); + + // expressApp.use("/", (req, res, next) => { + // if (LOG_DEBUG) console.log("HTTP: " + req.url); + // next(); + // }); + + expressApp.basedir = basedir; + expressApp.use("/", (req, res, next) => { + + for (const scriptPath of scripts) { + const filename = path.basename(scriptPath); + if (req.url.endsWith(`${HTTP_QUERY_PARAM}/${filename}`)) { + let js = jsCache[scriptPath]; + if (!js) { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} HTTP loading ${scriptPath}`); + js = fs.readFileSync(scriptPath, { encoding: "utf8" }); + // if (LOG_DEBUG) console.log(js); + jsCache[scriptPath] = js; + } else { + // if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} HTTP already loaded ${scriptPath}`); + } + res.setHeader("Content-Type", "text/javascript"); + res.send(js); + return; + } + } + + if (req.query[HTTP_QUERY_PARAM]) { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} HTTP intercept ${req.url}`); + + if (LOG_DEBUG_URLS) { + console.log(">>>>>>>>>> URL 1"); + console.log(req.url); + } + const ptn = url.parse(req.url).pathname; + if (LOG_DEBUG_URLS) { + console.log(">>>>>>>>>> URL 2"); + console.log(ptn); + } + const pn = decodeURI(ptn); + if (LOG_DEBUG_URLS) { + console.log(">>>>>>>>>> URL 3"); + console.log(pn); + } + let fileSystemPath = path.join(expressApp.basedir, pn); + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} filepath to read: ${fileSystemPath}`); + if (!fs.existsSync(fileSystemPath)) { + fileSystemPath = pn; + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} filepath to read (corrected): ${fileSystemPath}`); + } + + let html = fs.readFileSync(fileSystemPath, { encoding: "utf8" }); + // if (LOG_DEBUG) console.log(html); + + if (html.match(/<\/head>/)) { + html = html.replace(/<\/head>/, `${scriptsMarkup}`); + } else if (html.match(/<\/body>/)) { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} HTML no ? (using ) ${req.url}`); + html = html.replace(/<\/body>/, `${scriptsMarkup}`); + } else { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} HTML neither nor ?! ${req.url}`); + } + + res.setHeader("Content-Type", "application/xhtml+xml"); + res.send(html); + return; + } + + next(); + }); + + if (isDev) { // handle WebInspector JS maps etc. + expressApp.use("/", (req, res, next) => { + // const url = new URL(`https://fake.org${req.url}`); + // const pathname = url.pathname; + const pathname = decodeURI(url.parse(req.url).pathname); + + const filePath = path.join(basedir, pathname); + if (filePathsExpressStaticNotExist[filePath]) { + res.status(404).send(filePathsExpressStaticNotExist[filePath]); + return; + } + fsOriginal.exists(filePath, (exists) => { + if (exists) { + fsOriginal.readFile(filePath, undefined, (err, data) => { + if (err) { + if (LOG_DEBUG) { + console.log(`${ACE_LOG_PREFIX} HTTP FAIL fsOriginal.exists && ERR ${basedir} + ${req.url} => ${filePath}`, err); + } + filePathsExpressStaticNotExist[filePath] = err.toString(); + res.status(404).send(filePathsExpressStaticNotExist[filePath]); + } else { + // if (LOG_DEBUG) { + // console.log(`${ACE_LOG_PREFIX} HTTP OK fsOriginal.exists ${basedir} + ${req.url} => ${filePath}`); + // } + next(); + // res.send(data); + } + }); + } else { + fs.exists(filePath, (exists) => { + if (exists) { + fs.readFile(filePath, undefined, (err, data) => { + if (err) { + if (LOG_DEBUG) { + console.log(`${ACE_LOG_PREFIX} HTTP FAIL !fsOriginal.exists && fs.exists && ERR ${basedir} + ${req.url} => ${filePath}`, err); + } + filePathsExpressStaticNotExist[filePath] = err.toString(); + res.status(404).send(filePathsExpressStaticNotExist[filePath]); + } else { + if (LOG_DEBUG) { + console.log(`${ACE_LOG_PREFIX} HTTP OK !fsOriginal.exists && fs.exists ${basedir} + ${req.url} => ${filePath}`); + } + next(); + // res.send(data); + } + }); + } else { + if (LOG_DEBUG) { + console.log(`${ACE_LOG_PREFIX} HTTP FAIL !fsOriginal.exists && !fs.exists ${basedir} + ${req.url} => ${filePath}`); + } + res.status(404).end(); + } + }); + } + }); + }); + } + + // https://expressjs.com/en/4x/api.html#express.static + const staticOptions = { + dotfiles: "ignore", + etag: true, + // fallthrough: false, + immutable: true, + // index: "index.html", + maxAge: "1d", + redirect: false, + // extensions: ["css", "otf"], + // setHeaders: (res, _path, _stat) => { + // // res.set('x-timestamp', Date.now()) + // setResponseCORS(res); + // }, + }; + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} HTTP static path ${basedir}`); + expressApp.use("/", express.static(basedir, staticOptions)); + + const startHttp = function () { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner generateSelfSignedData...`); + generateSelfSignedData().then((certData) => { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner generateSelfSignedData OK.`); + + httpServer = https.createServer({ key: certData.private, cert: certData.cert }, expressApp).listen(port, () => { + const p = httpServer.address().port; + + port = p; + ip = "127.0.0.1"; + proto = "https"; + rootUrl = `${proto}://${ip}:${port}`; + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} server URL ${rootUrl}`); + + resolve(); + }); + }).catch((err) => { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} generateSelfSignedData error!`); + if (LOG_DEBUG) console.log(err); + httpServer = expressApp.listen(port, () => { + const p = httpServer.address().port; + + port = p; + ip = "127.0.0.1"; + proto = "http"; + rootUrl = `${proto}://${ip}:${port}`; + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} server URL ${rootUrl}`); + + resolve(); + }); + }); + } + + portfinder.getPortPromise().then((p) => { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner HTTP port ${p}`); + port = p; + startHttp(); + }).catch((err) => { + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner HTTP port error!`); + console.log(err); + port = 3000; + startHttp(); + }); + }); +} + +function prepareLaunch(eventEmmitter, CONCURRENT_INSTANCES) { + + eventEmmitter.on('AXE_RUNNER_LAUNCH', (event, arg) => { + // const payload = eventEmmitter.ace_notElectronIpcMainRenderer ? event : arg; + const sender = eventEmmitter.ace_notElectronIpcMainRenderer ? eventEmmitter : event.sender; + + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner AXE_RUNNER_LAUNCH ...`); + + axeRunnerInit(eventEmmitter, CONCURRENT_INSTANCES); + + if (LOG_DEBUG) console.log(`${ACE_LOG_PREFIX} axeRunner sending launched event ...`); + sender.send("AXE_RUNNER_LAUNCH_", { + ok: true + }); + }); +} +module.exports = { prepareLaunch }; \ No newline at end of file diff --git a/packages/ace-axe-runner-electron/src/selfsigned.js b/packages/ace-axe-runner-electron/src/selfsigned.js new file mode 100644 index 00000000..9f23761e --- /dev/null +++ b/packages/ace-axe-runner-electron/src/selfsigned.js @@ -0,0 +1,33 @@ + +const selfsigned = require('selfsigned'); +const { v4: uuidv4 } = require('uuid'); + +function generateSelfSignedData() { + return new Promise((resolve, reject) => { + const opts = { + algorithm: "sha256", + // clientCertificate: true, + // clientCertificateCN: "KB insecure client", + days: 30, + extensions: [{ + altNames: [{ + type: 2, // DNSName + value: "localhost", + }], + name: "subjectAltName", + }], + }; + const rand = uuidv4(); + const attributes = [{ name: "commonName", value: "KB insecure server " + rand }]; + + selfsigned.generate(attributes, opts, (err, keys) => { + if (err) { + reject(err); + return; + } + + resolve(keys); + }); + }); +} +module.exports = { generateSelfSignedData }; \ No newline at end of file diff --git a/packages/ace-axe-runner-puppeteer/package.json b/packages/ace-axe-runner-puppeteer/package.json index dd06f53e..83382a92 100644 --- a/packages/ace-axe-runner-puppeteer/package.json +++ b/packages/ace-axe-runner-puppeteer/package.json @@ -1,6 +1,11 @@ { "name": "@daisy/ace-axe-runner-puppeteer", - "version": "1.1.0", + "version": "1.2.0-beta.15", + "engines": { + "node": ">=10.0.0", + "yarn": "^1.22.5", + "npm": ">=6.14.12" + }, "description": "Puppeteer-based Axe runner for Ace", "author": { "name": "DAISY developers", @@ -18,8 +23,8 @@ "license": "MIT", "main": "lib/index.js", "dependencies": { - "@daisy/puppeteer-utils": "^1.1.0", - "puppeteer": "^1.0.0" + "@daisy/puppeteer-utils": "^1.2.0-beta.15", + "puppeteer": "^8.0.0" }, "publishConfig": { "access": "public" diff --git a/packages/ace-axe-runner-puppeteer/src/index.js b/packages/ace-axe-runner-puppeteer/src/index.js index 1fa1f2ad..1b27cdea 100644 --- a/packages/ace-axe-runner-puppeteer/src/index.js +++ b/packages/ace-axe-runner-puppeteer/src/index.js @@ -6,6 +6,8 @@ const utils = require('@daisy/puppeteer-utils'); let _browser = undefined; +const isDev = process && process.env && (process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true'); + module.exports = { concurrency: 4, launch: async function() { @@ -14,13 +16,37 @@ module.exports = { args.push('--no-sandbox') } _browser = await puppeteer.launch({ args }); + return Promise.resolve(); }, close: async function() { await _browser.close(); + return Promise.resolve(); }, run: async function(url, scripts, scriptContents, basedir) { const page = await _browser.newPage(); + + if (isDev) { + page.on('console', msg => { + console.log(msg.text()); + // process.stdout.write(msg.text()); + }); + } + + await page.setRequestInterception(true); + + page.on('request', (request) => { + const url = request.url(); + if (url && /^https?:\/\//.test(url)) { + if (isDev) { + console.log(`============> RequestInterception URL abort: ${url}`); + } + request.abort(); + return; + } + request.continue(); + }); + await page.goto(url); await utils.addScriptContents(scriptContents, page); diff --git a/packages/ace-cli-shared/package.json b/packages/ace-cli-shared/package.json new file mode 100644 index 00000000..473649bf --- /dev/null +++ b/packages/ace-cli-shared/package.json @@ -0,0 +1,36 @@ +{ + "name": "@daisy/ace-cli-shared", + "version": "1.2.0-beta.15", + "engines": { + "node": ">=10.0.0", + "yarn": "^1.22.5", + "npm": ">=6.14.12" + }, + "description": "Ace by DAISY, an Accessibility Checker for EPUB", + "author": { + "name": "DAISY developers", + "organization": "DAISY Consortium", + "url": "http://www.daisy.org/" + }, + "repository": { + "type": "git", + "url": "https://github.com/daisy/ace", + "directory": "packages/ace-cli-shared" + }, + "bugs": { + "url": "https://github.com/daisy/ace/issues" + }, + "license": "MIT", + "main": "lib/index.js", + "dependencies": { + "@daisy/ace-config": "^1.2.0-beta.15", + "@daisy/ace-core": "^1.2.0-beta.15", + "@daisy/ace-logger": "^1.2.0-beta.15", + "@daisy/ace-meta": "^1.2.0-beta.15", + "meow": "^9.0.0", + "winston": "^3.3.3" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/ace-cli/src/defaults.json b/packages/ace-cli-shared/src/defaults.json similarity index 100% rename from packages/ace-cli/src/defaults.json rename to packages/ace-cli-shared/src/defaults.json diff --git a/packages/ace-cli-shared/src/index.js b/packages/ace-cli-shared/src/index.js new file mode 100755 index 00000000..87a92dd3 --- /dev/null +++ b/packages/ace-cli-shared/src/index.js @@ -0,0 +1,164 @@ +'use strict'; + +const fs = require('fs'); +const meow = require('meow'); +const path = require('path'); +const winston = require('winston'); + +const logger = require('@daisy/ace-logger'); +const ace = require('@daisy/ace-core'); + +const { config, paths } = require('@daisy/ace-config'); +const defaults = require('./defaults'); +const cliConfig = config.get('cli', defaults.cli); + +const pkg = require('@daisy/ace-meta/package'); + +const meowHelpMessage = ` + Usage: ace [options] + + Options: + + -h, --help output usage information + -v, --version output the version number + + -o, --outdir save final reports to the specified directory + -t, --tempdir specify a custom directory to store the temporary reports + -f, --force override any existing output file or directory + --subdir output reports to a sub-directory named after the input EPUB + + -V, --verbose display verbose output + -s, --silent do not display any output + + -l, --lang language code for localized messages (e.g. "fr"), default is "en" + Examples + $ ace -o out ~/Documents/book.epub`; +const meowOptions = { + autoHelp: false, + autoVersion: false, + version: pkg.version, + flags: { + force: { + alias: 'f', + type: 'boolean' + }, + help: { + alias: 'h' + }, + outdir: { + alias: 'o', + type: 'string' + }, + silent: { + alias: 's', + type: 'boolean' + }, + tempdir: { + alias: 't', + type: 'string' + }, + subdir: { + type: 'boolean' + }, + version: { + alias: 'v' + }, + verbose: { + alias: 'V', + type: 'boolean' + }, + lang: { + alias: 'l', + type: 'string' + } + } +}; +const cli = meow(meowHelpMessage, meowOptions); + +async function run(axeRunner, exit, logFileName) { + + if (cli.flags.help) { + cli.showHelp(0); + return; + } + + if (cli.flags.version) { + cli.showVersion(2); + return; + } + + let timeBegin = process.hrtime(); + function quit() { + const timeElapsed = process.hrtime(timeBegin); + const allowPerfReport = process.env.ACE_PERF; // !cli.flags.silent && cli.flags.verbose; + if (allowPerfReport) console.log(`>>> ACE PERF: ${timeElapsed[0]} seconds + ${timeElapsed[1]} nanoseconds`); + exit(...arguments); + } + + logger.initLogger({ verbose: cli.flags.verbose, silent: cli.flags.silent, fileName: logFileName }); + + // Check that an EPUB path is specified + if (cli.input.length === 0) { + const res = await winston.logAndWaitFinish('error', 'Input required'); + console.log(cli.help); + quit(1); + return; + } + + // Check that output directories can be overridden + let outdir = cli.flags.outdir; + if (outdir) { + if (cli.flags.subdir) { + outdir = path.join(outdir, path.parse(cli.input[0]).name); + } + if (!cli.flags.force) { + const overrides = ['report.json', 'report.html', 'data', 'js'] + .map(file => path.join(outdir, file)) + .filter(fs.existsSync) + .map(file => file.replace(/\\/g, "/")); + if (overrides.length > 0) { + const res = await winston.logAndWaitFinish('warn', + `\ +Output directory is not empty. + + Running Ace would override the following files or directories: + +${overrides.map(file => ` - ${file}`).join('\n')} + + Use option --force to override. +` + ); + quit(1); + return; + } + } + } + + // finally, invoke Ace + ace(cli.input[0], { + cwd: cli.flags.cwd || process.cwd(), + outdir, + tmpdir: cli.flags.tempdir, + verbose: cli.flags.verbose, + silent: cli.flags.silent, + jobId: '', + lang: cli.flags.lang, + }, axeRunner) + .then(async (jobData) => { + var reportJson = jobData[1]; + // if there were violations from the validation process, return 2 + const fail = cliConfig['return-2-on-validation-error'] && reportJson['earl:result']['earl:outcome'] === 'fail'; + const res = await winston.logAndWaitFinish('info', 'Closing logs.'); + quit(fail ? 2 : 0); + }) + .catch(async (err) => { + winston.error(err.message ? err.message : err); + if (err.stack) winston.debug(err.stack); + + const res = await winston.logAndWaitFinish('info', 'Closing logs.'); + console.log('Re-run Ace using the --verbose option to enable full debug logging.'); + quit(1); + }); +} + +module.exports = { run }; diff --git a/packages/ace-cli/bin/ace.js b/packages/ace-cli/bin/ace.js index 60421d7b..6ce57785 100755 --- a/packages/ace-cli/bin/ace.js +++ b/packages/ace-cli/bin/ace.js @@ -1,3 +1,5 @@ #!/usr/bin/env node -require('../lib').run(); +(async () => { + await require('../lib').run(); +})(); diff --git a/packages/ace-cli/package.json b/packages/ace-cli/package.json index 0ea4c72f..18263b8a 100644 --- a/packages/ace-cli/package.json +++ b/packages/ace-cli/package.json @@ -1,6 +1,11 @@ { "name": "@daisy/ace-cli", - "version": "1.1.1", + "version": "1.2.0-beta.15", + "engines": { + "node": ">=10.0.0", + "yarn": "^1.22.5", + "npm": ">=6.14.12" + }, "description": "Ace by DAISY, an Accessibility Checker for EPUB", "author": { "name": "DAISY developers", @@ -19,13 +24,8 @@ "main": "lib/index.js", "bin": "bin/ace.js", "dependencies": { - "@daisy/ace-axe-runner-puppeteer": "^1.1.0", - "@daisy/ace-config": "^1.1.0", - "@daisy/ace-core": "^1.1.1", - "@daisy/ace-logger": "^1.1.0", - "@daisy/ace-meta": "^1.1.0", - "meow": "^3.7.0", - "winston": "^2.4.0" + "@daisy/ace-axe-runner-puppeteer": "^1.2.0-beta.15", + "@daisy/ace-cli-shared": "^1.2.0-beta.15" }, "publishConfig": { "access": "public" diff --git a/packages/ace-cli/src/index.js b/packages/ace-cli/src/index.js index 4a5949f1..53dbf07d 100755 --- a/packages/ace-cli/src/index.js +++ b/packages/ace-cli/src/index.js @@ -1,130 +1,10 @@ 'use strict'; -const fs = require('fs'); -const meow = require('meow'); -const path = require('path'); -const winston = require('winston'); - const axeRunner = require('@daisy/ace-axe-runner-puppeteer'); - -const logger = require('@daisy/ace-logger'); -const ace = require('@daisy/ace-core'); - -const { config, paths } = require('@daisy/ace-config'); -const defaults = require('./defaults'); -const cliConfig = config.get('cli', defaults.cli); - -const pkg = require('@daisy/ace-meta/package'); - -const cli = meow({ - help: -` - Usage: ace [options] - - Options: - - -h, --help output usage information - -v, --version output the version number - - -o, --outdir save final reports to the specified directory - -t, --tempdir specify a custom directory to store the temporary reports - -f, --force override any existing output file or directory - --subdir output reports to a sub-directory named after the input EPUB - - -V, --verbose display verbose output - -s, --silent do not display any output - - -l, --lang language code for localized messages (e.g. "fr"), default is "en" - Examples - $ ace -o out ~/Documents/book.epub -`, -// autoVersion: false, -version: pkg.version -}, { - alias: { - f: 'force', - h: 'help', - o: 'outdir', - s: 'silent', - t: 'tempdir', - v: 'version', - V: 'verbose', - l: 'lang', - }, - boolean: ['force', 'verbose', 'silent', 'subdir'], - string: ['outdir', 'tempdir', 'lang'], -}); - -function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} +const cli = require('@daisy/ace-cli-shared'); async function run() { - logger.initLogger({ verbose: cli.flags.verbose, silent: cli.flags.silent }); - - // Check that an EPUB path is specified - if (cli.input.length === 0) { - winston.logAndExit('error', 'Input required', () => { - console.log(cli.help); - process.exit(1); - }); - await sleep(5000); - process.exit(1); - } - - // Check that output directories can be overridden - let outdir = cli.flags.outdir; - if (outdir) { - if (cli.flags.subdir) { - outdir = path.join(outdir, path.parse(cli.input[0]).name); - } - if (!cli.flags.force) { - const overrides = ['report.json', 'report.html', 'data', 'js'] - .map(file => path.join(outdir, file)) - .filter(fs.existsSync); - if (overrides.length > 0) { - winston.logAndExit('warn', `\ -Output directory is not empty. - - Running Ace would override the following files or directories: - -${overrides.map(file => ` - ${file}`).join('\n')} - - Use option --force to override. -`, 1); - await sleep(5000); - process.exit(1); - } - } - } - - // finally, invoke Ace - ace(cli.input[0], { - cwd: cli.flags.cwd || process.cwd(), - outdir, - tmpdir: cli.flags.tempdir, - verbose: cli.flags.verbose, - silent: cli.flags.silent, - jobId: '', - lang: cli.flags.lang, - }, axeRunner) - .then((jobData) => { - var reportJson = jobData[1]; - // if there were violations from the validation process, return 2 - if (cliConfig['return-2-on-validation-error'] && - reportJson['earl:result']['earl:outcome'] === 'fail') { - winston.logAndExit('info', 'Closing logs.', () => { - process.exit(2); - }); - } - }) - .catch((err) => { - if (err && err.message) winston.error(err.message); - winston.logAndExit('info', 'Closing logs.', () => { - console.log('Re-run Ace using the --verbose option to enable full debug logging.'); - process.exit(1); - }); - }); + await cli.run(axeRunner, process.exit, (typeof process.env.JEST_TESTS !== "undefined" ? "ace-tests-cli.log" : "ace-cli.log")); } module.exports = { run }; diff --git a/packages/ace-config/package.json b/packages/ace-config/package.json index 814c4b52..d0fdbb28 100644 --- a/packages/ace-config/package.json +++ b/packages/ace-config/package.json @@ -1,6 +1,11 @@ { "name": "@daisy/ace-config", - "version": "1.1.0", + "version": "1.2.0-beta.15", + "engines": { + "node": ">=10.0.0", + "yarn": "^1.22.5", + "npm": ">=6.14.12" + }, "description": "Config utilities for Ace", "author": { "name": "DAISY developers", @@ -18,9 +23,9 @@ "license": "MIT", "main": "lib/index.js", "dependencies": { - "conf": "^1.3.1", - "env-paths": "^1.0.0", - "lodash.mergewith": "^4.6.0" + "conf": "^9.0.2", + "env-paths": "^2.2.1", + "lodash.mergewith": "^4.6.2" }, "publishConfig": { "access": "public" diff --git a/packages/ace-config/src/__tests__/index.test.js b/packages/ace-config/src/__tests__/index.test.js index c987a92e..2b046854 100644 --- a/packages/ace-config/src/__tests__/index.test.js +++ b/packages/ace-config/src/__tests__/index.test.js @@ -9,7 +9,13 @@ test('config store is defined', () => { test('config file default name', () => { expect(path.basename(config.path)).toEqual('config.json'); - expect(path.basename(path.dirname(config.path))).toEqual('DAISY Ace'); + if (process.platform === "win32") { + // https://github.com/sindresorhus/env-paths/blob/5944db4b2f8c635e8b39a363f6bdff40825be16e/index.js#L28 + expect(path.basename(path.dirname(path.dirname(config.path)))).toEqual('DAISY Ace'); + expect(path.basename(path.dirname(config.path))).toEqual('Config'); + } else { + expect(path.basename(path.dirname(config.path))).toEqual('DAISY Ace'); + } }); test('paths are defined', () => { diff --git a/packages/ace-core-legacy/package.json b/packages/ace-core-legacy/package.json index 7a4d217d..b6b0e9c3 100644 --- a/packages/ace-core-legacy/package.json +++ b/packages/ace-core-legacy/package.json @@ -1,6 +1,11 @@ { "name": "ace-core", - "version": "1.1.1", + "version": "1.2.0-beta.15", + "engines": { + "node": ">=10.0.0", + "yarn": "^1.22.5", + "npm": ">=6.14.12" + }, "description": "Ace by DAISY, an Accessibility Checker for EPUB", "keywords": [ "a11y", @@ -33,9 +38,9 @@ }, "main": "lib/index.js", "dependencies": { - "@daisy/ace-cli": "^1.1.1", - "@daisy/ace-core": "^1.1.1", - "@daisy/ace-http": "^1.1.1" + "@daisy/ace-cli": "^1.2.0-beta.15", + "@daisy/ace-core": "^1.2.0-beta.15", + "@daisy/ace-http": "^1.2.0-beta.15" }, "publishConfig": { "access": "public" diff --git a/packages/ace-core/package.json b/packages/ace-core/package.json index 62c07006..fbcbc820 100644 --- a/packages/ace-core/package.json +++ b/packages/ace-core/package.json @@ -1,6 +1,11 @@ { "name": "@daisy/ace-core", - "version": "1.1.1", + "version": "1.2.0-beta.15", + "engines": { + "node": ">=10.0.0", + "yarn": "^1.22.5", + "npm": ">=6.14.12" + }, "description": "Core library for Ace", "author": { "name": "DAISY developers", @@ -18,18 +23,18 @@ "license": "MIT", "main": "lib/index.js", "dependencies": { - "@daisy/ace-localize": "^1.1.0", - "@daisy/ace-logger": "^1.1.0", - "@daisy/ace-meta": "^1.1.0", - "@daisy/ace-report": "^1.1.1", - "@daisy/ace-report-axe": "^1.1.1", - "@daisy/epub-utils": "^1.1.0", - "axe-core": "^3.2.2", - "file-url": "^2.0.2", + "@daisy/ace-localize": "^1.2.0-beta.15", + "@daisy/ace-logger": "^1.2.0-beta.15", + "@daisy/ace-meta": "^1.2.0-beta.15", + "@daisy/ace-report": "^1.2.0-beta.15", + "@daisy/ace-report-axe": "^1.2.0-beta.15", + "@daisy/epub-utils": "^1.2.0-beta.15", + "@daisy/axe-core-for-ace": "4.1.4-canary.3", + "file-url": "^3.0.0", "h5o": "^0.11.3", - "p-map": "^1.2.0", - "tmp": "^0.0.33", - "winston": "^2.4.0" + "p-map": "^4.0.0", + "tmp": "^0.2.1", + "winston": "^3.3.3" }, "publishConfig": { "access": "public" diff --git a/packages/ace-core/src/checker/checker-chromium.js b/packages/ace-core/src/checker/checker-chromium.js index 98fc7a4f..058bbdbd 100644 --- a/packages/ace-core/src/checker/checker-chromium.js +++ b/packages/ace-core/src/checker/checker-chromium.js @@ -14,31 +14,51 @@ const { getRawResourcesForCurrentLanguage } = require('../l10n/localize').locali tmp.setGracefulCleanup(); const scripts = [ - path.resolve(require.resolve('axe-core'), '../axe.min.js'), + // require.resolve('../scripts/function-bind-bound-object.js'), require.resolve('../scripts/vendor/outliner.min.js'), - require.resolve('../scripts/axe-patch-aria-roles.js'), - require.resolve('../scripts/axe-patch-is-aria-role-allowed.js'), - require.resolve('../scripts/axe-patch-only-list-items.js'), - require.resolve('../scripts/axe-patch-listitem.js'), + path.resolve(require.resolve('@daisy/axe-core-for-ace'), '../axe.js'), + // require.resolve('../scripts/axe-patch-aria-roles.js'), + // require.resolve('../scripts/axe-patch-is-aria-role-allowed.js'), + // require.resolve('../scripts/axe-patch-only-list-items.js'), + // require.resolve('../scripts/axe-patch-listitem.js'), require.resolve('../scripts/ace-axe.js'), require.resolve('../scripts/ace-extraction.js'), ]; +const LOG_DEBUG_URLS = process.env.LOG_DEBUG_URLS === "1"; + async function checkSingle(spineItem, epub, lang, axeRunner) { winston.verbose(`- Processing ${spineItem.relpath}`); try { + if (LOG_DEBUG_URLS) { + console.log("....... URL 1"); + console.log(spineItem.url); + console.log(spineItem.filepath); + console.log(spineItem.relpath); + } let url = spineItem.url; let ext = path.extname(spineItem.filepath); // File extensions other than 'xhtml' or 'html' are not propertly loaded // by puppeteer, so we copy the file to a new `.xhtml` temp file. - if (ext !== '.xhtml' && ext !== '.html') { + if (!process.versions['electron'] && // The Electron-based Axe runner handles .xml files just fine + ext !== '.xhtml' && ext !== '.html') { + winston.warn(`Copying document with extension '${ext}' to a temporary '.xhtml' file…`); const tmpdir = tmp.dirSync({ unsafeCleanup: true }).name; const tmpFile = path.join(tmpdir, `${path.basename(spineItem.filepath, ext)}.xhtml`) fs.copySync(spineItem.filepath, tmpFile); + + // does encodeURI() as per https://tools.ietf.org/html/rfc3986#section-3.3 in a nutshell: encodeURI(`file://${tmpFile}`).replace(/[?#]/g, encodeURIComponent) url = fileUrl(tmpFile); - winston.debug(`checking copied file at ${url}`) + // url = "file://" + encodeURI(tmpFile); + + winston.debug(`checking copied file at ${tmpFile}`) + } + + if (LOG_DEBUG_URLS) { + console.log("....... URL 2"); + console.log(url); } const scriptContents = []; @@ -50,7 +70,7 @@ async function checkSingle(spineItem, epub, lang, axeRunner) { // https://github.com/dequelabs/axe-core/tree/develop/locales if (lang && lang !== "en" && lang.indexOf("en-") !== 0) { // default English built into Axe source code - localePath = path.resolve(require.resolve('axe-core'), `../locales/${lang}.json`); + localePath = path.resolve(require.resolve('@daisy/axe-core-for-ace'), `../locales/${lang}.json`); if (fs.existsSync(localePath)) { const localeStr = fs.readFileSync(localePath, { encoding: "utf8" }); const localeScript = `window.__axeLocale__=${localeStr};`; @@ -60,20 +80,24 @@ async function checkSingle(spineItem, epub, lang, axeRunner) { } } - let localizedScript = ""; - const rawJson = getRawResourcesForCurrentLanguage(); + // let localizedScript = ""; + // const rawJson = getRawResourcesForCurrentLanguage(); - ["axecheck", "axerule"].forEach((checkOrRule) => { - const checkOrRuleKeys = Object.keys(rawJson[checkOrRule]); - for (const checkOrRuleKey of checkOrRuleKeys) { - const msgs = Object.keys(rawJson[checkOrRule][checkOrRuleKey]); - for (const msg of msgs) { - const k = `__aceLocalize__${checkOrRule}_${checkOrRuleKey}_${msg}`; - localizedScript += `window['${k}']="${rawJson[checkOrRule][checkOrRuleKey][msg]}";\n`; - } - } - }); - scriptContents.push(localizedScript); + // ["axecheck", "axerule"].forEach((checkOrRule) => { + // const checkOrRuleKeys = Object.keys(rawJson[checkOrRule]); + // for (const checkOrRuleKey of checkOrRuleKeys) { + // const msgs = Object.keys(rawJson[checkOrRule][checkOrRuleKey]); + // for (const msg of msgs) { + // const k = `__aceLocalize__${checkOrRule}_${checkOrRuleKey}_${msg}`; + // let v = rawJson[checkOrRule][checkOrRuleKey][msg]; + // if (v) { + // v = v.replace(/"/g, '\\"'); + // } + // localizedScript += `window['${k}']="${v}";\n`; + // } + // } + // }); + // scriptContents.push(localizedScript); } catch (err) { console.log(err); @@ -84,8 +108,13 @@ async function checkSingle(spineItem, epub, lang, axeRunner) { const results = await axeRunner.run(url, scripts, scriptContents, epub.basedir); // Post-process results - results.assertions = (results.axe != null) ? axe2ace.axe2ace(spineItem, results.axe, lang) : []; - delete results.axe; + if (!results.axe) { + results.assertions = []; + } else { + results.assertions = await axe2ace.axe2ace(spineItem, results.axe, lang); + delete results.axe; + } + winston.info(`- ${spineItem.relpath}: ${ (results.assertions && results.assertions.assertions && results.assertions.assertions.length > 0) ? results.assertions.assertions.length @@ -99,18 +128,50 @@ async function checkSingle(spineItem, epub, lang, axeRunner) { if (Array.isArray(item.src)) { item.src = item.src.map((srcItem) => { if (srcItem.src !== undefined) { - srcItem.path = path.resolve(path.dirname(spineItem.filepath), - srcItem.src.toString()); - srcItem.src = path.relative(epub.basedir, srcItem.path); + if (LOG_DEBUG_URLS) { + console.log("----- ITEMs SRC 1"); + console.log(srcItem.src); + } + srcItem.path = path.resolve(path.dirname(spineItem.filepath), decodeURI(srcItem.src.toString())); + if (LOG_DEBUG_URLS) { + console.log("----- ITEMs SRC 2"); + console.log(srcItem.path); + } + srcItem.src = path.relative(epub.basedir, srcItem.path).replace(/\\/g, "/"); + if (LOG_DEBUG_URLS) { + console.log("----- ITEMs SRC 3"); + console.log(srcItem.src); + } } return srcItem; }); } else { - item.path = path.resolve(path.dirname(spineItem.filepath), item.src.toString()); - item.src = path.relative(epub.basedir, item.path); + if (LOG_DEBUG_URLS) { + console.log("----- ITEM SRC 1"); + console.log(item.src); + } + item.path = path.resolve(path.dirname(spineItem.filepath), decodeURI(item.src.toString())); + if (LOG_DEBUG_URLS) { + console.log("----- ITEM SRC 2"); + console.log(item.path); + } + item.src = path.relative(epub.basedir, item.path).replace(/\\/g, "/"); + if (LOG_DEBUG_URLS) { + console.log("----- ITEM SRC 3"); + console.log(item.src); + } } if (item.cfi !== undefined) { - item.location = `${spineItem.relpath}#epubcfi(${item.cfi})`; + if (LOG_DEBUG_URLS) { + console.log("----- CFI 1"); + console.log(spineItem.relpath); + console.log(item.cfi); + } + item.location = `${encodeURI(spineItem.relpath)}#epubcfi(${encodeURI(item.cfi)})`; + if (LOG_DEBUG_URLS) { + console.log("----- CFI 2"); + console.log(item.location); + } delete item.cfi; } } @@ -119,8 +180,11 @@ async function checkSingle(spineItem, epub, lang, axeRunner) { } return results; } catch (err) { - winston.debug(`Error when running HTML checks: ${err}`); - throw new Error(`Failed to check Content Document '${spineItem.relpath}'`); + console.log(err); + winston.debug(`Error when running HTML checks: ${err.message ? err.message : err}`); + if (err.stack) winston.debug(err.stack); + + throw new Error(`Failed to check Content Document '${spineItem.relpath}': ${err.message ? err.message : err}`); } } @@ -132,8 +196,11 @@ module.exports.check = async (epub, lang, axeRunner) => { await axeRunner.close(); return results; }).catch(async (err) => { - winston.info(`Error HTML check: ${err}`); + winston.error(`Ace HTML check error: ${err.message ? err.message : err}`); + if (err.stack) winston.debug(err.stack); + await axeRunner.close(); - return []; + + throw new Error(err); }); }; diff --git a/packages/ace-core/src/checker/checker-epub.js b/packages/ace-core/src/checker/checker-epub.js index f5399bf6..ddab15a0 100644 --- a/packages/ace-core/src/checker/checker-epub.js +++ b/packages/ace-core/src/checker/checker-epub.js @@ -5,104 +5,12 @@ const winston = require('winston'); const { localize } = require('../l10n/localize').localizer; +const a11yMetadata = require('../core/a11y-metadata'); + const ASSERTED_BY = 'Ace'; const MODE = 'automatic'; const KB_BASE = 'http://kb.daisy.org/publishing/'; -const A11Y_META = { - 'schema:accessMode': { - required: true, - allowedValues: [ - 'auditory', - 'chartOnVisual', - 'chemOnVisual', - 'colorDependent', - 'diagramOnVisual', - 'mathOnVisual', - 'musicOnVisual', - 'tactile', - 'textOnVisual', - 'textual', - 'visual', - ] - }, - 'schema:accessModeSufficient': { - recommended: true, - allowedValues: [ - 'auditory', - 'tactile', - 'textual', - 'visual', - ] - }, - 'schema:accessibilityAPI': { - allowedValues: [ - 'ARIA' - ] - }, - 'schema:accessibilityControl': { - allowedValues: [ - 'fullKeyboardControl', - 'fullMouseControl', - 'fullSwitchControl', - 'fullTouchControl', - 'fullVideoControl', - 'fullVoiceControl', - ] - }, - 'schema:accessibilityFeature': { - required: true, - allowedValues: [ - 'alternativeText', - 'annotations', - 'audioDescription', - 'bookmarks', - 'braille', - 'captions', - 'ChemML', - 'describedMath', - 'displayTransformability', - 'highContrastAudio', - 'highContrastDisplay', - 'index', - 'largePrint', - 'latex', - 'longDescription', - 'MathML', - 'none', - 'printPageNumbers', - 'readingOrder', - 'rubyAnnotations', - 'signLanguage', - 'structuralNavigation', - 'synchronizedAudioText', - 'tableOfContents', - 'taggedPDF', - 'tactileGraphic', - 'tactileObject', - 'timingControl', - 'transcript', - 'ttsMarkup', - 'unlocked', - ], - }, - 'schema:accessibilityHazard': { - allowedValues: [ - 'flashing', - 'noFlashingHazard', - 'motionSimulation', - 'noMotionSimulationHazard', - 'sound', - 'noSoundHazard', - 'unknown', - 'none', - ] - }, - 'schema:accessibilitySummary': { - required: true, - } -}; - function asString(arrayOrString) { if (Array.isArray(arrayOrString) && arrayOrString.length > 0) { return asString(arrayOrString[0]); @@ -140,16 +48,27 @@ function newMetadataAssertion(name, impact = 'serious') { title: `metadata-${name.toLowerCase().replace('schema:', '')}`, testDesc: localize("checkepub.metadataviolation.testdesc", { name, interpolation: { escapeValue: false } }), resDesc: localize("checkepub.metadataviolation.resdesc", { name, interpolation: { escapeValue: false } }), - kbPath: 'docs/metadata/schema-org.html', + kbPath: 'docs/metadata/schema.org/index.html', kbTitle: localize("checkepub.metadataviolation.kbtitle"), ruleDesc: localize("checkepub.metadataviolation.ruledesc", { name, interpolation: { escapeValue: false } }) }); } +// newMetadataAssertion => +// "metadataviolation" +// "Add a '{{name}}' metadata property to the Package Document", +// "Publications must declare the '{{name}}' metadata", +// "Ensures a '{{name}}' metadata is present" + +// otherwise custom newViolation() => +// "metadatainvalid" +// "Use one of the metadata values defined by schema.org", +// "'{{name}}' metadata must be set to one of the expected values", +// "Value '{{value}}' is invalid for '{{name}}' metadata" function checkMetadata(assertions, epub) { // Check metadata values - for (const name in A11Y_META) { - const meta = A11Y_META[name]; + for (const name in a11yMetadata.A11Y_META) { + const meta = a11yMetadata.A11Y_META[name]; var values = epub.metadata[name]; if (values === undefined) { // Report missing metadata if it is required or recommended @@ -159,39 +78,68 @@ function checkMetadata(assertions, epub) { if (!Array.isArray(values)) { values = [values] } - // Parse list values - values = values.map(value => value.trim().replace(',', ' ').replace(/\s{2,}/g, ' ').split(' ')) - values = [].concat(...values); - // Check metadata values are allowed - // see https://www.w3.org/wiki/WebSchemas/Accessibility - if (meta.allowedValues) { - values.filter(value => !meta.allowedValues.includes(value)) - .forEach(value => { - assertions.withAssertions(newViolation({ - impact: 'moderate', - title: `metadata-${name.toLowerCase().replace('schema:', '')}-invalid`, - testDesc: localize("checkepub.metadatainvalid.testdesc", { value, name, interpolation: { escapeValue: false } }), - resDesc: localize("checkepub.metadatainvalid.resdesc", { name, interpolation: { escapeValue: false } }), - kbPath: 'docs/metadata/schema-org.html', - kbTitle: localize("checkepub.metadatainvalid.kbtitle"), - ruleDesc: localize("checkepub.metadatainvalid.ruledesc", { name, interpolation: { escapeValue: false } }) - })) + + // TODO? + // "metadatamultiple" would be new localizable label for this kind of error! + // "A single occurence of schema.org metadata is expected", + // "Metadata '{{name}}' should not appear more than once", + // "Metadata '{{name}}' with value '{{value}}' is defined several times" + // if (name === 'schema:accessibilitySummary' && values.length > 1) { + // assertions.withAssertions(newViolation({ + // impact: 'minor', + // title: `metadata-${name.toLowerCase().replace('schema:', '')}-invalid`, + // testDesc: localize("checkepub.metadatamultiple.testdesc", { value, name, interpolation: { escapeValue: false } }), + // resDesc: localize("checkepub.metadatamultiple.resdesc", { name, interpolation: { escapeValue: false } }), + // kbPath: 'docs/metadata/schema.org/index.html', + // kbTitle: localize("checkepub.metadatamultiple.kbtitle"), + // ruleDesc: localize("checkepub.metadatamultiple.ruledesc", { name, interpolation: { escapeValue: false } }) + // })) + // } + + if (meta.allowedValues) { // effectively excludes schema:accessibilitySummary + + values.forEach(value => { + + // comma-separated only! (not space-separated) + // regexp note: /\s\s+/g === /\s{2,}/g + // no whitespace collapsing, individual items can contain (incorrect) whitespaces, which will be reported + const splitValues = + name === 'schema:accessModeSufficient' ? + value.trim().split(',').map(item => item.trim()).filter(item => item.length) : + [value]; + + if (meta.allowedValues) { + splitValues.filter(splitValue => !meta.allowedValues.includes(splitValue)) + .forEach(splitValue => { + assertions.withAssertions(newViolation({ + impact: 'moderate', + title: `metadata-${name.toLowerCase().replace('schema:', '')}-invalid`, + testDesc: localize("checkepub.metadatainvalid.testdesc", { value: splitValue, name, interpolation: { escapeValue: false } }), + resDesc: localize("checkepub.metadatainvalid.resdesc", { name, interpolation: { escapeValue: false } }), + kbPath: 'docs/metadata/schema.org/index.html', + kbTitle: localize("checkepub.metadatainvalid.kbtitle"), + ruleDesc: localize("checkepub.metadatainvalid.ruledesc", { name, interpolation: { escapeValue: false } }) + })) + }); + } + + // Check consistency of the printPageNumbers feature + if (name === 'schema:accessibilityFeature' + && splitValues.includes('printPageNumbers') + && !epub.navDoc.hasPageList) { + + assertions.withAssertions(newViolation({ + impact: 'moderate', + title: `metadata-accessibilityFeature-printPageNumbers-nopagelist`, + testDesc: localize("checkepub.metadataprintpagenumbers.testdesc", {}), + resDesc: localize("checkepub.metadataprintpagenumbers.resdesc", {}), + kbPath: 'docs/metadata/schema.org/index.html', + kbTitle: localize("checkepub.metadataprintpagenumbers.kbtitle"), + ruleDesc: localize("checkepub.metadataprintpagenumbers.ruledesc", {}) + })) + } }); } - // Check consistency of the printPageNumbers feature - if (name === 'schema:accessibilityFeature' - && values.includes('printPageNumbers') - && !epub.navDoc.hasPageList) { - assertions.withAssertions(newViolation({ - impact: 'moderate', - title: `metadata-accessibilityFeature-printPageNumbers-nopagelist`, - testDesc: localize("checkepub.metadataprintpagenumbers.testdesc", {}), - resDesc: localize("checkepub.metadataprintpagenumbers.resdesc", {}), - kbPath: 'docs/metadata/schema-org.html', - kbTitle: localize("checkepub.metadataprintpagenumbers.kbtitle"), - ruleDesc: localize("checkepub.metadataprintpagenumbers.ruledesc", {}) - })) - } } } } diff --git a/packages/ace-core/src/core/a11y-metadata.js b/packages/ace-core/src/core/a11y-metadata.js new file mode 100644 index 00000000..a53d8457 --- /dev/null +++ b/packages/ace-core/src/core/a11y-metadata.js @@ -0,0 +1,148 @@ +'use strict'; + +// http://kb.daisy.org/publishing/docs/metadata/schema.org/index.html +// http://kb.daisy.org/publishing/docs/metadata/evaluation.html +// https://www.w3.org/wiki/WebSchemas/Accessibility + +const conformsToURLs = [ + "http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-a", + "http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aa", + "http://www.idpf.org/epub/a11y/accessibility-20170105.html#wcag-aaa", +]; + +const a11yMeta_links = [ + "a11y:certifierReport", //(link in EPUB3) + "dcterms:conformsTo", //(link in EPUB3) +]; +const a11yMeta = [ + "schema:accessMode", // required + "schema:accessibilityFeature", // required + "schema:accessibilityHazard", // required + "schema:accessibilitySummary", // required + "schema:accessModeSufficient", // recommended + "schema:accessibilityAPI", // optional + "schema:accessibilityControl", // optional + + "a11y:certifiedBy", + "a11y:certifierCredential", //(MAY BE link in EPUB3) +].concat(a11yMeta_links); + +const A11Y_META = { + 'schema:accessMode': { + required: true, + allowedValues: [ + 'auditory', + 'tactile', + 'textual', + 'visual', + 'chartOnVisual', + 'chemOnVisual', + 'colorDependent', + 'diagramOnVisual', + 'mathOnVisual', + 'musicOnVisual', + 'textOnVisual', + ] + }, + 'schema:accessModeSufficient': { + recommended: true, + allowedValues: [ + 'auditory', + 'tactile', + 'textual', + 'visual', + 'chartOnVisual', + 'chemOnVisual', + 'colorDependent', + 'diagramOnVisual', + 'mathOnVisual', + 'musicOnVisual', + 'textOnVisual', + ] + }, + 'schema:accessibilityAPI': { + allowedValues: [ + 'ARIA' + ] + }, + 'schema:accessibilityControl': { + allowedValues: [ + 'fullKeyboardControl', + 'fullMouseControl', + 'fullSwitchControl', + 'fullTouchControl', + 'fullVideoControl', + 'fullAudioControl', + 'fullVoiceControl', + ] + }, + 'schema:accessibilityFeature': { + required: true, + allowedValues: [ + 'alternativeText', + 'annotations', + 'audioDescription', + 'bookmarks', + 'braille', + 'captions', + 'ChemML', + 'describedMath', + 'displayTransformability', + 'displayTransformability/font-size', + 'displayTransformability/font-family', + 'displayTransformability/line-height', + 'displayTransformability/word-spacing', + 'displayTransformability/letter-spacing', + 'displayTransformability/color', + 'displayTransformability/background-color', + 'highContrastAudio', + 'highContrastAudio/noBackground', + 'highContrastAudio/reducedBackground', + 'highContrastAudio/switchableBackground', + 'highContrastDisplay', + 'index', + 'largePrint', + 'latex', + 'longDescription', + 'MathML', + 'none', + 'printPageNumbers', + 'readingOrder', + 'rubyAnnotations', + 'signLanguage', + 'structuralNavigation', + 'synchronizedAudioText', + 'tableOfContents', + 'taggedPDF', + 'tactileGraphic', + 'tactileObject', + 'timingControl', + 'transcript', + 'ttsMarkup', + 'unlocked', + ], + }, + 'schema:accessibilityHazard': { + required: true, + allowedValues: [ + 'flashing', + 'noFlashingHazard', + 'motionSimulation', + 'noMotionSimulationHazard', + 'sound', + 'noSoundHazard', + 'unknown', + 'none', + ] + }, + 'schema:accessibilitySummary': { + required: true, + } +}; + +module.exports = { + conformsToURLs, + a11yMeta_links, + a11yMeta, + A11Y_META, +}; diff --git a/packages/ace-core/src/core/ace.js b/packages/ace-core/src/core/ace.js index de02e902..8b4efaab 100644 --- a/packages/ace-core/src/core/ace.js +++ b/packages/ace-core/src/core/ace.js @@ -18,82 +18,87 @@ tmp.setGracefulCleanup(); module.exports = function ace(epubPath, options, axeRunner) { - if (options.lang) { - setCurrentLanguage(options.lang); - } - - if (options.initLogger) { - logger.initLogger({ verbose: options.verbose, silent: options.silent }); - } - return new Promise((resolve, reject) => { - // the jobid option just gets returned in the resolve/reject - // so the calling function can track which job finished - var jobId = 'jobid' in options ? options.jobid : ''; - winston.verbose(`Ace ${pkg.version}, Node ${process.version}, ${os.type()} ${os.release()}`); - winston.verbose("Options:", options); - // Check that the EPUB exists - const epubPathResolved = path.resolve(options.cwd, epubPath); - if (!fs.existsSync(epubPathResolved)) { - winston.error(`Couldn’t find EPUB file '${epubPath}'`); - return reject(jobId); - } + function l10nDoneCallback() { - // Process options - /* eslint-disable no-param-reassign */ - if (typeof options.tmpdir === 'string') { - options.tmpdir = path.resolve(options.cwd, options.tmpdir); - if (!fs.existsSync(options.tmpdir)) { - fs.ensureDirSync(options.tmpdir); + if (options.initLogger) { + logger.initLogger({ verbose: options.verbose, silent: options.silent, fileName: options.fileName }); } - } else if (options.tmpdir === undefined) { - options.tmpdir = tmp.dirSync({ unsafeCleanup: true }).name; - } - if (typeof options.outdir === 'string') { - options.outdir = path.resolve(options.cwd, options.outdir); - if (!fs.existsSync(options.outdir)) { - fs.ensureDirSync(options.outdir); + + // the jobid option just gets returned in the resolve/reject + // so the calling function can track which job finished + var jobId = 'jobid' in options ? options.jobid : ''; + winston.verbose(`Ace ${pkg.version}, Node ${process.version}, ${os.type()} ${os.release()}`); + winston.verbose("Options:", options); + + // Check that the EPUB exists + const epubPathResolved = path.resolve(options.cwd, epubPath); + if (!fs.existsSync(epubPathResolved)) { + winston.error(`Couldn’t find EPUB file '${epubPath}'`); + return reject(jobId); + } + + // Process options + /* eslint-disable no-param-reassign */ + if (typeof options.tmpdir === 'string') { + options.tmpdir = path.resolve(options.cwd, options.tmpdir); + if (!fs.existsSync(options.tmpdir)) { + fs.ensureDirSync(options.tmpdir); + } + } else if (options.tmpdir === undefined) { + options.tmpdir = tmp.dirSync({ unsafeCleanup: true }).name; + } + if (typeof options.outdir === 'string') { + options.outdir = path.resolve(options.cwd, options.outdir); + if (!fs.existsSync(options.outdir)) { + fs.ensureDirSync(options.outdir); + } + } else { + delete options.outdir; } + + winston.info("Processing " + epubPath); + + /* eslint-enable no-param-reassign */ + + // Unzip the EPUB + const epub = new EPUB(epubPathResolved); + epub.extract() + .then(() => epub.parse()) + // initialize the report + .then(() => new Report(epub, options.outdir, options.lang).init()) + // Check each Content Doc + .then(report => checker.check(epub, report, options.lang, axeRunner)) + // Process the Results + .then((report) => { + if (options.outdir === undefined) { + report.cleanData(); + process.stdout.write(`${JSON.stringify(report.json, null, ' ')}\n`); + return report; + } + return report.copyData(options.outdir) + .then(() => report.cleanData()) + .then(() => Promise.all([ + report.saveJson(options.outdir), + report.saveHtml(options.outdir) + ])) + .then(() => report); + }) + .then((report) => { + winston.info('Done.'); + resolve([jobId, report.json]); + }) + .catch((err) => { + winston.error(`Ace processing error: ${err.message ? err.message : err}`); + if (err.stack) winston.debug(err.stack); + reject(err); + }); + } + if (options.lang) { + setCurrentLanguage(options.lang, l10nDoneCallback); } else { - delete options.outdir; + l10nDoneCallback(); } - - winston.info("Processing " + epubPath); - - /* eslint-enable no-param-reassign */ - - // Unzip the EPUB - const epub = new EPUB(epubPathResolved); - epub.extract() - .then(() => epub.parse()) - // initialize the report - .then(() => new Report(epub, options.outdir, options.lang)) - // Check each Content Doc - .then(report => checker.check(epub, report, options.lang, axeRunner)) - // Process the Results - .then((report) => { - if (options.outdir === undefined) { - report.cleanData(); - process.stdout.write(`${JSON.stringify(report.json, null, ' ')}\n`); - return report; - } - return report.copyData(options.outdir) - .then(() => report.cleanData()) - .then(() => Promise.all([ - report.saveJson(options.outdir), - report.saveHtml(options.outdir) - ])) - .then(() => report); - }) - .then((report) => { - winston.info('Done.'); - resolve([jobId, report.json]); - }) - .catch((err) => { - winston.error(`Unexpected error: ${(err.message !== undefined) ? err.message : err}`); - if (err.stack !== undefined) winston.debug(err.stack); - reject(jobId); - }); }); }; diff --git a/packages/ace-core/src/l10n/locales/da.json b/packages/ace-core/src/l10n/locales/da.json index 01066872..f6f20433 100644 --- a/packages/ace-core/src/l10n/locales/da.json +++ b/packages/ace-core/src/l10n/locales/da.json @@ -1,19 +1,4 @@ { - "axecheck": { - "matching-aria-role": { - "fail": "Elementet har ingen ARIA rolle, som matcher 'epub:type'", - "pass": "Elementet har en ARIA rolle, som matcher 'epub:type'" - } - }, - "axerule": { - "epub-type-has-matching-role": { - "desc": "Sikrer at elementet har en ARIA rolle, som matcher 'epub:type'", - "help": "ARIA rolle skal være til stede og matche den angivne 'epub:type'" - }, - "pagebreak-label": { - "desc": "Sikrer at sidemarkører har en tilgængelig etiket ('label')" - } - }, "checkepub": { "metadatainvalid": { "kbtitle": "Metadata for tilgængelighed fra Schema.org", diff --git a/packages/ace-core/src/l10n/locales/en.json b/packages/ace-core/src/l10n/locales/en.json index 48e8d436..86e47848 100644 --- a/packages/ace-core/src/l10n/locales/en.json +++ b/packages/ace-core/src/l10n/locales/en.json @@ -1,19 +1,4 @@ { - "axecheck": { - "matching-aria-role": { - "fail": "Element has no ARIA role matching its epub:type", - "pass": "Element has an ARIA role matching its epub:type" - } - }, - "axerule": { - "epub-type-has-matching-role": { - "desc": "Ensure the element has an ARIA role matching its epub:type", - "help": "ARIA role should be used in addition to epub:type" - }, - "pagebreak-label": { - "desc": "Ensure page markers have an accessible label" - } - }, "checkepub": { "metadatainvalid": { "kbtitle": "Schema.org Accessibility Metadata", diff --git a/packages/ace-core/src/l10n/locales/es.json b/packages/ace-core/src/l10n/locales/es.json index 79629eae..4b9192d3 100644 --- a/packages/ace-core/src/l10n/locales/es.json +++ b/packages/ace-core/src/l10n/locales/es.json @@ -1,19 +1,4 @@ { - "axecheck": { - "matching-aria-role": { - "fail": "El elemento no tiene un rol ARIA que corresponda a su epub:type", - "pass": "El elemento tiene un rol ARIA que corresponde a su epub:type" - } - }, - "axerule": { - "epub-type-has-matching-role": { - "desc": "Asegurarse de que el elemento tiene un rol ARIA que corresponda a su epub:type", - "help": "Debería usarse ARIA role, además de epub:type" - }, - "pagebreak-label": { - "desc": "Garantizar que los marcadores de página tienen una etiqueta accesible" - } - }, "checkepub": { "metadatainvalid": { "kbtitle": "Metadatos de Accesibilidad Schema.org", diff --git a/packages/ace-core/src/l10n/locales/fr.json b/packages/ace-core/src/l10n/locales/fr.json index 23c387d1..4994de50 100644 --- a/packages/ace-core/src/l10n/locales/fr.json +++ b/packages/ace-core/src/l10n/locales/fr.json @@ -1,19 +1,4 @@ { - "axecheck": { - "matching-aria-role": { - "fail": "L’élément n’a pas de rôle ARIA correspondant à son epub:type", - "pass": "L’élément a un rôle ARIA correspondant à son epub:type" - } - }, - "axerule": { - "epub-type-has-matching-role": { - "desc": "Vérifie qu’un élément a un rôle ARIA correspondant à son epub:type", - "help": "Un rôle ARIA devrait être spécifié en plus de l’epub:type" - }, - "pagebreak-label": { - "desc": "Vérifie que les sauts de page ont un label accessible" - } - }, "checkepub": { "metadatainvalid": { "kbtitle": "Métadonnées d'accessibilité Schema.org", diff --git a/packages/ace-core/src/l10n/locales/pt_BR.json b/packages/ace-core/src/l10n/locales/pt_BR.json index a60cafaf..2eb37a44 100644 --- a/packages/ace-core/src/l10n/locales/pt_BR.json +++ b/packages/ace-core/src/l10n/locales/pt_BR.json @@ -1,19 +1,4 @@ { - "axecheck": { - "matching-aria-role": { - "fail": "O elemento não tem um ARIA 'role' correspondente ao seu epub:type", - "pass": "O elemento tem um ARIA 'role' correspondente ao seu epub:type" - } - }, - "axerule": { - "epub-type-has-matching-role": { - "desc": "Certifique-se de que o elemento tem um ARIA 'role' correspondente ao seu epub:type", - "help": "Um ARIA 'role' deve ser usado em conjunto com o epub:type" - }, - "pagebreak-label": { - "desc": "Certifique-se de que os marcadores de páginas tenham um rótulo acessível" - } - }, "checkepub": { "metadatainvalid": { "kbtitle": "Metadados de Acessibilidade Schema.org", diff --git a/packages/ace-core/src/l10n/localize.js b/packages/ace-core/src/l10n/localize.js index 12bac8c1..65ec93a0 100644 --- a/packages/ace-core/src/l10n/localize.js +++ b/packages/ace-core/src/l10n/localize.js @@ -7,7 +7,7 @@ const esJson = require("./locales/es.json"); const daJson = require("./locales/da.json"); -export const localizer = newLocalizer({ +const localizer = newLocalizer({ en: { name: "English", default: true, @@ -30,3 +30,4 @@ export const localizer = newLocalizer({ translation: daJson, }, }); +module.exports = { localizer }; diff --git a/packages/ace-core/src/scripts/ace-axe.js b/packages/ace-core/src/scripts/ace-axe.js index 771aa572..1a3e5159 100644 --- a/packages/ace-core/src/scripts/ace-axe.js +++ b/packages/ace-core/src/scripts/ace-axe.js @@ -89,130 +89,131 @@ daisy.ace.run = function(done) { window.axe.configure({ locale: window.__axeLocale__, // configured from host bootstrapper page (checker-chromium) can be undefined - checks: [ - { - id: "matching-aria-role", - evaluate: function evaluate(node, options) { - var mappings = new Map([ - ['abstract', 'doc-abstract'], - ['acknowledgments', 'doc-acknowledgments'], - ['afterword', 'doc-afterword'], - ['appendix', 'doc-appendix'], - ['backlink', 'doc-backlink'], - ['biblioentry', 'doc-biblioentry'], - ['bibliography', 'doc-bibliography'], - ['biblioref', 'doc-biblioref'], - ['chapter', 'doc-chapter'], - ['colophon', 'doc-colophon'], - ['conclusion', 'doc-conclusion'], - ['credit', 'doc-credit'], - ['credits', 'doc-credits'], - ['dedication', 'doc-dedication'], - ['endnote', 'doc-endnote'], - ['endnotes', 'doc-endnotes'], - ['epigraph', 'doc-epigraph'], - ['epilogue', 'doc-epilogue'], - ['errata', 'doc-errata'], - ['figure', 'figure'], - ['footnote', 'doc-footnote'], - ['foreword', 'doc-foreword'], - ['glossary', 'doc-glossary'], - ['glossdef', 'definition'], - ['glossref', 'doc-glossref'], - ['glossterm', 'term'], - ['help', 'doc-tip'], - ['index', 'doc-index'], - ['introduction', 'doc-introduction'], - ['noteref', 'doc-noteref'], - ['notice', 'doc-notice'], - ['page-list', 'doc-pagelist'], - ['pagebreak', 'doc-pagebreak'], - ['part', 'doc-part'], - ['preface', 'doc-preface'], - ['prologue', 'doc-prologue'], - ['pullquote', 'doc-pullquote'], - ['qna', 'doc-qna'], - ['referrer', 'doc-backlink'], - ['subtitle', 'doc-subtitle'], - ['tip', 'doc-tip'], - ['toc', 'doc-toc'] - ]); + // checks: [ + // { + // id: "matching-aria-role", + // evaluate: function evaluate(node, options) { + // var mappings = new Map([ + // ['abstract', 'doc-abstract'], + // ['acknowledgments', 'doc-acknowledgments'], + // ['afterword', 'doc-afterword'], + // ['appendix', 'doc-appendix'], + // ['backlink', 'doc-backlink'], + // ['biblioentry', 'doc-biblioentry'], + // ['bibliography', 'doc-bibliography'], + // ['biblioref', 'doc-biblioref'], + // ['chapter', 'doc-chapter'], + // ['colophon', 'doc-colophon'], + // ['conclusion', 'doc-conclusion'], + // ['credit', 'doc-credit'], + // ['credits', 'doc-credits'], + // ['dedication', 'doc-dedication'], + // ['endnote', 'doc-endnote'], + // ['endnotes', 'doc-endnotes'], + // ['epigraph', 'doc-epigraph'], + // ['epilogue', 'doc-epilogue'], + // ['errata', 'doc-errata'], + // ['figure', 'figure'], + // ['footnote', 'doc-footnote'], + // ['foreword', 'doc-foreword'], + // ['glossary', 'doc-glossary'], + // ['glossdef', 'definition'], + // ['glossref', 'doc-glossref'], + // ['glossterm', 'term'], + // ['help', 'doc-tip'], + // ['index', 'doc-index'], + // ['introduction', 'doc-introduction'], + // ['noteref', 'doc-noteref'], + // ['notice', 'doc-notice'], + // ['page-list', 'doc-pagelist'], + // ['pagebreak', 'doc-pagebreak'], + // ['part', 'doc-part'], + // ['preface', 'doc-preface'], + // ['prologue', 'doc-prologue'], + // ['pullquote', 'doc-pullquote'], + // ['qna', 'doc-qna'], + // ['referrer', 'doc-backlink'], + // ['subtitle', 'doc-subtitle'], + // ['tip', 'doc-tip'], + // ['toc', 'doc-toc'] + // ]); - if (node.hasAttributeNS('http://www.idpf.org/2007/ops', 'type')) { - // abort if descendant of landmarks nav (nav with epub:type=landmarks) - if (axe.utils.matchesSelector(node, 'nav[*|type~="landmarks"] *')) { - return true; - } - - // iterate for each epub:type value - var types = axe.utils.tokenList(node.getAttributeNS('http://www.idpf.org/2007/ops', 'type')); - for (const type of types) { - // If there is a 1-1 mapping, check that the role is set (best practice) - if (mappings.has(type)) { - // Note: using axe’s `getRole` util returns the effective role of the element - // (either explicitly set with the role attribute or implicit) - // So this works for types mapping to core ARIA roles (eg. glossref/glossterm). - return mappings.get(type) == axe.commons.aria.getRole(node,{dpub: true}); - } - } - } - return true; - }, - metadata: { - impact: 'minor', - messages: { - pass: function anonymous(it) { - // configured from host bootstrapper page (checker-chromium) - const k = "__aceLocalize__axecheck_matching-aria-role_pass"; - return window[k] || k; - }, - fail: function anonymous(it) { - // configured from host bootstrapper page (checker-chromium) - const k = "__aceLocalize__axecheck_matching-aria-role_fail"; - return window[k] || k; - } - } - } - } - ], - rules: [ - { - id: 'pagebreak-label', - // selector: '[*|type~="pagebreak"], [role~="doc-pagebreak"]', - matches: function matches(node, virtualNode, context) { - return node.hasAttribute('role') - && node.getAttribute('role').match(/\S+/g).includes('doc-pagebreak') - || node.hasAttributeNS('http://www.idpf.org/2007/ops', 'type') - && node.getAttributeNS('http://www.idpf.org/2007/ops', 'type').match(/\S+/g).includes('pagebreak') - }, - any: ['aria-label', 'non-empty-title'], - metadata: { - // configured from host bootstrapper page (checker-chromium) - description: (() => { const k = "__aceLocalize__axerule_pagebreak-label_desc"; return window[k] || k; })() - }, - tags: ['cat.epub'] - }, - { - id: 'epub-type-has-matching-role', - // selector: '[*|type]', - matches: function matches(node, virtualNode, context) { - return node.hasAttributeNS('http://www.idpf.org/2007/ops', 'type') - }, - any: ['matching-aria-role'], - metadata: { - // configured from host bootstrapper page (checker-chromium) - help: (() => { const k = "__aceLocalize__axerule_epub-type-has-matching-role_help"; return window[k] || k; })(), - description: (() => { const k = "__aceLocalize__axerule_epub-type-has-matching-role_desc"; return window[k] || k; })() - }, - tags: ['best-practice'] - }, - { - id: 'landmark-one-main', - all: [ - "page-no-duplicate-main" - ], - } - ] + // if (node.hasAttributeNS('http://www.idpf.org/2007/ops', 'type')) { + // // abort if descendant of landmarks nav (nav with epub:type=landmarks) + // if (axe.utils.matchesSelector(node, 'nav[*|type~="landmarks"] *')) { + // return true; + // } + + // // iterate for each epub:type value + // var types = axe.utils.tokenList(node.getAttributeNS('http://www.idpf.org/2007/ops', 'type')); + // for (const type of types) { + // // If there is a 1-1 mapping, check that the role is set (best practice) + // if (mappings.has(type)) { + // // Note: using axe’s `getRole` util returns the effective role of the element + // // (either explicitly set with the role attribute or implicit) + // // So this works for types mapping to core ARIA roles (eg. glossref/glossterm). + // return mappings.get(type) == axe.commons.aria.getRole(node,{dpub: true}); + // } + // } + // } + // return true; + // }, + // metadata: { + // impact: 'minor', + // messages: { + // pass: function anonymous(it) { + // // configured from host bootstrapper page (checker-chromium) + // const k = "__aceLocalize__axecheck_matching-aria-role_pass"; + // return window[k] || k; + // }, + // fail: function anonymous(it) { + // // configured from host bootstrapper page (checker-chromium) + // const k = "__aceLocalize__axecheck_matching-aria-role_fail"; + // return window[k] || k; + // } + // } + // } + // } + // ], + // rules: [ + // { + // id: 'pagebreak-label', + // // selector: '[*|type~="pagebreak"], [role~="doc-pagebreak"]', + // matches: function matches(node, virtualNode, context) { + // return node.hasAttribute('role') + // && node.getAttribute('role').match(/\S+/g).includes('doc-pagebreak') + // || node.hasAttributeNS('http://www.idpf.org/2007/ops', 'type') + // && node.getAttributeNS('http://www.idpf.org/2007/ops', 'type').match(/\S+/g).includes('pagebreak') + // }, + // any: ['aria-label', 'non-empty-title'], + // metadata: { + // // configured from host bootstrapper page (checker-chromium) + // description: (() => { const k = "__aceLocalize__axerule_pagebreak-label_desc"; return window[k] || k; })() + // }, + // tags: ['cat.epub'] + // }, + // { + // id: 'epub-type-has-matching-role', + // // selector: '[*|type]', + // matches: function matches(node, virtualNode, context) { + // return node.hasAttributeNS('http://www.idpf.org/2007/ops', 'type') + // }, + // any: ['matching-aria-role'], + // metadata: { + // // configured from host bootstrapper page (checker-chromium) + // help: (() => { const k = "__aceLocalize__axerule_epub-type-has-matching-role_help"; return window[k] || k; })(), + // description: (() => { const k = "__aceLocalize__axerule_epub-type-has-matching-role_desc"; return window[k] || k; })() + // }, + // tags: ['best-practice'] + // }, + // // { + // // // overrides AXE's own rule + // // id: 'landmark-one-main', + // // all: [ + // // "page-no-duplicate-main" + // // ], + // // } + // ] }); window.axe.run( @@ -227,8 +228,10 @@ daisy.ace.run = function(done) { } }, function(axeError, axeResult) { + if (axeError) { done(axeError, null); + return; } addCFIs(axeResult); diff --git a/packages/ace-core/src/scripts/ace-extraction.test.js b/packages/ace-core/src/scripts/ace-extraction.test.js index 5d12ac68..8f3d3556 100644 --- a/packages/ace-core/src/scripts/ace-extraction.test.js +++ b/packages/ace-core/src/scripts/ace-extraction.test.js @@ -8,19 +8,32 @@ const $ = require('@daisy/jest-puppeteer'); beforeAll(async () => { + // $.redirectConsole(); await $.loadXHTMLPage(); await $.injectScripts([require.resolve('./ace-extraction.js')]); - await $.injectJestMock(); + + // https://jestjs.io/docs/en/puppeteer + // https://github.com/smooth-code/jest-puppeteer + // await $.injectJestMock(); await global.page.evaluate(() => { - const mockH5O = window.mock.fn(); - mockH5O.mockReturnValue({ - asHTML: window.mock.fn(), - }); - window.HTML5Outline = mockH5O; + // const mockH5O = window.mock.fn(); + // mockH5O.mockReturnValue({ + // asHTML: window.mock.fn(), + // }); + // window.HTML5Outline = mockH5O; + // window.daisy.epub = { + // createCFI: window.mock.fn(), + // }; + // window.daisy.epub.createCFI.mockReturnValue('42'); + + window.HTML5Outline = () => { + return { + asHTML: () => { return; } + } + }; window.daisy.epub = { - createCFI: window.mock.fn(), + createCFI: () => 42, }; - window.daisy.epub.createCFI.mockReturnValue('42'); }); }); diff --git a/packages/ace-core/src/scripts/axe-patch-aria-roles.js b/packages/ace-core/src/scripts/axe-patch-aria-roles.js index 30e8733b..e9f83341 100644 --- a/packages/ace-core/src/scripts/axe-patch-aria-roles.js +++ b/packages/ace-core/src/scripts/axe-patch-aria-roles.js @@ -1,11 +1,15 @@ -'use strict'; +// 'use strict'; -/* -This patch is needed to ensure that axe's ARIA lookup table is consistent -with the mappings defined in the ARIA in HTML spec. -*/ -(function axePatch(window) { - const axe = window.axe; - axe.commons.aria.lookupTable.role.listitem.implicit = ['li']; - axe.commons.aria.lookupTable.role.figure.implicit = ['figure']; -}(window)); +// /* +// This patch is needed to ensure that axe's ARIA lookup table is consistent +// with the mappings defined in the ARIA in HTML spec. +// */ +// (function axePatch(window) { +// const axe = window.axe; + +// // https://github.com/dequelabs/axe-core/blob/v4.0.2/lib/commons/aria/lookup-table.js#L1205 +// axe.commons.aria.lookupTable.role.listitem.implicit = ['li']; + +// // https://github.com/dequelabs/axe-core/blob/v4.0.2/lib/commons/aria/lookup-table.js#L1024 +// axe.commons.aria.lookupTable.role.figure.implicit = ['figure']; +// }(window)); diff --git a/packages/ace-core/src/scripts/axe-patch-is-aria-role-allowed.js b/packages/ace-core/src/scripts/axe-patch-is-aria-role-allowed.js index 1e514ea0..715ecccb 100644 --- a/packages/ace-core/src/scripts/axe-patch-is-aria-role-allowed.js +++ b/packages/ace-core/src/scripts/axe-patch-is-aria-role-allowed.js @@ -1,34 +1,111 @@ -'use strict'; - -/* -This patch is needed to ensure that roles that do not explicitly define allowed elements -are still evaluated for the element they're used on. -E.g. `doc-cover` on `img`. -*/ -(function axePatch(window) { - const axe = window.axe; - axe.commons.aria.isAriaRoleAllowedOnElement = function isAriaRoleAllowedOnElement(node, role) { - var nodeName = node.nodeName.toUpperCase(); - var lookupTable = axe.commons.aria.lookupTable; - if (matches(node, lookupTable.elementsAllowedNoRole)) { - return false; - } - if (matches(node, lookupTable.elementsAllowedAnyRole)) { - return true; - } - var roleValue = lookupTable.role[role]; - if (!roleValue) { - return false; - } - var allowedElements = roleValue.allowedElements || []; - var out = matches(node, allowedElements); - if (Object.keys(lookupTable.evaluateRoleForElement).includes(nodeName)) { - return lookupTable.evaluateRoleForElement[nodeName]({ - node: node, - role: role, - out: out - }); - } - return out; - }; -}(window)); +// 'use strict'; + +// /* +// This patch is needed to ensure that roles that do not explicitly define allowed elements +// are still evaluated for the element they're used on. +// E.g. `doc-cover` on `img`. +// */ +// (function axePatch(window) { +// const axe = window.axe; + +// // v4.0.2 +// // axe.commons.aria.isAriaRoleAllowedOnElement.toString() +// // => +// // function(e,t){var r=e instanceof o.default?e:Object(a.getNodeFromTree)(e);if(t===Object(i.default)(r))return!0;var n=Object(s.default)(r);return Array.isArray(n.allowedRoles)?n.allowedRoles.includes(t):!!n.allowedRoles} + +// axe.commons.aria.isAriaRoleAllowedOnElement_ = axe.commons.aria.isAriaRoleAllowedOnElement; + +// var func = function isAriaRoleAllowedOnElement(node, role) { + +// // ---------------------------------------------------------------------------------------------------- +// // https://github.com/dequelabs/axe-core/blob/v4.0.2/lib/commons/aria/is-aria-role-allowed-on-element.js +// // ---------------------------------------------------------------------------------------------------- +// // const vNode = +// // node instanceof AbstractVirtuaNode ? node : getNodeFromTree(node); +// // const implicitRole = getImplicitRole(vNode); + +// // // always allow the explicit role to match the implicit role +// // if (role === implicitRole) { +// // return true; +// // } + +// // const spec = getElementSpec(vNode); + +// // if (Array.isArray(spec.allowedRoles)) { +// // return spec.allowedRoles.includes(role); +// // } + +// // return !!spec.allowedRoles; + +// return axe.commons.aria.isAriaRoleAllowedOnElement_(node, role); + +// // ---------------------------------------------------------------------------------------------------- +// // https://github.com/dequelabs/axe-core/blob/v3.2.2/lib/commons/aria/is-aria-role-allowed-on-element.js +// // ---------------------------------------------------------------------------------------------------- +// // const nodeName = node.nodeName.toUpperCase(); +// // const lookupTable = axe.commons.aria.lookupTable; + +// // // if given node can have no role - return false +// // if (matches(node, lookupTable.elementsAllowedNoRole)) { +// // return false; +// // } +// // // if given node allows any role - return true +// // if (matches(node, lookupTable.elementsAllowedAnyRole)) { +// // return true; +// // } + +// // // get role value (if exists) from lookupTable.role +// // const roleValue = lookupTable.role[role]; + +// // // if given role does not exist in lookupTable - return false +// // if (!roleValue || !roleValue.allowedElements) { +// // return false; +// // } + +// // // validate attributes and conditions (if any) from allowedElement to given node +// // let out = matches(node, roleValue.allowedElements); + +// // // if given node type has complex condition to evaluate a given aria-role, execute the same +// // if (Object.keys(lookupTable.evaluateRoleForElement).includes(nodeName)) { +// // return lookupTable.evaluateRoleForElement[nodeName]({ node, role, out }); +// // } +// // return out; + +// // ---------------------------------------------------------------------------------------------------- +// // OLD PATCH FOR AXE 3.2.2 (see above) +// // https://github.com/daisy/ace/blob/v1.1.1/packages/ace-core/src/scripts/axe-patch-is-aria-role-allowed.js +// // ---------------------------------------------------------------------------------------------------- +// // var nodeName = node.nodeName.toUpperCase(); +// // var lookupTable = axe.commons.aria.lookupTable; +// // if (matches(node, lookupTable.elementsAllowedNoRole)) { +// // return false; +// // } +// // if (matches(node, lookupTable.elementsAllowedAnyRole)) { +// // return true; +// // } +// // var roleValue = lookupTable.role[role]; +// // // ------------------------------------------------ +// // // THIS IS THE ACTUAL PATCH: +// // if (!roleValue) { // || !roleValue.allowedElements +// // return false; +// // } +// // var allowedElements = roleValue.allowedElements || []; +// // // ------------------------------------------------ +// // var out = matches(node, allowedElements); +// // if (Object.keys(lookupTable.evaluateRoleForElement).includes(nodeName)) { +// // return lookupTable.evaluateRoleForElement[nodeName]({ +// // node: node, +// // role: role, +// // out: out +// // }); +// // } +// // return out; +// }; + +// axe.commons.aria.isAriaRoleAllowedOnElement = func; + +// if (axe.commons.aria.isAriaRoleAllowedOnElement_.boundContext) { +// axe.commons.aria.isAriaRoleAllowedOnElement = axe.commons.aria.isAriaRoleAllowedOnElement.bind(axe.commons.aria.isAriaRoleAllowedOnElement_.boundContext); +// } + +// }(window)); diff --git a/packages/ace-core/src/scripts/axe-patch-listitem.js b/packages/ace-core/src/scripts/axe-patch-listitem.js index f82e6b44..3796177f 100644 --- a/packages/ace-core/src/scripts/axe-patch-listitem.js +++ b/packages/ace-core/src/scripts/axe-patch-listitem.js @@ -1,32 +1,139 @@ -'use strict'; - -/* -This patch is needed to ensure that roles *inheriting* a list role are allowed -as listitem parents. -E.g. `
    ` as parent of `li`. -*/ - -(function axePatch(window) { - const axe = window.axe; - - axe._audit.checks['listitem'].evaluate = function evaluate(node, options, virtualNode, context) { - const parent = axe.commons.dom.getComposedParent(node); - if (!parent) { - // Can only happen with detached DOM nodes and roots: - return undefined; - } - - const parentTagName = parent.nodeName.toUpperCase(); - const parentRole = (parent.getAttribute('role') || '').toLowerCase(); - - if (parentRole === 'list') { - return true; - } - - if (parentRole && axe.commons.aria.isValidRole(parentRole)) { - return aria.getRoleType(role) === 'list'; - } - - return ['UL', 'OL'].includes(parentTagName); - } -}(window)); +// 'use strict'; + +// /* +// This patch is needed to ensure that roles *inheriting* a list role are allowed +// as listitem parents. +// E.g. `
      ` as parent of `li`. +// */ + +// (function axePatch(window) { +// const axe = window.axe; + +// // v4.0.2 +// // axe._audit.checks['listitem'].evaluate.toString() +// // => +// // function(e){var t=Object(a.getComposedParent)(e);if(t){var r=t.nodeName.toUpperCase(),n=(t.getAttribute("role")||"").toLowerCase();return!!["presentation","none","list"].includes(n)||(n&&Object(o.isValidRole)(n)?(this.data({messageKey:"roleNotValid"}),!1):["UL","OL"].includes(r))}} + +// // in the WebPack bundle (node_modules/axe-core/axe.js) +// // './lib/checks/lists/listitem-evaluate.js' + +// axe._audit.checks['listitem'].evaluate = function evaluate(node, options, virtualNode, context) { + +// // ---------------------------------------------------------------------------------------------------- +// // https://github.com/dequelabs/axe-core/blob/v4.0.2/lib/checks/lists/listitem-evaluate.js +// // ---------------------------------------------------------------------------------------------------- +// // const parent = getComposedParent(node); +// // if (!parent) { +// // // Can only happen with detached DOM nodes and roots: +// // return undefined; +// // } + +// // const parentTagName = parent.nodeName.toUpperCase(); +// // const parentRole = (parent.getAttribute('role') || '').toLowerCase(); + +// // if (['presentation', 'none', 'list'].includes(parentRole)) { +// // return true; +// // } + +// // if (parentRole && isValidRole(parentRole)) { +// // this.data({ +// // messageKey: 'roleNotValid' +// // }); +// // return false; +// // } + +// // return ['UL', 'OL'].includes(parentTagName); + +// // ---------------------------------------------------------------------------------------------------- +// // https://github.com/dequelabs/axe-core/blob/v3.5.4/lib/checks/lists/listitem.js +// // ---------------------------------------------------------------------------------------------------- +// // const parent = axe.commons.dom.getComposedParent(node); +// // if (!parent) { +// // // Can only happen with detached DOM nodes and roots: +// // return undefined; +// // } + +// // const parentTagName = parent.nodeName.toUpperCase(); +// // const parentRole = (parent.getAttribute('role') || '').toLowerCase(); + +// // if (parentRole === 'list') { +// // return true; +// // } + +// // if (parentRole && axe.commons.aria.isValidRole(parentRole)) { +// // this.data({ +// // messageKey: 'roleNotValid' +// // }); +// // return false; +// // } + +// // return ['UL', 'OL'].includes(parentTagName); + +// // ---------------------------------------------------------------------------------------------------- +// // https://github.com/dequelabs/axe-core/blob/v3.2.2/lib/checks/lists/listitem.js +// // ---------------------------------------------------------------------------------------------------- + // const parent = axe.commons.dom.getComposedParent(node); + // if (!parent) { + // // Can only happen with detached DOM nodes and roots: + // return undefined; + // } + + // const parentTagName = parent.nodeName.toUpperCase(); + // const parentRole = (parent.getAttribute('role') || '').toLowerCase(); + + // if (parentRole === 'list') { + // return true; + // } + + // if (parentRole && axe.commons.aria.isValidRole(parentRole)) { + // this.data({ + // messageKey: 'roleNotValid' + // }); + // return false; + // } + + // return ['UL', 'OL'].includes(parentTagName); + +// // ---------------------------------------------------------------------------------------------------- +// // OLD PATCH FOR AXE 3.2.2 (see above) +// // https://github.com/daisy/ace/blob/v1.1.1/packages/ace-core/src/scripts/axe-patch-listitem.js +// // ---------------------------------------------------------------------------------------------------- + +// const parent = axe.commons.dom.getComposedParent(node); +// if (!parent) { +// // Can only happen with detached DOM nodes and roots: +// return undefined; +// } + +// const parentTagName = parent.nodeName.toUpperCase(); +// const parentRole = (parent.getAttribute('role') || '').toLowerCase(); + +// if (['presentation', 'none', 'list'].includes(parentRole)) { +// return true; +// } + +// if (parentRole && axe.commons.aria.isValidRole(parentRole)) { +// // ------------------------------------------------ +// // THIS IS THE ACTUAL PATCH: +// if (axe.commons.aria.getRoleType(parentRole) === 'list') { +// return true; +// } +// // ------------------------------------------------ + +// try { +// this.data({ +// messageKey: 'roleNotValid' +// }); +// } catch (e) { +// // ignore error ("this" binding?) +// } +// return false; +// } + +// return ['UL', 'OL'].includes(parentTagName); +// }; + +// if (axe._audit.checks['listitem'].evaluate.boundContext) { +// axe._audit.checks['listitem'].evaluate = axe._audit.checks['listitem'].evaluate.bind(axe._audit.checks['listitem'].evaluate.boundContext); +// } +// }(window)); diff --git a/packages/ace-core/src/scripts/axe-patch-only-list-items.js b/packages/ace-core/src/scripts/axe-patch-only-list-items.js index 56313f18..62761205 100644 --- a/packages/ace-core/src/scripts/axe-patch-only-list-items.js +++ b/packages/ace-core/src/scripts/axe-patch-only-list-items.js @@ -1,63 +1,277 @@ -'use strict'; - -/* -This patch is needed to ensure that roles *inheriting* a listitem role are allowed -as list children. -E.g. `doc-biblioentry` and others as children of `ul`. -*/ - -(function axePatch(window) { - const axe = window.axe; - - axe._audit.checks['only-listitems'].evaluate = function evaluate(node, options, virtualNode, context) { - var dom = axe.commons.dom; - var aria = axe.commons.aria; - var getIsListItemRole = function getIsListItemRole(role, tagName) { - return role === 'listitem' || aria.getRoleType(role) === 'listitem' || tagName === 'LI' && !role; - }; - var getHasListItem = function getHasListItem(hasListItem, tagName, isListItemRole) { - return hasListItem || tagName === 'LI' && isListItemRole || isListItemRole; - }; - var base = { - badNodes: [], - isEmpty: true, - hasNonEmptyTextNode: false, - hasListItem: false, - liItemsWithRole: 0 - }; - var out = virtualNode.children.reduce(function(out, _ref6) { - var actualNode = _ref6.actualNode; - var tagName = actualNode.nodeName.toUpperCase(); - if (actualNode.nodeType === 1 && dom.isVisible(actualNode, true, false)) { - var role = (actualNode.getAttribute('role') || '').toLowerCase(); - var isListItemRole = getIsListItemRole(role, tagName); - out.hasListItem = getHasListItem(out.hasListItem, tagName, isListItemRole); - if (isListItemRole) { - out.isEmpty = false; - } - if (tagName === 'LI' && !isListItemRole) { - out.liItemsWithRole++; - } - if (tagName !== 'LI' && !isListItemRole) { - out.badNodes.push(actualNode); - } - } - if (actualNode.nodeType === 3) { - if (actualNode.nodeValue.trim() !== '') { - out.hasNonEmptyTextNode = true; - } - } - return out; - }, base); - var virtualNodeChildrenOfTypeLi = virtualNode.children.filter(function(_ref7) { - var actualNode = _ref7.actualNode; - return actualNode.nodeName.toUpperCase() === 'LI'; - }); - var allLiItemsHaveRole = out.liItemsWithRole > 0 && virtualNodeChildrenOfTypeLi.length === out.liItemsWithRole; - if (out.badNodes.length) { - this.relatedNodes(out.badNodes); - } - var isInvalidListItem = !(out.hasListItem || out.isEmpty && !allLiItemsHaveRole); - return isInvalidListItem || !!out.badNodes.length || out.hasNonEmptyTextNode; - } -}(window)); +// 'use strict'; + +// /* +// This patch is needed to ensure that roles *inheriting* a listitem role are allowed +// as list children. +// E.g. `doc-biblioentry` and others as children of `ul`. +// */ + +// (function axePatch(window) { +// const axe = window.axe; + +// // v4.0.2 +// // axe._audit.checks['only-listitems'].evaluate.toString() +// // => +// // "function(e,t,r){var o=!1,i=!1,s=!0,l=[],u=[],c=[];return r.children.forEach(function(e){var t,r,n,a=e.actualNode;3!==a.nodeType||""===a.nodeValue.trim()?1===a.nodeType&&Object(d.isVisible)(a,!0,!1)&&(s=!1,t="LI"===a.nodeName.toUpperCase(),n="listitem"===(r=Object(m.getRole)(e)),t||n||l.push(a),t&&!n&&(u.push(a),c.includes(r)||c.push(r)),n&&(i=!0)):o=!0}),o||l.length?(this.relatedNodes(l),!0):!s&&!i&&(this.relatedNodes(u),this.data({messageKey:"roleNotValid",roles:c.join(", ")}),!0)}" + +// // in the WebPack bundle (node_modules/axe-core/axe.js) +// // './lib/checks/lists/only-listitems-evaluate.js' + +// axe._audit.checks['only-listitems'].evaluate = function evaluate(node, options, virtualNode, context) { + +// // ---------------------------------------------------------------------------------------------------- +// // https://github.com/dequelabs/axe-core/blob/v4.0.2/lib/checks/lists/only-listitems-evaluate.js +// // ---------------------------------------------------------------------------------------------------- +// // let hasNonEmptyTextNode = false; +// // let atLeastOneListitem = false; +// // let isEmpty = true; +// // let badNodes = []; +// // let badRoleNodes = []; +// // let badRoles = []; + +// // virtualNode.children.forEach(vNode => { +// // const { actualNode } = vNode; + +// // if (actualNode.nodeType === 3 && actualNode.nodeValue.trim() !== '') { +// // hasNonEmptyTextNode = true; +// // return; +// // } + +// // if (actualNode.nodeType !== 1 || !isVisible(actualNode, true, false)) { +// // return; +// // } + +// // isEmpty = false; +// // const isLi = actualNode.nodeName.toUpperCase() === 'LI'; +// // const role = getRole(vNode); +// // const isListItemRole = role === 'listitem'; +// // if (!isLi && !isListItemRole) { +// // badNodes.push(actualNode); +// // } + +// // if (isLi && !isListItemRole) { +// // badRoleNodes.push(actualNode); + +// // if (!badRoles.includes(role)) { +// // badRoles.push(role); +// // } +// // } + +// // if (isListItemRole) { +// // atLeastOneListitem = true; +// // } +// // }); + +// // if (hasNonEmptyTextNode || badNodes.length) { +// // this.relatedNodes(badNodes); +// // return true; +// // } + +// // if (isEmpty || atLeastOneListitem) { +// // return false; +// // } + +// // this.relatedNodes(badRoleNodes); +// // this.data({ +// // messageKey: 'roleNotValid', +// // roles: badRoles.join(', ') +// // }); +// // return true; + +// // ---------------------------------------------------------------------------------------------------- +// // https://github.com/dequelabs/axe-core/blob/v3.5.4/lib/checks/lists/only-listitems.js +// // ---------------------------------------------------------------------------------------------------- +// // const { dom, aria } = axe.commons; + +// // let hasNonEmptyTextNode = false; +// // let atLeastOneListitem = false; +// // let isEmpty = true; +// // let badNodes = []; +// // let badRoleNodes = []; +// // let badRoles = []; + +// // virtualNode.children.forEach(vNode => { +// // const { actualNode } = vNode; + +// // if (actualNode.nodeType === 3 && actualNode.nodeValue.trim() !== '') { +// // hasNonEmptyTextNode = true; +// // return; +// // } + +// // if (actualNode.nodeType !== 1 || !dom.isVisible(actualNode, true, false)) { +// // return; +// // } + +// // isEmpty = false; +// // const isLi = actualNode.nodeName.toUpperCase() === 'LI'; +// // const role = aria.getRole(vNode); +// // const isListItemRole = role === 'listitem'; + +// // if (!isLi && !isListItemRole) { +// // badNodes.push(actualNode); +// // } + +// // if (isLi && !isListItemRole) { +// // badRoleNodes.push(actualNode); + +// // if (!badRoles.includes(role)) { +// // badRoles.push(role); +// // } +// // } + +// // if (isListItemRole) { +// // atLeastOneListitem = true; +// // } +// // }); + +// // if (hasNonEmptyTextNode || badNodes.length) { +// // this.relatedNodes(badNodes); +// // return true; +// // } + +// // if (isEmpty || atLeastOneListitem) { +// // return false; +// // } + +// // this.relatedNodes(badRoleNodes); +// // this.data({ +// // messageKey: 'roleNotValid', +// // roles: badRoles.join(', ') +// // }); +// // return true; + +// // ---------------------------------------------------------------------------------------------------- +// // https://github.com/dequelabs/axe-core/blob/v3.2.2/lib/checks/lists/only-listitems.js +// // ---------------------------------------------------------------------------------------------------- +// // const { dom } = axe.commons; +// // const getIsListItemRole = (role, tagName) => { +// // return role === 'listitem' || (tagName === 'LI' && !role); +// // }; + +// // const getHasListItem = (hasListItem, tagName, isListItemRole) => { +// // return hasListItem || (tagName === 'LI' && isListItemRole) || isListItemRole; +// // }; + +// // let base = { +// // badNodes: [], +// // isEmpty: true, +// // hasNonEmptyTextNode: false, +// // hasListItem: false, +// // liItemsWithRole: 0 +// // }; + +// // let out = virtualNode.children.reduce((out, { actualNode }) => { +// // /*eslint +// // max-statements: ["error", 20] +// // complexity: ["error", 12] +// // */ +// // const tagName = actualNode.nodeName.toUpperCase(); + +// // if (actualNode.nodeType === 1 && dom.isVisible(actualNode, true, false)) { +// // const role = (actualNode.getAttribute('role') || '').toLowerCase(); +// // const isListItemRole = getIsListItemRole(role, tagName); + +// // out.hasListItem = getHasListItem(out.hasListItem, tagName, isListItemRole); + +// // if (isListItemRole) { +// // out.isEmpty = false; +// // } +// // if (tagName === 'LI' && !isListItemRole) { +// // out.liItemsWithRole++; +// // } +// // if (tagName !== 'LI' && !isListItemRole) { +// // out.badNodes.push(actualNode); +// // } +// // } +// // if (actualNode.nodeType === 3) { +// // if (actualNode.nodeValue.trim() !== '') { +// // out.hasNonEmptyTextNode = true; +// // } +// // } + +// // return out; +// // }, base); + +// // const virtualNodeChildrenOfTypeLi = virtualNode.children.filter( +// // ({ actualNode }) => { +// // return actualNode.nodeName.toUpperCase() === 'LI'; +// // } +// // ); + +// // const allLiItemsHaveRole = +// // out.liItemsWithRole > 0 && +// // virtualNodeChildrenOfTypeLi.length === out.liItemsWithRole; + +// // if (out.badNodes.length) { +// // this.relatedNodes(out.badNodes); +// // } + +// // const isInvalidListItem = !( +// // out.hasListItem || +// // (out.isEmpty && !allLiItemsHaveRole) +// // ); +// // return isInvalidListItem || !!out.badNodes.length || out.hasNonEmptyTextNode; + +// // ---------------------------------------------------------------------------------------------------- +// // OLD PATCH FOR AXE 3.2.2 (see above) +// // https://github.com/daisy/ace/blob/v1.1.1/packages/ace-core/src/scripts/axe-patch-only-list-items.js +// // ---------------------------------------------------------------------------------------------------- + +// var dom = axe.commons.dom; +// var aria = axe.commons.aria; +// var getIsListItemRole = function getIsListItemRole(role, tagName) { +// // ------------------------------------------------ +// // THIS IS THE ACTUAL PATCH: +// return role === 'listitem' || aria.getRoleType(role) === 'listitem' || tagName === 'LI' && !role; +// // ------------------------------------------------ +// }; +// var getHasListItem = function getHasListItem(hasListItem, tagName, isListItemRole) { +// return hasListItem || tagName === 'LI' && isListItemRole || isListItemRole; +// }; +// var base = { +// badNodes: [], +// isEmpty: true, +// hasNonEmptyTextNode: false, +// hasListItem: false, +// liItemsWithRole: 0 +// }; +// var out = virtualNode.children.reduce(function(out, _ref6) { +// var actualNode = _ref6.actualNode; +// var tagName = actualNode.nodeName.toUpperCase(); +// if (actualNode.nodeType === 1 && dom.isVisible(actualNode, true, false)) { +// var role = (actualNode.getAttribute('role') || '').toLowerCase(); +// var isListItemRole = getIsListItemRole(role, tagName); +// out.hasListItem = getHasListItem(out.hasListItem, tagName, isListItemRole); +// if (isListItemRole) { +// out.isEmpty = false; +// } +// if (tagName === 'LI' && !isListItemRole) { +// out.liItemsWithRole++; +// } +// if (tagName !== 'LI' && !isListItemRole) { +// out.badNodes.push(actualNode); +// } +// } +// if (actualNode.nodeType === 3) { +// if (actualNode.nodeValue.trim() !== '') { +// out.hasNonEmptyTextNode = true; +// } +// } +// return out; +// }, base); +// var virtualNodeChildrenOfTypeLi = virtualNode.children.filter(function(_ref7) { +// var actualNode = _ref7.actualNode; +// return actualNode.nodeName.toUpperCase() === 'LI'; +// }); +// var allLiItemsHaveRole = out.liItemsWithRole > 0 && virtualNodeChildrenOfTypeLi.length === out.liItemsWithRole; +// if (out.badNodes.length) { +// this.relatedNodes(out.badNodes); +// } +// var isInvalidListItem = !(out.hasListItem || out.isEmpty && !allLiItemsHaveRole); +// return isInvalidListItem || !!out.badNodes.length || out.hasNonEmptyTextNode; +// }; + +// if (axe._audit.checks['only-listitems'].evaluate.boundContext) { +// axe._audit.checks['only-listitems'].evaluate = axe._audit.checks['only-listitems'].evaluate.bind(axe._audit.checks['only-listitems'].evaluate.boundContext); +// } +// }(window)); diff --git a/packages/ace-core/src/scripts/function-bind-bound-object.js b/packages/ace-core/src/scripts/function-bind-bound-object.js new file mode 100644 index 00000000..1b7abf89 --- /dev/null +++ b/packages/ace-core/src/scripts/function-bind-bound-object.js @@ -0,0 +1,30 @@ +// /* eslint-disable */ + +// 'use strict'; + +// // var _bind = Function.prototype.apply.bind(Function.prototype.bind); +// // Object.defineProperty(Function.prototype, 'bind', { +// // value: function(boundContext) { +// // var boundFunction = _bind(this, arguments); +// // boundFunction.boundContext = boundContext; +// // return boundFunction; +// // } +// // }); + +// (function (window, bind) { + +// Object.defineProperties(Function.prototype, { +// 'bind': { +// value: function (boundContext) { +// var newf = bind.apply(this, arguments); +// newf.boundContext = boundContext; +// return newf; +// } +// }, +// 'isBound': { +// value: function () { +// return this.hasOwnProperty('boundContext'); +// } +// } +// }); +// }(window, Function.prototype.bind)); \ No newline at end of file diff --git a/packages/ace-http/package.json b/packages/ace-http/package.json index 87e24852..0d56196e 100644 --- a/packages/ace-http/package.json +++ b/packages/ace-http/package.json @@ -1,6 +1,11 @@ { "name": "@daisy/ace-http", - "version": "1.1.1", + "version": "1.2.0-beta.15", + "engines": { + "node": ">=10.0.0", + "yarn": "^1.22.5", + "npm": ">=6.14.12" + }, "description": "HTTP API for Ace", "author": { "name": "DAISY developers", @@ -19,17 +24,17 @@ "main": "lib/index.js", "bin": "bin/ace-http.js", "dependencies": { - "@daisy/ace-axe-runner-puppeteer": "^1.1.0", - "@daisy/ace-core": "^1.1.1", - "@daisy/ace-logger": "^1.1.0", - "@daisy/ace-meta": "^1.1.0", - "express": "^4.15.5", - "express-easy-zip": "^1.1.4", - "meow": "^3.7.0", - "multer": "^1.3.0", - "tmp": "^0.0.33", - "uuidv4": "^0.5.0", - "winston": "^2.4.0" + "@daisy/ace-axe-runner-puppeteer": "^1.2.0-beta.15", + "@daisy/ace-core": "^1.2.0-beta.15", + "@daisy/ace-logger": "^1.2.0-beta.15", + "@daisy/ace-meta": "^1.2.0-beta.15", + "express": "^4.17.1", + "express-easy-zip": "^1.1.5", + "meow": "^9.0.0", + "multer": "^1.4.2", + "tmp": "^0.2.1", + "uuid": "^8.3.2", + "winston": "^3.3.3" }, "publishConfig": { "access": "public" diff --git a/packages/ace-http/src/index.js b/packages/ace-http/src/index.js index d2b410c2..9762ffbf 100644 --- a/packages/ace-http/src/index.js +++ b/packages/ace-http/src/index.js @@ -1,7 +1,7 @@ 'use strict'; const express = require('express'); -const uuidv4 = require('uuid/v4'); +const { v4: uuidv4 } = require('uuid'); const multer = require('multer'); const fs = require('fs'); const zip = require('express-easy-zip'); @@ -16,6 +16,8 @@ const axeRunner = require('@daisy/ace-axe-runner-puppeteer'); const pkg = require('@daisy/ace-meta/package'); +// tmp.setGracefulCleanup(); + const UPLOADS = tmp.dirSync({ unsafeCleanup: true }).name; const DEFAULTPORT = 8000; const DEFAULTHOST = "localhost"; @@ -25,9 +27,7 @@ var upload = multer({dest: UPLOADS}); var joblist = []; var baseurl = ""; -const cli = meow({ - help: -` +const meowHelpMessage = ` Usage: ace-http [options] Options: @@ -43,26 +43,54 @@ const cli = meow({ -l, --lang language code for localized messages (e.g. "fr"), default is "en" Examples - $ ace-http -p 3000 -`, -// autoVersion: false, -version: pkg.version -}, { - alias: { - h: 'help', - s: 'silent', - v: 'version', - V: 'verbose', - H: 'host', - p: 'port', - l: 'lang', - }, - boolean: ['verbose', 'silent'], - string: ['host', 'port', 'lang'], -}); + $ ace-http -p 3000`; +const meowOptions = { + autoHelp: false, + autoVersion: false, + version: pkg.version, + flags: { + help: { + alias: 'h' + }, + silent: { + alias: 's', + type: 'boolean' + }, + version: { + alias: 'v' + }, + verbose: { + alias: 'V', + type: 'boolean' + }, + host: { + alias: 'H', + type: 'string' + }, + port: { + alias: 'p', + type: 'string' + }, + lang: { + alias: 'l', + type: 'string' + } + } +}; +const cli = meow(meowHelpMessage, meowOptions); function run() { - logger.initLogger({verbose: cli.flags.verbose, silent: cli.flags.silent}); + if (cli.flags.help) { + cli.showHelp(0); + return; + } + + if (cli.flags.version) { + cli.showVersion(2); + return; + } + + logger.initLogger({verbose: cli.flags.verbose, silent: cli.flags.silent, fileName: "ace-http.log"}); server = express(); server.use(zip()); initRoutes(); diff --git a/packages/ace-localize/package.json b/packages/ace-localize/package.json index 9edb0d58..ae54ede5 100644 --- a/packages/ace-localize/package.json +++ b/packages/ace-localize/package.json @@ -1,6 +1,11 @@ { "name": "@daisy/ace-localize", - "version": "1.1.0", + "version": "1.2.0-beta.15", + "engines": { + "node": ">=10.0.0", + "yarn": "^1.22.5", + "npm": ">=6.14.12" + }, "description": "Localization utilities for Ace", "author": { "name": "DAISY developers", @@ -18,8 +23,8 @@ "license": "MIT", "main": "lib/localize.js", "dependencies": { - "i18next": "^15.1.0", - "winston": "^2.4.0" + "i18next": "^20.2.1", + "winston": "^3.3.3" }, "publishConfig": { "access": "public" diff --git a/packages/ace-localize/src/localize.js b/packages/ace-localize/src/localize.js index ed2dc7f8..32e8009d 100644 --- a/packages/ace-localize/src/localize.js +++ b/packages/ace-localize/src/localize.js @@ -24,6 +24,7 @@ export function newLocalizer(resources) { const i18nextInstance = i18n.createInstance(); // https://www.i18next.com/overview/configuration-options i18nextInstance.init({ + ignoreJSONStructure: false, debug: false, resources: resources, // lng: undefined, @@ -42,6 +43,27 @@ export function newLocalizer(resources) { }, }); + function ensureLanguage(doneCallback) { + if (i18nextInstance.language !== _currentLanguage) { + // https://github.com/i18next/i18next/blob/master/CHANGELOG.md#1800 + // i18nextInstance.language not instantly ready (because async loadResources()), + // but i18nextInstance.isLanguageChangingTo immediately informs which locale i18next is switching to. + i18nextInstance.changeLanguage(_currentLanguage).then((_t) => { + if (doneCallback) { + doneCallback(); + } + }).catch((err) => { + winston.info('i18next changeLanguage reject: ' + _currentLanguage); + winston.info(err); + if (doneCallback) { + doneCallback(); + } + }); + } else { + doneCallback(); + } + } + return { LANGUAGES: resources, @@ -52,24 +74,24 @@ export function newLocalizer(resources) { getCurrentLanguage: function() { return _currentLanguage; }, - setCurrentLanguage: function(language) { + setCurrentLanguage: function(language, doneCallback) { for (const lang of LANGUAGE_KEYS) { if (language === lang) { _currentLanguage = language; + ensureLanguage(doneCallback); return; } } // fallback _currentLanguage = DEFAULT_LANGUAGE; + ensureLanguage(doneCallback); }, localize: function(msg, options) { const opts = options || {}; - - if (i18nextInstance.language !== _currentLanguage) { - i18nextInstance.changeLanguage(_currentLanguage); - } + + // ensureLanguage(); return i18nextInstance.t(msg, opts); }, diff --git a/packages/ace-logger/package.json b/packages/ace-logger/package.json index 7321c37a..ae5a56cf 100644 --- a/packages/ace-logger/package.json +++ b/packages/ace-logger/package.json @@ -1,6 +1,11 @@ { "name": "@daisy/ace-logger", - "version": "1.1.0", + "version": "1.2.0-beta.15", + "engines": { + "node": ">=10.0.0", + "yarn": "^1.22.5", + "npm": ">=6.14.12" + }, "description": "Logger bootsrap for Ace", "author": { "name": "DAISY developers", @@ -18,9 +23,10 @@ "license": "MIT", "main": "lib/index.js", "dependencies": { - "@daisy/ace-config": "^1.1.0", - "fs-extra": "^6.0.1", - "winston": "^2.4.0" + "@daisy/ace-config": "^1.2.0-beta.15", + "fs-extra": "^9.1.0", + "uuid": "^8.3.2", + "winston": "^3.3.3" }, "publishConfig": { "access": "public" diff --git a/packages/ace-logger/src/defaults.json b/packages/ace-logger/src/defaults.json index b69ebc54..80a164b1 100644 --- a/packages/ace-logger/src/defaults.json +++ b/packages/ace-logger/src/defaults.json @@ -1,6 +1,7 @@ { "logging": { "level": "info", - "fileName": "ace.log" + "fileName": "ace.log", + "maxMinutes": 5 } } diff --git a/packages/ace-logger/src/index.js b/packages/ace-logger/src/index.js index de39303a..c02b77b6 100644 --- a/packages/ace-logger/src/index.js +++ b/packages/ace-logger/src/index.js @@ -4,71 +4,154 @@ const { config, paths } = require('@daisy/ace-config'); const fs = require('fs-extra'); const path = require('path'); const winston = require('winston'); +const { v4: uuidv4 } = require('uuid'); + const defaults = require('./defaults'); const logConfig = config.get('logging', defaults.logging); +const disableWinstonFileTransport = false; // (typeof process.env.JEST_TESTS !== "undefined") && process.platform === "win32"; + +// https://github.com/winstonjs/winston/blob/3.2.1/lib/winston/transports/file.js +const closeTransportAndWaitForFinish = async (transport) => { + if (!transport.close || // e.g. transport.name === 'console' + disableWinstonFileTransport) { + return Promise.resolve(); + } + // e.g. transport.name === 'file' + + return new Promise((resolve, reject) => { + transport._doneFinish = false; + function done(wasTimeout) { + if (transport._doneFinish) { + return; + } + transport._doneFinish = true; + if (!wasTimeout && transport._doneTimeoutID) { + clearTimeout(transport._doneTimeoutID); + } + resolve(); + } + transport._doneTimeoutID = setTimeout(() => { + console.log("WINSTON TIMEOUT"); + done(true); + }, 5000); + + if (transport._stream) { + // https://github.com/winstonjs/winston/blob/49ccdb6604ecce590eda2915b130970ee0f1b6a3/lib/winston/transports/file.js#L96 + transport._stream.once('finish', done); // emitted too early! + setImmediate(() => { + transport._stream.end(); // https://github.com/nodejs/readable-stream/blob/4ba93f84cf8812ca2af793c7304a5c16de72088a/lib/_stream_writable.js#L547 + }); + } else { + transport.once('closed', done); // emitted too early! also 'flush', see https://github.com/winstonjs/winston/blob/49ccdb6604ecce590eda2915b130970ee0f1b6a3/lib/winston/transports/file.js#L457-L469 + transport.close(); + } + }); +} + module.exports.initLogger = function initLogger(options = {}) { - // Check logging directoy exists - if (!fs.existsSync(paths.log)) { - fs.ensureDirSync(paths.log); + + const dateNow = new Date(); + const msfromPosixEpochUntilNow = dateNow.getTime(); + const dateNowFormatted = dateNow.toISOString().replace(/:/g, "-").replace(/\./g, "-"); + + if (!disableWinstonFileTransport) { + + if (!fs.existsSync(paths.log)) { + try { + fs.ensureDirSync(paths.log); + } catch (err) { + // ignore (other process won the dir creation race?) + } + } else { + try { + const msPer_second = 1000 * 1; + const msPer_minute = msPer_second * 60; + // const msPer_hour = msPer_minute * 60; + // const msPer_day = msPer_hour * 24; + // const msPer_year = msPer_day * 365; + + const msMax = (options.maxMinutes || defaults.logging.maxMinutes) * msPer_minute; // log files older than x minutes are deleted + + const dirContents = fs.readdirSync(paths.log); + dirContents.forEach((dirEntry) => { + const dirEntryPath = path.join(paths.log, dirEntry); + const stats = fs.statSync(dirEntryPath); + if (stats.isFile()) { + const msDiff = msfromPosixEpochUntilNow - stats.mtimeMs; // stats.mtime.getTime() + const doRemove = msDiff > msMax; + if (doRemove) { + // fs.removeSync(dirEntryPath); + fs.unlink(dirEntryPath, (_err) => { + // ignore (file busy / already open with read or write access?) + }); + } + } + }); + } catch (err) { + // ignore + } + } + } + + let logConfigFileName = options.fileName || logConfig.fileName; + let logfile = path.join(paths.log, logConfigFileName); + if (!disableWinstonFileTransport) { + do { + let uniqueID = uuidv4(); + const ext = path.extname(logConfigFileName); + const baseName = path.basename(logConfigFileName, ext && ext.length ? ext : undefined); + logfile = path.join(paths.log, `${baseName}_${dateNowFormatted}_${uniqueID}${ext}`); + } while (fs.existsSync(logfile)); } - // OS-dependant path to log file - const logfile = path.join(paths.log, logConfig.fileName); + const defaultLogger = winston.clear(); - // clear old log file - if (fs.existsSync(logfile)) { - fs.removeSync(logfile); + const fileTransport = new winston.transports.File({ name: 'file', filename: logfile, silent: disableWinstonFileTransport }); + const consoleTransport = new winston.transports.Console({ name: 'console', stderrLevels: ['error'], silent: false }); + const transports = [ + fileTransport + ]; + if (!options.silent) { + transports.push(consoleTransport); } - // set up logger const level = (options.verbose) ? 'verbose' : logConfig.level; winston.configure({ level, - transports: [ - new winston.transports.File({ name: 'file', filename: logfile }), - new winston.transports.Console({ name: 'console' }), - ], + transports, + format: winston.format.combine( + // winston.format.colorize(), + winston.format.cli() + ) }); - if (options.silent) { - winston.remove('console'); - } - winston.cli(); -}; -/* eslint-disable no-underscore-dangle */ -// Properly wait that loggers write to file before exitting -// See https://github.com/winstonjs/winston/issues/228 -winston.logAndExit = function logAndExit(level, msg, codeOrCallback) { - const self = this; - this.log(level, msg, () => { - let numFlushes = 0; - let numFlushed = 0; - Object.keys(self.default.transports).forEach((k) => { - if (self.default.transports[k]._stream) { - numFlushes += 1; - self.default.transports[k]._stream.once('finish', () => { - numFlushed += 1; - if (numFlushes === numFlushed) { - if (typeof codeOrCallback === 'function') { - codeOrCallback(); - } else { - process.exit(codeOrCallback); - } - } - }); - self.default.transports[k]._stream.end(); + // defaultLogger.on('error', () => { }); + // defaultLogger.emitErrs = false; + + // Properly wait that loggers write to file before exitting + // See https://github.com/winstonjs/winston/issues/228 + winston.logAndWaitFinish = async (level, msg) => { + return new Promise(async (resolve, reject) => { + + winston.log(level, msg + // , () => { + // resolve("LOG CALLBACK"); + // } + ); + + // defaultLogger.once("logged", () => {}); + + for (const transport of transports) { + try { + await closeTransportAndWaitForFinish(transport); + } catch (err) { + console.log(err); + } } + + resolve(); }); - if (numFlushes === 0) { - if (typeof codeOrCallback === 'function') { - codeOrCallback(); - } else { - process.exit(codeOrCallback); - } - } - }); + }; }; -/* eslint-enable no-underscore-dangle */ - diff --git a/packages/ace-meta/package.json b/packages/ace-meta/package.json index 8b3b0e92..6b5fc305 100644 --- a/packages/ace-meta/package.json +++ b/packages/ace-meta/package.json @@ -1,6 +1,11 @@ { "name": "@daisy/ace-meta", - "version": "1.1.1", + "version": "1.2.0-beta.15", + "engines": { + "node": ">=10.0.0", + "yarn": "^1.22.5", + "npm": ">=6.14.12" + }, "description": "Meta version information for Ace", "author": { "name": "DAISY developers", diff --git a/packages/ace-report-axe/package.json b/packages/ace-report-axe/package.json index c0877cc9..316b0f52 100644 --- a/packages/ace-report-axe/package.json +++ b/packages/ace-report-axe/package.json @@ -1,6 +1,11 @@ { "name": "@daisy/ace-report-axe", - "version": "1.1.1", + "version": "1.2.0-beta.15", + "engines": { + "node": ">=10.0.0", + "yarn": "^1.22.5", + "npm": ">=6.14.12" + }, "description": "Ace report adapter for aXe", "author": { "name": "DAISY developers", @@ -18,10 +23,10 @@ "license": "MIT", "main": "lib/index.js", "dependencies": { - "@daisy/ace-localize": "^1.1.0", - "@daisy/ace-report": "^1.1.1", - "fs-extra": "^6.0.1", - "winston": "^2.4.0" + "@daisy/ace-localize": "^1.2.0-beta.15", + "@daisy/ace-report": "^1.2.0-beta.15", + "fs-extra": "^9.1.0", + "winston": "^3.3.3" }, "publishConfig": { "access": "public" diff --git a/packages/ace-report-axe/src/axe-rules-kb-mapping.js b/packages/ace-report-axe/src/axe-rules-kb-mapping.js new file mode 100644 index 00000000..bc6311b8 --- /dev/null +++ b/packages/ace-report-axe/src/axe-rules-kb-mapping.js @@ -0,0 +1,2330 @@ +const { localize } = require('./l10n/localize').localizer; + +const kbMap = { + 'baseUrl': 'http://kb.daisy.org/publishing/', + 'map': { + // { + // "ruleId": "accesskeys", + // "description": "Ensures every accesskey attribute value is unique", + // "help": "accesskey attribute value must be unique", + // "helpUrl": "https://dequeuniversity.com/rules/axe/4.1/accesskeys?application=axeAPI", + // "tags": [ + // "cat.keyboard", + // "best-practice" + // ] + // }, + 'accesskeys': {url: 'docs/html/accesskeys.html', title: localize("kb.accesskeys")}, + + // { + // "ruleId": "area-alt", + // "description": "Ensures elements of image maps have alternate text", + // "help": "Active elements must have alternate text", + // "helpUrl": "https://dequeuniversity.com/rules/axe/4.1/area-alt?application=axeAPI", + // "tags": [ + // "cat.text-alternatives", + // "wcag2a", + // "wcag111", + // "wcag244", + // "wcag412", + // "section508", + // "section508.22.a", + // "ACT" + // ] + // }, + 'area-alt': {url: 'docs/html/maps.html', title: localize("kb.area-alt")}, + + // { + // "ruleId": "aria-allowed-attr", + // "description": "Ensures ARIA attributes are allowed for an element's role", + // "help": "Elements must only use allowed ARIA attributes", + // "helpUrl": "https://dequeuniversity.com/rules/axe/4.1/aria-allowed-attr?application=axeAPI", + // "tags": [ + // "cat.aria", + // "wcag2a", + // "wcag412" + // ] + // }, + 'aria-allowed-attr': {url: 'docs/script/aria.html', title: localize("kb.aria-allowed-attr")}, + + // { + // "ruleId": "aria-allowed-role", + // "description": "Ensures role attribute has an appropriate value for the element", + // "help": "ARIA role must be appropriate for the element", + // "helpUrl": "https://dequeuniversity.com/rules/axe/4.1/aria-allowed-role?application=axeAPI", + // "tags": [ + // "cat.aria", + // "best-practice" + // ] + // }, + 'aria-allowed-role': {url: 'docs/script/aria.html', title: localize("kb.aria-allowed-attr")}, + + // { + // "ruleId": "aria-command-name", + // "description": "Ensures every ARIA button, link and menuitem has an accessible name", + // "help": "ARIA commands must have an accessible name", + // "helpUrl": "https://dequeuniversity.com/rules/axe/4.1/aria-command-name?application=axeAPI", + // "tags": [ + // "cat.aria", + // "wcag2a", + // "wcag412" + // ] + // }, + 'aria-command-name': {url: 'docs/script/aria.html', title: localize("kb.button-name")}, + + // { + // "ruleId": "aria-dialog-name", + // "description": "Ensures every ARIA dialog and alertdialog node has an accessible name", + // "help": "ARIA dialog and alertdialog nodes must have an accessible name", + // "helpUrl": "https://dequeuniversity.com/rules/axe/4.1/aria-dialog-name?application=axeAPI", + // "tags": [ + // "cat.aria", + // "best-practice" + // ] + // }, + 'aria-dialog-name': {url: 'docs/script/aria.html', title: localize("kb.button-name")}, + + // { + // "ruleId": "aria-hidden-body", + // "description": "Ensures aria-hidden='true' is not present on the document body.", + // "help": "aria-hidden='true' must not be present on the document body", + // "helpUrl": "https://dequeuniversity.com/rules/axe/4.1/aria-hidden-body?application=axeAPI", + // "tags": [ + // "cat.aria", + // "wcag2a", + // "wcag412" + // ] + // }, + 'aria-hidden-body': {url: 'docs/script/aria.html', title: localize("kb.aria-hidden-body")}, + + // { + // "ruleId": "aria-hidden-focus", + // "description": "Ensures aria-hidden elements do not contain focusable elements", + // "help": "ARIA hidden element must not contain focusable elements", + // "helpUrl": "https://dequeuniversity.com/rules/axe/4.1/aria-hidden-focus?application=axeAPI", + // "tags": [ + // "cat.name-role-value", + // "wcag2a", + // "wcag412", + // "wcag131" + // ] + // }, + 'aria-hidden-focus': {url: 'docs/script/aria.html', title: localize("kb.aria-hidden-body")}, + + // { + // "ruleId": "aria-input-field-name", + // "description": "Ensures every ARIA input field has an accessible name", + // "help": "ARIA input fields must have an accessible name", + // "helpUrl": "https://dequeuniversity.com/rules/axe/4.1/aria-input-field-name?application=axeAPI", + // "tags": [ + // "cat.aria", + // "wcag2a", + // "wcag412", + // "ACT" + // ] + // }, + 'aria-input-field-name': {url: 'docs/script/aria.html', title: localize("kb.aria-hidden-body")}, + + // { + // "ruleId": "aria-meter-name", + // "description": "Ensures every ARIA meter node has an accessible name", + // "help": "ARIA meter nodes must have an accessible name", + // "helpUrl": "https://dequeuniversity.com/rules/axe/4.1/aria-meter-name?application=axeAPI", + // "tags": [ + // "cat.aria", + // "wcag2a", + // "wcag111" + // ] + // }, + 'aria-meter-name': {url: 'docs/script/aria.html', title: localize("kb.button-name")}, + + // { + // "ruleId": "aria-progressbar-name", + // "description": "Ensures every ARIA progressbar node has an accessible name", + // "help": "ARIA progressbar nodes must have an accessible name", + // "helpUrl": "https://dequeuniversity.com/rules/axe/4.1/aria-progressbar-name?application=axeAPI", + // "tags": [ + // "cat.aria", + // "wcag2a", + // "wcag111" + // ] + // }, + 'aria-progressbar-name': {url: 'docs/script/aria.html', title: localize("kb.button-name")}, + + // { + // "ruleId": "aria-required-attr", + // "description": "Ensures elements with ARIA roles have all required ARIA attributes", + // "help": "Required ARIA attributes must be provided", + // "helpUrl": "https://dequeuniversity.com/rules/axe/4.1/aria-required-attr?application=axeAPI", + // "tags": [ + // "cat.aria", + // "wcag2a", + // "wcag412" + // ] + // }, + 'aria-required-attr': {url: 'docs/script/aria.html', title: localize("kb.aria-required-attr")}, + + // { + // "ruleId": "aria-required-children", + // "description": "Ensures elements with an ARIA role that require child roles contain them", + // "help": "Certain ARIA roles must contain particular children", + // "helpUrl": "https://dequeuniversity.com/rules/axe/4.1/aria-required-children?application=axeAPI", + // "tags": [ + // "cat.aria", + // "wcag2a", + // "wcag131" + // ] + // }, + 'aria-required-children': {url: 'docs/script/aria.html', title: localize("kb.aria-required-children")}, + + // { + // "ruleId": "aria-required-parent", + // "description": "Ensures elements with an ARIA role that require parent roles are contained by them", + // "help": "Certain ARIA roles must be contained by particular parents", + // "helpUrl": "https://dequeuniversity.com/rules/axe/4.1/aria-required-parent?application=axeAPI", + // "tags": [ + // "cat.aria", + // "wcag2a", + // "wcag131" + // ] + // }, + 'aria-required-parent': {url: 'docs/script/aria.html', title: localize("kb.aria-required-parent")}, + + // { + // "ruleId": "aria-roledescription", + // "description": "Ensure aria-roledescription is only used on elements with an implicit or explicit role", + // "help": "Use aria-roledescription on elements with a semantic role", + // "helpUrl": "https://dequeuniversity.com/rules/axe/4.1/aria-roledescription?application=axeAPI", + // "tags": [ + // "cat.aria", + // "wcag2a", + // "wcag412" + // ] + // }, + 'aria-roledescription': {url: 'docs/script/aria.html', title: localize("kb.aria-required-parent")}, + + // { + // "ruleId": "aria-roles", + // "description": "Ensures all elements with a role attribute use a valid value", + // "help": "ARIA roles used must conform to valid values", + // "helpUrl": "https://dequeuniversity.com/rules/axe/4.1/aria-roles?application=axeAPI", + // "tags": [ + // "cat.aria", + // "wcag2a", + // "wcag412" + // ] + // }, + 'aria-roles': {url: 'docs/script/aria.html', title: localize("kb.aria-roles")}, + + // { + // "ruleId": "aria-toggle-field-name", + // "description": "Ensures every ARIA toggle field has an accessible name", + // "help": "ARIA toggle fields have an accessible name", + // "helpUrl": "https://dequeuniversity.com/rules/axe/4.1/aria-toggle-field-name?application=axeAPI", + // "tags": [ + // "cat.aria", + // "wcag2a", + // "wcag412", + // "ACT" + // ] + // }, + 'aria-toggle-field-name': {url: 'docs/script/aria.html', title: localize("kb.aria-roles")}, + + // { + // "ruleId": "aria-tooltip-name", + // "description": "Ensures every ARIA tooltip node has an accessible name", + // "help": "ARIA tooltip nodes must have an accessible name", + // "helpUrl": "https://dequeuniversity.com/rules/axe/4.1/aria-tooltip-name?application=axeAPI", + // "tags": [ + // "cat.aria", + // "wcag2a", + // "wcag412" + // ] + // }, + 'aria-tooltip-name': {url: 'docs/script/aria.html', title: localize("kb.button-name")}, + + // { + // "ruleId": "aria-treeitem-name", + // "description": "Ensures every ARIA treeitem node has an accessible name", + // "help": "ARIA treeitem nodes must have an accessible name", + // "helpUrl": "https://dequeuniversity.com/rules/axe/4.1/aria-treeitem-name?application=axeAPI", + // "tags": [ + // "cat.aria", + // "best-practice" + // ] + // }, + 'aria-treeitem-name': {url: 'docs/script/aria.html', title: localize("kb.button-name")}, + + // { + // "ruleId": "aria-valid-attr-value", + // "description": "Ensures all ARIA attributes have valid values", + // "help": "ARIA attributes must conform to valid values", + // "helpUrl": "https://dequeuniversity.com/rules/axe/4.1/aria-valid-attr-value?application=axeAPI", + // "tags": [ + // "cat.aria", + // "wcag2a", + // "wcag412" + // ] + // }, + 'aria-valid-attr-value': {url: 'docs/script/aria.html', title: localize("kb.aria-valid-attr-value")}, + + // { + // "ruleId": "aria-valid-attr", + // "description": "Ensures attributes that begin with aria- are valid ARIA attributes", + // "help": "ARIA attributes must conform to valid names", + // "helpUrl": "https://dequeuniversity.com/rules/axe/4.1/aria-valid-attr?application=axeAPI", + // "tags": [ + // "cat.aria", + // "wcag2a", + // "wcag412" + // ] + // }, + 'aria-valid-attr': {url: 'docs/script/aria.html', title: localize("kb.aria-valid-attr")}, + + // { + // "ruleId": "audio-caption", + // "description": "Ensures