markdown-magic 3.6.5 → 4.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.
- package/cli.js +1 -1
- package/package.json +23 -28
- package/src/{cli.js → cli-run.js} +0 -0
- package/src/{cli.test.js → cli-run.test.js} +1 -1
- package/src/defaults.js +2 -2
- package/src/index.js +30 -22
- package/src/index.test.js +2 -2
- package/src/process-contents.js +2 -458
- package/src/transforms/code/index.js +6 -6
- package/src/transforms/fileTree.js +213 -0
- package/src/transforms/index.js +87 -19
- package/src/transforms/install.js +3 -3
- package/src/transforms/sectionToc.js +4 -4
- package/src/transforms/toc.js +7 -7
- package/src/transforms/wordCount.js +3 -2
- package/src/types.js +1 -1
- package/src/utils/fs.js +22 -0
- package/src/utils/fs.test.js +5 -2
- package/src/utils/index.js +21 -0
- package/src/utils/regex-timeout.js +1 -1
- package/src/utils/syntax.js +29 -1
- package/src/utils/text.js +115 -16
- package/src/utils/text.test.js +76 -3
- package/README.md +0 -605
- package/src/block-parser-js.test.js +0 -171
- package/src/block-parser.js +0 -405
- package/src/block-parser.test.js +0 -481
- package/src/process-file.js +0 -66
package/src/process-contents.js
CHANGED
|
@@ -1,461 +1,5 @@
|
|
|
1
|
-
const {
|
|
2
|
-
const { deepLog } = require('./utils/logs')
|
|
3
|
-
const { getCodeLocation } = require('./utils')
|
|
4
|
-
const { indentString, trimString } = require('./utils/text')
|
|
5
|
-
const { OPEN_WORD, CLOSE_WORD, SYNTAX } = require('./defaults')
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Configuration object for processing contents.
|
|
9
|
-
* @typedef {Object} ProcessContentConfig
|
|
10
|
-
* @property {string} srcPath - The source path.
|
|
11
|
-
* @property {string} outputPath - The output path.
|
|
12
|
-
* @property {string} [open=OPEN_WORD] - The opening delimiter.
|
|
13
|
-
* @property {string} [close=CLOSE_WORD] - The closing delimiter.
|
|
14
|
-
* @property {string} [syntax='md'] - The syntax type.
|
|
15
|
-
* @property {Array<Function>} [transforms=[]] - The array of transform functions.
|
|
16
|
-
* @property {Array<Function>} [beforeMiddleware=[]] - The array of middleware functions to be executed before processing.
|
|
17
|
-
* @property {Array<Function>} [afterMiddleware=[]] - The array of middleware functions to be executed after processing.
|
|
18
|
-
* @property {boolean} [debug=false] - Enable debug mode.
|
|
19
|
-
* @property {boolean} [removeComments=false] - Remove comments from the processed contents.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Pull comment blocks out of content and process them
|
|
24
|
-
* @param {string} text
|
|
25
|
-
* @param {ProcessContentConfig} config
|
|
26
|
-
* @returns
|
|
27
|
-
*/
|
|
28
|
-
async function processContents(text, config) {
|
|
29
|
-
const opts = config || {}
|
|
30
|
-
|
|
31
|
-
const {
|
|
32
|
-
srcPath,
|
|
33
|
-
outputPath,
|
|
34
|
-
open = OPEN_WORD, // 'DOCS:START',
|
|
35
|
-
close = CLOSE_WORD, // 'DOCS:END',
|
|
36
|
-
syntax = SYNTAX, // 'md'
|
|
37
|
-
transforms = [],
|
|
38
|
-
beforeMiddleware = [],
|
|
39
|
-
afterMiddleware = [],
|
|
40
|
-
debug = false,
|
|
41
|
-
removeComments = false
|
|
42
|
-
} = opts
|
|
43
|
-
|
|
44
|
-
/*
|
|
45
|
-
console.log('Open word', open)
|
|
46
|
-
console.log('Close word', close)
|
|
47
|
-
console.log('syntax', syntax)
|
|
48
|
-
// console.log('text', text)
|
|
49
|
-
/** */
|
|
50
|
-
|
|
51
|
-
let foundBlocks = {}
|
|
52
|
-
try {
|
|
53
|
-
foundBlocks = parseBlocks(text, {
|
|
54
|
-
syntax,
|
|
55
|
-
open,
|
|
56
|
-
close,
|
|
57
|
-
})
|
|
58
|
-
} catch (e) {
|
|
59
|
-
throw new Error(`${e.message}\nFix content in ${srcPath}\n`)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// if (debug) {
|
|
63
|
-
// console.log(`foundBlocks ${srcPath}`)
|
|
64
|
-
// deepLog(foundBlocks)
|
|
65
|
-
// }
|
|
66
|
-
/*
|
|
67
|
-
deepLog(foundBlocks)
|
|
68
|
-
process.exit(1)
|
|
69
|
-
/** */
|
|
70
|
-
const { COMMENT_OPEN_REGEX, COMMENT_CLOSE_REGEX } = foundBlocks
|
|
71
|
-
|
|
72
|
-
const blocksWithTransforms = foundBlocks.blocks
|
|
73
|
-
.filter((block) => block.type)
|
|
74
|
-
.map((block, i) => {
|
|
75
|
-
const transform = block.type
|
|
76
|
-
delete block.type
|
|
77
|
-
return Object.assign({ transform }, block)
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const regexInfo = {
|
|
82
|
-
blocks: foundBlocks.pattern,
|
|
83
|
-
open: COMMENT_OPEN_REGEX,
|
|
84
|
-
close: COMMENT_CLOSE_REGEX,
|
|
85
|
-
}
|
|
86
|
-
// console.log('blocksWithTransforms', blocksWithTransforms)
|
|
87
|
-
// process.exit(1)
|
|
88
|
-
|
|
89
|
-
const transformsToRun = sortTranforms(blocksWithTransforms, transforms)
|
|
90
|
-
// .map((transform) => {
|
|
91
|
-
// return {
|
|
92
|
-
// ...transform,
|
|
93
|
-
// srcPath
|
|
94
|
-
// }
|
|
95
|
-
// })
|
|
96
|
-
// console.log('transformsToRun', transformsToRun)
|
|
97
|
-
|
|
98
|
-
// if (!transformsToRun.length) {
|
|
99
|
-
// process.exit(1)
|
|
100
|
-
// }
|
|
101
|
-
// console.log('transformsToRun', transformsToRun)
|
|
102
|
-
let missingTransforms = []
|
|
103
|
-
let updatedContents = await transformsToRun.reduce(async (contentPromise, originalMatch) => {
|
|
104
|
-
const md = await contentPromise
|
|
105
|
-
/* Apply Before middleware to all transforms */
|
|
106
|
-
const match = await applyMiddleware(originalMatch, md, beforeMiddleware)
|
|
107
|
-
const { block, content, open, close, transform, options, context } = match
|
|
108
|
-
// console.log("MATCH", match)
|
|
109
|
-
const closeTag = close.value
|
|
110
|
-
const openTag = open.value
|
|
111
|
-
|
|
112
|
-
/* Run transform plugins */
|
|
113
|
-
let tempContent = content.value
|
|
114
|
-
// console.log('transform', transform)
|
|
115
|
-
const currentTransformFn = getTransform(transform, transforms)
|
|
116
|
-
/* Run each transform */
|
|
117
|
-
if (currentTransformFn) {
|
|
118
|
-
// console.log('context', context)
|
|
119
|
-
let returnedContent
|
|
120
|
-
/* DISABLED legacy syntax */
|
|
121
|
-
/* // Support for legacy syntax... maybe later
|
|
122
|
-
if (context && context.isLegacy) {
|
|
123
|
-
console.log(`CALL legacy ${transform}`, srcPath)
|
|
124
|
-
// backward compat maybe
|
|
125
|
-
// CODE(content, options, config)
|
|
126
|
-
returnedContent = await currentTransformFn(content.value, options, {
|
|
127
|
-
originalPath: srcPath
|
|
128
|
-
})
|
|
129
|
-
} else {
|
|
130
|
-
returnedContent = await currentTransformFn(transformApi({
|
|
131
|
-
srcPath,
|
|
132
|
-
...match,
|
|
133
|
-
regex: regexInfo,
|
|
134
|
-
originalContents: text,
|
|
135
|
-
currentContents: md,
|
|
136
|
-
}, config))
|
|
137
|
-
}
|
|
138
|
-
/** */
|
|
139
|
-
|
|
140
|
-
returnedContent = await currentTransformFn(
|
|
141
|
-
transformApi({
|
|
142
|
-
srcPath,
|
|
143
|
-
...match,
|
|
144
|
-
regex: regexInfo,
|
|
145
|
-
originalContents: text,
|
|
146
|
-
currentContents: md,
|
|
147
|
-
}, config)
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
/* Run each transform */
|
|
151
|
-
// console.log('config', config)
|
|
152
|
-
|
|
153
|
-
// console.log('returnedContent', returnedContent)
|
|
154
|
-
// process.exit(1)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (returnedContent) {
|
|
158
|
-
tempContent = returnedContent
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/* Apply After middleware to all transforms */
|
|
163
|
-
const afterContent = await applyMiddleware({
|
|
164
|
-
...match,
|
|
165
|
-
...{
|
|
166
|
-
content: {
|
|
167
|
-
...match.content,
|
|
168
|
-
value: tempContent
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}, md, afterMiddleware)
|
|
172
|
-
/*
|
|
173
|
-
console.log('afterContent', afterContent)
|
|
174
|
-
process.exit(1)
|
|
175
|
-
/** */
|
|
176
|
-
|
|
177
|
-
if (debug) {
|
|
178
|
-
// console.log('afterContent', afterContent)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
if (!currentTransformFn) {
|
|
182
|
-
missingTransforms.push(afterContent)
|
|
183
|
-
// console.log(`Missing "${transform}" transform`)
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
const newContent = afterContent.content.value
|
|
187
|
-
const formattedNewContent = (options.noTrim) ? newContent : trimString(newContent)
|
|
188
|
-
// console.log('formattedNewContent', formattedNewContent)
|
|
189
|
-
/* Remove any conflicting imported comments */
|
|
190
|
-
const fix = removeConflictingComments(formattedNewContent, COMMENT_OPEN_REGEX, COMMENT_CLOSE_REGEX)
|
|
191
|
-
/*
|
|
192
|
-
console.log('fix')
|
|
193
|
-
deepLog(fix)
|
|
194
|
-
process.exit(1)
|
|
195
|
-
/** */
|
|
196
|
-
if (options.removeComments) {
|
|
197
|
-
// console.log('removeComments', options.removeComments)
|
|
198
|
-
}
|
|
199
|
-
// const fix = stripAllComments(formattedNewContent, foundBlocks.COMMENT_OPEN_REGEX, COMMENT_CLOSE_REGEX)
|
|
200
|
-
|
|
201
|
-
// console.log('COMMENT_CLOSE_REGEX', COMMENT_CLOSE_REGEX)
|
|
202
|
-
// console.log('formattedNewContent', formattedNewContent)
|
|
203
|
-
// console.log('fix', fix)
|
|
204
|
-
|
|
205
|
-
let preserveIndent = 0
|
|
206
|
-
if (match.content.indentation) {
|
|
207
|
-
preserveIndent = match.content.indentation
|
|
208
|
-
} else if (preserveIndent === 0) {
|
|
209
|
-
preserveIndent = block.indentation.length
|
|
210
|
-
}
|
|
211
|
-
// console.log('preserveIndent', preserveIndent)
|
|
212
|
-
let addTrailingNewline = ''
|
|
213
|
-
if (context.isMultiline && !fix.endsWith('\n') && fix !== '' && closeTag.indexOf('\n') === -1) {
|
|
214
|
-
addTrailingNewline = '\n'
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
let addLeadingNewline = ''
|
|
218
|
-
if (context.isMultiline && !fix.startsWith('\n') && fix !== '' && openTag.indexOf('\n') === -1) {
|
|
219
|
-
addLeadingNewline = '\n'
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
let fixWrapper = ''
|
|
223
|
-
/* If block wasn't multiline but the contents ARE multiline fix the block */
|
|
224
|
-
if (!context.isMultiline && fix.indexOf('\n') > -1) {
|
|
225
|
-
fixWrapper = '\n'
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// console.log("OPEN TAG", `"${openTag}"`)
|
|
229
|
-
// console.log("CLOSE TAG", `"${closeTag}"`)
|
|
230
|
-
|
|
231
|
-
const indent = addLeadingNewline + indentString(fix, preserveIndent) + addTrailingNewline
|
|
232
|
-
const newCont = `${openTag}${fixWrapper}${indent}${fixWrapper}${closeTag}`
|
|
233
|
-
/* Replace original contents */
|
|
234
|
-
// Must use replacer function because strings get coerced to regex or something
|
|
235
|
-
const newContents = md.replace(block.value, () => newCont)
|
|
236
|
-
/*
|
|
237
|
-
deepLog(newContents)
|
|
238
|
-
process.exit(1)
|
|
239
|
-
/** */
|
|
240
|
-
return Promise.resolve(newContents)
|
|
241
|
-
}, Promise.resolve(text))
|
|
242
|
-
|
|
243
|
-
// console.log('updatedContents')
|
|
244
|
-
// deepLog(updatedContents)
|
|
245
|
-
// process.exit(1)
|
|
246
|
-
|
|
247
|
-
// if (debug) {
|
|
248
|
-
// console.log('Output Markdown')
|
|
249
|
-
// console.log(updatedContents)
|
|
250
|
-
// }
|
|
251
|
-
|
|
252
|
-
/*
|
|
253
|
-
if (missingTransforms.length) {
|
|
254
|
-
console.log('missingTransforms', missingTransforms)
|
|
255
|
-
let matchOne = missingTransforms[1]
|
|
256
|
-
matchOne = missingTransforms[missingTransforms.length - 1]
|
|
257
|
-
// console.log('matchOne', matchOne)
|
|
258
|
-
const { block, transform, args } = matchOne
|
|
259
|
-
const { openTag, closeTag, content, contentStart, contentEnd, start, end} = block
|
|
260
|
-
|
|
261
|
-
// console.log('contentStart', contentStart)
|
|
262
|
-
// console.log('contentEnd', contentEnd)
|
|
263
|
-
// console.log('original text between', `"${getTextBetweenChars(md, contentStart, contentEnd)}"`)
|
|
264
|
-
// console.log('original block between', `"${getTextBetweenChars(md, start, end)}"`)
|
|
265
|
-
}
|
|
266
|
-
/** */
|
|
267
|
-
|
|
268
|
-
// console.log('detect slow srcPath', srcPath)
|
|
269
|
-
|
|
270
|
-
const isNewPath = srcPath !== outputPath
|
|
271
|
-
|
|
272
|
-
if (removeComments && !isNewPath) {
|
|
273
|
-
throw new Error('"removeComments" can only be used if "outputPath" option is set. Otherwise this will break doc generation.')
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/* Strip block comments from output files */
|
|
277
|
-
const stripComments = isNewPath && removeComments
|
|
278
|
-
|
|
279
|
-
// console.log('srcPath', srcPath)
|
|
280
|
-
// console.log('outputPath', outputPath)
|
|
281
|
-
// console.log('updatedContents', updatedContents)
|
|
282
|
-
// console.log('text', text)
|
|
283
|
-
// process.exit(1)
|
|
284
|
-
const result = {
|
|
285
|
-
/* Has markdown content changed? */
|
|
286
|
-
isChanged: text !== updatedContents,
|
|
287
|
-
isNewPath,
|
|
288
|
-
stripComments,
|
|
289
|
-
srcPath,
|
|
290
|
-
outputPath,
|
|
291
|
-
// config,
|
|
292
|
-
transforms: transformsToRun,
|
|
293
|
-
missingTransforms,
|
|
294
|
-
originalContents: text,
|
|
295
|
-
updatedContents,
|
|
296
|
-
}
|
|
297
|
-
/*
|
|
298
|
-
console.log('result')
|
|
299
|
-
deepLog(result)
|
|
300
|
-
process.exit(1)
|
|
301
|
-
/** */
|
|
302
|
-
return result
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function transformApi(stuff, opts) {
|
|
306
|
-
const { transforms, srcPath, outputPath, ...rest } = opts
|
|
307
|
-
return {
|
|
308
|
-
transform: stuff.transform,
|
|
309
|
-
content: stuff.content.value,
|
|
310
|
-
options: stuff.options || {},
|
|
311
|
-
srcPath: stuff.srcPath,
|
|
312
|
-
outputPath: outputPath,
|
|
313
|
-
/* Library settings */
|
|
314
|
-
settings: {
|
|
315
|
-
...rest,
|
|
316
|
-
regex: stuff.regex,
|
|
317
|
-
},
|
|
318
|
-
// blockContent: stuff.content.value,
|
|
319
|
-
currentFileContent: stuff.currentContents,
|
|
320
|
-
originalFileContent: stuff.originalContents,
|
|
321
|
-
/* Methods */
|
|
322
|
-
getCurrentContent: () => stuff.currentContents,
|
|
323
|
-
getOriginalContent: () => stuff.originalContents,
|
|
324
|
-
getOriginalBlock: () => stuff,
|
|
325
|
-
getBlockDetails: (content) => {
|
|
326
|
-
/* Re-parse current file for updated positions */
|
|
327
|
-
return getDetails({
|
|
328
|
-
contents: content || stuff.currentContents,
|
|
329
|
-
openValue: stuff.open.value,
|
|
330
|
-
srcPath: stuff.srcPath,
|
|
331
|
-
index: stuff.index,
|
|
332
|
-
opts: opts
|
|
333
|
-
})
|
|
334
|
-
},
|
|
335
|
-
// getOriginalBlockDetails: () => {
|
|
336
|
-
// return getDetails({
|
|
337
|
-
// contents: stuff.originalContents,
|
|
338
|
-
// openValue: stuff.open.value,
|
|
339
|
-
// srcPath: stuff.srcPath,
|
|
340
|
-
// index: stuff.index,
|
|
341
|
-
// opts: opts
|
|
342
|
-
// })
|
|
343
|
-
// },
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
function getDetails({
|
|
348
|
-
contents,
|
|
349
|
-
openValue,
|
|
350
|
-
srcPath,
|
|
351
|
-
index,
|
|
352
|
-
opts
|
|
353
|
-
}) {
|
|
354
|
-
/* Re-parse current file for updated positions */
|
|
355
|
-
const blockData = parseBlocks(contents, opts)
|
|
356
|
-
// console.log('blockData', blockData)
|
|
357
|
-
|
|
358
|
-
const matchingBlocks = blockData.blocks.filter((block) => {
|
|
359
|
-
return block.open.value === openValue
|
|
360
|
-
})
|
|
361
|
-
|
|
362
|
-
if (!matchingBlocks.length) {
|
|
363
|
-
return {}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
let foundBlock = matchingBlocks[0]
|
|
367
|
-
if (matchingBlocks.length > 1 && index) {
|
|
368
|
-
foundBlock = matchingBlocks.filter((block) => {
|
|
369
|
-
return block.index === index
|
|
370
|
-
})[0]
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if (srcPath) {
|
|
374
|
-
const location = getCodeLocation(srcPath, foundBlock.block.lines[0])
|
|
375
|
-
foundBlock.sourceLocation = location
|
|
376
|
-
}
|
|
377
|
-
return foundBlock
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/**
|
|
381
|
-
* Remove conflicting comments that might have been inserted from transforms
|
|
382
|
-
* @param {*} content
|
|
383
|
-
* @param {*} openPattern
|
|
384
|
-
* @param {*} closePattern
|
|
385
|
-
* @returns
|
|
386
|
-
*/
|
|
387
|
-
function removeConflictingComments(content, openPattern, closePattern) {
|
|
388
|
-
// console.log('openPattern', openPattern)
|
|
389
|
-
// console.log('closePattern', closePattern)
|
|
390
|
-
const removeOpen = content.replace(openPattern, '')
|
|
391
|
-
// TODO this probably needs to be a loop for larger blocks
|
|
392
|
-
closePattern.lastIndex = 0; // reset regex
|
|
393
|
-
const hasClose = closePattern.exec(content)
|
|
394
|
-
// console.log('closePattern', closePattern)
|
|
395
|
-
// console.log('has', content)
|
|
396
|
-
// console.log('hasClose', hasClose)
|
|
397
|
-
if (!hasClose) {
|
|
398
|
-
return removeOpen
|
|
399
|
-
}
|
|
400
|
-
const closeTag = `${hasClose[2]}${hasClose[3] || ''}`
|
|
401
|
-
// console.log('closeTag', closeTag)
|
|
402
|
-
return removeOpen
|
|
403
|
-
.replace(closePattern, '')
|
|
404
|
-
// .replaceAll(closeTag, '')
|
|
405
|
-
/* Trailing new line */
|
|
406
|
-
.replace(/\n$/, '')
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
function applyMiddleware(data, md, middlewares) {
|
|
410
|
-
return middlewares.reduce(async (acc, curr) => {
|
|
411
|
-
const realAcc = await acc
|
|
412
|
-
// console.log(`Running "${curr.name}" Middleware on "${realAcc.transform}" block`)
|
|
413
|
-
const updatedContent = await curr.transform(realAcc, md)
|
|
414
|
-
// realAcc.block.content = updatedContent
|
|
415
|
-
return Promise.resolve({
|
|
416
|
-
...realAcc,
|
|
417
|
-
...{
|
|
418
|
-
content: {
|
|
419
|
-
...realAcc.content,
|
|
420
|
-
value: updatedContent
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
})
|
|
424
|
-
}, Promise.resolve(data))
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
/**
|
|
428
|
-
* Get Transform function
|
|
429
|
-
* @param {string} name - transform name
|
|
430
|
-
* @param {object} transforms - transform fns
|
|
431
|
-
* @returns {function}
|
|
432
|
-
*/
|
|
433
|
-
function getTransform(name, transforms = {}) {
|
|
434
|
-
return transforms[name] || transforms[name.toLowerCase()]
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
function sortTranforms(foundTransForms, registeredTransforms) {
|
|
438
|
-
// console.log('transforms', transforms)
|
|
439
|
-
if (!foundTransForms) return []
|
|
440
|
-
return foundTransForms.sort((a, b) => {
|
|
441
|
-
// put table of contents (TOC) at end of tranforms
|
|
442
|
-
if (a.transform === 'TOC' || a.transform === 'sectionToc') return 1
|
|
443
|
-
if (b.transform === 'TOC' || b.transform === 'sectionToc') return -1
|
|
444
|
-
return 0
|
|
445
|
-
}).map((item) => {
|
|
446
|
-
if (getTransform(item.transform, registeredTransforms)) {
|
|
447
|
-
return item
|
|
448
|
-
}
|
|
449
|
-
return {
|
|
450
|
-
...item,
|
|
451
|
-
context: {
|
|
452
|
-
...item.context,
|
|
453
|
-
isMissing: true,
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
})
|
|
457
|
-
}
|
|
1
|
+
const { blockTransformer } = require('comment-block-transformer')
|
|
458
2
|
|
|
459
3
|
module.exports = {
|
|
460
|
-
processContents
|
|
4
|
+
processContents: blockTransformer
|
|
461
5
|
}
|
|
@@ -2,7 +2,7 @@ const fs = require('fs')
|
|
|
2
2
|
const path = require('path')
|
|
3
3
|
const { remoteRequest } = require('../../utils/remoteRequest')
|
|
4
4
|
const { isLocalPath } = require('../../utils/fs')
|
|
5
|
-
const { deepLog } = require('
|
|
5
|
+
const { deepLog } = require('../../../src/utils/logs')
|
|
6
6
|
const {
|
|
7
7
|
getLineCount,
|
|
8
8
|
getTextBetweenLines,
|
|
@@ -27,15 +27,15 @@ const RAW_URL_LIKE = /^([A-Za-z0-9_]+)\.([A-Za-z0-9_]*)\/(.*)/
|
|
|
27
27
|
* @property {boolean} [trimDeadCode] - Remove multi-line comments that start with `//` from the code.
|
|
28
28
|
* @example
|
|
29
29
|
```md
|
|
30
|
-
<!--
|
|
30
|
+
<!-- docs CODE src="./relative/path/to/code.js" -->
|
|
31
31
|
This content will be dynamically replaced with code from the file
|
|
32
|
-
<!--
|
|
32
|
+
<!-- /docs -->
|
|
33
33
|
```
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
```md
|
|
36
|
-
<!--
|
|
36
|
+
<!-- docs CODE src="./relative/path/to/code.js" lines="22-44" -->
|
|
37
37
|
This content will be dynamically replaced with code from the file lines 22 through 44
|
|
38
|
-
<!--
|
|
38
|
+
<!-- /docs -->
|
|
39
39
|
```
|
|
40
40
|
*/
|
|
41
41
|
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Options for configuring the file tree table of contents.
|
|
6
|
+
* @typedef {Object} FileTreeTransformOptions
|
|
7
|
+
* @property {string} [src="."] - The directory path to generate the file tree for. Default is `.` (current directory).
|
|
8
|
+
* @property {number} [maxDepth=3] - Maximum depth to traverse in the directory tree. Default is `3`.
|
|
9
|
+
* @property {boolean} [includeFiles=true] - Whether to include files in the tree or just directories. Default is `true`.
|
|
10
|
+
* @property {string[]} [exclude=[]] - Array of glob patterns to exclude from the tree.
|
|
11
|
+
* @property {boolean} [showSize=false] - Whether to show file sizes. Default is `false`.
|
|
12
|
+
* @property {string} [format="tree"] - Output format: "tree" or "list". Default is `"tree"`.
|
|
13
|
+
* @example
|
|
14
|
+
```md
|
|
15
|
+
<!-- docs fileTree src="./src" maxDepth=2 -->
|
|
16
|
+
file tree will be generated here
|
|
17
|
+
<!-- /docs -->
|
|
18
|
+
```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Generate a file tree table of contents
|
|
23
|
+
* @param {Object} api - The markdown-magic API object
|
|
24
|
+
* @returns {string} The generated file tree markdown
|
|
25
|
+
*/
|
|
26
|
+
module.exports = function fileTree(api) {
|
|
27
|
+
const { options, srcPath } = api
|
|
28
|
+
/** @type {FileTreeTransformOptions} */
|
|
29
|
+
const opts = options || {}
|
|
30
|
+
|
|
31
|
+
const targetPath = opts.src || '.'
|
|
32
|
+
const maxDepth = opts.maxDepth ? Number(opts.maxDepth) : 3
|
|
33
|
+
const includeFiles = opts.includeFiles !== false
|
|
34
|
+
const exclude = opts.exclude || []
|
|
35
|
+
const showSize = opts.showSize === true
|
|
36
|
+
const format = opts.format || 'tree'
|
|
37
|
+
|
|
38
|
+
// Resolve the target path relative to the source file
|
|
39
|
+
const fileDir = path.dirname(srcPath)
|
|
40
|
+
const resolvedPath = path.resolve(fileDir, targetPath)
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const tree = generateFileTree(resolvedPath, {
|
|
44
|
+
maxDepth,
|
|
45
|
+
includeFiles,
|
|
46
|
+
exclude,
|
|
47
|
+
showSize,
|
|
48
|
+
currentDepth: 0
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
if (format === 'list') {
|
|
52
|
+
return formatAsList(tree)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return formatAsTree(tree)
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error(`Error generating file tree for ${resolvedPath}:`, error.message)
|
|
58
|
+
return `<!-- Error: Could not generate file tree for ${targetPath} -->`
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Generate file tree structure
|
|
64
|
+
* @param {string} dirPath - Directory path to scan
|
|
65
|
+
* @param {Object} options - Options for tree generation
|
|
66
|
+
* @returns {Object} Tree structure
|
|
67
|
+
*/
|
|
68
|
+
function generateFileTree(dirPath, options) {
|
|
69
|
+
const { maxDepth, includeFiles, exclude, showSize, currentDepth } = options
|
|
70
|
+
|
|
71
|
+
if (currentDepth >= maxDepth) {
|
|
72
|
+
return { type: 'directory', name: path.basename(dirPath), children: [], truncated: true }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let items
|
|
76
|
+
try {
|
|
77
|
+
items = fs.readdirSync(dirPath)
|
|
78
|
+
} catch (error) {
|
|
79
|
+
return { type: 'directory', name: path.basename(dirPath), children: [], error: true }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Filter out excluded items
|
|
83
|
+
items = items.filter(item => {
|
|
84
|
+
// Skip hidden files and common ignored directories
|
|
85
|
+
if (item.startsWith('.') && !item.match(/\.(md|txt|json)$/)) {
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
if (['node_modules', '.git', '.DS_Store', 'dist', 'build'].includes(item)) {
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Apply custom exclude patterns
|
|
93
|
+
return !exclude.some(pattern => {
|
|
94
|
+
const regex = new RegExp(pattern.replace(/\*/g, '.*'))
|
|
95
|
+
return regex.test(item)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const children = []
|
|
100
|
+
|
|
101
|
+
for (const item of items) {
|
|
102
|
+
const itemPath = path.join(dirPath, item)
|
|
103
|
+
const stats = fs.statSync(itemPath)
|
|
104
|
+
|
|
105
|
+
if (stats.isDirectory()) {
|
|
106
|
+
const subTree = generateFileTree(itemPath, {
|
|
107
|
+
...options,
|
|
108
|
+
currentDepth: currentDepth + 1
|
|
109
|
+
})
|
|
110
|
+
children.push(subTree)
|
|
111
|
+
} else if (includeFiles) {
|
|
112
|
+
children.push({
|
|
113
|
+
type: 'file',
|
|
114
|
+
name: item,
|
|
115
|
+
size: showSize ? stats.size : undefined
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Sort: directories first, then files, alphabetically
|
|
121
|
+
children.sort((a, b) => {
|
|
122
|
+
if (a.type !== b.type) {
|
|
123
|
+
return a.type === 'directory' ? -1 : 1
|
|
124
|
+
}
|
|
125
|
+
return a.name.localeCompare(b.name)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
type: 'directory',
|
|
130
|
+
name: path.basename(dirPath) || path.basename(path.resolve(dirPath)),
|
|
131
|
+
children
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Format tree as ASCII tree structure
|
|
137
|
+
* @param {Object} tree - Tree structure
|
|
138
|
+
* @returns {string} Formatted tree
|
|
139
|
+
*/
|
|
140
|
+
function formatAsTree(tree) {
|
|
141
|
+
const lines = []
|
|
142
|
+
|
|
143
|
+
function traverse(node, prefix = '', isLast = true) {
|
|
144
|
+
const connector = isLast ? '└── ' : '├── '
|
|
145
|
+
const name = node.type === 'directory' ? `${node.name}/` : node.name
|
|
146
|
+
const size = node.size ? ` (${formatBytes(node.size)})` : ''
|
|
147
|
+
|
|
148
|
+
lines.push(`${prefix}${connector}${name}${size}`)
|
|
149
|
+
|
|
150
|
+
if (node.children && node.children.length > 0) {
|
|
151
|
+
const extension = isLast ? ' ' : '│ '
|
|
152
|
+
node.children.forEach((child, index) => {
|
|
153
|
+
const childIsLast = index === node.children.length - 1
|
|
154
|
+
traverse(child, prefix + extension, childIsLast)
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (node.truncated) {
|
|
159
|
+
const extension = isLast ? ' ' : '│ '
|
|
160
|
+
lines.push(`${prefix}${extension}...`)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
traverse(tree)
|
|
165
|
+
|
|
166
|
+
return '```\n' + lines.join('\n') + '\n```'
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Format tree as a list
|
|
171
|
+
* @param {Object} tree - Tree structure
|
|
172
|
+
* @returns {string} Formatted list
|
|
173
|
+
*/
|
|
174
|
+
function formatAsList(tree) {
|
|
175
|
+
const lines = []
|
|
176
|
+
|
|
177
|
+
function traverse(node, depth = 0) {
|
|
178
|
+
const indent = ' '.repeat(depth)
|
|
179
|
+
const name = node.type === 'directory' ? `**${node.name}/**` : node.name
|
|
180
|
+
const size = node.size ? ` *(${formatBytes(node.size)})*` : ''
|
|
181
|
+
|
|
182
|
+
lines.push(`${indent}- ${name}${size}`)
|
|
183
|
+
|
|
184
|
+
if (node.children && node.children.length > 0) {
|
|
185
|
+
node.children.forEach(child => {
|
|
186
|
+
traverse(child, depth + 1)
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (node.truncated) {
|
|
191
|
+
lines.push(`${indent} - ...`)
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
traverse(tree)
|
|
196
|
+
|
|
197
|
+
return lines.join('\n')
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Format bytes as human readable string
|
|
202
|
+
* @param {number} bytes - Bytes to format
|
|
203
|
+
* @returns {string} Formatted string
|
|
204
|
+
*/
|
|
205
|
+
function formatBytes(bytes) {
|
|
206
|
+
if (bytes === 0) return '0 B'
|
|
207
|
+
|
|
208
|
+
const k = 1024
|
|
209
|
+
const sizes = ['B', 'KB', 'MB', 'GB']
|
|
210
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
211
|
+
|
|
212
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
|
213
|
+
}
|