boxwood 2.13.0 → 2.15.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 +2 -3
- package/ui/grid/index.js +2 -5
- package/ui/index.js +2 -0
- package/ui/markdown/README.md +48 -0
- package/ui/markdown/index.js +43 -315
- package/ui/markdown/utilities/convertNodes.js +193 -0
- package/ui/markdown/utilities/format.js +15 -12
- package/ui/markdown/utilities/params.js +29 -0
- package/ui/markdown/utilities/parseBlock.js +284 -0
- package/ui/markdown/utilities/parseCustomTag.js +20 -0
- package/ui/markdown/utilities/replaceVariables.js +39 -4
- package/ui/normalize.js +1 -1
- package/ui/prose/README.md +492 -0
- package/ui/prose/index.js +159 -0
- package/ui/prose/utilities/brackets.js +21 -0
- package/ui/prose/utilities/convertNodes.js +193 -0
- package/ui/prose/utilities/format.js +184 -0
- package/ui/prose/utilities/params.js +29 -0
- package/ui/prose/utilities/parseBlock.js +284 -0
- package/ui/prose/utilities/parseCustomTag.js +218 -0
- package/ui/prose/utilities/processConditionals.js +264 -0
- package/ui/prose/utilities/processLoops.js +171 -0
- package/ui/prose/utilities/replaceVariables.js +115 -0
- package/examples/typescript-example.ts +0 -49
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "boxwood",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.15.0",
|
|
4
4
|
"description": "Compile HTML templates into JS",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"scripts": {
|
|
13
13
|
"test": "node --test --test-reporter=dot \"**/*.test.js\" \"**/*.spec.js\"",
|
|
14
14
|
"test:debug": "node --test --test-reporter=spec \"**/*.test.js\" \"**/*.spec.js\"",
|
|
15
|
-
"coverage": "
|
|
15
|
+
"coverage": "node --test --experimental-test-coverage \"**/*.test.js\" \"**/*.spec.js\"",
|
|
16
16
|
"benchmark": "node --test benchmark/index.js",
|
|
17
17
|
"watch": "npm test -- --watch",
|
|
18
18
|
"prepush": "npm test"
|
|
@@ -47,7 +47,6 @@
|
|
|
47
47
|
"homepage": "https://github.com/buxlabs/boxwood#readme",
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"benchmark": "2.1.4",
|
|
50
|
-
"c8": "^11.0.0",
|
|
51
50
|
"express": "^5.2.1",
|
|
52
51
|
"handlebars": "^4.7.9",
|
|
53
52
|
"jsdom": "^29.1.1",
|
package/ui/grid/index.js
CHANGED
|
@@ -11,7 +11,7 @@ const BREAKPOINTS = {
|
|
|
11
11
|
|
|
12
12
|
function Grid(
|
|
13
13
|
{ className, columns = 3, gap, id, breakpoint, style },
|
|
14
|
-
children
|
|
14
|
+
children,
|
|
15
15
|
) {
|
|
16
16
|
gap = normalizeGap(gap)
|
|
17
17
|
breakpoint = normalizeBreakpoint(breakpoint)
|
|
@@ -32,10 +32,7 @@ function Grid(
|
|
|
32
32
|
...(typeof columns === "object" &&
|
|
33
33
|
Object.keys(columns).reduce((object, key) => {
|
|
34
34
|
let value = toNumber(columns[key])
|
|
35
|
-
value =
|
|
36
|
-
typeof value === "number"
|
|
37
|
-
? `repeat(${value}, 1fr)`
|
|
38
|
-
: value
|
|
35
|
+
value = typeof value === "number" ? `repeat(${value}, 1fr)` : value
|
|
39
36
|
if (key === "default") {
|
|
40
37
|
object["grid-template-columns"] = value
|
|
41
38
|
} else if (typeof key === "string") {
|
package/ui/index.js
CHANGED
|
@@ -4,6 +4,7 @@ const Grid = require("./grid")
|
|
|
4
4
|
const Group = require("./group")
|
|
5
5
|
const Stack = require("./stack")
|
|
6
6
|
const Markdown = require("./markdown")
|
|
7
|
+
const Prose = require("./prose")
|
|
7
8
|
|
|
8
9
|
module.exports = {
|
|
9
10
|
Center,
|
|
@@ -12,4 +13,5 @@ module.exports = {
|
|
|
12
13
|
Group,
|
|
13
14
|
Stack,
|
|
14
15
|
Markdown,
|
|
16
|
+
Prose,
|
|
15
17
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Markdown Component
|
|
2
|
+
|
|
3
|
+
The `Markdown` component provides pure markdown rendering without any templating features. It's a simple, focused component for rendering static markdown content.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Headings (`#`, `##`, etc.)
|
|
8
|
+
- Paragraphs
|
|
9
|
+
- Lists (ordered and unordered)
|
|
10
|
+
- Links and images
|
|
11
|
+
- Code blocks with syntax highlighting support
|
|
12
|
+
- Bold and italic text
|
|
13
|
+
- Inline code
|
|
14
|
+
- Blockquotes
|
|
15
|
+
- Horizontal rules
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
const { Markdown } = require("boxwood/ui")
|
|
21
|
+
|
|
22
|
+
Markdown(`
|
|
23
|
+
# Hello World
|
|
24
|
+
|
|
25
|
+
This is **bold** and this is *italic*.
|
|
26
|
+
|
|
27
|
+
- First item
|
|
28
|
+
- Second item
|
|
29
|
+
- Third item
|
|
30
|
+
|
|
31
|
+
[Link text](https://example.com)
|
|
32
|
+
|
|
33
|
+
\`\`\`javascript
|
|
34
|
+
console.log('Hello')
|
|
35
|
+
\`\`\`
|
|
36
|
+
`)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## When to Use
|
|
40
|
+
|
|
41
|
+
Use `Markdown` when you need:
|
|
42
|
+
|
|
43
|
+
- Simple, static markdown rendering
|
|
44
|
+
- No variables or templating
|
|
45
|
+
- Fast, predictable output
|
|
46
|
+
- Standard markdown behavior
|
|
47
|
+
|
|
48
|
+
For dynamic content with variables and custom components, use the `Prose` component instead.
|
package/ui/markdown/index.js
CHANGED
|
@@ -1,332 +1,60 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const { replaceVariables } = require("./utilities/replaceVariables")
|
|
29
|
-
|
|
30
|
-
const ORDERED_LIST_REGEXP = /^\d+\.\s/
|
|
31
|
-
const UNORDERED_MARKERS = ["- ", "— ", "– ", "• "]
|
|
32
|
-
const HORIZONTAL_RULE_REGEXP = /^(?:\*\s*\*\s*\*+|-\s*-\s*-+|_\s*_\s*_+)\s*$/
|
|
33
|
-
const HEADINGS = [
|
|
34
|
-
{ prefix: "###### ", type: "h6" },
|
|
35
|
-
{ prefix: "##### ", type: "h5" },
|
|
36
|
-
{ prefix: "#### ", type: "h4" },
|
|
37
|
-
{ prefix: "### ", type: "h3" },
|
|
38
|
-
{ prefix: "## ", type: "h2" },
|
|
39
|
-
{ prefix: "# ", type: "h1" },
|
|
40
|
-
]
|
|
41
|
-
const COMPONENTS = {
|
|
42
|
-
h1: H1,
|
|
43
|
-
h2: H2,
|
|
44
|
-
h3: H3,
|
|
45
|
-
h4: H4,
|
|
46
|
-
h5: H5,
|
|
47
|
-
h6: H6,
|
|
48
|
-
blockquote: Blockquote,
|
|
49
|
-
hr: Hr,
|
|
1
|
+
const nodes = require("../..")
|
|
2
|
+
const { component } = nodes
|
|
3
|
+
|
|
4
|
+
const { extractHtmlParams, mergeComponents } = require("./utilities/params")
|
|
5
|
+
const { parseMarkdownLines } = require("./utilities/parseBlock")
|
|
6
|
+
const { convertItemsToNodes } = require("./utilities/convertNodes")
|
|
7
|
+
|
|
8
|
+
// Basic HTML components needed for markdown formatting (links, images, code, etc.)
|
|
9
|
+
const BASIC_HTML_COMPONENTS = {
|
|
10
|
+
h1: nodes.H1,
|
|
11
|
+
h2: nodes.H2,
|
|
12
|
+
h3: nodes.H3,
|
|
13
|
+
h4: nodes.H4,
|
|
14
|
+
h5: nodes.H5,
|
|
15
|
+
h6: nodes.H6,
|
|
16
|
+
a: nodes.A,
|
|
17
|
+
img: nodes.Img,
|
|
18
|
+
code: nodes.Code,
|
|
19
|
+
strong: nodes.Strong,
|
|
20
|
+
em: nodes.Em,
|
|
21
|
+
p: nodes.P,
|
|
22
|
+
ul: nodes.Ul,
|
|
23
|
+
ol: nodes.Ol,
|
|
24
|
+
li: nodes.Li,
|
|
25
|
+
pre: nodes.Pre,
|
|
26
|
+
blockquote: nodes.Blockquote,
|
|
27
|
+
hr: nodes.Hr,
|
|
50
28
|
}
|
|
51
29
|
|
|
52
|
-
const INLINE_COMPONENTS = { A, Img, Code, Strong, Em }
|
|
53
|
-
|
|
54
30
|
function Markdown(params, children) {
|
|
31
|
+
// Handle array of children recursively
|
|
55
32
|
if (Array.isArray(children)) {
|
|
56
33
|
return children.map((child) => Markdown(params, child))
|
|
57
34
|
}
|
|
58
35
|
|
|
36
|
+
// Only process string content
|
|
59
37
|
if (typeof children !== "string") {
|
|
60
38
|
return null
|
|
61
39
|
}
|
|
62
40
|
|
|
63
|
-
const
|
|
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
|
-
}
|
|
41
|
+
const htmlParams = extractHtmlParams(params)
|
|
133
42
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
tagName,
|
|
137
|
-
component: componentFn,
|
|
138
|
-
attributes,
|
|
139
|
-
content: contentLines.join("\n"),
|
|
140
|
-
selfClosing: false,
|
|
141
|
-
indent: openIndent,
|
|
142
|
-
})
|
|
143
|
-
continue
|
|
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 ```
|
|
152
|
-
|
|
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++
|
|
163
|
-
}
|
|
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
|
|
197
|
-
for (const { prefix, type } of HEADINGS) {
|
|
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
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
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
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
i++
|
|
223
|
-
}
|
|
43
|
+
// Merge basic HTML components - all components are now in one place
|
|
44
|
+
const allComponents = mergeComponents(BASIC_HTML_COMPONENTS, null)
|
|
224
45
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
function parseList(startIndex, parentIndent) {
|
|
229
|
-
const list = []
|
|
230
|
-
let currentIndex = startIndex
|
|
231
|
-
const parentListType = items[startIndex].list
|
|
232
|
-
|
|
233
|
-
while (currentIndex < items.length) {
|
|
234
|
-
const item = items[currentIndex]
|
|
235
|
-
|
|
236
|
-
// Stop if not a list item or indent is less than expected
|
|
237
|
-
if (item.type !== "li" || item.indent < parentIndent) {
|
|
238
|
-
break
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Stop if indent matches but list type differs
|
|
242
|
-
if (item.indent === parentIndent && item.list !== parentListType) {
|
|
243
|
-
break
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
// Skip items with greater indent (they belong to nested lists)
|
|
247
|
-
if (item.indent > parentIndent) {
|
|
248
|
-
break
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Process current item at the correct indent level
|
|
252
|
-
const processedContent = replaceVariables(item.content, data)
|
|
253
|
-
const content = [format(processedContent, INLINE_COMPONENTS)]
|
|
254
|
-
currentIndex++
|
|
255
|
-
|
|
256
|
-
// Check if next item is a nested list
|
|
257
|
-
const nextItem = items[currentIndex]
|
|
258
|
-
if (nextItem?.type === "li" && nextItem.indent > item.indent) {
|
|
259
|
-
const nestedResult = parseList(currentIndex, nextItem.indent)
|
|
260
|
-
content.push(nestedResult.list)
|
|
261
|
-
currentIndex = nestedResult.nextIndex
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
list.push(Li(htmlParams, content))
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const listElement =
|
|
268
|
-
parentListType === "ul" ? Ul(htmlParams, list) : Ol(htmlParams, list)
|
|
269
|
-
|
|
270
|
-
return { list: listElement, nextIndex: currentIndex }
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
while (i < items.length) {
|
|
274
|
-
const item = items[i]
|
|
275
|
-
|
|
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") {
|
|
290
|
-
const result = parseList(i, item.indent)
|
|
291
|
-
nodes.push(result.list)
|
|
292
|
-
i = result.nextIndex
|
|
293
|
-
} else if (item.type === "blockquote") {
|
|
294
|
-
const lines = []
|
|
295
|
-
|
|
296
|
-
while (i < items.length && items[i].type === "blockquote") {
|
|
297
|
-
lines.push(items[i].content)
|
|
298
|
-
i++
|
|
299
|
-
}
|
|
300
|
-
|
|
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++
|
|
318
|
-
} else {
|
|
319
|
-
const { type, content } = item
|
|
320
|
-
const Component = COMPONENTS[type] || P
|
|
321
|
-
const processedContent = replaceVariables(content, data)
|
|
322
|
-
nodes.push(
|
|
323
|
-
Component(htmlParams, format(processedContent, INLINE_COMPONENTS)),
|
|
324
|
-
)
|
|
325
|
-
i++
|
|
326
|
-
}
|
|
327
|
-
}
|
|
46
|
+
// Parse all markdown lines into structured items (no custom components, no data)
|
|
47
|
+
const items = parseMarkdownLines(children, null, null)
|
|
328
48
|
|
|
329
|
-
|
|
49
|
+
// Convert parsed items into final node tree
|
|
50
|
+
return convertItemsToNodes(
|
|
51
|
+
items,
|
|
52
|
+
params,
|
|
53
|
+
htmlParams,
|
|
54
|
+
null, // no data
|
|
55
|
+
allComponents,
|
|
56
|
+
Markdown,
|
|
57
|
+
)
|
|
330
58
|
}
|
|
331
59
|
|
|
332
60
|
module.exports = component(Markdown)
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
const { resolveAttributes } = require("./parseCustomTag")
|
|
2
|
+
const { replaceVariables } = require("./replaceVariables")
|
|
3
|
+
const { format } = require("./format")
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Recursively parses nested lists with proper indentation handling
|
|
7
|
+
*/
|
|
8
|
+
function parseList(
|
|
9
|
+
items,
|
|
10
|
+
startIndex,
|
|
11
|
+
parentIndent,
|
|
12
|
+
htmlParams,
|
|
13
|
+
data,
|
|
14
|
+
allComponents,
|
|
15
|
+
) {
|
|
16
|
+
const list = []
|
|
17
|
+
let currentIndex = startIndex
|
|
18
|
+
const parentListType = items[startIndex].list
|
|
19
|
+
|
|
20
|
+
while (currentIndex < items.length) {
|
|
21
|
+
const item = items[currentIndex]
|
|
22
|
+
|
|
23
|
+
// Stop conditions
|
|
24
|
+
if (item.type !== "li" || item.indent < parentIndent) {
|
|
25
|
+
break
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (item.indent === parentIndent && item.list !== parentListType) {
|
|
29
|
+
break
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (item.indent > parentIndent) {
|
|
33
|
+
break
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Process current list item
|
|
37
|
+
const processedContent = replaceVariables(item.content, data)
|
|
38
|
+
const content = [format(processedContent, allComponents)]
|
|
39
|
+
currentIndex++
|
|
40
|
+
|
|
41
|
+
// Check for nested lists
|
|
42
|
+
const nextItem = items[currentIndex]
|
|
43
|
+
if (nextItem?.type === "li" && nextItem.indent > item.indent) {
|
|
44
|
+
const nestedResult = parseList(
|
|
45
|
+
items,
|
|
46
|
+
currentIndex,
|
|
47
|
+
nextItem.indent,
|
|
48
|
+
htmlParams,
|
|
49
|
+
data,
|
|
50
|
+
allComponents,
|
|
51
|
+
)
|
|
52
|
+
content.push(nestedResult.list)
|
|
53
|
+
currentIndex = nestedResult.nextIndex
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
list.push(allComponents.li(htmlParams, content))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const listElement =
|
|
60
|
+
parentListType === "ul"
|
|
61
|
+
? allComponents.ul(htmlParams, list)
|
|
62
|
+
: allComponents.ol(htmlParams, list)
|
|
63
|
+
|
|
64
|
+
return { list: listElement, nextIndex: currentIndex }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Processes a custom component item into a node
|
|
69
|
+
*/
|
|
70
|
+
function processCustomComponentItem(item, params, data, markdownRenderer) {
|
|
71
|
+
const resolvedAttrs = resolveAttributes(item.attributes, data)
|
|
72
|
+
|
|
73
|
+
if (item.selfClosing) {
|
|
74
|
+
return item.component(resolvedAttrs)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (item.singleLine) {
|
|
78
|
+
return item.component(resolvedAttrs, item.content)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Multi-line component with children - recursively process markdown
|
|
82
|
+
const childContent = item.content
|
|
83
|
+
? markdownRenderer(params, item.content)
|
|
84
|
+
: []
|
|
85
|
+
return item.component(resolvedAttrs, childContent)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Processes blockquote items into a single blockquote node
|
|
90
|
+
*/
|
|
91
|
+
function processBlockquotes(
|
|
92
|
+
items,
|
|
93
|
+
startIndex,
|
|
94
|
+
htmlParams,
|
|
95
|
+
data,
|
|
96
|
+
allComponents,
|
|
97
|
+
) {
|
|
98
|
+
const lines = []
|
|
99
|
+
let i = startIndex
|
|
100
|
+
|
|
101
|
+
while (i < items.length && items[i].type === "blockquote") {
|
|
102
|
+
lines.push(items[i].content)
|
|
103
|
+
i++
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const content = replaceVariables(lines.join("\n"), data)
|
|
107
|
+
const blockquote = allComponents.blockquote(
|
|
108
|
+
htmlParams,
|
|
109
|
+
allComponents.p(htmlParams, format(content, allComponents)),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return { node: blockquote, nextIndex: i }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Converts parsed items into final node tree
|
|
117
|
+
*/
|
|
118
|
+
function convertItemsToNodes(
|
|
119
|
+
items,
|
|
120
|
+
params,
|
|
121
|
+
htmlParams,
|
|
122
|
+
data,
|
|
123
|
+
allComponents,
|
|
124
|
+
markdownRenderer,
|
|
125
|
+
) {
|
|
126
|
+
const result = []
|
|
127
|
+
let i = 0
|
|
128
|
+
|
|
129
|
+
while (i < items.length) {
|
|
130
|
+
const item = items[i]
|
|
131
|
+
|
|
132
|
+
if (item.type === "custom-component") {
|
|
133
|
+
result.push(
|
|
134
|
+
processCustomComponentItem(item, params, data, markdownRenderer),
|
|
135
|
+
)
|
|
136
|
+
i++
|
|
137
|
+
} else if (item.type === "li") {
|
|
138
|
+
const listResult = parseList(
|
|
139
|
+
items,
|
|
140
|
+
i,
|
|
141
|
+
item.indent,
|
|
142
|
+
htmlParams,
|
|
143
|
+
data,
|
|
144
|
+
allComponents,
|
|
145
|
+
)
|
|
146
|
+
result.push(listResult.list)
|
|
147
|
+
i = listResult.nextIndex
|
|
148
|
+
} else if (item.type === "blockquote") {
|
|
149
|
+
const { node, nextIndex } = processBlockquotes(
|
|
150
|
+
items,
|
|
151
|
+
i,
|
|
152
|
+
htmlParams,
|
|
153
|
+
data,
|
|
154
|
+
allComponents,
|
|
155
|
+
)
|
|
156
|
+
result.push(node)
|
|
157
|
+
i = nextIndex
|
|
158
|
+
} else if (item.type === "hr") {
|
|
159
|
+
result.push(allComponents.hr(htmlParams))
|
|
160
|
+
i++
|
|
161
|
+
} else if (item.type === "pre") {
|
|
162
|
+
// Code blocks - wrap in <pre><code>
|
|
163
|
+
const codeParams = item.language
|
|
164
|
+
? { class: `language-${item.language}` }
|
|
165
|
+
: {}
|
|
166
|
+
result.push(
|
|
167
|
+
allComponents.pre(
|
|
168
|
+
htmlParams,
|
|
169
|
+
allComponents.code(codeParams, item.content),
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
i++
|
|
173
|
+
} else {
|
|
174
|
+
// Regular block elements (h1-h6, p, etc.)
|
|
175
|
+
const { type, content } = item
|
|
176
|
+
const Component = allComponents[type] || allComponents.p
|
|
177
|
+
const processedContent = replaceVariables(content, data)
|
|
178
|
+
result.push(
|
|
179
|
+
Component(htmlParams, format(processedContent, allComponents)),
|
|
180
|
+
)
|
|
181
|
+
i++
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return result
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = {
|
|
189
|
+
parseList,
|
|
190
|
+
processCustomComponentItem,
|
|
191
|
+
processBlockquotes,
|
|
192
|
+
convertItemsToNodes,
|
|
193
|
+
}
|