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.
@@ -1,461 +1,5 @@
1
- const { parseBlocks } = require('./block-parser')
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('../../utils/logs')
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
- <!-- doc-gen CODE src="./relative/path/to/code.js" -->
30
+ <!-- docs CODE src="./relative/path/to/code.js" -->
31
31
  This content will be dynamically replaced with code from the file
32
- <!-- end-doc-gen -->
32
+ <!-- /docs -->
33
33
  ```
34
-
34
+
35
35
  ```md
36
- <!-- doc-gen CODE src="./relative/path/to/code.js" lines="22-44" -->
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
- <!-- end-doc-gen -->
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
+ }