boxwood 2.10.0 → 2.12.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/package.json
CHANGED
package/ui/markdown/index.js
CHANGED
|
@@ -14,10 +14,22 @@ const {
|
|
|
14
14
|
Strong,
|
|
15
15
|
Em,
|
|
16
16
|
Code,
|
|
17
|
+
A,
|
|
18
|
+
Hr,
|
|
19
|
+
Img,
|
|
20
|
+
Pre,
|
|
17
21
|
} = require("../..")
|
|
18
22
|
|
|
23
|
+
const { format } = require("./utilities/format")
|
|
24
|
+
const {
|
|
25
|
+
parseCustomTag,
|
|
26
|
+
resolveAttributes,
|
|
27
|
+
} = require("./utilities/parseCustomTag")
|
|
28
|
+
const { replaceVariables } = require("./utilities/replaceVariables")
|
|
29
|
+
|
|
19
30
|
const ORDERED_LIST_REGEXP = /^\d+\.\s/
|
|
20
31
|
const UNORDERED_MARKERS = ["- ", "— ", "– ", "• "]
|
|
32
|
+
const HORIZONTAL_RULE_REGEXP = /^(?:\*\s*\*\s*\*+|-\s*-\s*-+|_\s*_\s*_+)\s*$/
|
|
21
33
|
const HEADINGS = [
|
|
22
34
|
{ prefix: "###### ", type: "h6" },
|
|
23
35
|
{ prefix: "##### ", type: "h5" },
|
|
@@ -34,62 +46,10 @@ const COMPONENTS = {
|
|
|
34
46
|
h5: H5,
|
|
35
47
|
h6: H6,
|
|
36
48
|
blockquote: Blockquote,
|
|
49
|
+
hr: Hr,
|
|
37
50
|
}
|
|
38
51
|
|
|
39
|
-
|
|
40
|
-
if (!text.includes("*") && !text.includes("`")) {
|
|
41
|
-
return text
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const result = []
|
|
45
|
-
let i = 0
|
|
46
|
-
while (i < text.length) {
|
|
47
|
-
if (text[i] === "`") {
|
|
48
|
-
const start = i + 1
|
|
49
|
-
const end = text.indexOf("`", start)
|
|
50
|
-
if (end === -1) {
|
|
51
|
-
result.push(text[i])
|
|
52
|
-
i++
|
|
53
|
-
} else {
|
|
54
|
-
result.push(Code({}, text.substring(start, end)))
|
|
55
|
-
i = end + 1
|
|
56
|
-
}
|
|
57
|
-
} else if (text[i] === "*" && text[i + 1] === "*") {
|
|
58
|
-
const start = i + 2
|
|
59
|
-
const end = text.indexOf("**", start)
|
|
60
|
-
if (end === -1) {
|
|
61
|
-
result.push(text[i])
|
|
62
|
-
i++
|
|
63
|
-
} else {
|
|
64
|
-
result.push(Strong(text.substring(start, end)))
|
|
65
|
-
i = end + 2
|
|
66
|
-
}
|
|
67
|
-
} else if (text[i] === "*") {
|
|
68
|
-
const start = i + 1
|
|
69
|
-
const end = text.indexOf("*", start)
|
|
70
|
-
if (end === -1) {
|
|
71
|
-
result.push(text[i])
|
|
72
|
-
i++
|
|
73
|
-
} else {
|
|
74
|
-
result.push(Em(text.substring(start, end)))
|
|
75
|
-
i = end + 1
|
|
76
|
-
}
|
|
77
|
-
} else {
|
|
78
|
-
let next = text.length
|
|
79
|
-
const nextCode = text.indexOf("`", i)
|
|
80
|
-
const nextStar = text.indexOf("*", i)
|
|
81
|
-
if (nextCode !== -1 && nextCode < next) next = nextCode
|
|
82
|
-
if (nextStar !== -1 && nextStar < next) next = nextStar
|
|
83
|
-
if (next === text.length) {
|
|
84
|
-
result.push(text.substring(i))
|
|
85
|
-
break
|
|
86
|
-
}
|
|
87
|
-
result.push(text.substring(i, next))
|
|
88
|
-
i = next
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return result.length > 0 ? result : text
|
|
92
|
-
}
|
|
52
|
+
const INLINE_COMPONENTS = { A, Img, Code, Strong, Em }
|
|
93
53
|
|
|
94
54
|
function Markdown(params, children) {
|
|
95
55
|
if (Array.isArray(children)) {
|
|
@@ -100,47 +60,170 @@ function Markdown(params, children) {
|
|
|
100
60
|
return null
|
|
101
61
|
}
|
|
102
62
|
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
63
|
+
const customComponents = params && params.components
|
|
64
|
+
const data = params && params.data
|
|
65
|
+
|
|
66
|
+
// Filter out special params that shouldn't be passed to HTML elements
|
|
67
|
+
const htmlParams = params
|
|
68
|
+
? Object.keys(params).reduce((acc, key) => {
|
|
69
|
+
if (key !== "components" && key !== "data") {
|
|
70
|
+
acc[key] = params[key]
|
|
71
|
+
}
|
|
72
|
+
return acc
|
|
73
|
+
}, {})
|
|
74
|
+
: {}
|
|
75
|
+
|
|
76
|
+
// First pass: detect code blocks before processing lines
|
|
77
|
+
const allLines = children.trim().split("\n")
|
|
78
|
+
const items = []
|
|
79
|
+
let i = 0
|
|
80
|
+
|
|
81
|
+
while (i < allLines.length) {
|
|
82
|
+
const line = allLines[i]
|
|
83
|
+
const trimmed = line.trim()
|
|
84
|
+
|
|
85
|
+
// Check for custom component tags first
|
|
86
|
+
const customTag = parseCustomTag(line, customComponents)
|
|
87
|
+
if (customTag) {
|
|
88
|
+
if (customTag.type === "custom-component" && customTag.selfClosing) {
|
|
89
|
+
// Self-closing custom component
|
|
90
|
+
items.push({
|
|
91
|
+
type: "custom-component",
|
|
92
|
+
tagName: customTag.tagName,
|
|
93
|
+
component: customTag.component,
|
|
94
|
+
attributes: customTag.attributes,
|
|
95
|
+
selfClosing: true,
|
|
96
|
+
indent: line.length - line.trimStart().length,
|
|
97
|
+
})
|
|
98
|
+
i++
|
|
99
|
+
continue
|
|
100
|
+
} else if (customTag.type === "custom-component-open") {
|
|
101
|
+
// Opening tag of a custom component
|
|
102
|
+
const openIndent = line.length - line.trimStart().length
|
|
103
|
+
const tagName = customTag.tagName
|
|
104
|
+
const componentFn = customTag.component
|
|
105
|
+
const attributes = customTag.attributes
|
|
106
|
+
const contentLines = []
|
|
107
|
+
i++ // Move past opening tag
|
|
108
|
+
|
|
109
|
+
// Collect content until closing tag
|
|
110
|
+
let depth = 1
|
|
111
|
+
while (i < allLines.length && depth > 0) {
|
|
112
|
+
const contentLine = allLines[i]
|
|
113
|
+
const contentTag = parseCustomTag(contentLine, customComponents)
|
|
114
|
+
|
|
115
|
+
if (contentTag && contentTag.tagName === tagName) {
|
|
116
|
+
if (contentTag.type === "custom-component-open") {
|
|
117
|
+
depth++
|
|
118
|
+
contentLines.push(contentLine)
|
|
119
|
+
} else if (contentTag.type === "custom-component-close") {
|
|
120
|
+
depth--
|
|
121
|
+
if (depth === 0) {
|
|
122
|
+
// Found matching closing tag
|
|
123
|
+
i++
|
|
124
|
+
break
|
|
125
|
+
}
|
|
126
|
+
contentLines.push(contentLine)
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
contentLines.push(contentLine)
|
|
130
|
+
}
|
|
131
|
+
i++
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
items.push({
|
|
135
|
+
type: "custom-component",
|
|
136
|
+
tagName,
|
|
137
|
+
component: componentFn,
|
|
138
|
+
attributes,
|
|
139
|
+
content: contentLines.join("\n"),
|
|
140
|
+
selfClosing: false,
|
|
141
|
+
indent: openIndent,
|
|
142
|
+
})
|
|
143
|
+
continue
|
|
120
144
|
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check for code block start
|
|
148
|
+
if (trimmed.startsWith("```")) {
|
|
149
|
+
const language = trimmed.substring(3).trim()
|
|
150
|
+
const codeLines = []
|
|
151
|
+
i++ // Move past the opening ```
|
|
121
152
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
153
|
+
// Collect code block lines until closing ```
|
|
154
|
+
while (i < allLines.length) {
|
|
155
|
+
const codeLine = allLines[i]
|
|
156
|
+
if (codeLine.trim().startsWith("```")) {
|
|
157
|
+
// Found closing ```, don't include it
|
|
158
|
+
i++
|
|
159
|
+
break
|
|
160
|
+
}
|
|
161
|
+
codeLines.push(codeLine)
|
|
162
|
+
i++
|
|
126
163
|
}
|
|
127
164
|
|
|
165
|
+
items.push({
|
|
166
|
+
type: "pre",
|
|
167
|
+
content: codeLines.join("\n"),
|
|
168
|
+
language: language || undefined,
|
|
169
|
+
indent: 0,
|
|
170
|
+
})
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Skip empty lines
|
|
175
|
+
if (trimmed.length === 0) {
|
|
176
|
+
i++
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const leadingSpaces = line.length - line.trimStart().length
|
|
181
|
+
|
|
182
|
+
// Check for other block types
|
|
183
|
+
if (HORIZONTAL_RULE_REGEXP.test(trimmed)) {
|
|
184
|
+
items.push({ type: "hr", indent: leadingSpaces })
|
|
185
|
+
} else if (UNORDERED_MARKERS.some((marker) => trimmed.startsWith(marker))) {
|
|
186
|
+
const content = trimmed.substring(2)
|
|
187
|
+
if (content) {
|
|
188
|
+
items.push({ type: "li", list: "ul", content, indent: leadingSpaces })
|
|
189
|
+
}
|
|
190
|
+
} else if (ORDERED_LIST_REGEXP.test(trimmed)) {
|
|
191
|
+
const content = trimmed.replace(ORDERED_LIST_REGEXP, "")
|
|
192
|
+
if (content) {
|
|
193
|
+
items.push({ type: "li", list: "ol", content, indent: leadingSpaces })
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
let matched = false
|
|
128
197
|
for (const { prefix, type } of HEADINGS) {
|
|
129
|
-
if (
|
|
130
|
-
|
|
198
|
+
if (trimmed.startsWith(prefix)) {
|
|
199
|
+
items.push({
|
|
200
|
+
type,
|
|
201
|
+
content: trimmed.substring(prefix.length),
|
|
202
|
+
indent: leadingSpaces,
|
|
203
|
+
})
|
|
204
|
+
matched = true
|
|
205
|
+
break
|
|
131
206
|
}
|
|
132
207
|
}
|
|
133
208
|
|
|
134
|
-
if (
|
|
135
|
-
|
|
209
|
+
if (!matched) {
|
|
210
|
+
if (trimmed.startsWith("> ")) {
|
|
211
|
+
items.push({
|
|
212
|
+
type: "blockquote",
|
|
213
|
+
content: trimmed.substring(2),
|
|
214
|
+
indent: leadingSpaces,
|
|
215
|
+
})
|
|
216
|
+
} else {
|
|
217
|
+
items.push({ type: "p", content: trimmed, indent: leadingSpaces })
|
|
218
|
+
}
|
|
136
219
|
}
|
|
220
|
+
}
|
|
137
221
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
.filter(Boolean)
|
|
222
|
+
i++
|
|
223
|
+
}
|
|
141
224
|
|
|
142
225
|
const nodes = []
|
|
143
|
-
|
|
226
|
+
i = 0
|
|
144
227
|
|
|
145
228
|
function parseList(startIndex, parentIndent) {
|
|
146
229
|
const list = []
|
|
@@ -166,7 +249,8 @@ function Markdown(params, children) {
|
|
|
166
249
|
}
|
|
167
250
|
|
|
168
251
|
// Process current item at the correct indent level
|
|
169
|
-
const
|
|
252
|
+
const processedContent = replaceVariables(item.content, data)
|
|
253
|
+
const content = [format(processedContent, INLINE_COMPONENTS)]
|
|
170
254
|
currentIndex++
|
|
171
255
|
|
|
172
256
|
// Check if next item is a nested list
|
|
@@ -177,11 +261,11 @@ function Markdown(params, children) {
|
|
|
177
261
|
currentIndex = nestedResult.nextIndex
|
|
178
262
|
}
|
|
179
263
|
|
|
180
|
-
list.push(Li(
|
|
264
|
+
list.push(Li(htmlParams, content))
|
|
181
265
|
}
|
|
182
266
|
|
|
183
267
|
const listElement =
|
|
184
|
-
parentListType === "ul" ? Ul(
|
|
268
|
+
parentListType === "ul" ? Ul(htmlParams, list) : Ol(htmlParams, list)
|
|
185
269
|
|
|
186
270
|
return { list: listElement, nextIndex: currentIndex }
|
|
187
271
|
}
|
|
@@ -189,7 +273,20 @@ function Markdown(params, children) {
|
|
|
189
273
|
while (i < items.length) {
|
|
190
274
|
const item = items[i]
|
|
191
275
|
|
|
192
|
-
if (item.type === "
|
|
276
|
+
if (item.type === "custom-component") {
|
|
277
|
+
// Handle custom component
|
|
278
|
+
const resolvedAttrs = resolveAttributes(item.attributes, data)
|
|
279
|
+
|
|
280
|
+
if (item.selfClosing) {
|
|
281
|
+
// Self-closing component
|
|
282
|
+
nodes.push(item.component(resolvedAttrs))
|
|
283
|
+
} else {
|
|
284
|
+
// Component with children - recursively process markdown content
|
|
285
|
+
const childContent = item.content ? Markdown(params, item.content) : []
|
|
286
|
+
nodes.push(item.component(resolvedAttrs, childContent))
|
|
287
|
+
}
|
|
288
|
+
i++
|
|
289
|
+
} else if (item.type === "li") {
|
|
193
290
|
const result = parseList(i, item.indent)
|
|
194
291
|
nodes.push(result.list)
|
|
195
292
|
i = result.nextIndex
|
|
@@ -201,11 +298,30 @@ function Markdown(params, children) {
|
|
|
201
298
|
i++
|
|
202
299
|
}
|
|
203
300
|
|
|
204
|
-
|
|
301
|
+
const content = replaceVariables(lines.join("\n"), data)
|
|
302
|
+
nodes.push(
|
|
303
|
+
Blockquote(
|
|
304
|
+
htmlParams,
|
|
305
|
+
P(htmlParams, format(content, INLINE_COMPONENTS)),
|
|
306
|
+
),
|
|
307
|
+
)
|
|
308
|
+
} else if (item.type === "hr") {
|
|
309
|
+
nodes.push(Hr(htmlParams))
|
|
310
|
+
i++
|
|
311
|
+
} else if (item.type === "pre") {
|
|
312
|
+
// Code blocks - wrap in <pre><code>
|
|
313
|
+
const codeParams = item.language
|
|
314
|
+
? { class: `language-${item.language}` }
|
|
315
|
+
: {}
|
|
316
|
+
nodes.push(Pre(htmlParams, Code(codeParams, item.content)))
|
|
317
|
+
i++
|
|
205
318
|
} else {
|
|
206
319
|
const { type, content } = item
|
|
207
320
|
const Component = COMPONENTS[type] || P
|
|
208
|
-
|
|
321
|
+
const processedContent = replaceVariables(content, data)
|
|
322
|
+
nodes.push(
|
|
323
|
+
Component(htmlParams, format(processedContent, INLINE_COMPONENTS)),
|
|
324
|
+
)
|
|
209
325
|
i++
|
|
210
326
|
}
|
|
211
327
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Find the matching closing bracket, accounting for nested brackets
|
|
2
|
+
function findMatchingBracket(text, startPos) {
|
|
3
|
+
let depth = 1
|
|
4
|
+
let i = startPos + 1
|
|
5
|
+
|
|
6
|
+
while (i < text.length && depth > 0) {
|
|
7
|
+
if (text[i] === "[" && text[i - 1] !== "\\") {
|
|
8
|
+
depth++
|
|
9
|
+
} else if (text[i] === "]" && text[i - 1] !== "\\") {
|
|
10
|
+
depth--
|
|
11
|
+
if (depth === 0) {
|
|
12
|
+
return i
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
i++
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return -1
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
module.exports = { findMatchingBracket }
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
const { findMatchingBracket } = require("./brackets")
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Format inline markdown elements within text
|
|
5
|
+
* Handles: images, links, code, bold, italic
|
|
6
|
+
* @param {string} text - The text to format
|
|
7
|
+
* @param {Object} components - HTML component functions (A, Img, Code, Strong, Em)
|
|
8
|
+
* @returns {Array|string} - Formatted content as array of components and strings
|
|
9
|
+
*/
|
|
10
|
+
function format(text, components) {
|
|
11
|
+
const { A, Img, Code, Strong, Em } = components
|
|
12
|
+
|
|
13
|
+
if (
|
|
14
|
+
!text.includes("*") &&
|
|
15
|
+
!text.includes("`") &&
|
|
16
|
+
!text.includes("[") &&
|
|
17
|
+
!text.includes("!") &&
|
|
18
|
+
!text.includes("<") &&
|
|
19
|
+
!text.includes("\\")
|
|
20
|
+
) {
|
|
21
|
+
return text
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const result = []
|
|
25
|
+
let i = 0
|
|
26
|
+
|
|
27
|
+
while (i < text.length) {
|
|
28
|
+
// Handle escape characters
|
|
29
|
+
if (text[i] === "\\") {
|
|
30
|
+
if (i + 1 < text.length) {
|
|
31
|
+
const nextChar = text[i + 1]
|
|
32
|
+
// Check if next char is a special markdown character
|
|
33
|
+
if (
|
|
34
|
+
nextChar === "*" ||
|
|
35
|
+
nextChar === "`" ||
|
|
36
|
+
nextChar === "[" ||
|
|
37
|
+
nextChar === "]" ||
|
|
38
|
+
nextChar === "(" ||
|
|
39
|
+
nextChar === ")" ||
|
|
40
|
+
nextChar === "!" ||
|
|
41
|
+
nextChar === "<" ||
|
|
42
|
+
nextChar === ">" ||
|
|
43
|
+
nextChar === "\\"
|
|
44
|
+
) {
|
|
45
|
+
// Escape the next character - just add it as literal text
|
|
46
|
+
result.push(nextChar)
|
|
47
|
+
i += 2 // Skip both \ and the escaped character
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
// If not a special char, keep both the backslash and the next character
|
|
51
|
+
result.push(text[i])
|
|
52
|
+
result.push(nextChar)
|
|
53
|
+
i += 2
|
|
54
|
+
continue
|
|
55
|
+
} else {
|
|
56
|
+
// Backslash at end of string
|
|
57
|
+
result.push(text[i])
|
|
58
|
+
i++
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (text[i] === "!" && text[i + 1] === "[") {
|
|
64
|
+
// Try to parse markdown image 
|
|
65
|
+
const altEnd = text.indexOf("]", i + 2)
|
|
66
|
+
|
|
67
|
+
if (altEnd !== -1 && text[altEnd + 1] === "(") {
|
|
68
|
+
const urlEnd = text.indexOf(")", altEnd + 2)
|
|
69
|
+
|
|
70
|
+
if (urlEnd !== -1) {
|
|
71
|
+
const alt = text.substring(i + 2, altEnd)
|
|
72
|
+
const src = text.substring(altEnd + 2, urlEnd)
|
|
73
|
+
result.push(Img({ src, alt }))
|
|
74
|
+
i = urlEnd + 1
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Not a valid image, treat as regular text (skip both ! and [)
|
|
80
|
+
result.push(text[i])
|
|
81
|
+
i++
|
|
82
|
+
} else if (text[i] === "[") {
|
|
83
|
+
// Try to parse markdown link [text](url)
|
|
84
|
+
const textEnd = findMatchingBracket(text, i)
|
|
85
|
+
|
|
86
|
+
if (textEnd !== -1 && text[textEnd + 1] === "(") {
|
|
87
|
+
const urlEnd = text.indexOf(")", textEnd + 2)
|
|
88
|
+
|
|
89
|
+
if (urlEnd !== -1) {
|
|
90
|
+
const linkText = text.substring(i + 1, textEnd)
|
|
91
|
+
const url = text.substring(textEnd + 2, urlEnd)
|
|
92
|
+
// Recursively format the link text to support images, bold, italic inside links
|
|
93
|
+
result.push(A({ href: url }, format(linkText, components)))
|
|
94
|
+
i = urlEnd + 1
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Not a valid link, treat as regular text
|
|
100
|
+
result.push(text[i])
|
|
101
|
+
i++
|
|
102
|
+
} else if (text[i] === "<") {
|
|
103
|
+
// Try to parse autolink <url> or <email>
|
|
104
|
+
const end = text.indexOf(">", i + 1)
|
|
105
|
+
|
|
106
|
+
if (end !== -1) {
|
|
107
|
+
const content = text.substring(i + 1, end)
|
|
108
|
+
|
|
109
|
+
// Check if it's a URL (starts with http:// or https://)
|
|
110
|
+
if (content.startsWith("http://") || content.startsWith("https://")) {
|
|
111
|
+
result.push(A({ href: content }, content))
|
|
112
|
+
i = end + 1
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check if it's an email (contains @ and looks like email)
|
|
117
|
+
if (content.includes("@") && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(content)) {
|
|
118
|
+
result.push(A({ href: `mailto:${content}` }, content))
|
|
119
|
+
i = end + 1
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Not a valid autolink, treat as regular text
|
|
125
|
+
result.push(text[i])
|
|
126
|
+
i++
|
|
127
|
+
} else if (text[i] === "`") {
|
|
128
|
+
const end = text.indexOf("`", i + 1)
|
|
129
|
+
if (end === -1) {
|
|
130
|
+
result.push(text[i])
|
|
131
|
+
i++
|
|
132
|
+
} else {
|
|
133
|
+
result.push(Code({}, text.substring(i + 1, end)))
|
|
134
|
+
i = end + 1
|
|
135
|
+
}
|
|
136
|
+
} else if (text[i] === "*" && text[i + 1] === "*") {
|
|
137
|
+
const end = text.indexOf("**", i + 2)
|
|
138
|
+
if (end === -1) {
|
|
139
|
+
result.push(text[i])
|
|
140
|
+
i++
|
|
141
|
+
} else {
|
|
142
|
+
result.push(Strong(format(text.substring(i + 2, end), components)))
|
|
143
|
+
i = end + 2
|
|
144
|
+
}
|
|
145
|
+
} else if (text[i] === "*") {
|
|
146
|
+
const end = text.indexOf("*", i + 1)
|
|
147
|
+
if (end === -1) {
|
|
148
|
+
result.push(text[i])
|
|
149
|
+
i++
|
|
150
|
+
} else {
|
|
151
|
+
result.push(Em(format(text.substring(i + 1, end), components)))
|
|
152
|
+
i = end + 1
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
// Find next special character
|
|
156
|
+
const positions = [
|
|
157
|
+
text.indexOf("`", i),
|
|
158
|
+
text.indexOf("*", i),
|
|
159
|
+
text.indexOf("[", i),
|
|
160
|
+
text.indexOf("<", i),
|
|
161
|
+
text.indexOf("\\", i),
|
|
162
|
+
].filter((pos) => pos !== -1)
|
|
163
|
+
|
|
164
|
+
// Look for image pattern ![
|
|
165
|
+
const exclamPos = text.indexOf("!", i)
|
|
166
|
+
if (exclamPos !== -1 && text[exclamPos + 1] === "[") {
|
|
167
|
+
positions.push(exclamPos)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const next = positions.length > 0 ? Math.min(...positions) : text.length
|
|
171
|
+
|
|
172
|
+
result.push(text.substring(i, next))
|
|
173
|
+
if (next === text.length) break
|
|
174
|
+
i = next
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return result.length > 0 ? result : text
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = { format }
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a custom component tag from markdown
|
|
3
|
+
* Supports both <Component attr="value"> and <Component attr={variable}>
|
|
4
|
+
* @param {string} line - The line to parse
|
|
5
|
+
* @param {Object} customComponents - Map of component names to functions
|
|
6
|
+
* @returns {Object|null} - Parsed tag info or null if not a custom tag
|
|
7
|
+
*/
|
|
8
|
+
function parseCustomTag(line, customComponents) {
|
|
9
|
+
if (!customComponents || typeof customComponents !== "object") {
|
|
10
|
+
return null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const trimmed = line.trim()
|
|
14
|
+
|
|
15
|
+
// Check for self-closing tag: <ComponentName ... /> (space before / is optional)
|
|
16
|
+
const selfClosingMatch = trimmed.match(
|
|
17
|
+
/^<([A-Za-z][A-Za-z0-9-]*)(\s+[^>]*)?\s*\/>\s*$/,
|
|
18
|
+
)
|
|
19
|
+
if (selfClosingMatch) {
|
|
20
|
+
const [, tagName, attributesStr] = selfClosingMatch
|
|
21
|
+
const component = customComponents[tagName]
|
|
22
|
+
if (component) {
|
|
23
|
+
const attributes = parseAttributes(attributesStr || "")
|
|
24
|
+
return {
|
|
25
|
+
type: "custom-component",
|
|
26
|
+
tagName,
|
|
27
|
+
component,
|
|
28
|
+
attributes,
|
|
29
|
+
selfClosing: true,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check for opening tag: <ComponentName ...>
|
|
35
|
+
const openTagMatch = trimmed.match(
|
|
36
|
+
/^<([A-Za-z][A-Za-z0-9-]*)(\s+[^>]*)?>\s*$/,
|
|
37
|
+
)
|
|
38
|
+
if (openTagMatch) {
|
|
39
|
+
const [, tagName, attributesStr] = openTagMatch
|
|
40
|
+
const component = customComponents[tagName]
|
|
41
|
+
if (component) {
|
|
42
|
+
const attributes = parseAttributes(attributesStr || "")
|
|
43
|
+
return {
|
|
44
|
+
type: "custom-component-open",
|
|
45
|
+
tagName,
|
|
46
|
+
component,
|
|
47
|
+
attributes,
|
|
48
|
+
selfClosing: false,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check for closing tag: </ComponentName>
|
|
54
|
+
const closeTagMatch = trimmed.match(/^<\/([A-Za-z][A-Za-z0-9-]*)>\s*$/)
|
|
55
|
+
if (closeTagMatch) {
|
|
56
|
+
const [, tagName] = closeTagMatch
|
|
57
|
+
const component = customComponents[tagName]
|
|
58
|
+
if (component) {
|
|
59
|
+
return {
|
|
60
|
+
type: "custom-component-close",
|
|
61
|
+
tagName,
|
|
62
|
+
component,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parse attributes from a tag string
|
|
72
|
+
* Supports: attr="value", attr='value', attr={variable}, attr
|
|
73
|
+
* @param {string} attributesStr - The attributes string
|
|
74
|
+
* @returns {Object} - Parsed attributes
|
|
75
|
+
*/
|
|
76
|
+
function parseAttributes(attributesStr) {
|
|
77
|
+
const attributes = {}
|
|
78
|
+
if (!attributesStr || !attributesStr.trim()) {
|
|
79
|
+
return attributes
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const str = attributesStr.trim()
|
|
83
|
+
let i = 0
|
|
84
|
+
|
|
85
|
+
while (i < str.length) {
|
|
86
|
+
// Skip whitespace
|
|
87
|
+
while (i < str.length && /\s/.test(str[i])) {
|
|
88
|
+
i++
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (i >= str.length) break
|
|
92
|
+
|
|
93
|
+
// Parse attribute name
|
|
94
|
+
let nameStart = i
|
|
95
|
+
while (i < str.length && /[a-zA-Z0-9-_:]/.test(str[i])) {
|
|
96
|
+
i++
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const name = str.substring(nameStart, i)
|
|
100
|
+
if (!name) break
|
|
101
|
+
|
|
102
|
+
// Skip whitespace after name
|
|
103
|
+
while (i < str.length && /\s/.test(str[i])) {
|
|
104
|
+
i++
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Check for =
|
|
108
|
+
if (i >= str.length || str[i] !== "=") {
|
|
109
|
+
// Boolean attribute
|
|
110
|
+
attributes[name] = true
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
i++ // Skip =
|
|
115
|
+
|
|
116
|
+
// Skip whitespace after =
|
|
117
|
+
while (i < str.length && /\s/.test(str[i])) {
|
|
118
|
+
i++
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (i >= str.length) break
|
|
122
|
+
|
|
123
|
+
// Parse value
|
|
124
|
+
const quote = str[i]
|
|
125
|
+
|
|
126
|
+
if (quote === '"' || quote === "'") {
|
|
127
|
+
// Quoted string
|
|
128
|
+
i++ // Skip opening quote
|
|
129
|
+
let value = ""
|
|
130
|
+
while (i < str.length && str[i] !== quote) {
|
|
131
|
+
if (str[i] === "\\" && i + 1 < str.length) {
|
|
132
|
+
// Escaped character - add the next character literally
|
|
133
|
+
value += str[i + 1]
|
|
134
|
+
i += 2
|
|
135
|
+
} else {
|
|
136
|
+
value += str[i]
|
|
137
|
+
i++
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
attributes[name] = value
|
|
141
|
+
if (i < str.length) i++ // Skip closing quote
|
|
142
|
+
} else if (str[i] === "{") {
|
|
143
|
+
// Variable reference
|
|
144
|
+
i++ // Skip {
|
|
145
|
+
const valueStart = i
|
|
146
|
+
while (i < str.length && str[i] !== "}") {
|
|
147
|
+
i++
|
|
148
|
+
}
|
|
149
|
+
const variableName = str.substring(valueStart, i).trim()
|
|
150
|
+
attributes[name] = { __variable: variableName }
|
|
151
|
+
if (i < str.length) i++ // Skip }
|
|
152
|
+
} else {
|
|
153
|
+
// Unquoted value (until space or end)
|
|
154
|
+
const valueStart = i
|
|
155
|
+
while (i < str.length && !/\s/.test(str[i])) {
|
|
156
|
+
i++
|
|
157
|
+
}
|
|
158
|
+
const value = str.substring(valueStart, i)
|
|
159
|
+
attributes[name] = value
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return attributes
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Resolve attributes by replacing variable references with actual values
|
|
168
|
+
* @param {Object} attributes - Attributes with possible variable references
|
|
169
|
+
* @param {Object} data - Data object containing variable values
|
|
170
|
+
* @returns {Object} - Resolved attributes
|
|
171
|
+
*/
|
|
172
|
+
function resolveAttributes(attributes, data) {
|
|
173
|
+
if (!attributes || typeof attributes !== "object") {
|
|
174
|
+
return {}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const resolved = {}
|
|
178
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
179
|
+
if (value && typeof value === "object" && value.__variable) {
|
|
180
|
+
// Resolve variable
|
|
181
|
+
const variableName = value.__variable
|
|
182
|
+
resolved[key] =
|
|
183
|
+
data && data[variableName] !== undefined
|
|
184
|
+
? data[variableName]
|
|
185
|
+
: undefined
|
|
186
|
+
} else {
|
|
187
|
+
resolved[key] = value
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return resolved
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = {
|
|
195
|
+
parseCustomTag,
|
|
196
|
+
parseAttributes,
|
|
197
|
+
resolveAttributes,
|
|
198
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replace {variableName} placeholders in text with actual values from data
|
|
3
|
+
* @param {string} text - Text containing variable placeholders
|
|
4
|
+
* @param {Object} data - Data object with variable values
|
|
5
|
+
* @returns {string|Array} - Text with variables replaced, or array if mixed content
|
|
6
|
+
*/
|
|
7
|
+
function replaceVariables(text, data) {
|
|
8
|
+
if (!text || typeof text !== "string") {
|
|
9
|
+
return text
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (!data || typeof data !== "object") {
|
|
13
|
+
return text
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Check if text contains any variables
|
|
17
|
+
if (!text.includes("{") || !text.includes("}")) {
|
|
18
|
+
return text
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const result = []
|
|
22
|
+
let i = 0
|
|
23
|
+
let lastIndex = 0
|
|
24
|
+
|
|
25
|
+
while (i < text.length) {
|
|
26
|
+
if (text[i] === "\\" && text[i + 1] === "{") {
|
|
27
|
+
// Escaped opening brace
|
|
28
|
+
result.push(text.substring(lastIndex, i))
|
|
29
|
+
result.push("{")
|
|
30
|
+
i += 2
|
|
31
|
+
lastIndex = i
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (text[i] === "{") {
|
|
36
|
+
const closeIndex = text.indexOf("}", i + 1)
|
|
37
|
+
|
|
38
|
+
if (closeIndex !== -1) {
|
|
39
|
+
// Found a variable placeholder
|
|
40
|
+
const variableName = text.substring(i + 1, closeIndex).trim()
|
|
41
|
+
|
|
42
|
+
if (variableName) {
|
|
43
|
+
// Add text before the variable
|
|
44
|
+
if (i > lastIndex) {
|
|
45
|
+
result.push(text.substring(lastIndex, i))
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Add the variable value
|
|
49
|
+
const value = data[variableName]
|
|
50
|
+
if (value !== undefined && value !== null) {
|
|
51
|
+
result.push(String(value))
|
|
52
|
+
} else {
|
|
53
|
+
// Variable not found, keep the placeholder
|
|
54
|
+
result.push(text.substring(i, closeIndex + 1))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
i = closeIndex + 1
|
|
58
|
+
lastIndex = i
|
|
59
|
+
continue
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
i++
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Add remaining text
|
|
68
|
+
if (lastIndex < text.length) {
|
|
69
|
+
result.push(text.substring(lastIndex))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// If no substitutions were made, return original text
|
|
73
|
+
if (result.length === 0) {
|
|
74
|
+
return text
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return result.join("")
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { replaceVariables }
|