@uniweb/semantic-parser 1.0.10 → 1.0.12
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/AGENTS.md +49 -10
- package/README.md +9 -6
- package/docs/entity-consolidation.md +470 -0
- package/package.json +1 -1
- package/src/mappers/extractors.js +40 -14
- package/src/processors/groups.js +32 -30
package/AGENTS.md
CHANGED
|
@@ -61,17 +61,43 @@ The parser returns a flat content structure:
|
|
|
61
61
|
title: '', // Main heading
|
|
62
62
|
pretitle: '', // Heading before main title
|
|
63
63
|
subtitle: '', // Heading after main title
|
|
64
|
+
subtitle2: '', // Third heading level
|
|
64
65
|
paragraphs: [],
|
|
65
|
-
links: [],
|
|
66
|
+
links: [], // All link-like entities (including buttons, documents)
|
|
66
67
|
imgs: [],
|
|
67
68
|
icons: [],
|
|
68
69
|
videos: [],
|
|
69
70
|
lists: [],
|
|
70
|
-
|
|
71
|
-
data: {}, //
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
71
|
+
quotes: [],
|
|
72
|
+
data: {}, // Structured data (tagged code blocks, forms, cards)
|
|
73
|
+
headings: [], // Overflow headings after title/subtitle/subtitle2
|
|
74
|
+
items: [], // Child content groups (same structure recursively)
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Link Roles
|
|
79
|
+
|
|
80
|
+
Links include buttons and documents, distinguished by `role`:
|
|
81
|
+
|
|
82
|
+
```js
|
|
83
|
+
links: [
|
|
84
|
+
{ href: "/page", label: "Learn More", role: "link" },
|
|
85
|
+
{ href: "/action", label: "Get Started", role: "button", variant: "primary" },
|
|
86
|
+
{ href: "/file.pdf", label: "Download", role: "document", download: true },
|
|
87
|
+
]
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Structured Data
|
|
91
|
+
|
|
92
|
+
The `data` object holds all structured content:
|
|
93
|
+
|
|
94
|
+
```js
|
|
95
|
+
data: {
|
|
96
|
+
"nav-links": [...], // From ```yaml:nav-links
|
|
97
|
+
"config": {...}, // From ```yaml:config
|
|
98
|
+
"form": {...}, // From FormBlock editor widget or ```yaml:form
|
|
99
|
+
"person": [...], // From card-group with cardType="person"
|
|
100
|
+
"event": [...] // From card-group with cardType="event"
|
|
75
101
|
}
|
|
76
102
|
```
|
|
77
103
|
|
|
@@ -88,18 +114,29 @@ The sequence processor identifies several special element types by inspecting pa
|
|
|
88
114
|
- **Links**: Paragraphs containing only a single link mark
|
|
89
115
|
- **Images**: Paragraphs with single image (role: 'image' or 'banner')
|
|
90
116
|
- **Icons**: Paragraphs with single image (role: 'icon')
|
|
91
|
-
- **Buttons**:
|
|
117
|
+
- **Buttons**: Editor `button` nodes → mapped to links with `role: "button"`
|
|
92
118
|
- **Videos**: Paragraphs with single image (role: 'video')
|
|
93
119
|
|
|
94
|
-
|
|
120
|
+
### Editor Node Mappings
|
|
121
|
+
|
|
122
|
+
Editor-specific nodes are mapped to standard entities:
|
|
123
|
+
- `button` node → `links[]` with `role: "button"` and `variant` attribute
|
|
124
|
+
- `FormBlock` → `data.form`
|
|
125
|
+
- `card-group` → `data[cardType]` arrays (e.g., `data.person`, `data.event`)
|
|
126
|
+
- `document-group` → `links[]` with `role: "document"` and `download: true`
|
|
127
|
+
|
|
128
|
+
See `docs/entity-consolidation.md` for complete mapping documentation.
|
|
95
129
|
|
|
96
130
|
### Tagged Code Blocks
|
|
97
131
|
|
|
98
132
|
Code blocks with tags route parsed data to the `data` object:
|
|
99
133
|
|
|
100
134
|
```markdown
|
|
101
|
-
```
|
|
102
|
-
|
|
135
|
+
```yaml:nav-links
|
|
136
|
+
- label: Home
|
|
137
|
+
href: /
|
|
138
|
+
- label: About
|
|
139
|
+
href: /about
|
|
103
140
|
```
|
|
104
141
|
|
|
105
142
|
```yaml:config
|
|
@@ -108,6 +145,8 @@ theme: dark
|
|
|
108
145
|
```
|
|
109
146
|
```
|
|
110
147
|
|
|
148
|
+
JSON is also supported (`json:tag-name`) if you prefer.
|
|
149
|
+
|
|
111
150
|
Results in:
|
|
112
151
|
```js
|
|
113
152
|
content.data['nav-links'] = [{ label: "Home", href: "/" }]
|
package/README.md
CHANGED
|
@@ -64,19 +64,22 @@ Main content fields are at the top level. The `items` array contains additional
|
|
|
64
64
|
|
|
65
65
|
```js
|
|
66
66
|
result = {
|
|
67
|
-
//
|
|
67
|
+
// Header fields (from headings)
|
|
68
68
|
pretitle: "", // Heading before main title
|
|
69
69
|
title: "Welcome", // Main heading
|
|
70
70
|
subtitle: "", // Heading after main title
|
|
71
|
+
subtitle2: "", // Third heading level
|
|
72
|
+
|
|
73
|
+
// Body fields
|
|
71
74
|
paragraphs: ["Get started today."],
|
|
75
|
+
links: [], // All links (including buttons, documents)
|
|
72
76
|
imgs: [],
|
|
73
77
|
videos: [],
|
|
74
|
-
links: [],
|
|
75
|
-
lists: [],
|
|
76
78
|
icons: [],
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
//
|
|
79
|
+
lists: [],
|
|
80
|
+
quotes: [],
|
|
81
|
+
data: {}, // Structured data (tagged code blocks, forms, cards)
|
|
82
|
+
headings: [], // Overflow headings after title/subtitle/subtitle2
|
|
80
83
|
|
|
81
84
|
// Additional content groups (from headings after content)
|
|
82
85
|
items: [
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
# Semantic Parser Entity Consolidation
|
|
2
|
+
|
|
3
|
+
This document defines the standard semantic entities output by the parser and how editor-specific node types map to them.
|
|
4
|
+
|
|
5
|
+
## Design Principle
|
|
6
|
+
|
|
7
|
+
**Editor nodes are authoring conveniences → Parser outputs standardized semantic entities**
|
|
8
|
+
|
|
9
|
+
The semantic parser accepts ProseMirror/TipTap documents from two sources:
|
|
10
|
+
1. **File-based markdown** via `@uniweb/content-reader`
|
|
11
|
+
2. **Visual editor** via TipTap with custom node types
|
|
12
|
+
|
|
13
|
+
Both sources must produce the same standardized output. Editor-specific node types (like `card-group`, `FormBlock`, `button` node) are conveniences that map to standard entities.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Standard Entity Set
|
|
18
|
+
|
|
19
|
+
After consolidation, the parser outputs this flat structure:
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
{
|
|
23
|
+
// Header fields (from headings)
|
|
24
|
+
title: '',
|
|
25
|
+
pretitle: '',
|
|
26
|
+
subtitle: '',
|
|
27
|
+
subtitle2: '',
|
|
28
|
+
|
|
29
|
+
// Body fields
|
|
30
|
+
paragraphs: [], // Text blocks with inline HTML formatting
|
|
31
|
+
links: [], // All link-like entities (buttons, documents, nav links)
|
|
32
|
+
imgs: [], // All images (with role distinguishing purpose)
|
|
33
|
+
videos: [], // Video embeds
|
|
34
|
+
icons: [], // Standalone icons
|
|
35
|
+
lists: [], // Bullet/ordered lists (recursive structure)
|
|
36
|
+
quotes: [], // Blockquotes (recursive structure)
|
|
37
|
+
data: {}, // Structured data (tagged code blocks, forms, cards)
|
|
38
|
+
headings: [], // Overflow headings after title/subtitle/subtitle2
|
|
39
|
+
|
|
40
|
+
items: [], // Semantic groups (same structure recursively)
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Removed Fields
|
|
45
|
+
|
|
46
|
+
| Field | Status | Reason |
|
|
47
|
+
|-------|--------|--------|
|
|
48
|
+
| `alignment` | **Deprecated** | Editor-only concept, not expressible in markdown |
|
|
49
|
+
| `buttons` | **Merged into `links`** | Buttons are styled links |
|
|
50
|
+
| `cards` | **Merged into `data`** | Structured data with schema tag |
|
|
51
|
+
| `documents` | **Merged into `links`** | Documents are downloadable links |
|
|
52
|
+
| `forms` | **Merged into `data`** | Structured data with `form` tag |
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Entity Specifications
|
|
57
|
+
|
|
58
|
+
### Links
|
|
59
|
+
|
|
60
|
+
All link-like content merges into the `links` array. The `role` attribute distinguishes behavior.
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
{
|
|
64
|
+
href: "/contact",
|
|
65
|
+
label: "Contact Us",
|
|
66
|
+
|
|
67
|
+
// Role distinguishes link type
|
|
68
|
+
role: "link", // Default: standard hyperlink
|
|
69
|
+
| "button" // Call-to-action button
|
|
70
|
+
| "button-primary" // Primary CTA
|
|
71
|
+
| "button-outline" // Outline style button
|
|
72
|
+
| "nav-link" // Navigation link
|
|
73
|
+
| "footer-link" // Footer navigation
|
|
74
|
+
| "document" // Downloadable file
|
|
75
|
+
|
|
76
|
+
// Button-specific attributes (when role is button-*)
|
|
77
|
+
variant: "primary" | "secondary" | "outline" | "ghost",
|
|
78
|
+
size: "sm" | "md" | "lg",
|
|
79
|
+
icon: "icon-name",
|
|
80
|
+
|
|
81
|
+
// Link behavior
|
|
82
|
+
target: "_blank" | "_self",
|
|
83
|
+
rel: "noopener noreferrer",
|
|
84
|
+
download: true | "filename.pdf",
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Markdown syntax:**
|
|
89
|
+
```markdown
|
|
90
|
+
[Standard link](/page)
|
|
91
|
+
[Button link](button:/action){variant=primary}
|
|
92
|
+
[Download](report.pdf){download}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Images
|
|
96
|
+
|
|
97
|
+
All image content uses the `imgs` array. The `role` attribute distinguishes purpose.
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
{
|
|
101
|
+
url: "/images/hero.jpg",
|
|
102
|
+
alt: "Hero image",
|
|
103
|
+
caption: "Optional caption",
|
|
104
|
+
|
|
105
|
+
// Role distinguishes image purpose
|
|
106
|
+
role: "image", // Default: content image
|
|
107
|
+
| "icon" // Small icon/logo
|
|
108
|
+
| "background" // Section background
|
|
109
|
+
| "gallery" // Gallery item
|
|
110
|
+
| "banner" // Hero/banner image
|
|
111
|
+
|
|
112
|
+
// Layout attributes
|
|
113
|
+
direction: "left" | "right" | "center",
|
|
114
|
+
size: "basic" | "lg" | "full",
|
|
115
|
+
|
|
116
|
+
// Styling
|
|
117
|
+
filter: "grayscale" | "blur",
|
|
118
|
+
theme: "light" | "dark",
|
|
119
|
+
|
|
120
|
+
// Link wrapper (clickable image)
|
|
121
|
+
href: "/link-target",
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Data (Structured Content)
|
|
126
|
+
|
|
127
|
+
The `data` object holds all structured content from tagged code blocks and editor widgets.
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
{
|
|
131
|
+
// From tagged code blocks
|
|
132
|
+
"form": { fields: [...], submitLabel: "Send" },
|
|
133
|
+
"nav-links": [{ label: "Home", href: "/" }],
|
|
134
|
+
"config": { theme: "dark" },
|
|
135
|
+
|
|
136
|
+
// From editor card widgets (mapped by type)
|
|
137
|
+
"person": [
|
|
138
|
+
{ name: "John", title: "CEO", ... },
|
|
139
|
+
{ name: "Jane", title: "CTO", ... },
|
|
140
|
+
],
|
|
141
|
+
"event": [
|
|
142
|
+
{ title: "Launch Party", date: "2024-01-15", location: "NYC", ... },
|
|
143
|
+
],
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**Markdown syntax for structured data:**
|
|
148
|
+
```markdown
|
|
149
|
+
```yaml:form
|
|
150
|
+
fields:
|
|
151
|
+
- name: email
|
|
152
|
+
type: email
|
|
153
|
+
required: true
|
|
154
|
+
submitLabel: Subscribe
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
```yaml:nav-links
|
|
158
|
+
- label: Home
|
|
159
|
+
href: /
|
|
160
|
+
```
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
JSON is also supported (`json:tag-name`) if you prefer.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Editor Node Mappings
|
|
168
|
+
|
|
169
|
+
This section documents how TipTap/editor-specific nodes map to standard entities.
|
|
170
|
+
|
|
171
|
+
### `button` Node → `links[]`
|
|
172
|
+
|
|
173
|
+
**Editor input:**
|
|
174
|
+
```js
|
|
175
|
+
{
|
|
176
|
+
type: "button",
|
|
177
|
+
content: [{ type: "text", text: "Click me" }],
|
|
178
|
+
attrs: {
|
|
179
|
+
href: "/action",
|
|
180
|
+
variant: "primary",
|
|
181
|
+
size: "lg",
|
|
182
|
+
icon: "arrow-right"
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Standard output:**
|
|
188
|
+
```js
|
|
189
|
+
links: [{
|
|
190
|
+
href: "/action",
|
|
191
|
+
label: "Click me",
|
|
192
|
+
role: "button",
|
|
193
|
+
variant: "primary",
|
|
194
|
+
size: "lg",
|
|
195
|
+
icon: "arrow-right"
|
|
196
|
+
}]
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### `FormBlock` Node → `data.form`
|
|
200
|
+
|
|
201
|
+
**Editor input:**
|
|
202
|
+
```js
|
|
203
|
+
{
|
|
204
|
+
type: "FormBlock",
|
|
205
|
+
attrs: {
|
|
206
|
+
data: {
|
|
207
|
+
fields: [{ name: "email", type: "email" }],
|
|
208
|
+
submitLabel: "Subscribe"
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Standard output:**
|
|
215
|
+
```js
|
|
216
|
+
data: {
|
|
217
|
+
form: {
|
|
218
|
+
fields: [{ name: "email", type: "email" }],
|
|
219
|
+
submitLabel: "Subscribe"
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### `card-group` Node → `data[cardType]`
|
|
225
|
+
|
|
226
|
+
Cards are editor widgets for structured entities like people, events, addresses. Each card type becomes a key in `data`, with an array of all cards of that type. This follows the same pattern as tagged code blocks.
|
|
227
|
+
|
|
228
|
+
**Editor input:**
|
|
229
|
+
```js
|
|
230
|
+
{
|
|
231
|
+
type: "card-group",
|
|
232
|
+
content: [
|
|
233
|
+
{
|
|
234
|
+
type: "card",
|
|
235
|
+
attrs: {
|
|
236
|
+
cardType: "person",
|
|
237
|
+
title: "Jane Doe",
|
|
238
|
+
subtitle: "CEO",
|
|
239
|
+
coverImg: { src: "/jane.jpg" },
|
|
240
|
+
address: '{"city": "NYC"}',
|
|
241
|
+
icon: { svg: "..." }
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
type: "card",
|
|
246
|
+
attrs: {
|
|
247
|
+
cardType: "person",
|
|
248
|
+
title: "John Smith",
|
|
249
|
+
subtitle: "CTO",
|
|
250
|
+
coverImg: { src: "/john.jpg" }
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
type: "card",
|
|
255
|
+
attrs: {
|
|
256
|
+
cardType: "event",
|
|
257
|
+
title: "Launch Party",
|
|
258
|
+
date: "2024-03-15",
|
|
259
|
+
location: "San Francisco"
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
]
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**Standard output:**
|
|
267
|
+
```js
|
|
268
|
+
data: {
|
|
269
|
+
person: [
|
|
270
|
+
{
|
|
271
|
+
title: "Jane Doe",
|
|
272
|
+
subtitle: "CEO",
|
|
273
|
+
coverImg: "/jane.jpg",
|
|
274
|
+
address: { city: "NYC" },
|
|
275
|
+
icon: { svg: "..." }
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
title: "John Smith",
|
|
279
|
+
subtitle: "CTO",
|
|
280
|
+
coverImg: "/john.jpg"
|
|
281
|
+
}
|
|
282
|
+
],
|
|
283
|
+
event: [
|
|
284
|
+
{
|
|
285
|
+
title: "Launch Party",
|
|
286
|
+
date: "2024-03-15",
|
|
287
|
+
location: "San Francisco"
|
|
288
|
+
}
|
|
289
|
+
]
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
**Accessing cards by type:**
|
|
294
|
+
```js
|
|
295
|
+
// Get all person cards
|
|
296
|
+
const people = content.data.person || [];
|
|
297
|
+
|
|
298
|
+
// Get all event cards
|
|
299
|
+
const events = content.data.event || [];
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
**Card schemas:**
|
|
303
|
+
| Schema | Common Fields |
|
|
304
|
+
|--------|---------------|
|
|
305
|
+
| `person` | title (name), subtitle (role), coverImg (photo), address |
|
|
306
|
+
| `event` | title, date, location, description |
|
|
307
|
+
| `address` | street, city, state, country, postal |
|
|
308
|
+
| `document` | title, href, coverImg (preview), fileType |
|
|
309
|
+
|
|
310
|
+
### `document-group` Node → `links[]`
|
|
311
|
+
|
|
312
|
+
Documents are downloadable files. They map to links with `role: "document"`.
|
|
313
|
+
|
|
314
|
+
**Editor input:**
|
|
315
|
+
```js
|
|
316
|
+
{
|
|
317
|
+
type: "document-group",
|
|
318
|
+
content: [
|
|
319
|
+
{
|
|
320
|
+
type: "document",
|
|
321
|
+
attrs: {
|
|
322
|
+
title: "Annual Report",
|
|
323
|
+
src: "/reports/annual-2024.pdf",
|
|
324
|
+
coverImg: { src: "/preview.jpg" }
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
]
|
|
328
|
+
}
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
**Standard output:**
|
|
332
|
+
```js
|
|
333
|
+
links: [{
|
|
334
|
+
href: "/reports/annual-2024.pdf",
|
|
335
|
+
label: "Annual Report",
|
|
336
|
+
role: "document",
|
|
337
|
+
download: true,
|
|
338
|
+
preview: "/preview.jpg"
|
|
339
|
+
}]
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## Deprecation: `alignment`
|
|
345
|
+
|
|
346
|
+
The `alignment` field was extracted from heading's `textAlign` attribute in the editor. This is an editor-specific styling concern that:
|
|
347
|
+
- Cannot be expressed in file-based markdown
|
|
348
|
+
- Is a presentation concern, not semantic content
|
|
349
|
+
- Should be handled by component styling, not content structure
|
|
350
|
+
|
|
351
|
+
**Migration:** Components relying on `content.alignment` should:
|
|
352
|
+
1. Use CSS/Tailwind for text alignment
|
|
353
|
+
2. Or accept alignment as a component `param` in frontmatter
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## Migration Path
|
|
358
|
+
|
|
359
|
+
### Phase 1: Add Mappings (Non-Breaking)
|
|
360
|
+
|
|
361
|
+
1. Continue outputting legacy fields (`buttons`, `cards`, `documents`, `forms`, `alignment`)
|
|
362
|
+
2. Also populate new locations (`links` for buttons/documents, `data` for cards/forms)
|
|
363
|
+
3. Components can migrate gradually
|
|
364
|
+
|
|
365
|
+
### Phase 2: Deprecation Warnings
|
|
366
|
+
|
|
367
|
+
1. Log warnings when legacy fields are accessed
|
|
368
|
+
2. Document migration for each field
|
|
369
|
+
3. Provide codemod or migration script
|
|
370
|
+
|
|
371
|
+
### Phase 3: Remove Legacy Fields
|
|
372
|
+
|
|
373
|
+
1. Remove `buttons`, `cards`, `documents`, `forms`, `alignment` from output
|
|
374
|
+
2. Update all components to use new structure
|
|
375
|
+
3. Update documentation
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## Backwards Compatibility
|
|
380
|
+
|
|
381
|
+
During migration, the parser can provide a compatibility layer:
|
|
382
|
+
|
|
383
|
+
```js
|
|
384
|
+
// Parser option
|
|
385
|
+
const content = parse(doc, {
|
|
386
|
+
legacyFields: true // Include deprecated fields
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// Or via getter that warns
|
|
390
|
+
Object.defineProperty(content, 'buttons', {
|
|
391
|
+
get() {
|
|
392
|
+
console.warn('content.buttons is deprecated, use content.links with role="button"');
|
|
393
|
+
return content.links.filter(l => l.role?.startsWith('button'));
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
## Component Migration Examples
|
|
401
|
+
|
|
402
|
+
### Before: Using `buttons`
|
|
403
|
+
|
|
404
|
+
```jsx
|
|
405
|
+
function CTA({ content }) {
|
|
406
|
+
const { links, buttons } = content;
|
|
407
|
+
return (
|
|
408
|
+
<div>
|
|
409
|
+
{links.map(link => <a href={link.href}>{link.label}</a>)}
|
|
410
|
+
{buttons.map(btn => <button>{btn.content}</button>)}
|
|
411
|
+
</div>
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### After: Unified `links`
|
|
417
|
+
|
|
418
|
+
```jsx
|
|
419
|
+
function CTA({ content }) {
|
|
420
|
+
const { links } = content;
|
|
421
|
+
const buttons = links.filter(l => l.role?.startsWith('button'));
|
|
422
|
+
const plainLinks = links.filter(l => !l.role?.startsWith('button'));
|
|
423
|
+
|
|
424
|
+
return (
|
|
425
|
+
<div>
|
|
426
|
+
{plainLinks.map(link => <a href={link.href}>{link.label}</a>)}
|
|
427
|
+
{buttons.map(btn => (
|
|
428
|
+
<a href={btn.href} className={`btn btn-${btn.variant}`}>
|
|
429
|
+
{btn.label}
|
|
430
|
+
</a>
|
|
431
|
+
))}
|
|
432
|
+
</div>
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### Or: Role-based rendering
|
|
438
|
+
|
|
439
|
+
```jsx
|
|
440
|
+
function CTA({ content }) {
|
|
441
|
+
return (
|
|
442
|
+
<div>
|
|
443
|
+
{content.links.map(link => {
|
|
444
|
+
if (link.role?.startsWith('button')) {
|
|
445
|
+
return <Button variant={link.variant}>{link.label}</Button>;
|
|
446
|
+
}
|
|
447
|
+
if (link.role === 'document') {
|
|
448
|
+
return <DownloadLink href={link.href}>{link.label}</DownloadLink>;
|
|
449
|
+
}
|
|
450
|
+
return <a href={link.href}>{link.label}</a>;
|
|
451
|
+
})}
|
|
452
|
+
</div>
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## Implementation Checklist
|
|
460
|
+
|
|
461
|
+
- [ ] Update `processGroupContent` in `groups.js` to map button → links
|
|
462
|
+
- [ ] Update `processGroupContent` to map card-group → data.cards
|
|
463
|
+
- [ ] Update `processGroupContent` to map document-group → links
|
|
464
|
+
- [ ] Update `processGroupContent` to map FormBlock → data.form
|
|
465
|
+
- [ ] Remove `alignment` from header extraction
|
|
466
|
+
- [ ] Add `legacyFields` option for backwards compatibility
|
|
467
|
+
- [ ] Update `flattenGroup` to use new structure
|
|
468
|
+
- [ ] Update tests for new entity structure
|
|
469
|
+
- [ ] Update AGENTS.md and README.md
|
|
470
|
+
- [ ] Create migration guide for components
|
package/package.json
CHANGED
|
@@ -16,6 +16,10 @@ import { first, joinParagraphs } from "./helpers.js";
|
|
|
16
16
|
* @returns {Object} Hero component data
|
|
17
17
|
*/
|
|
18
18
|
function hero(parsed) {
|
|
19
|
+
const links = parsed?.links || [];
|
|
20
|
+
const buttonLink = links.find(l => l.role?.startsWith('button'));
|
|
21
|
+
const plainLink = links.find(l => !l.role?.startsWith('button'));
|
|
22
|
+
|
|
19
23
|
return {
|
|
20
24
|
title: parsed?.title || null,
|
|
21
25
|
subtitle: parsed?.subtitle || null,
|
|
@@ -24,8 +28,7 @@ function hero(parsed) {
|
|
|
24
28
|
image: first(parsed?.imgs)?.url || null,
|
|
25
29
|
imageAlt: first(parsed?.imgs)?.alt || null,
|
|
26
30
|
banner: null, // Banner detection would need to be added separately
|
|
27
|
-
cta:
|
|
28
|
-
button: first(parsed?.buttons) || null,
|
|
31
|
+
cta: buttonLink || plainLink || null,
|
|
29
32
|
};
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -45,6 +48,10 @@ function card(parsed, options = {}) {
|
|
|
45
48
|
const extractCard = (content) => {
|
|
46
49
|
if (!content) return null;
|
|
47
50
|
|
|
51
|
+
const links = content.links || [];
|
|
52
|
+
const buttonLink = links.find(l => l.role?.startsWith('button'));
|
|
53
|
+
const plainLink = links.find(l => !l.role?.startsWith('button'));
|
|
54
|
+
|
|
48
55
|
return {
|
|
49
56
|
title: content.title || null,
|
|
50
57
|
subtitle: content.subtitle || null,
|
|
@@ -52,8 +59,8 @@ function card(parsed, options = {}) {
|
|
|
52
59
|
image: first(content.imgs)?.url || null,
|
|
53
60
|
imageAlt: first(content.imgs)?.alt || null,
|
|
54
61
|
icon: first(content.icons) || null,
|
|
55
|
-
link:
|
|
56
|
-
|
|
62
|
+
link: plainLink || null,
|
|
63
|
+
cta: buttonLink || plainLink || null,
|
|
57
64
|
};
|
|
58
65
|
};
|
|
59
66
|
|
|
@@ -230,6 +237,8 @@ function pricing(parsed) {
|
|
|
230
237
|
return items
|
|
231
238
|
.map((item) => {
|
|
232
239
|
const firstList = first(item.lists);
|
|
240
|
+
const links = item.links || [];
|
|
241
|
+
const buttonLink = links.find(l => l.role?.startsWith('button'));
|
|
233
242
|
|
|
234
243
|
return {
|
|
235
244
|
name: item.title || null,
|
|
@@ -242,7 +251,7 @@ function pricing(parsed) {
|
|
|
242
251
|
)
|
|
243
252
|
.filter(Boolean)
|
|
244
253
|
: [],
|
|
245
|
-
cta:
|
|
254
|
+
cta: buttonLink || first(links) || null,
|
|
246
255
|
highlighted:
|
|
247
256
|
item.pretitle?.toLowerCase().includes("popular") || false,
|
|
248
257
|
};
|
|
@@ -314,6 +323,9 @@ function gallery(parsed, options = {}) {
|
|
|
314
323
|
* used by the legacy Article class, enabling drop-in replacement without
|
|
315
324
|
* breaking existing components.
|
|
316
325
|
*
|
|
326
|
+
* NOTE: Reconstructs deprecated fields (buttons, cards, documents, forms, alignment)
|
|
327
|
+
* from the new consolidated structure for backwards compatibility.
|
|
328
|
+
*
|
|
317
329
|
* @param {Object} parsed - Parsed content from parseContent() (flat structure)
|
|
318
330
|
* @returns {Object} Legacy format { main, items } with nested header/body structure
|
|
319
331
|
*
|
|
@@ -334,6 +346,20 @@ function legacy(parsed) {
|
|
|
334
346
|
|
|
335
347
|
if (!banner) banner = imgs[0];
|
|
336
348
|
|
|
349
|
+
// Reconstruct deprecated fields from new structure
|
|
350
|
+
const links = content.links || [];
|
|
351
|
+
const buttons = links
|
|
352
|
+
.filter(l => l.role?.startsWith('button'))
|
|
353
|
+
.map(l => ({ attrs: l, content: l.label }));
|
|
354
|
+
const documents = links
|
|
355
|
+
.filter(l => l.role === 'document')
|
|
356
|
+
.map(l => ({ title: l.label, href: l.href, coverImg: l.preview }));
|
|
357
|
+
const plainLinks = links.filter(l => !l.role?.startsWith('button') && l.role !== 'document');
|
|
358
|
+
|
|
359
|
+
const cards = content.data?.cards || [];
|
|
360
|
+
const form = content.data?.form || null;
|
|
361
|
+
const forms = form ? [form] : [];
|
|
362
|
+
|
|
337
363
|
return {
|
|
338
364
|
header: {
|
|
339
365
|
title: content.title || "",
|
|
@@ -345,7 +371,7 @@ function legacy(parsed) {
|
|
|
345
371
|
content.subtitle2 ||
|
|
346
372
|
first(content.paragraphs) ||
|
|
347
373
|
"",
|
|
348
|
-
alignment:
|
|
374
|
+
alignment: "", // Deprecated: always empty
|
|
349
375
|
},
|
|
350
376
|
banner,
|
|
351
377
|
body: {
|
|
@@ -354,16 +380,16 @@ function legacy(parsed) {
|
|
|
354
380
|
imgs,
|
|
355
381
|
videos: content.videos || [],
|
|
356
382
|
lists: content.lists || [],
|
|
357
|
-
links:
|
|
383
|
+
links: plainLinks,
|
|
358
384
|
icons: content.icons || [],
|
|
359
|
-
buttons
|
|
360
|
-
cards
|
|
361
|
-
documents
|
|
362
|
-
forms
|
|
363
|
-
form
|
|
385
|
+
buttons,
|
|
386
|
+
cards,
|
|
387
|
+
documents,
|
|
388
|
+
forms,
|
|
389
|
+
form,
|
|
364
390
|
quotes: content.quotes || [],
|
|
365
|
-
properties: content.
|
|
366
|
-
propertyBlocks:
|
|
391
|
+
properties: content.data || {},
|
|
392
|
+
propertyBlocks: [],
|
|
367
393
|
},
|
|
368
394
|
};
|
|
369
395
|
};
|
package/src/processors/groups.js
CHANGED
|
@@ -10,18 +10,13 @@ function flattenGroup(group) {
|
|
|
10
10
|
pretitle: group.header.pretitle || '',
|
|
11
11
|
subtitle: group.header.subtitle || '',
|
|
12
12
|
subtitle2: group.header.subtitle2 || '',
|
|
13
|
-
alignment: group.header.alignment || null,
|
|
14
13
|
paragraphs: group.body.paragraphs || [],
|
|
15
14
|
links: group.body.links || [],
|
|
16
15
|
imgs: group.body.imgs || [],
|
|
17
16
|
icons: group.body.icons || [],
|
|
18
17
|
lists: group.body.lists || [],
|
|
19
18
|
videos: group.body.videos || [],
|
|
20
|
-
buttons: group.body.buttons || [],
|
|
21
19
|
data: group.body.data || {},
|
|
22
|
-
cards: group.body.cards || [],
|
|
23
|
-
documents: group.body.documents || [],
|
|
24
|
-
forms: group.body.forms || [],
|
|
25
20
|
quotes: group.body.quotes || [],
|
|
26
21
|
headings: group.body.headings || [],
|
|
27
22
|
};
|
|
@@ -41,18 +36,13 @@ function processGroups(sequence, options = {}) {
|
|
|
41
36
|
pretitle: '',
|
|
42
37
|
subtitle: '',
|
|
43
38
|
subtitle2: '',
|
|
44
|
-
alignment: null,
|
|
45
39
|
paragraphs: [],
|
|
46
40
|
links: [],
|
|
47
41
|
imgs: [],
|
|
48
42
|
icons: [],
|
|
49
43
|
lists: [],
|
|
50
44
|
videos: [],
|
|
51
|
-
buttons: [],
|
|
52
45
|
data: {},
|
|
53
|
-
cards: [],
|
|
54
|
-
documents: [],
|
|
55
|
-
forms: [],
|
|
56
46
|
quotes: [],
|
|
57
47
|
headings: [],
|
|
58
48
|
items: [],
|
|
@@ -82,18 +72,13 @@ function processGroups(sequence, options = {}) {
|
|
|
82
72
|
pretitle: '',
|
|
83
73
|
subtitle: '',
|
|
84
74
|
subtitle2: '',
|
|
85
|
-
alignment: null,
|
|
86
75
|
paragraphs: [],
|
|
87
76
|
links: [],
|
|
88
77
|
imgs: [],
|
|
89
78
|
icons: [],
|
|
90
79
|
lists: [],
|
|
91
80
|
videos: [],
|
|
92
|
-
buttons: [],
|
|
93
81
|
data: {},
|
|
94
|
-
cards: [],
|
|
95
|
-
documents: [],
|
|
96
|
-
forms: [],
|
|
97
82
|
quotes: [],
|
|
98
83
|
headings: [],
|
|
99
84
|
};
|
|
@@ -225,7 +210,6 @@ function processGroupContent(elements) {
|
|
|
225
210
|
title: "",
|
|
226
211
|
subtitle: "",
|
|
227
212
|
subtitle2: "",
|
|
228
|
-
alignment: null,
|
|
229
213
|
};
|
|
230
214
|
|
|
231
215
|
const body = {
|
|
@@ -235,11 +219,7 @@ function processGroupContent(elements) {
|
|
|
235
219
|
paragraphs: [],
|
|
236
220
|
links: [],
|
|
237
221
|
lists: [],
|
|
238
|
-
buttons: [],
|
|
239
222
|
data: {},
|
|
240
|
-
cards: [],
|
|
241
|
-
documents: [],
|
|
242
|
-
forms: [],
|
|
243
223
|
quotes: [],
|
|
244
224
|
headings: [],
|
|
245
225
|
};
|
|
@@ -272,10 +252,6 @@ function processGroupContent(elements) {
|
|
|
272
252
|
//We shuold set the group level to the highest one instead of the first one.
|
|
273
253
|
metadata.level ??= element.level;
|
|
274
254
|
|
|
275
|
-
// Extract alignment from first heading
|
|
276
|
-
if (!header.alignment && element.attrs?.textAlign) {
|
|
277
|
-
header.alignment = element.attrs.textAlign;
|
|
278
|
-
}
|
|
279
255
|
// h3 h2 h1 h1
|
|
280
256
|
// Assign to header fields
|
|
281
257
|
// h3 h2 h3 h4
|
|
@@ -329,9 +305,16 @@ function processGroupContent(elements) {
|
|
|
329
305
|
break;
|
|
330
306
|
|
|
331
307
|
case "button":
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
308
|
+
// Map button to link with role
|
|
309
|
+
body.links.push({
|
|
310
|
+
href: element.attrs?.href || '',
|
|
311
|
+
label: element.text || '',
|
|
312
|
+
role: element.attrs?.variant ? `button-${element.attrs.variant}` : 'button',
|
|
313
|
+
variant: element.attrs?.variant || 'primary',
|
|
314
|
+
size: element.attrs?.size,
|
|
315
|
+
icon: element.attrs?.icon,
|
|
316
|
+
target: element.attrs?.target,
|
|
317
|
+
class: element.attrs?.class,
|
|
335
318
|
});
|
|
336
319
|
break;
|
|
337
320
|
|
|
@@ -356,15 +339,34 @@ function processGroupContent(elements) {
|
|
|
356
339
|
break;
|
|
357
340
|
|
|
358
341
|
case "form":
|
|
359
|
-
|
|
342
|
+
// Map FormBlock to data.form
|
|
343
|
+
body.data.form = element.data || element.attrs;
|
|
360
344
|
break;
|
|
361
345
|
|
|
362
346
|
case "card-group":
|
|
363
|
-
|
|
347
|
+
// Map cards to data by type: data.person = [...], data.event = [...]
|
|
348
|
+
// Each card type becomes a key, with an array of cards of that type
|
|
349
|
+
(element.cards || []).forEach(card => {
|
|
350
|
+
const cardType = card.cardType || 'card';
|
|
351
|
+
if (!body.data[cardType]) body.data[cardType] = [];
|
|
352
|
+
// Remove cardType from the card object since it's now the key
|
|
353
|
+
const { cardType: _, ...cardData } = card;
|
|
354
|
+
body.data[cardType].push(cardData);
|
|
355
|
+
});
|
|
364
356
|
break;
|
|
365
357
|
|
|
366
358
|
case "document-group":
|
|
367
|
-
|
|
359
|
+
// Map documents to links with role=document
|
|
360
|
+
element.documents.forEach(doc => {
|
|
361
|
+
body.links.push({
|
|
362
|
+
href: doc.href || doc.downloadUrl || '',
|
|
363
|
+
label: doc.title || '',
|
|
364
|
+
role: 'document',
|
|
365
|
+
download: true,
|
|
366
|
+
preview: doc.coverImg,
|
|
367
|
+
fileType: doc.fileType,
|
|
368
|
+
});
|
|
369
|
+
});
|
|
368
370
|
break;
|
|
369
371
|
}
|
|
370
372
|
}
|