markdown-magic 2.4.0 β†’ 2.6.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/README.md CHANGED
@@ -32,7 +32,9 @@ This `README.md` is generated with `markdown-magic` [view the raw file](https://
32
32
  - [Transforms](#transforms)
33
33
  - [> TOC](#-toc)
34
34
  - [> CODE](#-code)
35
+ - [> FILE](#-file)
35
36
  - [> REMOTE](#-remote)
37
+ - [Inline transforms](#inline-transforms)
36
38
  - [πŸ”Œ Markdown magic plugins](#-markdown-magic-plugins)
37
39
  - [Adding Custom Transforms](#adding-custom-transforms)
38
40
  - [Plugin Example](#plugin-example)
@@ -189,6 +191,24 @@ Default `MATCHWORD` is `AUTO-GENERATED-CONTENT`
189
191
 
190
192
  ---
191
193
 
194
+ ### > FILE
195
+
196
+ Get local file contents.
197
+
198
+ **Options:**
199
+ - `src`: The relative path to the file to pull in
200
+
201
+ **Example:**
202
+ ```md
203
+ <!-- AUTO-GENERATED-CONTENT:START (FILE:src=./path/to/file) -->
204
+ This content will be dynamically replaced from the local file
205
+ <!-- AUTO-GENERATED-CONTENT:END -->
206
+ ```
207
+
208
+ Default `MATCHWORD` is `AUTO-GENERATED-CONTENT`
209
+
210
+ ---
211
+
192
212
  ### > REMOTE
193
213
 
194
214
  Get any remote Data and put in markdown
@@ -199,7 +219,7 @@ Get any remote Data and put in markdown
199
219
  **Example:**
200
220
  ```md
201
221
  <!-- AUTO-GENERATED-CONTENT:START (REMOTE:url=http://url-to-raw-md-file.md) -->
202
- This content will be dynamically replace from the remote url
222
+ This content will be dynamically replaced from the remote url
203
223
  <!-- AUTO-GENERATED-CONTENT:END -->
204
224
  ```
205
225
 
@@ -208,6 +228,17 @@ Default `MATCHWORD` is `AUTO-GENERATED-CONTENT`
208
228
  ---
209
229
  <!-- ⛔️ MD-MAGIC-EXAMPLE:END - Do not remove or modify this section -->
210
230
 
231
+ ## Inline transforms
232
+
233
+ Any transform, including custom transforms can be used inline as well to insert content into paragraphs and other places.
234
+
235
+ The face symbol πŸ‘‰ <!-- MD-MAGIC-EXAMPLE:START (INLINE_EXAMPLE) -->**βŠ‚β—‰β€Ώβ—‰γ€**<!-- MD-MAGIC-EXAMPLE:END --> is auto generated inline.
236
+
237
+ **Example:**
238
+ ```md
239
+ <!-- AUTO-GENERATED-CONTENT:START (FILE:src=./path/to/file) -->xyz<!-- AUTO-GENERATED-CONTENT:END -->
240
+ ```
241
+
211
242
  ## πŸ”Œ Markdown magic plugins
212
243
 
213
244
  * [wordcount](https://github.com/DavidWells/markdown-magic-wordcount/) - Add wordcount to markdown files
@@ -266,6 +297,9 @@ const config = {
266
297
  })
267
298
  return updatedContent.replace(/^\s+|\s+$/g, '')
268
299
  },
300
+ INLINE_EXAMPLE: () => {
301
+ return '**βŠ‚β—‰β€Ώβ—‰γ€**'
302
+ },
269
303
  /* Match <!-- AUTO-GENERATED-CONTENT:START (pluginExample) --> */
270
304
  pluginExample: require('./plugin-example')({ addNewLine: true }),
271
305
  /* Include plugins from NPM */
@@ -359,3 +393,8 @@ This section was generated by the cli config markdown.config.js file
359
393
 
360
394
  - [Project using markdown-magic](https://github.com/search?o=desc&q=filename%3Apackage.json+%22markdown-magic%22&s=indexed&type=Code)
361
395
  - [Examples in md](https://github.com/search?l=Markdown&o=desc&q=AUTO-GENERATED-CONTENT&s=indexed&type=Code)
396
+
397
+
398
+ ## Misc Markdown helpers
399
+
400
+ - https://github.com/azu/markdown-function
package/cli.js CHANGED
File without changes
package/lib/index.js ADDED
@@ -0,0 +1,219 @@
1
+ const path = require('path')
2
+ const fs = require('fs').promises
3
+ const { getTextBetween, parseBlocks } = require('./utils/new-parser')
4
+
5
+ async function processContents({
6
+ filePath,
7
+ syntax,
8
+ open,
9
+ close,
10
+ transforms,
11
+ beforeMiddelwares,
12
+ afterMiddelwares,
13
+ DEBUG = false
14
+ }) {
15
+ const syntaxType = syntax || path.extname(filePath).replace(/^\./, '')
16
+ const originalContents = await fs.readFile(filePath, 'utf8')
17
+
18
+ const foundBlocks = parseBlocks(originalContents, {
19
+ syntax: syntaxType,
20
+ open,
21
+ close,
22
+ })
23
+
24
+ if (DEBUG) {
25
+ console.log('foundBlocks')
26
+ console.log(foundBlocks)
27
+ }
28
+
29
+ const transformsToRun = sortPlugins(foundBlocks.transforms)
30
+ // console.log('transformsToRun', transformsToRun)
31
+
32
+ // if (!transformsToRun.length) {
33
+ // process.exit(1)
34
+ // }
35
+
36
+ let missingTransforms = []
37
+ const updatedContents = await transformsToRun.reduce(async (updatedContent, ogmatch) => {
38
+ const md = await updatedContent
39
+ /* Apply Before middleware to all transforms */
40
+ const match = await applyMiddleware(ogmatch, md, beforeMiddelwares)
41
+ const { block, raw, transform, args } = match
42
+ const { openTag, closeTag, content, contentStart, contentEnd, start, end } = block
43
+
44
+ /* Run transform plugins */
45
+ let tempContent = content
46
+ if (transforms[transform]) {
47
+ tempContent = await transforms[transform](match, md)
48
+ }
49
+
50
+ /* Apply After middleware to all transforms */
51
+ const afterContent = await applyMiddleware({
52
+ ...match,
53
+ ...{
54
+ block: {
55
+ ...match.block,
56
+ content: tempContent
57
+ }
58
+ }
59
+ }, md, afterMiddelwares)
60
+ if (DEBUG) {
61
+ console.log('afterContent', afterContent)
62
+ }
63
+
64
+ if (!transforms[transform]) {
65
+ missingTransforms.push(afterContent)
66
+ console.log(`Missing "${transform}" transform`)
67
+ }
68
+
69
+ const newContent = afterContent.block.content
70
+ const formattedNewContent = (args.noTrim) ? newContent : smartTrim(newContent)
71
+ /* Remove any conflicting imported comments */
72
+ // const fix = removeConflictingComments(formattedNewContent, foundBlocks.commentOpen, foundBlocks.commentClose)
73
+ const fix = stripAllComments(formattedNewContent, foundBlocks.commentOpen, foundBlocks.commentClose)
74
+ // console.log('foundBlocks.commentClose', foundBlocks.commentClose)
75
+ // console.log('formattedNewContent', formattedNewContent)
76
+ // console.log('fix', fix)
77
+ const preserveIndent = (true || args.preserveIndent) ? block.indentation.length + block.contentIndent : block.indentation.length
78
+ const indent = indentString(fix, preserveIndent)
79
+ const newCont = `${openTag}${indent}${closeTag}`
80
+ /* Replace original contents */
81
+ const newContents = md.replace(raw.block, newCont)
82
+ return Promise.resolve(newContents)
83
+ }, Promise.resolve(originalContents))
84
+
85
+ if (DEBUG) {
86
+ console.log('Output Markdown')
87
+ console.log(updatedContents)
88
+ }
89
+
90
+ /*
91
+ if (missingTransforms.length) {
92
+ console.log('missingTransforms', missingTransforms)
93
+ let matchOne = missingTransforms[1]
94
+ matchOne = missingTransforms[missingTransforms.length - 1]
95
+ // console.log('matchOne', matchOne)
96
+ const { block, transform, args } = matchOne
97
+ const { openTag, closeTag, content, contentStart, contentEnd, start, end} = block
98
+
99
+ // console.log('contentStart', contentStart)
100
+ // console.log('contentEnd', contentEnd)
101
+ // console.log('original text between', `"${getTextBetween(md, contentStart, contentEnd)}"`)
102
+ // console.log('original block between', `"${getTextBetween(md, start, end)}"`)
103
+ }
104
+ /** */
105
+
106
+ return {
107
+ filePath,
108
+ transforms: transformsToRun,
109
+ missingTransforms,
110
+ originalContents,
111
+ updatedContents
112
+ }
113
+ }
114
+
115
+ function applyMiddleware(data, md, middlewares) {
116
+ return middlewares.reduce(async (acc, curr) => {
117
+ const realAcc = await acc
118
+ // console.log(`Running "${curr.name}" Middleware on "${realAcc.transform}" block`)
119
+ const updatedContent = await curr.transform(realAcc, md)
120
+ // realAcc.block.content = updatedContent
121
+ return Promise.resolve({
122
+ ...realAcc,
123
+ ...{
124
+ block: {
125
+ ...realAcc.block,
126
+ content: updatedContent
127
+ }
128
+ }
129
+ })
130
+ }, Promise.resolve(data))
131
+ }
132
+
133
+ /**
134
+ * Trim leading & trailing spaces/line breaks in code and keeps the indentation of the first non-empty line
135
+ * @param {string} str
136
+ * @returns string
137
+ */
138
+ function smartTrim(str) {
139
+ return str.replace(/^(?:[\t ]*(?:\r?\n|\r))+|\s+$/g, '')
140
+ }
141
+
142
+ function stripAllComments(block) {
143
+ const pattern = new RegExp(`([^\\s]*)?([ \\t]*)?(<!-+\\s?([\\s\\S]*?)?-+>)([^\\s<]*)?(\n{1,2})?`, 'gi')
144
+
145
+ // console.log('closeTagRegex', closeTagRegex)
146
+ let matches
147
+ let remove = []
148
+ while ((matches = pattern.exec(block)) !== null) {
149
+ if (matches.index === pattern.lastIndex) {
150
+ pattern.lastIndex++ // avoid infinite loops with zero-width matches
151
+ }
152
+ const [ match, leadingText, leadingSpace, comment, insideComment, trailingText, trailingNewLine ] = matches
153
+ /*
154
+ console.log('match', match)
155
+ console.log('leadingText', leadingText)
156
+ console.log('leadingSpace', leadingSpace)
157
+ console.log('comment', comment)
158
+ console.log('insideComment', insideComment)
159
+ console.log('trailingText', trailingText)
160
+ console.log('trailingNewLine', trailingNewLine)
161
+ /** */
162
+ const newLineCount = (trailingNewLine || '').length
163
+ const trailing = (!trailingText && newLineCount > 1) ? `${trailingNewLine || ''}` : ''
164
+ const leading = (leadingSpace) ? leadingSpace.slice(1) : ''
165
+ remove.push(`${leading}${comment}${trailing}`)
166
+ }
167
+ return remove.reduce((acc, curr) => {
168
+ return acc.replaceAll(curr, '')
169
+ }, block)
170
+ }
171
+
172
+ /**
173
+ * Remove conflicting comments that might have been inserted from transforms
174
+ * @param {*} content
175
+ * @param {*} openPattern
176
+ * @param {*} closePattern
177
+ * @returns
178
+ */
179
+ function removeConflictingComments(content, openPattern, closePattern) {
180
+ const removeOpen = content.replace(openPattern, '')
181
+ // TODO this probably needs to be a loop for larger blocks
182
+ closePattern.lastIndex = 0; // reset regex
183
+ const hasClose = closePattern.exec(content)
184
+ // console.log('closePattern', closePattern)
185
+ // console.log('has', content)
186
+ // console.log('hasClose', hasClose)
187
+ if (!hasClose) {
188
+ return removeOpen
189
+ }
190
+ const closeTag = `${hasClose[2]}${hasClose[3] || ''}`
191
+ // console.log('closeTag', closeTag)
192
+ return removeOpen
193
+ .replaceAll(closeTag, '')
194
+ /* Trailing new line */
195
+ .replace(/\n$/, '')
196
+ }
197
+
198
+ function sortPlugins(data) {
199
+ return data.sort((a, b) => {
200
+ // put table of contents (TOC) at end of tranforms
201
+ if (a.transform === 'TOC') return 1
202
+ if (b.transform === 'TOC') return -1
203
+ return 0
204
+ })
205
+ }
206
+
207
+ function indentString(string, count = 1, options = {}) {
208
+ const {
209
+ indent = ' ',
210
+ includeEmptyLines = false
211
+ } = options;
212
+ if (count === 0) return string
213
+ const regex = includeEmptyLines ? /^/gm : /^(?!\s*$)/gm
214
+ return string.replace(regex, indent.repeat(count))
215
+ }
216
+
217
+ module.exports = {
218
+ processContents
219
+ }
@@ -38,7 +38,7 @@ module.exports = async function processFile(filePath, config) {
38
38
  matchWord: 'AUTO-GENERATED-CONTENT',
39
39
  /**
40
40
  * - `DEBUG` - *Boolean* - (optional) set debug flag to `true` to inspect the process
41
- * @type {string}
41
+ * @type {boolean}
42
42
  */
43
43
  DEBUG: false,
44
44
  }
@@ -55,6 +55,7 @@ module.exports = async function processFile(filePath, config) {
55
55
  mergedConfig.outputContent = content
56
56
 
57
57
  const regex = regexUtils.matchCommentBlock(mergedConfig.matchWord)
58
+ // console.log(regex)
58
59
  const match = content.match(regex)
59
60
  const transformsFound = []
60
61
 
@@ -0,0 +1,37 @@
1
+ const path = require('path')
2
+ const fs = require('fs')
3
+ const isLocalPath = require('is-local-path')
4
+
5
+ module.exports = function FILE(content, options, config) {
6
+ let fileContents
7
+ let syntax = options.syntax
8
+ if (!options.src) {
9
+ return false
10
+ }
11
+ if (isLocalPath(options.src)) {
12
+ const fileDir = path.dirname(config.originalPath)
13
+ const filePath = path.join(fileDir, options.src)
14
+ try {
15
+ fileContents = fs.readFileSync(filePath, 'utf8', (err, contents) => {
16
+ if (err) {
17
+ console.log(`FILE NOT FOUND ${filePath}`)
18
+ console.log(err)
19
+ // throw err
20
+ }
21
+ return contents
22
+ })
23
+ } catch (e) {
24
+ console.log(`FILE NOT FOUND ${filePath}`)
25
+ throw e
26
+ }
27
+ if (!syntax) {
28
+ syntax = path.extname(filePath).replace(/^./, '')
29
+ }
30
+ }
31
+
32
+ // trim leading and trailing spaces/line breaks in code and keeps the indentation of the first non-empty line
33
+ fileContents = fileContents.replace(/^(?:[\t ]*(?:\r?\n|\r))+|\s+$/g, '')
34
+
35
+ return `<!-- The below content is automatically added from ${options.src} -->
36
+ ${fileContents}`
37
+ }
@@ -1,4 +1,5 @@
1
1
  const code = require('./code')
2
+ const file = require('./file')
2
3
  const remoteContent = require('./remote')
3
4
  const toc = require('./toc')
4
5
 
@@ -62,6 +63,29 @@ const transforms = {
62
63
  * @return {string} Updated inner contents of the comment block
63
64
  */
64
65
  CODE: code,
66
+ /**
67
+ * ### > FILE
68
+ *
69
+ * Get local file contents.
70
+ *
71
+ * **Options:**
72
+ * - `src`: The relative path to the file to pull in
73
+ *
74
+ * **Example:**
75
+ * ```md
76
+ * <!-- AUTO-GENERATED-CONTENT:START (FILE:src=./path/to/file) -->
77
+ * This content will be dynamically replaced from the local file
78
+ * <!-- AUTO-GENERATED-CONTENT:END -->
79
+ * ```
80
+ *
81
+ * Default `MATCHWORD` is `AUTO-GENERATED-CONTENT`
82
+ *
83
+ * ---
84
+ * @param {string} content The current content of the comment block
85
+ * @param {object} options The options passed in from the comment declaration
86
+ * @return {string} Updated content to place in the content block
87
+ */
88
+ FILE: file,
65
89
  /**
66
90
  * ### > REMOTE
67
91
  *
@@ -73,7 +97,7 @@ const transforms = {
73
97
  * **Example:**
74
98
  * ```md
75
99
  * <!-- AUTO-GENERATED-CONTENT:START (REMOTE:url=http://url-to-raw-md-file.md) -->
76
- * This content will be dynamically replace from the remote url
100
+ * This content will be dynamically replaced from the remote url
77
101
  * <!-- AUTO-GENERATED-CONTENT:END -->
78
102
  * ```
79
103
  *
@@ -66,13 +66,14 @@ module.exports = async function TOC(content, options, config) {
66
66
  const [ lineText, lineIndex, isFirst, matchText ] = firstHeadingData
67
67
 
68
68
  // verify its h1
69
-
69
+ const matchTextEscaped = escapeStringRegexp(matchText)
70
+ // console.log('matchTextEscaped', matchTextEscaped)
70
71
  // /^#{1}\s+(.*)/
71
- const FIRST_H1_REGEX = new RegExp(`^#\\s*${matchText}`, 'gim')
72
+ const FIRST_H1_REGEX = new RegExp(`^#\\s*\\[?${matchTextEscaped}\\]?(?:.*)?`, 'gim')
72
73
  // /^<h1\b[^>]*>([\s\S]*?)<\/h1>/
73
- const FIRST_HTML_H1_REGEX = new RegExp(`^<h1\\b[^>]*>[\\s]*?(${matchText})[\\s]*?<\\/h1>`, 'gim')
74
+ const FIRST_HTML_H1_REGEX = new RegExp(`^<h1\\b[^>]*>[\\s]*?(${matchTextEscaped})[\\s]*?<\\/h1>`, 'gim')
74
75
  // /^(.*)\n={3,}/
75
- const FIRST_UNDERSCORE_H1 = new RegExp(`^(${matchText})\n={3,}`, 'gim')
76
+ const FIRST_UNDERSCORE_H1 = new RegExp(`^(${matchTextEscaped})\n={3,}`, 'gim')
76
77
 
77
78
  let docHasHeading = false
78
79
  if (contents.match(FIRST_H1_REGEX)) {
@@ -245,7 +246,21 @@ function parseHtmlProps(mdContents) {
245
246
  return tags
246
247
  }
247
248
 
249
+ function escapeStringRegexp(string) {
250
+ if (typeof string !== 'string') {
251
+ throw new TypeError('Expected a string');
252
+ }
253
+
254
+ // Escape characters with special meaning either inside or outside character sets.
255
+ // Use a simple backslash escape when it’s always valid, and a `\xnn` escape when the simpler form would be disallowed by Unicode patterns’ stricter grammar.
256
+ return string
257
+ .replace(/[|\\{}()[\]^$+*?.]/g, '\\$&')
258
+ .replace(/-/g, '\\x2d');
259
+ }
260
+
248
261
  function removeUndefined(output) {
249
262
  // remove undefined from new line and start of string if first H1 is missing
250
263
  return output.replace(/\nundefined/g, '\n-').replace(/^undefined/g, '-')
251
264
  }
265
+
266
+ // Alt https://github.com/thlorenz/doctoc
@@ -42,12 +42,18 @@ module.exports = async function updateContents(block, config) {
42
42
  if (!newContent) {
43
43
  newContent = originalContent
44
44
  }
45
-
46
- return `${openingTag.openTag}
45
+
46
+ // If original block or new content contains a new line, preserve it
47
+ if (block.indexOf('\n') > -1 || newContent.indexOf('\n') > -1) {
48
+ return `${openingTag.openTag}
47
49
  ${newContent}
48
50
  ${closingTag.closeTag}`
49
51
  }
50
52
 
53
+ // Block has no newline, inline contents
54
+ return `${openingTag.openTag}${newContent}${closingTag.closeTag}`
55
+ }
56
+
51
57
  function parseOptions(options) {
52
58
  if (!options) {
53
59
  return null
@@ -0,0 +1,63 @@
1
+ const { test } = require('uvu')
2
+ const assert = require('uvu/assert')
3
+ const { parseBlocks, replaceContent } = require('./new-parser')
4
+
5
+ const defaultOpts = {
6
+ syntax: 'md',
7
+ open: 'DOCS:START',
8
+ close: 'DOCS:END',
9
+ }
10
+
11
+ test('Returns empty array', () => {
12
+ assert.equal(parseBlocks('', defaultOpts).transforms, [])
13
+ assert.equal(parseBlocks(' ', defaultOpts).transforms, [])
14
+ assert.equal(parseBlocks(`
15
+
16
+
17
+ `, defaultOpts).transforms, [])
18
+ assert.equal(parseBlocks(`
19
+ # No block in here
20
+
21
+ nope
22
+ `, defaultOpts).transforms, [])
23
+ })
24
+
25
+ const md = `
26
+ Very nice
27
+
28
+ <!-- DOCS:START(TOC) foo={{ rad: 'orange' }} ------>
29
+ ok
30
+ <!-- DOCS:END -->`
31
+
32
+ test('Parse md blocks', () => {
33
+ const parsedValue = parseBlocks(md, defaultOpts)
34
+ console.log('parsedValue', parsedValue)
35
+ assert.equal(parsedValue.transforms, [
36
+ {
37
+ transform: 'TOC',
38
+ args: { foo: { rad: 'orange' } },
39
+ block: {
40
+ indentation: '',
41
+ start: 12,
42
+ end: 85,
43
+ contentStart: 64,
44
+ contentEnd: 68,
45
+ contentIndent: 0,
46
+ openTag: "<!-- DOCS:START(TOC) foo={{ rad: 'orange' }} ------>\n",
47
+ content: 'ok',
48
+ closeTag: '\n<!-- DOCS:END -->'
49
+ },
50
+ raw: {
51
+ transform: '(TOC)',
52
+ args: "foo={{ rad: 'orange' }} ----",
53
+ content: '\nok\n',
54
+ block: "<!-- DOCS:START(TOC) foo={{ rad: 'orange' }} ------>\n" +
55
+ 'ok\n' +
56
+ '<!-- DOCS:END -->'
57
+ },
58
+ meta: { isMultiline: true }
59
+ }
60
+ ], '')
61
+ })
62
+
63
+ test.run()