From e97468408941b8a33e5c4cadd2e20d128bfdda32 Mon Sep 17 00:00:00 2001 From: Daniel Weck Date: Thu, 8 Apr 2021 07:20:55 +0100 Subject: [PATCH] chore(major): Ace 1.2 supersedes v1.1.1 (PR #314) Summary: * Based on DAISY's own fork of Deque's Axe (latest v4 version), instead of patching Axe's distributed Javascript bundles (see https://github.com/daisy/axe-core/pull/4 ) * Better localization tooling for Axe, integration of Axe translations * Two Axe "runners": Puppeteer (default for Ace CLI), and Electron (can be used in CLI, and used by Ace App), Chromium web browser engine is used in both cases, but Electron runner uses a HTTP server to simulate a typical reading system environment. Unit tests plumbing for both platforms (Continuous Integration server only executes the Puppeteer ones, for faster builds). Runtime performance is near-identical (there is a developer script to check this). * NodeJS >=10 requirement * NPM package dependencies updated to latest versions (except where NodeJS requirement is higher). Added developer scripts to facilitate version checking and incremental updates at regular intervals (i.e. `package.json` exact references and `yarn.lock` maintenance) * All Ace sub-packages (`@daisy/` organisation scope on NPM) have exact same semantic version (easier deployment with developer script that automates NPM publish) * Fixes bugs that couldn't easily be fixed in Ace 1.1.1 (the last of v1), due to older Axe (v3 instead of v4) and due to older NPM package dependencies that were necessary for compatibility with legacy NodeJS requirement (now deprecated / deemed unsecure). --- .babelrc | 2 +- .gitignore | 2 + .snyk | 15 - .travis.yml | 10 +- jest.config-cli.js | 7 + jest.config-common.js | 18 + jest.config-electron.js | 8 + jest.config-puppeteer.js | 7 + lerna.json | 4 +- package.json | 77 +- packages/ace-axe-runner-electron/bin/ace.js | 33 + packages/ace-axe-runner-electron/package.json | 40 + packages/ace-axe-runner-electron/src/cli.js | 83 + packages/ace-axe-runner-electron/src/index.js | 119 + packages/ace-axe-runner-electron/src/init.js | 793 ++ .../ace-axe-runner-electron/src/selfsigned.js | 33 + .../ace-axe-runner-puppeteer/package.json | 11 +- .../ace-axe-runner-puppeteer/src/index.js | 26 + packages/ace-cli-shared/package.json | 36 + .../src/defaults.json | 0 packages/ace-cli-shared/src/index.js | 164 + packages/ace-cli/bin/ace.js | 4 +- packages/ace-cli/package.json | 16 +- packages/ace-cli/src/index.js | 124 +- packages/ace-config/package.json | 13 +- .../ace-config/src/__tests__/index.test.js | 8 +- packages/ace-core-legacy/package.json | 13 +- packages/ace-core/package.json | 29 +- .../ace-core/src/checker/checker-chromium.js | 133 +- packages/ace-core/src/checker/checker-epub.js | 204 +- packages/ace-core/src/core/a11y-metadata.js | 148 + packages/ace-core/src/core/ace.js | 145 +- packages/ace-core/src/l10n/locales/da.json | 15 - packages/ace-core/src/l10n/locales/en.json | 15 - packages/ace-core/src/l10n/locales/es.json | 15 - packages/ace-core/src/l10n/locales/fr.json | 15 - packages/ace-core/src/l10n/locales/pt_BR.json | 15 - packages/ace-core/src/l10n/localize.js | 3 +- packages/ace-core/src/scripts/ace-axe.js | 249 +- .../src/scripts/ace-extraction.test.js | 29 +- .../src/scripts/axe-patch-aria-roles.js | 24 +- .../scripts/axe-patch-is-aria-role-allowed.js | 145 +- .../src/scripts/axe-patch-listitem.js | 171 +- .../src/scripts/axe-patch-only-list-items.js | 340 +- .../src/scripts/function-bind-bound-object.js | 30 + packages/ace-http/package.json | 29 +- packages/ace-http/src/index.js | 72 +- packages/ace-localize/package.json | 11 +- packages/ace-localize/src/localize.js | 32 +- packages/ace-logger/package.json | 14 +- packages/ace-logger/src/defaults.json | 3 +- packages/ace-logger/src/index.js | 183 +- packages/ace-meta/package.json | 7 +- packages/ace-report-axe/package.json | 15 +- .../src/axe-rules-kb-mapping.js | 2330 ++++ packages/ace-report-axe/src/index.js | 258 +- .../ace-report-axe/src/l10n/locales/da.json | 2 + .../ace-report-axe/src/l10n/locales/en.json | 2 + .../ace-report-axe/src/l10n/locales/es.json | 2 + .../ace-report-axe/src/l10n/locales/fr.json | 2 + .../src/l10n/locales/pt_BR.json | 2 + packages/ace-report-axe/src/l10n/localize.js | 3 +- packages/ace-report/package.json | 19 +- .../ace-report/src/analyze-a11y-metadata.js | 21 +- .../ace-report/src/generate-html-report.js | 108 +- packages/ace-report/src/l10n/locales/da.json | 6 +- packages/ace-report/src/l10n/locales/fr.json | 2 +- packages/ace-report/src/l10n/localize.js | 3 +- .../ace-report/src/report-template.handlebars | 12 +- packages/ace-report/src/report.js | 28 +- packages/ace/bin/ace-electron.js | 3 + packages/ace/package.json | 17 +- packages/epub-utils/package.json | 21 +- packages/epub-utils/src/epub-parse.js | 87 +- packages/epub-utils/src/epub.js | 113 +- packages/jest-env-puppeteer/package.json | 11 +- packages/jest-puppeteer/package.json | 11 +- packages/jest-puppeteer/src/index.js | 23 +- packages/puppeteer-utils/package.json | 7 +- scripts/axe-rules.html | 8 + scripts/compareAxeRunners.sh | 15 + scripts/compareAxeRunners_VERBOSE.sh | 15 + scripts/ncu.sh | 11 + scripts/npm-versions-check.js | 129 + scripts/publish.sh | 12 + scripts/replace-in-file.js | 25 + .../__tests__/__snapshots__/cli.test.js.snap | 3 + tests/__tests__/axe-rules.test.js | 76 + tests/__tests__/cli.test.js | 24 +- tests/__tests__/epub-rules.test.js | 69 +- tests/__tests__/regression.test.js | 11 + tests/__tests__/unzip.test.js | 64 +- tests/data/axerule-bypass/EPUB/nav.xhtml | 2 +- .../axerule-dpubroles/EPUB/content_001.xhtml | 18 +- tests/data/axerule-dpubroles/EPUB/nav.xhtml | 2 +- .../EPUB/content_001.xhtml | 80 + .../EPUB/image_001.jpg | 1 + .../axerule-landmark-unique/EPUB/nav.xhtml | 12 + .../axerule-landmark-unique/EPUB/package.opf | 24 + .../META-INF/container.xml | 6 + tests/data/axerule-landmark-unique/mimetype | 1 + .../EPUB/nav.xhtml | 2 +- .../EPUB/content_001.xhtml | 5 +- .../axerule-pagebreak-label/EPUB/nav.xhtml | 4 +- tests/data/base-epub-30/EPUB/nav.xhtml | 2 +- .../EPUB/content_001.xhtml | 9 + .../EPUB/nav.xhtml | 12 + .../EPUB/package.opf | 23 + .../META-INF/container.xml | 6 + .../mimetype | 1 + .../EPUB/content_001.xhtml | 9 + .../EPUB/nav.xhtml | 12 + .../EPUB/package.opf | 23 + .../META-INF/container.xml | 6 + .../mimetype | 1 + .../EPUB/content_001.xhtml | 9 + .../EPUB/nav.xhtml | 12 + .../EPUB/package.opf | 22 + .../META-INF/container.xml | 6 + .../mimetype | 1 + .../EPUB/content_001.xhtml | 9 + .../EPUB/nav.xhtml | 12 + .../EPUB/package.opf | 25 + .../META-INF/container.xml | 6 + .../mimetype | 1 + .../EPUB/content_001.xhtml | 9 + .../EPUB/nav.xhtml | 12 + .../EPUB/package.opf | 25 + .../META-INF/container.xml | 6 + .../mimetype | 1 + .../EPUB/nav.xhtml | 2 +- tests/data/epubrules-metadata/EPUB/nav.xhtml | 2 +- .../data/epubrules-metadata/EPUB/package.opf | 2 + tests/data/epubrules-pagelist/EPUB/nav.xhtml | 4 +- tests/data/feat-audio/EPUB/nav.xhtml | 2 +- tests/data/feat-bindings/EPUB/nav.xhtml | 2 +- tests/data/feat-epub-switch/EPUB/nav.xhtml | 2 +- tests/data/feat-epub-trigger/EPUB/nav.xhtml | 2 +- tests/data/feat-forms/EPUB/nav.xhtml | 2 +- tests/data/feat-image/EPUB/nav.xhtml | 2 +- tests/data/feat-links/EPUB/nav.xhtml | 2 +- .../feat-manifest-fallbacks/EPUB/nav.xhtml | 2 +- tests/data/feat-mathml/EPUB/nav.xhtml | 2 +- tests/data/feat-pagebreaks/EPUB/nav.xhtml | 2 +- tests/data/feat-svg/EPUB/nav.xhtml | 2 +- tests/data/feat-video-sources/EPUB/nav.xhtml | 2 +- tests/data/feat-video/EPUB/nav.xhtml | 2 +- tests/data/fs-no-leaks/EPUB/nav.xhtml | 2 +- tests/data/fs-resource-missing/EPUB/nav.xhtml | 2 +- tests/data/issue-108/EPUB/nav.xhtml | 2 +- tests/data/issue-114/EPUB/nav.xhtml | 2 +- tests/data/issue-122/EPUB/nav.xhtml | 2 +- tests/data/issue-170/EPUB/nav.xhtml | 2 +- tests/data/issue-182/EPUB/nav.xhtml | 2 +- tests/data/issue-209/EPUB/nav.xhtml | 2 +- tests/data/issue-239/EPUB/nav.xhtml | 2 +- tests/data/issue-290.epub | Bin 0 -> 12041 bytes .../c%on t&e%26n%2Ft_\303\250001_.xhtml" | 11 + .../E%PU B/i%ma g&e%26_%2F00\303\2501_.jpg" | Bin 0 -> 10969 bytes .../n%av i&g%26a%2Ftio\303\250n_.xhtml" | 17 + .../E%PU B/pa&c%26kag%2Fe\303\250_.opf" | 33 + tests/data/issue-290/META-INF/container.xml | 8 + tests/data/issue-290/mimetype | 1 + tests/data/issue-49/EPUB/nav.xhtml | 2 +- tests/data/issue-53/EPUB/nav.xhtml | 2 +- tests/data/issue-57/EPUB/nav.xhtml | 2 +- tests/data/issue-85/EPUB/content_001.xhtml | 3 +- tests/data/issue-85/EPUB/nav.xhtml | 10 +- tests/data/issue-85/EPUB/package.opf | 1 + tests/data/pack-epub.sh | 14 + tests/runAceCLI.js | 38 +- tests/runAceJS.js | 31 +- tests/utils.js | 2 +- website/config.toml | 2 +- website/content/_index.md | 2 +- website/content/getting-started/ace-app.md | 10 +- .../content/getting-started/installation.md | 4 +- website/content/rules/epub.md | 2 +- website/content/rules/html.md | 155 +- yarn.lock | 10682 ++++++++++------ 180 files changed, 13929 insertions(+), 4886 deletions(-) delete mode 100644 .snyk create mode 100644 jest.config-cli.js create mode 100644 jest.config-common.js create mode 100644 jest.config-electron.js create mode 100644 jest.config-puppeteer.js create mode 100644 packages/ace-axe-runner-electron/bin/ace.js create mode 100644 packages/ace-axe-runner-electron/package.json create mode 100644 packages/ace-axe-runner-electron/src/cli.js create mode 100644 packages/ace-axe-runner-electron/src/index.js create mode 100644 packages/ace-axe-runner-electron/src/init.js create mode 100644 packages/ace-axe-runner-electron/src/selfsigned.js create mode 100644 packages/ace-cli-shared/package.json rename packages/{ace-cli => ace-cli-shared}/src/defaults.json (100%) create mode 100755 packages/ace-cli-shared/src/index.js create mode 100644 packages/ace-core/src/core/a11y-metadata.js create mode 100644 packages/ace-core/src/scripts/function-bind-bound-object.js create mode 100644 packages/ace-report-axe/src/axe-rules-kb-mapping.js create mode 100755 packages/ace/bin/ace-electron.js create mode 100644 scripts/axe-rules.html create mode 100755 scripts/compareAxeRunners.sh create mode 100755 scripts/compareAxeRunners_VERBOSE.sh create mode 100755 scripts/ncu.sh create mode 100644 scripts/npm-versions-check.js create mode 100755 scripts/publish.sh create mode 100644 scripts/replace-in-file.js create mode 100644 tests/data/axerule-landmark-unique/EPUB/content_001.xhtml create mode 100644 tests/data/axerule-landmark-unique/EPUB/image_001.jpg create mode 100644 tests/data/axerule-landmark-unique/EPUB/nav.xhtml create mode 100644 tests/data/axerule-landmark-unique/EPUB/package.opf create mode 100644 tests/data/axerule-landmark-unique/META-INF/container.xml create mode 100644 tests/data/axerule-landmark-unique/mimetype create mode 100644 tests/data/epubrules-metadata-accessModeSufficient-invalid-item/EPUB/content_001.xhtml create mode 100644 tests/data/epubrules-metadata-accessModeSufficient-invalid-item/EPUB/nav.xhtml create mode 100644 tests/data/epubrules-metadata-accessModeSufficient-invalid-item/EPUB/package.opf create mode 100644 tests/data/epubrules-metadata-accessModeSufficient-invalid-item/META-INF/container.xml create mode 100644 tests/data/epubrules-metadata-accessModeSufficient-invalid-item/mimetype create mode 100644 tests/data/epubrules-metadata-accessModeSufficient-invalid-separator/EPUB/content_001.xhtml create mode 100644 tests/data/epubrules-metadata-accessModeSufficient-invalid-separator/EPUB/nav.xhtml create mode 100644 tests/data/epubrules-metadata-accessModeSufficient-invalid-separator/EPUB/package.opf create mode 100644 tests/data/epubrules-metadata-accessModeSufficient-invalid-separator/META-INF/container.xml create mode 100644 tests/data/epubrules-metadata-accessModeSufficient-invalid-separator/mimetype create mode 100644 tests/data/epubrules-metadata-accessModeSufficient-missing/EPUB/content_001.xhtml create mode 100644 tests/data/epubrules-metadata-accessModeSufficient-missing/EPUB/nav.xhtml create mode 100644 tests/data/epubrules-metadata-accessModeSufficient-missing/EPUB/package.opf create mode 100644 tests/data/epubrules-metadata-accessModeSufficient-missing/META-INF/container.xml create mode 100644 tests/data/epubrules-metadata-accessModeSufficient-missing/mimetype create mode 100644 tests/data/epubrules-metadata-accessModeSufficient-valid/EPUB/content_001.xhtml create mode 100644 tests/data/epubrules-metadata-accessModeSufficient-valid/EPUB/nav.xhtml create mode 100644 tests/data/epubrules-metadata-accessModeSufficient-valid/EPUB/package.opf create mode 100644 tests/data/epubrules-metadata-accessModeSufficient-valid/META-INF/container.xml create mode 100644 tests/data/epubrules-metadata-accessModeSufficient-valid/mimetype create mode 100644 tests/data/epubrules-metadata-accessibilityFeature-case-sensitive/EPUB/content_001.xhtml create mode 100644 tests/data/epubrules-metadata-accessibilityFeature-case-sensitive/EPUB/nav.xhtml create mode 100644 tests/data/epubrules-metadata-accessibilityFeature-case-sensitive/EPUB/package.opf create mode 100644 tests/data/epubrules-metadata-accessibilityFeature-case-sensitive/META-INF/container.xml create mode 100644 tests/data/epubrules-metadata-accessibilityFeature-case-sensitive/mimetype create mode 100644 tests/data/issue-290.epub create mode 100644 "tests/data/issue-290/E%PU B/c%on t&e%26n%2Ft_\303\250001_.xhtml" create mode 100644 "tests/data/issue-290/E%PU B/i%ma g&e%26_%2F00\303\2501_.jpg" create mode 100644 "tests/data/issue-290/E%PU B/n%av i&g%26a%2Ftio\303\250n_.xhtml" create mode 100644 "tests/data/issue-290/E%PU B/pa&c%26kag%2Fe\303\250_.opf" create mode 100644 tests/data/issue-290/META-INF/container.xml create mode 100644 tests/data/issue-290/mimetype create mode 100755 tests/data/pack-epub.sh 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