From 779dc5a538d7ada9e65e095a08f65787d2937936 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Mon, 2 Aug 2021 12:19:48 +0200 Subject: [PATCH] Add JSDoc based types --- .gitignore | 1 + index.js | 106 +++++++++++++++++++----- package.json | 20 ++++- test/index.js | 35 ++++++-- tsconfig.json | 16 ++++ types/index.d.ts | 41 --------- types/rehype-autolink-headings-tests.ts | 47 ----------- types/tsconfig.json | 10 --- types/tslint.json | 8 -- 9 files changed, 146 insertions(+), 138 deletions(-) create mode 100644 tsconfig.json delete mode 100644 types/index.d.ts delete mode 100644 types/rehype-autolink-headings-tests.ts delete mode 100644 types/tsconfig.json delete mode 100644 types/tslint.json diff --git a/.gitignore b/.gitignore index 33d4929..53a29e3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ coverage/ node_modules/ .DS_Store +*.d.ts *.log yarn.lock diff --git a/index.js b/index.js index b1bc754..2d7c9d8 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,40 @@ +/** + * @typedef {import('hast').Root} Root + * @typedef {import('hast').Parent} Parent + * @typedef {import('hast').Element} Element + * @typedef {Element['children'][number]} ElementChild + * @typedef {import('hast').Properties} Properties + * + * @typedef {'prepend'|'append'|'wrap'|'before'|'after'} Behavior + * + * @callback Build + * @param {Element} node + * @returns {ElementChild|ElementChild[]} + * + * @typedef Options + * Configuration. + * @property {Behavior} [behavior='prepend'] + * How to create links. + * @property {Behavior} [behaviour] + * Please use `behavior` instead + * @property {Properties} [properties] + * Extra properties to set on the link when injecting. + * Defaults to `{ariaHidden: true, tabIndex: -1}` when `'prepend'` or + * `'append'`. + * @property {ElementChild|ElementChild[]|Build} [content={type: 'element', tagName: 'span', properties: {className: ['icon', 'icon-link']}, children: []}] + * hast nodes to insert in the link. + * @property {ElementChild|ElementChild[]|Build} [group] + * hast node to wrap the heading and link with, if `behavior` is `'before'` or + * `'after'`. + * There is no default. + */ + import extend from 'extend' import {hasProperty} from 'hast-util-has-property' import {headingRank} from 'hast-util-heading-rank' -import {visit} from 'unist-util-visit' +import {visit, SKIP} from 'unist-util-visit' +/** @type {Element} */ const contentDefaults = { type: 'element', tagName: 'span', @@ -10,11 +42,18 @@ const contentDefaults = { children: [] } +/** + * Plugin to automatically add links to headings (h1-h6). + * + * @type {import('unified').Plugin<[Options?]|void[], Root>} + */ export default function rehypeAutolinkHeadings(options = {}) { let props = options.properties const behavior = options.behaviour || options.behavior || 'prepend' const content = options.content || contentDefaults const group = options.group + + /** @type {import('unist-util-visit').Visitor} */ let method if (behavior === 'wrap') { @@ -22,73 +61,98 @@ export default function rehypeAutolinkHeadings(options = {}) { } else if (behavior === 'before' || behavior === 'after') { method = around } else { - method = inject - if (!props) { props = {ariaHidden: 'true', tabIndex: -1} } - } - - return transformer - function transformer(tree) { - visit(tree, 'element', visitor) + method = inject } - function visitor(node, index, parent) { - if (headingRank(node) && hasProperty(node, 'id')) { - return method(node, index, parent) - } + return (tree) => { + visit(tree, 'element', (node, index, parent) => { + if (headingRank(node) && hasProperty(node, 'id')) { + return method(node, index, parent) + } + }) } + /** @type {import('unist-util-visit').Visitor} */ function inject(node) { node.children[behavior === 'prepend' ? 'unshift' : 'push']( create(node, extend(true, {}, props), toChildren(content, node)) ) - return [visit.SKIP] + return [SKIP] } + /** @type {import('unist-util-visit').Visitor} */ function around(node, index, parent) { + // Uncommon. + /* c8 ignore next */ + if (typeof index !== 'number' || !parent) return + const link = create( node, extend(true, {}, props), toChildren(content, node) ) let nodes = behavior === 'before' ? [link, node] : [node, link] - const grouping = group && toNode(group, node) - if (grouping) { - grouping.children = nodes - nodes = [grouping] + if (group) { + const grouping = toNode(group, node) + + if (grouping && !Array.isArray(grouping) && grouping.type === 'element') { + grouping.children = nodes + nodes = [grouping] + } } parent.children.splice(index, 1, ...nodes) - return [visit.SKIP, index + nodes.length] + return [SKIP, index + nodes.length] } + /** @type {import('unist-util-visit').Visitor} */ function wrap(node) { node.children = [create(node, extend(true, {}, props), node.children)] - - return [visit.SKIP] + return [SKIP] } + /** + * @param {ElementChild|ElementChild[]|Build} value + * @param {Element} node + * @returns {ElementChild[]} + */ function toChildren(value, node) { const result = toNode(value, node) return Array.isArray(result) ? result : [result] } + /** + * @param {ElementChild|ElementChild[]|Build} value + * @param {Element} node + * @returns {ElementChild|ElementChild[]} + */ function toNode(value, node) { if (typeof value === 'function') return value(node) return extend(true, Array.isArray(value) ? [] : {}, value) } + /** + * @param {Element} node + * @param {Properties} props + * @param {ElementChild[]} children + * @returns {Element} + */ function create(node, props, children) { return { type: 'element', tagName: 'a', - properties: Object.assign({}, props, {href: '#' + node.properties.id}), + properties: Object.assign({}, props, { + // Fix hast types and make them required. + /* c8 ignore next */ + href: '#' + (node.properties || {}).id + }), children } } diff --git a/package.json b/package.json index 08f2d44..80c35c7 100644 --- a/package.json +++ b/package.json @@ -25,17 +25,22 @@ "sideEffects": false, "type": "module", "main": "index.js", + "types": "index.d.ts", "files": [ + "index.d.ts", "index.js" ], "dependencies": { + "@types/hast": "^2.0.0", "extend": "^3.0.0", "hast-util-has-property": "^2.0.0", "hast-util-heading-rank": "^2.0.0", + "unified": "^10.0.0", "unist-util-visit": "^4.0.0" }, "devDependencies": { - "@types/hast": "^2.0.0", + "@types/extend": "^3.0.1", + "@types/tape": "^4.0.0", "bail": "^2.0.0", "c8": "^7.0.0", "is-hidden": "^2.0.0", @@ -43,16 +48,19 @@ "rehype": "^12.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", - "unified": "^10.0.0", + "type-coverage": "^2.0.0", + "typescript": "^4.0.0", "xo": "^0.38.0" }, "scripts": { + "build": "rimraf \"*.d.ts\" \"test/**/*.d.ts\" && tsc && type-coverage", "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 npm run test-api", - "test": "npm run format && npm run test-coverage" + "test": "npm run build && npm run format && npm run test-coverage" }, "prettier": { "tabWidth": 2, @@ -72,5 +80,11 @@ "plugins": [ "preset-wooorm" ] + }, + "typeCoverage": { + "atLeast": 100, + "detail": true, + "strict": true, + "ignoreCatch": true } } diff --git a/test/index.js b/test/index.js index e91b666..65516ca 100644 --- a/test/index.js +++ b/test/index.js @@ -1,3 +1,7 @@ +/** + * @typedef {import('../index.js').Options} Options + */ + import fs from 'fs' import path from 'path' import {bail} from 'bail' @@ -13,25 +17,32 @@ test('rehypeAutolinkHeadings', (t) => { t.test('fixtures', (t) => { fs.readdir(root, (error, files) => { bail(error) - files = files.filter((d) => !isHidden(d)) - t.plan(files.length) + const visible = files.filter((d) => !isHidden(d)) + + t.plan(visible.length) let index = -1 - while (++index < files.length) { - one(files[index]) + while (++index < visible.length) { + one(visible[index]) } }) + /** + * @param {string} fixture + */ function one(fixture) { const base = path.join(root, fixture) const input = readSync(path.join(base, 'input.html')) const output = readSync(path.join(base, 'output.html')) + /** @type {Options|undefined} */ let config try { - config = JSON.parse(fs.readFileSync(path.join(base, 'config.json'))) + config = JSON.parse( + String(fs.readFileSync(path.join(base, 'config.json'))) + ) } catch {} t.test(fixture, (t) => { @@ -56,17 +67,25 @@ test('rehypeAutolinkHeadings', (t) => { .use(rehypeAutolinkHeadings, { behavior: 'after', group: (node) => { - t.equal(node.properties.id, 'a', 'should pass `node` to `group`') + t.equal( + node.properties && node.properties.id, + 'a', + 'should pass `node` to `group`' + ) return {type: 'element', tagName: 'div', properties: {}, children: []} }, content: (node) => { - t.equal(node.properties.id, 'a', 'should pass `node` to `content`') + t.equal( + node.properties && node.properties.id, + 'a', + 'should pass `node` to `content`' + ) return {type: 'element', tagName: 'i', properties: {}, children: []} } }) .process('

b

', (error, file) => { t.deepEqual( - [error, file.messages.length, String(file)], + [error, (file || {messages: []}).messages.length, String(file)], [null, 0, '

b

'] ) }) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ab9f0b7 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "include": ["*.js", "test/**/*.js"], + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020"], + "module": "ES2020", + "moduleResolution": "node", + "allowJs": true, + "checkJs": true, + "declaration": true, + "emitDeclarationOnly": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "strict": true + } +} diff --git a/types/index.d.ts b/types/index.d.ts deleted file mode 100644 index 58c86cb..0000000 --- a/types/index.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -// Minimum TypeScript Version: 3.5 -import {Plugin} from 'unified' -import {Element, Node, Properties} from 'hast' - -/** - * Automatically add links to headings. - */ -declare const autolinkHeadings: Plugin<[autolinkHeadings.Options?]> - -declare namespace autolinkHeadings { - interface Options { - /** - * How to create links. - * - * @default 'prepend' - */ - behavior?: 'prepend' | 'append' | 'wrap' | 'before' | 'after' - - /** - * Extra properties to set on the link. - * - * @default {ariaHidden: true, tabIndex: -1} - */ - properties?: Properties | Properties[] - - /** - * `hast` nodes to insert in the link. - * - * @default { type: 'element', tagName: 'span', properties: {className: ['icon', 'icon-link']}, children: [] } - */ - content?: Node | ((heading: Element) => Node[]) - - /** - * `hast` node to wrap the heading and link with, if `behavior` is - * `'before'` or `'after'`. There is no default. - */ - group?: Node | ((heading: Element) => Node) - } -} - -export = autolinkHeadings diff --git a/types/rehype-autolink-headings-tests.ts b/types/rehype-autolink-headings-tests.ts deleted file mode 100644 index 33aeef9..0000000 --- a/types/rehype-autolink-headings-tests.ts +++ /dev/null @@ -1,47 +0,0 @@ -import unified = require('unified') -import autolinkHeadings = require('rehype-autolink-headings') - -unified().use(autolinkHeadings) -unified().use(autolinkHeadings, {behavior: 'prepend'}) -unified().use(autolinkHeadings, {behavior: 'append'}) -unified().use(autolinkHeadings, {behavior: 'wrap'}) -unified().use(autolinkHeadings, {behavior: 'before'}) -unified().use(autolinkHeadings, {behavior: 'after'}) -unified().use(autolinkHeadings, {properties: {ariaHidden: true, tabIndex: -1}}) -unified().use(autolinkHeadings, { - properties: [{ariaHidden: true, tabIndex: -1}] -}) -unified().use(autolinkHeadings, { - content: { - type: 'element', - tagName: 'span', - properties: {className: ['icon', 'icon-link']}, - children: [] - } -}) -unified().use(autolinkHeadings, { - content: (_currentHeading) => [ - { - type: 'element', - tagName: 'span', - properties: {className: ['icon', 'icon-link']}, - children: [] - } - ] -}) -unified().use(autolinkHeadings, { - group: { - type: 'element', - tagName: 'span', - properties: {className: ['icon', 'icon-link']}, - children: [] - } -}) -unified().use(autolinkHeadings, { - group: (_currentHeading) => ({ - type: 'element', - tagName: 'span', - properties: {className: ['icon', 'icon-link']}, - children: [] - }) -}) diff --git a/types/tsconfig.json b/types/tsconfig.json deleted file mode 100644 index 5b9e519..0000000 --- a/types/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "lib": ["es2015"], - "strict": true, - "baseUrl": ".", - "paths": { - "rehype-autolink-headings": ["index.d.ts"] - } - } -} diff --git a/types/tslint.json b/types/tslint.json deleted file mode 100644 index 759caa0..0000000 --- a/types/tslint.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "dtslint/dtslint.json", - "rules": { - "no-redundant-jsdoc": false, - "semicolon": false, - "whitespace": false - } -}