boxwood 2.10.0 → 2.11.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 +1 -1
- package/ui/markdown/index.js +105 -87
- package/ui/markdown/utilities/brackets.js +21 -0
- package/ui/markdown/utilities/format.js +181 -0
package/package.json
CHANGED
package/ui/markdown/index.js
CHANGED
|
@@ -14,10 +14,17 @@ const {
|
|
|
14
14
|
Strong,
|
|
15
15
|
Em,
|
|
16
16
|
Code,
|
|
17
|
+
A,
|
|
18
|
+
Hr,
|
|
19
|
+
Img,
|
|
20
|
+
Pre,
|
|
17
21
|
} = require("../..")
|
|
18
22
|
|
|
23
|
+
const { format } = require("./utilities/format")
|
|
24
|
+
|
|
19
25
|
const ORDERED_LIST_REGEXP = /^\d+\.\s/
|
|
20
26
|
const UNORDERED_MARKERS = ["- ", "— ", "– ", "• "]
|
|
27
|
+
const HORIZONTAL_RULE_REGEXP = /^(?:\*\s*\*\s*\*+|-\s*-\s*-+|_\s*_\s*_+)\s*$/
|
|
21
28
|
const HEADINGS = [
|
|
22
29
|
{ prefix: "###### ", type: "h6" },
|
|
23
30
|
{ prefix: "##### ", type: "h5" },
|
|
@@ -34,62 +41,10 @@ const COMPONENTS = {
|
|
|
34
41
|
h5: H5,
|
|
35
42
|
h6: H6,
|
|
36
43
|
blockquote: Blockquote,
|
|
44
|
+
hr: Hr,
|
|
37
45
|
}
|
|
38
46
|
|
|
39
|
-
|
|
40
|
-
if (!text.includes("*") && !text.includes("`")) {
|
|
41
|
-
return text
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const result = []
|
|
45
|
-
let i = 0
|
|
46
|
-
while (i < text.length) {
|
|
47
|
-
if (text[i] === "`") {
|
|
48
|
-
const start = i + 1
|
|
49
|
-
const end = text.indexOf("`", start)
|
|
50
|
-
if (end === -1) {
|
|
51
|
-
result.push(text[i])
|
|
52
|
-
i++
|
|
53
|
-
} else {
|
|
54
|
-
result.push(Code({}, text.substring(start, end)))
|
|
55
|
-
i = end + 1
|
|
56
|
-
}
|
|
57
|
-
} else if (text[i] === "*" && text[i + 1] === "*") {
|
|
58
|
-
const start = i + 2
|
|
59
|
-
const end = text.indexOf("**", start)
|
|
60
|
-
if (end === -1) {
|
|
61
|
-
result.push(text[i])
|
|
62
|
-
i++
|
|
63
|
-
} else {
|
|
64
|
-
result.push(Strong(text.substring(start, end)))
|
|
65
|
-
i = end + 2
|
|
66
|
-
}
|
|
67
|
-
} else if (text[i] === "*") {
|
|
68
|
-
const start = i + 1
|
|
69
|
-
const end = text.indexOf("*", start)
|
|
70
|
-
if (end === -1) {
|
|
71
|
-
result.push(text[i])
|
|
72
|
-
i++
|
|
73
|
-
} else {
|
|
74
|
-
result.push(Em(text.substring(start, end)))
|
|
75
|
-
i = end + 1
|
|
76
|
-
}
|
|
77
|
-
} else {
|
|
78
|
-
let next = text.length
|
|
79
|
-
const nextCode = text.indexOf("`", i)
|
|
80
|
-
const nextStar = text.indexOf("*", i)
|
|
81
|
-
if (nextCode !== -1 && nextCode < next) next = nextCode
|
|
82
|
-
if (nextStar !== -1 && nextStar < next) next = nextStar
|
|
83
|
-
if (next === text.length) {
|
|
84
|
-
result.push(text.substring(i))
|
|
85
|
-
break
|
|
86
|
-
}
|
|
87
|
-
result.push(text.substring(i, next))
|
|
88
|
-
i = next
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return result.length > 0 ? result : text
|
|
92
|
-
}
|
|
47
|
+
const INLINE_COMPONENTS = { A, Img, Code, Strong, Em }
|
|
93
48
|
|
|
94
49
|
function Markdown(params, children) {
|
|
95
50
|
if (Array.isArray(children)) {
|
|
@@ -100,47 +55,95 @@ function Markdown(params, children) {
|
|
|
100
55
|
return null
|
|
101
56
|
}
|
|
102
57
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
.map((line) => {
|
|
108
|
-
// Preserve leading spaces for indentation tracking
|
|
109
|
-
const trimmed = line.trim()
|
|
110
|
-
const leadingSpaces = line.length - line.trimStart().length
|
|
111
|
-
return { text: trimmed, indent: leadingSpaces }
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
const items = lines
|
|
115
|
-
.map(({ text, indent }) => {
|
|
116
|
-
if (UNORDERED_MARKERS.some((marker) => text.startsWith(marker))) {
|
|
117
|
-
const content = text.substring(2)
|
|
118
|
-
if (!content) return null
|
|
119
|
-
return { type: "li", list: "ul", content, indent }
|
|
120
|
-
}
|
|
58
|
+
// First pass: detect code blocks before processing lines
|
|
59
|
+
const allLines = children.trim().split("\n")
|
|
60
|
+
const items = []
|
|
61
|
+
let i = 0
|
|
121
62
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
63
|
+
while (i < allLines.length) {
|
|
64
|
+
const line = allLines[i]
|
|
65
|
+
const trimmed = line.trim()
|
|
66
|
+
|
|
67
|
+
// Check for code block start
|
|
68
|
+
if (trimmed.startsWith("```")) {
|
|
69
|
+
const language = trimmed.substring(3).trim()
|
|
70
|
+
const codeLines = []
|
|
71
|
+
i++ // Move past the opening ```
|
|
72
|
+
|
|
73
|
+
// Collect code block lines until closing ```
|
|
74
|
+
while (i < allLines.length) {
|
|
75
|
+
const codeLine = allLines[i]
|
|
76
|
+
if (codeLine.trim().startsWith("```")) {
|
|
77
|
+
// Found closing ```, don't include it
|
|
78
|
+
i++
|
|
79
|
+
break
|
|
80
|
+
}
|
|
81
|
+
codeLines.push(codeLine)
|
|
82
|
+
i++
|
|
126
83
|
}
|
|
127
84
|
|
|
85
|
+
items.push({
|
|
86
|
+
type: "pre",
|
|
87
|
+
content: codeLines.join("\n"),
|
|
88
|
+
language: language || undefined,
|
|
89
|
+
indent: 0,
|
|
90
|
+
})
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Skip empty lines
|
|
95
|
+
if (trimmed.length === 0) {
|
|
96
|
+
i++
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const leadingSpaces = line.length - line.trimStart().length
|
|
101
|
+
|
|
102
|
+
// Check for other block types
|
|
103
|
+
if (HORIZONTAL_RULE_REGEXP.test(trimmed)) {
|
|
104
|
+
items.push({ type: "hr", indent: leadingSpaces })
|
|
105
|
+
} else if (UNORDERED_MARKERS.some((marker) => trimmed.startsWith(marker))) {
|
|
106
|
+
const content = trimmed.substring(2)
|
|
107
|
+
if (content) {
|
|
108
|
+
items.push({ type: "li", list: "ul", content, indent: leadingSpaces })
|
|
109
|
+
}
|
|
110
|
+
} else if (ORDERED_LIST_REGEXP.test(trimmed)) {
|
|
111
|
+
const content = trimmed.replace(ORDERED_LIST_REGEXP, "")
|
|
112
|
+
if (content) {
|
|
113
|
+
items.push({ type: "li", list: "ol", content, indent: leadingSpaces })
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
let matched = false
|
|
128
117
|
for (const { prefix, type } of HEADINGS) {
|
|
129
|
-
if (
|
|
130
|
-
|
|
118
|
+
if (trimmed.startsWith(prefix)) {
|
|
119
|
+
items.push({
|
|
120
|
+
type,
|
|
121
|
+
content: trimmed.substring(prefix.length),
|
|
122
|
+
indent: leadingSpaces,
|
|
123
|
+
})
|
|
124
|
+
matched = true
|
|
125
|
+
break
|
|
131
126
|
}
|
|
132
127
|
}
|
|
133
128
|
|
|
134
|
-
if (
|
|
135
|
-
|
|
129
|
+
if (!matched) {
|
|
130
|
+
if (trimmed.startsWith("> ")) {
|
|
131
|
+
items.push({
|
|
132
|
+
type: "blockquote",
|
|
133
|
+
content: trimmed.substring(2),
|
|
134
|
+
indent: leadingSpaces,
|
|
135
|
+
})
|
|
136
|
+
} else {
|
|
137
|
+
items.push({ type: "p", content: trimmed, indent: leadingSpaces })
|
|
138
|
+
}
|
|
136
139
|
}
|
|
140
|
+
}
|
|
137
141
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
.filter(Boolean)
|
|
142
|
+
i++
|
|
143
|
+
}
|
|
141
144
|
|
|
142
145
|
const nodes = []
|
|
143
|
-
|
|
146
|
+
i = 0
|
|
144
147
|
|
|
145
148
|
function parseList(startIndex, parentIndent) {
|
|
146
149
|
const list = []
|
|
@@ -166,7 +169,7 @@ function Markdown(params, children) {
|
|
|
166
169
|
}
|
|
167
170
|
|
|
168
171
|
// Process current item at the correct indent level
|
|
169
|
-
const content = [format(item.content)]
|
|
172
|
+
const content = [format(item.content, INLINE_COMPONENTS)]
|
|
170
173
|
currentIndex++
|
|
171
174
|
|
|
172
175
|
// Check if next item is a nested list
|
|
@@ -201,11 +204,26 @@ function Markdown(params, children) {
|
|
|
201
204
|
i++
|
|
202
205
|
}
|
|
203
206
|
|
|
204
|
-
nodes.push(
|
|
207
|
+
nodes.push(
|
|
208
|
+
Blockquote(
|
|
209
|
+
params,
|
|
210
|
+
P(params, format(lines.join("\n"), INLINE_COMPONENTS)),
|
|
211
|
+
),
|
|
212
|
+
)
|
|
213
|
+
} else if (item.type === "hr") {
|
|
214
|
+
nodes.push(Hr(params))
|
|
215
|
+
i++
|
|
216
|
+
} else if (item.type === "pre") {
|
|
217
|
+
// Code blocks - wrap in <pre><code>
|
|
218
|
+
const codeParams = item.language
|
|
219
|
+
? { class: `language-${item.language}` }
|
|
220
|
+
: {}
|
|
221
|
+
nodes.push(Pre(params, Code(codeParams, item.content)))
|
|
222
|
+
i++
|
|
205
223
|
} else {
|
|
206
224
|
const { type, content } = item
|
|
207
225
|
const Component = COMPONENTS[type] || P
|
|
208
|
-
nodes.push(Component(params, format(content)))
|
|
226
|
+
nodes.push(Component(params, format(content, INLINE_COMPONENTS)))
|
|
209
227
|
i++
|
|
210
228
|
}
|
|
211
229
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
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 }
|
|
@@ -0,0 +1,181 @@
|
|
|
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} components - HTML component functions (A, Img, Code, Strong, Em)
|
|
8
|
+
* @returns {Array|string} - Formatted content as array of components and strings
|
|
9
|
+
*/
|
|
10
|
+
function format(text, components) {
|
|
11
|
+
const { A, Img, Code, Strong, Em } = components
|
|
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, components)))
|
|
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 (content.includes("@") && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(content)) {
|
|
118
|
+
result.push(A({ href: `mailto:${content}` }, content))
|
|
119
|
+
i = end + 1
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Not a valid autolink, treat as regular text
|
|
125
|
+
result.push(text[i])
|
|
126
|
+
i++
|
|
127
|
+
} else if (text[i] === "`") {
|
|
128
|
+
const end = text.indexOf("`", i + 1)
|
|
129
|
+
if (end === -1) {
|
|
130
|
+
result.push(text[i])
|
|
131
|
+
i++
|
|
132
|
+
} else {
|
|
133
|
+
result.push(Code({}, text.substring(i + 1, end)))
|
|
134
|
+
i = end + 1
|
|
135
|
+
}
|
|
136
|
+
} else if (text[i] === "*" && text[i + 1] === "*") {
|
|
137
|
+
const end = text.indexOf("**", i + 2)
|
|
138
|
+
if (end === -1) {
|
|
139
|
+
result.push(text[i])
|
|
140
|
+
i++
|
|
141
|
+
} else {
|
|
142
|
+
result.push(Strong(format(text.substring(i + 2, end), components)))
|
|
143
|
+
i = end + 2
|
|
144
|
+
}
|
|
145
|
+
} else if (text[i] === "*") {
|
|
146
|
+
const end = text.indexOf("*", i + 1)
|
|
147
|
+
if (end === -1) {
|
|
148
|
+
result.push(text[i])
|
|
149
|
+
i++
|
|
150
|
+
} else {
|
|
151
|
+
result.push(Em(format(text.substring(i + 1, end), components)))
|
|
152
|
+
i = end + 1
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
// Find next special character
|
|
156
|
+
const positions = [
|
|
157
|
+
text.indexOf("`", i),
|
|
158
|
+
text.indexOf("*", i),
|
|
159
|
+
text.indexOf("[", i),
|
|
160
|
+
text.indexOf("<", i),
|
|
161
|
+
text.indexOf("\\", i),
|
|
162
|
+
].filter((pos) => pos !== -1)
|
|
163
|
+
|
|
164
|
+
// Look for image pattern ![
|
|
165
|
+
const exclamPos = text.indexOf("!", i)
|
|
166
|
+
if (exclamPos !== -1 && text[exclamPos + 1] === "[") {
|
|
167
|
+
positions.push(exclamPos)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const next = positions.length > 0 ? Math.min(...positions) : text.length
|
|
171
|
+
|
|
172
|
+
result.push(text.substring(i, next))
|
|
173
|
+
if (next === text.length) break
|
|
174
|
+
i = next
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return result.length > 0 ? result : text
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = { format }
|