unist-util-inspect
Version:
unist utility to inspect nodes
411 lines (367 loc) • 9.22 kB
JavaScript
/**
* @import {Options} from 'unist-util-inspect'
* @import {Node} from 'unist'
* @import {State} from './types.js'
*/
import {color as colorDefault} from '#conditional-color'
/** @type {Readonly<Options>} */
const emptyOptions = {}
// To do: next major (?): use `Object.hasOwn`.
const own = {}.hasOwnProperty
/**
* Inspect a node, without color.
*
* @param {unknown} tree
* Tree to inspect.
* @param {Readonly<Options> | null | undefined} [options]
* Configuration.
* @returns {string}
* Pretty printed `tree`.
*/
export function inspect(tree, options) {
const settings = options || emptyOptions
const color =
typeof settings.color === 'boolean' ? settings.color : colorDefault
const showPositions =
typeof settings.showPositions === 'boolean' ? settings.showPositions : true
/** @type {State} */
const state = {
bold: color ? ansiColor(1, 22) : identity,
dim: color ? ansiColor(2, 22) : identity,
green: color ? ansiColor(32, 39) : identity,
showPositions,
yellow: color ? ansiColor(33, 39) : identity
}
return inspectValue(tree, state)
}
// To do: remove.
/**
* Inspect a node, without color.
*
* @deprecated
* Use `inspect` instead, with `color: false`.
* @param {unknown} tree
* Tree to inspect.
* @param {Readonly<Omit<Options, 'color'>> | null | undefined} [options]
* Configuration.
* @returns {string}
* Pretty printed `tree`.
*/
export function inspectNoColor(tree, options) {
return inspect(tree, {...options, color: false})
}
// To do: remove.
/**
* Inspects a node, using color.
*
* @deprecated
* Use `inspect` instead, with `color: true`.
* @param {unknown} tree
* Tree to inspect.
* @param {Readonly<Omit<Options, 'color'>> | null | undefined} [options]
* Configuration (optional).
* @returns {string}
* Pretty printed `tree`.
*/
export function inspectColor(tree, options) {
return inspect(tree, {...options, color: true})
}
/**
* 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)
}
if (isNode(node)) {
return inspectTree(node, state)
}
return inspectNonTree(node)
}
/**
* Format an unknown value.
*
* @param {unknown} value
* Thing to format.
* @returns {string}
* Formatted thing.
*/
function inspectNonTree(value) {
return JSON.stringify(value)
}
/**
* Format a list of nodes.
*
* @param {Array<unknown>} 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<string>} */
const result = []
let index = -1
while (++index < nodes.length) {
result.push(
state.dim(
(index < nodes.length - 1 ? '├' : '└') +
'─' +
String(index).padEnd(size)
) +
' ' +
indent(
inspectValue(nodes[index], state),
(index < nodes.length - 1 ? state.dim('│') : ' ') +
' '.repeat(size + 2),
true
)
)
}
return result.join('\n')
}
/**
* Format the fields in a node.
*
* @param {Record<string, unknown>} 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<string>} */
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.
// <https://github.com/syntax-tree/unist>
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 (
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 +
state.dim(':') +
(/\s/.test(formatted.charAt(0)) ? '' : ' ') +
formatted
)
}
return indent(
result.join('\n'),
(isArrayUnknown(object.children) && object.children.length > 0
? state.dim('│')
: ' ') + ' '
)
}
/**
* 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<string, unknown>} */ (
/** @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')
}
/**
* 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 = [state.bold(node.type)]
// Cast as record to allow indexing.
const map = /** @type {Record<string, unknown>} */ (
/** @type {unknown} */ (node)
)
const kind = map.tagName || map.name
const position = state.showPositions ? stringifyPosition(node.position) : ''
if (typeof kind === 'string') {
result.push('<', kind, '>')
}
if (isArrayUnknown(map.children)) {
result.push(
state.dim('['),
state.yellow(String(map.children.length)),
state.dim(']')
)
} else if (typeof map.value === 'string') {
result.push(' ', state.green(inspectNonTree(map.value)))
}
if (position) {
result.push(' ', state.dim('('), position, state.dim(')'))
}
return result.join('')
}
/**
* Indent a value.
*
* @param {string} value
* Value to indent.
* @param {string} indentation
* Indent to use.
* @param {boolean | undefined} [ignoreFirst=false]
* Whether to ignore indenting the first line (default: `false`).
* @returns {string}
* Indented `value`.
*/
function indent(value, indentation, ignoreFirst) {
if (!value) return value
const lines = value.split('\n')
let index = ignoreFirst ? 0 : -1
while (++index < lines.length) {
lines[index] = indentation + lines[index]
}
return lines.join('\n')
}
/**
* Serialize a position.
*
* @param {unknown} [value]
* Position to serialize.
* @returns {string}
* Serialized position.
*/
function stringifyPosition(value) {
/** @type {Array<string>} */
const result = []
/** @type {Array<string>} */
const positions = []
/** @type {Array<string>} */
const offsets = []
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('-'))
return result.join(', ')
/**
* Add a point.
*
* @param {unknown} value
* Point to add.
*/
function point(value) {
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 && typeof value.offset === 'number') {
offsets.push(String(value.offset || 0))
}
}
}
}
/**
* Factory to wrap values in ANSI colours.
*
* @param {number} open
* Opening color code.
* @param {number} close
* 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<unknown>}
*/
function isArrayUnknown(node) {
return Array.isArray(node)
}
/**
* @template T
* @param {T} value
* @returns {T}
*/
function identity(value) {
return value
}