boxwood 2.16.0 → 2.18.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/.claude/settings.local.json +14 -0
- package/package.json +1 -1
- package/ui/markdown/index.js +3 -3
- package/ui/prose/README.md +168 -16
- package/ui/prose/index.js +36 -5
- package/ui/prose/utilities/processConditionals.js +1 -1
- package/ui/prose/utilities/processLoops.js +70 -9
- package/ui/prose/utilities/protectCode.js +111 -0
- package/ui/prose/utilities/stripComments.js +19 -0
- package/ui/prose/utilities/validate.js +304 -0
- package/ui/{prose/utilities → utilities}/parseCustomTag.js +4 -3
- package/ui/utilities/replaceVariables.js +457 -0
- package/ui/markdown/utilities/parseCustomTag.js +0 -218
- package/ui/markdown/utilities/replaceVariables.js +0 -115
- package/ui/prose/utilities/brackets.js +0 -21
- package/ui/prose/utilities/convertNodes.js +0 -193
- package/ui/prose/utilities/format.js +0 -184
- package/ui/prose/utilities/params.js +0 -29
- package/ui/prose/utilities/parseBlock.js +0 -284
- package/ui/prose/utilities/replaceVariables.js +0 -120
- /package/ui/{markdown/utilities → utilities}/brackets.js +0 -0
- /package/ui/{markdown/utilities → utilities}/convertNodes.js +0 -0
- /package/ui/{markdown/utilities → utilities}/format.js +0 -0
- /package/ui/{markdown/utilities → utilities}/params.js +0 -0
- /package/ui/{markdown/utilities → utilities}/parseBlock.js +0 -0
|
@@ -1,284 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolve a path like "images[0].src" or "user.name" from a data object
|
|
3
|
-
* @param {Object} data - The data object to resolve the path from
|
|
4
|
-
* @param {string} path - The path to resolve (e.g., "images[0].src", "user.name")
|
|
5
|
-
* @returns {*} - The resolved value or undefined
|
|
6
|
-
*/
|
|
7
|
-
function resolvePath(data, path) {
|
|
8
|
-
// Handle null or undefined data
|
|
9
|
-
if (data === null || data === undefined) {
|
|
10
|
-
return undefined
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// Handle simple variable names (backwards compatibility)
|
|
14
|
-
if (!/[.\[]/.test(path)) {
|
|
15
|
-
return data[path]
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Split the path into parts, handling both dot notation and bracket notation
|
|
19
|
-
// e.g., "images[0].src" -> ["images", "0", "src"]
|
|
20
|
-
const parts = path
|
|
21
|
-
.replace(/\[(\d+)\]/g, ".$1") // Convert [0] to .0
|
|
22
|
-
.split(".")
|
|
23
|
-
.filter(Boolean)
|
|
24
|
-
|
|
25
|
-
let current = data
|
|
26
|
-
for (const part of parts) {
|
|
27
|
-
if (current === null || current === undefined) {
|
|
28
|
-
return undefined
|
|
29
|
-
}
|
|
30
|
-
current = current[part]
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return current
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Replace {variableName} placeholders in text with actual values from data
|
|
38
|
-
* Supports:
|
|
39
|
-
* - Simple variables: {name}
|
|
40
|
-
* - Array indexing: {images[0]}
|
|
41
|
-
* - Property access: {user.name}
|
|
42
|
-
* - Combined: {images[0].src}
|
|
43
|
-
* @param {string} text - Text containing variable placeholders
|
|
44
|
-
* @param {Object} data - Data object with variable values
|
|
45
|
-
* @returns {string|Array} - Text with variables replaced, or array if mixed content
|
|
46
|
-
*/
|
|
47
|
-
function replaceVariables(text, data) {
|
|
48
|
-
if (!text || typeof text !== "string") {
|
|
49
|
-
return text
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (!data || typeof data !== "object") {
|
|
53
|
-
return text
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Check if text contains any variables
|
|
57
|
-
if (!text.includes("{") || !text.includes("}")) {
|
|
58
|
-
return text
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const result = []
|
|
62
|
-
let i = 0
|
|
63
|
-
let lastIndex = 0
|
|
64
|
-
|
|
65
|
-
while (i < text.length) {
|
|
66
|
-
if (text[i] === "\\" && text[i + 1] === "{") {
|
|
67
|
-
// Escaped opening brace
|
|
68
|
-
result.push(text.substring(lastIndex, i))
|
|
69
|
-
result.push("{")
|
|
70
|
-
i += 2
|
|
71
|
-
lastIndex = i
|
|
72
|
-
continue
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (text[i] === "{") {
|
|
76
|
-
const closeIndex = text.indexOf("}", i + 1)
|
|
77
|
-
|
|
78
|
-
if (closeIndex !== -1) {
|
|
79
|
-
// Found a variable placeholder
|
|
80
|
-
const variablePath = text.substring(i + 1, closeIndex).trim()
|
|
81
|
-
|
|
82
|
-
if (variablePath) {
|
|
83
|
-
// Add text before the variable
|
|
84
|
-
if (i > lastIndex) {
|
|
85
|
-
result.push(text.substring(lastIndex, i))
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Resolve the variable value (supports paths like "images[0].src")
|
|
89
|
-
const value = resolvePath(data, variablePath)
|
|
90
|
-
if (value !== undefined && value !== null) {
|
|
91
|
-
result.push(String(value))
|
|
92
|
-
} else {
|
|
93
|
-
// Variable not found, keep the placeholder
|
|
94
|
-
result.push(text.substring(i, closeIndex + 1))
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
i = closeIndex + 1
|
|
98
|
-
lastIndex = i
|
|
99
|
-
continue
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
i++
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Add remaining text
|
|
108
|
-
if (lastIndex < text.length) {
|
|
109
|
-
result.push(text.substring(lastIndex))
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// If no substitutions were made, return original text
|
|
113
|
-
if (result.length === 0) {
|
|
114
|
-
return text
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return result.join("")
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
module.exports = { replaceVariables, resolvePath }
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|