From 0600603d4f20c6cf4bfe2388696141498d422e2e Mon Sep 17 00:00:00 2001 From: "Kenneth G. Franqueiro" Date: Wed, 6 May 2026 07:44:04 -0400 Subject: [PATCH 01/11] Migrate files under lib to TypeScript (#2090) Co-authored-by: Denis Ah-Kang <1696128+deniak@users.noreply.github.com> --- .c8rc | 1 + .cspell.json | 7 +- .gitignore | 7 +- .husky/pre-commit | 2 +- .vscode/settings.json | 3 + README.md | 24 +- app.js => app.ts | 21 +- eslint.config.cjs | 55 - lib/api.js | 217 - lib/api.ts | 172 + lib/exceptions.js | 62 - lib/exceptions.ts | 51 + lib/groups-db.json | 162 - lib/{l10n-en_GB.js => l10n-en_GB.ts} | 2 +- lib/l10n.js | 172 - lib/l10n.ts | 178 + lib/profiles/{SUBM.js => SUBM.ts} | 0 .../SUBM/{MEM-SUBM.js => MEM-SUBM.ts} | 3 +- lib/profiles/{TR.js => TR.ts} | 0 .../{DNOTE-Echidna.js => DNOTE-Echidna.ts} | 4 +- lib/profiles/TR/Note/{DNOTE.js => DNOTE.ts} | 3 +- .../Note/{NOTE-Echidna.js => NOTE-Echidna.ts} | 4 +- lib/profiles/TR/Note/{NOTE.js => NOTE.ts} | 3 +- lib/profiles/TR/Note/{STMT.js => STMT.ts} | 3 +- .../TR/Note/{note-base.js => note-base.ts} | 2 +- .../{CR-Echidna.js => CR-Echidna.ts} | 0 .../TR/Recommendation/{CR.js => CR.ts} | 3 +- .../{CRD-Echidna.js => CRD-Echidna.ts} | 0 .../TR/Recommendation/{CRD.js => CRD.ts} | 3 +- .../TR/Recommendation/{DISC.js => DISC.ts} | 3 +- .../TR/Recommendation/{FPWD.js => FPWD.ts} | 3 +- .../{REC-Echidna.js => REC-Echidna.ts} | 0 .../{REC-RSCND.js => REC-RSCND.ts} | 3 +- .../TR/Recommendation/{REC.js => REC.ts} | 3 +- .../{WD-Echidna.js => WD-Echidna.ts} | 0 .../TR/Recommendation/{WD.js => WD.ts} | 3 +- ...ndation-base.js => recommendation-base.ts} | 2 +- lib/profiles/TR/Registry/{CRY.js => CRY.ts} | 3 +- lib/profiles/TR/Registry/{CRYD.js => CRYD.ts} | 3 +- .../{DRY-Echidna.js => DRY-Echidna.ts} | 4 +- lib/profiles/TR/Registry/{DRY.js => DRY.ts} | 3 +- lib/profiles/TR/Registry/{RY.js => RY.ts} | 3 +- .../{registry-base.js => registry-base.ts} | 2 +- ...ionalMetadata.js => additionalMetadata.ts} | 0 lib/profiles/{base.js => base.ts} | 0 lib/profiles/{metadata.js => metadata.ts} | 0 lib/profiles/profileUtil.js | 45 - lib/profiles/profileUtil.ts | 52 + lib/rules/echidna/deliverer-change.js | 59 - lib/rules/echidna/deliverer-change.ts | 60 + .../{todays-date.js => todays-date.ts} | 19 +- .../headers/{copyright.js => copyright.ts} | 127 +- ...{details-summary.js => details-summary.ts} | 12 +- .../headers/{div-head.js => div-head.ts} | 12 +- lib/rules/headers/{dl.js => dl.ts} | 74 +- ...rticipation.js => editor-participation.ts} | 33 +- lib/rules/headers/{errata.js => errata.ts} | 21 +- .../{github-repo.js => github-repo.ts} | 13 +- .../headers/{h1-title.js => h1-title.ts} | 14 +- lib/rules/headers/{h2-toc.js => h2-toc.ts} | 12 +- lib/rules/headers/{hr.js => hr.ts} | 12 +- lib/rules/headers/{logo.js => logo.ts} | 18 +- ...emsub-copyright.js => memsub-copyright.ts} | 10 +- lib/rules/headers/{ol-toc.js => ol-toc.ts} | 12 +- lib/rules/headers/{secno.js => secno.ts} | 10 +- .../headers/{shortname.js => shortname.ts} | 48 +- .../headers/{subm-logo.js => subm-logo.ts} | 18 +- .../{translation.js => translation.ts} | 12 +- .../headers/{w3c-state.js => w3c-state.ts} | 40 +- .../{date-format.js => date-format.ts} | 22 +- lib/rules/links/{compound.js => compound.ts} | 29 +- lib/rules/links/{internal.js => internal.ts} | 12 +- .../links/{linkchecker.js => linkchecker.ts} | 40 +- .../links/{reliability.js => reliability.ts} | 17 +- lib/rules/metadata/abstract.js | 44 - lib/rules/metadata/abstract.ts | 32 + lib/rules/metadata/charters.js | 17 - lib/rules/metadata/charters.ts | 13 + lib/rules/metadata/deliverers.js | 16 - lib/rules/metadata/deliverers.ts | 16 + lib/rules/metadata/{dl.js => dl.ts} | 45 +- lib/rules/metadata/docDate.js | 23 - lib/rules/metadata/docDate.ts | 20 + .../metadata/{editor-ids.js => editor-ids.ts} | 21 +- .../{editor-names.js => editor-names.ts} | 20 +- lib/rules/metadata/{errata.js => errata.ts} | 14 +- .../{informative.js => informative.ts} | 26 +- lib/rules/metadata/{process.js => process.ts} | 19 +- lib/rules/metadata/{profile.js => profile.ts} | 78 +- lib/rules/metadata/sotd.js | 16 - lib/rules/metadata/sotd.ts | 16 + lib/rules/metadata/{title.js => title.ts} | 15 +- ...-review-end.js => candidate-review-end.ts} | 14 +- lib/rules/sotd/{charter.js => charter.ts} | 15 +- .../{deliverer-note.js => deliverer-note.ts} | 12 +- .../sotd/{deployment.js => deployment.ts} | 12 +- lib/rules/sotd/{diff.js => diff.ts} | 12 +- ...{draft-stability.js => draft-stability.ts} | 14 +- .../sotd/{new-features.js => new-features.ts} | 14 +- .../sotd/{obsl-rescind.js => obsl-rescind.ts} | 37 +- lib/rules/sotd/{pp.js => pp.ts} | 68 +- ...rocess-document.js => process-document.ts} | 14 +- lib/rules/sotd/{publish.js => publish.ts} | 44 +- .../sotd/{rec-addition.js => rec-addition.ts} | 30 +- ...{rec-comment-end.js => rec-comment-end.ts} | 30 +- lib/rules/sotd/{stability.js => stability.ts} | 63 +- .../sotd/{submission.js => submission.ts} | 64 +- .../sotd/{supersedable.js => supersedable.ts} | 14 +- lib/rules/sotd/{usage.js => usage.ts} | 12 +- .../structure/{canonical.js => canonical.ts} | 12 +- .../{display-only.js => display-only.ts} | 12 +- lib/rules/structure/{h2.js => h2.ts} | 12 +- lib/rules/structure/{name.js => name.ts} | 16 +- .../structure/{neutral.js => neutral.ts} | 28 +- .../{section-ids.js => section-ids.ts} | 12 +- ...ecurity-privacy.js => security-privacy.ts} | 12 +- .../style/{back-to-top.js => back-to-top.ts} | 14 +- ...ody-toc-sidebar.js => body-toc-sidebar.ts} | 10 +- lib/rules/style/{meta.js => meta.ts} | 14 +- lib/rules/style/{script.js => script.ts} | 18 +- lib/rules/style/{sheet.js => sheet.ts} | 15 +- lib/rules/validation/{html.js => html.ts} | 21 +- lib/rules/validation/{wcag.js => wcag.ts} | 12 +- lib/sink.js | 51 - lib/{throttled-ua.js => throttled-ua.ts} | 14 +- lib/types.d.ts | 118 + lib/util.js | 267 - lib/util.ts | 266 + lib/validator.js | 925 ---- lib/validator.ts | 959 ++++ lib/{views.js => views.ts} | 173 +- package-lock.json | 4890 +++++++---------- package.json | 33 +- test/api.js | 21 +- test/l10n.js | 29 +- test/rules.js | 53 +- tools/fetch-groups-db.js | 79 - tools/groups-sparql.json | 550 -- tools/make-groups-db.js | 40 - tsconfig.json | 16 +- 140 files changed, 4710 insertions(+), 6954 deletions(-) create mode 100644 .vscode/settings.json rename app.js => app.ts (93%) delete mode 100644 eslint.config.cjs delete mode 100644 lib/api.js create mode 100644 lib/api.ts delete mode 100644 lib/exceptions.js create mode 100644 lib/exceptions.ts delete mode 100644 lib/groups-db.json rename lib/{l10n-en_GB.js => l10n-en_GB.ts} (99%) delete mode 100644 lib/l10n.js create mode 100644 lib/l10n.ts rename lib/profiles/{SUBM.js => SUBM.ts} (100%) rename lib/profiles/SUBM/{MEM-SUBM.js => MEM-SUBM.ts} (88%) rename lib/profiles/{TR.js => TR.ts} (100%) rename lib/profiles/TR/Note/{DNOTE-Echidna.js => DNOTE-Echidna.ts} (78%) rename lib/profiles/TR/Note/{DNOTE.js => DNOTE.ts} (80%) rename lib/profiles/TR/Note/{NOTE-Echidna.js => NOTE-Echidna.ts} (77%) rename lib/profiles/TR/Note/{NOTE.js => NOTE.ts} (70%) rename lib/profiles/TR/Note/{STMT.js => STMT.ts} (82%) rename lib/profiles/TR/Note/{note-base.js => note-base.ts} (96%) rename lib/profiles/TR/Recommendation/{CR-Echidna.js => CR-Echidna.ts} (100%) rename lib/profiles/TR/Recommendation/{CR.js => CR.ts} (85%) rename lib/profiles/TR/Recommendation/{CRD-Echidna.js => CRD-Echidna.ts} (100%) rename lib/profiles/TR/Recommendation/{CRD.js => CRD.ts} (84%) rename lib/profiles/TR/Recommendation/{DISC.js => DISC.ts} (81%) rename lib/profiles/TR/Recommendation/{FPWD.js => FPWD.ts} (84%) rename lib/profiles/TR/Recommendation/{REC-Echidna.js => REC-Echidna.ts} (100%) rename lib/profiles/TR/Recommendation/{REC-RSCND.js => REC-RSCND.ts} (88%) rename lib/profiles/TR/Recommendation/{REC.js => REC.ts} (91%) rename lib/profiles/TR/Recommendation/{WD-Echidna.js => WD-Echidna.ts} (100%) rename lib/profiles/TR/Recommendation/{WD.js => WD.ts} (81%) rename lib/profiles/TR/Recommendation/{recommendation-base.js => recommendation-base.ts} (97%) rename lib/profiles/TR/Registry/{CRY.js => CRY.ts} (83%) rename lib/profiles/TR/Registry/{CRYD.js => CRYD.ts} (82%) rename lib/profiles/TR/Registry/{DRY-Echidna.js => DRY-Echidna.ts} (78%) rename lib/profiles/TR/Registry/{DRY.js => DRY.ts} (81%) rename lib/profiles/TR/Registry/{RY.js => RY.ts} (79%) rename lib/profiles/TR/Registry/{registry-base.js => registry-base.ts} (87%) rename lib/profiles/{additionalMetadata.js => additionalMetadata.ts} (100%) rename lib/profiles/{base.js => base.ts} (100%) rename lib/profiles/{metadata.js => metadata.ts} (100%) delete mode 100644 lib/profiles/profileUtil.js create mode 100644 lib/profiles/profileUtil.ts delete mode 100644 lib/rules/echidna/deliverer-change.js create mode 100644 lib/rules/echidna/deliverer-change.ts rename lib/rules/echidna/{todays-date.js => todays-date.ts} (66%) rename lib/rules/headers/{copyright.js => copyright.ts} (77%) rename lib/rules/headers/{details-summary.js => details-summary.ts} (85%) rename lib/rules/headers/{div-head.js => div-head.ts} (55%) rename lib/rules/headers/{dl.js => dl.ts} (85%) rename lib/rules/headers/{editor-participation.js => editor-participation.ts} (73%) rename lib/rules/headers/{errata.js => errata.ts} (64%) rename lib/rules/headers/{github-repo.js => github-repo.ts} (85%) rename lib/rules/headers/{h1-title.js => h1-title.ts} (70%) rename lib/rules/headers/{h2-toc.js => h2-toc.ts} (87%) rename lib/rules/headers/{hr.js => hr.ts} (74%) rename lib/rules/headers/{logo.js => logo.ts} (56%) rename lib/rules/headers/{memsub-copyright.js => memsub-copyright.ts} (82%) rename lib/rules/headers/{ol-toc.js => ol-toc.ts} (73%) rename lib/rules/headers/{secno.js => secno.ts} (81%) rename lib/rules/headers/{shortname.js => shortname.ts} (87%) rename lib/rules/headers/{subm-logo.js => subm-logo.ts} (67%) rename lib/rules/headers/{translation.js => translation.ts} (82%) rename lib/rules/headers/{w3c-state.js => w3c-state.ts} (64%) rename lib/rules/heuristic/{date-format.js => date-format.ts} (77%) rename lib/rules/links/{compound.js => compound.ts} (83%) rename lib/rules/links/{internal.js => internal.ts} (74%) rename lib/rules/links/{linkchecker.js => linkchecker.ts} (83%) rename lib/rules/links/{reliability.js => reliability.ts} (84%) delete mode 100644 lib/rules/metadata/abstract.js create mode 100644 lib/rules/metadata/abstract.ts delete mode 100644 lib/rules/metadata/charters.js create mode 100644 lib/rules/metadata/charters.ts delete mode 100644 lib/rules/metadata/deliverers.js create mode 100644 lib/rules/metadata/deliverers.ts rename lib/rules/metadata/{dl.js => dl.ts} (72%) delete mode 100644 lib/rules/metadata/docDate.js create mode 100644 lib/rules/metadata/docDate.ts rename lib/rules/metadata/{editor-ids.js => editor-ids.ts} (81%) rename lib/rules/metadata/{editor-names.js => editor-names.ts} (58%) rename lib/rules/metadata/{errata.js => errata.ts} (70%) rename lib/rules/metadata/{informative.js => informative.ts} (60%) rename lib/rules/metadata/{process.js => process.ts} (65%) rename lib/rules/metadata/{profile.js => profile.ts} (73%) delete mode 100644 lib/rules/metadata/sotd.js create mode 100644 lib/rules/metadata/sotd.ts rename lib/rules/metadata/{title.js => title.ts} (55%) rename lib/rules/sotd/{candidate-review-end.js => candidate-review-end.ts} (81%) rename lib/rules/sotd/{charter.js => charter.ts} (89%) rename lib/rules/sotd/{deliverer-note.js => deliverer-note.ts} (61%) rename lib/rules/sotd/{deployment.js => deployment.ts} (82%) rename lib/rules/sotd/{diff.js => diff.ts} (51%) rename lib/rules/sotd/{draft-stability.js => draft-stability.ts} (86%) rename lib/rules/sotd/{new-features.js => new-features.ts} (78%) rename lib/rules/sotd/{obsl-rescind.js => obsl-rescind.ts} (75%) rename lib/rules/sotd/{pp.js => pp.ts} (77%) rename lib/rules/sotd/{process-document.js => process-document.ts} (92%) rename lib/rules/sotd/{publish.js => publish.ts} (84%) rename lib/rules/sotd/{rec-addition.js => rec-addition.ts} (84%) rename lib/rules/sotd/{rec-comment-end.js => rec-comment-end.ts} (65%) rename lib/rules/sotd/{stability.js => stability.ts} (75%) rename lib/rules/sotd/{submission.js => submission.ts} (66%) rename lib/rules/sotd/{supersedable.js => supersedable.ts} (88%) rename lib/rules/sotd/{usage.js => usage.ts} (79%) rename lib/rules/structure/{canonical.js => canonical.ts} (79%) rename lib/rules/structure/{display-only.js => display-only.ts} (78%) rename lib/rules/structure/{h2.js => h2.ts} (84%) rename lib/rules/structure/{name.js => name.ts} (81%) rename lib/rules/structure/{neutral.js => neutral.ts} (72%) rename lib/rules/structure/{section-ids.js => section-ids.ts} (89%) rename lib/rules/structure/{security-privacy.js => security-privacy.ts} (81%) rename lib/rules/style/{back-to-top.js => back-to-top.ts} (54%) rename lib/rules/style/{body-toc-sidebar.js => body-toc-sidebar.ts} (64%) rename lib/rules/style/{meta.js => meta.ts} (87%) rename lib/rules/style/{script.js => script.ts} (56%) rename lib/rules/style/{sheet.js => sheet.ts} (77%) rename lib/rules/validation/{html.js => html.ts} (91%) rename lib/rules/validation/{wcag.js => wcag.ts} (51%) delete mode 100644 lib/sink.js rename lib/{throttled-ua.js => throttled-ua.ts} (73%) create mode 100644 lib/types.d.ts delete mode 100644 lib/util.js create mode 100644 lib/util.ts delete mode 100644 lib/validator.js create mode 100644 lib/validator.ts rename lib/{views.js => views.ts} (51%) delete mode 100644 tools/fetch-groups-db.js delete mode 100644 tools/groups-sparql.json delete mode 100644 tools/make-groups-db.js diff --git a/.c8rc b/.c8rc index bcde9c053..6e986c495 100644 --- a/.c8rc +++ b/.c8rc @@ -1,5 +1,6 @@ { "all": true, "exclude": ["public/*", "test/*", "tools/*"], + "extension": [".js"], "reporter": ["html"] } diff --git a/.cspell.json b/.cspell.json index 78d49d940..8c9fe455f 100644 --- a/.cspell.json +++ b/.cspell.json @@ -4,6 +4,7 @@ "ˈspɛk", "apikey", "badterms", + "basenames", "Beihang", "blocklist", "capi", @@ -114,10 +115,14 @@ "test/**/*.html", "package-lock.json", "tsconfig.json", + "lib/**/*.js", "node_modules/**", "design/**" ], - "ignoreRegExpList": ["/require\\(.*\\);/"], + "ignoreRegExpList": [ + "/require\\(.*\\);/", + "(FIXME|TODO|XXX)\\(.[^\\)]+\\)" + ], "overrides": [ { "filename": ["package.json"], diff --git a/.gitignore b/.gitignore index 15bc2eb29..6033c18af 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,12 @@ scratch # mac files .DS_Store */.DS_Store -.vscode/ .nyc_output .eslintcache + +# TS build output +app.js +*.d.ts +!lib/types.d.ts +lib/**/*.js diff --git a/.husky/pre-commit b/.husky/pre-commit index 928d1a896..3c2f93768 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -npx lint-staged && npx tsc && npm run spelling +npx lint-staged && npx tsc --noEmit && npm run spelling diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..6b21fe3f9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "js/ts.tsdk.path": "node_modules/typescript/lib" +} diff --git a/README.md b/README.md index bab665c8a..4a43287f8 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,20 @@ $ npm start $ npm start 3001 ``` +**Note:** `npm start` relies on JavaScript files, which must first be built via `npm run build`. +This step is not necessary when running the live development server or tests (see below). + +#### Auto reload when developing + +Run `npm run live [PORT]` when developing. The app will automatically reload when changes happen. + +Examples: + +```bash +$ npm run live +$ npm run live 3001 +``` + ### Environment variables #### `DEBUG` @@ -85,16 +99,6 @@ unauthenticated requests to 60 per hour. GH_TOKEN=github_pat_... npm start ``` -### Auto reload when developing - -Run `npm run live` when developing. The app will automatically reload when changes happen. - -```bash -$ npm run live - -$ npm run live 3001 -``` - ## 3. Testing #### 1. Simple test diff --git a/app.js b/app.ts similarity index 93% rename from app.js rename to app.ts index ebce6d4bc..f7da6dbcb 100644 --- a/app.js +++ b/app.ts @@ -2,6 +2,7 @@ * Main runnable file. */ +// @ts-ignore (no typings) import compression from 'compression'; import cors from 'cors'; import express from 'express'; @@ -9,17 +10,21 @@ import fileUpload from 'express-fileupload'; import EventEmitter from 'events'; import { writeFile } from 'fs'; import http from 'http'; +// @ts-ignore (no typings) import insafe from 'insafe'; import morgan from 'morgan'; import { Server } from 'socket.io'; import tmp from 'tmp'; + import * as api from './lib/api.js'; import * as l10n from './lib/l10n.js'; -import { allProfiles, importJSON } from './lib/util.js'; +import { allProfiles } from './lib/util.js'; import { Specberus } from './lib/validator.js'; import * as views from './lib/views.js'; +import type { ProfileModule } from './lib/types.js'; +import pkg from './package.json' with { type: 'json' }; -const { version } = importJSON('./package.json', import.meta.url); +const { version } = pkg; // Settings: const DEFAULT_PORT = 80; @@ -113,11 +118,13 @@ io.on('connection', socket => { message: 'Profile not provided.', }); const profilePath = allProfiles.find(p => - p.endsWith(`/${data.profile}.js`) + p.endsWith(`/${data.profile}`) ); let profile; try { - profile = await import(`./lib/profiles/${profilePath}`); + profile = (await import( + `./lib/profiles/${profilePath}.js` + )) as ProfileModule; } catch (err) { return socket.emit('exception', { message: `Failed to get profile ${profilePath}.`, @@ -178,7 +185,7 @@ io.on('connection', socket => { url: data.url, statusCodesAccepted: ['301', '406'], }) - .then(res => { + .then((res: any) => { if (res.status) { try { specberus.validate({ @@ -200,9 +207,9 @@ io.on('connection', socket => { socket.emit('exception', { message }); } }) - .catch(e => { + .catch((error: any) => { socket.emit('exception', { - message: `Insafe check blew up: ${e}`, + message: `Insafe check blew up: ${error}`, }); socket.emit('finished'); }); diff --git a/eslint.config.cjs b/eslint.config.cjs deleted file mode 100644 index 0642a565e..000000000 --- a/eslint.config.cjs +++ /dev/null @@ -1,55 +0,0 @@ -[ - { - extends: ['airbnb-base', 'prettier'], - plugins: ['prettier'], - rules: { - 'prettier/prettier': 'error', - 'no-shadow': 'off', - 'consistent-return': 'warn', - 'no-param-reassign': 'off', - strict: 'off', - 'global-require': 'off', - 'no-restricted-syntax': 'warn', - 'guard-for-in': 'warn', - 'prefer-destructuring': 'warn', - 'import/prefer-default-export': 'off', - 'import/extensions': 'off', - }, - ignores: ['doc/api'], - overrides: [ - { - files: ['public/js/*.js'], - env: { - browser: true, - jquery: true, - es6: true, - }, - rules: {}, - }, - { - files: ['app.js', 'lib/**/*.js', 'tools/**/*.js'], - plugins: ['node'], - parserOptions: { - ecmaVersion: 2020, - sourceType: 'module', - }, - extends: ['plugin:node/recommended'], - }, - { - files: ['test/**/*.js'], - env: { - mocha: true, - }, - rules: { - 'node/no-unpublished-require': 'off', - }, - plugins: ['node'], - extends: 'plugin:node/recommended', - parserOptions: { - ecmaVersion: 2022, - sourceType: 'module', - }, - }, - ], - }, -]; diff --git a/lib/api.js b/lib/api.js deleted file mode 100644 index 430ce3d7b..000000000 --- a/lib/api.js +++ /dev/null @@ -1,217 +0,0 @@ -/** - * REST API. - */ - -import { fileTypeFromFile } from 'file-type'; -import { Sink } from './sink.js'; -import { buildJSONresult, importJSON, processParams } from './util.js'; -import { Specberus } from './validator.js'; - -/** @import {Response} from "express" */ - -const { version } = importJSON('../package.json', import.meta.url); - -/** - * Send the JSON result to the client. - * - * @param {string[]} err - errors. - * @param {string[]} warn - warnings. - * @param {string[]} inf - informative messages. - * @param {Response} res - Express HTTP response. - * @param {Object} metadata - dictionary with some found metadata. - */ - -const sendJSONresult = function (err, warn, inf, res, metadata) { - delete metadata.file; - const wrapper = buildJSONresult(err, warn, inf, metadata); - res.status(wrapper.success ? 200 : 400).json(wrapper); -}; - -/** - * Handle an API request: parse method and parameters, handle common errors and call the validator. - */ -const processGet = () => async (req, res) => { - const path = req._parsedUrl.pathname; - if (path === '/api/version') { - res.status(200).send(version); - } else if (path === '/api/metadata' || path === '/api/validate') { - await processRequest(req, res, req.query); - } else { - res.status(400).send('Wrong API endpoint.'); - } -}; - -const processPost = () => async (req, res) => { - const path = req._parsedUrl.pathname; - - if (path === '/api/metadata' || path === '/api/validate') { - if (!req.files || !req.files.file) { - return res.send({ - status: 500, - message: 'Missing file.', - }); - } - try { - const { tempFilePath } = req.files.file; - - // file must be an html file - const type = await fileTypeFromFile(tempFilePath); - if (type != null) { - return res.send({ - status: 500, - message: 'Invalid file type. Please send an HTML file.', - }); - } - const params = req.body || {}; - params.file = tempFilePath; - - await processRequest(req, res, params); - } catch (err) { - res.status(500).send(err); - } - } else { - res.status(400).send('Wrong API endpoint.'); - } -}; - -const processRequest = async (req, res, params) => { - const validate = req._parsedUrl.pathname === '/api/validate'; - let v; - let v2; - let options; - let options2; - let errors; - let errors2; - let warnings; - let warnings2; - let info; - let info2; - let handler; - let handler2; - - try { - options = await processParams(params, undefined, { - required: validate ? ['profile'] : [], - forbidden: ['document', 'source'], - }); - } catch (err) { - return sendJSONresult([err.toString()], [], [], res, {}); - } - if (validate && options.profile === 'auto') { - errors = []; - v = new Specberus(); - handler = new Sink( - (...data) => { - errors.push(Object.assign({}, ...data)); - }, - async data => { - if (errors.length > 0) sendJSONresult(errors, [], [], res, {}); - else { - const meta = data.metadata; - if (options.url) meta.url = options.url; - else meta.file = options.file; - try { - options2 = await processParams(meta, undefined, { - allowUnknownParams: true, - }); - } catch (err) { - return sendJSONresult( - [err.toString()], - [], - [], - res, - {} - ); - } - errors2 = []; - warnings2 = []; - info2 = []; - v2 = new Specberus(); - handler2 = new Sink( - (...data2) => { - errors2.push(Object.assign({}, ...data2)); - }, - () => { - sendJSONresult( - errors2, - warnings2, - info2, - res, - meta - ); - }, - (...data2) => { - warnings2.push(Object.assign({}, ...data2)); - }, - (...data2) => { - info2.push(Object.assign({}, ...data2)); - } - ); - options2.events = handler2; - handler2.on('exception', data => { - sendJSONresult( - [data.message ? data.message : data], - [], - [], - res, - {} - ); - }); - v2.validate(options2); - } - } - ); - handler.on('exception', data => { - sendJSONresult( - [data.message ? data.message : data], - [], - [], - res, - {} - ); - }); - options.events = handler; - v.extractMetadata(options); - } else { - errors = []; - warnings = []; - info = []; - v = new Specberus(); - handler = new Sink( - (...data) => { - errors.push(Object.assign({}, ...data)); - }, - data => { - sendJSONresult(errors, warnings, info, res, data.metadata); - }, - (...data) => { - warnings.push(Object.assign({}, ...data)); - }, - (...data) => { - info.push(Object.assign({}, ...data)); - } - ); - handler.on('exception', data => { - sendJSONresult( - [data.message ? data.message : data], - [], - [], - res, - {} - ); - }); - options.events = handler; - options.additionalMetadata = req.query.additionalMetadata === 'true'; - if (validate) v.validate(options); - else v.extractMetadata(options); - } -}; - -/** - * Adds middleware for handling pubrules requests. - * NOTE: This requires express-fileupload middleware to be hooked up separately! - */ -export const setUp = function (app) { - app.get(/\/api\/.*/, processGet()); - app.post(/\/api\/.*/, processPost()); -}; diff --git a/lib/api.ts b/lib/api.ts new file mode 100644 index 000000000..5c90ea9e5 --- /dev/null +++ b/lib/api.ts @@ -0,0 +1,172 @@ +/** + * @file REST API. + */ + +import EventEmitter from 'events'; + +import { fileTypeFromFile } from 'file-type'; +import type { Express, Request, Response } from 'express'; + +import { buildJSONresult, processParams } from './util.js'; +import { Specberus, type ValidateOptions } from './validator.js'; +import type { HandlerMessage } from './types.js'; + +import pkg from '../package.json' with { type: 'json' }; + +/** + * Send the JSON result to the client. + * + * @param errors - errors + * @param warnings - warnings + * @param info - informative messages + * @param res - Express HTTP response + * @param metadata - dictionary with some found metadata + */ +const sendJSONresult = function ( + res: Response, + errors: HandlerMessage[] = [], + warnings: HandlerMessage[] = [], + info: HandlerMessage[] = [], + metadata: Record = {} +) { + delete metadata.file; + const wrapper = buildJSONresult(errors, warnings, info, metadata); + res.status(wrapper.success ? 200 : 400).json(wrapper); +}; + +const getFullUrl = (req: Request) => + new URL(`${req.protocol}://${req.host}${req.url}`); + +/** + * Handle an API request: parse method and parameters, handle common errors and call the validator. + */ +const processGet = () => async (req: Request, res: Response) => { + const path = getFullUrl(req).pathname; + if (path === '/api/version') { + res.status(200).send(pkg.version); + } else if (path === '/api/metadata' || path === '/api/validate') { + await processRequest(req, res, req.query); + } else { + res.status(400).send('Wrong API endpoint.'); + } +}; + +const processPost = () => async (req: Request, res: Response) => { + const path = getFullUrl(req).pathname; + + if (path === '/api/metadata' || path === '/api/validate') { + if (!req.files || !req.files.file) { + return res.send({ + status: 400, + message: 'Missing file.', + }); + } + if (Array.isArray(req.files.file)) { + return res.send({ + status: 400, + message: 'Expected a single file.', + }); + } + try { + const { tempFilePath } = req.files.file; + + // file must be an html file + const type = await fileTypeFromFile(tempFilePath); + if (type != null) { + return res.send({ + status: 500, + message: 'Invalid file type. Please send an HTML file.', + }); + } + const params = req.body || {}; + params.file = tempFilePath; + + await processRequest(req, res, params); + } catch (err) { + res.status(500).send(err); + } + } else { + res.status(400).send('Wrong API endpoint.'); + } +}; + +function createHandler(res: Response, metadataOverride?: Record) { + const handler = new EventEmitter(); + handler.on('exception', data => { + sendJSONresult(res, [data.message ? data.message : data]); + }); + handler.on('end-all', data => { + sendJSONresult( + res, + data.errors, + data.warnings, + data.info, + metadataOverride || data.metadata + ); + }); + return handler; +} + +const processRequest = async ( + req: Request, + res: Response, + params: qs.ParsedQs +) => { + const shouldValidate = getFullUrl(req).pathname === '/api/validate'; + + let options; + try { + options = await processParams(params, undefined, { + required: shouldValidate ? ['profile'] : [], + forbidden: ['source'], + }); + } catch (err) { + return sendJSONresult(res, [err.toString()]); + } + + if (shouldValidate && options.profile === 'auto') { + const sr = new Specberus(); + const handler = new EventEmitter(); + handler.on('exception', data => { + sendJSONresult(res, [data.message ? data.message : data]); + }); + handler.on('end-all', async data => { + if (data.errors.length) sendJSONresult(res, data.errors); + else { + const meta = data.metadata; + if (options.url) meta.url = options.url; + else meta.file = options.file; + let metaOptions: ValidateOptions; + try { + metaOptions = await processParams(meta, undefined, { + allowUnknownParams: true, + }); + } catch (err) { + return sendJSONresult(res, [err.toString()]); + } + metaOptions.events = createHandler(res, meta); + + const metaSr = new Specberus(); + metaSr.validate(metaOptions); + } + }); + options.events = handler; + sr.extractMetadata(options); + } else { + options.events = createHandler(res); + options.additionalMetadata = req.query.additionalMetadata === 'true'; + + const sr = new Specberus(); + if (shouldValidate) sr.validate(options); + else sr.extractMetadata(options); + } +}; + +/** + * Adds middleware for handling pubrules requests. + * NOTE: This requires express-fileupload middleware to be hooked up separately! + */ +export const setUp = function (app: Express) { + app.get(/\/api\/.*/, processGet()); + app.post(/\/api\/.*/, processPost()); +}; diff --git a/lib/exceptions.js b/lib/exceptions.js deleted file mode 100644 index 24c1f69f8..000000000 --- a/lib/exceptions.js +++ /dev/null @@ -1,62 +0,0 @@ -import { importJSON } from './util.js'; - -const records = importJSON('./exceptions.json', import.meta.url); - -/** - * @param data - * @param ref - * @returns {boolean} true if the data and ref are the same, or if the ref is undefined - */ -function compareValue(data, ref) { - return ref === undefined || data === ref; -} - -/** - * @param shortname - */ -function findSet(shortname) { - let count = 0; - /** - * @param name - */ - function recursiveFindSet(name) { - if (count > 10) return undefined; - const set = []; - for (const k in records) { - const regex = new RegExp(k); - if ( - Object.prototype.hasOwnProperty.call(records, k) && - regex.test(name) - ) { - set.push(records[k]); - } - } - count += 1; - if (typeof set === 'string') return recursiveFindSet(set); - return set; - } - return recursiveFindSet(shortname); -} - -export const Exceptions = function () {}; - -Exceptions.prototype.has = function (shortname, rule, key, extra) { - const set = findSet(shortname); - if (set === undefined) return false; - for (let i = set.length - 1; i >= 0; i -= 1) { - for (let y = set[i].length - 1; y >= 0; y -= 1) { - const exception = set[i][y]; - if ( - rule === exception.rule && - (extra === undefined || - (!exception.message && - compareValue(extra.type, exception.type)) || - (exception.message && - new RegExp(exception.message).test(extra.message))) - ) { - return true; - } - } - } - return false; -}; diff --git a/lib/exceptions.ts b/lib/exceptions.ts new file mode 100644 index 000000000..e3867f2e7 --- /dev/null +++ b/lib/exceptions.ts @@ -0,0 +1,51 @@ +import exceptions from './exceptions.json' with { type: 'json' }; + +interface Exception { + rule: string; + message?: string; + type?: string; +} + +function findSet(shortname: string) { + let count = 0; + function recursiveFindSet(name: string) { + if (count > 10) return undefined; + const set: Exception[][] = []; + for (const k in exceptions) { + const regex = new RegExp(k); + if (Object.hasOwn(exceptions, k) && regex.test(name)) { + set.push(exceptions[k as keyof typeof exceptions]); + } + } + count += 1; + if (typeof set === 'string') return recursiveFindSet(set); + return set; + } + return recursiveFindSet(shortname); +} + +export function hasExceptions( + shortname: string, + rule: string, + extra?: Record +) { + const set = findSet(shortname); + if (!set) return false; + for (let i = set.length - 1; i >= 0; i--) { + for (let y = set[i].length - 1; y >= 0; y--) { + const exception = set[i][y]; + if ( + rule === exception.rule && + (!extra || + (!exception.message && + (exception.type === undefined || + extra.type === exception.type)) || + (exception.message && + new RegExp(exception.message).test(extra.message))) + ) { + return true; + } + } + } + return false; +} diff --git a/lib/groups-db.json b/lib/groups-db.json deleted file mode 100644 index 80ce603f2..000000000 --- a/lib/groups-db.json +++ /dev/null @@ -1,162 +0,0 @@ -{ - "public-audio@w3.org": { - "url": "https://www.w3.org/2011/audio/", - "name": "Audio Working Group" - }, - "w3c-wai-au@w3.org": { - "url": "https://www.w3.org/WAI/AU/", - "name": "Authoring Tool Accessibility Guidelines Working Group" - }, - "public-browser-tools-testing@w3.org": { - "url": "https://www.w3.org/testing/browser/", - "name": "Browser Testing and Tools Working Group" - }, - "public-csv-wg@w3.org": { - "url": "https://www.w3.org/2013/csvw/", - "name": "CSV on the Web Working Group" - }, - "public-dwbp-wg@w3.org": { - "url": "https://www.w3.org/2013/dwbp/", - "name": "Data on the Web Best Practices Working Group" - }, - "public-device-apis@w3.org": { - "url": "https://www.w3.org/2009/dap/", - "name": "Device APIs Working Group" - }, - "w3c-wai-eo@w3.org": { - "url": "https://www.w3.org/WAI/EO/", - "name": "Education and Outreach Working Group" - }, - "public-exi@w3.org": { - "url": "https://www.w3.org/XML/EXI/", - "name": "Efficient XML Interchange Working Group" - }, - "public-wai-ert@w3.org": { - "url": "https://www.w3.org/WAI/ER/", - "name": "Evaluation and Repair Tools Working Group" - }, - "public-forms@w3.org": { - "url": "https://www.w3.org/MarkUp/Forms/", - "name": "Forms Working Group" - }, - "public-geolocation@w3.org": { - "url": "https://www.w3.org/2008/geolocation/", - "name": "Geolocation Working Group" - }, - "public-gld-wg@w3.org": { - "url": "https://www.w3.org/2011/gld/", - "name": "Government Linked Data Working Group" - }, - "public-html@w3.org": { - "url": "https://www.w3.org/html/wg/", - "name": "HTML Working Group" - }, - "public-indie-ui@w3.org": { - "url": "https://www.w3.org/WAI/IndieUI/", - "name": "Independent User Interface (Indie UI) Working Group" - }, - "public-ldp-wg@w3.org": { - "url": "https://www.w3.org/2012/ldp/", - "name": "Linked Data Platform (LDP) Working Group" - }, - "public-media-annotation@w3.org": { - "url": "https://www.w3.org/2008/WebVideo/Annotations/", - "name": "Media Annotations Working Group" - }, - "public-mbui@w3.org": { - "url": "https://www.w3.org/2011/mbui/", - "name": "Model-Based User Interfaces Working Group" - }, - "www-multimodal@w3.org": { - "url": "https://www.w3.org/2002/mmi/", - "name": "Multimodal Interaction Working Group" - }, - "public-nfc@w3.org": { - "url": "https://www.w3.org/2012/nfc/", - "name": "Near Field Communications Working Group" - }, - "public-pointer-events@w3.org": { - "url": "https://www.w3.org/2012/pointerevents/", - "name": "Pointer Events Working Group" - }, - "public-rdf-wg@w3.org": { - "url": "https://www.w3.org/2011/rdf-wg/", - "name": "RDF Working Group" - }, - "public-rdfa@w3.org": { - "url": "https://www.w3.org/2010/02/rdfa/", - "name": "RDFa Working Group" - }, - "public-wai-rd@w3.org": { - "url": "https://www.w3.org/WAI/RD/", - "name": "Research and Development Working Group" - }, - "public-svg-wg@w3.org": { - "url": "https://www.w3.org/Graphics/SVG/WG/", - "name": "SVG Working Group" - }, - "public-sysapps@w3.org": { - "url": "https://www.w3.org/2012/sysapps/", - "name": "System Applications Working Group" - }, - "www-tag@w3.org": { - "url": "https://www.w3.org/2001/tag/", - "name": "Technical Architecture Group" - }, - "public-tt@w3.org": { - "url": "https://www.w3.org/AudioVideo/TT/", - "name": "Timed Text Working Group" - }, - "public-tracking@w3.org": { - "url": "https://www.w3.org/2011/tracking-protection/", - "name": "Tracking Protection Working Group" - }, - "w3c-wai-ua@w3.org": { - "url": "https://www.w3.org/WAI/UA/", - "name": "User Agent Accessibility Guidelines Working Group" - }, - "public-webappsec@w3.org": { - "url": "https://www.w3.org/2011/webappsec/", - "name": "Web Application Security Working Group" - }, - "public-webapps@w3.org": { - "url": "https://www.w3.org/2008/webapps/", - "name": "Web Applications Working Group" - }, - "w3c-wai-gl@w3.org": { - "url": "https://www.w3.org/WAI/GL/", - "name": "Web Content Accessibility Guidelines Working Group" - }, - "public-webcrypto@w3.org": { - "url": "https://www.w3.org/2012/webcrypto/", - "name": "Web Cryptography Working Group" - }, - "public-web-notification@w3.org": { - "url": "https://www.w3.org/2010/web-notifications/", - "name": "Web Notification Working Group" - }, - "public-web-perf@w3.org": { - "url": "https://www.w3.org/2010/webperf/", - "name": "Web Performance Working Group" - }, - "public-webrtc@w3.org": { - "url": "https://www.w3.org/2011/04/webrtc/", - "name": "Web Real-Time Communications Working Group" - }, - "public-webfonts-wg@w3.org": { - "url": "https://www.w3.org/Fonts/WG/", - "name": "WebFonts Working Group" - }, - "public-xml-core-wg@w3.org": { - "url": "https://www.w3.org/XML/Core/", - "name": "XML Core Working Group" - }, - "public-xml-processing-model-wg@w3.org": { - "url": "https://www.w3.org/XML/Processing/", - "name": "XML Processing Model Working Group" - }, - "public-xmlsec@w3.org": { - "url": "https://www.w3.org/2008/xmlsec/", - "name": "XML Security Working Group" - } -} diff --git a/lib/l10n-en_GB.js b/lib/l10n-en_GB.ts similarity index 99% rename from lib/l10n-en_GB.js rename to lib/l10n-en_GB.ts index 855dc5d10..e406799f6 100644 --- a/lib/l10n-en_GB.js +++ b/lib/l10n-en_GB.ts @@ -379,4 +379,4 @@ export const messages = { 'metadata.charters': false, 'metadata.sotd': false, 'metadata.abstract': false, -}; +} as const; diff --git a/lib/l10n.js b/lib/l10n.js deleted file mode 100644 index b0534debd..000000000 --- a/lib/l10n.js +++ /dev/null @@ -1,172 +0,0 @@ -/** - * Compose user messages. - */ - -// Internal packages: -import { messages } from './l10n-en_GB.js'; -import { importJSON } from './util.js'; - -const originalRules = importJSON('./rules.json', import.meta.url); - -// Constants: -const enGB = messages; - -// Variables: -let lang; -const rules = {}; - -for (const t in originalRules) - if (t === '*') rules[t] = originalRules[t]; - else - for (const p in originalRules[t].profiles) - rules[p] = originalRules[t].profiles[p]; - -/** - * Set a locale to be used globally by this module. - * - * @param {string} language - locale, expressed as a string, eg en_GB. - */ - -/** - * @param language - */ -export function setLanguage(language) { - if (!language) - throw new Error( - 'l10n.setLanguage() invoked without passing a language code as parameter' - ); - else if (language.match(/^en([.\-_]?gb)?$/i)) lang = enGB; - else - throw new Error( - 'Language code passed to l10n.setLanguage() is not valid' - ); -} - -/** - * @param profileCode - * @param rule - * @param key - * @param extra - */ -export function assembleData(profileCode, rule, key, extra) { - const messageData = {}; - // Corner case: if the profile is unknown, let's assume 'WD' (most common). - const profile = profileCode ? profileCode.replace('-Echidna', '') : 'WD'; - let name; - if (typeof rule === 'string') name = rule; - else name = rule.name; - if (!lang) - throw new Error( - 'l10n.assembleData() invoked before a locale is defined; call l10n.setLanguage() first' - ); - else if (!Object.prototype.hasOwnProperty.call(rules, profile)) - throw new Error( - `l10n.assembleData(): unknown profile code “${profile}”` - ); - else { - if (!Object.prototype.hasOwnProperty.call(lang, `${name}.${key}`)) - throw new Error( - `l10n.assembleData() could not interpret key “${name}.${key}”` - ); - - messageData.message = lang[`${name}.${key}`] || ''; - if (extra) - messageData.message = messageData.message.replace( - /\$\{([^}]+)\}/g, - (m, p1) => { - if (Object.prototype.hasOwnProperty.call(extra, p1)) - return extra[p1]; - - messageData.additionalMessage = `
NB: Pubrules may have found an internal error while checking this rule. -Feel free to file this issue on GitHub -to let developers examine the problem (you can submit it as is; no additional information is required).
`; - return `[unknown ${p1}]`; - } - ); - messageData.profile = profile; - return messageData; - } -} - -/** - * @param profileCode - * @param rule - * @param key - * @param extra - */ -export function message(profileCode, rule, key, extra) { - const result = {}; - const messageData = assembleData(profileCode, rule, key, extra); - if (!messageData) return; - result.message = - messageData.message && messageData.message.length - ? `
${messageData.message}
` - : ''; - const { profile } = messageData; - - if (rule.section && rule.rule) { - let selector; - if (rules[profile].sections[rule.section]) { - if (rules[profile].sections[rule.section].rules[rule.rule]) { - if ( - typeof rules[profile].sections[rule.section].rules[ - rule.rule - ] === 'boolean' - ) { - // Common rule, with no parameters - result.message += `
${ - rules['*'].sections[rule.section].rules[rule.rule] - }
`; - } else if ( - typeof rules[profile].sections[rule.section].rules[ - rule.rule - ] === 'string' - ) { - // Specific rule - result.message += `
${ - rules[profile].sections[rule.section].rules[rule.rule] - }
`; - } else if ( - typeof rules[profile].sections[rule.section].rules[ - rule.rule - ] === 'object' - ) { - // Array (common rule with parameters) - const values = - rules[profile].sections[rule.section].rules[rule.rule]; - let template = - rules['*'].sections[rule.section].rules[rule.rule]; - for (let p = 0; p < values.length; p += 1) - template = template.replace( - new RegExp(`@{param${p + 1}}`, 'g'), - values[p] - ); - result.message += `
${template}
`; - } else { - // Error - selector = encodeURIComponent( - `[${profile}][${rule.section}][${rule.rule}].text` - ); - } - result.message = result.message.replace( - new RegExp(`@{year}`, 'g'), - new Date().getFullYear() - ); - } else - selector = encodeURIComponent( - `[${profile}][${rule.section}][${rule.rule}]` - ); - } else selector = encodeURIComponent(`[${profile}][${rule.section}]`); - if (selector) - result.message += `
NB: Pubrules hit an internal error while checking this rule. -Please file this issue on GitHub to help developers fix the problem -(you can submit it as is; no additional information is required).
`; - result.id = rule.rule; - result.name = rule.name; - } - if (messageData.additionalMessage) - result.message += messageData.additionalMessage; - return result; -} diff --git a/lib/l10n.ts b/lib/l10n.ts new file mode 100644 index 000000000..0277f62dc --- /dev/null +++ b/lib/l10n.ts @@ -0,0 +1,178 @@ +/** + * Compose user messages. + */ + +import { messages } from './l10n-en_GB.js'; +import originalRules from './rules.json' with { type: 'json' }; +import type { + GenericRulesSection, + RuleBase, + RuleMeta, + RulesProfile, + RulesSection, +} from './types.js'; +import { isRuleTrack } from './util.js'; + +const enGB = messages; + +type LanguageMap = Record; + +let lang: LanguageMap | undefined; +const profileRules: Record = {}; + +for (const t of Object.keys(originalRules)) + if (isRuleTrack(t)) + for (const [p, profile] of Object.entries(originalRules[t].profiles)) + profileRules[p] = profile; + +const genericSections = originalRules['*'].sections as Record< + string, + GenericRulesSection +>; + +/** + * Set a locale to be used globally by this module. + * + * @param {string} language - locale, expressed as a string, eg en_GB. + */ + +/** + * @param language + */ +export function setLanguage(language: string) { + if (!language) + throw new Error( + 'l10n.setLanguage() invoked without passing a language code as parameter' + ); + else if (language.match(/^en([.\-_]?gb)?$/i)) lang = enGB; + else + throw new Error( + 'Language code passed to l10n.setLanguage() is not valid' + ); +} + +function assertLanguageDefined( + lang: LanguageMap | undefined +): asserts lang is LanguageMap { + if (!lang) + throw new Error( + 'l10n.assembleData() invoked before a locale is defined; call l10n.setLanguage() first' + ); +} + +export function assembleData( + profileCode: string | null, + rule: RuleBase | RuleMeta | string, + key: string, + extra?: Record +) { + // Corner case: if the profile is unknown, let's assume 'WD' (most common). + const profile = profileCode ? profileCode.replace('-Echidna', '') : 'WD'; + const name = typeof rule === 'string' ? rule : rule.name; + assertLanguageDefined(lang); + + if (!Object.hasOwn(profileRules, profile)) + throw new Error( + `l10n.assembleData(): unknown profile code “${profile}”` + ); + + if (!Object.hasOwn(lang, `${name}.${key}`)) + throw new Error( + `l10n.assembleData() could not interpret key “${name}.${key}”` + ); + + const messageData = { + additionalMessage: '', + message: lang[`${name}.${key}`] || '', + profile, + }; + + if (extra) + messageData.message = messageData.message.replace( + /\$\{([^}]+)\}/g, + (_, p1) => { + if (Object.hasOwn(extra, p1)) return extra[p1]; + + messageData.additionalMessage = `
NB: Pubrules may have found an internal error while checking this rule. +Feel free to file this issue on GitHub +to let developers examine the problem (you can submit it as is; no additional information is required).
`; + return `[unknown ${p1}]`; + } + ); + return messageData; +} + +export function message( + profileCode: string | null, + rule: RuleBase | RuleMeta | string, + key: string, + extra?: Record +) { + const messageData = assembleData(profileCode, rule, key, extra); + if (!messageData) return; + let message = + messageData.message && messageData.message.length + ? `
${messageData.message}
` + : ''; + const { profile } = messageData; + + if (typeof rule === 'object' && 'rule' in rule) { + let selector; + const profileSections = profileRules[profile].sections as Record< + string, + RulesSection + >; + if (profileSections[rule.section]) { + if (profileSections[rule.section].rules[rule.rule]) { + const genericRules = + genericSections[ + rule.section as keyof typeof genericSections + ].rules; + const genericRule = + genericRules[rule.rule as keyof typeof genericRules]; + const profileRule = + profileSections[rule.section].rules[rule.rule]; + if (typeof profileRule === 'boolean') { + // Common rule, with no parameters + message += `
${genericRule}
`; + } else if (typeof profileRule === 'string') { + // Specific rule + message += `
${profileRule}
`; + } else if (typeof profileRule === 'object') { + // Array (common rule with parameters) + const template = profileRule.reduce( + (rule, value, i) => + rule.replace( + new RegExp(`@{param${i + 1}}`, 'g'), + value + ), + genericRule + ); + message += `
${template}
`; + } else { + // Error + selector = encodeURIComponent( + `[${profile}][${rule.section}][${rule.rule}].text` + ); + } + message = message.replace( + new RegExp(`@{year}`, 'g'), + '' + new Date().getFullYear() + ); + } else + selector = encodeURIComponent( + `[${profile}][${rule.section}][${rule.rule}]` + ); + } else selector = encodeURIComponent(`[${profile}][${rule.section}]`); + if (selector) + message += `
NB: Pubrules hit an internal error while checking this rule. +Please file this issue on GitHub to help developers fix the problem +(you can submit it as is; no additional information is required).
`; + } + if (messageData.additionalMessage) message += messageData.additionalMessage; + if (typeof rule === 'object' && 'rule' in rule) + return { id: rule.rule, name: rule.name, message }; + return { message }; +} diff --git a/lib/profiles/SUBM.js b/lib/profiles/SUBM.ts similarity index 100% rename from lib/profiles/SUBM.js rename to lib/profiles/SUBM.ts diff --git a/lib/profiles/SUBM/MEM-SUBM.js b/lib/profiles/SUBM/MEM-SUBM.ts similarity index 88% rename from lib/profiles/SUBM/MEM-SUBM.js rename to lib/profiles/SUBM/MEM-SUBM.ts index cb2030097..316f487fc 100644 --- a/lib/profiles/SUBM/MEM-SUBM.js +++ b/lib/profiles/SUBM/MEM-SUBM.ts @@ -1,10 +1,11 @@ import * as memsubCopyright from '../../rules/headers/memsub-copyright.js'; import * as submission from '../../rules/sotd/submission.js'; +import type { SpecberusConfig } from '../../types.js'; import { insertAfter, removeRules } from '../profileUtil.js'; import { rules as baseRules } from '../SUBM.js'; export const name = 'MEM-SUBM'; -export const config = { +export const config: SpecberusConfig = { status: 'SUBM', longStatus: 'Member Submission', styleSheet: 'W3C-Member-SUBM', diff --git a/lib/profiles/TR.js b/lib/profiles/TR.ts similarity index 100% rename from lib/profiles/TR.js rename to lib/profiles/TR.ts diff --git a/lib/profiles/TR/Note/DNOTE-Echidna.js b/lib/profiles/TR/Note/DNOTE-Echidna.ts similarity index 78% rename from lib/profiles/TR/Note/DNOTE-Echidna.js rename to lib/profiles/TR/Note/DNOTE-Echidna.ts index 122ff9d23..013bce85e 100644 --- a/lib/profiles/TR/Note/DNOTE-Echidna.js +++ b/lib/profiles/TR/Note/DNOTE-Echidna.ts @@ -3,10 +3,10 @@ import * as todaysDate from '../../../rules/echidna/todays-date.js'; import * as delivererChange from '../../../rules/echidna/deliverer-change.js'; import { insertAfter } from '../../profileUtil.js'; -import { config as baseConfig, rules as baseRules } from './DNOTE.js'; +import { rules as baseRules } from './DNOTE.js'; export const name = 'DNOTE-Echidna'; -export const config = baseConfig; +export { config } from './DNOTE.js'; export const rules = insertAfter(baseRules, 'sotd.process-document', [ todaysDate, diff --git a/lib/profiles/TR/Note/DNOTE.js b/lib/profiles/TR/Note/DNOTE.ts similarity index 80% rename from lib/profiles/TR/Note/DNOTE.js rename to lib/profiles/TR/Note/DNOTE.ts index 5e8b338f3..ca8111bc4 100644 --- a/lib/profiles/TR/Note/DNOTE.js +++ b/lib/profiles/TR/Note/DNOTE.ts @@ -1,9 +1,10 @@ import * as draftStability from '../../../rules/sotd/draft-stability.js'; +import type { SpecberusConfig } from '../../../types.js'; import { insertAfter } from '../../profileUtil.js'; import { config as baseConfig, rules as baseRules } from './note-base.js'; export const name = 'DNOTE'; -export const config = { +export const config: SpecberusConfig = { ...baseConfig, status: 'DNOTE', longStatus: 'Group Note Draft', diff --git a/lib/profiles/TR/Note/NOTE-Echidna.js b/lib/profiles/TR/Note/NOTE-Echidna.ts similarity index 77% rename from lib/profiles/TR/Note/NOTE-Echidna.js rename to lib/profiles/TR/Note/NOTE-Echidna.ts index 4f0c9e412..d9978556b 100644 --- a/lib/profiles/TR/Note/NOTE-Echidna.js +++ b/lib/profiles/TR/Note/NOTE-Echidna.ts @@ -1,10 +1,10 @@ import * as todaysDate from '../../../rules/echidna/todays-date.js'; import * as delivererChange from '../../../rules/echidna/deliverer-change.js'; import { insertAfter } from '../../profileUtil.js'; -import { config as baseConfig, rules as baseRules } from './NOTE.js'; +import { rules as baseRules } from './NOTE.js'; export const name = 'NOTE-Echidna'; -export const config = baseConfig; +export { config } from './NOTE.js'; export const rules = insertAfter(baseRules, 'sotd.process-document', [ todaysDate, diff --git a/lib/profiles/TR/Note/NOTE.js b/lib/profiles/TR/Note/NOTE.ts similarity index 70% rename from lib/profiles/TR/Note/NOTE.js rename to lib/profiles/TR/Note/NOTE.ts index 1eb9e8282..8ff292f29 100644 --- a/lib/profiles/TR/Note/NOTE.js +++ b/lib/profiles/TR/Note/NOTE.ts @@ -1,7 +1,8 @@ +import type { SpecberusConfig } from '../../../types.js'; import { config as baseConfig, rules as baseRules } from './note-base.js'; export const name = 'NOTE'; -export const config = { +export const config: SpecberusConfig = { ...baseConfig, status: 'NOTE', longStatus: 'Group Note', diff --git a/lib/profiles/TR/Note/STMT.js b/lib/profiles/TR/Note/STMT.ts similarity index 82% rename from lib/profiles/TR/Note/STMT.js rename to lib/profiles/TR/Note/STMT.ts index 6a28836c3..58daea9fd 100644 --- a/lib/profiles/TR/Note/STMT.js +++ b/lib/profiles/TR/Note/STMT.ts @@ -1,9 +1,10 @@ import * as translation from '../../../rules/headers/translation.js'; import { config as baseConfig, rules as baseRules } from './note-base.js'; import { insertAfter } from '../../profileUtil.js'; +import type { SpecberusConfig } from '../../../types.js'; export const name = 'STMT'; -export const config = { +export const config: SpecberusConfig = { ...baseConfig, status: 'STMT', longStatus: 'Statement', diff --git a/lib/profiles/TR/Note/note-base.js b/lib/profiles/TR/Note/note-base.ts similarity index 96% rename from lib/profiles/TR/Note/note-base.js rename to lib/profiles/TR/Note/note-base.ts index 53c706a48..c68c2034a 100644 --- a/lib/profiles/TR/Note/note-base.js +++ b/lib/profiles/TR/Note/note-base.ts @@ -4,6 +4,6 @@ import { rules as baseRules } from '../../TR.js'; export const config = { track: 'Note', -}; +} as const; export const rules = insertAfter(baseRules, 'sotd.pp', [deliverNote]); diff --git a/lib/profiles/TR/Recommendation/CR-Echidna.js b/lib/profiles/TR/Recommendation/CR-Echidna.ts similarity index 100% rename from lib/profiles/TR/Recommendation/CR-Echidna.js rename to lib/profiles/TR/Recommendation/CR-Echidna.ts diff --git a/lib/profiles/TR/Recommendation/CR.js b/lib/profiles/TR/Recommendation/CR.ts similarity index 85% rename from lib/profiles/TR/Recommendation/CR.js rename to lib/profiles/TR/Recommendation/CR.ts index b0032479f..6a3233664 100644 --- a/lib/profiles/TR/Recommendation/CR.js +++ b/lib/profiles/TR/Recommendation/CR.ts @@ -1,5 +1,6 @@ import * as candidateReviewEnd from '../../../rules/sotd/candidate-review-end.js'; import * as newFeatures from '../../../rules/sotd/new-features.js'; +import type { SpecberusConfig } from '../../../types.js'; import { insertAfter } from '../../profileUtil.js'; import { config as baseConfig, @@ -7,7 +8,7 @@ import { } from './recommendation-base.js'; export const name = 'CR'; -export const config = { +export const config: SpecberusConfig = { ...baseConfig, status: 'CR', longStatus: 'Candidate Recommendation', diff --git a/lib/profiles/TR/Recommendation/CRD-Echidna.js b/lib/profiles/TR/Recommendation/CRD-Echidna.ts similarity index 100% rename from lib/profiles/TR/Recommendation/CRD-Echidna.js rename to lib/profiles/TR/Recommendation/CRD-Echidna.ts diff --git a/lib/profiles/TR/Recommendation/CRD.js b/lib/profiles/TR/Recommendation/CRD.ts similarity index 84% rename from lib/profiles/TR/Recommendation/CRD.js rename to lib/profiles/TR/Recommendation/CRD.ts index fa6b132f3..6ad86f43f 100644 --- a/lib/profiles/TR/Recommendation/CRD.js +++ b/lib/profiles/TR/Recommendation/CRD.ts @@ -1,5 +1,6 @@ import * as draftStability from '../../../rules/sotd/draft-stability.js'; import * as newFeatures from '../../../rules/sotd/new-features.js'; +import type { SpecberusConfig } from '../../../types.js'; import { insertAfter } from '../../profileUtil.js'; import { config as baseConfig, @@ -7,7 +8,7 @@ import { } from './recommendation-base.js'; export const name = 'CRD'; -export const config = { +export const config: SpecberusConfig = { ...baseConfig, status: 'CRD', longStatus: 'Candidate Recommendation', diff --git a/lib/profiles/TR/Recommendation/DISC.js b/lib/profiles/TR/Recommendation/DISC.ts similarity index 81% rename from lib/profiles/TR/Recommendation/DISC.js rename to lib/profiles/TR/Recommendation/DISC.ts index 0ec103ef4..e0fb3feb0 100644 --- a/lib/profiles/TR/Recommendation/DISC.js +++ b/lib/profiles/TR/Recommendation/DISC.ts @@ -1,4 +1,5 @@ // customize rules +import type { SpecberusConfig } from '../../../types.js'; import { removeRules } from '../../profileUtil.js'; import { config as baseConfig, @@ -6,7 +7,7 @@ import { } from './recommendation-base.js'; export const name = 'DISC'; -export const config = { +export const config: SpecberusConfig = { ...baseConfig, status: 'DISC', longStatus: 'Discontinued Draft', diff --git a/lib/profiles/TR/Recommendation/FPWD.js b/lib/profiles/TR/Recommendation/FPWD.ts similarity index 84% rename from lib/profiles/TR/Recommendation/FPWD.js rename to lib/profiles/TR/Recommendation/FPWD.ts index 61e6709e6..1088ff198 100644 --- a/lib/profiles/TR/Recommendation/FPWD.js +++ b/lib/profiles/TR/Recommendation/FPWD.ts @@ -1,4 +1,5 @@ import * as draftStability from '../../../rules/sotd/draft-stability.js'; +import type { SpecberusConfig } from '../../../types.js'; import { insertAfter, removeRules } from '../../profileUtil.js'; import { config as baseConfig, @@ -6,7 +7,7 @@ import { } from './recommendation-base.js'; export const name = 'FPWD'; -export const config = { +export const config: SpecberusConfig = { ...baseConfig, status: 'WD', longStatus: 'First Public Working Draft', diff --git a/lib/profiles/TR/Recommendation/REC-Echidna.js b/lib/profiles/TR/Recommendation/REC-Echidna.ts similarity index 100% rename from lib/profiles/TR/Recommendation/REC-Echidna.js rename to lib/profiles/TR/Recommendation/REC-Echidna.ts diff --git a/lib/profiles/TR/Recommendation/REC-RSCND.js b/lib/profiles/TR/Recommendation/REC-RSCND.ts similarity index 88% rename from lib/profiles/TR/Recommendation/REC-RSCND.js rename to lib/profiles/TR/Recommendation/REC-RSCND.ts index ed887eae5..5300a509b 100644 --- a/lib/profiles/TR/Recommendation/REC-RSCND.js +++ b/lib/profiles/TR/Recommendation/REC-RSCND.ts @@ -1,9 +1,10 @@ import * as obslRescind from '../../../rules/sotd/obsl-rescind.js'; +import type { SpecberusConfig } from '../../../types.js'; import { insertAfter, removeRules } from '../../profileUtil.js'; import { config as baseConfig, rules as baseRules } from './REC.js'; export const name = 'REC-RSCND'; -export const config = { +export const config: SpecberusConfig = { ...baseConfig, status: 'REC', longStatus: 'Rescinded Recommendation', diff --git a/lib/profiles/TR/Recommendation/REC.js b/lib/profiles/TR/Recommendation/REC.ts similarity index 91% rename from lib/profiles/TR/Recommendation/REC.js rename to lib/profiles/TR/Recommendation/REC.ts index 06add888b..cf619418a 100644 --- a/lib/profiles/TR/Recommendation/REC.js +++ b/lib/profiles/TR/Recommendation/REC.ts @@ -4,6 +4,7 @@ import * as deployment from '../../../rules/sotd/deployment.js'; import * as newFeatures from '../../../rules/sotd/new-features.js'; import * as recAddition from '../../../rules/sotd/rec-addition.js'; import * as recCommentEnd from '../../../rules/sotd/rec-comment-end.js'; +import type { SpecberusConfig } from '../../../types.js'; import { insertAfter, removeRules } from '../../profileUtil.js'; import { config as baseConfig, @@ -11,7 +12,7 @@ import { } from './recommendation-base.js'; export const name = 'REC'; -export const config = { +export const config: SpecberusConfig = { ...baseConfig, status: 'REC', longStatus: 'Recommendation', diff --git a/lib/profiles/TR/Recommendation/WD-Echidna.js b/lib/profiles/TR/Recommendation/WD-Echidna.ts similarity index 100% rename from lib/profiles/TR/Recommendation/WD-Echidna.js rename to lib/profiles/TR/Recommendation/WD-Echidna.ts diff --git a/lib/profiles/TR/Recommendation/WD.js b/lib/profiles/TR/Recommendation/WD.ts similarity index 81% rename from lib/profiles/TR/Recommendation/WD.js rename to lib/profiles/TR/Recommendation/WD.ts index 3c3ed7224..74dfd8fd4 100644 --- a/lib/profiles/TR/Recommendation/WD.js +++ b/lib/profiles/TR/Recommendation/WD.ts @@ -1,4 +1,5 @@ import * as draftStability from '../../../rules/sotd/draft-stability.js'; +import type { SpecberusConfig } from '../../../types.js'; import { insertAfter } from '../../profileUtil.js'; import { config as baseConfig, @@ -6,7 +7,7 @@ import { } from './recommendation-base.js'; export const name = 'WD'; -export const config = { +export const config: SpecberusConfig = { ...baseConfig, status: 'WD', longStatus: 'Working Draft', diff --git a/lib/profiles/TR/Recommendation/recommendation-base.js b/lib/profiles/TR/Recommendation/recommendation-base.ts similarity index 97% rename from lib/profiles/TR/Recommendation/recommendation-base.js rename to lib/profiles/TR/Recommendation/recommendation-base.ts index 268501810..784bb96c1 100644 --- a/lib/profiles/TR/Recommendation/recommendation-base.js +++ b/lib/profiles/TR/Recommendation/recommendation-base.ts @@ -5,7 +5,7 @@ import { rules as baseRules } from '../../TR.js'; export const config = { track: 'Recommendation', -}; +} as const; export const rules = insertAfter(baseRules, 'sotd.supersedable', [ diff, diff --git a/lib/profiles/TR/Registry/CRY.js b/lib/profiles/TR/Registry/CRY.ts similarity index 83% rename from lib/profiles/TR/Registry/CRY.js rename to lib/profiles/TR/Registry/CRY.ts index 3030bb2da..04af07ed4 100644 --- a/lib/profiles/TR/Registry/CRY.js +++ b/lib/profiles/TR/Registry/CRY.ts @@ -1,10 +1,11 @@ // customize rules import * as candidateReviewEnd from '../../../rules/sotd/candidate-review-end.js'; +import type { SpecberusConfig } from '../../../types.js'; import { insertAfter } from '../../profileUtil.js'; import { config as baseConfig, rules as baseRules } from './registry-base.js'; export const name = 'CRY'; -export const config = { +export const config: SpecberusConfig = { ...baseConfig, status: 'CRY', longStatus: 'Candidate Registry', diff --git a/lib/profiles/TR/Registry/CRYD.js b/lib/profiles/TR/Registry/CRYD.ts similarity index 82% rename from lib/profiles/TR/Registry/CRYD.js rename to lib/profiles/TR/Registry/CRYD.ts index 2f75af88b..d55dfd317 100644 --- a/lib/profiles/TR/Registry/CRYD.js +++ b/lib/profiles/TR/Registry/CRYD.ts @@ -1,9 +1,10 @@ import * as draftStability from '../../../rules/sotd/draft-stability.js'; +import type { SpecberusConfig } from '../../../types.js'; import { insertAfter } from '../../profileUtil.js'; import { config as baseConfig, rules as baseRules } from './registry-base.js'; export const name = 'CRYD'; -export const config = { +export const config: SpecberusConfig = { ...baseConfig, status: 'CRYD', longStatus: 'Candidate Registry', diff --git a/lib/profiles/TR/Registry/DRY-Echidna.js b/lib/profiles/TR/Registry/DRY-Echidna.ts similarity index 78% rename from lib/profiles/TR/Registry/DRY-Echidna.js rename to lib/profiles/TR/Registry/DRY-Echidna.ts index facf893b4..d91888536 100644 --- a/lib/profiles/TR/Registry/DRY-Echidna.js +++ b/lib/profiles/TR/Registry/DRY-Echidna.ts @@ -3,10 +3,10 @@ import * as todaysDate from '../../../rules/echidna/todays-date.js'; import * as delivererChange from '../../../rules/echidna/deliverer-change.js'; import { insertAfter } from '../../profileUtil.js'; -import { config as baseConfig, rules as baseRules } from './DRY.js'; +import { rules as baseRules } from './DRY.js'; export const name = 'DRY-Echidna'; -export const config = baseConfig; +export { config } from './DRY.js'; export const rules = insertAfter(baseRules, 'sotd.process-document', [ todaysDate, diff --git a/lib/profiles/TR/Registry/DRY.js b/lib/profiles/TR/Registry/DRY.ts similarity index 81% rename from lib/profiles/TR/Registry/DRY.js rename to lib/profiles/TR/Registry/DRY.ts index e3daa6d89..d3523b7cf 100644 --- a/lib/profiles/TR/Registry/DRY.js +++ b/lib/profiles/TR/Registry/DRY.ts @@ -1,9 +1,10 @@ import * as draftStability from '../../../rules/sotd/draft-stability.js'; +import type { SpecberusConfig } from '../../../types.js'; import { insertAfter } from '../../profileUtil.js'; import { config as baseConfig, rules as baseRules } from './registry-base.js'; export const name = 'DRY'; -export const config = { +export const config: SpecberusConfig = { ...baseConfig, status: 'DRY', longStatus: 'Registry Draft', diff --git a/lib/profiles/TR/Registry/RY.js b/lib/profiles/TR/Registry/RY.ts similarity index 79% rename from lib/profiles/TR/Registry/RY.js rename to lib/profiles/TR/Registry/RY.ts index 9495a6d1c..3be4c187f 100644 --- a/lib/profiles/TR/Registry/RY.js +++ b/lib/profiles/TR/Registry/RY.ts @@ -1,9 +1,10 @@ import * as usage from '../../../rules/sotd/usage.js'; +import type { SpecberusConfig } from '../../../types.js'; import { insertAfter } from '../../profileUtil.js'; import { config as baseConfig, rules as baseRules } from './registry-base.js'; export const name = 'RY'; -export const config = { +export const config: SpecberusConfig = { ...baseConfig, status: 'RY', longStatus: 'Registry', diff --git a/lib/profiles/TR/Registry/registry-base.js b/lib/profiles/TR/Registry/registry-base.ts similarity index 87% rename from lib/profiles/TR/Registry/registry-base.js rename to lib/profiles/TR/Registry/registry-base.ts index 5fde85a8c..9baef0762 100644 --- a/lib/profiles/TR/Registry/registry-base.js +++ b/lib/profiles/TR/Registry/registry-base.ts @@ -1,5 +1,5 @@ export const config = { track: 'Registry', -}; +} as const; export { rules } from '../../TR.js'; diff --git a/lib/profiles/additionalMetadata.js b/lib/profiles/additionalMetadata.ts similarity index 100% rename from lib/profiles/additionalMetadata.js rename to lib/profiles/additionalMetadata.ts diff --git a/lib/profiles/base.js b/lib/profiles/base.ts similarity index 100% rename from lib/profiles/base.js rename to lib/profiles/base.ts diff --git a/lib/profiles/metadata.js b/lib/profiles/metadata.ts similarity index 100% rename from lib/profiles/metadata.js rename to lib/profiles/metadata.ts diff --git a/lib/profiles/profileUtil.js b/lib/profiles/profileUtil.js deleted file mode 100644 index 0e5608b46..000000000 --- a/lib/profiles/profileUtil.js +++ /dev/null @@ -1,45 +0,0 @@ -// take an array of rules, return a copy with new rules added after a given named anchor -/** - * @param original - * @param anchor - * @param rules - */ -export function insertAfter(original, anchor, rules) { - rules = Array.isArray(rules) ? rules : [rules]; - original = original.slice(); - const index = original.map(r => r.name).indexOf(anchor); - if (index < 0) return original.concat(rules); - - rules.forEach(r => { - // see if the rule to be inserted already exists. - const rIndex = original.map(rule => rule.name).indexOf(r.name); - if (rIndex < 0) original.splice(index, 0, r); - else - console.error( - `[EXCEPTION] Error when insertAfter, ${r.name} already exists at position ${rIndex} of the original list` - ); - }); - return original; -} - -// take an array of rules, return a copy with specified rules removed -// e.g. profileUtil.removeRules(rules, "sotd.pp") -/** - * @param original - * @param rules - */ -export function removeRules(original, rules) { - rules = Array.isArray(rules) ? rules : [rules]; - original = original.slice(); - rules.forEach(r => { - const index = original.map(rule => rule.name).indexOf(r); - if (index >= 0) { - original.splice(index, 1); - } else { - console.error( - `[EXCEPTION] Error when removeRules, ${r} doesn't exist in the original list` - ); - } - }); - return original; -} diff --git a/lib/profiles/profileUtil.ts b/lib/profiles/profileUtil.ts new file mode 100644 index 000000000..8645da3cf --- /dev/null +++ b/lib/profiles/profileUtil.ts @@ -0,0 +1,52 @@ +import type { RuleModule } from '../types.js'; + +/** + * Takes an array of rules, returns a copy with new rules added after a given named anchor + */ +export function insertAfter( + original: RuleModule[], + anchor: string, + newRules: RuleModule | RuleModule[] +) { + newRules = Array.isArray(newRules) ? newRules : [newRules]; + original = original.slice(); + const index = original.map(r => r.name).indexOf(anchor); + if (index < 0) return original.concat(newRules); + + newRules.forEach(newRule => { + // see if the rule to be inserted already exists. + const rIndex = original.map(rule => rule.name).indexOf(newRule.name); + if (rIndex < 0) original.splice(index, 0, newRule); + else + console.error( + `[EXCEPTION] Error when insertAfter, ${newRule.name} already exists at position ${rIndex} of the original list` + ); + }); + return original; +} + +/** + * Takes an array of rules, returns a copy with specified rules removed + * e.g. profileUtil.removeRules(rules, "sotd.pp") + * + * @param original + * @param ruleNames + */ +export function removeRules( + original: RuleModule[], + ruleNames: string | string[] +) { + ruleNames = Array.isArray(ruleNames) ? ruleNames : [ruleNames]; + original = original.slice(); + ruleNames.forEach(name => { + const index = original.map(rule => rule.name).indexOf(name); + if (index >= 0) { + original.splice(index, 1); + } else { + console.error( + `[EXCEPTION] Error when removeRules, ${name} doesn't exist in the original list` + ); + } + }); + return original; +} diff --git a/lib/rules/echidna/deliverer-change.js b/lib/rules/echidna/deliverer-change.js deleted file mode 100644 index 902e514f1..000000000 --- a/lib/rules/echidna/deliverer-change.js +++ /dev/null @@ -1,59 +0,0 @@ -import w3cApi from 'node-w3capi'; - -const self = { - name: 'echidna.deliverer-change', - section: 'metadata', - rule: 'delivererID', -}; - -export const { name } = self; - -function getPreviousDelivererIDs(shortname, previousUrl) { - const date = previousUrl.match(/-(\d{8})\/$/); - if (date) - return new Promise(resolve => { - // e.g. https://api.w3.org/specifications/WGSL/versions/20230221/deliverers - w3cApi - .specification(shortname) - .version(date[1]) - .deliverers() - .fetch({ embed: true }, (err, data) => { - resolve(data && data.map(obj => obj.id)); - }); - }); - return new Promise(resolve => { - resolve([]); - }); -} - -/** - * @param sr - * @param done - */ -export async function check(sr, done) { - const previousVersion = await sr.getPreviousVersion(sr); - const shortname = await sr.getShortname(sr); - - if (!previousVersion) { - return done(); - } - - const previousDelivererIDs = await getPreviousDelivererIDs( - shortname, - previousVersion - ); - const delivererIDs = await sr.getDelivererIDs(); - - const delivererChanged = - delivererIDs.sort().toString() !== - previousDelivererIDs.sort().toString(); - - if (delivererChanged) { - sr.error(self, 'deliverer-changed', { - this: delivererIDs.sort().toString(), - previous: previousDelivererIDs.sort().toString(), - }); - } - - done(); -} diff --git a/lib/rules/echidna/deliverer-change.ts b/lib/rules/echidna/deliverer-change.ts new file mode 100644 index 000000000..74e41d9d4 --- /dev/null +++ b/lib/rules/echidna/deliverer-change.ts @@ -0,0 +1,60 @@ +// @ts-ignore (no typings) +import w3cApi from 'node-w3capi'; +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; + +const self: RuleMeta = { + name: 'echidna.deliverer-change', + section: 'metadata', + rule: 'delivererID', +}; + +export const { name } = self; + +async function getPreviousDelivererIDs( + shortname: string, + previousUrl: string +): Promise { + const date = previousUrl.match(/-(\d{8})\/$/); + if (!date) return Promise.resolve([]); + + // e.g. https://api.w3.org/specifications/WGSL/versions/20230221/deliverers + const data: { id: number }[] = await w3cApi + .specification(shortname) + .version(date[1]) + .deliverers() + .fetch({ embed: true }) + .catch(() => []); + return data.map(({ id }) => id); +} + +/** + * @param sr + * @param done + */ +export const check: RuleCheckFunction = async (sr, done) => { + const previousVersion = await sr.getPreviousVersion(); + const shortname = await sr.getShortname(); + + if (!previousVersion || !shortname) { + return done(); + } + + const previousDelivererIDs = await getPreviousDelivererIDs( + shortname, + previousVersion + ); + const delivererIDs = await sr.getDelivererIDs(); + + const delivererChanged = + delivererIDs.sort().toString() !== + previousDelivererIDs.sort().toString(); + + if (delivererChanged) { + sr.error(self, 'deliverer-changed', { + this: delivererIDs.sort().toString(), + previous: previousDelivererIDs.sort().toString(), + }); + } + + done(); +}; diff --git a/lib/rules/echidna/todays-date.js b/lib/rules/echidna/todays-date.ts similarity index 66% rename from lib/rules/echidna/todays-date.js rename to lib/rules/echidna/todays-date.ts index 8404f3f7d..c51027ece 100644 --- a/lib/rules/echidna/todays-date.js +++ b/lib/rules/echidna/todays-date.ts @@ -1,4 +1,6 @@ -const self = { +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; + +const self: RuleMeta = { name: 'echidna.todays-date', section: 'front-matter', // @TODO: update this selector... when the rule is added to the JSON. @@ -7,11 +9,7 @@ const self = { export const { name } = self; -/** - * @param sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { /** * Get the timestamp of a day, regardless the time of the day. * This function creates a new `Date` to avoid modifying the original one. @@ -19,15 +17,16 @@ export function check(sr, done) { * @param {Date} date - The date to extract the day, month and year from * @returns {number} number of milliseconds between 1 January 1970 and `date` */ - function getDateTime(date) { + function getDateTime(date: Date) { return new Date(date.getTime()).setHours(0, 0, 0, 0); } - if (!(sr.getDocumentDate() instanceof Date)) { + const documentDate = sr.getDocumentDate(); + if (!(documentDate instanceof Date)) { sr.error(self, 'date-not-detected'); - } else if (getDateTime(sr.getDocumentDate()) !== getDateTime(new Date())) { + } else if (getDateTime(documentDate) !== getDateTime(new Date())) { sr.error(self, 'wrong-date'); } done(); -} +}; diff --git a/lib/rules/headers/copyright.js b/lib/rules/headers/copyright.ts similarity index 77% rename from lib/rules/headers/copyright.js rename to lib/rules/headers/copyright.ts index 3e6139396..d85dd5f16 100644 --- a/lib/rules/headers/copyright.js +++ b/lib/rules/headers/copyright.ts @@ -7,13 +7,16 @@ * 4. For "copyright-software", the url is https://www.w3.org/copyright/software-license/, the dated url is https://www.w3.org/copyright/software-license-2023/, they are both allowed. The name in the API is "W3C Software and Document License", but the document would use text "permissive document license". * 5. For "copyright-documents", the url is https://www.w3.org/document/document-license/, the dated url is https://www.w3.org/copyright/document-license-2023/. The name in the API is "W3C Document License", but the document would use text "document use". */ -import { AB, TAG, importJSON } from '../../util.js'; +import type { Cheerio } from 'cheerio'; +import type { Element } from 'domhandler'; -/** @import { Cheerio } from "cheerio" */ -/** @import { Element } from "domhandler" */ -/** @import { Specberus } from "../../validator.js" */ +import { AB, TAG } from '../../util.js'; +import type { Specberus } from '../../validator.js'; -const self = { +import copyrightExceptions from '../../copyright-exceptions.json' with { type: 'json' }; +import type { ApiCharter, RuleCheckFunction, RuleMeta } from '../../types.js'; + +const self: RuleMeta = { name: 'headers.copyright', section: 'front-matter', rule: 'copyright', @@ -30,12 +33,12 @@ const LICENSE_CD_URL = 'https://www.w3.org/copyright/document-license/'; const LICENSE_CD_DATED_URL = 'https://www.w3.org/copyright/document-license-2023/'; -const LICENSE_URL_TEXT_MAP = { +const LICENSE_URL_TEXT_MAP: Record = { [LICENSE_CD_URL]: LICENSE_CD_TEXT, [LICENSE_CS_URL]: LICENSE_CS_TEXT, }; -const LICENSE_TEXT_LINKS_MAP = { +const LICENSE_TEXT_LINKS_MAP: Record = { [LICENSE_CD_TEXT]: [LICENSE_CD_URL, LICENSE_CD_DATED_URL], [LICENSE_CS_TEXT]: [LICENSE_CS_URL, LICENSE_CS_DATED_URL], }; @@ -47,58 +50,63 @@ const latestBaseLinks = { trademark: 'https://www.w3.org/policies/#W3C_Trademarks', }; -const copyrightExceptions = importJSON( - '../../copyright-exceptions.json', - import.meta.url -); - export const { name } = self; -/** - * @param {Specberus} sr - */ -async function isOnlyPublishedByTagOrAb(sr) { - const TagID = TAG.id; - const AbID = AB.id; - +async function isOnlyPublishedByTagOrAb(sr: Specberus) { const delivererIDs = await sr.getDelivererIDs(); - return delivererIDs.every(id => [TagID, AbID].includes(id)); + return delivererIDs.every(id => id === TAG.id || id === AB.id); } // Create an array of license URIs per group -> array of arrays // Then find the intersection of all arrays, to extract union license(s) -function getCommonLicenseUri(data) { +function getCommonLicenseUri(data: ApiCharter[]) { return data .map(charter => charter['doc-licenses'].map(license => license.uri)) .reduce((first, cur) => first.filter(uri => cur.includes(uri))); } // The date can be 19xx-2023, or 2023. -/** - * @param {Specberus} sr - * @param licenseTexts - */ -function getLatestCopyrightMatchRegex(sr, licenseTexts) { +function getLatestCopyrightMatchRegex(sr: Specberus, licenseTexts: string[]) { const licenseRex = licenseTexts.join('|'); const year = (sr.getDocumentDate() || new Date()).getFullYear(); const startRex = '^Copyright [©|©] (?:(?:199\\d|20\\d\\d)-)?@YEAR *World Wide Web Consortium'.replace( '@YEAR', - year + '' + year ); const endRex = `\\. W3C[®|®] liability, trademark,? and (${licenseRex}) rules apply\\.$`; return new RegExp(startRex + endRex); } -/** - * @param {Specberus} sr - * @param {Cheerio} $copyright - * @param licenseTexts - * @param baseLinks - * @param matchRegex - */ -function checkCopyright(sr, $copyright, licenseTexts, baseLinks, matchRegex) { +// Some documents like epub-33 uses special copyrights listed in copyright-exception.json +function checkSpecialCopyright( + sr: Specberus, + $copyright: Cheerio, + specialCopyright: (typeof copyrightExceptions)[number], + shortname: string | undefined +) { + const year = (sr.getDocumentDate() || new Date()).getFullYear(); + + const domHtml = sr.norm($copyright.html()!); + const specHtml = sr.norm( + specialCopyright.copyright.replace(/@YEAR/g, '' + year) + ); + if (domHtml !== specHtml) { + sr.error(self, 'exception-no-html', { + copyright: domHtml, + expected: specHtml, + shortname, + }); + } +} + +function checkLatestCopyright( + sr: Specberus, + $copyright: Cheerio, + licenseTexts: string[] +) { + const matchRegex = getLatestCopyrightMatchRegex(sr, licenseTexts); const regResult = sr.norm($copyright.text()).match(matchRegex); if (!regResult) { sr.error(self, 'no-match', { rex: matchRegex }); @@ -108,9 +116,9 @@ function checkCopyright(sr, $copyright, licenseTexts, baseLinks, matchRegex) { const [, licenseInDoc] = regResult; const allowBothLicense = licenseTexts.length === 2; const textsToCheck = allowBothLicense ? [licenseInDoc] : licenseTexts; // When document can have 2 licenses, keep the one mentioned in document. - const linksToCheck = textsToCheck.reduce( + const linksToCheck: Record = textsToCheck.reduce( (acc, v) => ({ ...acc, [v]: LICENSE_TEXT_LINKS_MAP[v] }), - baseLinks + latestBaseLinks ); const links = $copyright.find('a').toArray(); @@ -140,44 +148,7 @@ function checkCopyright(sr, $copyright, licenseTexts, baseLinks, matchRegex) { }); } -// Some documents like epub-33 uses special copyrights listed in copyright-exception.json -/** - * @param {Specberus} sr - * @param {Cheerio} $copyright - * @param specialCopyright - * @param shortname - */ -function checkSpecialCopyright(sr, $copyright, specialCopyright, shortname) { - const year = (sr.getDocumentDate() || new Date()).getFullYear(); - - const domHtml = sr.norm($copyright.html()); - const specHtml = sr.norm( - specialCopyright.copyright.replace(/@YEAR/g, year) - ); - if (domHtml !== specHtml) { - sr.error(self, 'exception-no-html', { - copyright: domHtml, - expected: specHtml, - shortname, - }); - } -} - -/** - * @param {Specberus} sr - * @param {Cheerio} $copyright - * @param licenseTexts - */ -function checkLatestCopyright(sr, $copyright, licenseTexts) { - const matchRegex = getLatestCopyrightMatchRegex(sr, licenseTexts); - checkCopyright(sr, $copyright, licenseTexts, latestBaseLinks, matchRegex); -} - -/** - * @param {Specberus} sr - * @param done - */ -export async function check(sr, done) { +export const check: RuleCheckFunction = async (sr, done) => { const $copyright = sr.$('body div.head p.copyright').first(); if (!$copyright.length) { @@ -211,8 +182,8 @@ export async function check(sr, done) { // get exception rule for certain shortnames const shortname = await sr.getShortname(); - const specialCopyright = copyrightExceptions.find(({ specShortnames }) => - specShortnames.includes(shortname) + const specialCopyright = copyrightExceptions.find( + ({ specShortnames }) => shortname && specShortnames.includes(shortname) ); if (specialCopyright) { @@ -222,4 +193,4 @@ export async function check(sr, done) { } return done(); -} +}; diff --git a/lib/rules/headers/details-summary.js b/lib/rules/headers/details-summary.ts similarity index 85% rename from lib/rules/headers/details-summary.js rename to lib/rules/headers/details-summary.ts index 6d61f8029..721b47c03 100644 --- a/lib/rules/headers/details-summary.js +++ b/lib/rules/headers/details-summary.ts @@ -1,8 +1,8 @@ /* check if the document's headers link are in */ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'headers.details-summary', section: 'front-matter', rule: 'docIDFormat', @@ -10,11 +10,7 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { const $details = sr.$('.head details').first(); if (!$details.length) { sr.error(self, 'no-details'); @@ -42,4 +38,4 @@ export function check(sr, done) { } return done(); -} +}; diff --git a/lib/rules/headers/div-head.js b/lib/rules/headers/div-head.ts similarity index 55% rename from lib/rules/headers/div-head.js rename to lib/rules/headers/div-head.ts index edd542c97..70bf18c4a 100644 --- a/lib/rules/headers/div-head.js +++ b/lib/rules/headers/div-head.ts @@ -1,8 +1,8 @@ /* emits: 'not-found' */ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'headers.div-head', section: 'front-matter', rule: 'divClassHead', @@ -10,10 +10,6 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { sr.checkSelector('body div.head', self, done); -} +}; diff --git a/lib/rules/headers/dl.js b/lib/rules/headers/dl.ts similarity index 85% rename from lib/rules/headers/dl.js rename to lib/rules/headers/dl.ts index 4f627d142..fa0a2ad7b 100644 --- a/lib/rules/headers/dl.js +++ b/lib/rules/headers/dl.ts @@ -5,72 +5,75 @@ // #w3c-state date and this version date must match // dt for editor or author -import { resolveGithubUsernameToId } from '../../util.js'; +import type { Cheerio } from 'cheerio'; +import type { Element } from 'domhandler'; -/** @import { Cheerio } from "cheerio" */ -/** @import { Element } from "domhandler" */ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; +import { resolveGithubUsernameToId } from '../../util.js'; +import type { Specberus } from '../../validator.js'; -const self = { +const self: RuleMeta = { name: 'headers.dl', section: 'front-matter', rule: 'docIDFormat', }; -const thisError = { +const thisError: RuleMeta = { name: 'headers.dl', section: 'front-matter', rule: 'docIDThisVersion', }; -const latestError = { +const latestError: RuleMeta = { name: 'headers.dl', section: 'front-matter', rule: 'docIDLatestVersion', }; -const historyError = { +const historyError: RuleMeta = { name: 'headers.dl', section: 'front-matter', rule: 'docIDHistory', }; -const editorError = { +const editorError: RuleMeta = { name: 'headers.dl', section: 'front-matter', rule: 'editorSection', }; export const name = self.name; +interface CheckLinkOptions { + $element: Cheerio; + linkName: string; + mustHave?: boolean; + rule?: RuleMeta; + sr: Specberus; +} + /** * Check if link and href are consistent. - * @param {Object} object place to find , current error message title when there is error. - * @param {string} object.linkName - * @param {boolean=} object.mustHave - * @param {Object=} object.rule - * @param {Specberus} object.sr - * @param {Cheerio} object.$element * @returns boolean whether element exists and can continue */ -function checkLink({ sr, rule = self, $element, linkName, mustHave = true }) { +function checkLink({ + sr, + rule = self, + $element, + linkName, + mustHave = true, +}: CheckLinkOptions) { if (!$element?.length || !$element.attr('href')) { if (mustHave) sr.error(rule, 'not-found', { linkName, message: linkName }); return false; } const text = sr.norm($element.text()).trim(); - const href = $element.attr('href').trim(); + const href = ($element.attr('href') || '').trim(); if (href !== text) sr.error(rule, 'link-diff', { text, href, linkName }); return true; } -/** - * @param {Specberus} sr - * @param done - */ -export async function check(sr, done) { - const { rescinds } = sr.config; - const subType = sr.config.submissionType; +export const check: RuleCheckFunction = async (sr, done) => { + const { rescinds, status, submissionType } = sr.config!; let topLevel = 'TR'; - let thisURI = ''; - if (subType === 'member') topLevel = 'submissions'; + if (submissionType === 'member') topLevel = 'submissions'; const dts = sr.extractHeaders(); if (!dts.This) sr.error(self, 'this-version'); @@ -98,14 +101,13 @@ export async function check(sr, done) { if (exist) { const vThisRex = `^https:\\/\\/www\\.w3\\.org\\/${topLevel}\\/(\\d{4})\\/${ - sr.config.status || '[A-Z]+' + status || '[A-Z]+' }-(.+)-(\\d{4})(\\d\\d)(\\d\\d)\\/?$`; matches = ($linkThis.attr('href') || '') .trim() .match(new RegExp(vThisRex)); const docDate = sr.getDocumentDate(); if (matches) { - [thisURI] = matches; const year = +matches[1]; const year2 = +matches[3]; const month = +matches[4]; @@ -178,11 +180,11 @@ export async function check(sr, done) { // check "Implementation report" link. Unless in Sotd saying there's none. const needImplementation = - ['CR', 'CRD', 'PR', 'REC'].indexOf(sr.config.status) !== -1; + ['CR', 'CRD', 'PR', 'REC'].indexOf(status) !== -1; const $sotd = sr.getSotDSection(); const noImplementation = sr - .norm($sotd && $sotd.text()) + .norm(($sotd && $sotd.text()) || '') .indexOf('There is no preliminary implementation report.') > -1; const $linkImplementation = dts.Implementation && dts.Implementation.$dd.find('a').first(); @@ -195,14 +197,13 @@ export async function check(sr, done) { }); if ( implementationExist && - !$linkImplementation - .attr('href') + !($linkImplementation.attr('href') || '') .trim() .toLowerCase() .startsWith('https://') ) { sr.error(self, 'implelink-should-be-https', { - link: $linkImplementation.attr('href'), + link: $linkImplementation.attr('href') || '', }); } if (noImplementation && needImplementation) { @@ -219,7 +220,7 @@ export async function check(sr, done) { linkName: 'Implementation report', }); if (exist) { - const editorsDraft = $editorsDraftElement.attr('href'); + const editorsDraft = $editorsDraftElement.attr('href') || ''; if (!editorsDraft.trim().toLowerCase().startsWith('https://')) sr.error(self, 'editors-draft-should-be-https', { link: editorsDraft, @@ -250,8 +251,7 @@ export async function check(sr, done) { .filter(el => el.attribs['data-editor-github']) .map(el => el.attribs['data-editor-github']); - /** @type string[] */ - const unresolvedUsernames = []; + const unresolvedUsernames: string[] = []; for (const username of githubUsernames) { try { if (!(await resolveGithubUsernameToId(username))) @@ -273,4 +273,4 @@ export async function check(sr, done) { } done(); -} +}; diff --git a/lib/rules/headers/editor-participation.js b/lib/rules/headers/editor-participation.ts similarity index 73% rename from lib/rules/headers/editor-participation.js rename to lib/rules/headers/editor-participation.ts index 1210d904c..ae3645f21 100644 --- a/lib/rules/headers/editor-participation.js +++ b/lib/rules/headers/editor-participation.ts @@ -1,20 +1,16 @@ +// @ts-ignore (No typings) import w3cApi from 'node-w3capi'; import { resolveGithubUsernameToId } from '../../util.js'; +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -/** @import { Specberus } from "../../validator.js" */ - -const self = { +const self: RuleMeta = { name: 'headers.editor-participation', section: 'front-matter', rule: 'editorSection', }; export const name = self.name; -/** - * @param {Specberus} sr - * @param done - */ -export async function check(sr, done) { +export const check: RuleCheckFunction = async (sr, done) => { const groups = await sr.getDelivererIDs(); const editors = sr.extractHeaders()?.Editor; const editorsToCheck = []; @@ -40,18 +36,13 @@ export async function check(sr, done) { } } - const groupUsersPromises = []; - groups.forEach(id => { - groupUsersPromises.push( - new Promise(resolve => - w3cApi - .group(id) - .users() - .fetch({ embed: true }, (err, data) => { - resolve(data.map(user => user.id)); - }) - ) - ); + const groupUsersPromises = groups.map(async id => { + const data: { id: number }[] = await w3cApi + .group(id) + .users() + .fetch({ embed: true }) + .catch(() => []); + return data.map(({ id }) => id); }); const userIds = (await Promise.all(groupUsersPromises)).flat(); @@ -63,4 +54,4 @@ export async function check(sr, done) { }); done(); -} +}; diff --git a/lib/rules/headers/errata.js b/lib/rules/headers/errata.ts similarity index 64% rename from lib/rules/headers/errata.js rename to lib/rules/headers/errata.ts index a68157cf5..9c7ea70e2 100644 --- a/lib/rules/headers/errata.js +++ b/lib/rules/headers/errata.ts @@ -1,8 +1,9 @@ // errata, right after dl -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; +import type { Specberus } from '../../validator.js'; -const self = { +const self: RuleMeta = { name: 'headers.errata', section: 'front-matter', rule: 'docIDOrder', @@ -11,20 +12,14 @@ const self = { export const { name } = self; // Check if document is Recommendation, and uses inline changes(REC with Candidate/Proposed changes) -function isRECWithChanges(sr) { - if (sr.config.status !== 'REC') { - return false; - } +function isRECWithChanges(sr: Specberus) { + if (sr.config!.status !== 'REC') return false; - const recMeta = sr.getRecMetadata({}); + const recMeta = sr.getRecMetadata(); return Object.values(recMeta).length !== 0; } -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { // for REC with Candidate/Proposed changes, no need to check errata link if (isRECWithChanges(sr)) { return done(); @@ -37,4 +32,4 @@ export function check(sr, done) { return done(); } return done(); -} +}; diff --git a/lib/rules/headers/github-repo.js b/lib/rules/headers/github-repo.ts similarity index 85% rename from lib/rules/headers/github-repo.js rename to lib/rules/headers/github-repo.ts index 72b687121..d64a5dd0e 100644 --- a/lib/rules/headers/github-repo.js +++ b/lib/rules/headers/github-repo.ts @@ -2,9 +2,9 @@ // must include a public archived place to send comments to. // below: -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'headers.github-repo', section: 'front-matter', rule: 'docIDOrder', @@ -12,13 +12,8 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { const dts = sr.extractHeaders(); - // Check 'Feedback:' exist if (!dts.Feedback) { sr.error(self, 'no-feedback'); return done(); @@ -45,4 +40,4 @@ export function check(sr, done) { if (!foundRepo) sr.error(self, 'no-repo'); done(); -} +}; diff --git a/lib/rules/headers/h1-title.js b/lib/rules/headers/h1-title.ts similarity index 70% rename from lib/rules/headers/h1-title.js rename to lib/rules/headers/h1-title.ts index 97597e2c2..897c58060 100644 --- a/lib/rules/headers/h1-title.js +++ b/lib/rules/headers/h1-title.ts @@ -1,8 +1,8 @@ // must have h1, with same content as title -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'headers.h1-title', section: 'front-matter', rule: 'title', @@ -10,21 +10,17 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { const $title = sr.$('head > title').first(); const $h1 = sr.$('body div.head h1').first(); if (!$title.length || !$h1.length) { sr.error(self, 'not-found'); } else { const titleText = sr.norm($title.text()); - $h1.html($h1.html().replace(/:
/g, ': ').replace(/
/g, ' - ')); + $h1.html($h1.html()!.replace(/:
/g, ': ').replace(/
/g, ' - ')); const h1Text = sr.norm($h1.text()); if (titleText !== h1Text) sr.error(self, 'not-match', { titleText, h1Text }); } done(); -} +}; diff --git a/lib/rules/headers/h2-toc.js b/lib/rules/headers/h2-toc.ts similarity index 87% rename from lib/rules/headers/h2-toc.js rename to lib/rules/headers/h2-toc.ts index 2e643c213..ee157b843 100644 --- a/lib/rules/headers/h2-toc.js +++ b/lib/rules/headers/h2-toc.ts @@ -2,9 +2,9 @@ * Check the presence of a valid ToC. */ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'headers.h2-toc', // @TODO: fix the section... when it is fixed in the JSON. section: 'navigation', @@ -14,11 +14,7 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { const EXPECTED_HEADING = /^table\s+of\s+contents$/i; const $tocNav = sr.$('nav#toc > h2'); const $tocDiv = sr.$('div#toc > h2'); @@ -42,4 +38,4 @@ export function check(sr, done) { } done(); -} +}; diff --git a/lib/rules/headers/hr.js b/lib/rules/headers/hr.ts similarity index 74% rename from lib/rules/headers/hr.js rename to lib/rules/headers/hr.ts index b807e394d..13d00527f 100644 --- a/lib/rules/headers/hr.js +++ b/lib/rules/headers/hr.ts @@ -1,6 +1,6 @@ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'headers.hr', section: 'front-matter', rule: 'hrAfterCopyright', @@ -8,11 +8,7 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { const hasHrLastChild = sr.$('body div.head > hr:last-child').length === 1; const hasHrNextSibling = sr.$('body div.head + hr').length === 1; if (hasHrLastChild && hasHrNextSibling) { @@ -21,4 +17,4 @@ export function check(sr, done) { sr.error(self, 'not-found'); } done(); -} +}; diff --git a/lib/rules/headers/logo.js b/lib/rules/headers/logo.ts similarity index 56% rename from lib/rules/headers/logo.js rename to lib/rules/headers/logo.ts index 9874b5f87..2ee978674 100644 --- a/lib/rules/headers/logo.js +++ b/lib/rules/headers/logo.ts @@ -1,6 +1,6 @@ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'headers.logo', section: 'front-matter', rule: 'logo', @@ -8,20 +8,18 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { const $logo = sr.$("body div.head a[href] > img[src][alt='W3C']").first(); if ( !$logo.length || !/^(https:)?\/\/www\.w3\.org\/StyleSheets\/TR\/2021\/logos\/W3C?$/.test( - $logo.attr('src') + $logo.attr('src') || '' ) || - !/^(https:)?\/\/www\.w3\.org\/?$/.test($logo.parent().attr('href')) + !/^(https:)?\/\/www\.w3\.org\/?$/.test( + $logo.parent().attr('href') || '' + ) ) { sr.error(self, 'not-found'); } done(); -} +}; diff --git a/lib/rules/headers/memsub-copyright.js b/lib/rules/headers/memsub-copyright.ts similarity index 82% rename from lib/rules/headers/memsub-copyright.js rename to lib/rules/headers/memsub-copyright.ts index ec4192498..0016b32d1 100644 --- a/lib/rules/headers/memsub-copyright.js +++ b/lib/rules/headers/memsub-copyright.ts @@ -1,4 +1,4 @@ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction } from '../../types.js'; const self = { name: 'headers.memsub-copyright', @@ -6,11 +6,7 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { const $copyright = sr.$('body div.head p.copyright').first(); if ($copyright.length) { // , "https://www.w3.org/copyright/document-license/": "document use" @@ -26,4 +22,4 @@ export function check(sr, done) { if (!seen) sr.error(self, 'not-found'); } else sr.error(self, 'not-found'); done(); -} +}; diff --git a/lib/rules/headers/ol-toc.js b/lib/rules/headers/ol-toc.ts similarity index 73% rename from lib/rules/headers/ol-toc.js rename to lib/rules/headers/ol-toc.ts index 7f9680fc8..83a4ea699 100644 --- a/lib/rules/headers/ol-toc.js +++ b/lib/rules/headers/ol-toc.ts @@ -2,9 +2,9 @@ * Check the presence of the actual TOC (<ol class="toc">) inside the main navigation element. */ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'headers.ol-toc', section: 'navigation', // @TODO: update this selector... when the rule is added to the JSON. @@ -13,14 +13,10 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { const $toc = sr.$('nav#toc ol.toc, div#toc ol.toc'); if (!$toc.length) sr.warning(self, 'not-found'); done(); -} +}; diff --git a/lib/rules/headers/secno.js b/lib/rules/headers/secno.ts similarity index 81% rename from lib/rules/headers/secno.js rename to lib/rules/headers/secno.ts index 728451649..c5c32c2f9 100644 --- a/lib/rules/headers/secno.js +++ b/lib/rules/headers/secno.ts @@ -2,7 +2,7 @@ * Check (more or less) if items in the TOC and headings are wrapped inside <bdi class="secno">. */ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction } from '../../types.js'; const self = { name: 'headers.secno', @@ -10,11 +10,7 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { // TODO: once supported, use: ":is(h2, h3, h4, h5, h6) :is(bdi.secno,span.secno)" const $secnos = sr.$( 'h1 span.secno, h2 span.secno, h3 span.secno, h4 span.secno, h5 span.secno, h6 span.secno, #toc span.secno,' + @@ -24,4 +20,4 @@ export function check(sr, done) { if (!$secnos.length) sr.warning(self, 'not-found'); done(); -} +}; diff --git a/lib/rules/headers/shortname.js b/lib/rules/headers/shortname.ts similarity index 87% rename from lib/rules/headers/shortname.js rename to lib/rules/headers/shortname.ts index 6d2c1eb11..0c45d6322 100644 --- a/lib/rules/headers/shortname.js +++ b/lib/rules/headers/shortname.ts @@ -2,58 +2,50 @@ // this version is https://www.w3.org/TR/2013/WD-shortname-2013MMDD/ // latest version is https://www.w3.org/TR/shortname/ -/** @import { Specberus } from "../../validator.js" */ - import superagent from 'superagent'; +// TODO(kgf): Replace with promisify? +// @ts-ignore (no typings) import doAsync from 'doasync'; +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'headers.shortname', section: 'front-matter', rule: 'docIDFormat', }; -const thisError = { +const thisError: RuleMeta = { name: 'headers.shortname', section: 'front-matter', rule: 'docIDThisVersion', }; -const historyError = { +const historyError: RuleMeta = { name: 'headers.shortname', section: 'front-matter', rule: 'docIDHistory', }; export const name = self.name; -/** - * - * @param {Specberus} sr - * @param done - */ -export async function check(sr, done) { - const subType = sr.config.submissionType; +export const check: RuleCheckFunction = async (sr, done) => { let topLevel = 'TR'; - let thisURI = ''; - if (subType === 'member') topLevel = 'submissions'; + if (sr.config!.submissionType === 'member') topLevel = 'submissions'; const dts = sr.extractHeaders(); - let shortname; - let seriesShortname; - let matches; + let shortname = ''; + let seriesShortname = ''; if (dts.This) { const $linkThis = dts.This.$dd.find('a').first(); if ($linkThis.attr('href')) { const vThisRex = `^https:\\/\\/www\\.w3\\.org\\/${topLevel}\\/(\\d{4})\\/${ - sr.config.status || '[A-Z]+' + sr.config!.status || '[A-Z]+' }-(.+)-(\\d{4})(\\d\\d)(\\d\\d)\\/?$`; - matches = ($linkThis.attr('href') || '') + const matches = ($linkThis.attr('href') || '') .trim() .match(new RegExp(vThisRex)); if (matches) { - [thisURI] = matches; [, , shortname] = matches; const pattern = /-?\d[\d.]*$/; const dashPattern = /\d[\d.]*-/; @@ -82,7 +74,7 @@ export async function check(sr, done) { if ($linkLate.attr('href')) { const lateRex = `^https:\\/\\/www\\.w3\\.org\\/${topLevel}\\/(.+?)\\/?$`; - matches = ($linkLate.attr('href') || '') + const matches = ($linkLate.attr('href') || '') .trim() .match(new RegExp(lateRex)); if (matches) { @@ -104,7 +96,7 @@ export async function check(sr, done) { // e.g. https://www.w3.org/standards/history/hr-time-10086/ const historyReg = /^https:\/\/www\.w3\.org\/standards\/history\/(.+?)\/?$/; - matches = ($linkHistory.attr('href') || '') + const matches = ($linkHistory.attr('href') || '') .trim() .match(historyReg); if (matches) { @@ -166,7 +158,7 @@ export async function check(sr, done) { const $linkRescinds = dts.Rescinds.$dd.find('a').first(); if ($linkRescinds.attr('href')) { - matches = ($linkRescinds.attr('href') || '') + const matches = ($linkRescinds.attr('href') || '') .trim() .match( /^https:\/\/www\.w3\.org\/TR\/\d{4}\/REC-(.+)-\d{8}\/?$/ @@ -186,7 +178,7 @@ export async function check(sr, done) { const isFP = await sr.isFP(); // FP documents cannot use existing shortname if ( - sr.config.longStatus === 'First Public Working Draft' && + sr.config!.longStatus === 'First Public Working Draft' && !isFP && shortname ) { @@ -196,15 +188,15 @@ export async function check(sr, done) { // non-initial state documents should use existing shortname // TODO: Registry and Note track? if ( - sr.config.track === 'Recommendation' && - sr.config.longStatus !== 'First Public Working Draft' && + sr.config!.track === 'Recommendation' && + sr.config!.longStatus !== 'First Public Working Draft' && isFP && shortname ) { sr.error(self, 'shortname-not-existed', { - status: sr.config.longStatus, + status: sr.config!.longStatus, }); } done(); -} +}; diff --git a/lib/rules/headers/subm-logo.js b/lib/rules/headers/subm-logo.ts similarity index 67% rename from lib/rules/headers/subm-logo.js rename to lib/rules/headers/subm-logo.ts index 0c603b57e..4a6a4cbc8 100644 --- a/lib/rules/headers/subm-logo.js +++ b/lib/rules/headers/subm-logo.ts @@ -1,34 +1,32 @@ +import type { RuleCheckFunction } from '../../types.js'; + const self = { name: 'headers.subm-logo', }; export const { name } = self; -/** - * @param sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { const $logo = sr .$("body div.head a[href] > img[src][height='48'][width='211'][alt]") .first(); - const type = sr.config.submissionType || 'member'; + const type = sr.config!.submissionType || 'member'; const checks = { member: { alt: 'W3C Member Submission', src: /^(https:)?\/\/www\.w3\.org\/Icons\/member_subm(\.png|\.gif)?$/, href: /^(https:)?\/\/www\.w3\.org\/submissions\/?$/, }, - }; + } as const; if ( !$logo.length || $logo.attr('alt') !== checks[type].alt || - !checks[type].src.test($logo.attr('src')) || - !checks[type].href.test($logo.parent().attr('href')) + !checks[type].src.test($logo.attr('src') || '') || + !checks[type].href.test($logo.parent().attr('href') || '') ) { sr.error(self, 'not-found', { type: type.charAt(0).toUpperCase() + type.slice(1), }); } done(); -} +}; diff --git a/lib/rules/headers/translation.js b/lib/rules/headers/translation.ts similarity index 82% rename from lib/rules/headers/translation.js rename to lib/rules/headers/translation.ts index d79335705..6699d2363 100644 --- a/lib/rules/headers/translation.js +++ b/lib/rules/headers/translation.ts @@ -1,6 +1,6 @@ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'headers.translation', section: 'front-matter', rule: 'translation', @@ -8,11 +8,7 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { const translationLink = sr .$('body div.head a') .toArray() @@ -37,4 +33,4 @@ export function check(sr, done) { } done(); -} +}; diff --git a/lib/rules/headers/w3c-state.js b/lib/rules/headers/w3c-state.ts similarity index 64% rename from lib/rules/headers/w3c-state.js rename to lib/rules/headers/w3c-state.ts index 197a63d02..84180af4c 100644 --- a/lib/rules/headers/w3c-state.js +++ b/lib/rules/headers/w3c-state.ts @@ -1,8 +1,8 @@ -import { importJSON } from '../../util.js'; +import rules from '../../rules.json' with { type: 'json' }; +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; +import { isRuleTrack } from '../../util.js'; -const rules = importJSON('../../rules.json', import.meta.url); - -const self = { +const self: RuleMeta = { name: 'headers.w3c-state', section: 'front-matter', rule: 'dateState', @@ -10,14 +10,11 @@ const self = { export const { name } = self; -/** - * @param sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { + const config = sr.config!; let profileFound = false; - if (!sr.config.longStatus) return done(); + if (config.longStatus) return done(); const $stateEl = sr.getDocumentStateElement(); if (!$stateEl) { sr.error(self, 'no-w3c-state'); @@ -26,20 +23,17 @@ export function check(sr, done) { const txt = sr.norm($stateEl.text()); // crType/cryType: Add 'Draft', 'Snapshot' suffix to title. const docTitle = - sr.config.longStatus + - (sr.config.crType ? ` ${sr.config.crType}` : '') + - (sr.config.cryType ? ` ${sr.config.cryType}` : ''); + config.longStatus + + (config.crType ? ` ${config.crType}` : '') + + (config.cryType ? ` ${config.cryType}` : ''); if (!txt.startsWith(`W3C ${docTitle}`)) { sr.error(self, 'bad-w3c-state'); } for (const t in rules) - if (t !== '*' && !profileFound) - for (const p in rules[t].profiles) { - const rx = new RegExp( - `^w3c\\ ${rules[t].profiles[p].name}(\\ |,)`, - 'i' - ); + if (isRuleTrack(t) && !profileFound) + for (const profile of Object.values(rules[t].profiles)) { + const rx = new RegExp(`^w3c\\ ${profile.name}(\\ |,)`, 'i'); if (rx.test(txt)) { profileFound = true; break; @@ -50,9 +44,9 @@ export function check(sr, done) { else { // check the profile link const $standardLink = $stateEl.find('a').first(); - let hash = sr.config.status; + let hash = config.status; // mapping special hash. - if (sr.config.longStatus === 'First Public Working Draft') { + if (config.longStatus === 'First Public Working Draft') { hash = 'FPWD'; } const expectedLink = new RegExp( @@ -60,7 +54,7 @@ export function check(sr, done) { ); if (!$standardLink.length) { sr.error(self, 'no-w3c-state-link'); - } else if (!expectedLink.test($standardLink.attr('href'))) { + } else if (!expectedLink.test($standardLink.attr('href') || '')) { sr.error(self, 'wrong-w3c-state-link', { hash, linkFound: $standardLink.attr('href'), @@ -70,4 +64,4 @@ export function check(sr, done) { } done(); -} +}; diff --git a/lib/rules/heuristic/date-format.js b/lib/rules/heuristic/date-format.ts similarity index 77% rename from lib/rules/heuristic/date-format.js rename to lib/rules/heuristic/date-format.ts index ac8fa0bb6..0807d5f5e 100644 --- a/lib/rules/heuristic/date-format.js +++ b/lib/rules/heuristic/date-format.ts @@ -1,8 +1,7 @@ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; import { possibleMonths } from '../../validator.js'; -/** @import { Specberus } from "../../validator.js" */ - -const self = { +const self: RuleMeta = { name: 'heuristic.date-format', section: 'document-status', rule: 'datesFormat', @@ -10,11 +9,7 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { // Pseudo-constants: const POSSIBLE_DATE = new RegExp( `\\b([0123]?\\d|${possibleMonths})[\\ \\-\\/–—]([0123]?\\d|${possibleMonths})[\\ \\-\\/–—]([\\'‘’]?\\d{2})(\\d\\d)?\\b`, @@ -26,16 +21,15 @@ export function check(sr, done) { ); const boilerplateSections = [sr.$('div.head').first(), sr.getSotDSection()]; - const candidateDates = []; - let elem; + const candidateDates: string[] = []; for (const $section of boilerplateSections) { const text = $section && $section.text(); - elem = + const elem = text && text.replace(/(?:https?|ftp):\/\/\S+/gi, '').match(POSSIBLE_DATE); - for (const el of elem) { - candidateDates.push(el); + if (elem) { + for (const el of elem) candidateDates.push(el); } } @@ -46,4 +40,4 @@ export function check(sr, done) { } return done(); -} +}; diff --git a/lib/rules/links/compound.js b/lib/rules/links/compound.ts similarity index 83% rename from lib/rules/links/compound.js rename to lib/rules/links/compound.ts index fb91cfa12..b07455990 100644 --- a/lib/rules/links/compound.js +++ b/lib/rules/links/compound.ts @@ -1,7 +1,5 @@ -import url from 'url'; import { get } from '../../throttled-ua.js'; - -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction } from '../../types.js'; const self = { name: 'links.compound', @@ -10,23 +8,22 @@ const TIMEOUT = 10000; export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - if (sr.config.validation !== 'recursive') { +export const check: RuleCheckFunction = (sr, done) => { + const { validation } = sr.config!; + const url = sr.url!; + + if (validation !== 'recursive') { sr.warning(self, 'skipped'); return done(); } - let links = []; + let links: string[] = []; - if (sr.url) { + if (url) { sr.$('a[href]').each((_, el) => { - const u = new url.URL(el.attribs.href, sr.url); + const u = new URL(el.attribs.href, url); const l = u.origin + u.pathname; - if (l.startsWith(sr.url) && l !== sr.url) links.push(l); + if (l.startsWith(url) && l !== url) links.push(l); }); } // sort and remove duplicates @@ -36,7 +33,7 @@ export function check(sr, done) { let count = 0; if (links.length > 0) { links.forEach(l => { - if (sr.config.validation === 'recursive') { + if (validation === 'recursive') { const ua = `W3C-Pubrules/${sr.version}`; let isMarkupValid = false; const req = get(markupService) @@ -58,7 +55,7 @@ export function check(sr, done) { if (!json) return sr.throw('No JSON input.'); if (json.messages) { const errors = json.messages.filter( - msg => msg.type === 'error' + (msg: { type: string }) => msg.type === 'error' ); if (errors.length === 0) isMarkupValid = true; } @@ -87,4 +84,4 @@ export function check(sr, done) { } }); } else return done(); -} +}; diff --git a/lib/rules/links/internal.js b/lib/rules/links/internal.ts similarity index 74% rename from lib/rules/links/internal.js rename to lib/rules/links/internal.ts index 753b6ea45..89738b997 100644 --- a/lib/rules/links/internal.js +++ b/lib/rules/links/internal.ts @@ -1,6 +1,6 @@ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'links.internal', section: 'document-body', rule: 'brokenLink', @@ -8,11 +8,7 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { sr.$("a[href^='#']").each((_, el) => { const id = el.attribs.href.replace('#', ''); const escId = id.replace(/([.()#:[\]+*])/g, '\\$1'); @@ -22,4 +18,4 @@ export function check(sr, done) { } }); done(); -} +}; diff --git a/lib/rules/links/linkchecker.js b/lib/rules/links/linkchecker.ts similarity index 83% rename from lib/rules/links/linkchecker.js rename to lib/rules/links/linkchecker.ts index 5c5a5aba7..2ae6b8949 100644 --- a/lib/rules/links/linkchecker.js +++ b/lib/rules/links/linkchecker.ts @@ -1,13 +1,14 @@ // Check every sources(img, stylesheets, scripts) are in same folder as spec document, and they are reachable. import puppeteer from 'puppeteer'; +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'links.linkchecker', section: 'document-body', rule: 'brokenLink', }; -const compound = { +const compound: RuleMeta = { name: 'links.linkchecker', section: 'compound', rule: 'compoundFilesLocation', @@ -35,7 +36,7 @@ export const { name } = self; /** * @param url */ -function simplifyURL(url) { +function simplifyURL(url: string) { const urlObj = new URL(url); return ( (urlObj.origin !== 'null' ? urlObj.origin : urlObj.protocol) + @@ -44,36 +45,21 @@ function simplifyURL(url) { } /** - * Upgrade version of Array.include(). The array can be RegExp - * - * @param url - * @param regArray - * @returns {boolean} + * Upgraded version of Array.includes(), supporting both RegExp and exact string comparison. */ -function includedByReg(url, regArray = allowList) { - return regArray.some(item => { - if (typeof item === 'object') { - // item is RegExp - return item.test(url); - } - // item is simple string - return item === url; - }); +function includedByReg(url: string, regArray = allowList) { + return regArray.some(item => + item instanceof RegExp ? item.test(url) : item === url + ); } -/** - * @param sr - * @param done - */ -export async function check(sr, done) { +export const check: RuleCheckFunction = async (sr, done) => { // send out warning for /nu W3C link checker. sr.warning(self, 'display', { link: sr.url }); - if (!sr.url) { - return done(); - } + if (!sr.url) return done(); - // sr.url is used as base url. Every other resources should use in same folder as base. e.g. + // sr.url is used as base url. Every other resource should use in same folder as base. e.g. // - spec doc: https://www.w3.org/TR/2021/WD-pubrules-20210401/ // - image (pass): https://www.w3.org/TR/2021/WD-pubrules-20210401/images/sample.png // - image (pass): https://www.w3.org/TR/2021/WD-pubrules-20210401/sample.png @@ -128,4 +114,4 @@ export async function check(sr, done) { await browser.close(); done(); -} +}; diff --git a/lib/rules/links/reliability.js b/lib/rules/links/reliability.ts similarity index 84% rename from lib/rules/links/reliability.js rename to lib/rules/links/reliability.ts index 888ab1a9e..4c975f2b7 100644 --- a/lib/rules/links/reliability.js +++ b/lib/rules/links/reliability.ts @@ -1,4 +1,4 @@ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction } from '../../types.js'; const self = { name: 'links.reliability', @@ -20,16 +20,15 @@ const unreliableServices = [ // { domain: 'w3.org', path: /track(er)?\/(actions|issues|resolutions)/} ]; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { sr.$('a').each((_, el) => { const $el = sr.$(el); + const href = $el.attr('href'); + if (!href) return; + let url; try { - url = new URL($el.attr('href'), sr.url || 'https://www.w3.org'); + url = new URL(href, sr.url || 'https://www.w3.org'); } catch (e) { // when failed to parse URL, move on to next one. return; @@ -46,7 +45,7 @@ export function check(sr, done) { unreliableService.path.test(path))) ) { sr.warning(self, 'unreliable-link', { - link: $el.attr('href'), + link: href, text: sr.norm($el.text()), }); // when finding this URL unreliable, quit 'unreliableServices.some' @@ -56,4 +55,4 @@ export function check(sr, done) { }); }); done(); -} +}; diff --git a/lib/rules/metadata/abstract.js b/lib/rules/metadata/abstract.js deleted file mode 100644 index 9c8837acc..000000000 --- a/lib/rules/metadata/abstract.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Pseudo-rule for metadata extraction: abstract. - */ - -import { load } from 'cheerio'; - -/** @import { Cheerio } from "cheerio" */ -/** @import { Element } from "domhandler" */ -/** @import { Specberus } from "../../validator.js" */ - -export const name = 'metadata.abstract'; - -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - /** @type {Cheerio} */ - let $abstractTitle; - sr.$('h2').each((_, el) => { - const $el = sr.$(el); - if (sr.norm($el.text()).toLowerCase() === 'abstract') { - $abstractTitle = $el; - return false; - } - }); - - if ($abstractTitle) { - const $div = load('
', null, false)('div'); - $abstractTitle - .parent() - .children() - .each((_, child) => { - { - if (child !== $abstractTitle[0]) { - $div.append(child.cloneNode(true)); - } - } - }); - return done({ abstract: sr.norm($div.html()) }); - } else { - return done({ abstract: 'Not found' }); - } -} diff --git a/lib/rules/metadata/abstract.ts b/lib/rules/metadata/abstract.ts new file mode 100644 index 000000000..fdc08f97e --- /dev/null +++ b/lib/rules/metadata/abstract.ts @@ -0,0 +1,32 @@ +/** + * Pseudo-rule for metadata extraction: abstract. + */ + +import { load } from 'cheerio'; +import type { RuleCheckFunction } from '../../types.js'; + +export const name = 'metadata.abstract'; + +export const check: RuleCheckFunction<{ abstract: string }> = (sr, done) => { + const abstractHeadingEl = sr + .$('h2') + .toArray() + .find(el => sr.norm(sr.$(el).text()).toLowerCase() === 'abstract'); + + if (abstractHeadingEl) { + const $div = load('
', null, false)('div'); + sr.$(abstractHeadingEl) + .parent() + .children() + .each((_, child) => { + { + if (child !== abstractHeadingEl) { + $div.append(child.cloneNode(true)); + } + } + }); + return done({ abstract: sr.norm($div.html()!) }); + } else { + return done({ abstract: 'Not found' }); + } +}; diff --git a/lib/rules/metadata/charters.js b/lib/rules/metadata/charters.js deleted file mode 100644 index 3727c7b15..000000000 --- a/lib/rules/metadata/charters.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Pseudo-rule for metadata extraction: charters. - */ - -// 'self.name' would be 'metadata.charters' - -export const name = 'metadata.charters'; - -/** - * @param sr - * @param done - */ -export async function check(sr, done) { - const charters = await sr.getCharters(); - - return done({ charters }); -} diff --git a/lib/rules/metadata/charters.ts b/lib/rules/metadata/charters.ts new file mode 100644 index 000000000..290c7748f --- /dev/null +++ b/lib/rules/metadata/charters.ts @@ -0,0 +1,13 @@ +/** + * Pseudo-rule for metadata extraction: charters. + */ + +import type { RuleCheckFunction } from '../../types.js'; + +// 'self.name' would be 'metadata.charters' + +export const name = 'metadata.charters'; + +export const check: RuleCheckFunction<{ + charters: string[]; +}> = async (sr, done) => done({ charters: await sr.getCharters() }); diff --git a/lib/rules/metadata/deliverers.js b/lib/rules/metadata/deliverers.js deleted file mode 100644 index d6e8884f1..000000000 --- a/lib/rules/metadata/deliverers.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Pseudo-rule for metadata extraction: deliverers' IDs. - */ - -// 'self.name' would be 'metadata.deliverers' -export const name = 'metadata.deliverers'; - -/** - * @param sr - * @param done - */ -export async function check(sr, done) { - const ids = await sr.getDelivererIDs(); - - done({ delivererIDs: ids }); -} diff --git a/lib/rules/metadata/deliverers.ts b/lib/rules/metadata/deliverers.ts new file mode 100644 index 000000000..02355c41b --- /dev/null +++ b/lib/rules/metadata/deliverers.ts @@ -0,0 +1,16 @@ +/** + * Pseudo-rule for metadata extraction: deliverers' IDs. + */ + +import type { RuleCheckFunction } from '../../types.js'; + +// 'self.name' would be 'metadata.deliverers' +export const name = 'metadata.deliverers'; + +/** + * @param sr + * @param done + */ +export const check: RuleCheckFunction<{ + delivererIDs: number[]; +}> = async (sr, done) => done({ delivererIDs: await sr.getDelivererIDs() }); diff --git a/lib/rules/metadata/dl.js b/lib/rules/metadata/dl.ts similarity index 72% rename from lib/rules/metadata/dl.js rename to lib/rules/metadata/dl.ts index 3fc797bc2..7e3b08b19 100644 --- a/lib/rules/metadata/dl.js +++ b/lib/rules/metadata/dl.ts @@ -2,9 +2,8 @@ * Pseudo-rule for metadata extraction: dl. */ -/** @import { Specberus } from "../../validator.js" */ - import { get } from '../../throttled-ua.js'; +import type { RuleCheckFunction } from '../../types.js'; const latestRule = { name: 'metadata.dl', @@ -14,31 +13,36 @@ const latestRule = { export const name = 'metadata.dl'; -/** - * @param {Specberus} sr - * @param done - */ -export async function check(sr, done) { +interface DlMetadata { + editorsDraft?: string | undefined; + history?: string; + latestVersion?: string; + previousVersion?: string | undefined; + sameWorkAs?: string; + thisVersion?: string; + updated?: boolean; +} + +export const check: RuleCheckFunction = async (sr, done) => { const dts = sr.extractHeaders(); - const result = {}; + const result: DlMetadata = {}; let shortname; let previousShortname; let latestShortname; const $linkThis = dts.This ? dts.This.$dd.find('a').first() : null; - if ($linkThis) { - result.thisVersion = ($linkThis.attr('href') || '').trim(); + const thisHref = $linkThis?.attr('href')?.trim(); + if (thisHref) { + result.thisVersion = thisHref; shortname = await sr.getShortname(); } const $linkLate = dts.Latest ? dts.Latest.$dd.find('a').first() : null; - if ($linkLate) { + const latestHref = $linkLate?.attr('href')?.trim(); + if (latestHref) { // linkLate could be the series version, in which case, the metadata should list the shortlink instead. e.g. https://www.w3.org/TR/2021/WD-IndexedDB-3-20211006/ - result.latestVersion = $linkLate.attr('href').trim(); - const latestRegex = $linkLate - .attr('href') - .trim() - .match(/.*\/([^/]+)\/$/); + result.latestVersion = latestHref; + const latestRegex = latestHref.match(/.*\/([^/]+)\/$/); if (latestRegex && latestRegex.length > 0) { [, latestShortname] = latestRegex; if (latestShortname !== shortname) { @@ -48,9 +52,10 @@ export async function check(sr, done) { } const $linkHistory = dts.History ? dts.History.$dd.find('a').first() : null; + const historyHref = $linkHistory?.attr('href')?.trim(); - if ($linkHistory) { - result.history = $linkHistory.attr('href').trim(); + if ($linkHistory && historyHref) { + result.history = historyHref; result.previousVersion = await sr.getPreviousVersion(); previousShortname = $linkHistory.attr('data-previous-shortname'); } @@ -58,7 +63,7 @@ export async function check(sr, done) { const $linkEd = dts.EditorDraft ? dts.EditorDraft.$dd.find('a').first() : null; - if ($linkEd) result.editorsDraft = $linkEd.attr('href').trim(); + if ($linkEd) result.editorsDraft = $linkEd.attr('href')?.trim(); if (previousShortname && shortname && previousShortname !== shortname) { result.sameWorkAs = `https://www.w3.org/TR/${previousShortname}/`; @@ -85,4 +90,4 @@ export async function check(sr, done) { } else { sr.throw('[EXCEPTION] The document date could not be parsed.'); } -} +}; diff --git a/lib/rules/metadata/docDate.js b/lib/rules/metadata/docDate.js deleted file mode 100644 index 58565d89f..000000000 --- a/lib/rules/metadata/docDate.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Pseudo-rule for metadata extraction: docDate. - */ - -// 'self.name' would be 'metadata.docDate' -export const name = 'metadata.docDate'; - -/** - * @param sr - * @param done - */ -export function check(sr, done) { - const docDate = sr.getDocumentDate(); - if (!docDate) { - return done(); - } - const d = [ - docDate.getFullYear(), - docDate.getMonth() + 1, - docDate.getDate(), - ].join('-'); - return done({ docDate: d }); -} diff --git a/lib/rules/metadata/docDate.ts b/lib/rules/metadata/docDate.ts new file mode 100644 index 000000000..13fb1a153 --- /dev/null +++ b/lib/rules/metadata/docDate.ts @@ -0,0 +1,20 @@ +/** + * Pseudo-rule for metadata extraction: docDate. + */ + +import type { RuleCheckFunction } from '../../types.js'; + +// 'self.name' would be 'metadata.docDate' +export const name = 'metadata.docDate'; + +interface DocDateMetadata { + docDate: `${number}-${number}-${number}`; +} + +export const check: RuleCheckFunction = (sr, done) => { + const docDate = sr.getDocumentDate(); + if (!docDate) return done(); + return done({ + docDate: `${docDate.getFullYear()}-${docDate.getMonth() + 1}-${docDate.getDate()}`, + }); +}; diff --git a/lib/rules/metadata/editor-ids.js b/lib/rules/metadata/editor-ids.ts similarity index 81% rename from lib/rules/metadata/editor-ids.js rename to lib/rules/metadata/editor-ids.ts index baf9d0887..be969abbb 100644 --- a/lib/rules/metadata/editor-ids.js +++ b/lib/rules/metadata/editor-ids.ts @@ -2,11 +2,10 @@ * Pseudo-rule for metadata extraction: editor-ids. */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; import { resolveGithubUsernameToId } from '../../util.js'; -/** @import { Specberus } from "../../validator.js" */ - -const self = { +const self: RuleMeta = { name: 'metadata.editor-ids', section: 'front-matter', rule: 'editorSection', @@ -15,18 +14,18 @@ const self = { // 'self.name' would be 'metadata.editor-ids' export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export async function check(sr, done) { +interface EditorIDsMetadata { + editorIDs: number[]; +} + +export const check: RuleCheckFunction = async (sr, done) => { const dts = sr.extractHeaders(); - const editorIds = []; + const editorIds: number[] = []; const unresolvedUsernames = []; if (dts.Editor) { for (const el of dts.Editor.$dd.toArray()) { const editorId = el.attribs['data-editor-id']; - // Return ID as Number format, if the ID is not a digit-only string, it gets filtered out + // Return ID as number, if the ID is not a digit-only string, it gets filtered out if (editorId) editorIds.push(parseInt(editorId, 10)); else { const editorGithub = el.attribs['data-editor-github']; @@ -59,4 +58,4 @@ export async function check(sr, done) { } else { done({ editorIDs: [] }); } -} +}; diff --git a/lib/rules/metadata/editor-names.js b/lib/rules/metadata/editor-names.ts similarity index 58% rename from lib/rules/metadata/editor-names.js rename to lib/rules/metadata/editor-names.ts index 0d27c8069..1dd97fb04 100644 --- a/lib/rules/metadata/editor-names.js +++ b/lib/rules/metadata/editor-names.ts @@ -2,18 +2,18 @@ * Pseudo-rule for metadata extraction: editor-names. */ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction } from '../../types.js'; // 'self.name' would be 'metadata.editor-names' export const name = 'metadata.editor-names'; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +interface EditorNamesMetadata { + editorNames: string[]; +} + +export const check: RuleCheckFunction = (sr, done) => { const dts = sr.extractHeaders(); - const result = []; + const editorNames: string[] = []; if (dts.Editor) { dts.Editor.$dd.each((_, el) => { const editor = sr @@ -22,8 +22,8 @@ export function check(sr, done) { .trim() .replace(/[,(].*$/, '') .trim(); - if (editor) result.push(editor); + if (editor) editorNames.push(editor); }); } - return done({ editorNames: result }); -} + return done({ editorNames }); +}; diff --git a/lib/rules/metadata/errata.js b/lib/rules/metadata/errata.ts similarity index 70% rename from lib/rules/metadata/errata.js rename to lib/rules/metadata/errata.ts index e237b4540..3d7a527a4 100644 --- a/lib/rules/metadata/errata.js +++ b/lib/rules/metadata/errata.ts @@ -2,17 +2,17 @@ * Pseudo-rule for metadata extraction: errata. */ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction } from '../../types.js'; // 'self.name' would be 'metadata.errata' export const name = 'metadata.errata'; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +interface ErrataMetadata { + errata: string; +} + +export const check: RuleCheckFunction = (sr, done) => { const errataRegex = /errata/i; const $links = sr.$('body div.head details + p > a'); const errata = $links @@ -20,4 +20,4 @@ export function check(sr, done) { .filter(el => errataRegex.test(sr.$(el).text())); if (!errata.length || !errata[0].attribs.href) done(); else done({ errata: errata[0].attribs.href }); -} +}; diff --git a/lib/rules/metadata/informative.js b/lib/rules/metadata/informative.ts similarity index 60% rename from lib/rules/metadata/informative.js rename to lib/rules/metadata/informative.ts index d0e702a15..4414e5a35 100644 --- a/lib/rules/metadata/informative.js +++ b/lib/rules/metadata/informative.ts @@ -2,30 +2,28 @@ * Pseudo-rule for metadata extraction: informative. */ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction } from '../../types.js'; // 'self.name' would be 'metadata.informative' export const name = 'metadata.informative'; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +interface InformativeMetadata { + informative: boolean; +} + +export const check: RuleCheckFunction = ( + sr, + done +) => { const $sotd = sr.getSotDSection(); const expected = /This\s+document\s+is\s+informative\s+only\./; - let isInformative = false; - if (!$sotd) { - return done(); - } + if (!$sotd) return done(); const $stateEl = sr.getDocumentStateElement(); const candidate = $stateEl && sr.norm($stateEl.text()).toLowerCase(); - if (candidate && candidate.indexOf('group note') !== -1) { - isInformative = true; - } + const isInformative = !!candidate && candidate.indexOf('group note') !== -1; return done({ informative: expected.test($sotd && $sotd.text()) || isInformative, }); -} +}; diff --git a/lib/rules/metadata/process.js b/lib/rules/metadata/process.ts similarity index 65% rename from lib/rules/metadata/process.js rename to lib/rules/metadata/process.ts index 9ecbbcb15..f3ec1f638 100644 --- a/lib/rules/metadata/process.js +++ b/lib/rules/metadata/process.ts @@ -2,7 +2,7 @@ * Pseudo-rule for metadata extraction: process. */ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction } from '../../types.js'; // const self = { // name: 'metadata.process' @@ -12,16 +12,15 @@ export const name = 'metadata.process'; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +interface ProcessMetadata { + process: string; +} + +export const check: RuleCheckFunction = (sr, done) => { const $processDocument = sr.$('a#w3c_process_revision').first(); const processDocumentHref = $processDocument.length && $processDocument.attr('href'); - if (!processDocumentHref) { - return done(); - } + + if (!processDocumentHref) return done(); return done({ process: processDocumentHref }); -} +}; diff --git a/lib/rules/metadata/profile.js b/lib/rules/metadata/profile.ts similarity index 73% rename from lib/rules/metadata/profile.js rename to lib/rules/metadata/profile.ts index 2e1e87f09..6af4efd7e 100644 --- a/lib/rules/metadata/profile.js +++ b/lib/rules/metadata/profile.ts @@ -2,27 +2,29 @@ * Pseudo-rule for metadata extraction: profile. */ -/** @import { Specberus } from "../../validator.js" */ +import type { Cheerio } from 'cheerio'; +import type { Element } from 'domhandler'; -// Internal packages: -import { allProfiles, importJSON } from '../../util.js'; +import { allProfiles, isRuleTrack } from '../../util.js'; +import type { Specberus } from '../../validator.js'; import { sortedProfiles } from '../../views.js'; +import type { RecMetadata, RuleCheckFunction } from '../../types.js'; import { check as getTitle } from './title.js'; -const rules = importJSON('../../rules.json', import.meta.url); +import rules from '../../rules.json' with { type: 'json' }; // 'self.name' would be 'metadata.profile' export const name = 'metadata.profile'; -/** - * @param {Specberus} sr - * @param done - */ -export async function check(sr, done) { +export const check: RuleCheckFunction = async (sr, done) => { let matchedLength = 0; let id; - let $profileEl; - const reviewStatus = new Map(); + let $profileEl: Cheerio | undefined; + const reviewStatus = { + CR: 'implementationFeedbackDue', + PR: 'prReviewsDue', + CRY: 'cryFeedbackDue', + } as const; const $stateEl = sr.getDocumentStateElement(); if (!$stateEl) { @@ -32,10 +34,10 @@ export async function check(sr, done) { } const candidate = $stateEl && sr.norm($stateEl.text()).toLowerCase(); if (candidate) { - for (const t in rules) - if (t !== '*') - for (const p in rules[t].profiles) { - const name = rules[t].profiles[p].name.toLowerCase(); + for (const t in rules) { + if (isRuleTrack(t)) { + for (const [p, profile] of Object.entries(rules[t].profiles)) { + const name = profile.name.toLowerCase(); if ( candidate.indexOf(name) !== -1 && matchedLength < name.length @@ -45,34 +47,25 @@ export async function check(sr, done) { matchedLength = name.length; } } + } + } } - reviewStatus.set('CR', 'implementationFeedbackDue'); - reviewStatus.set('PR', 'prReviewsDue'); - reviewStatus.set('CRY', 'cryFeedbackDue'); - - /** - * @param id - * @param {Specberus} sr - */ - function assembleMeta(id, sr) { - let meta = { profile: id }; - if (reviewStatus.has(id)) { + function assembleMeta(id: string, sr: Specberus) { + let meta: RecMetadata = { profile: id }; + if (id in reviewStatus) { const dueDate = sr.getFeedbackDueDate(); const dates = dueDate && dueDate.valid; let res = dates[0]; if (dates.length === 0 || !res) return done({ profile: id }); - if (dates.length > 1) res = new Date(Math.min.apply(null, dates)); + if (dates.length > 1) + res = new Date(Math.min(...dates.map(d => +d))); - const d = [ - res.getFullYear(), - res.getMonth() + 1, - res.getDate(), - ].join('-'); - meta[reviewStatus.get(id)] = d; + meta[reviewStatus[id as keyof typeof reviewStatus]] = + `${res.getFullYear()}-${res.getMonth() + 1}-${res.getDate()}`; } // implementation report - if (['CR', 'CRD', 'PR', 'REC'].indexOf(id) > -1) { + if (['CR', 'CRD', 'PR', 'REC'].includes(id)) { const dts = sr.extractHeaders(); if (dts.Implementation?.$dd?.find('a').length) { meta.implementationReport = dts.Implementation.$dd @@ -87,21 +80,22 @@ export async function check(sr, done) { // Get 'track/rectrack' of the document based on id const profileRex = new RegExp( - `SUBM|(TR/(Registry|Recommendation|Note))/(${id}).js` + `SUBM|(TR/(Registry|Recommendation|Note))/(${id})` ); const thisProfile = allProfiles.filter(eachProfile => - // eg for eachProfile: 'TR/Note/NOTE-Echidna.js' + // e.g. for eachProfile: 'TR/Note/NOTE-Echidna' profileRex.test(eachProfile) ); - const profileMatch = - thisProfile.length && thisProfile[0].match(profileRex); + const profileMatch = thisProfile.length + ? thisProfile[0].match(profileRex) + : null; const track = profileMatch && profileMatch[profileMatch.length - 2]; meta.rectrack = track; return done(meta); } - const checkRecType = function () { + function checkRecType() { if ( $profileEl && sr.norm($profileEl.text()).indexOf('Candidate Recommendation') > 0 @@ -111,7 +105,7 @@ export async function check(sr, done) { : 'CR'; } return 'REC'; - }; + } if (id) { // W3C Candidate Recommendation (CR before 2020/CR snapshot/CR draft), W3C Recommendation will have "REC" if (id === 'REC' || id === 'CR') { @@ -122,7 +116,7 @@ export async function check(sr, done) { if (id === 'CRY') { // distinguish CRY CRYD id = - sr.norm($profileEl.text()).indexOf('Draft') > 0 + sr.norm($profileEl?.text() || '').indexOf('Draft') > 0 ? 'CRYD' : 'CRY'; } @@ -156,4 +150,4 @@ export async function check(sr, done) { ); } } -} +}; diff --git a/lib/rules/metadata/sotd.js b/lib/rules/metadata/sotd.js deleted file mode 100644 index f3b738a5c..000000000 --- a/lib/rules/metadata/sotd.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Pseudo-rule for metadata extraction: sotd. - */ - -/** @import { Specberus } from "../../validator.js" */ - -export const name = 'metadata.sotd'; - -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - const $sotd = sr.getSotDSection(); - return done({ sotd: $sotd ? sr.norm($sotd.html()) : 'Not found' }); -} diff --git a/lib/rules/metadata/sotd.ts b/lib/rules/metadata/sotd.ts new file mode 100644 index 000000000..f1cb27ca6 --- /dev/null +++ b/lib/rules/metadata/sotd.ts @@ -0,0 +1,16 @@ +/** + * Pseudo-rule for metadata extraction: sotd. + */ + +import type { RuleCheckFunction } from '../../types.js'; + +export const name = 'metadata.sotd'; + +interface SotdMetadata { + sotd: string; +} + +export const check: RuleCheckFunction = (sr, done) => { + const $sotd = sr.getSotDSection(); + return done({ sotd: $sotd ? sr.norm($sotd.html()!) : 'Not found' }); +}; diff --git a/lib/rules/metadata/title.js b/lib/rules/metadata/title.ts similarity index 55% rename from lib/rules/metadata/title.js rename to lib/rules/metadata/title.ts index 009512026..9efc569d5 100644 --- a/lib/rules/metadata/title.js +++ b/lib/rules/metadata/title.ts @@ -2,22 +2,21 @@ * Pseudo-rule for metadata extraction: title. */ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction } from '../../types.js'; // 'self.name' would be 'metadata.title' export const name = 'metadata.title'; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction<{ title: string } | void> = ( + sr, + done +) => { const $title = sr.$('body div.head h1').first(); if (!$title.length) return done(); - $title.html($title.html().replace(/:
/g, ': ').replace(/
/g, ' - ')); + $title.html($title.html()!.replace(/:
/g, ': ').replace(/
/g, ' - ')); return done({ title: sr.norm($title.text()), }); -} +}; diff --git a/lib/rules/sotd/candidate-review-end.js b/lib/rules/sotd/candidate-review-end.ts similarity index 81% rename from lib/rules/sotd/candidate-review-end.js rename to lib/rules/sotd/candidate-review-end.ts index f79c62c74..94c07d2d4 100644 --- a/lib/rules/sotd/candidate-review-end.js +++ b/lib/rules/sotd/candidate-review-end.ts @@ -1,4 +1,6 @@ -const self = { +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; + +const self: RuleMeta = { name: 'sotd.candidate-review-end', section: 'document-status', rule: 'reviewEndDate', @@ -6,13 +8,9 @@ const self = { export const { name } = self; -/** - * @param sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { const isEditorial = - (sr.config.editorial && /^true$/i.test(sr.config.editorial)) || false; + (sr.config!.editorial && /^true$/i.test(sr.config!.editorial)) || false; if (isEditorial) { sr.warning(self, 'editorial'); } else { @@ -36,4 +34,4 @@ export function check(sr, done) { } } done(); -} +}; diff --git a/lib/rules/sotd/charter.js b/lib/rules/sotd/charter.ts similarity index 89% rename from lib/rules/sotd/charter.js rename to lib/rules/sotd/charter.ts index 1ebf1109c..3607d670f 100644 --- a/lib/rules/sotd/charter.js +++ b/lib/rules/sotd/charter.ts @@ -1,10 +1,9 @@ /** * @file This rule checks if the Group have a current charter. For IG, also check if SOTD has certain text: $charterText. */ +import type { RuleCheckFunction } from '../../types.js'; import { AB, TAG } from '../../util.js'; -/** @import { Specberus } from "../../validator.js" */ - const self = { name: 'sotd.charter', }; @@ -14,11 +13,7 @@ export const { name } = self; const charterText = /The disclosure obligations of the Participants of this group are described in the charter\./; -/** - * @param {Specberus} sr - * @param done - */ -export async function check(sr, done) { +export const check: RuleCheckFunction = async (sr, done) => { const $sotd = sr.getSotDSection(); if ($sotd) { const deliverIds = await sr.getDelivererIDs(); @@ -33,7 +28,7 @@ export async function check(sr, done) { const AbID = AB.id; // groupIds: a list of ids without TAG or AB const groupIds = deliverIds.filter( - deliverer => ![TagID, AbID].includes(deliverer) + deliverer => !([TagID, AbID] as number[]).includes(deliverer) ); if (!groupIds.length) return done(); @@ -43,7 +38,7 @@ export async function check(sr, done) { return done(); } - if (sr.config.longStatus === 'Interest Group Note') { + if (sr.config!.longStatus === 'Interest Group Note') { const expectedHref = charters && `${charters[0]}#patentpolicy`; // check text exists const txt = sr.norm($sotd && $sotd.text()); @@ -77,4 +72,4 @@ export async function check(sr, done) { return done(); } -} +}; diff --git a/lib/rules/sotd/deliverer-note.js b/lib/rules/sotd/deliverer-note.ts similarity index 61% rename from lib/rules/sotd/deliverer-note.js rename to lib/rules/sotd/deliverer-note.ts index c85f00b2b..b36360e30 100644 --- a/lib/rules/sotd/deliverer-note.js +++ b/lib/rules/sotd/deliverer-note.ts @@ -1,4 +1,6 @@ -const self = { +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; + +const self: RuleMeta = { name: 'sotd.deliverer-note', section: 'metadata', rule: 'delivererID', @@ -6,13 +8,9 @@ const self = { export const { name } = self; -/** - * @param sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { const deliverers = sr.getDataDelivererIDs(); if (deliverers.length === 0) sr.error(self, 'not-found'); done(); -} +}; diff --git a/lib/rules/sotd/deployment.js b/lib/rules/sotd/deployment.ts similarity index 82% rename from lib/rules/sotd/deployment.js rename to lib/rules/sotd/deployment.ts index d5d78996f..8d4b1ef9c 100644 --- a/lib/rules/sotd/deployment.js +++ b/lib/rules/sotd/deployment.ts @@ -1,8 +1,8 @@ // for CR and REC. -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'sotd.deployment', section: 'document-status', rule: 'deployment', @@ -10,11 +10,7 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { const $sotd = sr.getSotDSection(); if ($sotd) { @@ -31,4 +27,4 @@ export function check(sr, done) { } } return done(); -} +}; diff --git a/lib/rules/sotd/diff.js b/lib/rules/sotd/diff.ts similarity index 51% rename from lib/rules/sotd/diff.js rename to lib/rules/sotd/diff.ts index 50b177449..80e7e120d 100644 --- a/lib/rules/sotd/diff.js +++ b/lib/rules/sotd/diff.ts @@ -1,4 +1,6 @@ -const self = { +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; + +const self: RuleMeta = { name: 'sotd.diff', section: 'document-status', rule: 'changesList', @@ -6,11 +8,7 @@ const self = { export const { name } = self; -/** - * @param sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { sr.info(self, 'note'); return done(); -} +}; diff --git a/lib/rules/sotd/draft-stability.js b/lib/rules/sotd/draft-stability.ts similarity index 86% rename from lib/rules/sotd/draft-stability.js rename to lib/rules/sotd/draft-stability.ts index 0ecf31afc..b8719d011 100644 --- a/lib/rules/sotd/draft-stability.js +++ b/lib/rules/sotd/draft-stability.ts @@ -1,8 +1,8 @@ // These 2 sentences exist in draft documents only, choose one of the 2. -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'sotd.draft-stability', section: 'document-status', rule: 'draftStability', @@ -10,13 +10,9 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { const $sotd = sr.getSotDSection(); - const { crType, cryType } = sr.config; + const { crType, cryType } = sr.config!; const STABILITY_REX = /This is a draft document and may be updated, replaced,? or obsoleted by other documents at any time\. It is inappropriate to cite this document as other than a work in progress\./; @@ -44,4 +40,4 @@ export function check(sr, done) { }); } done(); -} +}; diff --git a/lib/rules/sotd/new-features.js b/lib/rules/sotd/new-features.ts similarity index 78% rename from lib/rules/sotd/new-features.js rename to lib/rules/sotd/new-features.ts index a6e0d5d78..0b34fb5dc 100644 --- a/lib/rules/sotd/new-features.js +++ b/lib/rules/sotd/new-features.ts @@ -1,6 +1,6 @@ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'sotd.new-features', section: 'document-status', rule: 'newFeatures', @@ -8,13 +8,9 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { const $sotd = sr.getSotDSection(); - const docType = `${sr.config.status !== 'REC' ? 'upcoming ' : ''}Recommendation`; + const docType = `${sr.config!.status !== 'REC' ? 'upcoming ' : ''}Recommendation`; const warning = new RegExp( `Future updates to this ${docType} may incorporate new features.` ); @@ -38,4 +34,4 @@ export function check(sr, done) { sr.warning(self, 'no-warning'); } done(); -} +}; diff --git a/lib/rules/sotd/obsl-rescind.js b/lib/rules/sotd/obsl-rescind.ts similarity index 75% rename from lib/rules/sotd/obsl-rescind.js rename to lib/rules/sotd/obsl-rescind.ts index 4b4a66023..71f10d0b3 100644 --- a/lib/rules/sotd/obsl-rescind.js +++ b/lib/rules/sotd/obsl-rescind.ts @@ -5,18 +5,15 @@ // href="https://www.w3.org/2016/11/obsoleting-rescinding/">explanation of // Obsoleting and Rescinding W3C Specifications
.

-/** @import { Specberus } from "../../validator.js" */ +import type { Cheerio } from 'cheerio'; +import type { Specberus } from '../../validator.js'; +import type { Element } from 'domhandler'; +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -/** - * @param $candidates - * @param {Specberus} sr - */ -function findRscndRationale($candidates, sr) { - let $rationale = null; - let v; - if (sr.config.rescinds === true) v = 'rescind'; +function findRscndRationale($candidates: Cheerio, sr: Specberus) { + const v = sr.config!.rescinds === true ? 'rescind' : ''; - $candidates.each((_, p) => { + for (const p of $candidates.toArray()) { const $p = sr.$(p); const text = sr.norm($p.text()); const wanted1 = new RegExp( @@ -28,15 +25,11 @@ function findRscndRationale($candidates, sr) { ' please refer to the explanation of Obsoleting, Rescinding or Superseding W3C Specifications.', 'i' ); - if (wanted1.test(text) && wanted2.test(text)) { - $rationale = $p; - return false; - } - }); - return $rationale; + if (wanted1.test(text) && wanted2.test(text)) return $p; + } } -const self = { +const self: RuleMeta = { name: 'sotd.obsl-rescind', section: 'document-status', rule: 'rescindsRationale', @@ -44,17 +37,13 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { const $sotd = sr.getSotDSection(); if ($sotd) { const $rationale = findRscndRationale($sotd.filter('p'), sr) || findRscndRationale($sotd.find('p'), sr); - if (!$rationale || !$rationale.length) { + if (!$rationale?.length) { sr.error(self, 'no-rationale'); } else { const $a = $rationale.find('a:last-child').first(); @@ -70,4 +59,4 @@ export function check(sr, done) { } } done(); -} +}; diff --git a/lib/rules/sotd/pp.js b/lib/rules/sotd/pp.ts similarity index 77% rename from lib/rules/sotd/pp.js rename to lib/rules/sotd/pp.ts index 1c23f38e6..2161d03c5 100644 --- a/lib/rules/sotd/pp.js +++ b/lib/rules/sotd/pp.ts @@ -1,6 +1,10 @@ -/** @import { Specberus } from "../../validator.js" */ +import type { Cheerio } from 'cheerio'; +import type { Element } from 'domhandler'; -const self = { +import type { Specberus } from '../../validator.js'; +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; + +const self: RuleMeta = { name: 'sotd.pp', section: 'document-status', rule: 'patPolReq', @@ -10,15 +14,9 @@ const ppLink = 'https://www.w3.org/policies/patent-policy/'; const ppLink2020 = 'https://www.w3.org/policies/patent-policy/20200915/'; const ppLink2025 = 'https://www.w3.org/policies/patent-policy/20250515/'; -/** - * @param groups - * @param {Specberus} sr - * @param ppLink - */ -function buildWanted(groups, sr) { - const { config } = sr; +function buildWanted(groups: string[], sr: Specberus) { + const config = sr.config!; let wanted; - const result = {}; const isRecTrack = config.track === 'Recommendation'; const ppText = '( 15 September 2020| 15 May 2025)?'; @@ -55,47 +53,33 @@ function buildWanted(groups, sr) { // For documents not on REC-track, the sentence is different. wanted = `The${ppText} W3C Patent Policy does not carry any licensing requirements or commitments on this document.`; } - result.regex = new RegExp(wanted); - result.text = wanted.replace(/\\/g, ''); - return result; + + return { + regex: new RegExp(wanted), + text: wanted.replace(/\\/g, ''), + }; } -/** - * @param $candidates - * @param {Specberus} sr - * @param isIGDeliverable - */ -function findPP($candidates, sr) { - let $pp = null; +function findPP($candidates: Cheerio, sr: Specberus) { const delivererGroups = sr.getDelivererNames(); if (delivererGroups.length > 1) sr.warning(self, 'joint-publication'); const wanted = buildWanted(delivererGroups, sr); const expected = wanted.text; - $candidates.each((_, p) => { + for (const p of $candidates.toArray()) { const $p = sr.$(p); const text = sr.norm($p.text()); - if (wanted.regex.test(text)) { - $pp = $p; - return false; - } - }); - return { $pp, expected }; + if (wanted.regex.test(text)) return { $pp: $p, expected }; + } + return { $pp: null, expected }; } export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export async function check(sr, done) { - const groups = await sr.getDelivererGroups(); - // Check if the document is published by WGs only. If IG/AB/TAG(s) are involved, the document cannot be on REC-track and should have a sentence meaning 'pp does not apply'. - const publishedByWgOnly = groups.every(group => group.groupType === 'wg'); - +export const check: RuleCheckFunction = async (sr, done) => { const $sotd = sr.getSotDSection(); - const isRecTrack = sr.config.track === 'Recommendation'; + const track = sr.config!.track; + const isRecTrack = track === 'Recommendation'; if ($sotd) { const { $pp, expected } = findPP( @@ -113,7 +97,7 @@ export async function check(sr, done) { let foundSection6 = false; $pp.find('a[href]').each((_, a) => { const $a = sr.$(a); - const href = $a.attr('href'); + const href = $a.attr('href')!; const text = sr.norm($a.text()); const possiblePPLinks = [ppLink, ppLink2020, ppLink2025]; if ( @@ -155,8 +139,7 @@ export async function check(sr, done) { if (!foundLink) sr.error(self, 'no-link'); if (!foundPublicList && isRecTrack) sr.error(self, 'no-disclosures'); if ( - (sr.config.track === 'Recommendation' || - sr.config.track === 'Note') && + (track === 'Recommendation' || track === 'Note') && isRecTrack && !foundEssentials ) @@ -164,8 +147,7 @@ export async function check(sr, done) { link: `${ppLink}#def-essential`, }); if ( - (sr.config.track === 'Recommendation' || - sr.config.track === 'Note') && + (track === 'Recommendation' || track === 'Note') && isRecTrack && !foundSection6 ) @@ -174,4 +156,4 @@ export async function check(sr, done) { }); return done(); } -} +}; diff --git a/lib/rules/sotd/process-document.js b/lib/rules/sotd/process-document.ts similarity index 92% rename from lib/rules/sotd/process-document.js rename to lib/rules/sotd/process-document.ts index 1866ff257..cc3c6d83d 100644 --- a/lib/rules/sotd/process-document.js +++ b/lib/rules/sotd/process-document.ts @@ -1,6 +1,6 @@ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'sotd.process-document', section: 'document-status', rule: 'whichProcess', @@ -8,11 +8,7 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { const $sotd = sr.getSotDSection(); const BOILERPLATE_PREFIX = 'This document is governed by the '; const BOILERPLATE_SUFFIX = ' W3C Process Document.'; @@ -20,7 +16,7 @@ export function check(sr, done) { const newProcUri = 'https://www.w3.org/policies/process/20250818/'; const previousProc = '03 November 2023'; const previousProcUri = 'https://www.w3.org/policies/process/20231103/'; - let previousAllowed; + let previousAllowed = false; const boilerplate = BOILERPLATE_PREFIX + newProc + BOILERPLATE_SUFFIX; const previousBoilerplate = @@ -75,4 +71,4 @@ export function check(sr, done) { if (!found) sr.error(self, 'not-found', { process: newProc }); } done(); -} +}; diff --git a/lib/rules/sotd/publish.js b/lib/rules/sotd/publish.ts similarity index 84% rename from lib/rules/sotd/publish.js rename to lib/rules/sotd/publish.ts index 80cba39b1..5ad732659 100644 --- a/lib/rules/sotd/publish.js +++ b/lib/rules/sotd/publish.ts @@ -1,6 +1,6 @@ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'sotd.publish', section: 'document-status', rule: 'publish', @@ -8,21 +8,17 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export async function check(sr, done) { +export const check: RuleCheckFunction = async (sr, done) => { const $sotd = sr.getSotDSection(); - const { crType, cryType } = sr.config; - let docType = sr.config.longStatus; - if (sr.config.status === 'CR' || sr.config.status === 'CRD') { + const { crType, cryType, longStatus, status, track } = sr.config!; + let docType = longStatus; + if (status === 'CR' || status === 'CRD') { docType = `Candidate Recommendation ${crType}`; - } else if (sr.config.status === 'CRY' || sr.config.status === 'CRYD') { + } else if (status === 'CRY' || status === 'CRYD') { docType = `Candidate Registry ${cryType}`; } - const text = `^This document was (?:produced|published) by the (.+? Working Group|Technical Architecture Group|Advisory Board|.+? Interest Group)( and the (.+? Working Group|Technical Architecture Group|Advisory Board|.+? Interest Group))? as a ${docType} using the ${sr.config.track} track.`; + const text = `^This document was (?:produced|published) by the (.+? Working Group|Technical Architecture Group|Advisory Board|.+? Interest Group)( and the (.+? Working Group|Technical Architecture Group|Advisory Board|.+? Interest Group))? as a ${docType} using the ${sr.config!.track} track.`; if ($sotd) { // Find the paragraph of 'This document was published by ... , it includes ...' @@ -38,8 +34,7 @@ export async function check(sr, done) { const $sotdLinks = $sotd.find('a[href]'); - /** @type {RegExp} */ - let baseURL; + let baseURL = /^https:\/\/www.w3.org\/2023\/Process-20230612\//; // 1 month transition period sr.transition({ from: new Date('2023-11-02'), @@ -61,13 +56,11 @@ export async function check(sr, done) { const urlExpected = new RegExp(`${baseURL.source}#recs-and-notes$`); const trackEl = $sotdLinks .toArray() - .find(el => - sr.norm(sr.$(el).text()).match(`${sr.config.track} track`) - ); + .find(el => sr.norm(sr.$(el).text()).match(`${track} track`)); if (trackEl && !urlExpected.test(trackEl.attribs.href)) { sr.error(self, 'url-not-match', { url: urlExpected, - text: `${sr.config.track} track`, + text: `${track} track`, }); } @@ -89,12 +82,11 @@ export async function check(sr, done) { }); // recType: doc has sentence 'It includes proposed ...' in sotd. - const recType = - sr.config.status === 'REC' ? sr.getRecMetadata({}) : null; + const recType = status === 'REC' ? sr.getRecMetadata() : null; // check if 'candidate amendments' or 'proposed amendments' link in same paragraph is valid. if (recType && JSON.stringify(recType) !== '{}') { - let urlExpected; - let textExpected; + let urlExpected: RegExp; + let textExpected: RegExp; // for proposed amendments, proposed additions, proposed corrections. if (recType.pSubChanges && recType.pNewFeatures) { urlExpected = new RegExp( @@ -135,7 +127,7 @@ export async function check(sr, done) { .some(el => { const $el = sr.$(el); if (sr.norm($el.text()).match(textExpected)) { - if (!urlExpected.test($el.attr('href'))) { + if (!urlExpected.test($el.attr('href') || '')) { sr.error(self, 'url-not-match', { url: urlExpected, text: textExpected, @@ -147,10 +139,10 @@ export async function check(sr, done) { }); if (!linkFound) sr.error(self, 'url-text-not-found', { - url: urlExpected, - text: textExpected, + url: urlExpected!, + text: textExpected!, }); } } done(); -} +}; diff --git a/lib/rules/sotd/rec-addition.js b/lib/rules/sotd/rec-addition.ts similarity index 84% rename from lib/rules/sotd/rec-addition.js rename to lib/rules/sotd/rec-addition.ts index 586c57b30..005ee8190 100644 --- a/lib/rules/sotd/rec-addition.js +++ b/lib/rules/sotd/rec-addition.ts @@ -1,7 +1,10 @@ -/** @import { Specberus } from "../../validator.js" */ +import type { Cheerio } from 'cheerio'; +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; +import type { Specberus } from '../../validator.js'; +import type { Element } from 'domhandler'; // This rule only apply to REC, check changes with colored background. -const self = { +const self: RuleMeta = { name: 'sotd.rec-addition', section: 'document-status', rule: 'recAddition', @@ -14,13 +17,18 @@ const P_ADDITION = 'Proposed addition(s)? are marked in the document.'; const C_CORRECTION = 'Candidate correction(s)? are marked in the document.'; const C_ADDITION = 'Candidate addition(s)? are marked in the document.'; +interface CheckSectionOptions { + typeOfRec: boolean | undefined; + $htmlSection: Cheerio; + expectedText: string; + typeOfChange: string; + sectionClass: string; +} + /** * Check if the recommendation change type mentioned by text is consistent with the html element. - * - * @param {Specberus} sr - * @param options */ -function checkSection(sr, options) { +function checkSection(sr: Specberus, options: CheckSectionOptions) { if (options.typeOfRec) { if (options.$htmlSection.length) { const expectedReg = new RegExp(options.expectedText); @@ -47,14 +55,10 @@ function checkSection(sr, options) { }); } -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { const $sotd = sr.getSotDSection(); if ($sotd) { - const recType = sr.getRecMetadata({}); + const recType = sr.getRecMetadata(); const $pCorSection = $sotd.find('p.correction.proposed').first(); const $pAddSection = $sotd.find('p.addition.proposed').first(); const $cCorSection = $sotd.find('p.correction:not(.proposed)').first(); @@ -97,4 +101,4 @@ export function check(sr, done) { }); } return done(); -} +}; diff --git a/lib/rules/sotd/rec-comment-end.js b/lib/rules/sotd/rec-comment-end.ts similarity index 65% rename from lib/rules/sotd/rec-comment-end.js rename to lib/rules/sotd/rec-comment-end.ts index 748da5ee8..8b671920e 100644 --- a/lib/rules/sotd/rec-comment-end.js +++ b/lib/rules/sotd/rec-comment-end.ts @@ -1,6 +1,7 @@ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; +import { Specberus } from '../../validator.js'; -const self = { +const self: RuleMeta = { name: 'sotd.rec-comment-end', section: 'document-status', rule: 'commentEnd', @@ -8,22 +9,18 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { const $sotd = sr.getSotDSection(); if ($sotd) { - const recType = sr.getRecMetadata({}); + const recType = sr.getRecMetadata(); if (recType.pSubChanges || recType.pNewFeatures) { const txt = sr.norm($sotd.text()); - const rex = new RegExp(sr.dateRegexStrCapturing, 'g'); - const docDate = sr.getDocumentDate(); + const rex = new RegExp(Specberus.dateRegexStrCapturing, 'g'); + const docDate = sr.getDocumentDate()!; // 60 days later than docDate; const minimumEndDate = new Date( - docDate - 0 + 60 * 24 * 60 * 60 * 1000 + +docDate - 0 + 60 * 24 * 60 * 60 * 1000 ); // "Mon Nov 02 2020 16:26:28 GMT+0800 (@@ Standard Time)" -> "Nov 02 2020" const readableDate = minimumEndDate @@ -36,9 +33,12 @@ export function check(sr, done) { else { const matches = txt.match(rex); const dateFound = []; - for (const i in matches) { - if (sr.stringToDate(matches[i]) > minimumEndDate) { - dateFound.push(sr.stringToDate(matches[i])); + if (matches) { + for (const match of matches) { + const date = sr.stringToDate(match); + if (date && date > minimumEndDate) { + dateFound.push(sr.stringToDate(match)); + } } } if (dateFound.length > 1) { @@ -55,4 +55,4 @@ export function check(sr, done) { } } done(); -} +}; diff --git a/lib/rules/sotd/stability.js b/lib/rules/sotd/stability.ts similarity index 75% rename from lib/rules/sotd/stability.js rename to lib/rules/sotd/stability.ts index 0da0ca0fc..ec80f06d8 100644 --- a/lib/rules/sotd/stability.js +++ b/lib/rules/sotd/stability.ts @@ -2,36 +2,31 @@ // stability warning //

Publication as a Working Draft does not imply endorsement by W3C and its Members.

-/** @import { Cheerio } from "cheerio" */ -/** @import { Element } from "domhandler" */ -/** @import { Specberus } from "../../validator.js" */ +import type { Cheerio } from 'cheerio'; +import type { Element } from 'domhandler'; -/** - * @param $candidates - * @param {Specberus} sr - */ -async function findSW($candidates, sr) { +import type { Specberus } from '../../validator.js'; +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; + +async function findSW($candidates: Cheerio, sr: Specberus) { let wanted = ''; - /** @type {Cheerio | undefined} */ - let $sw; - if ( - sr.config.longStatus === 'Group Note' || - sr.config.longStatus === 'Group Note Draft' - ) { + let $sw: Cheerio | undefined; + const { crType, cryType, longStatus, status } = sr.config!; + + if (longStatus === 'Group Note' || longStatus === 'Group Note Draft') { // Find the sentence of 'Group Notes are not endorsed by W3C nor its Members.' or 'This Group Note is endorsed by the @@ Group, but is not endorsed by W3C itself nor its Members.' const groups = sr.getDelivererNames().join(' and the '); - wanted = `(${sr.config.longStatus}s are not endorsed by W3C nor its Members|This ${sr.config.longStatus} is endorsed by the ${groups}, but is not endorsed by W3C itself nor its Members).`; - } else if (sr.config.longStatus === 'Statement') { + wanted = `(${longStatus}s are not endorsed by W3C nor its Members|This ${longStatus} is endorsed by the ${groups}, but is not endorsed by W3C itself nor its Members).`; + } else if (longStatus === 'Statement') { wanted = 'A W3C Statement is a (specification|document) that, after extensive consensus-building, is endorsed by W3C and its Members.'; - } else if (sr.config.longStatus === 'Discontinued Draft') { + } else if (longStatus === 'Discontinued Draft') { wanted = 'Publication as a Discontinued Draft implies that this document is no longer intended to advance or to be maintained. It is inappropriate to cite this document as other than abandoned work.'; - } else if (sr.config.longStatus === 'Registry') { + } else if (longStatus === 'Registry') { wanted = 'A W3C Registry is a specification that, after extensive consensus-building, is endorsed by W3C and its Members.'; } else { - const { crType } = sr.config; const groupIds = await sr.getDelivererIDs(); const INTRO_S = ` A Candidate Recommendation Snapshot has received wide review, is intended to gather implementation experience, and has commitments from Working Group members to royalty-free licensing for implementations.`; const INTRO_D = ` A Candidate Recommendation Draft integrates changes from the previous Candidate Recommendation that the Working Group${ @@ -39,21 +34,19 @@ async function findSW($candidates, sr) { } to include in a subsequent Candidate Recommendation Snapshot.`; const CR_INTRO = crType === 'Draft' ? INTRO_D : INTRO_S; - const { cryType } = sr.config; const INTRO_CRY = ' A Candidate Registry Snapshot has received wide review.'; const INTRO_CRYD = ` A Candidate Registry Draft integrates changes from the previous Candidate Registry that the Working Group${ groupIds.length > 1 ? 's intend' : ' intends' } to include in a subsequent Candidate Registry Snapshot.`; const CRY_INTRO = cryType === 'Draft' ? INTRO_CRYD : INTRO_CRY; - const article = - sr.config.longStatus === 'Interest Group Note' ? 'an' : 'a'; + const article = longStatus === 'Interest Group Note' ? 'an' : 'a'; const represent = - sr.config.status === 'WD' || sr.config.status === 'FPWD' + status === 'WD' || status === 'FPWD' ? '( does not necessarily represent a consensus of the Working Group and)?' : ''; - wanted = `Publication as ${article} ${sr.config.longStatus}${ - sr.config.cryType ? ` ${sr.config.cryType}` : '' + wanted = `Publication as ${article} ${longStatus}${ + cryType ? ` ${cryType}` : '' }${represent} does not imply endorsement by W3C and its Members.${ crType ? CR_INTRO : '' }${cryType ? CRY_INTRO : ''}`; @@ -73,7 +66,7 @@ async function findSW($candidates, sr) { return { $sw, expected: wantedRE }; } -const self = { +const self: RuleMeta = { name: 'sotd.stability', section: 'document-status', rule: 'stability', @@ -81,14 +74,11 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export async function check(sr, done) { +export const check: RuleCheckFunction = async (sr, done) => { + const { crType, cryType, status } = sr.config!; const $sotd = sr.getSotDSection(); if ($sotd) { - if (sr.config.status === 'REC') { + if (status === 'REC') { const txt = sr.norm($sotd.text()); const wanted = `A W3C Recommendation is a specification that, after extensive consensus-building, is endorsed by W3C and its Members, and has commitments from Working Group members to royalty-free licensing for implementations.`; const rex = new RegExp(wanted); @@ -100,10 +90,7 @@ export async function check(sr, done) { sr ); if (!$sw) sr.error(self, 'no-stability', { expected }); - else if ( - sr.config.crType === 'Snapshot' || - sr.config.cryType === 'Snapshot' - ) { + else if (crType === 'Snapshot' || cryType === 'Snapshot') { const review = $sw .find('a') .toArray() @@ -118,7 +105,7 @@ export async function check(sr, done) { } // check 'royalty-free licensing' link - if (sr.config.status === 'REC' || sr.config.status === 'CR') { + if (status === 'REC' || status === 'CR') { const $links = $sotd.find('a'); const licensingText = 'royalty-free licensing'; const licensingLink = @@ -138,4 +125,4 @@ export async function check(sr, done) { } } return done(); -} +}; diff --git a/lib/rules/sotd/submission.js b/lib/rules/sotd/submission.ts similarity index 66% rename from lib/rules/sotd/submission.js rename to lib/rules/sotd/submission.ts index 6f82d96fe..39b71a509 100644 --- a/lib/rules/sotd/submission.js +++ b/lib/rules/sotd/submission.ts @@ -1,8 +1,10 @@ -/** @import { Cheerio } from "cheerio" */ -/** @import { Element } from "domhandler" */ -/** @import { Specberus } from "../../validator.js" */ +import type { Cheerio } from 'cheerio'; +import type { Element } from 'domhandler'; -const self = { +import type { Specberus } from '../../validator.js'; +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; + +const self: RuleMeta = { name: 'sotd.submission', section: 'document-status', rule: 'boilerplateSUBM', @@ -10,41 +12,29 @@ const self = { export const { name } = self; -/** - * @param {Cheerio} $candidates - * @param {Specberus} sr - * @returns {Cheerio | null} - */ -function findSubmText($candidates, sr) { - let $st = null; - $candidates.each((_, p) => { +function findSubmText($candidates: Cheerio, sr: Specberus) { + const wanted = + 'By publishing this document, W3C acknowledges that the Submitting Members ' + + 'have made a formal Submission request to W3C for discussion. Publication of ' + + 'this document by W3C indicates no endorsement of its content by W3C, nor ' + + 'that W3C has, is, or will be allocating any resources to the issues ' + + 'addressed by it. This document is not the product of a chartered W3C group, ' + + 'but is published as potential input to the W3C Process. A W3C Team Comment ' + + 'has been published in conjunction with this Member Submission. Publication ' + + 'of acknowledged Member Submissions at the W3C site is one of the benefits ' + + 'of W3C Membership. Please consult the requirements associated with Member ' + + 'Submissions of section 3.3 of the W3C Patent Policy. Please consult the ' + + 'complete list of acknowledged W3C Member Submissions.'; + + for (const p of $candidates.toArray()) { const $p = sr.$(p); const text = sr.norm($p.text()); - const wanted = - 'By publishing this document, W3C acknowledges that the Submitting Members ' + - 'have made a formal Submission request to W3C for discussion. Publication of ' + - 'this document by W3C indicates no endorsement of its content by W3C, nor ' + - 'that W3C has, is, or will be allocating any resources to the issues ' + - 'addressed by it. This document is not the product of a chartered W3C group, ' + - 'but is published as potential input to the W3C Process. A W3C Team Comment ' + - 'has been published in conjunction with this Member Submission. Publication ' + - 'of acknowledged Member Submissions at the W3C site is one of the benefits ' + - 'of W3C Membership. Please consult the requirements associated with Member ' + - 'Submissions of section 3.3 of the W3C Patent Policy. Please consult the ' + - 'complete list of acknowledged W3C Member Submissions.'; - if (text === wanted) { - $st = $p; - return false; - } - }); - return $st; + if (text === wanted) return $p; + } + return null; } -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { const $sotd = sr.getSotDSection(); if ($sotd) { const $st = @@ -71,7 +61,7 @@ export function check(sr, done) { let foundComment = false; $st.find('a[href]').each((_, a) => { const $a = sr.$(a); - const href = $a.attr('href'); + const href = $a.attr('href')!; const text = sr.norm($a.text()); if ( [w3cProcessNew, w3cProcessOld].includes(href) && @@ -130,4 +120,4 @@ export function check(sr, done) { if (!foundComment) sr.error(self, 'no-tc-link'); } done(); -} +}; diff --git a/lib/rules/sotd/supersedable.js b/lib/rules/sotd/supersedable.ts similarity index 88% rename from lib/rules/sotd/supersedable.js rename to lib/rules/sotd/supersedable.ts index d7ab85fcd..3d5dca007 100644 --- a/lib/rules/sotd/supersedable.js +++ b/lib/rules/sotd/supersedable.ts @@ -5,9 +5,9 @@ // latest revision of this technical report can be found in the // W3C Standards and draft index.

-/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'sotd.supersedable', section: 'document-status', rule: 'boilerplateTRDoc', @@ -15,18 +15,14 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { const $sotd = sr.getSotDSection(); if ($sotd) { let $em = $sotd.filter('p').children('em').first(); if (!$em.length) $em = $sotd.find('p em').first(); const txt = sr.norm($em.text()); const wanted = `${'This section describes the status of this document at the time of its publication. A list of current W3C publications '}${ - sr.config.status === 'SUBM' + sr.config!.status === 'SUBM' ? '' : 'and the latest revision of this technical report ' }can be found in the W3C standards and drafts index.`; @@ -48,4 +44,4 @@ export function check(sr, done) { if (!$a.length) sr.error(self, 'no-sotd-tr'); } done(); -} +}; diff --git a/lib/rules/sotd/usage.js b/lib/rules/sotd/usage.ts similarity index 79% rename from lib/rules/sotd/usage.js rename to lib/rules/sotd/usage.ts index 603e3d95a..ad5e2a4ce 100644 --- a/lib/rules/sotd/usage.js +++ b/lib/rules/sotd/usage.ts @@ -1,8 +1,8 @@ // for Registry. -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'sotd.usage', section: 'document-status', rule: 'usage', @@ -10,11 +10,7 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { const $sotd = sr.getSotDSection(); if ($sotd) { @@ -30,4 +26,4 @@ export function check(sr, done) { } } return done(); -} +}; diff --git a/lib/rules/structure/canonical.js b/lib/rules/structure/canonical.ts similarity index 79% rename from lib/rules/structure/canonical.js rename to lib/rules/structure/canonical.ts index 01dfb8430..196a9928f 100644 --- a/lib/rules/structure/canonical.js +++ b/lib/rules/structure/canonical.ts @@ -1,6 +1,6 @@ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'structure.canonical', section: 'metadata', rule: 'canonical', @@ -8,11 +8,7 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { const checkCanonical = function () { const $lnk = sr.$('head > link[rel=canonical]').first(); if (!$lnk.length || !$lnk.attr('href')) sr.error(self, 'not-found'); @@ -27,4 +23,4 @@ export function check(sr, done) { }); done(); -} +}; diff --git a/lib/rules/structure/display-only.js b/lib/rules/structure/display-only.ts similarity index 78% rename from lib/rules/structure/display-only.js rename to lib/rules/structure/display-only.ts index d03b73c37..bf6b52ac0 100644 --- a/lib/rules/structure/display-only.js +++ b/lib/rules/structure/display-only.ts @@ -1,11 +1,9 @@ +import type { RuleCheckFunction } from '../../types.js'; + export const name = 'structure.display-only'; -/** - * @param sr - * @param done - */ -export function check(sr, done) { - if (sr.config.status !== 'DISC') +export const check: RuleCheckFunction = (sr, done) => { + if (sr.config!.status !== 'DISC') sr.info( { name, section: 'document-status', rule: 'customParagraph' }, 'customised-paragraph' @@ -22,4 +20,4 @@ export function check(sr, done) { sr.info({ name }, 'index-list-tables'); sr.info({ name }, 'fit-in-a4'); done(); -} +}; diff --git a/lib/rules/structure/h2.js b/lib/rules/structure/h2.ts similarity index 84% rename from lib/rules/structure/h2.js rename to lib/rules/structure/h2.ts index 35215358a..cf5c70a64 100644 --- a/lib/rules/structure/h2.js +++ b/lib/rules/structure/h2.ts @@ -1,4 +1,4 @@ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction } from '../../types.js'; export const name = 'structure.h2'; const abstract = { @@ -18,12 +18,8 @@ const toc = { rule: 'toc', }; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - const h2s = []; +export const check: RuleCheckFunction = (sr, done) => { + const h2s: string[] = []; sr.$('h2').each((_, h2) => { const $h2 = sr.$(h2); if ($h2.parents('.head').length === 0) h2s.push(sr.norm($h2.text())); @@ -36,4 +32,4 @@ export function check(sr, done) { if (!/^Table [Oo]f [Cc]ontents$/.test(h2s[2])) sr.error(toc, 'toc', { was: h2s[2] }); done(); -} +}; diff --git a/lib/rules/structure/name.js b/lib/rules/structure/name.ts similarity index 81% rename from lib/rules/structure/name.js rename to lib/rules/structure/name.ts index 0bfa9d5e1..7570e6619 100644 --- a/lib/rules/structure/name.js +++ b/lib/rules/structure/name.ts @@ -1,6 +1,8 @@ import superagent from 'superagent'; -const self = { +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; + +const self: RuleMeta = { name: 'structure.name', section: 'compound', rule: 'compoundOverview', @@ -8,11 +10,7 @@ const self = { export const { name } = self; -/** - * @param sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { // Pseudo-constants: const EXPECTED_NAME = /\/Overview\.html$/; const OVERVIEW = 'Overview.html'; @@ -38,8 +36,8 @@ export function check(sr, done) { return done(); } - superagent.get(sr.url).end((err1, result1) => { - superagent.get(sr.url + OVERVIEW).end((err2, result2) => { + superagent.get(sr.url).end((_, result1) => { + superagent.get(sr.url + OVERVIEW).end((_, result2) => { if ( !result1 || !result2 || @@ -52,4 +50,4 @@ export function check(sr, done) { return done(); }); }); -} +}; diff --git a/lib/rules/structure/neutral.js b/lib/rules/structure/neutral.ts similarity index 72% rename from lib/rules/structure/neutral.js rename to lib/rules/structure/neutral.ts index 4ecffbe5a..6a7ce8e7d 100644 --- a/lib/rules/structure/neutral.js +++ b/lib/rules/structure/neutral.ts @@ -2,34 +2,24 @@ * @file make sure specification use neutral words. */ -import { importJSON } from '../../util.js'; - -/** @import { Specberus } from "../../validator.js" */ - -const badterms = importJSON('../../../public/badterms.json', import.meta.url); +import badterms from '../../../public/badterms.json' with { type: 'json' }; +import type { RuleCheckFunction } from '../../types.js'; const self = { name: 'structure.neutral', }; // get blocklist from json file -let blocklist = []; -badterms.forEach(item => { - blocklist = blocklist.concat(item.term); - if (item.variation) { - blocklist = blocklist.concat(item.variation); - } -}); +let blocklist: string[] = []; +for (const item of badterms) { + blocklist = blocklist.concat(item.term, item.variation || []); +} export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { const blocklistReg = new RegExp(`\\b${blocklist.join('\\b|\\b')}\\b`, 'ig'); - const unneutralList = []; + const unneutralList: string[] = []; // Use a cloned body instead of the original one, prevent '.remove()' side effects. const $body = sr.$('body').first().clone(); const $links = $body.find('a'); @@ -56,4 +46,4 @@ export function check(sr, done) { sr.warning(self, 'neutral', { words: unneutralList.join('", "') }); } done(); -} +}; diff --git a/lib/rules/structure/section-ids.js b/lib/rules/structure/section-ids.ts similarity index 89% rename from lib/rules/structure/section-ids.js rename to lib/rules/structure/section-ids.ts index 41009827b..d129e12a2 100644 --- a/lib/rules/structure/section-ids.js +++ b/lib/rules/structure/section-ids.ts @@ -1,6 +1,6 @@ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'structure.section-ids', section: 'document-body', rule: 'headingWithoutID', @@ -8,11 +8,7 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { const $ignoreH3 = sr.$('.head > h3').first(); sr.$('h2, h3, h4, h5, h6').each((_, el) => { @@ -48,4 +44,4 @@ export function check(sr, done) { sr.error(self, 'no-id', { text: el.name }); }); done(); -} +}; diff --git a/lib/rules/structure/security-privacy.js b/lib/rules/structure/security-privacy.ts similarity index 81% rename from lib/rules/structure/security-privacy.js rename to lib/rules/structure/security-privacy.ts index 9b281e581..bc1b00617 100644 --- a/lib/rules/structure/security-privacy.js +++ b/lib/rules/structure/security-privacy.ts @@ -1,6 +1,6 @@ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'structure.security-privacy', section: 'document-body', rule: 'securityAndPrivacy', @@ -8,11 +8,7 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { let security = false; let privacy = false; @@ -36,4 +32,4 @@ export function check(sr, done) { } done(); -} +}; diff --git a/lib/rules/style/back-to-top.js b/lib/rules/style/back-to-top.ts similarity index 54% rename from lib/rules/style/back-to-top.js rename to lib/rules/style/back-to-top.ts index 1bae85fe1..fd3897d1b 100644 --- a/lib/rules/style/back-to-top.js +++ b/lib/rules/style/back-to-top.ts @@ -2,7 +2,7 @@ * Check if there's a back-top-top hyperlink. */ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction } from '../../types.js'; const self = { name: 'style.back-to-top', @@ -10,18 +10,12 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { const $candidates = sr.$( "body p#back-to-top[role='navigation'] a[href='#title']" ); - if ($candidates.length !== 1) { - sr.warning(self, 'not-found'); - } + if ($candidates.length !== 1) sr.warning(self, 'not-found'); done(); -} +}; diff --git a/lib/rules/style/body-toc-sidebar.js b/lib/rules/style/body-toc-sidebar.ts similarity index 64% rename from lib/rules/style/body-toc-sidebar.js rename to lib/rules/style/body-toc-sidebar.ts index 26550fb83..427d82f0f 100644 --- a/lib/rules/style/body-toc-sidebar.js +++ b/lib/rules/style/body-toc-sidebar.ts @@ -1,4 +1,4 @@ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction } from '../../types.js'; const self = { name: 'style.body-toc-sidebar', @@ -6,15 +6,11 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { try { if (sr.$('body').hasClass('toc-sidebar')) sr.error(self, 'class-found'); } catch (e) { sr.error(self, 'selector-fail'); } done(); -} +}; diff --git a/lib/rules/style/meta.js b/lib/rules/style/meta.ts similarity index 87% rename from lib/rules/style/meta.js rename to lib/rules/style/meta.ts index b8056a93b..9ac6c0cf0 100644 --- a/lib/rules/style/meta.js +++ b/lib/rules/style/meta.ts @@ -2,11 +2,11 @@ * Check the presence of this meta tag in the head of the page: * <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> */ +// @ts-ignore (no typings) import mvp from 'metaviewport-parser'; +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -/** @import { Specberus } from "../../validator.js" */ - -const self = { +const self: RuleMeta = { name: 'style.meta', section: 'metadata', rule: 'viewport', @@ -17,11 +17,7 @@ export const { name } = self; const width = /^device-width$/i; const shrinkToFit = /^no$/i; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { const $meta = sr.$("head > meta[name='viewport'][content]"); if ($meta.length !== 1) { sr.error(self, 'not-found'); @@ -45,4 +41,4 @@ export function check(sr, done) { } } done(); -} +}; diff --git a/lib/rules/style/script.js b/lib/rules/style/script.ts similarity index 56% rename from lib/rules/style/script.js rename to lib/rules/style/script.ts index 35888fa66..3dae6c57a 100644 --- a/lib/rules/style/script.js +++ b/lib/rules/style/script.ts @@ -2,7 +2,7 @@ * Check whether the script fixup.js is linked in the page. */ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction } from '../../types.js'; const self = { name: 'style.script', @@ -10,11 +10,7 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { const PATTERN_SCRIPT = /^(https?:)?\/\/(www\.)?w3\.org\/scripts\/tr\/2021\/fixup\.js$/i; @@ -22,14 +18,10 @@ export function check(sr, done) { let found = 0; $candidates.each((_, el) => { - if (PATTERN_SCRIPT.test(el.attribs.src)) { - found += 1; - } + if (PATTERN_SCRIPT.test(el.attribs.src)) found++; }); - if (found !== 1) { - sr.error(self, 'not-found'); - } + if (found !== 1) sr.error(self, 'not-found'); done(); -} +}; diff --git a/lib/rules/style/sheet.js b/lib/rules/style/sheet.ts similarity index 77% rename from lib/rules/style/sheet.js rename to lib/rules/style/sheet.ts index 86d297279..215a95ba1 100644 --- a/lib/rules/style/sheet.js +++ b/lib/rules/style/sheet.ts @@ -1,4 +1,4 @@ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction } from '../../types.js'; export const name = 'style.sheet'; const section = 'metadata'; @@ -13,13 +13,10 @@ const notLast = { rule: 'lastStylesheet', }; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - if (!sr.config.styleSheet) return done(); - const url = `https://www.w3.org/StyleSheets/TR/2021/${sr.config.styleSheet}`; +export const check: RuleCheckFunction = (sr, done) => { + const { styleSheet } = sr.config!; + if (!styleSheet) return done(); + const url = `https://www.w3.org/StyleSheets/TR/2021/${styleSheet}`; const dark = 'https://www.w3.org/StyleSheets/TR/2021/dark'; const stylesheetLinks = [ `head > link[rel=stylesheet][href='${url}']`, @@ -39,4 +36,4 @@ export function check(sr, done) { } } done(); -} +}; diff --git a/lib/rules/validation/html.js b/lib/rules/validation/html.ts similarity index 91% rename from lib/rules/validation/html.js rename to lib/rules/validation/html.ts index 923d3dc88..ab1cd7140 100644 --- a/lib/rules/validation/html.js +++ b/lib/rules/validation/html.ts @@ -1,6 +1,7 @@ import { get, post } from '../../throttled-ua.js'; +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'validation.html', section: 'format', rule: 'validHTML', @@ -9,18 +10,10 @@ const TIMEOUT = 10000; export const { name } = self; -/** - * @param sr - * @param done - */ -export function check(sr, done) { - let service = null; - if (sr.config.htmlValidator !== undefined) { - service = sr.config.htmlValidator; - } else { - service = 'https://validator.w3.org/nu/'; - } - if (sr.config.skipValidation) { +export const check: RuleCheckFunction = (sr, done) => { + const { htmlValidator, skipValidation } = sr.config!; + const service = htmlValidator || 'https://validator.w3.org/nu/'; + if (skipValidation) { sr.warning(self, 'skipped'); return done(); } @@ -105,4 +98,4 @@ export function check(sr, done) { } done(); }); -} +}; diff --git a/lib/rules/validation/wcag.js b/lib/rules/validation/wcag.ts similarity index 51% rename from lib/rules/validation/wcag.js rename to lib/rules/validation/wcag.ts index d5c0f8e3a..46f7434a5 100644 --- a/lib/rules/validation/wcag.js +++ b/lib/rules/validation/wcag.ts @@ -1,4 +1,6 @@ -const self = { +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; + +const self: RuleMeta = { name: 'validation.wcag', section: 'document-body', rule: 'wcag', @@ -6,11 +8,7 @@ const self = { export const { name } = self; -/** - * @param sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = (sr, done) => { sr.info(self, 'tools'); return done(); -} +}; diff --git a/lib/sink.js b/lib/sink.js deleted file mode 100644 index 9c50a3735..000000000 --- a/lib/sink.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Generic sink. - */ - -import { EventEmitter } from 'events'; - -/** - * Build a generic event handler that can be used by Specberus. - * - * @param {Function=} error - function to call in case of exception or error. - * @param {Function=} done - function to call at the very end of the process. - * @param {Function=} warn - function to call in case of warning. - * @param {Function=} inf - function to call in case of informative message. - */ - -export class Sink extends EventEmitter { - constructor(error, done, warn, inf) { - super(); - this.ok = 0; - this.errors = []; - this.warnings = []; - this.done = 0; - - if (error) { - this.on('exception', data => { - error(data); - }); - this.on('err', (...data) => { - error(...data); - }); - } - - if (done) { - this.on('end-all', data => { - done(data); - }); - } - - if (warn) { - this.on('warning', (...data) => { - warn(...data); - }); - } - - if (inf) { - this.on('info', (...data) => { - inf(...data); - }); - } - } -} diff --git a/lib/throttled-ua.js b/lib/throttled-ua.ts similarity index 73% rename from lib/throttled-ua.js rename to lib/throttled-ua.ts index 563a67f78..c1bf3bafa 100644 --- a/lib/throttled-ua.js +++ b/lib/throttled-ua.ts @@ -3,10 +3,7 @@ import sua from 'superagent'; const delay = 1000; let lastCall = Date.now() - delay * 2; -/** - * @param {sua.Request} req - */ -function fixEnd(req) { +function fixEnd(req: sua.Request) { const oldEnd = req.end; req.end = function (cb) { let next = 0; @@ -19,16 +16,10 @@ function fixEnd(req) { return req; } -// this explicitly does not support the superagent call variant in which a callback -// is provided directly -// exports = function () { -// var req = sua.apply(sua, arguments); -// return fixEnd(req); -// }; - export const get = process.env.NO_THROTTLE ? sua.get : function () { + //@ts-ignore (arguments type mismatch) const req = sua.get.apply(sua, arguments); return fixEnd(req); }; @@ -36,6 +27,7 @@ export const get = process.env.NO_THROTTLE export const post = process.env.NO_THROTTLE ? sua.post : function () { + //@ts-ignore (arguments type mismatch) const req = sua.post.apply(sua, arguments); return fixEnd(req); }; diff --git a/lib/types.d.ts b/lib/types.d.ts new file mode 100644 index 000000000..64c32d5a2 --- /dev/null +++ b/lib/types.d.ts @@ -0,0 +1,118 @@ +import type { Specberus, ValidateOptions } from './validator.js'; + +type Status = + | 'FPWD' + | 'WD' + | 'CR' + | 'CRD' + | 'REC' + | 'DISC' + | 'DNOTE' + | 'NOTE' + | 'STMT' + | 'DRY' + | 'CRY' + | 'CRYD' + | 'RY' + | 'SUBM' + | 'MEM-SUBM'; + +type SubmissionType = 'member'; + +type Track = 'Note' | 'Recommendation' | 'Registry'; + +export interface SpecberusConfig { + /** Candidate Recommendation type is only defined for CR/CRD statuses */ + crType?: 'Draft' | 'Snapshot'; + /** Candidate Registry type is only defined for CRY/CRYD statuses */ + cryType?: 'Draft' | 'Snapshot'; + editorial?: 'true'; + htmlValidator?: string; + longStatus: string; + rescinds?: boolean; + skipValidation?: boolean; + status: Status; + styleSheet: string; + /** Submission type is only defined for MEM-SUBM status */ + submissionType?: SubmissionType; + /** Track (Recommendation or Note); not defined for MEM-SUBM status */ + track?: Track; + /** Validation setting, inherited from options passed to validate */ + validation?: ValidateOptions['validation']; +} + +export type HandlerMessage = Record; + +type IsoDateString = `${number}-${number}-${number}`; + +export interface RecMetadata { + cryFeedbackDue?: IsoDateString; + cSubChanges?: boolean; + cNewFeatures?: boolean; + implementationFeedbackDue?: IsoDateString; + implementationReport?: string | undefined; + prReviewsDue?: IsoDateString; + profile?: string; + pNewFeatures?: boolean; + pSubChanges?: boolean; + rectrack?: string | null; +} + +export type RuleCheckFunction = ( + sr: Specberus, + done: (result: R) => void +) => void | Promise; + +export interface RuleBase { + name: string; +} + +export interface RuleMeta extends RuleBase { + rule: string; + section: string; +} + +export interface RuleModule extends RuleBase { + check: RuleCheckFunction; +} + +/** `section` from lib/rules.json */ +export interface RulesSection { + name: string; + rules: Record; +} + +/** `section` under `*` from lib/rules.json */ +export interface GenericRulesSection { + name: string; + rules: Record; +} + +/** `profile` from lib/rules.json */ +export interface RulesProfile { + name: string; + order: number; + sections: { + '*': GenericRulesSection; + [index: string]: RulesSection; + }; +} + +export interface ProfileModule { + config: SpecberusConfig; + name: string; + rules: RuleModule[]; +} + +// The following are minimal types that would ideally be defined more completely in node-w3capi + +export interface ApiCharter { + 'doc-licenses': { name: string; uri: string }[]; + end: string; + start: string; + uri: string; +} + +export interface ApiSpecificationVersion { + uri: string; +} diff --git a/lib/util.js b/lib/util.js deleted file mode 100644 index 0defa2de5..000000000 --- a/lib/util.js +++ /dev/null @@ -1,267 +0,0 @@ -/** - * Miscellaneous utilities. - */ - -import fs from 'fs'; -import { dirname, resolve } from 'path'; -import { nextTick } from 'process'; -import { fileURLToPath } from 'url'; - -import { Octokit } from '@octokit/core'; -import w3cApi from 'node-w3capi'; - -/** @import { Specberus } from "./validator.js" */ - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -const filesTR = fs.readdirSync(`${__dirname}/profiles/TR/`); -const filesSUBM = fs.readdirSync(`${__dirname}/profiles/SUBM/`); - -export const importJSON = (relativeTo, importURL) => { - const dir = dirname(fileURLToPath(importURL)); - const pathname = resolve(dir, relativeTo); - - return JSON.parse(fs.readFileSync(pathname).toString()); -}; - -/** - * Build a JSON result (of validation, metadata extraction, etc). - * - * @param {Array} err - errors. - * @param {Array} warn - warnings. - * @param {Array} inf - informative messages. - * @param {Object} metadata - dictionary with some found metadata. - */ - -export const buildJSONresult = function (err, warn, inf, metadata) { - return { - success: err.length === 0, - errors: err, - warnings: warn, - info: inf, - metadata, - }; -}; - -// Get rules of each profile -let TRProfiles = []; -filesTR.forEach(track => { - if (!track.startsWith('.')) { - const profileNames = fs - .readdirSync(`${__dirname}/profiles/TR/${track}/`) - .map(profileName => `${track}/${profileName}`); - TRProfiles = [...TRProfiles, ...profileNames]; - } -}); - -export const allProfiles = [ - ...TRProfiles.map(x => `TR/${x}`), - ...filesSUBM.map(x => `SUBM/${x}`), -]; - -export const profiles = Object.fromEntries( - allProfiles - .map(file => { - const match = - /((TR|SUBM)\/([A-Za-z]+\/)?([A-Z][A-Z-]*[A-Z](-Echidna)?))\.js$/.exec( - file - ); - if (match && match[4]) { - const key = match[4]; - return [key, import(`./profiles/${match[0]}`)]; - } - return null; - }) - .filter(Boolean) -); - -/** - * Build a function that builds an “options” object based on certain parameters. - * - * @returns {Function} a function that builds an “options” object based on an HTTP query string or a similar object containing options. - */ - -const buildProcessParamsFunction = function () { - /** - * Build an “options” object based on an HTTP query string or a similar object containing options. - * - * An example of constraints: - *
{
-     *     "required": ["processDocument"],
-     *     "forbidden": ["echidnaReady", "bogusParam"],
-     *     "allowUnknownParams": true
-     * }
/blockquote> - * - * @param {Object} params - an HTTP request query, or a similar object. - * @param {Object} base - (optional) a template or “base” object to build from. - * @param {Object} constraints - (optional) an object listing “required” and/or “forbidden” parameters. - * @returns {Promise} an “options” object that can be used by Specberus. - * @throws {Error} if there is an error in the parameters. - */ - - return async function (params, base, constraints) { - const result = base ? JSON.parse(JSON.stringify(base)) : {}; - let originFound = false; - for (const p in params) { - if ( - p === 'url' || - p === 'source' || - p === 'file' || - p === 'document' - ) { - // Origins: only one allowed. - if (originFound) - throw new Error( - 'Only one of parameters {“url”, “source”, “file”, “document”} is allowed' - ); - originFound = true; - result[p] = params[p]; - } else if (p === 'profile') { - // Profile: if it's a string, load the corresponding object. - if (Object.prototype.hasOwnProperty.call(result, p)) - throw new Error(`Parameter “${p}” is used more than once`); - else if (typeof params[p] === 'string') { - const subPath = `/${params[p]}.js`; - const profilePath = allProfiles.find(p => - p.endsWith(subPath) - ); - - if (profilePath) - result[p] = await import( - `../lib/profiles/${profilePath}` - ); - else if (params[p] === 'auto') result[p] = 'auto'; - else throw new Error(`Unknown profile “${params[p]}”`); - } else result[p] = params[p]; - } else if ( - p === 'validation' || - p === 'htmlValidator' || - p === 'cssValidator' || - p === 'processDocument' || - p === 'events' || - p === 'editorial' || - p === 'additionalMetadata' - ) { - // Other params: - if (Object.prototype.hasOwnProperty.call(result, p)) - throw new Error(`Parameter “${p}” is used more than once`); - result[p] = params[p]; - } else if (!constraints || !constraints.allowUnknownParams) - // Illegal params: - throw new Error(`Illegal parameter “${p}”`); - } - if (!originFound) - // Origin: one required. - throw new Error( - 'One parameter of {“url”, “source”, “file”, “document”} is required' - ); - else { - let c; - if (constraints && constraints.required) { - // Extra required params: - for (c in constraints.required) - if ( - !Object.prototype.hasOwnProperty.call( - result, - constraints.required[c] - ) - ) - throw new Error( - `Parameter “${constraints.required[c]}” is required in this context` - ); - } - if (constraints && constraints.forbidden) { - // Forbidden params: - for (c in constraints.forbidden) - if ( - Object.prototype.hasOwnProperty.call( - result, - constraints.forbidden[c] - ) - ) - throw new Error( - `Parameter “${constraints.forbidden[c]}” is not allowed in this context` - ); - } - } - return result; - }; -}; - -export const processParams = buildProcessParamsFunction(); - -/** @type {Record} */ -const githubUsernameCache = {}; -const githubUsernameCacheTimeout = 3600000; - -/** @type {Octokit} */ -let octokit; - -function cleanGithubUsernameCache() { - const now = Date.now(); - for (const [key, { time }] of Object.entries(githubUsernameCache)) { - if (now - time >= githubUsernameCacheTimeout) - delete githubUsernameCache[key]; - } -} - -function cacheGithubUsername(username, id) { - githubUsernameCache[username] = { id, time: Date.now() }; - nextTick(cleanGithubUsernameCache); - return id; -} - -/** - * Attempts to resolve a GitHub username to a W3C user ID. - * Returns null on 404; throws on any other 4xx/5xx response. - * @param {string} username - */ -export async function resolveGithubUsernameToId(username) { - if ( - typeof githubUsernameCache[username] !== 'undefined' && - Date.now() - githubUsernameCache[username].time < - githubUsernameCacheTimeout - ) - return githubUsernameCache[username].id; - - if (!octokit) octokit = new Octokit({ auth: process.env.GH_TOKEN }); - - try { - const response = await octokit.request('GET /users/{username}', { - username, - }); - const githubId = response.data.id; - const { id } = await w3cApi - .user({ type: 'github', id: githubId }) - .fetch(); - return cacheGithubUsername(username, id); - } catch (error) { - // Both octokit and w3capi errors contain status and request.url - const status = 'status' in error ? error.status : null; - if (status === 404) { - return cacheGithubUsername(username, null); - } else { - const message = `Failed to resolve GitHub user ${username}${ - status ? `: ${error.request.url} responded with ${status}` : '' - }`; - console.error(message); - throw new Error(message, { cause: username }); - } - } -} - -export const TAG = { id: 34270, type: 'other' }; -export const AB = { id: 7756, type: 'other' }; - -export const REC_TEXT = { - SOTD_P_COR: 'It includes proposed correction(s)?.', - SOTD_P_ADD: - 'It includes proposed addition(s)?, introducing new feature(s)? since the previous Recommendation.', - SOTD_P_COR_ADD: - 'It includes proposed amendment(s)?, introducing substantive change(s)? and new feature(s)? since the previous Recommendation.', - SOTD_C_COR: 'It includes candidate correction(s)?.', - SOTD_C_ADD: - 'It includes candidate addition(s)?, introducing new feature(s)? since the previous Recommendation.', - SOTD_C_COR_ADD: - 'It includes candidate amendment(s)?, introducing substantive change(s)? and new feature(s)? since the previous Recommendation.', -}; diff --git a/lib/util.ts b/lib/util.ts new file mode 100644 index 000000000..b451db110 --- /dev/null +++ b/lib/util.ts @@ -0,0 +1,266 @@ +/** + * Miscellaneous utilities. + */ + +import { readdir } from 'fs/promises'; +import { dirname, join } from 'path'; +import { nextTick } from 'process'; +import { fileURLToPath } from 'url'; + +import { Octokit } from '@octokit/core'; +// @ts-ignore (no typings) +import w3cApi from 'node-w3capi'; + +import type { HandlerMessage, SpecberusConfig } from './types.js'; +import type { ValidateOptions } from './validator.js'; + +import rules from './rules.json' with { type: 'json' }; +type RuleTrack = Exclude; + +/** + * Builds a JSON result (of validation, metadata extraction, etc). + */ + +export const buildJSONresult = function ( + errors: HandlerMessage[], + warnings: HandlerMessage[], + info: HandlerMessage[], + metadata: Record +) { + return { + success: !errors.length, + errors, + warnings, + info, + metadata, + }; +}; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +async function readModuleBasenames(dir: string) { + const basenames = new Set(); + for (const filename of await readdir(dir)) { + if (filename.startsWith('.')) continue; + basenames.add(filename.replace(/(\.d)?\.[jt]s$/, '')); + } + return Array.from(basenames); +} + +const directoriesTR = await readdir(join(__dirname, 'profiles', 'TR')); +const SUBMProfiles = await readModuleBasenames( + join(__dirname, 'profiles', 'SUBM') +); + +// Get rules of each profile +const TRProfiles: string[] = []; +for (const track of directoriesTR) { + if (!track.startsWith('.')) { + const profileNames = ( + await readModuleBasenames(join(__dirname, 'profiles', 'TR', track)) + ).map(profileName => `${track}/${profileName}`); + TRProfiles.push(...profileNames); + } +} + +export const allProfiles = [ + ...TRProfiles.map(x => `TR/${x}`), + ...SUBMProfiles.map(x => `SUBM/${x}`), +]; + +export const profiles = Object.fromEntries( + allProfiles + .map(file => { + const match = + /((TR|SUBM)\/([A-Za-z]+\/)?([A-Z][A-Z-]*[A-Z](-Echidna)?))$/.exec( + file + ); + if (match && match[4]) { + const key = match[4]; + return [key, import(`./profiles/${match[0]}.js`)]; + } + return null; + }) + .filter(file => !!file) +); + +/** + * Build a function that builds an “options” object based on certain parameters. + * + * @returns {Function} a function that builds an “options” object based on an HTTP query string or a similar object containing options. + */ + +interface ProcessParamsConstraints { + allowUnknownParams?: boolean; + forbidden?: string[]; + required?: string[]; +} + +/** + * Build an “options” object based on an HTTP query string or a similar object containing options. + * + * An example of constraints: + *
{
+ *     "required": ["processDocument"],
+ *     "forbidden": ["echidnaReady", "bogusParam"],
+ *     "allowUnknownParams": true
+ * }
/blockquote> + * + * @param params - an HTTP request query, or a similar object. + * @param base - (optional) a template or “base” object to build from. + * @param constraints - (optional) an object listing “required” and/or “forbidden” parameters. + * @returns an “options” object that can be used by Specberus. + * @throws {Error} if there is an error in the parameters. + */ +export async function processParams( + params: qs.ParsedQs | ValidateOptions, + base: Partial = {}, + constraints: ProcessParamsConstraints = {} +) { + const result = JSON.parse(JSON.stringify(base)); + let originFound = false; + for (const p in params) { + if (p === 'url' || p === 'source' || p === 'file') { + // Origins: only one allowed. + if (originFound) + throw new Error( + 'Only one of parameters {“url”, “source”, “file”} is allowed' + ); + originFound = true; + result[p] = params[p]; + } else if (p === 'profile') { + // Profile: if it's a string, load the corresponding object. + if (Object.hasOwn(result, p)) + throw new Error(`Parameter “${p}” is used more than once`); + else if (typeof params[p] === 'string') { + const subPath = `/${params[p]}`; + const profilePath = allProfiles.find(p => p.endsWith(subPath)); + + if (profilePath) + result[p] = await import( + `../lib/profiles/${profilePath}.js` + ); + else if (params[p] === 'auto') result[p] = 'auto'; + else throw new Error(`Unknown profile “${params[p]}”`); + } else result[p] = params[p]; + } else if ( + p === 'validation' || + p === 'htmlValidator' || + p === 'cssValidator' || + p === 'processDocument' || + p === 'events' || + p === 'editorial' || + p === 'additionalMetadata' + ) { + // Other params: + if (Object.hasOwn(result, p)) + throw new Error(`Parameter “${p}” is used more than once`); + result[p] = (params as qs.ParsedQs)[p]; + } else if (!constraints?.allowUnknownParams) + // Illegal params: + throw new Error(`Illegal parameter “${p}”`); + } + if (!originFound) + // Origin: one required. + throw new Error( + 'One parameter of {“url”, “source”, “file”} is required' + ); + else { + if (constraints?.required) { + // Extra required params: + for (const c in constraints.required) + if (!Object.hasOwn(result, constraints.required[c])) + throw new Error( + `Parameter “${constraints.required[c]}” is required in this context` + ); + } + if (constraints?.forbidden) { + // Forbidden params: + for (const c in constraints.forbidden) + if (Object.hasOwn(result, constraints.forbidden[c])) + throw new Error( + `Parameter “${constraints.forbidden[c]}” is not allowed in this context` + ); + } + } + return result; +} + +/** Checks that the passed string is an existing specific track in rules.json. */ +export function isRuleTrack(track: string): track is RuleTrack { + return Object.hasOwn(rules, track) && track !== '*'; +} + +const githubUsernameCache: Record = + {}; +const githubUsernameCacheTimeout = 3600000; + +let octokit: Octokit; + +function cleanGithubUsernameCache() { + const now = Date.now(); + for (const [key, { time }] of Object.entries(githubUsernameCache)) { + if (now - time >= githubUsernameCacheTimeout) + delete githubUsernameCache[key]; + } +} + +function cacheGithubUsername(username: string, id: number | null) { + githubUsernameCache[username] = { id, time: Date.now() }; + nextTick(cleanGithubUsernameCache); + return id; +} + +/** + * Attempts to resolve a GitHub username to a W3C user ID. + * Returns null on 404; throws on any other 4xx/5xx response. + */ +export async function resolveGithubUsernameToId(username: string) { + if ( + typeof githubUsernameCache[username] !== 'undefined' && + Date.now() - githubUsernameCache[username].time < + githubUsernameCacheTimeout + ) + return githubUsernameCache[username].id; + + if (!octokit) octokit = new Octokit({ auth: process.env.GH_TOKEN }); + + try { + const response = await octokit.request('GET /users/{username}', { + username, + }); + const githubId = response.data.id; + const { id } = await w3cApi + .user({ type: 'github', id: githubId }) + .fetch(); + return cacheGithubUsername(username, id); + } catch (error) { + // Both octokit and w3capi errors contain status and request.url + const status = 'status' in error ? error.status : null; + if (status === 404) { + return cacheGithubUsername(username, null); + } else { + const message = `Failed to resolve GitHub user ${username}${ + status ? `: ${error.request.url} responded with ${status}` : '' + }`; + console.error(message); + throw new Error(message, { cause: username }); + } + } +} + +export const TAG = { id: 34270, type: 'other' } as const; +export const AB = { id: 7756, type: 'other' } as const; + +export const REC_TEXT = { + SOTD_P_COR: 'It includes proposed correction(s)?.', + SOTD_P_ADD: + 'It includes proposed addition(s)?, introducing new feature(s)? since the previous Recommendation.', + SOTD_P_COR_ADD: + 'It includes proposed amendment(s)?, introducing substantive change(s)? and new feature(s)? since the previous Recommendation.', + SOTD_C_COR: 'It includes candidate correction(s)?.', + SOTD_C_ADD: + 'It includes candidate addition(s)?, introducing new feature(s)? since the previous Recommendation.', + SOTD_C_COR_ADD: + 'It includes candidate amendment(s)?, introducing substantive change(s)? and new feature(s)? since the previous Recommendation.', +} as const; diff --git a/lib/validator.js b/lib/validator.js deleted file mode 100644 index e611810f2..000000000 --- a/lib/validator.js +++ /dev/null @@ -1,925 +0,0 @@ -/** - * Main file of the Specberus npm package. - * - * The most useful source: - * https://services.w3.org/xslt?xmlfile=https://www.w3.org/2005/07/13-pubrules-src.html&xslfile=https://www.w3.org/2005/07/13-pubrules-compare.xsl - */ - -import fs from 'fs'; -import { load } from 'cheerio'; -import w3cApi from 'node-w3capi'; -import { Exceptions } from './exceptions.js'; -import { assembleData, setLanguage } from './l10n.js'; -import * as profileMetadata from './profiles/metadata.js'; -import * as profileAdditionalMetadata from './profiles/additionalMetadata.js'; -import { get } from './throttled-ua.js'; -import { - AB, - buildJSONresult, - importJSON, - processParams, - REC_TEXT, - TAG, -} from './util.js'; - -/** @import { Cheerio, CheerioAPI } from "cheerio" */ -/** @import { Element } from "domhandler" */ - -const { version } = importJSON('../package.json', import.meta.url); - -setLanguage('en_GB'); - -const Specberus = function () { - this.version = version; - this.clearCache(); -}; - -Specberus.prototype.clearCache = function () { - this.docDate = null; - /** @type {Cheerio | null | undefined} */ - this.$docDateEl = null; - /** @type {Cheerio | null | undefined} */ - this.$sotdSection = undefined; - this.url = null; - this.source = null; - this.shortname = undefined; - this.delivererIDs = undefined; - this.delivererGroups = undefined; - this.exceptions = new Exceptions(); - this.chartersData = undefined; - this.charters = undefined; - this.headers = undefined; - this.isFirstPublic = undefined; - /** @type {CheerioAPI | undefined} */ - this.$ = undefined; -}; - -Specberus.prototype.extractMetadata = function (options) { - this.clearCache(); - const self = this; - - if (!options.events) - throw new Error( - '[EXCEPTION] The events option is required for reporting.' - ); - self.sink = options.events; - if (self.sink.listeners('exception').length === 0) - throw new Error( - '[WARNING] No handler for event `exception` which to report system errors.' - ); - - self.config = {}; - self.meta = {}; - const errors = []; - const warnings = []; - const infos = []; - self.sink.on('err', data => { - errors.push(data); - }); - self.sink.on('warning', data => { - warnings.push(data); - }); - self.sink.on('info', data => { - infos.push(data); - }); - /** - * @param err - * @param {CheerioAPI} $ - */ - const doMetadataExtraction = function (err, $) { - if (err) return self.throw(err); - self.$ = $; - const profile = options.additionalMetadata - ? profileAdditionalMetadata - : profileMetadata; - self.sink.emit('start-all', profile); - const total = (profile.rules || []).length; - let done = 0; - profile.rules.forEach(rule => { - try { - rule.check( - self, - function (result) { - if (result) { - for (const i in result) { - self.meta[i] = result[i]; - } - } - done += 1; - self.sink.emit('done', this.name); - if (done === total) - self.sink.emit( - 'end-all', - buildJSONresult( - errors, - warnings, - infos, - self.meta - ) - ); - }.bind(rule) - ); - } catch (e) { - self.throw(e.message); - } - }); - }; - if (options.url) this.loadURL(options.url, doMetadataExtraction); - else if (options.source) - this.loadSource(options.source, doMetadataExtraction); - else if (options.file) this.loadFile(options.file, doMetadataExtraction); - else if (options.document) - this.loadDocument(options.document, doMetadataExtraction); - else - return this.throw( - 'At least one of url, source, file, or document must be specified.' - ); -}; - -Specberus.prototype.validate = function (options) { - this.clearCache(); - const self = this; - - if (!options.events) - throw new Error( - '[EXCEPTION] The events option is required for reporting.' - ); - self.sink = options.events; - if (self.sink.listeners('exception').length === 0) - throw new Error( - '[WARNING] No handler for event `exception` which to report system errors.' - ); - - if (!options.profile) - return this.throw('Without a profile there is nothing to check.'); - const { profile } = options; - processParams(options, profile.config) - .then(config => { - self.config = config; - self.config.lang = 'en_GB'; - const errors = []; - const warnings = []; - const infos = []; - self.sink.on('err', (...data) => { - errors.push(Object.assign({}, ...data)); - }); - self.sink.on('warning', (...data) => { - warnings.push(Object.assign({}, ...data)); - }); - self.sink.on('info', (...data) => { - infos.push(Object.assign({}, ...data)); - }); - /** - * @param err - * @param {CheerioAPI} $ - */ - const doValidation = function (err, $) { - if (err) return self.throw(err); - self.$ = $; - self.sink.emit('start-all', profile.name); - const total = (profile.rules || []).length; - let done = 0; - profile.rules.forEach(rule => { - // XXX - // I would like to catch all exceptions here, but this derails the testing - // infrastructure which also uses exceptions that it expects aren't caught - rule.check( - self, - function () { - done += 1; - self.sink.emit('done', this.name); - if (done === total) - self.sink.emit( - 'end-all', - buildJSONresult(errors, warnings, infos, {}) - ); - }.bind(rule) - ); - }); - }; - if (options.url) this.loadURL(options.url, doValidation); - else if (options.source) - this.loadSource(options.source, doValidation); - else if (options.file) this.loadFile(options.file, doValidation); - else if (options.document) - this.loadDocument(options.document, doValidation); - else - return this.throw( - 'At least one of url, source, file, or document must be specified.' - ); - }) - .catch(err => this.throw(err.toString())); -}; - -Specberus.prototype.error = function (rule, key, extra) { - let name; - if (typeof rule === 'string') name = rule; - else name = rule.name; - const shortname = this.getShortname(); - if ( - shortname !== undefined && - this.exceptions.has(this.shortname, name, key, extra) - ) - this.warning(rule, key, extra); - else if (typeof rule === 'string') - this.sink.emit('err', name, { - key, - extra, - detailMessage: assembleData(null, name, key, extra).message, - }); - else - this.sink.emit('err', rule, { - key, - extra, - detailMessage: assembleData(null, rule, key, extra).message, - }); -}; - -Specberus.prototype.warning = function (rule, key, extra) { - this.sink.emit('warning', rule, { - key, - extra, - detailMessage: assembleData(null, rule, key, extra).message, - }); -}; - -Specberus.prototype.info = function (rule, key, extra) { - this.sink.emit('info', rule, { - key, - extra, - detailMessage: assembleData(null, rule, key, extra).message, - }); -}; - -Specberus.prototype.throw = function (msg) { - console.error(`[EXCEPTION] ${msg}`); - this.sink.emit('exception', { message: msg }); -}; - -Specberus.prototype.checkSelector = function (sel, name, done) { - try { - if (!this.$(sel).length) this.error(name, 'not-found'); - } catch (e) { - this.throw(`Selector '${sel}' caused the validator to blow up.`); - } - done(); -}; - -/** - * @param {string} str - */ -Specberus.prototype.norm = function (str) { - if (!str) return ''; - str = `${str}`; - return str.replace(/^\s+/, '').replace(/\s+$/, '').replace(/\s+/g, ' '); -}; - -const months = [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'October', - 'November', - 'December', -]; -const abbrMonths = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'May', - 'Jun', - 'Jul', - 'Aug', - 'Sep', - 'Oct', - 'Nov', - 'Dec', -]; - -export const possibleMonths = [...months, ...abbrMonths].join('|'); - -const separator = '[ -]{1}'; - -Specberus.prototype.dateRegexStrCapturing = `(\\d?\\d)${separator}(${possibleMonths})${separator}(\\d{4})`; -Specberus.prototype.dateRegexStrNonCapturing = `\\d?\\d${separator}(?:${possibleMonths})${separator}\\d{4}`; - -Specberus.prototype.stringToDate = function (str) { - const rex = new RegExp(this.dateRegexStrCapturing); - const matches = str.match(rex); - if (matches) { - return new Date( - matches[3] * 1, - months.indexOf(matches[2]), - matches[1] * 1 - ); - } -}; - -Specberus.prototype.getDocumentDate = function () { - if (this.docDate) return this.docDate; - const rex = new RegExp( - `${this.dateRegexStrCapturing}(?:, edited in place ${this.dateRegexStrNonCapturing})?$` - ); - const self = this; - const $el = this.$('#w3c-state'); - - const matches = $el.length && self.norm($el.text()).match(rex); - if (matches) { - self.docDate = self.stringToDate( - `${matches[1]} ${matches[2]} ${matches[3]}` - ); - self.$docDateEl = $el; - } - return this.docDate; -}; - -Specberus.prototype.getDocumentStateElement = function () { - if (this.$docDateEl) return this.$docDateEl; - this.getDocumentDate(); - return this.$docDateEl; -}; - -Specberus.prototype.getSotDSection = function () { - if (undefined !== this.$sotdSection) return this.$sotdSection; - - /** @type {Element | undefined} */ - let startH2; - /** @type {Element | undefined} */ - let endH2; - const $div = load('
', null, false)('div'); - const self = this; - const $nav = this.$('nav#toc'); - this.$('h2').each((_, h2) => { - if (startH2) { - endH2 = h2; - return false; - } - if ( - // cspell:disable-next-line - /^Status [Oo]f [Tt]his [Dd]ocument$/.test( - self.norm(this.$(h2).text()) - ) - ) { - startH2 = h2; - } - }); - if (!startH2) this.$sotdSection = null; - else { - let started = false; - this.$(startH2) - .parent() - .children() - .each((_, el) => { - if (startH2 === el) { - started = true; - return; - } - if (!started) return; - if (endH2 === el || $nav[0] === el) return false; - $div.append(el.cloneNode(true)); - }); - this.$sotdSection = $div.children().length ? $div : null; - } - if (!this.$sotdSection) - this.error( - { name: 'generic.sotd', section: 'document-status', rule: 'sotd' }, - 'not-found' - ); - return this.$sotdSection; -}; - -// The parameter`$dl` is optional. -// If not set, extractHeaders() would use the current document to extract headers link and cache them for future use. -// If set, the situation would be extract data from other document, the new element will be used and the result wouldn't be cached. -Specberus.prototype.extractHeaders = function ($dl) { - const self = this; - /** - * @type {Object. - * $dd: Cheerio - * }>} - */ - const dts = {}; - const EDITORS = /^editor(s)?$/; - const EDITORS_DRAFT = /^(latest )?editor's draft$/i; - - // If 'dl' doesn't exist, the function use 'current document' to extract. Return cached data if possible, - // If 'dl' is set, it may comes from another document, e.g. previous document, extract every time. - const extractThisDocument = !$dl; - if (extractThisDocument && this.headers !== undefined) { - return this.headers; - } - - $dl = $dl || this.$('body div.head dl'); - - if ($dl && $dl.length) { - $dl.find('dt').each((idx, dt) => { - const $dt = this.$(dt); - const txt = self - .norm($dt.text()) - .replace(':', '') - .toLowerCase() - .replace('published ', ''); - let $dd = $dt.next('dd'); - let key = null; - if (!$dd.length) - return this.throw(`No <dd> element found for ${txt}.`); - if (txt === 'this version') key = 'This'; - else if (!dts.Latest && txt.lastIndexOf('latest version', 0) === 0) - key = 'Latest'; - else if (/^history$/.test(txt)) key = 'History'; - else if (/^rescinds this recommendation?$/.test(txt)) - key = 'Rescinds'; - else if (/^implementation report?$/.test(txt)) - key = 'Implementation'; - else if (/^feedback$/.test(txt)) { - // feedback link can be multi-lines - key = 'Feedback'; - $dd = $dt.nextUntil('dt', 'dd'); - } else if (/^errata?$/.test(txt)) key = 'Errata'; - if (EDITORS_DRAFT.test(txt) && $dd.find('a').length) - key = 'EditorDraft'; - if (EDITORS.test(txt)) { - key = 'Editor'; - $dd = $dt.nextUntil('dt', 'dd'); - } - if (key) dts[key] = { pos: idx, $el: $dt, $dd }; - }); - } - this.headers = dts; - return dts; -}; - -Specberus.prototype.getShortname = function () { - if (undefined !== this.shortname) { - return this.shortname; - } - - let shortname; - const dts = this.extractHeaders(); - const $linkThis = dts.This ? dts.This.$dd.find('a') : null; - const linkThisHref = $linkThis?.attr('href') - ? $linkThis.attr('href').trim() - : ''; - const thisVersionMatches = - linkThisHref && linkThisHref.match(/.*\/[^/-]+-(.*)-\d{8}\/$/); - if (thisVersionMatches && thisVersionMatches.length > 0) - [, shortname] = thisVersionMatches; - - this.shortname = shortname; - return shortname; -}; - -// That rule tries to extract all the dates from the SOTD; -// only the dates posterior to the date of the doc and inferior to one year -// later are extracted... If there is only one, there is a good chance that it's -// the deadline for feedback. -Specberus.prototype.getFeedbackDueDate = function () { - const $sotd = this.getSotDSection(); - const dates = { list: [], valid: [] }; - if ($sotd) { - const txt = this.norm($sotd.text()); - const rex = new RegExp(this.dateRegexStrCapturing, 'g'); - const docDate = this.getDocumentDate(); - const lowBound = new Date(docDate).setDate( - new Date(docDate).getDate() + 27 - ); // minimum review period: 28 days (not counting the hours) - const highBound = new Date(docDate).setFullYear( - docDate.getFullYear() + 1 - ); - const candidates = txt.match(rex); - if (candidates !== null) { - for (let i = 0; i < candidates.length; i += 1) { - const d = this.stringToDate(candidates[i]); - if (+d >= lowBound) dates.list.push(d); - if (+d >= lowBound && +d < highBound) dates.valid.push(d); - } - } - } - return dates; -}; - -// Return array of group names, e.g. ['Internationalization Working Group', 'Technical Architecture Group'] -Specberus.prototype.getDelivererNames = function () { - const $sotd = this.getSotDSection(); - const delivererNamesRegex = - /This document was (?:produced|published) by the (.+? Working Group|.+? Interest Group|Technical Architecture Group|Advisory Board)( and the (.+? Working Group|.+? Interest Group|Technical Architecture Group|Advisory Board))? as/; - - const text = this.norm($sotd.text()); - const matches = text.match(delivererNamesRegex); - const groups = []; - if (matches) { - groups.push(matches[1]); - if (matches[3]) groups.push(matches[3]); - } - return groups; -}; - -/** - * getDelivererGroups get deliverers groupNames and types - * - * @returns {Promise} [{groupShortname: '', groupType: ''}] - */ -Specberus.prototype.getDelivererGroups = async function () { - if (undefined !== this.delivererGroups) { - return this.delivererGroups; - } - const REGEX_DELIVERER_URL = - /^https?:\/\/www\.w3\.org\/2004\/01\/pp-impl\/(\d+)\/status(#.*)?$/i; - const REGEX_DELIVERER_TEXT = - /^(charter|public\s+list\s+of\s+any\s+patent\s+disclosures(\s+\(.+\))?)$/i; - const REGEX_TAG_DISCLOSURE = - /https?:\/\/www.w3.org\/2001\/tag\/disclosures/; - const REGEX_DELIVERER_IPR_URL = - /^https:\/\/www\.w3\.org\/groups\/([^/]+)\/([^/]+)\/ipr\/?(#.*)?$/i; - const TagID = TAG.id; - - const $sotd = this.getSotDSection(); - const $sotdLinks = $sotd && $sotd.find('a[href]'); - const promiseArray = []; - let ids = []; - const delivererGroups = []; - - // getDataDelivererIDs first, apply if document is Note/Registry track. - ids = this.getDataDelivererIDs() || []; - // For rec-track - if (ids.length === 0 && $sotdLinks && $sotdLinks.length > 0) { - $sotdLinks.each((_, el) => { - const $el = this.$(el); - const href = $el.attr('href'); - const text = this.norm($el.text()); - const found = {}; - - if (REGEX_DELIVERER_TEXT.test(text)) { - if (REGEX_DELIVERER_IPR_URL.test(href)) { - // get group shortnames directly - const [, type, shortname] = - REGEX_DELIVERER_IPR_URL.exec(href); - delivererGroups.push({ - groupShortname: shortname, - groupType: type, - }); - } else { - // get group shortnames through groupId - const delivererUrlMatch = href.match(REGEX_DELIVERER_URL); - if (delivererUrlMatch) { - const id = delivererUrlMatch[1]; - if (id && id.length > 1 && !found[id]) { - found[id] = true; - ids.push(parseInt(id, 10)); - } - } else if (REGEX_TAG_DISCLOSURE.test(href)) { - ids.push(TagID); - } - } - } - }); - } - - // send request to W3C API if there's id extracted from the doc. - for (let i = 0; i < ids.length; i += 1) { - const groupApiUrl = `https://api.w3.org/groups/${ids[i]}`; - promiseArray.push( - new Promise(resolve => { - get(groupApiUrl) - .set('User-Agent', `W3C-Pubrules/${version}`) - .end((err, data) => { - resolve(data); - }); - }) - ); - } - - await Promise.all(promiseArray).then(res => { - for (let i = 0; i < res.length; i += 1) { - const data = res[i]; - if (data && data.body) { - let { type } = data.body; - switch (type) { - case 'working group': - type = 'wg'; - break; - case 'interest group': - type = 'ig'; - break; - default: - type = 'other'; - break; - } - - delivererGroups.push({ - groupShortname: data.body.shortname, - groupType: type, - }); - } - } - }); - this.delivererGroups = delivererGroups; - return delivererGroups; -}; - -Specberus.prototype.getDelivererIDs = async function () { - if (undefined !== this.delivererIDs) { - return this.delivererIDs; - } - const REGEX_DELIVERER_URL = - /^https?:\/\/www\.w3\.org\/2004\/01\/pp-impl\/(\d+)\/status(#.*)?$/i; - const REGEX_DELIVERER_TEXT = - /^(charter|public\s+list\s+of\s+any\s+patent\s+disclosures(\s+\(.+\))?)$/i; - const REGEX_TAG_DISCLOSURE = - /https?:\/\/www.w3.org\/2001\/tag\/disclosures/; - const REGEX_DELIVERER_IPR_URL = - /^https:\/\/www\.w3\.org\/groups\/([^/]+)\/([^/]+)\/ipr\/?(#.*)?$/i; - const TagID = TAG.id; - const ids = this.getDataDelivererIDs() || []; - const $sotd = this.getSotDSection(); - const $sotdLinks = $sotd && $sotd.find('a[href]'); - const promiseArray = []; - - if (ids.length === 0 && $sotdLinks && $sotdLinks.length > 0) { - $sotdLinks.each((_, el) => { - const $el = this.$(el); - const href = $el.attr('href'); - const text = this.norm($el.text()); - const found = {}; - if (REGEX_DELIVERER_TEXT.test(text)) { - const delivererUrlMatch = href.match(REGEX_DELIVERER_URL); - if (delivererUrlMatch) { - const id = delivererUrlMatch[1]; - if (id && id.length > 1 && !found[id]) { - found[id] = true; - ids.push(parseInt(id, 10)); - } - } else if (REGEX_TAG_DISCLOSURE.test(href)) { - ids.push(TagID); - } else if (REGEX_DELIVERER_IPR_URL.test(href)) { - const [, type, shortname] = - REGEX_DELIVERER_IPR_URL.exec(href); - const groupApiUrl = `https://api.w3.org/groups/${type}/${shortname}`; - promiseArray.push( - new Promise(resolve => { - get(groupApiUrl) - .set('User-Agent', `W3C-Pubrules/${version}`) - .end((err, data) => { - resolve(data); - }); - }) - ); - } - } - }); - - await Promise.all(promiseArray).then(res => { - for (const data of res) { - if (data && data.body && data.body.id) { - ids.push(data.body.id); - } - } - }); - } - this.delivererIDs = ids; - return ids; -}; - -Specberus.prototype.getDataDelivererIDs = function () { - const ids = []; - const $sotd = this.getSotDSection(); - const $deliver = $sotd && $sotd.find('[data-deliverer]'); - if ($deliver && $deliver.length > 0) { - $deliver.each((_, el) => { - const deliverers = el.attribs['data-deliverer'] - .trim() - .split(/[,\s]+/); - deliverers.forEach(id => { - if (/\d+/.test(id)) ids.push(parseInt(id, 10)); - }); - }); - } - return ids; -}; - -// Find the current charter(s) of the document. -Specberus.prototype.getChartersData = async function () { - if (undefined !== this.chartersData) return this.chartersData; - - const deliverers = await this.getDelivererIDs(); - const docDate = this.getDocumentDate(); - let groupsCharters = []; - const chartersData = []; - if (deliverers.length) { - const delivererPromises = []; - const TagID = TAG.id; - const AbID = AB.id; - // Get charter data from W3C API - // deliverers.forEach is for joint publication. - deliverers.forEach(deliverer => { - // Skip finding charter for the TAG which doesn't have any charter - if (deliverer === TagID || deliverer === AbID) return; - - delivererPromises.push( - new Promise(resolve => { - w3cApi - .group(deliverer) - .charters() - .fetch({ embed: true }, (err, charters) => { - resolve(charters); - }); - }) - ); - }); - groupsCharters = await Promise.all(delivererPromises); - - // groups -> group is for joint publication. - groupsCharters.forEach(groupCharters => { - if (groupCharters) { - groupCharters.forEach(groupCharter => { - if ( - docDate >= new Date(groupCharter.start) && - docDate <= new Date(groupCharter.end) - ) { - chartersData.push(groupCharter); - } - }); - } - }); - } - - this.chartersData = chartersData; - return chartersData; -}; - -Specberus.prototype.getCharters = async function () { - if (undefined !== this.charters) { - return this.charters; - } - - const chartersData = await this.getChartersData(); - const charters = []; - chartersData.forEach(charterData => charters.push(charterData.uri)); - this.charters = charters; - return charters; -}; - -// check if this document is a FP document. For shortname change document, data-previous-shortname="xxx" is needed. -Specberus.prototype.isFP = async function () { - if (undefined !== this.isFirstPublic) { - return this.isFirstPublic; - } - const previousLink = await this.getPreviousVersion(); - this.isFirstPublic = !previousLink; - return this.isFirstPublic; -}; - -// get previous version link from API using shortname -Specberus.prototype.getPreviousVersion = async function () { - const dts = this.extractHeaders(); - const shortname = this.shortname || (await this.getShortname()); - - if (!shortname) { - this.error( - { - name: 'generic.shortname', - section: 'front-matter', - rule: 'docIDThisVersion', - }, - 'not-found' - ); - return; - } - - function shortnameHistoryP() { - return new Promise(resolve => { - w3cApi - .specification(shortname) - .versions() - .fetch({ embed: true, items: 1000 }, (err, data) => { - if (err && err.status === 404) { - // check if it's not a shortname change - const shortnameChange = dts.History - ? dts.History.$dd - .find('a') - .attr('data-previous-shortname') - : null; - if (shortnameChange) { - w3cApi - .specification(shortnameChange) - .versions() - .fetch( - { embed: true, items: 1000 }, - (err, data) => { - resolve(data); - } - ); - } else { - resolve(); - } - } else { - resolve(data); - } - }); - }); - } - const versions = (await shortnameHistoryP()) || []; - - const linkThis = dts.This ? dts.This.$dd.find('a').attr('href') : ''; - - let previousVersion; - if (versions.length) { - const versionUris = versions.map(version => version.uri); - const index = versionUris.indexOf(linkThis); - previousVersion = - index === -1 ? versionUris[0] : versionUris[index + 1]; - } - return previousVersion; -}; - -/** - * @param {string} url - * @param {(err: any, $: CheerioAPI) => void} cb - */ -Specberus.prototype.loadURL = function (url, cb) { - if (!cb) return this.throw('Missing callback to loadURL.'); - const self = this; - get(url) - .set('User-Agent', `W3C-Pubrules/${version}`) - .end((err, res) => { - if (err) return self.throw(err.message); - if (!res.text) return self.throw(`Body of ${url} is empty.`); - self.url = url; - self.loadSource(res.text, cb); - }); -}; - -/** - * @param {string} src - * @param {(err: any, $: CheerioAPI) => void} cb - */ -Specberus.prototype.loadSource = function (src, cb) { - if (!cb) return this.throw('Missing callback to loadSource.'); - this.source = src; - let $; - try { - $ = load(src); - } catch (e) { - this.throw(`Cheerio failed to parse source: ${JSON.stringify(e)}`); - } - cb(null, $); -}; - -/** - * @param {string} file - * @param {(err: any, $?: CheerioAPI) => void} cb - */ -Specberus.prototype.loadFile = function (file, cb) { - if (!cb) return this.throw('Missing callback to loadFile.'); - const self = this; - fs.access(file, fs.constants.F_OK, errors => { - if (errors) return cb(`File '${file}' not found.`); - fs.readFile(file, { encoding: 'utf8' }, (err, src) => { - if (err) return cb(err); - self.loadSource(src, cb); - }); - }); -}; - -Specberus.prototype.loadDocument = function (doc, cb) { - if (!cb) return this.throw('Missing callback to loadDocument.'); - if (!doc) return cb('No document.'); - cb(null, (selector, context) => load(selector, context, doc)); -}; - -Specberus.prototype.transition = function (options) { - if (this.getDocumentDate() < options.from) options.doBefore(); - else if (this.getDocumentDate() > options.to) options.doAfter(); - else options.doMeanwhile(); -}; - -Specberus.prototype.getRecMetadata = function (meta) { - const sotdText = this.norm(this.getSotDSection().text()); - - // proposed corrections, proposed additions, proposed amendments: - const { SOTD_P_COR, SOTD_P_ADD, SOTD_P_COR_ADD } = REC_TEXT; - if (sotdText.match(new RegExp(`${SOTD_P_COR}|${SOTD_P_COR_ADD}`, 'i'))) - meta.pSubChanges = true; - if (sotdText.match(new RegExp(`${SOTD_P_ADD}|${SOTD_P_COR_ADD}`, 'i'))) - meta.pNewFeatures = true; - - // candidate corrections, candidate additions, candidate amendments: - const { SOTD_C_COR, SOTD_C_ADD, SOTD_C_COR_ADD } = REC_TEXT; - if (sotdText.match(new RegExp(`${SOTD_C_COR}|${SOTD_C_COR_ADD}`, 'i'))) - meta.cSubChanges = true; - if (sotdText.match(new RegExp(`${SOTD_C_ADD}|${SOTD_C_COR_ADD}`, 'i'))) - meta.cNewFeatures = true; - - return meta; -}; - -export { Specberus }; diff --git a/lib/validator.ts b/lib/validator.ts new file mode 100644 index 000000000..ddd293758 --- /dev/null +++ b/lib/validator.ts @@ -0,0 +1,959 @@ +/** + * @file Main file of the Specberus npm package. + */ + +import fs from 'fs'; +import type EventEmitter from 'events'; + +import { type Cheerio, type CheerioAPI, load } from 'cheerio'; +import type { Element } from 'domhandler'; +// @ts-ignore (no typings) +import w3cApi from 'node-w3capi'; + +import { hasExceptions } from './exceptions.js'; +import { assembleData, setLanguage } from './l10n.js'; +import * as profileMetadata from './profiles/metadata.js'; +import * as profileAdditionalMetadata from './profiles/additionalMetadata.js'; +import { get } from './throttled-ua.js'; +import { AB, buildJSONresult, processParams, REC_TEXT, TAG } from './util.js'; +import pkg from '../package.json' with { type: 'json' }; +import type { + ApiCharter, + HandlerMessage, + RuleModule, + RuleBase, + ApiSpecificationVersion, + RecMetadata, + RuleMeta, + SpecberusConfig, + ProfileModule, +} from './types.js'; + +setLanguage('en_GB'); + +interface BaseOptions { + events: EventEmitter; + file?: string; + source?: string; + url?: string; +} + +interface ExtractMetadataOptions extends BaseOptions { + additionalMetadata?: boolean; +} + +export interface ValidateOptions extends BaseOptions { + profile: ProfileModule; + validation?: 'no-validation' | 'recursive'; +} + +type HeaderMap = Record< + string, + { + pos: number; + $el: Cheerio; + $dd: Cheerio; + } +>; + +interface DelivererGroup { + groupShortname: string; + groupType: string; +} + +interface TransitionBaseOptions { + doMeanwhile: () => void; +} + +interface TransitionFromOptions { + doBefore: () => void; + from: Date; +} + +interface TransitionToOptions { + doAfter: () => void; + to: Date; +} + +type TransitionOptions = + | (TransitionBaseOptions & TransitionFromOptions) + | (TransitionBaseOptions & TransitionToOptions) + | (TransitionBaseOptions & TransitionFromOptions & TransitionToOptions); + +const months = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', +]; +const abbrMonths = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', +]; + +export const possibleMonths = [...months, ...abbrMonths].join('|'); + +const separator = '[ -]{1}'; + +export class Specberus { + $ = load(''); + config: SpecberusConfig | undefined; + // TODO(kgf): This is publicly documented, but is unused within the codebase; + // it would be better exposed as a Promise return value from extractMetadata + meta: Record | undefined; + source!: any | null; + url!: string | null; + version = pkg.version; + private $docDateEl: Cheerio | undefined; + private $sotdSection: Cheerio | null | undefined; + /** Group objects returned by W3C API charters endpoint */ + private chartersData: ApiCharter[] | undefined; + /** Charter URIs */ + private charters: string[] | undefined; + private delivererIDs: number[] | undefined; + private delivererGroups: DelivererGroup[] | undefined; + private docDate: Date | null | undefined; + private headers: HeaderMap | undefined; + private isFirstPublic: any | undefined; + private shortname: string | undefined; + private sink: EventEmitter | undefined; + + constructor() { + this.clearCache(); + } + + clearCache() { + this.$ = load(''); + this.config = undefined; + this.docDate = null; + this.$docDateEl = undefined; + this.$sotdSection = undefined; + this.url = null; + this.source = null; + this.shortname = undefined; + this.delivererIDs = undefined; + this.delivererGroups = undefined; + this.chartersData = undefined; + this.charters = undefined; + this.headers = undefined; + this.isFirstPublic = undefined; + } + + extractMetadata(options: ExtractMetadataOptions) { + this.clearCache(); + + if (!options.events) + throw new Error( + '[EXCEPTION] The events option is required for reporting.' + ); + const sink = (this.sink = options.events); + if (!this.sink.listeners('exception').length) + throw new Error( + '[WARNING] No handler for event `exception` which to report system errors.' + ); + + const meta: Record = (this.meta = {}); + const errors: HandlerMessage[] = []; + const warnings: HandlerMessage[] = []; + const infos: HandlerMessage[] = []; + sink.on('err', data => { + errors.push(data); + }); + sink.on('warning', data => { + warnings.push(data); + }); + sink.on('info', data => { + infos.push(data); + }); + /** + * @param err + * @param {CheerioAPI} $ + */ + const doMetadataExtraction = (err: any, $?: CheerioAPI) => { + if (err) return this.throw(err); + if ($) this.$ = $; + const profile = options.additionalMetadata + ? profileAdditionalMetadata + : profileMetadata; + sink.emit('start-all', profile); + const total = (profile.rules || []).length; + let done = 0; + profile.rules.forEach(rule => { + try { + rule.check(this, result => { + if (result) { + for (const i in result) { + meta[i] = result[i]; + } + } + done += 1; + sink.emit('done', rule.name); + if (done === total) + sink.emit( + 'end-all', + buildJSONresult(errors, warnings, infos, meta) + ); + }); + } catch (e) { + this.throw(e.message); + } + }); + }; + if (options.url) this.loadURL(options.url, doMetadataExtraction); + else if (options.source) + this.loadSource(options.source, doMetadataExtraction); + else if (options.file) + this.loadFile(options.file, doMetadataExtraction); + else + return this.throw( + 'At least one of url, source, file, or document must be specified.' + ); + } + + validate(options: ValidateOptions) { + this.clearCache(); + + if (!options.events) + throw new Error( + '[EXCEPTION] The events option is required for reporting.' + ); + const sink = (this.sink = options.events); + if (sink.listeners('exception').length === 0) + throw new Error( + '[WARNING] No handler for event `exception` which to report system errors.' + ); + + if (!options.profile) + return this.throw('Without a profile there is nothing to check.'); + const { profile } = options; + processParams(options, profile.config) + .then(config => { + this.config = config; + // TODO(kgf): Is this unused? We seem to hard-code it at the top of this file... + config.lang = 'en_GB'; + const errors: HandlerMessage[] = []; + const warnings: HandlerMessage[] = []; + const infos: HandlerMessage[] = []; + sink.on('err', (...data) => { + errors.push(Object.assign({}, ...data)); + }); + sink.on('warning', (...data) => { + warnings.push(Object.assign({}, ...data)); + }); + sink.on('info', (...data) => { + infos.push(Object.assign({}, ...data)); + }); + /** + * @param err + * @param {CheerioAPI} $ + */ + const doValidation = (err: any, $?: CheerioAPI) => { + if (err) return this.throw(err); + if ($) this.$ = $; + sink.emit('start-all', profile.name); + const total = (profile.rules || []).length; + let done = 0; + profile.rules.forEach((rule: RuleModule) => { + // XXX(darobin) + // I would like to catch all exceptions here, but this derails the testing + // infrastructure which also uses exceptions that it expects aren't caught + rule.check( + this, + function () { + done += 1; + sink.emit('done', rule.name); + if (done === total) + sink.emit( + 'end-all', + buildJSONresult( + errors, + warnings, + infos, + {} + ) + ); + }.bind(rule) + ); + }); + }; + if (options.url) this.loadURL(options.url, doValidation); + else if (options.source) + this.loadSource(options.source, doValidation); + else if (options.file) + this.loadFile(options.file, doValidation); + else + return this.throw( + 'At least one of url, source, file, or document must be specified.' + ); + }) + .catch(err => this.throw(err.toString())); + } + + error(rule: RuleBase | RuleMeta, key: string, extra?: Record) { + const name = typeof rule === 'string' ? rule : rule.name; + const shortname = this.getShortname(); + if ( + typeof shortname !== 'undefined' && + hasExceptions(shortname, name, extra) + ) + this.warning(rule, key, extra); + else + this.sink!.emit('err', rule, { + key, + extra, + detailMessage: assembleData(null, rule, key, extra).message, + }); + } + + warning( + rule: RuleBase | RuleMeta, + key: string, + extra?: Record + ) { + this.sink!.emit('warning', rule, { + key, + extra, + detailMessage: assembleData(null, rule, key, extra).message, + }); + } + + info(rule: RuleBase | RuleMeta, key: string, extra?: Record) { + this.sink!.emit('info', rule, { + key, + extra, + detailMessage: assembleData(null, rule, key, extra).message, + }); + } + + throw(message: string) { + console.error(`[EXCEPTION] ${message}`); + this.sink!.emit('exception', { message }); + } + + checkSelector(sel: string, rule: RuleMeta, done: () => void) { + try { + if (!this.$(sel).length) this.error(rule, 'not-found'); + } catch (e) { + this.throw(`Selector '${sel}' caused the validator to blow up.`); + } + done(); + } + + norm(str: string) { + if (!str) return ''; + return `${str}` + .replace(/^\s+/, '') + .replace(/\s+$/, '') + .replace(/\s+/g, ' '); + } + + static dateRegexStrCapturing = `(\\d?\\d)${separator}(${possibleMonths})${separator}(\\d{4})`; + static dateRegexStrNonCapturing = `\\d?\\d${separator}(?:${possibleMonths})${separator}\\d{4}`; + + stringToDate(str: string) { + const rex = new RegExp(Specberus.dateRegexStrCapturing); + const matches = str.match(rex); + if (matches) { + return new Date( + +matches[3], + months.indexOf(matches[2]), + +matches[1] + ); + } + } + + getDocumentDate() { + if (this.docDate) return this.docDate; + const rex = new RegExp( + `${Specberus.dateRegexStrCapturing}(?:, edited in place ${Specberus.dateRegexStrNonCapturing})?$` + ); + const $el = this.$('#w3c-state'); + + const matches = $el.length && this.norm($el.text()).match(rex); + if (matches) { + this.docDate = this.stringToDate( + `${matches[1]} ${matches[2]} ${matches[3]}` + ); + this.$docDateEl = $el; + } + return this.docDate; + } + + getDocumentStateElement() { + if (this.$docDateEl) return this.$docDateEl; + this.getDocumentDate(); + return this.$docDateEl; + } + + getSotDSection() { + if (typeof this.$sotdSection !== 'undefined') return this.$sotdSection; + + let startH2: Element | undefined; + let endH2: Element | undefined; + const $div = load('
', null, false)('div'); + const self = this; + const $nav = this.$('nav#toc'); + this.$('h2').each((_, h2) => { + if (startH2) { + endH2 = h2; + return false; + } + if ( + // cspell:disable-next-line + /^Status [Oo]f [Tt]his [Dd]ocument$/.test( + self.norm(this.$(h2).text()) + ) + ) { + startH2 = h2; + } + }); + if (!startH2) this.$sotdSection = null; + else { + let started = false; + this.$(startH2) + .parent() + .children() + .each((_, el) => { + if (startH2 === el) { + started = true; + return; + } + if (!started) return; + if (endH2 === el || $nav[0] === el) return false; + $div.append(el.cloneNode(true)); + }); + this.$sotdSection = $div.children().length ? $div : null; + } + if (!this.$sotdSection) + this.error( + { + name: 'generic.sotd', + section: 'document-status', + rule: 'sotd', + }, + 'not-found' + ); + return this.$sotdSection; + } + + /** + * @param $dl Optional Cheerio-wrapped dl element. + * If not set, extractHeaders() uses the current document to extract headers link and cache them for future use. + * If set, assume data is being extracted from another document; the new element will be used and the result will not be cached. + */ + extractHeaders($dl?: Cheerio) { + const dts: HeaderMap = {}; + const EDITORS = /^editor(s)?$/; + const EDITORS_DRAFT = /^(latest )?editor's draft$/i; + + if (!$dl && typeof this.headers !== 'undefined') return this.headers; + + $dl = $dl || this.$('body div.head dl'); + + if ($dl && $dl.length) { + $dl.find('dt').each((idx, dt) => { + const $dt = this.$(dt); + const txt = this.norm($dt.text()) + .replace(':', '') + .toLowerCase() + .replace('published ', ''); + let $dd = $dt.next('dd'); + let key = null; + if (!$dd.length) + return this.throw( + `No <dd> element found for ${txt}.` + ); + if (txt === 'this version') key = 'This'; + else if ( + !dts.Latest && + txt.lastIndexOf('latest version', 0) === 0 + ) + key = 'Latest'; + else if (/^history$/.test(txt)) key = 'History'; + else if (/^rescinds this recommendation?$/.test(txt)) + key = 'Rescinds'; + else if (/^implementation report?$/.test(txt)) + key = 'Implementation'; + else if (/^feedback$/.test(txt)) { + // feedback link can be multi-lines + key = 'Feedback'; + $dd = $dt.nextUntil('dt', 'dd'); + } else if (/^errata?$/.test(txt)) key = 'Errata'; + if (EDITORS_DRAFT.test(txt) && $dd.find('a').length) + key = 'EditorDraft'; + if (EDITORS.test(txt)) { + key = 'Editor'; + $dd = $dt.nextUntil('dt', 'dd'); + } + if (key) dts[key] = { pos: idx, $el: $dt, $dd }; + }); + } + this.headers = dts; + return dts; + } + + getShortname() { + if (typeof this.shortname !== 'undefined') return this.shortname; + + let shortname; + const dts = this.extractHeaders(); + const $linkThis = dts.This ? dts.This.$dd.find('a') : null; + const linkThisHref = $linkThis?.attr('href')?.trim() || ''; + const thisVersionMatches = + linkThisHref && linkThisHref.match(/.*\/[^/-]+-(.*)-\d{8}\/$/); + if (thisVersionMatches && thisVersionMatches.length > 0) + [, shortname] = thisVersionMatches; + + this.shortname = shortname; + return shortname; + } + + /** + * Attempts to extract all the dates from the SOTD. + * Only the dates after date of the doc and prior to one year later + * are extracted. If there is only one, there is a good chance that it's + * the deadline for feedback. + */ + getFeedbackDueDate() { + const $sotd = this.getSotDSection(); + const dates = { list: [] as Date[], valid: [] as Date[] }; + if ($sotd) { + const txt = this.norm($sotd.text()); + const rex = new RegExp(Specberus.dateRegexStrCapturing, 'g'); + const docDate = this.getDocumentDate()!; + const lowBound = new Date(docDate).setDate( + new Date(docDate).getDate() + 27 + ); // minimum review period: 28 days (not counting the hours) + const highBound = new Date(docDate).setFullYear( + docDate.getFullYear() + 1 + ); + const candidates = txt.match(rex); + if (candidates !== null) { + for (let i = 0; i < candidates.length; i += 1) { + const d = this.stringToDate(candidates[i]); + if (typeof d !== 'undefined' && +d >= lowBound) { + dates.list.push(d); + if (+d < highBound) dates.valid.push(d); + } + } + } + } + return dates; + } + + // Return array of group names, e.g. ['Internationalization Working Group', 'Technical Architecture Group'] + getDelivererNames() { + const $sotd = this.getSotDSection(); + const delivererNamesRegex = + /This document was (?:produced|published) by the (.+? Working Group|.+? Interest Group|Technical Architecture Group|Advisory Board)( and the (.+? Working Group|.+? Interest Group|Technical Architecture Group|Advisory Board))? as/; + + const text = this.norm($sotd!.text()); + const matches = text.match(delivererNamesRegex); + const groups = []; + if (matches) { + groups.push(matches[1]); + if (matches[3]) groups.push(matches[3]); + } + return groups; + } + + /** + * Retrieves deliverers groupNames and types. + */ + async getDelivererGroups() { + if (typeof this.delivererGroups !== 'undefined') + return this.delivererGroups; + const REGEX_DELIVERER_URL = + /^https?:\/\/www\.w3\.org\/2004\/01\/pp-impl\/(\d+)\/status(#.*)?$/i; + const REGEX_DELIVERER_TEXT = + /^(charter|public\s+list\s+of\s+any\s+patent\s+disclosures(\s+\(.+\))?)$/i; + const REGEX_TAG_DISCLOSURE = + /https?:\/\/www.w3.org\/2001\/tag\/disclosures/; + const REGEX_DELIVERER_IPR_URL = + /^https:\/\/www\.w3\.org\/groups\/([^/]+)\/([^/]+)\/ipr\/?(#.*)?$/i; + + const $sotd = this.getSotDSection(); + const $sotdLinks = $sotd && $sotd.find('a[href]'); + const promiseArray: Promise[] = []; + let ids = []; + const delivererGroups: DelivererGroup[] = []; + + // getDataDelivererIDs first, apply if document is Note/Registry track. + ids = this.getDataDelivererIDs() || []; + // For rec-track + if (ids.length === 0 && $sotdLinks && $sotdLinks.length > 0) { + $sotdLinks.each((_, el) => { + const $el = this.$(el); + const href = $el.attr('href')!; + const text = this.norm($el.text()); + const found: Record = {}; + + if (REGEX_DELIVERER_TEXT.test(text)) { + if (REGEX_DELIVERER_IPR_URL.test(href)) { + // get group shortnames directly + const [, type, shortname] = + REGEX_DELIVERER_IPR_URL.exec(href)!; + delivererGroups.push({ + groupShortname: shortname, + groupType: type, + }); + } else { + // get group shortnames through groupId + const delivererUrlMatch = + href.match(REGEX_DELIVERER_URL); + if (delivererUrlMatch) { + const id = delivererUrlMatch[1]; + if (id && id.length > 1 && !found[id]) { + found[id] = true; + ids.push(parseInt(id, 10)); + } + } else if (REGEX_TAG_DISCLOSURE.test(href)) { + ids.push(TAG.id); + } + } + } + }); + } + + // send request to W3C API if there's id extracted from the doc. + for (let i = 0; i < ids.length; i += 1) { + const groupApiUrl = `https://api.w3.org/groups/${ids[i]}`; + promiseArray.push( + new Promise(resolve => { + get(groupApiUrl) + .set('User-Agent', `W3C-Pubrules/${pkg.version}`) + .end((_, data) => { + resolve(data); + }); + }) + ); + } + + await Promise.all(promiseArray).then(res => { + for (let i = 0; i < res.length; i += 1) { + const data = res[i]; + if (data && data.body) { + let { type } = data.body; + switch (type) { + case 'working group': + type = 'wg'; + break; + case 'interest group': + type = 'ig'; + break; + default: + type = 'other'; + break; + } + + delivererGroups.push({ + groupShortname: data.body.shortname, + groupType: type, + }); + } + } + }); + this.delivererGroups = delivererGroups; + return delivererGroups; + } + + async getDelivererIDs() { + if (undefined !== this.delivererIDs) { + return this.delivererIDs; + } + const REGEX_DELIVERER_URL = + /^https?:\/\/www\.w3\.org\/2004\/01\/pp-impl\/(\d+)\/status(#.*)?$/i; + const REGEX_DELIVERER_TEXT = + /^(charter|public\s+list\s+of\s+any\s+patent\s+disclosures(\s+\(.+\))?)$/i; + const REGEX_TAG_DISCLOSURE = + /https?:\/\/www.w3.org\/2001\/tag\/disclosures/; + const REGEX_DELIVERER_IPR_URL = + /^https:\/\/www\.w3\.org\/groups\/([^/]+)\/([^/]+)\/ipr\/?(#.*)?$/i; + const ids: number[] = this.getDataDelivererIDs() || []; + const $sotd = this.getSotDSection(); + const $sotdLinks = $sotd && $sotd.find('a[href]'); + const promiseArray: Promise[] = []; + + if (ids.length === 0 && $sotdLinks && $sotdLinks.length > 0) { + $sotdLinks.each((_, el) => { + const $el = this.$(el); + const href = $el.attr('href')!; + const text = this.norm($el.text()); + const found: Record = {}; + if (REGEX_DELIVERER_TEXT.test(text)) { + const delivererUrlMatch = href.match(REGEX_DELIVERER_URL); + if (delivererUrlMatch) { + const id = delivererUrlMatch[1]; + if (id && id.length > 1 && !found[id]) { + found[id] = true; + ids.push(parseInt(id, 10)); + } + } else if (REGEX_TAG_DISCLOSURE.test(href)) { + ids.push(TAG.id); + } else if (REGEX_DELIVERER_IPR_URL.test(href)) { + const [, type, shortname] = + REGEX_DELIVERER_IPR_URL.exec(href)!; + const groupApiUrl = `https://api.w3.org/groups/${type}/${shortname}`; + promiseArray.push( + new Promise(resolve => { + get(groupApiUrl) + .set( + 'User-Agent', + `W3C-Pubrules/${pkg.version}` + ) + .end((_: any, data) => { + resolve(data); + }); + }) + ); + } + } + }); + + await Promise.all(promiseArray).then(res => { + for (const data of res) { + if (data?.body?.id) ids.push(data.body.id); + } + }); + } + this.delivererIDs = ids; + return ids; + } + + getDataDelivererIDs() { + const ids: number[] = []; + const $sotd = this.getSotDSection(); + const $deliver = $sotd && $sotd.find('[data-deliverer]'); + if ($deliver && $deliver.length > 0) { + $deliver.each((_, el) => { + const deliverers = el.attribs['data-deliverer'] + .trim() + .split(/[,\s]+/); + deliverers.forEach(id => { + if (/\d+/.test(id)) ids.push(parseInt(id, 10)); + }); + }); + } + return ids; + } + + /** Finds the current charter(s) of the document. */ + async getChartersData() { + if (undefined !== this.chartersData) return this.chartersData; + + const deliverers = await this.getDelivererIDs(); + const docDate = this.getDocumentDate()!; + const chartersData: ApiCharter[] = []; + if (deliverers.length) { + const delivererPromises: Promise[] = []; + const AbID = AB.id; + // Get charter data from W3C API + // deliverers.forEach is for joint publication. + deliverers.forEach(deliverer => { + // Skip finding charter for the TAG which doesn't have any charter + if (deliverer === TAG.id || deliverer === AbID) return; + + delivererPromises.push( + new Promise(resolve => { + w3cApi + .group(deliverer) + .charters() + .fetch( + { embed: true }, + (_: any, charters: ApiCharter[]) => { + resolve(charters); + } + ); + }) + ); + }); + + // groups -> group is for joint publication. + for (const groupCharters of await Promise.all(delivererPromises)) { + if (groupCharters) { + for (const groupCharter of groupCharters) { + if ( + docDate >= new Date(groupCharter.start) && + docDate <= new Date(groupCharter.end) + ) { + chartersData.push(groupCharter); + } + } + } + } + } + + this.chartersData = chartersData; + return chartersData; + } + + async getCharters() { + if (typeof this.charters !== 'undefined') return this.charters; + + this.charters = (await this.getChartersData()).map(({ uri }) => uri); + return this.charters; + } + + /** + * Checks if this document is a FP document. + * For shortname change document, data-previous-shortname attribute is needed. + */ + async isFP() { + if (typeof this.isFirstPublic !== 'undefined') + return this.isFirstPublic; + + this.isFirstPublic = !(await this.getPreviousVersion()); + return this.isFirstPublic; + } + + /** + * Gets previous version link from API via shortname. + */ + async getPreviousVersion() { + const dts = this.extractHeaders(); + const shortname = this.shortname || (await this.getShortname()); + + if (!shortname) { + this.error( + { + name: 'generic.shortname', + section: 'front-matter', + rule: 'docIDThisVersion', + }, + 'not-found' + ); + return; + } + + const shortnameHistory = await new Promise< + ApiSpecificationVersion[] | null + >(resolve => { + w3cApi + .specification(shortname) + .versions() + .fetch( + { embed: true, items: 1000 }, + (err: any, data: ApiSpecificationVersion[]) => { + if (err && err.status === 404) { + // check if it's not a shortname change + const shortnameChange = dts.History + ? dts.History.$dd + .find('a') + .attr('data-previous-shortname') + : null; + if (shortnameChange) { + w3cApi + .specification(shortnameChange) + .versions() + .fetch( + { embed: true, items: 1000 }, + ( + _: any, + data: ApiSpecificationVersion[] + ) => { + resolve(data); + } + ); + } else { + resolve(null); + } + } else { + resolve(data); + } + } + ); + }); + const versions = shortnameHistory || []; + const linkThis = dts.This ? dts.This.$dd.find('a').attr('href') : ''; + + if (versions.length && linkThis) { + const versionUris = versions.map(({ uri }) => uri); + const index = versionUris.indexOf(linkThis); + return index === -1 ? versionUris[0] : versionUris[index + 1]; + } + } + + loadURL(url: string, cb: (err: any, $?: CheerioAPI) => void) { + if (!cb) return this.throw('Missing callback to loadURL.'); + get(url) + .set('User-Agent', `W3C-Pubrules/${pkg.version}`) + .end((err, res) => { + if (err) return this.throw(err.message); + if (!res.text) return this.throw(`Body of ${url} is empty.`); + this.url = url; + this.loadSource(res.text, cb); + }); + } + + loadSource(src: string, cb: (err: Error | null, $?: CheerioAPI) => void) { + if (!cb) return this.throw('Missing callback to loadSource.'); + this.source = src; + let $: CheerioAPI; + try { + $ = load(src); + } catch (e) { + return this.throw( + `Cheerio failed to parse source: ${JSON.stringify(e)}` + ); + } + cb(null, $); + } + + loadFile(file: string, cb: (err: any, $?: CheerioAPI) => void) { + if (!cb) return this.throw('Missing callback to loadFile.'); + fs.access(file, fs.constants.F_OK, errors => { + if (errors) return cb(`File '${file}' not found.`); + fs.readFile(file, { encoding: 'utf8' }, (err, src) => { + if (err) return cb(err); + this.loadSource(src, cb); + }); + }); + } + + transition(options: TransitionOptions) { + const documentDate = this.getDocumentDate(); + if (documentDate && 'from' in options && documentDate < options.from) + options.doBefore(); + else if (documentDate && 'to' in options && documentDate > options.to) + options.doAfter(); + else options.doMeanwhile(); + } + + getRecMetadata(meta: RecMetadata = {}) { + const sotdText = this.norm(this.getSotDSection()!.text()); + + // proposed corrections, proposed additions, proposed amendments: + const { SOTD_P_COR, SOTD_P_ADD, SOTD_P_COR_ADD } = REC_TEXT; + if (sotdText.match(new RegExp(`${SOTD_P_COR}|${SOTD_P_COR_ADD}`, 'i'))) + meta.pSubChanges = true; + if (sotdText.match(new RegExp(`${SOTD_P_ADD}|${SOTD_P_COR_ADD}`, 'i'))) + meta.pNewFeatures = true; + + // candidate corrections, candidate additions, candidate amendments: + const { SOTD_C_COR, SOTD_C_ADD, SOTD_C_COR_ADD } = REC_TEXT; + if (sotdText.match(new RegExp(`${SOTD_C_COR}|${SOTD_C_COR_ADD}`, 'i'))) + meta.cSubChanges = true; + if (sotdText.match(new RegExp(`${SOTD_C_ADD}|${SOTD_C_COR_ADD}`, 'i'))) + meta.cNewFeatures = true; + + return meta; + } +} diff --git a/lib/views.js b/lib/views.ts similarity index 51% rename from lib/views.js rename to lib/views.ts index 0c711d6c2..e1a71f7ce 100644 --- a/lib/views.js +++ b/lib/views.ts @@ -1,11 +1,16 @@ -// Internal packages: +import type { Express, Request, Response } from 'express'; import handlebars from 'express-handlebars'; -import qs from 'querystring'; -import { importJSON } from './util.js'; +import { escape } from 'querystring'; -/** @import {Express} from "express" */ +import type { + GenericRulesSection, + RulesProfile, + RulesSection, +} from './types.js'; +import { isRuleTrack } from './util.js'; -const rules = importJSON('./rules.json', import.meta.url); +import pkg from '../package.json' with { type: 'json' }; +import rules from './rules.json' with { type: 'json' }; // Settings: const DEBUG = process && process.env && process.env.DEBUG; @@ -14,100 +19,101 @@ const BASE_URI = `${process.env.BASE_URI ? process.env.BASE_URI : ''}/`.replace( '/' ); -const { version } = importJSON('../package.json', import.meta.url); const nav = `

Site map · Github · Help

`; -/** - * @TODO Document. - */ -const serveStraight = function (req, res) { +// TODO(tripu): Document. +const serveStraight = function (req: Request, res: Response) { const fragment = req.path.replace(/^\/|\/$/gi, ''); res.render(fragment, { DEBUG, BASE_URI, - version, + version: pkg.version, nav, title: fragment, }); }; -/** - * @TODO Document. - */ - -const handleWrongPage = function (req, res) { +// TODO(tripu): Document. +const handleWrongPage = function (_: Request, res: Response) { res.render('error', { DEBUG, BASE_URI, - version, + version: pkg.version, nav, title: 'whut?', }); }; -/** - * @TODO Document. - */ +/** Profile object as returned by listProfiles (distinct from rules.json) */ +interface Profile { + order: number; + abbr: string; + name: string; +} -const listProfiles = function () { +// TODO(tripu): Document. +function listProfiles() { const result = []; - const sortByOrderField = (a, b) => { + const sortByOrderField = (a: Profile, b: Profile) => { + if (!a.order || !b.order) return 0; if (a.order < b.order) return -1; - if (a.order > b.order) return +1; + if (a.order > b.order) return 1; return 0; }; for (const t in rules) { - if (t !== '*') { + if (isRuleTrack(t)) { + const rule = rules[t]; result.push({ - order: rules[t].order, + order: rule.order, abbr: t, - name: rules[t].name, - profiles: [], + name: rule.name, + profiles: [] as Profile[], }); - for (const p in rules[t].profiles) + for (const [p, profile] of Object.entries(rule.profiles)) result[result.length - 1].profiles.push({ - order: rules[t].profiles[p].order, + order: profile.order, abbr: p, - name: rules[t].profiles[p].name, + name: profile.name, }); result[result.length - 1].profiles.sort(sortByOrderField); } result.sort(sortByOrderField); } return result; -}; - +} export const sortedProfiles = listProfiles(); -/** - * @TODO Document. - */ -const formatRules = function (sections, common) { +// TODO(tripu): Document. +function formatRules(sections: Record) { + const commonSections = rules['*'].sections as Record< + string, + GenericRulesSection + >; const total = []; for (const s in sections) { let result = `

${sections[s].name}

    `; - for (const r in sections[s].rules) - if (typeof sections[s].rules[r] === 'boolean') { + for (const [r, rValue] of Object.entries(sections[s].rules)) + if (typeof rValue === 'boolean') { // Common rule, with no parameters result += `
  • § - ${common.sections[s].rules[r]} + ${commonSections[s].rules[r]}
  • `; - } else if (typeof sections[s].rules[r] === 'string') { + } else if (typeof rValue === 'string') { // Specific rule result += `
  • § - ${sections[s].rules[r]} + ${rValue}
  • `; - } else if (typeof sections[s].rules[r] === 'object') { + } else if (typeof rValue === 'object') { // Array (common rule with parameters) - const values = sections[s].rules[r]; - let template = common.sections[s].rules[r]; + const values = rValue; + let template = commonSections[s].rules[r]; for (let p = 0; p < values.length; p += 1) template = template.replace( new RegExp(`@{param${p + 1}}`, 'g'), @@ -134,69 +140,74 @@ const formatRules = function (sections, common) { }) .map(a => a.content) .join(''); -}; +} -/** - * @TODO Document. - */ +interface RetrieveProfileResult { + abbr: string; + name: string; + body: string; +} -const retrieveProfile = function (query) { - const result = {}; +// TODO(tripu): Document. +function retrieveProfile(query: qs.ParsedQs) { + let result: RetrieveProfileResult | { error: string } | undefined; - if (query && query.profile) { - const codename = qs.escape(query.profile).trim().toUpperCase(); + if (query && query.profile && typeof query.profile === 'string') { + const codename = escape(query.profile).trim().toUpperCase(); if (rules) - for (const t in rules) - if ( - t !== '*' && - Object.prototype.hasOwnProperty.call( - rules[t].profiles, - codename - ) - ) { - const profile = rules[t].profiles[codename]; - result.abbr = codename; - result.name = `${profile.name}`; - result.body = formatRules(profile.sections, rules['*']); + for (const t in rules) { + if (isRuleTrack(t)) { + const profiles = rules[t].profiles; + if (Object.hasOwn(profiles, codename)) { + const profile = profiles[ + codename as keyof typeof profiles + ] as RulesProfile; + result = { + abbr: codename, + name: `${profile.name}`, + body: formatRules(profile.sections), + }; + } } - if (Object.keys(result).length === 0) - result.error = `

    Error: unknown profile ${qs.escape( - query.profile - )}.

    \n`; + } + if (!result) + result = { + error: `

    Error: unknown profile ${escape( + query.profile + )}.

    \n`, + }; } else - result.error = - '

    Error: no profile specified. Try appending eg ?profile=WD to the URL.

    \n'; + result = { + error: '

    Error: no profile specified. Try appending eg ?profile=WD to the URL.

    \n', + }; return result; -}; +} /** * Set up HTML views using templates and Express Handlebars. - * - * @param {Express} app - the Express application. */ - -export const setUp = function (app) { +export function setUp(app: Express) { const hb = handlebars.create({ defaultLayout: 'main' }); app.engine('handlebars', hb.engine); app.set('view engine', 'handlebars'); - app.get('/', (req, res) => { + app.get('/', (_, res) => { res.render('index', { DEBUG, BASE_URI, - version, + version: pkg.version, nav, interactive: true, tracks: sortedProfiles, }); }); - app.get('/doc', (req, res) => { + app.get('/doc', (_, res) => { res.render('doc', { DEBUG, BASE_URI, - version, + version: pkg.version, nav, title: 'documentation', tracks: sortedProfiles, @@ -207,7 +218,7 @@ export const setUp = function (app) { res.render('doc/rules', { DEBUG, BASE_URI, - version, + version: pkg.version, nav, title: 'publication rules', content: retrieveProfile(req.query), @@ -221,4 +232,4 @@ export const setUp = function (app) { // Catch-all: app.get(/(.*)/, handleWrongPage); -}; +} diff --git a/package-lock.json b/package-lock.json index f5a405a96..805e0aeb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,17 +31,16 @@ "devDependencies": { "@rvagg/chai-as-promised": "8.0.2", "@types/express": "^5.0.6", + "@types/express-fileupload": "^1.5.1", "@types/mocha": "^10.0.10", + "@types/morgan": "^1.9.10", "@types/node": "^24.10.9", "@types/superagent": "^8.1.9", + "@types/tmp": "^0.2.6", "c8": "^11.0.0", "chai": "6.2.2", "cspell": "9.0.2", "domhandler": "^6.0.1", - "eslint": "10.0.3", - "eslint-config-prettier": "10.0.3", - "eslint-plugin-node": "11.1.0", - "eslint-plugin-prettier": "5.0.1", "expect.js": "0.3", "husky": "9.0.11", "lint-staged": "16.4.0", @@ -50,120 +49,37 @@ "nock": "15.0.0", "nodemon": "3.0.3", "prettier": "3.8.4", - "typescript": "^5.9.3" + "tsx": "^4.21.0", + "typescript": "^6.0.2" }, "engines": { "node": "20 || 22 || 24", "npm": ">=7" } }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@bcoe/v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", @@ -332,19 +248,19 @@ "license": "MIT" }, "node_modules/@cspell/dict-bash": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@cspell/dict-bash/-/dict-bash-4.2.2.tgz", - "integrity": "sha512-kyWbwtX3TsCf5l49gGQIZkRLaB/P8g73GDRm41Zu8Mv51kjl2H7Au0TsEvHv7jzcsRLS6aUYaZv6Zsvk1fOz+Q==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@cspell/dict-bash/-/dict-bash-4.2.3.tgz", + "integrity": "sha512-ljUZoKHbDqw5Sx0qpL2qTUlmkmr+vhZH/sCNrNaBZKTbdgiswErSnIF1jRbGmEitJNxHRHWsuZyVgnTGfVO1Yw==", "dev": true, "license": "MIT", "dependencies": { - "@cspell/dict-shell": "1.1.2" + "@cspell/dict-shell": "1.2.0" } }, "node_modules/@cspell/dict-companies": { - "version": "3.2.10", - "resolved": "https://registry.npmjs.org/@cspell/dict-companies/-/dict-companies-3.2.10.tgz", - "integrity": "sha512-bJ1qnO1DkTn7JYGXvxp8FRQc4yq6tRXnrII+jbP8hHmq5TX5o1Wu+rdfpoUQaMWTl6balRvcMYiINDesnpR9Bw==", + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/@cspell/dict-companies/-/dict-companies-3.2.11.tgz", + "integrity": "sha512-0cmafbcz2pTHXLd59eLR1gvDvN6aWAOM0+cIL4LLF9GX9yB2iKDNrKsvs4tJRqutoaTdwNFBbV0FYv+6iCtebQ==", "dev": true, "license": "MIT" }, @@ -370,9 +286,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-css": { - "version": "4.0.19", - "resolved": "https://registry.npmjs.org/@cspell/dict-css/-/dict-css-4.0.19.tgz", - "integrity": "sha512-VYHtPnZt/Zd/ATbW3rtexWpBnHUohUrQOHff/2JBhsVgxOrksAxJnLAO43Q1ayLJBJUUwNVo+RU0sx0aaysZfg==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@cspell/dict-css/-/dict-css-4.1.2.tgz", + "integrity": "sha512-+ylGoKdwZ2sVOCOnU2Eq5wDZx+RaVX3HoKyNHGGsFvhSw6IidQ6tH/mAPKBDofViHJoWCPNlklE0lTr6MDG3QA==", "dev": true, "license": "MIT" }, @@ -384,9 +300,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-data-science": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/@cspell/dict-data-science/-/dict-data-science-2.0.13.tgz", - "integrity": "sha512-l1HMEhBJkPmw4I2YGVu2eBSKM89K9pVF+N6qIr5Uo5H3O979jVodtuwP8I7LyPrJnC6nz28oxeGRCLh9xC5CVA==", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/@cspell/dict-data-science/-/dict-data-science-2.0.14.tgz", + "integrity": "sha512-jl6Ds4u5u5JT+yY30pWQpAbdCHfy3lCcNkLbpL/AZKoUaLEoXbaYsps9xQtvD7DyaiXxiLZkdH2yHHXtoFtZyg==", "dev": true, "license": "MIT" }, @@ -405,9 +321,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-dotnet": { - "version": "5.0.11", - "resolved": "https://registry.npmjs.org/@cspell/dict-dotnet/-/dict-dotnet-5.0.11.tgz", - "integrity": "sha512-LSVKhpFf/ASTWJcfYeS0Sykcl1gVMsv2Z5Eo0TnTMSTLV3738HH+66pIsjUTChqU6SF3gKPuCe6EOaRYqb/evA==", + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/@cspell/dict-dotnet/-/dict-dotnet-5.0.13.tgz", + "integrity": "sha512-xPp7jMnFpOri7tzmqmm/dXMolXz1t2bhNqxYkOyMqXhvs08oc7BFs+EsbDY0X7hqiISgeFZGNqn0dOCr+ncPYw==", "dev": true, "license": "MIT" }, @@ -419,9 +335,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-en_us": { - "version": "4.4.29", - "resolved": "https://registry.npmjs.org/@cspell/dict-en_us/-/dict-en_us-4.4.29.tgz", - "integrity": "sha512-G3B27++9ziRdgbrY/G/QZdFAnMzzx17u8nCb2Xyd4q6luLpzViRM/CW3jA+Mb/cGT5zR/9N+Yz9SrGu1s0bq7g==", + "version": "4.4.35", + "resolved": "https://registry.npmjs.org/@cspell/dict-en_us/-/dict-en_us-4.4.35.tgz", + "integrity": "sha512-xWpxBCc/FzzMMo/A+0qwARVaIIhR0Ql8yhhv4rvsvg+GfQF+LG9yzg2GwTM5N2rjvzmM3nKuR9zxFZq2I6fJSg==", "dev": true, "license": "MIT" }, @@ -433,16 +349,16 @@ "license": "CC BY-SA 4.0" }, "node_modules/@cspell/dict-en-gb-mit": { - "version": "3.1.18", - "resolved": "https://registry.npmjs.org/@cspell/dict-en-gb-mit/-/dict-en-gb-mit-3.1.18.tgz", - "integrity": "sha512-AXaMzbaxhSc32MSzKX0cpwT+Thv1vPfxQz1nTly1VHw3wQcwPqVFSqrLOYwa8VNqAPR45583nnhD6iqJ9YESoQ==", + "version": "3.1.24", + "resolved": "https://registry.npmjs.org/@cspell/dict-en-gb-mit/-/dict-en-gb-mit-3.1.24.tgz", + "integrity": "sha512-Oowb/Uzkh7OmDRdCcETzMc9imEb4IpLlHJXoYjX8A8DS2X/54gqSjI915JFB8hKtFjBko5OM0BLQ+6cZhFEMmQ==", "dev": true, "license": "MIT" }, "node_modules/@cspell/dict-filetypes": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@cspell/dict-filetypes/-/dict-filetypes-3.0.15.tgz", - "integrity": "sha512-uDMeqYlLlK476w/muEFQGBy9BdQWS0mQ7BJiy/iQv5XUWZxE2O54ZQd9nW8GyQMzAgoyg5SG4hf9l039Qt66oA==", + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/@cspell/dict-filetypes/-/dict-filetypes-3.0.18.tgz", + "integrity": "sha512-yU7RKD/x1IWmDLzWeiItMwgV+6bUcU/af23uS0+uGiFUbsY1qWV/D4rxlAAO6Z7no3J2z8aZOkYIOvUrJq0Rcw==", "dev": true, "license": "MIT" }, @@ -454,9 +370,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-fonts": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@cspell/dict-fonts/-/dict-fonts-4.0.5.tgz", - "integrity": "sha512-BbpkX10DUX/xzHs6lb7yzDf/LPjwYIBJHJlUXSBXDtK/1HaeS+Wqol4Mlm2+NAgZ7ikIE5DQMViTgBUY3ezNoQ==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@cspell/dict-fonts/-/dict-fonts-4.0.6.tgz", + "integrity": "sha512-aR/0csY01dNb0A1tw/UmN9rKgHruUxsYsvXu6YlSBJFu60s26SKr/k1o4LavpHTQ+lznlYMqAvuxGkE4Flliqw==", "dev": true, "license": "MIT" }, @@ -468,9 +384,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-fullstack": { - "version": "3.2.8", - "resolved": "https://registry.npmjs.org/@cspell/dict-fullstack/-/dict-fullstack-3.2.8.tgz", - "integrity": "sha512-J6EeoeThvx/DFrcA2rJiCA6vfqwJMbkG0IcXhlsmRZmasIpanmxgt90OEaUazbZahFiuJT8wrhgQ1QgD1MsqBw==", + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/@cspell/dict-fullstack/-/dict-fullstack-3.2.9.tgz", + "integrity": "sha512-diZX+usW5aZ4/b2T0QM/H/Wl9aNMbdODa1Jq0ReBr/jazmNeWjd+PyqeVgzd1joEaHY+SAnjrf/i9CwKd2ZtWQ==", "dev": true, "license": "MIT" }, @@ -510,9 +426,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-html": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@cspell/dict-html/-/dict-html-4.0.14.tgz", - "integrity": "sha512-2bf7n+kS92g+cMKV0wr9o/Oq9n8JzU7CcrB96gIh2GHgnF+0xDOqO2W/1KeFAqOfqosoOVE48t+4dnEMkkoJ2Q==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@cspell/dict-html/-/dict-html-4.0.15.tgz", + "integrity": "sha512-GJYnYKoD9fmo2OI0aySEGZOjThnx3upSUvV7mmqUu8oG+mGgzqm82P/f7OqsuvTaInZZwZbo+PwJQd/yHcyFIw==", "dev": true, "license": "MIT" }, @@ -580,14 +496,14 @@ "license": "MIT" }, "node_modules/@cspell/dict-markdown": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/@cspell/dict-markdown/-/dict-markdown-2.0.14.tgz", - "integrity": "sha512-uLKPNJsUcumMQTsZZgAK9RgDLyQhUz/uvbQTEkvF/Q4XfC1i/BnA8XrOrd0+Vp6+tPOKyA+omI5LRWfMu5K/Lw==", + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@cspell/dict-markdown/-/dict-markdown-2.0.17.tgz", + "integrity": "sha512-H8bAxih6U8NOnSPL7R8My+tqjaB4tmnJTjERuz4zYqmf+cH+5xshX3UVgKlwWFcyjsYfv/zEDuRdMctQv1q6HQ==", "dev": true, "license": "MIT", "peerDependencies": { - "@cspell/dict-css": "^4.0.19", - "@cspell/dict-html": "^4.0.14", + "@cspell/dict-css": "^4.1.2", + "@cspell/dict-html": "^4.0.15", "@cspell/dict-html-symbol-entities": "^4.0.5", "@cspell/dict-typescript": "^3.2.3" } @@ -607,9 +523,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-npm": { - "version": "5.2.33", - "resolved": "https://registry.npmjs.org/@cspell/dict-npm/-/dict-npm-5.2.33.tgz", - "integrity": "sha512-U1gfDxdFR6nnojvtdkF2Ati3jfIlnW5nJkFb2jS1JunlhrSYdZXwz/4bI//h1W3aaeYQoSlvTIqk3vlnIDrNng==", + "version": "5.2.41", + "resolved": "https://registry.npmjs.org/@cspell/dict-npm/-/dict-npm-5.2.41.tgz", + "integrity": "sha512-To3xsfRmMBYVXtWVEdUgV35M9a/JZ54dSuoY6m6D3uHKKL3I326Wmy4xifZ3PU8MQaWhyEH7zbIcUEtKwTQMcA==", "dev": true, "license": "MIT" }, @@ -628,20 +544,20 @@ "license": "MIT" }, "node_modules/@cspell/dict-public-licenses": { - "version": "2.0.15", - "resolved": "https://registry.npmjs.org/@cspell/dict-public-licenses/-/dict-public-licenses-2.0.15.tgz", - "integrity": "sha512-cJEOs901H13Pfy0fl4dCD1U+xpWIMaEPq8MeYU83FfDZvellAuSo4GqWCripfIqlhns/L6+UZEIJSOZnjgy7Wg==", + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@cspell/dict-public-licenses/-/dict-public-licenses-2.0.16.tgz", + "integrity": "sha512-EQRrPvEOmwhwWezV+W7LjXbIBjiy6y/shrET6Qcpnk3XANTzfvWflf9PnJ5kId/oKWvihFy0za0AV1JHd03pSQ==", "dev": true, "license": "MIT" }, "node_modules/@cspell/dict-python": { - "version": "4.2.25", - "resolved": "https://registry.npmjs.org/@cspell/dict-python/-/dict-python-4.2.25.tgz", - "integrity": "sha512-hDdN0YhKgpbtZVRjQ2c8jk+n0wQdidAKj1Fk8w7KEHb3YlY5uPJ0mAKJk7AJKPNLOlILoUmN+HAVJz+cfSbWYg==", + "version": "4.2.27", + "resolved": "https://registry.npmjs.org/@cspell/dict-python/-/dict-python-4.2.27.tgz", + "integrity": "sha512-Rj6xQgYS4X6ienjgAZF+njA0GRY4oSPouJWv0vfikCTn6EWlfk0V6Dy1HP3Migj1O+IC2NmespgVq+BZNSp8OA==", "dev": true, "license": "MIT", "dependencies": { - "@cspell/dict-data-science": "^2.0.13" + "@cspell/dict-data-science": "^2.0.14" } }, "node_modules/@cspell/dict-r": { @@ -652,9 +568,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-ruby": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@cspell/dict-ruby/-/dict-ruby-5.1.0.tgz", - "integrity": "sha512-9PJQB3cfkBULrMLp5kSAcFPpzf8oz9vFN+QYZABhQwWkGbuzCIXSorHrmWSASlx4yejt3brjaWS57zZ/YL5ZQQ==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@cspell/dict-ruby/-/dict-ruby-5.1.1.tgz", + "integrity": "sha512-LHrp84oEV6q1ZxPPyj4z+FdKyq1XAKYPtmGptrd+uwHbrF/Ns5+fy6gtSi7pS+uc0zk3JdO9w/tPK+8N1/7WUA==", "dev": true, "license": "MIT" }, @@ -673,16 +589,16 @@ "license": "MIT" }, "node_modules/@cspell/dict-shell": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@cspell/dict-shell/-/dict-shell-1.1.2.tgz", - "integrity": "sha512-WqOUvnwcHK1X61wAfwyXq04cn7KYyskg90j4lLg3sGGKMW9Sq13hs91pqrjC44Q+lQLgCobrTkMDw9Wyl9nRFA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@cspell/dict-shell/-/dict-shell-1.2.0.tgz", + "integrity": "sha512-PVctvT22lJ49niMiakO8xieY7ELCAzjSqhejWR7bAMb5AZ9F4WDEs+XdGMnoVHWeXq7K5rcepLPmEJb+37zzIw==", "dev": true, "license": "MIT" }, "node_modules/@cspell/dict-software-terms": { - "version": "5.1.20", - "resolved": "https://registry.npmjs.org/@cspell/dict-software-terms/-/dict-software-terms-5.1.20.tgz", - "integrity": "sha512-TEk1xHvetTI4pv7Vzje1D322m6QEjaH2P6ucOOf6q7EJCppQIdC0lZSXkgHJAFU5HGSvEXSzvnVeW2RHW86ziQ==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@cspell/dict-software-terms/-/dict-software-terms-5.2.2.tgz", + "integrity": "sha512-0CaYd6TAsKtEoA7tNswm1iptEblTzEe3UG8beG2cpSTHk7afWIVMtJLgXDv0f/Li67Lf3Z1Jf3JeXR7GsJ2TRw==", "dev": true, "license": "MIT" }, @@ -772,282 +688,489 @@ "node": ">=20" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "node": ">=18" } }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=18" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=18" } }, - "node_modules/@eslint/config-array": { - "version": "0.23.3", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", - "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^3.0.3", - "debug": "^4.3.1", - "minimatch": "^10.2.4" - }, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=18" } }, - "node_modules/@eslint/config-array/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "18 || 20 || >=22" + "node": ">=18" } }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "18 || 20 || >=22" + "node": ">=18" } }, - "node_modules/@eslint/config-array/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=18" } }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=18" } }, - "node_modules/@eslint/config-array/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint/config-helpers": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.2.tgz", - "integrity": "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==", + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.1.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=18" } }, - "node_modules/@eslint/core": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", - "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=18" } }, - "node_modules/@eslint/object-schema": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", - "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=18" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", - "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.1.1", - "levn": "^0.4.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=18" } }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18.0" + "node": ">=18" } }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18.0" + "node": ">=18" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "engines": { + "node": ">=12" } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" }, "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { @@ -1055,6 +1178,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -1068,10 +1192,11 @@ } }, "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1081,30 +1206,33 @@ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@mswjs/interceptors": { - "version": "0.39.7", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.39.7.tgz", - "integrity": "sha512-sURvQbbKsq5f8INV54YJgJEdk8oxBanqkTiXXd33rKmofFCwZLhLRszPduMZ9TA9b8/1CHc/IJmOlBHJk2Q5AQ==", + "version": "0.39.8", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.39.8.tgz", + "integrity": "sha512-2+BzZbjRO7Ct61k8fMNHEtoKjeWI9pIlHFTqBwZ5icHpqszIgEZbjb1MW5Z0+bITTCTl3gk4PDBxs9tA/csXvA==", "dev": true, "license": "MIT", "dependencies": { @@ -1131,41 +1259,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@octokit/auth-token": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", @@ -1227,15 +1320,15 @@ "license": "MIT" }, "node_modules/@octokit/request": { - "version": "10.0.8", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.8.tgz", - "integrity": "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==", + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.10.tgz", + "integrity": "sha512-KxNC2pTqqhszMNrf12ZRd4PonRgyJdsM4F/jySiddQK+DsRcfBtUvqn8t7UsyZhnRJHvX46OohDt5N3VqIWC2w==", "license": "MIT", "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", - "fast-content-type-parse": "^3.0.0", + "content-type": "^2.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" }, @@ -1290,9 +1383,9 @@ "license": "MIT" }, "node_modules/@paralleldrive/cuid2": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", - "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", "license": "MIT", "dependencies": { "@noble/hashes": "^1.1.5" @@ -1303,31 +1396,12 @@ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", "dev": true, + "license": "MIT", "optional": true, "engines": { "node": ">=14" } }, - "node_modules/@pkgr/utils": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", - "integrity": "sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "fast-glob": "^3.3.0", - "is-glob": "^4.0.3", - "open": "^9.1.0", - "picocolors": "^1.0.0", - "tslib": "^2.6.0" - }, - "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" - } - }, "node_modules/@puppeteer/browsers": { "version": "2.13.0", "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", @@ -1380,9 +1454,10 @@ "license": "WTFPL" }, "node_modules/@socket.io/component-emitter": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", - "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" }, "node_modules/@tokenizer/inflate": { "version": "0.4.1", @@ -1447,6 +1522,16 @@ "@types/node": "*" } }, + "node_modules/@types/busboy": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/busboy/-/busboy-1.5.4.tgz", + "integrity": "sha512-kG7WrUuAKK0NoyxfQHsVE6j1m01s6kMma64E+OZenQABMQyTJop1DumUWcLwAQ2JzpefU7PDYoRDKl8uZosFjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -1465,27 +1550,14 @@ "license": "MIT" }, "node_modules/@types/cors": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", - "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", "dependencies": { "@types/node": "*" } }, - "node_modules/@types/esrecurse": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/express": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", @@ -1498,6 +1570,17 @@ "@types/serve-static": "^2" } }, + "node_modules/@types/express-fileupload": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@types/express-fileupload/-/express-fileupload-1.5.1.tgz", + "integrity": "sha512-DllImBVI1lCyjl2klky/TEwk60mbNebgXv1669h66g9TfptWSrEFq5a/raHSutaFzjSm1tmn9ypdNfu4jPSixQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/busboy": "*", + "@types/express": "*" + } + }, "node_modules/@types/express-serve-static-core": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", @@ -1525,13 +1608,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -1546,6 +1622,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/morgan": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz", + "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/node": { "version": "24.13.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", @@ -1556,9 +1642,9 @@ } }, "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", "dev": true, "license": "MIT" }, @@ -1603,6 +1689,13 @@ "form-data": "^4.0.0" } }, + "node_modules/@types/tmp": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz", + "integrity": "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -1622,45 +1715,26 @@ "@types/node": "*" } }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" }, "engines": { "node": ">= 0.6" } }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, + "node_modules/accepts/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "node": ">= 0.6" } }, "node_modules/agent-base": { @@ -1672,23 +1746,6 @@ "node": ">= 14" } }, - "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/ansi-escapes": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", @@ -1706,22 +1763,26 @@ } }, "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" @@ -1732,6 +1793,7 @@ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, + "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -1740,10 +1802,24 @@ "node": ">= 8" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" }, "node_modules/array-timsort": { "version": "1.0.3", @@ -1755,7 +1831,8 @@ "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "license": "MIT" }, "node_modules/ast-types": { "version": "0.13.4", @@ -1772,17 +1849,19 @@ "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" }, "node_modules/b4a": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", - "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", + "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", "license": "Apache-2.0", "peerDependencies": { "react-native-b4a": "*" @@ -1794,15 +1873,18 @@ } }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/bare-events": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", - "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", + "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", "license": "Apache-2.0", "peerDependencies": { "bare-abort-controller": "*" @@ -1814,9 +1896,9 @@ } }, "node_modules/bare-fs": { - "version": "4.5.6", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.6.tgz", - "integrity": "sha512-1QovqDrR80Pmt5HPAsMsXTCFcDYr+NSUKW6nd6WO5v0JBmnItc/irNRzm2KOQ5oZ69P37y+AMujNyNtG+1Rggw==", + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.2.tgz", + "integrity": "sha512-aTvMFUWkBmjzKtEQMDGGDNF8bkfpD5N1b/FCwt7A3wrU4t1o/e/85Wzkluh6JlODCjqVESYCkQCdTXqZ9G7VFg==", "license": "Apache-2.0", "dependencies": { "bare-events": "^2.5.4", @@ -1838,37 +1920,42 @@ } }, "node_modules/bare-os": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.0.tgz", - "integrity": "sha512-Dc9/SlwfxkXIGYhvMQNUtKaXCaGkZYGcd1vuNUUADVqzu4/vQfvnMkYYOUnt2VwQ2AqKr/8qAVFRtwETljgeFg==", + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz", + "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==", "license": "Apache-2.0", "engines": { "bare": ">=1.14.0" } }, "node_modules/bare-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", - "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.1.tgz", + "integrity": "sha512-ghj2DSK/2e99a1anTVPCV4m4YIYtrbXhfM7V3D7XZLOTsybnYyaJloymGqssQc8l/or0UoDyRtNQkmkEF/ysgQ==", "license": "Apache-2.0", "dependencies": { "bare-os": "^3.0.1" } }, "node_modules/bare-stream": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.10.0.tgz", - "integrity": "sha512-DOPZF/DDcDruKDA43cOw6e9Quq5daua7ygcAwJE/pKJsRWhgSSemi7qVNGE5kyDIxIeN1533G/zfbvWX7Wcb9w==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", + "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", "license": "Apache-2.0", "dependencies": { + "b4a": "^1.8.1", "streamx": "^2.25.0", "teex": "^1.0.1" }, "peerDependencies": { + "bare-abort-controller": "*", "bare-buffer": "*", "bare-events": "*" }, "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, "bare-buffer": { "optional": true }, @@ -1878,9 +1965,9 @@ } }, "node_modules/bare-url": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz", - "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==", + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.5.tgz", + "integrity": "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==", "license": "Apache-2.0", "dependencies": { "bare-path": "^3.0.0" @@ -1890,6 +1977,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", "engines": { "node": "^4.5.0 || >= 5.9" } @@ -1898,6 +1986,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", "dependencies": { "safe-buffer": "5.1.2" }, @@ -1905,6 +1994,12 @@ "node": ">= 0.8" } }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/basic-ftp": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", @@ -1920,39 +2015,34 @@ "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", "license": "Apache-2.0" }, - "node_modules/big-integer": { - "version": "1.6.51", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", - "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", - "dev": true, - "engines": { - "node": ">=0.6" - } - }, "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/body-parser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", - "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", - "content-type": "^1.0.5", + "content-type": "^2.0.0", "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" }, "engines": { "node": ">=18" @@ -1980,9 +2070,9 @@ } }, "node_modules/body-parser/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -2007,27 +2097,16 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "license": "ISC" }, - "node_modules/bplist-parser": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", - "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", - "dev": true, - "dependencies": { - "big-integer": "^1.6.44" - }, - "engines": { - "node": ">= 5.10.0" - } - }, "node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", - "dev": true, + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/braces": { @@ -2035,6 +2114,7 @@ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { "fill-range": "^7.1.1" }, @@ -2046,7 +2126,8 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/buffer-crc32": { "version": "0.2.13", @@ -2057,19 +2138,15 @@ "node": "*" } }, - "node_modules/bundle-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", - "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", - "dev": true, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", "dependencies": { - "run-applescript": "^5.0.0" + "streamsearch": "^1.1.0" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=10.16.0" } }, "node_modules/bytes": { @@ -2115,36 +2192,6 @@ } } }, - "node_modules/c8/node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/c8/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -2178,10 +2225,24 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -2193,26 +2254,24 @@ } }, "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, + "license": "MIT", "engines": { - "node": ">=10" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/chalk-template": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-1.1.0.tgz", - "integrity": "sha512-T2VJbcDuZQ0Tb2EWwSotMPJjgpy1/tGee1BTpUNsGZ/qgNjV2t7Mvu+d4600U564nbLesN1x2dPL+xii174Ekg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-1.1.2.tgz", + "integrity": "sha512-2bxTP2yUH7AJj/VAXfcA+4IcWGdQ87HwBANLt5XxGTeomo8yG0y95N1um9i5StvhT/Bl0/2cARA5v1PpPXUxUA==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^5.2.0" }, @@ -2223,18 +2282,6 @@ "url": "https://github.com/chalk/chalk-template?sponsor=1" } }, - "node_modules/chalk-template/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", - "dev": true, - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/cheerio": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", @@ -2307,55 +2354,20 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/cheerio/node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], + "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^4.0.1" }, "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" + "node": ">= 14.16.0" }, - "engines": { - "node": ">= 6" + "funding": { + "url": "https://paulmillr.com/funding/" } }, "node_modules/chromium-bidi": { @@ -2372,9 +2384,9 @@ } }, "node_modules/clear-module": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/clear-module/-/clear-module-4.1.2.tgz", - "integrity": "sha512-LWAxzHqdHsAZlPlEyJ2Poz6AIs384mPeqLVCru2p0BrP9G/kVGuhNyZYClLO6cXlnuJjzC8xtsJIuMjKqLXoAw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/clear-module/-/clear-module-4.1.3.tgz", + "integrity": "sha512-XdLrg7BnbXKntyrbs2dNjDN9CVoTQ+WV0i7jT5/r9ahzAaSDSzC9e2OVZB/QVwbxBb1/1AeObzjlxsYk5HFvww==", "dev": true, "license": "MIT", "dependencies": { @@ -2405,44 +2417,14 @@ } }, "node_modules/cli-truncate": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", - "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "slice-ansi": "^7.1.0", - "string-width": "^8.0.0" - }, - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-truncate/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/cli-truncate/node_modules/string-width": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.1.tgz", - "integrity": "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", "dev": true, "license": "MIT", "dependencies": { - "get-east-asian-width": "^1.3.0", - "strip-ansi": "^7.1.0" + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" }, "engines": { "node": ">=20" @@ -2451,22 +2433,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-truncate/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -2481,27 +2447,45 @@ "node": ">=12" } }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" @@ -2510,10 +2494,40 @@ "node": ">=8" } }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -2524,7 +2538,8 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" }, "node_modules/colorette": { "version": "2.0.20", @@ -2537,6 +2552,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -2555,14 +2571,13 @@ } }, "node_modules/comment-json": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.5.1.tgz", - "integrity": "sha512-taEtr3ozUmOB7it68Jll7s0Pwm+aoiHyXKrEC8SEodL4rNpdfDLqa7PfBlrgFoCNNdR8ImL+muti5IGvktJAAg==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.6.2.tgz", + "integrity": "sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w==", "dev": true, "license": "MIT", "dependencies": { "array-timsort": "^1.0.3", - "core-util-is": "^1.0.3", "esprima": "^4.0.1" }, "engines": { @@ -2582,6 +2597,7 @@ "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", "dependencies": { "mime-db": ">= 1.43.0 < 2" }, @@ -2607,92 +2623,60 @@ "node": ">= 0.8.0" } }, - "node_modules/compression/node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "dependencies": { - "safe-buffer": "5.2.1" - }, + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/content-disposition/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/cookie-signature": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.1.tgz", - "integrity": "sha512-78KWk9T26NhzXtuL26cIJ8/qNHANyJ/ZYrmEXFzUmhZdjpBv+DlWlOANRTGBt48YcyslsLrj0bMLFTmXvLRCOw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", "engines": { "node": ">=6.6.0" } @@ -2700,13 +2684,7 @@ "node_modules/cookiejar": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", - "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==" - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "license": "MIT" }, "node_modules/cors": { @@ -2726,6 +2704,41 @@ "url": "https://opencollective.com/express" } }, + "node_modules/cosmiconfig": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig/node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2840,19 +2853,6 @@ "node": ">=20" } }, - "node_modules/cspell-glob/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/cspell-grammar": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/cspell-grammar/-/cspell-grammar-9.0.2.tgz", @@ -2920,19 +2920,6 @@ "node": ">=20" } }, - "node_modules/cspell-lib/node_modules/env-paths": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", - "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cspell-trie-lib": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/cspell-trie-lib/-/cspell-trie-lib-9.0.2.tgz", @@ -2948,32 +2935,6 @@ "node": ">=20" } }, - "node_modules/cspell/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/cspell/node_modules/file-entry-cache": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-9.1.0.tgz", - "integrity": "sha512-/pqPFG+FdxWQj+/WSuzXSDaNzxgTLr/OrR1QuqfEZzDakpdYE70PwUxL7BPUa8hpjbvY1+qvCl8k+8Tq34xJgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/css-select": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", @@ -3030,57 +2991,19 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", "dependencies": { "ms": "2.0.0" } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/default-browser": { + "node_modules/decamelize": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", - "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", - "dev": true, - "dependencies": { - "bundle-name": "^3.0.0", - "default-browser-id": "^3.0.0", - "execa": "^7.1.1", - "titleize": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser-id": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", - "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", - "dev": true, - "dependencies": { - "bplist-parser": "^0.2.0", - "untildify": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -3104,6 +3027,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -3112,6 +3036,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -3126,6 +3051,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "license": "ISC", "dependencies": { "asap": "^2.0.0", "wrappy": "1" @@ -3145,6 +3071,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/doasync/-/doasync-2.0.1.tgz", "integrity": "sha512-5u7qAb+ACe2dvxHXrhjWNAfWuj42yB45Z9ght+U9QkB09nNGYMw5S4q6sXcpVnxjcKGl0jUYcxrGpR6kBTGJHw==", + "license": "MIT", "engines": { "node": ">= 8.2.1", "npm": ">= 5.3.0" @@ -3179,18 +3106,6 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/dom-serializer/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/domelementtype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", @@ -3283,18 +3198,20 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" }, "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" }, "node_modules/encodeurl": { "version": "2.0.0", @@ -3352,14 +3269,20 @@ "version": "5.2.3", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", "engines": { "node": ">=10.0.0" } }, - "node_modules/engine.io/node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, "engines": { "node": ">= 0.6" } @@ -3381,16 +3304,46 @@ } } }, + "node_modules/engine.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/engine.io/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/engine.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -3400,11 +3353,16 @@ } }, "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=6" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/environment": { @@ -3421,9 +3379,10 @@ } }, "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" } @@ -3447,9 +3406,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -3473,10 +3432,53 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", "engines": { "node": ">=6" } @@ -3492,6 +3494,7 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -3520,578 +3523,150 @@ "source-map": "~0.6.1" } }, - "node_modules/eslint": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.3.tgz", - "integrity": "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.3", - "@eslint/config-helpers": "^0.5.2", - "@eslint/core": "^1.1.1", - "@eslint/plugin-kit": "^0.6.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.2", - "eslint-visitor-keys": "^5.0.1", - "espree": "^11.1.1", - "esquery": "^1.7.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", "bin": { - "eslint": "bin/eslint.js" + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } + "node": ">=4" } }, - "node_modules/eslint-config-prettier": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.3.tgz", - "integrity": "sha512-wlypkcjrA6L2fG+WTmEELzZJFmiF3ftdirkmz8te3FOVwviFvaeYxHPtBvwaIlvjUC4QjrTrPxRMddx+CU7Tlw==", - "dev": true, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", "license": "MIT", - "bin": { - "eslint-config-prettier": "build/bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" + "engines": { + "node": ">= 0.6" } }, - "node_modules/eslint-plugin-es": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", - "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "dev": true, + "license": "MIT" + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", "dependencies": { - "eslint-utils": "^2.0.0", - "regexpp": "^3.0.0" - }, - "engines": { - "node": ">=8.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=4.19.1" + "bare-events": "^2.7.0" } }, - "node_modules/eslint-plugin-es/node_modules/eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, + "node_modules/expect.js": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/expect.js/-/expect.js-0.3.1.tgz", + "integrity": "sha512-okDF/FAPEul1ZFLae4hrgpIqAeapoo5TRdcg/lD0iN9S3GWrBFIJwNezGH1DMtIz+RxU4RrFmMq7WUUvDg3J6A==", + "dev": true + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", "dependencies": { - "eslint-visitor-keys": "^1.1.0" + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" }, "engines": { - "node": ">=6" + "node": ">= 18" }, "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/eslint-plugin-es/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "engines": { - "node": ">=4" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/eslint-plugin-node": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", - "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", - "dev": true, + "node_modules/express-fileupload": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/express-fileupload/-/express-fileupload-1.5.2.tgz", + "integrity": "sha512-wxUJn2vTHvj/kZCVmc5/bJO15C7aSMyHeuXYY3geKpeKibaAoQGcEv5+sM6nHS2T7VF+QHS4hTWPiY2mKofEdg==", + "license": "MIT", "dependencies": { - "eslint-plugin-es": "^3.0.0", - "eslint-utils": "^2.0.0", - "ignore": "^5.1.1", - "minimatch": "^3.0.4", - "resolve": "^1.10.1", - "semver": "^6.1.0" + "busboy": "^1.6.0" }, "engines": { - "node": ">=8.10.0" - }, - "peerDependencies": { - "eslint": ">=5.16.0" + "node": ">=12.0.0" } }, - "node_modules/eslint-plugin-node/node_modules/eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, + "node_modules/express-handlebars": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/express-handlebars/-/express-handlebars-9.0.1.tgz", + "integrity": "sha512-zkU1G3SHfOXFMVlfZuYruOw5U2oaz+GsDEmnVfE4n5Gx0l+4si61lmFzjsHvu2OTmcabafBETAWg+HmAjLpAMA==", + "license": "BSD-3-Clause", "dependencies": { - "eslint-visitor-keys": "^1.1.0" + "glob": "^13.0.6", + "graceful-fs": "^4.2.11", + "handlebars": "^4.7.9" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" + "node": ">=22.22.2" } }, - "node_modules/eslint-plugin-node/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, + "node_modules/express/node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-plugin-node/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-plugin-prettier": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.1.tgz", - "integrity": "sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.5" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/prettier" - }, - "peerDependencies": { - "@types/eslint": ">=8.0.0", - "eslint": ">=8.0.0", - "prettier": ">=3.0.0" - }, - "peerDependenciesMeta": { - "@types/eslint": { - "optional": true - }, - "eslint-config-prettier": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/balanced-match": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", - "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/eslint/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/eslint/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/espree": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.1.tgz", - "integrity": "sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.16.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", - "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.7.0" - } - }, - "node_modules/execa": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", - "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.1", - "human-signals": "^4.3.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^3.0.7", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": "^14.18.0 || ^16.14.0 || >=18.0.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/expect.js": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/expect.js/-/expect.js-0.3.1.tgz", - "integrity": "sha512-okDF/FAPEul1ZFLae4hrgpIqAeapoo5TRdcg/lD0iN9S3GWrBFIJwNezGH1DMtIz+RxU4RrFmMq7WUUvDg3J6A==", - "dev": true - }, - "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", - "dependencies": { - "accepts": "^2.0.0", - "body-parser": "^2.2.1", - "content-disposition": "^1.0.0", - "content-type": "^1.0.5", - "cookie": "^0.7.1", - "cookie-signature": "^1.2.1", - "debug": "^4.4.0", - "depd": "^2.0.0", - "encodeurl": "^2.0.0", - "escape-html": "^1.0.3", - "etag": "^1.8.1", - "finalhandler": "^2.1.0", - "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "merge-descriptors": "^2.0.0", - "mime-types": "^3.0.0", - "on-finished": "^2.4.1", - "once": "^1.4.0", - "parseurl": "^1.3.3", - "proxy-addr": "^2.0.7", - "qs": "^6.14.0", - "range-parser": "^1.2.1", - "router": "^2.2.0", - "send": "^1.1.0", - "serve-static": "^2.2.0", - "statuses": "^2.0.1", - "type-is": "^2.0.1", - "vary": "^1.1.2" - }, - "engines": { - "node": ">= 18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express-fileupload": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/express-fileupload/-/express-fileupload-1.5.2.tgz", - "integrity": "sha512-wxUJn2vTHvj/kZCVmc5/bJO15C7aSMyHeuXYY3geKpeKibaAoQGcEv5+sM6nHS2T7VF+QHS4hTWPiY2mKofEdg==", - "license": "MIT", - "dependencies": { - "busboy": "^1.6.0" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/express-fileupload/node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, - "node_modules/express-fileupload/node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/express-handlebars": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/express-handlebars/-/express-handlebars-9.0.1.tgz", - "integrity": "sha512-zkU1G3SHfOXFMVlfZuYruOw5U2oaz+GsDEmnVfE4n5Gx0l+4si61lmFzjsHvu2OTmcabafBETAWg+HmAjLpAMA==", - "license": "BSD-3-Clause", - "dependencies": { - "glob": "^13.0.6", - "graceful-fs": "^4.2.11", - "handlebars": "^4.7.9" - }, - "engines": { - "node": ">=22.22.2" - } - }, - "node_modules/express-handlebars/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/express-handlebars/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/express-handlebars/node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/express-handlebars/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/express/node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, - "engines": { - "node": ">= 0.6" + "node": ">= 0.6" } }, "node_modules/express/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4105,39 +3680,12 @@ } } }, - "node_modules/express/node_modules/mime-db": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", - "integrity": "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/mime-types": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.0.tgz", - "integrity": "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==", - "dependencies": { - "mime-db": "^1.53.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/express/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/express/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -4175,56 +3723,12 @@ } } }, - "node_modules/extract-zip/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/extract-zip/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/fast-content-type-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", - "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-diff": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true - }, "node_modules/fast-equals": { "version": "5.4.0", "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", @@ -4241,59 +3745,18 @@ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", "license": "MIT" }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" - }, - "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" }, "node_modules/fd-slicer": { "version": "1.1.0", @@ -4304,29 +3767,35 @@ "pend": "~1.2.0" } }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, - "dependencies": { - "flat-cache": "^4.0.0" - }, + "license": "MIT", "engines": { - "node": ">=16.0.0" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, - "node_modules/file-entry-cache/node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "node_modules/file-entry-cache": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-9.1.0.tgz", + "integrity": "sha512-/pqPFG+FdxWQj+/WSuzXSDaNzxgTLr/OrR1QuqfEZzDakpdYE70PwUxL7BPUa8hpjbvY1+qvCl8k+8Tq34xJgg==", "dev": true, + "license": "MIT", "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" + "flat-cache": "^5.0.0" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/file-type": { @@ -4352,6 +3821,7 @@ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -4360,9 +3830,9 @@ } }, "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -4373,13 +3843,17 @@ "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/finalhandler/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4404,6 +3878,7 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -4420,6 +3895,7 @@ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", "dev": true, + "license": "BSD-3-Clause", "bin": { "flat": "cli.js" } @@ -4445,6 +3921,23 @@ "dev": true, "license": "ISC" }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", @@ -4458,7 +3951,28 @@ "mime-types": "^2.1.35" }, "engines": { - "node": ">= 6" + "node": ">= 6" + } + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" } }, "node_modules/formidable": { @@ -4482,6 +3996,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -4490,6 +4005,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -4513,6 +4029,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -4531,14 +4048,15 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, "node_modules/get-east-asian-width": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", - "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", "dev": true, "license": "MIT", "engines": { @@ -4586,12 +4104,15 @@ } }, "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4635,127 +4156,33 @@ "license": "MIT" }, "node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dev": true, + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob/node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "dependencies": { - "@isaacs/cliui": "^8.0.2" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" } }, - "node_modules/glob/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" + "is-glob": "^4.0.1" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">= 6" } }, "node_modules/global-directory": { @@ -4789,7 +4216,8 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" }, "node_modules/handlebars": { "version": "4.7.9", @@ -4812,23 +4240,12 @@ "uglify-js": "^3.1.4" } }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -4877,6 +4294,7 @@ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", "dev": true, + "license": "MIT", "bin": { "he": "bin/he" } @@ -4885,7 +4303,8 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/htmlparser2": { "version": "10.1.0", @@ -5025,15 +4444,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/human-signals": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", - "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", - "dev": true, - "engines": { - "node": ">=14.18.0" - } - }, "node_modules/husky": { "version": "9.0.11", "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz", @@ -5082,20 +4492,12 @@ ], "license": "BSD-3-Clause" }, - "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/import-fresh": { "version": "3.3.1", @@ -5117,6 +4519,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -5128,6 +4531,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", "engines": { "node": ">=4" } @@ -5143,19 +4547,11 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" }, "node_modules/ini": { "version": "4.1.1", @@ -5170,7 +4566,8 @@ "node_modules/insafe": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/insafe/-/insafe-0.5.1.tgz", - "integrity": "sha512-LVGi8wNYSld/ElRmd06ikkY2uBV4ik1JaPf0FRQ4awW6t+3VqPBzePs74/P7CBTm5SpItIfOOFIs/K66cyx9Rw==" + "integrity": "sha512-LVGi8wNYSld/ElRmd06ikkY2uBV4ik1JaPf0FRQ4awW6t+3VqPBzePs74/P7CBTm5SpItIfOOFIs/K66cyx9Rw==", + "license": "MIT" }, "node_modules/ip-address": { "version": "10.2.0", @@ -5185,6 +4582,7 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", "engines": { "node": ">= 0.10" } @@ -5192,13 +4590,15 @@ "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -5206,38 +4606,12 @@ "node": ">=8" } }, - "node_modules/is-core-module": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", - "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "dev": true, - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5263,6 +4637,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -5270,24 +4645,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "dev": true, - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-node-process": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", @@ -5300,6 +4657,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -5309,6 +4667,7 @@ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -5318,6 +4677,7 @@ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -5328,23 +4688,12 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -5352,44 +4701,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-wsl/node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "dev": true, - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=8" } @@ -5409,40 +4733,41 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-report/node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "semver": "^7.5.3" + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", "dev": true, - "license": "BSD-3-Clause", + "license": "BlueOak-1.0.0", "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" + "@isaacs/cliui": "^8.0.2" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" } }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" }, "node_modules/js-yaml": { "version": "4.2.0", @@ -5470,36 +4795,26 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "license": "MIT" }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/json-with-bigint": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.3.tgz", - "integrity": "sha512-QObKu6nxy7NsxqR0VK4rkXnsNr5L9ElJaGEg+ucJ6J7/suoKZ0n+p76cu9aCqowytxEbwYNzvrMerfMkXneF5A==", + "version": "3.5.8", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.8.tgz", + "integrity": "sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==", "license": "MIT" }, "node_modules/keyv": { @@ -5507,27 +4822,16 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" }, "node_modules/lint-staged": { "version": "16.4.0", @@ -5553,19 +4857,6 @@ "url": "https://opencollective.com/lint-staged" } }, - "node_modules/lint-staged/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/listr2": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", @@ -5584,96 +4875,12 @@ "node": ">=20.0.0" } }, - "node_modules/listr2/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/listr2/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/listr2/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/listr2/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/listr2/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/listr2/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -5696,6 +4903,7 @@ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" @@ -5707,69 +4915,51 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { - "ansi-escapes": "^7.0.0", - "cli-cursor": "^5.0.0", - "slice-ansi": "^7.1.0", - "strip-ansi": "^7.1.0", - "wrap-ansi": "^9.0.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=18" + "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/log-update/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12" + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/log-update/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", "engines": { - "node": ">=12" + "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/log-update/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/log-update/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { "node": ">=18" @@ -5778,38 +4968,46 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update/node_modules/strip-ansi": { + "node_modules/log-update/node_modules/slice-ansi": { "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/log-update/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" + "semver": "^7.5.3" }, "engines": { - "node": ">=18" + "node": ">=10" }, "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/math-intrinsics": { @@ -5834,6 +5032,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", "engines": { "node": ">=18" }, @@ -5841,51 +5040,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/metaviewport-parser": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/metaviewport-parser/-/metaviewport-parser-0.3.0.tgz", - "integrity": "sha512-EoYJ8xfjQ6kpe9VbVHvZTZHiOl4HL1Z18CrZ+qahvLXT7ZO4YTC2JMyt5FaUp9JJp6J4Ybb/z7IsCXZt86/QkQ==" + "integrity": "sha512-EoYJ8xfjQ6kpe9VbVHvZTZHiOl4HL1Z18CrZ+qahvLXT7ZO4YTC2JMyt5FaUp9JJp6J4Ybb/z7IsCXZt86/QkQ==", + "license": "MIT" }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/mime": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "license": "MIT", "bin": { "mime": "cli.js" }, @@ -5894,34 +5068,28 @@ } }, "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, + "mime-db": "^1.54.0" + }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/mimic-function": { @@ -5938,22 +5106,25 @@ } }, "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.5" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6010,54 +5181,69 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/mocha/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, - "node_modules/mocha/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "node_modules/mocha/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { - "readdirp": "^4.0.1" + "ms": "^2.1.3" }, "engines": { - "node": ">= 14.16.0" + "node": ">=6.0" }, - "funding": { - "url": "https://paulmillr.com/funding/" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/mocha/node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "node_modules/mocha/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, + "license": "ISC", "dependencies": { - "ms": "2.1.2" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": ">=6.0" + "bin": { + "glob": "dist/esm/bin.mjs" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/mocha/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "node_modules/mocha/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" }, "node_modules/mocha/node_modules/minimatch": { "version": "9.0.9", @@ -6079,20 +5265,24 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/mocha/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "node_modules/mocha/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, "engines": { - "node": ">= 14.18.0" + "node": ">=16 || 14 >=14.18" }, "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/mocha/node_modules/supports-color": { @@ -6100,6 +5290,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -6133,18 +5324,14 @@ "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" }, "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -6152,12 +5339,13 @@ "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" }, "node_modules/netmask": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", - "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", "license": "MIT", "engines": { "node": ">= 0.4.0" @@ -6220,6 +5408,49 @@ "url": "https://opencollective.com/nodemon" } }, + "node_modules/nodemon/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/nodemon/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/nodemon/node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -6243,10 +5474,24 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/nodemon/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -6254,31 +5499,43 @@ "dev": true, "license": "MIT" }, - "node_modules/nodemon/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/nodemon/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/nodemon/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, + "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "picomatch": "^2.2.1" }, "engines": { - "node": ">=4" + "node": ">=8.10.0" } }, - "node_modules/nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, + "license": "MIT", "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" + "has-flag": "^3.0.0" }, "engines": { - "node": "*" + "node": ">=4" } }, "node_modules/normalize-path": { @@ -6286,37 +5543,11 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/npm-run-path": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", - "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", - "dev": true, - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -6333,6 +5564,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -6374,60 +5606,27 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", "dependencies": { "wrappy": "1" } }, "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", - "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "dev": true, + "license": "MIT", "dependencies": { - "default-browser": "^4.0.0", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "is-wsl": "^2.2.0" + "mimic-function": "^5.0.0" }, "engines": { - "node": ">=14.16" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", - "dev": true, - "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/outvariant": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", @@ -6440,6 +5639,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, + "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -6455,6 +5655,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -6544,6 +5745,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -6554,7 +5756,19 @@ "node": ">=8" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, "node_modules/parse5-htmlparser2-tree-adapter": { @@ -6585,18 +5799,6 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, "node_modules/parse5-parser-stream": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", @@ -6609,16 +5811,16 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/parse5-parser-stream/node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" }, "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" + "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/parseurl": { @@ -6635,6 +5837,7 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -6644,16 +5847,11 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -6670,19 +5868,10 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/path-to-regexp": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", - "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "license": "MIT", "funding": { "type": "opencollective", @@ -6702,27 +5891,18 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/prettier": { "version": "3.8.4", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", @@ -6739,18 +5919,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -6764,6 +5932,7 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", + "license": "MIT", "dependencies": { "asap": "~2.0.6" } @@ -6772,6 +5941,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -6841,7 +6011,8 @@ "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/pump": { "version": "3.0.4", @@ -6853,16 +6024,6 @@ "once": "^1.3.1" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/puppeteer": { "version": "24.40.0", "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.40.0.tgz", @@ -6925,31 +6086,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/puppeteer/node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/qs": { "version": "6.15.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", @@ -6965,31 +6101,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "^5.1.0" } @@ -7019,9 +6136,9 @@ } }, "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -7035,59 +6152,34 @@ } }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">= 14.18.0" }, "funding": { - "url": "https://github.com/sponsors/mysticatea" + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/resolve": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", - "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", - "dev": true, - "dependencies": { - "is-core-module": "^2.11.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -7109,45 +6201,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/restore-cursor/node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -7172,9 +6225,9 @@ } }, "node_modules/router/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -7194,115 +6247,10 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/run-applescript": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", - "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", - "dev": true, - "dependencies": { - "execa": "^5.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/run-applescript/node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/run-applescript/node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/run-applescript/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/run-applescript/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/run-applescript/node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/run-applescript/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/run-applescript/node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "funding": [ { "type": "github", @@ -7316,25 +6264,19 @@ "type": "consulting", "url": "https://feross.org/support" } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + ], + "license": "MIT" }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -7344,31 +6286,35 @@ } }, "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "license": "MIT", "dependencies": { - "debug": "^4.3.5", + "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", - "statuses": "^2.0.1" + "statuses": "^2.0.2" }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/send/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -7382,27 +6328,6 @@ } } }, - "node_modules/send/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -7414,14 +6339,15 @@ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "randombytes": "^2.1.0" } }, "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "license": "MIT", "dependencies": { "encodeurl": "^2.0.0", @@ -7431,6 +6357,10 @@ }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/setprototypeof": { @@ -7444,6 +6374,7 @@ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -7456,19 +6387,20 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -7480,13 +6412,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -7533,16 +6465,24 @@ } }, "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/simple-update-notifier": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", "dev": true, + "license": "MIT", "dependencies": { "semver": "^7.5.3" }, @@ -7551,35 +6491,22 @@ } }, "node_modules/slice-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", - "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", @@ -7677,6 +6604,19 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/socket.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/socket.io/node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -7694,19 +6634,49 @@ } } }, + "node_modules/socket.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/socket.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/socket.io/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/socket.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/socks": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", - "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", "license": "MIT", "dependencies": { - "ip-address": "^10.0.1", + "ip-address": "^10.1.1", "smart-buffer": "^4.2.0" }, "engines": { @@ -7755,6 +6725,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -7768,10 +6739,18 @@ "node": ">= 0.8" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/streamx": { - "version": "2.25.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", - "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "version": "2.28.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", + "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", "license": "MIT", "dependencies": { "events-universal": "^1.0.0", @@ -7791,22 +6770,23 @@ "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.6.19" } }, "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", "dev": true, + "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=12" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -7818,6 +6798,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -7827,40 +6808,47 @@ "node": ">=8" } }, - "node_modules/string-width-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } }, "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/string-width/node_modules/ansi-regex": { + "node_modules/string-width-cjs/node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "engines": { - "node": ">=12" + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "engines": { + "node": ">=8" } }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -7869,23 +6857,13 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -7893,16 +6871,14 @@ "node": ">=8" } }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/strip-json-comments": { @@ -7910,6 +6886,7 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -7981,6 +6958,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -7988,34 +6966,6 @@ "node": ">=8" } }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/synckit": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", - "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==", - "dev": true, - "dependencies": { - "@pkgr/utils": "^2.3.1", - "tslib": "^2.5.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" - } - }, "node_modules/tar-fs": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", @@ -8031,9 +6981,9 @@ } }, "node_modules/tar-stream": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", - "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", + "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", "license": "MIT", "dependencies": { "b4a": "^1.6.4", @@ -8043,84 +6993,27 @@ } }, "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "license": "MIT", - "dependencies": { - "streamx": "^2.12.5" - } - }, - "node_modules/test-exclude": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-8.0.0.tgz", - "integrity": "sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^13.0.6", - "minimatch": "^10.2.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/test-exclude/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "dev": true, - "license": "BlueOak-1.0.0", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "streamx": "^2.12.5" } }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "node_modules/test-exclude": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-8.0.0.tgz", + "integrity": "sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "ISC", "dependencies": { - "brace-expansion": "^5.0.2" + "@istanbuljs/schema": "^0.1.2", + "glob": "^13.0.6", + "minimatch": "^10.2.2" }, "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "20 || >=22" } }, "node_modules/text-decoder": { @@ -8133,9 +7026,9 @@ } }, "node_modules/tinyexec": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", - "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", "dev": true, "license": "MIT", "engines": { @@ -8143,14 +7036,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -8159,46 +7052,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/titleize": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", - "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/tmp": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", @@ -8213,6 +7066,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -8248,79 +7102,68 @@ } }, "node_modules/touch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", - "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", "dev": true, - "dependencies": { - "nopt": "~1.0.10" - }, + "license": "ISC", "bin": { "nodetouch": "bin/nodetouch.js" } }, "node_modules/tslib": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", - "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "node_modules/tsx": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", "dev": true, + "license": "MIT", "dependencies": { - "prelude-ls": "^1.2.1" + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" }, "engines": { - "node": ">= 0.8.0" + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" } }, "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "license": "MIT", "dependencies": { - "content-type": "^1.0.5", + "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" }, "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" + "node": ">= 18" }, - "engines": { - "node": ">= 0.6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/typed-query-selector": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.1.tgz", - "integrity": "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==", + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", + "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", "license": "MIT" }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -8335,6 +7178,7 @@ "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", "optional": true, "bin": { "uglifyjs": "bin/uglifyjs" @@ -8359,7 +7203,8 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/undici": { "version": "7.28.0", @@ -8391,25 +7236,6 @@ "node": ">= 0.8" } }, - "node_modules/untildify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -8425,17 +7251,11 @@ "node": ">=10.12.0" } }, - "node_modules/v8-to-istanbul/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -8487,6 +7307,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -8500,27 +7321,29 @@ "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" }, "node_modules/workerpool": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.2.tgz", - "integrity": "sha512-Xz4Nm9c+LiBHhDR5bDLnNzmj6+5F+cyEAWPMkbs2awq/dYazR/efelZzUAjB/y3kNHL+uzkHvxVVpaOfGCPV7A==", + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", "dev": true, "license": "Apache-2.0" }, "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" @@ -8532,6 +7355,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -8544,17 +7368,38 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } }, "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -8564,6 +7409,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -8573,39 +7419,49 @@ "node": ">=8" } }, - "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, "engines": { "node": ">=8" } }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, "node_modules/wrap-ansi/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" }, "node_modules/ws": { "version": "8.21.0", @@ -8645,14 +7501,15 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "dev": true, "license": "ISC", "bin": { @@ -8666,9 +7523,9 @@ } }, "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -8697,6 +7554,7 @@ "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", "dev": true, + "license": "MIT", "dependencies": { "camelcase": "^6.0.0", "decamelize": "^4.0.0", @@ -8707,36 +7565,15 @@ "node": ">=10" } }, - "node_modules/yargs-unparser/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yargs-unparser/node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, "node_modules/yargs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -8760,6 +7597,18 @@ "node": ">=8" } }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", @@ -8775,6 +7624,7 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, diff --git a/package.json b/package.json index cf5bf02ef..49dc8b316 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,12 @@ "type": "git", "url": "https://github.com/w3c/specberus.git" }, + "files": [ + "app.{js,d.ts}", + "lib/**/*.{js,d.ts}", + "public/*", + "views/*" + ], "dependencies": { "@octokit/core": "^7.0.6", "cheerio": "^1.1.2", @@ -32,17 +38,16 @@ "devDependencies": { "@rvagg/chai-as-promised": "8.0.2", "@types/express": "^5.0.6", + "@types/express-fileupload": "^1.5.1", "@types/mocha": "^10.0.10", + "@types/morgan": "^1.9.10", "@types/node": "^24.10.9", "@types/superagent": "^8.1.9", + "@types/tmp": "^0.2.6", "c8": "^11.0.0", "chai": "6.2.2", "cspell": "9.0.2", "domhandler": "^6.0.1", - "eslint": "10.0.3", - "eslint-config-prettier": "10.0.3", - "eslint-plugin-node": "11.1.0", - "eslint-plugin-prettier": "5.0.1", "expect.js": "0.3", "husky": "9.0.11", "lint-staged": "16.4.0", @@ -51,19 +56,22 @@ "nock": "15.0.0", "nodemon": "3.0.3", "prettier": "3.8.4", - "typescript": "^5.9.3" + "tsx": "^4.21.0", + "typescript": "^6.0.2" }, "scripts": { - "build": "npm run lint && npm run test", - "coverage": "c8 npm test", - "coverage:text": "c8 --reporter=text npm test", + "build": "tsc", + "check": "tsc --noEmit", + "coverage": "tsc && c8 npm test", + "coverage:text": "tsc && c8 --reporter=text npm test", "cspell": "cspell \"**/*\"", - "fix": "prettier -w . && eslint --fix .", - "lint": "eslint . --report-unused-disable-directives && tsc && prettier -c .", - "live": "nodemon --use_strict app", + "fix": "prettier -w .", + "lint": "npm run check && prettier -c .", + "live": "tsx watch app.ts", + "prepack": "tsc", "prepare": "husky install", "spelling": "cspell \"**/*\"", - "start": "node --use_strict app", + "start": "node app", "testserver": "nodemon test/lib/testserver.js", "test": "NO_THROTTLE=true mocha" }, @@ -72,7 +80,6 @@ "npm": ">=7" }, "lint-staged": { - "*.js": "eslint --cache", "*": [ "cspell --no-must-find-files", "prettier --write --ignore-unknown" diff --git a/test/api.js b/test/api.js index ef71a7c2e..911e87263 100644 --- a/test/api.js +++ b/test/api.js @@ -2,20 +2,19 @@ * Test the REST API. */ -// Native packages: -// External packages: -import * as chai from 'chai'; +import http from 'http'; +import { join } from 'path'; + import chaiAsPromised from '@rvagg/chai-as-promised'; +import * as chai from 'chai'; import express from 'express'; import fileUpload from 'express-fileupload'; -import http from 'http'; -import { join } from 'path'; import superagent from 'superagent'; + import { setUp } from '../lib/api.js'; -import { importJSON } from '../lib/util.js'; import { cleanupMocks, setupMocks } from './lib/utils.js'; -// Internal packages: -const meta = importJSON('../package.json', import.meta.url); +import meta from '../package.json' with { type: 'json' }; + const { expect } = chai; // Settings: @@ -146,13 +145,13 @@ describe('API', () => { }); describe('Parameter restrictions', () => { - it('Should reject the parameter “document”', () => { + it('Should reject the parameter "document" as unknown', () => { const query = get('metadata?document=foo'); return expect(query).to.eventually.be.rejectedWith( - 'Parameter “document” is not allowed in this context' + 'Error: Illegal parameter “document”' ); }); - it('Should reject the parameter “source”', () => { + it('Should reject the parameter "source" as forbidden', () => { const query = get('metadata?source=foo'); return expect(query).to.eventually.be.rejectedWith( 'Parameter “source” is not allowed in this context' diff --git a/test/l10n.js b/test/l10n.js index 0fd4a231e..0109fffb8 100644 --- a/test/l10n.js +++ b/test/l10n.js @@ -7,10 +7,8 @@ import { join } from 'path'; import * as chai from 'chai'; import * as l10n from '../lib/l10n-en_GB.js'; -import { importJSON } from '../lib/util.js'; -// Internal packages: -const rules = importJSON('../lib/rules.json', import.meta.url); +import rules from '../lib/rules.json' with { type: 'json' }; const { expect } = chai; @@ -39,7 +37,7 @@ const scanStrings = function () { ); if (c[0] !== 'generic') { // 1. Process the section: - if (!Object.prototype.hasOwnProperty.call(result, c[0])) { + if (!Object.hasOwn(result, c[0])) { if (c.length === 1) { if (messages[i] === false) result[c[0]] = false; else @@ -58,7 +56,7 @@ const scanStrings = function () { // 2. Process the rule: if (c.length > 1) { - if (!Object.prototype.hasOwnProperty.call(result[c[0]], c[1])) { + if (!Object.hasOwn(result[c[0]], c[1])) { if (c.length === 2) { if (messages[i] === false) result[c[0]][c[1]] = false; else @@ -78,12 +76,7 @@ const scanStrings = function () { // 3. Process the message ID: if (c.length > 2) { - if ( - !Object.prototype.hasOwnProperty.call( - result[c[0]][c[1]], - c[2] - ) - ) + if (!Object.hasOwn(result[c[0]][c[1]], c[2])) result[c[0]][c[1]][c[2]] = !!messages[i]; else throw new Error(`key “${i}” is defined more than once`); } @@ -108,6 +101,9 @@ async function scanFileSystem() { const filenames = await readdir(join(baseDir, dirname)); for (const filename of filenames) { + if (!filename.endsWith('.ts')) continue; + if (filename.endsWith('.d.ts')) continue; + const content = await readFile(join(baseDir, dirname, filename)); const name = filename.replace(extensionRemover, ''); result[dirname][name] = {}; @@ -130,20 +126,15 @@ async function scanFileSystem() { const findHoles = function (source, expected, labelSource, labelExpected) { let errors = ''; for (const i in expected) - if (!Object.prototype.hasOwnProperty.call(source, i)) + if (!Object.hasOwn(source, i)) errors += `Section “${i}” exists in ${labelExpected} but is missing in ${labelSource}.\n`; else if (source[i] !== false) for (const j in expected[i]) - if (!Object.prototype.hasOwnProperty.call(source[i], j)) + if (!Object.hasOwn(source[i], j)) errors += `Rule “${i}/${j}” exists in ${labelExpected} but is missing in ${labelSource}.\n`; else if (source[i][j] !== false) for (const k in expected[i][j]) - if ( - !Object.prototype.hasOwnProperty.call( - source[i][j], - k - ) - ) + if (!Object.hasOwn(source[i][j], k)) errors += `Message ID “${i}/${j}/${k}” exists in ${labelExpected} but is missing in ${labelSource}.\n`; if (errors) throw new Error(`${errors.slice(0, -2)}.`); }; diff --git a/test/rules.js b/test/rules.js index 96f0425b0..108a0718b 100644 --- a/test/rules.js +++ b/test/rules.js @@ -1,11 +1,10 @@ +import { EventEmitter } from 'events'; import { nextTick } from 'process'; // External modules: import { expect as chai } from 'chai'; import expect from 'expect.js'; -// Internal modules: -import { Sink } from '../lib/sink.js'; import { allProfiles } from '../lib/util.js'; import { Specberus } from '../lib/validator.js'; // A list of good documents to be tested, using all rules configured in the profiles. @@ -42,7 +41,7 @@ function compareMetadata(file, expectedObject) { const specberus = new Specberus(); const successExpected = !('errors' in expectedObject); - const handler = new Sink(); + const handler = new EventEmitter(); handler.on('exception', data => { throw new Error(data); }); @@ -135,45 +134,43 @@ after(done => { }); function buildHandler(test, done) { - const handler = new Sink(); + const handler = new EventEmitter(); - handler.on('err', (type, data) => { - if (DEBUG) console.log('error: \n', type, data); - handler.errors.push(`${type.name}.${data.key}`); - }); - handler.on('warning', (type, data) => { - if (DEBUG) console.log('warning: \n', type, data); - handler.warnings.push(`${type.name}.${data.key}`); - }); - handler.on('done', name => { - if (DEBUG) console.log(`----> ${name} check done`); - }); + if (DEBUG) { + handler.on('err', (type, data) => { + console.log('error:\n', type, data); + }); + handler.on('warning', (type, data) => { + console.log('warning:\n', type, data); + }); + handler.on('done', name => { + console.log(`----> ${name} check done`); + }); + } handler.on('exception', data => { console.error( `[EXCEPTION] Validator had a massive failure: ${data.message}` ); }); - handler.on('end-all', () => { + handler.on('end-all', ({ errors, warnings }) => { try { if (!test.errors) { - expect(handler.errors).to.be.empty(); + expect(errors).to.be.empty(); } else { - expect(handler.errors.length).to.eql(test.errors.length); - handler.errors.forEach((_, i) => { - expect(handler.errors).to.contain(test.errors[i]); + expect(errors.length).to.eql(test.errors.length); + errors.forEach(({ key, name }, i) => { + expect(`${name}.${key}`).to.equal(test.errors[i]); }); } if (!test.ignoreWarnings) { if (test.warnings) { - expect(handler.warnings.length).to.eql( - test.warnings.length - ); - handler.warnings.forEach((_, i) => { - expect(handler.warnings).to.contain(test.warnings[i]); + expect(warnings.length).to.eql(test.warnings.length); + warnings.forEach(({ key, name }, i) => { + expect(`${name}.${key}`).to.contain(test.warnings[i]); }); } else { - expect(handler.warnings).to.be.empty(); + expect(warnings).to.be.empty(); } } done(); @@ -206,9 +203,9 @@ describe('Making sure good documents pass Specberus...', () => { it(`should pass for ${docProfile} doc with ${url}`, done => { const profilePath = allProfiles.find(p => - p.endsWith(`/${docProfile}.js`) + p.endsWith(`/${docProfile}`) ); - import(`../lib/profiles/${profilePath}`).then(profile => { + import(`../lib/profiles/${profilePath}.js`).then(profile => { // add custom config to test const extendedProfile = { ...profile, diff --git a/tools/fetch-groups-db.js b/tools/fetch-groups-db.js deleted file mode 100644 index 1a58dfb79..000000000 --- a/tools/fetch-groups-db.js +++ /dev/null @@ -1,79 +0,0 @@ -// This script updates our local database of groups -// Usage: node fetch-groups-db.js your-w3c-user your-w3c-password - -// XXX also look at https://cvs.w3.org/Team/WWW/2000/04/mem-news/groups.rdf - -import { load } from 'cheerio'; -import fs from 'fs'; -import pth, { dirname } from 'path'; -import ua from 'superagent'; -import { fileURLToPath } from 'url'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -const user = process.argv[2]; -const pass = process.argv[3]; -const results = {}; -if (!user || !pass) { - throw new Error( - 'Please pass in your W3C username and password to fetch from the groups page.' - ); -} - -/** - * @param str source string to normalize - * @returns {string} whitespace normalized string - */ -function norm(str) { - return str.replace(/^\s+/, '').replace(/\s+$/, '').replace(/\s+/g, ' '); -} - -/** - * @param err - * @param res - */ -function munge(err, res) { - const $ = load(res.text); - - $('tr.WG, tr.IG, tr.CG').each((_, tr) => { - const $tr = $(tr); - const $tds = $tr.find('td'); - const $td1 = $tds.eq(0); - const $td4 = $tds.eq(3); - const name = $td1.length && norm($td1.text()); - let href = - $td1.length && - $td1.find('a').length && - $td1.find('a').first().attr('href'); - if ($td4.length) { - $td4.find('a').each((_, el) => { - let list = $(el).text(); - if (!/@w3\.org$/.test(list)) list += '@w3.org'; - if (href.indexOf('http') === 0) true; - else if (href.indexOf('/') === 0) - href = `https://www.w3.org${href}`; - else if (/^(\.\.\/){2}\w/.test(href)) - href = `https://www.w3.org/${href.replace( - /^(\.\.\/){2}/, - '' - )}`; - else - console.error( - '--------------- UNKNOWN URL FORM -------------------' - ); - results[list] = { name, href }; - }); - } - }); - fs.writeFileSync( - pth.join(__dirname, '../lib/groups-db.json'), - JSON.stringify(results, null, 4) - ); -} - -ua.get('https://www.w3.org/Member/Mail/Overview.html') - .auth(user, pass) - .buffer(true) - .end(munge); - -// munge(err, { text: fs.readFileSync(pth.join(__dirname, "groups.html"), "utf8") }); diff --git a/tools/groups-sparql.json b/tools/groups-sparql.json deleted file mode 100644 index 14530031e..000000000 --- a/tools/groups-sparql.json +++ /dev/null @@ -1,550 +0,0 @@ -{ - "head": { - "vars": ["name", "mailbox", "homepage"] - }, - "results": { - "ordered": true, - "distinct": true, - "bindings": [ - { - "name": { "type": "literal", "value": "Audio Working Group" }, - "mailbox": { - "type": "uri", - "value": "mailto:public-audio@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2011/audio/" - } - }, - { - "name": { - "type": "literal", - "value": "Authoring Tool Accessibility Guidelines Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:w3c-wai-au@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/WAI/AU/" - } - }, - { - "name": { - "type": "literal", - "value": "Browser Testing and Tools Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-browser-tools-testing@w3.org@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/testing/browser/" - } - }, - { - "name": { - "type": "literal", - "value": "CSV on the Web Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-csv-wg@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2013/csvw/" - } - }, - { - "name": { - "type": "literal", - "value": "Data on the Web Best Practices Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-dwbp-wg@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2013/dwbp/" - } - }, - { - "name": { - "type": "literal", - "value": "Device APIs Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-device-apis@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2009/dap/" - } - }, - { - "name": { - "type": "literal", - "value": "Education and Outreach Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:w3c-wai-eo@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/WAI/EO/" - } - }, - { - "name": { - "type": "literal", - "value": "Efficient XML Interchange Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-exi@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/XML/EXI/" - } - }, - { - "name": { - "type": "literal", - "value": "Evaluation and Repair Tools Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-wai-ert@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/WAI/ER/" - } - }, - { - "name": { "type": "literal", "value": "Forms Working Group" }, - "mailbox": { - "type": "uri", - "value": "mailto:public-forms@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/MarkUp/Forms/" - } - }, - { - "name": { - "type": "literal", - "value": "Geolocation Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-geolocation@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2008/geolocation/" - } - }, - { - "name": { - "type": "literal", - "value": "Government Linked Data Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-gld-wg@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2011/gld/" - } - }, - { - "name": { "type": "literal", "value": "HTML Working Group" }, - "mailbox": { - "type": "uri", - "value": "mailto:public-html@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/html/wg/" - } - }, - { - "name": { - "type": "literal", - "value": "Independent User Interface (Indie UI) Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-indie-ui@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/WAI/IndieUI/" - } - }, - { - "name": { - "type": "literal", - "value": "Linked Data Platform (LDP) Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-ldp-wg@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2012/ldp/" - } - }, - { - "name": { - "type": "literal", - "value": "Media Annotations Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-media-annotation@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2008/WebVideo/Annotations/" - } - }, - { - "name": { - "type": "literal", - "value": "Model-Based User Interfaces Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-mbui@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2011/mbui/" - } - }, - { - "name": { - "type": "literal", - "value": "Multimodal Interaction Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:www-multimodal@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2002/mmi/" - } - }, - { - "name": { - "type": "literal", - "value": "Near Field Communications Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-nfc@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2012/nfc/" - } - }, - { - "name": { - "type": "literal", - "value": "Pointer Events Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-pointer-events@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2012/pointerevents/" - } - }, - { - "name": { "type": "literal", "value": "RDF Working Group" }, - "mailbox": { - "type": "uri", - "value": "mailto:public-rdf-wg@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2011/rdf-wg/" - } - }, - { - "name": { "type": "literal", "value": "RDFa Working Group" }, - "mailbox": { - "type": "uri", - "value": "mailto:public-rdfa@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2010/02/rdfa/" - } - }, - { - "name": { - "type": "literal", - "value": "Research and Development Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-wai-rd@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/WAI/RD/" - } - }, - { - "name": { "type": "literal", "value": "SVG Working Group" }, - "mailbox": { - "type": "uri", - "value": "mailto:public-svg-wg@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/Graphics/SVG/WG/" - } - }, - { - "name": { - "type": "literal", - "value": "System Applications Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-sysapps@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2012/sysapps/" - } - }, - { - "name": { - "type": "literal", - "value": "Technical Architecture Group" - }, - "mailbox": { "type": "uri", "value": "mailto:www-tag@w3.org" }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2001/tag/" - } - }, - { - "name": { - "type": "literal", - "value": "Timed Text Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-tt@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/AudioVideo/TT/" - } - }, - { - "name": { - "type": "literal", - "value": "Tracking Protection Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-tracking@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2011/tracking-protection/" - } - }, - { - "name": { - "type": "literal", - "value": "User Agent Accessibility Guidelines Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:w3c-wai-ua@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/WAI/UA/" - } - }, - { - "name": { - "type": "literal", - "value": "Web Application Security Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-webappsec@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2011/webappsec/" - } - }, - { - "name": { - "type": "literal", - "value": "Web Applications Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-webapps@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2008/webapps/" - } - }, - { - "name": { - "type": "literal", - "value": "Web Content Accessibility Guidelines Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:w3c-wai-gl@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/WAI/GL/" - } - }, - { - "name": { - "type": "literal", - "value": "Web Cryptography Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-webcrypto@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2012/webcrypto/" - } - }, - { - "name": { - "type": "literal", - "value": "Web Notification Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-web-notification@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2010/web-notifications/" - } - }, - { - "name": { - "type": "literal", - "value": "Web Performance Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-web-perf@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2010/webperf/" - } - }, - { - "name": { - "type": "literal", - "value": "Web Real-Time Communications Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-webrtc@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2011/04/webrtc/" - } - }, - { - "name": { - "type": "literal", - "value": "WebFonts Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-webfonts-wg@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/Fonts/WG/" - } - }, - { - "name": { - "type": "literal", - "value": "XML Core Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-xml-core-wg@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/XML/Core/" - } - }, - { - "name": { - "type": "literal", - "value": "XML Processing Model Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-xml-processing-model-wg@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/XML/Processing/" - } - }, - { - "name": { - "type": "literal", - "value": "XML Security Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-xmlsec@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2008/xmlsec/" - } - } - ] - } -} diff --git a/tools/make-groups-db.js b/tools/make-groups-db.js deleted file mode 100644 index 26fbcd82b..000000000 --- a/tools/make-groups-db.js +++ /dev/null @@ -1,40 +0,0 @@ -// XXX -// This script is obsolete. -// I keep it here in case the RDF source becomes reliable and turns out to be better than the -// alternative we have. But for now, don't use this. - -// This script is used to update the database of groups used in recognising that a draft is -// really published by the right group as per the information provided. -// -// The database is not automatically up to date with the information available on the W3C -// web site since the latter is in RDF and in any case we don't wish to hit it up for every check. -// The process to update the database we have here is simple: -// -// 1. Visit http://kwz.me/8p. Copy the JSON into groups-sparql.json -// 2. Run this script - -'use strict'; - -import fs from 'fs'; -import pth, { dirname } from 'path'; -import { fileURLToPath } from 'url'; -import { importJSON } from '../lib/util.js'; - -const src = importJSON('./groups-sparql.json'); - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -const res = {}; -for (let i = 0, n = src.results.bindings.length; i < n; i += 1) { - const group = src.results.bindings[i]; - const key = group.mailbox.value.replace('mailto:', ''); - res[key] = { - url: group.homepage.value, - name: group.name.value, - }; -} - -fs.writeFileSync( - pth.join(__dirname, '../lib/groups-db.json'), - JSON.stringify(res, null, 4) -); diff --git a/tsconfig.json b/tsconfig.json index 4ad157798..a34e8eee5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,18 @@ { "compilerOptions": { - "allowJs": true, - "checkJs": true, + "declaration": true, "module": "nodenext", "moduleResolution": "nodenext", - "noEmit": true + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true, + "useUnknownInCatchVariables": false, + "verbatimModuleSyntax": true }, - "exclude": ["public/**/*", "test/**/*"] + "exclude": ["public/**/*", "test/**/*"], + "types": ["node"] } From a075da5173c62c07b62f7d712fc7acab64e5ab00 Mon Sep 17 00:00:00 2001 From: "Kenneth G. Franqueiro" Date: Mon, 18 May 2026 13:52:43 -0400 Subject: [PATCH 02/11] Convert JSON files to TypeScript modules (#2097) --- app.ts | 12 +- lib/api.ts | 6 +- lib/badterms.ts | 44 + lib/copyright-exceptions.json | 43 - lib/copyright-exceptions.ts | 54 ++ lib/exceptions-map.ts | 64 ++ lib/exceptions.json | 39 - lib/exceptions.ts | 28 +- lib/l10n.ts | 29 +- lib/rules-generic-sections.ts | 187 ++++ lib/rules-track.ts | 1427 ++++++++++++++++++++++++++++++ lib/rules.json | 1499 -------------------------------- lib/rules/headers/copyright.ts | 2 +- lib/rules/headers/w3c-state.ts | 9 +- lib/rules/metadata/profile.ts | 26 +- lib/rules/structure/neutral.ts | 2 +- lib/types.d.ts | 22 - lib/util.ts | 10 +- lib/validator.ts | 18 +- lib/views.ts | 86 +- public/badterms.json | 38 - test/api.js | 4 +- test/l10n.js | 4 +- 23 files changed, 1871 insertions(+), 1782 deletions(-) create mode 100644 lib/badterms.ts delete mode 100644 lib/copyright-exceptions.json create mode 100644 lib/copyright-exceptions.ts create mode 100644 lib/exceptions-map.ts delete mode 100644 lib/exceptions.json create mode 100644 lib/rules-generic-sections.ts create mode 100644 lib/rules-track.ts delete mode 100644 lib/rules.json delete mode 100644 public/badterms.json diff --git a/app.ts b/app.ts index f7da6dbcb..3e3946ac1 100644 --- a/app.ts +++ b/app.ts @@ -17,14 +17,12 @@ import { Server } from 'socket.io'; import tmp from 'tmp'; import * as api from './lib/api.js'; +import badterms from './lib/badterms.js'; import * as l10n from './lib/l10n.js'; -import { allProfiles } from './lib/util.js'; +import { allProfiles, specberusVersion } from './lib/util.js'; import { Specberus } from './lib/validator.js'; import * as views from './lib/views.js'; import type { ProfileModule } from './lib/types.js'; -import pkg from './package.json' with { type: 'json' }; - -const { version } = pkg; // Settings: const DEFAULT_PORT = 80; @@ -41,7 +39,6 @@ const io = new Server(server); // Middleware: app.use(morgan('combined')); app.use(compression()); -app.use('/badterms.json', cors()); app.use( fileUpload({ createParentPath: true, @@ -51,6 +48,9 @@ app.use( ); app.use(express.static('public')); +app.get('/badterms.json', cors(), (_, res) => { + res.json(badterms); +}); api.setUp(app); views.setUp(app); @@ -60,7 +60,7 @@ l10n.setLanguage('en_GB'); server.listen(process.argv[2] || process.env.PORT || DEFAULT_PORT); io.on('connection', socket => { - socket.emit('handshake', { version }); + socket.emit('handshake', { version: specberusVersion }); socket.on('extractMetadata', data => { if (!data.url && !data.file) return socket.emit('exception', { diff --git a/lib/api.ts b/lib/api.ts index 5c90ea9e5..0de128ae2 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -7,12 +7,10 @@ import EventEmitter from 'events'; import { fileTypeFromFile } from 'file-type'; import type { Express, Request, Response } from 'express'; -import { buildJSONresult, processParams } from './util.js'; +import { buildJSONresult, processParams, specberusVersion } from './util.js'; import { Specberus, type ValidateOptions } from './validator.js'; import type { HandlerMessage } from './types.js'; -import pkg from '../package.json' with { type: 'json' }; - /** * Send the JSON result to the client. * @@ -43,7 +41,7 @@ const getFullUrl = (req: Request) => const processGet = () => async (req: Request, res: Response) => { const path = getFullUrl(req).pathname; if (path === '/api/version') { - res.status(200).send(pkg.version); + res.status(200).send(specberusVersion); } else if (path === '/api/metadata' || path === '/api/validate') { await processRequest(req, res, req.query); } else { diff --git a/lib/badterms.ts b/lib/badterms.ts new file mode 100644 index 000000000..02ca91052 --- /dev/null +++ b/lib/badterms.ts @@ -0,0 +1,44 @@ +interface BadTerm { + term: string[]; + variation?: string[]; + alternatives: string[]; +} + +export default [ + { + term: ['master'], + variation: ['masters'], + alternatives: ['main'], + }, + { + term: ['slave'], + variation: ['slaves'], + alternatives: ['replica'], + }, + { + term: ['whitelist'], + variation: ['whitelists'], + alternatives: ['allowlist'], + }, + { + term: ['blacklist'], + variation: ['blacklists'], + alternatives: ['denylist'], + }, + { + term: ['grandfather'], + alternatives: ['legacy'], + }, + { + term: ['sanity'], + alternatives: ['coherence'], + }, + { + term: ['he', 'she', 'him', 'her'], + alternatives: ['they'], + }, + { + term: ['his', 'hers'], + alternatives: ['theirs'], + }, +] satisfies BadTerm[]; diff --git a/lib/copyright-exceptions.json b/lib/copyright-exceptions.json deleted file mode 100644 index 3397dfbf9..000000000 --- a/lib/copyright-exceptions.json +++ /dev/null @@ -1,43 +0,0 @@ -[ - { - "specShortnames": [ - "epub-overview-33", - "epub-overview-34", - "epub-33", - "epub-34", - "epub-rs-33", - "epub-rs-34", - "epub-multi-rend-11", - "epub-tts-10", - "epub-ssv-11", - "epub-a11y-11", - "epub-a11y-12", - "epub-a11y-tech-11", - "epub-a11y-tech-12", - "epub-aria-authoring-11", - "epubcfi", - "epubcfi-11" - ], - "copyright": "Copyright © 1999-@YEAR International Digital Publishing Forum and World Wide Web Consortium. W3C® liability, trademark and permissive document license rules apply." - }, - { - "specShortnames": ["webrtc"], - "copyright": "Initial Author of this Specification was Ian Hickson, Google Inc., with the following copyright statement:
    © Copyright 2004-2011 Apple Computer, Inc., Mozilla Foundation, and Opera Software ASA. You are granted a license to use, reproduce and create derivative works of this document. All subsequent changes since 26 July 2011 done by the W3C WebRTC Working Group are under the following Copyright:
    Copyright © 2011-@YEAR World Wide Web Consortium. W3C® liability, trademark and permissive document license rules apply." - }, - { - "specShortnames": ["security-privacy-questionnaire"], - "copyright": "\"CC0\" To the extent possible under law, the editors have waived all copyright and related or neighboring rights to this work. This document is also made available under the W3C Software and Document License." - }, - { - "specShortnames": ["mediacapture-streams"], - "copyright": "Initial Author of this Specification was Ian Hickson, Google Inc., with the following copyright statement:
    © Copyright 2004-2011 Apple Computer, Inc., Mozilla Foundation, and Opera Software ASA. You are granted a license to use, reproduce and create derivative works of this document." - }, - { - "specShortnames": ["sdw-bp"], - "copyright": "Copyright © @YEAR OGC & World Wide Web Consortium. W3C® liability, trademark, W3C and OGC document use rules apply." - }, - { - "specShortnames": ["html-ruby-extensions"], - "copyright": "Copyright © @YEAR World Wide Web Consortium and WHATWG (Apple, Google, Mozilla, Microsoft). W3C® liability, and trademark rules apply. This work is made available under a dual license: the Creative Commons Attribution 4.0 International License (CC BY 4.0), and the W3C Software and Document license. Portions of this work are derived from material originally published by the WHATWG under the CC BY 4.0 license; those portions remain available under that license." - } -] diff --git a/lib/copyright-exceptions.ts b/lib/copyright-exceptions.ts new file mode 100644 index 000000000..52c8ad15f --- /dev/null +++ b/lib/copyright-exceptions.ts @@ -0,0 +1,54 @@ +interface CopyrightException { + specShortnames: string[]; + copyright: string; +} + +export default [ + { + specShortnames: [ + 'epub-overview-33', + 'epub-overview-34', + 'epub-33', + 'epub-34', + 'epub-rs-33', + 'epub-rs-34', + 'epub-multi-rend-11', + 'epub-tts-10', + 'epub-ssv-11', + 'epub-a11y-11', + 'epub-a11y-12', + 'epub-a11y-tech-11', + 'epub-a11y-tech-12', + 'epub-aria-authoring-11', + 'epubcfi', + 'epubcfi-11', + ], + copyright: + 'Copyright © 1999-@YEAR International Digital Publishing Forum and World Wide Web Consortium. W3C® liability, trademark and permissive document license rules apply.', + }, + { + specShortnames: ['webrtc'], + copyright: + 'Initial Author of this Specification was Ian Hickson, Google Inc., with the following copyright statement:
    © Copyright 2004-2011 Apple Computer, Inc., Mozilla Foundation, and Opera Software ASA. You are granted a license to use, reproduce and create derivative works of this document. All subsequent changes since 26 July 2011 done by the W3C WebRTC Working Group are under the following Copyright:
    Copyright © 2011-@YEAR World Wide Web Consortium. W3C® liability, trademark and permissive document license rules apply.', + }, + { + specShortnames: ['security-privacy-questionnaire'], + copyright: + 'CC0 To the extent possible under law, the editors have waived all copyright and related or neighboring rights to this work. This document is also made available under the W3C Software and Document License.', + }, + { + specShortnames: ['mediacapture-streams'], + copyright: + 'Initial Author of this Specification was Ian Hickson, Google Inc., with the following copyright statement:
    © Copyright 2004-2011 Apple Computer, Inc., Mozilla Foundation, and Opera Software ASA. You are granted a license to use, reproduce and create derivative works of this document.', + }, + { + specShortnames: ['sdw-bp'], + copyright: + 'Copyright © @YEAR OGC & World Wide Web Consortium. W3C® liability, trademark, W3C and OGC document use rules apply.', + }, + { + specShortnames: ['html-ruby-extensions'], + copyright: + 'Copyright © @YEAR World Wide Web Consortium and WHATWG (Apple, Google, Mozilla, Microsoft). W3C® liability, and trademark rules apply. This work is made available under a dual license: the Creative Commons Attribution 4.0 International License (CC BY 4.0), and the W3C Software and Document license. Portions of this work are derived from material originally published by the WHATWG under the CC BY 4.0 license; those portions remain available under that license.', + }, +] satisfies CopyrightException[]; diff --git a/lib/exceptions-map.ts b/lib/exceptions-map.ts new file mode 100644 index 000000000..40a7b77df --- /dev/null +++ b/lib/exceptions-map.ts @@ -0,0 +1,64 @@ +export interface Exception { + rule: string; + message?: RegExp; + type?: string; +} + +export const exceptions = new Map([ + [ + /.*/, + [ + { + rule: 'validation.html', + type: 'noexistence-at-all', + message: /^CSS: .*$/, + }, + { + rule: 'validation.html', + message: + /Bad value “publication” for attribute “rel” on element “link”: The string “publication” is not a registered keyword./, + }, + ], + ], + [ + /^audiobooks$/, + [ + { + rule: 'sotd.candidate-review-end', + }, + ], + ], + [ + /^did-core$/, + [ + { + rule: 'heuristic.date-format', + }, + ], + ], + [ + /^privacy-principles$/, + [ + { + rule: 'headers.editor-participation', + }, + ], + ], + [ + /^pub-manifest$/, + [ + { + rule: 'sotd.candidate-review-end', + }, + ], + ], + [ + /^webgpu|WGSL$/, + [ + { + rule: 'headers.dl', + message: /Implementation report/, + }, + ], + ], +]); diff --git a/lib/exceptions.json b/lib/exceptions.json deleted file mode 100644 index 2cba7e587..000000000 --- a/lib/exceptions.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - ".*": [ - { - "rule": "validation.html", - "type": "noexistence-at-all", - "message": "^CSS: .*$" - }, - { - "rule": "validation.html", - "message": "Bad value “publication” for attribute “rel” on element “link”: The string “publication” is not a registered keyword." - } - ], - "^audiobooks$": [ - { - "rule": "sotd.candidate-review-end" - } - ], - "^did-core$": [ - { - "rule": "heuristic.date-format" - } - ], - "^privacy-principles$": [ - { - "rule": "headers.editor-participation" - } - ], - "^pub-manifest$": [ - { - "rule": "sotd.candidate-review-end" - } - ], - "^webgpu|WGSL$": [ - { - "rule": "headers.dl", - "message": "Implementation report" - } - ] -} diff --git a/lib/exceptions.ts b/lib/exceptions.ts index e3867f2e7..1dbbe4bc6 100644 --- a/lib/exceptions.ts +++ b/lib/exceptions.ts @@ -1,27 +1,11 @@ -import exceptions from './exceptions.json' with { type: 'json' }; - -interface Exception { - rule: string; - message?: string; - type?: string; -} +import { exceptions, type Exception } from './exceptions-map.js'; function findSet(shortname: string) { - let count = 0; - function recursiveFindSet(name: string) { - if (count > 10) return undefined; - const set: Exception[][] = []; - for (const k in exceptions) { - const regex = new RegExp(k); - if (Object.hasOwn(exceptions, k) && regex.test(name)) { - set.push(exceptions[k as keyof typeof exceptions]); - } - } - count += 1; - if (typeof set === 'string') return recursiveFindSet(set); - return set; + const set: Exception[][] = []; + for (const [regexp, applicableExceptions] of exceptions) { + if (regexp.test(shortname)) set.push(applicableExceptions); } - return recursiveFindSet(shortname); + return set; } export function hasExceptions( @@ -41,7 +25,7 @@ export function hasExceptions( (exception.type === undefined || extra.type === exception.type)) || (exception.message && - new RegExp(exception.message).test(extra.message))) + exception.message.test(extra.message))) ) { return true; } diff --git a/lib/l10n.ts b/lib/l10n.ts index 0277f62dc..bce7012ee 100644 --- a/lib/l10n.ts +++ b/lib/l10n.ts @@ -3,15 +3,9 @@ */ import { messages } from './l10n-en_GB.js'; -import originalRules from './rules.json' with { type: 'json' }; -import type { - GenericRulesSection, - RuleBase, - RuleMeta, - RulesProfile, - RulesSection, -} from './types.js'; -import { isRuleTrack } from './util.js'; +import originalRules, { type RulesProfile } from './rules-track.js'; +import genericSections from './rules-generic-sections.js'; +import type { RuleBase, RuleMeta } from './types.js'; const enGB = messages; @@ -20,15 +14,9 @@ type LanguageMap = Record; let lang: LanguageMap | undefined; const profileRules: Record = {}; -for (const t of Object.keys(originalRules)) - if (isRuleTrack(t)) - for (const [p, profile] of Object.entries(originalRules[t].profiles)) - profileRules[p] = profile; - -const genericSections = originalRules['*'].sections as Record< - string, - GenericRulesSection ->; +for (const { profiles } of Object.values(originalRules)) + for (const [p, profile] of Object.entries(profiles)) + profileRules[p] = profile; /** * Set a locale to be used globally by this module. @@ -119,10 +107,7 @@ export function message( if (typeof rule === 'object' && 'rule' in rule) { let selector; - const profileSections = profileRules[profile].sections as Record< - string, - RulesSection - >; + const profileSections = profileRules[profile].sections; if (profileSections[rule.section]) { if (profileSections[rule.section].rules[rule.rule]) { const genericRules = diff --git a/lib/rules-generic-sections.ts b/lib/rules-generic-sections.ts new file mode 100644 index 000000000..ae7a8c874 --- /dev/null +++ b/lib/rules-generic-sections.ts @@ -0,0 +1,187 @@ +export interface GenericRulesSection { + name: string; + rules: Record; +} + +export default { + format: { + name: '1. Normative Document Representation', + rules: { + normativeVersion: + 'At least one normative representation must be available for requests that use the "This Version" URI. More than one normative representation may be delivered in response to such requests. A "This Version" URI must not be used to identify a non-normative representation.', + validHTML: + 'recursive All normative representations must validate as HTML5 with the following limitations:
    • Inline markup for MathML is permitted but should use a (fallback) alternative.
    • If the HTML5 validator issues content warnings, the publication request must include rationale why the warning is not problematic.
    ', + visualStyle: + 'Visual styles should not vary significantly among normative alternatives.', + }, + }, + metadata: { + name: '2. Document Metadata', + rules: { + goodStylesheet: + 'recursive Each document must include the following absolute URI to identify a style sheet for this maturity level: https://www.w3.org/StyleSheets/TR/2021/W3C-@{param1}

    Include this source code:
    <link rel="stylesheet" type="text/css" href="https://www.w3.org/StyleSheets/TR/2021/W3C-@{param1}"/>

    ', + lastStylesheet: + 'recursive Any internal style sheets must be cascaded before this link; i.e., the internal style sheets must not override the W3C tech report styles.', + viewport: + 'recursive The viewport meta tag is required.

    Include this source code:
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    ', + canonical: + 'recursive The canonical link is required.

    Include this source code:
    <link rel="canonical" href="@@URL@@">

    ', + delivererID: + 'The Working Group(s) id(s) must be listed in a data-deliverer attribute in the section "Status of This Document".

    Include this source code:
    <p data-deliverer="@@ID1@@ @@ID2@@,...">

    ', + }, + }, + 'front-matter': { + name: '3. Front Matter', + rules: { + divClassHead: + 'The front matter must appear at the beginning of the body of the document, within <div class="head">. There is one exception to that requirement: the hr element after the copyright may appear inside or after the div element. Editors should not include other information in this section.', + logo: 'The document must include a link to the W3C logo identified below. The URI used to identify the logo must be absolute.
    Include this source code:
    <a href="https://www.w3.org/"><img height="48" width="72" alt="W3C" src="https://www.w3.org/StyleSheets/TR/2021/logos/W3C"/></a>
    ', + title: "The document's title must be in the title element and in an h1 element. When calculating text equation, special transformations made to h1 are:
    1. Replace ':<br>' with ': '
    2. Replace '<br>' with ' - '
    3. Extract text from h1, and ignore HTML tags.
    ", + versionNumber: + 'Technical report version information, i.e., version and edition numbers.
      @{param1}
    1. See the (non-normative) Version Management in W3C Technical Reports for more information.
    ', + dateState: + 'The document\'s status and date must be in a <p id="w3c-state"> element as follows (see also date syntax):
    <p id="w3c-state">W3C @{param1} DD Month YYYY</p>
    @{param2}', + docIDFormat: + 'Document identifier information must be presented in a dl list, where each dt element marks up an identifier role ("This Version", "Latest Version", "History", etc.) and each dd element includes a link whose link text is the identifier. That dl must itself be placed in a details element.

    Include this source code:
    <details open><summary>More details about this document</summary><dl>...</dl></details>

    ', + docIDOrder: + 'Document identifier information must be present in this order:
    • \'This version\' - URI to that version
    • \'Latest version\' - URI to the latest version. See also the (non-normative) Version Management in W3C Technical Reports for information about "latest version" URI and version management.
    • \'History\' - URI to the history of the specification
    • Editor(s)
    • Feedback - GitHub repository issue links are required in the <dl>after <dt>Feedback:</dt> in the headers (<div class="head">) of the document. Links are expected to be of the form https://github.com/<USER_OR_ORG>/<REPO_NAME>/[issues|labels][/…].)
    ', + docIDThisVersion: + 'The syntax of a “this version” URI must be https://www.w3.org/TR/YYYY/@{param1}-shortname-YYYYMMDD/. If the document introduces a new shortname, it must use lowercase letters.', + docIDDate: + 'The title page date and the date at the end of the "This Version" URI must match.', + docIDLatestVersion: + 'The syntax of a “latest version” URI must be https://www.w3.org/TR/shortname/.', + docIDHistory: + "The syntax of a “history” URI must be https://www.w3.org/standards/history/shortname/, and consistent with the shortname mentioned in 'Latest Version'. Note: If there's a shortname change it must be specified using the following data attribute data-previous-shortname='previous-shortname' on the <a> element.", + editorSection: + 'The editors\'/authors\' names must be listed, including one of the following attributes (listed in descending order of precedence):
    • data-editor-id="@@" (referencing W3C ID)
    • data-editor-github="@@" (referencing GitHub username)
    Affiliations and email addresses are optional; email addresses are not recommended. The affiliation of Invited Experts must always be "W3C Invited Expert" whether they are affiliated with another organization or not. If an editor/author is acknowledged in an earlier version of this document and the individual\'s affiliation has since changed, list the individual using the notation "<name>, <affiliation> (until DD Month YYYY)". If the list of authors is very long (e.g., the entire Working Group), identify the authors in the acknowledgments section, linked from the head of the document. Distinguish any contributors from authors in the acknowledgments section.
    Note: Editors must be participating in the group producing the document at the time of its publication. If an editor left the group before publication, they can still be listed but the rationale must be provided in a <span class="former"> element next to the name of the editor.', + altRepresentations: + 'Authors may provide links to alternative (non-normative) representations or packages for the document. For instance:

    <p>This document is also available in these non-normative formats: <a href="@{param1}-shortname-20180101.html">single HTML file</a>, <a href="@{param1}-shortname-20180101.tgz">gzipped tar file of HTML</a>.</p>

    ', + implReport: + 'It must include either:
    • a link to an interoperability or implementation report if the Director used such a report as part of the decision to advance the specification, or
    • a statement that the Director\'s decision did not involve such a report.
    ', + translation: + 'There must be a link to a translations page. The recommended markup is:

    <p>See also <a href="https://www.w3.org/Translations/?technology=shortname"><strong>translations</strong></a>.</p>

    See suggestions on translations in the manual of style.

    ', + copyright: + 'Starting from 01 February 2023, the copyright must follow the following markup (fill in with the appropriate year, years, or year range). The type of license the document is using can be found in the group\'s charter.
    1. For documents using W3C Document License:
      Include this source code:
      <p class="copyright"><a href="https://www.w3.org/policies/#copyright">Copyright</a> © @{year} <a href="https://www.w3.org/">World Wide Web Consortium</a>. <abbr title="World Wide Web Consortium">W3C</abbr><sup>®</sup> <a href="https://www.w3.org/policies/#Legal_Disclaimer">liability</a>, <a href="https://www.w3.org/policies/#W3C_Trademarks">trademark</a> and <a href="https://www.w3.org/copyright/document-license/">document use</a> rules apply.</p>
    2. For documents using W3C Software and Document License:
      Include this source code:
      <p class="copyright"><a href="https://www.w3.org/policies/#copyright">Copyright</a> © @{year} <a href="https://www.w3.org/">World Wide Web Consortium</a>. <abbr title="World Wide Web Consortium">W3C</abbr><sup>®</sup> <a href="https://www.w3.org/policies/#Legal_Disclaimer">liability</a>, <a href="https://www.w3.org/policies/#W3C_Trademarks">trademark</a> and <a href="https://www.w3.org/copyright/software-license/">permissive document license</a> rules apply.</p>

    Note: Exceptions are listed in the copyright-exceptions module.', + hrAfterCopyright: + 'A horizontal rule (hr) must follow the copyright.', + }, + }, + navigation: { + name: '6. Table of Contents', + rules: { + toc: 'There should be a table of contents after the status section, labeled with an h2 element with content "Table of Contents".', + tocNav: 'The table of content must be inside a navigation element (nav).

    Include this source code:
    <nav id="toc"><h2>Table of Contents</h2>

    ', + }, + }, + 'document-status': { + name: '5. Document Status Section', + rules: { + sotd: 'There must be a status section that follows the abstract, labeled with an h2 element with content "Status of This Document". The Team maintains the status section of a document.', + boilerplateTRDoc: + 'It must begin with the following boilerplate text:

    This section describes the status of this document at the time of its publication. A list of current W3C publications and the latest revision of this technical report can be found in the W3C standards and drafts index.

    Include this source code:
    <p><em>This section describes the status of this document at the time of its publication. A list of current W3C publications and the latest revision of this technical report can be found in the <a href="https://www.w3.org/TR/">W3C standards and drafts index</a>.</em></p>
    ', + datesFormat: + 'All dates must have one of the following forms:
    1. DD Month YYYY : 09 January 2020
    2. DD-Month-YYYY : 09-January-2020
    3. DD Mon YYYY : 09 Jan 2020
    4. DD-Mon-YYYY : 09-Jan-2020
    A leading zero in the day is optional.', + publish: + 'It must include the name of the W3C group that produced the document, the type of document and its track. The name must be a link to a public page for the group.

    This document was published by the @@@ Working/Interest Group as a @{param1} using the @{param2}.

    Include this source code:
    <p>This document was published by the <a href="https://www.w3.org/groups/(wg|ig)/@@/">@@@ Working/Interest Group</a> as a @{param1} using the @{param3}.</p>
    ', + customParagraph: + 'It must include at least one customized paragraph. This section should include the title page date (i.e., the one next to the maturity level at the top of the document). These paragraphs should explain the publication context, including rationale and relationships to other work. See examples and more discussion in the Manual of Style.', + changesList: + 'It @{param1} include a link to changes since the previous draft (e.g., a list of changes or a diff document or both; see the online HTML diff tool).@{param2}', + deployment: + 'It must include the expectations in terms of deployment. The recommended text is:
    W3C recommends the wide deployment of this specification as a standard for the Web.
    ', + stability: + 'It must set expectations about the (in)stability of the document. The recommended text @{param2} is:

    Publication as @{param1} does not imply endorsement by W3C and its Members.

    Include this source code:
    <p>Publication as @{param1} does not imply endorsement by W3C and its Members.</p>
    ', + draftStability: + 'It must include the following sentences in the "Status Of This Document":
    This is a draft document and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to cite this document as other than a work in progress.
    ', + patPolReq: + 'It must include a text related to the patent policy:
    • if the document is under REC track, the text is:

      This document was produced by a group operating under the W3C Patent Policy. W3C maintains a public list of any patent disclosures made in connection with the deliverables of the group; that page also includes instructions for disclosing a patent. An individual who has actual knowledge of a patent that the individual believes contains Essential Claim(s) must disclose the information in accordance with section 6 of the W3C Patent Policy.

      Include this source code:
      <p>This document was produced by a group operating under the <a href="https://www.w3.org/policies/patent-policy/">W3C Patent Policy</a>. W3C maintains a <a rel="disclosure" href="@@URI to IPP status or other page@@">public list of any patent disclosures</a> made in connection with the deliverables of the group; that page also includes instructions for disclosing a patent. An individual who has actual knowledge of a patent that the individual believes contains <a href="https://www.w3.org/policies/patent-policy/#def-essential">Essential Claim(s)</a> must disclose the information in accordance with <a href="https://www.w3.org/policies/patent-policy/#sec-Disclosure">section 6 of the W3C Patent Policy</a>.</p>
    • if the document is under Note track or Registry track, it must set the licensing requirements related to the Patent Policy. The text is:

      The W3C Patent Policy does not carry any licensing requirements or commitments on this document.

      Include this source code:
      <p>The <a href="https://www.w3.org/policies/patent-policy/">W3C Patent Policy</a> does not carry any licensing requirements or commitments on this document.</p>

    ', + knownDisclosureNumber: + 'It must not indicate the number of known disclosures at the time of publication.', + whichProcess: + 'The document must include the following boilerplate text in the status section to identify the governing process:

    This document is governed by the 18 August 2023 W3C Process Document.

    Include this source code:
    <p>This document is governed by the <a id="w3c_process_revision" href="https://www.w3.org/policies/process/20250818/">18 August 2023 W3C Process Document</a>. </p>
    ', + discontinue: + 'If the document was published due to a W3C decision to stop work on this material, the status section should include that rationale.', + expectations: + 'It should indicate the level of endorsement within the group for the material, set expectations that the group has completed work on the topics covered by the document, and set expectations about the group\'s commitment to respond to comments about the document.', + ACRepFeedbackEmail: + 'It also must provide information to Advisory Committee Representatives about how to send their review comments (e.g., the link to all AC reviews, or a link to a specific questionnaire)', + reviewEndDatePR: + 'It must include the end date of the review period.', + recRelation: + 'It must indicate that it rescinds a Recommendation and must link to the most recent Recommendation (if any) having the same major revision number.', + altTechno: + 'It should direct readers to alternative technologies.', + }, + }, + 'document-body': { + name: '7. Document Body', + rules: { + headingWithoutID: + 'recursive Every marked-up section and subsection of the document must have a target anchor. A section is identified by a heading element (h1-h6). The anchor may be specified using an id (or name if an a element is used) attribute on any of the following: the heading element itself, the parent div or section element of the heading element (where the heading element is the first child of the div or section), a descendant of the heading element, or an a immediately preceding the heading element.', + brokenLink: + 'The document must not have any broken internal links or broken links to other resources at w3.org. The document should not have any other broken links.', + cssValid: + 'recursive The document must not have any style sheet errors.', + namespaces: + 'recursive All proposed XML namespaces created by the publication of the document must follow URIs for W3C Namespaces .', + wcag: 'The document(s) must conform to the Web Content Accessibility Guidelines 2.2, Level AA. Note: You may wish to consult the customizable quick reference to Web Content Accessibility Guidelines 2.2.', + securityAndPrivacy: + 'The document should have a security and privacy considerations section.', + fixupJs: + 'The document must include the script fixup.js.

    Include this source code:
    <script src="//www.w3.org/scripts/TR/2021/fixup.js" type="application/javascript"></script>

    ', + }, + }, + compound: { + name: '8. Compound Documents', + rules: { + compoundFilesLocation: + 'If the document is compound (i.e., if it consists of more than one file), all the files must be under a directory /TR/YYYY/@{param1}-shortname-YYYYMMDD/. Exceptions are resources under these paths:
    1. https://www.w3.org/StyleSheets/
    2. https://www.w3.org/scripts/
    ', + compoundOverview: + 'The main page should be called Overview.html.', + compound: + 'All other files must be reachable by links from the document.', + }, + }, + 'rec-edit-install': { + name: '1. Install the document.', + rules: { + install: + '(if new dated version) Copy the original REC in /TR into a new dated <new date> directory in /TR', + }, + }, + 'rec-edit-stylesheet': { + name: '2. Use the corresponding stylesheet', + rules: { + stylesheet: + "For the W3C stylesheet, use the @{param1} version of the W3C stylesheet in use in the REC (old, or 2021). So, it's one of:
    1. https://www.w3.org/StyleSheets/TR/W3C-@{param1}.css
    2. https://www.w3.org/StyleSheets/TR/2021/W3C-@{param1}.css
    3. or reuse the (some?) content from https://www.w3.org/TR/html50/superseded.css as needed
    ", + }, + }, + 'rec-edit-w3c-state': { + name: '3. Change the title of the document', + rules: { + 'w3c-state': + 'In the <p id="w3c-state"> element containing the state and date ("W3C Recommendation DD Month YYYY"), add:
    1. a <br> element after the original date
    2. and "@{param1} <new date>" when the @{param1} document gets published by the webmaster
    ', + }, + }, + 'rec-edit-version': { + name: '4. Set the "This version" link', + rules: { + version: + '(if new dated version) This version link gets the form https://www.w3.org/TR/YYYY/@{param1}-<shortname-with-version>-<newdate>/', + }, + }, + 'rec-edit-previous': { + name: '5. Set the "Previous version" link', + rules: { + previous: + '(if new dated version) Previous version becomes the dated link of the REC being @{param1}', + }, + }, + 'rec-edit-sotd': { + name: '6. Change the "status of this document" section', + rules: { + sotd: 'The SOTD gets updated as follows:
    • 6.1. add the following markup (update link to proper Process version if needed):
      <p>This specification is a <a href="https://www.w3.org/policies/process/20250818/#rec-rescind">@{param1} Recommendation</a>. A newer specification exists that is recommended for new adoption in place of this specification. </p>
    • 6.2. Make sure to update or remove the sentence indicating that the document "is a Recommendation" if one appears.
    • 6.3. Remove the following paragraph:
      <p>This document has been reviewed by W3C Members, by software developers, and by other W3C groups and interested parties, and is endorsed by the Director as a W3C Recommendation. It is a stable document and may be used as reference material or cited from another document. W3C\'s role in making the Recommendation is to draw attention to the specification and to promote its widespread deployment. This enhances the functionality and interoperability of the Web.</p>
    • 6.4. ... and replace it with (update the @@ as needed):
      <p>For purposes of the W3C Patent Policy, this @{param1} Recommendation  has the same status as an active Recommendation; it retains licensing  commitments and remains available as a reference for old -- and  possibly still deployed -- implementations, but is not recommended for future implementation. New implementations should follow the <a href=\'@@\'>latest version</a> of the @@ specification.</p>
    • 6.5. DO NOT TOUCH THE PATENT POLICY PARAGRAPH. The document is still governed by IP commitments even if it is @{param2} (as described in the paragraph you added in the previous step).
    • 6.6. Look at the resulting SOTD, make sure it makes sense and "modify as needed". In other words, if you need to add more explanations about why the document was @{param2}, feel free to do so by adding to the first paragraph introduced by 6.1. The Director may ask to add or modify specific wordings as well. Just remember to keep the time spent as minimal since our goal is to minimize the time spent @{param3} documents, so don\'t overdo it.@{param4}
    ', + }, + }, +} satisfies Record; diff --git a/lib/rules-track.ts b/lib/rules-track.ts new file mode 100644 index 000000000..8ab7594a2 --- /dev/null +++ b/lib/rules-track.ts @@ -0,0 +1,1427 @@ +export interface RulesSection { + name: string; + rules: Record; +} + +export interface RulesProfile { + name: string; + order: number; + sections: { + [index: string]: RulesSection; + }; +} + +interface RulesEntry { + name: string; + order: number; + profiles: Record; +} + +export default { + SUBM: { + name: 'Submissions', + order: 4, + profiles: { + 'MEM-SUBM': { + order: 2, + name: 'Member Submission', + sections: { + format: { + name: '1. Normative Document Representation', + rules: { + normativeVersion: true, + validHTML: true, + visualStyle: true, + }, + }, + metadata: { + name: '2. Document Metadata', + rules: { + goodStylesheet: ['Member-SUBM'], + lastStylesheet: true, + viewport: true, + canonical: true, + }, + }, + 'front-matter': { + name: '3. Front Matter', + rules: { + divClassHead: true, + logo: true, + submlogo: + 'In addition to the W3C logo, use this logo:
    Include this source code:
    <a href="https://www.w3.org/submissions/"><img height="48" width="211" alt="W3C Member Submission" src="https://www.w3.org/Icons/member_subm"/></a>
    ', + title: true, + versionNumber: [''], + dateState: ['Member Submission', ''], + docIDFormat: true, + docIDOrder: true, + docIDThisVersion: + 'The syntax of a “This Version” URI must be https://www.w3.org/submissions/YYYY/SUBM-shortname-YYYYMMDD/.', + docIDLatestVersion: + 'The syntax of a “Latest Version” URI must be https://www.w3.org/submissions/shortname/.', + docIDDate: true, + editorSection: true, + altRepresentations: ['SUBM'], + copyright: + 'Copyright: The document must include a link to the W3C document notice (https://www.w3.org/copyright/document-license/). The copyright may be held by the Submitters.', + hrAfterCopyright: true, + }, + }, + navigation: { + name: '6. Table of Contents', + rules: { + toc: true, + tocNav: true, + }, + }, + 'document-status': { + name: '5. Document Status Section', + rules: { + sotd: true, + boilerplateTRDoc: + 'It must begin with the following boilerplate text:

    This section describes the status of this document at the time of its publication. A list of current W3C publications can be found in the W3C standards and drafts index.

    Include this source code:
    <p><em>This section describes the status of this document at the time of its publication. A list of current W3C publications can be found in the <a href="https://www.w3.org/TR/">W3C standards and drafts index</a>.</em></p>
    ', + boilerplateSUBM: + 'It must include this boilerplate text (with links to the published Submission and Team Comment):
    By publishing this document, W3C acknowledges that the Submitting Members have made a formal Submission request to W3C for discussion. Publication of this document by W3C indicates no endorsement of its content by W3C, nor that W3C has, is, or will be allocating any resources to the issues addressed by it. This document is not the product of a chartered W3C group, but is published as potential input to the W3C Process. A W3C Team Comment has been published in conjunction with this Member Submission. Publication of acknowledged Member Submissions at the W3C site is one of the benefits of W3C Membership. Please consult the requirements associated with Member Submissions of section 3.3 of the W3C Patent Policy. Please consult the complete list of acknowledged W3C Member Submissions.
    Include this source code:
    <p>By publishing this document, W3C acknowledges that the <a href="https://www.w3.org/submissions/@@@submissiondoc@@@">Submitting Members</a> have made a formal Submission request to W3C for discussion. Publication of this document by W3C indicates no endorsement of its content by W3C, nor that W3C has, is, or will be allocating any resources to the issues addressed by it. This document is not the product of a chartered W3C group, but is published as potential input to the <a href="https://www.w3.org/policies/process/">W3C Process</a>. A <a href="https://www.w3.org/submissions/@@@teamcomment@@@">W3C Team Comment</a> has been published in conjunction with this Member Submission. Publication of acknowledged Member Submissions at the W3C site is one of the benefits of <a href="https://www.w3.org/Consortium/Prospectus/Joining">W3C Membership</a>. Please consult the requirements associated with Member Submissions of <a href="https://www.w3.org/policies/patent-policy/#sec-submissions">section 3.3 of the W3C Patent Policy</a>. Please consult the complete <a href="https://www.w3.org/submissions/">list of acknowledged W3C Member Submissions</a>.</p>
    ', + customParagraph: true, + knownDisclosureNumber: true, + datesFormat: true, + }, + }, + 'document-body': { + name: '7. Document Body', + rules: { + headingWithoutID: true, + brokenLink: true, + cssValid: true, + namespaces: true, + wcag: true, + fixupJs: true, + }, + }, + compound: { + name: '8. Compound Documents', + rules: { + compoundFilesLocation: ['SUBM'], + compoundOverview: true, + compound: true, + }, + }, + }, + }, + }, + }, + REGISTRY: { + name: 'Registry Track', + order: 3, + profiles: { + DRY: { + order: 1, + name: 'Registry Draft', + sections: { + format: { + name: '1. Normative Document Representation', + rules: { + normativeVersion: true, + validHTML: true, + visualStyle: true, + }, + }, + metadata: { + name: '2. Document Metadata', + rules: { + goodStylesheet: ['DRY'], + lastStylesheet: true, + viewport: true, + canonical: true, + delivererID: true, + }, + }, + 'front-matter': { + name: '3. Front Matter', + rules: { + divClassHead: true, + logo: true, + title: true, + versionNumber: [''], + dateState: ['Registry Draft', ''], + docIDFormat: true, + docIDOrder: true, + docIDThisVersion: ['DRY'], + docIDDate: true, + docIDLatestVersion: true, + docIDHistory: true, + editorSection: true, + altRepresentations: ['DRY'], + copyright: true, + hrAfterCopyright: true, + }, + }, + navigation: { + name: '6. Table of Contents', + rules: { + toc: true, + tocNav: true, + }, + }, + 'document-status': { + name: '5. Document Status Section', + rules: { + sotd: true, + boilerplateTRDoc: true, + datesFormat: true, + publish: [ + 'Registry Draft', + 'Registry track', + '<a href="https://www.w3.org/policies/process/20250818/#recs-and-notes">Registry track</a>', + ], + customParagraph: true, + stability: + 'It must set expectations about the (in)stability of the document. The recommended text is:

    Publication as a Registry Draft does not imply endorsement by W3C and its Members

    ', + draftStability: true, + patPolReq: true, + knownDisclosureNumber: true, + whichProcess: true, + }, + }, + 'document-body': { + name: '7. Document Body', + rules: { + headingWithoutID: true, + brokenLink: true, + cssValid: true, + namespaces: true, + wcag: true, + fixupJs: true, + }, + }, + compound: { + name: '8. Compound Documents', + rules: { + compoundFilesLocation: ['DRY'], + compoundOverview: true, + compound: true, + }, + }, + }, + }, + CRY: { + order: 2, + name: 'Candidate Registry', + sections: { + format: { + name: '1. Normative Document Representation', + rules: { + normativeVersion: true, + validHTML: true, + visualStyle: true, + }, + }, + metadata: { + name: '2. Document Metadata', + rules: { + goodStylesheet: ['CRY'], + lastStylesheet: true, + viewport: true, + canonical: true, + delivererID: true, + }, + }, + 'front-matter': { + name: '3. Front Matter', + rules: { + divClassHead: true, + logo: true, + title: true, + versionNumber: [''], + dateState: ['Candidate Registry Snapshot', ''], + docIDFormat: true, + docIDOrder: true, + docIDThisVersion: ['CRY'], + docIDDate: true, + docIDLatestVersion: true, + docIDHistory: true, + editorSection: true, + altRepresentations: ['CRY'], + copyright: true, + hrAfterCopyright: true, + }, + }, + navigation: { + name: '6. Table of Contents', + rules: { + toc: true, + tocNav: true, + }, + }, + 'document-status': { + name: '5. Document Status Section', + rules: { + sotd: true, + boilerplateTRDoc: true, + datesFormat: true, + publish: [ + 'Candidate Registry Snapshot', + 'Registry track', + '<a href="https://www.w3.org/policies/process/20250818/#recs-and-notes">Registry track</a>', + ], + customParagraph: true, + stability: + 'It must set expectations about the (in)stability of the document. The recommended text is:

    Publication as a Registry Draft does not imply endorsement by W3C and its Members. A Candidate Registry Snapshot has received wide review.

    Include this source code:
    <p>Publication as a Registry Draft does not imply endorsement by W3C and its Members. A Candidate Registry Snapshot has received <a href="https://www.w3.org/policies/process/20250818/#dfn-wide-review">wide review</a>.</p>
    ', + reviewEndDate: + 'It must include a minimal duration (before which the group will not request the next transition). The duration must be expressed as an estimated date.', + knownDisclosureNumber: true, + patPolReq: true, + whichProcess: true, + }, + }, + 'document-body': { + name: '7. Document Body', + rules: { + headingWithoutID: true, + brokenLink: true, + cssValid: true, + namespaces: true, + wcag: true, + fixupJs: true, + }, + }, + compound: { + name: '8. Compound Documents', + rules: { + compoundFilesLocation: ['CRY'], + compoundOverview: true, + compound: true, + }, + }, + }, + }, + CRYD: { + order: 3, + name: 'Candidate Registry Draft', + sections: { + format: { + name: '1. Normative Document Representation', + rules: { + normativeVersion: true, + validHTML: true, + visualStyle: true, + }, + }, + metadata: { + name: '2. Document Metadata', + rules: { + goodStylesheet: ['CRYD'], + lastStylesheet: true, + viewport: true, + canonical: true, + delivererID: true, + }, + }, + 'front-matter': { + name: '3. Front Matter', + rules: { + divClassHead: true, + logo: true, + title: true, + versionNumber: [''], + dateState: ['Candidate Registry Draft', ''], + docIDFormat: true, + docIDOrder: true, + docIDThisVersion: ['CRYD'], + docIDDate: true, + docIDLatestVersion: true, + docIDHistory: true, + editorSection: true, + altRepresentations: ['CRYD'], + copyright: true, + hrAfterCopyright: true, + }, + }, + navigation: { + name: '6. Table of Contents', + rules: { + toc: true, + tocNav: true, + }, + }, + 'document-status': { + name: '5. Document Status Section', + rules: { + sotd: true, + boilerplateTRDoc: true, + datesFormat: true, + publish: [ + 'Candidate Registry Draft', + 'Registry track', + '<a href="https://www.w3.org/policies/process/20250818/#recs-and-notes">Registry track</a>', + ], + customParagraph: true, + stability: + 'It must set expectations about the (in)stability of the document. The recommended text is:

    Publication as a Candidate Registry Draft does not imply endorsement by W3C and its Members. A Candidate Registry Draft integrates changes from the previous Candidate Registry that the Working Group intends to include in a subsequent Candidate Registry Snapshot.

    ', + draftStability: + 'The document must include one of the following two paragraphs in the "Status Of This Document":
    This is a draft document and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to cite this document as other than a work in progress.
    This document is maintained and updated at any time. Some parts of this document are work in progress.
    ', + knownDisclosureNumber: true, + patPolReq: true, + whichProcess: true, + }, + }, + 'document-body': { + name: '7. Document Body', + rules: { + headingWithoutID: true, + brokenLink: true, + cssValid: true, + namespaces: true, + wcag: true, + fixupJs: true, + }, + }, + compound: { + name: '8. Compound Documents', + rules: { + compoundFilesLocation: ['CRYD'], + compoundOverview: true, + compound: true, + }, + }, + }, + }, + RY: { + order: 4, + name: 'Registry', + sections: { + format: { + name: '1. Normative Document Representation', + rules: { + normativeVersion: true, + validHTML: true, + visualStyle: true, + }, + }, + metadata: { + name: '2. Document Metadata', + rules: { + goodStylesheet: ['RY'], + lastStylesheet: true, + viewport: true, + canonical: true, + delivererID: true, + }, + }, + 'front-matter': { + name: '3. Front Matter', + rules: { + divClassHead: true, + logo: true, + title: true, + versionNumber: [''], + dateState: ['Registry', ''], + docIDFormat: true, + docIDOrder: true, + docIDThisVersion: ['RY'], + docIDDate: true, + docIDLatestVersion: true, + docIDHistory: true, + editorSection: true, + altRepresentations: ['RY'], + copyright: true, + hrAfterCopyright: true, + }, + }, + navigation: { + name: '6. Table of Contents', + rules: { + toc: true, + tocNav: true, + }, + }, + 'document-status': { + name: '5. Document Status Section', + rules: { + sotd: true, + boilerplateTRDoc: true, + datesFormat: true, + publish: [ + 'Registry', + 'Registry track', + '<a href="https://www.w3.org/policies/process/20250818/#recs-and-notes">Registry track</a>', + ], + customParagraph: true, + usage: 'It must include the expectations in terms of usage of this registry. The SOTD should include the paragraph:
    W3C recommends the wide usage of this registry.
    ', + stability: + 'It must set expectations about the (in)stability of the document. The recommended text is:

    A W3C Registry is a specification that, after extensive consensus-building, is endorsed by W3C and its Members.

    ', + knownDisclosureNumber: true, + patPolReq: true, + whichProcess: true, + }, + }, + 'document-body': { + name: '7. Document Body', + rules: { + headingWithoutID: true, + brokenLink: true, + cssValid: true, + namespaces: true, + wcag: true, + fixupJs: true, + }, + }, + compound: { + name: '8. Compound Documents', + rules: { + compoundFilesLocation: ['RY'], + compoundOverview: true, + compound: true, + }, + }, + }, + }, + }, + }, + NOTE: { + name: 'Note Track', + order: 2, + profiles: { + DNOTE: { + order: 1, + name: 'Group Note Draft', + sections: { + format: { + name: '1. Normative Document Representation', + rules: { + normativeVersion: true, + validHTML: true, + visualStyle: true, + }, + }, + metadata: { + name: '2. Document Metadata', + rules: { + goodStylesheet: ['DNOTE'], + lastStylesheet: true, + viewport: true, + canonical: true, + delivererID: true, + }, + }, + 'front-matter': { + name: '3. Front Matter', + rules: { + divClassHead: true, + logo: true, + title: true, + versionNumber: [''], + dateState: ['Group Note Draft', ''], + docIDFormat: true, + docIDOrder: true, + docIDThisVersion: ['DNOTE'], + docIDDate: true, + docIDLatestVersion: true, + docIDHistory: true, + editorSection: true, + altRepresentations: ['DNOTE'], + copyright: true, + hrAfterCopyright: true, + }, + }, + navigation: { + name: '6. Table of Contents', + rules: { + toc: true, + tocNav: true, + }, + }, + 'document-status': { + name: '5. Document Status Section', + rules: { + sotd: true, + boilerplateTRDoc: true, + publish: [ + 'Group Note Draft', + 'Note track', + '<a href="https://www.w3.org/policies/process/20250818/#recs-and-notes">Note track</a>', + ], + customParagraph: true, + stability: + 'It must set expectations about the (in)stability of the document. The recommended text is:

    Group Note Drafts are not endorsed by W3C nor its Members.

    or

    This Group Note Draft is endorsed by the @@@ Working/Interest Group (and the @@@ Working/Interest Group), but is not endorsed by W3C itself nor its Members.

    ', + draftStability: true, + knownDisclosureNumber: true, + patPolReq: true, + whichProcess: true, + }, + }, + 'document-body': { + name: '7. Document Body', + rules: { + headingWithoutID: true, + brokenLink: true, + cssValid: true, + namespaces: true, + wcag: true, + fixupJs: true, + }, + }, + compound: { + name: '8. Compound Documents', + rules: { + compoundFilesLocation: ['DNOTE'], + compoundOverview: true, + compound: true, + }, + }, + }, + }, + NOTE: { + order: 2, + name: 'Group Note', + sections: { + format: { + name: '1. Normative Document Representation', + rules: { + normativeVersion: true, + validHTML: true, + visualStyle: true, + }, + }, + metadata: { + name: '2. Document Metadata', + rules: { + goodStylesheet: ['NOTE'], + lastStylesheet: true, + viewport: true, + canonical: true, + delivererID: true, + }, + }, + 'front-matter': { + name: '3. Front Matter', + rules: { + divClassHead: true, + logo: true, + title: true, + versionNumber: [''], + dateState: ['Group Note', ''], + docIDFormat: true, + docIDOrder: true, + docIDThisVersion: ['NOTE'], + docIDDate: true, + docIDLatestVersion: true, + docIDHistory: true, + editorSection: true, + altRepresentations: ['NOTE'], + copyright: true, + hrAfterCopyright: true, + }, + }, + navigation: { + name: '6. Table of Contents', + rules: { + toc: true, + tocNav: true, + }, + }, + 'document-status': { + name: '5. Document Status Section', + rules: { + sotd: true, + boilerplateTRDoc: true, + datesFormat: true, + publish: [ + 'Group Note', + 'Note track', + '<a href="https://www.w3.org/policies/process/20250818/#recs-and-notes">Note track</a>', + ], + customParagraph: true, + stability: + 'It must set expectations about the (in)stability of the document. The recommended text is:

    Group Notes are not endorsed by W3C nor its Members.

    or

    This Group Note is endorsed by the @@@ Working/Interest Group (and the @@@ Working/Interest Group), but is not endorsed by W3C itself nor its Members.

    ', + knownDisclosureNumber: true, + patPolReq: true, + whichProcess: true, + }, + }, + 'document-body': { + name: '7. Document Body', + rules: { + headingWithoutID: true, + brokenLink: true, + cssValid: true, + namespaces: true, + wcag: true, + fixupJs: true, + }, + }, + compound: { + name: '8. Compound Documents', + rules: { + compoundFilesLocation: ['NOTE'], + compoundOverview: true, + compound: true, + }, + }, + }, + }, + STMT: { + order: 3, + name: 'Statement', + sections: { + format: { + name: '1. Normative Document Representation', + rules: { + normativeVersion: true, + validHTML: true, + visualStyle: true, + }, + }, + metadata: { + name: '2. Document Metadata', + rules: { + goodStylesheet: ['STMT'], + lastStylesheet: true, + viewport: true, + canonical: true, + delivererID: true, + }, + }, + 'front-matter': { + name: '3. Front Matter', + rules: { + divClassHead: true, + logo: true, + title: true, + versionNumber: [''], + dateState: ['Statement', ''], + docIDFormat: true, + docIDOrder: true, + docIDThisVersion: ['STMT'], + docIDDate: true, + docIDLatestVersion: true, + docIDHistory: true, + editorSection: true, + altRepresentations: ['STMT'], + translation: true, + copyright: true, + hrAfterCopyright: true, + }, + }, + navigation: { + name: '6. Table of Contents', + rules: { + toc: true, + tocNav: true, + }, + }, + 'document-status': { + name: '5. Document Status Section', + rules: { + sotd: true, + boilerplateTRDoc: true, + datesFormat: true, + publish: [ + 'Statement', + 'Note track', + '<a href="https://www.w3.org/policies/process/20250818/#recs-and-notes">Note track</a>', + ], + customParagraph: true, + stability: + 'It must set expectations about the (in)stability of the document. The recommended text is:

    A W3C Statement is a specification that, after extensive consensus-building, is endorsed by W3C and its Members.

    Include this source code:
    <p>A W3C Statement is a specification that, after extensive consensus-building, is endorsed by W3C and its Members.</p>
    ', + knownDisclosureNumber: true, + patPolReq: true, + whichProcess: true, + }, + }, + 'document-body': { + name: '7. Document Body', + rules: { + headingWithoutID: true, + brokenLink: true, + cssValid: true, + namespaces: true, + wcag: true, + fixupJs: true, + }, + }, + compound: { + name: '8. Compound Documents', + rules: { + compoundFilesLocation: ['STMT'], + compoundOverview: true, + compound: true, + }, + }, + }, + }, + }, + }, + REC: { + name: 'Recommendation Track', + order: 1, + profiles: { + FPWD: { + order: 1, + name: 'First Public Working Draft', + sections: { + format: { + name: '1. Normative Document Representation', + rules: { + normativeVersion: true, + validHTML: true, + visualStyle: true, + }, + }, + metadata: { + name: '2. Document Metadata', + rules: { + goodStylesheet: ['WD'], + lastStylesheet: true, + viewport: true, + canonical: true, + }, + }, + 'front-matter': { + name: '3. Front Matter', + rules: { + divClassHead: true, + logo: true, + title: true, + versionNumber: [''], + dateState: ['First Public Working Draft', ''], + docIDFormat: true, + docIDOrder: true, + docIDThisVersion: ['WD'], + docIDDate: true, + docIDLatestVersion: true, + docIDHistory: true, + editorSection: true, + altRepresentations: ['WD'], + copyright: true, + hrAfterCopyright: true, + }, + }, + navigation: { + name: '6. Table of Contents', + rules: { + toc: true, + tocNav: true, + }, + }, + 'document-status': { + name: '5. Document Status Section', + rules: { + sotd: true, + boilerplateTRDoc: true, + datesFormat: true, + publish: [ + 'First Public Working Draft', + 'Recommendation track', + '<a href="https://www.w3.org/policies/process/20250818/#recs-and-notes">Recommendation track</a>', + ], + customParagraph: true, + stability: ['a First Public Working Draft', ''], + draftStability: true, + patPolReq: true, + knownDisclosureNumber: true, + whichProcess: true, + }, + }, + 'document-body': { + name: '7. Document Body', + rules: { + headingWithoutID: true, + brokenLink: true, + cssValid: true, + namespaces: true, + wcag: true, + fixupJs: true, + }, + }, + compound: { + name: '8. Compound Documents', + rules: { + compoundFilesLocation: ['WD'], + compoundOverview: true, + compound: true, + }, + }, + }, + }, + WD: { + order: 2, + name: 'Working Draft', + sections: { + format: { + name: '1. Normative Document Representation', + rules: { + normativeVersion: true, + validHTML: true, + visualStyle: true, + }, + }, + metadata: { + name: '2. Document Metadata', + rules: { + goodStylesheet: ['WD'], + lastStylesheet: true, + viewport: true, + canonical: true, + }, + }, + 'front-matter': { + name: '3. Front Matter', + rules: { + divClassHead: true, + logo: true, + title: true, + versionNumber: [''], + dateState: ['Working Draft', ''], + docIDFormat: true, + docIDOrder: true, + docIDThisVersion: ['WD'], + docIDDate: true, + docIDLatestVersion: true, + docIDHistory: true, + editorSection: true, + altRepresentations: ['WD'], + copyright: true, + hrAfterCopyright: true, + }, + }, + navigation: { + name: '6. Table of Contents', + rules: { + toc: true, + tocNav: true, + }, + }, + 'document-status': { + name: '5. Document Status Section', + rules: { + sotd: true, + boilerplateTRDoc: true, + datesFormat: true, + publish: [ + 'Working Draft', + 'Recommendation track', + '<a href="https://www.w3.org/policies/process/20250818/#recs-and-notes">Recommendation track</a>', + ], + customParagraph: true, + changesList: ['should', ''], + stability: ['a Working Draft', ''], + draftStability: true, + patPolReq: true, + knownDisclosureNumber: true, + whichProcess: true, + }, + }, + 'document-body': { + name: '7. Document Body', + rules: { + headingWithoutID: true, + brokenLink: true, + cssValid: true, + namespaces: true, + wcag: true, + fixupJs: true, + securityAndPrivacy: true, + }, + }, + compound: { + name: '8. Compound Documents', + rules: { + compoundFilesLocation: ['WD'], + compoundOverview: true, + compound: true, + }, + }, + }, + }, + CR: { + order: 4, + name: 'Candidate Recommendation Snapshot', + sections: { + format: { + name: '1. Normative Document Representation', + rules: { + normativeVersion: true, + validHTML: true, + visualStyle: true, + }, + }, + metadata: { + name: '2. Document Metadata', + rules: { + goodStylesheet: ['CR'], + lastStylesheet: true, + viewport: true, + canonical: true, + }, + }, + 'front-matter': { + name: '3. Front Matter', + rules: { + divClassHead: true, + logo: true, + title: true, + versionNumber: [''], + dateState: [ + 'Candidate Recommendation Snapshot', + '', + ], + docIDFormat: true, + docIDOrder: true, + docIDThisVersion: ['CR'], + docIDDate: true, + docIDLatestVersion: true, + docIDHistory: true, + editorSection: true, + altRepresentations: ['CR'], + implReport: + 'It must include a link to a preliminary interoperability or implementation report, or a statement that no such report exists.', + copyright: true, + hrAfterCopyright: true, + }, + }, + navigation: { + name: '6. Table of Contents', + rules: { + toc: true, + tocNav: true, + }, + }, + 'document-status': { + name: '5. Document Status Section', + rules: { + sotd: true, + boilerplateTRDoc: true, + datesFormat: true, + publish: [ + 'Candidate Recommendation Snapshot', + 'Recommendation track', + '<a href="https://www.w3.org/policies/process/20250818/#recs-and-notes">Recommendation track</a>', + ], + reviewEndDate: + 'It must include a minimal duration (before which the group will not request the next transition). The duration must be expressed as an estimated date.', + implEstimation: + 'It should include an estimated date by which time the Working Group expects to have sufficient implementation experience.', + featAtRisk: + 'It must identify any "features at risk" declared by the Working Group (as defined in section 6.4 of the W3C Process Document).', + customParagraph: true, + changesList: ['must', ''], + stability: + 'It must set expectations about the (in)stability of the document. The recommended text is:

    Publication as a Candidate Recommendation does not imply endorsement by W3C and its Members. A Candidate Recommendation Snapshot has received wide review, is intended to gather implementation experience, and has commitments from Working Group members to royalty-free licensing for implementations.

    Include this source code:
    <p>Publication as a Candidate Recommendation does not imply endorsement by W3C and its Members. A Candidate Recommendation Snapshot has received <a href="https://www.w3.org/policies/process/20250818/#dfn-wide-review">wide review</a>, is intended to gather implementation experience, and has commitments from Working Group members to <a href="https://www.w3.org/policies/patent-policy/#sec-Requirements">royalty-free licensing</a> for implementations.</p>
    ', + patPolReq: true, + newFeatures: + 'If it is the intention to incorporate new features in future updates of the specification, please make sure to identify the document as intending to allow new features. Recommended text is:
    Future updates to this upcoming Recommendation may incorporate new features.
    Include one of this source code:
    <p>Future updates to this upcoming Recommendation may incorporate <a href="https://www.w3.org/policies/process/20250818/#allow-new-features">new features</a>.</p>', + knownDisclosureNumber: true, + whichProcess: true, + }, + }, + 'document-body': { + name: '7. Document Body', + rules: { + headingWithoutID: true, + brokenLink: true, + cssValid: true, + namespaces: true, + wcag: true, + fixupJs: true, + securityAndPrivacy: true, + }, + }, + compound: { + name: '8. Compound Documents', + rules: { + compoundFilesLocation: ['CR'], + compoundOverview: true, + compound: true, + }, + }, + }, + }, + CRD: { + order: 4, + name: 'Candidate Recommendation Draft', + sections: { + format: { + name: '1. Normative Document Representation', + rules: { + normativeVersion: true, + validHTML: true, + visualStyle: true, + }, + }, + metadata: { + name: '2. Document Metadata', + rules: { + goodStylesheet: ['CRD'], + lastStylesheet: true, + viewport: true, + canonical: true, + }, + }, + 'front-matter': { + name: '3. Front Matter', + rules: { + divClassHead: true, + logo: true, + title: true, + versionNumber: [''], + dateState: ['Candidate Recommendation Draft', ''], + docIDFormat: true, + docIDOrder: true, + docIDThisVersion: ['CRD'], + docIDDate: true, + docIDLatestVersion: true, + docIDHistory: true, + editorSection: true, + altRepresentations: ['CRD'], + implReport: + 'It must include a link to a preliminary interoperability or implementation report, or a statement that no such report exists.', + copyright: true, + hrAfterCopyright: true, + }, + }, + navigation: { + name: '6. Table of Contents', + rules: { + toc: true, + tocNav: true, + }, + }, + 'document-status': { + name: '5. Document Status Section', + rules: { + sotd: true, + boilerplateTRDoc: true, + datesFormat: true, + publish: [ + 'Candidate Recommendation Draft', + 'Recommendation track', + '<a href="https://www.w3.org/policies/process/20250818/#recs-and-notes">Recommendation track</a>', + ], + reviewEndDate: + 'It must include a minimal duration (before which the group will not request the next transition). The duration must be expressed as an estimated date.', + implEstimation: + 'It should include an estimated date by which time the Working Group expects to have sufficient implementation experience.', + featAtRisk: + 'It must identify any "features at risk" declared by the Working Group (as defined in section 6.4 of the W3C Process Document).', + customParagraph: true, + changesList: ['must', ''], + stability: + 'It must set expectations about the (in)stability of the document. The recommended text is:

    Publication as a Candidate Recommendation does not imply endorsement by W3C and its Members. A Candidate Recommendation Draft integrates changes from the previous Candidate Recommendation that the Working Group intends to include in a subsequent Candidate Recommendation Snapshot.

    Include this source code:
    <p>Publication as a Candidate Recommendation does not imply endorsement by W3C and its Members. A Candidate Recommendation Draft integrates changes from the previous Candidate Recommendation that the Working Group intends to include in a subsequent Candidate Recommendation Snapshot.</p>
    ', + draftStability: + 'W3C Candidate Recommendation Draft must include one of the following two paragraphs in the "Status Of This Document":
    This is a draft document and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to cite this document as other than a work in progress.
    This document is maintained and updated at any time. Some parts of this document are work in progress.
    ', + patPolReq: true, + newFeatures: + 'If it is the intention to incorporate new features in future updates of the specification, please make sure to identify the document as intending to allow new features. Recommended text is:
    Future updates to this upcoming Recommendation may incorporate new features.
    Include one of this source code:
    <p>Future updates to this upcoming Recommendation may incorporate <a href="https://www.w3.org/policies/process/20250818/#allow-new-features">new features</a>.</p>', + knownDisclosureNumber: true, + whichProcess: true, + }, + }, + 'document-body': { + name: '7. Document Body', + rules: { + headingWithoutID: true, + brokenLink: true, + cssValid: true, + namespaces: true, + wcag: true, + fixupJs: true, + securityAndPrivacy: true, + }, + }, + compound: { + name: '8. Compound Documents', + rules: { + compoundFilesLocation: ['CRD'], + compoundOverview: true, + compound: true, + }, + }, + }, + }, + REC: { + order: 6, + name: 'Recommendation', + sections: { + format: { + name: '1. Normative Document Representation', + rules: { + normativeVersion: true, + validHTML: true, + visualStyle: true, + }, + }, + metadata: { + name: '2. Document Metadata', + rules: { + goodStylesheet: ['REC'], + lastStylesheet: true, + viewport: true, + canonical: true, + }, + }, + 'front-matter': { + name: '3. Front Matter', + rules: { + divClassHead: true, + logo: true, + title: true, + versionNumber: [ + '
  • If a Recommendation modified in place, see the Comm Team\'s policy regarding in-place modification of W3C Technical Reports, otherwise
  • ', + ], + dateState: [ + 'Recommendation', + '

    If this is a modified Recommendation that was modified in place or is a new edition, the document must include both the original publication date and the modification date. For example:

    <p id="w3c-state">W3C Recommendation 7 April 2004, edited in place 19 August2004</p>
    ', + ], + docIDFormat: true, + docIDOrder: + 'Document identifier information must be present in this order:
    • \'This version\' - URI to that version
    • \'Latest version\' - URI to the latest version. See also the (non-normative) Version Management in W3C Technical Reports for information about "latest version" URI and version management.
    • \'History\' - URI to the history of the specification
    • Editor(s)
    • Feedback - GitHub repository issue links are required in the <dl>after <dt>Feedback:</dt> in the headers (<div class="head">) of the document. Links are expected to be of the form https://github.com/<USER_OR_ORG>/<REPO_NAME>/[issues|labels][/…].)
    • Errata - URI to an errata document for any errors or issues reported since publication. See also suggestions on errata page structure in the Manual of Style. Note:
      • Do not put the errata document in TR space as the expectation is that we will not modify document in TR space after publication; see the policy for in-place modification of W3C Technical Reports.
      • Recommendations with candidate/proposed changes are treated as inline errata, and these documents don\'t require an errata link.
    ', + docIDThisVersion: ['REC'], + docIDDate: true, + docIDLatestVersion: true, + docIDHistory: true, + editorSection: true, + altRepresentations: ['REC'], + implReport: + 'It must include a link to a preliminary interoperability or implementation report.', + translation: true, + copyright: true, + hrAfterCopyright: true, + }, + }, + navigation: { + name: '6. Table of Contents', + rules: { + toc: true, + tocNav: true, + }, + }, + 'document-status': { + name: '5. Document Status Section', + rules: { + sotd: true, + boilerplateTRDoc: true, + datesFormat: true, + deployment: true, + publish: + 'W3C Recommendation must include one of the following paragraphs in the "Status of This Document" depending on the type of Recommendations:
    1. Recommendation without modifications:
      This document was published by the @@ Working Group as a Recommendation using the Recommendation track.
      Include this source code:
      <p>This document was published by the @@ Working Group as a Recommendation using the <a href="https://www.w3.org/policies/process/20250818/#recs-and-notes">Recommendation track</a>.</p>
    2. Recommendation with candidate corrections:
      This document was published by the @@ Working Group as a Recommendation using the Recommendation track. It includes candidate corrections.
      Include this source code:
      <p>This document was published by the @@ Working Group as a Recommendation using the <a href="https://www.w3.org/policies/process/20250818/#recs-and-notes">Recommendation track</a>. It includes <a href="https://www.w3.org/policies/process/20250818/#candidate-correction">candidate corrections.</a>.</p>
    3. Recommendation with candidate additions:
      This document was published by the @@ Working Group as a Recommendation using the Recommendation track. It includes candidate additions, introducing new features since the Previous Recommendation.
      Include this source code:
      <p>This document was published by the @@ Working Group as a Recommendation using the <a href="https://www.w3.org/policies/process/20250818/#recs-and-notes">Recommendation track</a>. It includes <a href="https://www.w3.org/policies/process/20250818/#candidate-addition">candidate additions</a>, introducing new features since the Previous Recommendation.</p>
    4. Recommendation with candidate amendments:
      This document was published by the @@ Working Group as a Recommendation using the Recommendation track. It includes candidate amendments, introducing substantive changes and new features since the Previous Recommendation.
      Include this source code:
      <p>This document was published by the @@ Working Group as a Recommendation using the <a href="https://www.w3.org/policies/process/20250818/#recs-and-notes">Recommendation track</a>. It includes <a href="https://www.w3.org/policies/process/20250818/#candidate-amendments">candidate amendments</a>, introducing substantive changes and new features since the Previous Recommendation.</p>
    5. Recommendation with proposed corrections:
      This document was published by the @@ Working Group as a Recommendation using the Recommendation track. It includes proposed corrections.
      Include this source code:
      <p>This document was published by the @@ Working Group as a Recommendation using the <a href="https://www.w3.org/policies/process/20250818/#recs-and-notes">Recommendation track</a>. It includes <a href="https://www.w3.org/policies/process/20250818/#proposed-corrections">proposed corrections.</a>.</p>
    6. Recommendation with proposed additions:
      This document was published by the @@ Working Group as a Recommendation using the Recommendation track. It includes proposed additions, introducing new features since the Previous Recommendation.
      Include this source code:
      <p>This document was published by the @@ Working Group as a Recommendation using the <a href="https://www.w3.org/policies/process/20250818/#recs-and-notes">Recommendation track</a>. It includes <a href="https://www.w3.org/policies/process/20250818/#proposed-addition">proposed additions</a>, introducing new features since the Previous Recommendation.</p>
    7. Recommendation with proposed amendments:
      This document was published by the @@ Working Group as a Recommendation using the Recommendation track. It includes proposed amendments, introducing substantive changes and new features since the Previous Recommendation.
      Include this source code:
      <p>This document was published by the @@ Working Group as a Recommendation using the <a href="https://www.w3.org/policies/process/20250818/#recs-and-notes">Recommendation track</a>. It includes <a href="https://www.w3.org/policies/process/20250818/#proposed-amendments">proposed amendments</a>, introducing substantive changes and new features since the Previous Recommendation.</p>
    ', + recRelation: + 'It must indicate its relationship to previous related Recommendations (e.g., an indication that a Recommendation supersedes, obsoletes, or subsumes another, or that a Recommendation is an editorial revision) and must link to the most recent Recommendation (if any) having the same major revision number. The document thus links to two important resources: the previous edition of the Recommendation via the status section, and the previous draft (the Proposed Recommendation) via the "Previous version" link.', + customParagraph: true, + changesList: ['must', ''], + stability: + 'It must set expectations about the stability of the document. The recommended text is:

    A W3C Recommendation is a specification that, after extensive consensus-building, is endorsed by W3C and its Members, and has commitments from Working Group members to royalty-free licensing for implementations.

    Include this source code:
    <p>A W3C Recommendation is a specification that, after extensive consensus-building, is endorsed by <abbr title="World Wide Web Consortium">W3C</abbr> and its Members, and has commitments from Working Group members to <a href="https://www.w3.org/policies/patent-policy/#sec-Requirements">royalty-free licensing</a> for implementations.</p>
    ', + patPolReq: true, + knownDisclosureNumber: true, + newFeatures: + 'If it is the intention to incorporate new features in future updates of the Recommendation, please make sure to identify the document as intending to allow new features. Recommended text is:
    Future updates to this Recommendation may incorporate new features.
    Include one of this source code:
    <p>Future updates to this Recommendation may incorporate <a href="https://www.w3.org/policies/process/20250818/#allow-new-features">new features</a>.</p>', + whichProcess: true, + recAddition: + 'Modifications in W3C Recommendation are divided into "new features" and "changes". Recommendations with modifications must include the following paragraphs depending on the changes.
    1. proposed corrections, aka "substantive changes":

      there should be a paragraph with class="correction proposed"

      Proposed corrections are marked in the document.
      Include this source code:
      <p class="correction proposed">Proposed corrections are marked in the document.</p>
    2. proposed additions, aka "new features":

      there should be a paragraph with class="addition proposed"

      Proposed additions are marked in the document.
      Include this source code:
      <p class="addition proposed">Proposed additions are marked in the document.</p>
    3. candidate corrections, aka "substantive changes":

      there should be a paragraph with class="correction"

      Candidate corrections are marked in the document.
      Include this source code:
      <p class="correction">Candidate corrections are marked in the document.</p>
    4. candidate additions, aka "new features":

      there should be a paragraph with class="addition"

      Candidate additions are marked in the document.
      Include this source code:
      <p class="correction">Candidate additions are marked in the document.</p>
    ', + commentEnd: + 'W3C Recommendation with proposed amendments (substantive changes or new features) must have a comment review date of at least 60 days after the publication date.', + }, + }, + 'document-body': { + name: '7. Document Body', + rules: { + headingWithoutID: true, + brokenLink: true, + cssValid: true, + namespaces: true, + wcag: true, + fixupJs: true, + }, + }, + compound: { + name: '8. Compound Documents', + rules: { + compoundFilesLocation: ['REC'], + compoundOverview: true, + compound: true, + }, + }, + }, + }, + 'REC-RSCND': { + order: 7, + name: 'Rescinded Recommendation', + sections: { + format: { + name: '1. Normative Document Representation', + rules: { + normativeVersion: true, + validHTML: true, + visualStyle: true, + }, + }, + metadata: { + name: '2. Document Metadata', + rules: { + goodStylesheet: ['RSCND'], + lastStylesheet: true, + viewport: true, + canonical: true, + }, + }, + 'front-matter': { + name: '3. Front Matter', + rules: { + divClassHead: true, + logo: true, + title: true, + versionNumber: [ + '
  • If a Recommendation modified in place, see the Comm Team\'s policy regarding in-place modification of W3C Technical Reports, otherwise
  • ', + ], + dateState: ['Rescinded Recommendation', ''], + docIDFormat: true, + docIDOrder: + 'Document identifier information must be present in this order:
    • This version URI.
    • Latest version URI(s). See also the (non-normative) Version Management in W3C Technical Reports for information about "latest version" URI and version management.
    • "Rescinds this Recommendation" URI
    ', + docIDThisVersion: ['RSCND'], + docIDDate: true, + docIDLatestVersion: true, + docIDHistory: true, + editorSection: true, + altRepresentations: ['RSCND'], + copyright: true, + hrAfterCopyright: true, + }, + }, + navigation: { + name: '6. Table of Contents', + rules: { + toc: true, + tocNav: true, + }, + }, + 'document-status': { + name: '5. Document Status Section', + rules: { + sotd: true, + boilerplateTRDoc: true, + datesFormat: true, + publish: [ + 'Recommendation', + 'Recommendation track', + '<a href="https://www.w3.org/policies/process/20250818/#recs-and-notes">Recommendation track</a>', + ], + recRelation: true, + rescindsRationale: + 'It must include rationale for the decision to rescind the Recommendation.

    Include this source code:
    <p>W3C has chosen to rescind the <a href="@@PREVIOUS REC URI@@">@@TITLE@@ Recommendation</a> for the following reasons: [...list of reasons...]. For additional information about replacement or alternative technologies, please refer to the <a href="https://www.w3.org/2016/11/obsoleting-rescinding/">explanation of Obsoleting, Rescinding or Superseding W3C Specifications</a>.</p>

    ', + altTechno: true, + customParagraph: true, + knownDisclosureNumber: true, + whichProcess: true, + }, + }, + 'document-body': { + name: '7. Document Body', + rules: { + headingWithoutID: true, + brokenLink: true, + cssValid: true, + namespaces: true, + wcag: true, + fixupJs: true, + }, + }, + compound: { + name: '8. Compound Documents', + rules: { + compoundFilesLocation: ['RSCND'], + compoundOverview: true, + compound: true, + }, + }, + }, + }, + DISC: { + order: 9, + name: 'Discontinued Draft', + sections: { + format: { + name: '1. Normative Document Representation', + rules: { + normativeVersion: true, + validHTML: true, + visualStyle: true, + }, + }, + metadata: { + name: '2. Document Metadata', + rules: { + goodStylesheet: ['DISC'], + lastStylesheet: true, + viewport: true, + canonical: true, + }, + }, + 'front-matter': { + name: '3. Front Matter', + rules: { + divClassHead: true, + logo: true, + title: true, + versionNumber: [''], + dateState: ['Discontinued Draft', ''], + docIDFormat: true, + docIDOrder: true, + docIDThisVersion: ['DISC'], + docIDDate: true, + docIDLatestVersion: true, + docIDHistory: true, + editorSection: true, + altRepresentations: ['DISC'], + copyright: true, + hrAfterCopyright: true, + }, + }, + navigation: { + name: '6. Table of Contents', + rules: { + toc: true, + tocNav: true, + }, + }, + 'document-status': { + name: '5. Document Status Section', + rules: { + sotd: true, + boilerplateTRDoc: true, + datesFormat: true, + publish: [ + 'Discontinued Draft', + 'Recommendation track', + '<a href="https://www.w3.org/policies/process/20250818/#recs-and-notes">Recommendation track</a>', + ], + stability: + 'It must set expectations about the (in)stability of the document. The recommended text is:

    Publication as a Discontinued Draft implies that this document is no longer intended to advance or to be maintained. It is inappropriate to cite this document as other than abandoned work.

    Include this source code:
    <p>Publication as a Discontinued Draft implies that this document is no longer intended to advance or to be maintained. It is inappropriate to cite this document as other than abandoned work.</p>
    ', + patPolReq: true, + knownDisclosureNumber: true, + whichProcess: true, + }, + }, + 'document-body': { + name: '7. Document Body', + rules: { + headingWithoutID: true, + brokenLink: true, + cssValid: true, + namespaces: true, + wcag: true, + fixupJs: true, + }, + }, + compound: { + name: '8. Compound Documents', + rules: { + compoundFilesLocation: ['DISC'], + compoundOverview: true, + compound: true, + }, + }, + }, + }, + }, + }, +} satisfies Record; diff --git a/lib/rules.json b/lib/rules.json deleted file mode 100644 index ef731a1b0..000000000 --- a/lib/rules.json +++ /dev/null @@ -1,1499 +0,0 @@ -{ - "SUBM": { - "name": "Submissions", - "order": 4, - "profiles": { - "MEM-SUBM": { - "order": 2, - "name": "Member Submission", - "sections": { - "format": { - "name": "1. Normative Document Representation", - "rules": { - "normativeVersion": true, - "validHTML": true, - "visualStyle": true - } - }, - "metadata": { - "name": "2. Document Metadata", - "rules": { - "goodStylesheet": ["Member-SUBM"], - "lastStylesheet": true, - "viewport": true, - "canonical": true - } - }, - "front-matter": { - "name": "3. Front Matter", - "rules": { - "divClassHead": true, - "logo": true, - "submlogo": "In addition to the W3C logo, use this logo:
    Include this source code:
    <a href=\"https://www.w3.org/submissions/\"><img height=\"48\" width=\"211\" alt=\"W3C Member Submission\" src=\"https://www.w3.org/Icons/member_subm\"/></a>
    ", - "title": true, - "versionNumber": [""], - "dateState": ["Member Submission", ""], - "docIDFormat": true, - "docIDOrder": true, - "docIDThisVersion": "The syntax of a “This Version” URI must be https://www.w3.org/submissions/YYYY/SUBM-shortname-YYYYMMDD/.", - "docIDLatestVersion": "The syntax of a “Latest Version” URI must be https://www.w3.org/submissions/shortname/.", - "docIDDate": true, - "editorSection": true, - "altRepresentations": ["SUBM"], - "copyright": "Copyright: The document must include a link to the W3C document notice (https://www.w3.org/copyright/document-license/). The copyright may be held by the Submitters.", - "hrAfterCopyright": true - } - }, - "navigation": { - "name": "6. Table of Contents", - "rules": { - "toc": true, - "tocNav": true - } - }, - "document-status": { - "name": "5. Document Status Section", - "rules": { - "sotd": true, - "boilerplateTRDoc": "It must begin with the following boilerplate text:

    This section describes the status of this document at the time of its publication. A list of current W3C publications can be found in the W3C standards and drafts index.

    Include this source code:
    <p><em>This section describes the status of this document at the time of its publication. A list of current W3C publications can be found in the <a href=\"https://www.w3.org/TR/\">W3C standards and drafts index</a>.</em></p>
    ", - "boilerplateSUBM": "It must include this boilerplate text (with links to the published Submission and Team Comment):
    By publishing this document, W3C acknowledges that the Submitting Members have made a formal Submission request to W3C for discussion. Publication of this document by W3C indicates no endorsement of its content by W3C, nor that W3C has, is, or will be allocating any resources to the issues addressed by it. This document is not the product of a chartered W3C group, but is published as potential input to the W3C Process. A W3C Team Comment has been published in conjunction with this Member Submission. Publication of acknowledged Member Submissions at the W3C site is one of the benefits of W3C Membership. Please consult the requirements associated with Member Submissions of section 3.3 of the W3C Patent Policy. Please consult the complete list of acknowledged W3C Member Submissions.
    Include this source code:
    <p>By publishing this document, W3C acknowledges that the <a href=\"https://www.w3.org/submissions/@@@submissiondoc@@@\">Submitting Members</a> have made a formal Submission request to W3C for discussion. Publication of this document by W3C indicates no endorsement of its content by W3C, nor that W3C has, is, or will be allocating any resources to the issues addressed by it. This document is not the product of a chartered W3C group, but is published as potential input to the <a href=\"https://www.w3.org/policies/process/\">W3C Process</a>. A <a href=\"https://www.w3.org/submissions/@@@teamcomment@@@\">W3C Team Comment</a> has been published in conjunction with this Member Submission. Publication of acknowledged Member Submissions at the W3C site is one of the benefits of <a href=\"https://www.w3.org/Consortium/Prospectus/Joining\">W3C Membership</a>. Please consult the requirements associated with Member Submissions of <a href=\"https://www.w3.org/policies/patent-policy/#sec-submissions\">section 3.3 of the W3C Patent Policy</a>. Please consult the complete <a href=\"https://www.w3.org/submissions/\">list of acknowledged W3C Member Submissions</a>.</p>
    ", - "customParagraph": true, - "knownDisclosureNumber": true, - "datesFormat": true - } - }, - "document-body": { - "name": "7. Document Body", - "rules": { - "headingWithoutID": true, - "brokenLink": true, - "cssValid": true, - "namespaces": true, - "wcag": true, - "fixupJs": true - } - }, - "compound": { - "name": "8. Compound Documents", - "rules": { - "compoundFilesLocation": ["SUBM"], - "compoundOverview": true, - "compound": true - } - } - } - } - } - }, - "REGISTRY": { - "name": "Registry Track", - "order": 3, - "profiles": { - "DRY": { - "order": 1, - "name": "Registry Draft", - "sections": { - "format": { - "name": "1. Normative Document Representation", - "rules": { - "normativeVersion": true, - "validHTML": true, - "visualStyle": true - } - }, - "metadata": { - "name": "2. Document Metadata", - "rules": { - "goodStylesheet": ["DRY"], - "lastStylesheet": true, - "viewport": true, - "canonical": true, - "delivererID": true - } - }, - "front-matter": { - "name": "3. Front Matter", - "rules": { - "divClassHead": true, - "logo": true, - "title": true, - "versionNumber": [""], - "dateState": ["Registry Draft", ""], - "docIDFormat": true, - "docIDOrder": true, - "docIDThisVersion": ["DRY"], - "docIDDate": true, - "docIDLatestVersion": true, - "docIDHistory": true, - "editorSection": true, - "altRepresentations": ["DRY"], - "copyright": true, - "hrAfterCopyright": true - } - }, - "navigation": { - "name": "6. Table of Contents", - "rules": { - "toc": true, - "tocNav": true - } - }, - "document-status": { - "name": "5. Document Status Section", - "rules": { - "sotd": true, - "boilerplateTRDoc": true, - "datesFormat": true, - "publish": [ - "Registry Draft", - "Registry track", - "<a href=\"https://www.w3.org/policies/process/20250818/#recs-and-notes\">Registry track</a>" - ], - "customParagraph": true, - "stability": "It must set expectations about the (in)stability of the document. The recommended text is:

    Publication as a Registry Draft does not imply endorsement by W3C and its Members

    ", - "draftStability": true, - "patPolReq": true, - "knownDisclosureNumber": true, - "whichProcess": true - } - }, - "document-body": { - "name": "7. Document Body", - "rules": { - "headingWithoutID": true, - "brokenLink": true, - "cssValid": true, - "namespaces": true, - "wcag": true, - "fixupJs": true - } - }, - "compound": { - "name": "8. Compound Documents", - "rules": { - "compoundFilesLocation": ["DRY"], - "compoundOverview": true, - "compound": true - } - } - } - }, - "CRY": { - "order": 2, - "name": "Candidate Registry", - "sections": { - "format": { - "name": "1. Normative Document Representation", - "rules": { - "normativeVersion": true, - "validHTML": true, - "visualStyle": true - } - }, - "metadata": { - "name": "2. Document Metadata", - "rules": { - "goodStylesheet": ["CRY"], - "lastStylesheet": true, - "viewport": true, - "canonical": true, - "delivererID": true - } - }, - "front-matter": { - "name": "3. Front Matter", - "rules": { - "divClassHead": true, - "logo": true, - "title": true, - "versionNumber": [""], - "dateState": ["Candidate Registry Snapshot", ""], - "docIDFormat": true, - "docIDOrder": true, - "docIDThisVersion": ["CRY"], - "docIDDate": true, - "docIDLatestVersion": true, - "docIDHistory": true, - "editorSection": true, - "altRepresentations": ["CRY"], - "copyright": true, - "hrAfterCopyright": true - } - }, - "navigation": { - "name": "6. Table of Contents", - "rules": { - "toc": true, - "tocNav": true - } - }, - "document-status": { - "name": "5. Document Status Section", - "rules": { - "sotd": true, - "boilerplateTRDoc": true, - "datesFormat": true, - "publish": [ - "Candidate Registry Snapshot", - "Registry track", - "<a href=\"https://www.w3.org/policies/process/20250818/#recs-and-notes\">Registry track</a>" - ], - "customParagraph": true, - "stability": "It must set expectations about the (in)stability of the document. The recommended text is:

    Publication as a Registry Draft does not imply endorsement by W3C and its Members. A Candidate Registry Snapshot has received wide review.

    Include this source code:
    <p>Publication as a Registry Draft does not imply endorsement by W3C and its Members. A Candidate Registry Snapshot has received <a href=\"https://www.w3.org/policies/process/20250818/#dfn-wide-review\">wide review</a>.</p>
    ", - "reviewEndDate": "It must include a minimal duration (before which the group will not request the next transition). The duration must be expressed as an estimated date.", - "knownDisclosureNumber": true, - "patPolReq": true, - "whichProcess": true - } - }, - "document-body": { - "name": "7. Document Body", - "rules": { - "headingWithoutID": true, - "brokenLink": true, - "cssValid": true, - "namespaces": true, - "wcag": true, - "fixupJs": true - } - }, - "compound": { - "name": "8. Compound Documents", - "rules": { - "compoundFilesLocation": ["CRY"], - "compoundOverview": true, - "compound": true - } - } - } - }, - "CRYD": { - "order": 3, - "name": "Candidate Registry Draft", - "sections": { - "format": { - "name": "1. Normative Document Representation", - "rules": { - "normativeVersion": true, - "validHTML": true, - "visualStyle": true - } - }, - "metadata": { - "name": "2. Document Metadata", - "rules": { - "goodStylesheet": ["CRYD"], - "lastStylesheet": true, - "viewport": true, - "canonical": true, - "delivererID": true - } - }, - "front-matter": { - "name": "3. Front Matter", - "rules": { - "divClassHead": true, - "logo": true, - "title": true, - "versionNumber": [""], - "dateState": ["Candidate Registry Draft", ""], - "docIDFormat": true, - "docIDOrder": true, - "docIDThisVersion": ["CRYD"], - "docIDDate": true, - "docIDLatestVersion": true, - "docIDHistory": true, - "editorSection": true, - "altRepresentations": ["CRYD"], - "copyright": true, - "hrAfterCopyright": true - } - }, - "navigation": { - "name": "6. Table of Contents", - "rules": { - "toc": true, - "tocNav": true - } - }, - "document-status": { - "name": "5. Document Status Section", - "rules": { - "sotd": true, - "boilerplateTRDoc": true, - "datesFormat": true, - "publish": [ - "Candidate Registry Draft", - "Registry track", - "<a href=\"https://www.w3.org/policies/process/20250818/#recs-and-notes\">Registry track</a>" - ], - "customParagraph": true, - "stability": "It must set expectations about the (in)stability of the document. The recommended text is:

    Publication as a Candidate Registry Draft does not imply endorsement by W3C and its Members. A Candidate Registry Draft integrates changes from the previous Candidate Registry that the Working Group intends to include in a subsequent Candidate Registry Snapshot.

    ", - "draftStability": "The document must include one of the following two paragraphs in the \"Status Of This Document\":
    This is a draft document and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to cite this document as other than a work in progress.
    This document is maintained and updated at any time. Some parts of this document are work in progress.
    ", - "knownDisclosureNumber": true, - "patPolReq": true, - "whichProcess": true - } - }, - "document-body": { - "name": "7. Document Body", - "rules": { - "headingWithoutID": true, - "brokenLink": true, - "cssValid": true, - "namespaces": true, - "wcag": true, - "fixupJs": true - } - }, - "compound": { - "name": "8. Compound Documents", - "rules": { - "compoundFilesLocation": ["CRYD"], - "compoundOverview": true, - "compound": true - } - } - } - }, - "RY": { - "order": 4, - "name": "Registry", - "sections": { - "format": { - "name": "1. Normative Document Representation", - "rules": { - "normativeVersion": true, - "validHTML": true, - "visualStyle": true - } - }, - "metadata": { - "name": "2. Document Metadata", - "rules": { - "goodStylesheet": ["RY"], - "lastStylesheet": true, - "viewport": true, - "canonical": true, - "delivererID": true - } - }, - "front-matter": { - "name": "3. Front Matter", - "rules": { - "divClassHead": true, - "logo": true, - "title": true, - "versionNumber": [""], - "dateState": ["Registry", ""], - "docIDFormat": true, - "docIDOrder": true, - "docIDThisVersion": ["RY"], - "docIDDate": true, - "docIDLatestVersion": true, - "docIDHistory": true, - "editorSection": true, - "altRepresentations": ["RY"], - "copyright": true, - "hrAfterCopyright": true - } - }, - "navigation": { - "name": "6. Table of Contents", - "rules": { - "toc": true, - "tocNav": true - } - }, - "document-status": { - "name": "5. Document Status Section", - "rules": { - "sotd": true, - "boilerplateTRDoc": true, - "datesFormat": true, - "publish": [ - "Registry", - "Registry track", - "<a href=\"https://www.w3.org/policies/process/20250818/#recs-and-notes\">Registry track</a>" - ], - "customParagraph": true, - "usage": "It must include the expectations in terms of usage of this registry. The SOTD should include the paragraph:
    W3C recommends the wide usage of this registry.
    ", - "stability": "It must set expectations about the (in)stability of the document. The recommended text is:

    A W3C Registry is a specification that, after extensive consensus-building, is endorsed by W3C and its Members.

    ", - "knownDisclosureNumber": true, - "patPolReq": true, - "whichProcess": true - } - }, - "document-body": { - "name": "7. Document Body", - "rules": { - "headingWithoutID": true, - "brokenLink": true, - "cssValid": true, - "namespaces": true, - "wcag": true, - "fixupJs": true - } - }, - "compound": { - "name": "8. Compound Documents", - "rules": { - "compoundFilesLocation": ["RY"], - "compoundOverview": true, - "compound": true - } - } - } - } - } - }, - "NOTE": { - "name": "Note Track", - "order": 2, - "profiles": { - "DNOTE": { - "order": 1, - "name": "Group Note Draft", - "sections": { - "format": { - "name": "1. Normative Document Representation", - "rules": { - "normativeVersion": true, - "validHTML": true, - "visualStyle": true - } - }, - "metadata": { - "name": "2. Document Metadata", - "rules": { - "goodStylesheet": ["DNOTE"], - "lastStylesheet": true, - "viewport": true, - "canonical": true, - "delivererID": true - } - }, - "front-matter": { - "name": "3. Front Matter", - "rules": { - "divClassHead": true, - "logo": true, - "title": true, - "versionNumber": [""], - "dateState": ["Group Note Draft", ""], - "docIDFormat": true, - "docIDOrder": true, - "docIDThisVersion": ["DNOTE"], - "docIDDate": true, - "docIDLatestVersion": true, - "docIDHistory": true, - "editorSection": true, - "altRepresentations": ["DNOTE"], - "copyright": true, - "hrAfterCopyright": true - } - }, - "navigation": { - "name": "6. Table of Contents", - "rules": { - "toc": true, - "tocNav": true - } - }, - "document-status": { - "name": "5. Document Status Section", - "rules": { - "sotd": true, - "boilerplateTRDoc": true, - "publish": [ - "Group Note Draft", - "Note track", - "<a href=\"https://www.w3.org/policies/process/20250818/#recs-and-notes\">Note track</a>" - ], - "customParagraph": true, - "stability": "It must set expectations about the (in)stability of the document. The recommended text is:

    Group Note Drafts are not endorsed by W3C nor its Members.

    or

    This Group Note Draft is endorsed by the @@@ Working/Interest Group (and the @@@ Working/Interest Group), but is not endorsed by W3C itself nor its Members.

    ", - "draftStability": true, - "knownDisclosureNumber": true, - "patPolReq": true, - "whichProcess": true - } - }, - "document-body": { - "name": "7. Document Body", - "rules": { - "headingWithoutID": true, - "brokenLink": true, - "cssValid": true, - "namespaces": true, - "wcag": true, - "fixupJs": true - } - }, - "compound": { - "name": "8. Compound Documents", - "rules": { - "compoundFilesLocation": ["DNOTE"], - "compoundOverview": true, - "compound": true - } - } - } - }, - "NOTE": { - "order": 2, - "name": "Group Note", - "sections": { - "format": { - "name": "1. Normative Document Representation", - "rules": { - "normativeVersion": true, - "validHTML": true, - "visualStyle": true - } - }, - "metadata": { - "name": "2. Document Metadata", - "rules": { - "goodStylesheet": ["NOTE"], - "lastStylesheet": true, - "viewport": true, - "canonical": true, - "delivererID": true - } - }, - "front-matter": { - "name": "3. Front Matter", - "rules": { - "divClassHead": true, - "logo": true, - "title": true, - "versionNumber": [""], - "dateState": ["Group Note", ""], - "docIDFormat": true, - "docIDOrder": true, - "docIDThisVersion": ["NOTE"], - "docIDDate": true, - "docIDLatestVersion": true, - "docIDHistory": true, - "editorSection": true, - "altRepresentations": ["NOTE"], - "copyright": true, - "hrAfterCopyright": true - } - }, - "navigation": { - "name": "6. Table of Contents", - "rules": { - "toc": true, - "tocNav": true - } - }, - "document-status": { - "name": "5. Document Status Section", - "rules": { - "sotd": true, - "boilerplateTRDoc": true, - "datesFormat": true, - "publish": [ - "Group Note", - "Note track", - "<a href=\"https://www.w3.org/policies/process/20250818/#recs-and-notes\">Note track</a>" - ], - "customParagraph": true, - "stability": "It must set expectations about the (in)stability of the document. The recommended text is:

    Group Notes are not endorsed by W3C nor its Members.

    or

    This Group Note is endorsed by the @@@ Working/Interest Group (and the @@@ Working/Interest Group), but is not endorsed by W3C itself nor its Members.

    ", - "knownDisclosureNumber": true, - "patPolReq": true, - "whichProcess": true - } - }, - "document-body": { - "name": "7. Document Body", - "rules": { - "headingWithoutID": true, - "brokenLink": true, - "cssValid": true, - "namespaces": true, - "wcag": true, - "fixupJs": true - } - }, - "compound": { - "name": "8. Compound Documents", - "rules": { - "compoundFilesLocation": ["NOTE"], - "compoundOverview": true, - "compound": true - } - } - } - }, - "STMT": { - "order": 3, - "name": "Statement", - "sections": { - "format": { - "name": "1. Normative Document Representation", - "rules": { - "normativeVersion": true, - "validHTML": true, - "visualStyle": true - } - }, - "metadata": { - "name": "2. Document Metadata", - "rules": { - "goodStylesheet": ["STMT"], - "lastStylesheet": true, - "viewport": true, - "canonical": true, - "delivererID": true - } - }, - "front-matter": { - "name": "3. Front Matter", - "rules": { - "divClassHead": true, - "logo": true, - "title": true, - "versionNumber": [""], - "dateState": ["Statement", ""], - "docIDFormat": true, - "docIDOrder": true, - "docIDThisVersion": ["STMT"], - "docIDDate": true, - "docIDLatestVersion": true, - "docIDHistory": true, - "editorSection": true, - "altRepresentations": ["STMT"], - "translation": true, - "copyright": true, - "hrAfterCopyright": true - } - }, - "navigation": { - "name": "6. Table of Contents", - "rules": { - "toc": true, - "tocNav": true - } - }, - "document-status": { - "name": "5. Document Status Section", - "rules": { - "sotd": true, - "boilerplateTRDoc": true, - "datesFormat": true, - "publish": [ - "Statement", - "Note track", - "<a href=\"https://www.w3.org/policies/process/20250818/#recs-and-notes\">Note track</a>" - ], - "customParagraph": true, - "stability": "It must set expectations about the (in)stability of the document. The recommended text is:

    A W3C Statement is a specification that, after extensive consensus-building, is endorsed by W3C and its Members.

    Include this source code:
    <p>A W3C Statement is a specification that, after extensive consensus-building, is endorsed by W3C and its Members.</p>
    ", - "knownDisclosureNumber": true, - "patPolReq": true, - "whichProcess": true - } - }, - "document-body": { - "name": "7. Document Body", - "rules": { - "headingWithoutID": true, - "brokenLink": true, - "cssValid": true, - "namespaces": true, - "wcag": true, - "fixupJs": true - } - }, - "compound": { - "name": "8. Compound Documents", - "rules": { - "compoundFilesLocation": ["STMT"], - "compoundOverview": true, - "compound": true - } - } - } - } - } - }, - "REC": { - "name": "Recommendation Track", - "order": 1, - "profiles": { - "FPWD": { - "order": 1, - "name": "First Public Working Draft", - "sections": { - "format": { - "name": "1. Normative Document Representation", - "rules": { - "normativeVersion": true, - "validHTML": true, - "visualStyle": true - } - }, - "metadata": { - "name": "2. Document Metadata", - "rules": { - "goodStylesheet": ["WD"], - "lastStylesheet": true, - "viewport": true, - "canonical": true - } - }, - "front-matter": { - "name": "3. Front Matter", - "rules": { - "divClassHead": true, - "logo": true, - "title": true, - "versionNumber": [""], - "dateState": ["First Public Working Draft", ""], - "docIDFormat": true, - "docIDOrder": true, - "docIDThisVersion": ["WD"], - "docIDDate": true, - "docIDLatestVersion": true, - "docIDHistory": true, - "editorSection": true, - "altRepresentations": ["WD"], - "copyright": true, - "hrAfterCopyright": true - } - }, - "navigation": { - "name": "6. Table of Contents", - "rules": { - "toc": true, - "tocNav": true - } - }, - "document-status": { - "name": "5. Document Status Section", - "rules": { - "sotd": true, - "boilerplateTRDoc": true, - "datesFormat": true, - "publish": [ - "First Public Working Draft", - "Recommendation track", - "<a href=\"https://www.w3.org/policies/process/20250818/#recs-and-notes\">Recommendation track</a>" - ], - "customParagraph": true, - "stability": ["a First Public Working Draft", ""], - "draftStability": true, - "patPolReq": true, - "knownDisclosureNumber": true, - "whichProcess": true - } - }, - "document-body": { - "name": "7. Document Body", - "rules": { - "headingWithoutID": true, - "brokenLink": true, - "cssValid": true, - "namespaces": true, - "wcag": true, - "fixupJs": true - } - }, - "compound": { - "name": "8. Compound Documents", - "rules": { - "compoundFilesLocation": ["WD"], - "compoundOverview": true, - "compound": true - } - } - } - }, - "WD": { - "order": 2, - "name": "Working Draft", - "sections": { - "format": { - "name": "1. Normative Document Representation", - "rules": { - "normativeVersion": true, - "validHTML": true, - "visualStyle": true - } - }, - "metadata": { - "name": "2. Document Metadata", - "rules": { - "goodStylesheet": ["WD"], - "lastStylesheet": true, - "viewport": true, - "canonical": true - } - }, - "front-matter": { - "name": "3. Front Matter", - "rules": { - "divClassHead": true, - "logo": true, - "title": true, - "versionNumber": [""], - "dateState": ["Working Draft", ""], - "docIDFormat": true, - "docIDOrder": true, - "docIDThisVersion": ["WD"], - "docIDDate": true, - "docIDLatestVersion": true, - "docIDHistory": true, - "editorSection": true, - "altRepresentations": ["WD"], - "copyright": true, - "hrAfterCopyright": true - } - }, - "navigation": { - "name": "6. Table of Contents", - "rules": { - "toc": true, - "tocNav": true - } - }, - "document-status": { - "name": "5. Document Status Section", - "rules": { - "sotd": true, - "boilerplateTRDoc": true, - "datesFormat": true, - "publish": [ - "Working Draft", - "Recommendation track", - "<a href=\"https://www.w3.org/policies/process/20250818/#recs-and-notes\">Recommendation track</a>" - ], - "customParagraph": true, - "changesList": ["should", ""], - "stability": ["a Working Draft", ""], - "draftStability": true, - "patPolReq": true, - "knownDisclosureNumber": true, - "whichProcess": true - } - }, - "document-body": { - "name": "7. Document Body", - "rules": { - "headingWithoutID": true, - "brokenLink": true, - "cssValid": true, - "namespaces": true, - "wcag": true, - "fixupJs": true, - "securityAndPrivacy": true - } - }, - "compound": { - "name": "8. Compound Documents", - "rules": { - "compoundFilesLocation": ["WD"], - "compoundOverview": true, - "compound": true - } - } - } - }, - "CR": { - "order": 4, - "name": "Candidate Recommendation Snapshot", - "sections": { - "format": { - "name": "1. Normative Document Representation", - "rules": { - "normativeVersion": true, - "validHTML": true, - "visualStyle": true - } - }, - "metadata": { - "name": "2. Document Metadata", - "rules": { - "goodStylesheet": ["CR"], - "lastStylesheet": true, - "viewport": true, - "canonical": true - } - }, - "front-matter": { - "name": "3. Front Matter", - "rules": { - "divClassHead": true, - "logo": true, - "title": true, - "versionNumber": [""], - "dateState": [ - "Candidate Recommendation Snapshot", - "" - ], - "docIDFormat": true, - "docIDOrder": true, - "docIDThisVersion": ["CR"], - "docIDDate": true, - "docIDLatestVersion": true, - "docIDHistory": true, - "editorSection": true, - "altRepresentations": ["CR"], - "implReport": "It must include a link to a preliminary interoperability or implementation report, or a statement that no such report exists.", - "copyright": true, - "hrAfterCopyright": true - } - }, - "navigation": { - "name": "6. Table of Contents", - "rules": { - "toc": true, - "tocNav": true - } - }, - "document-status": { - "name": "5. Document Status Section", - "rules": { - "sotd": true, - "boilerplateTRDoc": true, - "datesFormat": true, - "publish": [ - "Candidate Recommendation Snapshot", - "Recommendation track", - "<a href=\"https://www.w3.org/policies/process/20250818/#recs-and-notes\">Recommendation track</a>" - ], - "reviewEndDate": "It must include a minimal duration (before which the group will not request the next transition). The duration must be expressed as an estimated date.", - "implEstimation": "It should include an estimated date by which time the Working Group expects to have sufficient implementation experience.", - "featAtRisk": "It must identify any \"features at risk\" declared by the Working Group (as defined in section 6.4 of the W3C Process Document).", - "customParagraph": true, - "changesList": ["must", ""], - "stability": "It must set expectations about the (in)stability of the document. The recommended text is:

    Publication as a Candidate Recommendation does not imply endorsement by W3C and its Members. A Candidate Recommendation Snapshot has received wide review, is intended to gather implementation experience, and has commitments from Working Group members to royalty-free licensing for implementations.

    Include this source code:
    <p>Publication as a Candidate Recommendation does not imply endorsement by W3C and its Members. A Candidate Recommendation Snapshot has received <a href=\"https://www.w3.org/policies/process/20250818/#dfn-wide-review\">wide review</a>, is intended to gather implementation experience, and has commitments from Working Group members to <a href=\"https://www.w3.org/policies/patent-policy/#sec-Requirements\">royalty-free licensing</a> for implementations.</p>
    ", - "patPolReq": true, - "newFeatures": "If it is the intention to incorporate new features in future updates of the specification, please make sure to identify the document as intending to allow new features. Recommended text is:
    Future updates to this upcoming Recommendation may incorporate new features.
    Include one of this source code:
    <p>Future updates to this upcoming Recommendation may incorporate <a href=\"https://www.w3.org/policies/process/20250818/#allow-new-features\">new features</a>.</p>", - "knownDisclosureNumber": true, - "whichProcess": true - } - }, - "document-body": { - "name": "7. Document Body", - "rules": { - "headingWithoutID": true, - "brokenLink": true, - "cssValid": true, - "namespaces": true, - "wcag": true, - "fixupJs": true, - "securityAndPrivacy": true - } - }, - "compound": { - "name": "8. Compound Documents", - "rules": { - "compoundFilesLocation": ["CR"], - "compoundOverview": true, - "compound": true - } - } - } - }, - "CRD": { - "order": 4, - "name": "Candidate Recommendation Draft", - "sections": { - "format": { - "name": "1. Normative Document Representation", - "rules": { - "normativeVersion": true, - "validHTML": true, - "visualStyle": true - } - }, - "metadata": { - "name": "2. Document Metadata", - "rules": { - "goodStylesheet": ["CRD"], - "lastStylesheet": true, - "viewport": true, - "canonical": true - } - }, - "front-matter": { - "name": "3. Front Matter", - "rules": { - "divClassHead": true, - "logo": true, - "title": true, - "versionNumber": [""], - "dateState": ["Candidate Recommendation Draft", ""], - "docIDFormat": true, - "docIDOrder": true, - "docIDThisVersion": ["CRD"], - "docIDDate": true, - "docIDLatestVersion": true, - "docIDHistory": true, - "editorSection": true, - "altRepresentations": ["CRD"], - "implReport": "It must include a link to a preliminary interoperability or implementation report, or a statement that no such report exists.", - "copyright": true, - "hrAfterCopyright": true - } - }, - "navigation": { - "name": "6. Table of Contents", - "rules": { - "toc": true, - "tocNav": true - } - }, - "document-status": { - "name": "5. Document Status Section", - "rules": { - "sotd": true, - "boilerplateTRDoc": true, - "datesFormat": true, - "publish": [ - "Candidate Recommendation Draft", - "Recommendation track", - "<a href=\"https://www.w3.org/policies/process/20250818/#recs-and-notes\">Recommendation track</a>" - ], - "reviewEndDate": "It must include a minimal duration (before which the group will not request the next transition). The duration must be expressed as an estimated date.", - "implEstimation": "It should include an estimated date by which time the Working Group expects to have sufficient implementation experience.", - "featAtRisk": "It must identify any \"features at risk\" declared by the Working Group (as defined in section 6.4 of the W3C Process Document).", - "customParagraph": true, - "changesList": ["must", ""], - "stability": "It must set expectations about the (in)stability of the document. The recommended text is:

    Publication as a Candidate Recommendation does not imply endorsement by W3C and its Members. A Candidate Recommendation Draft integrates changes from the previous Candidate Recommendation that the Working Group intends to include in a subsequent Candidate Recommendation Snapshot.

    Include this source code:
    <p>Publication as a Candidate Recommendation does not imply endorsement by W3C and its Members. A Candidate Recommendation Draft integrates changes from the previous Candidate Recommendation that the Working Group intends to include in a subsequent Candidate Recommendation Snapshot.</p>
    ", - "draftStability": "W3C Candidate Recommendation Draft must include one of the following two paragraphs in the \"Status Of This Document\":
    This is a draft document and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to cite this document as other than a work in progress.
    This document is maintained and updated at any time. Some parts of this document are work in progress.
    ", - "patPolReq": true, - "newFeatures": "If it is the intention to incorporate new features in future updates of the specification, please make sure to identify the document as intending to allow new features. Recommended text is:
    Future updates to this upcoming Recommendation may incorporate new features.
    Include one of this source code:
    <p>Future updates to this upcoming Recommendation may incorporate <a href=\"https://www.w3.org/policies/process/20250818/#allow-new-features\">new features</a>.</p>", - "knownDisclosureNumber": true, - "whichProcess": true - } - }, - "document-body": { - "name": "7. Document Body", - "rules": { - "headingWithoutID": true, - "brokenLink": true, - "cssValid": true, - "namespaces": true, - "wcag": true, - "fixupJs": true, - "securityAndPrivacy": true - } - }, - "compound": { - "name": "8. Compound Documents", - "rules": { - "compoundFilesLocation": ["CRD"], - "compoundOverview": true, - "compound": true - } - } - } - }, - "REC": { - "order": 6, - "name": "Recommendation", - "sections": { - "format": { - "name": "1. Normative Document Representation", - "rules": { - "normativeVersion": true, - "validHTML": true, - "visualStyle": true - } - }, - "metadata": { - "name": "2. Document Metadata", - "rules": { - "goodStylesheet": ["REC"], - "lastStylesheet": true, - "viewport": true, - "canonical": true - } - }, - "front-matter": { - "name": "3. Front Matter", - "rules": { - "divClassHead": true, - "logo": true, - "title": true, - "versionNumber": [ - "
  • If a Recommendation modified in place, see the Comm Team's policy regarding in-place modification of W3C Technical Reports, otherwise
  • " - ], - "dateState": [ - "Recommendation", - "

    If this is a modified Recommendation that was modified in place or is a new edition, the document must include both the original publication date and the modification date. For example:

    <p id=\"w3c-state\">W3C Recommendation 7 April 2004, edited in place 19 August2004</p>
    " - ], - "docIDFormat": true, - "docIDOrder": "Document identifier information must be present in this order:
    • 'This version' - URI to that version
    • 'Latest version' - URI to the latest version. See also the (non-normative) Version Management in W3C Technical Reports for information about \"latest version\" URI and version management.
    • 'History' - URI to the history of the specification
    • Editor(s)
    • Feedback - GitHub repository issue links are required in the <dl>after <dt>Feedback:</dt> in the headers (<div class=\"head\">) of the document. Links are expected to be of the form https://github.com/<USER_OR_ORG>/<REPO_NAME>/[issues|labels][/…].)
    • Errata - URI to an errata document for any errors or issues reported since publication. See also suggestions on errata page structure in the Manual of Style. Note:
      • Do not put the errata document in TR space as the expectation is that we will not modify document in TR space after publication; see the policy for in-place modification of W3C Technical Reports.
      • Recommendations with candidate/proposed changes are treated as inline errata, and these documents don't require an errata link.
    ", - "docIDThisVersion": ["REC"], - "docIDDate": true, - "docIDLatestVersion": true, - "docIDHistory": true, - "editorSection": true, - "altRepresentations": ["REC"], - "implReport": "It must include a link to a preliminary interoperability or implementation report.", - "translation": true, - "copyright": true, - "hrAfterCopyright": true - } - }, - "navigation": { - "name": "6. Table of Contents", - "rules": { - "toc": true, - "tocNav": true - } - }, - "document-status": { - "name": "5. Document Status Section", - "rules": { - "sotd": true, - "boilerplateTRDoc": true, - "datesFormat": true, - "deployment": true, - "publish": "W3C Recommendation must include one of the following paragraphs in the \"Status of This Document\" depending on the type of Recommendations:
    1. Recommendation without modifications:
      This document was published by the @@ Working Group as a Recommendation using the Recommendation track.
      Include this source code:
      <p>This document was published by the @@ Working Group as a Recommendation using the <a href=\"https://www.w3.org/policies/process/20250818/#recs-and-notes\">Recommendation track</a>.</p>
    2. Recommendation with candidate corrections:
      This document was published by the @@ Working Group as a Recommendation using the Recommendation track. It includes candidate corrections.
      Include this source code:
      <p>This document was published by the @@ Working Group as a Recommendation using the <a href=\"https://www.w3.org/policies/process/20250818/#recs-and-notes\">Recommendation track</a>. It includes <a href=\"https://www.w3.org/policies/process/20250818/#candidate-correction\">candidate corrections.</a>.</p>
    3. Recommendation with candidate additions:
      This document was published by the @@ Working Group as a Recommendation using the Recommendation track. It includes candidate additions, introducing new features since the Previous Recommendation.
      Include this source code:
      <p>This document was published by the @@ Working Group as a Recommendation using the <a href=\"https://www.w3.org/policies/process/20250818/#recs-and-notes\">Recommendation track</a>. It includes <a href=\"https://www.w3.org/policies/process/20250818/#candidate-addition\">candidate additions</a>, introducing new features since the Previous Recommendation.</p>
    4. Recommendation with candidate amendments:
      This document was published by the @@ Working Group as a Recommendation using the Recommendation track. It includes candidate amendments, introducing substantive changes and new features since the Previous Recommendation.
      Include this source code:
      <p>This document was published by the @@ Working Group as a Recommendation using the <a href=\"https://www.w3.org/policies/process/20250818/#recs-and-notes\">Recommendation track</a>. It includes <a href=\"https://www.w3.org/policies/process/20250818/#candidate-amendments\">candidate amendments</a>, introducing substantive changes and new features since the Previous Recommendation.</p>
    5. Recommendation with proposed corrections:
      This document was published by the @@ Working Group as a Recommendation using the Recommendation track. It includes proposed corrections.
      Include this source code:
      <p>This document was published by the @@ Working Group as a Recommendation using the <a href=\"https://www.w3.org/policies/process/20250818/#recs-and-notes\">Recommendation track</a>. It includes <a href=\"https://www.w3.org/policies/process/20250818/#proposed-corrections\">proposed corrections.</a>.</p>
    6. Recommendation with proposed additions:
      This document was published by the @@ Working Group as a Recommendation using the Recommendation track. It includes proposed additions, introducing new features since the Previous Recommendation.
      Include this source code:
      <p>This document was published by the @@ Working Group as a Recommendation using the <a href=\"https://www.w3.org/policies/process/20250818/#recs-and-notes\">Recommendation track</a>. It includes <a href=\"https://www.w3.org/policies/process/20250818/#proposed-addition\">proposed additions</a>, introducing new features since the Previous Recommendation.</p>
    7. Recommendation with proposed amendments:
      This document was published by the @@ Working Group as a Recommendation using the Recommendation track. It includes proposed amendments, introducing substantive changes and new features since the Previous Recommendation.
      Include this source code:
      <p>This document was published by the @@ Working Group as a Recommendation using the <a href=\"https://www.w3.org/policies/process/20250818/#recs-and-notes\">Recommendation track</a>. It includes <a href=\"https://www.w3.org/policies/process/20250818/#proposed-amendments\">proposed amendments</a>, introducing substantive changes and new features since the Previous Recommendation.</p>
    ", - "recRelation": "It must indicate its relationship to previous related Recommendations (e.g., an indication that a Recommendation supersedes, obsoletes, or subsumes another, or that a Recommendation is an editorial revision) and must link to the most recent Recommendation (if any) having the same major revision number. The document thus links to two important resources: the previous edition of the Recommendation via the status section, and the previous draft (the Proposed Recommendation) via the \"Previous version\" link.", - "customParagraph": true, - "changesList": ["must", ""], - "stability": "It must set expectations about the stability of the document. The recommended text is:

    A W3C Recommendation is a specification that, after extensive consensus-building, is endorsed by W3C and its Members, and has commitments from Working Group members to royalty-free licensing for implementations.

    Include this source code:
    <p>A W3C Recommendation is a specification that, after extensive consensus-building, is endorsed by <abbr title=\"World Wide Web Consortium\">W3C</abbr> and its Members, and has commitments from Working Group members to <a href=\"https://www.w3.org/policies/patent-policy/#sec-Requirements\">royalty-free licensing</a> for implementations.</p>
    ", - "patPolReq": true, - "knownDisclosureNumber": true, - "newFeatures": "If it is the intention to incorporate new features in future updates of the Recommendation, please make sure to identify the document as intending to allow new features. Recommended text is:
    Future updates to this Recommendation may incorporate new features.
    Include one of this source code:
    <p>Future updates to this Recommendation may incorporate <a href=\"https://www.w3.org/policies/process/20250818/#allow-new-features\">new features</a>.</p>", - "whichProcess": true, - "recAddition": "Modifications in W3C Recommendation are divided into \"new features\" and \"changes\". Recommendations with modifications must include the following paragraphs depending on the changes.
    1. proposed corrections, aka \"substantive changes\":

      there should be a paragraph with class=\"correction proposed\"

      Proposed corrections are marked in the document.
      Include this source code:
      <p class=\"correction proposed\">Proposed corrections are marked in the document.</p>
    2. proposed additions, aka \"new features\":

      there should be a paragraph with class=\"addition proposed\"

      Proposed additions are marked in the document.
      Include this source code:
      <p class=\"addition proposed\">Proposed additions are marked in the document.</p>
    3. candidate corrections, aka \"substantive changes\":

      there should be a paragraph with class=\"correction\"

      Candidate corrections are marked in the document.
      Include this source code:
      <p class=\"correction\">Candidate corrections are marked in the document.</p>
    4. candidate additions, aka \"new features\":

      there should be a paragraph with class=\"addition\"

      Candidate additions are marked in the document.
      Include this source code:
      <p class=\"correction\">Candidate additions are marked in the document.</p>
    ", - "commentEnd": "W3C Recommendation with proposed amendments (substantive changes or new features) must have a comment review date of at least 60 days after the publication date." - } - }, - "document-body": { - "name": "7. Document Body", - "rules": { - "headingWithoutID": true, - "brokenLink": true, - "cssValid": true, - "namespaces": true, - "wcag": true, - "fixupJs": true - } - }, - "compound": { - "name": "8. Compound Documents", - "rules": { - "compoundFilesLocation": ["REC"], - "compoundOverview": true, - "compound": true - } - } - } - }, - "REC-RSCND": { - "order": 7, - "name": "Rescinded Recommendation", - "sections": { - "format": { - "name": "1. Normative Document Representation", - "rules": { - "normativeVersion": true, - "validHTML": true, - "visualStyle": true - } - }, - "metadata": { - "name": "2. Document Metadata", - "rules": { - "goodStylesheet": ["RSCND"], - "lastStylesheet": true, - "viewport": true, - "canonical": true - } - }, - "front-matter": { - "name": "3. Front Matter", - "rules": { - "divClassHead": true, - "logo": true, - "title": true, - "versionNumber": [ - "
  • If a Recommendation modified in place, see the Comm Team's policy regarding in-place modification of W3C Technical Reports, otherwise
  • " - ], - "dateState": ["Rescinded Recommendation", ""], - "docIDFormat": true, - "docIDOrder": "Document identifier information must be present in this order:
    • This version URI.
    • Latest version URI(s). See also the (non-normative) Version Management in W3C Technical Reports for information about \"latest version\" URI and version management.
    • \"Rescinds this Recommendation\" URI
    ", - "docIDThisVersion": ["RSCND"], - "docIDDate": true, - "docIDLatestVersion": true, - "docIDHistory": true, - "editorSection": true, - "altRepresentations": ["RSCND"], - "copyright": true, - "hrAfterCopyright": true - } - }, - "navigation": { - "name": "6. Table of Contents", - "rules": { - "toc": true, - "tocNav": true - } - }, - "document-status": { - "name": "5. Document Status Section", - "rules": { - "sotd": true, - "boilerplateTRDoc": true, - "datesFormat": true, - "publish": [ - "Recommendation", - "Recommendation track", - "<a href=\"https://www.w3.org/policies/process/20250818/#recs-and-notes\">Recommendation track</a>" - ], - "recRelation": true, - "rescindsRationale": "It must include rationale for the decision to rescind the Recommendation.

    Include this source code:
    <p>W3C has chosen to rescind the <a href=\"@@PREVIOUS REC URI@@\">@@TITLE@@ Recommendation</a> for the following reasons: [...list of reasons...]. For additional information about replacement or alternative technologies, please refer to the <a href=\"https://www.w3.org/2016/11/obsoleting-rescinding/\">explanation of Obsoleting, Rescinding or Superseding W3C Specifications</a>.</p>

    ", - "altTechno": true, - "customParagraph": true, - "knownDisclosureNumber": true, - "whichProcess": true - } - }, - "document-body": { - "name": "7. Document Body", - "rules": { - "headingWithoutID": true, - "brokenLink": true, - "cssValid": true, - "namespaces": true, - "wcag": true, - "fixupJs": true - } - }, - "compound": { - "name": "8. Compound Documents", - "rules": { - "compoundFilesLocation": ["RSCND"], - "compoundOverview": true, - "compound": true - } - } - } - }, - "DISC": { - "order": 9, - "name": "Discontinued Draft", - "sections": { - "format": { - "name": "1. Normative Document Representation", - "rules": { - "normativeVersion": true, - "validHTML": true, - "visualStyle": true - } - }, - "metadata": { - "name": "2. Document Metadata", - "rules": { - "goodStylesheet": ["DISC"], - "lastStylesheet": true, - "viewport": true, - "canonical": true - } - }, - "front-matter": { - "name": "3. Front Matter", - "rules": { - "divClassHead": true, - "logo": true, - "title": true, - "versionNumber": [""], - "dateState": ["Discontinued Draft", ""], - "docIDFormat": true, - "docIDOrder": true, - "docIDThisVersion": ["DISC"], - "docIDDate": true, - "docIDLatestVersion": true, - "docIDHistory": true, - "editorSection": true, - "altRepresentations": ["DISC"], - "copyright": true, - "hrAfterCopyright": true - } - }, - "navigation": { - "name": "6. Table of Contents", - "rules": { - "toc": true, - "tocNav": true - } - }, - "document-status": { - "name": "5. Document Status Section", - "rules": { - "sotd": true, - "boilerplateTRDoc": true, - "datesFormat": true, - "publish": [ - "Discontinued Draft", - "Recommendation track", - "<a href=\"https://www.w3.org/policies/process/20250818/#recs-and-notes\">Recommendation track</a>" - ], - "stability": "It must set expectations about the (in)stability of the document. The recommended text is:

    Publication as a Discontinued Draft implies that this document is no longer intended to advance or to be maintained. It is inappropriate to cite this document as other than abandoned work.

    Include this source code:
    <p>Publication as a Discontinued Draft implies that this document is no longer intended to advance or to be maintained. It is inappropriate to cite this document as other than abandoned work.</p>
    ", - "patPolReq": true, - "knownDisclosureNumber": true, - "whichProcess": true - } - }, - "document-body": { - "name": "7. Document Body", - "rules": { - "headingWithoutID": true, - "brokenLink": true, - "cssValid": true, - "namespaces": true, - "wcag": true, - "fixupJs": true - } - }, - "compound": { - "name": "8. Compound Documents", - "rules": { - "compoundFilesLocation": ["DISC"], - "compoundOverview": true, - "compound": true - } - } - } - } - } - }, - "*": { - "sections": { - "format": { - "name": "1. Normative Document Representation", - "rules": { - "normativeVersion": "At least one normative representation must be available for requests that use the \"This Version\" URI. More than one normative representation may be delivered in response to such requests. A \"This Version\" URI must not be used to identify a non-normative representation.", - "validHTML": "recursive All normative representations must validate as HTML5 with the following limitations:
    • Inline markup for MathML is permitted but should use a (fallback) alternative.
    • If the HTML5 validator issues content warnings, the publication request must include rationale why the warning is not problematic.
    ", - "visualStyle": "Visual styles should not vary significantly among normative alternatives." - } - }, - "metadata": { - "name": "2. Document Metadata", - "rules": { - "goodStylesheet": "recursive Each document must include the following absolute URI to identify a style sheet for this maturity level: https://www.w3.org/StyleSheets/TR/2021/W3C-@{param1}

    Include this source code:
    <link rel=\"stylesheet\" type=\"text/css\" href=\"https://www.w3.org/StyleSheets/TR/2021/W3C-@{param1}\"/>

    ", - "lastStylesheet": "recursive Any internal style sheets must be cascaded before this link; i.e., the internal style sheets must not override the W3C tech report styles.", - "viewport": "recursive The viewport meta tag is required.

    Include this source code:
    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">

    ", - "canonical": "recursive The canonical link is required.

    Include this source code:
    <link rel=\"canonical\" href=\"@@URL@@\">

    ", - "delivererID": "The Working Group(s) id(s) must be listed in a data-deliverer attribute in the section \"Status of This Document\".

    Include this source code:
    <p data-deliverer=\"@@ID1@@ @@ID2@@,...\">

    " - } - }, - "front-matter": { - "name": "3. Front Matter", - "rules": { - "divClassHead": "The front matter must appear at the beginning of the body of the document, within <div class=\"head\">. There is one exception to that requirement: the hr element after the copyright may appear inside or after the div element. Editors should not include other information in this section.", - "logo": "The document must include a link to the W3C logo identified below. The URI used to identify the logo must be absolute.
    Include this source code:
    <a href=\"https://www.w3.org/\"><img height=\"48\" width=\"72\" alt=\"W3C\" src=\"https://www.w3.org/StyleSheets/TR/2021/logos/W3C\"/></a>
    ", - "title": "The document's title must be in the title element and in an h1 element. When calculating text equation, special transformations made to h1 are:
    1. Replace ':<br>' with ': '
    2. Replace '<br>' with ' - '
    3. Extract text from h1, and ignore HTML tags.
    ", - "versionNumber": "Technical report version information, i.e., version and edition numbers.
      @{param1}
    1. See the (non-normative) Version Management in W3C Technical Reports for more information.
    ", - "dateState": "The document's status and date must be in a <p id=\"w3c-state\"> element as follows (see also date syntax):
    <p id=\"w3c-state\">W3C @{param1} DD Month YYYY</p>
    @{param2}", - "docIDFormat": "Document identifier information must be presented in a dl list, where each dt element marks up an identifier role (\"This Version\", \"Latest Version\", \"History\", etc.) and each dd element includes a link whose link text is the identifier. That dl must itself be placed in a details element.

    Include this source code:
    <details open><summary>More details about this document</summary><dl>...</dl></details>

    ", - "docIDOrder": "Document identifier information must be present in this order:
    • 'This version' - URI to that version
    • 'Latest version' - URI to the latest version. See also the (non-normative) Version Management in W3C Technical Reports for information about \"latest version\" URI and version management.
    • 'History' - URI to the history of the specification
    • Editor(s)
    • Feedback - GitHub repository issue links are required in the <dl>after <dt>Feedback:</dt> in the headers (<div class=\"head\">) of the document. Links are expected to be of the form https://github.com/<USER_OR_ORG>/<REPO_NAME>/[issues|labels][/…].)
    ", - "docIDThisVersion": "The syntax of a “this version” URI must be https://www.w3.org/TR/YYYY/@{param1}-shortname-YYYYMMDD/. If the document introduces a new shortname, it must use lowercase letters.", - "docIDDate": "The title page date and the date at the end of the \"This Version\" URI must match.", - "docIDLatestVersion": "The syntax of a “latest version” URI must be https://www.w3.org/TR/shortname/.", - "docIDHistory": "The syntax of a “history” URI must be https://www.w3.org/standards/history/shortname/, and consistent with the shortname mentioned in 'Latest Version'. Note: If there's a shortname change it must be specified using the following data attribute data-previous-shortname='previous-shortname' on the <a> element.", - "editorSection": "The editors'/authors' names must be listed, including one of the following attributes (listed in descending order of precedence):
    • data-editor-id=\"@@\" (referencing W3C ID)
    • data-editor-github=\"@@\" (referencing GitHub username)
    Affiliations and email addresses are optional; email addresses are not recommended. The affiliation of Invited Experts must always be \"W3C Invited Expert\" whether they are affiliated with another organization or not. If an editor/author is acknowledged in an earlier version of this document and the individual's affiliation has since changed, list the individual using the notation \"<name>, <affiliation> (until DD Month YYYY)\". If the list of authors is very long (e.g., the entire Working Group), identify the authors in the acknowledgments section, linked from the head of the document. Distinguish any contributors from authors in the acknowledgments section.
    Note: Editors must be participating in the group producing the document at the time of its publication. If an editor left the group before publication, they can still be listed but the rationale must be provided in a <span class=\"former\"> element next to the name of the editor.", - "altRepresentations": "Authors may provide links to alternative (non-normative) representations or packages for the document. For instance:

    <p>This document is also available in these non-normative formats: <a href=\"@{param1}-shortname-20180101.html\">single HTML file</a>, <a href=\"@{param1}-shortname-20180101.tgz\">gzipped tar file of HTML</a>.</p>

    ", - "implReport": "It must include either:
    • a link to an interoperability or implementation report if the Director used such a report as part of the decision to advance the specification, or
    • a statement that the Director's decision did not involve such a report.
    ", - "translation": "There must be a link to a translations page. The recommended markup is:

    <p>See also <a href=\"https://www.w3.org/Translations/?technology=shortname\"><strong>translations</strong></a>.</p>

    See suggestions on translations in the manual of style.

    ", - "copyright": "Starting from 01 February 2023, the copyright must follow the following markup (fill in with the appropriate year, years, or year range). The type of license the document is using can be found in the group's charter.
    1. For documents using W3C Document License:
      Copyright © @{year} World Wide Web Consortium. W3C® liability, trademark and document use rules apply.
      Include this source code:
      <p class="copyright"><a href="https://www.w3.org/policies/#copyright">Copyright</a> © @{year} <a href="https://www.w3.org/">World Wide Web Consortium</a>. <abbr title="World Wide Web Consortium">W3C</abbr><sup>®</sup> <a href="https://www.w3.org/policies/#Legal_Disclaimer">liability</a>, <a href="https://www.w3.org/policies/#W3C_Trademarks">trademark</a> and <a href=\"https://www.w3.org/copyright/document-license/\">document use</a> rules apply.</p>
    2. For documents using W3C Software and Document License:
      Copyright © @{year} World Wide Web Consortium. W3C® liability, trademark and permissive document license rules apply.
      Include this source code:
      <p class="copyright"><a href="https://www.w3.org/policies/#copyright">Copyright</a> © @{year} <a href="https://www.w3.org/">World Wide Web Consortium</a>. <abbr title="World Wide Web Consortium">W3C</abbr><sup>®</sup> <a href="https://www.w3.org/policies/#Legal_Disclaimer">liability</a>, <a href="https://www.w3.org/policies/#W3C_Trademarks">trademark</a> and <a href="https://www.w3.org/copyright/software-license/">permissive document license</a> rules apply.</p>

    Note: Exceptions are listed in this json file.", - "hrAfterCopyright": "A horizontal rule (hr) must follow the copyright." - } - }, - "navigation": { - "name": "6. Table of Contents", - "rules": { - "toc": "There should be a table of contents after the status section, labeled with an h2 element with content \"Table of Contents\".", - "tocNav": "The table of content must be inside a navigation element (nav).

    Include this source code:
    <nav id=\"toc\"><h2>Table of Contents</h2>

    " - } - }, - "document-status": { - "name": "5. Document Status Section", - "rules": { - "sotd": "There must be a status section that follows the abstract, labeled with an h2 element with content \"Status of This Document\". The Team maintains the status section of a document.", - "boilerplateTRDoc": "It must begin with the following boilerplate text:

    This section describes the status of this document at the time of its publication. A list of current W3C publications and the latest revision of this technical report can be found in the W3C standards and drafts index.

    Include this source code:
    <p><em>This section describes the status of this document at the time of its publication. A list of current W3C publications and the latest revision of this technical report can be found in the <a href=\"https://www.w3.org/TR/\">W3C standards and drafts index</a>.</em></p>
    ", - "datesFormat": "All dates must have one of the following forms:
    1. DD Month YYYY : 09 January 2020
    2. DD-Month-YYYY : 09-January-2020
    3. DD Mon YYYY : 09 Jan 2020
    4. DD-Mon-YYYY : 09-Jan-2020
    A leading zero in the day is optional.", - "publish": "It must include the name of the W3C group that produced the document, the type of document and its track. The name must be a link to a public page for the group.

    This document was published by the @@@ Working/Interest Group as a @{param1} using the @{param2}.

    Include this source code:
    <p>This document was published by the <a href=\"https://www.w3.org/groups/(wg|ig)/@@/\">@@@ Working/Interest Group</a> as a @{param1} using the @{param3}.</p>
    ", - "customParagraph": "It must include at least one customized paragraph. This section should include the title page date (i.e., the one next to the maturity level at the top of the document). These paragraphs should explain the publication context, including rationale and relationships to other work. See examples and more discussion in the Manual of Style.", - "changesList": "It @{param1} include a link to changes since the previous draft (e.g., a list of changes or a diff document or both; see the online HTML diff tool).@{param2}", - "deployment": "It must include the expectations in terms of deployment. The recommended text is:
    W3C recommends the wide deployment of this specification as a standard for the Web.
    ", - "stability": "It must set expectations about the (in)stability of the document. The recommended text @{param2} is:

    Publication as @{param1} does not imply endorsement by W3C and its Members.

    Include this source code:
    <p>Publication as @{param1} does not imply endorsement by W3C and its Members.</p>
    ", - "draftStability": "It must include the following sentences in the \"Status Of This Document\":
    This is a draft document and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to cite this document as other than a work in progress.
    ", - "patPolReq": "It must include a text related to the patent policy:
    • if the document is under REC track, the text is:

      This document was produced by a group operating under the W3C Patent Policy. W3C maintains a public list of any patent disclosures made in connection with the deliverables of the group; that page also includes instructions for disclosing a patent. An individual who has actual knowledge of a patent that the individual believes contains Essential Claim(s) must disclose the information in accordance with section 6 of the W3C Patent Policy.

      Include this source code:
      <p>This document was produced by a group operating under the <a href=\"https://www.w3.org/policies/patent-policy/\">W3C Patent Policy</a>. W3C maintains a <a rel=\"disclosure\" href=\"@@URI to IPP status or other page@@\">public list of any patent disclosures</a> made in connection with the deliverables of the group; that page also includes instructions for disclosing a patent. An individual who has actual knowledge of a patent that the individual believes contains <a href=\"https://www.w3.org/policies/patent-policy/#def-essential\">Essential Claim(s)</a> must disclose the information in accordance with <a href=\"https://www.w3.org/policies/patent-policy/#sec-Disclosure\">section 6 of the W3C Patent Policy</a>.</p>
    • if the document is under Note track or Registry track, it must set the licensing requirements related to the Patent Policy. The text is:

      The W3C Patent Policy does not carry any licensing requirements or commitments on this document.

      Include this source code:
      <p>The <a href=\"https://www.w3.org/policies/patent-policy/\">W3C Patent Policy</a> does not carry any licensing requirements or commitments on this document.</p>

    ", - "knownDisclosureNumber": "It must not indicate the number of known disclosures at the time of publication.", - "whichProcess": "The document must include the following boilerplate text in the status section to identify the governing process:

    This document is governed by the 18 August 2023 W3C Process Document.

    Include this source code:
    <p>This document is governed by the <a id=\"w3c_process_revision\" href=\"https://www.w3.org/policies/process/20250818/\">18 August 2023 W3C Process Document</a>. </p>
    ", - "discontinue": "If the document was published due to a W3C decision to stop work on this material, the status section should include that rationale.", - "expectations": "It should indicate the level of endorsement within the group for the material, set expectations that the group has completed work on the topics covered by the document, and set expectations about the group's commitment to respond to comments about the document.", - "ACRepFeedbackEmail": "It also must provide information to Advisory Committee Representatives about how to send their review comments (e.g., the link to all AC reviews, or a link to a specific questionnaire)", - "reviewEndDatePR": "It must include the end date of the review period.", - "recRelation": "It must indicate that it rescinds a Recommendation and must link to the most recent Recommendation (if any) having the same major revision number.", - "altTechno": "It should direct readers to alternative technologies." - } - }, - "document-body": { - "name": "7. Document Body", - "rules": { - "headingWithoutID": "recursive Every marked-up section and subsection of the document must have a target anchor. A section is identified by a heading element (h1-h6). The anchor may be specified using an id (or name if an a element is used) attribute on any of the following: the heading element itself, the parent div or section element of the heading element (where the heading element is the first child of the div or section), a descendant of the heading element, or an a immediately preceding the heading element.", - "brokenLink": "The document must not have any broken internal links or broken links to other resources at w3.org. The document should not have any other broken links.", - "cssValid": "recursive The document must not have any style sheet errors.", - "namespaces": "recursive All proposed XML namespaces created by the publication of the document must follow URIs for W3C Namespaces .", - "wcag": "The document(s) must conform to the Web Content Accessibility Guidelines 2.2, Level AA. Note: You may wish to consult the customizable quick reference to Web Content Accessibility Guidelines 2.2.", - "securityAndPrivacy": "The document should have a security and privacy considerations section.", - "fixupJs": "The document must include the script fixup.js.

    Include this source code:
    <script src=\"//www.w3.org/scripts/TR/2021/fixup.js\" type=\"application/javascript\"></script>

    " - } - }, - "compound": { - "name": "8. Compound Documents", - "rules": { - "compoundFilesLocation": "If the document is compound (i.e., if it consists of more than one file), all the files must be under a directory /TR/YYYY/@{param1}-shortname-YYYYMMDD/. Exceptions are resources under these paths:
    1. https://www.w3.org/StyleSheets/
    2. https://www.w3.org/scripts/
    ", - "compoundOverview": "The main page should be called Overview.html.", - "compound": "All other files must be reachable by links from the document." - } - }, - "rec-edit-install": { - "name": "1. Install the document.", - "rules": { - "install": "(if new dated version) Copy the original REC in /TR into a new dated <new date> directory in /TR" - } - }, - "rec-edit-stylesheet": { - "name": "2. Use the corresponding stylesheet", - "rules": { - "stylesheet": "For the W3C stylesheet, use the @{param1} version of the W3C stylesheet in use in the REC (old, or 2021). So, it's one of:
    1. https://www.w3.org/StyleSheets/TR/W3C-@{param1}.css
    2. https://www.w3.org/StyleSheets/TR/2021/W3C-@{param1}.css
    3. or reuse the (some?) content from https://www.w3.org/TR/html50/superseded.css as needed
    " - } - }, - "rec-edit-w3c-state": { - "name": "3. Change the title of the document", - "rules": { - "w3c-state": "In the <p id=\"w3c-state\"> element containing the state and date (\"W3C Recommendation DD Month YYYY\"), add:
    1. a <br> element after the original date
    2. and \"@{param1} <new date>\" when the @{param1} document gets published by the webmaster
    " - } - }, - "rec-edit-version": { - "name": "4. Set the \"This version\" link", - "rules": { - "version": "(if new dated version) This version link gets the form https://www.w3.org/TR/YYYY/@{param1}-<shortname-with-version>-<newdate>/" - } - }, - "rec-edit-previous": { - "name": "5. Set the \"Previous version\" link", - "rules": { - "previous": "(if new dated version) Previous version becomes the dated link of the REC being @{param1}" - } - }, - "rec-edit-sotd": { - "name": "6. Change the \"status of this document\" section", - "rules": { - "sotd": "The SOTD gets updated as follows:
    • 6.1. add the following markup (update link to proper Process version if needed):
      <p>This specification is a <a href=\"https://www.w3.org/policies/process/20250818/#rec-rescind\">@{param1} Recommendation</a>. A newer specification exists that is recommended for new adoption in place of this specification. </p>
    • 6.2. Make sure to update or remove the sentence indicating that the document \"is a Recommendation\" if one appears.
    • 6.3. Remove the following paragraph:
      <p>This document has been reviewed by W3C Members, by software developers, and by other W3C groups and interested parties, and is endorsed by the Director as a W3C Recommendation. It is a stable document and may be used as reference material or cited from another document. W3C's role in making the Recommendation is to draw attention to the specification and to promote its widespread deployment. This enhances the functionality and interoperability of the Web.</p>
    • 6.4. ... and replace it with (update the @@ as needed):
      <p>For purposes of the W3C Patent Policy, this @{param1} Recommendation  has the same status as an active Recommendation; it retains licensing  commitments and remains available as a reference for old -- and  possibly still deployed -- implementations, but is not recommended for future implementation. New implementations should follow the <a href='@@'>latest version</a> of the @@ specification.</p>
    • 6.5. DO NOT TOUCH THE PATENT POLICY PARAGRAPH. The document is still governed by IP commitments even if it is @{param2} (as described in the paragraph you added in the previous step).
    • 6.6. Look at the resulting SOTD, make sure it makes sense and \"modify as needed\". In other words, if you need to add more explanations about why the document was @{param2}, feel free to do so by adding to the first paragraph introduced by 6.1. The Director may ask to add or modify specific wordings as well. Just remember to keep the time spent as minimal since our goal is to minimize the time spent @{param3} documents, so don't overdo it.@{param4}
    " - } - } - } - } -} diff --git a/lib/rules/headers/copyright.ts b/lib/rules/headers/copyright.ts index d85dd5f16..67efb6e09 100644 --- a/lib/rules/headers/copyright.ts +++ b/lib/rules/headers/copyright.ts @@ -13,7 +13,7 @@ import type { Element } from 'domhandler'; import { AB, TAG } from '../../util.js'; import type { Specberus } from '../../validator.js'; -import copyrightExceptions from '../../copyright-exceptions.json' with { type: 'json' }; +import copyrightExceptions from '../../copyright-exceptions.js'; import type { ApiCharter, RuleCheckFunction, RuleMeta } from '../../types.js'; const self: RuleMeta = { diff --git a/lib/rules/headers/w3c-state.ts b/lib/rules/headers/w3c-state.ts index 84180af4c..0e2fb1c12 100644 --- a/lib/rules/headers/w3c-state.ts +++ b/lib/rules/headers/w3c-state.ts @@ -1,6 +1,5 @@ -import rules from '../../rules.json' with { type: 'json' }; +import rules from '../../rules-track.js'; import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -import { isRuleTrack } from '../../util.js'; const self: RuleMeta = { name: 'headers.w3c-state', @@ -30,9 +29,9 @@ export const check: RuleCheckFunction = (sr, done) => { sr.error(self, 'bad-w3c-state'); } - for (const t in rules) - if (isRuleTrack(t) && !profileFound) - for (const profile of Object.values(rules[t].profiles)) { + for (const { profiles } of Object.values(rules)) + if (!profileFound) + for (const profile of Object.values(profiles)) { const rx = new RegExp(`^w3c\\ ${profile.name}(\\ |,)`, 'i'); if (rx.test(txt)) { profileFound = true; diff --git a/lib/rules/metadata/profile.ts b/lib/rules/metadata/profile.ts index 6af4efd7e..1c4b37fdd 100644 --- a/lib/rules/metadata/profile.ts +++ b/lib/rules/metadata/profile.ts @@ -5,13 +5,13 @@ import type { Cheerio } from 'cheerio'; import type { Element } from 'domhandler'; -import { allProfiles, isRuleTrack } from '../../util.js'; +import { allProfiles } from '../../util.js'; import type { Specberus } from '../../validator.js'; import { sortedProfiles } from '../../views.js'; import type { RecMetadata, RuleCheckFunction } from '../../types.js'; import { check as getTitle } from './title.js'; -import rules from '../../rules.json' with { type: 'json' }; +import rules from '../../rules-track.js'; // 'self.name' would be 'metadata.profile' export const name = 'metadata.profile'; @@ -34,18 +34,16 @@ export const check: RuleCheckFunction = async (sr, done) => { } const candidate = $stateEl && sr.norm($stateEl.text()).toLowerCase(); if (candidate) { - for (const t in rules) { - if (isRuleTrack(t)) { - for (const [p, profile] of Object.entries(rules[t].profiles)) { - const name = profile.name.toLowerCase(); - if ( - candidate.indexOf(name) !== -1 && - matchedLength < name.length - ) { - id = p; - $profileEl = $stateEl; - matchedLength = name.length; - } + for (const { profiles } of Object.values(rules)) { + for (const [p, profile] of Object.entries(profiles)) { + const name = profile.name.toLowerCase(); + if ( + candidate.indexOf(name) !== -1 && + matchedLength < name.length + ) { + id = p; + $profileEl = $stateEl; + matchedLength = name.length; } } } diff --git a/lib/rules/structure/neutral.ts b/lib/rules/structure/neutral.ts index 6a7ce8e7d..9174d18ff 100644 --- a/lib/rules/structure/neutral.ts +++ b/lib/rules/structure/neutral.ts @@ -2,7 +2,7 @@ * @file make sure specification use neutral words. */ -import badterms from '../../../public/badterms.json' with { type: 'json' }; +import badterms from '../../badterms.js'; import type { RuleCheckFunction } from '../../types.js'; const self = { diff --git a/lib/types.d.ts b/lib/types.d.ts index 64c32d5a2..549278ef2 100644 --- a/lib/types.d.ts +++ b/lib/types.d.ts @@ -76,28 +76,6 @@ export interface RuleModule extends RuleBase { check: RuleCheckFunction; } -/** `section` from lib/rules.json */ -export interface RulesSection { - name: string; - rules: Record; -} - -/** `section` under `*` from lib/rules.json */ -export interface GenericRulesSection { - name: string; - rules: Record; -} - -/** `profile` from lib/rules.json */ -export interface RulesProfile { - name: string; - order: number; - sections: { - '*': GenericRulesSection; - [index: string]: RulesSection; - }; -} - export interface ProfileModule { config: SpecberusConfig; name: string; diff --git a/lib/util.ts b/lib/util.ts index b451db110..b2824265f 100644 --- a/lib/util.ts +++ b/lib/util.ts @@ -13,9 +13,10 @@ import w3cApi from 'node-w3capi'; import type { HandlerMessage, SpecberusConfig } from './types.js'; import type { ValidateOptions } from './validator.js'; +import pkg from '../package.json' with { type: 'json' }; -import rules from './rules.json' with { type: 'json' }; -type RuleTrack = Exclude; +/** Current specberus version recorded in package.json */ +export const specberusVersion = pkg.version; /** * Builds a JSON result (of validation, metadata extraction, etc). @@ -186,11 +187,6 @@ export async function processParams( return result; } -/** Checks that the passed string is an existing specific track in rules.json. */ -export function isRuleTrack(track: string): track is RuleTrack { - return Object.hasOwn(rules, track) && track !== '*'; -} - const githubUsernameCache: Record = {}; const githubUsernameCacheTimeout = 3600000; diff --git a/lib/validator.ts b/lib/validator.ts index ddd293758..98a6489c0 100644 --- a/lib/validator.ts +++ b/lib/validator.ts @@ -15,8 +15,14 @@ import { assembleData, setLanguage } from './l10n.js'; import * as profileMetadata from './profiles/metadata.js'; import * as profileAdditionalMetadata from './profiles/additionalMetadata.js'; import { get } from './throttled-ua.js'; -import { AB, buildJSONresult, processParams, REC_TEXT, TAG } from './util.js'; -import pkg from '../package.json' with { type: 'json' }; +import { + AB, + buildJSONresult, + processParams, + REC_TEXT, + specberusVersion, + TAG, +} from './util.js'; import type { ApiCharter, HandlerMessage, @@ -121,7 +127,7 @@ export class Specberus { meta: Record | undefined; source!: any | null; url!: string | null; - version = pkg.version; + version = specberusVersion; private $docDateEl: Cheerio | undefined; private $sotdSection: Cheerio | null | undefined; /** Group objects returned by W3C API charters endpoint */ @@ -639,7 +645,7 @@ export class Specberus { promiseArray.push( new Promise(resolve => { get(groupApiUrl) - .set('User-Agent', `W3C-Pubrules/${pkg.version}`) + .set('User-Agent', `W3C-Pubrules/${specberusVersion}`) .end((_, data) => { resolve(data); }); @@ -717,7 +723,7 @@ export class Specberus { get(groupApiUrl) .set( 'User-Agent', - `W3C-Pubrules/${pkg.version}` + `W3C-Pubrules/${specberusVersion}` ) .end((_: any, data) => { resolve(data); @@ -894,7 +900,7 @@ export class Specberus { loadURL(url: string, cb: (err: any, $?: CheerioAPI) => void) { if (!cb) return this.throw('Missing callback to loadURL.'); get(url) - .set('User-Agent', `W3C-Pubrules/${pkg.version}`) + .set('User-Agent', `W3C-Pubrules/${specberusVersion}`) .end((err, res) => { if (err) return this.throw(err.message); if (!res.text) return this.throw(`Body of ${url} is empty.`); diff --git a/lib/views.ts b/lib/views.ts index e1a71f7ce..114437c60 100644 --- a/lib/views.ts +++ b/lib/views.ts @@ -2,15 +2,11 @@ import type { Express, Request, Response } from 'express'; import handlebars from 'express-handlebars'; import { escape } from 'querystring'; -import type { - GenericRulesSection, - RulesProfile, - RulesSection, -} from './types.js'; -import { isRuleTrack } from './util.js'; - -import pkg from '../package.json' with { type: 'json' }; -import rules from './rules.json' with { type: 'json' }; +import rules, { type RulesProfile, type RulesSection } from './rules-track.js'; +import genericSections, { + type GenericRulesSection, +} from './rules-generic-sections.js'; +import { specberusVersion } from './util.js'; // Settings: const DEBUG = process && process.env && process.env.DEBUG; @@ -31,7 +27,7 @@ const serveStraight = function (req: Request, res: Response) { res.render(fragment, { DEBUG, BASE_URI, - version: pkg.version, + version: specberusVersion, nav, title: fragment, }); @@ -42,13 +38,13 @@ const handleWrongPage = function (_: Request, res: Response) { res.render('error', { DEBUG, BASE_URI, - version: pkg.version, + version: specberusVersion, nav, title: 'whut?', }); }; -/** Profile object as returned by listProfiles (distinct from rules.json) */ +/** Profile object as returned by listProfiles (distinct from rules-track.ts) */ interface Profile { order: number; abbr: string; @@ -64,32 +60,28 @@ function listProfiles() { if (a.order > b.order) return 1; return 0; }; - for (const t in rules) { - if (isRuleTrack(t)) { - const rule = rules[t]; - result.push({ - order: rule.order, - abbr: t, - name: rule.name, - profiles: [] as Profile[], + for (const [track, entry] of Object.entries(rules)) { + result.push({ + order: entry.order, + abbr: track, + name: entry.name, + profiles: [] as Profile[], + }); + for (const [p, profile] of Object.entries(entry.profiles)) + result[result.length - 1].profiles.push({ + order: profile.order, + abbr: p, + name: profile.name, }); - for (const [p, profile] of Object.entries(rule.profiles)) - result[result.length - 1].profiles.push({ - order: profile.order, - abbr: p, - name: profile.name, - }); - result[result.length - 1].profiles.sort(sortByOrderField); - } - result.sort(sortByOrderField); + result[result.length - 1].profiles.sort(sortByOrderField); } - return result; + return result.sort(sortByOrderField); } export const sortedProfiles = listProfiles(); // TODO(tripu): Document. function formatRules(sections: Record) { - const commonSections = rules['*'].sections as Record< + const commonSections = genericSections as Record< string, GenericRulesSection >; @@ -154,22 +146,18 @@ function retrieveProfile(query: qs.ParsedQs) { if (query && query.profile && typeof query.profile === 'string') { const codename = escape(query.profile).trim().toUpperCase(); - if (rules) - for (const t in rules) { - if (isRuleTrack(t)) { - const profiles = rules[t].profiles; - if (Object.hasOwn(profiles, codename)) { - const profile = profiles[ - codename as keyof typeof profiles - ] as RulesProfile; - result = { - abbr: codename, - name: `${profile.name}`, - body: formatRules(profile.sections), - }; - } - } + for (const { profiles } of Object.values(rules)) { + if (Object.hasOwn(profiles, codename)) { + const profile = profiles[ + codename as keyof typeof profiles + ] as RulesProfile; + result = { + abbr: codename, + name: `${profile.name}`, + body: formatRules(profile.sections), + }; } + } if (!result) result = { error: `

    Error: unknown profile ${escape( @@ -196,7 +184,7 @@ export function setUp(app: Express) { res.render('index', { DEBUG, BASE_URI, - version: pkg.version, + version: specberusVersion, nav, interactive: true, tracks: sortedProfiles, @@ -207,7 +195,7 @@ export function setUp(app: Express) { res.render('doc', { DEBUG, BASE_URI, - version: pkg.version, + version: specberusVersion, nav, title: 'documentation', tracks: sortedProfiles, @@ -218,7 +206,7 @@ export function setUp(app: Express) { res.render('doc/rules', { DEBUG, BASE_URI, - version: pkg.version, + version: specberusVersion, nav, title: 'publication rules', content: retrieveProfile(req.query), diff --git a/public/badterms.json b/public/badterms.json deleted file mode 100644 index df5a3a31c..000000000 --- a/public/badterms.json +++ /dev/null @@ -1,38 +0,0 @@ -[ - { - "term": ["master"], - "variation": ["masters"], - "alternatives": ["main"] - }, - { - "term": ["slave"], - "variation": ["slaves"], - "alternatives": ["replica"] - }, - { - "term": ["whitelist"], - "variation": ["whitelists"], - "alternatives": ["allowlist"] - }, - { - "term": ["blacklist"], - "variation": ["blacklists"], - "alternatives": ["denylist"] - }, - { - "term": ["grandfather"], - "alternatives": ["legacy"] - }, - { - "term": ["sanity"], - "alternatives": ["coherence"] - }, - { - "term": ["he", "she", "him", "her"], - "alternatives": ["they"] - }, - { - "term": ["his", "hers"], - "alternatives": ["theirs"] - } -] diff --git a/test/api.js b/test/api.js index 911e87263..2340ef91a 100644 --- a/test/api.js +++ b/test/api.js @@ -12,8 +12,8 @@ import fileUpload from 'express-fileupload'; import superagent from 'superagent'; import { setUp } from '../lib/api.js'; +import { specberusVersion } from '../lib/util.js'; import { cleanupMocks, setupMocks } from './lib/utils.js'; -import meta from '../package.json' with { type: 'json' }; const { expect } = chai; @@ -97,7 +97,7 @@ describe('API', () => { describe('Method “version”', () => { it('Should return the right version string', () => { const query = get('version'); - return expect(query).to.eventually.become(meta.version); + return expect(query).to.eventually.become(specberusVersion); }); }); diff --git a/test/l10n.js b/test/l10n.js index 0109fffb8..ac7cfd01e 100644 --- a/test/l10n.js +++ b/test/l10n.js @@ -8,7 +8,7 @@ import { join } from 'path'; import * as chai from 'chai'; import * as l10n from '../lib/l10n-en_GB.js'; -import rules from '../lib/rules.json' with { type: 'json' }; +import rules from '../lib/rules-track.js'; const { expect } = chai; @@ -149,7 +149,7 @@ describe('L10n', () => { }); describe('UI messages module', () => { - it('“lib/rules-wrapper” should be a valid object', () => + it('“lib/rules-track" should be a valid object', () => expect(rules).to.be.an('object')); it('“lib/l10n-en_GB” should be a valid object', () => { expect(typeof l10n).to.equal('object'); From 65f1f250e44c1df098a00be87819477c4cb17ed8 Mon Sep 17 00:00:00 2001 From: "Kenneth G. Franqueiro" Date: Tue, 19 May 2026 16:05:21 -0400 Subject: [PATCH 03/11] Convert tests to TypeScript and remove Node v20 support (#2098) --- .c8rc | 4 +- .github/dependabot.yml | 2 - .gitignore | 1 + README.md | 27 +- nodemon.json | 6 - package-lock.json | 1075 +---------------- package.json | 21 +- test/api.js | 161 --- test/api.ts | 182 +++ test/data/SUBM/{MEM-SUBM.js => MEM-SUBM.ts} | 0 test/data/SUBM/{SUBMBase.js => SUBMBase.ts} | 0 .../{DNOTE-Echidna.js => DNOTE-Echidna.ts} | 0 test/data/TR/Note/{DNOTE.js => DNOTE.ts} | 0 .../Note/{NOTE-Echidna.js => NOTE-Echidna.ts} | 0 test/data/TR/Note/{NOTE.js => NOTE.ts} | 0 test/data/TR/Note/{STMT.js => STMT.ts} | 0 .../data/TR/Note/{noteBase.js => noteBase.ts} | 0 .../{CR-Echidna.js => CR-Echidna.ts} | 0 test/data/TR/Recommendation/{CR.js => CR.ts} | 0 .../{CRD-Echidna.js => CRD-Echidna.ts} | 0 .../data/TR/Recommendation/{CRD.js => CRD.ts} | 0 .../TR/Recommendation/{DISC.js => DISC.ts} | 0 .../TR/Recommendation/{FPWD.js => FPWD.ts} | 0 .../{REC-RSCND.js => REC-RSCND.ts} | 0 .../data/TR/Recommendation/{REC.js => REC.ts} | 0 .../{WD-Echidna.js => WD-Echidna.ts} | 0 test/data/TR/Recommendation/{WD.js => WD.ts} | 0 ...mendationBase.js => recommendationBase.ts} | 0 test/data/TR/Registry/{CRY.js => CRY.ts} | 0 test/data/TR/Registry/{CRYD.js => CRYD.ts} | 0 test/data/TR/Registry/{DRY.js => DRY.ts} | 0 test/data/TR/Registry/{RY.js => RY.ts} | 0 .../{registryBase.js => registryBase.ts} | 0 test/data/TR/{TRBase.js => TRBase.ts} | 0 .../{goodDocuments.js => goodDocuments.ts} | 2 +- test/data/{specBase.js => specBase.ts} | 0 .../SUBM/{MEM-SUBM.js => MEM-SUBM.ts} | 3 +- .../{DNOTE-Echidna.js => DNOTE-Echidna.ts} | 0 test/doc-views/TR/Note/{DNOTE.js => DNOTE.ts} | 0 .../Note/{NOTE-Echidna.js => NOTE-Echidna.ts} | 0 test/doc-views/TR/Note/{NOTE.js => NOTE.ts} | 0 test/doc-views/TR/Note/{STMT.js => STMT.ts} | 0 .../TR/Note/{noteBase.js => noteBase.ts} | 4 +- .../{CR-Echidna.js => CR-Echidna.ts} | 0 .../TR/Recommendation/{CR.js => CR.ts} | 0 .../{CRD-Echidna.js => CRD-Echidna.ts} | 0 .../TR/Recommendation/{CRD.js => CRD.ts} | 3 +- .../TR/Recommendation/{DISC.js => DISC.ts} | 0 .../TR/Recommendation/{FPWD.js => FPWD.ts} | 0 .../{REC-RSCND.js => REC-RSCND.ts} | 0 .../TR/Recommendation/{REC.js => REC.ts} | 0 .../{WD-Echidna.js => WD-Echidna.ts} | 0 .../TR/Recommendation/{WD.js => WD.ts} | 0 ...mendationBase.js => recommendationBase.ts} | 5 +- test/doc-views/TR/Registry/{CRY.js => CRY.ts} | 0 .../TR/Registry/{CRYD.js => CRYD.ts} | 6 +- test/doc-views/TR/Registry/{DRY.js => DRY.ts} | 0 test/doc-views/TR/Registry/{RY.js => RY.ts} | 0 .../{registryBase.js => registryBase.ts} | 3 +- test/doc-views/TR/{TRBase.js => TRBase.ts} | 19 +- test/doc-views/{specBase.js => specBase.ts} | 16 +- test/l10n.js | 165 --- test/l10n.ts | 202 ++++ test/lib/{nockData.js => nockData.ts} | 38 +- test/lib/{testserver.js => testserver.ts} | 78 +- test/lib/{utils.js => utils.ts} | 108 +- test/rules.js | 350 ------ test/rules.ts | 387 ++++++ test/{samples.js => samples.ts} | 0 test/validation.js | 22 - tsconfig.json | 2 +- 71 files changed, 988 insertions(+), 1904 deletions(-) delete mode 100644 nodemon.json delete mode 100644 test/api.js create mode 100644 test/api.ts rename test/data/SUBM/{MEM-SUBM.js => MEM-SUBM.ts} (100%) rename test/data/SUBM/{SUBMBase.js => SUBMBase.ts} (100%) rename test/data/TR/Note/{DNOTE-Echidna.js => DNOTE-Echidna.ts} (100%) rename test/data/TR/Note/{DNOTE.js => DNOTE.ts} (100%) rename test/data/TR/Note/{NOTE-Echidna.js => NOTE-Echidna.ts} (100%) rename test/data/TR/Note/{NOTE.js => NOTE.ts} (100%) rename test/data/TR/Note/{STMT.js => STMT.ts} (100%) rename test/data/TR/Note/{noteBase.js => noteBase.ts} (100%) rename test/data/TR/Recommendation/{CR-Echidna.js => CR-Echidna.ts} (100%) rename test/data/TR/Recommendation/{CR.js => CR.ts} (100%) rename test/data/TR/Recommendation/{CRD-Echidna.js => CRD-Echidna.ts} (100%) rename test/data/TR/Recommendation/{CRD.js => CRD.ts} (100%) rename test/data/TR/Recommendation/{DISC.js => DISC.ts} (100%) rename test/data/TR/Recommendation/{FPWD.js => FPWD.ts} (100%) rename test/data/TR/Recommendation/{REC-RSCND.js => REC-RSCND.ts} (100%) rename test/data/TR/Recommendation/{REC.js => REC.ts} (100%) rename test/data/TR/Recommendation/{WD-Echidna.js => WD-Echidna.ts} (100%) rename test/data/TR/Recommendation/{WD.js => WD.ts} (100%) rename test/data/TR/Recommendation/{recommendationBase.js => recommendationBase.ts} (100%) rename test/data/TR/Registry/{CRY.js => CRY.ts} (100%) rename test/data/TR/Registry/{CRYD.js => CRYD.ts} (100%) rename test/data/TR/Registry/{DRY.js => DRY.ts} (100%) rename test/data/TR/Registry/{RY.js => RY.ts} (100%) rename test/data/TR/Registry/{registryBase.js => registryBase.ts} (100%) rename test/data/TR/{TRBase.js => TRBase.ts} (100%) rename test/data/{goodDocuments.js => goodDocuments.ts} (96%) rename test/data/{specBase.js => specBase.ts} (100%) rename test/doc-views/SUBM/{MEM-SUBM.js => MEM-SUBM.ts} (99%) rename test/doc-views/TR/Note/{DNOTE-Echidna.js => DNOTE-Echidna.ts} (100%) rename test/doc-views/TR/Note/{DNOTE.js => DNOTE.ts} (100%) rename test/doc-views/TR/Note/{NOTE-Echidna.js => NOTE-Echidna.ts} (100%) rename test/doc-views/TR/Note/{NOTE.js => NOTE.ts} (100%) rename test/doc-views/TR/Note/{STMT.js => STMT.ts} (100%) rename test/doc-views/TR/Note/{noteBase.js => noteBase.ts} (96%) rename test/doc-views/TR/Recommendation/{CR-Echidna.js => CR-Echidna.ts} (100%) rename test/doc-views/TR/Recommendation/{CR.js => CR.ts} (100%) rename test/doc-views/TR/Recommendation/{CRD-Echidna.js => CRD-Echidna.ts} (100%) rename test/doc-views/TR/Recommendation/{CRD.js => CRD.ts} (93%) rename test/doc-views/TR/Recommendation/{DISC.js => DISC.ts} (100%) rename test/doc-views/TR/Recommendation/{FPWD.js => FPWD.ts} (100%) rename test/doc-views/TR/Recommendation/{REC-RSCND.js => REC-RSCND.ts} (100%) rename test/doc-views/TR/Recommendation/{REC.js => REC.ts} (100%) rename test/doc-views/TR/Recommendation/{WD-Echidna.js => WD-Echidna.ts} (100%) rename test/doc-views/TR/Recommendation/{WD.js => WD.ts} (100%) rename test/doc-views/TR/Recommendation/{recommendationBase.js => recommendationBase.ts} (84%) rename test/doc-views/TR/Registry/{CRY.js => CRY.ts} (100%) rename test/doc-views/TR/Registry/{CRYD.js => CRYD.ts} (90%) rename test/doc-views/TR/Registry/{DRY.js => DRY.ts} (100%) rename test/doc-views/TR/Registry/{RY.js => RY.ts} (100%) rename test/doc-views/TR/Registry/{registryBase.js => registryBase.ts} (90%) rename test/doc-views/TR/{TRBase.js => TRBase.ts} (94%) rename test/doc-views/{specBase.js => specBase.ts} (98%) delete mode 100644 test/l10n.js create mode 100644 test/l10n.ts rename test/lib/{nockData.js => nockData.ts} (95%) rename test/lib/{testserver.js => testserver.ts} (54%) rename test/lib/{utils.js => utils.ts} (66%) delete mode 100644 test/rules.js create mode 100644 test/rules.ts rename test/{samples.js => samples.ts} (100%) delete mode 100644 test/validation.js diff --git a/.c8rc b/.c8rc index 6e986c495..ef11cef38 100644 --- a/.c8rc +++ b/.c8rc @@ -1,6 +1,6 @@ { "all": true, - "exclude": ["public/*", "test/*", "tools/*"], - "extension": [".js"], + "exclude": ["public/*", "test/*", "**/*.d.ts"], + "extension": [".ts"], "reporter": ["html"] } diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d21eb5b82..d044b77cf 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -18,8 +18,6 @@ updates: update-types: ['version-update:semver-minor'] - dependency-name: 'husky' update-types: ['version-update:semver-minor'] - - dependency-name: 'nodemon' - update-types: ['version-update:semver-minor'] - package-ecosystem: github-actions directory: '/' schedule: diff --git a/.gitignore b/.gitignore index 6033c18af..4bdafb3e0 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ app.js *.d.ts !lib/types.d.ts lib/**/*.js +test/**/*.js diff --git a/README.md b/README.md index 4a43287f8..83bce8def 100644 --- a/README.md +++ b/README.md @@ -101,38 +101,25 @@ GH_TOKEN=github_pat_... npm start ## 3. Testing -#### 1. Simple test +### 1. Simple test -Testing is done using mocha. Simply run: +Run: ```bash -$ mocha +$ npm test ``` -from the root and you will be running the test suite. Mocha can be installed with: +from the root to run the test suite. -```bash -$ npm install -g mocha -``` - -#### 2. SKIP_NETWORK - -Some of the tests can on occasion take a long time, or fail outright because a remote service is -unavailable. To work around this, you can set SKIP_NETWORK: - -```bash -$ SKIP_NETWORK=1 mocha -``` - -#### 3. Run testserver +### 2. Run testserver -The testcase document can run independently +The testcase document server can run independently: ```bash $ npm run testserver ``` -#### 4. Run certain test +### 3. Run certain test Add process env before `npm run test` and `describe.only()` to run single test. diff --git a/nodemon.json b/nodemon.json deleted file mode 100644 index 7cc19b31e..000000000 --- a/nodemon.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "env": { - "PORT": 8001 - }, - "ignore": ["public/*"] -} diff --git a/package-lock.json b/package-lock.json index 805e0aeb0..3461b9b9c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,31 +29,27 @@ "tmp": "0.2.7" }, "devDependencies": { - "@rvagg/chai-as-promised": "8.0.2", "@types/express": "^5.0.6", "@types/express-fileupload": "^1.5.1", - "@types/mocha": "^10.0.10", + "@types/lodash.merge": "^4.6.9", "@types/morgan": "^1.9.10", "@types/node": "^24.10.9", "@types/superagent": "^8.1.9", "@types/tmp": "^0.2.6", "c8": "^11.0.0", - "chai": "6.2.2", "cspell": "9.0.2", "domhandler": "^6.0.1", "expect.js": "0.3", "husky": "9.0.11", "lint-staged": "16.4.0", "lodash.merge": "^4.6.2", - "mocha": "11.7.6", "nock": "15.0.0", - "nodemon": "3.0.3", "prettier": "3.8.4", "tsx": "^4.21.0", "typescript": "^6.0.2" }, "engines": { - "node": "20 || 22 || 24", + "node": "22 || 24", "npm": ">=7" } }, @@ -1130,67 +1126,6 @@ "node": ">=18" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/@istanbuljs/schema": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", @@ -1391,17 +1326,6 @@ "@noble/hashes": "^1.1.5" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=14" - } - }, "node_modules/@puppeteer/browsers": { "version": "2.13.0", "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", @@ -1446,13 +1370,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/@rvagg/chai-as-promised": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@rvagg/chai-as-promised/-/chai-as-promised-8.0.2.tgz", - "integrity": "sha512-fsObNkApOWJaGwWgfGMELMrPbgFJlPxzCaQJOVdrDuOZDP8wnxnjiXXo7cgSY6vgk6Re9pcIjoqZXB5d0cm4Ig==", - "dev": true, - "license": "WTFPL" - }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", @@ -1608,6 +1525,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash.merge": { + "version": "4.6.9", + "resolved": "https://registry.npmjs.org/@types/lodash.merge/-/lodash.merge-4.6.9.tgz", + "integrity": "sha512-23sHDPmzd59kUgWyKGiOMO2Qb9YtqRO/x4IhkgNUiPQ1+5MUVqi6bCZeq9nBJ17msjIMbEIO5u+XW4Kz6aGUhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/methods": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", @@ -1615,13 +1549,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/mocha": { - "version": "10.0.10", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", - "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/morgan": { "version": "1.9.10", "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz", @@ -1788,33 +1715,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2015,19 +1915,6 @@ "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", "license": "Apache-2.0" }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/body-parser": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", @@ -2109,26 +1996,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true, - "license": "ISC" - }, "node_modules/buffer-crc32": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", @@ -2230,29 +2097,6 @@ "node": ">=6" } }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -2354,22 +2198,6 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/chromium-bidi": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", @@ -2623,13 +2451,6 @@ "node": ">= 0.8.0" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, - "license": "MIT" - }, "node_modules/content-disposition": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", @@ -2996,19 +2817,6 @@ "ms": "2.0.0" } }, - "node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/degenerator": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", @@ -3057,16 +2865,6 @@ "wrappy": "1" } }, - "node_modules/diff": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/doasync": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/doasync/-/doasync-2.0.1.tgz", @@ -3194,13 +2992,6 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -3489,19 +3280,6 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/escodegen": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", @@ -3816,19 +3594,6 @@ "url": "https://github.com/sindresorhus/file-type?sponsor=1" } }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/finalhandler": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", @@ -3890,16 +3655,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "license": "BSD-3-Clause", - "bin": { - "flat": "cli.js" - } - }, "node_modules/flat-cache": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-5.0.0.tgz", @@ -4172,19 +3927,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/global-directory": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", @@ -4289,16 +4031,6 @@ "node": ">= 0.4" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -4492,13 +4224,6 @@ ], "license": "BSD-3-Clause" }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true, - "license": "ISC" - }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -4593,29 +4318,6 @@ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-fullwidth-code-point": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", @@ -4632,19 +4334,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-node-process": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", @@ -4652,55 +4341,12 @@ "dev": true, "license": "MIT" }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-promise": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4747,22 +4393,6 @@ "node": ">=8" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4898,60 +4528,10 @@ "dev": true, "license": "MIT" }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-symbols/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/log-update": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", - "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", "dev": true, "license": "MIT", "dependencies": { @@ -5144,163 +4724,6 @@ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "license": "MIT" }, - "node_modules/mocha": { - "version": "11.7.6", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.6.tgz", - "integrity": "sha512-nS9xOGbw2I3cjCpxwZAEJ9xK9lmJ08vEkQvLtz4du9ZrF9UrjRpeJGiIgl2Z+Qs++pmB4ecDe48Fwsh+j+j7xA==", - "dev": true, - "license": "MIT", - "dependencies": { - "browser-stdout": "^1.3.1", - "chokidar": "^4.0.1", - "debug": "^4.3.5", - "diff": "^7.0.0", - "escape-string-regexp": "^4.0.0", - "find-up": "^5.0.0", - "glob": "^10.4.5", - "he": "^1.2.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "log-symbols": "^4.1.0", - "minimatch": "^9.0.5", - "ms": "^2.1.3", - "picocolors": "^1.1.1", - "serialize-javascript": "^6.0.2", - "strip-json-comments": "^3.1.1", - "supports-color": "^8.1.1", - "workerpool": "^9.2.0", - "yargs": "^17.7.2", - "yargs-parser": "^21.1.1", - "yargs-unparser": "^2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/mocha/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/mocha/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/mocha/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mocha/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true, - "license": "ISC" - }, - "node_modules/mocha/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mocha/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/mocha/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/morgan": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.11.0.tgz", @@ -5379,175 +4802,6 @@ "npm": ">=7" } }, - "node_modules/nodemon": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.0.3.tgz", - "integrity": "sha512-7jH/NXbFPxVaMwmBCC2B9F/V6X1VkEdNgx3iu9jji8WxWcvhMWkmhNWhI5077zknOnZnBzba9hZP6bCPJLSReQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/nodemon/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/nodemon/node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/nodemon/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/nodemon/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/nodemon/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nodemon/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/nodemon/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nodemon/node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/nodemon/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/nodemon/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -5721,13 +4975,6 @@ "node": ">= 14" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, "node_modules/parent-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-2.0.0.tgz", @@ -6007,13 +5254,6 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true, - "license": "MIT" - }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -6101,16 +5341,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -6151,20 +5381,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -6334,16 +5550,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/serve-static": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", @@ -6477,19 +5683,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/slice-ansi": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", @@ -6792,55 +5985,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-ansi": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", @@ -6857,43 +6001,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/strtok3": { "version": "10.3.5", "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", @@ -7061,19 +6168,6 @@ "node": ">=14.14" } }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, "node_modules/toidentifier": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", @@ -7101,16 +6195,6 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/touch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", - "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", - "dev": true, - "license": "ISC", - "bin": { - "nodetouch": "bin/nodetouch.js" - } - }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -7199,13 +6283,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", - "dev": true, - "license": "MIT" - }, "node_modules/undici": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", @@ -7324,13 +6401,6 @@ "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", "license": "MIT" }, - "node_modules/workerpool": { - "version": "9.3.4", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", - "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/wrap-ansi": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", @@ -7349,89 +6419,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/wrap-ansi/node_modules/emoji-regex": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", @@ -7549,22 +6536,6 @@ "node": ">=12" } }, - "node_modules/yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/yargs/node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", diff --git a/package.json b/package.json index 49dc8b316..7b78c41ad 100644 --- a/package.json +++ b/package.json @@ -36,25 +36,21 @@ "tmp": "0.2.7" }, "devDependencies": { - "@rvagg/chai-as-promised": "8.0.2", "@types/express": "^5.0.6", "@types/express-fileupload": "^1.5.1", - "@types/mocha": "^10.0.10", + "@types/lodash.merge": "^4.6.9", "@types/morgan": "^1.9.10", "@types/node": "^24.10.9", "@types/superagent": "^8.1.9", "@types/tmp": "^0.2.6", "c8": "^11.0.0", - "chai": "6.2.2", "cspell": "9.0.2", "domhandler": "^6.0.1", "expect.js": "0.3", "husky": "9.0.11", "lint-staged": "16.4.0", "lodash.merge": "^4.6.2", - "mocha": "11.7.6", "nock": "15.0.0", - "nodemon": "3.0.3", "prettier": "3.8.4", "tsx": "^4.21.0", "typescript": "^6.0.2" @@ -62,8 +58,8 @@ "scripts": { "build": "tsc", "check": "tsc --noEmit", - "coverage": "tsc && c8 npm test", - "coverage:text": "tsc && c8 --reporter=text npm test", + "coverage": "NO_THROTTLE=true c8 tsx --test --test-timeout=120000 'test/*.ts'", + "coverage:text": "npm run coverage && c8 report --reporter=text", "cspell": "cspell \"**/*\"", "fix": "prettier -w .", "lint": "npm run check && prettier -c .", @@ -72,11 +68,11 @@ "prepare": "husky install", "spelling": "cspell \"**/*\"", "start": "node app", - "testserver": "nodemon test/lib/testserver.js", - "test": "NO_THROTTLE=true mocha" + "testserver": "tsx watch test/lib/testserver.ts", + "test": "NO_THROTTLE=true tsx --test --test-isolation=none 'test/*.ts'" }, "engines": { - "node": "20 || 22 || 24", + "node": "22 || 24", "npm": ">=7" }, "lint-staged": { @@ -84,10 +80,5 @@ "cspell --no-must-find-files", "prettier --write --ignore-unknown" ] - }, - "mocha": { - "colors": true, - "reporter": "spec", - "timeout": 40000 } } diff --git a/test/api.js b/test/api.js deleted file mode 100644 index 2340ef91a..000000000 --- a/test/api.js +++ /dev/null @@ -1,161 +0,0 @@ -/** - * Test the REST API. - */ - -import http from 'http'; -import { join } from 'path'; - -import chaiAsPromised from '@rvagg/chai-as-promised'; -import * as chai from 'chai'; -import express from 'express'; -import fileUpload from 'express-fileupload'; -import superagent from 'superagent'; - -import { setUp } from '../lib/api.js'; -import { specberusVersion } from '../lib/util.js'; -import { cleanupMocks, setupMocks } from './lib/utils.js'; - -const { expect } = chai; - -// Settings: -const DEFAULT_PORT = 8000; -const PORT = process.env.PORT || DEFAULT_PORT; -const ENDPOINT = `http://localhost:${PORT}/api/`; -const timeouts = { response: 30000 }; -const testDocsPath = join('test', 'docs', 'api'); - -let server; - -/** - * Sets up Chai, mocks, and the HTTP server for tests. - */ -function setup() { - setupMocks(); - chai.use(chaiAsPromised); - - const app = express(); - // fileUpload is _not_ covered by app.js setUp, so need to repeat it here - app.use( - fileUpload({ - createParentPath: true, - useTempFiles: true, - tempFileDir: '/tmp/', - }) - ); - server = http.createServer(app); - setUp(app); - server.listen(PORT).on('error', err => { - throw new Error(err); - }); -} - -const handleResponse = response => response.res?.text || response.body; -const handleError = error => { - throw new Error( - error.response?.error?.text || - `Fetching “${ENDPOINT}${suffix}” triggered a network error: ${error.message}` - ); -}; - -/** Performs a GET request. */ -const get = suffix => - superagent - .get(ENDPOINT + suffix) - .timeout(timeouts) - .then(handleResponse, handleError); - -/** Creates a POST request, returning the superagent object for parameter chaining. */ -const createPostRequest = suffix => - superagent.post(ENDPOINT + suffix).timeout(timeouts); - -describe('API', () => { - before(setup); - - after(() => { - cleanupMocks(); - server.close(); - }); - - describe('Endpoint', () => { - it('Should exist and listen to GET requests', () => { - const query = get(''); - return expect(query).to.eventually.be.rejectedWith( - /wrong api endpoint/i - ); - }); - it('Should exist and listen to POST requests', () => { - const query = createPostRequest('').then( - handleResponse, - handleError - ); - return expect(query).to.eventually.be.rejectedWith( - /wrong api endpoint/i - ); - }); - }); - - describe('Method “version”', () => { - it('Should return the right version string', () => { - const query = get('version'); - return expect(query).to.eventually.become(specberusVersion); - }); - }); - - describe('Method “metadata”', () => { - it('Should accept "file" via POST, and return the right profile and date', () => { - const query = createPostRequest('metadata') - .attach('file', join(testDocsPath, 'ttml-imsc1.html')) - .then(handleResponse, handleError); - // @TODO: parse result as an Object (it's JSON) instead of a String. - return expect(query) - .to.eventually.match(/"profile":\s*"rec"/i) - .and.to.eventually.match(/"docDate":\s*"2016-3-8"/i); - }); - }); - - describe('Method “validate”', () => { - it('Should 400 and return an array of errors when validation fails', () => { - const query = createPostRequest('validate') - .field('profile', 'REC') - .attach('file', join(testDocsPath, 'ttml-imsc1.html')) - .then(handleResponse, handleError); - // @TODO: parse result as an Object (it's JSON) instead of a String. - return expect(query).to.eventually.be.rejectedWith( - /"errors":\[\{"name":"headers\.\w+/ - ); - }); - it('Should accept "file" via POST, and succeed when the document is valid', function () { - const query = createPostRequest('validate') - .field('profile', 'WD') - .attach('file', join(testDocsPath, 'wd-good.html')) - .then(handleResponse, handleError); - // @TODO: parse result as an Object (it's JSON) instead of a String. - return expect(query).to.eventually.match(/"success":\s*true/); - }); - it('Special profile “auto”: should detect the right profile and validate the document', function () { - const query = createPostRequest('validate') - .field('profile', 'auto') - .attach('file', join(testDocsPath, 'wd-good.html')) - .then(handleResponse, handleError); - // @TODO: parse result as an Object (it's JSON) instead of a String. - return expect(query) - .to.eventually.match(/"success":\s*true/) - .and.to.eventually.match(/"profile":\s*"WD"/); - }); - }); - - describe('Parameter restrictions', () => { - it('Should reject the parameter "document" as unknown', () => { - const query = get('metadata?document=foo'); - return expect(query).to.eventually.be.rejectedWith( - 'Error: Illegal parameter “document”' - ); - }); - it('Should reject the parameter "source" as forbidden', () => { - const query = get('metadata?source=foo'); - return expect(query).to.eventually.be.rejectedWith( - 'Parameter “source” is not allowed in this context' - ); - }); - }); -}); diff --git a/test/api.ts b/test/api.ts new file mode 100644 index 000000000..965dae0c3 --- /dev/null +++ b/test/api.ts @@ -0,0 +1,182 @@ +/** + * Test the REST API. + */ + +import assert from 'assert'; +import http, { type Server } from 'http'; +import { join } from 'path'; +import { after, before, describe, it } from 'node:test'; + +import express from 'express'; +import fileUpload from 'express-fileupload'; +import superagent, { type Response, type ResponseError } from 'superagent'; + +import { setUp } from '../lib/api.js'; +import { specberusVersion } from '../lib/util.js'; +import { cleanupMocks, setupMocks } from './lib/utils.js'; + +// Settings: +const DEFAULT_PORT = 8000; +const PORT = process.env.PORT || DEFAULT_PORT; +const ENDPOINT = `http://localhost:${PORT}/api/`; +const timeouts = { response: 30000 }; +const testDocsPath = join('test', 'docs', 'api'); + +let server: Server; + +/** + * Sets up mocks and the HTTP server for tests. + */ +function setup() { + setupMocks(); + + const app = express(); + // fileUpload is _not_ covered by app.js setUp, so need to repeat it here + app.use( + fileUpload({ + createParentPath: true, + useTempFiles: true, + tempFileDir: '/tmp/', + }) + ); + server = http.createServer(app); + setUp(app); + server.listen(PORT).on('error', err => { + throw err; + }); +} + +const handleResponse = (response: Response) => response.text || response.body; +const handleJsonResponse = (response: Response) => + JSON.parse(handleResponse(response)); +function getErrorResponseText(error: ResponseError) { + const text = error.response?.text; + assert(text, 'Response data not available on error'); + return text; +} + +/** Performs a GET request. */ +const get = (suffix: string) => + superagent + .get(ENDPOINT + suffix) + .timeout(timeouts) + .then(handleResponse); + +/** Creates a POST request, returning the superagent object for parameter chaining. */ +const createPostRequest = (suffix: string) => + superagent.post(ENDPOINT + suffix).timeout(timeouts); + +describe('API', () => { + before(setup); + + after(() => { + cleanupMocks(); + server.close(); + }); + + describe('Endpoint', () => { + it('Should exist and listen to GET requests', () => + assert.rejects(get(''), (error: any) => { + const text = getErrorResponseText(error); + assert.strictEqual(text, 'Wrong API endpoint.'); + return true; + })); + it('Should exist and listen to POST requests', () => + assert.rejects(createPostRequest(''), (error: any) => { + const text = getErrorResponseText(error); + assert.strictEqual(text, 'Wrong API endpoint.'); + return true; + })); + }); + + describe('Method “version”', () => { + it('Should return the right version string', async () => { + assert.strictEqual(await get('version'), specberusVersion); + }); + }); + + describe('Method “metadata”', () => { + it('Should accept "file" via POST, and return the right profile and date', () => + createPostRequest('metadata') + .attach('file', join(testDocsPath, 'ttml-imsc1.html')) + .then(handleJsonResponse) + .then(({ metadata }) => { + assert.strictEqual(metadata.profile, 'REC'); + assert.strictEqual(metadata.docDate, '2016-3-8'); + })); + }); + + describe('Method “validate”', () => { + it('Should 400 and return an array of errors when validation fails', () => + assert.rejects( + createPostRequest('validate') + .field('profile', 'REC') + .attach('file', join(testDocsPath, 'ttml-imsc1.html')), + (error: any) => { + const { success, errors } = JSON.parse( + getErrorResponseText(error) + ); + assert.strictEqual(success, false); + assert(errors.length > 0, 'Response should report errors'); + for (const obj of errors) { + assert( + obj.name && + obj.rule && + obj.section && + obj.key && + obj.detailMessage, + 'Every error should consistently define fields' + ); + } + return true; + } + )); + + it('Should accept "file" via POST, and succeed when the document is valid', () => + createPostRequest('validate') + .field('profile', 'WD') + .attach('file', join(testDocsPath, 'wd-good.html')) + .then(handleJsonResponse) + .then(({ success }) => { + assert.strictEqual(success, true); + })); + + it('Special profile “auto”: should detect the right profile and validate the document', () => + createPostRequest('validate') + .field('profile', 'auto') + .attach('file', join(testDocsPath, 'wd-good.html')) + .then(handleJsonResponse) + .then(({ success, metadata }) => { + assert.strictEqual(success, true); + assert.strictEqual(metadata.profile, 'WD'); + })); + }); + + describe('Parameter restrictions', () => { + it('Should reject the parameter "document" as unknown', () => + assert.rejects(get('metadata?document=foo'), (error: any) => { + const { success, errors } = JSON.parse( + getErrorResponseText(error) + ); + assert.strictEqual(success, false); + assert.strictEqual( + errors[0], + 'Error: Illegal parameter “document”' + ); + return true; + })); + + it('Should reject the parameter "source" as forbidden', () => + assert.rejects(get('metadata?source=foo'), (error: any) => { + const { success, errors } = JSON.parse( + getErrorResponseText(error) + ); + assert.strictEqual(success, false); + assert.strictEqual( + errors[0], + 'Error: Parameter “source” is not allowed in this context' + ); + return true; + })); + }); +}); diff --git a/test/data/SUBM/MEM-SUBM.js b/test/data/SUBM/MEM-SUBM.ts similarity index 100% rename from test/data/SUBM/MEM-SUBM.js rename to test/data/SUBM/MEM-SUBM.ts diff --git a/test/data/SUBM/SUBMBase.js b/test/data/SUBM/SUBMBase.ts similarity index 100% rename from test/data/SUBM/SUBMBase.js rename to test/data/SUBM/SUBMBase.ts diff --git a/test/data/TR/Note/DNOTE-Echidna.js b/test/data/TR/Note/DNOTE-Echidna.ts similarity index 100% rename from test/data/TR/Note/DNOTE-Echidna.js rename to test/data/TR/Note/DNOTE-Echidna.ts diff --git a/test/data/TR/Note/DNOTE.js b/test/data/TR/Note/DNOTE.ts similarity index 100% rename from test/data/TR/Note/DNOTE.js rename to test/data/TR/Note/DNOTE.ts diff --git a/test/data/TR/Note/NOTE-Echidna.js b/test/data/TR/Note/NOTE-Echidna.ts similarity index 100% rename from test/data/TR/Note/NOTE-Echidna.js rename to test/data/TR/Note/NOTE-Echidna.ts diff --git a/test/data/TR/Note/NOTE.js b/test/data/TR/Note/NOTE.ts similarity index 100% rename from test/data/TR/Note/NOTE.js rename to test/data/TR/Note/NOTE.ts diff --git a/test/data/TR/Note/STMT.js b/test/data/TR/Note/STMT.ts similarity index 100% rename from test/data/TR/Note/STMT.js rename to test/data/TR/Note/STMT.ts diff --git a/test/data/TR/Note/noteBase.js b/test/data/TR/Note/noteBase.ts similarity index 100% rename from test/data/TR/Note/noteBase.js rename to test/data/TR/Note/noteBase.ts diff --git a/test/data/TR/Recommendation/CR-Echidna.js b/test/data/TR/Recommendation/CR-Echidna.ts similarity index 100% rename from test/data/TR/Recommendation/CR-Echidna.js rename to test/data/TR/Recommendation/CR-Echidna.ts diff --git a/test/data/TR/Recommendation/CR.js b/test/data/TR/Recommendation/CR.ts similarity index 100% rename from test/data/TR/Recommendation/CR.js rename to test/data/TR/Recommendation/CR.ts diff --git a/test/data/TR/Recommendation/CRD-Echidna.js b/test/data/TR/Recommendation/CRD-Echidna.ts similarity index 100% rename from test/data/TR/Recommendation/CRD-Echidna.js rename to test/data/TR/Recommendation/CRD-Echidna.ts diff --git a/test/data/TR/Recommendation/CRD.js b/test/data/TR/Recommendation/CRD.ts similarity index 100% rename from test/data/TR/Recommendation/CRD.js rename to test/data/TR/Recommendation/CRD.ts diff --git a/test/data/TR/Recommendation/DISC.js b/test/data/TR/Recommendation/DISC.ts similarity index 100% rename from test/data/TR/Recommendation/DISC.js rename to test/data/TR/Recommendation/DISC.ts diff --git a/test/data/TR/Recommendation/FPWD.js b/test/data/TR/Recommendation/FPWD.ts similarity index 100% rename from test/data/TR/Recommendation/FPWD.js rename to test/data/TR/Recommendation/FPWD.ts diff --git a/test/data/TR/Recommendation/REC-RSCND.js b/test/data/TR/Recommendation/REC-RSCND.ts similarity index 100% rename from test/data/TR/Recommendation/REC-RSCND.js rename to test/data/TR/Recommendation/REC-RSCND.ts diff --git a/test/data/TR/Recommendation/REC.js b/test/data/TR/Recommendation/REC.ts similarity index 100% rename from test/data/TR/Recommendation/REC.js rename to test/data/TR/Recommendation/REC.ts diff --git a/test/data/TR/Recommendation/WD-Echidna.js b/test/data/TR/Recommendation/WD-Echidna.ts similarity index 100% rename from test/data/TR/Recommendation/WD-Echidna.js rename to test/data/TR/Recommendation/WD-Echidna.ts diff --git a/test/data/TR/Recommendation/WD.js b/test/data/TR/Recommendation/WD.ts similarity index 100% rename from test/data/TR/Recommendation/WD.js rename to test/data/TR/Recommendation/WD.ts diff --git a/test/data/TR/Recommendation/recommendationBase.js b/test/data/TR/Recommendation/recommendationBase.ts similarity index 100% rename from test/data/TR/Recommendation/recommendationBase.js rename to test/data/TR/Recommendation/recommendationBase.ts diff --git a/test/data/TR/Registry/CRY.js b/test/data/TR/Registry/CRY.ts similarity index 100% rename from test/data/TR/Registry/CRY.js rename to test/data/TR/Registry/CRY.ts diff --git a/test/data/TR/Registry/CRYD.js b/test/data/TR/Registry/CRYD.ts similarity index 100% rename from test/data/TR/Registry/CRYD.js rename to test/data/TR/Registry/CRYD.ts diff --git a/test/data/TR/Registry/DRY.js b/test/data/TR/Registry/DRY.ts similarity index 100% rename from test/data/TR/Registry/DRY.js rename to test/data/TR/Registry/DRY.ts diff --git a/test/data/TR/Registry/RY.js b/test/data/TR/Registry/RY.ts similarity index 100% rename from test/data/TR/Registry/RY.js rename to test/data/TR/Registry/RY.ts diff --git a/test/data/TR/Registry/registryBase.js b/test/data/TR/Registry/registryBase.ts similarity index 100% rename from test/data/TR/Registry/registryBase.js rename to test/data/TR/Registry/registryBase.ts diff --git a/test/data/TR/TRBase.js b/test/data/TR/TRBase.ts similarity index 100% rename from test/data/TR/TRBase.js rename to test/data/TR/TRBase.ts diff --git a/test/data/goodDocuments.js b/test/data/goodDocuments.ts similarity index 96% rename from test/data/goodDocuments.js rename to test/data/goodDocuments.ts index 3a0b911c8..72d544cf8 100644 --- a/test/data/goodDocuments.js +++ b/test/data/goodDocuments.ts @@ -62,7 +62,7 @@ export const goodDocuments = { }, 'CRYD-2': { profile: 'CRYD', - url: 'doc-views/TR/Recommendation/CRYD?type=good2', + url: 'doc-views/TR/Registry/CRYD?type=good2', }, DRY: { url: 'doc-views/TR/Registry/DRY?type=good', diff --git a/test/data/specBase.js b/test/data/specBase.ts similarity index 100% rename from test/data/specBase.js rename to test/data/specBase.ts diff --git a/test/doc-views/SUBM/MEM-SUBM.js b/test/doc-views/SUBM/MEM-SUBM.ts similarity index 99% rename from test/doc-views/SUBM/MEM-SUBM.js rename to test/doc-views/SUBM/MEM-SUBM.ts index b1255cf9d..74724aa71 100644 --- a/test/doc-views/SUBM/MEM-SUBM.js +++ b/test/doc-views/SUBM/MEM-SUBM.ts @@ -22,10 +22,9 @@ const good = { }, config: { ...config, - ...data.config, profile: 'Member Submission', status: 'SUBM', - }, + } as const, }; export default { diff --git a/test/doc-views/TR/Note/DNOTE-Echidna.js b/test/doc-views/TR/Note/DNOTE-Echidna.ts similarity index 100% rename from test/doc-views/TR/Note/DNOTE-Echidna.js rename to test/doc-views/TR/Note/DNOTE-Echidna.ts diff --git a/test/doc-views/TR/Note/DNOTE.js b/test/doc-views/TR/Note/DNOTE.ts similarity index 100% rename from test/doc-views/TR/Note/DNOTE.js rename to test/doc-views/TR/Note/DNOTE.ts diff --git a/test/doc-views/TR/Note/NOTE-Echidna.js b/test/doc-views/TR/Note/NOTE-Echidna.ts similarity index 100% rename from test/doc-views/TR/Note/NOTE-Echidna.js rename to test/doc-views/TR/Note/NOTE-Echidna.ts diff --git a/test/doc-views/TR/Note/NOTE.js b/test/doc-views/TR/Note/NOTE.ts similarity index 100% rename from test/doc-views/TR/Note/NOTE.js rename to test/doc-views/TR/Note/NOTE.ts diff --git a/test/doc-views/TR/Note/STMT.js b/test/doc-views/TR/Note/STMT.ts similarity index 100% rename from test/doc-views/TR/Note/STMT.js rename to test/doc-views/TR/Note/STMT.ts diff --git a/test/doc-views/TR/Note/noteBase.js b/test/doc-views/TR/Note/noteBase.ts similarity index 96% rename from test/doc-views/TR/Note/noteBase.js rename to test/doc-views/TR/Note/noteBase.ts index f37cedde0..96627f795 100644 --- a/test/doc-views/TR/Note/noteBase.js +++ b/test/doc-views/TR/Note/noteBase.ts @@ -1,8 +1,9 @@ +import type { BaseCommonViewData } from '../../specBase.js'; import * as TRBase from '../TRBase.js'; const { buildCommonViewData: _buildCommonViewData, data, ...rest } = TRBase; -const buildCommonViewData = base => { +const buildCommonViewData = (base: BaseCommonViewData) => { const common = _buildCommonViewData(base); return { ...common, @@ -91,7 +92,6 @@ export default { data: { ...data, config: { - ...data.config, underPP: false, isNoteTrack: true, }, diff --git a/test/doc-views/TR/Recommendation/CR-Echidna.js b/test/doc-views/TR/Recommendation/CR-Echidna.ts similarity index 100% rename from test/doc-views/TR/Recommendation/CR-Echidna.js rename to test/doc-views/TR/Recommendation/CR-Echidna.ts diff --git a/test/doc-views/TR/Recommendation/CR.js b/test/doc-views/TR/Recommendation/CR.ts similarity index 100% rename from test/doc-views/TR/Recommendation/CR.js rename to test/doc-views/TR/Recommendation/CR.ts diff --git a/test/doc-views/TR/Recommendation/CRD-Echidna.js b/test/doc-views/TR/Recommendation/CRD-Echidna.ts similarity index 100% rename from test/doc-views/TR/Recommendation/CRD-Echidna.js rename to test/doc-views/TR/Recommendation/CRD-Echidna.ts diff --git a/test/doc-views/TR/Recommendation/CRD.js b/test/doc-views/TR/Recommendation/CRD.ts similarity index 93% rename from test/doc-views/TR/Recommendation/CRD.js rename to test/doc-views/TR/Recommendation/CRD.ts index 50f8d4b23..4db438409 100644 --- a/test/doc-views/TR/Recommendation/CRD.js +++ b/test/doc-views/TR/Recommendation/CRD.ts @@ -22,13 +22,14 @@ const good = { ...data, ...customData }; // Used in http://localhost:8001/doc-views/TR/Recommendation/CRD?type=good2 const good2 = { + ...good, config: { ...good.config, }, sotd: { ...good.sotd, draftText: - 'This document is maintained and updated at any time. Some parts of this document are a work in progress.', + 'This document is maintained and updated at any time. Some parts of this document are work in progress.', }, }; diff --git a/test/doc-views/TR/Recommendation/DISC.js b/test/doc-views/TR/Recommendation/DISC.ts similarity index 100% rename from test/doc-views/TR/Recommendation/DISC.js rename to test/doc-views/TR/Recommendation/DISC.ts diff --git a/test/doc-views/TR/Recommendation/FPWD.js b/test/doc-views/TR/Recommendation/FPWD.ts similarity index 100% rename from test/doc-views/TR/Recommendation/FPWD.js rename to test/doc-views/TR/Recommendation/FPWD.ts diff --git a/test/doc-views/TR/Recommendation/REC-RSCND.js b/test/doc-views/TR/Recommendation/REC-RSCND.ts similarity index 100% rename from test/doc-views/TR/Recommendation/REC-RSCND.js rename to test/doc-views/TR/Recommendation/REC-RSCND.ts diff --git a/test/doc-views/TR/Recommendation/REC.js b/test/doc-views/TR/Recommendation/REC.ts similarity index 100% rename from test/doc-views/TR/Recommendation/REC.js rename to test/doc-views/TR/Recommendation/REC.ts diff --git a/test/doc-views/TR/Recommendation/WD-Echidna.js b/test/doc-views/TR/Recommendation/WD-Echidna.ts similarity index 100% rename from test/doc-views/TR/Recommendation/WD-Echidna.js rename to test/doc-views/TR/Recommendation/WD-Echidna.ts diff --git a/test/doc-views/TR/Recommendation/WD.js b/test/doc-views/TR/Recommendation/WD.ts similarity index 100% rename from test/doc-views/TR/Recommendation/WD.js rename to test/doc-views/TR/Recommendation/WD.ts diff --git a/test/doc-views/TR/Recommendation/recommendationBase.js b/test/doc-views/TR/Recommendation/recommendationBase.ts similarity index 84% rename from test/doc-views/TR/Recommendation/recommendationBase.js rename to test/doc-views/TR/Recommendation/recommendationBase.ts index 88cdb26eb..a5fb66fd5 100644 --- a/test/doc-views/TR/Recommendation/recommendationBase.js +++ b/test/doc-views/TR/Recommendation/recommendationBase.ts @@ -1,8 +1,9 @@ +import type { BaseCommonViewData } from '../../specBase.js'; import * as TRBase from '../TRBase.js'; const { data, ...rest } = TRBase; -const buildSecurityPrivacy = base => ({ +const buildSecurityPrivacy = (base: BaseCommonViewData) => ({ noSecurityPrivacy: { ...base, }, @@ -22,7 +23,7 @@ const buildSecurityPrivacy = base => ({ }, }); -const buildRecStability = base => ({ +const buildRecStability = (base: BaseCommonViewData) => ({ noRECReview: { ...base, config: { diff --git a/test/doc-views/TR/Registry/CRY.js b/test/doc-views/TR/Registry/CRY.ts similarity index 100% rename from test/doc-views/TR/Registry/CRY.js rename to test/doc-views/TR/Registry/CRY.ts diff --git a/test/doc-views/TR/Registry/CRYD.js b/test/doc-views/TR/Registry/CRYD.ts similarity index 90% rename from test/doc-views/TR/Registry/CRYD.js rename to test/doc-views/TR/Registry/CRYD.ts index 867ed9c3c..9a768a468 100644 --- a/test/doc-views/TR/Registry/CRYD.js +++ b/test/doc-views/TR/Registry/CRYD.ts @@ -20,14 +20,15 @@ const customData = { const good = { ...data, ...customData }; // Used in http://localhost:8001/doc-views/TR/Recommendation/CRYD?type=good2 -export const good2 = { +const good2 = { + ...good, config: { ...good.config, }, sotd: { ...good.sotd, draftText: - 'This document is maintained and updated at any time. Some parts of this document are a work in progress.', + 'This document is maintained and updated at any time. Some parts of this document are work in progress.', }, }; @@ -35,6 +36,7 @@ const common = buildCommonViewData(good); export default { good, + good2, ...common, 'draft-stability': buildDraftStability(good), }; diff --git a/test/doc-views/TR/Registry/DRY.js b/test/doc-views/TR/Registry/DRY.ts similarity index 100% rename from test/doc-views/TR/Registry/DRY.js rename to test/doc-views/TR/Registry/DRY.ts diff --git a/test/doc-views/TR/Registry/RY.js b/test/doc-views/TR/Registry/RY.ts similarity index 100% rename from test/doc-views/TR/Registry/RY.js rename to test/doc-views/TR/Registry/RY.ts diff --git a/test/doc-views/TR/Registry/registryBase.js b/test/doc-views/TR/Registry/registryBase.ts similarity index 90% rename from test/doc-views/TR/Registry/registryBase.js rename to test/doc-views/TR/Registry/registryBase.ts index c81fac165..7ad8441f2 100644 --- a/test/doc-views/TR/Registry/registryBase.js +++ b/test/doc-views/TR/Registry/registryBase.ts @@ -1,7 +1,8 @@ +import type { BaseCommonViewData } from '../../specBase.js'; import * as TRBase from '../TRBase.js'; const { buildCommonViewData: _buildCommonViewData, data, ...rest } = TRBase; -const buildCommonViewData = base => { +const buildCommonViewData = (base: BaseCommonViewData) => { const common = _buildCommonViewData(base); return { ...common, diff --git a/test/doc-views/TR/TRBase.js b/test/doc-views/TR/TRBase.ts similarity index 94% rename from test/doc-views/TR/TRBase.js rename to test/doc-views/TR/TRBase.ts index ee3362eeb..776cce2de 100644 --- a/test/doc-views/TR/TRBase.js +++ b/test/doc-views/TR/TRBase.ts @@ -1,13 +1,14 @@ import { buildCommonViewData as _buildCommonViewData, data, + type BaseCommonViewData, } from '../specBase.js'; export { data }; const currentYear = new Date().getFullYear(); -export function buildCommonViewData(base) { +export function buildCommonViewData(base: BaseCommonViewData) { const common = _buildCommonViewData(base); return { ...common, @@ -213,7 +214,7 @@ export function buildCommonViewData(base) { }; } -export function buildCandidateReviewEnd(base) { +export function buildCandidateReviewEnd(base: BaseCommonViewData) { return { noDateFound: { ...base, @@ -239,7 +240,7 @@ export function buildCandidateReviewEnd(base) { }; } -export function buildTodaysDate(base) { +export function buildTodaysDate(base: BaseCommonViewData) { return { noDateDetected: { ...base, @@ -266,7 +267,7 @@ export function buildTodaysDate(base) { }; } -export function buildDelivererChange(base) { +export function buildDelivererChange(base: BaseCommonViewData) { return { delivererChanged: { ...base, @@ -282,7 +283,7 @@ export function buildDelivererChange(base) { }; } -export function buildDraftStability(base) { +export function buildDraftStability(base: BaseCommonViewData) { return { noDraftEither: { ...base, @@ -303,7 +304,7 @@ export function buildDraftStability(base) { }; } -export function buildNewFeatures(base) { +export function buildNewFeatures(base: BaseCommonViewData) { return { noWarning: { ...base, @@ -314,11 +315,7 @@ export function buildNewFeatures(base) { ...base.sotd, newFeatures: { show: true, - text: `Future updates to this ${ - base.config.status === 'PR' - ? 'specification' - : 'Recommendation' - } may incorporate new features.`, + text: `Future updates to this Recommendation may incorporate new features.`, }, }, }, diff --git a/test/doc-views/specBase.js b/test/doc-views/specBase.ts similarity index 98% rename from test/doc-views/specBase.js rename to test/doc-views/specBase.ts index 6137528e2..e91c45a38 100644 --- a/test/doc-views/specBase.js +++ b/test/doc-views/specBase.ts @@ -1,3 +1,5 @@ +import type { SpecberusConfig } from '../../lib/types.js'; + const currentYear = new Date().getFullYear(); export const data = { @@ -233,7 +235,7 @@ export const data = { }, sixMonthLater() { const later = new Date( - new Date() - 0 + 6 * 30 * 24 * 60 * 60 * 1000 + Date.now() - 0 + 6 * 30 * 24 * 60 * 60 * 1000 ); return `${later.getDate()} ${later.toLocaleDateString('en-US', { month: 'long', @@ -242,7 +244,11 @@ export const data = { }, }; -export function buildCommonViewData(base) { +export type BaseCommonViewData = typeof data & { + config: SpecberusConfig; +}; + +export function buildCommonViewData(base: BaseCommonViewData) { return { 'div-head': { noHead: { @@ -286,7 +292,7 @@ export function buildCommonViewData(base) { header: { ...base.header, logo: { - ...base.logo, + ...base.header.logo, src: 'http://invalid/source', }, }, @@ -296,7 +302,7 @@ export function buildCommonViewData(base) { header: { ...base.header, logo: { - ...base.logo, + ...base.header.logo, href: 'http://invalid/href', }, }, @@ -409,7 +415,7 @@ export function buildCommonViewData(base) { dl: { ...base.dl, latestVersion: { - ...base.latestVersion, + ...base.dl.latestVersion, text: 'wrong latest version key', }, }, diff --git a/test/l10n.js b/test/l10n.js deleted file mode 100644 index ac7cfd01e..000000000 --- a/test/l10n.js +++ /dev/null @@ -1,165 +0,0 @@ -/** - * Test L10n features. - */ - -import { readdir, readFile } from 'fs/promises'; -import { join } from 'path'; - -import * as chai from 'chai'; -import * as l10n from '../lib/l10n-en_GB.js'; - -import rules from '../lib/rules-track.js'; - -const { expect } = chai; - -// Constants: -const { messages } = l10n; -const baseDir = join('lib', 'rules'); -const extensionRemover = /\.[^.]+$/; -const messageFinder = - /\.(info|warning|error)\s*\([\s\S]+?,\s*["']([^"']+)["']/g; -const exceptionFinder = /emits\s*:\s*["']([^()"'{}]+)["']/g; - -/** - * Process “messages” and build a tree with up to 3 levels (section, rule, message ID). - * - * Leaf values “true” indicate that the message exists; “false” is used rarely to mean - * that one rule (or the whole section) is missing on purpose, to avoid false positives from the tests. - */ - -const scanStrings = function () { - const result = {}; - for (const i in messages) { - const c = i.split('.'); - if (!c || c.length < 1 || c.length > 3) - throw new Error( - `message key “${i}” doesn't follow the pattern “x[.y[.z]]”` - ); - if (c[0] !== 'generic') { - // 1. Process the section: - if (!Object.hasOwn(result, c[0])) { - if (c.length === 1) { - if (messages[i] === false) result[c[0]] = false; - else - throw new Error( - `key “${i}” can be used only to indicate an empty category using “false”` - ); - } else result[c[0]] = {}; - } else if ( - c.length === 1 && - ((result[c[0]] === false && messages[i] !== false) || - (result[c[0]] !== false && messages[i] === false)) - ) - throw new Error( - `key “${i}” can't be used to indicate an empty category because it's used for messages too` - ); - - // 2. Process the rule: - if (c.length > 1) { - if (!Object.hasOwn(result[c[0]], c[1])) { - if (c.length === 2) { - if (messages[i] === false) result[c[0]][c[1]] = false; - else - throw new Error( - `key “${i}” can be used only to indicate an empty category using “false”` - ); - } else result[c[0]][c[1]] = {}; - } else if ( - c.length === 2 && - ((result[c[0]][c[1]] === false && messages[i] !== false) || - (result[c[0]][c[1]] !== false && messages[i] === false)) - ) - throw new Error( - `key “${i}” can't be used to indicate an empty category because it's used for messages too` - ); - } - - // 3. Process the message ID: - if (c.length > 2) { - if (!Object.hasOwn(result[c[0]][c[1]], c[2])) - result[c[0]][c[1]][c[2]] = !!messages[i]; - else throw new Error(`key “${i}” is defined more than once`); - } - } - } - return result; -}; - -/** - * Scan “baseDir” and find heuristically all sections, rules, and message IDs. - * - * The search relies on a regex that tries to find instances of “sr.error()” etc, so it's not exact. - * Return a promise that will be fulfilled if/when all directories and files are read successfully. - */ - -async function scanFileSystem() { - const result = {}; - - const dirnames = await readdir(baseDir); - for (const dirname of dirnames) { - result[dirname] = {}; - - const filenames = await readdir(join(baseDir, dirname)); - for (const filename of filenames) { - if (!filename.endsWith('.ts')) continue; - if (filename.endsWith('.d.ts')) continue; - - const content = await readFile(join(baseDir, dirname, filename)); - const name = filename.replace(extensionRemover, ''); - result[dirname][name] = {}; - - let match; - while ((match = messageFinder.exec(content))) - result[dirname][name][match[2]] = true; - while ((match = exceptionFinder.exec(content))) - result[dirname][name][match[1]] = true; - } - } - - return result; -} - -/** - * Compare two trees of {sections, rules, message IDs} to find leaves that are missing. - */ - -const findHoles = function (source, expected, labelSource, labelExpected) { - let errors = ''; - for (const i in expected) - if (!Object.hasOwn(source, i)) - errors += `Section “${i}” exists in ${labelExpected} but is missing in ${labelSource}.\n`; - else if (source[i] !== false) - for (const j in expected[i]) - if (!Object.hasOwn(source[i], j)) - errors += `Rule “${i}/${j}” exists in ${labelExpected} but is missing in ${labelSource}.\n`; - else if (source[i][j] !== false) - for (const k in expected[i][j]) - if (!Object.hasOwn(source[i][j], k)) - errors += `Message ID “${i}/${j}/${k}” exists in ${labelExpected} but is missing in ${labelSource}.\n`; - if (errors) throw new Error(`${errors.slice(0, -2)}.`); -}; - -describe('L10n', () => { - let strings; - let files; - - before(async () => { - strings = scanStrings(); - files = await scanFileSystem(); - }); - - describe('UI messages module', () => { - it('“lib/rules-track" should be a valid object', () => - expect(rules).to.be.an('object')); - it('“lib/l10n-en_GB” should be a valid object', () => { - expect(typeof l10n).to.equal('object'); - }); - }); - - describe('Consistency between rules and L10n messages', () => { - it('All L10n messages should be used by some rule', () => - findHoles(files, strings, 'files', 'l10n strings')); - it('All message IDs used by rules should exist as L10n messages', () => - findHoles(strings, files, 'l10n strings', 'files')); - }); -}); diff --git a/test/l10n.ts b/test/l10n.ts new file mode 100644 index 000000000..3fcdaeb59 --- /dev/null +++ b/test/l10n.ts @@ -0,0 +1,202 @@ +/** + * @file Tests L10n features. + */ + +import assert from 'assert'; +import { readdir, readFile } from 'fs/promises'; +import { join } from 'path'; +import { describe, it } from 'node:test'; + +import { messages } from '../lib/l10n-en_GB.js'; + +import rules from '../lib/rules-track.js'; + +// Constants: +const baseDir = join('lib', 'rules'); +const extensionRemover = /\.[^.]+$/; +const messageFinder = + /\.(info|warning|error)\s*\([\s\S]+?,\s*["']([^"']+)["']/g; +const exceptionFinder = /emits\s*:\s*["']([^()"'{}]+)["']/g; + +/** + * Processes “messages” and builds a tree with up to 3 levels (section, rule, message ID). + * + * Leaf values “true” indicate that the message exists; “false” is used rarely to mean + * that one rule (or the whole section) is missing on purpose, to avoid false positives from the tests. + */ +const scanStrings = function () { + const result: Record< + string, + false | Record> + > = {}; + for (const [key, message] of Object.entries(messages)) { + const keyParts = key.split('.'); + if (keyParts.length < 1 || keyParts.length > 3) + throw new Error( + `message key “${key}” doesn't follow the pattern “x[.y[.z]]”` + ); + if (keyParts[0] !== 'generic') { + // 1. Process the section: + if (!Object.hasOwn(result, keyParts[0])) { + if (keyParts.length === 1) { + if (message === false) result[keyParts[0]] = false; + else + throw new Error( + `key “${key}” can be used only to indicate an empty category using “false”` + ); + } else result[keyParts[0]] = {}; + } else if ( + keyParts.length === 1 && + ((result[keyParts[0]] === false && message !== false) || + (result[keyParts[0]] !== false && message === false)) + ) + throw new Error( + `key “${key}” can't be used to indicate an empty category because it's used for messages too` + ); + + // 2. Process the rule: + if (keyParts.length > 1) { + const section = result[keyParts[0]]; + if (!section) + throw new Error( + 'key contains 2 or more parts, but first part resolved to false' + ); + + if (!Object.hasOwn(section, keyParts[1])) { + if (keyParts.length === 2) { + if (message === false) section[keyParts[1]] = false; + else + throw new Error( + `key “${key}” can be used only to indicate an empty category using “false”` + ); + } else section[keyParts[1]] = {}; + } else if ( + keyParts.length === 2 && + ((section[keyParts[1]] === false && message !== false) || + (section[keyParts[1]] !== false && message === false)) + ) + throw new Error( + `key “${key}” can't be used to indicate an empty category because it's used for messages too` + ); + + // 3. Process the message ID: + if (keyParts.length > 2) { + const rule = section[keyParts[1]]; + if (!rule) + throw new Error( + 'key contains 3 parts, but first 2 parts resolved to false' + ); + + if (!Object.hasOwn(rule, keyParts[2])) + rule[keyParts[2]] = !!rule; + else + throw new Error( + `key “${key}” is defined more than once` + ); + } + } + } + } + return result; +}; + +/** + * Scans “baseDir” and finds heuristically all sections, rules, and message IDs. + * + * The search relies on a regex that tries to find instances of “sr.error()” etc, so it's not exact. + * Return a promise that will be fulfilled if/when all directories and files are read successfully. + */ +async function scanFileSystem() { + const result: Record>> = {}; + + const dirnames = await readdir(baseDir); + for (const dirname of dirnames) { + result[dirname] = {}; + + const filenames = await readdir(join(baseDir, dirname)); + for (const filename of filenames) { + if (!filename.endsWith('.ts')) continue; + if (filename.endsWith('.d.ts')) continue; + + const content = await readFile( + join(baseDir, dirname, filename), + 'utf8' + ); + const name = filename.replace(extensionRemover, ''); + result[dirname][name] = {}; + + let match; + while ((match = messageFinder.exec(content))) + result[dirname][name][match[2]] = true; + while ((match = exceptionFinder.exec(content))) + result[dirname][name][match[1]] = true; + } + } + + return result; +} + +const strings = scanStrings(); +const files = await scanFileSystem(); + +/** + * Compares two trees of {sections, rules, message IDs} to find leaves that are missing. + */ +const findHoles = function ( + source: typeof files | typeof strings, + expected: typeof files | typeof strings, + labelSource: string, + labelExpected: string +) { + let errors = ''; + for (const [i, expectedSection] of Object.entries(expected)) + if (!Object.hasOwn(source, i)) + errors += `Section “${i}” exists in ${labelExpected} but is missing in ${labelSource}.\n`; + else if (source[i] !== false && expectedSection !== false) { + for (const [j, expectedRule] of Object.entries(expectedSection)) + if (!Object.hasOwn(source[i], j)) + errors += `Rule “${i}/${j}” exists in ${labelExpected} but is missing in ${labelSource}.\n`; + else if (source[i][j] !== false && expectedRule !== false) + for (const k of Object.keys(expectedRule)) + if (!source[i][j] || !Object.hasOwn(source[i][j], k)) + errors += `Message ID “${i}/${j}/${k}” exists in ${labelExpected} but is missing in ${labelSource}.\n`; + } + if (errors) assert.fail(`${errors.slice(0, -2)}.`); +}; + +describe('L10n', () => { + describe('UI messages module', () => { + it('“lib/rules-track” should be a valid object', () => { + assert.strictEqual(typeof rules, 'object'); + }); + it('“lib/l10n-en_GB” should be a valid object', () => { + assert.strictEqual(typeof messages, 'object'); + for (const [key, message] of Object.entries(messages)) { + assert.match( + key, + /^[\w-]+\.[\w-]+(\.[\w-]+)?$/, + 'All l10n keys should be the format of 2 or 3 period-delimited slugs' + ); + const numParts = key.split('.').length; + if (numParts < 3) + assert.strictEqual( + message, + false, + 'Any key with fewer than 3 path segments should have a value of false' + ); + else + assert( + message === false || typeof message === 'string', + 'Any key with a fully-qualified path should be either false or a string' + ); + } + }); + }); + + describe('Consistency between rules and L10n messages', () => { + it('All L10n messages should be used by some rule', () => + findHoles(files, strings, 'files', 'l10n strings')); + it('All message IDs used by rules should exist as L10n messages', () => + findHoles(strings, files, 'l10n strings', 'files')); + }); +}); diff --git a/test/lib/nockData.js b/test/lib/nockData.ts similarity index 95% rename from test/lib/nockData.js rename to test/lib/nockData.ts index f1a45296a..9ccd73fcb 100644 --- a/test/lib/nockData.js +++ b/test/lib/nockData.ts @@ -1,4 +1,40 @@ -export const nockData = { +import type { IsoDateString } from '../../lib/types.js'; + +interface NockGroupData { + group: { + id: number; + name: string; + is_closed: boolean; + closed_date?: IsoDateString; + description: string; + shortname: string; + discr: string; + type: string; + 'start-date': IsoDateString; + 'end-date': IsoDateString; + }; + charters: { + 'doc-licenses': { name: string; uri: string }[]; + 'patent-policy'?: string; + end: IsoDateString; + extensions?: { announcement_uri: string; end: IsoDateString }[]; + 'initial-end'?: IsoDateString; + start: IsoDateString; + uri?: string; + 'cfp-uri'?: string; + 'required-new-commitments'?: boolean; + }[]; + userIds?: number[]; +} + +export interface NockData { + delivererMap: Record; + versionUris: string[]; + githubUsers: Record; + groupData: Record; +} + +export const nockData: NockData = { delivererMap: { 'css-color-3': 32061, 'hr-time': 45211, diff --git a/test/lib/testserver.js b/test/lib/testserver.ts similarity index 54% rename from test/lib/testserver.js rename to test/lib/testserver.ts index 4a0e368ba..2e80e317b 100644 --- a/test/lib/testserver.js +++ b/test/lib/testserver.ts @@ -1,6 +1,7 @@ -// start an server to host doc, response to sr.url requests -import express from 'express'; -import pth, { dirname } from 'path'; +/** @file Starts a server to host doc, response to sr.url requests */ + +import express, { type Request, type Response } from 'express'; +import { dirname, join } from 'path'; import exphbs from 'express-handlebars'; import { fileURLToPath } from 'url'; @@ -11,74 +12,77 @@ const ENDPOINT = `http://localhost:${PORT}`; const __dirname = dirname(fileURLToPath(import.meta.url)); export const app = express(); -app.use('/docs', express.static(pth.join(__dirname, 'docs'))); +app.use('/docs', express.static(join(__dirname, 'docs'))); // use express-handlebars app.engine( 'handlebars', exphbs.engine({ - defaultLayout: pth.join(__dirname, '../doc-views/layout/spec'), - layoutsDir: pth.join(__dirname, '../doc-views'), - partialsDir: pth.join(__dirname, '../doc-views/partials/'), + defaultLayout: join(__dirname, '..', 'doc-views', 'layout', 'spec'), + layoutsDir: join(__dirname, '..', 'doc-views'), + partialsDir: join(__dirname, '..', 'doc-views', 'partials'), }) ); app.set('view engine', 'handlebars'); -app.set('views', pth.join(__dirname, '../doc-views')); +app.set('views', join(__dirname, '..', 'doc-views')); -function renderByConfig(req, res) { +async function renderByConfig(req: Request, res: Response) { const { rule, type } = req.query; const suffix = req.params.track ? `${req.params.track}/${req.params.profile}` : req.params.profile; // get data for template from json (.js) - const path = pth.join( + const path = join( __dirname, - `../doc-views/${req.params.docType}/${suffix}.js` + '..', + 'doc-views', + `${req.params.docType}`, + `${suffix}.js` ); + const data = (await import(path)).default; + let finalData; - import(path).then(module => { - const data = module.default; - - let finalData; - if (!type) - res.send( - '

    Error: please add the parameter "type" in the URL

    ' + if (typeof type !== 'string') { + res.status(400).send( + '

    Error: please add the parameter "type" in the URL

    ' + ); + return; + } else if (type.startsWith('good')) { + finalData = data[type]; + } else { + if (typeof rule !== 'string') { + res.status(400).send( + '

    Error: please add the parameter "rule" in the URL

    ' ); - else if (type.startsWith('good')) { - finalData = data[type]; - } else { - if (!rule) - res.send( - '

    Error: please add the parameter "rule" in the URL

    ' - ); - - // for data causes error, make rule and the type of error specific. - finalData = data[rule][type]; + return; } - res.render(pth.join(__dirname, '../doc-views/layout/spec'), finalData); - }); + // for data causes error, make rule and the type of error specific. + finalData = data[rule][type]; + } + + res.render(join(__dirname, '..', 'doc-views', 'layout', 'spec'), finalData); } app.get('/doc-views/:docType/:track/:profile', renderByConfig); app.get('/doc-views/:docType/:profile', renderByConfig); // config single redirection -app.get('/docs/links/image/logo', (req, res) => { +app.get('/docs/links/image/logo', (_, res) => { res.redirect('/docs/links/image/logo.png'); }); // config single redirection to no where (404) -app.get('/docs/links/image/logo-fail', (req, res) => { +app.get('/docs/links/image/logo-fail', (_, res) => { res.redirect('/docs/links/image/logo-fail.png'); }); // config multiple redirection -app.get('/docs/links/image/logo-redirection-1', (req, res) => { +app.get('/docs/links/image/logo-redirection-1', (_, res) => { res.redirect(301, '/docs/links/image/logo-redirection-2'); }); -app.get('/docs/links/image/logo-redirection-2', (req, res) => { +app.get('/docs/links/image/logo-redirection-2', (_, res) => { res.redirect(307, '/docs/links/image/logo-redirection-3'); }); -app.get('/docs/links/image/logo-redirection-3', (req, res) => { +app.get('/docs/links/image/logo-redirection-3', (_, res) => { res.redirect('/docs/links/image/logo.png'); }); @@ -99,6 +103,6 @@ if (import.meta.url === `file://${process.argv[1]}`) { ); }); } else { - // server run by mocha - console.log(`\nTestserver running for mocha. \nHosting ${ENDPOINT}`); + // server run by test suite + console.log(`\nTestserver running for test suite. \nHosting ${ENDPOINT}`); } diff --git a/test/lib/utils.js b/test/lib/utils.ts similarity index 66% rename from test/lib/utils.js rename to test/lib/utils.ts index 6900e431e..5552bb93e 100644 --- a/test/lib/utils.js +++ b/test/lib/utils.ts @@ -1,80 +1,102 @@ -import { lstatSync, readdirSync } from 'fs'; +import { lstat, readdir } from 'fs/promises'; +import { join } from 'path'; + import merge from 'lodash.merge'; import nock from 'nock'; -import { nockData } from './nockData.js'; +import { nockData, type NockData } from './nockData.js'; +import type { SpecberusConfig } from '../../lib/types.js'; -function listFilesOf(dir) { - const files = readdirSync(dir); +export interface RuleTest { + config?: Partial; + data: string; + errors?: string[]; + warnings?: string[]; +} - // ignore .DS_Store from Mac - const blocklist = ['.DS_Store', 'Base.js']; +interface RuleTestModule { + rules: Record>; +} - return files.filter(v => !blocklist.find(b => v.includes(b))); +async function listDirectories(dir: string) { + const directories: string[] = []; + for (const entry of await readdir(dir)) + if ((await lstat(join(dir, entry))).isDirectory()) + directories.push(entry); + return directories; } -const flat = objs => objs.reduce((acc, cur) => ({ ...acc, ...cur }), {}); +async function listModules(dir: string) { + const list = (await readdir(dir)) + .filter( + filename => + filename.endsWith('.ts') && + !filename.endsWith('.d.ts') && + !filename.endsWith('Base.ts') + ) + // Map to resolvable module identifiers + .map(filename => filename.replace(/\.ts$/, '.js')); + return list; +} -const buildProfileTestCases = async path => { - const { rules } = await import(path); +const buildProfileTestCases = async (path: string) => { + const { rules } = (await import(path)) as RuleTestModule; return rules; }; -const buildTrackTestCases = async path => { - if (lstatSync(path).isFile()) { - const profile = await buildProfileTestCases(path); - return profile; +const buildTrackTestCases = async (path: string) => { + const profiles: Record = {}; + for (const profile of await listModules(path)) { + profiles[profile] = await buildProfileTestCases(`${path}/${profile}`); } - - const profiles = await Promise.all( - listFilesOf(path).map(async profile => ({ - [profile]: await buildProfileTestCases(`${path}/${profile}`), - })) - ); - - return flat(profiles); + return profiles; }; -const buildDocTypeTestCases = async path => { - const tracks = await Promise.all( - listFilesOf(path).map(async track => ({ - [track]: await buildTrackTestCases(`${path}/${track}`), - })) - ); +const buildDocTypeTestCases = async (path: string) => { + const tracks: Record< + string, + RuleTestModule['rules'] | Record + > = {}; - return flat(tracks); + for (const module of await listModules(path)) { + tracks[module] = await buildProfileTestCases(`${path}/${module}`); + } + for (const track of await listDirectories(path)) { + tracks[track] = await buildTrackTestCases(`${path}/${track}`); + } + return tracks; }; export const buildBadTestCases = async () => { - const base = `${process.cwd()}/test/data`; - const docTypes = await Promise.all( - listFilesOf(base) - .filter(v => lstatSync(`${base}/${v}`).isDirectory()) - .map(async docType => ({ - [docType]: await buildDocTypeTestCases(`${base}/${docType}`), - })) - ); - - return flat(docTypes); + const base = join(process.cwd(), 'test', 'data'); + const docTypes: Record< + string, + Awaited> + > = {}; + for (const docType of await listDirectories(base)) { + docTypes[docType] = await buildDocTypeTestCases(`${base}/${docType}`); + } + return docTypes; }; /** * @param {Request} req */ -function warnOnNonLocalRequest(req) { +function warnOnNonLocalRequest(req: Request) { if (!req.url.includes('//localhost')) { console.warn('Unmocked non-local request:', req.url, req.body); } } /** Mocks external calls to speed up tests and make them consistently runnable locally */ -export function setupMocks(overrides) { +export function setupMocks(overrides?: Partial) { // Report non-local URLs that were not mocked during test runs nock.emitter.on('no match', warnOnNonLocalRequest); nock.enableNetConnect('localhost'); // Only allow localhost requests to proceed unmocked - /** @type {typeof nockData} */ - const mockData = overrides ? merge({}, nockData, overrides) : nockData; + const mockData: typeof nockData = overrides + ? merge({}, nockData, overrides) + : nockData; const notFoundNames = [ 'hr-foo-time', diff --git a/test/rules.js b/test/rules.js deleted file mode 100644 index 108a0718b..000000000 --- a/test/rules.js +++ /dev/null @@ -1,350 +0,0 @@ -import { EventEmitter } from 'events'; -import { nextTick } from 'process'; - -// External modules: -import { expect as chai } from 'chai'; -import expect from 'expect.js'; - -import { allProfiles } from '../lib/util.js'; -import { Specberus } from '../lib/validator.js'; -// A list of good documents to be tested, using all rules configured in the profiles. -// Shouldn't cause any error. -import { goodDocuments } from './data/goodDocuments.js'; -import { samples } from './samples.js'; -import { app } from './lib/testserver.js'; -import { buildBadTestCases, cleanupMocks, setupMocks } from './lib/utils.js'; - -/** - * Test the rules. - */ - -// Settings: -const DEBUG = process.env.DEBUG || false; -const DEFAULT_PORT = 8001; -const PORT = process.env.PORT || DEFAULT_PORT; -const ENDPOINT = `http://localhost:${PORT}`; - -// These 3 environment variables are to reduce test documents. -// e.g. `RULE=copyright TYPE=noCopyright PROFILE=WD npm run test` -const testRule = process.env.RULE; -const testType = process.env.TYPE; -const testProfile = process.env.PROFILE; - -/** - * Assert that metadata detected in a spec is equal to the expected values. - * - * @param {String} file - name of local file containing a spec (without path and without ".html" suffix). - * @param {Object} expectedObject - values that are expected to be found. - */ - -function compareMetadata(file, expectedObject) { - const specberus = new Specberus(); - const successExpected = !('errors' in expectedObject); - - const handler = new EventEmitter(); - handler.on('exception', data => { - throw new Error(data); - }); - - const testFile = `test/docs/${file}.html`; - // test only local fixtures - const opts = { events: handler, file: testFile }; - - it(`Should detect metadata for ${testFile}`, done => { - handler.on('end-all', result => { - // Use nextTick to prevent assertion failures from - // bubbling up through final check and hanging - nextTick(() => { - chai(result).to.have.property('success').equal(successExpected); - if (!successExpected) { - chai(result) - .to.have.property('errors') - .lengthOf(expectedObject.errors.length) - .satisfy( - errors => - expectedObject.errors.every((expected, i) => { - return Object.entries(expected).every( - ([key, value]) => { - return errors[i][key] === value; - } - ); - }), - `Errors should contain expected properties:\n${JSON.stringify( - expectedObject.errors, - null, - ' ' - )}` - ); - } - - for (const [key, value] of Object.entries(expectedObject)) { - if (key === 'errors' || key === 'file') continue; - let assertion = chai(specberus) - .to.have.property('meta') - .to.have.property(key); - - if (Array.isArray(value)) assertion = assertion.deep; - assertion.equal(value, `Expected meta.${key} to match`); - } - - done(); - }); - }); - specberus.extractMetadata(opts); - }); -} - -describe('Basics', () => { - const specberus = new Specberus(); - - beforeEach(() => setupMocks()); - afterEach(cleanupMocks); - - describe('Method "extractMetadata"', () => { - it('Should exist and be a function', done => { - chai(specberus) - .to.have.property('extractMetadata') - .that.is.a('function'); - done(); - }); - - samples.forEach(sample => { - compareMetadata(sample.file, sample); - }); - }); - - describe('Method "validate"', () => { - it('Should exist and be a function', done => { - chai(specberus).to.have.property('validate').that.is.a('function'); - done(); - }); - }); -}); - -let testserver; - -before(done => { - testserver = app.listen(PORT, done); -}); - -after(done => { - if (testserver) { - testserver.close(done); - } -}); - -function buildHandler(test, done) { - const handler = new EventEmitter(); - - if (DEBUG) { - handler.on('err', (type, data) => { - console.log('error:\n', type, data); - }); - handler.on('warning', (type, data) => { - console.log('warning:\n', type, data); - }); - handler.on('done', name => { - console.log(`----> ${name} check done`); - }); - } - handler.on('exception', data => { - console.error( - `[EXCEPTION] Validator had a massive failure: ${data.message}` - ); - }); - handler.on('end-all', ({ errors, warnings }) => { - try { - if (!test.errors) { - expect(errors).to.be.empty(); - } else { - expect(errors.length).to.eql(test.errors.length); - errors.forEach(({ key, name }, i) => { - expect(`${name}.${key}`).to.equal(test.errors[i]); - }); - } - - if (!test.ignoreWarnings) { - if (test.warnings) { - expect(warnings.length).to.eql(test.warnings.length); - warnings.forEach(({ key, name }, i) => { - expect(`${name}.${key}`).to.contain(test.warnings[i]); - }); - } else { - expect(warnings).to.be.empty(); - } - } - done(); - } catch (e) { - done(e); - } - }); - - return handler; -} - -const testsGoodDoc = goodDocuments; - -// The next check is running each profile using the rules configured. -describe('Making sure good documents pass Specberus...', () => { - beforeEach(() => - setupMocks({ - // hard-code group ID to match state of test documents - delivererMap: { 'hr-time': 32113 }, - }) - ); - afterEach(cleanupMocks); - - Object.keys(testsGoodDoc).forEach(docProfile => { - // testsGoodDoc[docProfile].profile is used to distinguish multiple cases for same profile. - docProfile = testsGoodDoc[docProfile].profile || docProfile; - if (testProfile && testProfile !== docProfile) return; - - const url = `${ENDPOINT}/${testsGoodDoc[docProfile].url}`; - - it(`should pass for ${docProfile} doc with ${url}`, done => { - const profilePath = allProfiles.find(p => - p.endsWith(`/${docProfile}`) - ); - import(`../lib/profiles/${profilePath}.js`).then(profile => { - // add custom config to test - const extendedProfile = { - ...profile, - config: { - patentPolicy: 'pp2020', // default config for all docs. - ...profile.config, - ...testsGoodDoc[docProfile].config, - }, - }; - - // remove unnecessary rules from test - import('../lib/profiles/profileUtil.js').then( - ({ removeRules }) => { - const rules = removeRules(extendedProfile.rules, [ - 'validation.html', - 'validation.wcag', - 'links.linkchecker', // too slow. will check separately. - ]); - - const options = { - profile: { - ...extendedProfile, - rules, // do not change profile.rules - }, - events: buildHandler( - { ignoreWarnings: true }, - done - ), - url, - }; - - // for (const o in test.options) options[o] = test.options[o]; - new Specberus().validate(options); - } - ); - }); - }); - }); -}); - -function checkRule(tests, options) { - const { docType, track, profile, category, rule } = options; - - tests.forEach(test => { - const passOrFail = !test.errors ? 'pass' : 'fail'; - const suffix = track ? `${track}/${profile}` : profile; - const url = `${ENDPOINT}/doc-views/${docType}/${suffix}?rule=${rule}&type=${test.data}`; - - // If the test is not mentioned in the environment variables, skip it. - if ( - (testRule && rule !== testRule) || - (testType && test.data !== testType) || - (testProfile && profile !== testProfile) - ) - return; - - it(`should ${passOrFail} for ${url}`, done => { - import(`../lib/profiles/${docType}/${suffix}.js`).then( - ({ config }) => { - import(`../lib/rules/${category}/${rule}.js`).then( - ruleModule => { - const options = { - url, - profile: { - name: `Synthetic ${profile}/${rule}`, - rules: [ruleModule], - config: { - ...config, - ...test.config, - }, - }, - events: buildHandler(test, done), - ...test.options, - }; - new Specberus().validate(options); - } - ); - } - ); - }); - }); -} - -function runTestsForProfile({ docType, track, profile, rules }) { - // Profile: CR/NOTE/RY ... - describe(`Profile: ${profile}`, () => { - Object.entries(rules).forEach(([category, rules]) => { - Object.entries(rules).forEach(([rule, tests]) => { - // Rule: hr/logo ... - describe(`Rule: ${category}.${rule}`, () => { - checkRule(tests, { - docType, - track, - profile: profile.substring(0, profile.lastIndexOf('.')), - category, - rule, - }); - }); - }); - }); - }); -} - -const badTestCases = await buildBadTestCases(); - -// The next check runs every rule for each profile, one rule at a time, and should trigger every existing errors and warnings in lib/l10n-en_GB.js -describe('Making sure Specberus is not broken...', () => { - beforeEach(() => setupMocks()); - afterEach(cleanupMocks); - - Object.entries(badTestCases).forEach(([docType, tracksOrProfiles]) => { - // DocType: TR/SUMB - describe(`DocType: ${docType}`, () => { - Object.entries(tracksOrProfiles).forEach( - ([trackOrProfile, profilesOrRules]) => { - // Profile: SUBM - if (trackOrProfile === 'MEM-SUBM.js') { - runTestsForProfile({ - docType, - profile: trackOrProfile, - rules: profilesOrRules, - }); - return; - } - - // Track: Note/Recommendation/Registry - describe(`Track: ${trackOrProfile}`, () => { - Object.entries(profilesOrRules).forEach( - ([profile, rules]) => - runTestsForProfile({ - docType, - track: trackOrProfile, - profile, - rules, - }) - ); - }); - } - ); - }); - }); -}); diff --git a/test/rules.ts b/test/rules.ts new file mode 100644 index 000000000..b8e03f575 --- /dev/null +++ b/test/rules.ts @@ -0,0 +1,387 @@ +import assert from 'assert'; +import { EventEmitter } from 'events'; +import type { Server } from 'http'; +import { after, afterEach, before, beforeEach, describe, it } from 'node:test'; + +import { removeRules } from '../lib/profiles/profileUtil.js'; +import { allProfiles, buildJSONresult } from '../lib/util.js'; +import { Specberus } from '../lib/validator.js'; +// A list of good documents to be tested, using all rules configured in the profiles. +// Shouldn't cause any error. +import { goodDocuments } from './data/goodDocuments.js'; +import { app } from './lib/testserver.js'; +import { + buildBadTestCases, + cleanupMocks, + setupMocks, + type RuleTest, +} from './lib/utils.js'; +import { samples } from './samples.js'; + +/** + * Test the rules. + */ + +// Settings: +const DEBUG = process.env.DEBUG || false; +const DEFAULT_PORT = 8001; +const PORT = process.env.PORT || DEFAULT_PORT; +const ENDPOINT = `http://localhost:${PORT}`; + +// These 3 environment variables are to reduce test documents. +// e.g. `RULE=copyright TYPE=noCopyright PROFILE=WD npm run test` +const testRule = process.env.RULE; +const testType = process.env.TYPE; +const testProfile = process.env.PROFILE; + +interface CompareMetadataObject { + errors?: Record[]; + [index: string]: any; +} + +/** + * Returns an EventEmitter and Promise, both reflecting progress/completion of a Specberus call. + */ +function createSpecberusPromiseHandler() { + const handler = new EventEmitter(); + const promise = new Promise>( + (resolve, reject) => { + handler.on('end-all', resolve); + handler.on('exception', reject); + } + ); + return { handler, promise }; +} + +/** + * Assert that metadata detected in a spec is equal to the expected values. + * + * @param file - name of local file containing a spec (without path and without ".html" suffix). + * @param expectedObject - values that are expected to be found. + */ +function compareMetadata(file: string, expectedObject: CompareMetadataObject) { + const testFile = `test/docs/${file}.html`; + + it(`Should detect metadata for ${testFile}`, async () => { + const specberus = new Specberus(); + const { handler, promise } = createSpecberusPromiseHandler(); + specberus.extractMetadata({ events: handler, file: testFile }); + const result = await promise; + + assert.strictEqual(result.success, !('errors' in expectedObject)); + if ('errors' in expectedObject) { + assert.strictEqual( + result.errors.length, + expectedObject.errors.length + ); + assert( + expectedObject.errors.every((expected, i) => + Object.entries(expected).every( + ([key, value]) => result.errors[i][key] === value + ) + ), + `Errors should contain expected properties:\n${JSON.stringify( + expectedObject.errors, + null, + ' ' + )}` + ); + } + + assert(specberus.meta, 'Expected specberus.meta to be defined'); + for (const [key, value] of Object.entries(expectedObject)) { + if (key === 'errors' || key === 'file') continue; + assert( + key in specberus.meta, + `Expected specberus.meta.${key} to be defined` + ); + assert.deepStrictEqual(specberus.meta[key], value); + } + }); +} + +describe('Basics', () => { + const specberus = new Specberus(); + + beforeEach(() => setupMocks()); + afterEach(cleanupMocks); + + describe('Method "extractMetadata"', () => { + it('Should exist and be a function', () => { + assert.strictEqual(typeof specberus.extractMetadata, 'function'); + }); + + samples.forEach(sample => { + compareMetadata(sample.file, sample); + }); + }); + + describe('Method "validate"', () => { + it('Should exist and be a function', () => { + assert.strictEqual(typeof specberus.validate, 'function'); + }); + }); +}); + +let testserver: Server; + +before( + () => + new Promise( + (resolve, reject) => + (testserver = app.listen(PORT, err => + err ? reject(err) : resolve() + )) + ) +); + +after( + () => + new Promise((resolve, reject) => + testserver.close(err => (err ? reject(err) : resolve())) + ) +); + +interface ValidationTestConfig { + errors?: any[]; + ignoreWarnings?: boolean; + warnings?: any[]; +} + +function buildValidationTestHandler(test: ValidationTestConfig) { + const { handler, promise } = createSpecberusPromiseHandler(); + + if (DEBUG) { + handler.on('err', (type, data) => { + console.log('error:\n', type, data); + }); + handler.on('warning', (type, data) => { + console.log('warning:\n', type, data); + }); + handler.on('done', name => { + console.log(`----> ${name} check done`); + }); + } + handler.on('exception', data => { + console.error( + `[EXCEPTION] Validator had a massive failure: ${data.message}` + ); + }); + + return { + handler, + promise: promise.then(({ errors, warnings }) => { + if (test.errors) { + assert.strictEqual(errors.length, test.errors.length); + errors.forEach(({ key, name }, i) => { + assert.strictEqual(`${name}.${key}`, test.errors![i]); + }); + } else { + assert.strictEqual( + errors.length, + 0, + 'Expected errors to be empty' + ); + } + + if (!test.ignoreWarnings) { + if (test.warnings) { + assert.strictEqual(warnings.length, test.warnings.length); + warnings.forEach(({ key, name }, i) => { + assert.strictEqual(`${name}.${key}`, test.warnings![i]); + }); + } else { + assert.strictEqual( + warnings.length, + 0, + 'Expected warnings to be empty' + ); + } + } + }), + }; +} + +const testsGoodDoc = goodDocuments; + +// The next check is running each profile using the rules configured. +describe('Making sure good documents pass Specberus...', () => { + beforeEach(() => + setupMocks({ + // hard-code group ID to match state of test documents + delivererMap: { 'hr-time': 32113 }, + }) + ); + afterEach(cleanupMocks); + + for (const [key, doc] of Object.entries(testsGoodDoc)) { + // testsGoodDoc[docProfile].profile is used to distinguish multiple cases for same profile. + const docProfile = 'profile' in doc ? doc.profile : key; + if (testProfile && testProfile !== docProfile) continue; + + const url = `${ENDPOINT}/${doc.url}`; + + it(`should pass for ${docProfile} doc with ${url}`, async () => { + const profilePath = allProfiles.find(p => + p.endsWith(`/${docProfile}`) + ); + const profile = await import(`../lib/profiles/${profilePath}.js`); + // add custom config to test + const extendedProfile = { + ...profile, + config: { + patentPolicy: 'pp2020', // default config for all docs. + ...profile.config, + ...('config' in doc && doc.config), + }, + }; + + // remove unnecessary rules from test + const rules = removeRules(extendedProfile.rules, [ + 'validation.html', + 'validation.wcag', + 'links.linkchecker', // too slow. will check separately. + ]); + + const { handler, promise } = buildValidationTestHandler({ + ignoreWarnings: true, + }); + const options = { + profile: { + ...extendedProfile, + rules, // do not change profile.rules + }, + events: handler, + url, + }; + + new Specberus().validate(options); + return promise; + }); + } +}); + +interface CheckRuleOptions { + category: string; + docType: string; + profile: string; + rule: string; + track?: string | undefined; +} + +function checkRule(tests: RuleTest[], options: CheckRuleOptions) { + const { docType, track, profile, category, rule } = options; + + tests.forEach(test => { + const passOrFail = !test.errors ? 'pass' : 'fail'; + const suffix = track ? `${track}/${profile}` : profile; + const url = `${ENDPOINT}/doc-views/${docType}/${suffix}?rule=${rule}&type=${test.data}`; + + // If the test is not mentioned in the environment variables, skip it. + if ( + (testRule && rule !== testRule) || + (testType && test.data !== testType) || + (testProfile && profile !== testProfile) + ) + return; + + it(`should ${passOrFail} for ${url}`, async () => { + const { config } = await import( + `../lib/profiles/${docType}/${suffix}.js` + ); + const ruleModule = await import( + `../lib/rules/${category}/${rule}.js` + ); + const { handler, promise } = buildValidationTestHandler(test); + + const options = { + url, + profile: { + name: `Synthetic ${profile}/${rule}`, + rules: [ruleModule], + config: { + ...config, + ...test.config, + }, + }, + events: handler, + }; + new Specberus().validate(options); + return promise; + }); + }); +} + +interface ProfileTestOptions { + docType: string; + profile: string; + rules: Record>; + track?: string; +} + +function runTestsForProfile({ + docType, + track, + profile, + rules, +}: ProfileTestOptions) { + // Profile: CR/NOTE/RY ... + describe(`Profile: ${profile}`, () => { + Object.entries(rules).forEach(([category, rules]) => { + Object.entries(rules).forEach(([rule, tests]) => { + // Rule: hr/logo ... + describe(`Rule: ${category}.${rule}`, () => { + checkRule(tests, { + docType, + track, + profile: profile.substring(0, profile.lastIndexOf('.')), + category, + rule, + }); + }); + }); + }); + }); +} + +const badTestCases = await buildBadTestCases(); + +// The next check runs every rule for each profile, one rule at a time, and should trigger every existing errors and warnings in lib/l10n-en_GB.js +describe('Making sure Specberus is not broken...', () => { + beforeEach(() => setupMocks()); + afterEach(cleanupMocks); + + Object.entries(badTestCases).forEach(([docType, tracksOrProfiles]) => { + // DocType: TR/SUBM + describe(`DocType: ${docType}`, () => { + Object.entries(tracksOrProfiles).forEach( + ([trackOrProfile, profilesOrRules]) => { + // Profile: SUBM + if (trackOrProfile === 'MEM-SUBM.js') { + return runTestsForProfile({ + docType, + profile: trackOrProfile, + rules: profilesOrRules as Record< + string, + Record + >, + }); + } + + // Track: Note/Recommendation/Registry + describe(`Track: ${trackOrProfile}`, () => { + Object.entries(profilesOrRules).forEach( + ([profile, rules]) => + runTestsForProfile({ + docType, + track: trackOrProfile, + profile, + rules, + }) + ); + }); + } + ); + }); + }); +}); diff --git a/test/samples.js b/test/samples.ts similarity index 100% rename from test/samples.js rename to test/samples.ts diff --git a/test/validation.js b/test/validation.js deleted file mode 100644 index bf67436f7..000000000 --- a/test/validation.js +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Test HTML and CSS checkers. - * - * This file is not runnable by Mocha directly; it is used by "rules.js". - * - * HTML and CSS validations often time out, and Travis CI thinks the build is broken when it happens. - * Therefore, we only add these test cases when testing locally. - * See w3c/specberus#164 and - * Travis documentation. - */ - -const shouldExportHTML = - !process || - !process.env || - (process.env.TRAVIS !== 'true' && !process.env.SKIP_NETWORK); - -export const html = shouldExportHTML - ? [ - { doc: 'validation/simple.html' }, - { doc: 'validation/invalid.html', errors: ['validation.html.error'] }, - ] - : []; diff --git a/tsconfig.json b/tsconfig.json index a34e8eee5..f5abb0453 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,6 @@ "useUnknownInCatchVariables": false, "verbatimModuleSyntax": true }, - "exclude": ["public/**/*", "test/**/*"], + "exclude": ["public/**/*"], "types": ["node"] } From 6f8ad2cc3ce0714865f9913bf6da5698393a63c4 Mon Sep 17 00:00:00 2001 From: "Kenneth G. Franqueiro" Date: Thu, 4 Jun 2026 11:48:27 -0400 Subject: [PATCH 04/11] Convert validate, extractMetadata, and rule check APIs to return promises (#2106) Additional improvements: - Switch all fs function usage in validator.ts to fs/promises - Remove Specberus#clearCache - Add Specberus#load which performs common logic of branching to loadFile/loadURL/loadSource (which are now also private) - Mark more class members as private where feasible - Replace sr.throw calls within rules with actual throw statements, centralizing all throw method calls within validator.ts, ensuring that rejection occurs in response to unexpected errors - Remove start-all/end-all events; use promise APIs in app, api, and tests - Remove doasync (and reorganize some variables for better type safety) - Remove public meta property; rely on resolved promise value instead - Make Specberus emit events directly; thoroughly define event types - Remove unused parameter from extractHeaders - lib/util: Remove unused parameter names; update JSDoc - Fire 'done' event even if an exception occurs - app.ts: Consistently report misc. (non-'exception') errors - Add new/missing test cases - Update docs (including some long-overdue updates) --------- Co-authored-by: Denis Ah-Kang <1696128+deniak@users.noreply.github.com> --- .cspell.json | 9 +- README.md | 93 +-- app.ts | 76 +- lib/api.ts | 152 ++-- lib/rules/echidna/deliverer-change.ts | 8 +- lib/rules/echidna/todays-date.ts | 4 +- lib/rules/headers/copyright.ts | 14 +- lib/rules/headers/details-summary.ts | 10 +- lib/rules/headers/div-head.ts | 4 +- lib/rules/headers/dl.ts | 4 +- lib/rules/headers/editor-participation.ts | 4 +- lib/rules/headers/errata.ts | 12 +- lib/rules/headers/github-repo.ts | 6 +- lib/rules/headers/h1-title.ts | 3 +- lib/rules/headers/h2-toc.ts | 4 +- lib/rules/headers/hr.ts | 3 +- lib/rules/headers/logo.ts | 3 +- lib/rules/headers/memsub-copyright.ts | 3 +- lib/rules/headers/ol-toc.ts | 4 +- lib/rules/headers/secno.ts | 4 +- lib/rules/headers/shortname.ts | 33 +- lib/rules/headers/subm-logo.ts | 3 +- lib/rules/headers/translation.ts | 6 +- lib/rules/headers/w3c-state.ts | 8 +- lib/rules/heuristic/date-format.ts | 4 +- lib/rules/links/compound.ts | 98 +-- lib/rules/links/internal.ts | 3 +- lib/rules/links/linkchecker.ts | 5 +- lib/rules/links/reliability.ts | 3 +- lib/rules/metadata/abstract.ts | 31 +- lib/rules/metadata/charters.ts | 2 +- lib/rules/metadata/deliverers.ts | 2 +- lib/rules/metadata/dl.ts | 20 +- lib/rules/metadata/docDate.ts | 10 +- lib/rules/metadata/editor-ids.ts | 8 +- lib/rules/metadata/editor-names.ts | 4 +- lib/rules/metadata/errata.ts | 6 +- lib/rules/metadata/informative.ts | 11 +- lib/rules/metadata/process.ts | 5 +- lib/rules/metadata/profile.ts | 29 +- lib/rules/metadata/sotd.ts | 4 +- lib/rules/metadata/title.ts | 11 +- lib/rules/sotd/candidate-review-end.ts | 3 +- lib/rules/sotd/charter.ts | 12 +- lib/rules/sotd/deliverer-note.ts | 4 +- lib/rules/sotd/deployment.ts | 8 +- lib/rules/sotd/diff.ts | 3 +- lib/rules/sotd/draft-stability.ts | 4 +- lib/rules/sotd/new-features.ts | 3 +- lib/rules/sotd/obsl-rescind.ts | 3 +- lib/rules/sotd/pp.ts | 5 +- lib/rules/sotd/process-document.ts | 3 +- lib/rules/sotd/publish.ts | 5 +- lib/rules/sotd/rec-addition.ts | 3 +- lib/rules/sotd/rec-comment-end.ts | 3 +- lib/rules/sotd/stability.ts | 3 +- lib/rules/sotd/submission.ts | 5 +- lib/rules/sotd/supersedable.ts | 3 +- lib/rules/sotd/usage.ts | 8 +- lib/rules/structure/canonical.ts | 4 +- lib/rules/structure/display-only.ts | 3 +- lib/rules/structure/h2.ts | 3 +- lib/rules/structure/name.ts | 28 +- lib/rules/structure/neutral.ts | 12 +- lib/rules/structure/section-ids.ts | 3 +- lib/rules/structure/security-privacy.ts | 4 +- lib/rules/style/back-to-top.ts | 4 +- lib/rules/style/body-toc-sidebar.ts | 3 +- lib/rules/style/meta.ts | 3 +- lib/rules/style/script.ts | 4 +- lib/rules/style/sheet.ts | 5 +- lib/rules/validation/html.ts | 31 +- lib/rules/validation/wcag.ts | 3 +- lib/types.d.ts | 13 +- lib/util.ts | 36 +- lib/validator.ts | 820 ++++++++++------------ package-lock.json | 11 - package.json | 1 - test/api.ts | 81 ++- test/data/goodDocuments.ts | 4 + test/doc-views/TR/Note/STMT.ts | 20 + test/docs/api/wd-fail-auto.html | 148 ++++ test/docs/api/wd-fail-date.html | 148 ++++ test/rules.ts | 192 +++-- 84 files changed, 1320 insertions(+), 1068 deletions(-) create mode 100644 test/docs/api/wd-fail-auto.html create mode 100644 test/docs/api/wd-fail-date.html diff --git a/.cspell.json b/.cspell.json index 8c9fe455f..064665e0b 100644 --- a/.cspell.json +++ b/.cspell.json @@ -18,7 +18,6 @@ "deniak", "discr", "DNOTE", - "doasync", "doctypes", "domhandler", "dvcs", @@ -126,13 +125,7 @@ "overrides": [ { "filename": ["package.json"], - "words": [ - "capi", - "doasync", - "metaviewport", - "nodesecurity", - "vulns" - ] + "words": ["capi", "metaviewport", "nodesecurity", "vulns"] } ], "ignoreWords": ["en", "gb"] diff --git a/README.md b/README.md index 83bce8def..650734c26 100644 --- a/README.md +++ b/README.md @@ -163,26 +163,26 @@ const specberus = new Specberus(); ### `validate(options)` -This method takes an object with the following fields: +This method returns a Promise that resolves with errors, warnings, and informative messages resulting from relevant checks. -- `url`: URL of the content to check. One of `url`, `source`, `file`, or `document` must be +`options` is an object accepting the following fields: + +- `url`: URL of the content to check. One of `url`, `source`, or `file` must be specified and if several are they will be used in this order. - `source`: A `String` with the content to check. - `file`: A file system path to the content to check. -- `document`: A DOM `Document` object to be checked. - `profile`: A profile object which defines the validation. Required. See below. -- `events`: An event sink which supports the same interface as the Node.js `EventEmitter`. Required. See - below for the events that get generated. ### `extractMetadata(options)` -This method eventually extends `this` with metadata inferred from the document. -Once the [event `end-all`](#validation-events) is emitted, the metadata should be available in a new property called `meta`. +This method returns a Promise that resolves with metadata inferred from the document. + +The `options` accepted are equal to those in `validate()`, with the following differences: -The `options` accepted are equal to those in `validate()`, except that a `profile` is not necessary and will be ignored (finding out the profile is one of the -goals of this method). +- Optional `additionalMetadata` property, which performs additional checks (e.g. errata URL) +- No `profile` or `validation` properties (this method can be used to _determine_ profile) -`this.meta` will be an `Object` and may include up to 20 properties described below: +The resolved object's `metadata` property points to an object with up to 20 properties, described below: - `profile` - `title`: The (possible) title of the document @@ -207,7 +207,7 @@ goals of this method). If some of these pieces of metadata cannot be deduced, that key will not exist, or its value will not be defined. -This is an example of the value of `Specberus.meta` after the execution of `Specberus.extractMetadata()`: +The following is an example of the value of the `metadata` object after the execution of `Specberus.extractMetadata()`: ```json { @@ -229,8 +229,8 @@ This is an example of the value of `Specberus.meta` after the execution of `Spec Similar to the [JS API](#4-js-api), Specberus exposes a REST API via HTTP too. -The endpoint is `/api/`. -Use either `url` or `file` to pass along the document (neither `source` nor `document` are allowed). +The base path is `/api/`. +Use either `url` or `file` to pass along the document (`source` is not allowed). Note: If you want to use the public W3C instance of Specberus, you can replace `` with `https://www.w3.org/pubrules`. @@ -402,47 +402,58 @@ Profiles that are identical to its parent profile, ie that do not add any new ru ## 7. Validation events -For a given checking run, the event sink you specify will be receiving a bunch of events as -indicated below. Events are shown as having parameters since those are passed to the event handler. - -- `start-all(profile-name)`: Fired first to indicate that the profile's checking has started. -- `end-all(profile-name)`: Fired last to indicate that the profile's checking has completed. When - you receive this you are promised that all testing operations, including asynchronous ones, have - terminated. -- `done(rule-name)`: Fired when a specific rule has finished processing, including its asynchronous - tasks. -- `ok(rule-name)`: Fired to indicate that a rule has succeeded. There is only one `ok` per rule. - There cannot also be `err` events but there can be `warning` events. -- `err(error-name, data)`: Fired when an error is detected. The `data` contains further details, - that depend on the error but _should_ feature a `message` field. There can be multiple errors for - a given rule. There cannot also be `ok` events but there can be `warning`s. -- `warning(warnings-name, data)`: Fired for non-fatal problems with the document that may - nevertheless require investigation. There may be several for a rule. -- `info(info-name, data)`: Fired for additional information items detected by the validator. -- `metadata(key, value)`: Fired for every piece of document metadata found by the validator. -- `exception(message)`: Fired when there is a system error, such as a _File not found_ error. `message` - contains details about this error. All exceptions are displayed on the error console in addition to +When using the JS API, the Specberus instance will fire various events. +The `Specberus` class extends [`EventEmitter`](https://nodejs.org/dist/latest/docs/api/events.html#class-eventemitter), +so listeners can be registered using the `on` API. + +```js +const specberus = new Specberus(); +specberus.on('error', (rule, data) => { + // ... +}); +``` + +Events listed below are expressed with parameters to reflect what is passed to the event handler. + +- `done(ruleName)`: Fired when a specific rule has finished processing, including its asynchronous + tasks. This fires regardless of whether the rule passes, fails, or encounters an unexpected + system error (exception). +- `err(rule, data)`: Fired when an error is detected. There can be multiple errors for each rule. + - `rule` contains information on which rule failed validation; + always includes a `name` string, and may also include `rule` and `section` strings + - `data` contains further details; always includes `key` and `detailMessage` strings, + and optionally includes an `extra` object with additional fields that vary by error +- `warning(rule, data)`: Fired for non-fatal problems with the document that may + nevertheless require investigation. There can be multiple warnings for each rule. + `rule` and `data` follow the same format as for `err` events. +- `info(rule, data)`: Fired for additional information items detected by the validator. + `rule` and `data` follow the same format as for `err` and `warning` events. +- `exception({ message })`: Fired when there is an unexpected system error, such as a + _File not found_ error. The event passes an object with a `message` property containing + details about this error. All exceptions are displayed on the error console in addition to this event being fired. ## 8. Writing rules -Rules are simple modules that just expose a `check(sr, cb)` method. They receive a Specberus object -and a callback, use the Specberus object to fire validation events and call the callback when -they're done. +Rules are simple modules that just expose a `check(sr)` method. They receive a Specberus object, +which they use to examine the document and fire validation events. They return a promise which +resolves on completion (regardless of pass or fail) or rejects on unexpected system error +(exception). Usually, they are written as `async` functions to automatically handle the +resolve vs. reject distinction. -The Specberus object exposes the following API that's useful for validation: +The Specberus object exposes the following APIs useful for validation: - `source`. The HTML source of the document being processed - `url`. The URL of the document being processed, only applicable if `options.url` was specified -- `error`, `warn`, `info`. Methods for firing respective levels of events to the instance's sink. +- `error`, `warn`, `info`. Methods for firing respective levels of events on the instance. All three methods accept the same arguments: - `rule` object: at minimum, an object with a `name` string. May also contain `rule` and `section` strings. - `key` string: specifies the precise occurrence within the particular `rule` - `extra` object (optional): any additional fields to include within the event - `version`. The Specberus version. -- `checkSelector(selector, rule-name, cb)`. Some rules need to do nothing other than to check that a - selector returns some content. For this case, the rule can just call this method with the selector - and its callback, and Specberus will conveniently take care of all the rest. +- `checkSelector(selector, ruleName)`. Some rules need to do nothing other than to check that a + selector returns some content. This handles checking the selector, reporting an error if it is + not found, or throwing an error if the selector is invalid. - `norm(text)`. Returns a whitespace-normalized version of the text. - `getDocumentDate()`. Returns a Date object that matches the document's date as specified in the headers' `stateElement` (id="w3c-state"). diff --git a/app.ts b/app.ts index 3e3946ac1..0051eacc6 100644 --- a/app.ts +++ b/app.ts @@ -7,20 +7,19 @@ import compression from 'compression'; import cors from 'cors'; import express from 'express'; import fileUpload from 'express-fileupload'; -import EventEmitter from 'events'; import { writeFile } from 'fs'; import http from 'http'; // @ts-ignore (no typings) import insafe from 'insafe'; import morgan from 'morgan'; -import { Server } from 'socket.io'; +import { Server, type Socket } from 'socket.io'; import tmp from 'tmp'; import * as api from './lib/api.js'; import badterms from './lib/badterms.js'; import * as l10n from './lib/l10n.js'; import { allProfiles, specberusVersion } from './lib/util.js'; -import { Specberus } from './lib/validator.js'; +import { ExceptionsError, Specberus } from './lib/validator.js'; import * as views from './lib/views.js'; import type { ProfileModule } from './lib/types.js'; @@ -59,16 +58,27 @@ l10n.setLanguage('en_GB'); server.listen(process.argv[2] || process.env.PORT || DEFAULT_PORT); +/** Emits misc. errors, for cases where 'exception' handler already emits individual exceptions. */ +function reportNonExceptionsError( + socket: Socket, + e: any, + subject: 'Metadata extraction' | 'Validation' +) { + if (!(e instanceof ExceptionsError)) + socket.emit('exception', { + message: `${subject} encountered an unexpected error: ${e}`, + }); +} + io.on('connection', socket => { socket.emit('handshake', { version: specberusVersion }); - socket.on('extractMetadata', data => { + socket.on('extractMetadata', async data => { if (!data.url && !data.file) return socket.emit('exception', { message: 'URL or file not provided.', }); const specberus = new Specberus(); - const handler = new EventEmitter(); - handler.on('err', (type, data) => { + specberus.on('err', (type, data) => { try { socket.emit( 'err', @@ -78,7 +88,7 @@ io.on('connection', socket => { socket.emit('exception', err.message); } }); - handler.on('warning', (type, data) => { + specberus.on('warning', (type, data) => { try { socket.emit( 'warning', @@ -88,7 +98,7 @@ io.on('connection', socket => { socket.emit('exception', err.message); } }); - handler.on('info', (type, data) => { + specberus.on('info', (type, data) => { try { socket.emit( 'info', @@ -98,15 +108,18 @@ io.on('connection', socket => { socket.emit('exception', err.message); } }); - handler.on('end-all', metadata => { - metadata.url = data.url; - socket.emit('finishedExtraction', metadata); - }); - handler.on('exception', data => { + specberus.on('exception', data => { socket.emit('exception', data); }); - data.events = handler; - specberus.extractMetadata(data); + try { + const metadata = await specberus.extractMetadata(data); + socket.emit('finishedExtraction', { + ...metadata, + url: data.url, + }); + } catch (e) { + reportNonExceptionsError(socket, e, 'Metadata extraction'); + } }); socket.on('validate', async data => { if (!data.url && !data.file) @@ -131,12 +144,11 @@ io.on('connection', socket => { }); } const specberus = new Specberus(); - const handler = new EventEmitter(); const profileCode = profile.name; socket.emit('start', { rules: (profile.rules || []).map(rule => rule.name), }); - handler.on('err', (type, data) => { + specberus.on('err', (type, data) => { try { socket.emit( 'err', @@ -146,7 +158,7 @@ io.on('connection', socket => { socket.emit('exception', err.message); } }); - handler.on('warning', (type, data) => { + specberus.on('warning', (type, data) => { try { socket.emit( 'warning', @@ -156,7 +168,7 @@ io.on('connection', socket => { socket.emit('exception', err.message); } }); - handler.on('info', (type, data) => { + specberus.on('info', (type, data) => { try { socket.emit( 'info', @@ -166,13 +178,10 @@ io.on('connection', socket => { socket.emit('exception', err.message); } }); - handler.on('done', name => { + specberus.on('done', name => { socket.emit('done', { name }); }); - handler.on('end-all', () => { - socket.emit('finished'); - }); - handler.on('exception', data => { + specberus.on('exception', data => { socket.emit('exception', data); }); @@ -185,19 +194,17 @@ io.on('connection', socket => { url: data.url, statusCodesAccepted: ['301', '406'], }) - .then((res: any) => { + .then(async (res: any) => { if (res.status) { try { - specberus.validate({ + await specberus.validate({ url: data.url, profile, - events: handler, validation: data.validation, }); } catch (e) { - socket.emit('exception', { - message: `Validation blew up: ${e}`, - }); + reportNonExceptionsError(socket, e, 'Validation'); + } finally { socket.emit('finished'); } } else { @@ -215,16 +222,15 @@ io.on('connection', socket => { }); } else { try { - specberus.validate({ + await specberus.validate({ file: data.file, profile, - events: handler, validation: data.validation, }); } catch (e) { - socket.emit('exception', { - message: `Validation blew up: ${e}`, - }); + reportNonExceptionsError(socket, e, 'Validation'); + } finally { + // Exceptions already emitted from event handler socket.emit('finished'); } } diff --git a/lib/api.ts b/lib/api.ts index 0de128ae2..cdc149349 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -2,35 +2,51 @@ * @file REST API. */ -import EventEmitter from 'events'; - import { fileTypeFromFile } from 'file-type'; import type { Express, Request, Response } from 'express'; -import { buildJSONresult, processParams, specberusVersion } from './util.js'; -import { Specberus, type ValidateOptions } from './validator.js'; import type { HandlerMessage } from './types.js'; +import { processParams, specberusVersion } from './util.js'; +import { + ExceptionsError, + Specberus, + type SpecberusResult, + type ValidateOptions, +} from './validator.js'; + +/** Data types emitted by error events */ +type ErrorHandlerMessage = + | { error: string } + | { exception: string } + | HandlerMessage; + +interface ApiResult extends Omit { + errors: ErrorHandlerMessage[]; +} + +/** Sends the result to the client. */ +const sendResult = function (res: Response, result: SpecberusResult) { + delete result.metadata.file; + res.status(result.success ? 200 : 400).json(result); +}; /** - * Send the JSON result to the client. - * - * @param errors - errors - * @param warnings - warnings - * @param info - informative messages - * @param res - Express HTTP response - * @param metadata - dictionary with some found metadata + * Sends a list of validation errors or processing exceptions to the client. + * @param res Express Response + * @param error An ExceptionsError (yields HTTP 500), or any other error (yields HTTP 400) */ -const sendJSONresult = function ( - res: Response, - errors: HandlerMessage[] = [], - warnings: HandlerMessage[] = [], - info: HandlerMessage[] = [], - metadata: Record = {} -) { - delete metadata.file; - const wrapper = buildJSONresult(errors, warnings, info, metadata); - res.status(wrapper.success ? 200 : 400).json(wrapper); -}; +function sendErrors(res: Response, error: ExceptionsError | Error) { + res.status(error instanceof ExceptionsError ? 500 : 400).json({ + errors: + error instanceof ExceptionsError + ? error.exceptions.map(exception => ({ exception })) + : [{ error: error.toString() }], + info: [], + metadata: {}, + success: false, + warnings: [], + } satisfies ApiResult); +} const getFullUrl = (req: Request) => new URL(`${req.protocol}://${req.host}${req.url}`); @@ -88,21 +104,24 @@ const processPost = () => async (req: Request, res: Response) => { } }; -function createHandler(res: Response, metadataOverride?: Record) { - const handler = new EventEmitter(); - handler.on('exception', data => { - sendJSONresult(res, [data.message ? data.message : data]); - }); - handler.on('end-all', data => { - sendJSONresult( - res, - data.errors, - data.warnings, - data.info, - metadataOverride || data.metadata - ); - }); - return handler; +function handlePromise( + promise: Promise, + res: Response, + metadataOverride?: Record +) { + return promise.then( + result => { + sendResult( + res, + metadataOverride + ? { ...result, metadata: metadataOverride } + : result + ); + }, + (error: ExceptionsError) => { + sendErrors(res, error); + } + ); } const processRequest = async ( @@ -118,45 +137,42 @@ const processRequest = async ( required: shouldValidate ? ['profile'] : [], forbidden: ['source'], }); - } catch (err) { - return sendJSONresult(res, [err.toString()]); + } catch (error) { + return sendErrors(res, error); } if (shouldValidate && options.profile === 'auto') { const sr = new Specberus(); - const handler = new EventEmitter(); - handler.on('exception', data => { - sendJSONresult(res, [data.message ? data.message : data]); - }); - handler.on('end-all', async data => { - if (data.errors.length) sendJSONresult(res, data.errors); - else { - const meta = data.metadata; - if (options.url) meta.url = options.url; - else meta.file = options.file; - let metaOptions: ValidateOptions; - try { - metaOptions = await processParams(meta, undefined, { - allowUnknownParams: true, - }); - } catch (err) { - return sendJSONresult(res, [err.toString()]); - } - metaOptions.events = createHandler(res, meta); - - const metaSr = new Specberus(); - metaSr.validate(metaOptions); - } - }); - options.events = handler; - sr.extractMetadata(options); + const result = await sr + .extractMetadata(options) + .catch((error: ExceptionsError) => { + sendErrors(res, error); + }); + if (!result) return; + if (result.errors.length) return sendResult(res, result); + + const meta = result.metadata; + if (options.url) meta.url = options.url; + else meta.file = options.file; + let metaOptions: ValidateOptions; + try { + metaOptions = await processParams(meta, undefined, { + allowUnknownParams: true, + }); + } catch (error) { + return sendErrors(res, error); + } + + const metaSr = new Specberus(); + return handlePromise(metaSr.validate(metaOptions), res, meta); } else { - options.events = createHandler(res); options.additionalMetadata = req.query.additionalMetadata === 'true'; const sr = new Specberus(); - if (shouldValidate) sr.validate(options); - else sr.extractMetadata(options); + return handlePromise( + shouldValidate ? sr.validate(options) : sr.extractMetadata(options), + res + ); } }; diff --git a/lib/rules/echidna/deliverer-change.ts b/lib/rules/echidna/deliverer-change.ts index 74e41d9d4..2690e9eea 100644 --- a/lib/rules/echidna/deliverer-change.ts +++ b/lib/rules/echidna/deliverer-change.ts @@ -31,13 +31,11 @@ async function getPreviousDelivererIDs( * @param sr * @param done */ -export const check: RuleCheckFunction = async (sr, done) => { +export const check: RuleCheckFunction = async sr => { const previousVersion = await sr.getPreviousVersion(); const shortname = await sr.getShortname(); - if (!previousVersion || !shortname) { - return done(); - } + if (!previousVersion || !shortname) return; const previousDelivererIDs = await getPreviousDelivererIDs( shortname, @@ -55,6 +53,4 @@ export const check: RuleCheckFunction = async (sr, done) => { previous: previousDelivererIDs.sort().toString(), }); } - - done(); }; diff --git a/lib/rules/echidna/todays-date.ts b/lib/rules/echidna/todays-date.ts index c51027ece..60b202c5f 100644 --- a/lib/rules/echidna/todays-date.ts +++ b/lib/rules/echidna/todays-date.ts @@ -9,7 +9,7 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { /** * Get the timestamp of a day, regardless the time of the day. * This function creates a new `Date` to avoid modifying the original one. @@ -27,6 +27,4 @@ export const check: RuleCheckFunction = (sr, done) => { } else if (getDateTime(documentDate) !== getDateTime(new Date())) { sr.error(self, 'wrong-date'); } - - done(); }; diff --git a/lib/rules/headers/copyright.ts b/lib/rules/headers/copyright.ts index 67efb6e09..b22ff895c 100644 --- a/lib/rules/headers/copyright.ts +++ b/lib/rules/headers/copyright.ts @@ -148,31 +148,31 @@ function checkLatestCopyright( }); } -export const check: RuleCheckFunction = async (sr, done) => { +export const check: RuleCheckFunction = async sr => { const $copyright = sr.$('body div.head p.copyright').first(); if (!$copyright.length) { sr.error(self, 'not-found'); - return done(); + return; } if (await isOnlyPublishedByTagOrAb(sr)) { - return done(); + return; } const chartersData = await sr.getChartersData(); if (!chartersData || !chartersData.length) { sr.error(self, 'no-data-from-API'); - return done(); + return; } const allowedLicenses = getCommonLicenseUri(chartersData); if (!allowedLicenses.length && chartersData.length > 1) { sr.error(self, 'no-license-found-joint'); - return done(); + return; } if (!allowedLicenses.length) { sr.error(self, 'no-license-found'); - return done(); + return; } // licenseTexts: ['permissive document license'] or ['document use'] or ['document use', 'permissive document license'] @@ -191,6 +191,4 @@ export const check: RuleCheckFunction = async (sr, done) => { } else { checkLatestCopyright(sr, $copyright, licenseTexts); } - - return done(); }; diff --git a/lib/rules/headers/details-summary.ts b/lib/rules/headers/details-summary.ts index 721b47c03..eef51847d 100644 --- a/lib/rules/headers/details-summary.ts +++ b/lib/rules/headers/details-summary.ts @@ -10,11 +10,11 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const $details = sr.$('.head details').first(); if (!$details.length) { sr.error(self, 'no-details'); - return done(); + return; } if (!$details.attr('open')) { @@ -23,19 +23,17 @@ export const check: RuleCheckFunction = (sr, done) => { if (!sr.$('.head details dl').length) { sr.error(self, 'no-details-dl'); - return done(); + return; } const $summary = sr.$('.head details summary').first(); if (!$summary.length) { sr.error(self, 'no-details-summary'); - return done(); + return; } const summaryText = sr.norm($summary.text()); if (summaryText !== 'More details about this document') { sr.error(self, 'wrong-summary-text'); } - - return done(); }; diff --git a/lib/rules/headers/div-head.ts b/lib/rules/headers/div-head.ts index 70bf18c4a..7928d3f71 100644 --- a/lib/rules/headers/div-head.ts +++ b/lib/rules/headers/div-head.ts @@ -10,6 +10,6 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { - sr.checkSelector('body div.head', self, done); +export const check: RuleCheckFunction = sr => { + sr.checkSelector('body div.head', self); }; diff --git a/lib/rules/headers/dl.ts b/lib/rules/headers/dl.ts index fa0a2ad7b..3d6fa5243 100644 --- a/lib/rules/headers/dl.ts +++ b/lib/rules/headers/dl.ts @@ -69,7 +69,7 @@ function checkLink({ return true; } -export const check: RuleCheckFunction = async (sr, done) => { +export const check: RuleCheckFunction = async sr => { const { rescinds, status, submissionType } = sr.config!; let topLevel = 'TR'; @@ -271,6 +271,4 @@ export const check: RuleCheckFunction = async (sr, done) => { // should at least have 1 editor sr.error(editorError, 'editor-not-found'); } - - done(); }; diff --git a/lib/rules/headers/editor-participation.ts b/lib/rules/headers/editor-participation.ts index ae3645f21..0a9fd2ffb 100644 --- a/lib/rules/headers/editor-participation.ts +++ b/lib/rules/headers/editor-participation.ts @@ -10,7 +10,7 @@ const self: RuleMeta = { }; export const name = self.name; -export const check: RuleCheckFunction = async (sr, done) => { +export const check: RuleCheckFunction = async sr => { const groups = await sr.getDelivererIDs(); const editors = sr.extractHeaders()?.Editor; const editorsToCheck = []; @@ -52,6 +52,4 @@ export const check: RuleCheckFunction = async (sr, done) => { sr.error(self, 'not-participating', { id }); } }); - - done(); }; diff --git a/lib/rules/headers/errata.ts b/lib/rules/headers/errata.ts index 9c7ea70e2..5d3ea50de 100644 --- a/lib/rules/headers/errata.ts +++ b/lib/rules/headers/errata.ts @@ -19,17 +19,11 @@ function isRECWithChanges(sr: Specberus) { return Object.values(recMeta).length !== 0; } -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { // for REC with Candidate/Proposed changes, no need to check errata link - if (isRECWithChanges(sr)) { - return done(); - } + if (isRECWithChanges(sr)) return; const dts = sr.extractHeaders(); // Check 'Errata:' exist, don't check any further. - if (!dts.Errata) { - sr.error(self, 'no-errata'); - return done(); - } - return done(); + if (!dts.Errata) sr.error(self, 'no-errata'); }; diff --git a/lib/rules/headers/github-repo.ts b/lib/rules/headers/github-repo.ts index d64a5dd0e..b3588ea24 100644 --- a/lib/rules/headers/github-repo.ts +++ b/lib/rules/headers/github-repo.ts @@ -12,11 +12,11 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const dts = sr.extractHeaders(); if (!dts.Feedback) { sr.error(self, 'no-feedback'); - return done(); + return; } // Check 'github repo' exist in 'Feedback:' @@ -38,6 +38,4 @@ export const check: RuleCheckFunction = (sr, done) => { // ); }); if (!foundRepo) sr.error(self, 'no-repo'); - - done(); }; diff --git a/lib/rules/headers/h1-title.ts b/lib/rules/headers/h1-title.ts index 897c58060..7c2bf3319 100644 --- a/lib/rules/headers/h1-title.ts +++ b/lib/rules/headers/h1-title.ts @@ -10,7 +10,7 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const $title = sr.$('head > title').first(); const $h1 = sr.$('body div.head h1').first(); if (!$title.length || !$h1.length) { @@ -22,5 +22,4 @@ export const check: RuleCheckFunction = (sr, done) => { if (titleText !== h1Text) sr.error(self, 'not-match', { titleText, h1Text }); } - done(); }; diff --git a/lib/rules/headers/h2-toc.ts b/lib/rules/headers/h2-toc.ts index ee157b843..f7640f6bf 100644 --- a/lib/rules/headers/h2-toc.ts +++ b/lib/rules/headers/h2-toc.ts @@ -14,7 +14,7 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const EXPECTED_HEADING = /^table\s+of\s+contents$/i; const $tocNav = sr.$('nav#toc > h2'); const $tocDiv = sr.$('div#toc > h2'); @@ -36,6 +36,4 @@ export const check: RuleCheckFunction = (sr, done) => { if (matches > 1) sr.error(self, 'too-many'); else if (matches === 0) sr.error(self, 'not-found'); } - - done(); }; diff --git a/lib/rules/headers/hr.ts b/lib/rules/headers/hr.ts index 13d00527f..0271052fc 100644 --- a/lib/rules/headers/hr.ts +++ b/lib/rules/headers/hr.ts @@ -8,7 +8,7 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const hasHrLastChild = sr.$('body div.head > hr:last-child').length === 1; const hasHrNextSibling = sr.$('body div.head + hr').length === 1; if (hasHrLastChild && hasHrNextSibling) { @@ -16,5 +16,4 @@ export const check: RuleCheckFunction = (sr, done) => { } else if (!hasHrLastChild && !hasHrNextSibling) { sr.error(self, 'not-found'); } - done(); }; diff --git a/lib/rules/headers/logo.ts b/lib/rules/headers/logo.ts index 2ee978674..b713fa8aa 100644 --- a/lib/rules/headers/logo.ts +++ b/lib/rules/headers/logo.ts @@ -8,7 +8,7 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const $logo = sr.$("body div.head a[href] > img[src][alt='W3C']").first(); if ( !$logo.length || @@ -21,5 +21,4 @@ export const check: RuleCheckFunction = (sr, done) => { ) { sr.error(self, 'not-found'); } - done(); }; diff --git a/lib/rules/headers/memsub-copyright.ts b/lib/rules/headers/memsub-copyright.ts index 0016b32d1..c1fc87367 100644 --- a/lib/rules/headers/memsub-copyright.ts +++ b/lib/rules/headers/memsub-copyright.ts @@ -6,7 +6,7 @@ const self = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const $copyright = sr.$('body div.head p.copyright').first(); if ($copyright.length) { // , "https://www.w3.org/copyright/document-license/": "document use" @@ -21,5 +21,4 @@ export const check: RuleCheckFunction = (sr, done) => { ); if (!seen) sr.error(self, 'not-found'); } else sr.error(self, 'not-found'); - done(); }; diff --git a/lib/rules/headers/ol-toc.ts b/lib/rules/headers/ol-toc.ts index 83a4ea699..b31e4225f 100644 --- a/lib/rules/headers/ol-toc.ts +++ b/lib/rules/headers/ol-toc.ts @@ -13,10 +13,8 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const $toc = sr.$('nav#toc ol.toc, div#toc ol.toc'); if (!$toc.length) sr.warning(self, 'not-found'); - - done(); }; diff --git a/lib/rules/headers/secno.ts b/lib/rules/headers/secno.ts index c5c32c2f9..5c9606fe8 100644 --- a/lib/rules/headers/secno.ts +++ b/lib/rules/headers/secno.ts @@ -10,7 +10,7 @@ const self = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { // TODO: once supported, use: ":is(h2, h3, h4, h5, h6) :is(bdi.secno,span.secno)" const $secnos = sr.$( 'h1 span.secno, h2 span.secno, h3 span.secno, h4 span.secno, h5 span.secno, h6 span.secno, #toc span.secno,' + @@ -18,6 +18,4 @@ export const check: RuleCheckFunction = (sr, done) => { ); if (!$secnos.length) sr.warning(self, 'not-found'); - - done(); }; diff --git a/lib/rules/headers/shortname.ts b/lib/rules/headers/shortname.ts index 0c45d6322..45e181571 100644 --- a/lib/rules/headers/shortname.ts +++ b/lib/rules/headers/shortname.ts @@ -3,9 +3,6 @@ // latest version is https://www.w3.org/TR/shortname/ import superagent from 'superagent'; -// TODO(kgf): Replace with promisify? -// @ts-ignore (no typings) -import doAsync from 'doasync'; import type { RuleCheckFunction, RuleMeta } from '../../types.js'; const self: RuleMeta = { @@ -25,7 +22,7 @@ const historyError: RuleMeta = { }; export const name = self.name; -export const check: RuleCheckFunction = async (sr, done) => { +export const check: RuleCheckFunction = async sr => { let topLevel = 'TR'; if (sr.config!.submissionType === 'member') topLevel = 'submissions'; @@ -71,12 +68,11 @@ export const check: RuleCheckFunction = async (sr, done) => { if (dts.Latest) { const $linkLate = dts.Latest.$dd.find('a').first(); + const lateHref = $linkLate.attr('href'); - if ($linkLate.attr('href')) { + if (lateHref) { const lateRex = `^https:\\/\\/www\\.w3\\.org\\/${topLevel}\\/(.+?)\\/?$`; - const matches = ($linkLate.attr('href') || '') - .trim() - .match(new RegExp(lateRex)); + const matches = lateHref.trim().match(new RegExp(lateRex)); if (matches) { sn = matches[1]; // latest version link mention either shortlink or the series shortlink @@ -91,24 +87,22 @@ export const check: RuleCheckFunction = async (sr, done) => { if (dts.History) { const $linkHistory = dts.History.$dd.find('a').first(); + const historyHref = $linkHistory.attr('href'); - if ($linkHistory.attr('href')) { + if (historyHref) { // e.g. https://www.w3.org/standards/history/hr-time-10086/ const historyReg = /^https:\/\/www\.w3\.org\/standards\/history\/(.+?)\/?$/; - const matches = ($linkHistory.attr('href') || '') - .trim() - .match(historyReg); + const matches = historyHref.trim().match(historyReg); if (matches) { const [, historyShortname] = matches; if (historyShortname !== shortname) { sr.error(historyError, 'history-syntax', { shortname }); } else { - const historyHref = $linkHistory.attr('href'); // Check if the history link exist let historyStatusCode; try { - const res = await doAsync(superagent).head(historyHref); + const res = await superagent.head(historyHref); historyStatusCode = res.statusCode; } catch (err) { historyStatusCode = err.status; @@ -133,9 +127,7 @@ export const check: RuleCheckFunction = async (sr, done) => { let previousHistoryStatusCode; try { const res = - await doAsync(superagent).head( - previousHistoryHref - ); + await superagent.head(previousHistoryHref); previousHistoryStatusCode = res.statusCode; } catch (err) { previousHistoryStatusCode = err.status; @@ -156,9 +148,10 @@ export const check: RuleCheckFunction = async (sr, done) => { if (dts.Rescinds) { const $linkRescinds = dts.Rescinds.$dd.find('a').first(); + const rescindsHref = $linkRescinds.attr('href'); - if ($linkRescinds.attr('href')) { - const matches = ($linkRescinds.attr('href') || '') + if (rescindsHref) { + const matches = rescindsHref .trim() .match( /^https:\/\/www\.w3\.org\/TR\/\d{4}\/REC-(.+)-\d{8}\/?$/ @@ -197,6 +190,4 @@ export const check: RuleCheckFunction = async (sr, done) => { status: sr.config!.longStatus, }); } - - done(); }; diff --git a/lib/rules/headers/subm-logo.ts b/lib/rules/headers/subm-logo.ts index 4a6a4cbc8..eca771d75 100644 --- a/lib/rules/headers/subm-logo.ts +++ b/lib/rules/headers/subm-logo.ts @@ -6,7 +6,7 @@ const self = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const $logo = sr .$("body div.head a[href] > img[src][height='48'][width='211'][alt]") .first(); @@ -28,5 +28,4 @@ export const check: RuleCheckFunction = (sr, done) => { type: type.charAt(0).toUpperCase() + type.slice(1), }); } - done(); }; diff --git a/lib/rules/headers/translation.ts b/lib/rules/headers/translation.ts index 6699d2363..1ffe7265f 100644 --- a/lib/rules/headers/translation.ts +++ b/lib/rules/headers/translation.ts @@ -8,7 +8,7 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const translationLink = sr .$('body div.head a') .toArray() @@ -18,7 +18,7 @@ export const check: RuleCheckFunction = (sr, done) => { if (!translationLink) { sr.error(self, 'not-found'); - return done(); + return; } const href = translationLink.attribs.href; @@ -31,6 +31,4 @@ export const check: RuleCheckFunction = (sr, done) => { ) { sr.warning(self, 'not-recommended-link'); } - - done(); }; diff --git a/lib/rules/headers/w3c-state.ts b/lib/rules/headers/w3c-state.ts index 0e2fb1c12..0b3481b50 100644 --- a/lib/rules/headers/w3c-state.ts +++ b/lib/rules/headers/w3c-state.ts @@ -9,15 +9,15 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const config = sr.config!; let profileFound = false; - if (config.longStatus) return done(); + if (config.longStatus) return; const $stateEl = sr.getDocumentStateElement(); if (!$stateEl) { sr.error(self, 'no-w3c-state'); - return done(); + return; } const txt = sr.norm($stateEl.text()); // crType/cryType: Add 'Draft', 'Snapshot' suffix to title. @@ -61,6 +61,4 @@ export const check: RuleCheckFunction = (sr, done) => { }); } } - - done(); }; diff --git a/lib/rules/heuristic/date-format.ts b/lib/rules/heuristic/date-format.ts index 0807d5f5e..2197dd260 100644 --- a/lib/rules/heuristic/date-format.ts +++ b/lib/rules/heuristic/date-format.ts @@ -9,7 +9,7 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { // Pseudo-constants: const POSSIBLE_DATE = new RegExp( `\\b([0123]?\\d|${possibleMonths})[\\ \\-\\/–—]([0123]?\\d|${possibleMonths})[\\ \\-\\/–—]([\\'‘’]?\\d{2})(\\d\\d)?\\b`, @@ -38,6 +38,4 @@ export const check: RuleCheckFunction = (sr, done) => { sr.error(self, 'wrong', { text: date }); } } - - return done(); }; diff --git a/lib/rules/links/compound.ts b/lib/rules/links/compound.ts index b07455990..27e765a2a 100644 --- a/lib/rules/links/compound.ts +++ b/lib/rules/links/compound.ts @@ -1,3 +1,4 @@ +import type { ResponseError } from 'superagent'; import { get } from '../../throttled-ua.js'; import type { RuleCheckFunction } from '../../types.js'; @@ -8,13 +9,13 @@ const TIMEOUT = 10000; export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const { validation } = sr.config!; const url = sr.url!; if (validation !== 'recursive') { sr.warning(self, 'skipped'); - return done(); + return; } let links: string[] = []; @@ -29,13 +30,13 @@ export const check: RuleCheckFunction = (sr, done) => { // sort and remove duplicates links = links.sort().filter((item, pos) => !pos || item !== links[pos - 1]); + if (!links.length) return; + const markupService = 'https://validator.w3.org/nu/'; - let count = 0; - if (links.length > 0) { - links.forEach(l => { - if (validation === 'recursive') { + if (validation === 'recursive') { + return Promise.all( + links.map(l => { const ua = `W3C-Pubrules/${sr.version}`; - let isMarkupValid = false; const req = get(markupService) .set('User-Agent', ua) .query({ doc: l, out: 'json' }) @@ -45,43 +46,50 @@ export const check: RuleCheckFunction = (sr, done) => { link: l, errMsg: err, }); - count += 1; - }); - req.timeout(TIMEOUT); - req.end((err1, res) => { - if (err1 && err1.timeout === TIMEOUT) - sr.warning(self, 'html-timeout'); - const json = res ? res.body : null; - if (!json) return sr.throw('No JSON input.'); - if (json.messages) { - const errors = json.messages.filter( - (msg: { type: string }) => msg.type === 'error' - ); - if (errors.length === 0) isMarkupValid = true; - } - if (isMarkupValid) { - sr.info(self, 'link', { - file: l.split('/').pop(), - link: l, - markup: '\u2714', - }); - } else { - const details = { - file: l.split('/').pop(), - link: l, - markup: isMarkupValid ? '\u2714' : '\u2718', - }; - sr.error(self, 'link', details); + }) + .timeout(TIMEOUT); + return req.then( + res => { + const json = res.body; + if (!json) + throw new Error( + 'No JSON returned from HTML validator.' + ); + + const errors = + json.messages?.filter( + (msg: { type: string }) => msg.type === 'error' + ) || []; + if (errors.length === 0) { + sr.info(self, 'link', { + file: l.split('/').pop(), + link: l, + markup: '\u2714', + }); + } else { + sr.error(self, 'link', { + file: l.split('/').pop(), + link: l, + markup: '\u2718', + }); + } + }, + (err: ResponseError) => { + if (err.timeout) sr.warning(self, 'html-timeout'); + else + throw new Error( + `HTML validator error: ${err.message}` + ); } - count += 1; - if (count === links.length) return done(); - }); - } else { - sr.info(self, 'no-validation', { - file: l.split('/').pop(), - link: l, - }); - } - }); - } else return done(); + ); + }) + ).then(() => {}); + } else { + for (const l of links) { + sr.info(self, 'no-validation', { + file: l.split('/').pop(), + link: l, + }); + } + } }; diff --git a/lib/rules/links/internal.ts b/lib/rules/links/internal.ts index 89738b997..d817dfd5c 100644 --- a/lib/rules/links/internal.ts +++ b/lib/rules/links/internal.ts @@ -8,7 +8,7 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { sr.$("a[href^='#']").each((_, el) => { const id = el.attribs.href.replace('#', ''); const escId = id.replace(/([.()#:[\]+*])/g, '\\$1'); @@ -17,5 +17,4 @@ export const check: RuleCheckFunction = (sr, done) => { sr.error(self, 'anchor', { id }); } }); - done(); }; diff --git a/lib/rules/links/linkchecker.ts b/lib/rules/links/linkchecker.ts index 2ae6b8949..7bbb1f404 100644 --- a/lib/rules/links/linkchecker.ts +++ b/lib/rules/links/linkchecker.ts @@ -53,11 +53,11 @@ function includedByReg(url: string, regArray = allowList) { ); } -export const check: RuleCheckFunction = async (sr, done) => { +export const check: RuleCheckFunction = async sr => { // send out warning for /nu W3C link checker. sr.warning(self, 'display', { link: sr.url }); - if (!sr.url) return done(); + if (!sr.url) return; // sr.url is used as base url. Every other resource should use in same folder as base. e.g. // - spec doc: https://www.w3.org/TR/2021/WD-pubrules-20210401/ @@ -113,5 +113,4 @@ export const check: RuleCheckFunction = async (sr, done) => { await page.goto(sr.url, { waitUntil: 'load', timeout: 60000 }); await browser.close(); - done(); }; diff --git a/lib/rules/links/reliability.ts b/lib/rules/links/reliability.ts index 4c975f2b7..6b9430007 100644 --- a/lib/rules/links/reliability.ts +++ b/lib/rules/links/reliability.ts @@ -20,7 +20,7 @@ const unreliableServices = [ // { domain: 'w3.org', path: /track(er)?\/(actions|issues|resolutions)/} ]; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { sr.$('a').each((_, el) => { const $el = sr.$(el); const href = $el.attr('href'); @@ -54,5 +54,4 @@ export const check: RuleCheckFunction = (sr, done) => { return false; }); }); - done(); }; diff --git a/lib/rules/metadata/abstract.ts b/lib/rules/metadata/abstract.ts index fdc08f97e..9b6171a6d 100644 --- a/lib/rules/metadata/abstract.ts +++ b/lib/rules/metadata/abstract.ts @@ -7,26 +7,23 @@ import type { RuleCheckFunction } from '../../types.js'; export const name = 'metadata.abstract'; -export const check: RuleCheckFunction<{ abstract: string }> = (sr, done) => { +export const check: RuleCheckFunction<{ abstract: string }> = sr => { const abstractHeadingEl = sr .$('h2') .toArray() .find(el => sr.norm(sr.$(el).text()).toLowerCase() === 'abstract'); - if (abstractHeadingEl) { - const $div = load('
    ', null, false)('div'); - sr.$(abstractHeadingEl) - .parent() - .children() - .each((_, child) => { - { - if (child !== abstractHeadingEl) { - $div.append(child.cloneNode(true)); - } - } - }); - return done({ abstract: sr.norm($div.html()!) }); - } else { - return done({ abstract: 'Not found' }); - } + if (!abstractHeadingEl) return { abstract: 'Not found' }; + + const $div = load('
    ', null, false)('div'); + sr.$(abstractHeadingEl) + .parent() + .children() + .each((_, child) => { + { + if (child !== abstractHeadingEl) + $div.append(child.cloneNode(true)); + } + }); + return { abstract: sr.norm($div.html()!) }; }; diff --git a/lib/rules/metadata/charters.ts b/lib/rules/metadata/charters.ts index 290c7748f..8f8cfe6e4 100644 --- a/lib/rules/metadata/charters.ts +++ b/lib/rules/metadata/charters.ts @@ -10,4 +10,4 @@ export const name = 'metadata.charters'; export const check: RuleCheckFunction<{ charters: string[]; -}> = async (sr, done) => done({ charters: await sr.getCharters() }); +}> = async sr => ({ charters: await sr.getCharters() }); diff --git a/lib/rules/metadata/deliverers.ts b/lib/rules/metadata/deliverers.ts index 02355c41b..3d9c35e80 100644 --- a/lib/rules/metadata/deliverers.ts +++ b/lib/rules/metadata/deliverers.ts @@ -13,4 +13,4 @@ export const name = 'metadata.deliverers'; */ export const check: RuleCheckFunction<{ delivererIDs: number[]; -}> = async (sr, done) => done({ delivererIDs: await sr.getDelivererIDs() }); +}> = async sr => ({ delivererIDs: await sr.getDelivererIDs() }); diff --git a/lib/rules/metadata/dl.ts b/lib/rules/metadata/dl.ts index 7e3b08b19..e6541509e 100644 --- a/lib/rules/metadata/dl.ts +++ b/lib/rules/metadata/dl.ts @@ -17,13 +17,13 @@ interface DlMetadata { editorsDraft?: string | undefined; history?: string; latestVersion?: string; - previousVersion?: string | undefined; + previousVersion?: string | null | undefined; sameWorkAs?: string; thisVersion?: string; updated?: boolean; } -export const check: RuleCheckFunction = async (sr, done) => { +export const check: RuleCheckFunction = async sr => { const dts = sr.extractHeaders(); const result: DlMetadata = {}; let shortname; @@ -83,11 +83,17 @@ export const check: RuleCheckFunction = async (sr, done) => { const endpoint = `https://api.w3.org/specifications/${latestShortname}/versions/${formattedDate}`; const req = get(endpoint).set('User-Agent', ua); - req.end((err, res) => { - result.updated = !(err || !res.ok); - return done(result); - }); + return req.then( + res => { + result.updated = res.ok; + return result; + }, + () => { + result.updated = false; + return result; + } + ); } else { - sr.throw('[EXCEPTION] The document date could not be parsed.'); + throw new Error('The document date could not be parsed.'); } }; diff --git a/lib/rules/metadata/docDate.ts b/lib/rules/metadata/docDate.ts index 13fb1a153..362565869 100644 --- a/lib/rules/metadata/docDate.ts +++ b/lib/rules/metadata/docDate.ts @@ -11,10 +11,10 @@ interface DocDateMetadata { docDate: `${number}-${number}-${number}`; } -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const docDate = sr.getDocumentDate(); - if (!docDate) return done(); - return done({ - docDate: `${docDate.getFullYear()}-${docDate.getMonth() + 1}-${docDate.getDate()}`, - }); + if (docDate) + return { + docDate: `${docDate.getFullYear()}-${docDate.getMonth() + 1}-${docDate.getDate()}`, + }; }; diff --git a/lib/rules/metadata/editor-ids.ts b/lib/rules/metadata/editor-ids.ts index be969abbb..14509ddbc 100644 --- a/lib/rules/metadata/editor-ids.ts +++ b/lib/rules/metadata/editor-ids.ts @@ -18,7 +18,7 @@ interface EditorIDsMetadata { editorIDs: number[]; } -export const check: RuleCheckFunction = async (sr, done) => { +export const check: RuleCheckFunction = async sr => { const dts = sr.extractHeaders(); const editorIds: number[] = []; const unresolvedUsernames = []; @@ -50,12 +50,12 @@ export const check: RuleCheckFunction = async (sr, done) => { } // remove duplicates - done({ + return { editorIDs: editorIds.filter( (item, pos) => editorIds.indexOf(item) === pos ), - }); + }; } else { - done({ editorIDs: [] }); + return { editorIDs: [] }; } }; diff --git a/lib/rules/metadata/editor-names.ts b/lib/rules/metadata/editor-names.ts index 1dd97fb04..7556214ba 100644 --- a/lib/rules/metadata/editor-names.ts +++ b/lib/rules/metadata/editor-names.ts @@ -11,7 +11,7 @@ interface EditorNamesMetadata { editorNames: string[]; } -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const dts = sr.extractHeaders(); const editorNames: string[] = []; if (dts.Editor) { @@ -25,5 +25,5 @@ export const check: RuleCheckFunction = (sr, done) => { if (editor) editorNames.push(editor); }); } - return done({ editorNames }); + return { editorNames }; }; diff --git a/lib/rules/metadata/errata.ts b/lib/rules/metadata/errata.ts index 3d7a527a4..9da0d04a2 100644 --- a/lib/rules/metadata/errata.ts +++ b/lib/rules/metadata/errata.ts @@ -12,12 +12,12 @@ interface ErrataMetadata { errata: string; } -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const errataRegex = /errata/i; const $links = sr.$('body div.head details + p > a'); const errata = $links .toArray() .filter(el => errataRegex.test(sr.$(el).text())); - if (!errata.length || !errata[0].attribs.href) done(); - else done({ errata: errata[0].attribs.href }); + if (errata.length && errata[0].attribs.href) + return { errata: errata[0].attribs.href }; }; diff --git a/lib/rules/metadata/informative.ts b/lib/rules/metadata/informative.ts index 4414e5a35..d942d1f96 100644 --- a/lib/rules/metadata/informative.ts +++ b/lib/rules/metadata/informative.ts @@ -11,19 +11,16 @@ interface InformativeMetadata { informative: boolean; } -export const check: RuleCheckFunction = ( - sr, - done -) => { +export const check: RuleCheckFunction = sr => { const $sotd = sr.getSotDSection(); const expected = /This\s+document\s+is\s+informative\s+only\./; - if (!$sotd) return done(); + if (!$sotd) return; const $stateEl = sr.getDocumentStateElement(); const candidate = $stateEl && sr.norm($stateEl.text()).toLowerCase(); const isInformative = !!candidate && candidate.indexOf('group note') !== -1; - return done({ + return { informative: expected.test($sotd && $sotd.text()) || isInformative, - }); + }; }; diff --git a/lib/rules/metadata/process.ts b/lib/rules/metadata/process.ts index f3ec1f638..abb26debf 100644 --- a/lib/rules/metadata/process.ts +++ b/lib/rules/metadata/process.ts @@ -16,11 +16,10 @@ interface ProcessMetadata { process: string; } -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const $processDocument = sr.$('a#w3c_process_revision').first(); const processDocumentHref = $processDocument.length && $processDocument.attr('href'); - if (!processDocumentHref) return done(); - return done({ process: processDocumentHref }); + if (processDocumentHref) return { process: processDocumentHref }; }; diff --git a/lib/rules/metadata/profile.ts b/lib/rules/metadata/profile.ts index 1c4b37fdd..5885b43ea 100644 --- a/lib/rules/metadata/profile.ts +++ b/lib/rules/metadata/profile.ts @@ -6,7 +6,6 @@ import type { Cheerio } from 'cheerio'; import type { Element } from 'domhandler'; import { allProfiles } from '../../util.js'; -import type { Specberus } from '../../validator.js'; import { sortedProfiles } from '../../views.js'; import type { RecMetadata, RuleCheckFunction } from '../../types.js'; import { check as getTitle } from './title.js'; @@ -16,7 +15,7 @@ import rules from '../../rules-track.js'; // 'self.name' would be 'metadata.profile' export const name = 'metadata.profile'; -export const check: RuleCheckFunction = async (sr, done) => { +export const check: RuleCheckFunction = async sr => { let matchedLength = 0; let id; let $profileEl: Cheerio | undefined; @@ -28,7 +27,7 @@ export const check: RuleCheckFunction = async (sr, done) => { const $stateEl = sr.getDocumentStateElement(); if (!$stateEl) { - sr.throw( + throw new Error( 'Cannot find the <p id="w3c-state"> element for profile and date.

    Please make sure the <p id="w3c-state">W3C @@Profile, DD Month Year</p> element can be selected by document.getElementById(\'w3c-state\');
    If you are using bikeshed, please update to the latest version.' ); } @@ -49,13 +48,13 @@ export const check: RuleCheckFunction = async (sr, done) => { } } - function assembleMeta(id: string, sr: Specberus) { + function assembleMeta(id: string) { let meta: RecMetadata = { profile: id }; if (id in reviewStatus) { const dueDate = sr.getFeedbackDueDate(); const dates = dueDate && dueDate.valid; let res = dates[0]; - if (dates.length === 0 || !res) return done({ profile: id }); + if (dates.length === 0 || !res) return { profile: id }; if (dates.length > 1) res = new Date(Math.min(...dates.map(d => +d))); @@ -90,7 +89,7 @@ export const check: RuleCheckFunction = async (sr, done) => { const track = profileMatch && profileMatch[profileMatch.length - 2]; meta.rectrack = track; - return done(meta); + return meta; } function checkRecType() { @@ -104,6 +103,7 @@ export const check: RuleCheckFunction = async (sr, done) => { } return 'REC'; } + if (id) { // W3C Candidate Recommendation (CR before 2020/CR snapshot/CR draft), W3C Recommendation will have "REC" if (id === 'REC' || id === 'CR') { @@ -118,15 +118,12 @@ export const check: RuleCheckFunction = async (sr, done) => { ? 'CRYD' : 'CRY'; } - assembleMeta(id, sr); + return assembleMeta(id); } else { - let docTitle; - await getTitle(sr, result => { - docTitle = result && result.title; - }); + const docTitle = (await getTitle(sr))?.title; if (candidate && candidate.indexOf("editor's draft") > -1) { - sr.throw( - `[EXCEPTION] The document "${docTitle}" seems to be an Editor's Draft, which is not supported.` + throw new Error( + `The document "${docTitle}" seems to be an Editor's Draft, which is not supported.` ); } else if ( sr.$('link[href^="https://www.w3.org/StyleSheets/TR/"]').length @@ -139,12 +136,12 @@ export const check: RuleCheckFunction = async (sr, done) => { }); profileList += '
'; }); - sr.throw( + throw new Error( `Pubrules is having a hard time identifying the profile of the document "${docTitle}" from its w3c-state element; Please make sure the <p id="w3c-state"> is in a valid format:
<p id="w3c-state">W3C @@type of the document@@ DD Month YYYY</p>
${profileList}` ); } else { - sr.throw( - `[EXCEPTION] The document "${docTitle}" could not be parsed, it's neither a TR document nor a Member Submission.` + throw new Error( + `The document "${docTitle}" could not be parsed, it's neither a TR document nor a Member Submission.` ); } } diff --git a/lib/rules/metadata/sotd.ts b/lib/rules/metadata/sotd.ts index f1cb27ca6..bb2092a30 100644 --- a/lib/rules/metadata/sotd.ts +++ b/lib/rules/metadata/sotd.ts @@ -10,7 +10,7 @@ interface SotdMetadata { sotd: string; } -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const $sotd = sr.getSotDSection(); - return done({ sotd: $sotd ? sr.norm($sotd.html()!) : 'Not found' }); + return { sotd: $sotd ? sr.norm($sotd.html()!) : 'Not found' }; }; diff --git a/lib/rules/metadata/title.ts b/lib/rules/metadata/title.ts index 9efc569d5..414e9e434 100644 --- a/lib/rules/metadata/title.ts +++ b/lib/rules/metadata/title.ts @@ -8,15 +8,12 @@ import type { RuleCheckFunction } from '../../types.js'; export const name = 'metadata.title'; -export const check: RuleCheckFunction<{ title: string } | void> = ( - sr, - done -) => { +export const check: RuleCheckFunction<{ title: string } | void> = sr => { const $title = sr.$('body div.head h1').first(); - if (!$title.length) return done(); + if (!$title.length) return; $title.html($title.html()!.replace(/:
/g, ': ').replace(/
/g, ' - ')); - return done({ + return { title: sr.norm($title.text()), - }); + }; }; diff --git a/lib/rules/sotd/candidate-review-end.ts b/lib/rules/sotd/candidate-review-end.ts index 94c07d2d4..ec4103e7b 100644 --- a/lib/rules/sotd/candidate-review-end.ts +++ b/lib/rules/sotd/candidate-review-end.ts @@ -8,7 +8,7 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const isEditorial = (sr.config!.editorial && /^true$/i.test(sr.config!.editorial)) || false; if (isEditorial) { @@ -33,5 +33,4 @@ export const check: RuleCheckFunction = (sr, done) => { } } } - done(); }; diff --git a/lib/rules/sotd/charter.ts b/lib/rules/sotd/charter.ts index 3607d670f..5b8969b3a 100644 --- a/lib/rules/sotd/charter.ts +++ b/lib/rules/sotd/charter.ts @@ -13,14 +13,14 @@ export const { name } = self; const charterText = /The disclosure obligations of the Participants of this group are described in the charter\./; -export const check: RuleCheckFunction = async (sr, done) => { +export const check: RuleCheckFunction = async sr => { const $sotd = sr.getSotDSection(); if ($sotd) { const deliverIds = await sr.getDelivererIDs(); if (!deliverIds.length) { sr.error(self, 'no-group'); - return done(); + return; } // Skip check if the document is only published by TAG and/or AB @@ -30,12 +30,12 @@ export const check: RuleCheckFunction = async (sr, done) => { const groupIds = deliverIds.filter( deliverer => !([TagID, AbID] as number[]).includes(deliverer) ); - if (!groupIds.length) return done(); + if (!groupIds.length) return; const charters = await sr.getCharters(); if (!charters.length) { sr.error(self, 'no-charter'); - return done(); + return; } if (sr.config!.longStatus === 'Interest Group Note') { @@ -44,7 +44,7 @@ export const check: RuleCheckFunction = async (sr, done) => { const txt = sr.norm($sotd && $sotd.text()); if (!charterText.test(txt)) { sr.error(self, 'text-not-found'); - return done(); + return; } // check "charter" link is found and correct @@ -69,7 +69,5 @@ export const check: RuleCheckFunction = async (sr, done) => { }); } } - - return done(); } }; diff --git a/lib/rules/sotd/deliverer-note.ts b/lib/rules/sotd/deliverer-note.ts index b36360e30..4fb0bb0b0 100644 --- a/lib/rules/sotd/deliverer-note.ts +++ b/lib/rules/sotd/deliverer-note.ts @@ -8,9 +8,7 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const deliverers = sr.getDataDelivererIDs(); - if (deliverers.length === 0) sr.error(self, 'not-found'); - done(); }; diff --git a/lib/rules/sotd/deployment.ts b/lib/rules/sotd/deployment.ts index 8d4b1ef9c..be0a0a56f 100644 --- a/lib/rules/sotd/deployment.ts +++ b/lib/rules/sotd/deployment.ts @@ -10,7 +10,7 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const $sotd = sr.getSotDSection(); if ($sotd) { @@ -21,10 +21,6 @@ export const check: RuleCheckFunction = (sr, done) => { .find('p') .toArray() .find(p => sr.norm(sr.$(p).text()) === depText); - if (!paragraph) { - sr.error(self, 'not-found'); - return done(); - } + if (!paragraph) sr.error(self, 'not-found'); } - return done(); }; diff --git a/lib/rules/sotd/diff.ts b/lib/rules/sotd/diff.ts index 80e7e120d..976c8e71b 100644 --- a/lib/rules/sotd/diff.ts +++ b/lib/rules/sotd/diff.ts @@ -8,7 +8,6 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { sr.info(self, 'note'); - return done(); }; diff --git a/lib/rules/sotd/draft-stability.ts b/lib/rules/sotd/draft-stability.ts index b8719d011..5512330b3 100644 --- a/lib/rules/sotd/draft-stability.ts +++ b/lib/rules/sotd/draft-stability.ts @@ -10,7 +10,7 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const $sotd = sr.getSotDSection(); const { crType, cryType } = sr.config!; const STABILITY_REX = @@ -32,12 +32,10 @@ export const check: RuleCheckFunction = (sr, done) => { expected2: STABILITY_2, }); } - // while other profiles allows only 'STABILITY' sentence else if (!txt.match(STABILITY_REX)) sr.error(self, 'not-found', { expected: STABILITY_REX, }); } - done(); }; diff --git a/lib/rules/sotd/new-features.ts b/lib/rules/sotd/new-features.ts index 0b34fb5dc..a94e320fa 100644 --- a/lib/rules/sotd/new-features.ts +++ b/lib/rules/sotd/new-features.ts @@ -8,7 +8,7 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const $sotd = sr.getSotDSection(); const docType = `${sr.config!.status !== 'REC' ? 'upcoming ' : ''}Recommendation`; const warning = new RegExp( @@ -33,5 +33,4 @@ export const check: RuleCheckFunction = (sr, done) => { } else { sr.warning(self, 'no-warning'); } - done(); }; diff --git a/lib/rules/sotd/obsl-rescind.ts b/lib/rules/sotd/obsl-rescind.ts index 71f10d0b3..27b39e2a6 100644 --- a/lib/rules/sotd/obsl-rescind.ts +++ b/lib/rules/sotd/obsl-rescind.ts @@ -37,7 +37,7 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const $sotd = sr.getSotDSection(); if ($sotd) { const $rationale = @@ -58,5 +58,4 @@ export const check: RuleCheckFunction = (sr, done) => { } } } - done(); }; diff --git a/lib/rules/sotd/pp.ts b/lib/rules/sotd/pp.ts index 2161d03c5..ca32d3a08 100644 --- a/lib/rules/sotd/pp.ts +++ b/lib/rules/sotd/pp.ts @@ -76,7 +76,7 @@ function findPP($candidates: Cheerio, sr: Specberus) { export const { name } = self; -export const check: RuleCheckFunction = async (sr, done) => { +export const check: RuleCheckFunction = sr => { const $sotd = sr.getSotDSection(); const track = sr.config!.track; const isRecTrack = track === 'Recommendation'; @@ -88,7 +88,7 @@ export const check: RuleCheckFunction = async (sr, done) => { ); if (!$pp) { sr.error(self, 'no-pp', { expected }); - return done(); + return; } let foundLink = false; @@ -154,6 +154,5 @@ export const check: RuleCheckFunction = async (sr, done) => { sr.error(self, 'no-section6', { link: `${ppLink}#sec-Disclosure`, }); - return done(); } }; diff --git a/lib/rules/sotd/process-document.ts b/lib/rules/sotd/process-document.ts index cc3c6d83d..a195cb5c8 100644 --- a/lib/rules/sotd/process-document.ts +++ b/lib/rules/sotd/process-document.ts @@ -8,7 +8,7 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const $sotd = sr.getSotDSection(); const BOILERPLATE_PREFIX = 'This document is governed by the '; const BOILERPLATE_SUFFIX = ' W3C Process Document.'; @@ -70,5 +70,4 @@ export const check: RuleCheckFunction = (sr, done) => { }); if (!found) sr.error(self, 'not-found', { process: newProc }); } - done(); }; diff --git a/lib/rules/sotd/publish.ts b/lib/rules/sotd/publish.ts index 5ad732659..8d43cc9f3 100644 --- a/lib/rules/sotd/publish.ts +++ b/lib/rules/sotd/publish.ts @@ -8,7 +8,7 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = async (sr, done) => { +export const check: RuleCheckFunction = async sr => { const $sotd = sr.getSotDSection(); const { crType, cryType, longStatus, status, track } = sr.config!; let docType = longStatus; @@ -29,7 +29,7 @@ export const check: RuleCheckFunction = async (sr, done) => { .find(p => sr.norm(sr.$(p).text()).match(publishReg)); if (!paragraph) { sr.error(self, 'not-found', { publishReg }); - return done(); + return; } const $sotdLinks = $sotd.find('a[href]'); @@ -144,5 +144,4 @@ export const check: RuleCheckFunction = async (sr, done) => { }); } } - done(); }; diff --git a/lib/rules/sotd/rec-addition.ts b/lib/rules/sotd/rec-addition.ts index 005ee8190..99686f676 100644 --- a/lib/rules/sotd/rec-addition.ts +++ b/lib/rules/sotd/rec-addition.ts @@ -55,7 +55,7 @@ function checkSection(sr: Specberus, options: CheckSectionOptions) { }); } -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const $sotd = sr.getSotDSection(); if ($sotd) { const recType = sr.getRecMetadata(); @@ -100,5 +100,4 @@ export const check: RuleCheckFunction = (sr, done) => { sectionClass: 'addition', }); } - return done(); }; diff --git a/lib/rules/sotd/rec-comment-end.ts b/lib/rules/sotd/rec-comment-end.ts index 8b671920e..bdb02948c 100644 --- a/lib/rules/sotd/rec-comment-end.ts +++ b/lib/rules/sotd/rec-comment-end.ts @@ -9,7 +9,7 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const $sotd = sr.getSotDSection(); if ($sotd) { const recType = sr.getRecMetadata(); @@ -54,5 +54,4 @@ export const check: RuleCheckFunction = (sr, done) => { } } } - done(); }; diff --git a/lib/rules/sotd/stability.ts b/lib/rules/sotd/stability.ts index ec80f06d8..0713b02ae 100644 --- a/lib/rules/sotd/stability.ts +++ b/lib/rules/sotd/stability.ts @@ -74,7 +74,7 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = async (sr, done) => { +export const check: RuleCheckFunction = async sr => { const { crType, cryType, status } = sr.config!; const $sotd = sr.getSotDSection(); if ($sotd) { @@ -124,5 +124,4 @@ export const check: RuleCheckFunction = async (sr, done) => { }); } } - return done(); }; diff --git a/lib/rules/sotd/submission.ts b/lib/rules/sotd/submission.ts index 39b71a509..7dbf55729 100644 --- a/lib/rules/sotd/submission.ts +++ b/lib/rules/sotd/submission.ts @@ -34,7 +34,7 @@ function findSubmText($candidates: Cheerio, sr: Specberus) { return null; } -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const $sotd = sr.getSotDSection(); if ($sotd) { const $st = @@ -42,7 +42,7 @@ export const check: RuleCheckFunction = (sr, done) => { findSubmText($sotd.find('p'), sr); if (!$st) { sr.error(self, 'no-submission-text'); - return done(); + return; } // check the links @@ -119,5 +119,4 @@ export const check: RuleCheckFunction = (sr, done) => { if (!foundSubmMembers) sr.error(self, 'no-sm-link'); if (!foundComment) sr.error(self, 'no-tc-link'); } - done(); }; diff --git a/lib/rules/sotd/supersedable.ts b/lib/rules/sotd/supersedable.ts index 3d5dca007..5b6c9f54c 100644 --- a/lib/rules/sotd/supersedable.ts +++ b/lib/rules/sotd/supersedable.ts @@ -15,7 +15,7 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const $sotd = sr.getSotDSection(); if ($sotd) { let $em = $sotd.filter('p').children('em').first(); @@ -43,5 +43,4 @@ export const check: RuleCheckFunction = (sr, done) => { const $a = $em.find("a[href='https://www.w3.org/TR/']"); if (!$a.length) sr.error(self, 'no-sotd-tr'); } - done(); }; diff --git a/lib/rules/sotd/usage.ts b/lib/rules/sotd/usage.ts index ad5e2a4ce..93bd4e4cf 100644 --- a/lib/rules/sotd/usage.ts +++ b/lib/rules/sotd/usage.ts @@ -10,7 +10,7 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const $sotd = sr.getSotDSection(); if ($sotd) { @@ -20,10 +20,6 @@ export const check: RuleCheckFunction = (sr, done) => { .find('p') .toArray() .find(p => sr.norm(sr.$(p).text()) === usageText); - if (!paragraph) { - sr.error(self, 'not-found'); - return done(); - } + if (!paragraph) sr.error(self, 'not-found'); } - return done(); }; diff --git a/lib/rules/structure/canonical.ts b/lib/rules/structure/canonical.ts index 196a9928f..af4fd8257 100644 --- a/lib/rules/structure/canonical.ts +++ b/lib/rules/structure/canonical.ts @@ -8,7 +8,7 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const checkCanonical = function () { const $lnk = sr.$('head > link[rel=canonical]').first(); if (!$lnk.length || !$lnk.attr('href')) sr.error(self, 'not-found'); @@ -21,6 +21,4 @@ export const check: RuleCheckFunction = (sr, done) => { doMeanwhile: () => {}, doAfter: checkCanonical, }); - - done(); }; diff --git a/lib/rules/structure/display-only.ts b/lib/rules/structure/display-only.ts index bf6b52ac0..3190466e3 100644 --- a/lib/rules/structure/display-only.ts +++ b/lib/rules/structure/display-only.ts @@ -2,7 +2,7 @@ import type { RuleCheckFunction } from '../../types.js'; export const name = 'structure.display-only'; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { if (sr.config!.status !== 'DISC') sr.info( { name, section: 'document-status', rule: 'customParagraph' }, @@ -19,5 +19,4 @@ export const check: RuleCheckFunction = (sr, done) => { sr.info({ name }, 'special-box-markup'); sr.info({ name }, 'index-list-tables'); sr.info({ name }, 'fit-in-a4'); - done(); }; diff --git a/lib/rules/structure/h2.ts b/lib/rules/structure/h2.ts index cf5c70a64..c8844ea08 100644 --- a/lib/rules/structure/h2.ts +++ b/lib/rules/structure/h2.ts @@ -18,7 +18,7 @@ const toc = { rule: 'toc', }; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const h2s: string[] = []; sr.$('h2').each((_, h2) => { const $h2 = sr.$(h2); @@ -31,5 +31,4 @@ export const check: RuleCheckFunction = (sr, done) => { // cspell:disable-next-line if (!/^Table [Oo]f [Cc]ontents$/.test(h2s[2])) sr.error(toc, 'toc', { was: h2s[2] }); - done(); }; diff --git a/lib/rules/structure/name.ts b/lib/rules/structure/name.ts index 7570e6619..dff188203 100644 --- a/lib/rules/structure/name.ts +++ b/lib/rules/structure/name.ts @@ -10,7 +10,7 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = async sr => { // Pseudo-constants: const EXPECTED_NAME = /\/Overview\.html$/; const OVERVIEW = 'Overview.html'; @@ -20,7 +20,7 @@ export const check: RuleCheckFunction = (sr, done) => { let fileName; if (!sr || !sr.url || EXPECTED_NAME.test(sr.url)) { - return done(); + return; } if (!ALTERNATIVE_ENDING.test(sr.url)) { @@ -33,21 +33,15 @@ export const check: RuleCheckFunction = (sr, done) => { } else { sr.warning(self, 'wrong', { note: '' }); } - return done(); + return; } - superagent.get(sr.url).end((_, result1) => { - superagent.get(sr.url + OVERVIEW).end((_, result2) => { - if ( - !result1 || - !result2 || - !result1.ok || - !result2.ok || - result1.text !== result2.text - ) { - sr.warning(self, 'wrong', { note: '' }); - } - return done(); - }); - }); + try { + const result1 = await superagent.get(sr.url); + const result2 = await superagent.get(sr.url + OVERVIEW); + if (!result1.ok || !result2.ok || result1.text !== result2.text) + sr.warning(self, 'wrong', { note: '' }); + } catch (error) { + sr.warning(self, 'wrong', { note: '' }); + } }; diff --git a/lib/rules/structure/neutral.ts b/lib/rules/structure/neutral.ts index 9174d18ff..db92c69de 100644 --- a/lib/rules/structure/neutral.ts +++ b/lib/rules/structure/neutral.ts @@ -17,7 +17,7 @@ for (const item of badterms) { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const blocklistReg = new RegExp(`\\b${blocklist.join('\\b|\\b')}\\b`, 'ig'); const unneutralList: string[] = []; // Use a cloned body instead of the original one, prevent '.remove()' side effects. @@ -33,17 +33,13 @@ export const check: RuleCheckFunction = (sr, done) => { } }); - const text = $body.text(); - const regResult = text.match(blocklistReg); - if (regResult) { + const regResult = $body.text().match(blocklistReg); + if (regResult) regResult.forEach(word => { if (unneutralList.indexOf(word.toLowerCase()) < 0) { unneutralList.push(word.toLowerCase()); } }); - } - if (unneutralList.length) { + if (unneutralList.length) sr.warning(self, 'neutral', { words: unneutralList.join('", "') }); - } - done(); }; diff --git a/lib/rules/structure/section-ids.ts b/lib/rules/structure/section-ids.ts index d129e12a2..34b8ede7f 100644 --- a/lib/rules/structure/section-ids.ts +++ b/lib/rules/structure/section-ids.ts @@ -8,7 +8,7 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const $ignoreH3 = sr.$('.head > h3').first(); sr.$('h2, h3, h4, h5, h6').each((_, el) => { @@ -43,5 +43,4 @@ export const check: RuleCheckFunction = (sr, done) => { sr.error(self, 'no-id', { text: el.name }); }); - done(); }; diff --git a/lib/rules/structure/security-privacy.ts b/lib/rules/structure/security-privacy.ts index bc1b00617..6e985d4ff 100644 --- a/lib/rules/structure/security-privacy.ts +++ b/lib/rules/structure/security-privacy.ts @@ -8,7 +8,7 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { let security = false; let privacy = false; @@ -30,6 +30,4 @@ export const check: RuleCheckFunction = (sr, done) => { if (!privacy) sr.warning(self, 'no-privacy'); } - - done(); }; diff --git a/lib/rules/style/back-to-top.ts b/lib/rules/style/back-to-top.ts index fd3897d1b..1998c2b76 100644 --- a/lib/rules/style/back-to-top.ts +++ b/lib/rules/style/back-to-top.ts @@ -10,12 +10,10 @@ const self = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const $candidates = sr.$( "body p#back-to-top[role='navigation'] a[href='#title']" ); if ($candidates.length !== 1) sr.warning(self, 'not-found'); - - done(); }; diff --git a/lib/rules/style/body-toc-sidebar.ts b/lib/rules/style/body-toc-sidebar.ts index 427d82f0f..da173f230 100644 --- a/lib/rules/style/body-toc-sidebar.ts +++ b/lib/rules/style/body-toc-sidebar.ts @@ -6,11 +6,10 @@ const self = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { try { if (sr.$('body').hasClass('toc-sidebar')) sr.error(self, 'class-found'); } catch (e) { sr.error(self, 'selector-fail'); } - done(); }; diff --git a/lib/rules/style/meta.ts b/lib/rules/style/meta.ts index 9ac6c0cf0..061999236 100644 --- a/lib/rules/style/meta.ts +++ b/lib/rules/style/meta.ts @@ -17,7 +17,7 @@ export const { name } = self; const width = /^device-width$/i; const shrinkToFit = /^no$/i; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const $meta = sr.$("head > meta[name='viewport'][content]"); if ($meta.length !== 1) { sr.error(self, 'not-found'); @@ -40,5 +40,4 @@ export const check: RuleCheckFunction = (sr, done) => { sr.error(self, 'not-found'); } } - done(); }; diff --git a/lib/rules/style/script.ts b/lib/rules/style/script.ts index 3dae6c57a..fd3d4ccaa 100644 --- a/lib/rules/style/script.ts +++ b/lib/rules/style/script.ts @@ -10,7 +10,7 @@ const self = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const PATTERN_SCRIPT = /^(https?:)?\/\/(www\.)?w3\.org\/scripts\/tr\/2021\/fixup\.js$/i; @@ -22,6 +22,4 @@ export const check: RuleCheckFunction = (sr, done) => { }); if (found !== 1) sr.error(self, 'not-found'); - - done(); }; diff --git a/lib/rules/style/sheet.ts b/lib/rules/style/sheet.ts index 215a95ba1..b80ea1fa6 100644 --- a/lib/rules/style/sheet.ts +++ b/lib/rules/style/sheet.ts @@ -13,9 +13,9 @@ const notLast = { rule: 'lastStylesheet', }; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const { styleSheet } = sr.config!; - if (!styleSheet) return done(); + if (!styleSheet) return; const url = `https://www.w3.org/StyleSheets/TR/2021/${styleSheet}`; const dark = 'https://www.w3.org/StyleSheets/TR/2021/dark'; const stylesheetLinks = [ @@ -35,5 +35,4 @@ export const check: RuleCheckFunction = (sr, done) => { sr.error(notLast, 'last'); } } - done(); }; diff --git a/lib/rules/validation/html.ts b/lib/rules/validation/html.ts index ab1cd7140..6771981d1 100644 --- a/lib/rules/validation/html.ts +++ b/lib/rules/validation/html.ts @@ -1,3 +1,4 @@ +import type { ResponseError } from 'superagent'; import { get, post } from '../../throttled-ua.js'; import type { RuleCheckFunction, RuleMeta } from '../../types.js'; @@ -10,16 +11,16 @@ const TIMEOUT = 10000; export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { const { htmlValidator, skipValidation } = sr.config!; const service = htmlValidator || 'https://validator.w3.org/nu/'; if (skipValidation) { sr.warning(self, 'skipped'); - return done(); + return; } if (!sr.url && !sr.source) { sr.warning(self, 'no-source'); - return done(); + return; } let req; const ua = `W3C-Pubrules/${sr.version}`; @@ -34,18 +35,13 @@ export const check: RuleCheckFunction = (sr, done) => { .query({ out: 'json' }); } req.timeout(TIMEOUT); - req.end((err, res) => { - if (err) { - if (err.timeout === TIMEOUT) { - sr.warning(self, 'timeout'); - } else { - sr.error(self, 'no-response'); - } - } else if (!res.ok) { - sr.error(self, 'failure', { status: res.status }); - } else { + return req.then( + res => { + if (!res.ok) + return sr.error(self, 'failure', { status: res.status }); + const json = res.body; - if (!json) return sr.throw('No JSON input.'); + if (!json) throw new Error('No JSON returned from HTML validator.'); if (json.messages && json.messages.length) { for (let i = 0, n = json.messages.length; i < n; i += 1) { @@ -95,7 +91,10 @@ export const check: RuleCheckFunction = (sr, done) => { } } } + }, + (err: ResponseError) => { + if (err.timeout) sr.warning(self, 'timeout'); + else sr.error(self, 'no-response'); } - done(); - }); + ); }; diff --git a/lib/rules/validation/wcag.ts b/lib/rules/validation/wcag.ts index 46f7434a5..9d82ecdb4 100644 --- a/lib/rules/validation/wcag.ts +++ b/lib/rules/validation/wcag.ts @@ -8,7 +8,6 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = (sr, done) => { +export const check: RuleCheckFunction = sr => { sr.info(self, 'tools'); - return done(); }; diff --git a/lib/types.d.ts b/lib/types.d.ts index 549278ef2..73bed261b 100644 --- a/lib/types.d.ts +++ b/lib/types.d.ts @@ -41,7 +41,13 @@ export interface SpecberusConfig { validation?: ValidateOptions['validation']; } -export type HandlerMessage = Record; +/** Data types emitted by error, warning, and info events */ +export type HandlerMessage = RuleBase & + Partial & { + detailMessage: string; + key: string; + extra?: Record; + }; type IsoDateString = `${number}-${number}-${number}`; @@ -58,10 +64,7 @@ export interface RecMetadata { rectrack?: string | null; } -export type RuleCheckFunction = ( - sr: Specberus, - done: (result: R) => void -) => void | Promise; +export type RuleCheckFunction = (sr: Specberus) => R | Promise; export interface RuleBase { name: string; diff --git a/lib/util.ts b/lib/util.ts index b2824265f..1c61adf1e 100644 --- a/lib/util.ts +++ b/lib/util.ts @@ -11,32 +11,13 @@ import { Octokit } from '@octokit/core'; // @ts-ignore (no typings) import w3cApi from 'node-w3capi'; -import type { HandlerMessage, SpecberusConfig } from './types.js'; +import type { SpecberusConfig } from './types.js'; import type { ValidateOptions } from './validator.js'; import pkg from '../package.json' with { type: 'json' }; /** Current specberus version recorded in package.json */ export const specberusVersion = pkg.version; -/** - * Builds a JSON result (of validation, metadata extraction, etc). - */ - -export const buildJSONresult = function ( - errors: HandlerMessage[], - warnings: HandlerMessage[], - info: HandlerMessage[], - metadata: Record -) { - return { - success: !errors.length, - errors, - warnings, - info, - metadata, - }; -}; - const __dirname = dirname(fileURLToPath(import.meta.url)); async function readModuleBasenames(dir: string) { @@ -100,12 +81,14 @@ interface ProcessParamsConstraints { /** * Build an “options” object based on an HTTP query string or a similar object containing options. * - * An example of constraints: - *
{
- *     "required": ["processDocument"],
- *     "forbidden": ["echidnaReady", "bogusParam"],
+ * An example of `constraints`:
+ * ```json
+ * {
+ *     "required": ["profile"],
+ *     "forbidden": ["source", "bogusParam"],
  *     "allowUnknownParams": true
- * }
/blockquote> + * } + * ``` * * @param params - an HTTP request query, or a similar object. * @param base - (optional) a template or “base” object to build from. @@ -147,9 +130,6 @@ export async function processParams( } else if ( p === 'validation' || p === 'htmlValidator' || - p === 'cssValidator' || - p === 'processDocument' || - p === 'events' || p === 'editorial' || p === 'additionalMetadata' ) { diff --git a/lib/validator.ts b/lib/validator.ts index 98a6489c0..b60b0fdc5 100644 --- a/lib/validator.ts +++ b/lib/validator.ts @@ -2,10 +2,10 @@ * @file Main file of the Specberus npm package. */ -import fs from 'fs'; -import type EventEmitter from 'events'; +import EventEmitter from 'events'; +import { access, constants, readFile } from 'fs/promises'; -import { type Cheerio, type CheerioAPI, load } from 'cheerio'; +import { type Cheerio, load } from 'cheerio'; import type { Element } from 'domhandler'; // @ts-ignore (no typings) import w3cApi from 'node-w3capi'; @@ -15,30 +15,21 @@ import { assembleData, setLanguage } from './l10n.js'; import * as profileMetadata from './profiles/metadata.js'; import * as profileAdditionalMetadata from './profiles/additionalMetadata.js'; import { get } from './throttled-ua.js'; -import { - AB, - buildJSONresult, - processParams, - REC_TEXT, - specberusVersion, - TAG, -} from './util.js'; +import { AB, processParams, REC_TEXT, specberusVersion, TAG } from './util.js'; import type { ApiCharter, - HandlerMessage, - RuleModule, - RuleBase, ApiSpecificationVersion, + HandlerMessage, + ProfileModule, RecMetadata, + RuleBase, RuleMeta, SpecberusConfig, - ProfileModule, } from './types.js'; setLanguage('en_GB'); interface BaseOptions { - events: EventEmitter; file?: string; source?: string; url?: string; @@ -117,214 +108,186 @@ const abbrMonths = [ export const possibleMonths = [...months, ...abbrMonths].join('|'); +// Regular expressions used by getDelivererIDs and getDelivererGroups + +const REGEX_DELIVERER_URL = + /^https?:\/\/www\.w3\.org\/2004\/01\/pp-impl\/(\d+)\/status(#.*)?$/i; +const REGEX_DELIVERER_TEXT = + /^(charter|public\s+list\s+of\s+any\s+patent\s+disclosures(\s+\(.+\))?)$/i; +const REGEX_TAG_DISCLOSURE = /https?:\/\/www.w3.org\/2001\/tag\/disclosures/; +const REGEX_DELIVERER_IPR_URL = + /^https:\/\/www\.w3\.org\/groups\/([^/]+)\/([^/]+)\/ipr\/?(#.*)?$/i; + const separator = '[ -]{1}'; -export class Specberus { +interface ExceptionsErrorOptions extends ErrorOptions { + exceptions: string[]; +} + +export interface SpecberusResult { + errors: HandlerMessage[]; + info: HandlerMessage[]; + metadata: Record; + success: boolean; + warnings: HandlerMessage[]; +} + +/** + * Error which includes list of exception messages, + * thrown in case of unexpected errors during extractMetadata or validate + */ +export class ExceptionsError extends Error { + exceptions: string[]; + + constructor(message?: string, options?: ExceptionsErrorOptions) { + super(message, options); + this.exceptions = options?.exceptions || []; + } +} + +type SpecberusMessageEventArgs = [ + RuleMeta | RuleBase, + { + detailMessage: string; + extra?: Record; + key: string; + }, +]; + +interface SpecberusEvents { + done: [string]; + err: SpecberusMessageEventArgs; + exception: [{ message: string }]; + info: SpecberusMessageEventArgs; + warning: SpecberusMessageEventArgs; +} + +export class Specberus extends EventEmitter { $ = load(''); config: SpecberusConfig | undefined; - // TODO(kgf): This is publicly documented, but is unused within the codebase; - // it would be better exposed as a Promise return value from extractMetadata - meta: Record | undefined; - source!: any | null; - url!: string | null; + source: string | undefined; + url: string | undefined; version = specberusVersion; - private $docDateEl: Cheerio | undefined; - private $sotdSection: Cheerio | null | undefined; + + // Private fields + + #$docDateEl: Cheerio | undefined; + #$sotdSection: Cheerio | null | undefined; /** Group objects returned by W3C API charters endpoint */ - private chartersData: ApiCharter[] | undefined; + #chartersData: [] | Promise | undefined; /** Charter URIs */ - private charters: string[] | undefined; - private delivererIDs: number[] | undefined; - private delivererGroups: DelivererGroup[] | undefined; - private docDate: Date | null | undefined; - private headers: HeaderMap | undefined; - private isFirstPublic: any | undefined; - private shortname: string | undefined; - private sink: EventEmitter | undefined; - - constructor() { - this.clearCache(); - } - - clearCache() { - this.$ = load(''); - this.config = undefined; - this.docDate = null; - this.$docDateEl = undefined; - this.$sotdSection = undefined; - this.url = null; - this.source = null; - this.shortname = undefined; - this.delivererIDs = undefined; - this.delivererGroups = undefined; - this.chartersData = undefined; - this.charters = undefined; - this.headers = undefined; - this.isFirstPublic = undefined; - } - - extractMetadata(options: ExtractMetadataOptions) { - this.clearCache(); - - if (!options.events) - throw new Error( - '[EXCEPTION] The events option is required for reporting.' - ); - const sink = (this.sink = options.events); - if (!this.sink.listeners('exception').length) - throw new Error( - '[WARNING] No handler for event `exception` which to report system errors.' + #charters: string[] | undefined; + #delivererIDs: number[] | Promise | undefined; + #delivererGroups: Promise | undefined; + #docDate: Date | undefined; + /** Stores messages from any unexpected errors encountered during process */ + #exceptions: string[] = []; + #headers: HeaderMap | undefined; + #isFirstPublic: boolean | undefined; + #previousVersion: Promise | undefined; + #shortname: string | undefined = undefined; + + /** + * Internal function for handling common end-state logic for extractMetadata and validate, + * returning results (resolving) or throwing an error if exceptions occurred (rejecting). + */ + #reportResult(result: Omit): SpecberusResult { + if (this.#exceptions.length) { + throw new ExceptionsError( + 'The following unexpected errors occurred:\n' + + this.#exceptions.join('\n'), + { exceptions: this.#exceptions } ); + } + return { + ...result, + success: !result.errors.length, + }; + } - const meta: Record = (this.meta = {}); + /** Internal function containing setup logic common to both extractMetadata and validate. */ + async #prepare(options: ExtractMetadataOptions | ValidateOptions) { const errors: HandlerMessage[] = []; const warnings: HandlerMessage[] = []; - const infos: HandlerMessage[] = []; - sink.on('err', data => { - errors.push(data); + const info: HandlerMessage[] = []; + + this.on('err', (rule, data) => { + errors.push({ ...rule, ...data }); }); - sink.on('warning', data => { - warnings.push(data); + this.on('warning', (rule, data) => { + warnings.push({ ...rule, ...data }); }); - sink.on('info', data => { - infos.push(data); + this.on('info', (rule, data) => { + info.push({ ...rule, ...data }); }); - /** - * @param err - * @param {CheerioAPI} $ - */ - const doMetadataExtraction = (err: any, $?: CheerioAPI) => { - if (err) return this.throw(err); - if ($) this.$ = $; - const profile = options.additionalMetadata - ? profileAdditionalMetadata - : profileMetadata; - sink.emit('start-all', profile); - const total = (profile.rules || []).length; - let done = 0; - profile.rules.forEach(rule => { - try { - rule.check(this, result => { - if (result) { - for (const i in result) { - meta[i] = result[i]; - } - } - done += 1; - sink.emit('done', rule.name); - if (done === total) - sink.emit( - 'end-all', - buildJSONresult(errors, warnings, infos, meta) - ); - }); - } catch (e) { - this.throw(e.message); - } - }); - }; - if (options.url) this.loadURL(options.url, doMetadataExtraction); - else if (options.source) - this.loadSource(options.source, doMetadataExtraction); - else if (options.file) - this.loadFile(options.file, doMetadataExtraction); - else - return this.throw( - 'At least one of url, source, file, or document must be specified.' - ); + + try { + this.$ = await this.#load(options); + } catch (error) { + this.#throw(error.toString()); + throw error; + } + + return { errors, info, warnings }; } - validate(options: ValidateOptions) { - this.clearCache(); + async extractMetadata(options: ExtractMetadataOptions) { + const messages = await this.#prepare(options); + const metadata: Record = {}; + const profile = options.additionalMetadata + ? profileAdditionalMetadata + : profileMetadata; - if (!options.events) - throw new Error( - '[EXCEPTION] The events option is required for reporting.' - ); - const sink = (this.sink = options.events); - if (sink.listeners('exception').length === 0) - throw new Error( - '[WARNING] No handler for event `exception` which to report system errors.' - ); + await Promise.all( + profile.rules.map(async rule => { + try { + const result = await rule.check(this); + if (result) + for (const [key, value] of Object.entries(result)) + metadata[key] = value; + } catch (error) { + this.#throw(error.message); + } finally { + this.emit('done', rule.name); + } + }) + ); + return this.#reportResult({ ...messages, metadata }); + } + async validate(options: ValidateOptions) { if (!options.profile) - return this.throw('Without a profile there is nothing to check.'); + throw new Error('Without a profile there is nothing to check.'); + const { profile } = options; - processParams(options, profile.config) - .then(config => { - this.config = config; - // TODO(kgf): Is this unused? We seem to hard-code it at the top of this file... - config.lang = 'en_GB'; - const errors: HandlerMessage[] = []; - const warnings: HandlerMessage[] = []; - const infos: HandlerMessage[] = []; - sink.on('err', (...data) => { - errors.push(Object.assign({}, ...data)); - }); - sink.on('warning', (...data) => { - warnings.push(Object.assign({}, ...data)); - }); - sink.on('info', (...data) => { - infos.push(Object.assign({}, ...data)); - }); - /** - * @param err - * @param {CheerioAPI} $ - */ - const doValidation = (err: any, $?: CheerioAPI) => { - if (err) return this.throw(err); - if ($) this.$ = $; - sink.emit('start-all', profile.name); - const total = (profile.rules || []).length; - let done = 0; - profile.rules.forEach((rule: RuleModule) => { - // XXX(darobin) - // I would like to catch all exceptions here, but this derails the testing - // infrastructure which also uses exceptions that it expects aren't caught - rule.check( - this, - function () { - done += 1; - sink.emit('done', rule.name); - if (done === total) - sink.emit( - 'end-all', - buildJSONresult( - errors, - warnings, - infos, - {} - ) - ); - }.bind(rule) - ); - }); - }; - if (options.url) this.loadURL(options.url, doValidation); - else if (options.source) - this.loadSource(options.source, doValidation); - else if (options.file) - this.loadFile(options.file, doValidation); - else - return this.throw( - 'At least one of url, source, file, or document must be specified.' - ); + this.config = await processParams(options, profile.config); + const messages = await this.#prepare(options); + + await Promise.all( + profile.rules.map(async rule => { + try { + await rule.check(this); + } catch (error) { + this.#throw(error.message); + } finally { + this.emit('done', rule.name); + } }) - .catch(err => this.throw(err.toString())); + ); + return this.#reportResult({ ...messages, metadata: {} }); } error(rule: RuleBase | RuleMeta, key: string, extra?: Record) { - const name = typeof rule === 'string' ? rule : rule.name; const shortname = this.getShortname(); if ( typeof shortname !== 'undefined' && - hasExceptions(shortname, name, extra) + hasExceptions(shortname, rule.name, extra) ) this.warning(rule, key, extra); else - this.sink!.emit('err', rule, { + this.emit('err', rule, { key, - extra, + ...(extra && { extra }), detailMessage: assembleData(null, rule, key, extra).message, }); } @@ -334,35 +297,50 @@ export class Specberus { key: string, extra?: Record ) { - this.sink!.emit('warning', rule, { + this.emit('warning', rule, { key, - extra, + ...(extra && { extra }), detailMessage: assembleData(null, rule, key, extra).message, }); } info(rule: RuleBase | RuleMeta, key: string, extra?: Record) { - this.sink!.emit('info', rule, { + this.emit('info', rule, { key, - extra, + ...(extra && { extra }), detailMessage: assembleData(null, rule, key, extra).message, }); } - throw(message: string) { + /** + * Emits an exception event, intended to signify that the process stopped on a critical error. + * + * NOTE: This should not be called from rules; they should throw an Error, + * which will result in extractMetadata or validate invoking this method. + */ + #throw(message: string) { console.error(`[EXCEPTION] ${message}`); - this.sink!.emit('exception', { message }); + this.emit('exception', { message }); + // Track in exceptions array, used to determine whether to resolve or reject process + this.#exceptions.push(message); } - checkSelector(sel: string, rule: RuleMeta, done: () => void) { + /** + * Checks for presence of a selector. + * Reports a not-found error for the specified rule if no match is found. + */ + checkSelector(sel: string, rule: RuleMeta) { try { if (!this.$(sel).length) this.error(rule, 'not-found'); } catch (e) { - this.throw(`Selector '${sel}' caused the validator to blow up.`); + throw new Error(`Invalid selector '${sel}': ${e}`); } - done(); } + /** + * Normalizes a string by removing leading/trailing whitespace + * and condensing multiple consecutive whitespace characters to one. + */ norm(str: string) { if (!str) return ''; return `${str}` @@ -387,7 +365,7 @@ export class Specberus { } getDocumentDate() { - if (this.docDate) return this.docDate; + if (this.#docDate) return this.#docDate; const rex = new RegExp( `${Specberus.dateRegexStrCapturing}(?:, edited in place ${Specberus.dateRegexStrNonCapturing})?$` ); @@ -395,22 +373,23 @@ export class Specberus { const matches = $el.length && this.norm($el.text()).match(rex); if (matches) { - this.docDate = this.stringToDate( + this.#docDate = this.stringToDate( `${matches[1]} ${matches[2]} ${matches[3]}` ); - this.$docDateEl = $el; + this.#$docDateEl = $el; } - return this.docDate; + return this.#docDate; } getDocumentStateElement() { - if (this.$docDateEl) return this.$docDateEl; + if (this.#$docDateEl) return this.#$docDateEl; this.getDocumentDate(); - return this.$docDateEl; + return this.#$docDateEl; } getSotDSection() { - if (typeof this.$sotdSection !== 'undefined') return this.$sotdSection; + if (typeof this.#$sotdSection !== 'undefined') + return this.#$sotdSection; let startH2: Element | undefined; let endH2: Element | undefined; @@ -431,7 +410,7 @@ export class Specberus { startH2 = h2; } }); - if (!startH2) this.$sotdSection = null; + if (!startH2) this.#$sotdSection = null; else { let started = false; this.$(startH2) @@ -446,9 +425,9 @@ export class Specberus { if (endH2 === el || $nav[0] === el) return false; $div.append(el.cloneNode(true)); }); - this.$sotdSection = $div.children().length ? $div : null; + this.#$sotdSection = $div.children().length ? $div : null; } - if (!this.$sotdSection) + if (!this.#$sotdSection) this.error( { name: 'generic.sotd', @@ -457,22 +436,16 @@ export class Specberus { }, 'not-found' ); - return this.$sotdSection; + return this.#$sotdSection; } - /** - * @param $dl Optional Cheerio-wrapped dl element. - * If not set, extractHeaders() uses the current document to extract headers link and cache them for future use. - * If set, assume data is being extracted from another document; the new element will be used and the result will not be cached. - */ - extractHeaders($dl?: Cheerio) { + extractHeaders() { + if (this.#headers) return this.#headers; + const dts: HeaderMap = {}; const EDITORS = /^editor(s)?$/; const EDITORS_DRAFT = /^(latest )?editor's draft$/i; - - if (!$dl && typeof this.headers !== 'undefined') return this.headers; - - $dl = $dl || this.$('body div.head dl'); + const $dl = this.$('body div.head dl'); if ($dl && $dl.length) { $dl.find('dt').each((idx, dt) => { @@ -484,9 +457,7 @@ export class Specberus { let $dd = $dt.next('dd'); let key = null; if (!$dd.length) - return this.throw( - `No <dd> element found for ${txt}.` - ); + throw new Error(`No <dd> element found for ${txt}.`); if (txt === 'this version') key = 'This'; else if ( !dts.Latest && @@ -512,12 +483,12 @@ export class Specberus { if (key) dts[key] = { pos: idx, $el: $dt, $dd }; }); } - this.headers = dts; + this.#headers = dts; return dts; } getShortname() { - if (typeof this.shortname !== 'undefined') return this.shortname; + if (typeof this.#shortname !== 'undefined') return this.#shortname; let shortname; const dts = this.extractHeaders(); @@ -528,7 +499,7 @@ export class Specberus { if (thisVersionMatches && thisVersionMatches.length > 0) [, shortname] = thisVersionMatches; - this.shortname = shortname; + this.#shortname = shortname; return shortname; } @@ -585,32 +556,20 @@ export class Specberus { * Retrieves deliverers groupNames and types. */ async getDelivererGroups() { - if (typeof this.delivererGroups !== 'undefined') - return this.delivererGroups; - const REGEX_DELIVERER_URL = - /^https?:\/\/www\.w3\.org\/2004\/01\/pp-impl\/(\d+)\/status(#.*)?$/i; - const REGEX_DELIVERER_TEXT = - /^(charter|public\s+list\s+of\s+any\s+patent\s+disclosures(\s+\(.+\))?)$/i; - const REGEX_TAG_DISCLOSURE = - /https?:\/\/www.w3.org\/2001\/tag\/disclosures/; - const REGEX_DELIVERER_IPR_URL = - /^https:\/\/www\.w3\.org\/groups\/([^/]+)\/([^/]+)\/ipr\/?(#.*)?$/i; + if (this.#delivererGroups) return this.#delivererGroups; const $sotd = this.getSotDSection(); const $sotdLinks = $sotd && $sotd.find('a[href]'); - const promiseArray: Promise[] = []; - let ids = []; const delivererGroups: DelivererGroup[] = []; // getDataDelivererIDs first, apply if document is Note/Registry track. - ids = this.getDataDelivererIDs() || []; + const ids = this.getDataDelivererIDs(); // For rec-track if (ids.length === 0 && $sotdLinks && $sotdLinks.length > 0) { $sotdLinks.each((_, el) => { const $el = this.$(el); const href = $el.attr('href')!; const text = this.norm($el.text()); - const found: Record = {}; if (REGEX_DELIVERER_TEXT.test(text)) { if (REGEX_DELIVERER_IPR_URL.test(href)) { @@ -627,10 +586,7 @@ export class Specberus { href.match(REGEX_DELIVERER_URL); if (delivererUrlMatch) { const id = delivererUrlMatch[1]; - if (id && id.length > 1 && !found[id]) { - found[id] = true; - ids.push(parseInt(id, 10)); - } + if (id && id.length > 1) ids.push(parseInt(id, 10)); } else if (REGEX_TAG_DISCLOSURE.test(href)) { ids.push(TAG.id); } @@ -639,109 +595,84 @@ export class Specberus { }); } - // send request to W3C API if there's id extracted from the doc. - for (let i = 0; i < ids.length; i += 1) { - const groupApiUrl = `https://api.w3.org/groups/${ids[i]}`; - promiseArray.push( - new Promise(resolve => { - get(groupApiUrl) - .set('User-Agent', `W3C-Pubrules/${specberusVersion}`) - .end((_, data) => { - resolve(data); - }); - }) - ); - } - - await Promise.all(promiseArray).then(res => { - for (let i = 0; i < res.length; i += 1) { - const data = res[i]; - if (data && data.body) { - let { type } = data.body; - switch (type) { - case 'working group': - type = 'wg'; - break; - case 'interest group': - type = 'ig'; - break; - default: - type = 'other'; - break; - } - - delivererGroups.push({ - groupShortname: data.body.shortname, - groupType: type, - }); - } - } - }); - this.delivererGroups = delivererGroups; - return delivererGroups; + // Send request to W3C API if there's ids extracted from the doc. + // Cache the promise (to avoid duplicate requests) + this.#delivererGroups = Promise.all([ + ...delivererGroups, // Any immediately-resolvable groups from links + ...ids.map(id => { + const groupApiUrl = `https://api.w3.org/groups/${id}`; + return get(groupApiUrl) + .set('User-Agent', `W3C-Pubrules/${specberusVersion}`) + .then( + res => { + if (!res.body) return; + const { shortname, type } = res.body; + let groupType = 'other'; + if (type === 'working group') groupType = 'wg'; + else if (type === 'interest group') + groupType = 'ig'; + + return { + groupShortname: shortname, + groupType, + }; + }, + () => {} + ); + }), + ]).then(groups => groups.filter(group => !!group)); + return this.#delivererGroups; } async getDelivererIDs() { - if (undefined !== this.delivererIDs) { - return this.delivererIDs; - } - const REGEX_DELIVERER_URL = - /^https?:\/\/www\.w3\.org\/2004\/01\/pp-impl\/(\d+)\/status(#.*)?$/i; - const REGEX_DELIVERER_TEXT = - /^(charter|public\s+list\s+of\s+any\s+patent\s+disclosures(\s+\(.+\))?)$/i; - const REGEX_TAG_DISCLOSURE = - /https?:\/\/www.w3.org\/2001\/tag\/disclosures/; - const REGEX_DELIVERER_IPR_URL = - /^https:\/\/www\.w3\.org\/groups\/([^/]+)\/([^/]+)\/ipr\/?(#.*)?$/i; + if (this.#delivererIDs) return this.#delivererIDs; + const ids: number[] = this.getDataDelivererIDs() || []; const $sotd = this.getSotDSection(); const $sotdLinks = $sotd && $sotd.find('a[href]'); - const promiseArray: Promise[] = []; - if (ids.length === 0 && $sotdLinks && $sotdLinks.length > 0) { - $sotdLinks.each((_, el) => { - const $el = this.$(el); - const href = $el.attr('href')!; - const text = this.norm($el.text()); - const found: Record = {}; - if (REGEX_DELIVERER_TEXT.test(text)) { - const delivererUrlMatch = href.match(REGEX_DELIVERER_URL); - if (delivererUrlMatch) { - const id = delivererUrlMatch[1]; - if (id && id.length > 1 && !found[id]) { - found[id] = true; - ids.push(parseInt(id, 10)); - } - } else if (REGEX_TAG_DISCLOSURE.test(href)) { - ids.push(TAG.id); - } else if (REGEX_DELIVERER_IPR_URL.test(href)) { - const [, type, shortname] = - REGEX_DELIVERER_IPR_URL.exec(href)!; - const groupApiUrl = `https://api.w3.org/groups/${type}/${shortname}`; - promiseArray.push( - new Promise(resolve => { - get(groupApiUrl) - .set( - 'User-Agent', - `W3C-Pubrules/${specberusVersion}` - ) - .end((_: any, data) => { - resolve(data); - }); - }) - ); - } - } - }); + if (ids.length > 0 || !$sotdLinks?.length) { + this.#delivererIDs = ids; + return ids; + } - await Promise.all(promiseArray).then(res => { - for (const data of res) { - if (data?.body?.id) ids.push(data.body.id); + const promiseArray: (number | Promise)[] = []; + $sotdLinks.each((_, el) => { + const $el = this.$(el); + const href = $el.attr('href')!; + const text = this.norm($el.text()); + if (REGEX_DELIVERER_TEXT.test(text)) { + const delivererUrlMatch = href.match(REGEX_DELIVERER_URL); + if (delivererUrlMatch) { + const id = delivererUrlMatch[1]; + if (id && id.length > 1) + promiseArray.push(parseInt(id, 10)); + } else if (REGEX_TAG_DISCLOSURE.test(href)) { + promiseArray.push(TAG.id); + } else if (REGEX_DELIVERER_IPR_URL.test(href)) { + const [, type, shortname] = + REGEX_DELIVERER_IPR_URL.exec(href)!; + const groupApiUrl = `https://api.w3.org/groups/${type}/${shortname}`; + promiseArray.push( + get(groupApiUrl) + .set( + 'User-Agent', + `W3C-Pubrules/${specberusVersion}` + ) + .then( + res => res.body?.id, + () => {} + ) + ); } - }); - } - this.delivererIDs = ids; - return ids; + } + }); + + // Cache the promise (to avoid duplicate requests) + this.#delivererIDs = Promise.all(promiseArray).then(ids => + ids.filter(id => typeof id !== 'undefined') + ); + return this.#delivererIDs; } getDataDelivererIDs() { @@ -763,59 +694,52 @@ export class Specberus { /** Finds the current charter(s) of the document. */ async getChartersData() { - if (undefined !== this.chartersData) return this.chartersData; + if (this.#chartersData) return this.#chartersData; const deliverers = await this.getDelivererIDs(); + if (!deliverers.length) { + this.#chartersData = []; + return this.#chartersData; + } + const docDate = this.getDocumentDate()!; - const chartersData: ApiCharter[] = []; - if (deliverers.length) { - const delivererPromises: Promise[] = []; - const AbID = AB.id; - // Get charter data from W3C API - // deliverers.forEach is for joint publication. - deliverers.forEach(deliverer => { - // Skip finding charter for the TAG which doesn't have any charter - if (deliverer === TAG.id || deliverer === AbID) return; - - delivererPromises.push( - new Promise(resolve => { - w3cApi - .group(deliverer) - .charters() - .fetch( - { embed: true }, - (_: any, charters: ApiCharter[]) => { - resolve(charters); - } + const delivererPromises: Promise[] = []; + // Get charter data from W3C API + // deliverers.forEach is for joint publication. + deliverers.forEach(deliverer => { + // Skip finding charter for the TAG which doesn't have any charter + if (deliverer === TAG.id || deliverer === AB.id) return; + + delivererPromises.push( + w3cApi + .group(deliverer) + .charters() + .fetch({ embed: true }) + .then( + (groupCharters: ApiCharter[]) => { + if (!groupCharters) return; + return groupCharters.filter( + groupCharter => + docDate >= new Date(groupCharter.start) && + docDate <= new Date(groupCharter.end) ); - }) - ); - }); - - // groups -> group is for joint publication. - for (const groupCharters of await Promise.all(delivererPromises)) { - if (groupCharters) { - for (const groupCharter of groupCharters) { - if ( - docDate >= new Date(groupCharter.start) && - docDate <= new Date(groupCharter.end) - ) { - chartersData.push(groupCharter); - } - } - } - } - } + }, + () => {} + ) + ); + }); - this.chartersData = chartersData; - return chartersData; + this.#chartersData = Promise.all(delivererPromises).then(lists => + lists.flat().filter(charter => !!charter) + ); + return this.#chartersData; } async getCharters() { - if (typeof this.charters !== 'undefined') return this.charters; + if (this.#charters) return this.#charters; - this.charters = (await this.getChartersData()).map(({ uri }) => uri); - return this.charters; + this.#charters = (await this.getChartersData()).map(({ uri }) => uri); + return this.#charters; } /** @@ -823,19 +747,21 @@ export class Specberus { * For shortname change document, data-previous-shortname attribute is needed. */ async isFP() { - if (typeof this.isFirstPublic !== 'undefined') - return this.isFirstPublic; + if (typeof this.#isFirstPublic !== 'undefined') + return this.#isFirstPublic; - this.isFirstPublic = !(await this.getPreviousVersion()); - return this.isFirstPublic; + this.#isFirstPublic = !(await this.getPreviousVersion()); + return this.#isFirstPublic; } /** * Gets previous version link from API via shortname. */ async getPreviousVersion() { + if (this.#previousVersion) return this.#previousVersion; + const dts = this.extractHeaders(); - const shortname = this.shortname || (await this.getShortname()); + const shortname = this.#shortname || (await this.getShortname()); if (!shortname) { this.error( @@ -849,89 +775,81 @@ export class Specberus { return; } - const shortnameHistory = await new Promise< - ApiSpecificationVersion[] | null - >(resolve => { + const shortnameHistory: Promise = w3cApi .specification(shortname) .versions() - .fetch( - { embed: true, items: 1000 }, - (err: any, data: ApiSpecificationVersion[]) => { - if (err && err.status === 404) { - // check if it's not a shortname change - const shortnameChange = dts.History - ? dts.History.$dd - .find('a') - .attr('data-previous-shortname') - : null; - if (shortnameChange) { - w3cApi - .specification(shortnameChange) - .versions() - .fetch( - { embed: true, items: 1000 }, - ( - _: any, - data: ApiSpecificationVersion[] - ) => { - resolve(data); - } - ); - } else { - resolve(null); - } - } else { - resolve(data); + .fetch({ embed: true, items: 1000 }) + .catch((err: any) => { + if (err.status === 404) { + // check if it's not a shortname change + const shortnameChange = dts.History + ? dts.History.$dd + .find('a') + .attr('data-previous-shortname') + : null; + if (shortnameChange) { + return w3cApi + .specification(shortnameChange) + .versions() + .fetch({ embed: true, items: 1000 }) + .catch(() => {}); } } - ); + }); + + // Cache the promise (to avoid duplicate requests) + this.#previousVersion = shortnameHistory.then(history => { + const versions = history || []; + const linkThis = dts.This + ? dts.This.$dd.find('a').attr('href') + : ''; + + if (versions.length && linkThis) { + const versionUris = versions.map(({ uri }) => uri); + const index = versionUris.indexOf(linkThis); + return index === -1 ? versionUris[0] : versionUris[index + 1]; + } + return null; }); - const versions = shortnameHistory || []; - const linkThis = dts.This ? dts.This.$dd.find('a').attr('href') : ''; + return this.#previousVersion; + } - if (versions.length && linkThis) { - const versionUris = versions.map(({ uri }) => uri); - const index = versionUris.indexOf(linkThis); - return index === -1 ? versionUris[0] : versionUris[index + 1]; - } + #load(options: ExtractMetadataOptions | ValidateOptions) { + if (options.url) return this.#loadURL(options.url); + if (options.source) return this.#loadSource(options.source); + if (options.file) return this.#loadFile(options.file); + throw new Error('url, source, or file must be specified.'); } - loadURL(url: string, cb: (err: any, $?: CheerioAPI) => void) { - if (!cb) return this.throw('Missing callback to loadURL.'); - get(url) + #loadURL(url: string) { + return get(url) .set('User-Agent', `W3C-Pubrules/${specberusVersion}`) - .end((err, res) => { - if (err) return this.throw(err.message); - if (!res.text) return this.throw(`Body of ${url} is empty.`); + .then(res => { + if (!res.text) throw new Error(`Body of ${url} is empty.`); this.url = url; - this.loadSource(res.text, cb); + return this.#loadSource(res.text); }); } - loadSource(src: string, cb: (err: Error | null, $?: CheerioAPI) => void) { - if (!cb) return this.throw('Missing callback to loadSource.'); + #loadSource(src: string) { this.source = src; - let $: CheerioAPI; try { - $ = load(src); + return load(src); } catch (e) { - return this.throw( + throw new Error( `Cheerio failed to parse source: ${JSON.stringify(e)}` ); } - cb(null, $); } - loadFile(file: string, cb: (err: any, $?: CheerioAPI) => void) { - if (!cb) return this.throw('Missing callback to loadFile.'); - fs.access(file, fs.constants.F_OK, errors => { - if (errors) return cb(`File '${file}' not found.`); - fs.readFile(file, { encoding: 'utf8' }, (err, src) => { - if (err) return cb(err); - this.loadSource(src, cb); - }); - }); + async #loadFile(file: string) { + try { + await access(file, constants.F_OK); + } catch (error) { + throw new Error(`File '${file}' not found or inaccessible.`); + } + return this.#loadSource(await readFile(file, 'utf8')); } transition(options: TransitionOptions) { diff --git a/package-lock.json b/package-lock.json index 3461b9b9c..20c0f5e8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "cheerio": "^1.1.2", "compression": "1.8.1", "cors": "2.8.6", - "doasync": "2.0.1", "express": "5.2.1", "express-fileupload": "1.5.2", "express-handlebars": "9.0.1", @@ -2865,16 +2864,6 @@ "wrappy": "1" } }, - "node_modules/doasync": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/doasync/-/doasync-2.0.1.tgz", - "integrity": "sha512-5u7qAb+ACe2dvxHXrhjWNAfWuj42yB45Z9ght+U9QkB09nNGYMw5S4q6sXcpVnxjcKGl0jUYcxrGpR6kBTGJHw==", - "license": "MIT", - "engines": { - "node": ">= 8.2.1", - "npm": ">= 5.3.0" - } - }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", diff --git a/package.json b/package.json index 7b78c41ad..13b40b101 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ "cheerio": "^1.1.2", "compression": "1.8.1", "cors": "2.8.6", - "doasync": "2.0.1", "express": "5.2.1", "express-fileupload": "1.5.2", "express-handlebars": "9.0.1", diff --git a/test/api.ts b/test/api.ts index 965dae0c3..9071c88e1 100644 --- a/test/api.ts +++ b/test/api.ts @@ -49,6 +49,14 @@ function setup() { const handleResponse = (response: Response) => response.text || response.body; const handleJsonResponse = (response: Response) => JSON.parse(handleResponse(response)); +function assertResponseStatus(response: Response | undefined, status: number) { + const actualStatus = response?.status; + assert.strictEqual( + status, + actualStatus, + `Expected ${status} response status but received ${actualStatus}` + ); +} function getErrorResponseText(error: ResponseError) { const text = error.response?.text; assert(text, 'Response data not available on error'); @@ -104,6 +112,24 @@ describe('API', () => { assert.strictEqual(metadata.profile, 'REC'); assert.strictEqual(metadata.docDate, '2016-3-8'); })); + + it('Should accept "file" via POST, and report exceptions', () => + assert.rejects( + createPostRequest('metadata').attach( + 'file', + join(testDocsPath, 'wd-fail-date.html') + ), + (error: any) => { + assertResponseStatus(error.response, 500); + const { errors } = JSON.parse(getErrorResponseText(error)); + assert.strictEqual( + errors.length, + 2, + 'Expected multiple errors to be reported' + ); + return true; + } + )); }); describe('Method “validate”', () => { @@ -113,6 +139,7 @@ describe('API', () => { .field('profile', 'REC') .attach('file', join(testDocsPath, 'ttml-imsc1.html')), (error: any) => { + assertResponseStatus(error.response, 400); const { success, errors } = JSON.parse( getErrorResponseText(error) ); @@ -132,6 +159,27 @@ describe('API', () => { } )); + it('Should 400 with error on POST if "file" is provided but "profile" is not', () => + assert.rejects( + createPostRequest('validate').attach( + 'file', + join(testDocsPath, 'wd-good.html') + ), + (error: any) => { + assertResponseStatus(error.response, 400); + const { success, errors } = JSON.parse( + getErrorResponseText(error) + ); + assert.strictEqual(success, false); + assert.deepStrictEqual(errors, [ + { + error: 'Error: Parameter “profile” is required in this context', + }, + ]); + return true; + } + )); + it('Should accept "file" via POST, and succeed when the document is valid', () => createPostRequest('validate') .field('profile', 'WD') @@ -150,32 +198,49 @@ describe('API', () => { assert.strictEqual(success, true); assert.strictEqual(metadata.profile, 'WD'); })); + + it('Special profile “auto”: should report exception if profile cannot be determined', () => + assert.rejects( + createPostRequest('validate') + .field('profile', 'auto') + .attach('file', join(testDocsPath, 'wd-fail-auto.html')), + (error: any) => { + assertResponseStatus(error.response, 500); + const { errors } = JSON.parse(getErrorResponseText(error)); + assert.strictEqual(errors.length, 1); + assert.match( + errors[0].exception, + /Pubrules is having a hard time identifying the profile of the document/ + ); + return true; + } + )); }); describe('Parameter restrictions', () => { it('Should reject the parameter "document" as unknown', () => assert.rejects(get('metadata?document=foo'), (error: any) => { + assertResponseStatus(error.response, 400); const { success, errors } = JSON.parse( getErrorResponseText(error) ); assert.strictEqual(success, false); - assert.strictEqual( - errors[0], - 'Error: Illegal parameter “document”' - ); + assert.deepStrictEqual(errors[0], { + error: 'Error: Illegal parameter “document”', + }); return true; })); it('Should reject the parameter "source" as forbidden', () => assert.rejects(get('metadata?source=foo'), (error: any) => { + assertResponseStatus(error.response, 400); const { success, errors } = JSON.parse( getErrorResponseText(error) ); assert.strictEqual(success, false); - assert.strictEqual( - errors[0], - 'Error: Parameter “source” is not allowed in this context' - ); + assert.deepStrictEqual(errors[0], { + error: 'Error: Parameter “source” is not allowed in this context', + }); return true; })); }); diff --git a/test/data/goodDocuments.ts b/test/data/goodDocuments.ts index 72d544cf8..5709021eb 100644 --- a/test/data/goodDocuments.ts +++ b/test/data/goodDocuments.ts @@ -16,6 +16,10 @@ export const goodDocuments = { STMT: { url: 'doc-views/TR/Note/STMT?type=good', }, + 'STMT-exception': { + profile: 'STMT', + url: 'doc-views/TR/Note/STMT?type=goodException', + }, // Recommendation track CR: { diff --git a/test/doc-views/TR/Note/STMT.ts b/test/doc-views/TR/Note/STMT.ts index 3458f4de6..b4a3f6ea9 100644 --- a/test/doc-views/TR/Note/STMT.ts +++ b/test/doc-views/TR/Note/STMT.ts @@ -16,10 +16,30 @@ const customData = { // Used in http://localhost:8001/doc-views/TR/Note/STMT?type=good const good = { ...data, ...customData }; + +// Test hasException by failing a rule, accompanied by +// metadata that downgrades the failure from error to warning +const goodException = { + ...good, + dl: { + ...good.dl, + editor2: { + show: true, + id: '3440', + }, + history: { + ...data.dl.history, + shortName: 'privacy-principles', + }, + shortName: 'privacy-principles', + seriesShortName: 'privacy-principles', + }, +}; const common = buildCommonViewData(good); export default { good, + goodException, ...common, stability: { ...common.stability, diff --git a/test/docs/api/wd-fail-auto.html b/test/docs/api/wd-fail-auto.html new file mode 100644 index 000000000..805b8b567 --- /dev/null +++ b/test/docs/api/wd-fail-auto.html @@ -0,0 +1,148 @@ + + + + + + + + + + WD-Echidna: test document - Specberus + + +
+ +

WD-Echidna: test document - Specberus

+ +

+ W3C Working Thing + +

+
+ More details about this document +
+ +
This Version
+
+ + https://www.w3.org/TR/2026/WD-hr-time-20260407/ + +
+ +
Latest published version: (@@note that version is not required in the latest version)
+
https://www.w3.org/TR/hr-time/ +
+ +
Latest editor's draft:
+
https://w3c.github.io/hr-time/ +
+ +
History:
+
https://www.w3.org/standards/history/hr-time +
+ + +
Editor:
+
George Herald (WebFoo) +
+ + + + +
Feedback:
+
https://github.com/w3c/hr-time/issues
+
public-foo@w3.org with subject line “[hr-time] … message topic …” (archives)
+
+
+ + + + +
+
+ +
+

Abstract

+

This specification defines an API that provides the time origin, and current time in sub-millisecond resolution, such that it is not subject to system clock skew or adjustments.

+ +
+ +
+

Status of This Document

+ +

This section describes the status of this document at the time of its publication. A list of current W3C publications and the latest revision of this technical report can be found in the W3C standards and drafts index.

+ + +

+ This document was published by the Internationalization Working Group as a Working Draft using the Recommendation track. +

+ + +

+ + + + + Publication as a Working Draft does not imply endorsement by W3C and its Members. + + + + + + This is a draft document and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to cite this document as other than a work in progress. + + + + +

+ + + + +

+ This document was produced by a group operating under the W3C Patent Policy. W3C maintains a public list of any patent disclosures made in connection with the deliverables of the group; that page also includes instructions for disclosing a patent. An individual who has actual knowledge of a patent that the individual believes contains Essential Claim(s) must disclose the information in accordance with section 6 of the W3C Patent Policy. +

+ +

+ This document is governed by the 18 August 2025 W3C Process Document. +

+ +
+ +
+

1. Introduction

+

This section is non-normative.

+
+
+

1. Time Origin

+

This section is non-normative.

+
+ + + + + + diff --git a/test/docs/api/wd-fail-date.html b/test/docs/api/wd-fail-date.html new file mode 100644 index 000000000..6f8ed02f4 --- /dev/null +++ b/test/docs/api/wd-fail-date.html @@ -0,0 +1,148 @@ + + + + + + + + + + WD-Echidna: test document - Specberus + + +
+ +

WD-Echidna: test document - Specberus

+ +

+ W3C Working Draft + +

+
+ More details about this document +
+ +
This Version
+
+ + https://www.w3.org/TR/2026/WD-hr-time-20260407/ + +
+ +
Latest published version: (@@note that version is not required in the latest version)
+
https://www.w3.org/TR/hr-time/ +
+ +
Latest editor's draft:
+
https://w3c.github.io/hr-time/ +
+ +
History:
+
https://www.w3.org/standards/history/hr-time +
+ + +
Editor:
+
George Herald (WebFoo) +
+ + + + +
Feedback:
+
https://github.com/w3c/hr-time/issues
+
public-foo@w3.org with subject line “[hr-time] … message topic …” (archives)
+
+
+ + + + +
+
+ +
+

Abstract

+

This specification defines an API that provides the time origin, and current time in sub-millisecond resolution, such that it is not subject to system clock skew or adjustments.

+ +
+ +
+

Status of This Document

+ +

This section describes the status of this document at the time of its publication. A list of current W3C publications and the latest revision of this technical report can be found in the W3C standards and drafts index.

+ + +

+ This document was published by the Internationalization Working Group as a Working Draft using the Recommendation track. +

+ + +

+ + + + + Publication as a Working Draft does not imply endorsement by W3C and its Members. + + + + + + This is a draft document and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to cite this document as other than a work in progress. + + + + +

+ + + + +

+ This document was produced by a group operating under the W3C Patent Policy. W3C maintains a public list of any patent disclosures made in connection with the deliverables of the group; that page also includes instructions for disclosing a patent. An individual who has actual knowledge of a patent that the individual believes contains Essential Claim(s) must disclose the information in accordance with section 6 of the W3C Patent Policy. +

+ +

+ This document is governed by the 18 August 2025 W3C Process Document. +

+ +
+ +
+

1. Introduction

+

This section is non-normative.

+
+
+

1. Time Origin

+

This section is non-normative.

+
+ + + + + + diff --git a/test/rules.ts b/test/rules.ts index b8e03f575..f21ecfc59 100644 --- a/test/rules.ts +++ b/test/rules.ts @@ -1,11 +1,17 @@ import assert from 'assert'; -import { EventEmitter } from 'events'; +import { readFile } from 'fs/promises'; import type { Server } from 'http'; import { after, afterEach, before, beforeEach, describe, it } from 'node:test'; +import { rules as metadataRules } from '../lib/profiles/metadata.js'; import { removeRules } from '../lib/profiles/profileUtil.js'; -import { allProfiles, buildJSONresult } from '../lib/util.js'; -import { Specberus } from '../lib/validator.js'; +import type { HandlerMessage } from '../lib/types.js'; +import { allProfiles } from '../lib/util.js'; +import { + ExceptionsError, + Specberus, + type SpecberusResult, +} from '../lib/validator.js'; // A list of good documents to be tested, using all rules configured in the profiles. // Shouldn't cause any error. import { goodDocuments } from './data/goodDocuments.js'; @@ -35,24 +41,10 @@ const testType = process.env.TYPE; const testProfile = process.env.PROFILE; interface CompareMetadataObject { - errors?: Record[]; + errors?: Partial[]; [index: string]: any; } -/** - * Returns an EventEmitter and Promise, both reflecting progress/completion of a Specberus call. - */ -function createSpecberusPromiseHandler() { - const handler = new EventEmitter(); - const promise = new Promise>( - (resolve, reject) => { - handler.on('end-all', resolve); - handler.on('exception', reject); - } - ); - return { handler, promise }; -} - /** * Assert that metadata detected in a spec is equal to the expected values. * @@ -64,9 +56,7 @@ function compareMetadata(file: string, expectedObject: CompareMetadataObject) { it(`Should detect metadata for ${testFile}`, async () => { const specberus = new Specberus(); - const { handler, promise } = createSpecberusPromiseHandler(); - specberus.extractMetadata({ events: handler, file: testFile }); - const result = await promise; + const result = await specberus.extractMetadata({ file: testFile }); assert.strictEqual(result.success, !('errors' in expectedObject)); if ('errors' in expectedObject) { @@ -77,7 +67,9 @@ function compareMetadata(file: string, expectedObject: CompareMetadataObject) { assert( expectedObject.errors.every((expected, i) => Object.entries(expected).every( - ([key, value]) => result.errors[i][key] === value + ([key, value]) => + result.errors[i][key as keyof HandlerMessage] === + value ) ), `Errors should contain expected properties:\n${JSON.stringify( @@ -88,14 +80,13 @@ function compareMetadata(file: string, expectedObject: CompareMetadataObject) { ); } - assert(specberus.meta, 'Expected specberus.meta to be defined'); for (const [key, value] of Object.entries(expectedObject)) { if (key === 'errors' || key === 'file') continue; assert( - key in specberus.meta, - `Expected specberus.meta.${key} to be defined` + key in result.metadata, + `Expected ${key} to be defined in metadata` ); - assert.deepStrictEqual(specberus.meta[key], value); + assert.deepStrictEqual(result.metadata[key], value); } }); } @@ -114,6 +105,48 @@ describe('Basics', () => { samples.forEach(sample => { compareMetadata(sample.file, sample); }); + + it('Should report multiple exceptions when failing to parse date', async () => { + const badHtml = ( + await readFile('test/docs/2021-wd.html', 'utf8') + ).replace(/04 November/, '04 11'); + + let observedDone = 0; + const observedExceptions: string[] = []; + const sr = new Specberus(); + sr.on('done', () => { + observedDone++; + }); + sr.on('exception', ({ message }) => { + observedExceptions.push(message); + }); + + return assert.rejects( + sr.extractMetadata({ source: badHtml }), + (error: ExceptionsError) => { + assert.strictEqual(error.exceptions.length, 2); + assert.match( + error.exceptions[0], + /^Cannot find the .* element for profile and date/ + ); + assert.strictEqual( + error.exceptions[1], + 'The document date could not be parsed.' + ); + assert.deepStrictEqual( + observedExceptions, + error.exceptions, + 'Exceptions in rejection error should match emitted exception events' + ); + assert.strictEqual( + observedDone, + metadataRules.length, + 'done event should fire for all rules regardless of exceptions' + ); + return true; + } + ); + }); }); describe('Method "validate"', () => { @@ -148,59 +181,58 @@ interface ValidationTestConfig { warnings?: any[]; } -function buildValidationTestHandler(test: ValidationTestConfig) { - const { handler, promise } = createSpecberusPromiseHandler(); - +function addValidationEventListeners(sr: Specberus) { if (DEBUG) { - handler.on('err', (type, data) => { + sr.on('err', (type, data) => { console.log('error:\n', type, data); }); - handler.on('warning', (type, data) => { + sr.on('warning', (type, data) => { console.log('warning:\n', type, data); }); - handler.on('done', name => { + sr.on('done', name => { console.log(`----> ${name} check done`); }); } - handler.on('exception', data => { + sr.on('exception', data => { console.error( `[EXCEPTION] Validator had a massive failure: ${data.message}` ); }); +} + +const verifySpecberusResult = ( + promise: Promise, + test: ValidationTestConfig +) => + promise.then(result => { + const { errors, warnings } = result; + if (test.errors) { + assert.strictEqual(errors.length, test.errors.length); + errors.forEach(({ key, name }, i) => { + assert.strictEqual(`${name}.${key}`, test.errors![i]); + }); + } else { + assert.strictEqual(errors.length, 0, 'Expected errors to be empty'); + } - return { - handler, - promise: promise.then(({ errors, warnings }) => { - if (test.errors) { - assert.strictEqual(errors.length, test.errors.length); - errors.forEach(({ key, name }, i) => { - assert.strictEqual(`${name}.${key}`, test.errors![i]); + if (!test.ignoreWarnings) { + if (test.warnings) { + assert.strictEqual(warnings.length, test.warnings.length); + warnings.forEach(({ key, name }, i) => { + assert.strictEqual(`${name}.${key}`, test.warnings![i]); }); } else { assert.strictEqual( - errors.length, + warnings.length, 0, - 'Expected errors to be empty' + 'Expected warnings to be empty' ); } + } - if (!test.ignoreWarnings) { - if (test.warnings) { - assert.strictEqual(warnings.length, test.warnings.length); - warnings.forEach(({ key, name }, i) => { - assert.strictEqual(`${name}.${key}`, test.warnings![i]); - }); - } else { - assert.strictEqual( - warnings.length, - 0, - 'Expected warnings to be empty' - ); - } - } - }), - }; -} + // Pass through result to allow further verifications + return result; + }); const testsGoodDoc = goodDocuments; @@ -243,20 +275,19 @@ describe('Making sure good documents pass Specberus...', () => { 'links.linkchecker', // too slow. will check separately. ]); - const { handler, promise } = buildValidationTestHandler({ - ignoreWarnings: true, - }); + const sr = new Specberus(); + addValidationEventListeners(sr); const options = { profile: { ...extendedProfile, rules, // do not change profile.rules }, - events: handler, url, }; - new Specberus().validate(options); - return promise; + await verifySpecberusResult(sr.validate(options), { + ignoreWarnings: true, + }); }); } }); @@ -292,8 +323,9 @@ function checkRule(tests: RuleTest[], options: CheckRuleOptions) { const ruleModule = await import( `../lib/rules/${category}/${rule}.js` ); - const { handler, promise } = buildValidationTestHandler(test); + const sr = new Specberus(); + addValidationEventListeners(sr); const options = { url, profile: { @@ -304,10 +336,32 @@ function checkRule(tests: RuleTest[], options: CheckRuleOptions) { ...test.config, }, }, - events: handler, }; - new Specberus().validate(options); - return promise; + + const counts = { errors: 0, info: 0, warnings: 0 }; + sr.on('err', () => counts.errors++); + sr.on('info', () => counts.info++); + sr.on('warning', () => counts.warnings++); + + const result = await verifySpecberusResult( + sr.validate(options), + test + ); + assert.strictEqual( + result.errors.length, + counts.errors, + 'Number of err events emitted should match number in resolved promise' + ); + assert.strictEqual( + result.info.length, + counts.info, + 'Number of info events emitted should match number in resolved promise' + ); + assert.strictEqual( + result.warnings.length, + counts.warnings, + 'Number of warning events emitted should match number in resolved promise' + ); }); }); } From 92b415e321d8985b1ac41d5817a913675e467b45 Mon Sep 17 00:00:00 2001 From: "Kenneth G. Franqueiro" Date: Thu, 4 Jun 2026 13:38:04 -0400 Subject: [PATCH 05/11] Perform major-version dependency upgrades that drop Node v20 support --- package-lock.json | 1885 +++++++++++++-------------------------------- package.json | 8 +- 2 files changed, 546 insertions(+), 1347 deletions(-) diff --git a/package-lock.json b/package-lock.json index 20c0f5e8e..a2e443a2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "morgan": "1.11.0", "node-w3capi": "2.2.2", "promise": "8.3.0", - "puppeteer": "24.40.0", + "puppeteer": "25.2.0", "socket.io": "4.8.3", "superagent": "10.3.0", "tmp": "0.2.7" @@ -36,45 +36,22 @@ "@types/superagent": "^8.1.9", "@types/tmp": "^0.2.6", "c8": "^11.0.0", - "cspell": "9.0.2", + "cspell": "10.0.1", "domhandler": "^6.0.1", "expect.js": "0.3", "husky": "9.0.11", - "lint-staged": "16.4.0", + "lint-staged": "17.0.8", "lodash.merge": "^4.6.2", "nock": "15.0.0", "prettier": "3.8.4", "tsx": "^4.21.0", - "typescript": "^6.0.2" + "typescript": "^6.0.3" }, "engines": { "node": "22 || 24", "npm": ">=7" } }, - "node_modules/@babel/code-frame": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", - "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.29.7", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.29.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", - "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@bcoe/v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", @@ -96,129 +73,153 @@ } }, "node_modules/@cspell/cspell-bundled-dicts": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-9.0.2.tgz", - "integrity": "sha512-gGFSfVIvYtO95O3Yhcd1o0sOZHjVaCPwYq3MnaNsBBzaMviIZli4FZW9Z+XNKsgo1zRzbl2SdOXJPP0VcyAY0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/dict-ada": "^4.1.0", - "@cspell/dict-al": "^1.1.0", - "@cspell/dict-aws": "^4.0.10", - "@cspell/dict-bash": "^4.2.0", - "@cspell/dict-companies": "^3.2.1", - "@cspell/dict-cpp": "^6.0.8", - "@cspell/dict-cryptocurrencies": "^5.0.4", - "@cspell/dict-csharp": "^4.0.6", - "@cspell/dict-css": "^4.0.17", - "@cspell/dict-dart": "^2.3.0", - "@cspell/dict-data-science": "^2.0.8", - "@cspell/dict-django": "^4.1.4", - "@cspell/dict-docker": "^1.1.14", - "@cspell/dict-dotnet": "^5.0.9", - "@cspell/dict-elixir": "^4.0.7", - "@cspell/dict-en_us": "^4.4.8", - "@cspell/dict-en-common-misspellings": "^2.0.11", - "@cspell/dict-en-gb-mit": "^3.0.6", - "@cspell/dict-filetypes": "^3.0.12", - "@cspell/dict-flutter": "^1.1.0", - "@cspell/dict-fonts": "^4.0.4", - "@cspell/dict-fsharp": "^1.1.0", - "@cspell/dict-fullstack": "^3.2.6", - "@cspell/dict-gaming-terms": "^1.1.1", - "@cspell/dict-git": "^3.0.5", - "@cspell/dict-golang": "^6.0.21", - "@cspell/dict-google": "^1.0.8", - "@cspell/dict-haskell": "^4.0.5", - "@cspell/dict-html": "^4.0.11", - "@cspell/dict-html-symbol-entities": "^4.0.3", - "@cspell/dict-java": "^5.0.11", - "@cspell/dict-julia": "^1.1.0", - "@cspell/dict-k8s": "^1.0.10", - "@cspell/dict-kotlin": "^1.1.0", - "@cspell/dict-latex": "^4.0.3", - "@cspell/dict-lorem-ipsum": "^4.0.4", - "@cspell/dict-lua": "^4.0.7", - "@cspell/dict-makefile": "^1.0.4", - "@cspell/dict-markdown": "^2.0.10", - "@cspell/dict-monkeyc": "^1.0.10", - "@cspell/dict-node": "^5.0.7", - "@cspell/dict-npm": "^5.2.3", - "@cspell/dict-php": "^4.0.14", - "@cspell/dict-powershell": "^5.0.14", - "@cspell/dict-public-licenses": "^2.0.13", - "@cspell/dict-python": "^4.2.18", - "@cspell/dict-r": "^2.1.0", - "@cspell/dict-ruby": "^5.0.8", - "@cspell/dict-rust": "^4.0.11", - "@cspell/dict-scala": "^5.0.7", - "@cspell/dict-shell": "^1.1.0", - "@cspell/dict-software-terms": "^5.0.9", - "@cspell/dict-sql": "^2.2.0", - "@cspell/dict-svelte": "^1.0.6", - "@cspell/dict-swift": "^2.0.5", - "@cspell/dict-terraform": "^1.1.1", - "@cspell/dict-typescript": "^3.2.1", - "@cspell/dict-vue": "^3.0.4" - }, - "engines": { - "node": ">=20" + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@cspell/cspell-bundled-dicts/-/cspell-bundled-dicts-10.0.1.tgz", + "integrity": "sha512-WvkSDNX4Uyyj/ZgbPO6L38iFNMfK1EqsH1FteRiI2qLz6QZMXRFrIt12OqiWIplzZDDaVpBH9FCJOPJll0fjCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspell/dict-ada": "^4.1.1", + "@cspell/dict-al": "^1.1.1", + "@cspell/dict-aws": "^4.0.17", + "@cspell/dict-bash": "^4.2.2", + "@cspell/dict-companies": "^3.2.11", + "@cspell/dict-cpp": "^7.0.2", + "@cspell/dict-cryptocurrencies": "^5.0.5", + "@cspell/dict-csharp": "^4.0.8", + "@cspell/dict-css": "^4.1.1", + "@cspell/dict-dart": "^2.3.2", + "@cspell/dict-data-science": "^2.0.13", + "@cspell/dict-django": "^4.1.6", + "@cspell/dict-docker": "^1.1.17", + "@cspell/dict-dotnet": "^5.0.13", + "@cspell/dict-elixir": "^4.0.8", + "@cspell/dict-en_us": "^4.4.33", + "@cspell/dict-en-common-misspellings": "^2.1.12", + "@cspell/dict-en-gb-mit": "^3.1.22", + "@cspell/dict-filetypes": "^3.0.18", + "@cspell/dict-flutter": "^1.1.1", + "@cspell/dict-fonts": "^4.0.6", + "@cspell/dict-fsharp": "^1.1.1", + "@cspell/dict-fullstack": "^3.2.9", + "@cspell/dict-gaming-terms": "^1.1.2", + "@cspell/dict-git": "^3.1.0", + "@cspell/dict-golang": "^6.0.26", + "@cspell/dict-google": "^1.0.9", + "@cspell/dict-haskell": "^4.0.6", + "@cspell/dict-html": "^4.0.15", + "@cspell/dict-html-symbol-entities": "^4.0.5", + "@cspell/dict-java": "^5.0.12", + "@cspell/dict-julia": "^1.1.1", + "@cspell/dict-k8s": "^1.0.12", + "@cspell/dict-kotlin": "^1.1.1", + "@cspell/dict-latex": "^5.1.0", + "@cspell/dict-lorem-ipsum": "^4.0.5", + "@cspell/dict-lua": "^4.0.8", + "@cspell/dict-makefile": "^1.0.5", + "@cspell/dict-markdown": "^2.0.16", + "@cspell/dict-monkeyc": "^1.0.12", + "@cspell/dict-node": "^5.0.9", + "@cspell/dict-npm": "^5.2.38", + "@cspell/dict-php": "^4.1.1", + "@cspell/dict-powershell": "^5.0.15", + "@cspell/dict-public-licenses": "^2.0.16", + "@cspell/dict-python": "^4.2.26", + "@cspell/dict-r": "^2.1.1", + "@cspell/dict-ruby": "^5.1.1", + "@cspell/dict-rust": "^4.1.2", + "@cspell/dict-scala": "^5.0.9", + "@cspell/dict-shell": "^1.1.2", + "@cspell/dict-software-terms": "^5.2.2", + "@cspell/dict-sql": "^2.2.1", + "@cspell/dict-svelte": "^1.0.7", + "@cspell/dict-swift": "^2.0.6", + "@cspell/dict-terraform": "^1.1.3", + "@cspell/dict-typescript": "^3.2.3", + "@cspell/dict-vue": "^3.0.5", + "@cspell/dict-zig": "^1.0.0" + }, + "engines": { + "node": ">=22.18.0" } }, "node_modules/@cspell/cspell-json-reporter": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/@cspell/cspell-json-reporter/-/cspell-json-reporter-9.0.2.tgz", - "integrity": "sha512-Hy9hKG53cFhLwiSZuRVAd5YfBb5pPj3V2Val69TW1j4+sy3podewqm4sb3RqoB01LcDkLI/mOeMwHz1xyIjfoA==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@cspell/cspell-json-reporter/-/cspell-json-reporter-10.0.1.tgz", + "integrity": "sha512-/nes1RGILec3WCBcoMOd0byNTBtnJuPaVz/+ZzqYkLtY7x58VMcBG5kyP6hPyN8cIwjRADE/SR43gwdXuqk/FA==", "dev": true, "license": "MIT", "dependencies": { - "@cspell/cspell-types": "9.0.2" + "@cspell/cspell-types": "10.0.1" }, "engines": { - "node": ">=20" + "node": ">=22.18.0" + } + }, + "node_modules/@cspell/cspell-performance-monitor": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@cspell/cspell-performance-monitor/-/cspell-performance-monitor-10.0.1.tgz", + "integrity": "sha512-9tVcHXwRnbazUv4WSG0h3MqV4+LgmLNgSALAQUflPPW0EMxTf7C4Dmv9cgxJyCEQrdnVKCr58nPPaahhz9LJUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.18.0" } }, "node_modules/@cspell/cspell-pipe": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/@cspell/cspell-pipe/-/cspell-pipe-9.0.2.tgz", - "integrity": "sha512-M1e+u3dyGCJicSZ16xmoVut4pI8ynfqILYiDAYC9+rbn04wJdnWD46ElIZnRriFXx7fu/UsUEexu3lFaqKVGEg==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@cspell/cspell-pipe/-/cspell-pipe-10.0.1.tgz", + "integrity": "sha512-HPeXMD9AZ3V/qPkvQaPcak+C7cJ2z7JTHN8smd6J8L2aThLRky2cHc2OyeaHPSHB7WA47b4z2n5u5nawZhv5VQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=20" + "node": ">=22.18.0" } }, "node_modules/@cspell/cspell-resolver": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/@cspell/cspell-resolver/-/cspell-resolver-9.0.2.tgz", - "integrity": "sha512-JkMQb+hcEyZ2ALvEeJvfxoIblRpZlnek50Ew5sLSSZciRuhNvQZS5+Apwt1GXHitTo8/bqXFxABNP36O++YAwA==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@cspell/cspell-resolver/-/cspell-resolver-10.0.1.tgz", + "integrity": "sha512-PIzkZHD1fGUQx1XteK2d1iQ0Mzq/maYcoB4jkvAiiR6WqP3MWYNKFdI9z+R5pOq5KgMfW+5Ig1q0oSR6h8irlA==", "dev": true, "license": "MIT", "dependencies": { - "global-directory": "^4.0.1" + "global-directory": "^5.0.0" }, "engines": { - "node": ">=20" + "node": ">=22.18.0" } }, "node_modules/@cspell/cspell-service-bus": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/@cspell/cspell-service-bus/-/cspell-service-bus-9.0.2.tgz", - "integrity": "sha512-OjfZ3vnBjmkctC9xs/87/9bx/3kZYUPJkWsZxzfH4rla/HeIUrm9UZlDqCibhWifhPHrDdV9hDW5QEGXkYR2hw==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@cspell/cspell-service-bus/-/cspell-service-bus-10.0.1.tgz", + "integrity": "sha512-y6NcIGP2IdXaBL4PVH8vxsr7K27wzz3Ech87UtUtrDSXAiVEOvXgAIknEOUVp59rTlUE8Rn4IRURC6f/hgMyfw==", "dev": true, "license": "MIT", "engines": { - "node": ">=20" + "node": ">=22.18.0" } }, "node_modules/@cspell/cspell-types": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/@cspell/cspell-types/-/cspell-types-9.0.2.tgz", - "integrity": "sha512-RioULo34qbUXuCCLi/DCDxdb++Nm1ospNXzVkKZrSvTG4AjkC95ZhfIOp9jbGSWqL2PGdaHVXgG77EyQbAk5xA==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@cspell/cspell-types/-/cspell-types-10.0.1.tgz", + "integrity": "sha512-kLgLShnWADDVreKC63pBrWkcvxgZzFIfO34Jhx/SWfuOIA3cD8AXT+HjyuLfoGJ7mUb58hv2kUziKzEy4INb1w==", "dev": true, "license": "MIT", "engines": { - "node": ">=20" + "node": ">=22.18.0" + } + }, + "node_modules/@cspell/cspell-worker": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@cspell/cspell-worker/-/cspell-worker-10.0.1.tgz", + "integrity": "sha512-L2bJerfuYOls2wEknm8FmynLtj/G7O4UqX9I/HznRggEW6i2yZIxagDetpVDNowpyavNHJ3SJtUFiyMiZc16Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cspell-lib": "10.0.1" + }, + "engines": { + "node": ">=22.18.0" } }, "node_modules/@cspell/dict-ada": { @@ -260,9 +261,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-cpp": { - "version": "6.0.15", - "resolved": "https://registry.npmjs.org/@cspell/dict-cpp/-/dict-cpp-6.0.15.tgz", - "integrity": "sha512-N7MKK3llRNoBncygvrnLaGvmjo4xzVr5FbtAc9+MFGHK6/LeSySBupr1FM72XDaVSIsmBEe7sDYCHHwlI9Jb2w==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@cspell/dict-cpp/-/dict-cpp-7.0.2.tgz", + "integrity": "sha512-dfbeERiVNeqmo/npivdR6rDiBCqZi3QtjH2Z0HFcXwpdj6i97dX1xaKyK2GUsO/p4u1TOv63Dmj5Vm48haDpuA==", "dev": true, "license": "MIT" }, @@ -463,9 +464,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-latex": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@cspell/dict-latex/-/dict-latex-4.0.4.tgz", - "integrity": "sha512-YdTQhnTINEEm/LZgTzr9Voz4mzdOXH7YX+bSFs3hnkUHCUUtX/mhKgf1CFvZ0YNM2afjhQcmLaR9bDQVyYBvpA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@cspell/dict-latex/-/dict-latex-5.1.0.tgz", + "integrity": "sha512-qxT4guhysyBt0gzoliXYEBYinkAdEtR2M7goRaUH0a7ltCsoqqAeEV8aXYRIdZGcV77gYSobvu3jJL038tlPAw==", "dev": true, "license": "MIT" }, @@ -639,48 +640,65 @@ "dev": true, "license": "MIT" }, + "node_modules/@cspell/dict-zig": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@cspell/dict-zig/-/dict-zig-1.0.0.tgz", + "integrity": "sha512-XibBIxBlVosU06+M6uHWkFeT0/pW5WajDRYdXG2CgHnq85b0TI/Ks0FuBJykmsgi2CAD3Qtx8UHFEtl/DSFnAQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@cspell/dynamic-import": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/@cspell/dynamic-import/-/dynamic-import-9.0.2.tgz", - "integrity": "sha512-KhcoNUj6Ij2P8fbRC7QOn3jzbTZFxoQpFGanGU9f+4DfZBH86PCADyKYH+ZpJPlYgrI+Jh4wKzF5y5YKKNrdrw==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@cspell/dynamic-import/-/dynamic-import-10.0.1.tgz", + "integrity": "sha512-mP1gdq00aIcH8HxNMqnH11X6BKxLcneDtFgl/ecjIKnaGKwi44m8AndP5Kr4ODaYdl8UUw9O3dJh7KaQXnLHZQ==", "dev": true, "license": "MIT", "dependencies": { - "@cspell/url": "9.0.2", - "import-meta-resolve": "^4.1.0" + "@cspell/url": "10.0.1", + "import-meta-resolve": "^4.2.0" }, "engines": { - "node": ">=20" + "node": ">=22.18.0" } }, "node_modules/@cspell/filetypes": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/@cspell/filetypes/-/filetypes-9.0.2.tgz", - "integrity": "sha512-8KEIgptldoZT3pM+yhYV8nXq5T9Sz0YvZIqwDGEqKJ6j447K+I91QWS7RQDrvHkElMi/2g/h08Efg0RIT+QEaQ==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@cspell/filetypes/-/filetypes-10.0.1.tgz", + "integrity": "sha512-Z5S35giU5IW49fBBq6BksUbE8PC4IYPfaKuwl5Nl9jkf/OkAKiBmCowKX45NzRUQInwK/GSqqIUifrNeI6LdLw==", "dev": true, "license": "MIT", "engines": { - "node": ">=20" + "node": ">=22.18.0" + } + }, + "node_modules/@cspell/rpc": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@cspell/rpc/-/rpc-10.0.1.tgz", + "integrity": "sha512-axSRKv3zEAmBm66iD/FV/MPmE4/Yf7c3PZiwTW894Yd3iEhtn3KPKeTrqQ2/tDrhB1Z2qTsap/Hue0MK4o5WXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.18.0" } }, "node_modules/@cspell/strong-weak-map": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/@cspell/strong-weak-map/-/strong-weak-map-9.0.2.tgz", - "integrity": "sha512-SHTPUcu2e6aYxI5sr1L/9pzz68CArV6WzMvAio//5LbtKI6NtDp/7tARBwLi1G3A3C0289zDHbDKm3wc1lRNhQ==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@cspell/strong-weak-map/-/strong-weak-map-10.0.1.tgz", + "integrity": "sha512-lenN1DVyPi8nJLSMSJJ670ddTjyiruLueuSZO1qLcxBqUhgxDt/mALu9N/1m6WdOVcg6m/5cLiZVg2KOo2UzRw==", "dev": true, "license": "MIT", "engines": { - "node": ">=20" + "node": ">=22.18.0" } }, "node_modules/@cspell/url": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/@cspell/url/-/url-9.0.2.tgz", - "integrity": "sha512-KwCDL0ejgwVSZB8KTp8FhDe42UOaebTVIMi3O5GcYHi9Cut8B5QU4tbQOFGXP6E4pjimeO9yIkr9Z34kTljj/g==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@cspell/url/-/url-10.0.1.tgz", + "integrity": "sha512-abYYgI29wJhWIfWTYrYuzRYDcHQUQ1N5ylnhxYn1NJnIQMqUWGLbDmt12JABtZ+R6h6UNatQrS7rhP86etvJyQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=20" + "node": ">=22.18.0" } }, "node_modules/@esbuild/aix-ppc64": { @@ -1326,48 +1344,108 @@ } }, "node_modules/@puppeteer/browsers": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", - "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.0.5.tgz", + "integrity": "sha512-xYXNuEQmHNIPWWcbL/skf2KF7seyp7c1xmKFRk3wmdFx7VwBsKVrtOLKs8ecaezsKPsWeF1YsgwIiElAscaryA==", "license": "Apache-2.0", "dependencies": { - "debug": "^4.4.3", - "extract-zip": "^2.0.1", - "progress": "^2.0.3", - "proxy-agent": "^6.5.0", - "semver": "^7.7.4", - "tar-fs": "^3.1.1", - "yargs": "^17.7.2" + "modern-tar": "^0.7.6", + "yargs": "^18.0.0" }, "bin": { - "browsers": "lib/cjs/main-cli.js" + "browsers": "lib/main-cli.js" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1" + }, + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + } + } + }, + "node_modules/@puppeteer/browsers/node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@puppeteer/browsers/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/@puppeteer/browsers/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@puppeteer/browsers/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/@puppeteer/browsers/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=6.0" + "node": ">=18" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/@puppeteer/browsers/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" + "node_modules/@puppeteer/browsers/node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/@puppeteer/browsers/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", @@ -1421,12 +1499,6 @@ "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", "license": "MIT" }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "license": "MIT" - }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -1631,16 +1703,6 @@ "@types/node": "*" } }, - "node_modules/@types/yauzl": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", - "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", - "license": "MIT", - "optional": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/accepts": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", @@ -1663,15 +1725,6 @@ "node": ">= 0.6" } }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/ansi-escapes": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", @@ -1692,7 +1745,6 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -1705,7 +1757,6 @@ "version": "6.2.3", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -1714,12 +1765,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, "node_modules/array-timsort": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", @@ -1733,18 +1778,6 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "license": "MIT" }, - "node_modules/ast-types": { - "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.1" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -1757,20 +1790,6 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, - "node_modules/b4a": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz", - "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==", - "license": "Apache-2.0", - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -1780,98 +1799,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/bare-events": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.9.1.tgz", - "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", - "license": "Apache-2.0", - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.7.2", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.2.tgz", - "integrity": "sha512-aTvMFUWkBmjzKtEQMDGGDNF8bkfpD5N1b/FCwt7A3wrU4t1o/e/85Wzkluh6JlODCjqVESYCkQCdTXqZ9G7VFg==", - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.5.4", - "bare-path": "^3.0.0", - "bare-stream": "^2.6.4", - "bare-url": "^2.2.2", - "fast-fifo": "^1.3.2" - }, - "engines": { - "bare": ">=1.16.0" - }, - "peerDependencies": { - "bare-buffer": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - } - } - }, - "node_modules/bare-os": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz", - "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==", - "license": "Apache-2.0", - "engines": { - "bare": ">=1.14.0" - } - }, - "node_modules/bare-path": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.1.tgz", - "integrity": "sha512-ghj2DSK/2e99a1anTVPCV4m4YIYtrbXhfM7V3D7XZLOTsybnYyaJloymGqssQc8l/or0UoDyRtNQkmkEF/ysgQ==", - "license": "Apache-2.0", - "dependencies": { - "bare-os": "^3.0.1" - } - }, - "node_modules/bare-stream": { - "version": "2.13.3", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.3.tgz", - "integrity": "sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==", - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.8.1", - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-abort-controller": "*", - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - }, - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.5.tgz", - "integrity": "sha512-K+y9xF1tN+CdPu4qWwr0QiK1Al07eFPGYK5M2pDXcmHdMdgC/tT/bpmMe1hrmRHaidKLkXrC+cRNYf3XVDUhSQ==", - "license": "Apache-2.0", - "dependencies": { - "bare-path": "^3.0.0" - } - }, "node_modules/base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", @@ -1899,15 +1826,6 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, - "node_modules/basic-ftp": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", - "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/before-after-hook": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", @@ -1995,15 +1913,6 @@ "node": "18 || 20 || >=22" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -2087,15 +1996,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -2198,33 +2098,19 @@ } }, "node_modules/chromium-bidi": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", - "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-16.0.1.tgz", + "integrity": "sha512-J63PGu/9PpeCwLIcKYyzWP6yaVL5pxuBc0shlYCYM8BaAkmlwiQboXO1iNbOgSDbVklEyYFfNEcHD8oOAWacUA==", "license": "Apache-2.0", "dependencies": { "mitt": "^3.0.1", "zod": "^3.24.1" }, - "peerDependencies": { - "devtools-protocol": "*" - } - }, - "node_modules/clear-module": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/clear-module/-/clear-module-4.1.3.tgz", - "integrity": "sha512-XdLrg7BnbXKntyrbs2dNjDN9CVoTQ+WV0i7jT5/r9ahzAaSDSzC9e2OVZB/QVwbxBb1/1AeObzjlxsYk5HFvww==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^2.0.0", - "resolve-from": "^5.0.0" - }, "engines": { - "node": ">=8" + "node": ">=20.19.0 <22.0.0 || >=22.12.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "devtools-protocol": "*" } }, "node_modules/cli-cursor": { @@ -2264,6 +2150,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -2278,6 +2165,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2287,6 +2175,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -2302,6 +2191,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2311,6 +2201,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -2325,6 +2216,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -2337,6 +2229,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -2354,6 +2247,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -2366,12 +2260,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true, "license": "MIT" }, @@ -2398,9 +2286,9 @@ } }, "node_modules/comment-json": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.6.2.tgz", - "integrity": "sha512-R2rze/hDX30uul4NZoIZ76ImSJLFxn/1/ZxtKC1L77y2X1k+yYu1joKbAtMA2Fg3hZrTOiw0I5mwVMo0cf250w==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-5.0.0.tgz", + "integrity": "sha512-uiqLcOiVDJtBP8WGkZHEP+FZIhTzP1dxvn59EfoYUi9gqupjrBWVQkO2atDrbnKPwLeotFYDsuNb26uBMqB+hw==", "dev": true, "license": "MIT", "dependencies": { @@ -2524,46 +2412,11 @@ "url": "https://opencollective.com/express" } }, - "node_modules/cosmiconfig": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", - "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", - "license": "MIT", - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/cosmiconfig/node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -2575,184 +2428,187 @@ } }, "node_modules/cspell": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cspell/-/cspell-9.0.2.tgz", - "integrity": "sha512-VwPNTTivvv/NyovXUMcTYc7BaOgun7k8FhRWaVKxZPEsl/9r9WTLmQ1dNbHRq56LajH2b7wKGQYuRsfov3UWTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/cspell-json-reporter": "9.0.2", - "@cspell/cspell-pipe": "9.0.2", - "@cspell/cspell-types": "9.0.2", - "@cspell/dynamic-import": "9.0.2", - "@cspell/url": "9.0.2", - "chalk": "^5.4.1", - "chalk-template": "^1.1.0", - "commander": "^14.0.0", - "cspell-dictionary": "9.0.2", - "cspell-gitignore": "9.0.2", - "cspell-glob": "9.0.2", - "cspell-io": "9.0.2", - "cspell-lib": "9.0.2", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/cspell/-/cspell-10.0.1.tgz", + "integrity": "sha512-Gg6w/flT3fKfl3la62hfTnhtNnDQ+9mU7kUhVqw/axl/Ms4oENw0oJMkWFIoj4f6nL/SDPz7KcPXd2XbkKFNmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspell/cspell-json-reporter": "10.0.1", + "@cspell/cspell-performance-monitor": "10.0.1", + "@cspell/cspell-pipe": "10.0.1", + "@cspell/cspell-types": "10.0.1", + "@cspell/cspell-worker": "10.0.1", + "@cspell/dynamic-import": "10.0.1", + "@cspell/url": "10.0.1", + "ansi-regex": "^6.2.2", + "chalk": "^5.6.2", + "chalk-template": "^1.1.2", + "commander": "^14.0.3", + "cspell-config-lib": "10.0.1", + "cspell-dictionary": "10.0.1", + "cspell-gitignore": "10.0.1", + "cspell-glob": "10.0.1", + "cspell-io": "10.0.1", + "cspell-lib": "10.0.1", "fast-json-stable-stringify": "^2.1.0", - "file-entry-cache": "^9.1.0", - "semver": "^7.7.2", - "tinyglobby": "^0.2.13" + "flatted": "^3.4.2", + "semver": "^7.8.1", + "tinyglobby": "^0.2.16" }, "bin": { "cspell": "bin.mjs", "cspell-esm": "bin.mjs" }, "engines": { - "node": ">=20" + "node": ">=22.18.0" }, "funding": { "url": "https://github.com/streetsidesoftware/cspell?sponsor=1" } }, "node_modules/cspell-config-lib": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cspell-config-lib/-/cspell-config-lib-9.0.2.tgz", - "integrity": "sha512-8rCmGUEzlytnNeAazvbBdLeUoN18Cct8k6KLePiUS0GglYomSAvcPWsamSk9jeh947m0cu2dhjZPnKQlp11XBA==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/cspell-config-lib/-/cspell-config-lib-10.0.1.tgz", + "integrity": "sha512-hMpo/0j6k7pbiqrLDOLJKD2IGP9XwhjKf2miiM6p84Xeo4nyuFZaxxDCQ68R851HSYFrrdltgpoipMbj1h2Tnw==", "dev": true, "license": "MIT", "dependencies": { - "@cspell/cspell-types": "9.0.2", - "comment-json": "^4.2.5", - "yaml": "^2.8.0" + "@cspell/cspell-types": "10.0.1", + "comment-json": "^5.0.0", + "smol-toml": "^1.6.1", + "yaml": "^2.9.0" }, "engines": { - "node": ">=20" + "node": ">=22.18.0" } }, "node_modules/cspell-dictionary": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cspell-dictionary/-/cspell-dictionary-9.0.2.tgz", - "integrity": "sha512-u1jLnqu+2IJiGKdUP9LF1/vseOrCh6hUACHZQ8JsCbHC2KU/DL68s4IgS5jDyK5lBcwPOWzQOiTuXQSEardpFQ==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/cspell-dictionary/-/cspell-dictionary-10.0.1.tgz", + "integrity": "sha512-3cZ659vgsZWkzGQJR/sNqGDVt/OnvTSieLKI76V++4t1bHJfochb9ZrrwsuMsb1VPGiyqClUP1/O6WrefF/FVg==", "dev": true, "license": "MIT", "dependencies": { - "@cspell/cspell-pipe": "9.0.2", - "@cspell/cspell-types": "9.0.2", - "cspell-trie-lib": "9.0.2", - "fast-equals": "^5.2.2" + "@cspell/cspell-performance-monitor": "10.0.1", + "@cspell/cspell-pipe": "10.0.1", + "@cspell/cspell-types": "10.0.1", + "cspell-trie-lib": "10.0.1", + "fast-equals": "^6.0.0" }, "engines": { - "node": ">=20" + "node": ">=22.18.0" } }, "node_modules/cspell-gitignore": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cspell-gitignore/-/cspell-gitignore-9.0.2.tgz", - "integrity": "sha512-2CXpUYa+mf1I0oMH/V0qzT0zP95IqYzaS9BfEB7AcSmjrvuIgmiGLztUNrG5mMMBAlHk7sfI8gAEMMvr/Q7sTQ==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/cspell-gitignore/-/cspell-gitignore-10.0.1.tgz", + "integrity": "sha512-wN23U61Mx6qPJN3CesOmBU9vnbJ0jQm/ylK0iaVui3CcnO7Zzl5qLu5mPHUzGQGm8yso6qjyxqo16Ho7LpZGOQ==", "dev": true, "license": "MIT", "dependencies": { - "@cspell/url": "9.0.2", - "cspell-glob": "9.0.2", - "cspell-io": "9.0.2" + "@cspell/url": "10.0.1", + "cspell-glob": "10.0.1", + "cspell-io": "10.0.1" }, "bin": { "cspell-gitignore": "bin.mjs" }, "engines": { - "node": ">=20" + "node": ">=22.18.0" } }, "node_modules/cspell-glob": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cspell-glob/-/cspell-glob-9.0.2.tgz", - "integrity": "sha512-trTskAU7tw9RpCb+/uPM4zWByZEavHh3SIrjz7Du/ritjZi85O80HItNw5O3ext4zSPfNNLL3kBT7fLLphFHrw==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/cspell-glob/-/cspell-glob-10.0.1.tgz", + "integrity": "sha512-7bII9J3aSSpZDwhx7w+zfQXbMxHZQ3be0ilUp5bHrsjz6o07v/NqOHMGcwKdPn1sw2dxDz9sv057xE5pqXnSdw==", "dev": true, "license": "MIT", "dependencies": { - "@cspell/url": "9.0.2", - "picomatch": "^4.0.2" + "@cspell/url": "10.0.1", + "picomatch": "^4.0.4" }, "engines": { - "node": ">=20" + "node": ">=22.18.0" } }, "node_modules/cspell-grammar": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cspell-grammar/-/cspell-grammar-9.0.2.tgz", - "integrity": "sha512-3hrNZJYEgWSaCvH3rpFq43PX9pxdJt60+pFG3CTZAdpcI97DDsrdH3f7a6h8sNAb+pN59JnV2DtWexsAVL6vjA==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/cspell-grammar/-/cspell-grammar-10.0.1.tgz", + "integrity": "sha512-xC9AFYmaI9wsO//a7S5tdDGKGJVD5UEEsTg+Up2fi7lPfXIryisYmV6tePNL1SEg0idYss4ja8LUZ3Mib09BjQ==", "dev": true, "license": "MIT", "dependencies": { - "@cspell/cspell-pipe": "9.0.2", - "@cspell/cspell-types": "9.0.2" + "@cspell/cspell-pipe": "10.0.1", + "@cspell/cspell-types": "10.0.1" }, "bin": { "cspell-grammar": "bin.mjs" }, "engines": { - "node": ">=20" + "node": ">=22.18.0" } }, "node_modules/cspell-io": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cspell-io/-/cspell-io-9.0.2.tgz", - "integrity": "sha512-TO93FTgQjjp62nAn213885RdyOTsQwdjSHdeYaaNiaTBOBgj2jR8M8bi3+h2imGBlinlYERoVbPF9wghJEK2nw==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/cspell-io/-/cspell-io-10.0.1.tgz", + "integrity": "sha512-8C2ka07faxflnaqEBO3pektS21XViE/SEHT7F5ZD1ou7FyMR5u3xawTBJSczClfsxLt/WYeztBYrpmGAjmjksw==", "dev": true, "license": "MIT", "dependencies": { - "@cspell/cspell-service-bus": "9.0.2", - "@cspell/url": "9.0.2" + "@cspell/cspell-service-bus": "10.0.1", + "@cspell/url": "10.0.1" }, "engines": { - "node": ">=20" + "node": ">=22.18.0" } }, "node_modules/cspell-lib": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cspell-lib/-/cspell-lib-9.0.2.tgz", - "integrity": "sha512-uoPQ0f+umOGUQB/q0H+K/gWfd7xJMaPlt5rXMMTeKIPHLDRBE7lBx4mHVCmgevL+oTNSLpIE5FdqRDbr+Q+Awg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@cspell/cspell-bundled-dicts": "9.0.2", - "@cspell/cspell-pipe": "9.0.2", - "@cspell/cspell-resolver": "9.0.2", - "@cspell/cspell-types": "9.0.2", - "@cspell/dynamic-import": "9.0.2", - "@cspell/filetypes": "9.0.2", - "@cspell/strong-weak-map": "9.0.2", - "@cspell/url": "9.0.2", - "clear-module": "^4.1.2", - "comment-json": "^4.2.5", - "cspell-config-lib": "9.0.2", - "cspell-dictionary": "9.0.2", - "cspell-glob": "9.0.2", - "cspell-grammar": "9.0.2", - "cspell-io": "9.0.2", - "cspell-trie-lib": "9.0.2", - "env-paths": "^3.0.0", - "fast-equals": "^5.2.2", - "gensequence": "^7.0.0", - "import-fresh": "^3.3.1", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/cspell-lib/-/cspell-lib-10.0.1.tgz", + "integrity": "sha512-RpsIPiLzc4/YMW8BMRKpyJ81x439qjYWcqgdKeXnMkbKM88J9PexzutfFf/4v97v96KzfNitEzMpbI0uj8OeUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspell/cspell-bundled-dicts": "10.0.1", + "@cspell/cspell-performance-monitor": "10.0.1", + "@cspell/cspell-pipe": "10.0.1", + "@cspell/cspell-resolver": "10.0.1", + "@cspell/cspell-types": "10.0.1", + "@cspell/dynamic-import": "10.0.1", + "@cspell/filetypes": "10.0.1", + "@cspell/rpc": "10.0.1", + "@cspell/strong-weak-map": "10.0.1", + "@cspell/url": "10.0.1", + "cspell-config-lib": "10.0.1", + "cspell-dictionary": "10.0.1", + "cspell-glob": "10.0.1", + "cspell-grammar": "10.0.1", + "cspell-io": "10.0.1", + "cspell-trie-lib": "10.0.1", + "env-paths": "^4.0.0", + "gensequence": "^8.0.8", + "import-fresh": "^4.0.0", "resolve-from": "^5.0.0", "vscode-languageserver-textdocument": "^1.0.12", "vscode-uri": "^3.1.0", "xdg-basedir": "^5.1.0" }, "engines": { - "node": ">=20" + "node": ">=22.18.0" } }, "node_modules/cspell-trie-lib": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/cspell-trie-lib/-/cspell-trie-lib-9.0.2.tgz", - "integrity": "sha512-inXu6YEoJFLYnxgcXy3quCoGgSWYRye1kM4dj8kbYtNAQgUVD93hPFdmPWObwhVawsS3rQybckG3DSnmxBe9Fg==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/cspell-trie-lib/-/cspell-trie-lib-10.0.1.tgz", + "integrity": "sha512-BFvhalSkRQFjKrZ//FKK7fRGrZFpifnxB5AwCkzsIsBZqicsfafcQ1xP21qpb0QqyV/IomjNgviG+tRJs+0rMw==", "dev": true, "license": "MIT", - "dependencies": { - "@cspell/cspell-pipe": "9.0.2", - "@cspell/cspell-types": "9.0.2", - "gensequence": "^7.0.0" - }, "engines": { - "node": ">=20" + "node": ">=22.18.0" + }, + "peerDependencies": { + "@cspell/cspell-types": "10.0.1" } }, "node_modules/css-select": { @@ -2798,15 +2654,6 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/data-uri-to-buffer": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", - "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -2816,20 +2663,6 @@ "ms": "2.0.0" } }, - "node_modules/degenerator": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", - "license": "MIT", - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -2849,9 +2682,9 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.1581282", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz", - "integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==", + "version": "0.0.1638949", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1638949.tgz", + "integrity": "sha512-mXwg4Fqnv0WR4iuAT/gYUmctNkjILwXFHyZ+m7Ty1dfr0ezZt2U3gnrrJTfRobJTHoXf+IbuFvFITzLrLFjwJA==", "license": "BSD-3-Clause" }, "node_modules/dezalgo": { @@ -2991,6 +2824,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, "license": "MIT" }, "node_modules/encodeurl": { @@ -3015,15 +2849,6 @@ "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" } }, - "node_modules/end-of-stream": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", - "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, "node_modules/engine.io": { "version": "6.6.9", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.9.tgz", @@ -3133,13 +2958,16 @@ } }, "node_modules/env-paths": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", - "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-4.0.0.tgz", + "integrity": "sha512-pxP8eL2SwwaTRi/KHYwLYXinDs7gL3jxFcBYmEdYfZmZXbaVDvdppd0XBU8qVz03rDfKZMXg1omHCbsJjZrMsw==", "dev": true, "license": "MIT", + "dependencies": { + "is-safe-filename": "^0.1.0" + }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -3158,15 +2986,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -3269,31 +3088,11 @@ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", "license": "MIT" }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "license": "BSD-2-Clause", - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", @@ -3303,24 +3102,6 @@ "node": ">=4" } }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -3337,15 +3118,6 @@ "dev": true, "license": "MIT" }, - "node_modules/events-universal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", - "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", - "license": "Apache-2.0", - "dependencies": { - "bare-events": "^2.7.0" - } - }, "node_modules/expect.js": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/expect.js/-/expect.js-0.3.1.tgz", @@ -3453,65 +3225,16 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", - "license": "BSD-2-Clause", - "dependencies": { - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "bin": { - "extract-zip": "cli.js" - }, - "engines": { - "node": ">= 10.17.0" - }, - "optionalDependencies": { - "@types/yauzl": "^2.9.1" - } - }, - "node_modules/extract-zip/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/extract-zip/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/fast-equals": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", - "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-6.0.0.tgz", + "integrity": "sha512-PFhhIGgdM79r5Uztdj9Zb6Tt1zKafqVfdMGwVca1z5z6fbX7DmsySSuJd8HiP6I1j505DCS83cLxo5rmSNeVEA==", "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" } }, - "node_modules/fast-fifo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", - "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", - "license": "MIT" - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -3525,15 +3248,6 @@ "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", "license": "MIT" }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -3552,19 +3266,6 @@ } } }, - "node_modules/file-entry-cache": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-9.1.0.tgz", - "integrity": "sha512-/pqPFG+FdxWQj+/WSuzXSDaNzxgTLr/OrR1QuqfEZzDakpdYE70PwUxL7BPUa8hpjbvY1+qvCl8k+8Tq34xJgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/file-type": { "version": "22.0.1", "resolved": "https://registry.npmjs.org/file-type/-/file-type-22.0.1.tgz", @@ -3644,20 +3345,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat-cache": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-5.0.0.tgz", - "integrity": "sha512-JrqFmyUl2PnPi1OvLyTVHnQvwQ0S+e6lGSwu8OkAZlSaNIZciTY2H/cOOROxsBA1m/LZNHDsqAgDZt6akWcjsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.3.1", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/flatted": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", @@ -3779,13 +3466,13 @@ } }, "node_modules/gensequence": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/gensequence/-/gensequence-7.0.0.tgz", - "integrity": "sha512-47Frx13aZh01afHJTB3zTtKIlFI6vWY+MYCN9Qpew6i52rfKjnhCF/l1YlC8UmEMvvntZZ6z4PiCcmyuedR2aQ==", + "version": "8.0.8", + "resolved": "https://registry.npmjs.org/gensequence/-/gensequence-8.0.8.tgz", + "integrity": "sha512-omMVniXEXpdx/vKxGnPRoO2394Otlze28TyxECbFVyoSpZ9H3EO7lemjcB12OpQJzRW4e5tt/dL1rOxry6aMHg==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/get-caller-file": { @@ -3801,7 +3488,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -3847,58 +3533,6 @@ "node": ">= 0.4" } }, - "node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/get-uri": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", - "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", - "license": "MIT", - "dependencies": { - "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^6.0.2", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/get-uri/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/get-uri/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/glob": { "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", @@ -3917,16 +3551,16 @@ } }, "node_modules/global-directory": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", - "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-5.0.0.tgz", + "integrity": "sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w==", "dev": true, "license": "MIT", "dependencies": { - "ini": "4.1.1" + "ini": "6.0.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4093,78 +3727,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/http-proxy-agent/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/http-proxy-agent/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/https-proxy-agent/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/husky": { "version": "9.0.11", "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz", @@ -4214,42 +3776,18 @@ "license": "BSD-3-Clause" }, "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-4.0.0.tgz", + "integrity": "sha512-Fpi660c7VPDM3fPKYovStd9IP1CPOikf6v/dGxJJMmHPcwYQIMJ4W7kO1avBYEpMqkCh+Dx3Ln6H7VYqgztLjw==", + "dev": true, "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, "engines": { - "node": ">=6" + "node": ">=22.15" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/import-fresh/node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/import-fresh/node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/import-meta-resolve": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", @@ -4268,13 +3806,13 @@ "license": "ISC" }, "node_modules/ini": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", - "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-6.0.0.tgz", + "integrity": "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==", "dev": true, "license": "ISC", "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^20.17.0 || >=22.9.0" } }, "node_modules/insafe": { @@ -4283,15 +3821,6 @@ "integrity": "sha512-LVGi8wNYSld/ElRmd06ikkY2uBV4ik1JaPf0FRQ4awW6t+3VqPBzePs74/P7CBTm5SpItIfOOFIs/K66cyx9Rw==", "license": "MIT" }, - "node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -4301,12 +3830,6 @@ "node": ">= 0.10" } }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, "node_modules/is-fullwidth-code-point": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", @@ -4336,6 +3859,19 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, + "node_modules/is-safe-filename": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-safe-filename/-/is-safe-filename-0.1.1.tgz", + "integrity": "sha512-4SrR7AdnY11LHfDKTZY1u6Ga3RuxZdl3YKWWShO5iyuG5h8QS4GD2tOb04peBJ5I7pXbR+CGBNEhTcwK+FzN3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4382,47 +3918,6 @@ "node": ">=8" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/puzrin" - }, - { - "type": "github", - "url": "https://github.com/sponsors/nodeca" - } - ], - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" - }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -4436,62 +3931,58 @@ "integrity": "sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==", "license": "MIT" }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" } }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, "node_modules/lint-staged": { - "version": "16.4.0", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.4.0.tgz", - "integrity": "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==", + "version": "17.0.8", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.0.8.tgz", + "integrity": "sha512-B2P/d+jVW0UXOQ0MVMLrB/9ydA1P+zz6jYfdrbbEd9ur3S2rcbduFWKiUCC02Sm5hbC8nrm7y24WuYMG54HfxA==", "dev": true, "license": "MIT", "dependencies": { - "commander": "^14.0.3", - "listr2": "^9.0.5", - "picomatch": "^4.0.3", + "listr2": "^10.2.1", + "picomatch": "^4.0.4", "string-argv": "^0.3.2", - "tinyexec": "^1.0.4", - "yaml": "^2.8.2" + "tinyexec": "^1.2.4" }, "bin": { "lint-staged": "bin/lint-staged.js" }, "engines": { - "node": ">=20.17" + "node": ">=22.22.1" }, "funding": { "url": "https://opencollective.com/lint-staged" + }, + "optionalDependencies": { + "yaml": "^2.9.0" } }, "node_modules/listr2": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", - "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-10.2.1.tgz", + "integrity": "sha512-7I5knELsJKTUjXG+A6BkKAiGkW1i25fNa/xlUl9hFtk15WbE9jndA89xu5FzQKrY5llajE1hfZZFMILXkDHk/Q==", "dev": true, "license": "MIT", "dependencies": { - "cli-truncate": "^5.0.0", - "colorette": "^2.0.20", - "eventemitter3": "^5.0.1", + "cli-truncate": "^5.2.0", + "eventemitter3": "^5.0.4", "log-update": "^6.1.0", "rfdc": "^1.4.1", - "wrap-ansi": "^9.0.0" + "wrap-ansi": "^10.0.0" }, "engines": { - "node": ">=20.0.0" + "node": ">=22.13.0" } }, "node_modules/locate-path": { @@ -4537,6 +4028,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, "node_modules/log-update/node_modules/slice-ansi": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", @@ -4554,6 +4052,42 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/lru-cache": { "version": "11.5.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", @@ -4713,6 +4247,15 @@ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", "license": "MIT" }, + "node_modules/modern-tar": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.6.tgz", + "integrity": "sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/morgan": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.11.0.tgz", @@ -4754,15 +4297,6 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "license": "MIT" }, - "node_modules/netmask": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", - "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/nock": { "version": "15.0.0", "resolved": "https://registry.npmjs.org/nock/-/nock-15.0.0.tgz", @@ -4909,92 +4443,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/pac-proxy-agent": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", - "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", - "license": "MIT", - "dependencies": { - "@tootallnate/quickjs-emscripten": "^0.23.0", - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "get-uri": "^6.0.1", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.6", - "pac-resolver": "^7.0.1", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/pac-proxy-agent/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/pac-proxy-agent/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/pac-resolver": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", - "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", - "license": "MIT", - "dependencies": { - "degenerator": "^5.0.0", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/parent-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-2.0.0.tgz", - "integrity": "sha512-uo0Z9JJeWzv8BG+tRcapBKNJ0dro9cLyczGzulS6EfeyAdeC9sbojtW6XwvYxJkEne9En+J2XEl4zyglVeIwFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", @@ -5114,18 +4562,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, "node_modules/picomatch": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", @@ -5155,15 +4591,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/promise": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", @@ -5186,135 +4613,44 @@ "node": ">= 0.10" } }, - "node_modules/proxy-agent": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", - "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "http-proxy-agent": "^7.0.1", - "https-proxy-agent": "^7.0.6", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.1.0", - "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.5" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/proxy-agent/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/proxy-agent/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/proxy-agent/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" - }, - "node_modules/pump": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", - "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, "node_modules/puppeteer": { - "version": "24.40.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.40.0.tgz", - "integrity": "sha512-IxQbDq93XHVVLWHrAkFP7F7iHvb9o0mgfsSIMlhHb+JM+JjM1V4v4MNSQfcRWJopx9dsNOr9adYv0U5fm9BJBQ==", + "version": "25.2.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-25.2.0.tgz", + "integrity": "sha512-JPMPd/2+lgdkLhEyPqH895oR3ccMt1wSra6oewgjjTuLmo2s9zPZpKXQTFEIiA/fMKpiL01kjU3+2zPEReRWNg==", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.0", - "chromium-bidi": "14.0.0", - "cosmiconfig": "^9.0.0", - "devtools-protocol": "0.0.1581282", - "puppeteer-core": "24.40.0", - "typed-query-selector": "^2.12.1" + "@puppeteer/browsers": "3.0.5", + "chromium-bidi": "16.0.1", + "devtools-protocol": "0.0.1638949", + "lilconfig": "^3.1.3", + "puppeteer-core": "25.2.0", + "typed-query-selector": "^2.12.2" }, "bin": { - "puppeteer": "lib/cjs/puppeteer/node/cli.js" + "puppeteer": "lib/puppeteer/node/cli.js" }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, "node_modules/puppeteer-core": { - "version": "24.40.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.40.0.tgz", - "integrity": "sha512-MWL3XbUCfVgGR0gRsidzT6oKJT2QydPLhMITU6HoVWiiv4gkb6gJi3pcdAa8q4HwjBTbqISOWVP4aJiiyUJvag==", + "version": "25.2.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.2.0.tgz", + "integrity": "sha512-jGhuGAlkgOcbyGRc0Cm9b/y4vvqoxhyAyl6a1diVe8F3sHsgTaQ60QQT5F3rGegTZV3prysgHVc+0LsvPZo3GA==", "license": "Apache-2.0", "dependencies": { - "@puppeteer/browsers": "2.13.0", - "chromium-bidi": "14.0.0", - "debug": "^4.4.3", - "devtools-protocol": "0.0.1581282", - "typed-query-selector": "^2.12.1", - "webdriver-bidi-protocol": "0.4.1", - "ws": "^8.19.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/puppeteer-core/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" + "@puppeteer/browsers": "3.0.5", + "chromium-bidi": "16.0.1", + "devtools-protocol": "0.0.1638949", + "typed-query-selector": "^2.12.2", + "webdriver-bidi-protocol": "0.4.2", + "ws": "^8.21.0" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=22.12.0" } }, - "node_modules/puppeteer-core/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/qs": { "version": "6.15.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", @@ -5374,6 +4710,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -5482,6 +4819,7 @@ "version": "7.8.5", "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -5689,14 +5027,17 @@ "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/smart-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", - "license": "MIT", + "node_modules/smol-toml": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", + "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", + "dev": true, + "license": "BSD-3-Clause", "engines": { - "node": ">= 6.0.0", - "npm": ">= 3.0.0" + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" } }, "node_modules/socket.io": { @@ -5852,57 +5193,6 @@ "node": ">= 0.6" } }, - "node_modules/socks": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", - "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", - "license": "MIT", - "dependencies": { - "ip-address": "^10.1.1", - "smart-buffer": "^4.2.0" - }, - "engines": { - "node": ">= 10.0.0", - "npm": ">= 3.0.0" - } - }, - "node_modules/socks-proxy-agent": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", - "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/socks-proxy-agent/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/socks-proxy-agent/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -5929,17 +5219,6 @@ "node": ">=10.0.0" } }, - "node_modules/streamx": { - "version": "2.28.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.28.0.tgz", - "integrity": "sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==", - "license": "MIT", - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" - } - }, "node_modules/strict-event-emitter": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", @@ -5978,7 +5257,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^6.2.2" @@ -6062,41 +5340,6 @@ "node": ">=8" } }, - "node_modules/tar-fs": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", - "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0", - "tar-stream": "^3.1.5" - }, - "optionalDependencies": { - "bare-fs": "^4.0.1", - "bare-path": "^3.0.0" - } - }, - "node_modules/tar-stream": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz", - "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==", - "license": "MIT", - "dependencies": { - "b4a": "^1.6.4", - "bare-fs": "^4.5.5", - "fast-fifo": "^1.2.0", - "streamx": "^2.15.0" - } - }, - "node_modules/teex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", - "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", - "license": "MIT", - "dependencies": { - "streamx": "^2.12.5" - } - }, "node_modules/test-exclude": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-8.0.0.tgz", @@ -6112,15 +5355,6 @@ "node": "20 || >=22" } }, - "node_modules/text-decoder": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", - "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", - "license": "Apache-2.0", - "dependencies": { - "b4a": "^1.6.4" - } - }, "node_modules/tinyexec": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", @@ -6184,12 +5418,6 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, "node_modules/tsx": { "version": "4.22.4", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", @@ -6237,7 +5465,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -6341,9 +5569,9 @@ "license": "MIT" }, "node_modules/webdriver-bidi-protocol": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", - "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz", + "integrity": "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA==", "license": "Apache-2.0" }, "node_modules/whatwg-encoding": { @@ -6391,48 +5619,23 @@ "license": "MIT" }, "node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.0.tgz", + "integrity": "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" + "ansi-styles": "^6.2.3", + "string-width": "^8.2.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -6502,6 +5705,7 @@ "version": "17.7.3", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -6520,6 +5724,7 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -6529,6 +5734,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6538,6 +5744,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6547,6 +5754,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -6561,6 +5769,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -6569,16 +5778,6 @@ "node": ">=8" } }, - "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "license": "MIT", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 13b40b101..dcf8ced3c 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "morgan": "1.11.0", "node-w3capi": "2.2.2", "promise": "8.3.0", - "puppeteer": "24.40.0", + "puppeteer": "25.2.0", "socket.io": "4.8.3", "superagent": "10.3.0", "tmp": "0.2.7" @@ -43,16 +43,16 @@ "@types/superagent": "^8.1.9", "@types/tmp": "^0.2.6", "c8": "^11.0.0", - "cspell": "9.0.2", + "cspell": "10.0.1", "domhandler": "^6.0.1", "expect.js": "0.3", "husky": "9.0.11", - "lint-staged": "16.4.0", + "lint-staged": "17.0.8", "lodash.merge": "^4.6.2", "nock": "15.0.0", "prettier": "3.8.4", "tsx": "^4.21.0", - "typescript": "^6.0.2" + "typescript": "^6.0.3" }, "scripts": { "build": "tsc", From 8647139360532a7b429e638a7ca0a48e9a8317c6 Mon Sep 17 00:00:00 2001 From: "Kenneth G. Franqueiro" Date: Fri, 5 Jun 2026 15:12:09 -0400 Subject: [PATCH 06/11] Rules tests: Prevent empty suite noise when filtering via env vars (#2119) --- test/rules.ts | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/test/rules.ts b/test/rules.ts index f21ecfc59..f612a5ca4 100644 --- a/test/rules.ts +++ b/test/rules.ts @@ -309,12 +309,7 @@ function checkRule(tests: RuleTest[], options: CheckRuleOptions) { const url = `${ENDPOINT}/doc-views/${docType}/${suffix}?rule=${rule}&type=${test.data}`; // If the test is not mentioned in the environment variables, skip it. - if ( - (testRule && rule !== testRule) || - (testType && test.data !== testType) || - (testProfile && profile !== testProfile) - ) - return; + if (testType && test.data !== testType) return; it(`should ${passOrFail} for ${url}`, async () => { const { config } = await import( @@ -384,11 +379,15 @@ function runTestsForProfile({ Object.entries(rules).forEach(([category, rules]) => { Object.entries(rules).forEach(([rule, tests]) => { // Rule: hr/logo ... + if (testRule && rule !== testRule) return; + if (testType && !tests.some(({ data }) => data === testType)) + return; + describe(`Rule: ${category}.${rule}`, () => { checkRule(tests, { docType, track, - profile: profile.substring(0, profile.lastIndexOf('.')), + profile, category, rule, }); @@ -412,9 +411,10 @@ describe('Making sure Specberus is not broken...', () => { ([trackOrProfile, profilesOrRules]) => { // Profile: SUBM if (trackOrProfile === 'MEM-SUBM.js') { + if (testProfile && testProfile !== 'MEM-SUBM') return; return runTestsForProfile({ docType, - profile: trackOrProfile, + profile: 'MEM-SUBM', rules: profilesOrRules as Record< string, Record @@ -425,13 +425,21 @@ describe('Making sure Specberus is not broken...', () => { // Track: Note/Recommendation/Registry describe(`Track: ${trackOrProfile}`, () => { Object.entries(profilesOrRules).forEach( - ([profile, rules]) => + ([filename, rules]) => { + const profile = filename.slice( + 0, + filename.lastIndexOf('.') + ); + if (testProfile && testProfile !== profile) + return; + runTestsForProfile({ docType, track: trackOrProfile, profile, rules, - }) + }); + } ); }); } From f4d153554457cc7563d71415189c165fa84b2e56 Mon Sep 17 00:00:00 2001 From: "Kenneth G. Franqueiro" Date: Tue, 9 Jun 2026 12:49:35 -0400 Subject: [PATCH 07/11] Add test for validation=recursive; remove unreachable code & l10n string (#2120) --- lib/l10n-en_GB.ts | 2 - lib/rules/links/compound.ts | 99 +++++++++++++++++-------------------- test/api.ts | 46 ++++++++++++++++- test/lib/utils.ts | 16 +++--- 4 files changed, 99 insertions(+), 64 deletions(-) diff --git a/lib/l10n-en_GB.ts b/lib/l10n-en_GB.ts index e406799f6..25af80622 100644 --- a/lib/l10n-en_GB.ts +++ b/lib/l10n-en_GB.ts @@ -155,8 +155,6 @@ export const messages = { // links/compound 'links.compound.skipped': 'HTML and CSS validations for compound documents were skipped. ', - 'links.compound.no-validation': - 'Validation of ${file} HTML.', 'links.compound.link': 'Validation of ${file} HTML ${markup}.', 'links.compound.error': diff --git a/lib/rules/links/compound.ts b/lib/rules/links/compound.ts index 27e765a2a..d9be5d058 100644 --- a/lib/rules/links/compound.ts +++ b/lib/rules/links/compound.ts @@ -33,63 +33,52 @@ export const check: RuleCheckFunction = sr => { if (!links.length) return; const markupService = 'https://validator.w3.org/nu/'; - if (validation === 'recursive') { - return Promise.all( - links.map(l => { - const ua = `W3C-Pubrules/${sr.version}`; - const req = get(markupService) - .set('User-Agent', ua) - .query({ doc: l, out: 'json' }) - .on('error', err => { - sr.error(self, 'error', { + return Promise.all( + links.map(l => { + const ua = `W3C-Pubrules/${sr.version}`; + const req = get(markupService) + .set('User-Agent', ua) + .query({ doc: l, out: 'json' }) + .on('error', err => { + sr.error(self, 'error', { + file: l.split('/').pop(), + link: l, + errMsg: err, + }); + }) + .timeout(TIMEOUT); + return req.then( + res => { + const json = res.body; + if (!json) + throw new Error( + 'No JSON returned from HTML validator.' + ); + + const errors = + json.messages?.filter( + (msg: { type: string }) => msg.type === 'error' + ) || []; + if (errors.length === 0) { + sr.info(self, 'link', { file: l.split('/').pop(), link: l, - errMsg: err, + markup: '\u2714', + }); + } else { + sr.error(self, 'link', { + file: l.split('/').pop(), + link: l, + markup: '\u2718', }); - }) - .timeout(TIMEOUT); - return req.then( - res => { - const json = res.body; - if (!json) - throw new Error( - 'No JSON returned from HTML validator.' - ); - - const errors = - json.messages?.filter( - (msg: { type: string }) => msg.type === 'error' - ) || []; - if (errors.length === 0) { - sr.info(self, 'link', { - file: l.split('/').pop(), - link: l, - markup: '\u2714', - }); - } else { - sr.error(self, 'link', { - file: l.split('/').pop(), - link: l, - markup: '\u2718', - }); - } - }, - (err: ResponseError) => { - if (err.timeout) sr.warning(self, 'html-timeout'); - else - throw new Error( - `HTML validator error: ${err.message}` - ); } - ); - }) - ).then(() => {}); - } else { - for (const l of links) { - sr.info(self, 'no-validation', { - file: l.split('/').pop(), - link: l, - }); - } - } + }, + (err: ResponseError) => { + if (err.timeout) sr.warning(self, 'html-timeout'); + else + throw new Error(`HTML validator error: ${err.message}`); + } + ); + }) + ).then(() => {}); }; diff --git a/test/api.ts b/test/api.ts index 9071c88e1..849edca02 100644 --- a/test/api.ts +++ b/test/api.ts @@ -3,16 +3,19 @@ */ import assert from 'assert'; +import { readFile } from 'fs/promises'; import http, { type Server } from 'http'; import { join } from 'path'; import { after, before, describe, it } from 'node:test'; import express from 'express'; import fileUpload from 'express-fileupload'; +import nock from 'nock'; import superagent, { type Response, type ResponseError } from 'superagent'; import { setUp } from '../lib/api.js'; import { specberusVersion } from '../lib/util.js'; +import type { SpecberusResult } from '../lib/validator.js'; import { cleanupMocks, setupMocks } from './lib/utils.js'; // Settings: @@ -133,11 +136,20 @@ describe('API', () => { }); describe('Method “validate”', () => { + const imscPath = join(testDocsPath, 'ttml-imsc1.html'); + before(async () => { + // Additional mock for recursive validation test (which requires url) + nock('https://www.w3.org') + .persist() + .get(/^\/TR\/ttml-imsc1.3\/(Overview\.html)?$/) + .reply(200, await readFile(imscPath, 'utf8')); + }); + it('Should 400 and return an array of errors when validation fails', () => assert.rejects( createPostRequest('validate') .field('profile', 'REC') - .attach('file', join(testDocsPath, 'ttml-imsc1.html')), + .attach('file', imscPath), (error: any) => { assertResponseStatus(error.response, 400); const { success, errors } = JSON.parse( @@ -159,6 +171,38 @@ describe('API', () => { } )); + it('Should run compound and linkchecker rules when validation=recursive and url are specified', () => + // Note: This uses the same document as the previous test, so it still expects errors + assert.rejects( + get( + `validate?profile=REC&validation=recursive&url=${encodeURIComponent( + 'https://www.w3.org/TR/ttml-imsc1.3/' + )}` + ), + (error: any) => { + assertResponseStatus(error.response, 400); + const { success, errors, info, warnings } = JSON.parse( + getErrorResponseText(error) + ) as SpecberusResult; + assert.strictEqual(success, false); + assert(errors.length > 0, 'Response should report errors'); + assert( + info.some( + ({ name, key }) => + name === 'links.compound' && key === 'link' + ) + ); + assert( + warnings.some( + ({ name, key }) => + name === 'links.linkchecker' && + key === 'display' + ) + ); + return true; + } + )); + it('Should 400 with error on POST if "file" is provided but "profile" is not', () => assert.rejects( createPostRequest('validate').attach( diff --git a/test/lib/utils.ts b/test/lib/utils.ts index 5552bb93e..a864772d0 100644 --- a/test/lib/utils.ts +++ b/test/lib/utils.ts @@ -83,7 +83,7 @@ export const buildBadTestCases = async () => { * @param {Request} req */ function warnOnNonLocalRequest(req: Request) { - if (!req.url.includes('//localhost')) { + if (!/\/\/(localhost|127\.0\.0\.1)/.test(req.url)) { console.warn('Unmocked non-local request:', req.url, req.body); } } @@ -92,7 +92,9 @@ function warnOnNonLocalRequest(req: Request) { export function setupMocks(overrides?: Partial) { // Report non-local URLs that were not mocked during test runs nock.emitter.on('no match', warnOnNonLocalRequest); - nock.enableNetConnect('localhost'); // Only allow localhost requests to proceed unmocked + // Only allow localhost requests to proceed unmocked + // (localhost matches documents; 127.0.0.1 matches puppeteer debug request) + nock.enableNetConnect(/localhost|127\.0\.0\.1/); const mockData: typeof nockData = overrides ? merge({}, nockData, overrides) @@ -215,10 +217,12 @@ export function setupMocks(overrides?: Partial) { // Mock Nu HTML Checker requests to return no messages // (without mocks, the validate API tests result in an error from the service anyway) - nock('https://validator.w3.org') - .persist() - .post('/nu/?out=json') - .reply(200, { messages: [] }); + for (const method of ['GET', 'POST']) { + nock('https://validator.w3.org') + .persist() + .intercept(/^\/nu\/.*out=json/, method) + .reply(200, { messages: [] }); + } } /** Cleans up mocks and event handler from setupMocks. */ From 24956fa68e14370afdd03592818fb23410baf72c Mon Sep 17 00:00:00 2001 From: "Kenneth G. Franqueiro" Date: Tue, 23 Jun 2026 11:47:39 -0400 Subject: [PATCH 08/11] Remove stale ignore rules and ignore built *.d.ts for cspell (#2128) --- .cspell.json | 8 ++------ .gitignore | 3 --- .prettierignore | 1 - 3 files changed, 2 insertions(+), 10 deletions(-) diff --git a/.cspell.json b/.cspell.json index 064665e0b..7ab442e91 100644 --- a/.cspell.json +++ b/.cspell.json @@ -108,20 +108,16 @@ "**/*.ttf", "**/*.woff", "**/*.svg", - ".nyc_output/**", ".github/**", "coverage/**", "test/**/*.html", "package-lock.json", "tsconfig.json", - "lib/**/*.js", + "{lib,test}/**/*.{d.ts,js}", "node_modules/**", "design/**" ], - "ignoreRegExpList": [ - "/require\\(.*\\);/", - "(FIXME|TODO|XXX)\\(.[^\\)]+\\)" - ], + "ignoreRegExpList": ["(FIXME|TODO|XXX)\\(.[^\\)]+\\)"], "overrides": [ { "filename": ["package.json"], diff --git a/.gitignore b/.gitignore index 4bdafb3e0..f51d3fce9 100644 --- a/.gitignore +++ b/.gitignore @@ -26,9 +26,6 @@ scratch .DS_Store */.DS_Store -.nyc_output -.eslintcache - # TS build output app.js *.d.ts diff --git a/.prettierignore b/.prettierignore index 6d20308e3..36dc9de2a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,2 @@ *.html *.handlebars -.nyc_output From 2f48ece5f86622eef261f5b224ba3fc5510d1016 Mon Sep 17 00:00:00 2001 From: "Kenneth G. Franqueiro" Date: Tue, 23 Jun 2026 12:47:30 -0400 Subject: [PATCH 09/11] Split main Specberus and rule check APIs into separate classes (#2114) Co-authored-by: Denis Ah-Kang <1696128+deniak@users.noreply.github.com> --- README.md | 20 +- app.ts | 2 +- lib/api.ts | 2 +- lib/{validator.ts => rule-context.ts} | 310 +++++----------------- lib/rules/echidna/deliverer-change.ts | 14 +- lib/rules/echidna/todays-date.ts | 8 +- lib/rules/headers/copyright.ts | 59 ++-- lib/rules/headers/details-summary.ts | 20 +- lib/rules/headers/div-head.ts | 4 +- lib/rules/headers/dl.ts | 79 +++--- lib/rules/headers/editor-participation.ts | 12 +- lib/rules/headers/errata.ts | 16 +- lib/rules/headers/github-repo.ts | 10 +- lib/rules/headers/h1-title.ts | 14 +- lib/rules/headers/h2-toc.ts | 19 +- lib/rules/headers/hr.ts | 11 +- lib/rules/headers/logo.ts | 8 +- lib/rules/headers/memsub-copyright.ts | 8 +- lib/rules/headers/ol-toc.ts | 6 +- lib/rules/headers/secno.ts | 6 +- lib/rules/headers/shortname.ts | 52 ++-- lib/rules/headers/subm-logo.ts | 8 +- lib/rules/headers/translation.ts | 18 +- lib/rules/headers/w3c-state.ts | 20 +- lib/rules/heuristic/date-format.ts | 11 +- lib/rules/links/compound.ts | 20 +- lib/rules/links/internal.ts | 8 +- lib/rules/links/linkchecker.ts | 27 +- lib/rules/links/reliability.ts | 12 +- lib/rules/metadata/abstract.ts | 14 +- lib/rules/metadata/charters.ts | 2 +- lib/rules/metadata/deliverers.ts | 6 +- lib/rules/metadata/dl.ts | 14 +- lib/rules/metadata/docDate.ts | 4 +- lib/rules/metadata/editor-ids.ts | 8 +- lib/rules/metadata/editor-names.ts | 6 +- lib/rules/metadata/errata.ts | 6 +- lib/rules/metadata/informative.ts | 8 +- lib/rules/metadata/process.ts | 4 +- lib/rules/metadata/profile.ts | 24 +- lib/rules/metadata/sotd.ts | 6 +- lib/rules/metadata/title.ts | 6 +- lib/rules/sotd/candidate-review-end.ts | 22 +- lib/rules/sotd/charter.ts | 28 +- lib/rules/sotd/deliverer-note.ts | 6 +- lib/rules/sotd/deployment.ts | 8 +- lib/rules/sotd/diff.ts | 4 +- lib/rules/sotd/draft-stability.ts | 12 +- lib/rules/sotd/new-features.ts | 14 +- lib/rules/sotd/obsl-rescind.ts | 27 +- lib/rules/sotd/pp.ts | 41 +-- lib/rules/sotd/process-document.ts | 20 +- lib/rules/sotd/publish.ts | 36 +-- lib/rules/sotd/rec-addition.ts | 26 +- lib/rules/sotd/rec-comment-end.ts | 26 +- lib/rules/sotd/stability.ts | 43 +-- lib/rules/sotd/submission.ts | 34 +-- lib/rules/sotd/supersedable.ts | 14 +- lib/rules/sotd/usage.ts | 8 +- lib/rules/structure/canonical.ts | 9 +- lib/rules/structure/display-only.ts | 20 +- lib/rules/structure/h2.ts | 16 +- lib/rules/structure/name.ts | 20 +- lib/rules/structure/neutral.ts | 8 +- lib/rules/structure/section-ids.ts | 14 +- lib/rules/structure/security-privacy.ts | 12 +- lib/rules/style/back-to-top.ts | 6 +- lib/rules/style/body-toc-sidebar.ts | 7 +- lib/rules/style/meta.ts | 8 +- lib/rules/style/script.ts | 6 +- lib/rules/style/sheet.ts | 10 +- lib/rules/validation/html.ts | 34 +-- lib/rules/validation/wcag.ts | 4 +- lib/specberus.ts | 232 ++++++++++++++++ lib/types.d.ts | 7 +- lib/util.ts | 2 +- package.json | 2 +- test/api.ts | 2 +- test/l10n.ts | 4 +- test/rules.ts | 2 +- 80 files changed, 901 insertions(+), 805 deletions(-) rename lib/{validator.ts => rule-context.ts} (75%) create mode 100644 lib/specberus.ts diff --git a/README.md b/README.md index 650734c26..625787c49 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Specberus is a [Node.js](https://nodejs.org/en/) application, [distributed throu Alternatively, you can clone [the repository](https://github.com/w3c/specberus) and run: ```bash -$ npm install -d +$ npm install ``` In order to get all the dependencies installed. Naturally, this requires that you have a reasonably @@ -30,8 +30,7 @@ recent version of Node.js installed. ## 2. Running -Currently there is no shell to run Specberus. Later we will add both Web and CLI interfaces based -on the same core library. +Specberus runs as a web server, providing both HTML form UI and API endpoints. ### Syntax and command-line parameters @@ -147,12 +146,12 @@ $ RULE=copyright TYPE=noCopyright npm run test ## 4. JS API -The interface you get when you `import { Specberus } from "specberus"` is that from `lib/validator`. +The interface you get when you `import { Specberus } from "specberus"` is that from `lib/specberus`. `Specberus` is a class configured for operation in the Node.js environment. (See also [the REST API](#5-rest-api).) -## Creating a Validator instance +## Creating a Specberus instance ```js import { Specberus } from 'specberus'; @@ -435,22 +434,27 @@ Events listed below are expressed with parameters to reflect what is passed to t ## 8. Writing rules -Rules are simple modules that just expose a `check(sr)` method. They receive a Specberus object, +Rules are simple modules that expose a `check(context)` method. They receive a context object, which they use to examine the document and fire validation events. They return a promise which resolves on completion (regardless of pass or fail) or rejects on unexpected system error (exception). Usually, they are written as `async` functions to automatically handle the resolve vs. reject distinction. -The Specberus object exposes the following APIs useful for validation: +The context object includes the following APIs useful for validation: + +### Properties - `source`. The HTML source of the document being processed - `url`. The URL of the document being processed, only applicable if `options.url` was specified +- `version`. The Specberus version. + +### Methods + - `error`, `warn`, `info`. Methods for firing respective levels of events on the instance. All three methods accept the same arguments: - `rule` object: at minimum, an object with a `name` string. May also contain `rule` and `section` strings. - `key` string: specifies the precise occurrence within the particular `rule` - `extra` object (optional): any additional fields to include within the event -- `version`. The Specberus version. - `checkSelector(selector, ruleName)`. Some rules need to do nothing other than to check that a selector returns some content. This handles checking the selector, reporting an error if it is not found, or throwing an error if the selector is invalid. diff --git a/app.ts b/app.ts index 0051eacc6..6d05f1237 100644 --- a/app.ts +++ b/app.ts @@ -19,7 +19,7 @@ import * as api from './lib/api.js'; import badterms from './lib/badterms.js'; import * as l10n from './lib/l10n.js'; import { allProfiles, specberusVersion } from './lib/util.js'; -import { ExceptionsError, Specberus } from './lib/validator.js'; +import { ExceptionsError, Specberus } from './lib/specberus.js'; import * as views from './lib/views.js'; import type { ProfileModule } from './lib/types.js'; diff --git a/lib/api.ts b/lib/api.ts index cdc149349..7ba139903 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -12,7 +12,7 @@ import { Specberus, type SpecberusResult, type ValidateOptions, -} from './validator.js'; +} from './specberus.js'; /** Data types emitted by error events */ type ErrorHandlerMessage = diff --git a/lib/validator.ts b/lib/rule-context.ts similarity index 75% rename from lib/validator.ts rename to lib/rule-context.ts index b60b0fdc5..a601012b3 100644 --- a/lib/validator.ts +++ b/lib/rule-context.ts @@ -2,48 +2,26 @@ * @file Main file of the Specberus npm package. */ -import EventEmitter from 'events'; -import { access, constants, readFile } from 'fs/promises'; - -import { type Cheerio, load } from 'cheerio'; +import { type Cheerio, type CheerioAPI, load } from 'cheerio'; import type { Element } from 'domhandler'; // @ts-ignore (no typings) import w3cApi from 'node-w3capi'; import { hasExceptions } from './exceptions.js'; -import { assembleData, setLanguage } from './l10n.js'; -import * as profileMetadata from './profiles/metadata.js'; -import * as profileAdditionalMetadata from './profiles/additionalMetadata.js'; +import { assembleData } from './l10n.js'; +// Reminder: specberus.js imports this module; only types may be imported in the other direction +import type { Specberus } from './specberus.js'; import { get } from './throttled-ua.js'; -import { AB, processParams, REC_TEXT, specberusVersion, TAG } from './util.js'; +import { AB, REC_TEXT, specberusVersion, TAG } from './util.js'; import type { ApiCharter, ApiSpecificationVersion, - HandlerMessage, - ProfileModule, RecMetadata, RuleBase, RuleMeta, SpecberusConfig, } from './types.js'; -setLanguage('en_GB'); - -interface BaseOptions { - file?: string; - source?: string; - url?: string; -} - -interface ExtractMetadataOptions extends BaseOptions { - additionalMetadata?: boolean; -} - -export interface ValidateOptions extends BaseOptions { - profile: ProfileModule; - validation?: 'no-validation' | 'recursive'; -} - type HeaderMap = Record< string, { @@ -107,6 +85,10 @@ const abbrMonths = [ ]; export const possibleMonths = [...months, ...abbrMonths].join('|'); +const separator = '[ -]{1}'; + +export const dateRegexStrCapturing = `(\\d?\\d)${separator}(${possibleMonths})${separator}(\\d{4})`; +const dateRegexStrNonCapturing = `\\d?\\d${separator}(?:${possibleMonths})${separator}\\d{4}`; // Regular expressions used by getDelivererIDs and getDelivererGroups @@ -118,59 +100,20 @@ const REGEX_TAG_DISCLOSURE = /https?:\/\/www.w3.org\/2001\/tag\/disclosures/; const REGEX_DELIVERER_IPR_URL = /^https:\/\/www\.w3\.org\/groups\/([^/]+)\/([^/]+)\/ipr\/?(#.*)?$/i; -const separator = '[ -]{1}'; - -interface ExceptionsErrorOptions extends ErrorOptions { - exceptions: string[]; -} - -export interface SpecberusResult { - errors: HandlerMessage[]; - info: HandlerMessage[]; - metadata: Record; - success: boolean; - warnings: HandlerMessage[]; -} - /** - * Error which includes list of exception messages, - * thrown in case of unexpected errors during extractMetadata or validate + * Encapsulates all methods of interest to rule check functions, + * separate from the APIs responsible for core Specberus requests. */ -export class ExceptionsError extends Error { - exceptions: string[]; - - constructor(message?: string, options?: ExceptionsErrorOptions) { - super(message, options); - this.exceptions = options?.exceptions || []; - } -} - -type SpecberusMessageEventArgs = [ - RuleMeta | RuleBase, - { - detailMessage: string; - extra?: Record; - key: string; - }, -]; - -interface SpecberusEvents { - done: [string]; - err: SpecberusMessageEventArgs; - exception: [{ message: string }]; - info: SpecberusMessageEventArgs; - warning: SpecberusMessageEventArgs; -} - -export class Specberus extends EventEmitter { - $ = load(''); +export class RuleContext { + $: CheerioAPI; + /** + * Configuration that Specberus parsed from params. + * Only present for validation (not metadata extraction). + */ config: SpecberusConfig | undefined; - source: string | undefined; - url: string | undefined; version = specberusVersion; - // Private fields - + #sr: Specberus; #$docDateEl: Cheerio | undefined; #$sotdSection: Cheerio | null | undefined; /** Group objects returned by W3C API charters endpoint */ @@ -180,101 +123,59 @@ export class Specberus extends EventEmitter { #delivererIDs: number[] | Promise | undefined; #delivererGroups: Promise | undefined; #docDate: Date | undefined; - /** Stores messages from any unexpected errors encountered during process */ - #exceptions: string[] = []; #headers: HeaderMap | undefined; #isFirstPublic: boolean | undefined; #previousVersion: Promise | undefined; #shortname: string | undefined = undefined; - /** - * Internal function for handling common end-state logic for extractMetadata and validate, - * returning results (resolving) or throwing an error if exceptions occurred (rejecting). - */ - #reportResult(result: Omit): SpecberusResult { - if (this.#exceptions.length) { - throw new ExceptionsError( - 'The following unexpected errors occurred:\n' + - this.#exceptions.join('\n'), - { exceptions: this.#exceptions } - ); - } - return { - ...result, - success: !result.errors.length, - }; + constructor(sr: Specberus, $: CheerioAPI, config?: SpecberusConfig) { + this.$ = $; + this.#sr = sr; + if (config) this.config = config; } - /** Internal function containing setup logic common to both extractMetadata and validate. */ - async #prepare(options: ExtractMetadataOptions | ValidateOptions) { - const errors: HandlerMessage[] = []; - const warnings: HandlerMessage[] = []; - const info: HandlerMessage[] = []; + get source() { + return this.#sr.source; + } - this.on('err', (rule, data) => { - errors.push({ ...rule, ...data }); - }); - this.on('warning', (rule, data) => { - warnings.push({ ...rule, ...data }); - }); - this.on('info', (rule, data) => { - info.push({ ...rule, ...data }); - }); + get url() { + return this.#sr.url; + } + /** + * Checks for presence of a selector. + * Reports a not-found error for the specified rule if no match is found. + */ + checkSelector(sel: string, rule: RuleMeta) { try { - this.$ = await this.#load(options); - } catch (error) { - this.#throw(error.toString()); - throw error; + if (!this.$(sel).length) this.error(rule, 'not-found'); + } catch (e) { + throw new Error(`Invalid selector '${sel}': ${e}`); } - - return { errors, info, warnings }; } - async extractMetadata(options: ExtractMetadataOptions) { - const messages = await this.#prepare(options); - const metadata: Record = {}; - const profile = options.additionalMetadata - ? profileAdditionalMetadata - : profileMetadata; - - await Promise.all( - profile.rules.map(async rule => { - try { - const result = await rule.check(this); - if (result) - for (const [key, value] of Object.entries(result)) - metadata[key] = value; - } catch (error) { - this.#throw(error.message); - } finally { - this.emit('done', rule.name); - } - }) - ); - return this.#reportResult({ ...messages, metadata }); + /** + * Normalizes a string by removing leading/trailing whitespace + * and condensing multiple consecutive whitespace characters to one. + */ + norm(str: string) { + if (!str) return ''; + return `${str}` + .replace(/^\s+/, '') + .replace(/\s+$/, '') + .replace(/\s+/g, ' '); } - async validate(options: ValidateOptions) { - if (!options.profile) - throw new Error('Without a profile there is nothing to check.'); - - const { profile } = options; - this.config = await processParams(options, profile.config); - const messages = await this.#prepare(options); - - await Promise.all( - profile.rules.map(async rule => { - try { - await rule.check(this); - } catch (error) { - this.#throw(error.message); - } finally { - this.emit('done', rule.name); - } - }) - ); - return this.#reportResult({ ...messages, metadata: {} }); + stringToDate(str: string) { + const rex = new RegExp(dateRegexStrCapturing); + const matches = str.match(rex); + if (matches) { + return new Date( + +matches[3], + months.indexOf(matches[2]), + +matches[1] + ); + } } error(rule: RuleBase | RuleMeta, key: string, extra?: Record) { @@ -285,7 +186,7 @@ export class Specberus extends EventEmitter { ) this.warning(rule, key, extra); else - this.emit('err', rule, { + this.#sr.emit('err', rule, { key, ...(extra && { extra }), detailMessage: assembleData(null, rule, key, extra).message, @@ -297,7 +198,7 @@ export class Specberus extends EventEmitter { key: string, extra?: Record ) { - this.emit('warning', rule, { + this.#sr.emit('warning', rule, { key, ...(extra && { extra }), detailMessage: assembleData(null, rule, key, extra).message, @@ -305,69 +206,17 @@ export class Specberus extends EventEmitter { } info(rule: RuleBase | RuleMeta, key: string, extra?: Record) { - this.emit('info', rule, { + this.#sr.emit('info', rule, { key, ...(extra && { extra }), detailMessage: assembleData(null, rule, key, extra).message, }); } - /** - * Emits an exception event, intended to signify that the process stopped on a critical error. - * - * NOTE: This should not be called from rules; they should throw an Error, - * which will result in extractMetadata or validate invoking this method. - */ - #throw(message: string) { - console.error(`[EXCEPTION] ${message}`); - this.emit('exception', { message }); - // Track in exceptions array, used to determine whether to resolve or reject process - this.#exceptions.push(message); - } - - /** - * Checks for presence of a selector. - * Reports a not-found error for the specified rule if no match is found. - */ - checkSelector(sel: string, rule: RuleMeta) { - try { - if (!this.$(sel).length) this.error(rule, 'not-found'); - } catch (e) { - throw new Error(`Invalid selector '${sel}': ${e}`); - } - } - - /** - * Normalizes a string by removing leading/trailing whitespace - * and condensing multiple consecutive whitespace characters to one. - */ - norm(str: string) { - if (!str) return ''; - return `${str}` - .replace(/^\s+/, '') - .replace(/\s+$/, '') - .replace(/\s+/g, ' '); - } - - static dateRegexStrCapturing = `(\\d?\\d)${separator}(${possibleMonths})${separator}(\\d{4})`; - static dateRegexStrNonCapturing = `\\d?\\d${separator}(?:${possibleMonths})${separator}\\d{4}`; - - stringToDate(str: string) { - const rex = new RegExp(Specberus.dateRegexStrCapturing); - const matches = str.match(rex); - if (matches) { - return new Date( - +matches[3], - months.indexOf(matches[2]), - +matches[1] - ); - } - } - getDocumentDate() { if (this.#docDate) return this.#docDate; const rex = new RegExp( - `${Specberus.dateRegexStrCapturing}(?:, edited in place ${Specberus.dateRegexStrNonCapturing})?$` + `${dateRegexStrCapturing}(?:, edited in place ${dateRegexStrNonCapturing})?$` ); const $el = this.$('#w3c-state'); @@ -514,7 +363,7 @@ export class Specberus extends EventEmitter { const dates = { list: [] as Date[], valid: [] as Date[] }; if ($sotd) { const txt = this.norm($sotd.text()); - const rex = new RegExp(Specberus.dateRegexStrCapturing, 'g'); + const rex = new RegExp(dateRegexStrCapturing, 'g'); const docDate = this.getDocumentDate()!; const lowBound = new Date(docDate).setDate( new Date(docDate).getDate() + 27 @@ -815,43 +664,6 @@ export class Specberus extends EventEmitter { return this.#previousVersion; } - #load(options: ExtractMetadataOptions | ValidateOptions) { - if (options.url) return this.#loadURL(options.url); - if (options.source) return this.#loadSource(options.source); - if (options.file) return this.#loadFile(options.file); - throw new Error('url, source, or file must be specified.'); - } - - #loadURL(url: string) { - return get(url) - .set('User-Agent', `W3C-Pubrules/${specberusVersion}`) - .then(res => { - if (!res.text) throw new Error(`Body of ${url} is empty.`); - this.url = url; - return this.#loadSource(res.text); - }); - } - - #loadSource(src: string) { - this.source = src; - try { - return load(src); - } catch (e) { - throw new Error( - `Cheerio failed to parse source: ${JSON.stringify(e)}` - ); - } - } - - async #loadFile(file: string) { - try { - await access(file, constants.F_OK); - } catch (error) { - throw new Error(`File '${file}' not found or inaccessible.`); - } - return this.#loadSource(await readFile(file, 'utf8')); - } - transition(options: TransitionOptions) { const documentDate = this.getDocumentDate(); if (documentDate && 'from' in options && documentDate < options.from) diff --git a/lib/rules/echidna/deliverer-change.ts b/lib/rules/echidna/deliverer-change.ts index 2690e9eea..ec8f4ab45 100644 --- a/lib/rules/echidna/deliverer-change.ts +++ b/lib/rules/echidna/deliverer-change.ts @@ -27,13 +27,9 @@ async function getPreviousDelivererIDs( return data.map(({ id }) => id); } -/** - * @param sr - * @param done - */ -export const check: RuleCheckFunction = async sr => { - const previousVersion = await sr.getPreviousVersion(); - const shortname = await sr.getShortname(); +export const check: RuleCheckFunction = async context => { + const previousVersion = await context.getPreviousVersion(); + const shortname = await context.getShortname(); if (!previousVersion || !shortname) return; @@ -41,14 +37,14 @@ export const check: RuleCheckFunction = async sr => { shortname, previousVersion ); - const delivererIDs = await sr.getDelivererIDs(); + const delivererIDs = await context.getDelivererIDs(); const delivererChanged = delivererIDs.sort().toString() !== previousDelivererIDs.sort().toString(); if (delivererChanged) { - sr.error(self, 'deliverer-changed', { + context.error(self, 'deliverer-changed', { this: delivererIDs.sort().toString(), previous: previousDelivererIDs.sort().toString(), }); diff --git a/lib/rules/echidna/todays-date.ts b/lib/rules/echidna/todays-date.ts index 60b202c5f..3c3ba94fe 100644 --- a/lib/rules/echidna/todays-date.ts +++ b/lib/rules/echidna/todays-date.ts @@ -9,7 +9,7 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = sr => { +export const check: RuleCheckFunction = context => { /** * Get the timestamp of a day, regardless the time of the day. * This function creates a new `Date` to avoid modifying the original one. @@ -21,10 +21,10 @@ export const check: RuleCheckFunction = sr => { return new Date(date.getTime()).setHours(0, 0, 0, 0); } - const documentDate = sr.getDocumentDate(); + const documentDate = context.getDocumentDate(); if (!(documentDate instanceof Date)) { - sr.error(self, 'date-not-detected'); + context.error(self, 'date-not-detected'); } else if (getDateTime(documentDate) !== getDateTime(new Date())) { - sr.error(self, 'wrong-date'); + context.error(self, 'wrong-date'); } }; diff --git a/lib/rules/headers/copyright.ts b/lib/rules/headers/copyright.ts index b22ff895c..f96610ea4 100644 --- a/lib/rules/headers/copyright.ts +++ b/lib/rules/headers/copyright.ts @@ -11,7 +11,7 @@ import type { Cheerio } from 'cheerio'; import type { Element } from 'domhandler'; import { AB, TAG } from '../../util.js'; -import type { Specberus } from '../../validator.js'; +import type { RuleContext } from '../../rule-context.js'; import copyrightExceptions from '../../copyright-exceptions.js'; import type { ApiCharter, RuleCheckFunction, RuleMeta } from '../../types.js'; @@ -52,8 +52,8 @@ const latestBaseLinks = { export const { name } = self; -async function isOnlyPublishedByTagOrAb(sr: Specberus) { - const delivererIDs = await sr.getDelivererIDs(); +async function isOnlyPublishedByTagOrAb(context: RuleContext) { + const delivererIDs = await context.getDelivererIDs(); return delivererIDs.every(id => id === TAG.id || id === AB.id); } @@ -66,9 +66,12 @@ function getCommonLicenseUri(data: ApiCharter[]) { } // The date can be 19xx-2023, or 2023. -function getLatestCopyrightMatchRegex(sr: Specberus, licenseTexts: string[]) { +function getLatestCopyrightMatchRegex( + context: RuleContext, + licenseTexts: string[] +) { const licenseRex = licenseTexts.join('|'); - const year = (sr.getDocumentDate() || new Date()).getFullYear(); + const year = (context.getDocumentDate() || new Date()).getFullYear(); const startRex = '^Copyright [©|©] (?:(?:199\\d|20\\d\\d)-)?@YEAR *World Wide Web Consortium'.replace( @@ -81,19 +84,19 @@ function getLatestCopyrightMatchRegex(sr: Specberus, licenseTexts: string[]) { // Some documents like epub-33 uses special copyrights listed in copyright-exception.json function checkSpecialCopyright( - sr: Specberus, + context: RuleContext, $copyright: Cheerio, specialCopyright: (typeof copyrightExceptions)[number], shortname: string | undefined ) { - const year = (sr.getDocumentDate() || new Date()).getFullYear(); + const year = (context.getDocumentDate() || new Date()).getFullYear(); - const domHtml = sr.norm($copyright.html()!); - const specHtml = sr.norm( + const domHtml = context.norm($copyright.html()!); + const specHtml = context.norm( specialCopyright.copyright.replace(/@YEAR/g, '' + year) ); if (domHtml !== specHtml) { - sr.error(self, 'exception-no-html', { + context.error(self, 'exception-no-html', { copyright: domHtml, expected: specHtml, shortname, @@ -102,14 +105,14 @@ function checkSpecialCopyright( } function checkLatestCopyright( - sr: Specberus, + context: RuleContext, $copyright: Cheerio, licenseTexts: string[] ) { - const matchRegex = getLatestCopyrightMatchRegex(sr, licenseTexts); - const regResult = sr.norm($copyright.text()).match(matchRegex); + const matchRegex = getLatestCopyrightMatchRegex(context, licenseTexts); + const regResult = context.norm($copyright.text()).match(matchRegex); if (!regResult) { - sr.error(self, 'no-match', { rex: matchRegex }); + context.error(self, 'no-match', { rex: matchRegex }); return; } @@ -124,11 +127,11 @@ function checkLatestCopyright( const links = $copyright.find('a').toArray(); Object.keys(linksToCheck).forEach(linkText => { const link = links.find(link => - sr.norm(sr.$(link).text()).includes(linkText) + context.norm(context.$(link).text()).includes(linkText) ); if (!link) { - return sr.error(self, 'no-link', { text: linkText }); + return context.error(self, 'no-link', { text: linkText }); } const linkHref = link.attribs.href; @@ -139,7 +142,7 @@ function checkLatestCopyright( : expected === linkHref; if (!linkFound) { - sr.error(self, 'href-not-match', { + context.error(self, 'href-not-match', { expected: isExpectedArray ? expected.join("' or '") : expected, hrefInDoc: linkHref, text: linkText, @@ -148,30 +151,30 @@ function checkLatestCopyright( }); } -export const check: RuleCheckFunction = async sr => { - const $copyright = sr.$('body div.head p.copyright').first(); +export const check: RuleCheckFunction = async context => { + const $copyright = context.$('body div.head p.copyright').first(); if (!$copyright.length) { - sr.error(self, 'not-found'); + context.error(self, 'not-found'); return; } - if (await isOnlyPublishedByTagOrAb(sr)) { + if (await isOnlyPublishedByTagOrAb(context)) { return; } - const chartersData = await sr.getChartersData(); + const chartersData = await context.getChartersData(); if (!chartersData || !chartersData.length) { - sr.error(self, 'no-data-from-API'); + context.error(self, 'no-data-from-API'); return; } const allowedLicenses = getCommonLicenseUri(chartersData); if (!allowedLicenses.length && chartersData.length > 1) { - sr.error(self, 'no-license-found-joint'); + context.error(self, 'no-license-found-joint'); return; } if (!allowedLicenses.length) { - sr.error(self, 'no-license-found'); + context.error(self, 'no-license-found'); return; } @@ -181,14 +184,14 @@ export const check: RuleCheckFunction = async sr => { .map(v => LICENSE_URL_TEXT_MAP[v]); // get exception rule for certain shortnames - const shortname = await sr.getShortname(); + const shortname = await context.getShortname(); const specialCopyright = copyrightExceptions.find( ({ specShortnames }) => shortname && specShortnames.includes(shortname) ); if (specialCopyright) { - checkSpecialCopyright(sr, $copyright, specialCopyright, shortname); + checkSpecialCopyright(context, $copyright, specialCopyright, shortname); } else { - checkLatestCopyright(sr, $copyright, licenseTexts); + checkLatestCopyright(context, $copyright, licenseTexts); } }; diff --git a/lib/rules/headers/details-summary.ts b/lib/rules/headers/details-summary.ts index eef51847d..d62444ade 100644 --- a/lib/rules/headers/details-summary.ts +++ b/lib/rules/headers/details-summary.ts @@ -10,30 +10,30 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = sr => { - const $details = sr.$('.head details').first(); +export const check: RuleCheckFunction = context => { + const $details = context.$('.head details').first(); if (!$details.length) { - sr.error(self, 'no-details'); + context.error(self, 'no-details'); return; } if (!$details.attr('open')) { - sr.error(self, 'no-details-open'); + context.error(self, 'no-details-open'); } - if (!sr.$('.head details dl').length) { - sr.error(self, 'no-details-dl'); + if (!context.$('.head details dl').length) { + context.error(self, 'no-details-dl'); return; } - const $summary = sr.$('.head details summary').first(); + const $summary = context.$('.head details summary').first(); if (!$summary.length) { - sr.error(self, 'no-details-summary'); + context.error(self, 'no-details-summary'); return; } - const summaryText = sr.norm($summary.text()); + const summaryText = context.norm($summary.text()); if (summaryText !== 'More details about this document') { - sr.error(self, 'wrong-summary-text'); + context.error(self, 'wrong-summary-text'); } }; diff --git a/lib/rules/headers/div-head.ts b/lib/rules/headers/div-head.ts index 7928d3f71..e1b8197b3 100644 --- a/lib/rules/headers/div-head.ts +++ b/lib/rules/headers/div-head.ts @@ -10,6 +10,6 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = sr => { - sr.checkSelector('body div.head', self); +export const check: RuleCheckFunction = context => { + context.checkSelector('body div.head', self); }; diff --git a/lib/rules/headers/dl.ts b/lib/rules/headers/dl.ts index 3d6fa5243..822a2881f 100644 --- a/lib/rules/headers/dl.ts +++ b/lib/rules/headers/dl.ts @@ -10,7 +10,7 @@ import type { Element } from 'domhandler'; import type { RuleCheckFunction, RuleMeta } from '../../types.js'; import { resolveGithubUsernameToId } from '../../util.js'; -import type { Specberus } from '../../validator.js'; +import type { RuleContext } from '../../rule-context.js'; const self: RuleMeta = { name: 'headers.dl', @@ -44,7 +44,7 @@ interface CheckLinkOptions { linkName: string; mustHave?: boolean; rule?: RuleMeta; - sr: Specberus; + context: RuleContext; } /** @@ -52,7 +52,7 @@ interface CheckLinkOptions { * @returns boolean whether element exists and can continue */ function checkLink({ - sr, + context, rule = self, $element, linkName, @@ -60,40 +60,41 @@ function checkLink({ }: CheckLinkOptions) { if (!$element?.length || !$element.attr('href')) { if (mustHave) - sr.error(rule, 'not-found', { linkName, message: linkName }); + context.error(rule, 'not-found', { linkName, message: linkName }); return false; } - const text = sr.norm($element.text()).trim(); + const text = context.norm($element.text()).trim(); const href = ($element.attr('href') || '').trim(); - if (href !== text) sr.error(rule, 'link-diff', { text, href, linkName }); + if (href !== text) + context.error(rule, 'link-diff', { text, href, linkName }); return true; } -export const check: RuleCheckFunction = async sr => { - const { rescinds, status, submissionType } = sr.config!; +export const check: RuleCheckFunction = async context => { + const { rescinds, status, submissionType } = context.config!; let topLevel = 'TR'; if (submissionType === 'member') topLevel = 'submissions'; - const dts = sr.extractHeaders(); - if (!dts.This) sr.error(self, 'this-version'); - if (!dts.Latest) sr.error(self, 'latest-version'); - if (!dts.History) sr.error(self, 'no-history'); - if (rescinds && !dts.Rescinds) sr.error(self, 'rescinds'); - if (!rescinds && dts.Rescinds) sr.warning(self, 'rescinds-not-needed'); + const dts = context.extractHeaders(); + if (!dts.This) context.error(self, 'this-version'); + if (!dts.Latest) context.error(self, 'latest-version'); + if (!dts.History) context.error(self, 'no-history'); + if (rescinds && !dts.Rescinds) context.error(self, 'rescinds'); + if (!rescinds && dts.Rescinds) context.warning(self, 'rescinds-not-needed'); if (dts.This && dts.Latest && dts.This.pos > dts.Latest.pos) - sr.error(self, 'this-latest-order'); + context.error(self, 'this-latest-order'); // TODO: What's the order for History? if (dts.Latest && dts.Rescinds && dts.Latest.pos > dts.Rescinds.pos) - sr.error(self, 'latest-rescinds-order'); + context.error(self, 'latest-rescinds-order'); let matches; if (dts.This) { const $linkThis = dts.This.$dd.find('a').first(); const exist = checkLink({ - sr, + context, rule: self, $element: $linkThis, linkName: 'This version', @@ -106,7 +107,7 @@ export const check: RuleCheckFunction = async sr => { matches = ($linkThis.attr('href') || '') .trim() .match(new RegExp(vThisRex)); - const docDate = sr.getDocumentDate(); + const docDate = context.getDocumentDate(); if (matches) { const year = +matches[1]; const year2 = +matches[3]; @@ -119,16 +120,16 @@ export const check: RuleCheckFunction = async sr => { month - 1 !== docDate.getMonth() || day !== docDate.getDate() ) - sr.error(self, 'this-date'); - } else sr.warning(self, 'no-date'); - } else sr.error(thisError, 'this-syntax'); + context.error(self, 'this-date'); + } else context.warning(self, 'no-date'); + } else context.error(thisError, 'this-syntax'); } } if (dts.Latest) { const $linkLate = dts.Latest.$dd.find('a').first(); const exist = checkLink({ - sr, + context, rule: self, $element: $linkLate, linkName: 'Latest published version', @@ -141,7 +142,7 @@ export const check: RuleCheckFunction = async sr => { .match(new RegExp(lateRex)); if (!matches) { - sr.error(latestError, 'latest-syntax'); + context.error(latestError, 'latest-syntax'); } } } @@ -149,7 +150,7 @@ export const check: RuleCheckFunction = async sr => { if (dts.History) { const $linkHistory = dts.History.$dd.find('a').first(); checkLink({ - sr, + context, rule: historyError, $element: $linkHistory, linkName: 'History', @@ -159,7 +160,7 @@ export const check: RuleCheckFunction = async sr => { if (dts.Rescinds) { const $linkRescinds = dts.Rescinds.$dd.find('a').first(); const exist = checkLink({ - sr, + context, rule: self, $element: $linkRescinds, linkName: 'Rescinds this Recommendation', @@ -173,7 +174,7 @@ export const check: RuleCheckFunction = async sr => { ); if (!matches) { - sr.error(self, 'rescinds-syntax'); + context.error(self, 'rescinds-syntax'); } } } @@ -181,15 +182,15 @@ export const check: RuleCheckFunction = async sr => { // check "Implementation report" link. Unless in Sotd saying there's none. const needImplementation = ['CR', 'CRD', 'PR', 'REC'].indexOf(status) !== -1; - const $sotd = sr.getSotDSection(); + const $sotd = context.getSotDSection(); const noImplementation = - sr + context .norm(($sotd && $sotd.text()) || '') .indexOf('There is no preliminary implementation report.') > -1; const $linkImplementation = dts.Implementation && dts.Implementation.$dd.find('a').first(); const implementationExist = checkLink({ - sr, + context, rule: self, $element: $linkImplementation, linkName: 'Implementation report', @@ -202,19 +203,19 @@ export const check: RuleCheckFunction = async sr => { .toLowerCase() .startsWith('https://') ) { - sr.error(self, 'implelink-should-be-https', { + context.error(self, 'implelink-should-be-https', { link: $linkImplementation.attr('href') || '', }); } if (noImplementation && needImplementation) { - sr.warning(self, 'implelink-confirm-no'); + context.warning(self, 'implelink-confirm-no'); } // check "Editor's draft" link if (dts.EditorDraft) { const $editorsDraftElement = dts.EditorDraft.$dd.find('a').first(); const exist = checkLink({ - sr, + context, rule: self, $element: $editorsDraftElement, linkName: 'Implementation report', @@ -222,7 +223,7 @@ export const check: RuleCheckFunction = async sr => { if (exist) { const editorsDraft = $editorsDraftElement.attr('href') || ''; if (!editorsDraft.trim().toLowerCase().startsWith('https://')) - sr.error(self, 'editors-draft-should-be-https', { + context.error(self, 'editors-draft-should-be-https', { link: editorsDraft, }); } @@ -233,15 +234,15 @@ export const check: RuleCheckFunction = async sr => { editor => !editor.attribs['data-editor-id'] && !editor.attribs['data-editor-github'] && - !sr + !context .$(editor) .text() .match(/(working|interest) group/i) ); if (missingElements.length) { - sr.error(editorError, 'editor-missing-id', { + context.error(editorError, 'editor-missing-id', { names: missingElements - .map(editor => sr.$(editor).text()) + .map(editor => context.$(editor).text()) .join(', '), }); } @@ -257,18 +258,18 @@ export const check: RuleCheckFunction = async sr => { if (!(await resolveGithubUsernameToId(username))) unresolvedUsernames.push(username); } catch (error) { - sr.error(editorError, 'editor-github-failed', { + context.error(editorError, 'editor-github-failed', { name: error.cause, }); } } if (unresolvedUsernames.length) { - sr.error(editorError, 'editor-github-unresolvable', { + context.error(editorError, 'editor-github-unresolvable', { names: unresolvedUsernames.join(', '), }); } } else { // should at least have 1 editor - sr.error(editorError, 'editor-not-found'); + context.error(editorError, 'editor-not-found'); } }; diff --git a/lib/rules/headers/editor-participation.ts b/lib/rules/headers/editor-participation.ts index 0a9fd2ffb..43d87a039 100644 --- a/lib/rules/headers/editor-participation.ts +++ b/lib/rules/headers/editor-participation.ts @@ -10,15 +10,15 @@ const self: RuleMeta = { }; export const name = self.name; -export const check: RuleCheckFunction = async sr => { - const groups = await sr.getDelivererIDs(); - const editors = sr.extractHeaders()?.Editor; +export const check: RuleCheckFunction = async context => { + const groups = await context.getDelivererIDs(); + const editors = context.extractHeaders()?.Editor; const editorsToCheck = []; if (editors) { // only check editors elements that don't have a span with class "former" for (const dd of editors.$dd.toArray()) { - const $former = sr.$(dd).find('span.former').first(); - if (!$former.length || !sr.norm($former.text())) { + const $former = context.$(dd).find('span.former').first(); + if (!$former.length || !context.norm($former.text())) { if (dd.attribs['data-editor-id']) editorsToCheck.push( parseInt(dd.attribs['data-editor-id'], 10) @@ -49,7 +49,7 @@ export const check: RuleCheckFunction = async sr => { editorsToCheck.forEach(id => { if (!userIds.includes(id)) { - sr.error(self, 'not-participating', { id }); + context.error(self, 'not-participating', { id }); } }); }; diff --git a/lib/rules/headers/errata.ts b/lib/rules/headers/errata.ts index 5d3ea50de..116110cd9 100644 --- a/lib/rules/headers/errata.ts +++ b/lib/rules/headers/errata.ts @@ -1,7 +1,7 @@ // errata, right after dl import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -import type { Specberus } from '../../validator.js'; +import type { RuleContext } from '../../rule-context.js'; const self: RuleMeta = { name: 'headers.errata', @@ -12,18 +12,18 @@ const self: RuleMeta = { export const { name } = self; // Check if document is Recommendation, and uses inline changes(REC with Candidate/Proposed changes) -function isRECWithChanges(sr: Specberus) { - if (sr.config!.status !== 'REC') return false; +function isRECWithChanges(context: RuleContext) { + if (context.config!.status !== 'REC') return false; - const recMeta = sr.getRecMetadata(); + const recMeta = context.getRecMetadata(); return Object.values(recMeta).length !== 0; } -export const check: RuleCheckFunction = sr => { +export const check: RuleCheckFunction = context => { // for REC with Candidate/Proposed changes, no need to check errata link - if (isRECWithChanges(sr)) return; + if (isRECWithChanges(context)) return; - const dts = sr.extractHeaders(); + const dts = context.extractHeaders(); // Check 'Errata:' exist, don't check any further. - if (!dts.Errata) sr.error(self, 'no-errata'); + if (!dts.Errata) context.error(self, 'no-errata'); }; diff --git a/lib/rules/headers/github-repo.ts b/lib/rules/headers/github-repo.ts index b3588ea24..c46513eb5 100644 --- a/lib/rules/headers/github-repo.ts +++ b/lib/rules/headers/github-repo.ts @@ -12,16 +12,16 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = sr => { - const dts = sr.extractHeaders(); +export const check: RuleCheckFunction = context => { + const dts = context.extractHeaders(); if (!dts.Feedback) { - sr.error(self, 'no-feedback'); + context.error(self, 'no-feedback'); return; } // Check 'github repo' exist in 'Feedback:' const foundRepo = dts.Feedback.$dd.toArray().some(feedbackEl => { - const links = sr + const links = context .$(feedbackEl) .find('a[href]') .toArray() @@ -37,5 +37,5 @@ export const check: RuleCheckFunction = sr => { // href // ); }); - if (!foundRepo) sr.error(self, 'no-repo'); + if (!foundRepo) context.error(self, 'no-repo'); }; diff --git a/lib/rules/headers/h1-title.ts b/lib/rules/headers/h1-title.ts index 7c2bf3319..c226c3662 100644 --- a/lib/rules/headers/h1-title.ts +++ b/lib/rules/headers/h1-title.ts @@ -10,16 +10,16 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = sr => { - const $title = sr.$('head > title').first(); - const $h1 = sr.$('body div.head h1').first(); +export const check: RuleCheckFunction = context => { + const $title = context.$('head > title').first(); + const $h1 = context.$('body div.head h1').first(); if (!$title.length || !$h1.length) { - sr.error(self, 'not-found'); + context.error(self, 'not-found'); } else { - const titleText = sr.norm($title.text()); + const titleText = context.norm($title.text()); $h1.html($h1.html()!.replace(/:
/g, ': ').replace(/
/g, ' - ')); - const h1Text = sr.norm($h1.text()); + const h1Text = context.norm($h1.text()); if (titleText !== h1Text) - sr.error(self, 'not-match', { titleText, h1Text }); + context.error(self, 'not-match', { titleText, h1Text }); } }; diff --git a/lib/rules/headers/h2-toc.ts b/lib/rules/headers/h2-toc.ts index f7640f6bf..ce9148af8 100644 --- a/lib/rules/headers/h2-toc.ts +++ b/lib/rules/headers/h2-toc.ts @@ -14,26 +14,27 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = sr => { +export const check: RuleCheckFunction = context => { const EXPECTED_HEADING = /^table\s+of\s+contents$/i; - const $tocNav = sr.$('nav#toc > h2'); - const $tocDiv = sr.$('div#toc > h2'); + const $tocNav = context.$('nav#toc > h2'); + const $tocDiv = context.$('div#toc > h2'); let $toc; if ($tocDiv.length > 0) { - if ($tocNav.length > 0) sr.error(self, 'mixed'); + if ($tocNav.length > 0) context.error(self, 'mixed'); else { - sr.warning(self, 'not-html5'); + context.warning(self, 'not-html5'); $toc = $tocDiv; } } else if ($tocNav.length > 0) $toc = $tocNav; - else sr.error(self, 'not-found'); + else context.error(self, 'not-found'); if ($toc && $toc.length > 0) { let matches = 0; $toc.each((_, el) => { - if (EXPECTED_HEADING.test(sr.norm(sr.$(el).text()))) matches += 1; + if (EXPECTED_HEADING.test(context.norm(context.$(el).text()))) + matches += 1; }); - if (matches > 1) sr.error(self, 'too-many'); - else if (matches === 0) sr.error(self, 'not-found'); + if (matches > 1) context.error(self, 'too-many'); + else if (matches === 0) context.error(self, 'not-found'); } }; diff --git a/lib/rules/headers/hr.ts b/lib/rules/headers/hr.ts index 0271052fc..aba26b7da 100644 --- a/lib/rules/headers/hr.ts +++ b/lib/rules/headers/hr.ts @@ -8,12 +8,13 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = sr => { - const hasHrLastChild = sr.$('body div.head > hr:last-child').length === 1; - const hasHrNextSibling = sr.$('body div.head + hr').length === 1; +export const check: RuleCheckFunction = context => { + const hasHrLastChild = + context.$('body div.head > hr:last-child').length === 1; + const hasHrNextSibling = context.$('body div.head + hr').length === 1; if (hasHrLastChild && hasHrNextSibling) { - sr.error(self, 'duplicate'); + context.error(self, 'duplicate'); } else if (!hasHrLastChild && !hasHrNextSibling) { - sr.error(self, 'not-found'); + context.error(self, 'not-found'); } }; diff --git a/lib/rules/headers/logo.ts b/lib/rules/headers/logo.ts index b713fa8aa..c4d08ea7f 100644 --- a/lib/rules/headers/logo.ts +++ b/lib/rules/headers/logo.ts @@ -8,8 +8,10 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = sr => { - const $logo = sr.$("body div.head a[href] > img[src][alt='W3C']").first(); +export const check: RuleCheckFunction = context => { + const $logo = context + .$("body div.head a[href] > img[src][alt='W3C']") + .first(); if ( !$logo.length || !/^(https:)?\/\/www\.w3\.org\/StyleSheets\/TR\/2021\/logos\/W3C?$/.test( @@ -19,6 +21,6 @@ export const check: RuleCheckFunction = sr => { $logo.parent().attr('href') || '' ) ) { - sr.error(self, 'not-found'); + context.error(self, 'not-found'); } }; diff --git a/lib/rules/headers/memsub-copyright.ts b/lib/rules/headers/memsub-copyright.ts index c1fc87367..eabb7e376 100644 --- a/lib/rules/headers/memsub-copyright.ts +++ b/lib/rules/headers/memsub-copyright.ts @@ -6,8 +6,8 @@ const self = { export const { name } = self; -export const check: RuleCheckFunction = sr => { - const $copyright = sr.$('body div.head p.copyright').first(); +export const check: RuleCheckFunction = context => { + const $copyright = context.$('body div.head p.copyright').first(); if ($copyright.length) { // , "https://www.w3.org/copyright/document-license/": "document use" const seen = $copyright @@ -19,6 +19,6 @@ export const check: RuleCheckFunction = sr => { 'https://www.w3.org/copyright/document-license/' ) === 0 ); - if (!seen) sr.error(self, 'not-found'); - } else sr.error(self, 'not-found'); + if (!seen) context.error(self, 'not-found'); + } else context.error(self, 'not-found'); }; diff --git a/lib/rules/headers/ol-toc.ts b/lib/rules/headers/ol-toc.ts index b31e4225f..db55bb107 100644 --- a/lib/rules/headers/ol-toc.ts +++ b/lib/rules/headers/ol-toc.ts @@ -13,8 +13,8 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = sr => { - const $toc = sr.$('nav#toc ol.toc, div#toc ol.toc'); +export const check: RuleCheckFunction = context => { + const $toc = context.$('nav#toc ol.toc, div#toc ol.toc'); - if (!$toc.length) sr.warning(self, 'not-found'); + if (!$toc.length) context.warning(self, 'not-found'); }; diff --git a/lib/rules/headers/secno.ts b/lib/rules/headers/secno.ts index 5c9606fe8..43a3a1090 100644 --- a/lib/rules/headers/secno.ts +++ b/lib/rules/headers/secno.ts @@ -10,12 +10,12 @@ const self = { export const { name } = self; -export const check: RuleCheckFunction = sr => { +export const check: RuleCheckFunction = context => { // TODO: once supported, use: ":is(h2, h3, h4, h5, h6) :is(bdi.secno,span.secno)" - const $secnos = sr.$( + const $secnos = context.$( 'h1 span.secno, h2 span.secno, h3 span.secno, h4 span.secno, h5 span.secno, h6 span.secno, #toc span.secno,' + 'h1 bdi.secno, h2 bdi.secno, h3 bdi.secno, h4 bdi.secno, h5 bdi.secno, h6 bdi.secno, #toc bdi.secno' ); - if (!$secnos.length) sr.warning(self, 'not-found'); + if (!$secnos.length) context.warning(self, 'not-found'); }; diff --git a/lib/rules/headers/shortname.ts b/lib/rules/headers/shortname.ts index 45e181571..2b313c88b 100644 --- a/lib/rules/headers/shortname.ts +++ b/lib/rules/headers/shortname.ts @@ -22,12 +22,12 @@ const historyError: RuleMeta = { }; export const name = self.name; -export const check: RuleCheckFunction = async sr => { +export const check: RuleCheckFunction = async context => { let topLevel = 'TR'; - if (sr.config!.submissionType === 'member') topLevel = 'submissions'; + if (context.config!.submissionType === 'member') topLevel = 'submissions'; - const dts = sr.extractHeaders(); + const dts = context.extractHeaders(); let shortname = ''; let seriesShortname = ''; @@ -37,7 +37,7 @@ export const check: RuleCheckFunction = async sr => { if ($linkThis.attr('href')) { const vThisRex = `^https:\\/\\/www\\.w3\\.org\\/${topLevel}\\/(\\d{4})\\/${ - sr.config!.status || '[A-Z]+' + context.config!.status || '[A-Z]+' }-(.+)-(\\d{4})(\\d\\d)(\\d\\d)\\/?$`; const matches = ($linkThis.attr('href') || '') .trim() @@ -57,9 +57,11 @@ export const check: RuleCheckFunction = async sr => { .find('a') .first() .attr('data-previous-shortname'); - const needLowercase = shortnameChange || (await sr.isFP()); + const needLowercase = shortnameChange || (await context.isFP()); if (needLowercase && shortname.toLowerCase() !== shortname) - sr.error(thisError, 'shortname-lowercase', { shortname }); + context.error(thisError, 'shortname-lowercase', { + shortname, + }); } } } @@ -77,7 +79,7 @@ export const check: RuleCheckFunction = async sr => { sn = matches[1]; // latest version link mention either shortlink or the series shortlink if (sn !== shortname && sn !== seriesShortname) - sr.error(self, 'this-latest-shortname', { + context.error(self, 'this-latest-shortname', { thisShortname: shortname, latestShortname: sn, }); @@ -97,7 +99,9 @@ export const check: RuleCheckFunction = async sr => { if (matches) { const [, historyShortname] = matches; if (historyShortname !== shortname) { - sr.error(historyError, 'history-syntax', { shortname }); + context.error(historyError, 'history-syntax', { + shortname, + }); } else { // Check if the history link exist let historyStatusCode; @@ -107,7 +111,7 @@ export const check: RuleCheckFunction = async sr => { } catch (err) { historyStatusCode = err.status; } - var hasPreviousVersion = !(await sr.isFP()); + var hasPreviousVersion = !(await context.isFP()); if (hasPreviousVersion && historyStatusCode === 404) { // it's a none FP spec, but the history page doesn't exist. There should be a 'valid' previous-shortname. const previousShortname = $linkHistory.attr( @@ -115,7 +119,7 @@ export const check: RuleCheckFunction = async sr => { ); if (previousShortname) { // prettier-ignore - sr.warning(historyError, 'this-previous-shortname', + context.warning(historyError, 'this-previous-shortname', { previousShortname, thisShortname: shortname, @@ -134,10 +138,14 @@ export const check: RuleCheckFunction = async sr => { } if (previousHistoryStatusCode === 404) { - sr.error(historyError, 'history-bad-previous', { - previousShortname, - url: previousHistoryHref, - }); + context.error( + historyError, + 'history-bad-previous', + { + previousShortname, + url: previousHistoryHref, + } + ); } } } @@ -159,7 +167,7 @@ export const check: RuleCheckFunction = async sr => { if (matches) { sn = matches[1]; if (sn !== shortname) - sr.error(self, 'this-rescinds-shortname', { + context.error(self, 'this-rescinds-shortname', { rescindsShortname: sn, thisShortname: shortname, }); @@ -168,26 +176,26 @@ export const check: RuleCheckFunction = async sr => { } // check shortname is valid. - const isFP = await sr.isFP(); + const isFP = await context.isFP(); // FP documents cannot use existing shortname if ( - sr.config!.longStatus === 'First Public Working Draft' && + context.config!.longStatus === 'First Public Working Draft' && !isFP && shortname ) { - sr.error(self, 'shortname-existed'); + context.error(self, 'shortname-existed'); } // non-initial state documents should use existing shortname // TODO: Registry and Note track? if ( - sr.config!.track === 'Recommendation' && - sr.config!.longStatus !== 'First Public Working Draft' && + context.config!.track === 'Recommendation' && + context.config!.longStatus !== 'First Public Working Draft' && isFP && shortname ) { - sr.error(self, 'shortname-not-existed', { - status: sr.config!.longStatus, + context.error(self, 'shortname-not-existed', { + status: context.config!.longStatus, }); } }; diff --git a/lib/rules/headers/subm-logo.ts b/lib/rules/headers/subm-logo.ts index eca771d75..be3292958 100644 --- a/lib/rules/headers/subm-logo.ts +++ b/lib/rules/headers/subm-logo.ts @@ -6,11 +6,11 @@ const self = { export const { name } = self; -export const check: RuleCheckFunction = sr => { - const $logo = sr +export const check: RuleCheckFunction = context => { + const $logo = context .$("body div.head a[href] > img[src][height='48'][width='211'][alt]") .first(); - const type = sr.config!.submissionType || 'member'; + const type = context.config!.submissionType || 'member'; const checks = { member: { alt: 'W3C Member Submission', @@ -24,7 +24,7 @@ export const check: RuleCheckFunction = sr => { !checks[type].src.test($logo.attr('src') || '') || !checks[type].href.test($logo.parent().attr('href') || '') ) { - sr.error(self, 'not-found', { + context.error(self, 'not-found', { type: type.charAt(0).toUpperCase() + type.slice(1), }); } diff --git a/lib/rules/headers/translation.ts b/lib/rules/headers/translation.ts index 1ffe7265f..927e3abc6 100644 --- a/lib/rules/headers/translation.ts +++ b/lib/rules/headers/translation.ts @@ -8,27 +8,31 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = sr => { - const translationLink = sr +export const check: RuleCheckFunction = context => { + const translationLink = context .$('body div.head a') .toArray() .find(link => { - return sr.$(link).text().toLowerCase().includes('translations'); + return context + .$(link) + .text() + .toLowerCase() + .includes('translations'); }); if (!translationLink) { - sr.error(self, 'not-found'); + context.error(self, 'not-found'); return; } const href = translationLink.attribs.href; - sr.info(self, 'found', { link: href }); + context.info(self, 'found', { link: href }); if ( - !sr + !context .norm(href) .toLowerCase() .startsWith('https://www.w3.org/translations/') ) { - sr.warning(self, 'not-recommended-link'); + context.warning(self, 'not-recommended-link'); } }; diff --git a/lib/rules/headers/w3c-state.ts b/lib/rules/headers/w3c-state.ts index 0b3481b50..7dd406af4 100644 --- a/lib/rules/headers/w3c-state.ts +++ b/lib/rules/headers/w3c-state.ts @@ -9,24 +9,24 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = sr => { - const config = sr.config!; +export const check: RuleCheckFunction = context => { + const config = context.config!; let profileFound = false; if (config.longStatus) return; - const $stateEl = sr.getDocumentStateElement(); + const $stateEl = context.getDocumentStateElement(); if (!$stateEl) { - sr.error(self, 'no-w3c-state'); + context.error(self, 'no-w3c-state'); return; } - const txt = sr.norm($stateEl.text()); + const txt = context.norm($stateEl.text()); // crType/cryType: Add 'Draft', 'Snapshot' suffix to title. const docTitle = config.longStatus + (config.crType ? ` ${config.crType}` : '') + (config.cryType ? ` ${config.cryType}` : ''); if (!txt.startsWith(`W3C ${docTitle}`)) { - sr.error(self, 'bad-w3c-state'); + context.error(self, 'bad-w3c-state'); } for (const { profiles } of Object.values(rules)) @@ -39,7 +39,7 @@ export const check: RuleCheckFunction = sr => { } } - if (!profileFound) sr.error(self, 'bad-w3c-state'); + if (!profileFound) context.error(self, 'bad-w3c-state'); else { // check the profile link const $standardLink = $stateEl.find('a').first(); @@ -52,12 +52,12 @@ export const check: RuleCheckFunction = sr => { `https://www.w3.org/standards/types/?#${hash}` ); if (!$standardLink.length) { - sr.error(self, 'no-w3c-state-link'); + context.error(self, 'no-w3c-state-link'); } else if (!expectedLink.test($standardLink.attr('href') || '')) { - sr.error(self, 'wrong-w3c-state-link', { + context.error(self, 'wrong-w3c-state-link', { hash, linkFound: $standardLink.attr('href'), - text: sr.norm($standardLink.text()), + text: context.norm($standardLink.text()), }); } } diff --git a/lib/rules/heuristic/date-format.ts b/lib/rules/heuristic/date-format.ts index 2197dd260..928b28f91 100644 --- a/lib/rules/heuristic/date-format.ts +++ b/lib/rules/heuristic/date-format.ts @@ -1,5 +1,5 @@ import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -import { possibleMonths } from '../../validator.js'; +import { possibleMonths } from '../../rule-context.js'; const self: RuleMeta = { name: 'heuristic.date-format', @@ -9,7 +9,7 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = sr => { +export const check: RuleCheckFunction = context => { // Pseudo-constants: const POSSIBLE_DATE = new RegExp( `\\b([0123]?\\d|${possibleMonths})[\\ \\-\\/–—]([0123]?\\d|${possibleMonths})[\\ \\-\\/–—]([\\'‘’]?\\d{2})(\\d\\d)?\\b`, @@ -20,7 +20,10 @@ export const check: RuleCheckFunction = sr => { 'i' ); - const boilerplateSections = [sr.$('div.head').first(), sr.getSotDSection()]; + const boilerplateSections = [ + context.$('div.head').first(), + context.getSotDSection(), + ]; const candidateDates: string[] = []; for (const $section of boilerplateSections) { @@ -35,7 +38,7 @@ export const check: RuleCheckFunction = sr => { for (const date of candidateDates) { if (!date.match(EXPECTED_DATE_FORMAT)) { - sr.error(self, 'wrong', { text: date }); + context.error(self, 'wrong', { text: date }); } } }; diff --git a/lib/rules/links/compound.ts b/lib/rules/links/compound.ts index d9be5d058..7f2c3176d 100644 --- a/lib/rules/links/compound.ts +++ b/lib/rules/links/compound.ts @@ -9,19 +9,19 @@ const TIMEOUT = 10000; export const { name } = self; -export const check: RuleCheckFunction = sr => { - const { validation } = sr.config!; - const url = sr.url!; +export const check: RuleCheckFunction = context => { + const { validation } = context.config!; + const url = context.url!; if (validation !== 'recursive') { - sr.warning(self, 'skipped'); + context.warning(self, 'skipped'); return; } let links: string[] = []; if (url) { - sr.$('a[href]').each((_, el) => { + context.$('a[href]').each((_, el) => { const u = new URL(el.attribs.href, url); const l = u.origin + u.pathname; if (l.startsWith(url) && l !== url) links.push(l); @@ -35,12 +35,12 @@ export const check: RuleCheckFunction = sr => { const markupService = 'https://validator.w3.org/nu/'; return Promise.all( links.map(l => { - const ua = `W3C-Pubrules/${sr.version}`; + const ua = `W3C-Pubrules/${context.version}`; const req = get(markupService) .set('User-Agent', ua) .query({ doc: l, out: 'json' }) .on('error', err => { - sr.error(self, 'error', { + context.error(self, 'error', { file: l.split('/').pop(), link: l, errMsg: err, @@ -60,13 +60,13 @@ export const check: RuleCheckFunction = sr => { (msg: { type: string }) => msg.type === 'error' ) || []; if (errors.length === 0) { - sr.info(self, 'link', { + context.info(self, 'link', { file: l.split('/').pop(), link: l, markup: '\u2714', }); } else { - sr.error(self, 'link', { + context.error(self, 'link', { file: l.split('/').pop(), link: l, markup: '\u2718', @@ -74,7 +74,7 @@ export const check: RuleCheckFunction = sr => { } }, (err: ResponseError) => { - if (err.timeout) sr.warning(self, 'html-timeout'); + if (err.timeout) context.warning(self, 'html-timeout'); else throw new Error(`HTML validator error: ${err.message}`); } diff --git a/lib/rules/links/internal.ts b/lib/rules/links/internal.ts index d817dfd5c..ddf8e3836 100644 --- a/lib/rules/links/internal.ts +++ b/lib/rules/links/internal.ts @@ -8,13 +8,13 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = sr => { - sr.$("a[href^='#']").each((_, el) => { +export const check: RuleCheckFunction = context => { + context.$("a[href^='#']").each((_, el) => { const id = el.attribs.href.replace('#', ''); const escId = id.replace(/([.()#:[\]+*])/g, '\\$1'); if (id === '') return; - if (!sr.$(`#${escId}, a[name='${id}']`).length) { - sr.error(self, 'anchor', { id }); + if (!context.$(`#${escId}, a[name='${id}']`).length) { + context.error(self, 'anchor', { id }); } }); }; diff --git a/lib/rules/links/linkchecker.ts b/lib/rules/links/linkchecker.ts index 7bbb1f404..3bb895454 100644 --- a/lib/rules/links/linkchecker.ts +++ b/lib/rules/links/linkchecker.ts @@ -53,13 +53,13 @@ function includedByReg(url: string, regArray = allowList) { ); } -export const check: RuleCheckFunction = async sr => { +export const check: RuleCheckFunction = async context => { // send out warning for /nu W3C link checker. - sr.warning(self, 'display', { link: sr.url }); + context.warning(self, 'display', { link: context.url }); - if (!sr.url) return; + if (!context.url) return; - // sr.url is used as base url. Every other resource should use in same folder as base. e.g. + // context.url is used as base url. Every other resource should use in same folder as base. e.g. // - spec doc: https://www.w3.org/TR/2021/WD-pubrules-20210401/ // - image (pass): https://www.w3.org/TR/2021/WD-pubrules-20210401/images/sample.png // - image (pass): https://www.w3.org/TR/2021/WD-pubrules-20210401/sample.png @@ -69,8 +69,10 @@ export const check: RuleCheckFunction = async sr => { args: ['--disable-gpu'], }); const page = await browser.newPage(); - const docPath = sr.url.replace(/\/[^/]+$/, '/').replace(/^https?:/, ''); - const origin = new URL(sr.url).origin; + const docPath = context.url + .replace(/\/[^/]+$/, '/') + .replace(/^https?:/, ''); + const origin = new URL(context.url).origin; page.on('response', response => { const url = simplifyURL(response.url()); @@ -81,9 +83,12 @@ export const check: RuleCheckFunction = async sr => { if ( !url.replace(/^https?:/, '').startsWith(docPath) && !(includedByReg(url) || includedByReg(referer)) && - url !== sr.url + url !== context.url ) { - sr.error(compound, 'not-same-folder', { base: docPath, url }); + context.error(compound, 'not-same-folder', { + base: docPath, + url, + }); } // check if every resource's status code is ok, ignore 3xx @@ -91,7 +96,7 @@ export const check: RuleCheckFunction = async sr => { const chain = response.request().redirectChain(); // If an url is redirected from another, chain shall exist if (chain.length) { - sr.error(compound, 'response-error-with-redirect', { + context.error(compound, 'response-error-with-redirect', { url, originUrl: chain[0].url(), status: response.status(), @@ -99,7 +104,7 @@ export const check: RuleCheckFunction = async sr => { referer, }); } else { - sr.error(compound, 'response-error', { + context.error(compound, 'response-error', { url, status: response.status(), text: response.statusText(), @@ -110,7 +115,7 @@ export const check: RuleCheckFunction = async sr => { } }); - await page.goto(sr.url, { waitUntil: 'load', timeout: 60000 }); + await page.goto(context.url, { waitUntil: 'load', timeout: 60000 }); await browser.close(); }; diff --git a/lib/rules/links/reliability.ts b/lib/rules/links/reliability.ts index 6b9430007..b4a6136b7 100644 --- a/lib/rules/links/reliability.ts +++ b/lib/rules/links/reliability.ts @@ -20,15 +20,15 @@ const unreliableServices = [ // { domain: 'w3.org', path: /track(er)?\/(actions|issues|resolutions)/} ]; -export const check: RuleCheckFunction = sr => { - sr.$('a').each((_, el) => { - const $el = sr.$(el); +export const check: RuleCheckFunction = context => { + context.$('a').each((_, el) => { + const $el = context.$(el); const href = $el.attr('href'); if (!href) return; let url; try { - url = new URL(href, sr.url || 'https://www.w3.org'); + url = new URL(href, context.url || 'https://www.w3.org'); } catch (e) { // when failed to parse URL, move on to next one. return; @@ -44,9 +44,9 @@ export const check: RuleCheckFunction = sr => { (unreliableService.path && unreliableService.path.test(path))) ) { - sr.warning(self, 'unreliable-link', { + context.warning(self, 'unreliable-link', { link: href, - text: sr.norm($el.text()), + text: context.norm($el.text()), }); // when finding this URL unreliable, quit 'unreliableServices.some' return true; diff --git a/lib/rules/metadata/abstract.ts b/lib/rules/metadata/abstract.ts index 9b6171a6d..c26f04715 100644 --- a/lib/rules/metadata/abstract.ts +++ b/lib/rules/metadata/abstract.ts @@ -7,16 +7,20 @@ import type { RuleCheckFunction } from '../../types.js'; export const name = 'metadata.abstract'; -export const check: RuleCheckFunction<{ abstract: string }> = sr => { - const abstractHeadingEl = sr +export const check: RuleCheckFunction<{ abstract: string }> = context => { + const abstractHeadingEl = context .$('h2') .toArray() - .find(el => sr.norm(sr.$(el).text()).toLowerCase() === 'abstract'); + .find( + el => + context.norm(context.$(el).text()).toLowerCase() === 'abstract' + ); if (!abstractHeadingEl) return { abstract: 'Not found' }; const $div = load('
', null, false)('div'); - sr.$(abstractHeadingEl) + context + .$(abstractHeadingEl) .parent() .children() .each((_, child) => { @@ -25,5 +29,5 @@ export const check: RuleCheckFunction<{ abstract: string }> = sr => { $div.append(child.cloneNode(true)); } }); - return { abstract: sr.norm($div.html()!) }; + return { abstract: context.norm($div.html()!) }; }; diff --git a/lib/rules/metadata/charters.ts b/lib/rules/metadata/charters.ts index 8f8cfe6e4..481ffadfb 100644 --- a/lib/rules/metadata/charters.ts +++ b/lib/rules/metadata/charters.ts @@ -10,4 +10,4 @@ export const name = 'metadata.charters'; export const check: RuleCheckFunction<{ charters: string[]; -}> = async sr => ({ charters: await sr.getCharters() }); +}> = async context => ({ charters: await context.getCharters() }); diff --git a/lib/rules/metadata/deliverers.ts b/lib/rules/metadata/deliverers.ts index 3d9c35e80..c329eaaed 100644 --- a/lib/rules/metadata/deliverers.ts +++ b/lib/rules/metadata/deliverers.ts @@ -7,10 +7,6 @@ import type { RuleCheckFunction } from '../../types.js'; // 'self.name' would be 'metadata.deliverers' export const name = 'metadata.deliverers'; -/** - * @param sr - * @param done - */ export const check: RuleCheckFunction<{ delivererIDs: number[]; -}> = async sr => ({ delivererIDs: await sr.getDelivererIDs() }); +}> = async context => ({ delivererIDs: await context.getDelivererIDs() }); diff --git a/lib/rules/metadata/dl.ts b/lib/rules/metadata/dl.ts index e6541509e..964142d1c 100644 --- a/lib/rules/metadata/dl.ts +++ b/lib/rules/metadata/dl.ts @@ -23,8 +23,8 @@ interface DlMetadata { updated?: boolean; } -export const check: RuleCheckFunction = async sr => { - const dts = sr.extractHeaders(); +export const check: RuleCheckFunction = async context => { + const dts = context.extractHeaders(); const result: DlMetadata = {}; let shortname; let previousShortname; @@ -34,7 +34,7 @@ export const check: RuleCheckFunction = async sr => { const thisHref = $linkThis?.attr('href')?.trim(); if (thisHref) { result.thisVersion = thisHref; - shortname = await sr.getShortname(); + shortname = await context.getShortname(); } const $linkLate = dts.Latest ? dts.Latest.$dd.find('a').first() : null; @@ -48,7 +48,7 @@ export const check: RuleCheckFunction = async sr => { if (latestShortname !== shortname) { result.latestVersion = `https://www.w3.org/TR/${shortname}/`; } - } else sr.error(latestRule, 'latest-not-found'); + } else context.error(latestRule, 'latest-not-found'); } const $linkHistory = dts.History ? dts.History.$dd.find('a').first() : null; @@ -56,7 +56,7 @@ export const check: RuleCheckFunction = async sr => { if ($linkHistory && historyHref) { result.history = historyHref; - result.previousVersion = await sr.getPreviousVersion(); + result.previousVersion = await context.getPreviousVersion(); previousShortname = $linkHistory.attr('data-previous-shortname'); } @@ -70,8 +70,8 @@ export const check: RuleCheckFunction = async sr => { } // check same day publications - const ua = `W3C-Pubrules/${sr.version}`; - const docDate = sr.getDocumentDate(); + const ua = `W3C-Pubrules/${context.version}`; + const docDate = context.getDocumentDate(); if (docDate) { const year = docDate.getFullYear(); const month = (docDate.getMonth() + 1).toString(); diff --git a/lib/rules/metadata/docDate.ts b/lib/rules/metadata/docDate.ts index 362565869..40c7a3830 100644 --- a/lib/rules/metadata/docDate.ts +++ b/lib/rules/metadata/docDate.ts @@ -11,8 +11,8 @@ interface DocDateMetadata { docDate: `${number}-${number}-${number}`; } -export const check: RuleCheckFunction = sr => { - const docDate = sr.getDocumentDate(); +export const check: RuleCheckFunction = context => { + const docDate = context.getDocumentDate(); if (docDate) return { docDate: `${docDate.getFullYear()}-${docDate.getMonth() + 1}-${docDate.getDate()}`, diff --git a/lib/rules/metadata/editor-ids.ts b/lib/rules/metadata/editor-ids.ts index 14509ddbc..616ce73d0 100644 --- a/lib/rules/metadata/editor-ids.ts +++ b/lib/rules/metadata/editor-ids.ts @@ -18,8 +18,8 @@ interface EditorIDsMetadata { editorIDs: number[]; } -export const check: RuleCheckFunction = async sr => { - const dts = sr.extractHeaders(); +export const check: RuleCheckFunction = async context => { + const dts = context.extractHeaders(); const editorIds: number[] = []; const unresolvedUsernames = []; if (dts.Editor) { @@ -36,7 +36,7 @@ export const check: RuleCheckFunction = async sr => { if (id) editorIds.push(id); else unresolvedUsernames.push(editorGithub); } catch (error) { - sr.error(self, 'editor-github-failed', { + context.error(self, 'editor-github-failed', { name: error.cause, }); } @@ -44,7 +44,7 @@ export const check: RuleCheckFunction = async sr => { } } if (unresolvedUsernames.length) { - sr.error(self, 'editor-github-unresolvable', { + context.error(self, 'editor-github-unresolvable', { names: unresolvedUsernames, }); } diff --git a/lib/rules/metadata/editor-names.ts b/lib/rules/metadata/editor-names.ts index 7556214ba..f880d1758 100644 --- a/lib/rules/metadata/editor-names.ts +++ b/lib/rules/metadata/editor-names.ts @@ -11,12 +11,12 @@ interface EditorNamesMetadata { editorNames: string[]; } -export const check: RuleCheckFunction = sr => { - const dts = sr.extractHeaders(); +export const check: RuleCheckFunction = context => { + const dts = context.extractHeaders(); const editorNames: string[] = []; if (dts.Editor) { dts.Editor.$dd.each((_, el) => { - const editor = sr + const editor = context .$(el) .text() .trim() diff --git a/lib/rules/metadata/errata.ts b/lib/rules/metadata/errata.ts index 9da0d04a2..8b29c9972 100644 --- a/lib/rules/metadata/errata.ts +++ b/lib/rules/metadata/errata.ts @@ -12,12 +12,12 @@ interface ErrataMetadata { errata: string; } -export const check: RuleCheckFunction = sr => { +export const check: RuleCheckFunction = context => { const errataRegex = /errata/i; - const $links = sr.$('body div.head details + p > a'); + const $links = context.$('body div.head details + p > a'); const errata = $links .toArray() - .filter(el => errataRegex.test(sr.$(el).text())); + .filter(el => errataRegex.test(context.$(el).text())); if (errata.length && errata[0].attribs.href) return { errata: errata[0].attribs.href }; }; diff --git a/lib/rules/metadata/informative.ts b/lib/rules/metadata/informative.ts index d942d1f96..b9df694fd 100644 --- a/lib/rules/metadata/informative.ts +++ b/lib/rules/metadata/informative.ts @@ -11,13 +11,13 @@ interface InformativeMetadata { informative: boolean; } -export const check: RuleCheckFunction = sr => { - const $sotd = sr.getSotDSection(); +export const check: RuleCheckFunction = context => { + const $sotd = context.getSotDSection(); const expected = /This\s+document\s+is\s+informative\s+only\./; if (!$sotd) return; - const $stateEl = sr.getDocumentStateElement(); - const candidate = $stateEl && sr.norm($stateEl.text()).toLowerCase(); + const $stateEl = context.getDocumentStateElement(); + const candidate = $stateEl && context.norm($stateEl.text()).toLowerCase(); const isInformative = !!candidate && candidate.indexOf('group note') !== -1; return { diff --git a/lib/rules/metadata/process.ts b/lib/rules/metadata/process.ts index abb26debf..8d7f88697 100644 --- a/lib/rules/metadata/process.ts +++ b/lib/rules/metadata/process.ts @@ -16,8 +16,8 @@ interface ProcessMetadata { process: string; } -export const check: RuleCheckFunction = sr => { - const $processDocument = sr.$('a#w3c_process_revision').first(); +export const check: RuleCheckFunction = context => { + const $processDocument = context.$('a#w3c_process_revision').first(); const processDocumentHref = $processDocument.length && $processDocument.attr('href'); diff --git a/lib/rules/metadata/profile.ts b/lib/rules/metadata/profile.ts index 5885b43ea..b400a8317 100644 --- a/lib/rules/metadata/profile.ts +++ b/lib/rules/metadata/profile.ts @@ -15,7 +15,7 @@ import rules from '../../rules-track.js'; // 'self.name' would be 'metadata.profile' export const name = 'metadata.profile'; -export const check: RuleCheckFunction = async sr => { +export const check: RuleCheckFunction = async context => { let matchedLength = 0; let id; let $profileEl: Cheerio | undefined; @@ -25,13 +25,13 @@ export const check: RuleCheckFunction = async sr => { CRY: 'cryFeedbackDue', } as const; - const $stateEl = sr.getDocumentStateElement(); + const $stateEl = context.getDocumentStateElement(); if (!$stateEl) { throw new Error( 'Cannot find the <p id="w3c-state"> element for profile and date.

Please make sure the <p id="w3c-state">W3C @@Profile, DD Month Year</p> element can be selected by document.getElementById(\'w3c-state\');
If you are using bikeshed, please update to the latest version.' ); } - const candidate = $stateEl && sr.norm($stateEl.text()).toLowerCase(); + const candidate = $stateEl && context.norm($stateEl.text()).toLowerCase(); if (candidate) { for (const { profiles } of Object.values(rules)) { for (const [p, profile] of Object.entries(profiles)) { @@ -51,7 +51,7 @@ export const check: RuleCheckFunction = async sr => { function assembleMeta(id: string) { let meta: RecMetadata = { profile: id }; if (id in reviewStatus) { - const dueDate = sr.getFeedbackDueDate(); + const dueDate = context.getFeedbackDueDate(); const dates = dueDate && dueDate.valid; let res = dates[0]; if (dates.length === 0 || !res) return { profile: id }; @@ -63,7 +63,7 @@ export const check: RuleCheckFunction = async sr => { } // implementation report if (['CR', 'CRD', 'PR', 'REC'].includes(id)) { - const dts = sr.extractHeaders(); + const dts = context.extractHeaders(); if (dts.Implementation?.$dd?.find('a').length) { meta.implementationReport = dts.Implementation.$dd .find('a') @@ -72,7 +72,7 @@ export const check: RuleCheckFunction = async sr => { } } if (id === 'REC') { - meta = sr.getRecMetadata(meta); + meta = context.getRecMetadata(meta); } // Get 'track/rectrack' of the document based on id @@ -95,9 +95,11 @@ export const check: RuleCheckFunction = async sr => { function checkRecType() { if ( $profileEl && - sr.norm($profileEl.text()).indexOf('Candidate Recommendation') > 0 + context + .norm($profileEl.text()) + .indexOf('Candidate Recommendation') > 0 ) { - return sr.norm($profileEl.text()).indexOf('Draft') > 0 + return context.norm($profileEl.text()).indexOf('Draft') > 0 ? 'CRD' : 'CR'; } @@ -114,19 +116,19 @@ export const check: RuleCheckFunction = async sr => { if (id === 'CRY') { // distinguish CRY CRYD id = - sr.norm($profileEl?.text() || '').indexOf('Draft') > 0 + context.norm($profileEl?.text() || '').indexOf('Draft') > 0 ? 'CRYD' : 'CRY'; } return assembleMeta(id); } else { - const docTitle = (await getTitle(sr))?.title; + const docTitle = (await getTitle(context))?.title; if (candidate && candidate.indexOf("editor's draft") > -1) { throw new Error( `The document "${docTitle}" seems to be an Editor's Draft, which is not supported.` ); } else if ( - sr.$('link[href^="https://www.w3.org/StyleSheets/TR/"]').length + context.$('link[href^="https://www.w3.org/StyleSheets/TR/"]').length ) { let profileList = ''; sortedProfiles.forEach(category => { diff --git a/lib/rules/metadata/sotd.ts b/lib/rules/metadata/sotd.ts index bb2092a30..3bed90a09 100644 --- a/lib/rules/metadata/sotd.ts +++ b/lib/rules/metadata/sotd.ts @@ -10,7 +10,7 @@ interface SotdMetadata { sotd: string; } -export const check: RuleCheckFunction = sr => { - const $sotd = sr.getSotDSection(); - return { sotd: $sotd ? sr.norm($sotd.html()!) : 'Not found' }; +export const check: RuleCheckFunction = context => { + const $sotd = context.getSotDSection(); + return { sotd: $sotd ? context.norm($sotd.html()!) : 'Not found' }; }; diff --git a/lib/rules/metadata/title.ts b/lib/rules/metadata/title.ts index 414e9e434..2adb9f913 100644 --- a/lib/rules/metadata/title.ts +++ b/lib/rules/metadata/title.ts @@ -8,12 +8,12 @@ import type { RuleCheckFunction } from '../../types.js'; export const name = 'metadata.title'; -export const check: RuleCheckFunction<{ title: string } | void> = sr => { - const $title = sr.$('body div.head h1').first(); +export const check: RuleCheckFunction<{ title: string } | void> = context => { + const $title = context.$('body div.head h1').first(); if (!$title.length) return; $title.html($title.html()!.replace(/:
/g, ': ').replace(/
/g, ' - ')); return { - title: sr.norm($title.text()), + title: context.norm($title.text()), }; }; diff --git a/lib/rules/sotd/candidate-review-end.ts b/lib/rules/sotd/candidate-review-end.ts index ec4103e7b..1baf48695 100644 --- a/lib/rules/sotd/candidate-review-end.ts +++ b/lib/rules/sotd/candidate-review-end.ts @@ -8,28 +8,32 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = sr => { +export const check: RuleCheckFunction = context => { const isEditorial = - (sr.config!.editorial && /^true$/i.test(sr.config!.editorial)) || false; + (context.config!.editorial && + /^true$/i.test(context.config!.editorial)) || + false; if (isEditorial) { - sr.warning(self, 'editorial'); + context.warning(self, 'editorial'); } else { - const dates = sr.getFeedbackDueDate(); - if (dates.list.length === 0) sr.error(self, 'not-found'); + const dates = context.getFeedbackDueDate(); + if (dates.list.length === 0) context.error(self, 'not-found'); else { let res; if (dates.valid.length === 1) { [res] = dates.valid; - sr.info(self, 'date-found', { date: res.toDateString() }); + context.info(self, 'date-found', { date: res.toDateString() }); } else if (dates.valid.length > 1) { - sr.warning(self, 'multiple-found'); + context.warning(self, 'multiple-found'); res = dates.valid.map(item => new Date(item).toDateString()); - sr.info(self, 'date-found', { date: res.join(', ') }); + context.info(self, 'date-found', { date: res.join(', ') }); } else { // dates found but not valid res = dates.list.map(item => new Date(item).toDateString()); - sr.error(self, 'found-not-valid', { date: res.join(', ') }); + context.error(self, 'found-not-valid', { + date: res.join(', '), + }); } } } diff --git a/lib/rules/sotd/charter.ts b/lib/rules/sotd/charter.ts index 5b8969b3a..bad8bcba0 100644 --- a/lib/rules/sotd/charter.ts +++ b/lib/rules/sotd/charter.ts @@ -13,13 +13,13 @@ export const { name } = self; const charterText = /The disclosure obligations of the Participants of this group are described in the charter\./; -export const check: RuleCheckFunction = async sr => { - const $sotd = sr.getSotDSection(); +export const check: RuleCheckFunction = async context => { + const $sotd = context.getSotDSection(); if ($sotd) { - const deliverIds = await sr.getDelivererIDs(); + const deliverIds = await context.getDelivererIDs(); if (!deliverIds.length) { - sr.error(self, 'no-group'); + context.error(self, 'no-group'); return; } @@ -32,18 +32,18 @@ export const check: RuleCheckFunction = async sr => { ); if (!groupIds.length) return; - const charters = await sr.getCharters(); + const charters = await context.getCharters(); if (!charters.length) { - sr.error(self, 'no-charter'); + context.error(self, 'no-charter'); return; } - if (sr.config!.longStatus === 'Interest Group Note') { + if (context.config!.longStatus === 'Interest Group Note') { const expectedHref = charters && `${charters[0]}#patentpolicy`; // check text exists - const txt = sr.norm($sotd && $sotd.text()); + const txt = context.norm($sotd && $sotd.text()); if (!charterText.test(txt)) { - sr.error(self, 'text-not-found'); + context.error(self, 'text-not-found'); return; } @@ -51,10 +51,10 @@ export const check: RuleCheckFunction = async sr => { let charterLinkFound = false; let charterHrefInDocument; $sotd.find('a[href]').each((_, a) => { - const $a = sr.$(a); + const $a = context.$(a); const charterHref = $a.attr('href'); - const text = sr.norm($a.text()); - const pText = sr.norm($a.parent().text()); + const text = context.norm($a.text()); + const pText = context.norm($a.parent().text()); // Find the right paragraph and right link. if (charterText.test(pText) && text === 'charter') { charterLinkFound = true; @@ -62,9 +62,9 @@ export const check: RuleCheckFunction = async sr => { } }); if (!charterLinkFound) { - sr.error(self, 'link-not-found'); + context.error(self, 'link-not-found'); } else if (expectedHref !== charterHrefInDocument) { - sr.error(self, 'wrong-link', { + context.error(self, 'wrong-link', { expectedHref, }); } diff --git a/lib/rules/sotd/deliverer-note.ts b/lib/rules/sotd/deliverer-note.ts index 4fb0bb0b0..14445d294 100644 --- a/lib/rules/sotd/deliverer-note.ts +++ b/lib/rules/sotd/deliverer-note.ts @@ -8,7 +8,7 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = sr => { - const deliverers = sr.getDataDelivererIDs(); - if (deliverers.length === 0) sr.error(self, 'not-found'); +export const check: RuleCheckFunction = context => { + const deliverers = context.getDataDelivererIDs(); + if (deliverers.length === 0) context.error(self, 'not-found'); }; diff --git a/lib/rules/sotd/deployment.ts b/lib/rules/sotd/deployment.ts index be0a0a56f..93dfef0c3 100644 --- a/lib/rules/sotd/deployment.ts +++ b/lib/rules/sotd/deployment.ts @@ -10,8 +10,8 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = sr => { - const $sotd = sr.getSotDSection(); +export const check: RuleCheckFunction = context => { + const $sotd = context.getSotDSection(); if ($sotd) { // Find the sentence of 'W3C recommends the wide deployment of this specification as a standard for the Web.' @@ -20,7 +20,7 @@ export const check: RuleCheckFunction = sr => { const paragraph = $sotd .find('p') .toArray() - .find(p => sr.norm(sr.$(p).text()) === depText); - if (!paragraph) sr.error(self, 'not-found'); + .find(p => context.norm(context.$(p).text()) === depText); + if (!paragraph) context.error(self, 'not-found'); } }; diff --git a/lib/rules/sotd/diff.ts b/lib/rules/sotd/diff.ts index 976c8e71b..b0ff9a139 100644 --- a/lib/rules/sotd/diff.ts +++ b/lib/rules/sotd/diff.ts @@ -8,6 +8,6 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = sr => { - sr.info(self, 'note'); +export const check: RuleCheckFunction = context => { + context.info(self, 'note'); }; diff --git a/lib/rules/sotd/draft-stability.ts b/lib/rules/sotd/draft-stability.ts index 5512330b3..bf0030103 100644 --- a/lib/rules/sotd/draft-stability.ts +++ b/lib/rules/sotd/draft-stability.ts @@ -10,9 +10,9 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = sr => { - const $sotd = sr.getSotDSection(); - const { crType, cryType } = sr.config!; +export const check: RuleCheckFunction = context => { + const $sotd = context.getSotDSection(); + const { crType, cryType } = context.config!; const STABILITY_REX = /This is a draft document and may be updated, replaced,? or obsoleted by other documents at any time\. It is inappropriate to cite this document as other than a work in progress\./; @@ -20,21 +20,21 @@ export const check: RuleCheckFunction = sr => { 'This document is maintained and updated at any time. Some parts of this document are work in progress.'; if ($sotd) { - const txt = sr.norm($sotd.text()); + const txt = context.norm($sotd.text()); // CRD and CRYD allows both sentence. if ( (crType && crType === 'Draft') || (cryType && cryType === 'Draft') ) { if (!txt.match(STABILITY_REX) && !txt.includes(STABILITY_2)) - sr.error(self, 'not-found-either', { + context.error(self, 'not-found-either', { expected1: STABILITY_REX, expected2: STABILITY_2, }); } // while other profiles allows only 'STABILITY' sentence else if (!txt.match(STABILITY_REX)) - sr.error(self, 'not-found', { + context.error(self, 'not-found', { expected: STABILITY_REX, }); } diff --git a/lib/rules/sotd/new-features.ts b/lib/rules/sotd/new-features.ts index a94e320fa..1f8b38688 100644 --- a/lib/rules/sotd/new-features.ts +++ b/lib/rules/sotd/new-features.ts @@ -8,9 +8,9 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = sr => { - const $sotd = sr.getSotDSection(); - const docType = `${sr.config!.status !== 'REC' ? 'upcoming ' : ''}Recommendation`; +export const check: RuleCheckFunction = context => { + const $sotd = context.getSotDSection(); + const docType = `${context.config!.status !== 'REC' ? 'upcoming ' : ''}Recommendation`; const warning = new RegExp( `Future updates to this ${docType} may incorporate new features.` ); @@ -18,19 +18,19 @@ export const check: RuleCheckFunction = sr => { const linkHref = 'https://www.w3.org/policies/process/20250818/#allow-new-features'; - if ($sotd && sr.norm($sotd.text()).match(warning)) { + if ($sotd && context.norm($sotd.text()).match(warning)) { const foundLink = $sotd .find('a') .toArray() .some( a => - sr.norm(sr.$(a).text()) === linkTxt && + context.norm(context.$(a).text()) === linkTxt && a.attribs.href === linkHref ); if (!foundLink) { - sr.error(self, 'no-link'); + context.error(self, 'no-link'); } } else { - sr.warning(self, 'no-warning'); + context.warning(self, 'no-warning'); } }; diff --git a/lib/rules/sotd/obsl-rescind.ts b/lib/rules/sotd/obsl-rescind.ts index 27b39e2a6..450437943 100644 --- a/lib/rules/sotd/obsl-rescind.ts +++ b/lib/rules/sotd/obsl-rescind.ts @@ -6,16 +6,19 @@ // Obsoleting and Rescinding W3C Specifications.

import type { Cheerio } from 'cheerio'; -import type { Specberus } from '../../validator.js'; +import type { RuleContext } from '../../rule-context.js'; import type { Element } from 'domhandler'; import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -function findRscndRationale($candidates: Cheerio, sr: Specberus) { - const v = sr.config!.rescinds === true ? 'rescind' : ''; +function findRscndRationale( + $candidates: Cheerio, + context: RuleContext +) { + const v = context.config!.rescinds === true ? 'rescind' : ''; for (const p of $candidates.toArray()) { - const $p = sr.$(p); - const text = sr.norm($p.text()); + const $p = context.$(p); + const text = context.norm($p.text()); const wanted1 = new RegExp( `W3C has chosen to ${v} the .*? Recommendation for the following reasons:`, 'i' @@ -37,24 +40,24 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = sr => { - const $sotd = sr.getSotDSection(); +export const check: RuleCheckFunction = context => { + const $sotd = context.getSotDSection(); if ($sotd) { const $rationale = - findRscndRationale($sotd.filter('p'), sr) || - findRscndRationale($sotd.find('p'), sr); + findRscndRationale($sotd.filter('p'), context) || + findRscndRationale($sotd.find('p'), context); if (!$rationale?.length) { - sr.error(self, 'no-rationale'); + context.error(self, 'no-rationale'); } else { const $a = $rationale.find('a:last-child').first(); const href = $a.attr('href'); - const text = sr.norm($a.text()); + const text = context.norm($a.text()); if ( href !== 'https://www.w3.org/2016/11/obsoleting-rescinding/' || text !== 'explanation of Obsoleting, Rescinding or Superseding W3C Specifications' ) { - sr.error(self, 'no-explanation-link'); + context.error(self, 'no-explanation-link'); } } } diff --git a/lib/rules/sotd/pp.ts b/lib/rules/sotd/pp.ts index ca32d3a08..5f9e3fe99 100644 --- a/lib/rules/sotd/pp.ts +++ b/lib/rules/sotd/pp.ts @@ -1,7 +1,7 @@ import type { Cheerio } from 'cheerio'; import type { Element } from 'domhandler'; -import type { Specberus } from '../../validator.js'; +import type { RuleContext } from '../../rule-context.js'; import type { RuleCheckFunction, RuleMeta } from '../../types.js'; const self: RuleMeta = { @@ -14,8 +14,8 @@ const ppLink = 'https://www.w3.org/policies/patent-policy/'; const ppLink2020 = 'https://www.w3.org/policies/patent-policy/20200915/'; const ppLink2025 = 'https://www.w3.org/policies/patent-policy/20250515/'; -function buildWanted(groups: string[], sr: Specberus) { - const config = sr.config!; +function buildWanted(groups: string[], context: RuleContext) { + const config = context.config!; let wanted; const isRecTrack = config.track === 'Recommendation'; const ppText = '( 15 September 2020| 15 May 2025)?'; @@ -60,15 +60,15 @@ function buildWanted(groups: string[], sr: Specberus) { }; } -function findPP($candidates: Cheerio, sr: Specberus) { - const delivererGroups = sr.getDelivererNames(); - if (delivererGroups.length > 1) sr.warning(self, 'joint-publication'); +function findPP($candidates: Cheerio, context: RuleContext) { + const delivererGroups = context.getDelivererNames(); + if (delivererGroups.length > 1) context.warning(self, 'joint-publication'); - const wanted = buildWanted(delivererGroups, sr); + const wanted = buildWanted(delivererGroups, context); const expected = wanted.text; for (const p of $candidates.toArray()) { - const $p = sr.$(p); - const text = sr.norm($p.text()); + const $p = context.$(p); + const text = context.norm($p.text()); if (wanted.regex.test(text)) return { $pp: $p, expected }; } return { $pp: null, expected }; @@ -76,18 +76,18 @@ function findPP($candidates: Cheerio, sr: Specberus) { export const { name } = self; -export const check: RuleCheckFunction = sr => { - const $sotd = sr.getSotDSection(); - const track = sr.config!.track; +export const check: RuleCheckFunction = context => { + const $sotd = context.getSotDSection(); + const track = context.config!.track; const isRecTrack = track === 'Recommendation'; if ($sotd) { const { $pp, expected } = findPP( $sotd.filter('p').add($sotd.find('p')), - sr + context ); if (!$pp) { - sr.error(self, 'no-pp', { expected }); + context.error(self, 'no-pp', { expected }); return; } @@ -96,9 +96,9 @@ export const check: RuleCheckFunction = sr => { let foundEssentials = false; let foundSection6 = false; $pp.find('a[href]').each((_, a) => { - const $a = sr.$(a); + const $a = context.$(a); const href = $a.attr('href')!; - const text = sr.norm($a.text()); + const text = context.norm($a.text()); const possiblePPLinks = [ppLink, ppLink2020, ppLink2025]; if ( possiblePPLinks.includes(href) && @@ -136,14 +136,15 @@ export const check: RuleCheckFunction = sr => { } }); - if (!foundLink) sr.error(self, 'no-link'); - if (!foundPublicList && isRecTrack) sr.error(self, 'no-disclosures'); + if (!foundLink) context.error(self, 'no-link'); + if (!foundPublicList && isRecTrack) + context.error(self, 'no-disclosures'); if ( (track === 'Recommendation' || track === 'Note') && isRecTrack && !foundEssentials ) - sr.error(self, 'no-claims', { + context.error(self, 'no-claims', { link: `${ppLink}#def-essential`, }); if ( @@ -151,7 +152,7 @@ export const check: RuleCheckFunction = sr => { isRecTrack && !foundSection6 ) - sr.error(self, 'no-section6', { + context.error(self, 'no-section6', { link: `${ppLink}#sec-Disclosure`, }); } diff --git a/lib/rules/sotd/process-document.ts b/lib/rules/sotd/process-document.ts index a195cb5c8..04ec0292d 100644 --- a/lib/rules/sotd/process-document.ts +++ b/lib/rules/sotd/process-document.ts @@ -8,8 +8,8 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = sr => { - const $sotd = sr.getSotDSection(); +export const check: RuleCheckFunction = context => { + const $sotd = context.getSotDSection(); const BOILERPLATE_PREFIX = 'This document is governed by the '; const BOILERPLATE_SUFFIX = ' W3C Process Document.'; const newProc = '18 August 2025'; @@ -23,7 +23,7 @@ export const check: RuleCheckFunction = sr => { BOILERPLATE_PREFIX + previousProc + BOILERPLATE_SUFFIX; // 1 month transition period - sr.transition({ + context.transition({ from: new Date('2023-11-02'), to: new Date('2023-12-03'), doBefore() {}, @@ -38,36 +38,36 @@ export const check: RuleCheckFunction = sr => { if ($sotd) { let found = false; $sotd.find('p').each((_, p) => { - const $p = sr.$(p); + const $p = context.$(p); const pText = $p.text(); const $a = $p.find('a').first(); if ( - sr.norm(pText) === boilerplate && + context.norm(pText) === boilerplate && $a.length && $a.attr('href') === newProcUri ) { if (found) - sr.error(self, 'multiple-times', { process: newProc }); + context.error(self, 'multiple-times', { process: newProc }); else { found = true; } } else if ( - sr.norm(pText) === previousBoilerplate && + context.norm(pText) === previousBoilerplate && $a.length && $a.attr('href') === previousProcUri ) { if (previousAllowed) { - sr.warning(self, 'previous-allowed', { + context.warning(self, 'previous-allowed', { process: previousProc, }); found = true; } else { - sr.error(self, 'previous-not-allowed', { + context.error(self, 'previous-not-allowed', { process: previousProc, }); } } }); - if (!found) sr.error(self, 'not-found', { process: newProc }); + if (!found) context.error(self, 'not-found', { process: newProc }); } }; diff --git a/lib/rules/sotd/publish.ts b/lib/rules/sotd/publish.ts index 8d43cc9f3..c47bc88e6 100644 --- a/lib/rules/sotd/publish.ts +++ b/lib/rules/sotd/publish.ts @@ -8,9 +8,9 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = async sr => { - const $sotd = sr.getSotDSection(); - const { crType, cryType, longStatus, status, track } = sr.config!; +export const check: RuleCheckFunction = async context => { + const $sotd = context.getSotDSection(); + const { crType, cryType, longStatus, status, track } = context.config!; let docType = longStatus; if (status === 'CR' || status === 'CRD') { docType = `Candidate Recommendation ${crType}`; @@ -18,7 +18,7 @@ export const check: RuleCheckFunction = async sr => { docType = `Candidate Registry ${cryType}`; } - const text = `^This document was (?:produced|published) by the (.+? Working Group|Technical Architecture Group|Advisory Board|.+? Interest Group)( and the (.+? Working Group|Technical Architecture Group|Advisory Board|.+? Interest Group))? as a ${docType} using the ${sr.config!.track} track.`; + const text = `^This document was (?:produced|published) by the (.+? Working Group|Technical Architecture Group|Advisory Board|.+? Interest Group)( and the (.+? Working Group|Technical Architecture Group|Advisory Board|.+? Interest Group))? as a ${docType} using the ${context.config!.track} track.`; if ($sotd) { // Find the paragraph of 'This document was published by ... , it includes ...' @@ -26,9 +26,9 @@ export const check: RuleCheckFunction = async sr => { const paragraph = $sotd .find('p') .toArray() - .find(p => sr.norm(sr.$(p).text()).match(publishReg)); + .find(p => context.norm(context.$(p).text()).match(publishReg)); if (!paragraph) { - sr.error(self, 'not-found', { publishReg }); + context.error(self, 'not-found', { publishReg }); return; } @@ -36,7 +36,7 @@ export const check: RuleCheckFunction = async sr => { let baseURL = /^https:\/\/www.w3.org\/2023\/Process-20230612\//; // 1 month transition period - sr.transition({ + context.transition({ from: new Date('2023-11-02'), to: new Date('2023-12-03'), doBefore() { @@ -56,16 +56,18 @@ export const check: RuleCheckFunction = async sr => { const urlExpected = new RegExp(`${baseURL.source}#recs-and-notes$`); const trackEl = $sotdLinks .toArray() - .find(el => sr.norm(sr.$(el).text()).match(`${track} track`)); + .find(el => + context.norm(context.$(el).text()).match(`${track} track`) + ); if (trackEl && !urlExpected.test(trackEl.attribs.href)) { - sr.error(self, 'url-not-match', { + context.error(self, 'url-not-match', { url: urlExpected, text: `${track} track`, }); } // Check the Deliverer Group link. - const delivererGroups = await sr.getDelivererGroups(); + const delivererGroups = await context.getDelivererGroups(); const groupsURLRegExpExpected = delivererGroups.map( delivererGroup => new RegExp( @@ -75,14 +77,14 @@ export const check: RuleCheckFunction = async sr => { const sotdLinksHrefs = $sotdLinks.toArray().map(l => l.attribs.href); groupsURLRegExpExpected.forEach(groupURLRegExpExpected => { if (!sotdLinksHrefs.some(l => groupURLRegExpExpected.test(l))) { - sr.error(self, 'no-homepage-link', { + context.error(self, 'no-homepage-link', { homepage: groupURLRegExpExpected, }); } }); // recType: doc has sentence 'It includes proposed ...' in sotd. - const recType = status === 'REC' ? sr.getRecMetadata() : null; + const recType = status === 'REC' ? context.getRecMetadata() : null; // check if 'candidate amendments' or 'proposed amendments' link in same paragraph is valid. if (recType && JSON.stringify(recType) !== '{}') { let urlExpected: RegExp; @@ -120,15 +122,15 @@ export const check: RuleCheckFunction = async sr => { ); textExpected = /candidate addition(s)?/; } - const linkFound = sr + const linkFound = context .$(paragraph) .find('a') .toArray() .some(el => { - const $el = sr.$(el); - if (sr.norm($el.text()).match(textExpected)) { + const $el = context.$(el); + if (context.norm($el.text()).match(textExpected)) { if (!urlExpected.test($el.attr('href') || '')) { - sr.error(self, 'url-not-match', { + context.error(self, 'url-not-match', { url: urlExpected, text: textExpected, }); @@ -138,7 +140,7 @@ export const check: RuleCheckFunction = async sr => { return false; }); if (!linkFound) - sr.error(self, 'url-text-not-found', { + context.error(self, 'url-text-not-found', { url: urlExpected!, text: textExpected!, }); diff --git a/lib/rules/sotd/rec-addition.ts b/lib/rules/sotd/rec-addition.ts index 99686f676..59c870e11 100644 --- a/lib/rules/sotd/rec-addition.ts +++ b/lib/rules/sotd/rec-addition.ts @@ -1,6 +1,6 @@ import type { Cheerio } from 'cheerio'; import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -import type { Specberus } from '../../validator.js'; +import type { RuleContext } from '../../rule-context.js'; import type { Element } from 'domhandler'; // This rule only apply to REC, check changes with colored background. @@ -28,44 +28,44 @@ interface CheckSectionOptions { /** * Check if the recommendation change type mentioned by text is consistent with the html element. */ -function checkSection(sr: Specberus, options: CheckSectionOptions) { +function checkSection(context: RuleContext, options: CheckSectionOptions) { if (options.typeOfRec) { if (options.$htmlSection.length) { const expectedReg = new RegExp(options.expectedText); - const text = sr.norm(options.$htmlSection.text()); + const text = context.norm(options.$htmlSection.text()); if (!expectedReg.test(text)) { - sr.error(self, 'wrong-text', { + context.error(self, 'wrong-text', { typeOfChange: options.typeOfChange, sectionClass: options.sectionClass, expectText: options.expectedText, }); } } else { - sr.error(self, 'no-section', { + context.error(self, 'no-section', { typeOfChange: options.typeOfChange, sectionClass: options.sectionClass, expectText: options.expectedText, }); } } else if (options.$htmlSection.length) - sr.error(self, 'unnecessary-section', { + context.error(self, 'unnecessary-section', { typeOfChange: options.typeOfChange, sectionClass: options.sectionClass, expectText: options.expectedText, }); } -export const check: RuleCheckFunction = sr => { - const $sotd = sr.getSotDSection(); +export const check: RuleCheckFunction = context => { + const $sotd = context.getSotDSection(); if ($sotd) { - const recType = sr.getRecMetadata(); + const recType = context.getRecMetadata(); const $pCorSection = $sotd.find('p.correction.proposed').first(); const $pAddSection = $sotd.find('p.addition.proposed').first(); const $cCorSection = $sotd.find('p.correction:not(.proposed)').first(); const $cAddSection = $sotd.find('p.addition:not(.proposed)').first(); // check for 'proposed corrections' - checkSection(sr, { + checkSection(context, { typeOfRec: recType.pSubChanges, $htmlSection: $pCorSection, expectedText: P_CORRECTION, @@ -74,7 +74,7 @@ export const check: RuleCheckFunction = sr => { }); // check for 'proposed additions' - checkSection(sr, { + checkSection(context, { typeOfRec: recType.pNewFeatures, $htmlSection: $pAddSection, expectedText: P_ADDITION, @@ -83,7 +83,7 @@ export const check: RuleCheckFunction = sr => { }); // check for 'candidate corrections' - checkSection(sr, { + checkSection(context, { typeOfRec: recType.cSubChanges, $htmlSection: $cCorSection, expectedText: C_CORRECTION, @@ -92,7 +92,7 @@ export const check: RuleCheckFunction = sr => { }); // check for 'candidate additions' - checkSection(sr, { + checkSection(context, { typeOfRec: recType.cNewFeatures, $htmlSection: $cAddSection, expectedText: C_ADDITION, diff --git a/lib/rules/sotd/rec-comment-end.ts b/lib/rules/sotd/rec-comment-end.ts index bdb02948c..750c84307 100644 --- a/lib/rules/sotd/rec-comment-end.ts +++ b/lib/rules/sotd/rec-comment-end.ts @@ -1,5 +1,5 @@ import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -import { Specberus } from '../../validator.js'; +import { dateRegexStrCapturing } from '../../rule-context.js'; const self: RuleMeta = { name: 'sotd.rec-comment-end', @@ -9,14 +9,14 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = sr => { - const $sotd = sr.getSotDSection(); +export const check: RuleCheckFunction = context => { + const $sotd = context.getSotDSection(); if ($sotd) { - const recType = sr.getRecMetadata(); + const recType = context.getRecMetadata(); if (recType.pSubChanges || recType.pNewFeatures) { - const txt = sr.norm($sotd.text()); - const rex = new RegExp(Specberus.dateRegexStrCapturing, 'g'); - const docDate = sr.getDocumentDate()!; + const txt = context.norm($sotd.text()); + const rex = new RegExp(dateRegexStrCapturing, 'g'); + const docDate = context.getDocumentDate()!; // 60 days later than docDate; const minimumEndDate = new Date( @@ -29,25 +29,27 @@ export const check: RuleCheckFunction = sr => { .slice(1) .join(' '); if (!rex.test(txt)) - sr.error(self, 'not-found', { minimumEndDate: readableDate }); + context.error(self, 'not-found', { + minimumEndDate: readableDate, + }); else { const matches = txt.match(rex); const dateFound = []; if (matches) { for (const match of matches) { - const date = sr.stringToDate(match); + const date = context.stringToDate(match); if (date && date > minimumEndDate) { - dateFound.push(sr.stringToDate(match)); + dateFound.push(context.stringToDate(match)); } } } if (dateFound.length > 1) { - sr.warning(self, 'multi-found', { + context.warning(self, 'multi-found', { date: dateFound.join(', '), minimumEndDate: readableDate, }); } else if (!dateFound.length) { - sr.error(self, 'not-found', { + context.error(self, 'not-found', { minimumEndDate: readableDate, }); } diff --git a/lib/rules/sotd/stability.ts b/lib/rules/sotd/stability.ts index 0713b02ae..7dbd1944e 100644 --- a/lib/rules/sotd/stability.ts +++ b/lib/rules/sotd/stability.ts @@ -5,17 +5,17 @@ import type { Cheerio } from 'cheerio'; import type { Element } from 'domhandler'; -import type { Specberus } from '../../validator.js'; +import type { RuleContext } from '../../rule-context.js'; import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -async function findSW($candidates: Cheerio, sr: Specberus) { +async function findSW($candidates: Cheerio, context: RuleContext) { let wanted = ''; let $sw: Cheerio | undefined; - const { crType, cryType, longStatus, status } = sr.config!; + const { crType, cryType, longStatus, status } = context.config!; if (longStatus === 'Group Note' || longStatus === 'Group Note Draft') { // Find the sentence of 'Group Notes are not endorsed by W3C nor its Members.' or 'This Group Note is endorsed by the @@ Group, but is not endorsed by W3C itself nor its Members.' - const groups = sr.getDelivererNames().join(' and the '); + const groups = context.getDelivererNames().join(' and the '); wanted = `(${longStatus}s are not endorsed by W3C nor its Members|This ${longStatus} is endorsed by the ${groups}, but is not endorsed by W3C itself nor its Members).`; } else if (longStatus === 'Statement') { wanted = @@ -27,7 +27,7 @@ async function findSW($candidates: Cheerio, sr: Specberus) { wanted = 'A W3C Registry is a specification that, after extensive consensus-building, is endorsed by W3C and its Members.'; } else { - const groupIds = await sr.getDelivererIDs(); + const groupIds = await context.getDelivererIDs(); const INTRO_S = ` A Candidate Recommendation Snapshot has received wide review, is intended to gather implementation experience, and has commitments from Working Group members to royalty-free licensing for implementations.`; const INTRO_D = ` A Candidate Recommendation Draft integrates changes from the previous Candidate Recommendation that the Working Group${ groupIds.length > 1 ? 's intend' : ' intends' @@ -56,8 +56,8 @@ async function findSW($candidates: Cheerio, sr: Specberus) { // TODO: better loop $candidates.each((_, p) => { - const $p = sr.$(p); - const text = sr.norm($p.text()); + const $p = context.$(p); + const text = context.norm($p.text()); if (text.match(wantedRE)) { $sw = $p; return false; @@ -74,33 +74,36 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = async sr => { - const { crType, cryType, status } = sr.config!; - const $sotd = sr.getSotDSection(); +export const check: RuleCheckFunction = async context => { + const { crType, cryType, status } = context.config!; + const $sotd = context.getSotDSection(); if ($sotd) { if (status === 'REC') { - const txt = sr.norm($sotd.text()); + const txt = context.norm($sotd.text()); const wanted = `A W3C Recommendation is a specification that, after extensive consensus-building, is endorsed by W3C and its Members, and has commitments from Working Group members to royalty-free licensing for implementations.`; const rex = new RegExp(wanted); - if (!rex.test(txt)) sr.error(self, 'no-rec-review'); + if (!rex.test(txt)) context.error(self, 'no-rec-review'); } else { const $paragraphs = $sotd.filter('p'); const { $sw, expected } = await findSW( $paragraphs.length ? $paragraphs : $sotd.find('p'), - sr + context ); - if (!$sw) sr.error(self, 'no-stability', { expected }); + if (!$sw) context.error(self, 'no-stability', { expected }); else if (crType === 'Snapshot' || cryType === 'Snapshot') { const review = $sw .find('a') .toArray() - .find(el => sr.norm(sr.$(el).text()) === 'wide review'); - if (!review) sr.error(self, 'no-cr-review'); + .find( + el => + context.norm(context.$(el).text()) === 'wide review' + ); + if (!review) context.error(self, 'no-cr-review'); else if ( review.attribs.href !== 'https://www.w3.org/policies/process/20250818/#dfn-wide-review' ) - sr.error(self, 'wrong-cr-review-link'); + context.error(self, 'wrong-cr-review-link'); } } @@ -114,11 +117,11 @@ export const check: RuleCheckFunction = async sr => { .toArray() .some( link => - sr.norm(sr.$(link).text()) === licensingText && - link.attribs.href === licensingLink + context.norm(context.$(link).text()) === + licensingText && link.attribs.href === licensingLink ); if (!licensingFound) - sr.error(self, 'no-licensing-link', { + context.error(self, 'no-licensing-link', { licensingText, licensingLink, }); diff --git a/lib/rules/sotd/submission.ts b/lib/rules/sotd/submission.ts index 7dbf55729..7855f05a4 100644 --- a/lib/rules/sotd/submission.ts +++ b/lib/rules/sotd/submission.ts @@ -1,7 +1,7 @@ import type { Cheerio } from 'cheerio'; import type { Element } from 'domhandler'; -import type { Specberus } from '../../validator.js'; +import type { RuleContext } from '../../rule-context.js'; import type { RuleCheckFunction, RuleMeta } from '../../types.js'; const self: RuleMeta = { @@ -12,7 +12,7 @@ const self: RuleMeta = { export const { name } = self; -function findSubmText($candidates: Cheerio, sr: Specberus) { +function findSubmText($candidates: Cheerio, context: RuleContext) { const wanted = 'By publishing this document, W3C acknowledges that the Submitting Members ' + 'have made a formal Submission request to W3C for discussion. Publication of ' + @@ -27,21 +27,21 @@ function findSubmText($candidates: Cheerio, sr: Specberus) { 'complete list of acknowledged W3C Member Submissions.'; for (const p of $candidates.toArray()) { - const $p = sr.$(p); - const text = sr.norm($p.text()); + const $p = context.$(p); + const text = context.norm($p.text()); if (text === wanted) return $p; } return null; } -export const check: RuleCheckFunction = sr => { - const $sotd = sr.getSotDSection(); +export const check: RuleCheckFunction = context => { + const $sotd = context.getSotDSection(); if ($sotd) { const $st = - findSubmText($sotd.filter('p'), sr) || - findSubmText($sotd.find('p'), sr); + findSubmText($sotd.filter('p'), context) || + findSubmText($sotd.find('p'), context); if (!$st) { - sr.error(self, 'no-submission-text'); + context.error(self, 'no-submission-text'); return; } @@ -60,9 +60,9 @@ export const check: RuleCheckFunction = sr => { let foundSubmMembers = false; let foundComment = false; $st.find('a[href]').each((_, a) => { - const $a = sr.$(a); + const $a = context.$(a); const href = $a.attr('href')!; - const text = sr.norm($a.text()); + const text = context.norm($a.text()); if ( [w3cProcessNew, w3cProcessOld].includes(href) && text === 'W3C Process' @@ -97,26 +97,26 @@ export const check: RuleCheckFunction = sr => { } }); if (!foundW3CProcess) - sr.error(self, 'link-text', { + context.error(self, 'link-text', { href: w3cProcessNew, text: 'W3C Process', }); if (!foundW3CMembership) - sr.error(self, 'link-text', { + context.error(self, 'link-text', { href: w3cMembership, text: 'W3C Membership', }); if (!foundPP) - sr.error(self, 'link-text', { + context.error(self, 'link-text', { href: w3cPP, text: 'section 3.3 of the W3C Patent Policy', }); if (!foundSubm) - sr.error(self, 'link-text', { + context.error(self, 'link-text', { href: w3cSubm, text: 'list of acknowledged W3C Member Submissions', }); - if (!foundSubmMembers) sr.error(self, 'no-sm-link'); - if (!foundComment) sr.error(self, 'no-tc-link'); + if (!foundSubmMembers) context.error(self, 'no-sm-link'); + if (!foundComment) context.error(self, 'no-tc-link'); } }; diff --git a/lib/rules/sotd/supersedable.ts b/lib/rules/sotd/supersedable.ts index 5b6c9f54c..008865c1d 100644 --- a/lib/rules/sotd/supersedable.ts +++ b/lib/rules/sotd/supersedable.ts @@ -15,14 +15,14 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = sr => { - const $sotd = sr.getSotDSection(); +export const check: RuleCheckFunction = context => { + const $sotd = context.getSotDSection(); if ($sotd) { let $em = $sotd.filter('p').children('em').first(); if (!$em.length) $em = $sotd.find('p em').first(); - const txt = sr.norm($em.text()); + const txt = context.norm($em.text()); const wanted = `${'This section describes the status of this document at the time of its publication. A list of current W3C publications '}${ - sr.config!.status === 'SUBM' + context.config!.status === 'SUBM' ? '' : 'and the latest revision of this technical report ' }can be found in the W3C standards and drafts index.`; @@ -34,13 +34,13 @@ export const check: RuleCheckFunction = sr => { if (txt !== wanted) { if (txt === deprecatedWanted) { - sr.warning(self, 'deprecated'); + context.warning(self, 'deprecated'); } else { - sr.error(self, 'no-sotd-intro'); + context.error(self, 'no-sotd-intro'); } } const $a = $em.find("a[href='https://www.w3.org/TR/']"); - if (!$a.length) sr.error(self, 'no-sotd-tr'); + if (!$a.length) context.error(self, 'no-sotd-tr'); } }; diff --git a/lib/rules/sotd/usage.ts b/lib/rules/sotd/usage.ts index 93bd4e4cf..84f264001 100644 --- a/lib/rules/sotd/usage.ts +++ b/lib/rules/sotd/usage.ts @@ -10,8 +10,8 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = sr => { - const $sotd = sr.getSotDSection(); +export const check: RuleCheckFunction = context => { + const $sotd = context.getSotDSection(); if ($sotd) { // Find the sentence of 'W3C recommends the wide usage of this registry.' @@ -19,7 +19,7 @@ export const check: RuleCheckFunction = sr => { const paragraph = $sotd .find('p') .toArray() - .find(p => sr.norm(sr.$(p).text()) === usageText); - if (!paragraph) sr.error(self, 'not-found'); + .find(p => context.norm(context.$(p).text()) === usageText); + if (!paragraph) context.error(self, 'not-found'); } }; diff --git a/lib/rules/structure/canonical.ts b/lib/rules/structure/canonical.ts index af4fd8257..d6c8d3f46 100644 --- a/lib/rules/structure/canonical.ts +++ b/lib/rules/structure/canonical.ts @@ -8,15 +8,16 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = sr => { +export const check: RuleCheckFunction = context => { const checkCanonical = function () { - const $lnk = sr.$('head > link[rel=canonical]').first(); - if (!$lnk.length || !$lnk.attr('href')) sr.error(self, 'not-found'); + const $lnk = context.$('head > link[rel=canonical]').first(); + if (!$lnk.length || !$lnk.attr('href')) + context.error(self, 'not-found'); }; // That canonical link is mandatory starting from Oct 1, 2017. // See https://lists.w3.org/Archives/Public/spec-prod/2017JulSep/0005.html - sr.transition({ + context.transition({ to: new Date('2017-09-30'), doMeanwhile: () => {}, doAfter: checkCanonical, diff --git a/lib/rules/structure/display-only.ts b/lib/rules/structure/display-only.ts index 3190466e3..d9d8443aa 100644 --- a/lib/rules/structure/display-only.ts +++ b/lib/rules/structure/display-only.ts @@ -2,21 +2,21 @@ import type { RuleCheckFunction } from '../../types.js'; export const name = 'structure.display-only'; -export const check: RuleCheckFunction = sr => { - if (sr.config!.status !== 'DISC') - sr.info( +export const check: RuleCheckFunction = context => { + if (context.config!.status !== 'DISC') + context.info( { name, section: 'document-status', rule: 'customParagraph' }, 'customised-paragraph' ); - sr.info( + context.info( { name, section: 'document-status', rule: 'knownDisclosureNumber' }, 'known-disclosures' ); - sr.info({ name }, 'normative-representation'); - sr.info({ name }, 'visual-style'); - sr.info({ name }, 'alt-representation'); - sr.info({ name }, 'special-box-markup'); - sr.info({ name }, 'index-list-tables'); - sr.info({ name }, 'fit-in-a4'); + context.info({ name }, 'normative-representation'); + context.info({ name }, 'visual-style'); + context.info({ name }, 'alt-representation'); + context.info({ name }, 'special-box-markup'); + context.info({ name }, 'index-list-tables'); + context.info({ name }, 'fit-in-a4'); }; diff --git a/lib/rules/structure/h2.ts b/lib/rules/structure/h2.ts index c8844ea08..1b7ef8bf4 100644 --- a/lib/rules/structure/h2.ts +++ b/lib/rules/structure/h2.ts @@ -18,17 +18,19 @@ const toc = { rule: 'toc', }; -export const check: RuleCheckFunction = sr => { +export const check: RuleCheckFunction = context => { const h2s: string[] = []; - sr.$('h2').each((_, h2) => { - const $h2 = sr.$(h2); - if ($h2.parents('.head').length === 0) h2s.push(sr.norm($h2.text())); + context.$('h2').each((_, h2) => { + const $h2 = context.$(h2); + if ($h2.parents('.head').length === 0) + h2s.push(context.norm($h2.text())); }); - if (h2s[0] !== 'Abstract') sr.error(abstract, 'abstract', { was: h2s[0] }); + if (h2s[0] !== 'Abstract') + context.error(abstract, 'abstract', { was: h2s[0] }); // cspell:disable-next-line if (!/^Status [Oo]f [Tt]his [Dd]ocument$/.test(h2s[1])) - sr.error(sotd, 'sotd', { was: h2s[1] }); + context.error(sotd, 'sotd', { was: h2s[1] }); // cspell:disable-next-line if (!/^Table [Oo]f [Cc]ontents$/.test(h2s[2])) - sr.error(toc, 'toc', { was: h2s[2] }); + context.error(toc, 'toc', { was: h2s[2] }); }; diff --git a/lib/rules/structure/name.ts b/lib/rules/structure/name.ts index dff188203..b17da5d2c 100644 --- a/lib/rules/structure/name.ts +++ b/lib/rules/structure/name.ts @@ -10,7 +10,7 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = async sr => { +export const check: RuleCheckFunction = async context => { // Pseudo-constants: const EXPECTED_NAME = /\/Overview\.html$/; const OVERVIEW = 'Overview.html'; @@ -19,29 +19,29 @@ export const check: RuleCheckFunction = async sr => { let fileName; - if (!sr || !sr.url || EXPECTED_NAME.test(sr.url)) { + if (!context || !context.url || EXPECTED_NAME.test(context.url)) { return; } - if (!ALTERNATIVE_ENDING.test(sr.url)) { - fileName = sr.url.match(FILE_NAME); + if (!ALTERNATIVE_ENDING.test(context.url)) { + fileName = context.url.match(FILE_NAME); if (fileName && fileName.length === 1) { fileName = fileName[0]; - sr.warning(self, 'wrong', { + context.warning(self, 'wrong', { note: ` (instead of ${fileName})`, }); } else { - sr.warning(self, 'wrong', { note: '' }); + context.warning(self, 'wrong', { note: '' }); } return; } try { - const result1 = await superagent.get(sr.url); - const result2 = await superagent.get(sr.url + OVERVIEW); + const result1 = await superagent.get(context.url); + const result2 = await superagent.get(context.url + OVERVIEW); if (!result1.ok || !result2.ok || result1.text !== result2.text) - sr.warning(self, 'wrong', { note: '' }); + context.warning(self, 'wrong', { note: '' }); } catch (error) { - sr.warning(self, 'wrong', { note: '' }); + context.warning(self, 'wrong', { note: '' }); } }; diff --git a/lib/rules/structure/neutral.ts b/lib/rules/structure/neutral.ts index db92c69de..0c2358e8c 100644 --- a/lib/rules/structure/neutral.ts +++ b/lib/rules/structure/neutral.ts @@ -17,14 +17,14 @@ for (const item of badterms) { export const { name } = self; -export const check: RuleCheckFunction = sr => { +export const check: RuleCheckFunction = context => { const blocklistReg = new RegExp(`\\b${blocklist.join('\\b|\\b')}\\b`, 'ig'); const unneutralList: string[] = []; // Use a cloned body instead of the original one, prevent '.remove()' side effects. - const $body = sr.$('body').first().clone(); + const $body = context.$('body').first().clone(); const $links = $body.find('a'); $links.each((_, link) => { - const $link = sr.$(link); + const $link = context.$(link); const href = $link.attr('href'); const linkText = $link.text(); // let words in link like: https://github.com/master/usage --> pass the check @@ -41,5 +41,5 @@ export const check: RuleCheckFunction = sr => { } }); if (unneutralList.length) - sr.warning(self, 'neutral', { words: unneutralList.join('", "') }); + context.warning(self, 'neutral', { words: unneutralList.join('", "') }); }; diff --git a/lib/rules/structure/section-ids.ts b/lib/rules/structure/section-ids.ts index 34b8ede7f..9365aecca 100644 --- a/lib/rules/structure/section-ids.ts +++ b/lib/rules/structure/section-ids.ts @@ -8,17 +8,17 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = sr => { - const $ignoreH3 = sr.$('.head > h3').first(); +export const check: RuleCheckFunction = context => { + const $ignoreH3 = context.$('.head > h3').first(); - sr.$('h2, h3, h4, h5, h6').each((_, el) => { - const $el = sr.$(el); + context.$('h2, h3, h4, h5, h6').each((_, el) => { + const $el = context.$(el); // has an id if ($el.attr('id') || el === $ignoreH3[0]) return; // has no element previous sibling, has parent div or section, and that has an id // without prevAll that sucks... get children of parent and find self - const $parent = sr.$(el).parent(); + const $parent = context.$(el).parent(); const $sibs = $parent.children(); if ( $sibs[0] === el && @@ -38,9 +38,9 @@ export const check: RuleCheckFunction = sr => { } // this is the status h2 - const $stateEl = sr.getDocumentStateElement(); + const $stateEl = context.getDocumentStateElement(); if ($stateEl && el === $stateEl[0]) return; - sr.error(self, 'no-id', { text: el.name }); + context.error(self, 'no-id', { text: el.name }); }); }; diff --git a/lib/rules/structure/security-privacy.ts b/lib/rules/structure/security-privacy.ts index 6e985d4ff..48d4c1900 100644 --- a/lib/rules/structure/security-privacy.ts +++ b/lib/rules/structure/security-privacy.ts @@ -8,12 +8,12 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = sr => { +export const check: RuleCheckFunction = context => { let security = false; let privacy = false; - sr.$('h2, h3, h4, h5, h6').each((_, el) => { - const text = sr.norm(sr.$(el).text()).toLowerCase(); + context.$('h2, h3, h4, h5, h6').each((_, el) => { + const text = context.norm(context.$(el).text()).toLowerCase(); if (text.includes('security')) { security = true; @@ -24,10 +24,10 @@ export const check: RuleCheckFunction = sr => { }); if (!security && !privacy) { - sr.warning(self, 'no-security-privacy'); + context.warning(self, 'no-security-privacy'); } else { - if (!security) sr.warning(self, 'no-security'); + if (!security) context.warning(self, 'no-security'); - if (!privacy) sr.warning(self, 'no-privacy'); + if (!privacy) context.warning(self, 'no-privacy'); } }; diff --git a/lib/rules/style/back-to-top.ts b/lib/rules/style/back-to-top.ts index 1998c2b76..ac2e04f0c 100644 --- a/lib/rules/style/back-to-top.ts +++ b/lib/rules/style/back-to-top.ts @@ -10,10 +10,10 @@ const self = { export const { name } = self; -export const check: RuleCheckFunction = sr => { - const $candidates = sr.$( +export const check: RuleCheckFunction = context => { + const $candidates = context.$( "body p#back-to-top[role='navigation'] a[href='#title']" ); - if ($candidates.length !== 1) sr.warning(self, 'not-found'); + if ($candidates.length !== 1) context.warning(self, 'not-found'); }; diff --git a/lib/rules/style/body-toc-sidebar.ts b/lib/rules/style/body-toc-sidebar.ts index da173f230..f2a2857d9 100644 --- a/lib/rules/style/body-toc-sidebar.ts +++ b/lib/rules/style/body-toc-sidebar.ts @@ -6,10 +6,11 @@ const self = { export const { name } = self; -export const check: RuleCheckFunction = sr => { +export const check: RuleCheckFunction = context => { try { - if (sr.$('body').hasClass('toc-sidebar')) sr.error(self, 'class-found'); + if (context.$('body').hasClass('toc-sidebar')) + context.error(self, 'class-found'); } catch (e) { - sr.error(self, 'selector-fail'); + context.error(self, 'selector-fail'); } }; diff --git a/lib/rules/style/meta.ts b/lib/rules/style/meta.ts index 061999236..1f5a46674 100644 --- a/lib/rules/style/meta.ts +++ b/lib/rules/style/meta.ts @@ -17,10 +17,10 @@ export const { name } = self; const width = /^device-width$/i; const shrinkToFit = /^no$/i; -export const check: RuleCheckFunction = sr => { - const $meta = sr.$("head > meta[name='viewport'][content]"); +export const check: RuleCheckFunction = context => { + const $meta = context.$("head > meta[name='viewport'][content]"); if ($meta.length !== 1) { - sr.error(self, 'not-found'); + context.error(self, 'not-found'); } else { const props = mvp.parseMetaViewPortContent( $meta.first().attr('content') @@ -37,7 +37,7 @@ export const check: RuleCheckFunction = sr => { !shrinkToFit.test(props.validProperties['shrink-to-fit']) || Object.keys(props.unknownProperties).length !== 0 ) { - sr.error(self, 'not-found'); + context.error(self, 'not-found'); } } }; diff --git a/lib/rules/style/script.ts b/lib/rules/style/script.ts index fd3d4ccaa..c664953a3 100644 --- a/lib/rules/style/script.ts +++ b/lib/rules/style/script.ts @@ -10,16 +10,16 @@ const self = { export const { name } = self; -export const check: RuleCheckFunction = sr => { +export const check: RuleCheckFunction = context => { const PATTERN_SCRIPT = /^(https?:)?\/\/(www\.)?w3\.org\/scripts\/tr\/2021\/fixup\.js$/i; - const $candidates = sr.$('script[src]'); + const $candidates = context.$('script[src]'); let found = 0; $candidates.each((_, el) => { if (PATTERN_SCRIPT.test(el.attribs.src)) found++; }); - if (found !== 1) sr.error(self, 'not-found'); + if (found !== 1) context.error(self, 'not-found'); }; diff --git a/lib/rules/style/sheet.ts b/lib/rules/style/sheet.ts index b80ea1fa6..15337b71c 100644 --- a/lib/rules/style/sheet.ts +++ b/lib/rules/style/sheet.ts @@ -13,8 +13,8 @@ const notLast = { rule: 'lastStylesheet', }; -export const check: RuleCheckFunction = sr => { - const { styleSheet } = sr.config!; +export const check: RuleCheckFunction = context => { + const { styleSheet } = context.config!; if (!styleSheet) return; const url = `https://www.w3.org/StyleSheets/TR/2021/${styleSheet}`; const dark = 'https://www.w3.org/StyleSheets/TR/2021/dark'; @@ -22,8 +22,8 @@ export const check: RuleCheckFunction = sr => { `head > link[rel=stylesheet][href='${url}']`, `head > link[rel=stylesheet][href='${url}.css']`, ]; - const $lnk = sr.$(stylesheetLinks.join(', ')).first(); - if (!$lnk.length) sr.error(missing, 'not-found', { url }); + const $lnk = context.$(stylesheetLinks.join(', ')).first(); + if (!$lnk.length) context.error(missing, 'not-found', { url }); else { const $siblings = $lnk.nextAll('link[rel=stylesheet], style'); if ( @@ -32,7 +32,7 @@ export const check: RuleCheckFunction = sr => { $siblings.eq(0).attr('href') !== dark && $siblings.eq(0).attr('href') !== `${dark}.css`) ) { - sr.error(notLast, 'last'); + context.error(notLast, 'last'); } } }; diff --git a/lib/rules/validation/html.ts b/lib/rules/validation/html.ts index 6771981d1..bd782d9a0 100644 --- a/lib/rules/validation/html.ts +++ b/lib/rules/validation/html.ts @@ -11,34 +11,34 @@ const TIMEOUT = 10000; export const { name } = self; -export const check: RuleCheckFunction = sr => { - const { htmlValidator, skipValidation } = sr.config!; +export const check: RuleCheckFunction = context => { + const { htmlValidator, skipValidation } = context.config!; const service = htmlValidator || 'https://validator.w3.org/nu/'; if (skipValidation) { - sr.warning(self, 'skipped'); + context.warning(self, 'skipped'); return; } - if (!sr.url && !sr.source) { - sr.warning(self, 'no-source'); + if (!context.url && !context.source) { + context.warning(self, 'no-source'); return; } let req; - const ua = `W3C-Pubrules/${sr.version}`; - if (sr.url) { + const ua = `W3C-Pubrules/${context.version}`; + if (context.url) { req = get(service).set('User-Agent', ua); - req.query({ doc: sr.url, out: 'json' }); + req.query({ doc: context.url, out: 'json' }); } else { req = post(service) .set('User-Agent', ua) .set('Content-Type', 'text/html') - .send(sr.source) + .send(context.source) .query({ out: 'json' }); } req.timeout(TIMEOUT); return req.then( res => { if (!res.ok) - return sr.error(self, 'failure', { status: res.status }); + return context.error(self, 'failure', { status: res.status }); const json = res.body; if (!json) throw new Error('No JSON returned from HTML validator.'); @@ -57,11 +57,11 @@ export const check: RuleCheckFunction = sr => { // "hiliteLength": 8 // } if (msg.type === 'error') { - sr.error(self, 'error', { + context.error(self, 'error', { line: msg.lastLine, column: msg.lastColumn, message: msg.message, - link: `${service}?doc=${sr.url}`, + link: `${service}?doc=${context.url}`, }); } // { @@ -72,9 +72,9 @@ export const check: RuleCheckFunction = sr => { // } else if (msg.type === 'info') { if (msg.subtype === 'warning') { - sr.warning(self, 'warning', { + context.warning(self, 'warning', { message: msg.message, - link: `${service}?doc=${sr.url}`, + link: `${service}?doc=${context.url}`, }); } } @@ -84,7 +84,7 @@ export const check: RuleCheckFunction = sr => { // "message":"HTTP resource not retrievable. The HTTP status from the remote server was: 404." // } else if (msg.type === 'non-document-error') { - sr.error(self, 'non-document-error', { + context.error(self, 'non-document-error', { subType: msg.subType, message: msg.message, }); @@ -93,8 +93,8 @@ export const check: RuleCheckFunction = sr => { } }, (err: ResponseError) => { - if (err.timeout) sr.warning(self, 'timeout'); - else sr.error(self, 'no-response'); + if (err.timeout) context.warning(self, 'timeout'); + else context.error(self, 'no-response'); } ); }; diff --git a/lib/rules/validation/wcag.ts b/lib/rules/validation/wcag.ts index 9d82ecdb4..07e8e1dd0 100644 --- a/lib/rules/validation/wcag.ts +++ b/lib/rules/validation/wcag.ts @@ -8,6 +8,6 @@ const self: RuleMeta = { export const { name } = self; -export const check: RuleCheckFunction = sr => { - sr.info(self, 'tools'); +export const check: RuleCheckFunction = context => { + context.info(self, 'tools'); }; diff --git a/lib/specberus.ts b/lib/specberus.ts new file mode 100644 index 000000000..564e9b7fb --- /dev/null +++ b/lib/specberus.ts @@ -0,0 +1,232 @@ +/** + * @file Main file of the Specberus npm package. + */ + +import EventEmitter from 'events'; +import { access, constants, readFile } from 'fs/promises'; + +import { load } from 'cheerio'; +// @ts-ignore (no typings) +import w3cApi from 'node-w3capi'; + +import { setLanguage } from './l10n.js'; +import * as profileMetadata from './profiles/metadata.js'; +import * as profileAdditionalMetadata from './profiles/additionalMetadata.js'; +import { get } from './throttled-ua.js'; +import { processParams, specberusVersion } from './util.js'; +import type { + HandlerMessage, + ProfileModule, + RuleBase, + RuleMeta, +} from './types.js'; +import { RuleContext } from './rule-context.js'; + +setLanguage('en_GB'); + +interface BaseOptions { + file?: string; + source?: string; + url?: string; +} + +interface ExtractMetadataOptions extends BaseOptions { + additionalMetadata?: boolean; +} + +export interface ValidateOptions extends BaseOptions { + profile: ProfileModule; + validation?: 'no-validation' | 'recursive'; +} + +interface ExceptionsErrorOptions extends ErrorOptions { + exceptions: string[]; +} + +export interface SpecberusResult { + errors: HandlerMessage[]; + info: HandlerMessage[]; + metadata: Record; + success: boolean; + warnings: HandlerMessage[]; +} + +/** + * Error which includes list of exception messages, + * thrown in case of unexpected errors during extractMetadata or validate + */ +export class ExceptionsError extends Error { + exceptions: string[]; + + constructor(message?: string, options?: ExceptionsErrorOptions) { + super(message, options); + this.exceptions = options?.exceptions || []; + } +} + +type SpecberusMessageEventArgs = [ + RuleMeta | RuleBase, + { + detailMessage: string; + extra?: Record; + key: string; + }, +]; + +interface SpecberusEvents { + done: [string]; + err: SpecberusMessageEventArgs; + exception: [{ message: string }]; + info: SpecberusMessageEventArgs; + warning: SpecberusMessageEventArgs; +} + +export class Specberus extends EventEmitter { + source: string | undefined; + url: string | undefined; + version = specberusVersion; + + /** Stores messages from any unexpected errors encountered during process */ + #exceptions: string[] = []; + + /** + * Internal function for handling common end-state logic for extractMetadata and validate, + * returning results (resolving) or throwing an error if exceptions occurred (rejecting). + */ + #reportResult(result: Omit): SpecberusResult { + if (this.#exceptions.length) { + throw new ExceptionsError( + 'The following unexpected errors occurred:\n' + + this.#exceptions.join('\n'), + { exceptions: this.#exceptions } + ); + } + return { + ...result, + success: !result.errors.length, + }; + } + + /** Internal function containing setup logic common to both extractMetadata and validate. */ + async #prepare(options: ExtractMetadataOptions | ValidateOptions) { + const errors: HandlerMessage[] = []; + const warnings: HandlerMessage[] = []; + const info: HandlerMessage[] = []; + + this.on('err', (rule, data) => { + errors.push({ ...rule, ...data }); + }); + this.on('warning', (rule, data) => { + warnings.push({ ...rule, ...data }); + }); + this.on('info', (rule, data) => { + info.push({ ...rule, ...data }); + }); + + try { + const $ = await this.#load(options); + return { $, errors, info, warnings }; + } catch (error) { + this.#throw(error.toString()); + throw error; + } + } + + async extractMetadata(options: ExtractMetadataOptions) { + const { $, ...messages } = await this.#prepare(options); + const metadata: Record = {}; + const profile = options.additionalMetadata + ? profileAdditionalMetadata + : profileMetadata; + const ruleContext = new RuleContext(this, $); + + await Promise.all( + profile.rules.map(async rule => { + try { + const result = await rule.check(ruleContext); + if (result) + for (const [key, value] of Object.entries(result)) + metadata[key] = value; + } catch (error) { + this.#throw(error.message); + } finally { + this.emit('done', rule.name); + } + }) + ); + return this.#reportResult({ ...messages, metadata }); + } + + async validate(options: ValidateOptions) { + if (!options.profile) + throw new Error('Without a profile there is nothing to check.'); + + const { profile } = options; + const config = await processParams(options, profile.config); + const { $, ...messages } = await this.#prepare(options); + const ruleContext = new RuleContext(this, $, config); + + await Promise.all( + profile.rules.map(async rule => { + try { + await rule.check(ruleContext); + } catch (error) { + this.#throw(error.message); + } finally { + this.emit('done', rule.name); + } + }) + ); + return this.#reportResult({ ...messages, metadata: {} }); + } + + /** + * Emits an exception event, intended to signify that the process stopped on a critical error. + * + * NOTE: This should not be called from rules; they should throw an Error, + * which will result in extractMetadata or validate invoking this method. + */ + #throw(message: string) { + console.error(`[EXCEPTION] ${message}`); + this.emit('exception', { message }); + // Track in exceptions array, used to determine whether to resolve or reject process + this.#exceptions.push(message); + } + + #load(options: ExtractMetadataOptions | ValidateOptions) { + if (options.url) return this.#loadURL(options.url); + if (options.source) return this.#loadSource(options.source); + if (options.file) return this.#loadFile(options.file); + throw new Error('url, source, or file must be specified.'); + } + + #loadURL(url: string) { + return get(url) + .set('User-Agent', `W3C-Pubrules/${specberusVersion}`) + .then(res => { + if (!res.text) throw new Error(`Body of ${url} is empty.`); + this.url = url; + return this.#loadSource(res.text); + }); + } + + #loadSource(src: string) { + this.source = src; + try { + return load(src); + } catch (e) { + throw new Error( + `Cheerio failed to parse source: ${JSON.stringify(e)}` + ); + } + } + + async #loadFile(file: string) { + try { + await access(file, constants.F_OK); + } catch (error) { + throw new Error(`File '${file}' not found or inaccessible.`); + } + return this.#loadSource(await readFile(file, 'utf8')); + } +} diff --git a/lib/types.d.ts b/lib/types.d.ts index 73bed261b..9bb8ee7c7 100644 --- a/lib/types.d.ts +++ b/lib/types.d.ts @@ -1,4 +1,5 @@ -import type { Specberus, ValidateOptions } from './validator.js'; +import type { ValidateOptions } from './specberus.js'; +import type { RuleContext } from './rule-context.js'; type Status = | 'FPWD' @@ -64,7 +65,9 @@ export interface RecMetadata { rectrack?: string | null; } -export type RuleCheckFunction = (sr: Specberus) => R | Promise; +export type RuleCheckFunction = ( + context: RuleContext +) => R | Promise; export interface RuleBase { name: string; diff --git a/lib/util.ts b/lib/util.ts index 1c61adf1e..1cbd07264 100644 --- a/lib/util.ts +++ b/lib/util.ts @@ -12,7 +12,7 @@ import { Octokit } from '@octokit/core'; import w3cApi from 'node-w3capi'; import type { SpecberusConfig } from './types.js'; -import type { ValidateOptions } from './validator.js'; +import type { ValidateOptions } from './specberus.js'; import pkg from '../package.json' with { type: 'json' }; /** Current specberus version recorded in package.json */ diff --git a/package.json b/package.json index dcf8ced3c..84efdf0ee 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "12.3.1", "description": "Specberus is a checker used at W3C to validate the compliance of Technical Reports with publication rules.", "license": "MIT", - "main": "lib/validator", + "main": "lib/specberus.js", "type": "module", "repository": { "type": "git", diff --git a/test/api.ts b/test/api.ts index 849edca02..0373af77e 100644 --- a/test/api.ts +++ b/test/api.ts @@ -15,7 +15,7 @@ import superagent, { type Response, type ResponseError } from 'superagent'; import { setUp } from '../lib/api.js'; import { specberusVersion } from '../lib/util.js'; -import type { SpecberusResult } from '../lib/validator.js'; +import type { SpecberusResult } from '../lib/specberus.js'; import { cleanupMocks, setupMocks } from './lib/utils.js'; // Settings: diff --git a/test/l10n.ts b/test/l10n.ts index 3fcdaeb59..85976f70b 100644 --- a/test/l10n.ts +++ b/test/l10n.ts @@ -103,8 +103,8 @@ const scanStrings = function () { /** * Scans “baseDir” and finds heuristically all sections, rules, and message IDs. * - * The search relies on a regex that tries to find instances of “sr.error()” etc, so it's not exact. - * Return a promise that will be fulfilled if/when all directories and files are read successfully. + * The search relies on a regex that tries to find instances of “context.error()” etc, so it's not exact. + * Returns a promise that will be fulfilled if/when all directories and files are read successfully. */ async function scanFileSystem() { const result: Record>> = {}; diff --git a/test/rules.ts b/test/rules.ts index f612a5ca4..d564ddaf5 100644 --- a/test/rules.ts +++ b/test/rules.ts @@ -11,7 +11,7 @@ import { ExceptionsError, Specberus, type SpecberusResult, -} from '../lib/validator.js'; +} from '../lib/specberus.js'; // A list of good documents to be tested, using all rules configured in the profiles. // Shouldn't cause any error. import { goodDocuments } from './data/goodDocuments.js'; From 5015b00f470a720a14f171b77582a863227efe23 Mon Sep 17 00:00:00 2001 From: "Kenneth G. Franqueiro" Date: Thu, 25 Jun 2026 15:08:50 -0400 Subject: [PATCH 10/11] API: Respond with 400 status on invalid requests (#2130) --- lib/api.ts | 9 +++------ test/api.ts | 12 ++++++++++++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/lib/api.ts b/lib/api.ts index 7ba139903..ed33ea22c 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -70,14 +70,12 @@ const processPost = () => async (req: Request, res: Response) => { if (path === '/api/metadata' || path === '/api/validate') { if (!req.files || !req.files.file) { - return res.send({ - status: 400, + return res.status(400).send({ message: 'Missing file.', }); } if (Array.isArray(req.files.file)) { - return res.send({ - status: 400, + return res.status(400).send({ message: 'Expected a single file.', }); } @@ -87,8 +85,7 @@ const processPost = () => async (req: Request, res: Response) => { // file must be an html file const type = await fileTypeFromFile(tempFilePath); if (type != null) { - return res.send({ - status: 500, + return res.status(400).send({ message: 'Invalid file type. Please send an HTML file.', }); } diff --git a/test/api.ts b/test/api.ts index 0373af77e..71999a8a2 100644 --- a/test/api.ts +++ b/test/api.ts @@ -107,6 +107,12 @@ describe('API', () => { }); describe('Method “metadata”', () => { + it('Should respond with 400 status if "file" is not specified via POST', () => + assert.rejects(createPostRequest('metadata'), (error: any) => { + assertResponseStatus(error.response, 400); + return true; + })); + it('Should accept "file" via POST, and return the right profile and date', () => createPostRequest('metadata') .attach('file', join(testDocsPath, 'ttml-imsc1.html')) @@ -145,6 +151,12 @@ describe('API', () => { .reply(200, await readFile(imscPath, 'utf8')); }); + it('Should respond with 400 status if "file" is not specified via POST', () => + assert.rejects(createPostRequest('validate'), (error: any) => { + assertResponseStatus(error.response, 400); + return true; + })); + it('Should 400 and return an array of errors when validation fails', () => assert.rejects( createPostRequest('validate') From 4677b266f96a1ec6f07bc5bc4b3b05f8d0b02a7b Mon Sep 17 00:00:00 2001 From: "Kenneth G. Franqueiro" Date: Mon, 29 Jun 2026 14:52:26 -0400 Subject: [PATCH 11/11] 13.0.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a2e443a2a..733bc2d9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "specberus", - "version": "12.3.1", + "version": "13.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "specberus", - "version": "12.3.1", + "version": "13.0.0", "license": "MIT", "dependencies": { "@octokit/core": "^7.0.6", diff --git a/package.json b/package.json index 84efdf0ee..9d5c4c078 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "specberus", - "version": "12.3.1", + "version": "13.0.0", "description": "Specberus is a checker used at W3C to validate the compliance of Technical Reports with publication rules.", "license": "MIT", "main": "lib/specberus.js",