diff --git a/.c8rc b/.c8rc index bcde9c053..ef11cef38 100644 --- a/.c8rc +++ b/.c8rc @@ -1,5 +1,6 @@ { "all": true, - "exclude": ["public/*", "test/*", "tools/*"], + "exclude": ["public/*", "test/*", "**/*.d.ts"], + "extension": [".ts"], "reporter": ["html"] } diff --git a/.cspell.json b/.cspell.json index 78d49d940..7ab442e91 100644 --- a/.cspell.json +++ b/.cspell.json @@ -4,6 +4,7 @@ "ˈspɛk", "apikey", "badterms", + "basenames", "Beihang", "blocklist", "capi", @@ -17,7 +18,6 @@ "deniak", "discr", "DNOTE", - "doasync", "doctypes", "domhandler", "dvcs", @@ -108,26 +108,20 @@ "**/*.ttf", "**/*.woff", "**/*.svg", - ".nyc_output/**", ".github/**", "coverage/**", "test/**/*.html", "package-lock.json", "tsconfig.json", + "{lib,test}/**/*.{d.ts,js}", "node_modules/**", "design/**" ], - "ignoreRegExpList": ["/require\\(.*\\);/"], + "ignoreRegExpList": ["(FIXME|TODO|XXX)\\(.[^\\)]+\\)"], "overrides": [ { "filename": ["package.json"], - "words": [ - "capi", - "doasync", - "metaviewport", - "nodesecurity", - "vulns" - ] + "words": ["capi", "metaviewport", "nodesecurity", "vulns"] } ], "ignoreWords": ["en", "gb"] 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 15bc2eb29..f51d3fce9 100644 --- a/.gitignore +++ b/.gitignore @@ -25,7 +25,10 @@ scratch # mac files .DS_Store */.DS_Store -.vscode/ -.nyc_output -.eslintcache +# TS build output +app.js +*.d.ts +!lib/types.d.ts +lib/**/*.js +test/**/*.js diff --git a/.husky/pre-commit b/.husky/pre-commit index 928d1a896..3c2f93768 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1,4 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -npx lint-staged && npx tsc && npm run spelling +npx lint-staged && npx tsc --noEmit && npm run spelling diff --git a/.prettierignore b/.prettierignore index 6d20308e3..36dc9de2a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,2 @@ *.html *.handlebars -.nyc_output diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..6b21fe3f9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "js/ts.tsdk.path": "node_modules/typescript/lib" +} diff --git a/README.md b/README.md index bab665c8a..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 @@ -51,6 +50,20 @@ $ npm start $ npm start 3001 ``` +**Note:** `npm start` relies on JavaScript files, which must first be built via `npm run build`. +This step is not necessary when running the live development server or tests (see below). + +#### Auto reload when developing + +Run `npm run live [PORT]` when developing. The app will automatically reload when changes happen. + +Examples: + +```bash +$ npm run live +$ npm run live 3001 +``` + ### Environment variables #### `DEBUG` @@ -85,50 +98,27 @@ unauthenticated requests to 60 per hour. GH_TOKEN=github_pat_... npm start ``` -### Auto reload when developing - -Run `npm run live` when developing. The app will automatically reload when changes happen. - -```bash -$ npm run live - -$ npm run live 3001 -``` - ## 3. Testing -#### 1. Simple test - -Testing is done using mocha. Simply run: +### 1. Simple test -```bash -$ mocha -``` - -from the root and you will be running the test suite. Mocha can be installed with: +Run: ```bash -$ npm install -g mocha +$ npm test ``` -#### 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 -``` +from the root to run the test suite. -#### 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. @@ -156,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'; @@ -172,26 +162,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. + +`options` is an object accepting the following fields: -- `url`: URL of the content to check. One of `url`, `source`, `file`, or `document` must be +- `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()`, except that a `profile` is not necessary and will be ignored (finding out the profile is one of the -goals of this method). +The `options` accepted are equal to those in `validate()`, with the following differences: -`this.meta` will be an `Object` and may include up to 20 properties described below: +- Optional `additionalMetadata` property, which performs additional checks (e.g. errata URL) +- No `profile` or `validation` properties (this method can be used to _determine_ profile) + +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 @@ -216,7 +206,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 { @@ -238,8 +228,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`. @@ -411,47 +401,63 @@ 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 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 context object includes the following APIs useful for validation: -The Specberus object exposes the following API that's 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 -- `error`, `warn`, `info`. Methods for firing respective levels of events to the instance's sink. +- `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, 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.js b/app.ts similarity index 72% rename from app.js rename to app.ts index ebce6d4bc..6d05f1237 100644 --- a/app.js +++ b/app.ts @@ -2,24 +2,26 @@ * Main runnable file. */ +// @ts-ignore (no typings) 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, importJSON } from './lib/util.js'; -import { Specberus } from './lib/validator.js'; +import { allProfiles, specberusVersion } from './lib/util.js'; +import { ExceptionsError, Specberus } from './lib/specberus.js'; import * as views from './lib/views.js'; - -const { version } = importJSON('./package.json', import.meta.url); +import type { ProfileModule } from './lib/types.js'; // Settings: const DEFAULT_PORT = 80; @@ -36,7 +38,6 @@ const io = new Server(server); // Middleware: app.use(morgan('combined')); app.use(compression()); -app.use('/badterms.json', cors()); app.use( fileUpload({ createParentPath: true, @@ -46,6 +47,9 @@ app.use( ); app.use(express.static('public')); +app.get('/badterms.json', cors(), (_, res) => { + res.json(badterms); +}); api.setUp(app); views.setUp(app); @@ -54,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 }); - socket.on('extractMetadata', data => { + socket.emit('handshake', { version: specberusVersion }); + 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', @@ -73,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', @@ -83,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', @@ -93,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) @@ -113,23 +131,24 @@ io.on('connection', socket => { message: 'Profile not provided.', }); const profilePath = allProfiles.find(p => - p.endsWith(`/${data.profile}.js`) + p.endsWith(`/${data.profile}`) ); let profile; try { - profile = await import(`./lib/profiles/${profilePath}`); + profile = (await import( + `./lib/profiles/${profilePath}.js` + )) as ProfileModule; } catch (err) { return socket.emit('exception', { message: `Failed to get profile ${profilePath}.`, }); } 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', @@ -139,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', @@ -149,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', @@ -159,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); }); @@ -178,19 +194,17 @@ io.on('connection', socket => { url: data.url, statusCodesAccepted: ['301', '406'], }) - .then(res => { + .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 { @@ -200,24 +214,23 @@ io.on('connection', socket => { socket.emit('exception', { message }); } }) - .catch(e => { + .catch((error: any) => { socket.emit('exception', { - message: `Insafe check blew up: ${e}`, + message: `Insafe check blew up: ${error}`, }); socket.emit('finished'); }); } 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/eslint.config.cjs b/eslint.config.cjs deleted file mode 100644 index 0642a565e..000000000 --- a/eslint.config.cjs +++ /dev/null @@ -1,55 +0,0 @@ -[ - { - extends: ['airbnb-base', 'prettier'], - plugins: ['prettier'], - rules: { - 'prettier/prettier': 'error', - 'no-shadow': 'off', - 'consistent-return': 'warn', - 'no-param-reassign': 'off', - strict: 'off', - 'global-require': 'off', - 'no-restricted-syntax': 'warn', - 'guard-for-in': 'warn', - 'prefer-destructuring': 'warn', - 'import/prefer-default-export': 'off', - 'import/extensions': 'off', - }, - ignores: ['doc/api'], - overrides: [ - { - files: ['public/js/*.js'], - env: { - browser: true, - jquery: true, - es6: true, - }, - rules: {}, - }, - { - files: ['app.js', 'lib/**/*.js', 'tools/**/*.js'], - plugins: ['node'], - parserOptions: { - ecmaVersion: 2020, - sourceType: 'module', - }, - extends: ['plugin:node/recommended'], - }, - { - files: ['test/**/*.js'], - env: { - mocha: true, - }, - rules: { - 'node/no-unpublished-require': 'off', - }, - plugins: ['node'], - extends: 'plugin:node/recommended', - parserOptions: { - ecmaVersion: 2022, - sourceType: 'module', - }, - }, - ], - }, -]; diff --git a/lib/api.js b/lib/api.js deleted file mode 100644 index 430ce3d7b..000000000 --- a/lib/api.js +++ /dev/null @@ -1,217 +0,0 @@ -/** - * REST API. - */ - -import { fileTypeFromFile } from 'file-type'; -import { Sink } from './sink.js'; -import { buildJSONresult, importJSON, processParams } from './util.js'; -import { Specberus } from './validator.js'; - -/** @import {Response} from "express" */ - -const { version } = importJSON('../package.json', import.meta.url); - -/** - * Send the JSON result to the client. - * - * @param {string[]} err - errors. - * @param {string[]} warn - warnings. - * @param {string[]} inf - informative messages. - * @param {Response} res - Express HTTP response. - * @param {Object} metadata - dictionary with some found metadata. - */ - -const sendJSONresult = function (err, warn, inf, res, metadata) { - delete metadata.file; - const wrapper = buildJSONresult(err, warn, inf, metadata); - res.status(wrapper.success ? 200 : 400).json(wrapper); -}; - -/** - * Handle an API request: parse method and parameters, handle common errors and call the validator. - */ -const processGet = () => async (req, res) => { - const path = req._parsedUrl.pathname; - if (path === '/api/version') { - res.status(200).send(version); - } else if (path === '/api/metadata' || path === '/api/validate') { - await processRequest(req, res, req.query); - } else { - res.status(400).send('Wrong API endpoint.'); - } -}; - -const processPost = () => async (req, res) => { - const path = req._parsedUrl.pathname; - - if (path === '/api/metadata' || path === '/api/validate') { - if (!req.files || !req.files.file) { - return res.send({ - status: 500, - message: 'Missing file.', - }); - } - try { - const { tempFilePath } = req.files.file; - - // file must be an html file - const type = await fileTypeFromFile(tempFilePath); - if (type != null) { - return res.send({ - status: 500, - message: 'Invalid file type. Please send an HTML file.', - }); - } - const params = req.body || {}; - params.file = tempFilePath; - - await processRequest(req, res, params); - } catch (err) { - res.status(500).send(err); - } - } else { - res.status(400).send('Wrong API endpoint.'); - } -}; - -const processRequest = async (req, res, params) => { - const validate = req._parsedUrl.pathname === '/api/validate'; - let v; - let v2; - let options; - let options2; - let errors; - let errors2; - let warnings; - let warnings2; - let info; - let info2; - let handler; - let handler2; - - try { - options = await processParams(params, undefined, { - required: validate ? ['profile'] : [], - forbidden: ['document', 'source'], - }); - } catch (err) { - return sendJSONresult([err.toString()], [], [], res, {}); - } - if (validate && options.profile === 'auto') { - errors = []; - v = new Specberus(); - handler = new Sink( - (...data) => { - errors.push(Object.assign({}, ...data)); - }, - async data => { - if (errors.length > 0) sendJSONresult(errors, [], [], res, {}); - else { - const meta = data.metadata; - if (options.url) meta.url = options.url; - else meta.file = options.file; - try { - options2 = await processParams(meta, undefined, { - allowUnknownParams: true, - }); - } catch (err) { - return sendJSONresult( - [err.toString()], - [], - [], - res, - {} - ); - } - errors2 = []; - warnings2 = []; - info2 = []; - v2 = new Specberus(); - handler2 = new Sink( - (...data2) => { - errors2.push(Object.assign({}, ...data2)); - }, - () => { - sendJSONresult( - errors2, - warnings2, - info2, - res, - meta - ); - }, - (...data2) => { - warnings2.push(Object.assign({}, ...data2)); - }, - (...data2) => { - info2.push(Object.assign({}, ...data2)); - } - ); - options2.events = handler2; - handler2.on('exception', data => { - sendJSONresult( - [data.message ? data.message : data], - [], - [], - res, - {} - ); - }); - v2.validate(options2); - } - } - ); - handler.on('exception', data => { - sendJSONresult( - [data.message ? data.message : data], - [], - [], - res, - {} - ); - }); - options.events = handler; - v.extractMetadata(options); - } else { - errors = []; - warnings = []; - info = []; - v = new Specberus(); - handler = new Sink( - (...data) => { - errors.push(Object.assign({}, ...data)); - }, - data => { - sendJSONresult(errors, warnings, info, res, data.metadata); - }, - (...data) => { - warnings.push(Object.assign({}, ...data)); - }, - (...data) => { - info.push(Object.assign({}, ...data)); - } - ); - handler.on('exception', data => { - sendJSONresult( - [data.message ? data.message : data], - [], - [], - res, - {} - ); - }); - options.events = handler; - options.additionalMetadata = req.query.additionalMetadata === 'true'; - if (validate) v.validate(options); - else v.extractMetadata(options); - } -}; - -/** - * Adds middleware for handling pubrules requests. - * NOTE: This requires express-fileupload middleware to be hooked up separately! - */ -export const setUp = function (app) { - app.get(/\/api\/.*/, processGet()); - app.post(/\/api\/.*/, processPost()); -}; diff --git a/lib/api.ts b/lib/api.ts new file mode 100644 index 000000000..ed33ea22c --- /dev/null +++ b/lib/api.ts @@ -0,0 +1,183 @@ +/** + * @file REST API. + */ + +import { fileTypeFromFile } from 'file-type'; +import type { Express, Request, Response } from 'express'; + +import type { HandlerMessage } from './types.js'; +import { processParams, specberusVersion } from './util.js'; +import { + ExceptionsError, + Specberus, + type SpecberusResult, + type ValidateOptions, +} from './specberus.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); +}; + +/** + * 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) + */ +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}`); + +/** + * Handle an API request: parse method and parameters, handle common errors and call the validator. + */ +const processGet = () => async (req: Request, res: Response) => { + const path = getFullUrl(req).pathname; + if (path === '/api/version') { + res.status(200).send(specberusVersion); + } else if (path === '/api/metadata' || path === '/api/validate') { + await processRequest(req, res, req.query); + } else { + res.status(400).send('Wrong API endpoint.'); + } +}; + +const processPost = () => async (req: Request, res: Response) => { + const path = getFullUrl(req).pathname; + + if (path === '/api/metadata' || path === '/api/validate') { + if (!req.files || !req.files.file) { + return res.status(400).send({ + message: 'Missing file.', + }); + } + if (Array.isArray(req.files.file)) { + return res.status(400).send({ + message: 'Expected a single file.', + }); + } + try { + const { tempFilePath } = req.files.file; + + // file must be an html file + const type = await fileTypeFromFile(tempFilePath); + if (type != null) { + return res.status(400).send({ + message: 'Invalid file type. Please send an HTML file.', + }); + } + const params = req.body || {}; + params.file = tempFilePath; + + await processRequest(req, res, params); + } catch (err) { + res.status(500).send(err); + } + } else { + res.status(400).send('Wrong API endpoint.'); + } +}; + +function 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 ( + req: Request, + res: Response, + params: qs.ParsedQs +) => { + const shouldValidate = getFullUrl(req).pathname === '/api/validate'; + + let options; + try { + options = await processParams(params, undefined, { + required: shouldValidate ? ['profile'] : [], + forbidden: ['source'], + }); + } catch (error) { + return sendErrors(res, error); + } + + if (shouldValidate && options.profile === 'auto') { + const sr = new Specberus(); + 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.additionalMetadata = req.query.additionalMetadata === 'true'; + + const sr = new Specberus(); + return handlePromise( + shouldValidate ? sr.validate(options) : sr.extractMetadata(options), + res + ); + } +}; + +/** + * Adds middleware for handling pubrules requests. + * NOTE: This requires express-fileupload middleware to be hooked up separately! + */ +export const setUp = function (app: Express) { + app.get(/\/api\/.*/, processGet()); + app.post(/\/api\/.*/, processPost()); +}; diff --git a/lib/badterms.ts b/lib/badterms.ts new file mode 100644 index 000000000..02ca91052 --- /dev/null +++ b/lib/badterms.ts @@ -0,0 +1,44 @@ +interface BadTerm { + term: string[]; + variation?: string[]; + alternatives: string[]; +} + +export default [ + { + term: ['master'], + variation: ['masters'], + alternatives: ['main'], + }, + { + term: ['slave'], + variation: ['slaves'], + alternatives: ['replica'], + }, + { + term: ['whitelist'], + variation: ['whitelists'], + alternatives: ['allowlist'], + }, + { + term: ['blacklist'], + variation: ['blacklists'], + alternatives: ['denylist'], + }, + { + term: ['grandfather'], + alternatives: ['legacy'], + }, + { + term: ['sanity'], + alternatives: ['coherence'], + }, + { + term: ['he', 'she', 'him', 'her'], + alternatives: ['they'], + }, + { + term: ['his', 'hers'], + alternatives: ['theirs'], + }, +] satisfies BadTerm[]; diff --git a/lib/copyright-exceptions.json b/lib/copyright-exceptions.json deleted file mode 100644 index 3397dfbf9..000000000 --- a/lib/copyright-exceptions.json +++ /dev/null @@ -1,43 +0,0 @@ -[ - { - "specShortnames": [ - "epub-overview-33", - "epub-overview-34", - "epub-33", - "epub-34", - "epub-rs-33", - "epub-rs-34", - "epub-multi-rend-11", - "epub-tts-10", - "epub-ssv-11", - "epub-a11y-11", - "epub-a11y-12", - "epub-a11y-tech-11", - "epub-a11y-tech-12", - "epub-aria-authoring-11", - "epubcfi", - "epubcfi-11" - ], - "copyright": "Copyright © 1999-@YEAR International Digital Publishing Forum and World Wide Web Consortium. W3C® liability, trademark and permissive document license rules apply." - }, - { - "specShortnames": ["webrtc"], - "copyright": "Initial Author of this Specification was Ian Hickson, Google Inc., with the following copyright statement:
© Copyright 2004-2011 Apple Computer, Inc., Mozilla Foundation, and Opera Software ASA. You are granted a license to use, reproduce and create derivative works of this document. All subsequent changes since 26 July 2011 done by the W3C WebRTC Working Group are under the following Copyright:
Copyright © 2011-@YEAR World Wide Web Consortium. W3C® liability, trademark and permissive document license rules apply." - }, - { - "specShortnames": ["security-privacy-questionnaire"], - "copyright": "\"CC0\" To the extent possible under law, the editors have waived all copyright and related or neighboring rights to this work. This document is also made available under the W3C Software and Document License." - }, - { - "specShortnames": ["mediacapture-streams"], - "copyright": "Initial Author of this Specification was Ian Hickson, Google Inc., with the following copyright statement:
© Copyright 2004-2011 Apple Computer, Inc., Mozilla Foundation, and Opera Software ASA. You are granted a license to use, reproduce and create derivative works of this document." - }, - { - "specShortnames": ["sdw-bp"], - "copyright": "Copyright © @YEAR OGC & World Wide Web Consortium. W3C® liability, trademark, W3C and OGC document use rules apply." - }, - { - "specShortnames": ["html-ruby-extensions"], - "copyright": "Copyright © @YEAR World Wide Web Consortium and WHATWG (Apple, Google, Mozilla, Microsoft). W3C® liability, and trademark rules apply. This work is made available under a dual license: the Creative Commons Attribution 4.0 International License (CC BY 4.0), and the W3C Software and Document license. Portions of this work are derived from material originally published by the WHATWG under the CC BY 4.0 license; those portions remain available under that license." - } -] diff --git a/lib/copyright-exceptions.ts b/lib/copyright-exceptions.ts new file mode 100644 index 000000000..52c8ad15f --- /dev/null +++ b/lib/copyright-exceptions.ts @@ -0,0 +1,54 @@ +interface CopyrightException { + specShortnames: string[]; + copyright: string; +} + +export default [ + { + specShortnames: [ + 'epub-overview-33', + 'epub-overview-34', + 'epub-33', + 'epub-34', + 'epub-rs-33', + 'epub-rs-34', + 'epub-multi-rend-11', + 'epub-tts-10', + 'epub-ssv-11', + 'epub-a11y-11', + 'epub-a11y-12', + 'epub-a11y-tech-11', + 'epub-a11y-tech-12', + 'epub-aria-authoring-11', + 'epubcfi', + 'epubcfi-11', + ], + copyright: + 'Copyright © 1999-@YEAR International Digital Publishing Forum and World Wide Web Consortium. W3C® liability, trademark and permissive document license rules apply.', + }, + { + specShortnames: ['webrtc'], + copyright: + 'Initial Author of this Specification was Ian Hickson, Google Inc., with the following copyright statement:
© Copyright 2004-2011 Apple Computer, Inc., Mozilla Foundation, and Opera Software ASA. You are granted a license to use, reproduce and create derivative works of this document. All subsequent changes since 26 July 2011 done by the W3C WebRTC Working Group are under the following Copyright:
Copyright © 2011-@YEAR World Wide Web Consortium. W3C® liability, trademark and permissive document license rules apply.', + }, + { + specShortnames: ['security-privacy-questionnaire'], + copyright: + 'CC0 To the extent possible under law, the editors have waived all copyright and related or neighboring rights to this work. This document is also made available under the W3C Software and Document License.', + }, + { + specShortnames: ['mediacapture-streams'], + copyright: + 'Initial Author of this Specification was Ian Hickson, Google Inc., with the following copyright statement:
© Copyright 2004-2011 Apple Computer, Inc., Mozilla Foundation, and Opera Software ASA. You are granted a license to use, reproduce and create derivative works of this document.', + }, + { + specShortnames: ['sdw-bp'], + copyright: + 'Copyright © @YEAR OGC & World Wide Web Consortium. W3C® liability, trademark, W3C and OGC document use rules apply.', + }, + { + specShortnames: ['html-ruby-extensions'], + copyright: + 'Copyright © @YEAR World Wide Web Consortium and WHATWG (Apple, Google, Mozilla, Microsoft). W3C® liability, and trademark rules apply. This work is made available under a dual license: the Creative Commons Attribution 4.0 International License (CC BY 4.0), and the W3C Software and Document license. Portions of this work are derived from material originally published by the WHATWG under the CC BY 4.0 license; those portions remain available under that license.', + }, +] satisfies CopyrightException[]; diff --git a/lib/exceptions-map.ts b/lib/exceptions-map.ts new file mode 100644 index 000000000..40a7b77df --- /dev/null +++ b/lib/exceptions-map.ts @@ -0,0 +1,64 @@ +export interface Exception { + rule: string; + message?: RegExp; + type?: string; +} + +export const exceptions = new Map([ + [ + /.*/, + [ + { + rule: 'validation.html', + type: 'noexistence-at-all', + message: /^CSS: .*$/, + }, + { + rule: 'validation.html', + message: + /Bad value “publication” for attribute “rel” on element “link”: The string “publication” is not a registered keyword./, + }, + ], + ], + [ + /^audiobooks$/, + [ + { + rule: 'sotd.candidate-review-end', + }, + ], + ], + [ + /^did-core$/, + [ + { + rule: 'heuristic.date-format', + }, + ], + ], + [ + /^privacy-principles$/, + [ + { + rule: 'headers.editor-participation', + }, + ], + ], + [ + /^pub-manifest$/, + [ + { + rule: 'sotd.candidate-review-end', + }, + ], + ], + [ + /^webgpu|WGSL$/, + [ + { + rule: 'headers.dl', + message: /Implementation report/, + }, + ], + ], +]); diff --git a/lib/exceptions.js b/lib/exceptions.js deleted file mode 100644 index 24c1f69f8..000000000 --- a/lib/exceptions.js +++ /dev/null @@ -1,62 +0,0 @@ -import { importJSON } from './util.js'; - -const records = importJSON('./exceptions.json', import.meta.url); - -/** - * @param data - * @param ref - * @returns {boolean} true if the data and ref are the same, or if the ref is undefined - */ -function compareValue(data, ref) { - return ref === undefined || data === ref; -} - -/** - * @param shortname - */ -function findSet(shortname) { - let count = 0; - /** - * @param name - */ - function recursiveFindSet(name) { - if (count > 10) return undefined; - const set = []; - for (const k in records) { - const regex = new RegExp(k); - if ( - Object.prototype.hasOwnProperty.call(records, k) && - regex.test(name) - ) { - set.push(records[k]); - } - } - count += 1; - if (typeof set === 'string') return recursiveFindSet(set); - return set; - } - return recursiveFindSet(shortname); -} - -export const Exceptions = function () {}; - -Exceptions.prototype.has = function (shortname, rule, key, extra) { - const set = findSet(shortname); - if (set === undefined) return false; - for (let i = set.length - 1; i >= 0; i -= 1) { - for (let y = set[i].length - 1; y >= 0; y -= 1) { - const exception = set[i][y]; - if ( - rule === exception.rule && - (extra === undefined || - (!exception.message && - compareValue(extra.type, exception.type)) || - (exception.message && - new RegExp(exception.message).test(extra.message))) - ) { - return true; - } - } - } - return false; -}; diff --git a/lib/exceptions.json b/lib/exceptions.json deleted file mode 100644 index 2cba7e587..000000000 --- a/lib/exceptions.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - ".*": [ - { - "rule": "validation.html", - "type": "noexistence-at-all", - "message": "^CSS: .*$" - }, - { - "rule": "validation.html", - "message": "Bad value “publication” for attribute “rel” on element “link”: The string “publication” is not a registered keyword." - } - ], - "^audiobooks$": [ - { - "rule": "sotd.candidate-review-end" - } - ], - "^did-core$": [ - { - "rule": "heuristic.date-format" - } - ], - "^privacy-principles$": [ - { - "rule": "headers.editor-participation" - } - ], - "^pub-manifest$": [ - { - "rule": "sotd.candidate-review-end" - } - ], - "^webgpu|WGSL$": [ - { - "rule": "headers.dl", - "message": "Implementation report" - } - ] -} diff --git a/lib/exceptions.ts b/lib/exceptions.ts new file mode 100644 index 000000000..1dbbe4bc6 --- /dev/null +++ b/lib/exceptions.ts @@ -0,0 +1,35 @@ +import { exceptions, type Exception } from './exceptions-map.js'; + +function findSet(shortname: string) { + const set: Exception[][] = []; + for (const [regexp, applicableExceptions] of exceptions) { + if (regexp.test(shortname)) set.push(applicableExceptions); + } + return set; +} + +export function hasExceptions( + shortname: string, + rule: string, + extra?: Record +) { + const set = findSet(shortname); + if (!set) return false; + for (let i = set.length - 1; i >= 0; i--) { + for (let y = set[i].length - 1; y >= 0; y--) { + const exception = set[i][y]; + if ( + rule === exception.rule && + (!extra || + (!exception.message && + (exception.type === undefined || + extra.type === exception.type)) || + (exception.message && + exception.message.test(extra.message))) + ) { + return true; + } + } + } + return false; +} diff --git a/lib/groups-db.json b/lib/groups-db.json deleted file mode 100644 index 80ce603f2..000000000 --- a/lib/groups-db.json +++ /dev/null @@ -1,162 +0,0 @@ -{ - "public-audio@w3.org": { - "url": "https://www.w3.org/2011/audio/", - "name": "Audio Working Group" - }, - "w3c-wai-au@w3.org": { - "url": "https://www.w3.org/WAI/AU/", - "name": "Authoring Tool Accessibility Guidelines Working Group" - }, - "public-browser-tools-testing@w3.org": { - "url": "https://www.w3.org/testing/browser/", - "name": "Browser Testing and Tools Working Group" - }, - "public-csv-wg@w3.org": { - "url": "https://www.w3.org/2013/csvw/", - "name": "CSV on the Web Working Group" - }, - "public-dwbp-wg@w3.org": { - "url": "https://www.w3.org/2013/dwbp/", - "name": "Data on the Web Best Practices Working Group" - }, - "public-device-apis@w3.org": { - "url": "https://www.w3.org/2009/dap/", - "name": "Device APIs Working Group" - }, - "w3c-wai-eo@w3.org": { - "url": "https://www.w3.org/WAI/EO/", - "name": "Education and Outreach Working Group" - }, - "public-exi@w3.org": { - "url": "https://www.w3.org/XML/EXI/", - "name": "Efficient XML Interchange Working Group" - }, - "public-wai-ert@w3.org": { - "url": "https://www.w3.org/WAI/ER/", - "name": "Evaluation and Repair Tools Working Group" - }, - "public-forms@w3.org": { - "url": "https://www.w3.org/MarkUp/Forms/", - "name": "Forms Working Group" - }, - "public-geolocation@w3.org": { - "url": "https://www.w3.org/2008/geolocation/", - "name": "Geolocation Working Group" - }, - "public-gld-wg@w3.org": { - "url": "https://www.w3.org/2011/gld/", - "name": "Government Linked Data Working Group" - }, - "public-html@w3.org": { - "url": "https://www.w3.org/html/wg/", - "name": "HTML Working Group" - }, - "public-indie-ui@w3.org": { - "url": "https://www.w3.org/WAI/IndieUI/", - "name": "Independent User Interface (Indie UI) Working Group" - }, - "public-ldp-wg@w3.org": { - "url": "https://www.w3.org/2012/ldp/", - "name": "Linked Data Platform (LDP) Working Group" - }, - "public-media-annotation@w3.org": { - "url": "https://www.w3.org/2008/WebVideo/Annotations/", - "name": "Media Annotations Working Group" - }, - "public-mbui@w3.org": { - "url": "https://www.w3.org/2011/mbui/", - "name": "Model-Based User Interfaces Working Group" - }, - "www-multimodal@w3.org": { - "url": "https://www.w3.org/2002/mmi/", - "name": "Multimodal Interaction Working Group" - }, - "public-nfc@w3.org": { - "url": "https://www.w3.org/2012/nfc/", - "name": "Near Field Communications Working Group" - }, - "public-pointer-events@w3.org": { - "url": "https://www.w3.org/2012/pointerevents/", - "name": "Pointer Events Working Group" - }, - "public-rdf-wg@w3.org": { - "url": "https://www.w3.org/2011/rdf-wg/", - "name": "RDF Working Group" - }, - "public-rdfa@w3.org": { - "url": "https://www.w3.org/2010/02/rdfa/", - "name": "RDFa Working Group" - }, - "public-wai-rd@w3.org": { - "url": "https://www.w3.org/WAI/RD/", - "name": "Research and Development Working Group" - }, - "public-svg-wg@w3.org": { - "url": "https://www.w3.org/Graphics/SVG/WG/", - "name": "SVG Working Group" - }, - "public-sysapps@w3.org": { - "url": "https://www.w3.org/2012/sysapps/", - "name": "System Applications Working Group" - }, - "www-tag@w3.org": { - "url": "https://www.w3.org/2001/tag/", - "name": "Technical Architecture Group" - }, - "public-tt@w3.org": { - "url": "https://www.w3.org/AudioVideo/TT/", - "name": "Timed Text Working Group" - }, - "public-tracking@w3.org": { - "url": "https://www.w3.org/2011/tracking-protection/", - "name": "Tracking Protection Working Group" - }, - "w3c-wai-ua@w3.org": { - "url": "https://www.w3.org/WAI/UA/", - "name": "User Agent Accessibility Guidelines Working Group" - }, - "public-webappsec@w3.org": { - "url": "https://www.w3.org/2011/webappsec/", - "name": "Web Application Security Working Group" - }, - "public-webapps@w3.org": { - "url": "https://www.w3.org/2008/webapps/", - "name": "Web Applications Working Group" - }, - "w3c-wai-gl@w3.org": { - "url": "https://www.w3.org/WAI/GL/", - "name": "Web Content Accessibility Guidelines Working Group" - }, - "public-webcrypto@w3.org": { - "url": "https://www.w3.org/2012/webcrypto/", - "name": "Web Cryptography Working Group" - }, - "public-web-notification@w3.org": { - "url": "https://www.w3.org/2010/web-notifications/", - "name": "Web Notification Working Group" - }, - "public-web-perf@w3.org": { - "url": "https://www.w3.org/2010/webperf/", - "name": "Web Performance Working Group" - }, - "public-webrtc@w3.org": { - "url": "https://www.w3.org/2011/04/webrtc/", - "name": "Web Real-Time Communications Working Group" - }, - "public-webfonts-wg@w3.org": { - "url": "https://www.w3.org/Fonts/WG/", - "name": "WebFonts Working Group" - }, - "public-xml-core-wg@w3.org": { - "url": "https://www.w3.org/XML/Core/", - "name": "XML Core Working Group" - }, - "public-xml-processing-model-wg@w3.org": { - "url": "https://www.w3.org/XML/Processing/", - "name": "XML Processing Model Working Group" - }, - "public-xmlsec@w3.org": { - "url": "https://www.w3.org/2008/xmlsec/", - "name": "XML Security Working Group" - } -} diff --git a/lib/l10n-en_GB.js b/lib/l10n-en_GB.ts similarity index 99% rename from lib/l10n-en_GB.js rename to lib/l10n-en_GB.ts index 855dc5d10..25af80622 100644 --- a/lib/l10n-en_GB.js +++ 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': @@ -379,4 +377,4 @@ export const messages = { 'metadata.charters': false, 'metadata.sotd': false, 'metadata.abstract': false, -}; +} as const; diff --git a/lib/l10n.js b/lib/l10n.js deleted file mode 100644 index b0534debd..000000000 --- a/lib/l10n.js +++ /dev/null @@ -1,172 +0,0 @@ -/** - * Compose user messages. - */ - -// Internal packages: -import { messages } from './l10n-en_GB.js'; -import { importJSON } from './util.js'; - -const originalRules = importJSON('./rules.json', import.meta.url); - -// Constants: -const enGB = messages; - -// Variables: -let lang; -const rules = {}; - -for (const t in originalRules) - if (t === '*') rules[t] = originalRules[t]; - else - for (const p in originalRules[t].profiles) - rules[p] = originalRules[t].profiles[p]; - -/** - * Set a locale to be used globally by this module. - * - * @param {string} language - locale, expressed as a string, eg en_GB. - */ - -/** - * @param language - */ -export function setLanguage(language) { - if (!language) - throw new Error( - 'l10n.setLanguage() invoked without passing a language code as parameter' - ); - else if (language.match(/^en([.\-_]?gb)?$/i)) lang = enGB; - else - throw new Error( - 'Language code passed to l10n.setLanguage() is not valid' - ); -} - -/** - * @param profileCode - * @param rule - * @param key - * @param extra - */ -export function assembleData(profileCode, rule, key, extra) { - const messageData = {}; - // Corner case: if the profile is unknown, let's assume 'WD' (most common). - const profile = profileCode ? profileCode.replace('-Echidna', '') : 'WD'; - let name; - if (typeof rule === 'string') name = rule; - else name = rule.name; - if (!lang) - throw new Error( - 'l10n.assembleData() invoked before a locale is defined; call l10n.setLanguage() first' - ); - else if (!Object.prototype.hasOwnProperty.call(rules, profile)) - throw new Error( - `l10n.assembleData(): unknown profile code “${profile}”` - ); - else { - if (!Object.prototype.hasOwnProperty.call(lang, `${name}.${key}`)) - throw new Error( - `l10n.assembleData() could not interpret key “${name}.${key}”` - ); - - messageData.message = lang[`${name}.${key}`] || ''; - if (extra) - messageData.message = messageData.message.replace( - /\$\{([^}]+)\}/g, - (m, p1) => { - if (Object.prototype.hasOwnProperty.call(extra, p1)) - return extra[p1]; - - messageData.additionalMessage = `
NB: Pubrules may have found an internal error while checking this rule. -Feel free to file this issue on GitHub -to let developers examine the problem (you can submit it as is; no additional information is required).
`; - return `[unknown ${p1}]`; - } - ); - messageData.profile = profile; - return messageData; - } -} - -/** - * @param profileCode - * @param rule - * @param key - * @param extra - */ -export function message(profileCode, rule, key, extra) { - const result = {}; - const messageData = assembleData(profileCode, rule, key, extra); - if (!messageData) return; - result.message = - messageData.message && messageData.message.length - ? `
${messageData.message}
` - : ''; - const { profile } = messageData; - - if (rule.section && rule.rule) { - let selector; - if (rules[profile].sections[rule.section]) { - if (rules[profile].sections[rule.section].rules[rule.rule]) { - if ( - typeof rules[profile].sections[rule.section].rules[ - rule.rule - ] === 'boolean' - ) { - // Common rule, with no parameters - result.message += `
${ - rules['*'].sections[rule.section].rules[rule.rule] - }
`; - } else if ( - typeof rules[profile].sections[rule.section].rules[ - rule.rule - ] === 'string' - ) { - // Specific rule - result.message += `
${ - rules[profile].sections[rule.section].rules[rule.rule] - }
`; - } else if ( - typeof rules[profile].sections[rule.section].rules[ - rule.rule - ] === 'object' - ) { - // Array (common rule with parameters) - const values = - rules[profile].sections[rule.section].rules[rule.rule]; - let template = - rules['*'].sections[rule.section].rules[rule.rule]; - for (let p = 0; p < values.length; p += 1) - template = template.replace( - new RegExp(`@{param${p + 1}}`, 'g'), - values[p] - ); - result.message += `
${template}
`; - } else { - // Error - selector = encodeURIComponent( - `[${profile}][${rule.section}][${rule.rule}].text` - ); - } - result.message = result.message.replace( - new RegExp(`@{year}`, 'g'), - new Date().getFullYear() - ); - } else - selector = encodeURIComponent( - `[${profile}][${rule.section}][${rule.rule}]` - ); - } else selector = encodeURIComponent(`[${profile}][${rule.section}]`); - if (selector) - result.message += `
NB: Pubrules hit an internal error while checking this rule. -Please file this issue on GitHub to help developers fix the problem -(you can submit it as is; no additional information is required).
`; - result.id = rule.rule; - result.name = rule.name; - } - if (messageData.additionalMessage) - result.message += messageData.additionalMessage; - return result; -} diff --git a/lib/l10n.ts b/lib/l10n.ts new file mode 100644 index 000000000..bce7012ee --- /dev/null +++ b/lib/l10n.ts @@ -0,0 +1,163 @@ +/** + * Compose user messages. + */ + +import { messages } from './l10n-en_GB.js'; +import originalRules, { type RulesProfile } from './rules-track.js'; +import genericSections from './rules-generic-sections.js'; +import type { RuleBase, RuleMeta } from './types.js'; + +const enGB = messages; + +type LanguageMap = Record; + +let lang: LanguageMap | undefined; +const profileRules: Record = {}; + +for (const { profiles } of Object.values(originalRules)) + for (const [p, profile] of Object.entries(profiles)) + profileRules[p] = profile; + +/** + * Set a locale to be used globally by this module. + * + * @param {string} language - locale, expressed as a string, eg en_GB. + */ + +/** + * @param language + */ +export function setLanguage(language: string) { + if (!language) + throw new Error( + 'l10n.setLanguage() invoked without passing a language code as parameter' + ); + else if (language.match(/^en([.\-_]?gb)?$/i)) lang = enGB; + else + throw new Error( + 'Language code passed to l10n.setLanguage() is not valid' + ); +} + +function assertLanguageDefined( + lang: LanguageMap | undefined +): asserts lang is LanguageMap { + if (!lang) + throw new Error( + 'l10n.assembleData() invoked before a locale is defined; call l10n.setLanguage() first' + ); +} + +export function assembleData( + profileCode: string | null, + rule: RuleBase | RuleMeta | string, + key: string, + extra?: Record +) { + // Corner case: if the profile is unknown, let's assume 'WD' (most common). + const profile = profileCode ? profileCode.replace('-Echidna', '') : 'WD'; + const name = typeof rule === 'string' ? rule : rule.name; + assertLanguageDefined(lang); + + if (!Object.hasOwn(profileRules, profile)) + throw new Error( + `l10n.assembleData(): unknown profile code “${profile}”` + ); + + if (!Object.hasOwn(lang, `${name}.${key}`)) + throw new Error( + `l10n.assembleData() could not interpret key “${name}.${key}”` + ); + + const messageData = { + additionalMessage: '', + message: lang[`${name}.${key}`] || '', + profile, + }; + + if (extra) + messageData.message = messageData.message.replace( + /\$\{([^}]+)\}/g, + (_, p1) => { + if (Object.hasOwn(extra, p1)) return extra[p1]; + + messageData.additionalMessage = `
NB: Pubrules may have found an internal error while checking this rule. +Feel free to file this issue on GitHub +to let developers examine the problem (you can submit it as is; no additional information is required).
`; + return `[unknown ${p1}]`; + } + ); + return messageData; +} + +export function message( + profileCode: string | null, + rule: RuleBase | RuleMeta | string, + key: string, + extra?: Record +) { + const messageData = assembleData(profileCode, rule, key, extra); + if (!messageData) return; + let message = + messageData.message && messageData.message.length + ? `
${messageData.message}
` + : ''; + const { profile } = messageData; + + if (typeof rule === 'object' && 'rule' in rule) { + let selector; + const profileSections = profileRules[profile].sections; + if (profileSections[rule.section]) { + if (profileSections[rule.section].rules[rule.rule]) { + const genericRules = + genericSections[ + rule.section as keyof typeof genericSections + ].rules; + const genericRule = + genericRules[rule.rule as keyof typeof genericRules]; + const profileRule = + profileSections[rule.section].rules[rule.rule]; + if (typeof profileRule === 'boolean') { + // Common rule, with no parameters + message += `
${genericRule}
`; + } else if (typeof profileRule === 'string') { + // Specific rule + message += `
${profileRule}
`; + } else if (typeof profileRule === 'object') { + // Array (common rule with parameters) + const template = profileRule.reduce( + (rule, value, i) => + rule.replace( + new RegExp(`@{param${i + 1}}`, 'g'), + value + ), + genericRule + ); + message += `
${template}
`; + } else { + // Error + selector = encodeURIComponent( + `[${profile}][${rule.section}][${rule.rule}].text` + ); + } + message = message.replace( + new RegExp(`@{year}`, 'g'), + '' + new Date().getFullYear() + ); + } else + selector = encodeURIComponent( + `[${profile}][${rule.section}][${rule.rule}]` + ); + } else selector = encodeURIComponent(`[${profile}][${rule.section}]`); + if (selector) + message += `
NB: Pubrules hit an internal error while checking this rule. +Please file this issue on GitHub to help developers fix the problem +(you can submit it as is; no additional information is required).
`; + } + if (messageData.additionalMessage) message += messageData.additionalMessage; + if (typeof rule === 'object' && 'rule' in rule) + return { id: rule.rule, name: rule.name, message }; + return { message }; +} diff --git a/lib/profiles/SUBM.js b/lib/profiles/SUBM.ts similarity index 100% rename from lib/profiles/SUBM.js rename to lib/profiles/SUBM.ts diff --git a/lib/profiles/SUBM/MEM-SUBM.js b/lib/profiles/SUBM/MEM-SUBM.ts similarity index 88% rename from lib/profiles/SUBM/MEM-SUBM.js rename to lib/profiles/SUBM/MEM-SUBM.ts index cb2030097..316f487fc 100644 --- a/lib/profiles/SUBM/MEM-SUBM.js +++ b/lib/profiles/SUBM/MEM-SUBM.ts @@ -1,10 +1,11 @@ import * as memsubCopyright from '../../rules/headers/memsub-copyright.js'; import * as submission from '../../rules/sotd/submission.js'; +import type { SpecberusConfig } from '../../types.js'; import { insertAfter, removeRules } from '../profileUtil.js'; import { rules as baseRules } from '../SUBM.js'; export const name = 'MEM-SUBM'; -export const config = { +export const config: SpecberusConfig = { status: 'SUBM', longStatus: 'Member Submission', styleSheet: 'W3C-Member-SUBM', diff --git a/lib/profiles/TR.js b/lib/profiles/TR.ts similarity index 100% rename from lib/profiles/TR.js rename to lib/profiles/TR.ts diff --git a/lib/profiles/TR/Note/DNOTE-Echidna.js b/lib/profiles/TR/Note/DNOTE-Echidna.ts similarity index 78% rename from lib/profiles/TR/Note/DNOTE-Echidna.js rename to lib/profiles/TR/Note/DNOTE-Echidna.ts index 122ff9d23..013bce85e 100644 --- a/lib/profiles/TR/Note/DNOTE-Echidna.js +++ b/lib/profiles/TR/Note/DNOTE-Echidna.ts @@ -3,10 +3,10 @@ import * as todaysDate from '../../../rules/echidna/todays-date.js'; import * as delivererChange from '../../../rules/echidna/deliverer-change.js'; import { insertAfter } from '../../profileUtil.js'; -import { config as baseConfig, rules as baseRules } from './DNOTE.js'; +import { rules as baseRules } from './DNOTE.js'; export const name = 'DNOTE-Echidna'; -export const config = baseConfig; +export { config } from './DNOTE.js'; export const rules = insertAfter(baseRules, 'sotd.process-document', [ todaysDate, diff --git a/lib/profiles/TR/Note/DNOTE.js b/lib/profiles/TR/Note/DNOTE.ts similarity index 80% rename from lib/profiles/TR/Note/DNOTE.js rename to lib/profiles/TR/Note/DNOTE.ts index 5e8b338f3..ca8111bc4 100644 --- a/lib/profiles/TR/Note/DNOTE.js +++ b/lib/profiles/TR/Note/DNOTE.ts @@ -1,9 +1,10 @@ import * as draftStability from '../../../rules/sotd/draft-stability.js'; +import type { SpecberusConfig } from '../../../types.js'; import { insertAfter } from '../../profileUtil.js'; import { config as baseConfig, rules as baseRules } from './note-base.js'; export const name = 'DNOTE'; -export const config = { +export const config: SpecberusConfig = { ...baseConfig, status: 'DNOTE', longStatus: 'Group Note Draft', diff --git a/lib/profiles/TR/Note/NOTE-Echidna.js b/lib/profiles/TR/Note/NOTE-Echidna.ts similarity index 77% rename from lib/profiles/TR/Note/NOTE-Echidna.js rename to lib/profiles/TR/Note/NOTE-Echidna.ts index 4f0c9e412..d9978556b 100644 --- a/lib/profiles/TR/Note/NOTE-Echidna.js +++ b/lib/profiles/TR/Note/NOTE-Echidna.ts @@ -1,10 +1,10 @@ import * as todaysDate from '../../../rules/echidna/todays-date.js'; import * as delivererChange from '../../../rules/echidna/deliverer-change.js'; import { insertAfter } from '../../profileUtil.js'; -import { config as baseConfig, rules as baseRules } from './NOTE.js'; +import { rules as baseRules } from './NOTE.js'; export const name = 'NOTE-Echidna'; -export const config = baseConfig; +export { config } from './NOTE.js'; export const rules = insertAfter(baseRules, 'sotd.process-document', [ todaysDate, diff --git a/lib/profiles/TR/Note/NOTE.js b/lib/profiles/TR/Note/NOTE.ts similarity index 70% rename from lib/profiles/TR/Note/NOTE.js rename to lib/profiles/TR/Note/NOTE.ts index 1eb9e8282..8ff292f29 100644 --- a/lib/profiles/TR/Note/NOTE.js +++ b/lib/profiles/TR/Note/NOTE.ts @@ -1,7 +1,8 @@ +import type { SpecberusConfig } from '../../../types.js'; import { config as baseConfig, rules as baseRules } from './note-base.js'; export const name = 'NOTE'; -export const config = { +export const config: SpecberusConfig = { ...baseConfig, status: 'NOTE', longStatus: 'Group Note', diff --git a/lib/profiles/TR/Note/STMT.js b/lib/profiles/TR/Note/STMT.ts similarity index 82% rename from lib/profiles/TR/Note/STMT.js rename to lib/profiles/TR/Note/STMT.ts index 6a28836c3..58daea9fd 100644 --- a/lib/profiles/TR/Note/STMT.js +++ b/lib/profiles/TR/Note/STMT.ts @@ -1,9 +1,10 @@ import * as translation from '../../../rules/headers/translation.js'; import { config as baseConfig, rules as baseRules } from './note-base.js'; import { insertAfter } from '../../profileUtil.js'; +import type { SpecberusConfig } from '../../../types.js'; export const name = 'STMT'; -export const config = { +export const config: SpecberusConfig = { ...baseConfig, status: 'STMT', longStatus: 'Statement', diff --git a/lib/profiles/TR/Note/note-base.js b/lib/profiles/TR/Note/note-base.ts similarity index 96% rename from lib/profiles/TR/Note/note-base.js rename to lib/profiles/TR/Note/note-base.ts index 53c706a48..c68c2034a 100644 --- a/lib/profiles/TR/Note/note-base.js +++ b/lib/profiles/TR/Note/note-base.ts @@ -4,6 +4,6 @@ import { rules as baseRules } from '../../TR.js'; export const config = { track: 'Note', -}; +} as const; export const rules = insertAfter(baseRules, 'sotd.pp', [deliverNote]); diff --git a/lib/profiles/TR/Recommendation/CR-Echidna.js b/lib/profiles/TR/Recommendation/CR-Echidna.ts similarity index 100% rename from lib/profiles/TR/Recommendation/CR-Echidna.js rename to lib/profiles/TR/Recommendation/CR-Echidna.ts diff --git a/lib/profiles/TR/Recommendation/CR.js b/lib/profiles/TR/Recommendation/CR.ts similarity index 85% rename from lib/profiles/TR/Recommendation/CR.js rename to lib/profiles/TR/Recommendation/CR.ts index b0032479f..6a3233664 100644 --- a/lib/profiles/TR/Recommendation/CR.js +++ b/lib/profiles/TR/Recommendation/CR.ts @@ -1,5 +1,6 @@ import * as candidateReviewEnd from '../../../rules/sotd/candidate-review-end.js'; import * as newFeatures from '../../../rules/sotd/new-features.js'; +import type { SpecberusConfig } from '../../../types.js'; import { insertAfter } from '../../profileUtil.js'; import { config as baseConfig, @@ -7,7 +8,7 @@ import { } from './recommendation-base.js'; export const name = 'CR'; -export const config = { +export const config: SpecberusConfig = { ...baseConfig, status: 'CR', longStatus: 'Candidate Recommendation', diff --git a/lib/profiles/TR/Recommendation/CRD-Echidna.js b/lib/profiles/TR/Recommendation/CRD-Echidna.ts similarity index 100% rename from lib/profiles/TR/Recommendation/CRD-Echidna.js rename to lib/profiles/TR/Recommendation/CRD-Echidna.ts diff --git a/lib/profiles/TR/Recommendation/CRD.js b/lib/profiles/TR/Recommendation/CRD.ts similarity index 84% rename from lib/profiles/TR/Recommendation/CRD.js rename to lib/profiles/TR/Recommendation/CRD.ts index fa6b132f3..6ad86f43f 100644 --- a/lib/profiles/TR/Recommendation/CRD.js +++ b/lib/profiles/TR/Recommendation/CRD.ts @@ -1,5 +1,6 @@ import * as draftStability from '../../../rules/sotd/draft-stability.js'; import * as newFeatures from '../../../rules/sotd/new-features.js'; +import type { SpecberusConfig } from '../../../types.js'; import { insertAfter } from '../../profileUtil.js'; import { config as baseConfig, @@ -7,7 +8,7 @@ import { } from './recommendation-base.js'; export const name = 'CRD'; -export const config = { +export const config: SpecberusConfig = { ...baseConfig, status: 'CRD', longStatus: 'Candidate Recommendation', diff --git a/lib/profiles/TR/Recommendation/DISC.js b/lib/profiles/TR/Recommendation/DISC.ts similarity index 81% rename from lib/profiles/TR/Recommendation/DISC.js rename to lib/profiles/TR/Recommendation/DISC.ts index 0ec103ef4..e0fb3feb0 100644 --- a/lib/profiles/TR/Recommendation/DISC.js +++ b/lib/profiles/TR/Recommendation/DISC.ts @@ -1,4 +1,5 @@ // customize rules +import type { SpecberusConfig } from '../../../types.js'; import { removeRules } from '../../profileUtil.js'; import { config as baseConfig, @@ -6,7 +7,7 @@ import { } from './recommendation-base.js'; export const name = 'DISC'; -export const config = { +export const config: SpecberusConfig = { ...baseConfig, status: 'DISC', longStatus: 'Discontinued Draft', diff --git a/lib/profiles/TR/Recommendation/FPWD.js b/lib/profiles/TR/Recommendation/FPWD.ts similarity index 84% rename from lib/profiles/TR/Recommendation/FPWD.js rename to lib/profiles/TR/Recommendation/FPWD.ts index 61e6709e6..1088ff198 100644 --- a/lib/profiles/TR/Recommendation/FPWD.js +++ b/lib/profiles/TR/Recommendation/FPWD.ts @@ -1,4 +1,5 @@ import * as draftStability from '../../../rules/sotd/draft-stability.js'; +import type { SpecberusConfig } from '../../../types.js'; import { insertAfter, removeRules } from '../../profileUtil.js'; import { config as baseConfig, @@ -6,7 +7,7 @@ import { } from './recommendation-base.js'; export const name = 'FPWD'; -export const config = { +export const config: SpecberusConfig = { ...baseConfig, status: 'WD', longStatus: 'First Public Working Draft', diff --git a/lib/profiles/TR/Recommendation/REC-Echidna.js b/lib/profiles/TR/Recommendation/REC-Echidna.ts similarity index 100% rename from lib/profiles/TR/Recommendation/REC-Echidna.js rename to lib/profiles/TR/Recommendation/REC-Echidna.ts diff --git a/lib/profiles/TR/Recommendation/REC-RSCND.js b/lib/profiles/TR/Recommendation/REC-RSCND.ts similarity index 88% rename from lib/profiles/TR/Recommendation/REC-RSCND.js rename to lib/profiles/TR/Recommendation/REC-RSCND.ts index ed887eae5..5300a509b 100644 --- a/lib/profiles/TR/Recommendation/REC-RSCND.js +++ b/lib/profiles/TR/Recommendation/REC-RSCND.ts @@ -1,9 +1,10 @@ import * as obslRescind from '../../../rules/sotd/obsl-rescind.js'; +import type { SpecberusConfig } from '../../../types.js'; import { insertAfter, removeRules } from '../../profileUtil.js'; import { config as baseConfig, rules as baseRules } from './REC.js'; export const name = 'REC-RSCND'; -export const config = { +export const config: SpecberusConfig = { ...baseConfig, status: 'REC', longStatus: 'Rescinded Recommendation', diff --git a/lib/profiles/TR/Recommendation/REC.js b/lib/profiles/TR/Recommendation/REC.ts similarity index 91% rename from lib/profiles/TR/Recommendation/REC.js rename to lib/profiles/TR/Recommendation/REC.ts index 06add888b..cf619418a 100644 --- a/lib/profiles/TR/Recommendation/REC.js +++ b/lib/profiles/TR/Recommendation/REC.ts @@ -4,6 +4,7 @@ import * as deployment from '../../../rules/sotd/deployment.js'; import * as newFeatures from '../../../rules/sotd/new-features.js'; import * as recAddition from '../../../rules/sotd/rec-addition.js'; import * as recCommentEnd from '../../../rules/sotd/rec-comment-end.js'; +import type { SpecberusConfig } from '../../../types.js'; import { insertAfter, removeRules } from '../../profileUtil.js'; import { config as baseConfig, @@ -11,7 +12,7 @@ import { } from './recommendation-base.js'; export const name = 'REC'; -export const config = { +export const config: SpecberusConfig = { ...baseConfig, status: 'REC', longStatus: 'Recommendation', diff --git a/lib/profiles/TR/Recommendation/WD-Echidna.js b/lib/profiles/TR/Recommendation/WD-Echidna.ts similarity index 100% rename from lib/profiles/TR/Recommendation/WD-Echidna.js rename to lib/profiles/TR/Recommendation/WD-Echidna.ts diff --git a/lib/profiles/TR/Recommendation/WD.js b/lib/profiles/TR/Recommendation/WD.ts similarity index 81% rename from lib/profiles/TR/Recommendation/WD.js rename to lib/profiles/TR/Recommendation/WD.ts index 3c3ed7224..74dfd8fd4 100644 --- a/lib/profiles/TR/Recommendation/WD.js +++ b/lib/profiles/TR/Recommendation/WD.ts @@ -1,4 +1,5 @@ import * as draftStability from '../../../rules/sotd/draft-stability.js'; +import type { SpecberusConfig } from '../../../types.js'; import { insertAfter } from '../../profileUtil.js'; import { config as baseConfig, @@ -6,7 +7,7 @@ import { } from './recommendation-base.js'; export const name = 'WD'; -export const config = { +export const config: SpecberusConfig = { ...baseConfig, status: 'WD', longStatus: 'Working Draft', diff --git a/lib/profiles/TR/Recommendation/recommendation-base.js b/lib/profiles/TR/Recommendation/recommendation-base.ts similarity index 97% rename from lib/profiles/TR/Recommendation/recommendation-base.js rename to lib/profiles/TR/Recommendation/recommendation-base.ts index 268501810..784bb96c1 100644 --- a/lib/profiles/TR/Recommendation/recommendation-base.js +++ b/lib/profiles/TR/Recommendation/recommendation-base.ts @@ -5,7 +5,7 @@ import { rules as baseRules } from '../../TR.js'; export const config = { track: 'Recommendation', -}; +} as const; export const rules = insertAfter(baseRules, 'sotd.supersedable', [ diff, diff --git a/lib/profiles/TR/Registry/CRY.js b/lib/profiles/TR/Registry/CRY.ts similarity index 83% rename from lib/profiles/TR/Registry/CRY.js rename to lib/profiles/TR/Registry/CRY.ts index 3030bb2da..04af07ed4 100644 --- a/lib/profiles/TR/Registry/CRY.js +++ b/lib/profiles/TR/Registry/CRY.ts @@ -1,10 +1,11 @@ // customize rules import * as candidateReviewEnd from '../../../rules/sotd/candidate-review-end.js'; +import type { SpecberusConfig } from '../../../types.js'; import { insertAfter } from '../../profileUtil.js'; import { config as baseConfig, rules as baseRules } from './registry-base.js'; export const name = 'CRY'; -export const config = { +export const config: SpecberusConfig = { ...baseConfig, status: 'CRY', longStatus: 'Candidate Registry', diff --git a/lib/profiles/TR/Registry/CRYD.js b/lib/profiles/TR/Registry/CRYD.ts similarity index 82% rename from lib/profiles/TR/Registry/CRYD.js rename to lib/profiles/TR/Registry/CRYD.ts index 2f75af88b..d55dfd317 100644 --- a/lib/profiles/TR/Registry/CRYD.js +++ b/lib/profiles/TR/Registry/CRYD.ts @@ -1,9 +1,10 @@ import * as draftStability from '../../../rules/sotd/draft-stability.js'; +import type { SpecberusConfig } from '../../../types.js'; import { insertAfter } from '../../profileUtil.js'; import { config as baseConfig, rules as baseRules } from './registry-base.js'; export const name = 'CRYD'; -export const config = { +export const config: SpecberusConfig = { ...baseConfig, status: 'CRYD', longStatus: 'Candidate Registry', diff --git a/lib/profiles/TR/Registry/DRY-Echidna.js b/lib/profiles/TR/Registry/DRY-Echidna.ts similarity index 78% rename from lib/profiles/TR/Registry/DRY-Echidna.js rename to lib/profiles/TR/Registry/DRY-Echidna.ts index facf893b4..d91888536 100644 --- a/lib/profiles/TR/Registry/DRY-Echidna.js +++ b/lib/profiles/TR/Registry/DRY-Echidna.ts @@ -3,10 +3,10 @@ import * as todaysDate from '../../../rules/echidna/todays-date.js'; import * as delivererChange from '../../../rules/echidna/deliverer-change.js'; import { insertAfter } from '../../profileUtil.js'; -import { config as baseConfig, rules as baseRules } from './DRY.js'; +import { rules as baseRules } from './DRY.js'; export const name = 'DRY-Echidna'; -export const config = baseConfig; +export { config } from './DRY.js'; export const rules = insertAfter(baseRules, 'sotd.process-document', [ todaysDate, diff --git a/lib/profiles/TR/Registry/DRY.js b/lib/profiles/TR/Registry/DRY.ts similarity index 81% rename from lib/profiles/TR/Registry/DRY.js rename to lib/profiles/TR/Registry/DRY.ts index e3daa6d89..d3523b7cf 100644 --- a/lib/profiles/TR/Registry/DRY.js +++ b/lib/profiles/TR/Registry/DRY.ts @@ -1,9 +1,10 @@ import * as draftStability from '../../../rules/sotd/draft-stability.js'; +import type { SpecberusConfig } from '../../../types.js'; import { insertAfter } from '../../profileUtil.js'; import { config as baseConfig, rules as baseRules } from './registry-base.js'; export const name = 'DRY'; -export const config = { +export const config: SpecberusConfig = { ...baseConfig, status: 'DRY', longStatus: 'Registry Draft', diff --git a/lib/profiles/TR/Registry/RY.js b/lib/profiles/TR/Registry/RY.ts similarity index 79% rename from lib/profiles/TR/Registry/RY.js rename to lib/profiles/TR/Registry/RY.ts index 9495a6d1c..3be4c187f 100644 --- a/lib/profiles/TR/Registry/RY.js +++ b/lib/profiles/TR/Registry/RY.ts @@ -1,9 +1,10 @@ import * as usage from '../../../rules/sotd/usage.js'; +import type { SpecberusConfig } from '../../../types.js'; import { insertAfter } from '../../profileUtil.js'; import { config as baseConfig, rules as baseRules } from './registry-base.js'; export const name = 'RY'; -export const config = { +export const config: SpecberusConfig = { ...baseConfig, status: 'RY', longStatus: 'Registry', diff --git a/lib/profiles/TR/Registry/registry-base.js b/lib/profiles/TR/Registry/registry-base.ts similarity index 87% rename from lib/profiles/TR/Registry/registry-base.js rename to lib/profiles/TR/Registry/registry-base.ts index 5fde85a8c..9baef0762 100644 --- a/lib/profiles/TR/Registry/registry-base.js +++ b/lib/profiles/TR/Registry/registry-base.ts @@ -1,5 +1,5 @@ export const config = { track: 'Registry', -}; +} as const; export { rules } from '../../TR.js'; diff --git a/lib/profiles/additionalMetadata.js b/lib/profiles/additionalMetadata.ts similarity index 100% rename from lib/profiles/additionalMetadata.js rename to lib/profiles/additionalMetadata.ts diff --git a/lib/profiles/base.js b/lib/profiles/base.ts similarity index 100% rename from lib/profiles/base.js rename to lib/profiles/base.ts diff --git a/lib/profiles/metadata.js b/lib/profiles/metadata.ts similarity index 100% rename from lib/profiles/metadata.js rename to lib/profiles/metadata.ts diff --git a/lib/profiles/profileUtil.js b/lib/profiles/profileUtil.js deleted file mode 100644 index 0e5608b46..000000000 --- a/lib/profiles/profileUtil.js +++ /dev/null @@ -1,45 +0,0 @@ -// take an array of rules, return a copy with new rules added after a given named anchor -/** - * @param original - * @param anchor - * @param rules - */ -export function insertAfter(original, anchor, rules) { - rules = Array.isArray(rules) ? rules : [rules]; - original = original.slice(); - const index = original.map(r => r.name).indexOf(anchor); - if (index < 0) return original.concat(rules); - - rules.forEach(r => { - // see if the rule to be inserted already exists. - const rIndex = original.map(rule => rule.name).indexOf(r.name); - if (rIndex < 0) original.splice(index, 0, r); - else - console.error( - `[EXCEPTION] Error when insertAfter, ${r.name} already exists at position ${rIndex} of the original list` - ); - }); - return original; -} - -// take an array of rules, return a copy with specified rules removed -// e.g. profileUtil.removeRules(rules, "sotd.pp") -/** - * @param original - * @param rules - */ -export function removeRules(original, rules) { - rules = Array.isArray(rules) ? rules : [rules]; - original = original.slice(); - rules.forEach(r => { - const index = original.map(rule => rule.name).indexOf(r); - if (index >= 0) { - original.splice(index, 1); - } else { - console.error( - `[EXCEPTION] Error when removeRules, ${r} doesn't exist in the original list` - ); - } - }); - return original; -} diff --git a/lib/profiles/profileUtil.ts b/lib/profiles/profileUtil.ts new file mode 100644 index 000000000..8645da3cf --- /dev/null +++ b/lib/profiles/profileUtil.ts @@ -0,0 +1,52 @@ +import type { RuleModule } from '../types.js'; + +/** + * Takes an array of rules, returns a copy with new rules added after a given named anchor + */ +export function insertAfter( + original: RuleModule[], + anchor: string, + newRules: RuleModule | RuleModule[] +) { + newRules = Array.isArray(newRules) ? newRules : [newRules]; + original = original.slice(); + const index = original.map(r => r.name).indexOf(anchor); + if (index < 0) return original.concat(newRules); + + newRules.forEach(newRule => { + // see if the rule to be inserted already exists. + const rIndex = original.map(rule => rule.name).indexOf(newRule.name); + if (rIndex < 0) original.splice(index, 0, newRule); + else + console.error( + `[EXCEPTION] Error when insertAfter, ${newRule.name} already exists at position ${rIndex} of the original list` + ); + }); + return original; +} + +/** + * Takes an array of rules, returns a copy with specified rules removed + * e.g. profileUtil.removeRules(rules, "sotd.pp") + * + * @param original + * @param ruleNames + */ +export function removeRules( + original: RuleModule[], + ruleNames: string | string[] +) { + ruleNames = Array.isArray(ruleNames) ? ruleNames : [ruleNames]; + original = original.slice(); + ruleNames.forEach(name => { + const index = original.map(rule => rule.name).indexOf(name); + if (index >= 0) { + original.splice(index, 1); + } else { + console.error( + `[EXCEPTION] Error when removeRules, ${name} doesn't exist in the original list` + ); + } + }); + return original; +} diff --git a/lib/rule-context.ts b/lib/rule-context.ts new file mode 100644 index 000000000..a601012b3 --- /dev/null +++ b/lib/rule-context.ts @@ -0,0 +1,695 @@ +/** + * @file Main file of the Specberus npm package. + */ + +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 } 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, REC_TEXT, specberusVersion, TAG } from './util.js'; +import type { + ApiCharter, + ApiSpecificationVersion, + RecMetadata, + RuleBase, + RuleMeta, + SpecberusConfig, +} from './types.js'; + +type HeaderMap = Record< + string, + { + pos: number; + $el: Cheerio; + $dd: Cheerio; + } +>; + +interface DelivererGroup { + groupShortname: string; + groupType: string; +} + +interface TransitionBaseOptions { + doMeanwhile: () => void; +} + +interface TransitionFromOptions { + doBefore: () => void; + from: Date; +} + +interface TransitionToOptions { + doAfter: () => void; + to: Date; +} + +type TransitionOptions = + | (TransitionBaseOptions & TransitionFromOptions) + | (TransitionBaseOptions & TransitionToOptions) + | (TransitionBaseOptions & TransitionFromOptions & TransitionToOptions); + +const months = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', +]; +const abbrMonths = [ + 'Jan', + 'Feb', + 'Mar', + 'Apr', + 'May', + 'Jun', + 'Jul', + 'Aug', + 'Sep', + 'Oct', + 'Nov', + 'Dec', +]; + +export const possibleMonths = [...months, ...abbrMonths].join('|'); +const separator = '[ -]{1}'; + +export 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 + +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; + +/** + * Encapsulates all methods of interest to rule check functions, + * separate from the APIs responsible for core Specberus requests. + */ +export class RuleContext { + $: CheerioAPI; + /** + * Configuration that Specberus parsed from params. + * Only present for validation (not metadata extraction). + */ + config: SpecberusConfig | undefined; + version = specberusVersion; + + #sr: Specberus; + #$docDateEl: Cheerio | undefined; + #$sotdSection: Cheerio | null | undefined; + /** Group objects returned by W3C API charters endpoint */ + #chartersData: [] | Promise | undefined; + /** Charter URIs */ + #charters: string[] | undefined; + #delivererIDs: number[] | Promise | undefined; + #delivererGroups: Promise | undefined; + #docDate: Date | undefined; + #headers: HeaderMap | undefined; + #isFirstPublic: boolean | undefined; + #previousVersion: Promise | undefined; + #shortname: string | undefined = undefined; + + constructor(sr: Specberus, $: CheerioAPI, config?: SpecberusConfig) { + this.$ = $; + this.#sr = sr; + if (config) this.config = config; + } + + get source() { + return this.#sr.source; + } + + 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 { + 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, ' '); + } + + 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) { + const shortname = this.getShortname(); + if ( + typeof shortname !== 'undefined' && + hasExceptions(shortname, rule.name, extra) + ) + this.warning(rule, key, extra); + else + this.#sr.emit('err', rule, { + key, + ...(extra && { extra }), + detailMessage: assembleData(null, rule, key, extra).message, + }); + } + + warning( + rule: RuleBase | RuleMeta, + key: string, + extra?: Record + ) { + this.#sr.emit('warning', rule, { + key, + ...(extra && { extra }), + detailMessage: assembleData(null, rule, key, extra).message, + }); + } + + info(rule: RuleBase | RuleMeta, key: string, extra?: Record) { + this.#sr.emit('info', rule, { + key, + ...(extra && { extra }), + detailMessage: assembleData(null, rule, key, extra).message, + }); + } + + getDocumentDate() { + if (this.#docDate) return this.#docDate; + const rex = new RegExp( + `${dateRegexStrCapturing}(?:, edited in place ${dateRegexStrNonCapturing})?$` + ); + const $el = this.$('#w3c-state'); + + const matches = $el.length && this.norm($el.text()).match(rex); + if (matches) { + this.#docDate = this.stringToDate( + `${matches[1]} ${matches[2]} ${matches[3]}` + ); + this.#$docDateEl = $el; + } + return this.#docDate; + } + + getDocumentStateElement() { + if (this.#$docDateEl) return this.#$docDateEl; + this.getDocumentDate(); + return this.#$docDateEl; + } + + getSotDSection() { + if (typeof this.#$sotdSection !== 'undefined') + return this.#$sotdSection; + + let startH2: Element | undefined; + let endH2: Element | undefined; + const $div = load('
', null, false)('div'); + const self = this; + const $nav = this.$('nav#toc'); + this.$('h2').each((_, h2) => { + if (startH2) { + endH2 = h2; + return false; + } + if ( + // cspell:disable-next-line + /^Status [Oo]f [Tt]his [Dd]ocument$/.test( + self.norm(this.$(h2).text()) + ) + ) { + startH2 = h2; + } + }); + if (!startH2) this.#$sotdSection = null; + else { + let started = false; + this.$(startH2) + .parent() + .children() + .each((_, el) => { + if (startH2 === el) { + started = true; + return; + } + if (!started) return; + if (endH2 === el || $nav[0] === el) return false; + $div.append(el.cloneNode(true)); + }); + this.#$sotdSection = $div.children().length ? $div : null; + } + if (!this.#$sotdSection) + this.error( + { + name: 'generic.sotd', + section: 'document-status', + rule: 'sotd', + }, + 'not-found' + ); + return this.#$sotdSection; + } + + extractHeaders() { + if (this.#headers) return this.#headers; + + const dts: HeaderMap = {}; + const EDITORS = /^editor(s)?$/; + const EDITORS_DRAFT = /^(latest )?editor's draft$/i; + const $dl = this.$('body div.head dl'); + + if ($dl && $dl.length) { + $dl.find('dt').each((idx, dt) => { + const $dt = this.$(dt); + const txt = this.norm($dt.text()) + .replace(':', '') + .toLowerCase() + .replace('published ', ''); + let $dd = $dt.next('dd'); + let key = null; + if (!$dd.length) + throw new Error(`No <dd> element found for ${txt}.`); + if (txt === 'this version') key = 'This'; + else if ( + !dts.Latest && + txt.lastIndexOf('latest version', 0) === 0 + ) + key = 'Latest'; + else if (/^history$/.test(txt)) key = 'History'; + else if (/^rescinds this recommendation?$/.test(txt)) + key = 'Rescinds'; + else if (/^implementation report?$/.test(txt)) + key = 'Implementation'; + else if (/^feedback$/.test(txt)) { + // feedback link can be multi-lines + key = 'Feedback'; + $dd = $dt.nextUntil('dt', 'dd'); + } else if (/^errata?$/.test(txt)) key = 'Errata'; + if (EDITORS_DRAFT.test(txt) && $dd.find('a').length) + key = 'EditorDraft'; + if (EDITORS.test(txt)) { + key = 'Editor'; + $dd = $dt.nextUntil('dt', 'dd'); + } + if (key) dts[key] = { pos: idx, $el: $dt, $dd }; + }); + } + this.#headers = dts; + return dts; + } + + getShortname() { + if (typeof this.#shortname !== 'undefined') return this.#shortname; + + let shortname; + const dts = this.extractHeaders(); + const $linkThis = dts.This ? dts.This.$dd.find('a') : null; + const linkThisHref = $linkThis?.attr('href')?.trim() || ''; + const thisVersionMatches = + linkThisHref && linkThisHref.match(/.*\/[^/-]+-(.*)-\d{8}\/$/); + if (thisVersionMatches && thisVersionMatches.length > 0) + [, shortname] = thisVersionMatches; + + this.#shortname = shortname; + return shortname; + } + + /** + * Attempts to extract all the dates from the SOTD. + * Only the dates after date of the doc and prior to one year later + * are extracted. If there is only one, there is a good chance that it's + * the deadline for feedback. + */ + getFeedbackDueDate() { + const $sotd = this.getSotDSection(); + const dates = { list: [] as Date[], valid: [] as Date[] }; + if ($sotd) { + const txt = this.norm($sotd.text()); + const rex = new RegExp(dateRegexStrCapturing, 'g'); + const docDate = this.getDocumentDate()!; + const lowBound = new Date(docDate).setDate( + new Date(docDate).getDate() + 27 + ); // minimum review period: 28 days (not counting the hours) + const highBound = new Date(docDate).setFullYear( + docDate.getFullYear() + 1 + ); + const candidates = txt.match(rex); + if (candidates !== null) { + for (let i = 0; i < candidates.length; i += 1) { + const d = this.stringToDate(candidates[i]); + if (typeof d !== 'undefined' && +d >= lowBound) { + dates.list.push(d); + if (+d < highBound) dates.valid.push(d); + } + } + } + } + return dates; + } + + // Return array of group names, e.g. ['Internationalization Working Group', 'Technical Architecture Group'] + getDelivererNames() { + const $sotd = this.getSotDSection(); + const delivererNamesRegex = + /This document was (?:produced|published) by the (.+? Working Group|.+? Interest Group|Technical Architecture Group|Advisory Board)( and the (.+? Working Group|.+? Interest Group|Technical Architecture Group|Advisory Board))? as/; + + const text = this.norm($sotd!.text()); + const matches = text.match(delivererNamesRegex); + const groups = []; + if (matches) { + groups.push(matches[1]); + if (matches[3]) groups.push(matches[3]); + } + return groups; + } + + /** + * Retrieves deliverers groupNames and types. + */ + async getDelivererGroups() { + if (this.#delivererGroups) return this.#delivererGroups; + + const $sotd = this.getSotDSection(); + const $sotdLinks = $sotd && $sotd.find('a[href]'); + const delivererGroups: DelivererGroup[] = []; + + // getDataDelivererIDs first, apply if document is Note/Registry track. + 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()); + + if (REGEX_DELIVERER_TEXT.test(text)) { + if (REGEX_DELIVERER_IPR_URL.test(href)) { + // get group shortnames directly + const [, type, shortname] = + REGEX_DELIVERER_IPR_URL.exec(href)!; + delivererGroups.push({ + groupShortname: shortname, + groupType: type, + }); + } else { + // get group shortnames through groupId + const delivererUrlMatch = + href.match(REGEX_DELIVERER_URL); + if (delivererUrlMatch) { + const id = delivererUrlMatch[1]; + if (id && id.length > 1) ids.push(parseInt(id, 10)); + } else if (REGEX_TAG_DISCLOSURE.test(href)) { + ids.push(TAG.id); + } + } + } + }); + } + + // 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 (this.#delivererIDs) return this.#delivererIDs; + + const ids: number[] = this.getDataDelivererIDs() || []; + const $sotd = this.getSotDSection(); + const $sotdLinks = $sotd && $sotd.find('a[href]'); + + if (ids.length > 0 || !$sotdLinks?.length) { + this.#delivererIDs = ids; + return ids; + } + + 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, + () => {} + ) + ); + } + } + }); + + // Cache the promise (to avoid duplicate requests) + this.#delivererIDs = Promise.all(promiseArray).then(ids => + ids.filter(id => typeof id !== 'undefined') + ); + return this.#delivererIDs; + } + + getDataDelivererIDs() { + const ids: number[] = []; + const $sotd = this.getSotDSection(); + const $deliver = $sotd && $sotd.find('[data-deliverer]'); + if ($deliver && $deliver.length > 0) { + $deliver.each((_, el) => { + const deliverers = el.attribs['data-deliverer'] + .trim() + .split(/[,\s]+/); + deliverers.forEach(id => { + if (/\d+/.test(id)) ids.push(parseInt(id, 10)); + }); + }); + } + return ids; + } + + /** Finds the current charter(s) of the document. */ + async getChartersData() { + if (this.#chartersData) return this.#chartersData; + + const deliverers = await this.getDelivererIDs(); + if (!deliverers.length) { + this.#chartersData = []; + return this.#chartersData; + } + + const docDate = this.getDocumentDate()!; + 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) + ); + }, + () => {} + ) + ); + }); + + this.#chartersData = Promise.all(delivererPromises).then(lists => + lists.flat().filter(charter => !!charter) + ); + return this.#chartersData; + } + + async getCharters() { + if (this.#charters) return this.#charters; + + this.#charters = (await this.getChartersData()).map(({ uri }) => uri); + return this.#charters; + } + + /** + * Checks if this document is a FP document. + * For shortname change document, data-previous-shortname attribute is needed. + */ + async isFP() { + if (typeof this.#isFirstPublic !== 'undefined') + return this.#isFirstPublic; + + this.#isFirstPublic = !(await this.getPreviousVersion()); + return this.#isFirstPublic; + } + + /** + * Gets previous version link from API via shortname. + */ + async getPreviousVersion() { + if (this.#previousVersion) return this.#previousVersion; + + const dts = this.extractHeaders(); + const shortname = this.#shortname || (await this.getShortname()); + + if (!shortname) { + this.error( + { + name: 'generic.shortname', + section: 'front-matter', + rule: 'docIDThisVersion', + }, + 'not-found' + ); + return; + } + + const shortnameHistory: Promise = + w3cApi + .specification(shortname) + .versions() + .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; + }); + return this.#previousVersion; + } + + transition(options: TransitionOptions) { + const documentDate = this.getDocumentDate(); + if (documentDate && 'from' in options && documentDate < options.from) + options.doBefore(); + else if (documentDate && 'to' in options && documentDate > options.to) + options.doAfter(); + else options.doMeanwhile(); + } + + getRecMetadata(meta: RecMetadata = {}) { + const sotdText = this.norm(this.getSotDSection()!.text()); + + // proposed corrections, proposed additions, proposed amendments: + const { SOTD_P_COR, SOTD_P_ADD, SOTD_P_COR_ADD } = REC_TEXT; + if (sotdText.match(new RegExp(`${SOTD_P_COR}|${SOTD_P_COR_ADD}`, 'i'))) + meta.pSubChanges = true; + if (sotdText.match(new RegExp(`${SOTD_P_ADD}|${SOTD_P_COR_ADD}`, 'i'))) + meta.pNewFeatures = true; + + // candidate corrections, candidate additions, candidate amendments: + const { SOTD_C_COR, SOTD_C_ADD, SOTD_C_COR_ADD } = REC_TEXT; + if (sotdText.match(new RegExp(`${SOTD_C_COR}|${SOTD_C_COR_ADD}`, 'i'))) + meta.cSubChanges = true; + if (sotdText.match(new RegExp(`${SOTD_C_ADD}|${SOTD_C_COR_ADD}`, 'i'))) + meta.cNewFeatures = true; + + return meta; + } +} diff --git a/lib/rules-generic-sections.ts b/lib/rules-generic-sections.ts new file mode 100644 index 000000000..ae7a8c874 --- /dev/null +++ b/lib/rules-generic-sections.ts @@ -0,0 +1,187 @@ +export interface GenericRulesSection { + name: string; + rules: Record; +} + +export default { + format: { + name: '1. Normative Document Representation', + rules: { + normativeVersion: + 'At least one normative representation must be available for requests that use the "This Version" URI. More than one normative representation may be delivered in response to such requests. A "This Version" URI must not be used to identify a non-normative representation.', + validHTML: + 'recursive All normative representations must validate as HTML5 with the following limitations:
  • Inline markup for MathML is permitted but should use a (fallback) alternative.
  • If the HTML5 validator issues content warnings, the publication request must include rationale why the warning is not problematic.
', + visualStyle: + 'Visual styles should not vary significantly among normative alternatives.', + }, + }, + metadata: { + name: '2. Document Metadata', + rules: { + goodStylesheet: + 'recursive Each document must include the following absolute URI to identify a style sheet for this maturity level: https://www.w3.org/StyleSheets/TR/2021/W3C-@{param1}

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

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

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

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

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

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

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

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

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

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

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

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

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

See suggestions on translations in the manual of style.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

or

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

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

Group Notes are not endorsed by W3C nor its Members.

or

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      there should be a paragraph with class="correction"

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

      there should be a paragraph with class="addition"

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    or

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

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

    Group Notes are not endorsed by W3C nor its Members.

    or

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    See suggestions on translations in the manual of style.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    " - } - }, - "compound": { - "name": "8. Compound Documents", - "rules": { - "compoundFilesLocation": "If the document is compound (i.e., if it consists of more than one file), all the files must be under a directory /TR/YYYY/@{param1}-shortname-YYYYMMDD/. Exceptions are resources under these paths:
    1. https://www.w3.org/StyleSheets/
    2. https://www.w3.org/scripts/
    ", - "compoundOverview": "The main page should be called Overview.html.", - "compound": "All other files must be reachable by links from the document." - } - }, - "rec-edit-install": { - "name": "1. Install the document.", - "rules": { - "install": "(if new dated version) Copy the original REC in /TR into a new dated <new date> directory in /TR" - } - }, - "rec-edit-stylesheet": { - "name": "2. Use the corresponding stylesheet", - "rules": { - "stylesheet": "For the W3C stylesheet, use the @{param1} version of the W3C stylesheet in use in the REC (old, or 2021). So, it's one of:
    1. https://www.w3.org/StyleSheets/TR/W3C-@{param1}.css
    2. https://www.w3.org/StyleSheets/TR/2021/W3C-@{param1}.css
    3. or reuse the (some?) content from https://www.w3.org/TR/html50/superseded.css as needed
    " - } - }, - "rec-edit-w3c-state": { - "name": "3. Change the title of the document", - "rules": { - "w3c-state": "In the <p id=\"w3c-state\"> element containing the state and date (\"W3C Recommendation DD Month YYYY\"), add:
    1. a <br> element after the original date
    2. and \"@{param1} <new date>\" when the @{param1} document gets published by the webmaster
    " - } - }, - "rec-edit-version": { - "name": "4. Set the \"This version\" link", - "rules": { - "version": "(if new dated version) This version link gets the form https://www.w3.org/TR/YYYY/@{param1}-<shortname-with-version>-<newdate>/" - } - }, - "rec-edit-previous": { - "name": "5. Set the \"Previous version\" link", - "rules": { - "previous": "(if new dated version) Previous version becomes the dated link of the REC being @{param1}" - } - }, - "rec-edit-sotd": { - "name": "6. Change the \"status of this document\" section", - "rules": { - "sotd": "The SOTD gets updated as follows:
    • 6.1. add the following markup (update link to proper Process version if needed):
      <p>This specification is a <a href=\"https://www.w3.org/policies/process/20250818/#rec-rescind\">@{param1} Recommendation</a>. A newer specification exists that is recommended for new adoption in place of this specification. </p>
    • 6.2. Make sure to update or remove the sentence indicating that the document \"is a Recommendation\" if one appears.
    • 6.3. Remove the following paragraph:
      <p>This document has been reviewed by W3C Members, by software developers, and by other W3C groups and interested parties, and is endorsed by the Director as a W3C Recommendation. It is a stable document and may be used as reference material or cited from another document. W3C's role in making the Recommendation is to draw attention to the specification and to promote its widespread deployment. This enhances the functionality and interoperability of the Web.</p>
    • 6.4. ... and replace it with (update the @@ as needed):
      <p>For purposes of the W3C Patent Policy, this @{param1} Recommendation  has the same status as an active Recommendation; it retains licensing  commitments and remains available as a reference for old -- and  possibly still deployed -- implementations, but is not recommended for future implementation. New implementations should follow the <a href='@@'>latest version</a> of the @@ specification.</p>
    • 6.5. DO NOT TOUCH THE PATENT POLICY PARAGRAPH. The document is still governed by IP commitments even if it is @{param2} (as described in the paragraph you added in the previous step).
    • 6.6. Look at the resulting SOTD, make sure it makes sense and \"modify as needed\". In other words, if you need to add more explanations about why the document was @{param2}, feel free to do so by adding to the first paragraph introduced by 6.1. The Director may ask to add or modify specific wordings as well. Just remember to keep the time spent as minimal since our goal is to minimize the time spent @{param3} documents, so don't overdo it.@{param4}
    " - } - } - } - } -} diff --git a/lib/rules/echidna/deliverer-change.js b/lib/rules/echidna/deliverer-change.js deleted file mode 100644 index 902e514f1..000000000 --- a/lib/rules/echidna/deliverer-change.js +++ /dev/null @@ -1,59 +0,0 @@ -import w3cApi from 'node-w3capi'; - -const self = { - name: 'echidna.deliverer-change', - section: 'metadata', - rule: 'delivererID', -}; - -export const { name } = self; - -function getPreviousDelivererIDs(shortname, previousUrl) { - const date = previousUrl.match(/-(\d{8})\/$/); - if (date) - return new Promise(resolve => { - // e.g. https://api.w3.org/specifications/WGSL/versions/20230221/deliverers - w3cApi - .specification(shortname) - .version(date[1]) - .deliverers() - .fetch({ embed: true }, (err, data) => { - resolve(data && data.map(obj => obj.id)); - }); - }); - return new Promise(resolve => { - resolve([]); - }); -} - -/** - * @param sr - * @param done - */ -export async function check(sr, done) { - const previousVersion = await sr.getPreviousVersion(sr); - const shortname = await sr.getShortname(sr); - - if (!previousVersion) { - return done(); - } - - const previousDelivererIDs = await getPreviousDelivererIDs( - shortname, - previousVersion - ); - const delivererIDs = await sr.getDelivererIDs(); - - const delivererChanged = - delivererIDs.sort().toString() !== - previousDelivererIDs.sort().toString(); - - if (delivererChanged) { - sr.error(self, 'deliverer-changed', { - this: delivererIDs.sort().toString(), - previous: previousDelivererIDs.sort().toString(), - }); - } - - done(); -} diff --git a/lib/rules/echidna/deliverer-change.ts b/lib/rules/echidna/deliverer-change.ts new file mode 100644 index 000000000..ec8f4ab45 --- /dev/null +++ b/lib/rules/echidna/deliverer-change.ts @@ -0,0 +1,52 @@ +// @ts-ignore (no typings) +import w3cApi from 'node-w3capi'; +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; + +const self: RuleMeta = { + name: 'echidna.deliverer-change', + section: 'metadata', + rule: 'delivererID', +}; + +export const { name } = self; + +async function getPreviousDelivererIDs( + shortname: string, + previousUrl: string +): Promise { + const date = previousUrl.match(/-(\d{8})\/$/); + if (!date) return Promise.resolve([]); + + // e.g. https://api.w3.org/specifications/WGSL/versions/20230221/deliverers + const data: { id: number }[] = await w3cApi + .specification(shortname) + .version(date[1]) + .deliverers() + .fetch({ embed: true }) + .catch(() => []); + return data.map(({ id }) => id); +} + +export const check: RuleCheckFunction = async context => { + const previousVersion = await context.getPreviousVersion(); + const shortname = await context.getShortname(); + + if (!previousVersion || !shortname) return; + + const previousDelivererIDs = await getPreviousDelivererIDs( + shortname, + previousVersion + ); + const delivererIDs = await context.getDelivererIDs(); + + const delivererChanged = + delivererIDs.sort().toString() !== + previousDelivererIDs.sort().toString(); + + if (delivererChanged) { + context.error(self, 'deliverer-changed', { + this: delivererIDs.sort().toString(), + previous: previousDelivererIDs.sort().toString(), + }); + } +}; diff --git a/lib/rules/echidna/todays-date.js b/lib/rules/echidna/todays-date.ts similarity index 57% rename from lib/rules/echidna/todays-date.js rename to lib/rules/echidna/todays-date.ts index 8404f3f7d..3c3ba94fe 100644 --- a/lib/rules/echidna/todays-date.js +++ b/lib/rules/echidna/todays-date.ts @@ -1,4 +1,6 @@ -const self = { +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; + +const self: RuleMeta = { name: 'echidna.todays-date', section: 'front-matter', // @TODO: update this selector... when the rule is added to the JSON. @@ -7,11 +9,7 @@ const self = { export const { name } = self; -/** - * @param sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = 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. @@ -19,15 +17,14 @@ export function check(sr, done) { * @param {Date} date - The date to extract the day, month and year from * @returns {number} number of milliseconds between 1 January 1970 and `date` */ - function getDateTime(date) { + function getDateTime(date: Date) { return new Date(date.getTime()).setHours(0, 0, 0, 0); } - if (!(sr.getDocumentDate() instanceof Date)) { - sr.error(self, 'date-not-detected'); - } else if (getDateTime(sr.getDocumentDate()) !== getDateTime(new Date())) { - sr.error(self, 'wrong-date'); + const documentDate = context.getDocumentDate(); + if (!(documentDate instanceof Date)) { + context.error(self, 'date-not-detected'); + } else if (getDateTime(documentDate) !== getDateTime(new Date())) { + context.error(self, 'wrong-date'); } - - done(); -} +}; diff --git a/lib/rules/headers/copyright.js b/lib/rules/headers/copyright.ts similarity index 60% rename from lib/rules/headers/copyright.js rename to lib/rules/headers/copyright.ts index 3e6139396..f96610ea4 100644 --- a/lib/rules/headers/copyright.js +++ b/lib/rules/headers/copyright.ts @@ -7,13 +7,16 @@ * 4. For "copyright-software", the url is https://www.w3.org/copyright/software-license/, the dated url is https://www.w3.org/copyright/software-license-2023/, they are both allowed. The name in the API is "W3C Software and Document License", but the document would use text "permissive document license". * 5. For "copyright-documents", the url is https://www.w3.org/document/document-license/, the dated url is https://www.w3.org/copyright/document-license-2023/. The name in the API is "W3C Document License", but the document would use text "document use". */ -import { AB, TAG, importJSON } from '../../util.js'; +import type { Cheerio } from 'cheerio'; +import type { Element } from 'domhandler'; -/** @import { Cheerio } from "cheerio" */ -/** @import { Element } from "domhandler" */ -/** @import { Specberus } from "../../validator.js" */ +import { AB, TAG } from '../../util.js'; +import type { RuleContext } from '../../rule-context.js'; -const self = { +import copyrightExceptions from '../../copyright-exceptions.js'; +import type { ApiCharter, RuleCheckFunction, RuleMeta } from '../../types.js'; + +const self: RuleMeta = { name: 'headers.copyright', section: 'front-matter', rule: 'copyright', @@ -30,12 +33,12 @@ const LICENSE_CD_URL = 'https://www.w3.org/copyright/document-license/'; const LICENSE_CD_DATED_URL = 'https://www.w3.org/copyright/document-license-2023/'; -const LICENSE_URL_TEXT_MAP = { +const LICENSE_URL_TEXT_MAP: Record = { [LICENSE_CD_URL]: LICENSE_CD_TEXT, [LICENSE_CS_URL]: LICENSE_CS_TEXT, }; -const LICENSE_TEXT_LINKS_MAP = { +const LICENSE_TEXT_LINKS_MAP: Record = { [LICENSE_CD_TEXT]: [LICENSE_CD_URL, LICENSE_CD_DATED_URL], [LICENSE_CS_TEXT]: [LICENSE_CS_URL, LICENSE_CS_DATED_URL], }; @@ -47,80 +50,88 @@ const latestBaseLinks = { trademark: 'https://www.w3.org/policies/#W3C_Trademarks', }; -const copyrightExceptions = importJSON( - '../../copyright-exceptions.json', - import.meta.url -); - export const { name } = self; -/** - * @param {Specberus} sr - */ -async function isOnlyPublishedByTagOrAb(sr) { - const TagID = TAG.id; - const AbID = AB.id; - - const delivererIDs = await sr.getDelivererIDs(); - return delivererIDs.every(id => [TagID, AbID].includes(id)); +async function isOnlyPublishedByTagOrAb(context: RuleContext) { + const delivererIDs = await context.getDelivererIDs(); + return delivererIDs.every(id => id === TAG.id || id === AB.id); } // Create an array of license URIs per group -> array of arrays // Then find the intersection of all arrays, to extract union license(s) -function getCommonLicenseUri(data) { +function getCommonLicenseUri(data: ApiCharter[]) { return data .map(charter => charter['doc-licenses'].map(license => license.uri)) .reduce((first, cur) => first.filter(uri => cur.includes(uri))); } // The date can be 19xx-2023, or 2023. -/** - * @param {Specberus} sr - * @param licenseTexts - */ -function getLatestCopyrightMatchRegex(sr, licenseTexts) { +function getLatestCopyrightMatchRegex( + 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( '@YEAR', - year + '' + year ); const endRex = `\\. W3C[®|®] liability, trademark,? and (${licenseRex}) rules apply\\.$`; return new RegExp(startRex + endRex); } -/** - * @param {Specberus} sr - * @param {Cheerio} $copyright - * @param licenseTexts - * @param baseLinks - * @param matchRegex - */ -function checkCopyright(sr, $copyright, licenseTexts, baseLinks, matchRegex) { - const regResult = sr.norm($copyright.text()).match(matchRegex); +// Some documents like epub-33 uses special copyrights listed in copyright-exception.json +function checkSpecialCopyright( + context: RuleContext, + $copyright: Cheerio, + specialCopyright: (typeof copyrightExceptions)[number], + shortname: string | undefined +) { + const year = (context.getDocumentDate() || new Date()).getFullYear(); + + const domHtml = context.norm($copyright.html()!); + const specHtml = context.norm( + specialCopyright.copyright.replace(/@YEAR/g, '' + year) + ); + if (domHtml !== specHtml) { + context.error(self, 'exception-no-html', { + copyright: domHtml, + expected: specHtml, + shortname, + }); + } +} + +function checkLatestCopyright( + context: RuleContext, + $copyright: Cheerio, + licenseTexts: string[] +) { + 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; } const [, licenseInDoc] = regResult; const allowBothLicense = licenseTexts.length === 2; const textsToCheck = allowBothLicense ? [licenseInDoc] : licenseTexts; // When document can have 2 licenses, keep the one mentioned in document. - const linksToCheck = textsToCheck.reduce( + const linksToCheck: Record = textsToCheck.reduce( (acc, v) => ({ ...acc, [v]: LICENSE_TEXT_LINKS_MAP[v] }), - baseLinks + latestBaseLinks ); const links = $copyright.find('a').toArray(); 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; @@ -131,7 +142,7 @@ function checkCopyright(sr, $copyright, licenseTexts, baseLinks, matchRegex) { : 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, @@ -140,68 +151,31 @@ function checkCopyright(sr, $copyright, licenseTexts, baseLinks, matchRegex) { }); } -// Some documents like epub-33 uses special copyrights listed in copyright-exception.json -/** - * @param {Specberus} sr - * @param {Cheerio} $copyright - * @param specialCopyright - * @param shortname - */ -function checkSpecialCopyright(sr, $copyright, specialCopyright, shortname) { - const year = (sr.getDocumentDate() || new Date()).getFullYear(); - - const domHtml = sr.norm($copyright.html()); - const specHtml = sr.norm( - specialCopyright.copyright.replace(/@YEAR/g, year) - ); - if (domHtml !== specHtml) { - sr.error(self, 'exception-no-html', { - copyright: domHtml, - expected: specHtml, - shortname, - }); - } -} - -/** - * @param {Specberus} sr - * @param {Cheerio} $copyright - * @param licenseTexts - */ -function checkLatestCopyright(sr, $copyright, licenseTexts) { - const matchRegex = getLatestCopyrightMatchRegex(sr, licenseTexts); - checkCopyright(sr, $copyright, licenseTexts, latestBaseLinks, matchRegex); -} - -/** - * @param {Specberus} sr - * @param done - */ -export async function check(sr, done) { - 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'); - return done(); + context.error(self, 'not-found'); + return; } - if (await isOnlyPublishedByTagOrAb(sr)) { - return done(); + 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'); - return done(); + 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'); - return done(); + context.error(self, 'no-license-found-joint'); + return; } if (!allowedLicenses.length) { - sr.error(self, 'no-license-found'); - return done(); + context.error(self, 'no-license-found'); + return; } // licenseTexts: ['permissive document license'] or ['document use'] or ['document use', 'permissive document license'] @@ -210,16 +184,14 @@ export async function check(sr, done) { .map(v => LICENSE_URL_TEXT_MAP[v]); // get exception rule for certain shortnames - const shortname = await sr.getShortname(); - const specialCopyright = copyrightExceptions.find(({ specShortnames }) => - specShortnames.includes(shortname) + 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); } - - return done(); -} +}; diff --git a/lib/rules/headers/details-summary.js b/lib/rules/headers/details-summary.js deleted file mode 100644 index 6d61f8029..000000000 --- a/lib/rules/headers/details-summary.js +++ /dev/null @@ -1,45 +0,0 @@ -/* check if the document's headers link are in */ - -/** @import { Specberus } from "../../validator.js" */ - -const self = { - name: 'headers.details-summary', - section: 'front-matter', - rule: 'docIDFormat', -}; - -export const { name } = self; - -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - const $details = sr.$('.head details').first(); - if (!$details.length) { - sr.error(self, 'no-details'); - return done(); - } - - if (!$details.attr('open')) { - sr.error(self, 'no-details-open'); - } - - if (!sr.$('.head details dl').length) { - sr.error(self, 'no-details-dl'); - return done(); - } - - const $summary = sr.$('.head details summary').first(); - if (!$summary.length) { - sr.error(self, 'no-details-summary'); - return done(); - } - - 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/details-summary.ts b/lib/rules/headers/details-summary.ts new file mode 100644 index 000000000..d62444ade --- /dev/null +++ b/lib/rules/headers/details-summary.ts @@ -0,0 +1,39 @@ +/* check if the document's headers link are in */ + +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; + +const self: RuleMeta = { + name: 'headers.details-summary', + section: 'front-matter', + rule: 'docIDFormat', +}; + +export const { name } = self; + +export const check: RuleCheckFunction = context => { + const $details = context.$('.head details').first(); + if (!$details.length) { + context.error(self, 'no-details'); + return; + } + + if (!$details.attr('open')) { + context.error(self, 'no-details-open'); + } + + if (!context.$('.head details dl').length) { + context.error(self, 'no-details-dl'); + return; + } + + const $summary = context.$('.head details summary').first(); + if (!$summary.length) { + context.error(self, 'no-details-summary'); + return; + } + + const summaryText = context.norm($summary.text()); + if (summaryText !== 'More details about this document') { + context.error(self, 'wrong-summary-text'); + } +}; diff --git a/lib/rules/headers/div-head.js b/lib/rules/headers/div-head.js deleted file mode 100644 index edd542c97..000000000 --- a/lib/rules/headers/div-head.js +++ /dev/null @@ -1,19 +0,0 @@ -/* emits: 'not-found' */ - -/** @import { Specberus } from "../../validator.js" */ - -const self = { - name: 'headers.div-head', - section: 'front-matter', - rule: 'divClassHead', -}; - -export const { name } = self; - -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - sr.checkSelector('body div.head', self, done); -} diff --git a/lib/rules/headers/div-head.ts b/lib/rules/headers/div-head.ts new file mode 100644 index 000000000..e1b8197b3 --- /dev/null +++ b/lib/rules/headers/div-head.ts @@ -0,0 +1,15 @@ +/* emits: 'not-found' */ + +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; + +const self: RuleMeta = { + name: 'headers.div-head', + section: 'front-matter', + rule: 'divClassHead', +}; + +export const { name } = self; + +export const check: RuleCheckFunction = context => { + context.checkSelector('body div.head', self); +}; diff --git a/lib/rules/headers/dl.js b/lib/rules/headers/dl.ts similarity index 67% rename from lib/rules/headers/dl.js rename to lib/rules/headers/dl.ts index 4f627d142..822a2881f 100644 --- a/lib/rules/headers/dl.js +++ b/lib/rules/headers/dl.ts @@ -5,92 +5,96 @@ // #w3c-state date and this version date must match // dt for editor or author -import { resolveGithubUsernameToId } from '../../util.js'; +import type { Cheerio } from 'cheerio'; +import type { Element } from 'domhandler'; -/** @import { Cheerio } from "cheerio" */ -/** @import { Element } from "domhandler" */ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; +import { resolveGithubUsernameToId } from '../../util.js'; +import type { RuleContext } from '../../rule-context.js'; -const self = { +const self: RuleMeta = { name: 'headers.dl', section: 'front-matter', rule: 'docIDFormat', }; -const thisError = { +const thisError: RuleMeta = { name: 'headers.dl', section: 'front-matter', rule: 'docIDThisVersion', }; -const latestError = { +const latestError: RuleMeta = { name: 'headers.dl', section: 'front-matter', rule: 'docIDLatestVersion', }; -const historyError = { +const historyError: RuleMeta = { name: 'headers.dl', section: 'front-matter', rule: 'docIDHistory', }; -const editorError = { +const editorError: RuleMeta = { name: 'headers.dl', section: 'front-matter', rule: 'editorSection', }; export const name = self.name; +interface CheckLinkOptions { + $element: Cheerio; + linkName: string; + mustHave?: boolean; + rule?: RuleMeta; + context: RuleContext; +} + /** * Check if link and href are consistent. - * @param {Object} object place to find , current error message title when there is error. - * @param {string} object.linkName - * @param {boolean=} object.mustHave - * @param {Object=} object.rule - * @param {Specberus} object.sr - * @param {Cheerio} object.$element * @returns boolean whether element exists and can continue */ -function checkLink({ sr, rule = self, $element, linkName, mustHave = true }) { +function checkLink({ + context, + rule = self, + $element, + linkName, + mustHave = true, +}: 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 href = $element.attr('href').trim(); - if (href !== text) sr.error(rule, 'link-diff', { text, href, linkName }); + const text = context.norm($element.text()).trim(); + const href = ($element.attr('href') || '').trim(); + if (href !== text) + context.error(rule, 'link-diff', { text, href, linkName }); return true; } -/** - * @param {Specberus} sr - * @param done - */ -export async function check(sr, done) { - const { rescinds } = sr.config; - const subType = sr.config.submissionType; +export const check: RuleCheckFunction = async context => { + const { rescinds, status, submissionType } = context.config!; let topLevel = 'TR'; - let thisURI = ''; - if (subType === 'member') topLevel = 'submissions'; + if (submissionType === 'member') topLevel = 'submissions'; - const dts = sr.extractHeaders(); - if (!dts.This) sr.error(self, 'this-version'); - 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', @@ -98,14 +102,13 @@ export async function check(sr, done) { if (exist) { const vThisRex = `^https:\\/\\/www\\.w3\\.org\\/${topLevel}\\/(\\d{4})\\/${ - sr.config.status || '[A-Z]+' + status || '[A-Z]+' }-(.+)-(\\d{4})(\\d\\d)(\\d\\d)\\/?$`; matches = ($linkThis.attr('href') || '') .trim() .match(new RegExp(vThisRex)); - const docDate = sr.getDocumentDate(); + const docDate = context.getDocumentDate(); if (matches) { - [thisURI] = matches; const year = +matches[1]; const year2 = +matches[3]; const month = +matches[4]; @@ -117,16 +120,16 @@ export async function check(sr, done) { 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', @@ -139,7 +142,7 @@ export async function check(sr, done) { .match(new RegExp(lateRex)); if (!matches) { - sr.error(latestError, 'latest-syntax'); + context.error(latestError, 'latest-syntax'); } } } @@ -147,7 +150,7 @@ export async function check(sr, done) { if (dts.History) { const $linkHistory = dts.History.$dd.find('a').first(); checkLink({ - sr, + context, rule: historyError, $element: $linkHistory, linkName: 'History', @@ -157,7 +160,7 @@ export async function check(sr, done) { if (dts.Rescinds) { const $linkRescinds = dts.Rescinds.$dd.find('a').first(); const exist = checkLink({ - sr, + context, rule: self, $element: $linkRescinds, linkName: 'Rescinds this Recommendation', @@ -171,23 +174,23 @@ export async function check(sr, done) { ); if (!matches) { - sr.error(self, 'rescinds-syntax'); + context.error(self, 'rescinds-syntax'); } } } // check "Implementation report" link. Unless in Sotd saying there's none. const needImplementation = - ['CR', 'CRD', 'PR', 'REC'].indexOf(sr.config.status) !== -1; - const $sotd = sr.getSotDSection(); + ['CR', 'CRD', 'PR', 'REC'].indexOf(status) !== -1; + const $sotd = context.getSotDSection(); const noImplementation = - sr - .norm($sotd && $sotd.text()) + 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', @@ -195,33 +198,32 @@ export async function check(sr, done) { }); if ( implementationExist && - !$linkImplementation - .attr('href') + !($linkImplementation.attr('href') || '') .trim() .toLowerCase() .startsWith('https://') ) { - sr.error(self, 'implelink-should-be-https', { - link: $linkImplementation.attr('href'), + 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', }); if (exist) { - const editorsDraft = $editorsDraftElement.attr('href'); + const editorsDraft = $editorsDraftElement.attr('href') || ''; if (!editorsDraft.trim().toLowerCase().startsWith('https://')) - sr.error(self, 'editors-draft-should-be-https', { + context.error(self, 'editors-draft-should-be-https', { link: editorsDraft, }); } @@ -232,15 +234,15 @@ export async function check(sr, done) { 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(', '), }); } @@ -250,27 +252,24 @@ export async function check(sr, done) { .filter(el => el.attribs['data-editor-github']) .map(el => el.attribs['data-editor-github']); - /** @type string[] */ - const unresolvedUsernames = []; + const unresolvedUsernames: string[] = []; for (const username of githubUsernames) { try { if (!(await resolveGithubUsernameToId(username))) 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'); } - - done(); -} +}; diff --git a/lib/rules/headers/editor-participation.js b/lib/rules/headers/editor-participation.ts similarity index 59% rename from lib/rules/headers/editor-participation.js rename to lib/rules/headers/editor-participation.ts index 1210d904c..43d87a039 100644 --- a/lib/rules/headers/editor-participation.js +++ b/lib/rules/headers/editor-participation.ts @@ -1,28 +1,24 @@ +// @ts-ignore (No typings) import w3cApi from 'node-w3capi'; import { resolveGithubUsernameToId } from '../../util.js'; +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -/** @import { Specberus } from "../../validator.js" */ - -const self = { +const self: RuleMeta = { name: 'headers.editor-participation', section: 'front-matter', rule: 'editorSection', }; export const name = self.name; -/** - * @param {Specberus} sr - * @param done - */ -export async function check(sr, done) { - 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) @@ -40,27 +36,20 @@ export async function check(sr, done) { } } - const groupUsersPromises = []; - groups.forEach(id => { - groupUsersPromises.push( - new Promise(resolve => - w3cApi - .group(id) - .users() - .fetch({ embed: true }, (err, data) => { - resolve(data.map(user => user.id)); - }) - ) - ); + const groupUsersPromises = groups.map(async id => { + const data: { id: number }[] = await w3cApi + .group(id) + .users() + .fetch({ embed: true }) + .catch(() => []); + return data.map(({ id }) => id); }); const userIds = (await Promise.all(groupUsersPromises)).flat(); editorsToCheck.forEach(id => { if (!userIds.includes(id)) { - sr.error(self, 'not-participating', { id }); + context.error(self, 'not-participating', { id }); } }); - - done(); -} +}; diff --git a/lib/rules/headers/errata.js b/lib/rules/headers/errata.js deleted file mode 100644 index a68157cf5..000000000 --- a/lib/rules/headers/errata.js +++ /dev/null @@ -1,40 +0,0 @@ -// errata, right after dl - -/** @import { Specberus } from "../../validator.js" */ - -const self = { - name: 'headers.errata', - section: 'front-matter', - rule: 'docIDOrder', -}; - -export const { name } = self; - -// Check if document is Recommendation, and uses inline changes(REC with Candidate/Proposed changes) -function isRECWithChanges(sr) { - if (sr.config.status !== 'REC') { - return false; - } - - const recMeta = sr.getRecMetadata({}); - return Object.values(recMeta).length !== 0; -} - -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - // for REC with Candidate/Proposed changes, no need to check errata link - if (isRECWithChanges(sr)) { - return done(); - } - - const dts = sr.extractHeaders(); - // Check 'Errata:' exist, don't check any further. - if (!dts.Errata) { - sr.error(self, 'no-errata'); - return done(); - } - return done(); -} diff --git a/lib/rules/headers/errata.ts b/lib/rules/headers/errata.ts new file mode 100644 index 000000000..116110cd9 --- /dev/null +++ b/lib/rules/headers/errata.ts @@ -0,0 +1,29 @@ +// errata, right after dl + +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; +import type { RuleContext } from '../../rule-context.js'; + +const self: RuleMeta = { + name: 'headers.errata', + section: 'front-matter', + rule: 'docIDOrder', +}; + +export const { name } = self; + +// Check if document is Recommendation, and uses inline changes(REC with Candidate/Proposed changes) +function isRECWithChanges(context: RuleContext) { + if (context.config!.status !== 'REC') return false; + + const recMeta = context.getRecMetadata(); + return Object.values(recMeta).length !== 0; +} + +export const check: RuleCheckFunction = context => { + // for REC with Candidate/Proposed changes, no need to check errata link + if (isRECWithChanges(context)) return; + + const dts = context.extractHeaders(); + // Check 'Errata:' exist, don't check any further. + if (!dts.Errata) context.error(self, 'no-errata'); +}; diff --git a/lib/rules/headers/github-repo.js b/lib/rules/headers/github-repo.ts similarity index 71% rename from lib/rules/headers/github-repo.js rename to lib/rules/headers/github-repo.ts index 72b687121..c46513eb5 100644 --- a/lib/rules/headers/github-repo.js +++ b/lib/rules/headers/github-repo.ts @@ -2,9 +2,9 @@ // must include a public archived place to send comments to. // below: -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'headers.github-repo', section: 'front-matter', rule: 'docIDOrder', @@ -12,21 +12,16 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - const dts = sr.extractHeaders(); - // Check 'Feedback:' exist +export const check: RuleCheckFunction = context => { + const dts = context.extractHeaders(); if (!dts.Feedback) { - sr.error(self, 'no-feedback'); - return done(); + 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() @@ -42,7 +37,5 @@ export function check(sr, done) { // href // ); }); - if (!foundRepo) sr.error(self, 'no-repo'); - - done(); -} + if (!foundRepo) context.error(self, 'no-repo'); +}; diff --git a/lib/rules/headers/h1-title.js b/lib/rules/headers/h1-title.js deleted file mode 100644 index 97597e2c2..000000000 --- a/lib/rules/headers/h1-title.js +++ /dev/null @@ -1,30 +0,0 @@ -// must have h1, with same content as title - -/** @import { Specberus } from "../../validator.js" */ - -const self = { - name: 'headers.h1-title', - section: 'front-matter', - rule: 'title', -}; - -export const { name } = self; - -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - const $title = sr.$('head > title').first(); - const $h1 = sr.$('body div.head h1').first(); - if (!$title.length || !$h1.length) { - sr.error(self, 'not-found'); - } else { - const titleText = sr.norm($title.text()); - $h1.html($h1.html().replace(/:
    /g, ': ').replace(/
    /g, ' - ')); - const h1Text = sr.norm($h1.text()); - if (titleText !== h1Text) - sr.error(self, 'not-match', { titleText, h1Text }); - } - done(); -} diff --git a/lib/rules/headers/h1-title.ts b/lib/rules/headers/h1-title.ts new file mode 100644 index 000000000..c226c3662 --- /dev/null +++ b/lib/rules/headers/h1-title.ts @@ -0,0 +1,25 @@ +// must have h1, with same content as title + +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; + +const self: RuleMeta = { + name: 'headers.h1-title', + section: 'front-matter', + rule: 'title', +}; + +export const { name } = self; + +export const check: RuleCheckFunction = context => { + const $title = context.$('head > title').first(); + const $h1 = context.$('body div.head h1').first(); + if (!$title.length || !$h1.length) { + context.error(self, 'not-found'); + } else { + const titleText = context.norm($title.text()); + $h1.html($h1.html()!.replace(/:
    /g, ': ').replace(/
    /g, ' - ')); + const h1Text = context.norm($h1.text()); + if (titleText !== h1Text) + context.error(self, 'not-match', { titleText, h1Text }); + } +}; diff --git a/lib/rules/headers/h2-toc.js b/lib/rules/headers/h2-toc.js deleted file mode 100644 index 2e643c213..000000000 --- a/lib/rules/headers/h2-toc.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Check the presence of a valid ToC. - */ - -/** @import { Specberus } from "../../validator.js" */ - -const self = { - name: 'headers.h2-toc', - // @TODO: fix the section... when it is fixed in the JSON. - section: 'navigation', - // @TODO: update this selector... when the rule is added to the JSON. - rule: 'toc', -}; - -export const { name } = self; - -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - const EXPECTED_HEADING = /^table\s+of\s+contents$/i; - const $tocNav = sr.$('nav#toc > h2'); - const $tocDiv = sr.$('div#toc > h2'); - let $toc; - - if ($tocDiv.length > 0) { - if ($tocNav.length > 0) sr.error(self, 'mixed'); - else { - sr.warning(self, 'not-html5'); - $toc = $tocDiv; - } - } else if ($tocNav.length > 0) $toc = $tocNav; - else sr.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 (matches > 1) sr.error(self, 'too-many'); - else if (matches === 0) sr.error(self, 'not-found'); - } - - done(); -} diff --git a/lib/rules/headers/h2-toc.ts b/lib/rules/headers/h2-toc.ts new file mode 100644 index 000000000..ce9148af8 --- /dev/null +++ b/lib/rules/headers/h2-toc.ts @@ -0,0 +1,40 @@ +/** + * Check the presence of a valid ToC. + */ + +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; + +const self: RuleMeta = { + name: 'headers.h2-toc', + // @TODO: fix the section... when it is fixed in the JSON. + section: 'navigation', + // @TODO: update this selector... when the rule is added to the JSON. + rule: 'toc', +}; + +export const { name } = self; + +export const check: RuleCheckFunction = context => { + const EXPECTED_HEADING = /^table\s+of\s+contents$/i; + const $tocNav = context.$('nav#toc > h2'); + const $tocDiv = context.$('div#toc > h2'); + let $toc; + + if ($tocDiv.length > 0) { + if ($tocNav.length > 0) context.error(self, 'mixed'); + else { + context.warning(self, 'not-html5'); + $toc = $tocDiv; + } + } else if ($tocNav.length > 0) $toc = $tocNav; + else context.error(self, 'not-found'); + if ($toc && $toc.length > 0) { + let matches = 0; + $toc.each((_, el) => { + if (EXPECTED_HEADING.test(context.norm(context.$(el).text()))) + matches += 1; + }); + if (matches > 1) context.error(self, 'too-many'); + else if (matches === 0) context.error(self, 'not-found'); + } +}; diff --git a/lib/rules/headers/hr.js b/lib/rules/headers/hr.js deleted file mode 100644 index b807e394d..000000000 --- a/lib/rules/headers/hr.js +++ /dev/null @@ -1,24 +0,0 @@ -/** @import { Specberus } from "../../validator.js" */ - -const self = { - name: 'headers.hr', - section: 'front-matter', - rule: 'hrAfterCopyright', -}; - -export const { name } = self; - -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - const hasHrLastChild = sr.$('body div.head > hr:last-child').length === 1; - const hasHrNextSibling = sr.$('body div.head + hr').length === 1; - if (hasHrLastChild && hasHrNextSibling) { - sr.error(self, 'duplicate'); - } else if (!hasHrLastChild && !hasHrNextSibling) { - sr.error(self, 'not-found'); - } - done(); -} diff --git a/lib/rules/headers/hr.ts b/lib/rules/headers/hr.ts new file mode 100644 index 000000000..aba26b7da --- /dev/null +++ b/lib/rules/headers/hr.ts @@ -0,0 +1,20 @@ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; + +const self: RuleMeta = { + name: 'headers.hr', + section: 'front-matter', + rule: 'hrAfterCopyright', +}; + +export const { name } = self; + +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) { + context.error(self, 'duplicate'); + } else if (!hasHrLastChild && !hasHrNextSibling) { + context.error(self, 'not-found'); + } +}; diff --git a/lib/rules/headers/logo.js b/lib/rules/headers/logo.js deleted file mode 100644 index 9874b5f87..000000000 --- a/lib/rules/headers/logo.js +++ /dev/null @@ -1,27 +0,0 @@ -/** @import { Specberus } from "../../validator.js" */ - -const self = { - name: 'headers.logo', - section: 'front-matter', - rule: 'logo', -}; - -export const { name } = self; - -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - const $logo = sr.$("body div.head a[href] > img[src][alt='W3C']").first(); - if ( - !$logo.length || - !/^(https:)?\/\/www\.w3\.org\/StyleSheets\/TR\/2021\/logos\/W3C?$/.test( - $logo.attr('src') - ) || - !/^(https:)?\/\/www\.w3\.org\/?$/.test($logo.parent().attr('href')) - ) { - sr.error(self, 'not-found'); - } - done(); -} diff --git a/lib/rules/headers/logo.ts b/lib/rules/headers/logo.ts new file mode 100644 index 000000000..c4d08ea7f --- /dev/null +++ b/lib/rules/headers/logo.ts @@ -0,0 +1,26 @@ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; + +const self: RuleMeta = { + name: 'headers.logo', + section: 'front-matter', + rule: 'logo', +}; + +export const { name } = self; + +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( + $logo.attr('src') || '' + ) || + !/^(https:)?\/\/www\.w3\.org\/?$/.test( + $logo.parent().attr('href') || '' + ) + ) { + context.error(self, 'not-found'); + } +}; diff --git a/lib/rules/headers/memsub-copyright.js b/lib/rules/headers/memsub-copyright.ts similarity index 61% rename from lib/rules/headers/memsub-copyright.js rename to lib/rules/headers/memsub-copyright.ts index ec4192498..eabb7e376 100644 --- a/lib/rules/headers/memsub-copyright.js +++ b/lib/rules/headers/memsub-copyright.ts @@ -1,4 +1,4 @@ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction } from '../../types.js'; const self = { name: 'headers.memsub-copyright', @@ -6,12 +6,8 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - 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 @@ -23,7 +19,6 @@ export function check(sr, done) { 'https://www.w3.org/copyright/document-license/' ) === 0 ); - if (!seen) sr.error(self, 'not-found'); - } else sr.error(self, 'not-found'); - done(); -} + if (!seen) context.error(self, 'not-found'); + } else context.error(self, 'not-found'); +}; diff --git a/lib/rules/headers/ol-toc.js b/lib/rules/headers/ol-toc.ts similarity index 52% rename from lib/rules/headers/ol-toc.js rename to lib/rules/headers/ol-toc.ts index 7f9680fc8..db55bb107 100644 --- a/lib/rules/headers/ol-toc.js +++ b/lib/rules/headers/ol-toc.ts @@ -2,9 +2,9 @@ * Check the presence of the actual TOC (<ol class="toc">) inside the main navigation element. */ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'headers.ol-toc', section: 'navigation', // @TODO: update this selector... when the rule is added to the JSON. @@ -13,14 +13,8 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - 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'); - - done(); -} + if (!$toc.length) context.warning(self, 'not-found'); +}; diff --git a/lib/rules/headers/secno.js b/lib/rules/headers/secno.ts similarity index 69% rename from lib/rules/headers/secno.js rename to lib/rules/headers/secno.ts index 728451649..43a3a1090 100644 --- a/lib/rules/headers/secno.js +++ b/lib/rules/headers/secno.ts @@ -2,7 +2,7 @@ * Check (more or less) if items in the TOC and headings are wrapped inside <bdi class="secno">. */ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction } from '../../types.js'; const self = { name: 'headers.secno', @@ -10,18 +10,12 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +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'); - - done(); -} + if (!$secnos.length) context.warning(self, 'not-found'); +}; diff --git a/lib/rules/headers/shortname.js b/lib/rules/headers/shortname.ts similarity index 67% rename from lib/rules/headers/shortname.js rename to lib/rules/headers/shortname.ts index 6d2c1eb11..2b313c88b 100644 --- a/lib/rules/headers/shortname.js +++ b/lib/rules/headers/shortname.ts @@ -2,58 +2,47 @@ // this version is https://www.w3.org/TR/2013/WD-shortname-2013MMDD/ // latest version is https://www.w3.org/TR/shortname/ -/** @import { Specberus } from "../../validator.js" */ - import superagent from 'superagent'; -import doAsync from 'doasync'; +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'headers.shortname', section: 'front-matter', rule: 'docIDFormat', }; -const thisError = { +const thisError: RuleMeta = { name: 'headers.shortname', section: 'front-matter', rule: 'docIDThisVersion', }; -const historyError = { +const historyError: RuleMeta = { name: 'headers.shortname', section: 'front-matter', rule: 'docIDHistory', }; export const name = self.name; -/** - * - * @param {Specberus} sr - * @param done - */ -export async function check(sr, done) { - const subType = sr.config.submissionType; +export const check: RuleCheckFunction = async context => { let topLevel = 'TR'; - let thisURI = ''; - if (subType === 'member') topLevel = 'submissions'; + if (context.config!.submissionType === 'member') topLevel = 'submissions'; - const dts = sr.extractHeaders(); + const dts = context.extractHeaders(); - let shortname; - let seriesShortname; - let matches; + let shortname = ''; + let seriesShortname = ''; if (dts.This) { const $linkThis = dts.This.$dd.find('a').first(); if ($linkThis.attr('href')) { const vThisRex = `^https:\\/\\/www\\.w3\\.org\\/${topLevel}\\/(\\d{4})\\/${ - sr.config.status || '[A-Z]+' + context.config!.status || '[A-Z]+' }-(.+)-(\\d{4})(\\d\\d)(\\d\\d)\\/?$`; - matches = ($linkThis.attr('href') || '') + const matches = ($linkThis.attr('href') || '') .trim() .match(new RegExp(vThisRex)); if (matches) { - [thisURI] = matches; [, , shortname] = matches; const pattern = /-?\d[\d.]*$/; const dashPattern = /\d[\d.]*-/; @@ -68,9 +57,11 @@ export async function check(sr, done) { .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, + }); } } } @@ -79,17 +70,16 @@ export async function check(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}\\/(.+?)\\/?$`; - 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 if (sn !== shortname && sn !== seriesShortname) - sr.error(self, 'this-latest-shortname', { + context.error(self, 'this-latest-shortname', { thisShortname: shortname, latestShortname: sn, }); @@ -99,29 +89,29 @@ export async function check(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\/(.+?)\/?$/; - 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 }); + context.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; } - 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( @@ -129,7 +119,7 @@ export async function check(sr, done) { ); if (previousShortname) { // prettier-ignore - sr.warning(historyError, 'this-previous-shortname', + context.warning(historyError, 'this-previous-shortname', { previousShortname, thisShortname: shortname, @@ -141,19 +131,21 @@ export async function check(sr, done) { let previousHistoryStatusCode; try { const res = - await doAsync(superagent).head( - previousHistoryHref - ); + await superagent.head(previousHistoryHref); previousHistoryStatusCode = res.statusCode; } catch (err) { previousHistoryStatusCode = err.status; } if (previousHistoryStatusCode === 404) { - sr.error(historyError, 'history-bad-previous', { - previousShortname, - url: previousHistoryHref, - }); + context.error( + historyError, + 'history-bad-previous', + { + previousShortname, + url: previousHistoryHref, + } + ); } } } @@ -164,9 +156,10 @@ export async function check(sr, done) { if (dts.Rescinds) { const $linkRescinds = dts.Rescinds.$dd.find('a').first(); + const rescindsHref = $linkRescinds.attr('href'); - if ($linkRescinds.attr('href')) { - matches = ($linkRescinds.attr('href') || '') + if (rescindsHref) { + const matches = rescindsHref .trim() .match( /^https:\/\/www\.w3\.org\/TR\/\d{4}\/REC-(.+)-\d{8}\/?$/ @@ -174,7 +167,7 @@ export async function check(sr, done) { if (matches) { sn = matches[1]; if (sn !== shortname) - sr.error(self, 'this-rescinds-shortname', { + context.error(self, 'this-rescinds-shortname', { rescindsShortname: sn, thisShortname: shortname, }); @@ -183,28 +176,26 @@ export async function check(sr, done) { } // 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, }); } - - done(); -} +}; diff --git a/lib/rules/headers/subm-logo.js b/lib/rules/headers/subm-logo.ts similarity index 60% rename from lib/rules/headers/subm-logo.js rename to lib/rules/headers/subm-logo.ts index 0c603b57e..be3292958 100644 --- a/lib/rules/headers/subm-logo.js +++ b/lib/rules/headers/subm-logo.ts @@ -1,34 +1,31 @@ +import type { RuleCheckFunction } from '../../types.js'; + const self = { name: 'headers.subm-logo', }; export const { name } = self; -/** - * @param sr - * @param done - */ -export function check(sr, done) { - 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', src: /^(https:)?\/\/www\.w3\.org\/Icons\/member_subm(\.png|\.gif)?$/, href: /^(https:)?\/\/www\.w3\.org\/submissions\/?$/, }, - }; + } as const; if ( !$logo.length || $logo.attr('alt') !== checks[type].alt || - !checks[type].src.test($logo.attr('src')) || - !checks[type].href.test($logo.parent().attr('href')) + !checks[type].src.test($logo.attr('src') || '') || + !checks[type].href.test($logo.parent().attr('href') || '') ) { - sr.error(self, 'not-found', { + context.error(self, 'not-found', { type: type.charAt(0).toUpperCase() + type.slice(1), }); } - done(); -} +}; diff --git a/lib/rules/headers/translation.js b/lib/rules/headers/translation.js deleted file mode 100644 index d79335705..000000000 --- a/lib/rules/headers/translation.js +++ /dev/null @@ -1,40 +0,0 @@ -/** @import { Specberus } from "../../validator.js" */ - -const self = { - name: 'headers.translation', - section: 'front-matter', - rule: 'translation', -}; - -export const { name } = self; - -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - const translationLink = sr - .$('body div.head a') - .toArray() - .find(link => { - return sr.$(link).text().toLowerCase().includes('translations'); - }); - - if (!translationLink) { - sr.error(self, 'not-found'); - return done(); - } - - const href = translationLink.attribs.href; - sr.info(self, 'found', { link: href }); - if ( - !sr - .norm(href) - .toLowerCase() - .startsWith('https://www.w3.org/translations/') - ) { - sr.warning(self, 'not-recommended-link'); - } - - done(); -} diff --git a/lib/rules/headers/translation.ts b/lib/rules/headers/translation.ts new file mode 100644 index 000000000..927e3abc6 --- /dev/null +++ b/lib/rules/headers/translation.ts @@ -0,0 +1,38 @@ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; + +const self: RuleMeta = { + name: 'headers.translation', + section: 'front-matter', + rule: 'translation', +}; + +export const { name } = self; + +export const check: RuleCheckFunction = context => { + const translationLink = context + .$('body div.head a') + .toArray() + .find(link => { + return context + .$(link) + .text() + .toLowerCase() + .includes('translations'); + }); + + if (!translationLink) { + context.error(self, 'not-found'); + return; + } + + const href = translationLink.attribs.href; + context.info(self, 'found', { link: href }); + if ( + !context + .norm(href) + .toLowerCase() + .startsWith('https://www.w3.org/translations/') + ) { + context.warning(self, 'not-recommended-link'); + } +}; diff --git a/lib/rules/headers/w3c-state.js b/lib/rules/headers/w3c-state.js deleted file mode 100644 index 197a63d02..000000000 --- a/lib/rules/headers/w3c-state.js +++ /dev/null @@ -1,73 +0,0 @@ -import { importJSON } from '../../util.js'; - -const rules = importJSON('../../rules.json', import.meta.url); - -const self = { - name: 'headers.w3c-state', - section: 'front-matter', - rule: 'dateState', -}; - -export const { name } = self; - -/** - * @param sr - * @param done - */ -export function check(sr, done) { - let profileFound = false; - - if (!sr.config.longStatus) return done(); - const $stateEl = sr.getDocumentStateElement(); - if (!$stateEl) { - sr.error(self, 'no-w3c-state'); - return done(); - } - const txt = sr.norm($stateEl.text()); - // crType/cryType: Add 'Draft', 'Snapshot' suffix to title. - const docTitle = - sr.config.longStatus + - (sr.config.crType ? ` ${sr.config.crType}` : '') + - (sr.config.cryType ? ` ${sr.config.cryType}` : ''); - if (!txt.startsWith(`W3C ${docTitle}`)) { - sr.error(self, 'bad-w3c-state'); - } - - for (const t in rules) - if (t !== '*' && !profileFound) - for (const p in rules[t].profiles) { - const rx = new RegExp( - `^w3c\\ ${rules[t].profiles[p].name}(\\ |,)`, - 'i' - ); - if (rx.test(txt)) { - profileFound = true; - break; - } - } - - if (!profileFound) sr.error(self, 'bad-w3c-state'); - else { - // check the profile link - const $standardLink = $stateEl.find('a').first(); - let hash = sr.config.status; - // mapping special hash. - if (sr.config.longStatus === 'First Public Working Draft') { - hash = 'FPWD'; - } - const expectedLink = new RegExp( - `https://www.w3.org/standards/types/?#${hash}` - ); - if (!$standardLink.length) { - sr.error(self, 'no-w3c-state-link'); - } else if (!expectedLink.test($standardLink.attr('href'))) { - sr.error(self, 'wrong-w3c-state-link', { - hash, - linkFound: $standardLink.attr('href'), - text: sr.norm($standardLink.text()), - }); - } - } - - done(); -} diff --git a/lib/rules/headers/w3c-state.ts b/lib/rules/headers/w3c-state.ts new file mode 100644 index 000000000..7dd406af4 --- /dev/null +++ b/lib/rules/headers/w3c-state.ts @@ -0,0 +1,64 @@ +import rules from '../../rules-track.js'; +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; + +const self: RuleMeta = { + name: 'headers.w3c-state', + section: 'front-matter', + rule: 'dateState', +}; + +export const { name } = self; + +export const check: RuleCheckFunction = context => { + const config = context.config!; + let profileFound = false; + + if (config.longStatus) return; + const $stateEl = context.getDocumentStateElement(); + if (!$stateEl) { + context.error(self, 'no-w3c-state'); + return; + } + 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}`)) { + context.error(self, 'bad-w3c-state'); + } + + for (const { profiles } of Object.values(rules)) + if (!profileFound) + for (const profile of Object.values(profiles)) { + const rx = new RegExp(`^w3c\\ ${profile.name}(\\ |,)`, 'i'); + if (rx.test(txt)) { + profileFound = true; + break; + } + } + + if (!profileFound) context.error(self, 'bad-w3c-state'); + else { + // check the profile link + const $standardLink = $stateEl.find('a').first(); + let hash = config.status; + // mapping special hash. + if (config.longStatus === 'First Public Working Draft') { + hash = 'FPWD'; + } + const expectedLink = new RegExp( + `https://www.w3.org/standards/types/?#${hash}` + ); + if (!$standardLink.length) { + context.error(self, 'no-w3c-state-link'); + } else if (!expectedLink.test($standardLink.attr('href') || '')) { + context.error(self, 'wrong-w3c-state-link', { + hash, + linkFound: $standardLink.attr('href'), + text: context.norm($standardLink.text()), + }); + } + } +}; diff --git a/lib/rules/heuristic/date-format.js b/lib/rules/heuristic/date-format.ts similarity index 60% rename from lib/rules/heuristic/date-format.js rename to lib/rules/heuristic/date-format.ts index ac8fa0bb6..928b28f91 100644 --- a/lib/rules/heuristic/date-format.js +++ b/lib/rules/heuristic/date-format.ts @@ -1,8 +1,7 @@ -import { possibleMonths } from '../../validator.js'; +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; +import { possibleMonths } from '../../rule-context.js'; -/** @import { Specberus } from "../../validator.js" */ - -const self = { +const self: RuleMeta = { name: 'heuristic.date-format', section: 'document-status', rule: 'datesFormat', @@ -10,11 +9,7 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { +export const check: RuleCheckFunction = context => { // Pseudo-constants: const POSSIBLE_DATE = new RegExp( `\\b([0123]?\\d|${possibleMonths})[\\ \\-\\/–—]([0123]?\\d|${possibleMonths})[\\ \\-\\/–—]([\\'‘’]?\\d{2})(\\d\\d)?\\b`, @@ -25,25 +20,25 @@ export function check(sr, done) { 'i' ); - const boilerplateSections = [sr.$('div.head').first(), sr.getSotDSection()]; - const candidateDates = []; - let elem; + const boilerplateSections = [ + context.$('div.head').first(), + context.getSotDSection(), + ]; + const candidateDates: string[] = []; for (const $section of boilerplateSections) { const text = $section && $section.text(); - elem = + const elem = text && text.replace(/(?:https?|ftp):\/\/\S+/gi, '').match(POSSIBLE_DATE); - for (const el of elem) { - candidateDates.push(el); + if (elem) { + for (const el of elem) candidateDates.push(el); } } for (const date of candidateDates) { if (!date.match(EXPECTED_DATE_FORMAT)) { - sr.error(self, 'wrong', { text: date }); + context.error(self, 'wrong', { text: date }); } } - - return done(); -} +}; diff --git a/lib/rules/links/compound.js b/lib/rules/links/compound.js deleted file mode 100644 index fb91cfa12..000000000 --- a/lib/rules/links/compound.js +++ /dev/null @@ -1,90 +0,0 @@ -import url from 'url'; -import { get } from '../../throttled-ua.js'; - -/** @import { Specberus } from "../../validator.js" */ - -const self = { - name: 'links.compound', -}; -const TIMEOUT = 10000; - -export const { name } = self; - -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - if (sr.config.validation !== 'recursive') { - sr.warning(self, 'skipped'); - return done(); - } - - let links = []; - - if (sr.url) { - sr.$('a[href]').each((_, el) => { - const u = new url.URL(el.attribs.href, sr.url); - const l = u.origin + u.pathname; - if (l.startsWith(sr.url) && l !== sr.url) links.push(l); - }); - } - // sort and remove duplicates - links = links.sort().filter((item, pos) => !pos || item !== links[pos - 1]); - - const markupService = 'https://validator.w3.org/nu/'; - let count = 0; - if (links.length > 0) { - links.forEach(l => { - if (sr.config.validation === 'recursive') { - const ua = `W3C-Pubrules/${sr.version}`; - let isMarkupValid = false; - 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, - }); - 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 => 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); - } - count += 1; - if (count === links.length) return done(); - }); - } else { - sr.info(self, 'no-validation', { - file: l.split('/').pop(), - link: l, - }); - } - }); - } else return done(); -} diff --git a/lib/rules/links/compound.ts b/lib/rules/links/compound.ts new file mode 100644 index 000000000..7f2c3176d --- /dev/null +++ b/lib/rules/links/compound.ts @@ -0,0 +1,84 @@ +import type { ResponseError } from 'superagent'; +import { get } from '../../throttled-ua.js'; +import type { RuleCheckFunction } from '../../types.js'; + +const self = { + name: 'links.compound', +}; +const TIMEOUT = 10000; + +export const { name } = self; + +export const check: RuleCheckFunction = context => { + const { validation } = context.config!; + const url = context.url!; + + if (validation !== 'recursive') { + context.warning(self, 'skipped'); + return; + } + + let links: string[] = []; + + if (url) { + 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); + }); + } + // 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/'; + return Promise.all( + links.map(l => { + const ua = `W3C-Pubrules/${context.version}`; + const req = get(markupService) + .set('User-Agent', ua) + .query({ doc: l, out: 'json' }) + .on('error', err => { + context.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) { + context.info(self, 'link', { + file: l.split('/').pop(), + link: l, + markup: '\u2714', + }); + } else { + context.error(self, 'link', { + file: l.split('/').pop(), + link: l, + markup: '\u2718', + }); + } + }, + (err: ResponseError) => { + if (err.timeout) context.warning(self, 'html-timeout'); + else + throw new Error(`HTML validator error: ${err.message}`); + } + ); + }) + ).then(() => {}); +}; diff --git a/lib/rules/links/internal.js b/lib/rules/links/internal.js deleted file mode 100644 index 753b6ea45..000000000 --- a/lib/rules/links/internal.js +++ /dev/null @@ -1,25 +0,0 @@ -/** @import { Specberus } from "../../validator.js" */ - -const self = { - name: 'links.internal', - section: 'document-body', - rule: 'brokenLink', -}; - -export const { name } = self; - -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - sr.$("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 }); - } - }); - done(); -} diff --git a/lib/rules/links/internal.ts b/lib/rules/links/internal.ts new file mode 100644 index 000000000..ddf8e3836 --- /dev/null +++ b/lib/rules/links/internal.ts @@ -0,0 +1,20 @@ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; + +const self: RuleMeta = { + name: 'links.internal', + section: 'document-body', + rule: 'brokenLink', +}; + +export const { name } = self; + +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 (!context.$(`#${escId}, a[name='${id}']`).length) { + context.error(self, 'anchor', { id }); + } + }); +}; diff --git a/lib/rules/links/linkchecker.js b/lib/rules/links/linkchecker.ts similarity index 71% rename from lib/rules/links/linkchecker.js rename to lib/rules/links/linkchecker.ts index 5c5a5aba7..3bb895454 100644 --- a/lib/rules/links/linkchecker.js +++ b/lib/rules/links/linkchecker.ts @@ -1,13 +1,14 @@ // Check every sources(img, stylesheets, scripts) are in same folder as spec document, and they are reachable. import puppeteer from 'puppeteer'; +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'links.linkchecker', section: 'document-body', rule: 'brokenLink', }; -const compound = { +const compound: RuleMeta = { name: 'links.linkchecker', section: 'compound', rule: 'compoundFilesLocation', @@ -35,7 +36,7 @@ export const { name } = self; /** * @param url */ -function simplifyURL(url) { +function simplifyURL(url: string) { const urlObj = new URL(url); return ( (urlObj.origin !== 'null' ? urlObj.origin : urlObj.protocol) + @@ -44,36 +45,21 @@ function simplifyURL(url) { } /** - * Upgrade version of Array.include(). The array can be RegExp - * - * @param url - * @param regArray - * @returns {boolean} + * Upgraded version of Array.includes(), supporting both RegExp and exact string comparison. */ -function includedByReg(url, regArray = allowList) { - return regArray.some(item => { - if (typeof item === 'object') { - // item is RegExp - return item.test(url); - } - // item is simple string - return item === url; - }); +function includedByReg(url: string, regArray = allowList) { + return regArray.some(item => + item instanceof RegExp ? item.test(url) : item === url + ); } -/** - * @param sr - * @param done - */ -export async function check(sr, done) { +export const check: RuleCheckFunction = async 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 done(); - } + if (!context.url) return; - // sr.url is used as base url. Every other resources 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 @@ -83,8 +69,10 @@ export async function check(sr, done) { 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()); @@ -95,9 +83,12 @@ export async function check(sr, done) { 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 @@ -105,7 +96,7 @@ export async function check(sr, done) { 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(), @@ -113,7 +104,7 @@ export async function check(sr, done) { referer, }); } else { - sr.error(compound, 'response-error', { + context.error(compound, 'response-error', { url, status: response.status(), text: response.statusText(), @@ -124,8 +115,7 @@ export async function check(sr, done) { } }); - await page.goto(sr.url, { waitUntil: 'load', timeout: 60000 }); + await page.goto(context.url, { waitUntil: 'load', timeout: 60000 }); await browser.close(); - done(); -} +}; diff --git a/lib/rules/links/reliability.js b/lib/rules/links/reliability.ts similarity index 73% rename from lib/rules/links/reliability.js rename to lib/rules/links/reliability.ts index 888ab1a9e..b4a6136b7 100644 --- a/lib/rules/links/reliability.js +++ b/lib/rules/links/reliability.ts @@ -1,4 +1,4 @@ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction } from '../../types.js'; const self = { name: 'links.reliability', @@ -20,16 +20,15 @@ const unreliableServices = [ // { domain: 'w3.org', path: /track(er)?\/(actions|issues|resolutions)/} ]; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - 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($el.attr('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; @@ -45,9 +44,9 @@ export function check(sr, done) { (unreliableService.path && unreliableService.path.test(path))) ) { - sr.warning(self, 'unreliable-link', { - link: $el.attr('href'), - text: sr.norm($el.text()), + context.warning(self, 'unreliable-link', { + link: href, + text: context.norm($el.text()), }); // when finding this URL unreliable, quit 'unreliableServices.some' return true; @@ -55,5 +54,4 @@ export function check(sr, done) { return false; }); }); - done(); -} +}; diff --git a/lib/rules/metadata/abstract.js b/lib/rules/metadata/abstract.js deleted file mode 100644 index 9c8837acc..000000000 --- a/lib/rules/metadata/abstract.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Pseudo-rule for metadata extraction: abstract. - */ - -import { load } from 'cheerio'; - -/** @import { Cheerio } from "cheerio" */ -/** @import { Element } from "domhandler" */ -/** @import { Specberus } from "../../validator.js" */ - -export const name = 'metadata.abstract'; - -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - /** @type {Cheerio} */ - let $abstractTitle; - sr.$('h2').each((_, el) => { - const $el = sr.$(el); - if (sr.norm($el.text()).toLowerCase() === 'abstract') { - $abstractTitle = $el; - return false; - } - }); - - if ($abstractTitle) { - const $div = load('
    ', null, false)('div'); - $abstractTitle - .parent() - .children() - .each((_, child) => { - { - if (child !== $abstractTitle[0]) { - $div.append(child.cloneNode(true)); - } - } - }); - return done({ abstract: sr.norm($div.html()) }); - } else { - return done({ abstract: 'Not found' }); - } -} diff --git a/lib/rules/metadata/abstract.ts b/lib/rules/metadata/abstract.ts new file mode 100644 index 000000000..c26f04715 --- /dev/null +++ b/lib/rules/metadata/abstract.ts @@ -0,0 +1,33 @@ +/** + * Pseudo-rule for metadata extraction: abstract. + */ + +import { load } from 'cheerio'; +import type { RuleCheckFunction } from '../../types.js'; + +export const name = 'metadata.abstract'; + +export const check: RuleCheckFunction<{ abstract: string }> = context => { + const abstractHeadingEl = context + .$('h2') + .toArray() + .find( + el => + context.norm(context.$(el).text()).toLowerCase() === 'abstract' + ); + + if (!abstractHeadingEl) return { abstract: 'Not found' }; + + const $div = load('
    ', null, false)('div'); + context + .$(abstractHeadingEl) + .parent() + .children() + .each((_, child) => { + { + if (child !== abstractHeadingEl) + $div.append(child.cloneNode(true)); + } + }); + return { abstract: context.norm($div.html()!) }; +}; diff --git a/lib/rules/metadata/charters.js b/lib/rules/metadata/charters.js deleted file mode 100644 index 3727c7b15..000000000 --- a/lib/rules/metadata/charters.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Pseudo-rule for metadata extraction: charters. - */ - -// 'self.name' would be 'metadata.charters' - -export const name = 'metadata.charters'; - -/** - * @param sr - * @param done - */ -export async function check(sr, done) { - const charters = await sr.getCharters(); - - return done({ charters }); -} diff --git a/lib/rules/metadata/charters.ts b/lib/rules/metadata/charters.ts new file mode 100644 index 000000000..481ffadfb --- /dev/null +++ b/lib/rules/metadata/charters.ts @@ -0,0 +1,13 @@ +/** + * Pseudo-rule for metadata extraction: charters. + */ + +import type { RuleCheckFunction } from '../../types.js'; + +// 'self.name' would be 'metadata.charters' + +export const name = 'metadata.charters'; + +export const check: RuleCheckFunction<{ + charters: string[]; +}> = async context => ({ charters: await context.getCharters() }); diff --git a/lib/rules/metadata/deliverers.js b/lib/rules/metadata/deliverers.js deleted file mode 100644 index d6e8884f1..000000000 --- a/lib/rules/metadata/deliverers.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Pseudo-rule for metadata extraction: deliverers' IDs. - */ - -// 'self.name' would be 'metadata.deliverers' -export const name = 'metadata.deliverers'; - -/** - * @param sr - * @param done - */ -export async function check(sr, done) { - const ids = await sr.getDelivererIDs(); - - done({ delivererIDs: ids }); -} diff --git a/lib/rules/metadata/deliverers.ts b/lib/rules/metadata/deliverers.ts new file mode 100644 index 000000000..c329eaaed --- /dev/null +++ b/lib/rules/metadata/deliverers.ts @@ -0,0 +1,12 @@ +/** + * Pseudo-rule for metadata extraction: deliverers' IDs. + */ + +import type { RuleCheckFunction } from '../../types.js'; + +// 'self.name' would be 'metadata.deliverers' +export const name = 'metadata.deliverers'; + +export const check: RuleCheckFunction<{ + delivererIDs: number[]; +}> = async context => ({ delivererIDs: await context.getDelivererIDs() }); diff --git a/lib/rules/metadata/dl.js b/lib/rules/metadata/dl.ts similarity index 54% rename from lib/rules/metadata/dl.js rename to lib/rules/metadata/dl.ts index 3fc797bc2..964142d1c 100644 --- a/lib/rules/metadata/dl.js +++ b/lib/rules/metadata/dl.ts @@ -2,9 +2,8 @@ * Pseudo-rule for metadata extraction: dl. */ -/** @import { Specberus } from "../../validator.js" */ - import { get } from '../../throttled-ua.js'; +import type { RuleCheckFunction } from '../../types.js'; const latestRule = { name: 'metadata.dl', @@ -14,59 +13,65 @@ const latestRule = { export const name = 'metadata.dl'; -/** - * @param {Specberus} sr - * @param done - */ -export async function check(sr, done) { - const dts = sr.extractHeaders(); - const result = {}; +interface DlMetadata { + editorsDraft?: string | undefined; + history?: string; + latestVersion?: string; + previousVersion?: string | null | undefined; + sameWorkAs?: string; + thisVersion?: string; + updated?: boolean; +} + +export const check: RuleCheckFunction = async context => { + const dts = context.extractHeaders(); + const result: DlMetadata = {}; let shortname; let previousShortname; let latestShortname; const $linkThis = dts.This ? dts.This.$dd.find('a').first() : null; - if ($linkThis) { - result.thisVersion = ($linkThis.attr('href') || '').trim(); - shortname = await sr.getShortname(); + const thisHref = $linkThis?.attr('href')?.trim(); + if (thisHref) { + result.thisVersion = thisHref; + shortname = await context.getShortname(); } const $linkLate = dts.Latest ? dts.Latest.$dd.find('a').first() : null; - if ($linkLate) { + const latestHref = $linkLate?.attr('href')?.trim(); + if (latestHref) { // linkLate could be the series version, in which case, the metadata should list the shortlink instead. e.g. https://www.w3.org/TR/2021/WD-IndexedDB-3-20211006/ - result.latestVersion = $linkLate.attr('href').trim(); - const latestRegex = $linkLate - .attr('href') - .trim() - .match(/.*\/([^/]+)\/$/); + result.latestVersion = latestHref; + const latestRegex = latestHref.match(/.*\/([^/]+)\/$/); if (latestRegex && latestRegex.length > 0) { [, latestShortname] = latestRegex; if (latestShortname !== shortname) { 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; + const historyHref = $linkHistory?.attr('href')?.trim(); - if ($linkHistory) { - result.history = $linkHistory.attr('href').trim(); - result.previousVersion = await sr.getPreviousVersion(); + if ($linkHistory && historyHref) { + result.history = historyHref; + result.previousVersion = await context.getPreviousVersion(); previousShortname = $linkHistory.attr('data-previous-shortname'); } const $linkEd = dts.EditorDraft ? dts.EditorDraft.$dd.find('a').first() : null; - if ($linkEd) result.editorsDraft = $linkEd.attr('href').trim(); + if ($linkEd) result.editorsDraft = $linkEd.attr('href')?.trim(); if (previousShortname && shortname && previousShortname !== shortname) { result.sameWorkAs = `https://www.w3.org/TR/${previousShortname}/`; } // 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(); @@ -78,11 +83,17 @@ export async function check(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.js b/lib/rules/metadata/docDate.js deleted file mode 100644 index 58565d89f..000000000 --- a/lib/rules/metadata/docDate.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Pseudo-rule for metadata extraction: docDate. - */ - -// 'self.name' would be 'metadata.docDate' -export const name = 'metadata.docDate'; - -/** - * @param sr - * @param done - */ -export function check(sr, done) { - const docDate = sr.getDocumentDate(); - if (!docDate) { - return done(); - } - const d = [ - docDate.getFullYear(), - docDate.getMonth() + 1, - docDate.getDate(), - ].join('-'); - return done({ docDate: d }); -} diff --git a/lib/rules/metadata/docDate.ts b/lib/rules/metadata/docDate.ts new file mode 100644 index 000000000..40c7a3830 --- /dev/null +++ b/lib/rules/metadata/docDate.ts @@ -0,0 +1,20 @@ +/** + * Pseudo-rule for metadata extraction: docDate. + */ + +import type { RuleCheckFunction } from '../../types.js'; + +// 'self.name' would be 'metadata.docDate' +export const name = 'metadata.docDate'; + +interface DocDateMetadata { + docDate: `${number}-${number}-${number}`; +} + +export const check: RuleCheckFunction = context => { + const docDate = context.getDocumentDate(); + if (docDate) + return { + docDate: `${docDate.getFullYear()}-${docDate.getMonth() + 1}-${docDate.getDate()}`, + }; +}; diff --git a/lib/rules/metadata/editor-ids.js b/lib/rules/metadata/editor-ids.ts similarity index 69% rename from lib/rules/metadata/editor-ids.js rename to lib/rules/metadata/editor-ids.ts index baf9d0887..616ce73d0 100644 --- a/lib/rules/metadata/editor-ids.js +++ b/lib/rules/metadata/editor-ids.ts @@ -2,11 +2,10 @@ * Pseudo-rule for metadata extraction: editor-ids. */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; import { resolveGithubUsernameToId } from '../../util.js'; -/** @import { Specberus } from "../../validator.js" */ - -const self = { +const self: RuleMeta = { name: 'metadata.editor-ids', section: 'front-matter', rule: 'editorSection', @@ -15,18 +14,18 @@ const self = { // 'self.name' would be 'metadata.editor-ids' export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export async function check(sr, done) { - const dts = sr.extractHeaders(); - const editorIds = []; +interface EditorIDsMetadata { + editorIDs: number[]; +} + +export const check: RuleCheckFunction = async context => { + const dts = context.extractHeaders(); + const editorIds: number[] = []; const unresolvedUsernames = []; if (dts.Editor) { for (const el of dts.Editor.$dd.toArray()) { const editorId = el.attribs['data-editor-id']; - // Return ID as Number format, if the ID is not a digit-only string, it gets filtered out + // Return ID as number, if the ID is not a digit-only string, it gets filtered out if (editorId) editorIds.push(parseInt(editorId, 10)); else { const editorGithub = el.attribs['data-editor-github']; @@ -37,7 +36,7 @@ export async function check(sr, done) { 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, }); } @@ -45,18 +44,18 @@ export async function check(sr, done) { } } if (unresolvedUsernames.length) { - sr.error(self, 'editor-github-unresolvable', { + context.error(self, 'editor-github-unresolvable', { names: unresolvedUsernames, }); } // 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.js b/lib/rules/metadata/editor-names.js deleted file mode 100644 index 0d27c8069..000000000 --- a/lib/rules/metadata/editor-names.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Pseudo-rule for metadata extraction: editor-names. - */ - -/** @import { Specberus } from "../../validator.js" */ - -// 'self.name' would be 'metadata.editor-names' -export const name = 'metadata.editor-names'; - -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - const dts = sr.extractHeaders(); - const result = []; - if (dts.Editor) { - dts.Editor.$dd.each((_, el) => { - const editor = sr - .$(el) - .text() - .trim() - .replace(/[,(].*$/, '') - .trim(); - if (editor) result.push(editor); - }); - } - return done({ editorNames: result }); -} diff --git a/lib/rules/metadata/editor-names.ts b/lib/rules/metadata/editor-names.ts new file mode 100644 index 000000000..f880d1758 --- /dev/null +++ b/lib/rules/metadata/editor-names.ts @@ -0,0 +1,29 @@ +/** + * Pseudo-rule for metadata extraction: editor-names. + */ + +import type { RuleCheckFunction } from '../../types.js'; + +// 'self.name' would be 'metadata.editor-names' +export const name = 'metadata.editor-names'; + +interface EditorNamesMetadata { + editorNames: string[]; +} + +export const check: RuleCheckFunction = context => { + const dts = context.extractHeaders(); + const editorNames: string[] = []; + if (dts.Editor) { + dts.Editor.$dd.each((_, el) => { + const editor = context + .$(el) + .text() + .trim() + .replace(/[,(].*$/, '') + .trim(); + if (editor) editorNames.push(editor); + }); + } + return { editorNames }; +}; diff --git a/lib/rules/metadata/errata.js b/lib/rules/metadata/errata.js deleted file mode 100644 index e237b4540..000000000 --- a/lib/rules/metadata/errata.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Pseudo-rule for metadata extraction: errata. - */ - -/** @import { Specberus } from "../../validator.js" */ - -// 'self.name' would be 'metadata.errata' - -export const name = 'metadata.errata'; - -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - 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 }); -} diff --git a/lib/rules/metadata/errata.ts b/lib/rules/metadata/errata.ts new file mode 100644 index 000000000..8b29c9972 --- /dev/null +++ b/lib/rules/metadata/errata.ts @@ -0,0 +1,23 @@ +/** + * Pseudo-rule for metadata extraction: errata. + */ + +import type { RuleCheckFunction } from '../../types.js'; + +// 'self.name' would be 'metadata.errata' + +export const name = 'metadata.errata'; + +interface ErrataMetadata { + errata: string; +} + +export const check: RuleCheckFunction = context => { + const errataRegex = /errata/i; + const $links = context.$('body div.head details + p > a'); + const errata = $links + .toArray() + .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.js b/lib/rules/metadata/informative.js deleted file mode 100644 index d0e702a15..000000000 --- a/lib/rules/metadata/informative.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Pseudo-rule for metadata extraction: informative. - */ - -/** @import { Specberus } from "../../validator.js" */ - -// 'self.name' would be 'metadata.informative' -export const name = 'metadata.informative'; - -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - const $sotd = sr.getSotDSection(); - const expected = /This\s+document\s+is\s+informative\s+only\./; - let isInformative = false; - if (!$sotd) { - return done(); - } - - const $stateEl = sr.getDocumentStateElement(); - const candidate = $stateEl && sr.norm($stateEl.text()).toLowerCase(); - if (candidate && candidate.indexOf('group note') !== -1) { - isInformative = true; - } - - return done({ - informative: expected.test($sotd && $sotd.text()) || isInformative, - }); -} diff --git a/lib/rules/metadata/informative.ts b/lib/rules/metadata/informative.ts new file mode 100644 index 000000000..b9df694fd --- /dev/null +++ b/lib/rules/metadata/informative.ts @@ -0,0 +1,26 @@ +/** + * Pseudo-rule for metadata extraction: informative. + */ + +import type { RuleCheckFunction } from '../../types.js'; + +// 'self.name' would be 'metadata.informative' +export const name = 'metadata.informative'; + +interface InformativeMetadata { + informative: boolean; +} + +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 = context.getDocumentStateElement(); + const candidate = $stateEl && context.norm($stateEl.text()).toLowerCase(); + const isInformative = !!candidate && candidate.indexOf('group note') !== -1; + + return { + informative: expected.test($sotd && $sotd.text()) || isInformative, + }; +}; diff --git a/lib/rules/metadata/process.js b/lib/rules/metadata/process.js deleted file mode 100644 index 9ecbbcb15..000000000 --- a/lib/rules/metadata/process.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Pseudo-rule for metadata extraction: process. - */ - -/** @import { Specberus } from "../../validator.js" */ - -// const self = { -// name: 'metadata.process' -// , section: 'document-status' -// , rule: 'whichProcess' -// }; - -export const name = 'metadata.process'; - -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - const $processDocument = sr.$('a#w3c_process_revision').first(); - const processDocumentHref = - $processDocument.length && $processDocument.attr('href'); - if (!processDocumentHref) { - return done(); - } - return done({ process: processDocumentHref }); -} diff --git a/lib/rules/metadata/process.ts b/lib/rules/metadata/process.ts new file mode 100644 index 000000000..8d7f88697 --- /dev/null +++ b/lib/rules/metadata/process.ts @@ -0,0 +1,25 @@ +/** + * Pseudo-rule for metadata extraction: process. + */ + +import type { RuleCheckFunction } from '../../types.js'; + +// const self = { +// name: 'metadata.process' +// , section: 'document-status' +// , rule: 'whichProcess' +// }; + +export const name = 'metadata.process'; + +interface ProcessMetadata { + process: string; +} + +export const check: RuleCheckFunction = context => { + const $processDocument = context.$('a#w3c_process_revision').first(); + const processDocumentHref = + $processDocument.length && $processDocument.attr('href'); + + if (processDocumentHref) return { process: processDocumentHref }; +}; diff --git a/lib/rules/metadata/profile.js b/lib/rules/metadata/profile.ts similarity index 51% rename from lib/rules/metadata/profile.js rename to lib/rules/metadata/profile.ts index 2e1e87f09..b400a8317 100644 --- a/lib/rules/metadata/profile.js +++ b/lib/rules/metadata/profile.ts @@ -2,78 +2,68 @@ * Pseudo-rule for metadata extraction: profile. */ -/** @import { Specberus } from "../../validator.js" */ +import type { Cheerio } from 'cheerio'; +import type { Element } from 'domhandler'; -// Internal packages: -import { allProfiles, importJSON } from '../../util.js'; +import { allProfiles } from '../../util.js'; import { sortedProfiles } from '../../views.js'; +import type { RecMetadata, RuleCheckFunction } from '../../types.js'; import { check as getTitle } from './title.js'; -const rules = importJSON('../../rules.json', import.meta.url); +import rules from '../../rules-track.js'; // 'self.name' would be 'metadata.profile' export const name = 'metadata.profile'; -/** - * @param {Specberus} sr - * @param done - */ -export async function check(sr, done) { +export const check: RuleCheckFunction = async context => { let matchedLength = 0; let id; - let $profileEl; - const reviewStatus = new Map(); + let $profileEl: Cheerio | undefined; + const reviewStatus = { + CR: 'implementationFeedbackDue', + PR: 'prReviewsDue', + CRY: 'cryFeedbackDue', + } as const; - const $stateEl = sr.getDocumentStateElement(); + const $stateEl = context.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.' ); } - const candidate = $stateEl && sr.norm($stateEl.text()).toLowerCase(); + const candidate = $stateEl && context.norm($stateEl.text()).toLowerCase(); if (candidate) { - for (const t in rules) - if (t !== '*') - for (const p in rules[t].profiles) { - const name = rules[t].profiles[p].name.toLowerCase(); - if ( - candidate.indexOf(name) !== -1 && - matchedLength < name.length - ) { - id = p; - $profileEl = $stateEl; - matchedLength = name.length; - } + for (const { profiles } of Object.values(rules)) { + for (const [p, profile] of Object.entries(profiles)) { + const name = profile.name.toLowerCase(); + if ( + candidate.indexOf(name) !== -1 && + matchedLength < name.length + ) { + id = p; + $profileEl = $stateEl; + matchedLength = name.length; } + } + } } - reviewStatus.set('CR', 'implementationFeedbackDue'); - reviewStatus.set('PR', 'prReviewsDue'); - reviewStatus.set('CRY', 'cryFeedbackDue'); - - /** - * @param id - * @param {Specberus} sr - */ - function assembleMeta(id, sr) { - let meta = { profile: id }; - if (reviewStatus.has(id)) { - const dueDate = sr.getFeedbackDueDate(); + function assembleMeta(id: string) { + let meta: RecMetadata = { profile: id }; + if (id in reviewStatus) { + const dueDate = context.getFeedbackDueDate(); const dates = dueDate && dueDate.valid; let res = dates[0]; - if (dates.length === 0 || !res) return done({ profile: id }); - if (dates.length > 1) res = new Date(Math.min.apply(null, dates)); + if (dates.length === 0 || !res) return { profile: id }; + if (dates.length > 1) + res = new Date(Math.min(...dates.map(d => +d))); - const d = [ - res.getFullYear(), - res.getMonth() + 1, - res.getDate(), - ].join('-'); - meta[reviewStatus.get(id)] = d; + meta[reviewStatus[id as keyof typeof reviewStatus]] = + `${res.getFullYear()}-${res.getMonth() + 1}-${res.getDate()}`; } // implementation report - if (['CR', 'CRD', 'PR', 'REC'].indexOf(id) > -1) { - const dts = sr.extractHeaders(); + if (['CR', 'CRD', 'PR', 'REC'].includes(id)) { + const dts = context.extractHeaders(); if (dts.Implementation?.$dd?.find('a').length) { meta.implementationReport = dts.Implementation.$dd .find('a') @@ -82,36 +72,40 @@ export async function check(sr, done) { } } if (id === 'REC') { - meta = sr.getRecMetadata(meta); + meta = context.getRecMetadata(meta); } // Get 'track/rectrack' of the document based on id const profileRex = new RegExp( - `SUBM|(TR/(Registry|Recommendation|Note))/(${id}).js` + `SUBM|(TR/(Registry|Recommendation|Note))/(${id})` ); const thisProfile = allProfiles.filter(eachProfile => - // eg for eachProfile: 'TR/Note/NOTE-Echidna.js' + // e.g. for eachProfile: 'TR/Note/NOTE-Echidna' profileRex.test(eachProfile) ); - const profileMatch = - thisProfile.length && thisProfile[0].match(profileRex); + const profileMatch = thisProfile.length + ? thisProfile[0].match(profileRex) + : null; const track = profileMatch && profileMatch[profileMatch.length - 2]; meta.rectrack = track; - return done(meta); + return meta; } - const checkRecType = function () { + 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'; } return 'REC'; - }; + } + if (id) { // W3C Candidate Recommendation (CR before 2020/CR snapshot/CR draft), W3C Recommendation will have "REC" if (id === 'REC' || id === 'CR') { @@ -122,22 +116,19 @@ export async function check(sr, done) { if (id === 'CRY') { // distinguish CRY CRYD id = - sr.norm($profileEl.text()).indexOf('Draft') > 0 + context.norm($profileEl?.text() || '').indexOf('Draft') > 0 ? 'CRYD' : 'CRY'; } - assembleMeta(id, sr); + return assembleMeta(id); } else { - let docTitle; - await getTitle(sr, result => { - docTitle = result && result.title; - }); + const docTitle = (await getTitle(context))?.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 + context.$('link[href^="https://www.w3.org/StyleSheets/TR/"]').length ) { let profileList = ''; sortedProfiles.forEach(category => { @@ -147,13 +138,13 @@ export async function check(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.js b/lib/rules/metadata/sotd.js deleted file mode 100644 index f3b738a5c..000000000 --- a/lib/rules/metadata/sotd.js +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Pseudo-rule for metadata extraction: sotd. - */ - -/** @import { Specberus } from "../../validator.js" */ - -export const name = 'metadata.sotd'; - -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - const $sotd = sr.getSotDSection(); - return done({ sotd: $sotd ? sr.norm($sotd.html()) : 'Not found' }); -} diff --git a/lib/rules/metadata/sotd.ts b/lib/rules/metadata/sotd.ts new file mode 100644 index 000000000..3bed90a09 --- /dev/null +++ b/lib/rules/metadata/sotd.ts @@ -0,0 +1,16 @@ +/** + * Pseudo-rule for metadata extraction: sotd. + */ + +import type { RuleCheckFunction } from '../../types.js'; + +export const name = 'metadata.sotd'; + +interface SotdMetadata { + sotd: string; +} + +export const check: RuleCheckFunction = context => { + const $sotd = context.getSotDSection(); + return { sotd: $sotd ? context.norm($sotd.html()!) : 'Not found' }; +}; diff --git a/lib/rules/metadata/title.js b/lib/rules/metadata/title.js deleted file mode 100644 index 009512026..000000000 --- a/lib/rules/metadata/title.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Pseudo-rule for metadata extraction: title. - */ - -/** @import { Specberus } from "../../validator.js" */ - -// 'self.name' would be 'metadata.title' - -export const name = 'metadata.title'; - -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - const $title = sr.$('body div.head h1').first(); - if (!$title.length) return done(); - - $title.html($title.html().replace(/:
    /g, ': ').replace(/
    /g, ' - ')); - return done({ - title: sr.norm($title.text()), - }); -} diff --git a/lib/rules/metadata/title.ts b/lib/rules/metadata/title.ts new file mode 100644 index 000000000..2adb9f913 --- /dev/null +++ b/lib/rules/metadata/title.ts @@ -0,0 +1,19 @@ +/** + * Pseudo-rule for metadata extraction: title. + */ + +import type { RuleCheckFunction } from '../../types.js'; + +// 'self.name' would be 'metadata.title' + +export const name = 'metadata.title'; + +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: context.norm($title.text()), + }; +}; diff --git a/lib/rules/sotd/candidate-review-end.js b/lib/rules/sotd/candidate-review-end.js deleted file mode 100644 index f79c62c74..000000000 --- a/lib/rules/sotd/candidate-review-end.js +++ /dev/null @@ -1,39 +0,0 @@ -const self = { - name: 'sotd.candidate-review-end', - section: 'document-status', - rule: 'reviewEndDate', -}; - -export const { name } = self; - -/** - * @param sr - * @param done - */ -export function check(sr, done) { - const isEditorial = - (sr.config.editorial && /^true$/i.test(sr.config.editorial)) || false; - if (isEditorial) { - sr.warning(self, 'editorial'); - } else { - const dates = sr.getFeedbackDueDate(); - if (dates.list.length === 0) sr.error(self, 'not-found'); - else { - let res; - - if (dates.valid.length === 1) { - [res] = dates.valid; - sr.info(self, 'date-found', { date: res.toDateString() }); - } else if (dates.valid.length > 1) { - sr.warning(self, 'multiple-found'); - res = dates.valid.map(item => new Date(item).toDateString()); - sr.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(', ') }); - } - } - } - done(); -} diff --git a/lib/rules/sotd/candidate-review-end.ts b/lib/rules/sotd/candidate-review-end.ts new file mode 100644 index 000000000..1baf48695 --- /dev/null +++ b/lib/rules/sotd/candidate-review-end.ts @@ -0,0 +1,40 @@ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; + +const self: RuleMeta = { + name: 'sotd.candidate-review-end', + section: 'document-status', + rule: 'reviewEndDate', +}; + +export const { name } = self; + +export const check: RuleCheckFunction = context => { + const isEditorial = + (context.config!.editorial && + /^true$/i.test(context.config!.editorial)) || + false; + if (isEditorial) { + context.warning(self, 'editorial'); + } else { + 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; + context.info(self, 'date-found', { date: res.toDateString() }); + } else if (dates.valid.length > 1) { + context.warning(self, 'multiple-found'); + res = dates.valid.map(item => new Date(item).toDateString()); + context.info(self, 'date-found', { date: res.join(', ') }); + } else { + // dates found but not valid + res = dates.list.map(item => new Date(item).toDateString()); + context.error(self, 'found-not-valid', { + date: res.join(', '), + }); + } + } + } +}; diff --git a/lib/rules/sotd/charter.js b/lib/rules/sotd/charter.ts similarity index 61% rename from lib/rules/sotd/charter.js rename to lib/rules/sotd/charter.ts index 1ebf1109c..bad8bcba0 100644 --- a/lib/rules/sotd/charter.js +++ b/lib/rules/sotd/charter.ts @@ -1,10 +1,9 @@ /** * @file This rule checks if the Group have a current charter. For IG, also check if SOTD has certain text: $charterText. */ +import type { RuleCheckFunction } from '../../types.js'; import { AB, TAG } from '../../util.js'; -/** @import { Specberus } from "../../validator.js" */ - const self = { name: 'sotd.charter', }; @@ -14,18 +13,14 @@ export const { name } = self; const charterText = /The disclosure obligations of the Participants of this group are described in the charter\./; -/** - * @param {Specberus} sr - * @param done - */ -export async function check(sr, done) { - 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'); - return done(); + context.error(self, 'no-group'); + return; } // Skip check if the document is only published by TAG and/or AB @@ -33,33 +28,33 @@ export async function check(sr, done) { const AbID = AB.id; // groupIds: a list of ids without TAG or AB const groupIds = deliverIds.filter( - deliverer => ![TagID, AbID].includes(deliverer) + deliverer => !([TagID, AbID] as number[]).includes(deliverer) ); - if (!groupIds.length) return done(); + if (!groupIds.length) return; - const charters = await sr.getCharters(); + const charters = await context.getCharters(); if (!charters.length) { - sr.error(self, 'no-charter'); - return done(); + 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'); - return done(); + context.error(self, 'text-not-found'); + return; } // check "charter" link is found and correct 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; @@ -67,14 +62,12 @@ export async function check(sr, done) { } }); 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, }); } } - - return done(); } -} +}; diff --git a/lib/rules/sotd/deliverer-note.js b/lib/rules/sotd/deliverer-note.js deleted file mode 100644 index c85f00b2b..000000000 --- a/lib/rules/sotd/deliverer-note.js +++ /dev/null @@ -1,18 +0,0 @@ -const self = { - name: 'sotd.deliverer-note', - section: 'metadata', - rule: 'delivererID', -}; - -export const { name } = self; - -/** - * @param sr - * @param done - */ -export function check(sr, done) { - const deliverers = sr.getDataDelivererIDs(); - - if (deliverers.length === 0) sr.error(self, 'not-found'); - done(); -} diff --git a/lib/rules/sotd/deliverer-note.ts b/lib/rules/sotd/deliverer-note.ts new file mode 100644 index 000000000..14445d294 --- /dev/null +++ b/lib/rules/sotd/deliverer-note.ts @@ -0,0 +1,14 @@ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; + +const self: RuleMeta = { + name: 'sotd.deliverer-note', + section: 'metadata', + rule: 'delivererID', +}; + +export const { name } = self; + +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.js b/lib/rules/sotd/deployment.ts similarity index 56% rename from lib/rules/sotd/deployment.js rename to lib/rules/sotd/deployment.ts index d5d78996f..93dfef0c3 100644 --- a/lib/rules/sotd/deployment.js +++ b/lib/rules/sotd/deployment.ts @@ -1,8 +1,8 @@ // for CR and REC. -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'sotd.deployment', section: 'document-status', rule: 'deployment', @@ -10,12 +10,8 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - 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.' @@ -24,11 +20,7 @@ export function check(sr, done) { const paragraph = $sotd .find('p') .toArray() - .find(p => sr.norm(sr.$(p).text()) === depText); - if (!paragraph) { - sr.error(self, 'not-found'); - return done(); - } + .find(p => context.norm(context.$(p).text()) === depText); + if (!paragraph) context.error(self, 'not-found'); } - return done(); -} +}; diff --git a/lib/rules/sotd/diff.js b/lib/rules/sotd/diff.js deleted file mode 100644 index 50b177449..000000000 --- a/lib/rules/sotd/diff.js +++ /dev/null @@ -1,16 +0,0 @@ -const self = { - name: 'sotd.diff', - section: 'document-status', - rule: 'changesList', -}; - -export const { name } = self; - -/** - * @param sr - * @param done - */ -export function check(sr, done) { - sr.info(self, 'note'); - return done(); -} diff --git a/lib/rules/sotd/diff.ts b/lib/rules/sotd/diff.ts new file mode 100644 index 000000000..b0ff9a139 --- /dev/null +++ b/lib/rules/sotd/diff.ts @@ -0,0 +1,13 @@ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; + +const self: RuleMeta = { + name: 'sotd.diff', + section: 'document-status', + rule: 'changesList', +}; + +export const { name } = self; + +export const check: RuleCheckFunction = context => { + context.info(self, 'note'); +}; diff --git a/lib/rules/sotd/draft-stability.js b/lib/rules/sotd/draft-stability.ts similarity index 73% rename from lib/rules/sotd/draft-stability.js rename to lib/rules/sotd/draft-stability.ts index 0ecf31afc..bf0030103 100644 --- a/lib/rules/sotd/draft-stability.js +++ b/lib/rules/sotd/draft-stability.ts @@ -1,8 +1,8 @@ // These 2 sentences exist in draft documents only, choose one of the 2. -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'sotd.draft-stability', section: 'document-status', rule: 'draftStability', @@ -10,13 +10,9 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - 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\./; @@ -24,24 +20,22 @@ export function check(sr, done) { '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, }); } - done(); -} +}; diff --git a/lib/rules/sotd/new-features.js b/lib/rules/sotd/new-features.ts similarity index 54% rename from lib/rules/sotd/new-features.js rename to lib/rules/sotd/new-features.ts index a6e0d5d78..1f8b38688 100644 --- a/lib/rules/sotd/new-features.js +++ b/lib/rules/sotd/new-features.ts @@ -1,6 +1,6 @@ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'sotd.new-features', section: 'document-status', rule: 'newFeatures', @@ -8,13 +8,9 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - 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.` ); @@ -22,20 +18,19 @@ export function check(sr, done) { 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'); } - done(); -} +}; diff --git a/lib/rules/sotd/obsl-rescind.js b/lib/rules/sotd/obsl-rescind.ts similarity index 57% rename from lib/rules/sotd/obsl-rescind.js rename to lib/rules/sotd/obsl-rescind.ts index 4b4a66023..450437943 100644 --- a/lib/rules/sotd/obsl-rescind.js +++ b/lib/rules/sotd/obsl-rescind.ts @@ -5,20 +5,20 @@ // href="https://www.w3.org/2016/11/obsoleting-rescinding/">explanation of // Obsoleting and Rescinding W3C Specifications.

    -/** @import { Specberus } from "../../validator.js" */ +import type { Cheerio } from 'cheerio'; +import type { RuleContext } from '../../rule-context.js'; +import type { Element } from 'domhandler'; +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -/** - * @param $candidates - * @param {Specberus} sr - */ -function findRscndRationale($candidates, sr) { - let $rationale = null; - let v; - if (sr.config.rescinds === true) v = 'rescind'; +function findRscndRationale( + $candidates: Cheerio, + context: RuleContext +) { + const v = context.config!.rescinds === true ? 'rescind' : ''; - $candidates.each((_, p) => { - const $p = sr.$(p); - const text = sr.norm($p.text()); + for (const p of $candidates.toArray()) { + 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' @@ -28,15 +28,11 @@ function findRscndRationale($candidates, sr) { ' please refer to the explanation of Obsoleting, Rescinding or Superseding W3C Specifications.', 'i' ); - if (wanted1.test(text) && wanted2.test(text)) { - $rationale = $p; - return false; - } - }); - return $rationale; + if (wanted1.test(text) && wanted2.test(text)) return $p; + } } -const self = { +const self: RuleMeta = { name: 'sotd.obsl-rescind', section: 'document-status', rule: 'rescindsRationale', @@ -44,30 +40,25 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - 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); - if (!$rationale || !$rationale.length) { - sr.error(self, 'no-rationale'); + findRscndRationale($sotd.filter('p'), context) || + findRscndRationale($sotd.find('p'), context); + if (!$rationale?.length) { + 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'); } } } - done(); -} +}; diff --git a/lib/rules/sotd/pp.js b/lib/rules/sotd/pp.ts similarity index 65% rename from lib/rules/sotd/pp.js rename to lib/rules/sotd/pp.ts index 1c23f38e6..5f9e3fe99 100644 --- a/lib/rules/sotd/pp.js +++ b/lib/rules/sotd/pp.ts @@ -1,6 +1,10 @@ -/** @import { Specberus } from "../../validator.js" */ +import type { Cheerio } from 'cheerio'; +import type { Element } from 'domhandler'; -const self = { +import type { RuleContext } from '../../rule-context.js'; +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; + +const self: RuleMeta = { name: 'sotd.pp', section: 'document-status', rule: 'patPolReq', @@ -10,15 +14,9 @@ const ppLink = 'https://www.w3.org/policies/patent-policy/'; const ppLink2020 = 'https://www.w3.org/policies/patent-policy/20200915/'; const ppLink2025 = 'https://www.w3.org/policies/patent-policy/20250515/'; -/** - * @param groups - * @param {Specberus} sr - * @param ppLink - */ -function buildWanted(groups, sr) { - const { config } = sr; +function buildWanted(groups: string[], context: RuleContext) { + const config = context.config!; let wanted; - const result = {}; const isRecTrack = config.track === 'Recommendation'; const ppText = '( 15 September 2020| 15 May 2025)?'; @@ -55,56 +53,42 @@ function buildWanted(groups, sr) { // For documents not on REC-track, the sentence is different. wanted = `The${ppText} W3C Patent Policy does not carry any licensing requirements or commitments on this document.`; } - result.regex = new RegExp(wanted); - result.text = wanted.replace(/\\/g, ''); - return result; + + return { + regex: new RegExp(wanted), + text: wanted.replace(/\\/g, ''), + }; } -/** - * @param $candidates - * @param {Specberus} sr - * @param isIGDeliverable - */ -function findPP($candidates, sr) { - let $pp = null; - const delivererGroups = sr.getDelivererNames(); - if (delivererGroups.length > 1) sr.warning(self, 'joint-publication'); - - const wanted = buildWanted(delivererGroups, sr); +function findPP($candidates: Cheerio, context: RuleContext) { + const delivererGroups = context.getDelivererNames(); + if (delivererGroups.length > 1) context.warning(self, 'joint-publication'); + + const wanted = buildWanted(delivererGroups, context); const expected = wanted.text; - $candidates.each((_, p) => { - const $p = sr.$(p); - const text = sr.norm($p.text()); - if (wanted.regex.test(text)) { - $pp = $p; - return false; - } - }); - return { $pp, expected }; + for (const p of $candidates.toArray()) { + const $p = context.$(p); + const text = context.norm($p.text()); + if (wanted.regex.test(text)) return { $pp: $p, expected }; + } + return { $pp: null, expected }; } export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export async function check(sr, done) { - const groups = await sr.getDelivererGroups(); - // Check if the document is published by WGs only. If IG/AB/TAG(s) are involved, the document cannot be on REC-track and should have a sentence meaning 'pp does not apply'. - const publishedByWgOnly = groups.every(group => group.groupType === 'wg'); - - const $sotd = sr.getSotDSection(); - const isRecTrack = sr.config.track === 'Recommendation'; +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 }); - return done(); + context.error(self, 'no-pp', { expected }); + return; } let foundLink = false; @@ -112,9 +96,9 @@ export async function check(sr, done) { let foundEssentials = false; let foundSection6 = false; $pp.find('a[href]').each((_, a) => { - const $a = sr.$(a); - const href = $a.attr('href'); - const text = sr.norm($a.text()); + const $a = context.$(a); + const href = $a.attr('href')!; + const text = context.norm($a.text()); const possiblePPLinks = [ppLink, ppLink2020, ppLink2025]; if ( possiblePPLinks.includes(href) && @@ -152,26 +136,24 @@ export async function check(sr, done) { } }); - 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 ( - (sr.config.track === 'Recommendation' || - sr.config.track === 'Note') && + (track === 'Recommendation' || track === 'Note') && isRecTrack && !foundEssentials ) - sr.error(self, 'no-claims', { + context.error(self, 'no-claims', { link: `${ppLink}#def-essential`, }); if ( - (sr.config.track === 'Recommendation' || - sr.config.track === 'Note') && + (track === 'Recommendation' || track === 'Note') && isRecTrack && !foundSection6 ) - sr.error(self, 'no-section6', { + context.error(self, 'no-section6', { link: `${ppLink}#sec-Disclosure`, }); - return done(); } -} +}; diff --git a/lib/rules/sotd/process-document.js b/lib/rules/sotd/process-document.ts similarity index 71% rename from lib/rules/sotd/process-document.js rename to lib/rules/sotd/process-document.ts index 1866ff257..04ec0292d 100644 --- a/lib/rules/sotd/process-document.js +++ b/lib/rules/sotd/process-document.ts @@ -1,6 +1,6 @@ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'sotd.process-document', section: 'document-status', rule: 'whichProcess', @@ -8,26 +8,22 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - 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'; const newProcUri = 'https://www.w3.org/policies/process/20250818/'; const previousProc = '03 November 2023'; const previousProcUri = 'https://www.w3.org/policies/process/20231103/'; - let previousAllowed; + let previousAllowed = false; const boilerplate = BOILERPLATE_PREFIX + newProc + BOILERPLATE_SUFFIX; const previousBoilerplate = 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() {}, @@ -42,37 +38,36 @@ export function check(sr, done) { 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 }); } - done(); -} +}; diff --git a/lib/rules/sotd/publish.js b/lib/rules/sotd/publish.ts similarity index 74% rename from lib/rules/sotd/publish.js rename to lib/rules/sotd/publish.ts index 80cba39b1..c47bc88e6 100644 --- a/lib/rules/sotd/publish.js +++ b/lib/rules/sotd/publish.ts @@ -1,6 +1,6 @@ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; -const self = { +const self: RuleMeta = { name: 'sotd.publish', section: 'document-status', rule: 'publish', @@ -8,21 +8,17 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export async function check(sr, done) { - const $sotd = sr.getSotDSection(); - const { crType, cryType } = sr.config; - let docType = sr.config.longStatus; - if (sr.config.status === 'CR' || sr.config.status === 'CRD') { +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}`; - } else if (sr.config.status === 'CRY' || sr.config.status === 'CRYD') { + } else if (status === 'CRY' || status === 'CRYD') { docType = `Candidate Registry ${cryType}`; } - const text = `^This document was (?:produced|published) by the (.+? Working Group|Technical Architecture Group|Advisory Board|.+? Interest Group)( and the (.+? Working Group|Technical Architecture Group|Advisory Board|.+? Interest Group))? as a ${docType} using the ${sr.config.track} track.`; + const text = `^This document was (?:produced|published) by the (.+? Working Group|Technical Architecture Group|Advisory Board|.+? Interest Group)( and the (.+? Working Group|Technical Architecture Group|Advisory Board|.+? Interest Group))? as a ${docType} using the ${context.config!.track} track.`; if ($sotd) { // Find the paragraph of 'This document was published by ... , it includes ...' @@ -30,18 +26,17 @@ export async function check(sr, done) { 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 }); - return done(); + context.error(self, 'not-found', { publishReg }); + return; } const $sotdLinks = $sotd.find('a[href]'); - /** @type {RegExp} */ - let baseURL; + let baseURL = /^https:\/\/www.w3.org\/2023\/Process-20230612\//; // 1 month transition period - sr.transition({ + context.transition({ from: new Date('2023-11-02'), to: new Date('2023-12-03'), doBefore() { @@ -62,17 +57,17 @@ export async function check(sr, done) { const trackEl = $sotdLinks .toArray() .find(el => - sr.norm(sr.$(el).text()).match(`${sr.config.track} track`) + 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: `${sr.config.track} track`, + 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( @@ -82,19 +77,18 @@ export async function check(sr, done) { 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 = - sr.config.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; - let textExpected; + let urlExpected: RegExp; + let textExpected: RegExp; // for proposed amendments, proposed additions, proposed corrections. if (recType.pSubChanges && recType.pNewFeatures) { urlExpected = new RegExp( @@ -128,15 +122,15 @@ export async function check(sr, done) { ); 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)) { - if (!urlExpected.test($el.attr('href'))) { - sr.error(self, 'url-not-match', { + const $el = context.$(el); + if (context.norm($el.text()).match(textExpected)) { + if (!urlExpected.test($el.attr('href') || '')) { + context.error(self, 'url-not-match', { url: urlExpected, text: textExpected, }); @@ -146,11 +140,10 @@ export async function check(sr, done) { return false; }); if (!linkFound) - sr.error(self, 'url-text-not-found', { - url: urlExpected, - text: textExpected, + context.error(self, 'url-text-not-found', { + url: urlExpected!, + text: textExpected!, }); } } - done(); -} +}; diff --git a/lib/rules/sotd/rec-addition.js b/lib/rules/sotd/rec-addition.ts similarity index 73% rename from lib/rules/sotd/rec-addition.js rename to lib/rules/sotd/rec-addition.ts index 586c57b30..59c870e11 100644 --- a/lib/rules/sotd/rec-addition.js +++ b/lib/rules/sotd/rec-addition.ts @@ -1,7 +1,10 @@ -/** @import { Specberus } from "../../validator.js" */ +import type { Cheerio } from 'cheerio'; +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; +import type { RuleContext } from '../../rule-context.js'; +import type { Element } from 'domhandler'; // This rule only apply to REC, check changes with colored background. -const self = { +const self: RuleMeta = { name: 'sotd.rec-addition', section: 'document-status', rule: 'recAddition', @@ -14,54 +17,55 @@ const P_ADDITION = 'Proposed addition(s)? are marked in the document.'; const C_CORRECTION = 'Candidate correction(s)? are marked in the document.'; const C_ADDITION = 'Candidate addition(s)? are marked in the document.'; +interface CheckSectionOptions { + typeOfRec: boolean | undefined; + $htmlSection: Cheerio; + expectedText: string; + typeOfChange: string; + sectionClass: string; +} + /** * Check if the recommendation change type mentioned by text is consistent with the html element. - * - * @param {Specberus} sr - * @param options */ -function checkSection(sr, options) { +function checkSection(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, }); } -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - 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, @@ -70,7 +74,7 @@ export function check(sr, done) { }); // check for 'proposed additions' - checkSection(sr, { + checkSection(context, { typeOfRec: recType.pNewFeatures, $htmlSection: $pAddSection, expectedText: P_ADDITION, @@ -79,7 +83,7 @@ export function check(sr, done) { }); // check for 'candidate corrections' - checkSection(sr, { + checkSection(context, { typeOfRec: recType.cSubChanges, $htmlSection: $cCorSection, expectedText: C_CORRECTION, @@ -88,7 +92,7 @@ export function check(sr, done) { }); // check for 'candidate additions' - checkSection(sr, { + checkSection(context, { typeOfRec: recType.cNewFeatures, $htmlSection: $cAddSection, expectedText: C_ADDITION, @@ -96,5 +100,4 @@ export function check(sr, done) { sectionClass: 'addition', }); } - return done(); -} +}; diff --git a/lib/rules/sotd/rec-comment-end.js b/lib/rules/sotd/rec-comment-end.ts similarity index 50% rename from lib/rules/sotd/rec-comment-end.js rename to lib/rules/sotd/rec-comment-end.ts index 748da5ee8..750c84307 100644 --- a/lib/rules/sotd/rec-comment-end.js +++ b/lib/rules/sotd/rec-comment-end.ts @@ -1,6 +1,7 @@ -/** @import { Specberus } from "../../validator.js" */ +import type { RuleCheckFunction, RuleMeta } from '../../types.js'; +import { dateRegexStrCapturing } from '../../rule-context.js'; -const self = { +const self: RuleMeta = { name: 'sotd.rec-comment-end', section: 'document-status', rule: 'commentEnd', @@ -8,22 +9,18 @@ const self = { export const { name } = self; -/** - * @param {Specberus} sr - * @param done - */ -export function check(sr, done) { - 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(sr.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( - docDate - 0 + 60 * 24 * 60 * 60 * 1000 + +docDate - 0 + 60 * 24 * 60 * 60 * 1000 ); // "Mon Nov 02 2020 16:26:28 GMT+0800 (@@ Standard Time)" -> "Nov 02 2020" const readableDate = minimumEndDate @@ -32,27 +29,31 @@ export function check(sr, done) { .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 = []; - for (const i in matches) { - if (sr.stringToDate(matches[i]) > minimumEndDate) { - dateFound.push(sr.stringToDate(matches[i])); + if (matches) { + for (const match of matches) { + const date = context.stringToDate(match); + if (date && date > minimumEndDate) { + 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, }); } } } } - done(); -} +}; diff --git a/lib/rules/sotd/stability.js b/lib/rules/sotd/stability.ts similarity index 62% rename from lib/rules/sotd/stability.js rename to lib/rules/sotd/stability.ts index 0da0ca0fc..7dbd1944e 100644 --- a/lib/rules/sotd/stability.js +++ b/lib/rules/sotd/stability.ts @@ -2,58 +2,51 @@ // stability warning //

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

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

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

    - Site map · - Github · - Help -

    `; -/** - * @TODO Document. - */ - -const serveStraight = function (req, res) { - const fragment = req.path.replace(/^\/|\/$/gi, ''); - res.render(fragment, { - DEBUG, - BASE_URI, - version, - nav, - title: fragment, - }); -}; - -/** - * @TODO Document. - */ - -const handleWrongPage = function (req, res) { - res.render('error', { - DEBUG, - BASE_URI, - version, - nav, - title: 'whut?', - }); -}; - -/** - * @TODO Document. - */ - -const listProfiles = function () { - const result = []; - const sortByOrderField = (a, b) => { - if (a.order < b.order) return -1; - if (a.order > b.order) return +1; - return 0; - }; - for (const t in rules) { - if (t !== '*') { - result.push({ - order: rules[t].order, - abbr: t, - name: rules[t].name, - profiles: [], - }); - for (const p in rules[t].profiles) - result[result.length - 1].profiles.push({ - order: rules[t].profiles[p].order, - abbr: p, - name: rules[t].profiles[p].name, - }); - result[result.length - 1].profiles.sort(sortByOrderField); - } - result.sort(sortByOrderField); - } - return result; -}; - -export const sortedProfiles = listProfiles(); -/** - * @TODO Document. - */ - -const formatRules = function (sections, common) { - const total = []; - for (const s in sections) { - let result = `

    ${sections[s].name}

    -
      `; - for (const r in sections[s].rules) - if (typeof sections[s].rules[r] === 'boolean') { - // Common rule, with no parameters - result += `
    • - § - ${common.sections[s].rules[r]} -
    • `; - } else if (typeof sections[s].rules[r] === 'string') { - // Specific rule - result += `
    • - § - ${sections[s].rules[r]} -
    • `; - } else if (typeof sections[s].rules[r] === 'object') { - // Array (common rule with parameters) - const values = sections[s].rules[r]; - let template = common.sections[s].rules[r]; - for (let p = 0; p < values.length; p += 1) - template = template.replace( - new RegExp(`@{param${p + 1}}`, 'g'), - values[p] - ); - result += `
    • - § - ${template} -
    • `; - } - result += '
    \n'; - result = result.replace( - new RegExp(`@{year}`, 'g'), - '' + new Date().getFullYear() - ); - total.push({ order: sections[s].name, content: result }); - } - - return total - .sort((a, b) => { - if (a.order < b.order) return -1; - if (a.order > b.order) return +1; - return 0; - }) - .map(a => a.content) - .join(''); -}; - -/** - * @TODO Document. - */ - -const retrieveProfile = function (query) { - const result = {}; - - if (query && query.profile) { - const codename = qs.escape(query.profile).trim().toUpperCase(); - if (rules) - for (const t in rules) - if ( - t !== '*' && - Object.prototype.hasOwnProperty.call( - rules[t].profiles, - codename - ) - ) { - const profile = rules[t].profiles[codename]; - result.abbr = codename; - result.name = `${profile.name}`; - result.body = formatRules(profile.sections, rules['*']); - } - if (Object.keys(result).length === 0) - result.error = `

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

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

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

    \n'; - - return result; -}; - -/** - * Set up HTML views using templates and Express Handlebars. - * - * @param {Express} app - the Express application. - */ - -export const setUp = function (app) { - const hb = handlebars.create({ defaultLayout: 'main' }); - app.engine('handlebars', hb.engine); - app.set('view engine', 'handlebars'); - - app.get('/', (req, res) => { - res.render('index', { - DEBUG, - BASE_URI, - version, - nav, - interactive: true, - tracks: sortedProfiles, - }); - }); - - app.get('/doc', (req, res) => { - res.render('doc', { - DEBUG, - BASE_URI, - version, - nav, - title: 'documentation', - tracks: sortedProfiles, - }); - }); - - app.get('/doc/rules', (req, res) => { - res.render('doc/rules', { - DEBUG, - BASE_URI, - version, - nav, - title: 'publication rules', - content: retrieveProfile(req.query), - scroll: true, - }); - }); - - // Sections without any additional logic: - app.get('/sitemap', serveStraight); - app.get('/help', serveStraight); - - // Catch-all: - app.get(/(.*)/, handleWrongPage); -}; diff --git a/lib/views.ts b/lib/views.ts new file mode 100644 index 000000000..114437c60 --- /dev/null +++ b/lib/views.ts @@ -0,0 +1,223 @@ +import type { Express, Request, Response } from 'express'; +import handlebars from 'express-handlebars'; +import { escape } from 'querystring'; + +import rules, { type RulesProfile, type RulesSection } from './rules-track.js'; +import genericSections, { + type GenericRulesSection, +} from './rules-generic-sections.js'; +import { specberusVersion } from './util.js'; + +// Settings: +const DEBUG = process && process.env && process.env.DEBUG; +const BASE_URI = `${process.env.BASE_URI ? process.env.BASE_URI : ''}/`.replace( + /\/+$/, + '/' +); + +const nav = `

    + Site map · + Github · + Help +

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

    ${sections[s].name}

    +
      `; + for (const [r, rValue] of Object.entries(sections[s].rules)) + if (typeof rValue === 'boolean') { + // Common rule, with no parameters + result += `
    • + § + ${commonSections[s].rules[r]} +
    • `; + } else if (typeof rValue === 'string') { + // Specific rule + result += `
    • + § + ${rValue} +
    • `; + } else if (typeof rValue === 'object') { + // Array (common rule with parameters) + const values = rValue; + let template = commonSections[s].rules[r]; + for (let p = 0; p < values.length; p += 1) + template = template.replace( + new RegExp(`@{param${p + 1}}`, 'g'), + values[p] + ); + result += `
    • + § + ${template} +
    • `; + } + result += '
    \n'; + result = result.replace( + new RegExp(`@{year}`, 'g'), + '' + new Date().getFullYear() + ); + total.push({ order: sections[s].name, content: result }); + } + + return total + .sort((a, b) => { + if (a.order < b.order) return -1; + if (a.order > b.order) return +1; + return 0; + }) + .map(a => a.content) + .join(''); +} + +interface RetrieveProfileResult { + abbr: string; + name: string; + body: string; +} + +// TODO(tripu): Document. +function retrieveProfile(query: qs.ParsedQs) { + let result: RetrieveProfileResult | { error: string } | undefined; + + if (query && query.profile && typeof query.profile === 'string') { + const codename = escape(query.profile).trim().toUpperCase(); + 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( + query.profile + )}.

    \n`, + }; + } else + result = { + error: '

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

    \n', + }; + + return result; +} + +/** + * Set up HTML views using templates and Express Handlebars. + */ +export function setUp(app: Express) { + const hb = handlebars.create({ defaultLayout: 'main' }); + app.engine('handlebars', hb.engine); + app.set('view engine', 'handlebars'); + + app.get('/', (_, res) => { + res.render('index', { + DEBUG, + BASE_URI, + version: specberusVersion, + nav, + interactive: true, + tracks: sortedProfiles, + }); + }); + + app.get('/doc', (_, res) => { + res.render('doc', { + DEBUG, + BASE_URI, + version: specberusVersion, + nav, + title: 'documentation', + tracks: sortedProfiles, + }); + }); + + app.get('/doc/rules', (req, res) => { + res.render('doc/rules', { + DEBUG, + BASE_URI, + version: specberusVersion, + nav, + title: 'publication rules', + content: retrieveProfile(req.query), + scroll: true, + }); + }); + + // Sections without any additional logic: + app.get('/sitemap', serveStraight); + app.get('/help', serveStraight); + + // Catch-all: + app.get(/(.*)/, handleWrongPage); +} 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 f5a405a96..733bc2d9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,18 @@ { "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", "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", @@ -23,147 +22,36 @@ "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" }, "devDependencies": { - "@rvagg/chai-as-promised": "8.0.2", "@types/express": "^5.0.6", - "@types/mocha": "^10.0.10", + "@types/express-fileupload": "^1.5.1", + "@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", + "cspell": "10.0.1", "domhandler": "^6.0.1", - "eslint": "10.0.3", - "eslint-config-prettier": "10.0.3", - "eslint-plugin-node": "11.1.0", - "eslint-plugin-prettier": "5.0.1", "expect.js": "0.3", "husky": "9.0.11", - "lint-staged": "16.4.0", + "lint-staged": "17.0.8", "lodash.merge": "^4.6.2", - "mocha": "11.7.6", "nock": "15.0.0", - "nodemon": "3.0.3", "prettier": "3.8.4", - "typescript": "^5.9.3" + "tsx": "^4.21.0", + "typescript": "^6.0.3" }, "engines": { - "node": "20 || 22 || 24", + "node": "22 || 24", "npm": ">=7" } }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", - "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", - "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", - "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", - "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" - }, - "node_modules/@babel/highlight/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@bcoe/v8-coverage": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", @@ -185,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": { @@ -332,26 +244,26 @@ "license": "MIT" }, "node_modules/@cspell/dict-bash": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@cspell/dict-bash/-/dict-bash-4.2.2.tgz", - "integrity": "sha512-kyWbwtX3TsCf5l49gGQIZkRLaB/P8g73GDRm41Zu8Mv51kjl2H7Au0TsEvHv7jzcsRLS6aUYaZv6Zsvk1fOz+Q==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@cspell/dict-bash/-/dict-bash-4.2.3.tgz", + "integrity": "sha512-ljUZoKHbDqw5Sx0qpL2qTUlmkmr+vhZH/sCNrNaBZKTbdgiswErSnIF1jRbGmEitJNxHRHWsuZyVgnTGfVO1Yw==", "dev": true, "license": "MIT", "dependencies": { - "@cspell/dict-shell": "1.1.2" + "@cspell/dict-shell": "1.2.0" } }, "node_modules/@cspell/dict-companies": { - "version": "3.2.10", - "resolved": "https://registry.npmjs.org/@cspell/dict-companies/-/dict-companies-3.2.10.tgz", - "integrity": "sha512-bJ1qnO1DkTn7JYGXvxp8FRQc4yq6tRXnrII+jbP8hHmq5TX5o1Wu+rdfpoUQaMWTl6balRvcMYiINDesnpR9Bw==", + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/@cspell/dict-companies/-/dict-companies-3.2.11.tgz", + "integrity": "sha512-0cmafbcz2pTHXLd59eLR1gvDvN6aWAOM0+cIL4LLF9GX9yB2iKDNrKsvs4tJRqutoaTdwNFBbV0FYv+6iCtebQ==", "dev": true, "license": "MIT" }, "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" }, @@ -370,9 +282,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-css": { - "version": "4.0.19", - "resolved": "https://registry.npmjs.org/@cspell/dict-css/-/dict-css-4.0.19.tgz", - "integrity": "sha512-VYHtPnZt/Zd/ATbW3rtexWpBnHUohUrQOHff/2JBhsVgxOrksAxJnLAO43Q1ayLJBJUUwNVo+RU0sx0aaysZfg==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@cspell/dict-css/-/dict-css-4.1.2.tgz", + "integrity": "sha512-+ylGoKdwZ2sVOCOnU2Eq5wDZx+RaVX3HoKyNHGGsFvhSw6IidQ6tH/mAPKBDofViHJoWCPNlklE0lTr6MDG3QA==", "dev": true, "license": "MIT" }, @@ -384,9 +296,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-data-science": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/@cspell/dict-data-science/-/dict-data-science-2.0.13.tgz", - "integrity": "sha512-l1HMEhBJkPmw4I2YGVu2eBSKM89K9pVF+N6qIr5Uo5H3O979jVodtuwP8I7LyPrJnC6nz28oxeGRCLh9xC5CVA==", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/@cspell/dict-data-science/-/dict-data-science-2.0.14.tgz", + "integrity": "sha512-jl6Ds4u5u5JT+yY30pWQpAbdCHfy3lCcNkLbpL/AZKoUaLEoXbaYsps9xQtvD7DyaiXxiLZkdH2yHHXtoFtZyg==", "dev": true, "license": "MIT" }, @@ -405,9 +317,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-dotnet": { - "version": "5.0.11", - "resolved": "https://registry.npmjs.org/@cspell/dict-dotnet/-/dict-dotnet-5.0.11.tgz", - "integrity": "sha512-LSVKhpFf/ASTWJcfYeS0Sykcl1gVMsv2Z5Eo0TnTMSTLV3738HH+66pIsjUTChqU6SF3gKPuCe6EOaRYqb/evA==", + "version": "5.0.13", + "resolved": "https://registry.npmjs.org/@cspell/dict-dotnet/-/dict-dotnet-5.0.13.tgz", + "integrity": "sha512-xPp7jMnFpOri7tzmqmm/dXMolXz1t2bhNqxYkOyMqXhvs08oc7BFs+EsbDY0X7hqiISgeFZGNqn0dOCr+ncPYw==", "dev": true, "license": "MIT" }, @@ -419,9 +331,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-en_us": { - "version": "4.4.29", - "resolved": "https://registry.npmjs.org/@cspell/dict-en_us/-/dict-en_us-4.4.29.tgz", - "integrity": "sha512-G3B27++9ziRdgbrY/G/QZdFAnMzzx17u8nCb2Xyd4q6luLpzViRM/CW3jA+Mb/cGT5zR/9N+Yz9SrGu1s0bq7g==", + "version": "4.4.35", + "resolved": "https://registry.npmjs.org/@cspell/dict-en_us/-/dict-en_us-4.4.35.tgz", + "integrity": "sha512-xWpxBCc/FzzMMo/A+0qwARVaIIhR0Ql8yhhv4rvsvg+GfQF+LG9yzg2GwTM5N2rjvzmM3nKuR9zxFZq2I6fJSg==", "dev": true, "license": "MIT" }, @@ -433,16 +345,16 @@ "license": "CC BY-SA 4.0" }, "node_modules/@cspell/dict-en-gb-mit": { - "version": "3.1.18", - "resolved": "https://registry.npmjs.org/@cspell/dict-en-gb-mit/-/dict-en-gb-mit-3.1.18.tgz", - "integrity": "sha512-AXaMzbaxhSc32MSzKX0cpwT+Thv1vPfxQz1nTly1VHw3wQcwPqVFSqrLOYwa8VNqAPR45583nnhD6iqJ9YESoQ==", + "version": "3.1.24", + "resolved": "https://registry.npmjs.org/@cspell/dict-en-gb-mit/-/dict-en-gb-mit-3.1.24.tgz", + "integrity": "sha512-Oowb/Uzkh7OmDRdCcETzMc9imEb4IpLlHJXoYjX8A8DS2X/54gqSjI915JFB8hKtFjBko5OM0BLQ+6cZhFEMmQ==", "dev": true, "license": "MIT" }, "node_modules/@cspell/dict-filetypes": { - "version": "3.0.15", - "resolved": "https://registry.npmjs.org/@cspell/dict-filetypes/-/dict-filetypes-3.0.15.tgz", - "integrity": "sha512-uDMeqYlLlK476w/muEFQGBy9BdQWS0mQ7BJiy/iQv5XUWZxE2O54ZQd9nW8GyQMzAgoyg5SG4hf9l039Qt66oA==", + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/@cspell/dict-filetypes/-/dict-filetypes-3.0.18.tgz", + "integrity": "sha512-yU7RKD/x1IWmDLzWeiItMwgV+6bUcU/af23uS0+uGiFUbsY1qWV/D4rxlAAO6Z7no3J2z8aZOkYIOvUrJq0Rcw==", "dev": true, "license": "MIT" }, @@ -454,9 +366,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-fonts": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@cspell/dict-fonts/-/dict-fonts-4.0.5.tgz", - "integrity": "sha512-BbpkX10DUX/xzHs6lb7yzDf/LPjwYIBJHJlUXSBXDtK/1HaeS+Wqol4Mlm2+NAgZ7ikIE5DQMViTgBUY3ezNoQ==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@cspell/dict-fonts/-/dict-fonts-4.0.6.tgz", + "integrity": "sha512-aR/0csY01dNb0A1tw/UmN9rKgHruUxsYsvXu6YlSBJFu60s26SKr/k1o4LavpHTQ+lznlYMqAvuxGkE4Flliqw==", "dev": true, "license": "MIT" }, @@ -468,9 +380,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-fullstack": { - "version": "3.2.8", - "resolved": "https://registry.npmjs.org/@cspell/dict-fullstack/-/dict-fullstack-3.2.8.tgz", - "integrity": "sha512-J6EeoeThvx/DFrcA2rJiCA6vfqwJMbkG0IcXhlsmRZmasIpanmxgt90OEaUazbZahFiuJT8wrhgQ1QgD1MsqBw==", + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/@cspell/dict-fullstack/-/dict-fullstack-3.2.9.tgz", + "integrity": "sha512-diZX+usW5aZ4/b2T0QM/H/Wl9aNMbdODa1Jq0ReBr/jazmNeWjd+PyqeVgzd1joEaHY+SAnjrf/i9CwKd2ZtWQ==", "dev": true, "license": "MIT" }, @@ -510,9 +422,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-html": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/@cspell/dict-html/-/dict-html-4.0.14.tgz", - "integrity": "sha512-2bf7n+kS92g+cMKV0wr9o/Oq9n8JzU7CcrB96gIh2GHgnF+0xDOqO2W/1KeFAqOfqosoOVE48t+4dnEMkkoJ2Q==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@cspell/dict-html/-/dict-html-4.0.15.tgz", + "integrity": "sha512-GJYnYKoD9fmo2OI0aySEGZOjThnx3upSUvV7mmqUu8oG+mGgzqm82P/f7OqsuvTaInZZwZbo+PwJQd/yHcyFIw==", "dev": true, "license": "MIT" }, @@ -552,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" }, @@ -580,14 +492,14 @@ "license": "MIT" }, "node_modules/@cspell/dict-markdown": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/@cspell/dict-markdown/-/dict-markdown-2.0.14.tgz", - "integrity": "sha512-uLKPNJsUcumMQTsZZgAK9RgDLyQhUz/uvbQTEkvF/Q4XfC1i/BnA8XrOrd0+Vp6+tPOKyA+omI5LRWfMu5K/Lw==", + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@cspell/dict-markdown/-/dict-markdown-2.0.17.tgz", + "integrity": "sha512-H8bAxih6U8NOnSPL7R8My+tqjaB4tmnJTjERuz4zYqmf+cH+5xshX3UVgKlwWFcyjsYfv/zEDuRdMctQv1q6HQ==", "dev": true, "license": "MIT", "peerDependencies": { - "@cspell/dict-css": "^4.0.19", - "@cspell/dict-html": "^4.0.14", + "@cspell/dict-css": "^4.1.2", + "@cspell/dict-html": "^4.0.15", "@cspell/dict-html-symbol-entities": "^4.0.5", "@cspell/dict-typescript": "^3.2.3" } @@ -607,9 +519,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-npm": { - "version": "5.2.33", - "resolved": "https://registry.npmjs.org/@cspell/dict-npm/-/dict-npm-5.2.33.tgz", - "integrity": "sha512-U1gfDxdFR6nnojvtdkF2Ati3jfIlnW5nJkFb2jS1JunlhrSYdZXwz/4bI//h1W3aaeYQoSlvTIqk3vlnIDrNng==", + "version": "5.2.41", + "resolved": "https://registry.npmjs.org/@cspell/dict-npm/-/dict-npm-5.2.41.tgz", + "integrity": "sha512-To3xsfRmMBYVXtWVEdUgV35M9a/JZ54dSuoY6m6D3uHKKL3I326Wmy4xifZ3PU8MQaWhyEH7zbIcUEtKwTQMcA==", "dev": true, "license": "MIT" }, @@ -628,20 +540,20 @@ "license": "MIT" }, "node_modules/@cspell/dict-public-licenses": { - "version": "2.0.15", - "resolved": "https://registry.npmjs.org/@cspell/dict-public-licenses/-/dict-public-licenses-2.0.15.tgz", - "integrity": "sha512-cJEOs901H13Pfy0fl4dCD1U+xpWIMaEPq8MeYU83FfDZvellAuSo4GqWCripfIqlhns/L6+UZEIJSOZnjgy7Wg==", + "version": "2.0.16", + "resolved": "https://registry.npmjs.org/@cspell/dict-public-licenses/-/dict-public-licenses-2.0.16.tgz", + "integrity": "sha512-EQRrPvEOmwhwWezV+W7LjXbIBjiy6y/shrET6Qcpnk3XANTzfvWflf9PnJ5kId/oKWvihFy0za0AV1JHd03pSQ==", "dev": true, "license": "MIT" }, "node_modules/@cspell/dict-python": { - "version": "4.2.25", - "resolved": "https://registry.npmjs.org/@cspell/dict-python/-/dict-python-4.2.25.tgz", - "integrity": "sha512-hDdN0YhKgpbtZVRjQ2c8jk+n0wQdidAKj1Fk8w7KEHb3YlY5uPJ0mAKJk7AJKPNLOlILoUmN+HAVJz+cfSbWYg==", + "version": "4.2.27", + "resolved": "https://registry.npmjs.org/@cspell/dict-python/-/dict-python-4.2.27.tgz", + "integrity": "sha512-Rj6xQgYS4X6ienjgAZF+njA0GRY4oSPouJWv0vfikCTn6EWlfk0V6Dy1HP3Migj1O+IC2NmespgVq+BZNSp8OA==", "dev": true, "license": "MIT", "dependencies": { - "@cspell/dict-data-science": "^2.0.13" + "@cspell/dict-data-science": "^2.0.14" } }, "node_modules/@cspell/dict-r": { @@ -652,9 +564,9 @@ "license": "MIT" }, "node_modules/@cspell/dict-ruby": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@cspell/dict-ruby/-/dict-ruby-5.1.0.tgz", - "integrity": "sha512-9PJQB3cfkBULrMLp5kSAcFPpzf8oz9vFN+QYZABhQwWkGbuzCIXSorHrmWSASlx4yejt3brjaWS57zZ/YL5ZQQ==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@cspell/dict-ruby/-/dict-ruby-5.1.1.tgz", + "integrity": "sha512-LHrp84oEV6q1ZxPPyj4z+FdKyq1XAKYPtmGptrd+uwHbrF/Ns5+fy6gtSi7pS+uc0zk3JdO9w/tPK+8N1/7WUA==", "dev": true, "license": "MIT" }, @@ -673,16 +585,16 @@ "license": "MIT" }, "node_modules/@cspell/dict-shell": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@cspell/dict-shell/-/dict-shell-1.1.2.tgz", - "integrity": "sha512-WqOUvnwcHK1X61wAfwyXq04cn7KYyskg90j4lLg3sGGKMW9Sq13hs91pqrjC44Q+lQLgCobrTkMDw9Wyl9nRFA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@cspell/dict-shell/-/dict-shell-1.2.0.tgz", + "integrity": "sha512-PVctvT22lJ49niMiakO8xieY7ELCAzjSqhejWR7bAMb5AZ9F4WDEs+XdGMnoVHWeXq7K5rcepLPmEJb+37zzIw==", "dev": true, "license": "MIT" }, "node_modules/@cspell/dict-software-terms": { - "version": "5.1.20", - "resolved": "https://registry.npmjs.org/@cspell/dict-software-terms/-/dict-software-terms-5.1.20.tgz", - "integrity": "sha512-TEk1xHvetTI4pv7Vzje1D322m6QEjaH2P6ucOOf6q7EJCppQIdC0lZSXkgHJAFU5HGSvEXSzvnVeW2RHW86ziQ==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@cspell/dict-software-terms/-/dict-software-terms-5.2.2.tgz", + "integrity": "sha512-0CaYd6TAsKtEoA7tNswm1iptEblTzEe3UG8beG2cpSTHk7afWIVMtJLgXDv0f/Li67Lf3Z1Jf3JeXR7GsJ2TRw==", "dev": true, "license": "MIT" }, @@ -728,350 +640,515 @@ "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/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==", + "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": ">=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==", + "node_modules/@cspell/strong-weak-map": { + "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/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "node_modules/@cspell/url": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@cspell/url/-/url-10.0.1.tgz", + "integrity": "sha512-abYYgI29wJhWIfWTYrYuzRYDcHQUQ1N5ylnhxYn1NJnIQMqUWGLbDmt12JABtZ+R6h6UNatQrS7rhP86etvJyQ==", "dev": true, "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "node": ">=22.18.0" } }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=18" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=18" } }, - "node_modules/@eslint/config-array": { - "version": "0.23.3", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", - "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^3.0.3", - "debug": "^4.3.1", - "minimatch": "^10.2.4" - }, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=18" } }, - "node_modules/@eslint/config-array/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "18 || 20 || >=22" + "node": ">=18" } }, - "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "18 || 20 || >=22" + "node": ">=18" } }, - "node_modules/@eslint/config-array/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">=18" } }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=18" } }, - "node_modules/@eslint/config-array/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@eslint/config-helpers": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.2.tgz", - "integrity": "sha512-a5MxrdDXEvqnIq+LisyCX6tQMPF/dSJpCfBgBauY+pNZ28yCtSsTvyTYrMhaI+LK26bVyCJfJkT0u8KIj2i1dQ==", + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.1.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=18" } }, - "node_modules/@eslint/core": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", - "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=18" } }, - "node_modules/@eslint/object-schema": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", - "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=18" } }, - "node_modules/@eslint/plugin-kit": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", - "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.1.1", - "levn": "^0.4.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=18" } }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18.0" + "node": ">=18" } }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18.0" + "node": ">=18" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], "dev": true, - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], "dev": true, - "license": "Apache-2.0", + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=18" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": ">=18" } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=18" } }, - "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==", + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1081,30 +1158,33 @@ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@mswjs/interceptors": { - "version": "0.39.7", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.39.7.tgz", - "integrity": "sha512-sURvQbbKsq5f8INV54YJgJEdk8oxBanqkTiXXd33rKmofFCwZLhLRszPduMZ9TA9b8/1CHc/IJmOlBHJk2Q5AQ==", + "version": "0.39.8", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.39.8.tgz", + "integrity": "sha512-2+BzZbjRO7Ct61k8fMNHEtoKjeWI9pIlHFTqBwZ5icHpqszIgEZbjb1MW5Z0+bITTCTl3gk4PDBxs9tA/csXvA==", "dev": true, "license": "MIT", "dependencies": { @@ -1131,41 +1211,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@octokit/auth-token": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", @@ -1227,15 +1272,15 @@ "license": "MIT" }, "node_modules/@octokit/request": { - "version": "10.0.8", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.8.tgz", - "integrity": "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==", + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.10.tgz", + "integrity": "sha512-KxNC2pTqqhszMNrf12ZRd4PonRgyJdsM4F/jySiddQK+DsRcfBtUvqn8t7UsyZhnRJHvX46OohDt5N3VqIWC2w==", "license": "MIT", "dependencies": { "@octokit/endpoint": "^11.0.3", "@octokit/request-error": "^7.0.2", "@octokit/types": "^16.0.0", - "fast-content-type-parse": "^3.0.0", + "content-type": "^2.0.0", "json-with-bigint": "^3.5.3", "universal-user-agent": "^7.0.2" }, @@ -1290,99 +1335,123 @@ "license": "MIT" }, "node_modules/@paralleldrive/cuid2": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.2.2.tgz", - "integrity": "sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz", + "integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==", "license": "MIT", "dependencies": { "@noble/hashes": "^1.1.5" } }, - "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, - "optional": true, + "node_modules/@puppeteer/browsers": { + "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": { + "modern-tar": "^0.7.6", + "yargs": "^18.0.0" + }, + "bin": { + "browsers": "lib/main-cli.js" + }, "engines": { - "node": ">=14" + "node": ">=22.12.0" + }, + "peerDependencies": { + "proxy-agent": ">=8.0.1" + }, + "peerDependenciesMeta": { + "proxy-agent": { + "optional": true + } } }, - "node_modules/@pkgr/utils": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@pkgr/utils/-/utils-2.4.2.tgz", - "integrity": "sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==", - "dev": true, + "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": { - "cross-spawn": "^7.0.3", - "fast-glob": "^3.3.0", - "is-glob": "^4.0.3", - "open": "^9.1.0", - "picocolors": "^1.0.0", - "tslib": "^2.6.0" + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" }, "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" + "node": ">=20" } }, - "node_modules/@puppeteer/browsers": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", - "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==", - "license": "Apache-2.0", + "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": { - "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" - }, - "bin": { - "browsers": "lib/cjs/main-cli.js" + "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/@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/@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.0", - "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", - "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" }, "node_modules/@tokenizer/inflate": { "version": "0.4.1", @@ -1430,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", @@ -1447,6 +1510,16 @@ "@types/node": "*" } }, + "node_modules/@types/busboy": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/busboy/-/busboy-1.5.4.tgz", + "integrity": "sha512-kG7WrUuAKK0NoyxfQHsVE6j1m01s6kMma64E+OZenQABMQyTJop1DumUWcLwAQ2JzpefU7PDYoRDKl8uZosFjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -1465,27 +1538,14 @@ "license": "MIT" }, "node_modules/@types/cors": { - "version": "2.8.17", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", - "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", "dependencies": { "@types/node": "*" } }, - "node_modules/@types/esrecurse": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/express": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", @@ -1498,10 +1558,21 @@ "@types/serve-static": "^2" } }, - "node_modules/@types/express-serve-static-core": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", - "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "node_modules/@types/express-fileupload": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@types/express-fileupload/-/express-fileupload-1.5.1.tgz", + "integrity": "sha512-DllImBVI1lCyjl2klky/TEwk60mbNebgXv1669h66g9TfptWSrEFq5a/raHSutaFzjSm1tmn9ypdNfu4jPSixQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/busboy": "*", + "@types/express": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", "dev": true, "license": "MIT", "dependencies": { @@ -1525,13 +1596,23 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "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", @@ -1539,12 +1620,15 @@ "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==", + "node_modules/@types/morgan": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz", + "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "@types/node": "*" + } }, "node_modules/@types/node": { "version": "24.13.2", @@ -1556,9 +1640,9 @@ } }, "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", "dev": true, "license": "MIT" }, @@ -1603,6 +1687,13 @@ "form-data": "^4.0.0" } }, + "node_modules/@types/tmp": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz", + "integrity": "sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -1612,81 +1703,26 @@ "@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/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" }, "engines": { "node": ">= 0.6" } }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_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==", + "node_modules/accepts/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "license": "MIT", "engines": { - "node": ">= 14" - } - }, - "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "node": ">= 0.6" } }, "node_modules/ansi-escapes": { @@ -1706,45 +1742,29 @@ } }, "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=12" }, "funding": { "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, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" - }, "node_modules/array-timsort": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", @@ -1755,141 +1775,35 @@ "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" - }, - "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" - } + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "license": "MIT" }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", - "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==" + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/b4a": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", - "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", - "license": "Apache-2.0", - "peerDependencies": { - "react-native-b4a": "*" - }, - "peerDependenciesMeta": { - "react-native-b4a": { - "optional": true - } - } + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" }, "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 - }, - "node_modules/bare-events": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", - "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", - "license": "Apache-2.0", - "peerDependencies": { - "bare-abort-controller": "*" - }, - "peerDependenciesMeta": { - "bare-abort-controller": { - "optional": true - } - } - }, - "node_modules/bare-fs": { - "version": "4.5.6", - "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.6.tgz", - "integrity": "sha512-1QovqDrR80Pmt5HPAsMsXTCFcDYr+NSUKW6nd6WO5v0JBmnItc/irNRzm2KOQ5oZ69P37y+AMujNyNtG+1Rggw==", - "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.8.0", - "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.0.tgz", - "integrity": "sha512-Dc9/SlwfxkXIGYhvMQNUtKaXCaGkZYGcd1vuNUUADVqzu4/vQfvnMkYYOUnt2VwQ2AqKr/8qAVFRtwETljgeFg==", - "license": "Apache-2.0", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", "engines": { - "bare": ">=1.14.0" - } - }, - "node_modules/bare-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", - "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", - "license": "Apache-2.0", - "dependencies": { - "bare-os": "^3.0.1" - } - }, - "node_modules/bare-stream": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.10.0.tgz", - "integrity": "sha512-DOPZF/DDcDruKDA43cOw6e9Quq5daua7ygcAwJE/pKJsRWhgSSemi7qVNGE5kyDIxIeN1533G/zfbvWX7Wcb9w==", - "license": "Apache-2.0", - "dependencies": { - "streamx": "^2.25.0", - "teex": "^1.0.1" - }, - "peerDependencies": { - "bare-buffer": "*", - "bare-events": "*" - }, - "peerDependenciesMeta": { - "bare-buffer": { - "optional": true - }, - "bare-events": { - "optional": true - } - } - }, - "node_modules/bare-url": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz", - "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==", - "license": "Apache-2.0", - "dependencies": { - "bare-path": "^3.0.0" + "node": "18 || 20 || >=22" } }, "node_modules/base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", "engines": { "node": "^4.5.0 || >= 5.9" } @@ -1898,6 +1812,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", "dependencies": { "safe-buffer": "5.1.2" }, @@ -1905,14 +1820,11 @@ "node": ">= 0.8" } }, - "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/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" }, "node_modules/before-after-hook": { "version": "4.0.0", @@ -1920,39 +1832,21 @@ "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", "license": "Apache-2.0" }, - "node_modules/big-integer": { - "version": "1.6.51", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", - "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", - "dev": true, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/body-parser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", - "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz", + "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==", "license": "MIT", "dependencies": { "bytes": "^3.1.2", - "content-type": "^1.0.5", + "content-type": "^2.0.0", "debug": "^4.4.3", - "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", - "qs": "^6.14.0", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" + "qs": "^6.15.2", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" }, "engines": { "node": ">=18" @@ -1980,9 +1874,9 @@ } }, "node_modules/body-parser/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -2007,69 +1901,27 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "license": "ISC" }, - "node_modules/bplist-parser": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", - "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", - "dev": true, - "dependencies": { - "big-integer": "^1.6.44" - }, - "engines": { - "node": ">= 5.10.0" - } - }, "node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", - "dev": true, + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "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, - "dependencies": { - "fill-range": "^7.1.1" + "balanced-match": "^4.0.2" }, "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 - }, - "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": "18 || 20 || >=22" } }, - "node_modules/bundle-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", - "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", - "dev": true, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", "dependencies": { - "run-applescript": "^5.0.0" + "streamsearch": "^1.1.0" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=10.16.0" } }, "node_modules/bytes": { @@ -2115,47 +1967,17 @@ } } }, - "node_modules/c8/node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" }, "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/c8/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" + "node": ">= 0.4" } }, "node_modules/call-bound": { @@ -2174,45 +1996,25 @@ "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==", - "engines": { - "node": ">=6" - } - }, - "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": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, + "license": "MIT", "engines": { - "node": ">=10" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, "funding": { "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/chalk-template": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-1.1.0.tgz", - "integrity": "sha512-T2VJbcDuZQ0Tb2EWwSotMPJjgpy1/tGee1BTpUNsGZ/qgNjV2t7Mvu+d4600U564nbLesN1x2dPL+xii174Ekg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-1.1.2.tgz", + "integrity": "sha512-2bxTP2yUH7AJj/VAXfcA+4IcWGdQ87HwBANLt5XxGTeomo8yG0y95N1um9i5StvhT/Bl0/2cARA5v1PpPXUxUA==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^5.2.0" }, @@ -2223,18 +2025,6 @@ "url": "https://github.com/chalk/chalk-template?sponsor=1" } }, - "node_modules/chalk-template/node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", - "dev": true, - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/cheerio": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", @@ -2307,85 +2097,20 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/cheerio/node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "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" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "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.2", - "resolved": "https://registry.npmjs.org/clear-module/-/clear-module-4.1.2.tgz", - "integrity": "sha512-LWAxzHqdHsAZlPlEyJ2Poz6AIs384mPeqLVCru2p0BrP9G/kVGuhNyZYClLO6cXlnuJjzC8xtsJIuMjKqLXoAw==", - "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": { @@ -2405,14 +2130,14 @@ } }, "node_modules/cli-truncate": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", - "integrity": "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", "dev": true, "license": "MIT", "dependencies": { - "slice-ansi": "^7.1.0", - "string-width": "^8.0.0" + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" }, "engines": { "node": ">=20" @@ -2421,76 +2146,52 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-truncate/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "node_modules/cliui": { + "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": "MIT", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, "engines": { "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/cli-truncate/node_modules/string-width": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.1.1.tgz", - "integrity": "sha512-KpqHIdDL9KwYk22wEOg/VIqYbrnLeSApsKT/bSj6Ez7pn3CftUiLAv2Lccpq1ALcpLV9UX1Ppn92npZWu2w/aw==", + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.0", - "strip-ansi": "^7.1.0" - }, "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/cli-truncate/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=12" + "node": ">=8" }, "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, "node_modules/cliui/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -2500,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", @@ -2510,10 +2212,43 @@ "node": ">=8" } }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "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/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -2524,12 +2259,7 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true, "license": "MIT" }, @@ -2537,6 +2267,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -2555,14 +2286,13 @@ } }, "node_modules/comment-json": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.5.1.tgz", - "integrity": "sha512-taEtr3ozUmOB7it68Jll7s0Pwm+aoiHyXKrEC8SEodL4rNpdfDLqa7PfBlrgFoCNNdR8ImL+muti5IGvktJAAg==", + "version": "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": { "array-timsort": "^1.0.3", - "core-util-is": "^1.0.3", "esprima": "^4.0.1" }, "engines": { @@ -2582,6 +2312,7 @@ "version": "2.0.18", "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", "dependencies": { "mime-db": ">= 1.43.0 < 2" }, @@ -2607,92 +2338,53 @@ "node": ">= 0.8.0" } }, - "node_modules/compression/node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", "license": "MIT", "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/content-disposition": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", - "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", - "dependencies": { - "safe-buffer": "5.2.1" + "node": ">=18" }, - "engines": { - "node": ">= 0.6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/content-disposition/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", "license": "MIT", "engines": { - "node": ">= 0.6" + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/cookie-signature": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.1.tgz", - "integrity": "sha512-78KWk9T26NhzXtuL26cIJ8/qNHANyJ/ZYrmEXFzUmhZdjpBv+DlWlOANRTGBt48YcyslsLrj0bMLFTmXvLRCOw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", "engines": { "node": ">=6.6.0" } @@ -2700,13 +2392,7 @@ "node_modules/cookiejar": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", - "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==" - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "license": "MIT" }, "node_modules/cors": { @@ -2742,236 +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_modules/cspell-glob/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "node": ">=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_modules/cspell-lib/node_modules/env-paths": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", - "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=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==", - "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_modules/cspell/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "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", "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "node": ">=22.18.0" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/cspell/node_modules/file-entry-cache": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-9.1.0.tgz", - "integrity": "sha512-/pqPFG+FdxWQj+/WSuzXSDaNzxgTLr/OrR1QuqfEZzDakpdYE70PwUxL7BPUa8hpjbvY1+qvCl8k+8Tq34xJgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^5.0.0" - }, - "engines": { - "node": ">=18" + "peerDependencies": { + "@cspell/cspell-types": "10.0.1" } }, "node_modules/css-select": { @@ -3017,93 +2654,20 @@ "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", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/default-browser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", - "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", - "dev": true, - "dependencies": { - "bundle-name": "^3.0.0", - "default-browser-id": "^3.0.0", - "execa": "^7.1.1", - "titleize": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser-id": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", - "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", - "dev": true, - "dependencies": { - "bplist-parser": "^0.2.0", - "untildify": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "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", - "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" + "ms": "2.0.0" } }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -3112,44 +2676,27 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", "engines": { "node": ">= 0.8" } }, "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": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", + "license": "ISC", "dependencies": { "asap": "^2.0.0", "wrappy": "1" } }, - "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", - "integrity": "sha512-5u7qAb+ACe2dvxHXrhjWNAfWuj42yB45Z9ght+U9QkB09nNGYMw5S4q6sXcpVnxjcKGl0jUYcxrGpR6kBTGJHw==", - "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", @@ -3179,18 +2726,6 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/dom-serializer/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/domelementtype": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", @@ -3279,22 +2814,18 @@ "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 - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" }, "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" }, "node_modules/encodeurl": { "version": "2.0.0", @@ -3318,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", @@ -3352,14 +2874,20 @@ "version": "5.2.3", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", "engines": { "node": ">=10.0.0" } }, - "node_modules/engine.io/node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "node_modules/engine.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, "engines": { "node": ">= 0.6" } @@ -3381,16 +2909,46 @@ } } }, + "node_modules/engine.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/engine.io/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/engine.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -3400,11 +2958,19 @@ } }, "node_modules/env-paths": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "version": "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": ">=6" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/environment": { @@ -3420,14 +2986,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "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", @@ -3447,9 +3005,9 @@ } }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -3473,10 +3031,53 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", "engines": { "node": ">=6" } @@ -3487,398 +3088,18 @@ "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==", + "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, - "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", - "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" + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" }, "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/eslint": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.3.tgz", - "integrity": "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.3", - "@eslint/config-helpers": "^0.5.2", - "@eslint/core": "^1.1.1", - "@eslint/plugin-kit": "^0.6.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.2", - "eslint-visitor-keys": "^5.0.1", - "espree": "^11.1.1", - "esquery": "^1.7.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-config-prettier": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.0.3.tgz", - "integrity": "sha512-wlypkcjrA6L2fG+WTmEELzZJFmiF3ftdirkmz8te3FOVwviFvaeYxHPtBvwaIlvjUC4QjrTrPxRMddx+CU7Tlw==", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "build/bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-es": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", - "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", - "dev": true, - "dependencies": { - "eslint-utils": "^2.0.0", - "regexpp": "^3.0.0" - }, - "engines": { - "node": ">=8.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=4.19.1" - } - }, - "node_modules/eslint-plugin-es/node_modules/eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/eslint-plugin-es/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-plugin-node": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", - "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", - "dev": true, - "dependencies": { - "eslint-plugin-es": "^3.0.0", - "eslint-utils": "^2.0.0", - "ignore": "^5.1.1", - "minimatch": "^3.0.4", - "resolve": "^1.10.1", - "semver": "^6.1.0" - }, - "engines": { - "node": ">=8.10.0" - }, - "peerDependencies": { - "eslint": ">=5.16.0" - } - }, - "node_modules/eslint-plugin-node/node_modules/eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "dependencies": { - "eslint-visitor-keys": "^1.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, - "node_modules/eslint-plugin-node/node_modules/eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/eslint-plugin-node/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-plugin-prettier": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.0.1.tgz", - "integrity": "sha512-m3u5RnR56asrwV/lDC4GHorlW75DsFfmUcjfCYylTUs85dBRnB7VM6xG8eCMJdeDRnppzmxZVf1GEPJvl1JmNg==", - "dev": true, - "license": "MIT", - "dependencies": { - "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.5" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/prettier" - }, - "peerDependencies": { - "@types/eslint": ">=8.0.0", - "eslint": ">=8.0.0", - "prettier": ">=3.0.0" - }, - "peerDependenciesMeta": { - "@types/eslint": { - "optional": true - }, - "eslint-config-prettier": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/balanced-match": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz", - "integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/eslint/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/eslint/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/eslint/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/espree": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.1.1.tgz", - "integrity": "sha512-AVHPqQoZYc+RUM4/3Ly5udlZY/U4LS8pIG05jEjWM2lQMU/oaZ7qshzAl2YP1tfNmXfftH3ohurfwNAug+MnsQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.16.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, "node_modules/etag": { @@ -3897,38 +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/execa": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", - "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.1", - "human-signals": "^4.3.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^3.0.7", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": "^14.18.0 || ^16.14.0 || >=18.0.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, "node_modules/expect.js": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/expect.js/-/expect.js-0.3.1.tgz", @@ -3990,25 +3179,6 @@ "node": ">=12.0.0" } }, - "node_modules/express-fileupload/node_modules/busboy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", - "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", - "dependencies": { - "streamsearch": "^1.1.0" - }, - "engines": { - "node": ">=10.16.0" - } - }, - "node_modules/express-fileupload/node_modules/streamsearch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", - "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/express-handlebars": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/express-handlebars/-/express-handlebars-9.0.1.tgz", @@ -4023,310 +3193,77 @@ "node": ">=22.22.2" } }, - "node_modules/express-handlebars/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/express-handlebars/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "node_modules/express/node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/express-handlebars/node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/express-handlebars/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/express/node_modules/accepts": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", - "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", - "dependencies": { - "mime-types": "^3.0.0", - "negotiator": "^1.0.0" - }, "engines": { "node": ">= 0.6" } }, "node_modules/express/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/express/node_modules/mime-db": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.53.0.tgz", - "integrity": "sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/mime-types": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.0.tgz", - "integrity": "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==", - "dependencies": { - "mime-db": "^1.53.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/express/node_modules/negotiator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", - "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "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/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" + "ms": "^2.1.3" }, "engines": { - "node": ">=8" + "node": ">=6.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/extract-zip/node_modules/ms": { + "node_modules/express/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/fast-content-type-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", - "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-diff": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", - "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", - "dev": true - }, "node_modules/fast-equals": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", - "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-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" }, - "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } - }, - "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/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "dependencies": { - "flat-cache": "^4.0.0" - }, "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/file-entry-cache/node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" + "node": ">=12.0.0" }, - "engines": { - "node": ">=16" + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } } }, "node_modules/file-type": { @@ -4347,22 +3284,10 @@ "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, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/finalhandler": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", - "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", "license": "MIT", "dependencies": { "debug": "^4.4.0", @@ -4373,13 +3298,17 @@ "statuses": "^2.0.1" }, "engines": { - "node": ">= 0.8" + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/finalhandler/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4404,6 +3333,7 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -4415,36 +3345,30 @@ "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==", + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, - "bin": { - "flat": "cli.js" - } + "license": "ISC" }, - "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==", + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "flatted": "^3.3.1", - "keyv": "^4.5.4" + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" }, "engines": { - "node": ">=18" + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", - "dev": true, - "license": "ISC" - }, "node_modules/form-data": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", @@ -4461,6 +3385,27 @@ "node": ">= 6" } }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/formidable": { "version": "3.5.4", "resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz", @@ -4482,6 +3427,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -4490,6 +3436,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -4513,33 +3460,34 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "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": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, "node_modules/get-east-asian-width": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz", - "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==", - "dev": true, + "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==", "license": "MIT", "engines": { "node": ">=18" @@ -4569,206 +3517,50 @@ "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "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": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/glob/node_modules/foreground-child": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", - "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.0", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob/node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, - "node_modules/glob/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true - }, - "node_modules/glob/node_modules/minimatch": { - "version": "9.0.9", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", - "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.2" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob/node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" }, "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">= 0.4" } }, - "node_modules/glob/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, "engines": { - "node": ">=14" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "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" @@ -4789,7 +3581,8 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" }, "node_modules/handlebars": { "version": "4.7.9", @@ -4812,23 +3605,12 @@ "uglify-js": "^3.1.4" } }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -4872,20 +3654,12 @@ "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, - "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", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/htmlparser2": { "version": "10.1.0", @@ -4953,87 +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/human-signals": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", - "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", - "dev": true, - "engines": { - "node": ">=14.18.0" - } - }, "node_modules/husky": { "version": "9.0.11", "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz", @@ -5082,56 +3775,19 @@ ], "license": "BSD-3-Clause" }, - "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true - }, "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==", - "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==", - "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", @@ -5143,237 +3799,74 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" }, "node_modules/ini": { - "version": "4.1.1", - "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": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/insafe/-/insafe-0.5.1.tgz", - "integrity": "sha512-LVGi8wNYSld/ElRmd06ikkY2uBV4ik1JaPf0FRQ4awW6t+3VqPBzePs74/P7CBTm5SpItIfOOFIs/K66cyx9Rw==" - }, - "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" - } + "integrity": "sha512-LVGi8wNYSld/ElRmd06ikkY2uBV4ik1JaPf0FRQ4awW6t+3VqPBzePs74/P7CBTm5SpItIfOOFIs/K66cyx9Rw==", + "license": "MIT" }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "engines": { - "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==" - }, - "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, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-core-module": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", - "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "dev": true, - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "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", - "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", - "dev": true, "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.3.1" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "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, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "dev": true, - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-node-process": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", - "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", - "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, - "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, - "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, - "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-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">= 0.10" } }, - "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==", + "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", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", "dev": true, - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } + "license": "MIT" }, - "node_modules/is-wsl/node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "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-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, - "bin": { - "is-docker": "cli.js" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -5383,13 +3876,15 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=8" } @@ -5409,22 +3904,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-report/node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/istanbul-reports": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", @@ -5439,234 +3918,71 @@ "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==" - }, - "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 - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/json-with-bigint": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.3.tgz", - "integrity": "sha512-QObKu6nxy7NsxqR0VK4rkXnsNr5L9ElJaGEg+ucJ6J7/suoKZ0n+p76cu9aCqowytxEbwYNzvrMerfMkXneF5A==", + "version": "3.5.8", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.8.tgz", + "integrity": "sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==", "license": "MIT" }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, + "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", "engines": { - "node": ">= 0.8.0" + "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==" - }, "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" - } - }, - "node_modules/lint-staged/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "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" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/listr2/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/listr2/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/listr2/node_modules/emoji-regex": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", - "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", - "dev": true, - "license": "MIT" - }, - "node_modules/listr2/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/listr2/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/listr2/node_modules/wrap-ansi": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", - "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "string-width": "^7.0.0", - "strip-ansi": "^7.1.0" + "wrap-ansi": "^10.0.0" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": ">=22.13.0" } }, "node_modules/locate-path": { @@ -5674,6 +3990,7 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -5691,22 +4008,6 @@ "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, - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/log-update": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", @@ -5727,32 +4028,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/log-update/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/log-update/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/log-update/node_modules/emoji-regex": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", @@ -5760,38 +4035,39 @@ "dev": true, "license": "MIT" }, - "node_modules/log-update/node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" }, "engines": { "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/log-update/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "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": { - "ansi-regex": "^6.0.1" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/log-update/node_modules/wrap-ansi": { @@ -5812,6 +4088,31 @@ "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", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5834,6 +4135,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", "engines": { "node": ">=18" }, @@ -5841,51 +4143,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/metaviewport-parser": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/metaviewport-parser/-/metaviewport-parser-0.3.0.tgz", - "integrity": "sha512-EoYJ8xfjQ6kpe9VbVHvZTZHiOl4HL1Z18CrZ+qahvLXT7ZO4YTC2JMyt5FaUp9JJp6J4Ybb/z7IsCXZt86/QkQ==" + "integrity": "sha512-EoYJ8xfjQ6kpe9VbVHvZTZHiOl4HL1Z18CrZ+qahvLXT7ZO4YTC2JMyt5FaUp9JJp6J4Ybb/z7IsCXZt86/QkQ==", + "license": "MIT" }, "node_modules/methods": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, "node_modules/mime": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "license": "MIT", "bin": { "mime": "cli.js" }, @@ -5894,34 +4171,28 @@ } }, "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", "dependencies": { - "mime-db": "1.52.0" + "mime-db": "^1.54.0" }, "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/mimic-function": { @@ -5938,22 +4209,25 @@ } }, "node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.5" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -5973,141 +4247,13 @@ "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/brace-expansion": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", - "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/mocha/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/mocha/node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/mocha/node_modules/debug/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/mocha/node_modules/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 - }, - "node_modules/mocha/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, + "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": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "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, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "node": ">=18.0.0" } }, "node_modules/morgan": { @@ -6133,18 +4279,14 @@ "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" }, "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -6152,169 +4294,35 @@ "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" - }, - "node_modules/netmask": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", - "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" }, "node_modules/nock": { "version": "15.0.0", "resolved": "https://registry.npmjs.org/nock/-/nock-15.0.0.tgz", "integrity": "sha512-EoAVk4Y8Yv4JUQz62sv8zmv+DoBblD/pht/q7aW/td1WietaFWrivzhMGGCLmx2qjpLcOrbyammedW8IRUU5TA==", "dev": true, - "license": "MIT", - "dependencies": { - "@mswjs/interceptors": "^0.39.5", - "json-stringify-safe": "^5.0.1" - }, - "engines": { - "node": ">=18.20.0 <20 || >=20.12.1" - } - }, - "node_modules/node-w3capi": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/node-w3capi/-/node-w3capi-2.2.2.tgz", - "integrity": "sha512-Is3FVoNOptDqqxlfXlQkJ+kCgHKyE+cOT/6vvCqF8EqxWstjZ9MjkNrQz7r+9vt4ctzy7IHN4R0zrVeMNQm8og==", - "license": "MIT", - "dependencies": { - "async": "3.2.6", - "superagent": "10.3.0" - }, - "engines": { - "node": "20 || 22 || 24", - "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/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, - "engines": { - "node": ">=4" - } - }, - "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/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, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", - "dev": true, - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "*" - } - }, - "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, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-path": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", - "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", - "dev": true, + "license": "MIT", "dependencies": { - "path-key": "^4.0.0" + "@mswjs/interceptors": "^0.39.5", + "json-stringify-safe": "^5.0.1" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18.20.0 <20 || >=20.12.1" } }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "engines": { - "node": ">=12" + "node_modules/node-w3capi": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/node-w3capi/-/node-w3capi-2.2.2.tgz", + "integrity": "sha512-Is3FVoNOptDqqxlfXlQkJ+kCgHKyE+cOT/6vvCqF8EqxWstjZ9MjkNrQz7r+9vt4ctzy7IHN4R0zrVeMNQm8og==", + "license": "MIT", + "dependencies": { + "async": "3.2.6", + "superagent": "10.3.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": "20 || 22 || 24", + "npm": ">=7" } }, "node_modules/nth-check": { @@ -6333,6 +4341,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -6374,60 +4383,27 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", "dependencies": { "wrappy": "1" } }, "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", - "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "dev": true, + "license": "MIT", "dependencies": { - "default-browser": "^4.0.0", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "is-wsl": "^2.2.0" + "mimic-function": "^5.0.0" }, "engines": { - "node": ">=14.16" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", - "dev": true, - "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/outvariant": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", @@ -6440,6 +4416,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", "dev": true, + "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -6455,6 +4432,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -6465,96 +4443,16 @@ "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/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", - "integrity": "sha512-uo0Z9JJeWzv8BG+tRcapBKNJ0dro9cLyczGzulS6EfeyAdeC9sbojtW6XwvYxJkEne9En+J2XEl4zyglVeIwFg==", - "dev": true, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "license": "MIT", "dependencies": { - "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==", - "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" + "entities": "^6.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, "node_modules/parse5-htmlparser2-tree-adapter": { @@ -6585,18 +4483,6 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, - "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, "node_modules/parse5-parser-stream": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", @@ -6609,16 +4495,16 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/parse5-parser-stream/node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" }, "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" + "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/parseurl": { @@ -6635,6 +4521,7 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -6644,16 +4531,11 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", @@ -6670,59 +4552,29 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/path-to-regexp": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.0.tgz", - "integrity": "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg==", + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", "license": "MIT", "funding": { "type": "opencollective", "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": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/prettier": { "version": "3.8.4", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", @@ -6739,31 +4591,11 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "dependencies": { - "fast-diff": "^1.1.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "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", "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", + "license": "MIT", "dependencies": { "asap": "~2.0.6" } @@ -6772,6 +4604,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" @@ -6780,174 +4613,42 @@ "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/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 - }, - "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/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/puppeteer": { - "version": "24.40.0", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.40.0.tgz", - "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" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/puppeteer-core": { - "version": "24.40.0", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.40.0.tgz", - "integrity": "sha512-MWL3XbUCfVgGR0gRsidzT6oKJT2QydPLhMITU6HoVWiiv4gkb6gJi3pcdAa8q4HwjBTbqISOWVP4aJiiyUJvag==", - "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": "lib/puppeteer/node/cli.js" }, "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/puppeteer/node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "node_modules/puppeteer-core": { + "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": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" + "@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": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "node": ">=22.12.0" } }, "node_modules/qs": { @@ -6965,35 +4666,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "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", @@ -7019,9 +4691,9 @@ } }, "node_modules/raw-body/node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -7034,60 +4706,22 @@ "url": "https://opencollective.com/express" } }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/mysticatea" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, - "node_modules/resolve": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", - "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", - "dev": true, - "dependencies": { - "is-core-module": "^2.11.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/resolve-from": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -7109,45 +4743,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/restore-cursor/node_modules/onetime": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", - "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-function": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/restore-cursor/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -7172,9 +4767,9 @@ } }, "node_modules/router/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -7194,115 +4789,10 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/run-applescript": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", - "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", - "dev": true, - "dependencies": { - "execa": "^5.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/run-applescript/node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/run-applescript/node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/run-applescript/node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/run-applescript/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/run-applescript/node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/run-applescript/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/run-applescript/node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", "funding": [ { "type": "github", @@ -7317,24 +4807,19 @@ "url": "https://feross.org/support" } ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "license": "MIT" }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -7344,31 +4829,35 @@ } }, "node_modules/send": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", - "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", "license": "MIT", "dependencies": { - "debug": "^4.3.5", + "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", - "http-errors": "^2.0.0", - "mime-types": "^3.0.1", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", - "statuses": "^2.0.1" + "statuses": "^2.0.2" }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/send/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -7382,46 +4871,16 @@ } } }, - "node_modules/send/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "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, - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/serve-static": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", - "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", "license": "MIT", "dependencies": { "encodeurl": "^2.0.0", @@ -7431,6 +4890,10 @@ }, "engines": { "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/setprototypeof": { @@ -7444,6 +4907,7 @@ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -7456,19 +4920,20 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -7480,13 +4945,13 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -7533,61 +4998,46 @@ } }, "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "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, - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/slice-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", - "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.2.1", - "is-fullwidth-code-point": "^5.0.0" - }, + "license": "ISC", "engines": { - "node": ">=18" + "node": ">=14" }, "funding": { - "url": "https://github.com/chalk/slice-ansi?sponsor=1" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "node_modules/slice-ansi": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", "dev": true, "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" + }, "engines": { - "node": ">=12" + "node": ">=20" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "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": { @@ -7677,6 +5127,19 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/socket.io/node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/socket.io/node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -7694,67 +5157,47 @@ } } }, - "node_modules/socket.io/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/socks": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", - "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", - "license": "MIT", - "dependencies": { - "ip-address": "^10.0.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==", + "node_modules/socket.io/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, "engines": { - "node": ">= 14" + "node": ">= 0.6" } }, - "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==", + "node_modules/socket.io/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "license": "MIT", "dependencies": { - "ms": "^2.1.3" + "mime-db": "1.52.0" }, "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "node": ">= 0.6" } }, - "node_modules/socks-proxy-agent/node_modules/ms": { + "node_modules/socket.io/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/socket.io/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -7768,15 +5211,12 @@ "node": ">= 0.8" } }, - "node_modules/streamx": { - "version": "2.25.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", - "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", - "license": "MIT", - "dependencies": { - "events-universal": "^1.0.0", - "fast-fifo": "^1.3.2", - "text-decoder": "^1.1.0" + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" } }, "node_modules/strict-event-emitter": { @@ -7791,130 +5231,41 @@ "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.6.19" } }, "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.1.tgz", + "integrity": "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==", "dev": true, + "license": "MIT", "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=12" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "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, - "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/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/string-width-cjs/node_modules/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, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^6.2.2" }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, "engines": { "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "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, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "node_modules/strtok3": { @@ -7974,168 +5325,40 @@ "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/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/synckit": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.5.tgz", - "integrity": "sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==", - "dev": true, - "dependencies": { - "@pkgr/utils": "^2.3.1", - "tslib": "^2.5.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/unts" - } - }, - "node_modules/tar-fs": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", - "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.1.8", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", - "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", - "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", - "integrity": "sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^13.0.6", - "minimatch": "^10.2.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/test-exclude/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "13.0.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "minimatch": "^10.2.2", - "minipass": "^7.1.3", - "path-scurry": "^2.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "license": "MIT" }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", "dependencies": { - "brace-expansion": "^5.0.2" + "has-flag": "^4.0.0" }, "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=8" } }, - "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", + "node_modules/test-exclude": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-8.0.0.tgz", + "integrity": "sha512-ZOffsNrXYggvU1mDGHk54I96r26P8SyMjO5slMKSc7+IWmtB/MQKnEC2fP51imB3/pT6YK5cT5E8f+Dd9KdyOQ==", + "dev": true, + "license": "ISC", "dependencies": { - "b4a": "^1.6.4" + "@istanbuljs/schema": "^0.1.2", + "glob": "^13.0.6", + "minimatch": "^10.2.2" + }, + "engines": { + "node": "20 || >=22" } }, "node_modules/tinyexec": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", - "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", "dev": true, "license": "MIT", "engines": { @@ -8143,14 +5366,14 @@ } }, "node_modules/tinyglobby": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", - "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -8159,46 +5382,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.4.4", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", - "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/titleize": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", - "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/tmp": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", @@ -8208,18 +5391,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, - "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", @@ -8247,81 +5418,54 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/touch": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", - "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "node_modules/tsx": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", + "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==", "dev": true, + "license": "MIT", "dependencies": { - "nopt": "~1.0.10" + "esbuild": "~0.28.0" }, "bin": { - "nodetouch": "bin/nodetouch.js" - } - }, - "node_modules/tslib": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", - "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" - }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1" + "tsx": "dist/cli.mjs" }, "engines": { - "node": ">= 0.8.0" + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" } }, "node_modules/type-is": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", - "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", "license": "MIT", "dependencies": { - "content-type": "^1.0.5", + "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" }, "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "license": "MIT", - "dependencies": { - "mime-db": "^1.54.0" + "node": ">= 18" }, - "engines": { - "node": ">= 0.6" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/typed-query-selector": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.1.tgz", - "integrity": "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==", + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", + "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", "license": "MIT" }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -8335,6 +5479,7 @@ "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "license": "BSD-2-Clause", "optional": true, "bin": { "uglifyjs": "bin/uglifyjs" @@ -8355,12 +5500,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 - }, "node_modules/undici": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", @@ -8391,25 +5530,6 @@ "node": ">= 0.8" } }, - "node_modules/untildify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -8425,17 +5545,11 @@ "node": ">=10.12.0" } }, - "node_modules/v8-to-istanbul/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -8455,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": { @@ -8487,6 +5601,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -8500,112 +5615,32 @@ "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==" - }, - "node_modules/workerpool": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.2.tgz", - "integrity": "sha512-Xz4Nm9c+LiBHhDR5bDLnNzmj6+5F+cyEAWPMkbs2awq/dYazR/efelZzUAjB/y3kNHL+uzkHvxVVpaOfGCPV7A==", - "dev": true, - "license": "Apache-2.0" + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" }, "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/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==", + "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": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" + "ansi-styles": "^6.2.3", + "string-width": "^8.2.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=10" + "node": ">=20" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/wrap-ansi-cjs/node_modules/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, - "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, - "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/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "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/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" }, "node_modules/ws": { "version": "8.21.0", @@ -8645,14 +5680,15 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "dev": true, "license": "ISC", "bin": { @@ -8666,9 +5702,10 @@ } }, "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -8687,60 +5724,27 @@ "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" } }, - "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, - "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-unparser/node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/yargs-unparser/node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, "node_modules/yargs/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -8750,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", @@ -8760,14 +5765,17 @@ "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==", + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, "node_modules/yocto-queue": { @@ -8775,6 +5783,7 @@ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, diff --git a/package.json b/package.json index cf5bf02ef..9d5c4c078 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,25 @@ { "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/validator", + "main": "lib/specberus.js", "type": "module", "repository": { "type": "git", "url": "https://github.com/w3c/specberus.git" }, + "files": [ + "app.{js,d.ts}", + "lib/**/*.{js,d.ts}", + "public/*", + "views/*" + ], "dependencies": { "@octokit/core": "^7.0.6", "cheerio": "^1.1.2", "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", @@ -24,63 +29,55 @@ "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" }, "devDependencies": { - "@rvagg/chai-as-promised": "8.0.2", "@types/express": "^5.0.6", - "@types/mocha": "^10.0.10", + "@types/express-fileupload": "^1.5.1", + "@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", + "cspell": "10.0.1", "domhandler": "^6.0.1", - "eslint": "10.0.3", - "eslint-config-prettier": "10.0.3", - "eslint-plugin-node": "11.1.0", - "eslint-plugin-prettier": "5.0.1", "expect.js": "0.3", "husky": "9.0.11", - "lint-staged": "16.4.0", + "lint-staged": "17.0.8", "lodash.merge": "^4.6.2", - "mocha": "11.7.6", "nock": "15.0.0", - "nodemon": "3.0.3", "prettier": "3.8.4", - "typescript": "^5.9.3" + "tsx": "^4.21.0", + "typescript": "^6.0.3" }, "scripts": { - "build": "npm run lint && npm run test", - "coverage": "c8 npm test", - "coverage:text": "c8 --reporter=text npm test", + "build": "tsc", + "check": "tsc --noEmit", + "coverage": "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 . && eslint --fix .", - "lint": "eslint . --report-unused-disable-directives && tsc && prettier -c .", - "live": "nodemon --use_strict app", + "fix": "prettier -w .", + "lint": "npm run check && prettier -c .", + "live": "tsx watch app.ts", + "prepack": "tsc", "prepare": "husky install", "spelling": "cspell \"**/*\"", - "start": "node --use_strict app", - "testserver": "nodemon test/lib/testserver.js", - "test": "NO_THROTTLE=true mocha" + "start": "node app", + "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": { - "*.js": "eslint --cache", "*": [ "cspell --no-must-find-files", "prettier --write --ignore-unknown" ] - }, - "mocha": { - "colors": true, - "reporter": "spec", - "timeout": 40000 } } 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 deleted file mode 100644 index ef71a7c2e..000000000 --- a/test/api.js +++ /dev/null @@ -1,162 +0,0 @@ -/** - * Test the REST API. - */ - -// Native packages: -// External packages: -import * as chai from 'chai'; -import chaiAsPromised from '@rvagg/chai-as-promised'; -import express from 'express'; -import fileUpload from 'express-fileupload'; -import http from 'http'; -import { join } from 'path'; -import superagent from 'superagent'; -import { setUp } from '../lib/api.js'; -import { importJSON } from '../lib/util.js'; -import { cleanupMocks, setupMocks } from './lib/utils.js'; -// Internal packages: -const meta = importJSON('../package.json', import.meta.url); -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(meta.version); - }); - }); - - 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”', () => { - const query = get('metadata?document=foo'); - return expect(query).to.eventually.be.rejectedWith( - 'Parameter “document” is not allowed in this context' - ); - }); - it('Should reject the parameter “source”', () => { - 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..71999a8a2 --- /dev/null +++ b/test/api.ts @@ -0,0 +1,303 @@ +/** + * Test the REST API. + */ + +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/specberus.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 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'); + 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 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')) + .then(handleJsonResponse) + .then(({ metadata }) => { + 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”', () => { + 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 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') + .field('profile', 'REC') + .attach('file', imscPath), + (error: any) => { + assertResponseStatus(error.response, 400); + 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 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( + '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') + .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'); + })); + + 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.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.deepStrictEqual(errors[0], { + error: '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 91% rename from test/data/goodDocuments.js rename to test/data/goodDocuments.ts index 3a0b911c8..5709021eb 100644 --- a/test/data/goodDocuments.js +++ 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: { @@ -62,7 +66,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 65% rename from test/doc-views/TR/Note/STMT.js rename to test/doc-views/TR/Note/STMT.ts index 3458f4de6..b4a3f6ea9 100644 --- a/test/doc-views/TR/Note/STMT.js +++ 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/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/docs/api/wd-fail-auto.html b/test/docs/api/wd-fail-auto.html new file mode 100644 index 000000000..805b8b567 --- /dev/null +++ b/test/docs/api/wd-fail-auto.html @@ -0,0 +1,148 @@ + + + + + + + + + + WD-Echidna: test document - Specberus + + +
    + +

    WD-Echidna: test document - Specberus

    + +

    + W3C Working Thing + +

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

    Abstract

    +

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

    + +
    + +
    +

    Status of This Document

    + +

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

    + + +

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

    + + +

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

    + + + + +

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

    + +

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

    + +
    + +
    +

    1. Introduction

    +

    This section is non-normative.

    +
    +
    +

    1. Time Origin

    +

    This section is non-normative.

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

    WD-Echidna: test document - Specberus

    + +

    + W3C Working Draft + +

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

    Abstract

    +

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

    + +
    + +
    +

    Status of This Document

    + +

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

    + + +

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

    + + +

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

    + + + + +

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

    + +

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

    + +
    + +
    +

    1. Introduction

    +

    This section is non-normative.

    +
    +
    +

    1. Time Origin

    +

    This section is non-normative.

    +
    + + + + + + diff --git a/test/l10n.js b/test/l10n.js deleted file mode 100644 index 0fd4a231e..000000000 --- a/test/l10n.js +++ /dev/null @@ -1,174 +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 { importJSON } from '../lib/util.js'; - -// Internal packages: -const rules = importJSON('../lib/rules.json', import.meta.url); - -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.prototype.hasOwnProperty.call(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.prototype.hasOwnProperty.call(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.prototype.hasOwnProperty.call( - 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) { - 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.prototype.hasOwnProperty.call(source, i)) - errors += `Section “${i}” exists in ${labelExpected} but is missing in ${labelSource}.\n`; - else if (source[i] !== false) - for (const j in expected[i]) - if (!Object.prototype.hasOwnProperty.call(source[i], j)) - errors += `Rule “${i}/${j}” exists in ${labelExpected} but is missing in ${labelSource}.\n`; - else if (source[i][j] !== false) - for (const k in expected[i][j]) - if ( - !Object.prototype.hasOwnProperty.call( - source[i][j], - k - ) - ) - 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-wrapper” 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..85976f70b --- /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 “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>> = {}; + + 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 61% rename from test/lib/utils.js rename to test/lib/utils.ts index 6900e431e..a864772d0 100644 --- a/test/lib/utils.js +++ b/test/lib/utils.ts @@ -1,80 +1,104 @@ -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) { - if (!req.url.includes('//localhost')) { +function warnOnNonLocalRequest(req: Request) { + if (!/\/\/(localhost|127\.0\.0\.1)/.test(req.url)) { 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 + // 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/); - /** @type {typeof nockData} */ - const mockData = overrides ? merge({}, nockData, overrides) : nockData; + const mockData: typeof nockData = overrides + ? merge({}, nockData, overrides) + : nockData; const notFoundNames = [ 'hr-foo-time', @@ -193,10 +217,12 @@ export function setupMocks(overrides) { // 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. */ diff --git a/test/rules.js b/test/rules.js deleted file mode 100644 index 96f0425b0..000000000 --- a/test/rules.js +++ /dev/null @@ -1,353 +0,0 @@ -import { nextTick } from 'process'; - -// External modules: -import { expect as chai } from 'chai'; -import expect from 'expect.js'; - -// Internal modules: -import { Sink } from '../lib/sink.js'; -import { allProfiles } from '../lib/util.js'; -import { Specberus } from '../lib/validator.js'; -// A list of good documents to be tested, using all rules configured in the profiles. -// 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 Sink(); - 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 Sink(); - - handler.on('err', (type, data) => { - if (DEBUG) console.log('error: \n', type, data); - handler.errors.push(`${type.name}.${data.key}`); - }); - handler.on('warning', (type, data) => { - if (DEBUG) console.log('warning: \n', type, data); - handler.warnings.push(`${type.name}.${data.key}`); - }); - handler.on('done', name => { - if (DEBUG) console.log(`----> ${name} check done`); - }); - handler.on('exception', data => { - console.error( - `[EXCEPTION] Validator had a massive failure: ${data.message}` - ); - }); - handler.on('end-all', () => { - try { - if (!test.errors) { - expect(handler.errors).to.be.empty(); - } else { - expect(handler.errors.length).to.eql(test.errors.length); - handler.errors.forEach((_, i) => { - expect(handler.errors).to.contain(test.errors[i]); - }); - } - - if (!test.ignoreWarnings) { - if (test.warnings) { - expect(handler.warnings.length).to.eql( - test.warnings.length - ); - handler.warnings.forEach((_, i) => { - expect(handler.warnings).to.contain(test.warnings[i]); - }); - } else { - expect(handler.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}.js`) - ); - import(`../lib/profiles/${profilePath}`).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..d564ddaf5 --- /dev/null +++ b/test/rules.ts @@ -0,0 +1,449 @@ +import assert from 'assert'; +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 type { HandlerMessage } from '../lib/types.js'; +import { allProfiles } from '../lib/util.js'; +import { + ExceptionsError, + Specberus, + type SpecberusResult, +} 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'; +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?: Partial[]; + [index: string]: any; +} + +/** + * 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 result = await specberus.extractMetadata({ file: testFile }); + + 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 as keyof HandlerMessage] === + 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; + assert( + key in result.metadata, + `Expected ${key} to be defined in metadata` + ); + assert.deepStrictEqual(result.metadata[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); + }); + + 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"', () => { + 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 addValidationEventListeners(sr: Specberus) { + if (DEBUG) { + sr.on('err', (type, data) => { + console.log('error:\n', type, data); + }); + sr.on('warning', (type, data) => { + console.log('warning:\n', type, data); + }); + sr.on('done', name => { + console.log(`----> ${name} check done`); + }); + } + 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'); + } + + 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; + +// 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 sr = new Specberus(); + addValidationEventListeners(sr); + const options = { + profile: { + ...extendedProfile, + rules, // do not change profile.rules + }, + url, + }; + + await verifySpecberusResult(sr.validate(options), { + ignoreWarnings: true, + }); + }); + } +}); + +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 (testType && test.data !== testType) 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 sr = new Specberus(); + addValidationEventListeners(sr); + const options = { + url, + profile: { + name: `Synthetic ${profile}/${rule}`, + rules: [ruleModule], + config: { + ...config, + ...test.config, + }, + }, + }; + + 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' + ); + }); + }); +} + +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 ... + if (testRule && rule !== testRule) return; + if (testType && !tests.some(({ data }) => data === testType)) + return; + + describe(`Rule: ${category}.${rule}`, () => { + checkRule(tests, { + docType, + track, + profile, + 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') { + if (testProfile && testProfile !== 'MEM-SUBM') return; + return runTestsForProfile({ + docType, + profile: 'MEM-SUBM', + rules: profilesOrRules as Record< + string, + Record + >, + }); + } + + // Track: Note/Recommendation/Registry + describe(`Track: ${trackOrProfile}`, () => { + Object.entries(profilesOrRules).forEach( + ([filename, rules]) => { + const profile = filename.slice( + 0, + filename.lastIndexOf('.') + ); + if (testProfile && testProfile !== profile) + return; + + 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/tools/fetch-groups-db.js b/tools/fetch-groups-db.js deleted file mode 100644 index 1a58dfb79..000000000 --- a/tools/fetch-groups-db.js +++ /dev/null @@ -1,79 +0,0 @@ -// This script updates our local database of groups -// Usage: node fetch-groups-db.js your-w3c-user your-w3c-password - -// XXX also look at https://cvs.w3.org/Team/WWW/2000/04/mem-news/groups.rdf - -import { load } from 'cheerio'; -import fs from 'fs'; -import pth, { dirname } from 'path'; -import ua from 'superagent'; -import { fileURLToPath } from 'url'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -const user = process.argv[2]; -const pass = process.argv[3]; -const results = {}; -if (!user || !pass) { - throw new Error( - 'Please pass in your W3C username and password to fetch from the groups page.' - ); -} - -/** - * @param str source string to normalize - * @returns {string} whitespace normalized string - */ -function norm(str) { - return str.replace(/^\s+/, '').replace(/\s+$/, '').replace(/\s+/g, ' '); -} - -/** - * @param err - * @param res - */ -function munge(err, res) { - const $ = load(res.text); - - $('tr.WG, tr.IG, tr.CG').each((_, tr) => { - const $tr = $(tr); - const $tds = $tr.find('td'); - const $td1 = $tds.eq(0); - const $td4 = $tds.eq(3); - const name = $td1.length && norm($td1.text()); - let href = - $td1.length && - $td1.find('a').length && - $td1.find('a').first().attr('href'); - if ($td4.length) { - $td4.find('a').each((_, el) => { - let list = $(el).text(); - if (!/@w3\.org$/.test(list)) list += '@w3.org'; - if (href.indexOf('http') === 0) true; - else if (href.indexOf('/') === 0) - href = `https://www.w3.org${href}`; - else if (/^(\.\.\/){2}\w/.test(href)) - href = `https://www.w3.org/${href.replace( - /^(\.\.\/){2}/, - '' - )}`; - else - console.error( - '--------------- UNKNOWN URL FORM -------------------' - ); - results[list] = { name, href }; - }); - } - }); - fs.writeFileSync( - pth.join(__dirname, '../lib/groups-db.json'), - JSON.stringify(results, null, 4) - ); -} - -ua.get('https://www.w3.org/Member/Mail/Overview.html') - .auth(user, pass) - .buffer(true) - .end(munge); - -// munge(err, { text: fs.readFileSync(pth.join(__dirname, "groups.html"), "utf8") }); diff --git a/tools/groups-sparql.json b/tools/groups-sparql.json deleted file mode 100644 index 14530031e..000000000 --- a/tools/groups-sparql.json +++ /dev/null @@ -1,550 +0,0 @@ -{ - "head": { - "vars": ["name", "mailbox", "homepage"] - }, - "results": { - "ordered": true, - "distinct": true, - "bindings": [ - { - "name": { "type": "literal", "value": "Audio Working Group" }, - "mailbox": { - "type": "uri", - "value": "mailto:public-audio@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2011/audio/" - } - }, - { - "name": { - "type": "literal", - "value": "Authoring Tool Accessibility Guidelines Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:w3c-wai-au@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/WAI/AU/" - } - }, - { - "name": { - "type": "literal", - "value": "Browser Testing and Tools Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-browser-tools-testing@w3.org@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/testing/browser/" - } - }, - { - "name": { - "type": "literal", - "value": "CSV on the Web Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-csv-wg@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2013/csvw/" - } - }, - { - "name": { - "type": "literal", - "value": "Data on the Web Best Practices Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-dwbp-wg@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2013/dwbp/" - } - }, - { - "name": { - "type": "literal", - "value": "Device APIs Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-device-apis@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2009/dap/" - } - }, - { - "name": { - "type": "literal", - "value": "Education and Outreach Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:w3c-wai-eo@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/WAI/EO/" - } - }, - { - "name": { - "type": "literal", - "value": "Efficient XML Interchange Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-exi@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/XML/EXI/" - } - }, - { - "name": { - "type": "literal", - "value": "Evaluation and Repair Tools Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-wai-ert@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/WAI/ER/" - } - }, - { - "name": { "type": "literal", "value": "Forms Working Group" }, - "mailbox": { - "type": "uri", - "value": "mailto:public-forms@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/MarkUp/Forms/" - } - }, - { - "name": { - "type": "literal", - "value": "Geolocation Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-geolocation@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2008/geolocation/" - } - }, - { - "name": { - "type": "literal", - "value": "Government Linked Data Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-gld-wg@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2011/gld/" - } - }, - { - "name": { "type": "literal", "value": "HTML Working Group" }, - "mailbox": { - "type": "uri", - "value": "mailto:public-html@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/html/wg/" - } - }, - { - "name": { - "type": "literal", - "value": "Independent User Interface (Indie UI) Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-indie-ui@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/WAI/IndieUI/" - } - }, - { - "name": { - "type": "literal", - "value": "Linked Data Platform (LDP) Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-ldp-wg@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2012/ldp/" - } - }, - { - "name": { - "type": "literal", - "value": "Media Annotations Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-media-annotation@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2008/WebVideo/Annotations/" - } - }, - { - "name": { - "type": "literal", - "value": "Model-Based User Interfaces Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-mbui@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2011/mbui/" - } - }, - { - "name": { - "type": "literal", - "value": "Multimodal Interaction Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:www-multimodal@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2002/mmi/" - } - }, - { - "name": { - "type": "literal", - "value": "Near Field Communications Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-nfc@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2012/nfc/" - } - }, - { - "name": { - "type": "literal", - "value": "Pointer Events Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-pointer-events@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2012/pointerevents/" - } - }, - { - "name": { "type": "literal", "value": "RDF Working Group" }, - "mailbox": { - "type": "uri", - "value": "mailto:public-rdf-wg@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2011/rdf-wg/" - } - }, - { - "name": { "type": "literal", "value": "RDFa Working Group" }, - "mailbox": { - "type": "uri", - "value": "mailto:public-rdfa@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2010/02/rdfa/" - } - }, - { - "name": { - "type": "literal", - "value": "Research and Development Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-wai-rd@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/WAI/RD/" - } - }, - { - "name": { "type": "literal", "value": "SVG Working Group" }, - "mailbox": { - "type": "uri", - "value": "mailto:public-svg-wg@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/Graphics/SVG/WG/" - } - }, - { - "name": { - "type": "literal", - "value": "System Applications Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-sysapps@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2012/sysapps/" - } - }, - { - "name": { - "type": "literal", - "value": "Technical Architecture Group" - }, - "mailbox": { "type": "uri", "value": "mailto:www-tag@w3.org" }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2001/tag/" - } - }, - { - "name": { - "type": "literal", - "value": "Timed Text Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-tt@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/AudioVideo/TT/" - } - }, - { - "name": { - "type": "literal", - "value": "Tracking Protection Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-tracking@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2011/tracking-protection/" - } - }, - { - "name": { - "type": "literal", - "value": "User Agent Accessibility Guidelines Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:w3c-wai-ua@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/WAI/UA/" - } - }, - { - "name": { - "type": "literal", - "value": "Web Application Security Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-webappsec@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2011/webappsec/" - } - }, - { - "name": { - "type": "literal", - "value": "Web Applications Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-webapps@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2008/webapps/" - } - }, - { - "name": { - "type": "literal", - "value": "Web Content Accessibility Guidelines Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:w3c-wai-gl@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/WAI/GL/" - } - }, - { - "name": { - "type": "literal", - "value": "Web Cryptography Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-webcrypto@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2012/webcrypto/" - } - }, - { - "name": { - "type": "literal", - "value": "Web Notification Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-web-notification@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2010/web-notifications/" - } - }, - { - "name": { - "type": "literal", - "value": "Web Performance Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-web-perf@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2010/webperf/" - } - }, - { - "name": { - "type": "literal", - "value": "Web Real-Time Communications Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-webrtc@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2011/04/webrtc/" - } - }, - { - "name": { - "type": "literal", - "value": "WebFonts Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-webfonts-wg@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/Fonts/WG/" - } - }, - { - "name": { - "type": "literal", - "value": "XML Core Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-xml-core-wg@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/XML/Core/" - } - }, - { - "name": { - "type": "literal", - "value": "XML Processing Model Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-xml-processing-model-wg@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/XML/Processing/" - } - }, - { - "name": { - "type": "literal", - "value": "XML Security Working Group" - }, - "mailbox": { - "type": "uri", - "value": "mailto:public-xmlsec@w3.org" - }, - "homepage": { - "type": "uri", - "value": "https://www.w3.org/2008/xmlsec/" - } - } - ] - } -} diff --git a/tools/make-groups-db.js b/tools/make-groups-db.js deleted file mode 100644 index 26fbcd82b..000000000 --- a/tools/make-groups-db.js +++ /dev/null @@ -1,40 +0,0 @@ -// XXX -// This script is obsolete. -// I keep it here in case the RDF source becomes reliable and turns out to be better than the -// alternative we have. But for now, don't use this. - -// This script is used to update the database of groups used in recognising that a draft is -// really published by the right group as per the information provided. -// -// The database is not automatically up to date with the information available on the W3C -// web site since the latter is in RDF and in any case we don't wish to hit it up for every check. -// The process to update the database we have here is simple: -// -// 1. Visit http://kwz.me/8p. Copy the JSON into groups-sparql.json -// 2. Run this script - -'use strict'; - -import fs from 'fs'; -import pth, { dirname } from 'path'; -import { fileURLToPath } from 'url'; -import { importJSON } from '../lib/util.js'; - -const src = importJSON('./groups-sparql.json'); - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -const res = {}; -for (let i = 0, n = src.results.bindings.length; i < n; i += 1) { - const group = src.results.bindings[i]; - const key = group.mailbox.value.replace('mailto:', ''); - res[key] = { - url: group.homepage.value, - name: group.name.value, - }; -} - -fs.writeFileSync( - pth.join(__dirname, '../lib/groups-db.json'), - JSON.stringify(res, null, 4) -); diff --git a/tsconfig.json b/tsconfig.json index 4ad157798..f5abb0453 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,18 @@ { "compilerOptions": { - "allowJs": true, - "checkJs": true, + "declaration": true, "module": "nodenext", "moduleResolution": "nodenext", - "noEmit": true + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true, + "useUnknownInCatchVariables": false, + "verbatimModuleSyntax": true }, - "exclude": ["public/**/*", "test/**/*"] + "exclude": ["public/**/*"], + "types": ["node"] }