UNPKG

8.95 kBJavaScriptView Raw
1/**
2 * @typedef {import('unist').Node} Node
3 */
4
5/**
6 * @typedef Options
7 * Configuration.
8 * @property {boolean | null | undefined} [showPositions=true]
9 * Whether to include positional information (default: `true`).
10 *
11 * @typedef State
12 * Info passed around.
13 * @property {boolean} showPositions
14 * Whether to include positional information.
15 */
16
17import {color} from 'unist-util-inspect/do-not-use-conditional-color'
18
19/**
20 * Inspect a node, with color in Node, without color in browsers.
21 *
22 * @param tree
23 * Tree to inspect.
24 * @param options
25 * Configuration (optional).
26 * @returns
27 * Pretty printed `tree`.
28 */
29/* c8 ignore next */
30export const inspect = color ? inspectColor : inspectNoColor
31
32const own = {}.hasOwnProperty
33
34const bold = ansiColor(1, 22)
35const dim = ansiColor(2, 22)
36const yellow = ansiColor(33, 39)
37const green = ansiColor(32, 39)
38
39// ANSI color regex.
40/* eslint-disable no-control-regex */
41const colorExpression =
42 /(?:(?:\u001B\[)|\u009B)(?:\d{1,3})?(?:(?:;\d{0,3})*)?[A-M|f-m]|\u001B[A-M]/g
43/* eslint-enable no-control-regex */
44
45/**
46 * Inspect a node, without color.
47 *
48 * @param {unknown} tree
49 * Tree to inspect.
50 * @param {Options | null | undefined} [options]
51 * Configuration.
52 * @returns {string}
53 * Pretty printed `tree`.
54 */
55export function inspectNoColor(tree, options) {
56 return inspectColor(tree, options).replace(colorExpression, '')
57}
58
59/**
60 * Inspects a node, using color.
61 *
62 * @param {unknown} tree
63 * Tree to inspect.
64 * @param {Options | null | undefined} [options]
65 * Configuration (optional).
66 * @returns {string}
67 * Pretty printed `tree`.
68 */
69export function inspectColor(tree, options) {
70 /** @type {State} */
71 const state = {
72 showPositions:
73 !options ||
74 options.showPositions === null ||
75 options.showPositions === undefined
76 ? true
77 : options.showPositions
78 }
79
80 return inspectValue(tree, state)
81}
82
83/**
84 * Format any value.
85 *
86 * @param {unknown} node
87 * Thing to format.
88 * @param {State} state
89 * Info passed around.
90 * @returns {string}
91 * Formatted thing.
92 */
93function inspectValue(node, state) {
94 if (isArrayUnknown(node)) {
95 return inspectNodes(node, state)
96 }
97
98 if (isNode(node)) {
99 return inspectTree(node, state)
100 }
101
102 return inspectNonTree(node)
103}
104
105/**
106 * Format an unknown value.
107 *
108 * @param {unknown} value
109 * Thing to format.
110 * @returns {string}
111 * Formatted thing.
112 */
113function inspectNonTree(value) {
114 return JSON.stringify(value)
115}
116
117/**
118 * Format a list of nodes.
119 *
120 * @param {Array<unknown>} nodes
121 * Nodes to format.
122 * @param {State} state
123 * Info passed around.
124 * @returns {string}
125 * Formatted nodes.
126 */
127function inspectNodes(nodes, state) {
128 const size = String(nodes.length - 1).length
129 /** @type {Array<string>} */
130 const result = []
131 let index = -1
132
133 while (++index < nodes.length) {
134 result.push(
135 dim(
136 (index < nodes.length - 1 ? '├' : '└') +
137 '─' +
138 String(index).padEnd(size)
139 ) +
140 ' ' +
141 indent(
142 inspectValue(nodes[index], state),
143 (index < nodes.length - 1 ? dim('│') : ' ') + ' '.repeat(size + 2),
144 true
145 )
146 )
147 }
148
149 return result.join('\n')
150}
151
152/**
153 * Format the fields in a node.
154 *
155 * @param {Record<string, unknown>} object
156 * Node to format.
157 * @param {State} state
158 * Info passed around.
159 * @returns {string}
160 * Formatted node.
161 */
162// eslint-disable-next-line complexity
163function inspectFields(object, state) {
164 /** @type {Array<string>} */
165 const result = []
166 /** @type {string} */
167 let key
168
169 for (key in object) {
170 /* c8 ignore next 1 */
171 if (!own.call(object, key)) continue
172
173 const value = object[key]
174 /** @type {string} */
175 let formatted
176
177 if (
178 value === undefined ||
179 // Standard keys defined by unist that we format differently.
180 // <https://github.com/syntax-tree/unist>
181 key === 'type' ||
182 key === 'value' ||
183 key === 'children' ||
184 key === 'position' ||
185 // Ignore `name` (from xast) and `tagName` (from `hast`) when string.
186 (typeof value === 'string' && (key === 'name' || key === 'tagName'))
187 ) {
188 continue
189 }
190
191 // A single node.
192 if (
193 isNode(value) &&
194 key !== 'data' &&
195 key !== 'attributes' &&
196 key !== 'properties'
197 ) {
198 formatted = inspectTree(value, state)
199 }
200 // A list of nodes.
201 else if (value && isArrayUnknown(value) && isNode(value[0])) {
202 formatted = '\n' + inspectNodes(value, state)
203 } else {
204 formatted = inspectNonTree(value)
205 }
206
207 result.push(
208 key + dim(':') + (/\s/.test(formatted.charAt(0)) ? '' : ' ') + formatted
209 )
210 }
211
212 return indent(
213 result.join('\n'),
214 (isArrayUnknown(object.children) && object.children.length > 0
215 ? dim('│')
216 : ' ') + ' '
217 )
218}
219
220/**
221 * Format a node, its fields, and its children.
222 *
223 * @param {Node} node
224 * Node to format.
225 * @param {State} state
226 * Info passed around.
227 * @returns {string}
228 * Formatted node.
229 */
230function inspectTree(node, state) {
231 const result = [formatNode(node, state)]
232 // Cast as record to allow indexing.
233 const map = /** @type {Record<string, unknown>} */ (
234 /** @type {unknown} */ (node)
235 )
236 const fields = inspectFields(map, state)
237 const content = isArrayUnknown(map.children)
238 ? inspectNodes(map.children, state)
239 : ''
240 if (fields) result.push(fields)
241 if (content) result.push(content)
242 return result.join('\n')
243}
244
245/**
246 * Format a node itself.
247 *
248 * @param {Node} node
249 * Node to format.
250 * @param {State} state
251 * Info passed around.
252 * @returns {string}
253 * Formatted node.
254 */
255function formatNode(node, state) {
256 const result = [bold(node.type)]
257 // Cast as record to allow indexing.
258 const map = /** @type {Record<string, unknown>} */ (
259 /** @type {unknown} */ (node)
260 )
261 const kind = map.tagName || map.name
262 const position = state.showPositions ? stringifyPosition(node.position) : ''
263
264 if (typeof kind === 'string') {
265 result.push('<', kind, '>')
266 }
267
268 if (isArrayUnknown(map.children)) {
269 result.push(dim('['), yellow(String(map.children.length)), dim(']'))
270 } else if (typeof map.value === 'string') {
271 result.push(' ', green(inspectNonTree(map.value)))
272 }
273
274 if (position) {
275 result.push(' ', dim('('), position, dim(')'))
276 }
277
278 return result.join('')
279}
280
281/**
282 * Indent a value.
283 *
284 * @param {string} value
285 * Value to indent.
286 * @param {string} indentation
287 * Indent to use.
288 * @param {boolean | undefined} [ignoreFirst=false]
289 * Whether to ignore indenting the first line (default: `false`).
290 * @returns {string}
291 * Indented `value`.
292 */
293function indent(value, indentation, ignoreFirst) {
294 if (!value) return value
295
296 const lines = value.split('\n')
297 let index = ignoreFirst ? 0 : -1
298
299 while (++index < lines.length) {
300 lines[index] = indentation + lines[index]
301 }
302
303 return lines.join('\n')
304}
305
306/**
307 * Serialize a position.
308 *
309 * @param {unknown} [value]
310 * Position to serialize.
311 * @returns {string}
312 * Serialized position.
313 */
314function stringifyPosition(value) {
315 /** @type {Array<string>} */
316 const result = []
317 /** @type {Array<string>} */
318 const positions = []
319 /** @type {Array<string>} */
320 const offsets = []
321
322 if (value && typeof value === 'object') {
323 point('start' in value ? value.start : undefined)
324 point('end' in value ? value.end : undefined)
325 }
326
327 if (positions.length > 0) result.push(positions.join('-'))
328 if (offsets.length > 0) result.push(offsets.join('-'))
329
330 return result.join(', ')
331
332 /**
333 * Add a point.
334 *
335 * @param {unknown} value
336 * Point to add.
337 */
338 function point(value) {
339 if (value && typeof value === 'object') {
340 const line =
341 'line' in value && typeof value.line === 'number' ? value.line : 1
342 const column =
343 'column' in value && typeof value.column === 'number' ? value.column : 1
344
345 positions.push(line + ':' + column)
346
347 if ('offset' in value && typeof value.offset === 'number') {
348 offsets.push(String(value.offset || 0))
349 }
350 }
351 }
352}
353
354/**
355 * Factory to wrap values in ANSI colours.
356 *
357 * @param {number} open
358 * Opening color code.
359 * @param {number} close
360 * Closing color code.
361 * @returns {(value: string) => string}
362 * Color `value`.
363 */
364function ansiColor(open, close) {
365 return color
366
367 /**
368 * Color `value`.
369 *
370 * @param {string} value
371 * Value to color.
372 * @returns {string}
373 * Colored `value`.
374 */
375 function color(value) {
376 return '\u001B[' + open + 'm' + value + '\u001B[' + close + 'm'
377 }
378}
379
380/**
381 * @param {unknown} value
382 * @returns {value is Node}
383 */
384function isNode(value) {
385 return Boolean(
386 value &&
387 typeof value === 'object' &&
388 'type' in value &&
389 typeof value.type === 'string'
390 )
391}
392
393/**
394 * @param {unknown} node
395 * @returns {node is Array<unknown>}
396 */
397function isArrayUnknown(node) {
398 return Array.isArray(node)
399}