@@ -22,31 +22,74 @@ function parseProps(propsString) {
22
22
}
23
23
24
24
/**
25
- * Converts <Screenshot /> components to bold alt text
26
- * Example: <Screenshot alt="test" src="..." /> -> **test**
25
+ *
26
+ * @param {string } content - The content to search
27
+ * @param {string } componentName - Name of the component (e.g., "Screenshot")
28
+ * @returns {Array } Array of match objects with { props, children, fullMatch, index }
27
29
*/
28
- function convertScreenshot ( content ) {
29
- const screenshotRegex = / < S c r e e n s h o t \s + ( [ ^ > ] * ? ) (?: \/ ? > | > \s * < \/ S c r e e n s h o t > ) / gs
30
+ function matchComponent ( content , componentName ) {
31
+ const matches = [ ]
30
32
31
- return content . replace ( screenshotRegex , ( match , propsString ) => {
32
- const props = parseProps ( propsString )
33
- if ( props . alt ) {
34
- return `\n\n**${ props . alt } **\n\n`
35
- }
36
- return "\n\n**[Screenshot]**\n\n"
33
+ // Universal regex that matches:
34
+ // <ComponentName (props with any content including >) (self-closing /> OR open-tag > children </ComponentName>)
35
+ // Uses [\s\S]*? to match props across newlines (non-greedy to stop at /> or >)
36
+ const pattern = `<${ componentName } ([\\s\\S]*?)(?:\\/>|>([\\s\\S]*?)<\\/${ componentName } >)`
37
+ const regex = new RegExp ( pattern , 'g' )
38
+
39
+ let match
40
+
41
+ while ( ( match = regex . exec ( content ) ) !== null ) {
42
+ matches . push ( {
43
+ fullMatch : match [ 0 ] ,
44
+ propsString : match [ 1 ] ? match [ 1 ] . trim ( ) : '' ,
45
+ props : parseProps ( match [ 1 ] ? match [ 1 ] . trim ( ) : '' ) ,
46
+ children : match [ 2 ] || '' , // undefined if self-closing
47
+ index : match . index ,
48
+ isSelfClosing : ! match [ 2 ] && match [ 2 ] !== ''
49
+ } )
50
+ }
51
+
52
+ return matches
53
+ }
54
+
55
+ /**
56
+ * Replace component occurrences using the unified matcher
57
+ *
58
+ * @param {string } content - The content to process
59
+ * @param {string } componentName - Name of the component
60
+ * @param {function } replacer - Function that receives match object and returns replacement string
61
+ * @returns {string } Processed content
62
+ */
63
+ function replaceComponent ( content , componentName , replacer ) {
64
+ const matches = matchComponent ( content , componentName )
65
+
66
+ // Replace from end to start to maintain correct indices
67
+ let result = content
68
+ for ( let i = matches . length - 1 ; i >= 0 ; i -- ) {
69
+ const match = matches [ i ]
70
+ const replacement = replacer ( match )
71
+ result = result . substring ( 0 , match . index ) + replacement + result . substring ( match . index + match . fullMatch . length )
72
+ }
73
+
74
+ return result
75
+ }
76
+
77
+ /**
78
+ * Removes <Screenshot /> components
79
+ */
80
+ function convertScreenshot ( content ) {
81
+ return replaceComponent ( content , 'Screenshot' , ( _ ) => {
82
+ return ""
37
83
} )
38
84
}
39
85
40
86
/**
41
87
* Converts <CodeBlock /> components to markdown code blocks
42
88
*/
43
89
function convertCodeBlock ( content ) {
44
- const codeBlockRegex = / < C o d e B l o c k \s + ( [ ^ > ] * ?) > ( [ \s \S ] * ?) < \/ C o d e B l o c k > / g
45
-
46
- return content . replace ( codeBlockRegex , ( match , propsString , children ) => {
47
- const props = parseProps ( propsString )
48
- const language = props . language || ""
49
- const trimmedCode = children . trim ( )
90
+ return replaceComponent ( content , 'CodeBlock' , ( match ) => {
91
+ const language = match . props . language || match . props . className ?. replace ( 'language-' , '' ) || ""
92
+ const trimmedCode = match . children . trim ( )
50
93
return `\n\n\`\`\`${ language } \n${ trimmedCode } \n\`\`\`\n\n`
51
94
} )
52
95
}
@@ -55,12 +98,14 @@ function convertCodeBlock(content) {
55
98
* Converts <DocButton /> components to markdown links
56
99
*/
57
100
function convertDocButton ( content ) {
58
- const docButtonRegex = / < D o c B u t t o n \s + ( [ ^ > ] * ?) > ( [ \s \S ] * ?) < \/ D o c B u t t o n > / g
59
-
60
- return content . replace ( docButtonRegex , ( match , propsString , children ) => {
61
- const props = parseProps ( propsString )
62
- const href = props . href || props . to || "#"
63
- const text = children . trim ( )
101
+ return replaceComponent ( content , 'DocButton' , ( match ) => {
102
+ const href = match . props . href || match . props . to || "#"
103
+ // Strip JSX tags like <>, </>, and other HTML/JSX elements from children
104
+ const text = match . children
105
+ . trim ( )
106
+ . replace ( / < \/ ? > / g, '' ) // Remove React fragments <> and </>
107
+ . replace ( / < [ ^ > ] + > / g, '' ) // Remove other HTML/JSX tags
108
+ . trim ( )
64
109
return `[${ text } ](${ href } )`
65
110
} )
66
111
}
@@ -69,13 +114,11 @@ function convertDocButton(content) {
69
114
* Converts <Tabs> and <TabItem> to markdown table
70
115
*/
71
116
function convertTabs ( content ) {
72
- const tabsRegex = / < T a b s \s + ( [ \s \S ] * ?) > ( [ \s \S ] * ?) < \/ T a b s > / g
73
-
74
- return content . replace ( tabsRegex , ( match , propsString , children ) => {
117
+ return replaceComponent ( content , 'Tabs' , ( match ) => {
75
118
// Extract values array from props (can span multiple lines)
76
- const valuesMatch = propsString . match ( / v a l u e s = \{ ( \[ [ \s \S ] * ?\] ) \} / s)
119
+ const valuesMatch = match . propsString . match ( / v a l u e s = \{ ( \[ [ \s \S ] * ?\] ) \} / s)
77
120
if ( ! valuesMatch ) {
78
- return match // Return original if can't parse
121
+ return match . fullMatch // Return original if can't parse
79
122
}
80
123
81
124
let valuesArray
@@ -88,15 +131,15 @@ function convertTabs(content) {
88
131
valuesArray = JSON . parse ( valuesStr )
89
132
} catch ( e ) {
90
133
console . warn ( "[convert-components] Failed to parse Tabs values:" , e . message )
91
- return match
134
+ return match . fullMatch
92
135
}
93
136
94
137
// Extract TabItem contents
95
138
const tabItems = [ ]
96
139
const tabItemRegex = / < T a b I t e m \s + v a l u e = [ " ' ] ( [ ^ " ' ] + ) [ " ' ] [ ^ > ] * ?> ( [ \s \S ] * ?) < \/ T a b I t e m > / g
97
140
let tabMatch
98
141
99
- while ( ( tabMatch = tabItemRegex . exec ( children ) ) !== null ) {
142
+ while ( ( tabMatch = tabItemRegex . exec ( match . children ) ) !== null ) {
100
143
const value = tabMatch [ 1 ]
101
144
const content = tabMatch [ 2 ] . trim ( )
102
145
tabItems . push ( { value, content } )
@@ -134,16 +177,12 @@ function convertConfigTable(content, docsPath) {
134
177
importMap . set ( varName , unescapedPath )
135
178
}
136
179
137
- const configTableRegex = / < C o n f i g T a b l e \s + ( [ ^ > ] * ?) \/ > / g
138
-
139
- return content . replace ( configTableRegex , ( match , propsString ) => {
140
- const props = parseProps ( propsString )
141
-
180
+ return replaceComponent ( content , 'ConfigTable' , ( match ) => {
142
181
// ConfigTable typically uses a rows prop pointing to JSON data
143
- if ( props . rows ) {
182
+ if ( match . props . rows ) {
144
183
try {
145
184
// Get the import path for this variable
146
- const importPath = importMap . get ( props . rows )
185
+ const importPath = importMap . get ( match . props . rows )
147
186
if ( importPath ) {
148
187
// Resolve the path relative to the markdown file's directory
149
188
const configPath = path . join ( docsPath , importPath )
@@ -222,22 +261,25 @@ async function fetchReleaseVersion() {
222
261
}
223
262
224
263
function convertInterpolateReleaseData ( content , releaseVersion ) {
225
- // Match <InterpolateReleaseData ... /> (self-closing)
226
- // Extract what renderText returns and replace {release.name} with version
227
- const interpolateRegex = / < I n t e r p o l a t e R e l e a s e D a t a [ \s \S ] * ?\/ > / g
228
-
229
- return content . replace ( interpolateRegex , ( match ) => {
230
- // Extract everything inside renderText={(...) => ( ... )}
231
- // Match from the opening paren after => to the closing paren before ) }
232
- const renderTextMatch = match . match ( / r e n d e r T e x t = \{ [ ^ ( ] * \( [ ^ ) ] * \) \s * = > \s * \( ( [ \s \S ] * ?) \) \s * \} / ) ;
264
+ return replaceComponent ( content , 'InterpolateReleaseData' , ( match ) => {
265
+ // Try two patterns:
266
+ // 1. Arrow function with implicit return: (release) => (...)
267
+ // 2. Arrow function with explicit return: (release) => { return (...) }
268
+ let renderTextMatch = match . fullMatch . match ( / r e n d e r T e x t = \{ [ ^ ( ] * \( [ ^ ) ] * \) \s * = > \s * \( ( [ \s \S ] * ?) \) \s * \} / ) ;
269
+
270
+ if ( ! renderTextMatch ) {
271
+ // Try pattern with { return (...) }
272
+ renderTextMatch = match . fullMatch . match ( / r e n d e r T e x t = \{ [ ^ ( ] * \( [ ^ ) ] * \) \s * = > \s * \{ \s * r e t u r n \s * \( ( [ \s \S ] * ?) \) \s * \} \s * \} / ) ;
273
+ }
233
274
234
275
if ( renderTextMatch ) {
235
276
// Extract the JSX content being returned
236
277
let extracted = renderTextMatch [ 1 ] . trim ( ) ;
237
278
238
- // Replace ${release.name} with the actual version (note the $ before the {)
239
- extracted = extracted . replace ( / \$ \{ r e l e a s e \. n a m e \} / g, releaseVersion ) ;
240
- extracted = extracted . replace ( / \$ \{ r e l e a s e \. t a g _ n a m e \} / g, releaseVersion ) ;
279
+ // Replace template literal placeholders with actual version
280
+ // Handles both ${release.name} (with $) and {release.name} (without $ inside template literals)
281
+ extracted = extracted . replace ( / \$ ? \{ r e l e a s e \. n a m e \} / g, releaseVersion ) ;
282
+ extracted = extracted . replace ( / \$ ? \{ r e l e a s e \. t a g _ n a m e \} / g, releaseVersion ) ;
241
283
242
284
// Remove JSX template literal syntax: {`...`} becomes just the content
243
285
extracted = extracted . replace ( / \{ ` / g, '' ) ;
@@ -307,13 +349,9 @@ function convertRailroadDiagrams(content, docsPath) {
307
349
* @param {object } repoExamples - Repository examples data from remote-repo-example plugin
308
350
*/
309
351
function convertRemoteRepoExample ( content , repoExamples = { } ) {
310
- // Use [\s\S]*? to match across multiple lines
311
- const remoteRepoRegex = / < R e m o t e R e p o E x a m p l e \s + ( [ \s \S ] * ?) \/ > / g
312
-
313
- return content . replace ( remoteRepoRegex , ( match , propsString ) => {
314
- const props = parseProps ( propsString )
315
- const name = props . name || 'unknown'
316
- const lang = props . lang || 'text'
352
+ return replaceComponent ( content , 'RemoteRepoExample' , ( match ) => {
353
+ const name = match . props . name || 'unknown'
354
+ const lang = match . props . lang || 'text'
317
355
const id = `${ name } /${ lang } `
318
356
319
357
// Get the example from plugin data
@@ -328,7 +366,7 @@ function convertRemoteRepoExample(content, repoExamples = {}) {
328
366
let output = '\n\n'
329
367
330
368
// Add header if it exists and header prop is not false
331
- if ( props . header !== 'false' && example . header ) {
369
+ if ( match . props . header !== 'false' && example . header ) {
332
370
output += `${ example . header } \n\n`
333
371
}
334
372
@@ -383,12 +421,8 @@ const clients = [
383
421
} ,
384
422
]
385
423
function convertILPClientsTable ( content ) {
386
- // Use [\s\S]*? to match across multiple lines
387
- const ilpClientsRegex = / < I L P C l i e n t s T a b l e \s + ( [ \s \S ] * ?) \/ > / g
388
-
389
- return content . replace ( ilpClientsRegex , ( match , propsString ) => {
390
- const props = parseProps ( propsString )
391
- const language = props . language
424
+ return replaceComponent ( content , 'ILPClientsTable' , ( match ) => {
425
+ const language = match . props . language
392
426
393
427
// Filter by language if specified
394
428
const filteredClients = language
@@ -573,23 +607,16 @@ function prependFrontmatter(content, frontmatter, includeTitle = true) {
573
607
* @param {string } content - The processed markdown content
574
608
* @returns {string } Cleaned markdown
575
609
*/
576
- function cleanForLLM ( content ) {
610
+ function normalizeNewLines ( content ) {
577
611
return content
578
612
. replace ( / \n { 3 , } / g, '\n\n' ) // Normalize multiple newlines
579
613
. trim ( )
580
614
}
581
615
582
616
module . exports = {
583
617
convertAllComponents,
584
- convertScreenshot,
585
- convertDocButton,
586
- convertCodeBlock,
587
- convertTabs,
588
- convertConfigTable,
589
- convertInterpolateReleaseData,
590
- fetchReleaseVersion,
591
618
bumpHeadings,
592
- cleanForLLM ,
619
+ normalizeNewLines ,
593
620
removeImports,
594
621
processPartialImports,
595
622
prependFrontmatter,
0 commit comments