boxwood 2.13.0 → 2.15.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.
@@ -0,0 +1,492 @@
1
+ # Prose Component
2
+
3
+ The `Prose` component is an enhanced content component for blog systems, documentation, and other content-heavy use cases. It extends basic markdown with templating features.
4
+
5
+ ## Features
6
+
7
+ ### Markdown Support
8
+
9
+ All standard markdown features:
10
+
11
+ - Headings (`#`, `##`, etc.)
12
+ - Lists (ordered and unordered)
13
+ - Links and images
14
+ - Code blocks
15
+ - Bold, italic, and inline code
16
+ - Blockquotes
17
+ - Horizontal rules
18
+
19
+ ### Templating Features
20
+
21
+ - **Variable replacement**: `{variable}`, `{user.name}`, `{images[0].src}`
22
+ - **Conditionals**: `{#if variable}...{/if}` blocks for conditional rendering
23
+ - **Loops**: `{#each items}...{/each}` for iteration over arrays
24
+ - **Custom components**: `<Alert type="warning">...</Alert>`
25
+ - **Builtin HTML tags**: `<article>`, `<section>`, `<div>`, etc.
26
+
27
+ ### Planned Features
28
+
29
+ None at the moment - all core features are implemented!
30
+
31
+ ## Usage
32
+
33
+ ```javascript
34
+ const { Prose } = require("boxwood/ui")
35
+
36
+ // Basic usage with variables
37
+ Prose(
38
+ { data: { title: "Hello", author: "John" } },
39
+ `
40
+ # {title}
41
+
42
+ Written by {author}
43
+ `,
44
+ )
45
+
46
+ // With conditionals
47
+ Prose(
48
+ { data: { image: "hero.jpg", showAuthor: true, author: "Jane" } },
49
+ `
50
+ {#if image}
51
+ ![Featured Image]({image})
52
+ {/if}
53
+
54
+ # My Blog Post
55
+
56
+ {#if showAuthor}
57
+ Written by {author}
58
+ {/if}
59
+ `,
60
+ )
61
+
62
+ // With custom components
63
+ Prose(
64
+ {
65
+ data: { message: "Important!" },
66
+ components: { Alert },
67
+ },
68
+ `
69
+ <Alert type="warning">{message}</Alert>
70
+ `,
71
+ )
72
+
73
+ // Blog post example
74
+ Prose(
75
+ {
76
+ data: {
77
+ title: "Getting Started",
78
+ author: "Alice",
79
+ bio: "Software Engineer",
80
+ image: "cover.jpg",
81
+ tags: ["tutorial", "beginner"],
82
+ },
83
+ },
84
+ `
85
+ # {title}
86
+
87
+ {#if image}
88
+ ![Cover]({image})
89
+ {/if}
90
+
91
+ {#if bio}
92
+ **About the author:** {bio}
93
+ {/if}
94
+
95
+ {#if tags.length > 0}
96
+ **Tags:** {tags[0]}, {tags[1]}
97
+ {/if}
98
+ `,
99
+ )
100
+ ```
101
+
102
+ ## Conditional Rendering
103
+
104
+ Use `{#if}` blocks to conditionally render content based on data:
105
+
106
+ ```markdown
107
+ {#if premium}
108
+ Access premium content here
109
+ {/if}
110
+
111
+ {#if items[0]}
112
+ First item: {items[0].name}
113
+ {/if}
114
+ ```
115
+
116
+ ### Negation
117
+
118
+ You can negate conditions using the `!` operator:
119
+
120
+ ```markdown
121
+ {#if !hidden}
122
+ This content is visible
123
+ {/if}
124
+
125
+ {#if !user.verified}
126
+ Please verify your email
127
+ {/if}
128
+
129
+ {#if !premium}
130
+ Upgrade to access premium features
131
+ {/if}
132
+ ```
133
+
134
+ The `!` operator inverts the condition, making it true when the value is falsy.
135
+
136
+ ### Comparison Operators
137
+
138
+ You can use comparison operators in conditions:
139
+
140
+ ```markdown
141
+ {#if tags.length > 0}
142
+ **Tags:** {tags[0]}, {tags[1]}
143
+ {/if}
144
+
145
+ {#if age >= 18}
146
+ You can vote
147
+ {/if}
148
+
149
+ {#if count < 10}
150
+ Limited quantity remaining
151
+ {/if}
152
+
153
+ {#if status == 'published'}
154
+ This post is live
155
+ {/if}
156
+
157
+ {#if temperature != 0}
158
+ Temperature: {temperature}°C
159
+ {/if}
160
+ ```
161
+
162
+ Supported operators:
163
+
164
+ - `>` - Greater than
165
+ - `<` - Less than
166
+ - `>=` - Greater than or equal
167
+ - `<=` - Less than or equal
168
+ - `==` - Equals
169
+ - `!=` - Not equals
170
+
171
+ You can compare with:
172
+
173
+ - **Numbers**: `{#if count > 5}`
174
+ - **Strings**: `{#if status == 'active'}` or `{#if status == "active"}`
175
+ - **Booleans**: `{#if verified == true}`
176
+ - **Variables**: `{#if current > previous}`
177
+
178
+ ### Simple Conditions
179
+
180
+ Conditions support:
181
+
182
+ - Simple variables: `{#if show}`
183
+ - Nested properties: `{#if user.verified}`
184
+ - Array indexing: `{#if images[0]}`
185
+
186
+ Falsy values (removes content):
187
+
188
+ - `null`, `undefined`
189
+ - `false`
190
+ - `0`
191
+ - `""` (empty string)
192
+ - `[]` (empty array)
193
+
194
+ ### Else Blocks
195
+
196
+ Use `{#else}` to provide alternative content when a condition is false:
197
+
198
+ ```markdown
199
+ {#if isPremium}
200
+ **Premium Content**
201
+
202
+ Access exclusive features
203
+ {#else}
204
+ **Standard Content**
205
+
206
+ Upgrade for more features
207
+ {/if}
208
+ ```
209
+
210
+ ```markdown
211
+ {#if age >= 18}
212
+ You can vote
213
+ {#else}
214
+ You cannot vote yet
215
+ {/if}
216
+ ```
217
+
218
+ ```markdown
219
+ {#if items.length > 0}
220
+ You have {items.length} items
221
+ {#else}
222
+ Your cart is empty
223
+ {/if}
224
+ ```
225
+
226
+ Else blocks work with all conditional features:
227
+
228
+ - Simple conditions: `{#if show}...{#else}...{/if}`
229
+ - Comparison operators: `{#if count > 5}...{#else}...{/if}`
230
+ - Negation: `{#if !hidden}...{#else}...{/if}`
231
+ - Nested conditions: `{#if outer}{#if inner}...{#else}...{/if}{#else}...{/if}`
232
+ - Inside loops: Conditionally render content for each item
233
+
234
+ ### Else-If Chains
235
+
236
+ Use `{#elseif}` to test multiple conditions in sequence:
237
+
238
+ ```markdown
239
+ {#if score >= 90}
240
+ Grade: A - Excellent!
241
+ {#elseif score >= 80}
242
+ Grade: B - Good job!
243
+ {#elseif score >= 70}
244
+ Grade: C - Passing
245
+ {#elseif score >= 60}
246
+ Grade: D - Needs improvement
247
+ {#else}
248
+ Grade: F - Failed
249
+ {/if}
250
+ ```
251
+
252
+ ```markdown
253
+ {#if role == 'admin'}
254
+
255
+ # Admin Dashboard
256
+
257
+ Welcome, administrator!
258
+ {#elseif role == 'moderator'}
259
+
260
+ # Moderator Panel
261
+
262
+ Welcome, moderator!
263
+ {#elseif role == 'user'}
264
+
265
+ # User Profile
266
+
267
+ Welcome, user!
268
+ {#else}
269
+
270
+ # Guest Access
271
+
272
+ Please log in.
273
+ {/if}
274
+ ```
275
+
276
+ Else-if features:
277
+
278
+ - **Multiple branches**: Chain as many `{#elseif}` blocks as needed
279
+ - **Short-circuit evaluation**: First matching condition wins, remaining branches are skipped
280
+ - **Optional else**: The final `{#else}` block is optional
281
+ - **All comparison operators**: Use `==`, `!=`, `>`, `<`, `>=`, `<=` in elseif conditions
282
+ - **Negation support**: Use `!` to negate elseif conditions
283
+ - **Nested conditionals**: Place if/else blocks inside elseif branches
284
+
285
+ Example with real-world blog scenario:
286
+
287
+ ```javascript
288
+ Prose(
289
+ {
290
+ data: {
291
+ status: "draft",
292
+ publishedAt: null,
293
+ scheduledAt: "2026-07-10",
294
+ },
295
+ },
296
+ `
297
+ # My Blog Post
298
+
299
+ {#if publishedAt}
300
+ *Published on {publishedAt}*
301
+ {#elseif scheduledAt}
302
+ *Scheduled for {scheduledAt}*
303
+ {#else}
304
+ *Draft - not yet published*
305
+ {/if}
306
+
307
+ ## Content
308
+
309
+ ...
310
+ `,
311
+ )
312
+ ```
313
+
314
+ ## Loops
315
+
316
+ Use `{#each}` blocks to iterate over arrays and render content for each item:
317
+
318
+ ### Basic Loop Syntax
319
+
320
+ ```markdown
321
+ {#each items}
322
+
323
+ - {item}
324
+ {/each}
325
+ ```
326
+
327
+ By default, the current item is available as `{item}`. For arrays of primitives (strings, numbers, etc.), this is the value itself.
328
+
329
+ ### Custom Variable Names
330
+
331
+ You can specify a custom variable name for better clarity:
332
+
333
+ ```markdown
334
+ {#each users as user}
335
+
336
+ - {user.name} ({user.email})
337
+ {/each}
338
+ ```
339
+
340
+ ### Loop with Index
341
+
342
+ Access the current index by providing a second variable name:
343
+
344
+ ```markdown
345
+ {#each items as item, i}
346
+ {i}. {item}
347
+ {/each}
348
+ ```
349
+
350
+ The index is zero-based (starts at 0).
351
+
352
+ ### Nested Properties
353
+
354
+ Loop items can be objects with nested properties:
355
+
356
+ ```markdown
357
+ {#each products as product}
358
+ **{product.name}**
359
+
360
+ Price: ${product.price}
361
+ {/each}
362
+ ```
363
+
364
+ ### Accessing External Variables
365
+
366
+ Variables from outside the loop are still accessible:
367
+
368
+ ```markdown
369
+ {#each items as item}
370
+
371
+ - {prefix}{item}{suffix}
372
+ {/each}
373
+ ```
374
+
375
+ ### Nested Loops
376
+
377
+ Loops can be nested to create complex structures:
378
+
379
+ ```markdown
380
+ {#each categories as category}
381
+
382
+ ## {category.name}
383
+
384
+ {#each category.items as item}
385
+
386
+ - {item}
387
+ {/each}
388
+ {/each}
389
+ ```
390
+
391
+ ### Combining Loops with Conditionals
392
+
393
+ You can use conditionals inside loops to filter or conditionally render content:
394
+
395
+ ```markdown
396
+ {#each users as user}
397
+ **{user.name}**
398
+
399
+ {#if user.verified}
400
+ ✓ Verified user
401
+ {/if}
402
+
403
+ {#if user.role == 'admin'}
404
+ 👑 Administrator
405
+ {/if}
406
+ {/each}
407
+ ```
408
+
409
+ ### Loop Examples
410
+
411
+ **Simple list:**
412
+
413
+ ```javascript
414
+ Prose(
415
+ { data: { fruits: ["Apple", "Banana", "Cherry"] } },
416
+ `
417
+ {#each fruits as fruit}
418
+ - {fruit}
419
+ {/each}
420
+ `,
421
+ )
422
+ ```
423
+
424
+ **User cards with conditionals:**
425
+
426
+ ```javascript
427
+ Prose(
428
+ {
429
+ data: {
430
+ users: [
431
+ { name: "Alice", verified: true, role: "admin" },
432
+ { name: "Bob", verified: false, role: "user" },
433
+ { name: "Charlie", verified: true, role: "user" },
434
+ ],
435
+ },
436
+ },
437
+ `
438
+ {#each users as user}
439
+ ### {user.name}
440
+
441
+ {#if user.verified}
442
+ ✓ Verified
443
+ {/if}
444
+
445
+ {#if user.role == 'admin'}
446
+ **Role:** Administrator
447
+ {/if}
448
+
449
+ ---
450
+
451
+ {/each}
452
+ `,
453
+ )
454
+ ```
455
+
456
+ **Numbered list with index:**
457
+
458
+ ```javascript
459
+ Prose(
460
+ { data: { steps: ["Create account", "Verify email", "Complete profile"] } },
461
+ `
462
+ {#each steps as step, i}
463
+ **Step {i + 1}:** {step}
464
+ {/each}
465
+ `,
466
+ )
467
+ ```
468
+
469
+ Note: Since template literals evaluate expressions like `{i + 1}`, you may need to use alternative approaches if you want to perform calculations. In this case, pre-process your data.
470
+
471
+ **Empty arrays:**
472
+
473
+ When an array is empty, the loop content is not rendered:
474
+
475
+ ```javascript
476
+ Prose(
477
+ { data: { items: [] } },
478
+ `
479
+ {#each items as item}
480
+ - {item}
481
+ {/each}
482
+ `,
483
+ )
484
+ // Renders nothing
485
+ ```
486
+
487
+ ## Difference from Markdown Component
488
+
489
+ - **Markdown**: Pure markdown rendering, no variables or custom components
490
+ - **Prose**: Markdown + templating (variables, custom components, conditionals, loops)
491
+
492
+ Use `Markdown` for simple, static content. Use `Prose` for dynamic content in blogs, CMS, or documentation systems.
@@ -0,0 +1,159 @@
1
+ const nodes = require("../..")
2
+ const { component } = nodes
3
+
4
+ const Center = require("../center")
5
+ const Container = require("../container")
6
+ const Grid = require("../grid")
7
+ const Group = require("../group")
8
+ const Stack = require("../stack")
9
+
10
+ const { extractHtmlParams, mergeComponents } = require("./utilities/params")
11
+ const { parseMarkdownLines } = require("./utilities/parseBlock")
12
+ const { convertItemsToNodes } = require("./utilities/convertNodes")
13
+ const { processConditionals } = require("./utilities/processConditionals")
14
+ const { processLoops } = require("./utilities/processLoops")
15
+
16
+ // Safe builtin HTML tags that can be used as custom components in markdown
17
+ // These are always available and don't need to be explicitly passed in params.components
18
+ const SAFE_TAG_NAMES = [
19
+ // Containers
20
+ "div",
21
+ "span",
22
+ // Semantic structure
23
+ "article",
24
+ "section",
25
+ "header",
26
+ "footer",
27
+ "main",
28
+ "aside",
29
+ "nav",
30
+ // Headings
31
+ "h1",
32
+ "h2",
33
+ "h3",
34
+ "h4",
35
+ "h5",
36
+ "h6",
37
+ // Text formatting
38
+ "p",
39
+ "strong",
40
+ "em",
41
+ "b",
42
+ "i",
43
+ "u",
44
+ "s",
45
+ "small",
46
+ "mark",
47
+ "sub",
48
+ "sup",
49
+ "br",
50
+ "wbr",
51
+ "abbr",
52
+ "cite",
53
+ "q",
54
+ "kbd",
55
+ "samp",
56
+ "var",
57
+ "del",
58
+ "ins",
59
+ "dfn",
60
+ // Lists
61
+ "ul",
62
+ "ol",
63
+ "li",
64
+ "dl",
65
+ "dt",
66
+ "dd",
67
+ // Links & media
68
+ "a",
69
+ "img",
70
+ "picture",
71
+ "source",
72
+ // Code
73
+ "code",
74
+ "pre",
75
+ // Tables
76
+ "table",
77
+ "thead",
78
+ "tbody",
79
+ "tfoot",
80
+ "tr",
81
+ "th",
82
+ "td",
83
+ "caption",
84
+ "colgroup",
85
+ "col",
86
+ // Block elements
87
+ "blockquote",
88
+ "hr",
89
+ // Figures
90
+ "figure",
91
+ "figcaption",
92
+ // Interactive
93
+ "details",
94
+ "summary",
95
+ // Other semantic
96
+ "address",
97
+ "time",
98
+ "data",
99
+ ]
100
+
101
+ // Build BUILTIN_HTML_TAGS dynamically from tag names
102
+ const BUILTIN_HTML_TAGS = SAFE_TAG_NAMES.reduce((acc, tagName) => {
103
+ const componentName = tagName.charAt(0).toUpperCase() + tagName.slice(1)
104
+ if (nodes[componentName]) {
105
+ acc[tagName] = nodes[componentName]
106
+ }
107
+ return acc
108
+ }, {})
109
+
110
+ // Add built-in UI components
111
+ const BUILTIN_UI_COMPONENTS = {
112
+ Center,
113
+ Container,
114
+ Grid,
115
+ Group,
116
+ Stack,
117
+ }
118
+
119
+ // Merge HTML tags and UI components
120
+ const BUILTIN_COMPONENTS = { ...BUILTIN_HTML_TAGS, ...BUILTIN_UI_COMPONENTS }
121
+
122
+ function Prose(params, children) {
123
+ // Handle array of children recursively
124
+ if (Array.isArray(children)) {
125
+ return children.map((child) => Prose(params, child))
126
+ }
127
+
128
+ // Only process string content
129
+ if (typeof children !== "string") {
130
+ return null
131
+ }
132
+
133
+ const customComponents = params && params.components
134
+ const data = params && params.data
135
+ const allComponents = mergeComponents(BUILTIN_COMPONENTS, customComponents)
136
+ const htmlParams = extractHtmlParams(params)
137
+
138
+ // Process {#each}...{/each} loop blocks first
139
+ // Conditionals within loops are processed during loop expansion
140
+ let processedChildren = processLoops(children, data)
141
+
142
+ // Process any remaining {#if}...{/if} conditional blocks (those outside loops)
143
+ processedChildren = processConditionals(processedChildren, data)
144
+
145
+ // Parse all markdown lines into structured items
146
+ const items = parseMarkdownLines(processedChildren, allComponents, data)
147
+
148
+ // Convert parsed items into final node tree
149
+ return convertItemsToNodes(
150
+ items,
151
+ params,
152
+ htmlParams,
153
+ data,
154
+ allComponents,
155
+ Prose,
156
+ )
157
+ }
158
+
159
+ module.exports = component(Prose)
@@ -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 }