markdown-magic 2.6.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +6 -10
  2. package/cli.js +5 -82
  3. package/lib/block-parser-js.test.js +179 -0
  4. package/lib/block-parser.js +389 -0
  5. package/lib/{utils/new-parser.test.js → block-parser.test.js} +168 -50
  6. package/lib/cli.js +234 -0
  7. package/lib/cli.test.js +409 -0
  8. package/lib/defaults.js +12 -0
  9. package/lib/index.js +319 -184
  10. package/lib/index.test.js +11 -0
  11. package/lib/options-parser.js +498 -0
  12. package/lib/options-parser.test.js +1237 -0
  13. package/lib/process-contents.js +330 -0
  14. package/lib/process-file.js +34 -0
  15. package/lib/transforms/code.js +67 -22
  16. package/lib/transforms/file.js +13 -10
  17. package/lib/transforms/remote.js +9 -6
  18. package/lib/transforms/toc.js +136 -64
  19. package/lib/transforms/wordCount.js +5 -0
  20. package/lib/utils/fs.js +340 -0
  21. package/lib/utils/fs.test.js +268 -0
  22. package/lib/utils/html-to-json/compat.js +42 -0
  23. package/lib/utils/html-to-json/format.js +64 -0
  24. package/lib/utils/html-to-json/index.js +37 -0
  25. package/lib/utils/html-to-json/lexer.js +345 -0
  26. package/lib/utils/html-to-json/parser.js +146 -0
  27. package/lib/utils/html-to-json/stringify.js +37 -0
  28. package/lib/utils/html-to-json/tags.js +171 -0
  29. package/lib/utils/index.js +19 -0
  30. package/{cli-utils.js → lib/utils/load-config.js} +2 -6
  31. package/lib/utils/logs.js +89 -0
  32. package/lib/utils/md/filters.js +20 -0
  33. package/lib/utils/md/find-code-blocks.js +80 -0
  34. package/lib/utils/md/find-date.js +32 -0
  35. package/lib/utils/md/find-frontmatter.js +94 -0
  36. package/lib/utils/md/find-frontmatter.test.js +17 -0
  37. package/lib/utils/md/find-html-tags.js +105 -0
  38. package/lib/utils/md/find-images.js +102 -0
  39. package/lib/utils/md/find-links.js +202 -0
  40. package/lib/utils/md/find-unmatched-html-tags.js +33 -0
  41. package/lib/utils/md/fixtures/2022-01-22-date-in-filename.md +14 -0
  42. package/lib/utils/md/fixtures/file-with-frontmatter.md +32 -0
  43. package/lib/utils/md/fixtures/file-with-links.md +143 -0
  44. package/lib/utils/md/md.test.js +37 -0
  45. package/lib/utils/md/parse.js +122 -0
  46. package/lib/utils/md/utils.js +19 -0
  47. package/lib/utils/regex-timeout.js +83 -0
  48. package/lib/utils/regex.js +38 -5
  49. package/lib/utils/remoteRequest.js +54 -0
  50. package/lib/utils/syntax.js +79 -0
  51. package/lib/utils/text.js +260 -0
  52. package/lib/utils/text.test.js +311 -0
  53. package/package.json +25 -25
  54. package/index.js +0 -46
  55. package/lib/processFile.js +0 -154
  56. package/lib/transforms/index.js +0 -114
  57. package/lib/updateContents.js +0 -125
  58. package/lib/utils/_md.test.js +0 -63
  59. package/lib/utils/new-parser.js +0 -412
  60. package/lib/utils/weird-parse.js +0 -230
  61. package/lib/utils/weird-parse.test.js +0 -217
