boxwood 2.12.0 → 2.14.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 +7 -5
- package/ui/index.js +2 -0
- package/ui/markdown/README.md +48 -0
- package/ui/markdown/index.js +45 -314
- package/ui/markdown/utilities/convertNodes.js +194 -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 +31 -16
- package/ui/prose/README.md +492 -0
- package/ui/prose/index.js +153 -0
- package/ui/prose/utilities/brackets.js +21 -0
- package/ui/prose/utilities/convertNodes.js +194 -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.14.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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { component, css, Div } = require("../..")
|
|
2
2
|
const { normalizeGap, normalizeBreakpoint } = require("../normalize")
|
|
3
|
+
const { toNumber } = require("../normalize")
|
|
3
4
|
|
|
4
5
|
const BREAKPOINTS = {
|
|
5
6
|
xl: "1199px",
|
|
@@ -10,11 +11,14 @@ const BREAKPOINTS = {
|
|
|
10
11
|
|
|
11
12
|
function Grid(
|
|
12
13
|
{ className, columns = 3, gap, id, breakpoint, style },
|
|
13
|
-
children
|
|
14
|
+
children,
|
|
14
15
|
) {
|
|
15
16
|
gap = normalizeGap(gap)
|
|
16
17
|
breakpoint = normalizeBreakpoint(breakpoint)
|
|
17
18
|
|
|
19
|
+
// Normalize columns: convert string numbers to numbers
|
|
20
|
+
columns = toNumber(columns)
|
|
21
|
+
|
|
18
22
|
const styleObject = {
|
|
19
23
|
"box-sizing": "border-box",
|
|
20
24
|
display: "grid",
|
|
@@ -27,10 +31,8 @@ function Grid(
|
|
|
27
31
|
}),
|
|
28
32
|
...(typeof columns === "object" &&
|
|
29
33
|
Object.keys(columns).reduce((object, key) => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
? `repeat(${columns[key]}, 1fr)`
|
|
33
|
-
: columns[key]
|
|
34
|
+
let value = toNumber(columns[key])
|
|
35
|
+
value = typeof value === "number" ? `repeat(${value}, 1fr)` : value
|
|
34
36
|
if (key === "default") {
|
|
35
37
|
object["grid-template-columns"] = value
|
|
36
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,63 @@
|
|
|
1
|
-
const
|
|
2
|
-
|
|
3
|
-
H1,
|
|
4
|
-
H2,
|
|
5
|
-
H3,
|
|
6
|
-
H4,
|
|
7
|
-
H5,
|
|
8
|
-
H6,
|
|
9
|
-
P,
|
|
10
|
-
Blockquote,
|
|
11
|
-
Ul,
|
|
12
|
-
Ol,
|
|
13
|
-
Li,
|
|
14
|
-
Strong,
|
|
15
|
-
Em,
|
|
16
|
-
Code,
|
|
17
|
-
A,
|
|
18
|
-
Hr,
|
|
19
|
-
Img,
|
|
20
|
-
Pre,
|
|
21
|
-
} = require("../..")
|
|
1
|
+
const nodes = require("../..")
|
|
2
|
+
const { component } = nodes
|
|
22
3
|
|
|
23
|
-
const {
|
|
24
|
-
const {
|
|
25
|
-
|
|
26
|
-
resolveAttributes,
|
|
27
|
-
} = require("./utilities/parseCustomTag")
|
|
28
|
-
const { replaceVariables } = require("./utilities/replaceVariables")
|
|
4
|
+
const { extractHtmlParams } = require("./utilities/params")
|
|
5
|
+
const { parseMarkdownLines } = require("./utilities/parseBlock")
|
|
6
|
+
const { convertItemsToNodes } = require("./utilities/convertNodes")
|
|
29
7
|
|
|
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
8
|
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,
|
|
9
|
+
h1: nodes.H1,
|
|
10
|
+
h2: nodes.H2,
|
|
11
|
+
h3: nodes.H3,
|
|
12
|
+
h4: nodes.H4,
|
|
13
|
+
h5: nodes.H5,
|
|
14
|
+
h6: nodes.H6,
|
|
15
|
+
blockquote: nodes.Blockquote,
|
|
16
|
+
hr: nodes.Hr,
|
|
50
17
|
}
|
|
51
18
|
|
|
52
|
-
|
|
19
|
+
// Basic HTML components needed for markdown formatting (links, images, code, etc.)
|
|
20
|
+
const BASIC_HTML_COMPONENTS = {
|
|
21
|
+
a: nodes.A,
|
|
22
|
+
img: nodes.Img,
|
|
23
|
+
code: nodes.Code,
|
|
24
|
+
strong: nodes.Strong,
|
|
25
|
+
em: nodes.Em,
|
|
26
|
+
p: nodes.P,
|
|
27
|
+
ul: nodes.Ul,
|
|
28
|
+
ol: nodes.Ol,
|
|
29
|
+
li: nodes.Li,
|
|
30
|
+
pre: nodes.Pre,
|
|
31
|
+
blockquote: nodes.Blockquote,
|
|
32
|
+
hr: nodes.Hr,
|
|
33
|
+
}
|
|
53
34
|
|
|
54
35
|
function Markdown(params, children) {
|
|
36
|
+
// Handle array of children recursively
|
|
55
37
|
if (Array.isArray(children)) {
|
|
56
38
|
return children.map((child) => Markdown(params, child))
|
|
57
39
|
}
|
|
58
40
|
|
|
41
|
+
// Only process string content
|
|
59
42
|
if (typeof children !== "string") {
|
|
60
43
|
return null
|
|
61
44
|
}
|
|
62
45
|
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
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
|
-
}
|
|
224
|
-
|
|
225
|
-
const nodes = []
|
|
226
|
-
i = 0
|
|
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
|
-
}
|
|
328
|
-
|
|
329
|
-
return nodes
|
|
46
|
+
const htmlParams = extractHtmlParams(params)
|
|
47
|
+
|
|
48
|
+
// Parse all markdown lines into structured items (no custom components, no data)
|
|
49
|
+
const items = parseMarkdownLines(children, null, null)
|
|
50
|
+
|
|
51
|
+
// Convert parsed items into final node tree
|
|
52
|
+
return convertItemsToNodes(
|
|
53
|
+
items,
|
|
54
|
+
params,
|
|
55
|
+
htmlParams,
|
|
56
|
+
null, // no data
|
|
57
|
+
BASIC_HTML_COMPONENTS, // basic HTML components for formatting
|
|
58
|
+
COMPONENTS,
|
|
59
|
+
Markdown,
|
|
60
|
+
)
|
|
330
61
|
}
|
|
331
62
|
|
|
332
63
|
module.exports = component(Markdown)
|
|
@@ -0,0 +1,194 @@
|
|
|
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
|
+
components,
|
|
125
|
+
markdownRenderer,
|
|
126
|
+
) {
|
|
127
|
+
const result = []
|
|
128
|
+
let i = 0
|
|
129
|
+
|
|
130
|
+
while (i < items.length) {
|
|
131
|
+
const item = items[i]
|
|
132
|
+
|
|
133
|
+
if (item.type === "custom-component") {
|
|
134
|
+
result.push(
|
|
135
|
+
processCustomComponentItem(item, params, data, markdownRenderer),
|
|
136
|
+
)
|
|
137
|
+
i++
|
|
138
|
+
} else if (item.type === "li") {
|
|
139
|
+
const listResult = parseList(
|
|
140
|
+
items,
|
|
141
|
+
i,
|
|
142
|
+
item.indent,
|
|
143
|
+
htmlParams,
|
|
144
|
+
data,
|
|
145
|
+
allComponents,
|
|
146
|
+
)
|
|
147
|
+
result.push(listResult.list)
|
|
148
|
+
i = listResult.nextIndex
|
|
149
|
+
} else if (item.type === "blockquote") {
|
|
150
|
+
const { node, nextIndex } = processBlockquotes(
|
|
151
|
+
items,
|
|
152
|
+
i,
|
|
153
|
+
htmlParams,
|
|
154
|
+
data,
|
|
155
|
+
allComponents,
|
|
156
|
+
)
|
|
157
|
+
result.push(node)
|
|
158
|
+
i = nextIndex
|
|
159
|
+
} else if (item.type === "hr") {
|
|
160
|
+
result.push(allComponents.hr(htmlParams))
|
|
161
|
+
i++
|
|
162
|
+
} else if (item.type === "pre") {
|
|
163
|
+
// Code blocks - wrap in <pre><code>
|
|
164
|
+
const codeParams = item.language
|
|
165
|
+
? { class: `language-${item.language}` }
|
|
166
|
+
: {}
|
|
167
|
+
result.push(
|
|
168
|
+
allComponents.pre(
|
|
169
|
+
htmlParams,
|
|
170
|
+
allComponents.code(codeParams, item.content),
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
i++
|
|
174
|
+
} else {
|
|
175
|
+
// Regular block elements (h1-h6, p, etc.)
|
|
176
|
+
const { type, content } = item
|
|
177
|
+
const Component = components[type] || allComponents.p
|
|
178
|
+
const processedContent = replaceVariables(content, data)
|
|
179
|
+
result.push(
|
|
180
|
+
Component(htmlParams, format(processedContent, allComponents)),
|
|
181
|
+
)
|
|
182
|
+
i++
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return result
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = {
|
|
190
|
+
parseList,
|
|
191
|
+
processCustomComponentItem,
|
|
192
|
+
processBlockquotes,
|
|
193
|
+
convertItemsToNodes,
|
|
194
|
+
}
|