markdown-magic 4.8.0 → 4.10.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 (66) hide show
  1. package/package.json +7 -7
  2. package/src/argparse/README.md +3 -8
  3. package/src/argparse/argparse.js +7 -313
  4. package/src/argparse/argparse.test.js +168 -90
  5. package/src/argparse/index.js +2 -2
  6. package/src/argparse/splitOutsideQuotes.js +3 -75
  7. package/src/cli-run.js +97 -166
  8. package/src/cli-run.test.js +71 -3
  9. package/src/globparse.js +14 -159
  10. package/src/index.dependency-graph.test.js +49 -0
  11. package/src/index.js +41 -11
  12. package/src/transforms/code/index.js +19 -13
  13. package/src/transforms/index.js +12 -1
  14. package/src/utils/format-md.js +165 -12
  15. package/src/utils/format-md.test.js +175 -0
  16. package/src/utils/remoteRequest.js +30 -6
  17. package/types/src/argparse/argparse.d.ts +2 -32
  18. package/types/src/argparse/argparse.d.ts.map +1 -1
  19. package/types/src/argparse/index.d.ts +2 -2
  20. package/types/src/argparse/splitOutsideQuotes.d.ts +2 -1
  21. package/types/src/argparse/splitOutsideQuotes.d.ts.map +1 -1
  22. package/types/src/cli-run.d.ts +4 -2
  23. package/types/src/cli-run.d.ts.map +1 -1
  24. package/types/src/globparse.d.ts +11 -17
  25. package/types/src/globparse.d.ts.map +1 -1
  26. package/types/src/index.d.ts +36 -9
  27. package/types/src/index.d.ts.map +1 -1
  28. package/types/src/transforms/code/index.d.ts.map +1 -1
  29. package/types/src/utils/format-md.d.ts +12 -0
  30. package/types/src/utils/format-md.d.ts.map +1 -1
  31. package/types/src/utils/fs.d.ts +1 -1
  32. package/types/src/utils/remoteRequest.d.ts.map +1 -1
  33. package/types/_tests/config.d.ts +0 -4
  34. package/types/_tests/config.d.ts.map +0 -1
  35. package/types/_tests/errors.test.d.ts +0 -2
  36. package/types/_tests/errors.test.d.ts.map +0 -1
  37. package/types/_tests/fixtures/js/simple.d.ts +0 -8
  38. package/types/_tests/fixtures/js/simple.d.ts.map +0 -1
  39. package/types/_tests/fixtures/local-code-file-lines.d.ts +0 -1
  40. package/types/_tests/fixtures/local-code-file-lines.d.ts.map +0 -1
  41. package/types/_tests/fixtures/local-code-file.d.ts +0 -2
  42. package/types/_tests/fixtures/local-code-file.d.ts.map +0 -1
  43. package/types/_tests/fixtures/local-code-id.d.ts +0 -3
  44. package/types/_tests/fixtures/local-code-id.d.ts.map +0 -1
  45. package/types/_tests/transforms-toc.test.d.ts +0 -2
  46. package/types/_tests/transforms-toc.test.d.ts.map +0 -1
  47. package/types/_tests/transforms.test.d.ts +0 -2
  48. package/types/_tests/transforms.test.d.ts.map +0 -1
  49. package/types/_tests/utils/diff.d.ts +0 -3
  50. package/types/_tests/utils/diff.d.ts.map +0 -1
  51. package/types/src/argparse/argparse.test.d.ts +0 -2
  52. package/types/src/argparse/argparse.test.d.ts.map +0 -1
  53. package/types/src/argparse/splitOutsideQuotes.test.d.ts +0 -2
  54. package/types/src/argparse/splitOutsideQuotes.test.d.ts.map +0 -1
  55. package/types/src/cli-run.test.d.ts +0 -2
  56. package/types/src/cli-run.test.d.ts.map +0 -1
  57. package/types/src/globparse.test.d.ts +0 -2
  58. package/types/src/globparse.test.d.ts.map +0 -1
  59. package/types/src/index.test.d.ts +0 -2
  60. package/types/src/index.test.d.ts.map +0 -1
  61. package/types/src/transforms/code/resolve-github-file.test.d.ts +0 -2
  62. package/types/src/transforms/code/resolve-github-file.test.d.ts.map +0 -1
  63. package/types/src/utils/fs.test.d.ts +0 -2
  64. package/types/src/utils/fs.test.d.ts.map +0 -1
  65. package/types/src/utils/text.test.d.ts +0 -2
  66. package/types/src/utils/text.test.d.ts.map +0 -1
