boxwood 2.17.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.
@@ -1,218 +0,0 @@
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 single-line tag: <tag>content</tag> or <tag attr="value">content</tag>
16
- const singleLineMatch = trimmed.match(
17
- /^<([A-Za-z][A-Za-z0-9-]*)(\s+[^>]*)?>(.+?)<\/\1>\s*$/,
18
- )
19
- if (singleLineMatch) {
20
- const [, tagName, attributesStr, content] = singleLineMatch
21
- const component = customComponents[tagName]
22
- if (component) {
23
- const attributes = parseAttributes(attributesStr || "")
24
- return {
25
- type: "custom-component-single-line",
26
- tagName,
27
- component,
28
- attributes,
29
- content,
30
- selfClosing: false,
31
- }
32
- }
33
- }
34
-
35
- // Check for self-closing tag: <ComponentName ... /> (space before / is optional)
36
- const selfClosingMatch = trimmed.match(
37
- /^<([A-Za-z][A-Za-z0-9-]*)(\s+[^>]*)?\s*\/>\s*$/,
38
- )
39
- if (selfClosingMatch) {
40
- const [, tagName, attributesStr] = selfClosingMatch
41
- const component = customComponents[tagName]
42
- if (component) {
43
- const attributes = parseAttributes(attributesStr || "")
44
- return {
45
- type: "custom-component",
46
- tagName,
47
- component,
48
- attributes,
49
- selfClosing: true,
50
- }
51
- }
52
- }
53
-
54
- // Check for opening tag: <ComponentName ...>
55
- const openTagMatch = trimmed.match(
56
- /^<([A-Za-z][A-Za-z0-9-]*)(\s+[^>]*)?>\s*$/,
57
- )
58
- if (openTagMatch) {
59
- const [, tagName, attributesStr] = openTagMatch
60
- const component = customComponents[tagName]
61
- if (component) {
62
- const attributes = parseAttributes(attributesStr || "")
63
- return {
64
- type: "custom-component-open",
65
- tagName,
66
- component,
67
- attributes,
68
- selfClosing: false,
69
- }
70
- }
71
- }
72
-
73
- // Check for closing tag: </ComponentName>
74
- const closeTagMatch = trimmed.match(/^<\/([A-Za-z][A-Za-z0-9-]*)>\s*$/)
75
- if (closeTagMatch) {
76
- const [, tagName] = closeTagMatch
77
- const component = customComponents[tagName]
78
- if (component) {
79
- return {
80
- type: "custom-component-close",
81
- tagName,
82
- component,
83
- }
84
- }
85
- }
86
-
87
- return null
88
- }
89
-
90
- /**
91
- * Parse attributes from a tag string
92
- * Supports: attr="value", attr='value', attr={variable}, attr
93
- * @param {string} attributesStr - The attributes string
94
- * @returns {Object} - Parsed attributes
95
- */
96
- function parseAttributes(attributesStr) {
97
- const attributes = {}
98
- if (!attributesStr || !attributesStr.trim()) {
99
- return attributes
100
- }
101
-
102
- const str = attributesStr.trim()
103
- let i = 0
104
-
105
- while (i < str.length) {
106
- // Skip whitespace
107
- while (i < str.length && /\s/.test(str[i])) {
108
- i++
109
- }
110
-
111
- if (i >= str.length) break
112
-
113
- // Parse attribute name
114
- let nameStart = i
115
- while (i < str.length && /[a-zA-Z0-9-_:]/.test(str[i])) {
116
- i++
117
- }
118
-
119
- const name = str.substring(nameStart, i)
120
- if (!name) break
121
-
122
- // Skip whitespace after name
123
- while (i < str.length && /\s/.test(str[i])) {
124
- i++
125
- }
126
-
127
- // Check for =
128
- if (i >= str.length || str[i] !== "=") {
129
- // Boolean attribute
130
- attributes[name] = true
131
- continue
132
- }
133
-
134
- i++ // Skip =
135
-
136
- // Skip whitespace after =
137
- while (i < str.length && /\s/.test(str[i])) {
138
- i++
139
- }
140
-
141
- if (i >= str.length) break
142
-
143
- // Parse value
144
- const quote = str[i]
145
-
146
- if (quote === '"' || quote === "'") {
147
- // Quoted string
148
- i++ // Skip opening quote
149
- let value = ""
150
- while (i < str.length && str[i] !== quote) {
151
- if (str[i] === "\\" && i + 1 < str.length) {
152
- // Escaped character - add the next character literally
153
- value += str[i + 1]
154
- i += 2
155
- } else {
156
- value += str[i]
157
- i++
158
- }
159
- }
160
- attributes[name] = value
161
- if (i < str.length) i++ // Skip closing quote
162
- } else if (str[i] === "{") {
163
- // Variable reference
164
- i++ // Skip {
165
- const valueStart = i
166
- while (i < str.length && str[i] !== "}") {
167
- i++
168
- }
169
- const variableName = str.substring(valueStart, i).trim()
170
- attributes[name] = { __variable: variableName }
171
- if (i < str.length) i++ // Skip }
172
- } else {
173
- // Unquoted value (until space or end)
174
- const valueStart = i
175
- while (i < str.length && !/\s/.test(str[i])) {
176
- i++
177
- }
178
- const value = str.substring(valueStart, i)
179
- attributes[name] = value
180
- }
181
- }
182
-
183
- return attributes
184
- }
185
-
186
- /**
187
- * Resolve attributes by replacing variable references with actual values
188
- * @param {Object} attributes - Attributes with possible variable references
189
- * @param {Object} data - Data object containing variable values
190
- * @returns {Object} - Resolved attributes
191
- */
192
- function resolveAttributes(attributes, data) {
193
- if (!attributes || typeof attributes !== "object") {
194
- return {}
195
- }
196
-
197
- const resolved = {}
198
- for (const [key, value] of Object.entries(attributes)) {
199
- if (value && typeof value === "object" && value.__variable) {
200
- // Resolve variable
201
- const variableName = value.__variable
202
- resolved[key] =
203
- data && data[variableName] !== undefined
204
- ? data[variableName]
205
- : undefined
206
- } else {
207
- resolved[key] = value
208
- }
209
- }
210
-
211
- return resolved
212
- }
213
-
214
- module.exports = {
215
- parseCustomTag,
216
- parseAttributes,
217
- resolveAttributes,
218
- }
@@ -1,115 +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 simple variable names (backwards compatibility)
9
- if (!/[.\[]/.test(path)) {
10
- return data[path]
11
- }
12
-
13
- // Split the path into parts, handling both dot notation and bracket notation
14
- // e.g., "images[0].src" -> ["images", "0", "src"]
15
- const parts = path
16
- .replace(/\[(\d+)\]/g, ".$1") // Convert [0] to .0
17
- .split(".")
18
- .filter(Boolean)
19
-
20
- let current = data
21
- for (const part of parts) {
22
- if (current === null || current === undefined) {
23
- return undefined
24
- }
25
- current = current[part]
26
- }
27
-
28
- return current
29
- }
30
-
31
- /**
32
- * Replace {variableName} placeholders in text with actual values from data
33
- * Supports:
34
- * - Simple variables: {name}
35
- * - Array indexing: {images[0]}
36
- * - Property access: {user.name}
37
- * - Combined: {images[0].src}
38
- * @param {string} text - Text containing variable placeholders
39
- * @param {Object} data - Data object with variable values
40
- * @returns {string|Array} - Text with variables replaced, or array if mixed content
41
- */
42
- function replaceVariables(text, data) {
43
- if (!text || typeof text !== "string") {
44
- return text
45
- }
46
-
47
- if (!data || typeof data !== "object") {
48
- return text
49
- }
50
-
51
- // Check if text contains any variables
52
- if (!text.includes("{") || !text.includes("}")) {
53
- return text
54
- }
55
-
56
- const result = []
57
- let i = 0
58
- let lastIndex = 0
59
-
60
- while (i < text.length) {
61
- if (text[i] === "\\" && text[i + 1] === "{") {
62
- // Escaped opening brace
63
- result.push(text.substring(lastIndex, i))
64
- result.push("{")
65
- i += 2
66
- lastIndex = i
67
- continue
68
- }
69
-
70
- if (text[i] === "{") {
71
- const closeIndex = text.indexOf("}", i + 1)
72
-
73
- if (closeIndex !== -1) {
74
- // Found a variable placeholder
75
- const variablePath = text.substring(i + 1, closeIndex).trim()
76
-
77
- if (variablePath) {
78
- // Add text before the variable
79
- if (i > lastIndex) {
80
- result.push(text.substring(lastIndex, i))
81
- }
82
-
83
- // Resolve the variable value (supports paths like "images[0].src")
84
- const value = resolvePath(data, variablePath)
85
- if (value !== undefined && value !== null) {
86
- result.push(String(value))
87
- } else {
88
- // Variable not found, keep the placeholder
89
- result.push(text.substring(i, closeIndex + 1))
90
- }
91
-
92
- i = closeIndex + 1
93
- lastIndex = i
94
- continue
95
- }
96
- }
97
- }
98
-
99
- i++
100
- }
101
-
102
- // Add remaining text
103
- if (lastIndex < text.length) {
104
- result.push(text.substring(lastIndex))
105
- }
106
-
107
- // If no substitutions were made, return original text
108
- if (result.length === 0) {
109
- return text
110
- }
111
-
112
- return result.join("")
113
- }
114
-
115
- module.exports = { replaceVariables }
@@ -1,21 +0,0 @@
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 }
@@ -1,193 +0,0 @@
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
- }