boxwood 2.13.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 +2 -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 +1 -1
- 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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
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} allComponents - HTML component functions (contains a, img, code, strong, em, etc.)
|
|
8
|
+
* @returns {Array|string} - Formatted content as array of components and strings
|
|
9
|
+
*/
|
|
10
|
+
function format(text, allComponents) {
|
|
11
|
+
const { a: A, img: Img, code: Code, strong: Strong, em: Em } = allComponents
|
|
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, allComponents)))
|
|
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 (
|
|
118
|
+
content.includes("@") &&
|
|
119
|
+
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(content)
|
|
120
|
+
) {
|
|
121
|
+
result.push(A({ href: `mailto:${content}` }, content))
|
|
122
|
+
i = end + 1
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Not a valid autolink, treat as regular text
|
|
128
|
+
result.push(text[i])
|
|
129
|
+
i++
|
|
130
|
+
} else if (text[i] === "`") {
|
|
131
|
+
const end = text.indexOf("`", i + 1)
|
|
132
|
+
if (end === -1) {
|
|
133
|
+
result.push(text[i])
|
|
134
|
+
i++
|
|
135
|
+
} else {
|
|
136
|
+
result.push(Code({}, text.substring(i + 1, end)))
|
|
137
|
+
i = end + 1
|
|
138
|
+
}
|
|
139
|
+
} else if (text[i] === "*" && text[i + 1] === "*") {
|
|
140
|
+
const end = text.indexOf("**", i + 2)
|
|
141
|
+
if (end === -1) {
|
|
142
|
+
result.push(text[i])
|
|
143
|
+
i++
|
|
144
|
+
} else {
|
|
145
|
+
result.push(Strong(format(text.substring(i + 2, end), allComponents)))
|
|
146
|
+
i = end + 2
|
|
147
|
+
}
|
|
148
|
+
} else if (text[i] === "*") {
|
|
149
|
+
const end = text.indexOf("*", i + 1)
|
|
150
|
+
if (end === -1) {
|
|
151
|
+
result.push(text[i])
|
|
152
|
+
i++
|
|
153
|
+
} else {
|
|
154
|
+
result.push(Em(format(text.substring(i + 1, end), allComponents)))
|
|
155
|
+
i = end + 1
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
// Find next special character
|
|
159
|
+
const positions = [
|
|
160
|
+
text.indexOf("`", i),
|
|
161
|
+
text.indexOf("*", i),
|
|
162
|
+
text.indexOf("[", i),
|
|
163
|
+
text.indexOf("<", i),
|
|
164
|
+
text.indexOf("\\", i),
|
|
165
|
+
].filter((pos) => pos !== -1)
|
|
166
|
+
|
|
167
|
+
// Look for image pattern ![
|
|
168
|
+
const exclamPos = text.indexOf("!", i)
|
|
169
|
+
if (exclamPos !== -1 && text[exclamPos + 1] === "[") {
|
|
170
|
+
positions.push(exclamPos)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const next = positions.length > 0 ? Math.min(...positions) : text.length
|
|
174
|
+
|
|
175
|
+
result.push(text.substring(i, next))
|
|
176
|
+
if (next === text.length) break
|
|
177
|
+
i = next
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return result.length > 0 ? result : text
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = { format }
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts HTML parameters from params object by filtering out special keys
|
|
3
|
+
*/
|
|
4
|
+
function extractHtmlParams(params) {
|
|
5
|
+
if (!params) return {}
|
|
6
|
+
|
|
7
|
+
return Object.keys(params).reduce((acc, key) => {
|
|
8
|
+
if (key !== "components" && key !== "data") {
|
|
9
|
+
acc[key] = params[key]
|
|
10
|
+
}
|
|
11
|
+
return acc
|
|
12
|
+
}, {})
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Merges builtin HTML tags with custom components
|
|
17
|
+
* Custom components can override builtin tags if needed
|
|
18
|
+
*/
|
|
19
|
+
function mergeComponents(builtinHtmlTags, customComponents) {
|
|
20
|
+
return {
|
|
21
|
+
...builtinHtmlTags,
|
|
22
|
+
...customComponents,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
extractHtmlParams,
|
|
28
|
+
mergeComponents,
|
|
29
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
const { parseCustomTag } = require("./parseCustomTag")
|
|
2
|
+
const { replaceVariables } = require("./replaceVariables")
|
|
3
|
+
const { format } = require("./format")
|
|
4
|
+
|
|
5
|
+
// Markdown patterns
|
|
6
|
+
const ORDERED_LIST_REGEXP = /^\d+\.\s/
|
|
7
|
+
const UNORDERED_MARKERS = ["- ", "— ", "– ", "• "]
|
|
8
|
+
const HORIZONTAL_RULE_REGEXP = /^(?:\*\s*\*\s*\*+|-\s*-\s*-+|_\s*_\s*_+)\s*$/
|
|
9
|
+
const HEADINGS = [
|
|
10
|
+
{ prefix: "###### ", type: "h6" },
|
|
11
|
+
{ prefix: "##### ", type: "h5" },
|
|
12
|
+
{ prefix: "#### ", type: "h4" },
|
|
13
|
+
{ prefix: "### ", type: "h3" },
|
|
14
|
+
{ prefix: "## ", type: "h2" },
|
|
15
|
+
{ prefix: "# ", type: "h1" },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Checks if a line is a code block delimiter (```)
|
|
20
|
+
*/
|
|
21
|
+
function isCodeBlockDelimiter(trimmedLine) {
|
|
22
|
+
return trimmedLine.startsWith("```")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parses a code block starting from the current index
|
|
27
|
+
* Returns the code block item and the next index to process
|
|
28
|
+
*/
|
|
29
|
+
function parseCodeBlock(allLines, startIndex) {
|
|
30
|
+
const line = allLines[startIndex]
|
|
31
|
+
const language = line.trim().substring(3).trim()
|
|
32
|
+
const codeLines = []
|
|
33
|
+
let i = startIndex + 1
|
|
34
|
+
|
|
35
|
+
// Collect lines until closing ```
|
|
36
|
+
while (i < allLines.length) {
|
|
37
|
+
const codeLine = allLines[i]
|
|
38
|
+
if (isCodeBlockDelimiter(codeLine.trim())) {
|
|
39
|
+
i++ // Skip closing delimiter
|
|
40
|
+
break
|
|
41
|
+
}
|
|
42
|
+
codeLines.push(codeLine)
|
|
43
|
+
i++
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
item: {
|
|
48
|
+
type: "pre",
|
|
49
|
+
content: codeLines.join("\n"),
|
|
50
|
+
language: language || undefined,
|
|
51
|
+
indent: 0,
|
|
52
|
+
},
|
|
53
|
+
nextIndex: i,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Collects content lines for a multi-line custom component
|
|
59
|
+
*/
|
|
60
|
+
function collectComponentContent(allLines, startIndex, tagName, allComponents) {
|
|
61
|
+
const contentLines = []
|
|
62
|
+
let i = startIndex
|
|
63
|
+
let depth = 1
|
|
64
|
+
|
|
65
|
+
while (i < allLines.length && depth > 0) {
|
|
66
|
+
const contentLine = allLines[i]
|
|
67
|
+
const contentTag = parseCustomTag(contentLine, allComponents)
|
|
68
|
+
|
|
69
|
+
if (contentTag && contentTag.tagName === tagName) {
|
|
70
|
+
if (contentTag.type === "custom-component-open") {
|
|
71
|
+
depth++
|
|
72
|
+
contentLines.push(contentLine)
|
|
73
|
+
} else if (contentTag.type === "custom-component-close") {
|
|
74
|
+
depth--
|
|
75
|
+
if (depth === 0) {
|
|
76
|
+
i++
|
|
77
|
+
break
|
|
78
|
+
}
|
|
79
|
+
contentLines.push(contentLine)
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
contentLines.push(contentLine)
|
|
83
|
+
}
|
|
84
|
+
i++
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
content: contentLines.join("\n"),
|
|
89
|
+
nextIndex: i,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Processes a custom component tag and returns the appropriate item
|
|
95
|
+
*/
|
|
96
|
+
function processCustomComponent(
|
|
97
|
+
line,
|
|
98
|
+
allLines,
|
|
99
|
+
currentIndex,
|
|
100
|
+
customTag,
|
|
101
|
+
data,
|
|
102
|
+
allComponents,
|
|
103
|
+
) {
|
|
104
|
+
const indent = line.length - line.trimStart().length
|
|
105
|
+
|
|
106
|
+
// Single-line component with content
|
|
107
|
+
if (customTag.type === "custom-component-single-line") {
|
|
108
|
+
const processedContent = replaceVariables(customTag.content, data)
|
|
109
|
+
const formattedContent = format(processedContent, allComponents)
|
|
110
|
+
return {
|
|
111
|
+
item: {
|
|
112
|
+
type: "custom-component",
|
|
113
|
+
tagName: customTag.tagName,
|
|
114
|
+
component: customTag.component,
|
|
115
|
+
attributes: customTag.attributes,
|
|
116
|
+
content: formattedContent,
|
|
117
|
+
selfClosing: false,
|
|
118
|
+
singleLine: true,
|
|
119
|
+
indent,
|
|
120
|
+
},
|
|
121
|
+
nextIndex: currentIndex + 1,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Self-closing component
|
|
126
|
+
if (customTag.type === "custom-component" && customTag.selfClosing) {
|
|
127
|
+
return {
|
|
128
|
+
item: {
|
|
129
|
+
type: "custom-component",
|
|
130
|
+
tagName: customTag.tagName,
|
|
131
|
+
component: customTag.component,
|
|
132
|
+
attributes: customTag.attributes,
|
|
133
|
+
selfClosing: true,
|
|
134
|
+
indent,
|
|
135
|
+
},
|
|
136
|
+
nextIndex: currentIndex + 1,
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Multi-line component
|
|
141
|
+
if (customTag.type === "custom-component-open") {
|
|
142
|
+
const { content, nextIndex } = collectComponentContent(
|
|
143
|
+
allLines,
|
|
144
|
+
currentIndex + 1,
|
|
145
|
+
customTag.tagName,
|
|
146
|
+
allComponents,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
item: {
|
|
151
|
+
type: "custom-component",
|
|
152
|
+
tagName: customTag.tagName,
|
|
153
|
+
component: customTag.component,
|
|
154
|
+
attributes: customTag.attributes,
|
|
155
|
+
content,
|
|
156
|
+
selfClosing: false,
|
|
157
|
+
indent,
|
|
158
|
+
},
|
|
159
|
+
nextIndex,
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return null
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Determines the type and content of a markdown line
|
|
168
|
+
*/
|
|
169
|
+
function parseMarkdownLine(line) {
|
|
170
|
+
const trimmed = line.trim()
|
|
171
|
+
const leadingSpaces = line.length - line.trimStart().length
|
|
172
|
+
|
|
173
|
+
// Horizontal rule
|
|
174
|
+
if (HORIZONTAL_RULE_REGEXP.test(trimmed)) {
|
|
175
|
+
return { type: "hr", indent: leadingSpaces }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Unordered list
|
|
179
|
+
const unorderedMarker = UNORDERED_MARKERS.find((marker) =>
|
|
180
|
+
trimmed.startsWith(marker),
|
|
181
|
+
)
|
|
182
|
+
if (unorderedMarker) {
|
|
183
|
+
const content = trimmed.substring(2)
|
|
184
|
+
if (content) {
|
|
185
|
+
return { type: "li", list: "ul", content, indent: leadingSpaces }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Ordered list
|
|
190
|
+
if (ORDERED_LIST_REGEXP.test(trimmed)) {
|
|
191
|
+
const content = trimmed.replace(ORDERED_LIST_REGEXP, "")
|
|
192
|
+
if (content) {
|
|
193
|
+
return { type: "li", list: "ol", content, indent: leadingSpaces }
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Headings
|
|
198
|
+
for (const { prefix, type } of HEADINGS) {
|
|
199
|
+
if (trimmed.startsWith(prefix)) {
|
|
200
|
+
return {
|
|
201
|
+
type,
|
|
202
|
+
content: trimmed.substring(prefix.length),
|
|
203
|
+
indent: leadingSpaces,
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Blockquote
|
|
209
|
+
if (trimmed.startsWith("> ")) {
|
|
210
|
+
return {
|
|
211
|
+
type: "blockquote",
|
|
212
|
+
content: trimmed.substring(2),
|
|
213
|
+
indent: leadingSpaces,
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Default to paragraph
|
|
218
|
+
return { type: "p", content: trimmed, indent: leadingSpaces }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Parses all lines of markdown into structured items
|
|
223
|
+
*/
|
|
224
|
+
function parseMarkdownLines(children, allComponents, data) {
|
|
225
|
+
const allLines = children.trim().split("\n")
|
|
226
|
+
const items = []
|
|
227
|
+
let i = 0
|
|
228
|
+
|
|
229
|
+
while (i < allLines.length) {
|
|
230
|
+
const line = allLines[i]
|
|
231
|
+
const trimmed = line.trim()
|
|
232
|
+
|
|
233
|
+
// Skip empty lines
|
|
234
|
+
if (trimmed.length === 0) {
|
|
235
|
+
i++
|
|
236
|
+
continue
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Check for custom component tags
|
|
240
|
+
const customTag = parseCustomTag(line, allComponents)
|
|
241
|
+
if (customTag) {
|
|
242
|
+
const result = processCustomComponent(
|
|
243
|
+
line,
|
|
244
|
+
allLines,
|
|
245
|
+
i,
|
|
246
|
+
customTag,
|
|
247
|
+
data,
|
|
248
|
+
allComponents,
|
|
249
|
+
)
|
|
250
|
+
if (result) {
|
|
251
|
+
items.push(result.item)
|
|
252
|
+
i = result.nextIndex
|
|
253
|
+
continue
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Check for code blocks
|
|
258
|
+
if (isCodeBlockDelimiter(trimmed)) {
|
|
259
|
+
const { item, nextIndex } = parseCodeBlock(allLines, i)
|
|
260
|
+
items.push(item)
|
|
261
|
+
i = nextIndex
|
|
262
|
+
continue
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Parse regular markdown line
|
|
266
|
+
const item = parseMarkdownLine(line)
|
|
267
|
+
if (item) {
|
|
268
|
+
items.push(item)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
i++
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return items
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
module.exports = {
|
|
278
|
+
isCodeBlockDelimiter,
|
|
279
|
+
parseCodeBlock,
|
|
280
|
+
collectComponentContent,
|
|
281
|
+
processCustomComponent,
|
|
282
|
+
parseMarkdownLine,
|
|
283
|
+
parseMarkdownLines,
|
|
284
|
+
}
|