From f160b63af006b84209812ffd9622d01bd2adfabf Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Fri, 30 Jul 2021 17:27:41 +0200 Subject: [PATCH] Add JSDoc based types --- .gitignore | 8 +- package.json | 18 ++- packages/rehype-cli/cli.js | 1 + packages/rehype-cli/package.json | 12 +- packages/rehype-cli/tsconfig.json | 4 + packages/rehype-parse/index.d.ts | 7 + packages/rehype-parse/index.js | 93 +------------ packages/rehype-parse/{ => lib}/errors.js | 0 packages/rehype-parse/lib/index.js | 126 ++++++++++++++++++ packages/rehype-parse/package.json | 21 ++- packages/rehype-parse/readme.md | 2 +- packages/rehype-parse/tsconfig.json | 4 + packages/rehype-parse/types/index.d.ts | 47 ------- .../rehype-parse/types/rehype-parse-tests.ts | 15 --- packages/rehype-parse/types/tsconfig.json | 10 -- packages/rehype-parse/types/tslint.json | 8 -- packages/rehype-stringify/index.d.ts | 9 ++ packages/rehype-stringify/index.js | 12 +- packages/rehype-stringify/lib/index.js | 22 +++ packages/rehype-stringify/package.json | 20 ++- packages/rehype-stringify/tsconfig.json | 4 + packages/rehype-stringify/types/index.d.ts | 28 ---- .../types/rehype-stringify-tests.ts | 10 -- packages/rehype-stringify/types/tsconfig.json | 10 -- packages/rehype-stringify/types/tslint.json | 7 - packages/rehype/index.d.ts | 6 + packages/rehype/package.json | 17 ++- packages/rehype/tsconfig.json | 4 + packages/rehype/types/index.d.ts | 13 -- packages/rehype/types/rehype-tests.ts | 12 -- packages/rehype/types/tsconfig.json | 10 -- packages/rehype/types/tslint.json | 15 --- script/parse-error.js | 99 ++++++++------ script/regenerate-fixtures.js | 23 ++-- test/api.js | 98 +++++++++++--- test/parse-error.js | 4 +- tsconfig.json | 17 +++ 37 files changed, 444 insertions(+), 372 deletions(-) create mode 100644 packages/rehype-cli/tsconfig.json create mode 100644 packages/rehype-parse/index.d.ts rename packages/rehype-parse/{ => lib}/errors.js (100%) create mode 100644 packages/rehype-parse/lib/index.js create mode 100644 packages/rehype-parse/tsconfig.json delete mode 100644 packages/rehype-parse/types/index.d.ts delete mode 100644 packages/rehype-parse/types/rehype-parse-tests.ts delete mode 100644 packages/rehype-parse/types/tsconfig.json delete mode 100644 packages/rehype-parse/types/tslint.json create mode 100644 packages/rehype-stringify/index.d.ts create mode 100644 packages/rehype-stringify/lib/index.js create mode 100644 packages/rehype-stringify/tsconfig.json delete mode 100644 packages/rehype-stringify/types/index.d.ts delete mode 100644 packages/rehype-stringify/types/rehype-stringify-tests.ts delete mode 100644 packages/rehype-stringify/types/tsconfig.json delete mode 100644 packages/rehype-stringify/types/tslint.json create mode 100644 packages/rehype/index.d.ts create mode 100644 packages/rehype/tsconfig.json delete mode 100644 packages/rehype/types/index.d.ts delete mode 100644 packages/rehype/types/rehype-tests.ts delete mode 100644 packages/rehype/types/tsconfig.json delete mode 100644 packages/rehype/types/tslint.json create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 4fb0e322..0ee0c54f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,11 @@ -node_modules/ coverage/ +node_modules/ +packages/rehype/*.d.ts +packages/rehype-cli/*.d.ts +packages/rehype-parse/lib/*.d.ts +packages/rehype-stringify/lib/*.d.ts +script/*.d.ts +test/*.d.ts .DS_Store *.log yarn.lock diff --git a/package.json b/package.json index b0e29c5b..ba3b0ced 100644 --- a/package.json +++ b/package.json @@ -10,17 +10,21 @@ }, "type": "module", "devDependencies": { + "@types/tape": "^4.0.0", "bail": "^2.0.0", "c8": "^7.0.0", - "dtslint": "^4.0.0", "hast-util-assert": "^3.0.0", "lerna": "^4.0.0", "mdast-zone": "^5.0.0", "prettier": "^2.0.0", "remark-cli": "^9.0.0", "remark-preset-wooorm": "^8.0.0", + "rimraf": "^3.0.0", "tape": "^5.0.0", "to-vfile": "^7.0.0", + "type-coverage": "^2.0.0", + "type-fest": "^0.21.3", + "typescript": "^4.0.0", "unified": "^10.0.0", "unist-builder": "^3.0.0", "unist-util-remove-position": "^4.0.0", @@ -28,11 +32,13 @@ }, "scripts": { "postinstall": "lerna bootstrap --no-ci", + "build-workspace": "lerna run build", + "build-monorepo": "rimraf \"*.d.ts\" \"{test,script}/**/*.d.ts\" && tsc && type-coverage", + "build": "npm run build-workspace && npm run build-monorepo", "format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", "test-api": "node --conditions development test/index.js", "test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov node --conditions development test/index.js", - "test-types": "dtslint packages/rehype-parse/types && dtslint packages/rehype-stringify/types && dtslint packages/rehype/types", - "test": "npm run format && npm run test-coverage && npm run test-types" + "test": "npm run build && npm run format && npm run test-coverage" }, "prettier": { "tabWidth": 2, @@ -53,5 +59,11 @@ "preset-wooorm", "./script/parse-error.js" ] + }, + "typeCoverage": { + "atLeast": 100, + "detail": true, + "strict": true, + "ignoreCatch": true } } diff --git a/packages/rehype-cli/cli.js b/packages/rehype-cli/cli.js index 396619bc..51510efc 100755 --- a/packages/rehype-cli/cli.js +++ b/packages/rehype-cli/cli.js @@ -8,6 +8,7 @@ const proc = require('rehype/package.json') const cli = require('./package.json') start({ + // @ts-expect-error: fine. processor: rehype, name: proc.name, description: cli.description, diff --git a/packages/rehype-cli/package.json b/packages/rehype-cli/package.json index f7a31608..cd41097a 100644 --- a/packages/rehype-cli/package.json +++ b/packages/rehype-cli/package.json @@ -32,6 +32,14 @@ "rehype": "^11.0.0", "unified-args": "^8.0.0" }, - "scripts": {}, - "xo": false + "scripts": { + "build": "rimraf \"*.d.ts\" && tsc && type-coverage" + }, + "xo": false, + "typeCoverage": { + "atLeast": 100, + "detail": true, + "strict": true, + "ignoreCatch": true + } } diff --git a/packages/rehype-cli/tsconfig.json b/packages/rehype-cli/tsconfig.json new file mode 100644 index 00000000..7e61871a --- /dev/null +++ b/packages/rehype-cli/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["*.js"] +} diff --git a/packages/rehype-parse/index.d.ts b/packages/rehype-parse/index.d.ts new file mode 100644 index 00000000..c2ead0b1 --- /dev/null +++ b/packages/rehype-parse/index.d.ts @@ -0,0 +1,7 @@ +// This wrapper exists because JS in TS can’t export a `@type` of a function. +import type {Options, ErrorCode, ErrorSeverity} from './lib/index.js' +import type {Root} from 'hast' +import type {Plugin} from 'unified' +const rehypeParse: Plugin<[Options] | [], Root, string> +export default rehypeParse +export type {Options, ErrorCode, ErrorSeverity} diff --git a/packages/rehype-parse/index.js b/packages/rehype-parse/index.js index 4964418d..85664375 100644 --- a/packages/rehype-parse/index.js +++ b/packages/rehype-parse/index.js @@ -1,91 +1,2 @@ -import Parser5 from 'parse5/lib/parser/index.js' -import {fromParse5} from 'hast-util-from-parse5' -import {errors} from './errors.js' - -const base = 'https://html.spec.whatwg.org/multipage/parsing.html#parse-error-' - -const fatalities = {2: true, 1: false, 0: null} - -export default function rehypeParse(options) { - const settings = Object.assign({}, options, this.data('settings')) - let position = settings.position - - position = typeof position === 'boolean' ? position : true - - this.Parser = parser - - function parser(doc, file) { - const fn = settings.fragment ? 'parseFragment' : 'parse' - const onParseError = settings.emitParseErrors ? onerror : null - const parse5 = new Parser5({ - sourceCodeLocationInfo: position, - onParseError, - scriptingEnabled: false - }) - - return fromParse5(parse5[fn](doc), { - space: settings.space, - file, - verbose: settings.verbose - }) - - function onerror(error) { - const code = error.code - const name = camelcase(code) - const setting = settings[name] - const config = setting === undefined || setting === null ? true : setting - const level = typeof config === 'number' ? config : config ? 1 : 0 - const start = { - line: error.startLine, - column: error.startCol, - offset: error.startOffset - } - const end = { - line: error.endLine, - column: error.endCol, - offset: error.endOffset - } - let info - let message - - if (level) { - /* c8 ignore next */ - info = errors[name] || {reason: '', description: ''} - - message = file.message(format(info.reason), {start, end}) - message.source = 'parse-error' - message.ruleId = code - message.fatal = fatalities[level] - message.note = format(info.description) - message.url = info.url === false ? null : base + code - } - - function format(value) { - return value.replace(/%c(?:-(\d+))?/g, char).replace(/%x/g, encodedChar) - } - - function char($0, $1) { - const offset = $1 ? -Number.parseInt($1, 10) : 0 - const char = doc.charAt(error.startOffset + offset) - return char === '`' ? '` ` `' : char - } - - function encodedChar() { - const char = doc - .charCodeAt(error.startOffset) - .toString(16) - .toUpperCase() - - return '0x' + char - } - } - } -} - -function camelcase(value) { - return value.replace(/-[a-z]/g, replacer) -} - -function replacer($0) { - return $0.charAt(1).toUpperCase() -} +import rehypeParse from './lib/index.js' +export default rehypeParse diff --git a/packages/rehype-parse/errors.js b/packages/rehype-parse/lib/errors.js similarity index 100% rename from packages/rehype-parse/errors.js rename to packages/rehype-parse/lib/errors.js diff --git a/packages/rehype-parse/lib/index.js b/packages/rehype-parse/lib/index.js new file mode 100644 index 00000000..f8aca3a1 --- /dev/null +++ b/packages/rehype-parse/lib/index.js @@ -0,0 +1,126 @@ +/** + * @typedef {import('hast').Root} Root + * @typedef {Pick} FromParse5Options + * + * @typedef {keyof errors} ErrorCode + * @typedef {0|1|2|boolean|null|undefined} ErrorSeverity + * @typedef {Partial>} ErrorFields + * + * @typedef ParseFields + * @property {boolean|undefined} [position=true] + * @property {boolean|undefined} [fragment=false] + * Specify whether to parse a fragment, instead of a complete document. + * In document mode, unopened `html`, `head`, and `body` elements are opened + * in just the right places. + * @property {boolean|undefined} [emitParseErrors=false] + * > ⚠️ Parse errors are currently being added to HTML. + * > Not all errors emitted by parse5 (or rehype-parse) are specced yet. + * > Some documentation may still be missing. + * + * Emit parse errors while parsing on the vfile. + * Setting this to `true` starts emitting HTML parse errors. + * + * Specific rules can be turned off by setting them to `false` (or `0`). + * The default, when `emitParseErrors: true`, is `true` (or `1`), and means + * that rules emit as warnings. + * Rules can also be configured with `2`, to turn them into fatal errors. + * + * @typedef {FromParse5Options & ParseFields & ErrorFields} Options + */ + +// @ts-expect-error: remove when typed +import Parser5 from 'parse5/lib/parser/index.js' +import {fromParse5} from 'hast-util-from-parse5' +import {errors} from './errors.js' + +const base = 'https://html.spec.whatwg.org/multipage/parsing.html#parse-error-' + +const fatalities = {2: true, 1: false, 0: null} + +/** @type {import('unified').Plugin<[Options?] | void[], string, Root>} */ +export default function rehypeParse(options) { + const processorSettings = /** @type {Options} */ (this.data('settings')) + const settings = Object.assign({}, options, processorSettings) + const position = + typeof settings.position === 'boolean' ? settings.position : true + + Object.assign(this, {Parser: parser}) + + /** @type {import('unified').ParserFunction} */ + function parser(doc, file) { + const fn = settings.fragment ? 'parseFragment' : 'parse' + const onParseError = settings.emitParseErrors ? onerror : null + const parse5 = new Parser5({ + sourceCodeLocationInfo: position, + onParseError, + scriptingEnabled: false + }) + + // @ts-expect-error: `parse5` returns document or fragment, which are always + // mapped to roots. + return fromParse5(parse5[fn](doc), { + space: settings.space, + file, + verbose: settings.verbose + }) + + /** + * @param {{code: string, startLine: number, startCol: number, startOffset: number, endLine: number, endCol: number, endOffset: number}} error + */ + function onerror(error) { + const code = error.code + const name = camelcase(code) + const setting = settings[name] + const config = setting === undefined || setting === null ? true : setting + const level = typeof config === 'number' ? config : config ? 1 : 0 + const start = { + line: error.startLine, + column: error.startCol, + offset: error.startOffset + } + const end = { + line: error.endLine, + column: error.endCol, + offset: error.endOffset + } + if (level) { + /* c8 ignore next */ + const info = errors[name] || {reason: '', description: '', url: ''} + const message = file.message(format(info.reason), {start, end}) + message.source = 'parse-error' + message.ruleId = code + message.fatal = fatalities[level] + message.note = format(info.description) + message.url = 'url' in info && info.url === false ? null : base + code + } + + /** + * @param {string} value + * @returns {string} + */ + function format(value) { + return value + .replace(/%c(?:-(\d+))?/g, (_, /** @type {string} */ $1) => { + const offset = $1 ? -Number.parseInt($1, 10) : 0 + const char = doc.charAt(error.startOffset + offset) + return char === '`' ? '` ` `' : char + }) + .replace( + /%x/g, + () => + '0x' + + doc.charCodeAt(error.startOffset).toString(16).toUpperCase() + ) + } + } + } +} + +/** + * @param {string} value + * @returns {ErrorCode} + */ +function camelcase(value) { + // @ts-expect-error: this returns a valid error code. + return value.replace(/-[a-z]/g, ($0) => $0.charAt(1).toUpperCase()) +} diff --git a/packages/rehype-parse/package.json b/packages/rehype-parse/package.json index 05184d6e..3d3646a1 100644 --- a/packages/rehype-parse/package.json +++ b/packages/rehype-parse/package.json @@ -29,15 +29,26 @@ "sideEffects": false, "type": "module", "main": "index.js", - "types": "types/index.d.ts", + "types": "index.d.ts", "files": [ - "types/index.d.ts", - "errors.js", + "lib/", + "index.d.ts", "index.js" ], "dependencies": { + "@types/hast": "^2.0.0", "hast-util-from-parse5": "^7.0.0", - "parse5": "^6.0.0" + "parse5": "^6.0.0", + "unified": "^10.0.0" }, - "xo": false + "scripts": { + "build": "rimraf \"lib/**/*.d.ts\" && tsc && type-coverage" + }, + "xo": false, + "typeCoverage": { + "atLeast": 100, + "detail": true, + "strict": true, + "ignoreCatch": true + } } diff --git a/packages/rehype-parse/readme.md b/packages/rehype-parse/readme.md index 4c145533..0580a885 100644 --- a/packages/rehype-parse/readme.md +++ b/packages/rehype-parse/readme.md @@ -169,7 +169,7 @@ back when [**exiting**][exit]. Emit parse errors while parsing on the [vfile][] (`boolean`, default: `false`). -Setting this to true starts emitting [HTML parse errors][parse-errors]. +Setting this to `true` starts emitting [HTML parse errors][parse-errors]. Specific rules can be turned off by setting them to `false` (or `0`). The default, when `emitParseErrors: true`, is `true` (or `1`), and means that diff --git a/packages/rehype-parse/tsconfig.json b/packages/rehype-parse/tsconfig.json new file mode 100644 index 00000000..4f4b4b06 --- /dev/null +++ b/packages/rehype-parse/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["lib/**/*.js"] +} diff --git a/packages/rehype-parse/types/index.d.ts b/packages/rehype-parse/types/index.d.ts deleted file mode 100644 index 41781e6d..00000000 --- a/packages/rehype-parse/types/index.d.ts +++ /dev/null @@ -1,47 +0,0 @@ -// TypeScript Version: 3.5 -import {Parser, Plugin} from 'unified' -import {Node, Parent} from 'unist' -import {HastUtilFromParse5Options} from 'hast-util-from-parse5' - -declare namespace rehypeParse { - interface Parse extends Plugin<[RehypeParseOptions?]> { - Parser: Parser - } - - interface RehypeParseOptions - extends Pick { - /** - * Specify whether to parse a fragment, instead of a complete document. - * In document mode, unopened `html`, `head`, and `body` elements are opened in - * just the right places. - * - * @default false - */ - fragment?: boolean - - /** - * > ⚠️ Parse errors are currently being added to HTML. - * > Not all errors emitted by parse5 (or rehype-parse) are specced yet. - * > Some documentation may still be missing. - * - * Emit parse errors while parsing on the [vfile](https://github.com/vfile/vfile) - * - * Setting this to true starts emitting - * [HTML parse errors](https://html.spec.whatwg.org/multipage/parsing.html#parse-errors). - * - * Specific rules can be turned off by setting them to `false` (or `0`). - * The default, when `emitParseErrors: true`, is `true` (or `1`), and means that - * rules emit as warnings. - * Rules can also be configured with `2`, to turn them into fatal errors. - * - * @default false - */ - emitParseErrors?: boolean - } - - type Visitor = (node: Node, parent?: Parent) => string -} - -declare const rehypeParse: rehypeParse.Parse - -export = rehypeParse diff --git a/packages/rehype-parse/types/rehype-parse-tests.ts b/packages/rehype-parse/types/rehype-parse-tests.ts deleted file mode 100644 index 5228f96b..00000000 --- a/packages/rehype-parse/types/rehype-parse-tests.ts +++ /dev/null @@ -1,15 +0,0 @@ -import unified = require('unified') -import parse = require('rehype-parse') - -unified().use(parse) - -unified().use(parse, {}) - -unified().use(parse, {fragment: true}) - -unified().use(parse, {space: 'html'}) -unified().use(parse, {space: 'svg'}) - -unified().use(parse, {emitParseErrors: true}) - -unified().use(parse, {verbose: true}) diff --git a/packages/rehype-parse/types/tsconfig.json b/packages/rehype-parse/types/tsconfig.json deleted file mode 100644 index 43e5a153..00000000 --- a/packages/rehype-parse/types/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "lib": ["es2015"], - "strict": true, - "baseUrl": ".", - "paths": { - "rehype-parse": ["index.d.ts"] - } - } -} diff --git a/packages/rehype-parse/types/tslint.json b/packages/rehype-parse/types/tslint.json deleted file mode 100644 index 759caa05..00000000 --- a/packages/rehype-parse/types/tslint.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "dtslint/dtslint.json", - "rules": { - "no-redundant-jsdoc": false, - "semicolon": false, - "whitespace": false - } -} diff --git a/packages/rehype-stringify/index.d.ts b/packages/rehype-stringify/index.d.ts new file mode 100644 index 00000000..83644def --- /dev/null +++ b/packages/rehype-stringify/index.d.ts @@ -0,0 +1,9 @@ +// This wrapper exists because JS in TS can’t export a `@type` of a function. +import type {Options} from './lib/index.js' +import type {Root} from 'hast' +import type {Plugin} from 'unified' +// Note: defining all nodes here, such as with `Root | Element | ...` seems +// to trip TS up. +const rehypeStringify: Plugin<[Options] | [], Root, string> +export default rehypeStringify +export type {Options} diff --git a/packages/rehype-stringify/index.js b/packages/rehype-stringify/index.js index 0a01bba2..90c70976 100644 --- a/packages/rehype-stringify/index.js +++ b/packages/rehype-stringify/index.js @@ -1,11 +1,3 @@ -import {toHtml} from 'hast-util-to-html' +import rehypeStringify from './lib/index.js' -export default function rehypeStringify(config) { - const settings = Object.assign({}, config, this.data('settings')) - - this.Compiler = compiler - - function compiler(tree) { - return toHtml(tree, settings) - } -} +export default rehypeStringify diff --git a/packages/rehype-stringify/lib/index.js b/packages/rehype-stringify/lib/index.js new file mode 100644 index 00000000..a40c38e5 --- /dev/null +++ b/packages/rehype-stringify/lib/index.js @@ -0,0 +1,22 @@ +/** + * @typedef {import('hast').Root} Root + * @typedef {Root|Root['children'][number]} Node + * @typedef {import('hast-util-to-html').Options} Options + */ + +import {toHtml} from 'hast-util-to-html' + +/** @type {import('unified').Plugin<[Options]|void[], Node, string>} */ +export default function rehypeStringify(config) { + const processorSettings = /** @type {Options} */ (this.data('settings')) + const settings = Object.assign({}, config, processorSettings) + + Object.assign(this, {Compiler: compiler}) + + /** + * @type {import('unified').CompilerFunction} + */ + function compiler(tree) { + return toHtml(tree, settings) + } +} diff --git a/packages/rehype-stringify/package.json b/packages/rehype-stringify/package.json index 2ddb78d1..bd44c697 100644 --- a/packages/rehype-stringify/package.json +++ b/packages/rehype-stringify/package.json @@ -31,13 +31,25 @@ "sideEffects": false, "type": "module", "main": "index.js", - "types": "types/index.d.ts", + "types": "index.d.ts", "files": [ - "types/index.d.ts", + "lib/", + "index.d.ts", "index.js" ], "dependencies": { - "hast-util-to-html": "^8.0.0" + "@types/hast": "^2.0.0", + "hast-util-to-html": "^8.0.0", + "unified": "^10.0.0" }, - "xo": false + "scripts": { + "build": "rimraf \"lib/**/*.d.ts\" && tsc && type-coverage" + }, + "xo": false, + "typeCoverage": { + "atLeast": 100, + "detail": true, + "strict": true, + "ignoreCatch": true + } } diff --git a/packages/rehype-stringify/tsconfig.json b/packages/rehype-stringify/tsconfig.json new file mode 100644 index 00000000..4f4b4b06 --- /dev/null +++ b/packages/rehype-stringify/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["lib/**/*.js"] +} diff --git a/packages/rehype-stringify/types/index.d.ts b/packages/rehype-stringify/types/index.d.ts deleted file mode 100644 index 650027f1..00000000 --- a/packages/rehype-stringify/types/index.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -// TypeScript Version: 3.5 -import {Compiler, Processor, Plugin} from 'unified' -import {Node, Parent} from 'unist' -import {HastUtilToHtmlOptions} from 'hast-util-to-html' - -declare class RehypeCompiler implements Compiler { - compile(): string - visitors: { - [key: string]: rehypeStringify.Visitor - } -} - -declare namespace rehypeStringify { - interface Stringify extends Plugin<[HastUtilToHtmlOptions?]> { - Compiler: typeof RehypeCompiler - (this: Processor, options?: HastUtilToHtmlOptions): void - } - - type RehypeStringifyOptions = HastUtilToHtmlOptions - - type Compiler = RehypeCompiler - - type Visitor = (node: Node, parent?: Parent) => string -} - -declare const rehypeStringify: rehypeStringify.Stringify - -export = rehypeStringify diff --git a/packages/rehype-stringify/types/rehype-stringify-tests.ts b/packages/rehype-stringify/types/rehype-stringify-tests.ts deleted file mode 100644 index aeac757e..00000000 --- a/packages/rehype-stringify/types/rehype-stringify-tests.ts +++ /dev/null @@ -1,10 +0,0 @@ -import unified = require('unified') -import stringify = require('rehype-stringify') - -unified().use(stringify) - -unified().use(stringify, {}) - -unified().use(stringify, { - upperDoctype: true -}) diff --git a/packages/rehype-stringify/types/tsconfig.json b/packages/rehype-stringify/types/tsconfig.json deleted file mode 100644 index 4a7e1974..00000000 --- a/packages/rehype-stringify/types/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "lib": ["es2015"], - "strict": true, - "baseUrl": ".", - "paths": { - "rehype-stringify": ["index.d.ts"] - } - } -} diff --git a/packages/rehype-stringify/types/tslint.json b/packages/rehype-stringify/types/tslint.json deleted file mode 100644 index 70c4494b..00000000 --- a/packages/rehype-stringify/types/tslint.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "dtslint/dtslint.json", - "rules": { - "semicolon": false, - "whitespace": false - } -} diff --git a/packages/rehype/index.d.ts b/packages/rehype/index.d.ts new file mode 100644 index 00000000..ca338de6 --- /dev/null +++ b/packages/rehype/index.d.ts @@ -0,0 +1,6 @@ +export const rehype: import('unified').FrozenProcessor< + import('hast').Root, + import('hast').Root, + import('hast').Root, + string +> diff --git a/packages/rehype/package.json b/packages/rehype/package.json index 67f03d69..d97423ba 100644 --- a/packages/rehype/package.json +++ b/packages/rehype/package.json @@ -17,6 +17,7 @@ "process" ], "dependencies": { + "@types/hast": "^2.0.0", "rehype-parse": "^7.0.0", "rehype-stringify": "^8.0.0", "unified": "^10.0.0" @@ -35,11 +36,19 @@ "sideEffects": false, "type": "module", "main": "index.js", - "types": "types/index.d.ts", + "types": "index.d.ts", "files": [ - "types/index.d.ts", + "index.d.ts", "index.js" ], - "scripts": {}, - "xo": false + "scripts": { + "build": "rimraf \"*.d.ts\" && tsc && type-coverage" + }, + "xo": false, + "typeCoverage": { + "atLeast": 100, + "detail": true, + "strict": true, + "ignoreCatch": true + } } diff --git a/packages/rehype/tsconfig.json b/packages/rehype/tsconfig.json new file mode 100644 index 00000000..7e61871a --- /dev/null +++ b/packages/rehype/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.json", + "include": ["*.js"] +} diff --git a/packages/rehype/types/index.d.ts b/packages/rehype/types/index.d.ts deleted file mode 100644 index 092c93b4..00000000 --- a/packages/rehype/types/index.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// TypeScript Version: 3.5 - -import {Processor} from 'unified' -import {RehypeParseOptions} from 'rehype-parse' -import {RehypeStringifyOptions} from 'rehype-stringify' - -declare namespace rehype { - type RehypeOptions = RehypeStringifyOptions & RehypeParseOptions -} - -declare function rehype(): Processor - -export = rehype diff --git a/packages/rehype/types/rehype-tests.ts b/packages/rehype/types/rehype-tests.ts deleted file mode 100644 index 99e2b9e9..00000000 --- a/packages/rehype/types/rehype-tests.ts +++ /dev/null @@ -1,12 +0,0 @@ -import rehype = require('rehype') - -interface PluginOptions { - example: boolean -} - -const plugin = (options?: PluginOptions) => {} - -rehype().use(plugin) -rehype().use(plugin, {example: true}) -rehype().use({settings: {fragment: true}}) -rehype().use({settings: {upperDoctype: true}}) diff --git a/packages/rehype/types/tsconfig.json b/packages/rehype/types/tsconfig.json deleted file mode 100644 index 79d561a6..00000000 --- a/packages/rehype/types/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "lib": ["es2015"], - "strict": true, - "baseUrl": ".", - "paths": { - "rehype": ["index.d.ts"] - } - } -} diff --git a/packages/rehype/types/tslint.json b/packages/rehype/types/tslint.json deleted file mode 100644 index c72b387b..00000000 --- a/packages/rehype/types/tslint.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "extends": "dtslint/dtslint.json", - "rules": { - "max-line-length": false, - "no-redundant-jsdoc": false, - "no-void-expression": false, - "only-arrow-functions": false, - "semicolon": false, - "unified-signatures": false, - "whitespace": false, - "interface-over-type-literal": false, - "no-unnecessary-generics": false, - "strict-export-declare-modifiers": false - } -} diff --git a/script/parse-error.js b/script/parse-error.js index 0cc3727e..71fcc968 100644 --- a/script/parse-error.js +++ b/script/parse-error.js @@ -1,11 +1,20 @@ +/** + * @typedef {import('mdast').Root} Root + * @typedef {import('mdast').ListItem} ListItem + * @typedef {import('mdast').PhrasingContent} PhrasingContent + */ + import fs from 'fs' import {u} from 'unist-builder' import {unified} from 'unified' import parse from 'remark-parse' import {zone} from 'mdast-zone' -import {errors} from '../packages/rehype-parse/errors.js' +import {errors} from '../packages/rehype-parse/lib/errors.js' + +const own = {}.hasOwnProperty -const pkg = JSON.parse(fs.readFileSync('package.json')) +/** @type {import('type-fest').PackageJson} */ +const pkg = JSON.parse(String(fs.readFileSync('package.json'))) const repo = pkg.repository @@ -13,50 +22,62 @@ const whatwg = 'https://html.spec.whatwg.org/multipage/parsing.html#parse-error-' const base = 'https://github.com/' + repo + '/blob/main' +/** @type {Partial>} */ const ignoreFixture = { surrogateInInputStream: true } +/** @type {import('unified').Plugin} */ export default function remarkParseErrors() { - return transform -} - -function transform(tree) { - zone(tree, 'parse-error', visit) -} + return (tree) => { + zone(tree, 'parse-error', (start, _, end) => { + /** @type {ListItem[]} */ + const list = [] + /** @type {keyof errors} */ + let key -function visit(start, _, end) { - return [ - start, - u( - 'list', - {ordered: false, spread: false}, - Object.keys(errors).map((name) => { - const info = errors[name] - const kebab = name.replace(/[A-Z]/g, ($0) => '-' + $0.toLowerCase()) - const reason = - info.reason.charAt(0).toLowerCase() + info.reason.slice(1) - const head = u('inlineCode', name) - const fields = [ - info.url === false ? head : u('link', {url: whatwg + kebab}, [head]), - u('text', ' — ') - ].concat(unified().use(parse).parse(reason).children) - - if (!ignoreFixture[name]) { - fields.push( - u('text', ' ('), - u( - 'link', - {url: base + '/test/parse-error/' + kebab + '/index.html'}, - [u('text', 'example')] - ), - u('text', ')') + for (key in errors) { + if (own.call(errors, key)) { + const info = errors[key] + const kebab = key.replace(/[A-Z]/g, ($0) => '-' + $0.toLowerCase()) + const reason = + info.reason.charAt(0).toLowerCase() + info.reason.slice(1) + const descriptionRoot = /** @type {Root} */ ( + unified().use(parse).parse(reason) ) + const headParagraph = descriptionRoot.children[0] + + if (!headParagraph || headParagraph.type !== 'paragraph') { + throw new Error('Expected paragraph') + } + + const head = u('inlineCode', key) + /** @type {PhrasingContent[]} */ + const fields = [ + 'url' in info && info.url === false + ? head + : u('link', {url: whatwg + kebab}, [head]), + u('text', ' — '), + ...headParagraph.children + ] + + if (!ignoreFixture[key]) { + fields.push( + u('text', ' ('), + u( + 'link', + {url: base + '/test/parse-error/' + kebab + '/index.html'}, + [u('text', 'example')] + ), + u('text', ')') + ) + } + + list.push(u('listItem', [u('paragraph', fields)])) } + } - return u('listItem', [u('paragraph', fields)]) - }) - ), - end - ] + return [start, u('list', {ordered: false, spread: false}, list), end] + }) + } } diff --git a/script/regenerate-fixtures.js b/script/regenerate-fixtures.js index 2bca8da6..974b43e9 100644 --- a/script/regenerate-fixtures.js +++ b/script/regenerate-fixtures.js @@ -3,17 +3,16 @@ import path from 'path' import {bail} from 'bail' import {rehype} from '../packages/rehype/index.js' -const join = path.join +const root = path.join(process.cwd(), 'test', 'fixtures') -const root = join(process.cwd(), 'test', 'fixtures') - -fs.readdir(join(root), (error, files) => { +fs.readdir(path.join(root), (error, files) => { let index = -1 bail(error) while (++index < files.length) { - const base = join(root, files[index]) + const base = path.join(root, files[index]) + /** @type {Record|undefined} */ let config if (files[index].charAt(0) === '.') { @@ -21,12 +20,14 @@ fs.readdir(join(root), (error, files) => { } try { - config = JSON.parse(fs.readFileSync(join(base, 'config.json'))) + config = JSON.parse( + String(fs.readFileSync(path.join(base, 'config.json'))) + ) } catch { config = {} } - fs.readFile(join(base, 'index.html'), 'utf8', (error, doc) => { + fs.readFile(path.join(base, 'index.html'), 'utf8', (error, doc) => { const processor = rehype().use({settings: config}) const tree = processor.parse(doc) const result = processor.stringify(tree) @@ -34,15 +35,15 @@ fs.readdir(join(root), (error, files) => { bail(error) fs.writeFile( - join(base, 'index.json'), - JSON.stringify(tree, 0, 2) + '\n', + path.join(base, 'index.json'), + JSON.stringify(tree, null, 2) + '\n', bail ) if (result === doc) { - fs.unlink(join(base, 'result.html'), Function.prototype) + fs.unlink(path.join(base, 'result.html'), bail) } else { - fs.writeFile(join(base, 'result.html'), result, bail) + fs.writeFile(path.join(base, 'result.html'), result, bail) } }) } diff --git a/test/api.js b/test/api.js index 202aa34a..ab0b74e5 100644 --- a/test/api.js +++ b/test/api.js @@ -1,3 +1,7 @@ +/** + * @typedef {import('hast').Root} Root + */ + import fs from 'fs' import path from 'path' import test from 'tape' @@ -12,15 +16,47 @@ import {rehype} from '../packages/rehype/index.js' const fragment = {fragment: true} test('rehype().parse(file)', (t) => { - t.equal( - unified().use(rehypeParse).parse('Alfred').children.length, - 1, + t.deepEqual( + unified().use(rehypeParse).parse('Alfred'), + { + type: 'root', + children: [ + { + type: 'element', + tagName: 'html', + properties: {}, + children: [ + {type: 'element', tagName: 'head', properties: {}, children: []}, + { + type: 'element', + tagName: 'body', + properties: {}, + children: [ + { + type: 'text', + value: 'Alfred', + position: { + start: {line: 1, column: 1, offset: 0}, + end: {line: 1, column: 7, offset: 6} + } + } + ] + } + ] + } + ], + data: {quirksMode: true}, + position: { + start: {line: 1, column: 1, offset: 0}, + end: {line: 1, column: 7, offset: 6} + } + }, 'should accept a `string`' ) t.deepEqual( removePosition( - unified().use(rehypeParse, fragment).parse(''), + unified().use(rehypeParse, {fragment: true}).parse(''), true ), { @@ -77,6 +113,7 @@ test('rehype().parse(file)', (t) => { test('rehype().stringify(ast, file, options?)', (t) => { t.throws( () => { + // @ts-expect-error: incorrect value. unified().use(rehypeStringify).stringify(false) }, /false/, @@ -85,6 +122,7 @@ test('rehype().stringify(ast, file, options?)', (t) => { t.throws( () => { + // @ts-expect-error: unknown node. unified().use(rehypeStringify).stringify({type: 'unicorn'}) }, /unicorn/, @@ -94,7 +132,10 @@ test('rehype().stringify(ast, file, options?)', (t) => { t.equal( unified() .use(rehypeStringify) - .stringify({type: 'text', value: 'alpha < bravo'}), + .stringify({ + type: 'root', + children: [{type: 'text', value: 'alpha < bravo'}] + }), 'alpha < bravo', 'should escape entities' ) @@ -102,7 +143,10 @@ test('rehype().stringify(ast, file, options?)', (t) => { t.equal( unified() .use(rehypeStringify, {entities: {}}) - .stringify({type: 'text', value: 'alpha < bravo'}), + .stringify({ + type: 'root', + children: [{type: 'text', value: 'alpha < bravo'}] + }), 'alpha < bravo', 'should encode entities (numbered by default)' ) @@ -110,13 +154,21 @@ test('rehype().stringify(ast, file, options?)', (t) => { t.equal( unified() .use(rehypeStringify, {entities: {useNamedReferences: true}}) - .stringify({type: 'text', value: 'alpha < bravo'}), + .stringify({ + type: 'root', + children: [{type: 'text', value: 'alpha < bravo'}] + }), 'alpha < bravo', 'should encode entities (numbered by default)' ) t.equal( - unified().use(rehypeStringify).stringify({type: 'element', tagName: 'img'}), + unified() + .use(rehypeStringify) + .stringify({ + type: 'root', + children: [{type: 'element', tagName: 'img', children: []}] + }), '', 'should not close void elements' ) @@ -124,21 +176,32 @@ test('rehype().stringify(ast, file, options?)', (t) => { t.equal( unified() .use(rehypeStringify, {closeSelfClosing: true}) - .stringify({type: 'element', tagName: 'img'}), + .stringify({ + type: 'root', + children: [{type: 'element', tagName: 'img', children: []}] + }), '', 'should close void elements if `closeSelfClosing` is given' ) t.equal( - unified().use(rehypeStringify).stringify({type: 'element', tagName: 'foo'}), + unified() + .use(rehypeStringify) + .stringify({ + type: 'root', + children: [{type: 'element', tagName: 'foo', children: []}] + }), '', 'should not close unknown elements by default' ) t.equal( unified() - .use(rehypeStringify, {voids: 'foo'}) - .stringify({type: 'element', tagName: 'foo'}), + .use(rehypeStringify, {voids: ['foo']}) + .stringify({ + type: 'root', + children: [{type: 'element', tagName: 'foo', children: []}] + }), '', 'should close void elements if configured' ) @@ -234,14 +297,19 @@ test('fixtures', (t) => { t.test(fixture, (st) => { const file = readSync(path.join(fp, 'index.html')) + /** @type {{fragment?: boolean, reprocess?: boolean}} */ let config = {} + /** @type {Root|undefined} */ let tree + /** @type {string|undefined} */ let result file.dirname = '' try { - config = JSON.parse(fs.readFileSync(path.join(fp, 'config.json'))) + config = JSON.parse( + String(fs.readFileSync(path.join(fp, 'config.json'))) + ) } catch {} try { @@ -251,11 +319,11 @@ test('fixtures', (t) => { const node = rehype().data('settings', config).parse(file) try { - tree = JSON.parse(fs.readFileSync(path.join(fp, 'index.json'))) + tree = JSON.parse(String(fs.readFileSync(path.join(fp, 'index.json')))) } catch { fs.writeFileSync( path.join(fp, 'index.json'), - JSON.stringify(node, 0, 2) + '\n' + JSON.stringify(node, null, 2) + '\n' ) return } diff --git a/test/parse-error.js b/test/parse-error.js index 2f8e269a..dd09e000 100644 --- a/test/parse-error.js +++ b/test/parse-error.js @@ -3,8 +3,9 @@ import path from 'path' import test from 'tape' import {toVFile, readSync} from 'to-vfile' import {rehype} from '../packages/rehype/index.js' +// @ts-expect-error: untyped. import p5errors from '../packages/rehype-parse/node_modules/parse5/lib/common/error-codes.js' -import {errors as rerrors} from '../packages/rehype-parse/errors.js' +import {errors as rerrors} from '../packages/rehype-parse/lib/errors.js' // Related to https://github.com/inikulin/parse5/issues/255 // and https://github.com/inikulin/parse5/pull/257. @@ -76,6 +77,7 @@ test('parse-errors', (t) => { t.test(fixture, (st) => { const file = readSync(path.join(fp, 'index.html'), 'utf8') + /** @type {Error[]} */ const messages = JSON.parse( fs.readFileSync(path.join(fp, 'messages.json'), 'utf8') ) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..35fcab3f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "include": ["*.js", "script/**/*.js", "test/**/*.js"], + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020"], + "module": "ES2020", + "moduleResolution": "node", + "resolveJsonModule": true, + "allowJs": true, + "checkJs": true, + "declaration": true, + "emitDeclarationOnly": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "strict": true + } +}