) {
- 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
+
+
+
+
+
+ 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 .
+
+
+
+
+ Table of Contents
+
+ 1. Introduction
+ 1.1 Examples
+
+
+
+ 2. Time Origin
+
+
+
+
+
+
+ ↑
+
+
+
+
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
+
+
+
+
+
+ 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 .
+
+
+
+
+ Table of Contents
+
+ 1. Introduction
+ 1.1 Examples
+
+
+
+ 2. Time Origin
+
+
+
+
+
+
+ ↑
+
+
+
+
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",