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.
@@ -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
- }
@@ -1,184 +0,0 @@
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 ![alt](url)
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 }
@@ -1,29 +0,0 @@
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
- }