markdown-magic 3.0.0 → 3.0.1
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.
- package/README.md +43 -29
- package/lib/block-parser-js.test.js +148 -156
- package/lib/block-parser.js +255 -262
- package/lib/block-parser.test.js +43 -6
- package/lib/cli.js +30 -19
- package/lib/cli.test.js +73 -73
- package/lib/globals.d.ts +66 -0
- package/lib/index.js +43 -9
- package/lib/process-contents.js +80 -39
- package/lib/process-file.js +4 -1
- package/lib/transforms/code.js +4 -10
- package/lib/transforms/file.js +7 -10
- package/lib/transforms/index.js +0 -0
- package/lib/transforms/remote.js +2 -3
- package/lib/transforms/sectionToc.js +18 -0
- package/lib/transforms/toc.js +10 -335
- package/lib/types.js +11 -0
- package/lib/utils/fs.js +21 -19
- package/lib/utils/fs.test.js +4 -5
- package/lib/utils/logs.js +7 -2
- package/lib/utils/md/filters.js +5 -5
- package/lib/utils/md/find-code-blocks.js +16 -8
- package/lib/utils/md/find-frontmatter.js +11 -13
- package/lib/utils/md/find-frontmatter.test.js +2 -2
- package/lib/utils/md/find-html-tags.js +1 -1
- package/lib/utils/md/find-images-md.js +27 -0
- package/lib/utils/md/find-images.js +39 -34
- package/lib/utils/md/find-links.js +72 -54
- package/lib/utils/md/find-unmatched-html-tags.js +1 -2
- package/lib/utils/md/fixtures/file-with-links.md +10 -0
- package/lib/utils/md/md.test.js +72 -4
- package/lib/utils/md/parse.js +91 -67
- package/lib/utils/regex-timeout.js +2 -1
- package/lib/utils/regex.js +3 -2
- package/lib/utils/remoteRequest.js +1 -0
- package/lib/utils/syntax.js +3 -0
- package/lib/utils/text.js +71 -3
- package/lib/utils/text.test.js +3 -9
- package/lib/utils/toc.js +315 -0
- package/package.json +7 -3
- package/lib/options-parser.js +0 -498
- package/lib/options-parser.test.js +0 -1237
- package/lib/utils/html-to-json/compat.js +0 -42
- package/lib/utils/html-to-json/format.js +0 -64
- package/lib/utils/html-to-json/index.js +0 -37
- package/lib/utils/html-to-json/lexer.js +0 -345
- package/lib/utils/html-to-json/parser.js +0 -146
- package/lib/utils/html-to-json/stringify.js +0 -37
- package/lib/utils/html-to-json/tags.js +0 -171
package/lib/utils/toc.js
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
const { transform } = require('@technote-space/doctoc')
|
|
2
|
+
const { removeLeadingAndTrailingLineBreaks, escapeRegexString } = require('./regex')
|
|
3
|
+
const { findMinIndent } = require('./text')
|
|
4
|
+
const { readFile } = require('./fs')
|
|
5
|
+
|
|
6
|
+
// Alt https://github.com/thlorenz/doctoc
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {object} TocOptions
|
|
10
|
+
* @property {boolean} [collapse = false] - Collapse toc in <details> pane
|
|
11
|
+
* @property {string} [collapseText] - Text in expand pane
|
|
12
|
+
* @property {string} [excludeText] - Text to exclude from toc
|
|
13
|
+
* @property {boolean} [firsth1 = true] - Exclude first heading from toc
|
|
14
|
+
* @property {boolean} [sub = false] - Mark as sub section table of contents
|
|
15
|
+
* @property {number} [maxDepth = 4] - Max depth of headings to add to toc.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {object} TocAPI
|
|
20
|
+
* @property {TocOptions} options - Toc generation options
|
|
21
|
+
* @property {string} fileContent - Markdown contents
|
|
22
|
+
* @property {string} srcPath - Path to markdown file
|
|
23
|
+
* @property {function} getBlockDetails - Get block details for sub tables
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generate a table of contents
|
|
28
|
+
* @param {TocAPI} api
|
|
29
|
+
* @returns
|
|
30
|
+
*/
|
|
31
|
+
async function generateToc({
|
|
32
|
+
options = {},
|
|
33
|
+
fileContent,
|
|
34
|
+
srcPath,
|
|
35
|
+
getBlockDetails
|
|
36
|
+
}) {
|
|
37
|
+
// const currentBlock = api.getCurrentBlock()
|
|
38
|
+
// console.log('originalBlock', originalBlock)
|
|
39
|
+
|
|
40
|
+
// process.exit(1)
|
|
41
|
+
const opts = options || {}
|
|
42
|
+
const isSub = opts.sub
|
|
43
|
+
opts.firsth1 = (opts.firsth1) ? true : false
|
|
44
|
+
let contents = fileContent
|
|
45
|
+
// console.log('contents', contents)
|
|
46
|
+
|
|
47
|
+
let collapseText = opts.collapseText
|
|
48
|
+
|
|
49
|
+
let debugFileMatch
|
|
50
|
+
// console.log('config', config.originalPath)
|
|
51
|
+
// debugFileMatch = config.originalPath.match(/packages\/analytics\/README\.md/)
|
|
52
|
+
|
|
53
|
+
// https://www.npmjs.com/package/@technote-space/toc-generator
|
|
54
|
+
const tocOptions = {
|
|
55
|
+
// mode: 'github.com', // github.com | bitbucket.org | gitlab.com | nodejs.org | ghost.org (default: github.com)
|
|
56
|
+
isNotitle: true,
|
|
57
|
+
//isCustomMode: true,
|
|
58
|
+
openingComment: '',
|
|
59
|
+
closingComment: '',
|
|
60
|
+
maxHeaderLevel: (opts.maxDepth) ? Number(opts.maxDepth) : 4,
|
|
61
|
+
// maxHeaderLevel: 2, // default: 4
|
|
62
|
+
// title: '**Table of Contents**',
|
|
63
|
+
// isNotitle: true,
|
|
64
|
+
// isFolding: true,
|
|
65
|
+
// entryPrefix: '*',
|
|
66
|
+
// processAll: true,
|
|
67
|
+
// updateOnly: true,
|
|
68
|
+
// openingComment: '<!-- toc -->',
|
|
69
|
+
// closingComment: '<!-- tocstop --> ',
|
|
70
|
+
// checkOpeningComments: ['<!-- toc '],
|
|
71
|
+
// checkClosingComments: ['<!-- tocstop '],
|
|
72
|
+
// isCustomMode: false,
|
|
73
|
+
// customTemplate: '<p align="center">${ITEMS}</p>',
|
|
74
|
+
// itemTemplate: '<a href="${LINK}">${TEXT}</a>',
|
|
75
|
+
// separator: '<span>|</span>',
|
|
76
|
+
// footer: 'end',
|
|
77
|
+
}
|
|
78
|
+
const t = await transform(contents, tocOptions)
|
|
79
|
+
let outputText = t.wrappedToc || ''
|
|
80
|
+
|
|
81
|
+
if (debugFileMatch) {
|
|
82
|
+
console.log('before firsth1 removal', outputText)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* remove first h1 */
|
|
86
|
+
if (!opts.firsth1) {
|
|
87
|
+
// console.log('outputText', outputText)
|
|
88
|
+
const lines = outputText.split('\n')
|
|
89
|
+
let firstH1
|
|
90
|
+
let firstLine
|
|
91
|
+
const countOfH1s = lines.reduce((acc, line, index) => {
|
|
92
|
+
if (line !== '' && !firstLine) {
|
|
93
|
+
firstLine = index
|
|
94
|
+
}
|
|
95
|
+
if (line.match(/^-\s+/)) {
|
|
96
|
+
if (!firstH1) {
|
|
97
|
+
const headingText = line.match(/\[(.*)\]/)
|
|
98
|
+
if (headingText) {
|
|
99
|
+
const rawText = headingText[1]
|
|
100
|
+
firstH1 = [line, index, firstLine === index, rawText]
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
acc = acc + 1
|
|
104
|
+
}
|
|
105
|
+
return acc
|
|
106
|
+
}, 0)
|
|
107
|
+
|
|
108
|
+
const firstHeadingData = (firstH1 && Array.isArray(firstH1)) ? firstH1 : []
|
|
109
|
+
const [ lineText, lineIndex, isFirst, matchText ] = firstHeadingData
|
|
110
|
+
|
|
111
|
+
let docHasHeading = false
|
|
112
|
+
if (matchText) {
|
|
113
|
+
// verify its h1
|
|
114
|
+
const matchTextEscaped = escapeRegexString(matchText)
|
|
115
|
+
// console.log('matchTextEscaped', matchTextEscaped)
|
|
116
|
+
// /^#{1}\s+(.*)/
|
|
117
|
+
const FIRST_H1_REGEX = new RegExp(`^#\\s*\\[?${matchTextEscaped}\\]?(?:.*)?`, 'gim')
|
|
118
|
+
// /^<h1\b[^>]*>([\s\S]*?)<\/h1>/
|
|
119
|
+
const FIRST_HTML_H1_REGEX = new RegExp(`^<h1\\b[^>]*>[\\s]*?(${matchTextEscaped})[\\s]*?<\\/h1>`, 'gim')
|
|
120
|
+
// /^(.*)\n={3,}/
|
|
121
|
+
const FIRST_UNDERSCORE_H1 = new RegExp(`^(${matchTextEscaped})\n={3,}`, 'gim')
|
|
122
|
+
|
|
123
|
+
if (contents.match(FIRST_H1_REGEX)) {
|
|
124
|
+
docHasHeading = matchText
|
|
125
|
+
} else if (contents.match(FIRST_HTML_H1_REGEX)) {
|
|
126
|
+
docHasHeading = matchText
|
|
127
|
+
} else if (contents.match(FIRST_UNDERSCORE_H1)) {
|
|
128
|
+
docHasHeading = matchText
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (debugFileMatch) {
|
|
133
|
+
console.log('matchText', matchText)
|
|
134
|
+
if (docHasHeading) {
|
|
135
|
+
console.log('Found heading 1', docHasHeading)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (debugFileMatch) {
|
|
140
|
+
console.log('top level toc item count', countOfH1s)
|
|
141
|
+
if (docHasHeading) {
|
|
142
|
+
console.log('Found heading 1', docHasHeading)
|
|
143
|
+
console.log('firstH1', firstH1)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let firstItemWithContent
|
|
148
|
+
let foundFirstH1
|
|
149
|
+
|
|
150
|
+
if (docHasHeading) {
|
|
151
|
+
outputText = lines.reduce((acc, line, i) => {
|
|
152
|
+
const isLineEmpty = line === ''
|
|
153
|
+
if (!isLineEmpty && !firstItemWithContent) {
|
|
154
|
+
firstItemWithContent = i
|
|
155
|
+
}
|
|
156
|
+
if (!foundFirstH1 && (firstItemWithContent === i) && line.match(/^-\s+/)) {
|
|
157
|
+
foundFirstH1 = true
|
|
158
|
+
return acc
|
|
159
|
+
}
|
|
160
|
+
// Add back line and remove level
|
|
161
|
+
let newLineValue = line
|
|
162
|
+
if (lineText) {
|
|
163
|
+
// const untilFirstH1 = i < lineIndex
|
|
164
|
+
/* @TODO make option? flatten all non first h1 tags */
|
|
165
|
+
if (countOfH1s > 0 && !isLineEmpty && newLineValue.match(/^\s+-/)) {
|
|
166
|
+
newLineValue = line.substring(2)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
acc = acc.concat(newLineValue)
|
|
171
|
+
return acc
|
|
172
|
+
}, []).join('\n')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// console.log('outputText', outputText)
|
|
176
|
+
if (debugFileMatch) {
|
|
177
|
+
console.log('after firsth1 removal', outputText)
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// console.log(t)
|
|
181
|
+
// Fix <h1> with multiple lines
|
|
182
|
+
const spaces = outputText.match(/\[\n([\s\S]*?)\]/gm)
|
|
183
|
+
if (spaces) {
|
|
184
|
+
const fixed = spaces[0]
|
|
185
|
+
// all new lines
|
|
186
|
+
.replace(/\n/g, '')
|
|
187
|
+
// leading spaces
|
|
188
|
+
.replace(/\[\s+/, '[')
|
|
189
|
+
.trim()
|
|
190
|
+
outputText = outputText.replace(spaces[0], fixed)
|
|
191
|
+
}
|
|
192
|
+
// console.log(outputText)
|
|
193
|
+
outputText = outputText
|
|
194
|
+
.replace(/(.*)\[Table of Contents\]\(.*\)\n?/i, '')
|
|
195
|
+
.replace(/(.*)\[toc\]\(.*\)\n?/i, '')
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
if (opts.excludeText) {
|
|
199
|
+
|
|
200
|
+
outputText = excludeTocItem(outputText, opts.excludeText)
|
|
201
|
+
// // (\s+)?-(.*)\[Usage\]\(.*\)
|
|
202
|
+
// const regex = new RegExp(`\(\\s+\)?-(.*)\\[${opts.excludeText}\\]\\(.*\\)`, 'i')
|
|
203
|
+
// // /(\s+)?-(.*)\[Usage\]\(.*\)(\n\s+(.*))+/im
|
|
204
|
+
// const nestedRegex = new RegExp(`\(\\s+\)?-(.*)\\[${opts.excludeText}\\]\\(.*\\)\(\\n\\s+\(.*\)\)+`, 'i')
|
|
205
|
+
|
|
206
|
+
// const hasNested = nestedRegex.exec(outputText)
|
|
207
|
+
// if (hasNested) {
|
|
208
|
+
// // Count indentation of spaces
|
|
209
|
+
// const numberOfSpaces = hasNested[1].replace(/\n/g, '').length
|
|
210
|
+
// const subItems = numberOfSpaces + 1
|
|
211
|
+
// // Update regex to only remove sub items
|
|
212
|
+
// const nestedRegexSpaces = new RegExp(`\(\\s+\)?-(.*)\\[${opts.excludeText}\\]\\(.*\\)\(\\n\\s{${subItems},}\(.*\)\)+`, 'i')
|
|
213
|
+
// // console.log('nestedRegexSpaces', nestedRegexSpaces)
|
|
214
|
+
// // If exclude value has nested sections remove them as well.
|
|
215
|
+
// outputText = outputText.replace(nestedRegexSpaces, '')
|
|
216
|
+
// outputText = outputText.replace(regex, '')
|
|
217
|
+
// } else {
|
|
218
|
+
// outputText = outputText.replace(regex, '')
|
|
219
|
+
// }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* Sub table of contents */
|
|
223
|
+
if (isSub) {
|
|
224
|
+
// const start = fileContent.indexOf(open.value)
|
|
225
|
+
// const linesBefore = fileContent.substr(0, start).split('\n')
|
|
226
|
+
// const closestHeading = linesBefore.reverse().find((line) => {
|
|
227
|
+
// return line.match((/^#+/))
|
|
228
|
+
// })
|
|
229
|
+
const originalContents = await readFile(srcPath, { encoding: 'utf8' })
|
|
230
|
+
const originalBlock = getBlockDetails(originalContents)
|
|
231
|
+
const linesBefore = originalContents.substr(0, originalBlock.block.start).split('\n')
|
|
232
|
+
const closestHeading = linesBefore.reverse().find((line) => {
|
|
233
|
+
return line.match((/^#+/))
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
if (closestHeading) {
|
|
237
|
+
const headingText = closestHeading.replace(/^#*\s*/, '')
|
|
238
|
+
// console.log('BEFORE', linesBefore)
|
|
239
|
+
// console.log('closest parent heading', closestHeading)
|
|
240
|
+
// console.log('headingText', headingText)
|
|
241
|
+
collapseText = collapseText || `${headingText} contents`
|
|
242
|
+
// https://regex101.com/r/MB85zm/1
|
|
243
|
+
|
|
244
|
+
const findSubToc = new RegExp(`^\(\\s+\)?-(.*)\\[${headingText}\\]\\(.*\\)(\\n\\s.*)+`, 'gm')
|
|
245
|
+
// console.log('findSubToc', findSubToc)
|
|
246
|
+
const single = singleLinePattern(headingText)
|
|
247
|
+
// console.log('single', single)
|
|
248
|
+
const subItems = outputText.match(findSubToc)
|
|
249
|
+
if (subItems) {
|
|
250
|
+
const items = subItems[0].replace(single, '').split('\n')
|
|
251
|
+
.filter(Boolean)
|
|
252
|
+
// console.log('items', items)
|
|
253
|
+
const finalItems = items // .slice(1, items.length)
|
|
254
|
+
if (finalItems) {
|
|
255
|
+
const indent = findMinIndent(finalItems.join('\n'))
|
|
256
|
+
// console.log('min indent', indent)
|
|
257
|
+
// console.log('finalItems', finalItems)
|
|
258
|
+
outputText = finalItems.map((thing) => thing.substring(indent)).join('\n')
|
|
259
|
+
} else {
|
|
260
|
+
// console.log('No sub items')
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// If collapse wrap in <details>
|
|
267
|
+
if (opts.collapse) {
|
|
268
|
+
return `<details>
|
|
269
|
+
<summary>${collapseText || 'Table of Contents'}</summary>
|
|
270
|
+
|
|
271
|
+
${outputText
|
|
272
|
+
// Replace leading double spaces
|
|
273
|
+
.replace(/^\n*/g, '')
|
|
274
|
+
// Replace trailing double spaces
|
|
275
|
+
.replace(/\n*$/g, '')
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
</details>`
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return outputText.replace(removeLeadingAndTrailingLineBreaks, '')
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function singleLinePattern(text) {
|
|
285
|
+
/* (\s+)?-(.*)\[Usage\]\(.*\) */
|
|
286
|
+
return new RegExp(`\(\\s+\)?-(.*)\\[${text}\\]\\(.*\\)`, 'i')
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function excludeTocItem(str, excludeText) {
|
|
290
|
+
const matchTextEscaped = escapeRegexString(excludeText)
|
|
291
|
+
/* (\s+)?-(.*)\[Usage\]\(.*\) */
|
|
292
|
+
const regex = singleLinePattern(matchTextEscaped) // new RegExp(`\(\\s+\)?-(.*)\\[${matchTextEscaped}\\]\\(.*\\)`, 'i')
|
|
293
|
+
/* /(\s+)?-(.*)\[Usage\]\(.*\)(\n\s+(.*))+/im */
|
|
294
|
+
const nestedRegex = new RegExp(`\(\\s+\)?-(.*)\\[${matchTextEscaped}\\]\\(.*\\)\(\\n\\s+\(.*\)\)+`, 'i')
|
|
295
|
+
|
|
296
|
+
const hasNested = nestedRegex.exec(str)
|
|
297
|
+
if (hasNested) {
|
|
298
|
+
// Count indentation of spaces
|
|
299
|
+
const numberOfSpaces = (hasNested[1] || '').replace(/\n/g, '').length
|
|
300
|
+
const subItems = numberOfSpaces + 1
|
|
301
|
+
// Update regex to only remove sub items
|
|
302
|
+
const nestedRegexSpaces = new RegExp(`\(\\s+\)?-(.*)\\[${matchTextEscaped}\\]\\(.*\\)\(\\n\\s{${subItems},}\(.*\)\)+`, 'i')
|
|
303
|
+
// console.log('nestedRegexSpaces', nestedRegexSpaces)
|
|
304
|
+
// If exclude value has nested sections remove them as well.
|
|
305
|
+
str = str.replace(nestedRegexSpaces, '')
|
|
306
|
+
str = str.replace(regex, '')
|
|
307
|
+
} else {
|
|
308
|
+
str = str.replace(regex, '')
|
|
309
|
+
}
|
|
310
|
+
return str
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
module.exports = {
|
|
314
|
+
generateToc
|
|
315
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "markdown-magic",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "Automatically update markdown files with content from external sources",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
"/lib"
|
|
16
16
|
],
|
|
17
17
|
"scripts": {
|
|
18
|
+
"types": "tsc",
|
|
19
|
+
"emit-types": "tsc --noEmit false --emitDeclarationOnly true",
|
|
18
20
|
"docs": "node examples/generate-readme.js",
|
|
19
21
|
"test": "npm run test:lib && npm run test:test && echo 'tests done'",
|
|
20
22
|
"test:lib": "uvu lib '.test.([mc]js|[jt]sx?)$'",
|
|
@@ -48,17 +50,19 @@
|
|
|
48
50
|
"is-glob": "^4.0.3",
|
|
49
51
|
"is-local-path": "^0.1.6",
|
|
50
52
|
"is-valid-path": "^0.1.1",
|
|
51
|
-
"
|
|
53
|
+
"micro-mdx-parser": "^1.1.0",
|
|
52
54
|
"mri": "^1.2.0",
|
|
55
|
+
"oparser": "^2.1.1",
|
|
53
56
|
"smart-glob": "^1.0.2",
|
|
54
57
|
"sync-request": "^6.1.0"
|
|
55
58
|
},
|
|
56
59
|
"devDependencies": {
|
|
57
60
|
"ansi-styles": "^4.2.1",
|
|
58
61
|
"concordance": "^5.0.1",
|
|
59
|
-
"doxxx": "^
|
|
62
|
+
"doxxx": "^2.0.1",
|
|
60
63
|
"rimraf": "^3.0.2",
|
|
61
64
|
"safe-chalk": "^1.0.0",
|
|
65
|
+
"typescript": "^5.0.2",
|
|
62
66
|
"uvu": "^0.5.1"
|
|
63
67
|
}
|
|
64
68
|
}
|