Create a plain text representation of an HTML document's skeleton. Great for logging.
1let FLARE = true
2
3const selfClosingTags = [
4 'area',
5 'base',
6 'br',
7 'col',
8 'command',
9 'embed',
10 'hr',
11 'img',
12 'input',
13 'keygen',
14 'link',
15 'meta',
16 'param',
17 'source',
18 'track',
19 'wbr',
20]
21
22export default function report(title, str, opts = {}) {
23 const { flare = true } = opts
24 FLARE = flare
25
26 const bones = selectorSkelly(str)
27 const lines = bones.split('\n')
28 const longestLine = lines.reduce((a, b) => (a.length > b.length ? a : b))
29 const titleLine = `${c.pink('┌─')} ${c.b(c.blue(title))}${flare ? ' 🩻' : ''} ${c.pink('─○')}`
30 const lastLine = c.pink(`└${'─'.repeat(longestLine.length + 3)}●`)
31
32 return `${titleLine}
33${lines.map((line) => `${c.pink('│')} ${line}`).join('\n')}
34${lastLine}`
35}
36
37export function selectorSkelly(h) {
38 const html = h.replace(/<([^>]+)\/>/g, '<$1/>')
39
40 const tags = html.match(/<[^>]+>/g) || []
41 let indentLevel = 0
42 let formattedHtml = ''
43
44 for (const tag of tags) {
45 const isClosing = tag.startsWith('</')
46 const tagMatch = tag.match(/^<\/?([\w-]+)/)
47 const idMatch = tag.match(/id="([^"]+)"/)
48 const classMatch = tag.match(/class="([^"]+)"/)
49 const isSelfClosing =
50 tag.endsWith('/>') || selfClosingTags.some((selfTag) => tag.startsWith(`<${selfTag}`))
51
52 if (!isClosing && tagMatch) {
53 const tagName = tagMatch[1]
54 const idSelector = idMatch ? c.red(`#${idMatch[1]}`) : ''
55 const classSelector = classMatch ? c.orange(`.${classMatch[1].replace(/\s+/g, '.')}`) : ''
56 const selector = `${c.dim('<')}${tagName}${idSelector}${classSelector}${c.dim('>')}`
57
58 formattedHtml += `${' '.repeat(indentLevel)}${selector}\n`
59
60 if (!isSelfClosing) indentLevel++
61 } else if (isClosing && tagMatch) {
62 indentLevel = Math.max(indentLevel - 1, 0)
63 }
64 }
65
66 return formattedHtml.trim()
67}
68
69export function tagsSkelly(h, opts = {}) {
70 const { attrs = false } = opts
71 const html = h.replace(/<([^>]+)\/>/g, '<$1/>')
72
73 const tags = html.match(/<[^>]+>/g) || []
74 let indentLevel = 0
75 let formattedHtml = ''
76
77 for (const tag of tags) {
78 const isClosing = tag.startsWith('</')
79 const isSelfClosing =
80 tag.endsWith('/>') || selfClosingTags.some((selfTag) => tag.startsWith(`<${selfTag}`))
81 const tagMatch = tag.match(/^<\/?([\w-]+)/)
82 const insert = attrs ? tag : tag.replace(/ .+/, '>')
83
84 if (!isClosing) {
85 if (isSelfClosing) {
86 formattedHtml += `${' '.repeat(indentLevel)}${insert}\n`
87 } else {
88 formattedHtml += `${' '.repeat(indentLevel)}${insert}\n`
89 indentLevel++
90 }
91 } else if (tagMatch && isClosing) {
92 indentLevel = Math.max(indentLevel - 1, 0)
93 }
94 }
95
96 return formattedHtml
97}
98
99export const c = {
100 b(str) {
101 return FLARE ? `\x1b[1m${str}\x1b[22m` : str
102 },
103 color(str, code) {
104 return FLARE ? `\x1b[${code}m${str}\x1b[0m` : str
105 },
106 dim(str) {
107 return this.color(str, 2)
108 },
109 red(str) {
110 return this.color(str, 31)
111 },
112 orange(str) {
113 return this.color(str, 33)
114 },
115 blue(str) {
116 return this.color(str, 34)
117 },
118 pink(str) {
119 return this.color(str, 35)
120 },
121}