@@ -0,0 +1,345 @@
1
+ const {
2
+ startsWith,
3
+ endsWith,
4
+ stringIncludes,
5
+ arrayIncludes
6
+ } = require('./compat')
7
+
8
+ function feedPosition(position, str, len) {
9
+ const start = position.index
10
+ const end = position.index = start + len
11
+ for (let i = start; i < end; i++) {
12
+ const char = str.charAt(i)
13
+ if (char === '\n') {
14
+ position.line++
15
+ position.column = 0
16
+ } else {
17
+ position.column++
18
+ }
19
+ }
20
+ }
21
+
22
+ function jumpPosition (position, str, end) {
23
+ const len = end - position.index
24
+ return feedPosition(position, str, len)
25
+ }
26
+
27
+ function makeInitialPosition () {
28
+ return {
29
+ index: 0,
30
+ column: 0,
31
+ line: 1
32
+ }
33
+ }
34
+
35
+ function copyPositionStart(position) {
36
+ return {
37
+ index: position.index,
38
+ line: position.line,
39
+ column: position.column + 1
40
+ }
41
+ }
42
+
43
+ function copyPositionEnd(position) {
44
+ return {
45
+ index: position.index + 1,
46
+ line: position.line,
47
+ column: position.column + 1
48
+ }
49
+ }
50
+
51
+ // Old offset was index/column was 1 off in some cases
52
+ function copyPosition(position) {
53
+ return {
54
+ index: position.index,
55
+ line: position.line,
56
+ column: position.column
57
+ }
58
+ }
59
+
60
+ function lexer (str, options) {
61
+ const state = {
62
+ str,
63
+ options,
64
+ position: makeInitialPosition(),
65
+ tokens: []
66
+ }
67
+ lex(state)
68
+ return state.tokens
69
+ }
70
+
71
+ function lex (state) {
72
+ const {str, options: {childlessTags}} = state
73
+ const len = str?.length || 0
74
+ while (state.position.index < len) {
75
+ const start = state.position.index
76
+ lexText(state)
77
+ if (state.position.index === start) {
78
+ const isComment = startsWith(str, '!--', start + 1)
79
+ if (isComment) {
80
+ lexComment(state)
81
+ } else {
82
+ const tagName = lexTag(state)
83
+ const safeTag = tagName.toLowerCase()
84
+ if (arrayIncludes(childlessTags, safeTag)) {
85
+ lexSkipTag(tagName, state)
86
+ }
87
+ }
88
+ }
89
+ }
90
+ }
91
+
92
+ const alphanumeric = /[A-Za-z0-9]/
93
+ function findTextEnd (str, index) {
94
+ while (true) {
95
+ const textEnd = str.indexOf('<', index)
96
+ if (textEnd === -1) {
97
+ return textEnd
98
+ }
99
+ const char = str.charAt(textEnd + 1)
100
+ if (char === '/' || char === '!' || alphanumeric.test(char)) {
101
+ return textEnd
102
+ }
103
+ index = textEnd + 1
104
+ }
105
+ }
106
+
107
+ function lexText (state) {
108
+ const type = 'text'
109
+ const {str, position} = state
110
+ let textEnd = findTextEnd(str, position.index)
111
+ if (textEnd === position.index) return
112
+ if (textEnd === -1) {
113
+ textEnd = str.length
114
+ }
115
+
116
+ const start = copyPositionStart(position)
117
+ const content = str.slice(position.index, textEnd)
118
+ jumpPosition(position, str, textEnd)
119
+ const end = copyPositionEnd(position)
120
+ state.tokens.push({type, content, position: {start, end}})
121
+ }
122
+
123
+ function lexComment (state) {
124
+ const {str, position} = state
125
+ const start = copyPositionStart(position)
126
+ feedPosition(position, str, 4) // "<!--".length
127
+ let contentEnd = str.indexOf('-->', position.index)
128
+ let commentEnd = contentEnd + 3 // "-->".length
129
+ if (contentEnd === -1) {
130
+ contentEnd = commentEnd = str.length
131
+ }
132
+
133
+ const content = str.slice(position.index, contentEnd)
134
+ jumpPosition(position, str, commentEnd)
135
+ state.tokens.push({
136
+ type: 'comment',
137
+ content,
138
+ position: {
139
+ start,
140
+ end: copyPositionEnd(position)
141
+ }
142
+ })
143
+ }
144
+
145
+ function lexTag (state) {
146
+ const {str, position} = state
147
+ {
148
+ const secondChar = str.charAt(position.index + 1)
149
+ const close = secondChar === '/'
150
+ const start = copyPositionStart(position, 0)
151
+ feedPosition(position, str, close ? 2 : 1)
152
+ state.tokens.push({type: 'tag-start', close, position: {start}})
153
+ }
154
+ const tagName = lexTagName(state)
155
+ lexTagAttributes(state)
156
+ {
157
+ const firstChar = str.charAt(position.index)
158
+ const close = firstChar === '/'
159
+ feedPosition(position, str, close ? 2 : 1)
160
+ const end = copyPositionEnd(position)
161
+ state.tokens.push({type: 'tag-end', close, position: {end}})
162
+ }
163
+ return tagName
164
+ }
165
+
166
+ // See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#special-white-space
167
+ const whitespace = /\s/
168
+ function isWhitespaceChar (char) {
169
+ return whitespace.test(char)
170
+ }
171
+
172
+ function lexTagName (state) {
173
+ const {str, position} = state
174
+ const len = str.length
175
+ let start = position.index
176
+ while (start < len) {
177
+ const char = str.charAt(start)
178
+ const isTagChar = !(isWhitespaceChar(char) || char === '/' || char === '>')
179
+ if (isTagChar) break
180
+ start++
181
+ }
182
+
183
+ let end = start + 1
184
+ while (end < len) {
185
+ const char = str.charAt(end)
186
+ const isTagChar = !(isWhitespaceChar(char) || char === '/' || char === '>')
187
+ if (!isTagChar) break
188
+ end++
189
+ }
190
+
191
+ jumpPosition(position, str, end)
192
+ const tagName = str.slice(start, end)
193
+ state.tokens.push({
194
+ type: 'tag',
195
+ content: tagName
196
+ })
197
+ return tagName
198
+ }
199
+
200
+ function lexTagAttributes (state) {
201
+ const {str, position, tokens} = state
202
+ let cursor = position.index
203
+ let quote = null // null, single-, or double-quote
204
+ let wordBegin = cursor // index of word start
205
+ const words = [] // "key", "key=value", "key='value'", etc
206
+ const len = str.length
207
+ while (cursor < len) {
208
+ const char = str.charAt(cursor)
209
+ if (quote) {
210
+ const isQuoteEnd = char === quote
211
+ if (isQuoteEnd) {
212
+ quote = null
213
+ }
214
+ cursor++
215
+ continue
216
+ }
217
+
218
+ const isTagEnd = char === '/' || char === '>'
219
+ if (isTagEnd) {
220
+ if (cursor !== wordBegin) {
221
+ words.push(str.slice(wordBegin, cursor))
222
+ }
223
+ break
224
+ }
225
+
226
+ const isWordEnd = isWhitespaceChar(char)
227
+ if (isWordEnd) {
228
+ if (cursor !== wordBegin) {
229
+ words.push(str.slice(wordBegin, cursor))
230
+ }
231
+ wordBegin = cursor + 1
232
+ cursor++
233
+ continue
234
+ }
235
+
236
+ const isQuoteStart = char === '\'' || char === '"'
237
+ if (isQuoteStart) {
238
+ quote = char
239
+ cursor++
240
+ continue
241
+ }
242
+
243
+ cursor++
244
+ }
245
+ jumpPosition(position, str, cursor)
246
+
247
+ const wLen = words.length
248
+ const type = 'attribute'
249
+ for (let i = 0; i < wLen; i++) {
250
+ const word = words[i]
251
+ const isNotPair = word.indexOf('=') === -1
252
+ if (isNotPair) {
253
+ const secondWord = words[i + 1]
254
+ if (secondWord && startsWith(secondWord, '=')) {
255
+ if (secondWord.length > 1) {
256
+ const newWord = word + secondWord
257
+ tokens.push({type, content: newWord})
258
+ i += 1
259
+ continue
260
+ }
261
+ const thirdWord = words[i + 2]
262
+ i += 1
263
+ if (thirdWord) {
264
+ const newWord = word + '=' + thirdWord
265
+ tokens.push({type, content: newWord})
266
+ i += 1
267
+ continue
268
+ }
269
+ }
270
+ }
271
+ if (endsWith(word, '=')) {
272
+ const secondWord = words[i + 1]
273
+ if (secondWord && !stringIncludes(secondWord, '=')) {
274
+ const newWord = word + secondWord
275
+ tokens.push({type, content: newWord})
276
+ i += 1
277
+ continue
278
+ }
279
+
280
+ const newWord = word.slice(0, -1)
281
+ tokens.push({type, content: newWord})
282
+ continue
283
+ }
284
+
285
+ tokens.push({type, content: word})
286
+ }
287
+ }
288
+
289
+ const push = [].push
290
+
291
+ function lexSkipTag (tagName, state) {
292
+ const {str, position, tokens} = state
293
+ const safeTagName = tagName.toLowerCase()
294
+ const len = str.length
295
+ let index = position.index
296
+ while (index < len) {
297
+ const nextTag = str.indexOf('</', index)
298
+ if (nextTag === -1) {
299
+ lexText(state)
300
+ break
301
+ }
302
+
303
+ const tagStartPosition = copyPositionStart(position)
304
+ jumpPosition(tagStartPosition, str, nextTag)
305
+ const tagState = {str, position: tagStartPosition, tokens: []}
306
+ const name = lexTag(tagState)
307
+ if (safeTagName !== name.toLowerCase()) {
308
+ index = tagState.position.index
309
+ continue
310
+ }
311
+
312
+ if (nextTag !== position.index) {
313
+ const textStart = copyPositionStart(position)
314
+ jumpPosition(position, str, nextTag)
315
+ tokens.push({
316
+ type: 'text',
317
+ content: str.slice(textStart.index, nextTag),
318
+ position: {
319
+ start: textStart,
320
+ end: copyPositionEnd(position)
321
+ }
322
+ })
323
+ }
324
+
325
+ push.apply(tokens, tagState.tokens)
326
+ jumpPosition(position, str, tagState.position.index)
327
+ break
328
+ }
329
+ }
330
+
331
+ module.exports = {
332
+ feedPosition,
333
+ jumpPosition,
334
+ makeInitialPosition,
335
+ copyPosition,
336
+ lexer,
337
+ lex,
338
+ findTextEnd,
339
+ lexText,
340
+ lexTag,
341
+ isWhitespaceChar,
342
+ lexTagName,
343
+ lexTagAttributes,
344
+ lexSkipTag,
345
+ }
@@ -0,0 +1,146 @@
1
+ const { optionsParse } = require('../../options-parser')
2
+ const { arrayIncludes }= require('./compat')
3
+
4
+ function parser (tokens, options) {
5
+ const root = {tagName: null, children: []}
6
+ const state = {tokens, options, cursor: 0, stack: [root]}
7
+ parse(state)
8
+ return root.children
9
+ }
10
+
11
+ function hasTerminalParent (tagName, stack, terminals) {
12
+ const tagParents = terminals[tagName]
13
+ if (tagParents) {
14
+ let currentIndex = stack.length - 1
15
+ while (currentIndex >= 0) {
16
+ const parentTagName = stack[currentIndex].tagName
17
+ if (parentTagName === tagName) {
18
+ break
19
+ }
20
+ if (arrayIncludes(tagParents, parentTagName)) {
21
+ return true
22
+ }
23
+ currentIndex--
24
+ }
25
+ }
26
+ return false
27
+ }
28
+
29
+ function rewindStack (stack, newLength, childrenEndPosition, endPosition) {
30
+ stack[newLength].position.end = endPosition
31
+ for (let i = newLength + 1, len = stack.length; i < len; i++) {
32
+ stack[i].position.end = childrenEndPosition
33
+ }
34
+ stack.splice(newLength)
35
+ }
36
+
37
+ function parse (state) {
38
+ const {tokens, options} = state
39
+ let {stack} = state
40
+ let nodes = stack[stack.length - 1].children
41
+ const len = tokens.length
42
+ let {cursor} = state
43
+ while (cursor < len) {
44
+ const token = tokens[cursor]
45
+ if (token.type !== 'tag-start') {
46
+ nodes.push(token)
47
+ cursor++
48
+ continue
49
+ }
50
+
51
+ const tagToken = tokens[++cursor]
52
+ cursor++
53
+ const tagName = tagToken.content.toLowerCase()
54
+ if (token.close) {
55
+ let index = stack.length
56
+ let shouldRewind = false
57
+ while (--index > -1) {
58
+ if (stack[index].tagName === tagName) {
59
+ shouldRewind = true
60
+ break
61
+ }
62
+ }
63
+ while (cursor < len) {
64
+ const endToken = tokens[cursor]
65
+ if (endToken.type !== 'tag-end') break
66
+ cursor++
67
+ }
68
+ if (shouldRewind) {
69
+ rewindStack(stack, index, token.position.start, tokens[cursor - 1].position.end)
70
+ break
71
+ } else {
72
+ continue
73
+ }
74
+ }
75
+
76
+ const isClosingTag = arrayIncludes(options.closingTags, tagName)
77
+ let shouldRewindToAutoClose = isClosingTag
78
+ if (shouldRewindToAutoClose) {
79
+ const { closingTagAncestorBreakers: terminals } = options
80
+ shouldRewindToAutoClose = !hasTerminalParent(tagName, stack, terminals)
81
+ }
82
+
83
+ if (shouldRewindToAutoClose) {
84
+ // rewind the stack to just above the previous
85
+ // closing tag of the same name
86
+ let currentIndex = stack.length - 1
87
+ while (currentIndex > 0) {
88
+ if (tagName === stack[currentIndex].tagName) {
89
+ rewindStack(stack, currentIndex, token.position.start, token.position.start)
90
+ const previousIndex = currentIndex - 1
91
+ nodes = stack[previousIndex].children
92
+ break
93
+ }
94
+ currentIndex = currentIndex - 1
95
+ }
96
+ }
97
+
98
+ // let attributes = []
99
+ let propsRaw = ''
100
+ let attrToken
101
+ while (cursor < len) {
102
+ attrToken = tokens[cursor]
103
+ if (attrToken.type === 'tag-end') break
104
+ // attributes.push(attrToken.content)
105
+ propsRaw+= ((propsRaw !== '') ? ' ' : '') + attrToken.content
106
+ cursor++
107
+ }
108
+
109
+ cursor++
110
+ const children = []
111
+ const position = {
112
+ start: token.position.start,
113
+ end: attrToken.position.end
114
+ }
115
+ const elementNode = {
116
+ type: 'element',
117
+ tagName: tagToken.content,
118
+ // attributes,
119
+ props: (propsRaw) ? optionsParse(propsRaw) : {},
120
+ propsRaw,
121
+ children,
122
+ position
123
+ }
124
+ nodes.push(elementNode)
125
+
126
+ const hasChildren = !(attrToken.close || arrayIncludes(options.voidTags, tagName))
127
+ if (hasChildren) {
128
+ const size = stack.push({tagName, children, position})
129
+ const innerState = {tokens, options, cursor, stack}
130
+ parse(innerState)
131
+ cursor = innerState.cursor
132
+ const rewoundInElement = stack.length === size
133
+ if (rewoundInElement) {
134
+ elementNode.position.end = tokens[cursor - 1].position.end
135
+ }
136
+ }
137
+ }
138
+ state.cursor = cursor
139
+ }
140
+
141
+ module.exports = {
142
+ parser,
143
+ hasTerminalParent,
144
+ rewindStack,
145
+ parse,
146
+ }
@@ -0,0 +1,37 @@
1
+ const { arrayIncludes }= require('./compat')
2
+
3
+ // Old
4
+ function formatAttributes(attributes) {
5
+ return attributes.reduce((attrs, attribute) => {
6
+ const {key, value} = attribute
7
+ if (value === null) {
8
+ return `${attrs} ${key}`
9
+ }
10
+ const quoteEscape = value.indexOf('\'') !== -1
11
+ const quote = quoteEscape ? '"' : '\''
12
+ return `${attrs} ${key}=${quote}${value}${quote}`
13
+ }, '')
14
+ }
15
+
16
+ function toHTML(tree, options) {
17
+ return tree.map((node) => {
18
+ if (node.type === 'text') {
19
+ return node.content
20
+ }
21
+ if (node.type === 'comment') {
22
+ return `<!--${node.content}-->`
23
+ }
24
+ const {tagName, propsRaw, children} = node
25
+ // @TODO update prop parsing to keep new lines
26
+ const propsString = (propsRaw) ? ` ${propsRaw}` : ''
27
+ const isSelfClosing = arrayIncludes(options.voidTags, tagName.toLowerCase())
28
+ return isSelfClosing
29
+ ? `<${tagName}${propsString}>`
30
+ : `<${tagName}${propsString}>${toHTML(children, options)}</${tagName}>`
31
+ }).join('')
32
+ }
33
+
34
+ module.exports = {
35
+ formatAttributes,
36
+ toHTML
37
+ }
@@ -0,0 +1,171 @@
1
+ /*
2
+ Tags which contain arbitrary non-parsed content
3
+ For example: <script> JavaScript should not be parsed
4
+ */
5
+ const childlessTags = ['style', 'script', 'template']
6
+
7
+ /*
8
+ Tags which auto-close because they cannot be nested
9
+ For example: <p>Outer<p>Inner is <p>Outer</p><p>Inner</p>
10
+ */
11
+ const closingTags = [
12
+ 'html', 'head', 'body', 'p', 'dt', 'dd', 'li', 'option',
13
+ 'thead', 'th', 'tbody', 'tr', 'td', 'tfoot', 'colgroup'
14
+ ]
15
+
16
+ /*
17
+ Closing tags which have ancestor tags which
18
+ may exist within them which prevent the
19
+ closing tag from auto-closing.
20
+ For example: in <li><ul><li></ul></li>,
21
+ the top-level <li> should not auto-close.
22
+ */
23
+ const closingTagAncestorBreakers = {
24
+ li: ['ul', 'ol', 'menu'],
25
+ dt: ['dl'],
26
+ dd: ['dl'],
27
+ tbody: ['table'],
28
+ thead: ['table'],
29
+ tfoot: ['table'],
30
+ tr: ['table'],
31
+ td: ['table']
32
+ }
33
+
34
+ /*
35
+ Tags which do not need the closing tag
36
+ For example: <img> does not need </img>
37
+ */
38
+ const voidTags = [
39
+ '!doctype', 'area', 'base', 'br', 'col', 'command',
40
+ 'embed', 'hr', 'img', 'input', 'keygen', 'link',
41
+ 'meta', 'param', 'source', 'track', 'wbr'
42
+ ]
43
+
44
+ // https://github.com/sindresorhus/html-tags/blob/main/html-tags.json
45
+ const htmlTags = [
46
+ "a",
47
+ "abbr",
48
+ "address",
49
+ "area",
50
+ "article",
51
+ "aside",
52
+ "audio",
53
+ "b",
54
+ "base",
55
+ "bdi",
56
+ "bdo",
57
+ "blockquote",
58
+ "body",
59
+ "br",
60
+ "button",
61
+ "canvas",
62
+ "caption",
63
+ "cite",
64
+ "code",
65
+ "col",
66
+ "colgroup",
67
+ "data",
68
+ "datalist",
69
+ "dd",
70
+ "del",
71
+ "details",
72
+ "dfn",
73
+ "dialog",
74
+ "div",
75
+ "dl",
76
+ "dt",
77
+ "em",
78
+ "embed",
79
+ "fieldset",
80
+ "figcaption",
81
+ "figure",
82
+ "footer",
83
+ "form",
84
+ "h1",
85
+ "h2",
86
+ "h3",
87
+ "h4",
88
+ "h5",
89
+ "h6",
90
+ "head",
91
+ "header",
92
+ "hgroup",
93
+ "hr",
94
+ "html",
95
+ "i",
96
+ "iframe",
97
+ "img",
98
+ "input",
99
+ "ins",
100
+ "kbd",
101
+ "label",
102
+ "legend",
103
+ "li",
104
+ "link",
105
+ "main",
106
+ "map",
107
+ "mark",
108
+ "math",
109
+ "menu",
110
+ "menuitem",
111
+ "meta",
112
+ "meter",
113
+ "nav",
114
+ "noscript",
115
+ "object",
116
+ "ol",
117
+ "optgroup",
118
+ "option",
119
+ "output",
120
+ "p",
121
+ "param",
122
+ "picture",
123
+ "pre",
124
+ "progress",
125
+ "q",
126
+ "rb",
127
+ "rp",
128
+ "rt",
129
+ "rtc",
130
+ "ruby",
131
+ "s",
132
+ "samp",
133
+ "script",
134
+ "section",
135
+ "select",
136
+ "slot",
137
+ "small",
138
+ "source",
139
+ "span",
140
+ "strong",
141
+ "style",
142
+ "sub",
143
+ "summary",
144
+ "sup",
145
+ "svg",
146
+ "table",
147
+ "tbody",
148
+ "td",
149
+ "template",
150
+ "textarea",
151
+ "tfoot",
152
+ "th",
153
+ "thead",
154
+ "time",
155
+ "title",
156
+ "tr",
157
+ "track",
158
+ "u",
159
+ "ul",
160
+ "var",
161
+ "video",
162
+ "wbr"
163
+ ]
164
+
165
+ module.exports = {
166
+ childlessTags,
167
+ closingTags,
168
+ closingTagAncestorBreakers,
169
+ voidTags,
170
+ htmlTags
171
+ }
@@ -0,0 +1,19 @@
1
+
2
+ function onlyUnique(value, index, self) {
3
+ return self.indexOf(value) === index
4
+ }
5
+
6
+ function getCodeLocation(srcPath, line, column = '0') {
7
+ return `${srcPath}:${line}:${column}`
8
+ }
9
+
10
+ function pluralize(thing, single = '', plural = '') {
11
+ const count = Array.isArray(thing) ? thing.length : Number(thing)
12
+ return count === 1 ? single : plural
13
+ }
14
+
15
+ module.exports = {
16
+ pluralize,
17
+ onlyUnique,
18
+ getCodeLocation
19
+ }