From 2f769847006d56ba9d72ec53aa91ab4eaefa4595 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 24 Jan 2023 16:07:02 +0100 Subject: [PATCH 1/9] Update dev-dependencies --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6557954..f8d9e10 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "unist-builder": "^3.0.0", "xast-util-from-xml": "^2.0.0", "xastscript": "^3.0.0", - "xo": "^0.51.0" + "xo": "^0.53.0" }, "scripts": { "prepack": "npm run build && npm run format", From 0baad6030dd330f42156b1260f47f508cc632fa1 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 24 Jan 2023 16:07:10 +0100 Subject: [PATCH 2/9] Update Actions --- .github/workflows/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 69924a4..89dc06c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,13 +7,13 @@ jobs: name: ${{matrix.node}} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: dcodeIO/setup-node-nvm@master + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: node-version: ${{matrix.node}} - run: npm install - run: npm test - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v3 strategy: matrix: node: From d1d6df9d082c51b1260b16cd9f4a950d2eb84413 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 24 Jan 2023 16:07:36 +0100 Subject: [PATCH 3/9] Update `tsconfig.json` --- package.json | 7 +++---- tsconfig.json | 17 +++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index f8d9e10..d69ad13 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,6 @@ "remark-cli": "^11.0.0", "remark-preset-wooorm": "^9.0.0", "retext": "^8.0.0", - "rimraf": "^3.0.0", "strip-ansi": "^7.0.0", "tape": "^5.0.0", "type-coverage": "^2.0.0", @@ -65,10 +64,10 @@ }, "scripts": { "prepack": "npm run build && npm run format", - "build": "rimraf \"*.d.ts\" && tsc && type-coverage", + "build": "tsc --build --clean && tsc --build && type-coverage", "format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", - "test-api": "node test.js", - "test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov node test.js", + "test-api": "node --conditions development test.js", + "test-coverage": "c8 --check-coverage --100 --reporter lcov npm run test-api", "test": "npm run build && npm run format && npm run test-coverage" }, "prettier": { diff --git a/tsconfig.json b/tsconfig.json index e31adf8..ebe8889 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,17 @@ { - "include": ["*.js"], + "include": ["**/*.js"], + "exclude": ["coverage/", "node_modules/"], "compilerOptions": { - "target": "ES2020", - "lib": ["ES2020"], - "module": "ES2020", - "moduleResolution": "node", - "allowJs": true, "checkJs": true, "declaration": true, "emitDeclarationOnly": true, - "allowSyntheticDefaultImports": true, + "exactOptionalPropertyTypes": true, + "forceConsistentCasingInFileNames": true, + "lib": ["es2020"], + "module": "node16", + "newLine": "lf", "skipLibCheck": true, - "strict": true + "strict": true, + "target": "es2020" } } From 1a2f84b2bab1b0052f51a0b569a756352fcb5313 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 24 Jan 2023 16:09:03 +0100 Subject: [PATCH 4/9] Refactor to move implementation to `lib/` --- index.js | 306 +--------------------- color-browser.js => lib/color-browser.js | 0 color.js => lib/color.js | 0 lib/index.js | 307 +++++++++++++++++++++++ package.json | 9 +- 5 files changed, 312 insertions(+), 310 deletions(-) rename color-browser.js => lib/color-browser.js (100%) rename color.js => lib/color.js (100%) create mode 100644 lib/index.js diff --git a/index.js b/index.js index 0081f26..3a0b54d 100644 --- a/index.js +++ b/index.js @@ -1,310 +1,8 @@ /** - * @typedef {import('unist').Node} Node - * @typedef {import('unist').Position} Position - * @typedef {import('unist').Point} Point - * - * @typedef Options - * @property {boolean} [showPositions=true] + * @typedef {import('./lib/index.js')} Options * * @typedef {Options} InspectOptions * Deprecated, use `Options`. */ -import {color} from './color.js' - -/* c8 ignore next */ -export const inspect = color ? inspectColor : inspectNoColor - -const own = {}.hasOwnProperty - -const bold = ansiColor(1, 22) -const dim = ansiColor(2, 22) -const yellow = ansiColor(33, 39) -const green = ansiColor(32, 39) - -// ANSI color regex. -/* eslint-disable no-control-regex */ -const colorExpression = - /(?:(?:\u001B\[)|\u009B)(?:\d{1,3})?(?:(?:;\d{0,3})*)?[A-M|f-m]|\u001B[A-M]/g -/* eslint-enable no-control-regex */ - -/** - * Inspects a node, without using color. - * - * @param {unknown} node - * @param {Options} [options] - * @returns {string} - */ -export function inspectNoColor(node, options) { - return inspectColor(node, options).replace(colorExpression, '') -} - -/** - * Inspects a node, using color. - * - * @param {unknown} tree - * @param {Options} [options] - * @returns {string} - */ -export function inspectColor(tree, options = {}) { - const positions = - options.showPositions === null || options.showPositions === undefined - ? true - : options.showPositions - - return inspectValue(tree) - - /** - * @param {unknown} node - * @returns {string} - */ - function inspectValue(node) { - if (node && typeof node === 'object' && 'length' in node) { - // @ts-expect-error looks like a list of nodes. - return inspectNodes(node) - } - - // @ts-expect-error looks like a single node. - if (node && node.type) { - // @ts-expect-error looks like a single node. - return inspectTree(node) - } - - return inspectNonTree(node) - } - - /** - * @param {unknown} value - * @returns {string} - */ - function inspectNonTree(value) { - return JSON.stringify(value) - } - - /** - * @param {Array} nodes - * @returns {string} - */ - function inspectNodes(nodes) { - const size = String(nodes.length - 1).length - /** @type {Array} */ - const result = [] - let index = -1 - - while (++index < nodes.length) { - result.push( - dim( - (index < nodes.length - 1 ? '├' : '└') + - '─' + - String(index).padEnd(size) - ) + - ' ' + - indent( - inspectValue(nodes[index]), - (index < nodes.length - 1 ? dim('│') : ' ') + ' '.repeat(size + 2), - true - ) - ) - } - - return result.join('\n') - } - - /** - * @param {Record} object - * @returns {string} - */ - // eslint-disable-next-line complexity - function inspectFields(object) { - /** @type {Array} */ - const result = [] - /** @type {string} */ - let key - - for (key in object) { - /* c8 ignore next 1 */ - if (!own.call(object, key)) continue - - const value = object[key] - /** @type {string} */ - let formatted - - if ( - value === undefined || - // Standard keys defined by unist that we format differently. - // - key === 'type' || - key === 'value' || - key === 'children' || - key === 'position' || - // Ignore `name` (from xast) and `tagName` (from `hast`) when string. - (typeof value === 'string' && (key === 'name' || key === 'tagName')) - ) { - continue - } - - // A single node. - if ( - value && - typeof value === 'object' && - // @ts-expect-error looks like a node. - value.type && - key !== 'data' && - key !== 'attributes' && - key !== 'properties' - ) { - // @ts-expect-error looks like a node. - formatted = inspectTree(value) - } - // A list of nodes. - else if ( - value && - Array.isArray(value) && - // Looks like a node. - // type-coverage:ignore-next-line - value[0] && - // Looks like a node. - // type-coverage:ignore-next-line - value[0].type - ) { - formatted = '\n' + inspectNodes(value) - } else { - formatted = inspectNonTree(value) - } - - result.push( - key + dim(':') + (/\s/.test(formatted.charAt(0)) ? '' : ' ') + formatted - ) - } - - return indent( - result.join('\n'), - // @ts-expect-error looks like a parent node. - (object.children && object.children.length > 0 ? dim('│') : ' ') + ' ' - ) - } - - /** - * @param {Node} node - * @returns {string} - */ - function inspectTree(node) { - const result = [formatNode(node)] - // @ts-expect-error: looks like a record. - const fields = inspectFields(node) - // @ts-expect-error looks like a parent. - const content = inspectNodes(node.children || []) - if (fields) result.push(fields) - if (content) result.push(content) - return result.join('\n') - } - - /** - * Colored node formatter. - * - * @param {Node} node - * @returns {string} - */ - function formatNode(node) { - const result = [bold(node.type)] - /** @type {string|undefined} */ - // @ts-expect-error: might be available. - const kind = node.tagName || node.name - const position = positions ? stringifyPosition(node.position) : '' - - if (typeof kind === 'string') { - result.push('<', kind, '>') - } - - // @ts-expect-error: looks like a parent. - if (node.children) { - // @ts-expect-error looks like a parent. - result.push(dim('['), yellow(node.children.length), dim(']')) - // @ts-expect-error: looks like a literal. - } else if (typeof node.value === 'string') { - // @ts-expect-error: looks like a literal. - result.push(' ', green(inspectNonTree(node.value))) - } - - if (position) { - result.push(' ', dim('('), position, dim(')')) - } - - return result.join('') - } -} - -/** - * @param {string} value - * @param {string} indentation - * @param {boolean} [ignoreFirst=false] - * @returns {string} - */ -function indent(value, indentation, ignoreFirst) { - const lines = value.split('\n') - let index = ignoreFirst ? 0 : -1 - - if (!value) return value - - while (++index < lines.length) { - lines[index] = indentation + lines[index] - } - - return lines.join('\n') -} - -/** - * @param {Position|undefined} [value] - * @returns {string} - */ -function stringifyPosition(value) { - /** @type {Position} */ - // @ts-expect-error: fine. - const position = value || {} - /** @type {Array} */ - const result = [] - /** @type {Array} */ - const positions = [] - /** @type {Array} */ - const offsets = [] - - point(position.start) - point(position.end) - - if (positions.length > 0) result.push(positions.join('-')) - if (offsets.length > 0) result.push(offsets.join('-')) - - return result.join(', ') - - /** - * @param {Point} value - */ - function point(value) { - if (value) { - positions.push((value.line || 1) + ':' + (value.column || 1)) - - if ('offset' in value) { - offsets.push(String(value.offset || 0)) - } - } - } -} - -/** - * Factory to wrap values in ANSI colours. - * - * @param {number} open - * @param {number} close - * @returns {function(string): string} - */ -function ansiColor(open, close) { - return color - - /** - * @param {string} value - * @returns {string} - */ - function color(value) { - return '\u001B[' + open + 'm' + value + '\u001B[' + close + 'm' - } -} +export {inspect, inspectColor, inspectNoColor} from './lib/index.js' diff --git a/color-browser.js b/lib/color-browser.js similarity index 100% rename from color-browser.js rename to lib/color-browser.js diff --git a/color.js b/lib/color.js similarity index 100% rename from color.js rename to lib/color.js diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..f045d24 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,307 @@ +/** + * @typedef {import('unist').Node} Node + * @typedef {import('unist').Position} Position + * @typedef {import('unist').Point} Point + * + * @typedef Options + * @property {boolean} [showPositions=true] + */ + +import {color} from './color.js' + +/* c8 ignore next */ +export const inspect = color ? inspectColor : inspectNoColor + +const own = {}.hasOwnProperty + +const bold = ansiColor(1, 22) +const dim = ansiColor(2, 22) +const yellow = ansiColor(33, 39) +const green = ansiColor(32, 39) + +// ANSI color regex. +/* eslint-disable no-control-regex */ +const colorExpression = + /(?:(?:\u001B\[)|\u009B)(?:\d{1,3})?(?:(?:;\d{0,3})*)?[A-M|f-m]|\u001B[A-M]/g +/* eslint-enable no-control-regex */ + +/** + * Inspects a node, without using color. + * + * @param {unknown} node + * @param {Options} [options] + * @returns {string} + */ +export function inspectNoColor(node, options) { + return inspectColor(node, options).replace(colorExpression, '') +} + +/** + * Inspects a node, using color. + * + * @param {unknown} tree + * @param {Options} [options] + * @returns {string} + */ +export function inspectColor(tree, options = {}) { + const positions = + options.showPositions === null || options.showPositions === undefined + ? true + : options.showPositions + + return inspectValue(tree) + + /** + * @param {unknown} node + * @returns {string} + */ + function inspectValue(node) { + if (node && typeof node === 'object' && 'length' in node) { + // @ts-expect-error looks like a list of nodes. + return inspectNodes(node) + } + + // @ts-expect-error looks like a single node. + if (node && node.type) { + // @ts-expect-error looks like a single node. + return inspectTree(node) + } + + return inspectNonTree(node) + } + + /** + * @param {unknown} value + * @returns {string} + */ + function inspectNonTree(value) { + return JSON.stringify(value) + } + + /** + * @param {Array} nodes + * @returns {string} + */ + function inspectNodes(nodes) { + const size = String(nodes.length - 1).length + /** @type {Array} */ + const result = [] + let index = -1 + + while (++index < nodes.length) { + result.push( + dim( + (index < nodes.length - 1 ? '├' : '└') + + '─' + + String(index).padEnd(size) + ) + + ' ' + + indent( + inspectValue(nodes[index]), + (index < nodes.length - 1 ? dim('│') : ' ') + ' '.repeat(size + 2), + true + ) + ) + } + + return result.join('\n') + } + + /** + * @param {Record} object + * @returns {string} + */ + // eslint-disable-next-line complexity + function inspectFields(object) { + /** @type {Array} */ + const result = [] + /** @type {string} */ + let key + + for (key in object) { + /* c8 ignore next 1 */ + if (!own.call(object, key)) continue + + const value = object[key] + /** @type {string} */ + let formatted + + if ( + value === undefined || + // Standard keys defined by unist that we format differently. + // + key === 'type' || + key === 'value' || + key === 'children' || + key === 'position' || + // Ignore `name` (from xast) and `tagName` (from `hast`) when string. + (typeof value === 'string' && (key === 'name' || key === 'tagName')) + ) { + continue + } + + // A single node. + if ( + value && + typeof value === 'object' && + // @ts-expect-error looks like a node. + value.type && + key !== 'data' && + key !== 'attributes' && + key !== 'properties' + ) { + // @ts-expect-error looks like a node. + formatted = inspectTree(value) + } + // A list of nodes. + else if ( + value && + Array.isArray(value) && + // Looks like a node. + // type-coverage:ignore-next-line + value[0] && + // Looks like a node. + // type-coverage:ignore-next-line + value[0].type + ) { + formatted = '\n' + inspectNodes(value) + } else { + formatted = inspectNonTree(value) + } + + result.push( + key + dim(':') + (/\s/.test(formatted.charAt(0)) ? '' : ' ') + formatted + ) + } + + return indent( + result.join('\n'), + // @ts-expect-error looks like a parent node. + (object.children && object.children.length > 0 ? dim('│') : ' ') + ' ' + ) + } + + /** + * @param {Node} node + * @returns {string} + */ + function inspectTree(node) { + const result = [formatNode(node)] + // @ts-expect-error: looks like a record. + const fields = inspectFields(node) + // @ts-expect-error looks like a parent. + const content = inspectNodes(node.children || []) + if (fields) result.push(fields) + if (content) result.push(content) + return result.join('\n') + } + + /** + * Colored node formatter. + * + * @param {Node} node + * @returns {string} + */ + function formatNode(node) { + const result = [bold(node.type)] + /** @type {string|undefined} */ + // @ts-expect-error: might be available. + const kind = node.tagName || node.name + const position = positions ? stringifyPosition(node.position) : '' + + if (typeof kind === 'string') { + result.push('<', kind, '>') + } + + // @ts-expect-error: looks like a parent. + if (node.children) { + // @ts-expect-error looks like a parent. + result.push(dim('['), yellow(node.children.length), dim(']')) + // @ts-expect-error: looks like a literal. + } else if (typeof node.value === 'string') { + // @ts-expect-error: looks like a literal. + result.push(' ', green(inspectNonTree(node.value))) + } + + if (position) { + result.push(' ', dim('('), position, dim(')')) + } + + return result.join('') + } +} + +/** + * @param {string} value + * @param {string} indentation + * @param {boolean} [ignoreFirst=false] + * @returns {string} + */ +function indent(value, indentation, ignoreFirst) { + const lines = value.split('\n') + let index = ignoreFirst ? 0 : -1 + + if (!value) return value + + while (++index < lines.length) { + lines[index] = indentation + lines[index] + } + + return lines.join('\n') +} + +/** + * @param {Position|undefined} [value] + * @returns {string} + */ +function stringifyPosition(value) { + /** @type {Position} */ + // @ts-expect-error: fine. + const position = value || {} + /** @type {Array} */ + const result = [] + /** @type {Array} */ + const positions = [] + /** @type {Array} */ + const offsets = [] + + point(position.start) + point(position.end) + + if (positions.length > 0) result.push(positions.join('-')) + if (offsets.length > 0) result.push(offsets.join('-')) + + return result.join(', ') + + /** + * @param {Point} value + */ + function point(value) { + if (value) { + positions.push((value.line || 1) + ':' + (value.column || 1)) + + if ('offset' in value) { + offsets.push(String(value.offset || 0)) + } + } + } +} + +/** + * Factory to wrap values in ANSI colours. + * + * @param {number} open + * @param {number} close + * @returns {function(string): string} + */ +function ansiColor(open, close) { + return color + + /** + * @param {string} value + * @returns {string} + */ + function color(value) { + return '\u001B[' + open + 'm' + value + '\u001B[' + close + 'm' + } +} diff --git a/package.json b/package.json index d69ad13..6c74962 100644 --- a/package.json +++ b/package.json @@ -27,17 +27,14 @@ "type": "module", "main": "index.js", "browser": { - "./color.js": "./color-browser.js" + "./lib/color.js": "./lib/color-browser.js" }, "react-native": { - "./color.js": "./color-browser.js" + "./lib/color.js": "./lib/color-browser.js" }, "types": "index.d.ts", "files": [ - "color.d.ts", - "color.js", - "color-browser.d.ts", - "color-browser.js", + "lib/", "index.d.ts", "index.js" ], From 22b9c44bfa8cdf8ac183f8617d611da0e010ba47 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 24 Jan 2023 16:42:13 +0100 Subject: [PATCH 5/9] Refactor code-style * Add more docs to JSDoc * Remove `ts-expects` * Add support for `null` in input of API types --- lib/index.js | 449 ++++++++++++++++++++++++++++++--------------------- test.js | 14 +- 2 files changed, 279 insertions(+), 184 deletions(-) diff --git a/lib/index.js b/lib/index.js index f045d24..463083e 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,10 +1,18 @@ /** * @typedef {import('unist').Node} Node + * @typedef {import('unist').Parent} Parent * @typedef {import('unist').Position} Position * @typedef {import('unist').Point} Point * * @typedef Options - * @property {boolean} [showPositions=true] + * Configuration. + * @property {boolean | null | undefined} [showPositions=true] + * Whether to include positional information. + * + * @typedef State + * Info passed around. + * @property {boolean} showPositions + * Whether to include positional information. */ import {color} from './color.js' @@ -26,11 +34,14 @@ const colorExpression = /* eslint-enable no-control-regex */ /** - * Inspects a node, without using color. + * Inspect a node, without color. * * @param {unknown} node - * @param {Options} [options] + * Tree to inspect. + * @param {Options | null | undefined} [options] + * Configuration. * @returns {string} + * Pretty printed `tree`. */ export function inspectNoColor(node, options) { return inspectColor(node, options).replace(colorExpression, '') @@ -40,209 +51,242 @@ export function inspectNoColor(node, options) { * Inspects a node, using color. * * @param {unknown} tree - * @param {Options} [options] + * Tree to inspect. + * @param {Options | null | undefined} [options] + * Configuration. * @returns {string} + * Pretty printed `tree`. */ -export function inspectColor(tree, options = {}) { - const positions = - options.showPositions === null || options.showPositions === undefined - ? true - : options.showPositions - - return inspectValue(tree) - - /** - * @param {unknown} node - * @returns {string} - */ - function inspectValue(node) { - if (node && typeof node === 'object' && 'length' in node) { - // @ts-expect-error looks like a list of nodes. - return inspectNodes(node) - } +export function inspectColor(tree, options) { + /** @type {State} */ + const state = { + showPositions: + !options || + options.showPositions === null || + options.showPositions === undefined + ? true + : options.showPositions + } - // @ts-expect-error looks like a single node. - if (node && node.type) { - // @ts-expect-error looks like a single node. - return inspectTree(node) - } + return inspectValue(tree, state) +} - return inspectNonTree(node) +/** + * Format any value. + * + * @param {unknown} node + * Thing to format. + * @param {State} state + * Info passed around. + * @returns {string} + * Formatted thing. + */ +function inspectValue(node, state) { + if (isArrayUnknown(node)) { + return inspectNodes(node, state) } - /** - * @param {unknown} value - * @returns {string} - */ - function inspectNonTree(value) { - return JSON.stringify(value) + if (isNode(node)) { + return inspectTree(node, state) } - /** - * @param {Array} nodes - * @returns {string} - */ - function inspectNodes(nodes) { - const size = String(nodes.length - 1).length - /** @type {Array} */ - const result = [] - let index = -1 - - while (++index < nodes.length) { - result.push( - dim( - (index < nodes.length - 1 ? '├' : '└') + - '─' + - String(index).padEnd(size) - ) + - ' ' + - indent( - inspectValue(nodes[index]), - (index < nodes.length - 1 ? dim('│') : ' ') + ' '.repeat(size + 2), - true - ) - ) - } + return inspectNonTree(node) +} + +/** + * Format an unknown value. + * + * @param {unknown} value + * Thing to format. + * @returns {string} + * Formatted thing. + */ +function inspectNonTree(value) { + return JSON.stringify(value) +} - return result.join('\n') +/** + * Format a list of nodes. + * + * @param {Array} nodes + * Nodes to format. + * @param {State} state + * Info passed around. + * @returns {string} + * Formatted nodes. + */ +function inspectNodes(nodes, state) { + const size = String(nodes.length - 1).length + /** @type {Array} */ + const result = [] + let index = -1 + + while (++index < nodes.length) { + result.push( + dim( + (index < nodes.length - 1 ? '├' : '└') + + '─' + + String(index).padEnd(size) + ) + + ' ' + + indent( + inspectValue(nodes[index], state), + (index < nodes.length - 1 ? dim('│') : ' ') + ' '.repeat(size + 2), + true + ) + ) } - /** - * @param {Record} object - * @returns {string} - */ - // eslint-disable-next-line complexity - function inspectFields(object) { - /** @type {Array} */ - const result = [] - /** @type {string} */ - let key - - for (key in object) { - /* c8 ignore next 1 */ - if (!own.call(object, key)) continue - - const value = object[key] - /** @type {string} */ - let formatted - - if ( - value === undefined || - // Standard keys defined by unist that we format differently. - // - key === 'type' || - key === 'value' || - key === 'children' || - key === 'position' || - // Ignore `name` (from xast) and `tagName` (from `hast`) when string. - (typeof value === 'string' && (key === 'name' || key === 'tagName')) - ) { - continue - } + return result.join('\n') +} - // A single node. - if ( - value && - typeof value === 'object' && - // @ts-expect-error looks like a node. - value.type && - key !== 'data' && - key !== 'attributes' && - key !== 'properties' - ) { - // @ts-expect-error looks like a node. - formatted = inspectTree(value) - } - // A list of nodes. - else if ( - value && - Array.isArray(value) && - // Looks like a node. - // type-coverage:ignore-next-line - value[0] && - // Looks like a node. - // type-coverage:ignore-next-line - value[0].type - ) { - formatted = '\n' + inspectNodes(value) - } else { - formatted = inspectNonTree(value) - } +/** + * Format the fields in a node. + * + * @param {Record} object + * Node to format. + * @param {State} state + * Info passed around. + * @returns {string} + * Formatted node. + */ +// eslint-disable-next-line complexity +function inspectFields(object, state) { + /** @type {Array} */ + const result = [] + /** @type {string} */ + let key + + for (key in object) { + /* c8 ignore next 1 */ + if (!own.call(object, key)) continue - result.push( - key + dim(':') + (/\s/.test(formatted.charAt(0)) ? '' : ' ') + formatted - ) + const value = object[key] + /** @type {string} */ + let formatted + + if ( + value === undefined || + // Standard keys defined by unist that we format differently. + // + key === 'type' || + key === 'value' || + key === 'children' || + key === 'position' || + // Ignore `name` (from xast) and `tagName` (from `hast`) when string. + (typeof value === 'string' && (key === 'name' || key === 'tagName')) + ) { + continue } - return indent( - result.join('\n'), - // @ts-expect-error looks like a parent node. - (object.children && object.children.length > 0 ? dim('│') : ' ') + ' ' + // A single node. + if ( + isNode(value) && + key !== 'data' && + key !== 'attributes' && + key !== 'properties' + ) { + formatted = inspectTree(value, state) + } + // A list of nodes. + else if (value && isArrayUnknown(value) && isNode(value[0])) { + formatted = '\n' + inspectNodes(value, state) + } else { + formatted = inspectNonTree(value) + } + + result.push( + key + dim(':') + (/\s/.test(formatted.charAt(0)) ? '' : ' ') + formatted ) } - /** - * @param {Node} node - * @returns {string} - */ - function inspectTree(node) { - const result = [formatNode(node)] - // @ts-expect-error: looks like a record. - const fields = inspectFields(node) - // @ts-expect-error looks like a parent. - const content = inspectNodes(node.children || []) - if (fields) result.push(fields) - if (content) result.push(content) - return result.join('\n') - } + return indent( + result.join('\n'), + (isArrayUnknown(object.children) && object.children.length > 0 + ? dim('│') + : ' ') + ' ' + ) +} - /** - * Colored node formatter. - * - * @param {Node} node - * @returns {string} - */ - function formatNode(node) { - const result = [bold(node.type)] - /** @type {string|undefined} */ - // @ts-expect-error: might be available. - const kind = node.tagName || node.name - const position = positions ? stringifyPosition(node.position) : '' - - if (typeof kind === 'string') { - result.push('<', kind, '>') - } +/** + * Format a node, its fields, and its children. + * + * @param {Node} node + * Node to format. + * @param {State} state + * Info passed around. + * @returns {string} + * Formatted node. + */ +function inspectTree(node, state) { + const result = [formatNode(node, state)] + // Cast as record to allow indexing. + const map = /** @type {Record} */ ( + /** @type {unknown} */ (node) + ) + const fields = inspectFields(map, state) + const content = isArrayUnknown(map.children) + ? inspectNodes(map.children, state) + : '' + if (fields) result.push(fields) + if (content) result.push(content) + return result.join('\n') +} - // @ts-expect-error: looks like a parent. - if (node.children) { - // @ts-expect-error looks like a parent. - result.push(dim('['), yellow(node.children.length), dim(']')) - // @ts-expect-error: looks like a literal. - } else if (typeof node.value === 'string') { - // @ts-expect-error: looks like a literal. - result.push(' ', green(inspectNonTree(node.value))) - } +/** + * Format a node itself. + * + * @param {Node} node + * Node to format. + * @param {State} state + * Info passed around. + * @returns {string} + * Formatted node. + */ +function formatNode(node, state) { + const result = [bold(node.type)] + // Cast as record to allow indexing. + const map = /** @type {Record} */ ( + /** @type {unknown} */ (node) + ) + const kind = map.tagName || map.name + const position = state.showPositions ? stringifyPosition(node.position) : '' + + if (typeof kind === 'string') { + result.push('<', kind, '>') + } - if (position) { - result.push(' ', dim('('), position, dim(')')) - } + if (isArrayUnknown(map.children)) { + result.push(dim('['), yellow(String(map.children.length)), dim(']')) + } else if (typeof map.value === 'string') { + result.push(' ', green(inspectNonTree(map.value))) + } - return result.join('') + if (position) { + result.push(' ', dim('('), position, dim(')')) } + + return result.join('') } /** + * Indent a value. + * * @param {string} value + * Value to indent. * @param {string} indentation - * @param {boolean} [ignoreFirst=false] + * Indent to use. + * @param {boolean | undefined} [ignoreFirst=false] + * Whether to ignore indenting the first line. * @returns {string} + * Indented `value`. */ function indent(value, indentation, ignoreFirst) { + if (!value) return value + const lines = value.split('\n') let index = ignoreFirst ? 0 : -1 - if (!value) return value - while (++index < lines.length) { lines[index] = indentation + lines[index] } @@ -251,13 +295,14 @@ function indent(value, indentation, ignoreFirst) { } /** - * @param {Position|undefined} [value] + * Serialize a position. + * + * @param {unknown | null | undefined} [value] + * Position to serialize. * @returns {string} + * Serialized position. */ function stringifyPosition(value) { - /** @type {Position} */ - // @ts-expect-error: fine. - const position = value || {} /** @type {Array} */ const result = [] /** @type {Array} */ @@ -265,8 +310,10 @@ function stringifyPosition(value) { /** @type {Array} */ const offsets = [] - point(position.start) - point(position.end) + if (value && typeof value === 'object') { + point('start' in value ? value.start : undefined) + point('end' in value ? value.end : undefined) + } if (positions.length > 0) result.push(positions.join('-')) if (offsets.length > 0) result.push(offsets.join('-')) @@ -274,13 +321,21 @@ function stringifyPosition(value) { return result.join(', ') /** - * @param {Point} value + * Add a point. + * + * @param {unknown} value + * Point to add. */ function point(value) { - if (value) { - positions.push((value.line || 1) + ':' + (value.column || 1)) + if (value && typeof value === 'object') { + const line = + 'line' in value && typeof value.line === 'number' ? value.line : 1 + const column = + 'column' in value && typeof value.column === 'number' ? value.column : 1 + + positions.push(line + ':' + column) - if ('offset' in value) { + if ('offset' in value && typeof value.offset === 'number') { offsets.push(String(value.offset || 0)) } } @@ -291,17 +346,45 @@ function stringifyPosition(value) { * Factory to wrap values in ANSI colours. * * @param {number} open + * Opening color code. * @param {number} close - * @returns {function(string): string} + * Closing color code. + * @returns {(value: string) => string} + * Color `value`. */ function ansiColor(open, close) { return color /** + * Color `value`. + * * @param {string} value + * Value to color. * @returns {string} + * Colored `value`. */ function color(value) { return '\u001B[' + open + 'm' + value + '\u001B[' + close + 'm' } } + +/** + * @param {unknown} value + * @returns {value is Node} + */ +function isNode(value) { + return Boolean( + value && + typeof value === 'object' && + 'type' in value && + typeof value.type === 'string' + ) +} + +/** + * @param {unknown} node + * @returns {node is Array} + */ +function isArrayUnknown(node) { + return Array.isArray(node) +} diff --git a/test.js b/test.js index 2fd5caa..db7fddc 100644 --- a/test.js +++ b/test.js @@ -307,6 +307,18 @@ test('inspect()', (t) => { 'should work without `offset` in `position`' ) + t.equal( + strip( + inspect({ + type: 'foo', + value: 'foo\nbaar', + position: {} + }) + ), + 'foo "foo\\nbaar"', + 'should work without `start` and `end` in `position`' + ) + t.equal( strip( inspect({ @@ -316,7 +328,7 @@ test('inspect()', (t) => { }) ), 'foo "foo\\nbaar" (1:1-1:1)', - 'should work without `line` and `column` in `position`' + 'should work without `line` and `column` in `point`' ) t.equal( From 8901d8b2f56ddfe4b5a1aaea64fe640e4c82075c Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 24 Jan 2023 16:50:54 +0100 Subject: [PATCH 6/9] Use Node test runner --- .github/workflows/main.yml | 2 +- package.json | 3 +- test.js | 75 +++++++++++++++++--------------------- 3 files changed, 35 insertions(+), 45 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 89dc06c..fb63387 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,5 +17,5 @@ jobs: strategy: matrix: node: - - lts/fermium + - lts/gallium - node diff --git a/package.json b/package.json index 6c74962..fb666a4 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "@types/unist": "^2.0.0" }, "devDependencies": { - "@types/tape": "^4.0.0", + "@types/node": "^18.0.0", "c8": "^7.0.0", "chalk": "^5.0.0", "hastscript": "^7.0.0", @@ -51,7 +51,6 @@ "remark-preset-wooorm": "^9.0.0", "retext": "^8.0.0", "strip-ansi": "^7.0.0", - "tape": "^5.0.0", "type-coverage": "^2.0.0", "typescript": "^4.0.0", "unist-builder": "^3.0.0", diff --git a/test.js b/test.js index db7fddc..7a98fe6 100644 --- a/test.js +++ b/test.js @@ -1,4 +1,5 @@ -import test from 'tape' +import assert from 'node:assert/strict' +import test from 'node:test' /* eslint-disable-next-line unicorn/import-style */ import {Chalk} from 'chalk' import strip from 'strip-ansi' @@ -13,14 +14,12 @@ const chalkEnabled = new Chalk({level: 1}) const paragraph = 'Some simple text. Other “sentence”.' -test('inspect', (t) => { - t.equal(typeof inspect, 'function', 'should be a `function`') - - t.end() +test('inspect', () => { + assert.equal(typeof inspect, 'function', 'should be a `function`') }) -test('inspect()', (t) => { - t.equal( +test('inspect()', () => { + assert.equal( strip(inspect(retext().parse(paragraph))), [ 'RootNode[1] (1:1-1:36, 0-35)', @@ -49,22 +48,20 @@ test('inspect()', (t) => { 'should work on `RootNode`' ) - t.equal( + assert.equal( strip(inspect([u('SymbolNode', '$'), u('WordNode', [u('text', '5,00')])])), '├─0 SymbolNode "$"\n└─1 WordNode[1]\n └─0 text "5,00"', 'should work with a list of nodes' ) - t.test('should work on non-nodes', (st) => { - st.equal(strip(inspect('foo')), '"foo"') - st.equal(strip(inspect(null)), 'null') - st.equal(strip(inspect(Number.NaN)), 'null') - st.equal(strip(inspect(3)), '3') - - st.end() - }) + assert.doesNotThrow(() => { + assert.equal(strip(inspect('foo')), '"foo"') + assert.equal(strip(inspect(null)), 'null') + assert.equal(strip(inspect(Number.NaN)), 'null') + assert.equal(strip(inspect(3)), '3') + }, 'should work on non-nodes') - t.equal( + assert.equal( strip( inspect( Array.from({length: 11}).map((/** @type {undefined} */ d, i) => ({ @@ -101,7 +98,7 @@ test('inspect()', (t) => { 'should align and indent large numbers' ) - t.equal( + assert.equal( strip( inspect({ type: 'SymbolNode', @@ -113,7 +110,7 @@ test('inspect()', (t) => { 'should work with data attributes' ) - t.equal( + assert.equal( strip( inspect({ type: 'table', @@ -165,7 +162,7 @@ test('inspect()', (t) => { 'should work with other attributes' ) - t.equal( + assert.equal( strip( inspect({ type: 'element', @@ -177,19 +174,19 @@ test('inspect()', (t) => { 'should work on parent nodes without children' ) - t.equal( + assert.equal( strip(inspect({type: 'text', value: ''})), 'text ""', 'should work on text nodes without value' ) - t.equal( + assert.equal( strip(inspect({type: 'thematicBreak'})), 'thematicBreak', 'should work on void nodes' ) - t.equal( + assert.equal( strip(inspect(h('button', {type: 'submit', value: 'Send'}))), [ 'element