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.
- package/package.json +2 -3
- package/ui/grid/index.js +2 -5
- package/ui/index.js +2 -0
- package/ui/markdown/README.md +48 -0
- package/ui/markdown/index.js +43 -315
- package/ui/markdown/utilities/convertNodes.js +193 -0
- package/ui/markdown/utilities/format.js +15 -12
- package/ui/markdown/utilities/params.js +29 -0
- package/ui/markdown/utilities/parseBlock.js +284 -0
- package/ui/markdown/utilities/parseCustomTag.js +20 -0
- package/ui/markdown/utilities/replaceVariables.js +39 -4
- package/ui/normalize.js +1 -1
- package/ui/prose/README.md +492 -0
- package/ui/prose/index.js +159 -0
- package/ui/prose/utilities/brackets.js +21 -0
- package/ui/prose/utilities/convertNodes.js +193 -0
- package/ui/prose/utilities/format.js +184 -0
- package/ui/prose/utilities/params.js +29 -0
- package/ui/prose/utilities/parseBlock.js +284 -0
- package/ui/prose/utilities/parseCustomTag.js +218 -0
- package/ui/prose/utilities/processConditionals.js +264 -0
- package/ui/prose/utilities/processLoops.js +171 -0
- package/ui/prose/utilities/replaceVariables.js +115 -0
- package/examples/typescript-example.ts +0 -49
|
@@ -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
|
+

|
|
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
|
+

|
|
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 }
|