package/src/index.js CHANGED
@@ -344,22 +344,24 @@ async function markdownMagic(globOrOpts = {}, options = {}) {
344
344
  name: file,
345
345
  id: file,
346
346
  srcPath: file,
347
- blocks: foundBlocks.blocks
347
+ blocks: foundBlocks.blocks,
348
+ parsedBlocks: foundBlocks
348
349
  }
349
350
  })
350
351
 
351
352
  const blocks = blocksByPath.map((item) => {
352
353
  const dir = path.dirname(item.srcPath)
353
- item.dependencies = []
354
+ const dependencySet = new Set()
354
355
  item.blocks.forEach((block) => {
355
356
  if (block.options && block.options.src) {
356
357
  const resolvedPath = path.resolve(dir, block.options.src)
357
358
  // if (resolvedPath.match(/\.md$/)) {
358
359
  // console.log('resolvedPath', resolvedPath)
359
- item.dependencies = item.dependencies.concat(resolvedPath)
360
+ dependencySet.add(resolvedPath)
360
361
  //}
361
362
  }
362
363
  })
364
+ item.dependencies = Array.from(dependencySet)
363
365
  return item
364
366
  })
365
367
 
@@ -381,23 +383,23 @@ async function markdownMagic(globOrOpts = {}, options = {}) {
381
383
  /** */
382
384
 
383
385
  // Convert items into a format suitable for toposort
384
- const graph = blocks
385
- .filter((item) => item.blocks && item.blocks.length)
386
- .map((item) => {
387
- return [ item.id, ...item.dependencies ]
388
- })
386
+ const { itemsWithBlocks, itemIds, graph } = createDependencyGraph(blocks)
389
387
  // console.log('graph', graph)
390
388
  // Perform the topological sort and reverse for execution order
391
- const sortedIds = toposort(graph).reverse();
389
+ const sortedIds = graph.length ? toposort.array(itemIds, graph).reverse() : itemIds
392
390
 
393
391
  // Reorder items based on sorted ids
394
- const sortedItems = sortedIds.map(id => blocks.find(item => item.id === id)).filter(Boolean);
392
+ const itemById = new Map(itemsWithBlocks.map((item) => [item.id, item]))
393
+ const sortedItems = sortedIds.map((id) => itemById.get(id)).filter(Boolean)
395
394
 
396
395
  // topoSort(blocks)
397
396
  const orderedFiles = sortedItems.map((block) => block.id)
398
397
  // console.log('sortedItems', sortedItems)
399
398
  // console.log('orderedFiles', orderedFiles)
400
399
 
400
+ const fileContentByPath = new Map(files.map((file, i) => [file, fileContents[i]]))
401
+ const parsedBlocksByPath = new Map(blocksByPath.map((item) => [item.id, item.parsedBlocks]))
402
+
401
403
  const processedFiles = []
402
404
  await asyncForEach(orderedFiles, async (file) => {
403
405
  // logger('file', file)
@@ -424,6 +426,8 @@ async function markdownMagic(globOrOpts = {}, options = {}) {
424
426
  // logger('newPath', newPath)
425
427
  const result = await processFile({
426
428
  ...opts,
429
+ content: fileContentByPath.get(file),
430
+ parsedBlocks: parsedBlocksByPath.get(file),
427
431
  patterns,
428
432
  open,
429
433
  close,
@@ -787,6 +791,29 @@ function changedFiles(files) {
787
791
  return files.filter(({ isChanged }) => isChanged)
788
792
  }
789
793
 
794
+ /**
795
+ * Create graph data for deterministic dependency ordering
796
+ * @param {Array<{id: string, blocks: Array<any>, dependencies?: Array<string>}>} blockItems
797
+ * @returns {{itemsWithBlocks: Array<any>, itemIds: Array<string>, graph: Array<[string, string]>}}
798
+ */
799
+ function createDependencyGraph(blockItems = []) {
800
+ const itemsWithBlocks = blockItems.filter((item) => item.blocks && item.blocks.length)
801
+ const itemIds = itemsWithBlocks.map((item) => item.id)
802
+ const itemIdSet = new Set(itemIds)
803
+ const graph = itemsWithBlocks
804
+ .flatMap((item) => {
805
+ return (item.dependencies || [])
806
+ .filter((dependency) => itemIdSet.has(dependency))
807
+ .map((dependency) => [item.id, dependency])
808
+ })
809
+
810
+ return {
811
+ itemsWithBlocks,
812
+ itemIds,
813
+ graph
814
+ }
815
+ }
816
+
790
817
  async function asyncForEach(array, callback) {
791
818
  for (let index = 0; index < array.length; index++) {
792
819
  await callback(array[index], index, array)
@@ -802,5 +829,8 @@ module.exports = {
802
829
  parseMarkdown,
803
830
  blockTransformer,
804
831
  processFile,
805
- stringUtils
832
+ stringUtils,
833
+ __private: {
834
+ createDependencyGraph
835
+ }
806
836
  }
@@ -158,23 +158,29 @@ module.exports = async function CODE(api) {
158
158
 
159
159
  /* Check for Id */
160
160
  if (id) {
161
- const lines = code.split("\n")
162
- const startLineIndex = lines.findIndex(line => line.includes(`CODE_SECTION:${id}:START`));
163
- const startLine = startLineIndex !== -1 ? startLineIndex : 0;
161
+ const lines = code.split('\n')
162
+ const startLineIndex = lines.findIndex((line) => line.includes(`CODE_SECTION:${id}:START`))
163
+ const endLineIndex = lines.findIndex((line) => line.includes(`CODE_SECTION:${id}:END`))
164
+
165
+ if (startLineIndex === -1 || endLineIndex === -1) {
166
+ throw new Error(`Missing ${id} code section from ${codeFilePath}`)
167
+ }
168
+
169
+ if (endLineIndex <= startLineIndex) {
170
+ throw new Error(`Invalid ${id} code section in ${codeFilePath}. End marker must be after start marker`)
171
+ }
164
172
 
165
- const endLineIndex = lines.findIndex(line => line.includes(`CODE_SECTION:${id}:END`));
166
- const endLine = endLineIndex !== -1 ? endLineIndex : lines.length - 1;
167
173
  // console.log('startLine', startLine)
168
174
  // console.log('endLine', endLine)
169
- if (startLine === -1 && endLine === -1) {
170
- throw new Error(`Missing ${id} code section from ${codeFilePath}`)
175
+ const selectedLines = lines.slice(startLineIndex + 1, endLineIndex)
176
+
177
+ if (!selectedLines.length) {
178
+ throw new Error(`Empty ${id} code section in ${codeFilePath}`)
171
179
  }
172
-
173
- const selectedLines = lines.slice(startLine + 1, endLine)
174
-
175
- const firstMatch = selectedLines[0] && selectedLines[0].match(/^(\s*)/);
176
- const trimBy = firstMatch && firstMatch[1] ? firstMatch[1].length : 0;
177
- const newValue = `${selectedLines.map(line => line.substring(trimBy).replace(/^\/\/ CODE_SECTION:INCLUDE /g, "")).join("\n")}`
180
+
181
+ const firstMatch = selectedLines[0] && selectedLines[0].match(/^(\s*)/)
182
+ const trimBy = firstMatch && firstMatch[1] ? firstMatch[1].length : 0
183
+ const newValue = `${selectedLines.map((line) => line.substring(trimBy).replace(/^\/\/ CODE_SECTION:INCLUDE /g, '')).join('\n')}`
178
184
  // console.log('newValue', newValue)
179
185
  code = newValue
180
186
  }
@@ -74,6 +74,11 @@ const transforms = {
74
74
  *
75
75
  * **Options:**
76
76
  * - `src`: The relative path to the file to pull in
77
+ * - `sections`: Comma-separated list or array of markdown section headings to include
78
+ * - `section`: Single markdown section heading to include
79
+ * - `headings`: Array of markdown heading levels to include, such as `headings={[2,3]}`
80
+ * - `removeLeadingH1`: Remove the first H1 from imported markdown
81
+ * - `shiftHeaders`: Shift imported markdown headings up or down by a number
77
82
  *
78
83
  * **Example:**
79
84
  * ```md
@@ -97,6 +102,12 @@ const transforms = {
97
102
  *
98
103
  * **Options:**
99
104
  * - `url`: The URL of the remote content to pull in
105
+ * - `src`: Alias for `url`
106
+ * - `sections`: Comma-separated list or array of markdown section headings to include
107
+ * - `section`: Single markdown section heading to include
108
+ * - `headings`: Array of markdown heading levels to include, such as `headings={[2,3]}`
109
+ * - `removeLeadingH1`: Remove the first H1 from imported markdown
110
+ * - `shiftHeaders`: Shift imported markdown headings up or down by a number
100
111
  *
101
112
  * **Example:**
102
113
  * ```md
@@ -208,4 +219,4 @@ const transforms = {
208
219
  install: install,
209
220
  }
210
221
 
211
- module.exports = transforms
222
+ module.exports = transforms
@@ -3,6 +3,17 @@ const { removeLeadingH1 } = require('@davidwells/md-utils/string-utils')
3
3
 
4
4
  function formatMd(content, options = {}) {
5
5
  let fileContents = content
6
+
7
+ /* automatically trim frontmatter if file is markdown */
8
+ if (options.trimFrontmatter !== false) {
9
+ const frontmatter = findFrontmatter(fileContents)
10
+ if (frontmatter && frontmatter.frontMatterRaw) {
11
+ fileContents = fileContents.replace(frontmatter.frontMatterRaw, '')
12
+ }
13
+ }
14
+
15
+ fileContents = selectMarkdownSections(fileContents, options)
16
+
6
17
  if (options.removeLeadingH1 || options.stripFirstH1) {
7
18
  fileContents = removeLeadingH1(fileContents)
8
19
  }
@@ -10,24 +21,166 @@ function formatMd(content, options = {}) {
10
21
  // Shift headers up or down by the specified number of levels if shiftHeaders is enabled and file is markdown
11
22
  if (options.shiftHeaders) {
12
23
  fileContents = fileContents.replace(/^(#{1,6})\s/gm, (match, hashes) => {
13
- const currentLevel = hashes.length;
14
- const shiftAmount = Number(options.shiftHeaders);
15
- const newLevel = Math.max(1, Math.min(6, currentLevel + shiftAmount));
16
- return '#'.repeat(newLevel) + ' ';
24
+ const currentLevel = hashes.length
25
+ const shiftAmount = Number(options.shiftHeaders)
26
+ const newLevel = Math.max(1, Math.min(6, currentLevel + shiftAmount))
27
+ return '#'.repeat(newLevel) + ' '
17
28
  })
18
29
  }
19
30
 
20
- /* automatically trim frontmatter if file is markdown */
21
- if (options.trimFrontmatter !== false) {
22
- const frontmatter = findFrontmatter(fileContents)
23
- if (frontmatter && frontmatter.frontMatterRaw) {
24
- fileContents = fileContents.replace(frontmatter.frontMatterRaw, '')
31
+ return fileContents
32
+ }
33
+
34
+ function selectMarkdownSections(content, options = {}) {
35
+ const requestedSections = [
36
+ ...coerceListOption(options.section),
37
+ ...coerceListOption(options.sections)
38
+ ]
39
+ const requestedHeadings = [
40
+ ...coerceHeadingLevels(options.heading),
41
+ ...coerceHeadingLevels(options.headings)
42
+ ]
43
+
44
+ if (!requestedSections.length && !requestedHeadings.length) {
45
+ return content
46
+ }
47
+
48
+ const headings = parseMarkdownHeadings(content)
49
+ const sectionNames = requestedSections.map(normalizeHeadingText)
50
+ const ranges = []
51
+ const missingSections = new Set(sectionNames)
52
+
53
+ headings.forEach((heading) => {
54
+ if (sectionNames.includes(heading.normalizedText)) {
55
+ ranges.push({ start: heading.start, end: heading.end })
56
+ missingSections.delete(heading.normalizedText)
57
+ }
58
+
59
+ if (requestedHeadings.includes(heading.level)) {
60
+ ranges.push({ start: heading.start, end: heading.end })
25
61
  }
62
+ })
63
+
64
+ if (missingSections.size && !options.allowMissingSections) {
65
+ const missing = Array.from(missingSections).join(', ')
66
+ throw new Error(`Missing markdown section${missingSections.size > 1 ? 's' : ''}: ${missing}`)
26
67
  }
27
68
 
28
- return fileContents
69
+ const mergedRanges = mergeRanges(ranges)
70
+ if (!mergedRanges.length) {
71
+ return ''
72
+ }
73
+
74
+ return mergedRanges.map((range) => content.slice(range.start, range.end).trim()).join('\n\n')
75
+ }
76
+
77
+ function parseMarkdownHeadings(content) {
78
+ const headings = []
79
+ const lineRegex = /.*(?:\r\n|\n|\r|$)/g
80
+ let match
81
+ let offset = 0
82
+ let fenceMarker = null
83
+
84
+ while ((match = lineRegex.exec(content)) !== null) {
85
+ const line = match[0]
86
+ if (!line) break
87
+
88
+ const lineText = line.replace(/\r?\n$|\r$/, '')
89
+ const fenceMatch = lineText.match(/^ {0,3}(`{3,}|~{3,})/)
90
+ if (fenceMarker) {
91
+ if (fenceMatch && fenceMatch[1][0] === fenceMarker[0] && fenceMatch[1].length >= fenceMarker.length) {
92
+ fenceMarker = null
93
+ }
94
+ offset += line.length
95
+ continue
96
+ }
97
+
98
+ if (fenceMatch) {
99
+ fenceMarker = fenceMatch[1]
100
+ offset += line.length
101
+ continue
102
+ }
103
+
104
+ const headingMatch = lineText.match(/^ {0,3}(#{1,6})(?:[ \t]+|$)(.*)$/)
105
+ if (headingMatch) {
106
+ const text = cleanHeadingText(headingMatch[2])
107
+ headings.push({
108
+ level: headingMatch[1].length,
109
+ text,
110
+ normalizedText: normalizeHeadingText(text),
111
+ start: offset,
112
+ end: content.length
113
+ })
114
+ }
115
+
116
+ offset += line.length
117
+ }
118
+
119
+ headings.forEach((heading, index) => {
120
+ const nextHeading = headings.slice(index + 1).find((candidate) => candidate.level <= heading.level)
121
+ heading.end = nextHeading ? nextHeading.start : content.length
122
+ })
123
+
124
+ return headings
125
+ }
126
+
127
+ function cleanHeadingText(text = '') {
128
+ return stripSimpleMarkdownLinks(text.replace(/[ \t]+#+[ \t]*$/, '').trim())
129
+ }
130
+
131
+ function normalizeHeadingText(text = '') {
132
+ return cleanHeadingText(String(text))
133
+ .replace(/[`*_~]/g, '')
134
+ .replace(/\s+/g, ' ')
135
+ .trim()
136
+ .toLowerCase()
137
+ }
138
+
139
+ function stripSimpleMarkdownLinks(text = '') {
140
+ return text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
141
+ }
142
+
143
+ function coerceListOption(value) {
144
+ if (typeof value === 'undefined' || value === null || value === false) {
145
+ return []
146
+ }
147
+ if (Array.isArray(value)) {
148
+ return value.map((item) => String(item).trim()).filter(Boolean)
149
+ }
150
+ return String(value)
151
+ .replace(/^\[|\]$/g, '')
152
+ .split(',')
153
+ .map((item) => item.trim())
154
+ .filter(Boolean)
155
+ }
156
+
157
+ function coerceHeadingLevels(value) {
158
+ return coerceListOption(value)
159
+ .map((item) => Number(item))
160
+ .filter((item) => Number.isInteger(item) && item >= 1 && item <= 6)
161
+ }
162
+
163
+ function mergeRanges(ranges) {
164
+ return ranges
165
+ .filter((range) => range && Number.isInteger(range.start) && Number.isInteger(range.end) && range.end > range.start)
166
+ .sort((a, b) => a.start - b.start || b.end - a.end)
167
+ .reduce((merged, range) => {
168
+ const previous = merged[merged.length - 1]
169
+ if (previous && range.start <= previous.end) {
170
+ previous.end = Math.max(previous.end, range.end)
171
+ return merged
172
+ }
173
+ merged.push({ start: range.start, end: range.end })
174
+ return merged
175
+ }, [])
29
176
  }
30
177
 
31
178
  module.exports = {
32
- formatMd
33
- }
179
+ formatMd,
180
+ parseMarkdownHeadings,
181
+ selectMarkdownSections,
182
+ normalizeHeadingText,
183
+ coerceListOption,
184
+ coerceHeadingLevels,
185
+ mergeRanges
186
+ }
@@ -0,0 +1,175 @@
1
+ const { test } = require('uvu')
2
+ const assert = require('uvu/assert')
3
+ const {
4
+ formatMd,
5
+ parseMarkdownHeadings,
6
+ normalizeHeadingText
7
+ } = require('./format-md')
8
+
9
+ const SAMPLE = `---
10
+ title: Example
11
+ ---
12
+ # Package Name
13
+
14
+ Intro text.
15
+
16
+ ## Installation
17
+
18
+ Install the package.
19
+
20
+ ### Browser
21
+
22
+ Browser install notes.
23
+
24
+ ## Usage
25
+
26
+ Use the package.
27
+
28
+ ### Node
29
+
30
+ Node usage notes.
31
+
32
+ ## API ##
33
+
34
+ API docs.
35
+
36
+ \`\`\`md
37
+ ## Ignored Code Heading
38
+ \`\`\`
39
+
40
+ ## [Contributing](#contributing)
41
+
42
+ Contribution notes.
43
+ `
44
+
45
+ test('parseMarkdownHeadings finds ATX headings and ignores fenced code headings', () => {
46
+ const headings = parseMarkdownHeadings(SAMPLE)
47
+ assert.equal(
48
+ headings.map((heading) => heading.text),
49
+ ['Package Name', 'Installation', 'Browser', 'Usage', 'Node', 'API', 'Contributing']
50
+ )
51
+ })
52
+
53
+ test('normalizeHeadingText normalizes case, whitespace, closing hashes, and markdown links', () => {
54
+ assert.is(normalizeHeadingText(' Installation ## '), 'installation')
55
+ assert.is(normalizeHeadingText('[Contributing](#contributing)'), 'contributing')
56
+ assert.is(normalizeHeadingText('Quick Start'), 'quick start')
57
+ })
58
+
59
+ test('formatMd sections selects named sections with nested child headings', () => {
60
+ const result = formatMd(SAMPLE, {
61
+ sections: 'Installation'
62
+ })
63
+
64
+ assert.ok(result.includes('## Installation'), 'includes requested section heading')
65
+ assert.ok(result.includes('### Browser'), 'includes nested child heading')
66
+ assert.not.ok(result.includes('## Usage'), 'excludes sibling section')
67
+ assert.not.ok(result.includes('Ignored Code Heading'), 'excludes unrelated fenced code heading')
68
+ })
69
+
70
+ test('formatMd sections accepts arrays and preserves source order', () => {
71
+ const result = formatMd(SAMPLE, {
72
+ sections: ['API', 'Installation']
73
+ })
74
+
75
+ assert.ok(result.indexOf('## Installation') < result.indexOf('## API'), 'uses source order')
76
+ assert.ok(result.includes('Install the package.'), 'includes first matched section')
77
+ assert.ok(result.includes('API docs.'), 'includes second matched section')
78
+ assert.not.ok(result.includes('## Usage'), 'excludes unselected middle section')
79
+ })
80
+
81
+ test('formatMd section shorthand works', () => {
82
+ const result = formatMd(SAMPLE, {
83
+ section: 'Usage'
84
+ })
85
+
86
+ assert.ok(result.includes('## Usage'), 'includes shorthand section')
87
+ assert.ok(result.includes('### Node'), 'includes shorthand section children')
88
+ assert.not.ok(result.includes('## Installation'), 'excludes previous sibling')
89
+ })
90
+
91
+ test('formatMd combines section and sections options', () => {
92
+ const result = formatMd(SAMPLE, {
93
+ section: 'Installation',
94
+ sections: 'API'
95
+ })
96
+
97
+ assert.ok(result.includes('## Installation'), 'includes section shorthand')
98
+ assert.ok(result.includes('## API'), 'includes sections option')
99
+ assert.not.ok(result.includes('## Usage'), 'excludes unselected section')
100
+ })
101
+
102
+ test('formatMd headings selects sections by heading level', () => {
103
+ const result = formatMd(SAMPLE, {
104
+ headings: [3]
105
+ })
106
+
107
+ assert.ok(result.includes('### Browser'), 'includes first h3 section')
108
+ assert.ok(result.includes('### Node'), 'includes second h3 section')
109
+ assert.not.ok(result.includes('## Installation'), 'excludes h2 parent')
110
+ assert.not.ok(result.includes('## API'), 'excludes h2 sibling')
111
+ })
112
+
113
+ test('formatMd headings accepts bracketed string values', () => {
114
+ const result = formatMd(SAMPLE, {
115
+ headings: '[3]'
116
+ })
117
+
118
+ assert.ok(result.includes('### Browser'), 'includes h3 from bracketed string')
119
+ assert.not.ok(result.includes('## Installation'), 'excludes h2 parent')
120
+ })
121
+
122
+ test('formatMd headings suppresses duplicate nested ranges', () => {
123
+ const result = formatMd(SAMPLE, {
124
+ headings: [2, 3]
125
+ })
126
+
127
+ assert.is(result.match(/### Browser/g).length, 1)
128
+ assert.is(result.match(/### Node/g).length, 1)
129
+ assert.ok(result.includes('## Installation'), 'includes h2 section')
130
+ assert.ok(result.includes('## Usage'), 'includes h2 sibling')
131
+ })
132
+
133
+ test('formatMd combines sections and headings as a deduped union', () => {
134
+ const result = formatMd(SAMPLE, {
135
+ sections: 'Usage',
136
+ headings: [3]
137
+ })
138
+
139
+ assert.ok(result.includes('### Browser'), 'includes h3 selected by level')
140
+ assert.ok(result.includes('## Usage'), 'includes named section')
141
+ assert.is(result.match(/### Node/g).length, 1)
142
+ assert.not.ok(result.includes('## API'), 'excludes unselected h2')
143
+ })
144
+
145
+ test('formatMd extracts before removeLeadingH1 and shiftHeaders', () => {
146
+ const result = formatMd(SAMPLE, {
147
+ sections: 'Installation',
148
+ removeLeadingH1: true,
149
+ shiftHeaders: 1
150
+ })
151
+
152
+ assert.ok(result.includes('### Installation'), 'shifts selected h2 after extraction')
153
+ assert.ok(result.includes('#### Browser'), 'shifts nested h3 after extraction')
154
+ assert.not.ok(result.includes('# Package Name'), 'does not include original leading h1')
155
+ })
156
+
157
+ test('formatMd throws when requested section is missing by default', () => {
158
+ assert.throws(() => {
159
+ formatMd(SAMPLE, {
160
+ sections: 'Missing'
161
+ })
162
+ }, /Missing markdown section: missing/)
163
+ })
164
+
165
+ test('formatMd can ignore missing requested sections', () => {
166
+ const result = formatMd(SAMPLE, {
167
+ sections: 'Missing,Usage',
168
+ allowMissingSections: true
169
+ })
170
+
171
+ assert.ok(result.includes('## Usage'), 'includes found section')
172
+ assert.not.ok(result.includes('## Installation'), 'excludes unrequested section')
173
+ })
174
+
175
+ test.run()
@@ -1,28 +1,52 @@
1
1
  const fetch = require('node-fetch')
2
2
 
3
3
  function formatUrl(url = '') {
4
- return url.match(/^https?:\/\//) ? url : `https://${url}`
4
+ if (typeof url !== 'string') return ''
5
+ const trimmed = url.trim()
6
+ if (!trimmed) return ''
7
+ return trimmed.match(/^https?:\/\//) ? trimmed : `https://${trimmed}`
5
8
  }
6
9
 
7
10
  async function remoteRequest(url, settings = {}, srcPath) {
8
- let body
9
11
  const finalUrl = formatUrl(url)
12
+ const fixText = srcPath ? `\nFix "${url}" value in ${srcPath}` : ''
13
+ if (!finalUrl) {
14
+ const msg = `Invalid URL "${url}"${fixText}`
15
+ if (settings.failOnMissingRemote) {
16
+ throw new Error(msg)
17
+ }
18
+ console.log(msg)
19
+ return
20
+ }
21
+
10
22
  // ignore demo url todo remove one day
11
23
  if (finalUrl === 'http://url-to-raw-md-file.md') {
12
24
  return
13
25
  }
26
+
27
+ let response
14
28
  try {
15
- const res = await fetch(finalUrl)
16
- body = await res.text()
29
+ response = await fetch(finalUrl)
17
30
  } catch (e) {
18
31
  console.log(`⚠️ WARNING: REMOTE URL "${finalUrl}" NOT FOUND`)
19
- const msg = (e.message || '').split('\n')[0] + `\nFix "${url}" value in ${srcPath}`
32
+ const msg = (e.message || '').split('\n')[0] + fixText
20
33
  console.log(msg)
21
34
  if (settings.failOnMissingRemote) {
22
35
  throw new Error(msg)
23
36
  }
37
+ return
38
+ }
39
+
40
+ if (!response.ok) {
41
+ const msg = `Remote request failed with status ${response.status} (${response.statusText}) for "${finalUrl}"${fixText}`
42
+ console.log(`⚠️ WARNING: ${msg}`)
43
+ if (settings.failOnMissingRemote) {
44
+ throw new Error(msg)
45
+ }
46
+ return
24
47
  }
25
- return body
48
+
49
+ return response.text()
26
50
  }
27
51
 
28
52
  module.exports = {
@@ -1,33 +1,3 @@
1
- export function uxParse(_rawArgv?: any[], opts?: {}): {
2
- leadingCommands: string[];
3
- extraParse: {};
4
- mriOptionsOriginal: mri.Argv<{
5
- [x: string]: any;
6
- }>;
7
- globGroups: any[];
8
- rawArgv?: undefined;
9
- mriOptionsClean?: undefined;
10
- mriDiff?: undefined;
11
- yargsParsed?: undefined;
12
- mergedOptions?: undefined;
13
- } | {
14
- rawArgv: string;
15
- leadingCommands: string[];
16
- globGroups: any[];
17
- extraParse: {};
18
- mriOptionsOriginal: mri.Argv<{
19
- [x: string]: any;
20
- }>;
21
- mriOptionsClean: {
22
- [x: string]: any;
23
- } & {
24
- _: string[];
25
- };
26
- mriDiff: boolean;
27
- yargsParsed: string;
28
- mergedOptions: {
29
- _: string[];
30
- };
31
- };
32
- import mri = require("mri");
1
+ export { dxParse };
2
+ import { dxParse } from "@davidwells/dx-args";
33
3
  //# sourceMappingURL=argparse.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"argparse.d.ts","sourceRoot":"","sources":["../../../src/argparse/argparse.js"],"names":[],"mappings":"AAoCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2QC"}
1
+ {"version":3,"file":"argparse.d.ts","sourceRoot":"","sources":["../../../src/argparse/argparse.js"],"names":[],"mappings":""}
@@ -1,3 +1,3 @@
1
- export { uxParse };
2
- import { uxParse } from "./argparse";
1
+ export { dxParse };
2
+ import { dxParse } from "./argparse";
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1,2 +1,3 @@
1
- export function splitOutsideQuotes(str: any): any[];
1
+ export { splitOutsideQuotes };
2
+ import { splitOutsideQuotes } from "@davidwells/dx-args";
2
3
  //# sourceMappingURL=splitOutsideQuotes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"splitOutsideQuotes.d.ts","sourceRoot":"","sources":["../../../src/argparse/splitOutsideQuotes.js"],"names":[],"mappings":"AAEA,oDAuEC"}
1
+ {"version":3,"file":"splitOutsideQuotes.d.ts","sourceRoot":"","sources":["../../../src/argparse/splitOutsideQuotes.js"],"names":[],"mappings":""}
@@ -1,4 +1,6 @@
1
- import { getGlobGroupsFromArgs } from "./globparse";
2
- export function runCli(options: {}, rawArgv: any): Promise<import("./").MarkdownMagicResult>;
1
+ import { getGlobGroupsFromArgs } from "@davidwells/dx-args/src/globparse";
2
+ export function parseCliArgv(rawArgv?: any[]): any;
3
+ export function normalizeCliOptions(parsed: any): any;
4
+ export function runCli(options: {}, rawArgv: any, deps?: {}): Promise<any>;
3
5
  export { getGlobGroupsFromArgs };
4
6
  //# sourceMappingURL=cli-run.d.ts.map