@uniweb/semantic-parser 1.1.4 → 1.1.6

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,379 +0,0 @@
1
- /**
2
- * Transform a sequence into content groups with semantic structure
3
- * @param {Array} sequence Flat sequence of elements
4
- * @param {Object} options Parsing options
5
- * @returns {Object} Content organized into groups with identified main content
6
- */
7
- function processGroups(sequence, options = {}) {
8
- const result = {
9
- main: null,
10
- items: [],
11
- metadata: {
12
- dividerMode: false,
13
- groups: 0,
14
- },
15
- };
16
-
17
- if (!sequence.length) return result;
18
-
19
- // Check if using divider mode
20
- result.metadata.dividerMode = sequence.some((el) => el.type === "divider");
21
-
22
- // Split sequence into raw groups
23
- const groups = result.metadata.dividerMode
24
- ? splitByDividers(sequence)
25
- : splitByHeadings(sequence, options);
26
-
27
- // Process each group's structure
28
- const processedGroups = groups.map((group) => processGroupContent(group));
29
-
30
- // Special handling for first group in divider mode
31
- if (result.metadata.dividerMode && groups.startsWithDivider) {
32
- result.items = processedGroups;
33
- } else {
34
- // Organize into main content and items
35
- const shouldBeMain = identifyMainContent(processedGroups);
36
- if (shouldBeMain) {
37
- result.main = processedGroups[0];
38
- result.items = processedGroups.slice(1);
39
- } else {
40
- result.items = processedGroups;
41
- }
42
- }
43
-
44
- // result.metadata.groups = processedGroups.length;
45
- return result;
46
- }
47
-
48
- /**
49
- * Split sequence into groups using dividers
50
- */
51
- function splitByDividers(sequence) {
52
- const groups = [];
53
- let currentGroup = [];
54
- let startsWithDivider = false;
55
-
56
- // Check if content effectively starts with divider (ignoring whitespace etc)
57
- for (let i = 0; i < sequence.length; i++) {
58
- const element = sequence[i];
59
-
60
- if (element.type === "divider") {
61
- if (currentGroup.length === 0 && groups.length === 0) {
62
- startsWithDivider = true;
63
- } else if (currentGroup.length > 0) {
64
- groups.push(currentGroup);
65
- currentGroup = [];
66
- }
67
- } else {
68
- currentGroup.push(element);
69
- }
70
- }
71
-
72
- if (currentGroup.length > 0) {
73
- groups.push(currentGroup);
74
- }
75
-
76
- groups.startsWithDivider = startsWithDivider;
77
- return groups;
78
- }
79
-
80
- /**
81
- * Split sequence into groups using heading patterns
82
- */
83
- function splitByHeadings(sequence, options = {}) {
84
- const groups = [];
85
- let currentGroup = [];
86
- let isPreOpened = false;
87
-
88
- // Consider if current group is pre opened (only has banner or pretitle)
89
- // before starting a new group.
90
- const startGroup = (preOpen) => {
91
- if (currentGroup.length && !isPreOpened) {
92
- groups.push(currentGroup);
93
- currentGroup = [];
94
- }
95
- isPreOpened = preOpen;
96
- };
97
-
98
- for (let i = 0; i < sequence.length; i++) {
99
- // Only allow a banner for the first group
100
- if (!groups.length && isBannerImage(sequence, i)) {
101
- startGroup(true); // pre open a new group
102
- currentGroup.push(sequence[i]);
103
- i++; // move to known next element (it will be a heading)
104
- }
105
-
106
- // Handle special pretitle case before consuming all consecutive
107
- // headings with increasing levels
108
- if (isPreTitle(sequence, i)) {
109
- startGroup(true); // pre open a new group
110
- currentGroup.push(sequence[i]);
111
- i++; // move to known next element (it will be a heading)
112
- }
113
-
114
- const element = sequence[i];
115
-
116
- if (element.type === "heading") {
117
- const headings = readHeadingGroup(sequence, i);
118
- startGroup(false);
119
-
120
- // Add headings to the current group
121
- currentGroup.push(...headings);
122
- i += headings.length - 1; // skip all the added headings
123
- } else {
124
- currentGroup.push(element);
125
- }
126
- }
127
-
128
- if (currentGroup.length > 0) {
129
- groups.push(currentGroup);
130
- }
131
-
132
- return groups;
133
- }
134
-
135
- /**
136
- * Check if this is a pretitle - any heading followed by a more important heading
137
- * (e.g., H3→H1, H2→H1, H6→H5, etc.)
138
- */
139
- function isPreTitle(sequence, i) {
140
- return (
141
- i + 1 < sequence.length &&
142
- sequence[i].type === "heading" &&
143
- sequence[i + 1].type === "heading" &&
144
- sequence[i].level > sequence[i + 1].level // Smaller heading before larger
145
- );
146
- }
147
-
148
- function isBannerImage(sequence, i) {
149
- return (
150
- i + 1 < sequence.length &&
151
- sequence[i].type === "image" &&
152
- (sequence[i].role === "banner" || sequence[i + 1].type === "heading")
153
- );
154
- }
155
-
156
- /**
157
- * Eagerly consume all consecutive headings with increasing levels
158
- * and return them as an array.
159
- */
160
- function readHeadingGroup(sequence, i) {
161
- const elements = [sequence[i]];
162
- for (i++; i < sequence.length; i++) {
163
- const element = sequence[i];
164
-
165
- if (
166
- element.type === "heading" &&
167
- element.level > sequence[i - 1].level
168
- ) {
169
- elements.push(element);
170
- } else {
171
- break;
172
- }
173
- }
174
- return elements;
175
- }
176
-
177
- /**
178
- * Process a group's content to identify its structure
179
- */
180
- function processGroupContent(elements) {
181
- const header = {
182
- pretitle: "",
183
- title: "",
184
- subtitle: "",
185
- subtitle2: "",
186
- alignment: null,
187
- };
188
- let banner = null;
189
- const body = {
190
- imgs: [],
191
- icons: [],
192
- videos: [],
193
- paragraphs: [],
194
- links: [],
195
- lists: [],
196
- buttons: [],
197
- properties: {},
198
- propertyBlocks: [],
199
- cards: [],
200
- documents: [],
201
- forms: [],
202
- quotes: [],
203
- headings: [],
204
- };
205
-
206
- const metadata = {
207
- level: null,
208
- contentTypes: new Set(),
209
- };
210
-
211
- let inBody = false; // Track when we've finished header section
212
-
213
- for (let i = 0; i < elements.length; i++) {
214
- if (isPreTitle(elements, i)) {
215
- header.pretitle = elements[i].content;
216
- i++; // move to known next heading (H1 or h2)
217
- }
218
-
219
- if (isBannerImage(elements, i)) {
220
- banner = {
221
- url: elements[i].src,
222
- caption: elements[i].caption,
223
- alt: elements[i].alt,
224
- };
225
- i++;
226
- }
227
-
228
- const element = elements[i];
229
-
230
- if (element.type === "heading") {
231
- metadata.level ??= element.level;
232
-
233
- // Extract alignment from first heading
234
- if (!header.alignment && element.attrs?.textAlign) {
235
- header.alignment = element.attrs.textAlign;
236
- }
237
-
238
- // Assign to header fields
239
- if (!header.title) {
240
- header.title = element.content;
241
- } else if (!header.subtitle) {
242
- header.subtitle = element.content;
243
- } else if (!header.subtitle2) {
244
- header.subtitle2 = element.content;
245
- } else {
246
- // After subtitle2, we're in body - collect heading
247
- inBody = true;
248
- body.headings.push(element.content);
249
- }
250
- } else if (element.type === "list") {
251
- inBody = true;
252
- body.lists.push(processListContent(element));
253
- } else {
254
- inBody = true;
255
-
256
- switch (element.type) {
257
- case "paragraph":
258
- body.paragraphs.push(element.content);
259
- break;
260
-
261
- case "image":
262
- body.imgs.push({
263
- url: element.src,
264
- caption: element.caption,
265
- alt: element.alt,
266
- });
267
- break;
268
-
269
- case "link":
270
- body.links.push({
271
- href: element.content.href,
272
- label: element.content.label,
273
- });
274
- break;
275
-
276
- case "styledLink":
277
- // Styled link (multi-part with same href)
278
- body.links.push({
279
- href: element.href,
280
- label: element.content,
281
- target: element.target,
282
- });
283
- break;
284
-
285
- case "icon":
286
- body.icons.push(element.svg);
287
- break;
288
-
289
- case "button":
290
- body.buttons.push(element);
291
- break;
292
-
293
- case "video":
294
- body.videos.push({
295
- src: element.src,
296
- caption: element.caption,
297
- alt: element.alt,
298
- });
299
- break;
300
-
301
- case "blockquote":
302
- // Process blockquote content recursively
303
- const quoteContent = processGroupContent(
304
- element.content,
305
- options
306
- );
307
- body.quotes.push(quoteContent.body);
308
- break;
309
-
310
- case "codeBlock":
311
- // Use parsed JSON if available, otherwise use text content
312
- const codeData =
313
- element.parsed !== null
314
- ? element.parsed
315
- : element.content;
316
- body.properties = codeData; // Last one
317
- body.propertyBlocks.push(codeData); // All of them
318
- break;
319
-
320
- case "card-group":
321
- body.cards.push(...element.cards);
322
- break;
323
-
324
- case "document-group":
325
- body.documents.push(...element.documents);
326
- break;
327
-
328
- case "form":
329
- body.forms.push(element.data || element.attrs);
330
- break;
331
- }
332
- }
333
- }
334
-
335
- return {
336
- header,
337
- body,
338
- banner,
339
- metadata,
340
- };
341
- }
342
-
343
- function processListContent(list) {
344
- const { items } = list;
345
-
346
- return items.map((item) => {
347
- const { items: nestedList, content: listContent } = item;
348
-
349
- const parsedContent = processGroupContent(listContent).body;
350
-
351
- if (nestedList.length) {
352
- const parsedNestedList = nestedList.map(
353
- (nestedItem) => processGroupContent(nestedItem.content).body
354
- );
355
-
356
- parsedContent.lists = [parsedNestedList];
357
- }
358
-
359
- return parsedContent;
360
- });
361
- }
362
-
363
- /**
364
- * Determine if the first group should be treated as main content
365
- */
366
- function identifyMainContent(groups) {
367
- if (groups.length === 0) return false;
368
-
369
- // Single group is main content
370
- if (groups.length === 1) return true;
371
-
372
- // First group should be more important (lower level) than second to be main
373
- const first = groups[0].metadata.level;
374
- const second = groups[1].metadata.level;
375
-
376
- return first ? !second || first < second : false;
377
- }
378
-
379
- export { processGroups };
@@ -1,179 +0,0 @@
1
- # Content Grouping Logic
2
-
3
- This document outlines how the `processGroups` function interprets flat arrays of content (Headings, Paragraphs, etc.) and organizes them into semantic **Main Content** and **List Items**.
4
-
5
- ## The Core Challenge
6
-
7
- The parser must distinguish between two visually similar but semantically different patterns:
8
-
9
- 1. **Subtitles:** A smaller heading that belongs to the main title (Merge).
10
-
11
- 2. **List Items:** A smaller heading that starts a new list item (Split).
12
-
13
- ## The Logic (Heuristics)
14
-
15
- To make this decision, the parser looks ahead at the structure:
16
-
17
- 1. **Sibling Boundary:** If we are at Level X and encounter another Level X, it is always a sibling. We **Split**.
18
-
19
- 2. **Peer Detection:** If we are stepping down (H1 → H2), we check if that H2 has a "peer" (another H2) later in the section.
20
-
21
- 3. **Leaf vs. Branch:**
22
-
23
- - **Leaf:** A heading with no sub-headings underneath it.
24
-
25
- - **Branch:** A heading with sub-headings (e.g., H2 followed by H3 dates).
26
-
27
- ## Supported Patterns & Behavior
28
-
29
- ### 1. The "Resume" Pattern (Items)
30
-
31
- - **Structure:** `H1` → `H2 (Branch)` → `H2 (Branch)`
32
-
33
- - **Use Case:** Academic Experience, Work History.
34
-
35
- - **Behavior:** The parser sees the first H2 has a peer. Both are "Branches" (have children).
36
-
37
- - **Result:** **Split**. The H1 becomes Main; the H2s become separate Items.
38
-
39
- **Input Data Structure:**
40
-
41
- ```
42
- {
43
- "type": "doc",
44
- "content": [
45
- { "type": "heading", "attrs": { "level": 1 }, "content": [{ "type": "text", "text": "Academic Experience" }] },
46
- { "type": "heading", "attrs": { "level": 2 }, "content": [{ "type": "text", "text": "Ph.D. in CS" }] },
47
- { "type": "heading", "attrs": { "level": 3 }, "content": [{ "type": "text", "text": "2014-2018" }] },
48
- { "type": "paragraph", "content": [{ "type": "text", "text": "MIT" }] },
49
- { "type": "heading", "attrs": { "level": 2 }, "content": [{ "type": "text", "text": "Masters in Data" }] },
50
- { "type": "heading", "attrs": { "level": 3 }, "content": [{ "type": "text", "text": "2012-2014" }] },
51
- { "type": "paragraph", "content": [{ "type": "text", "text": "Berkeley" }] }
52
- ]
53
- }
54
- ```
55
-
56
- **Parsed Output::**
57
-
58
- ```
59
- [Main] title: Academic Experience
60
-
61
- [Item] title: Ph.D. in CS
62
- subtitle: 2014-2018
63
-
64
- [Item] title: Masters in Data
65
- subtitle: 2012-2014
66
- ```
67
-
68
- ### 2. The "Standard" Pattern (Leaf Items)
69
-
70
- - **Structure:** `H1` → `H2 (Leaf)` → `H2 (Leaf)`
71
-
72
- - **Use Case:** Features list, standard sections.
73
-
74
- - **Behavior:** Even though the H2s are leaves (no children), the parser detects a peer (another H2).
75
-
76
- - **Result:** **Split**. Sibling detection forces them into separate items.
77
-
78
- **Input Data Structure:**
79
-
80
- ```
81
- {
82
- "type": "doc",
83
- "content": [
84
- { "type": "heading", "attrs": { "level": 1 }, "content": [{ "type": "text", "text": "Features" }] },
85
- { "type": "paragraph", "content": [{ "type": "text", "text": "Our main features." }] },
86
- { "type": "heading", "attrs": { "level": 2 }, "content": [{ "type": "text", "text": "Feature One" }] },
87
- { "type": "paragraph", "content": [{ "type": "text", "text": "First feature description." }] },
88
- { "type": "heading", "attrs": { "level": 2 }, "content": [{ "type": "text", "text": "Feature Two" }] },
89
- { "type": "paragraph", "content": [{ "type": "text", "text": "Second feature description." }] }
90
- ]
91
- }
92
- ```
93
-
94
- **Parsed Output::**
95
-
96
- ```
97
- [Main] title: Features
98
- body: Our main features.
99
-
100
- [Item] title: Feature One
101
- body: First feature description.
102
-
103
- [Item] title: Feature Two
104
- body: Second feature description.
105
- ```
106
-
107
- ### 3. The "Hybrid" Pattern (Intro Subtitle + Items)
108
-
109
- - **Structure:** `H1` → `H2 (Leaf)` → `H2 (Branch)`
110
-
111
- - **Use Case:** A section with a summary heading before the list starts.
112
-
113
- - **Behavior:** The parser compares the first H2 (Leaf) against the second H2 (Branch).
114
-
115
- - **Result:** **Merge then Split**. The first H2 merges into Main. The second H2 starts the first Item.
116
-
117
- **Input Data Structure:**
118
-
119
- ```
120
- {
121
- "type": "doc",
122
- "content": [
123
- { "type": "heading", "attrs": { "level": 1 }, "content": [{ "type": "text", "text": "Work History" }] },
124
- { "type": "heading", "attrs": { "level": 2 }, "content": [{ "type": "text", "text": "A summary of my roles." }] },
125
- { "type": "heading", "attrs": { "level": 2 }, "content": [{ "type": "text", "text": "Google" }] },
126
- { "type": "heading", "attrs": { "level": 3 }, "content": [{ "type": "text", "text": "2020-Present" }] },
127
- { "type": "heading", "attrs": { "level": 2 }, "content": [{ "type": "text", "text": "Facebook" }] },
128
- { "type": "heading", "attrs": { "level": 3 }, "content": [{ "type": "text", "text": "2018-2020" }] }
129
- ]
130
- }
131
- ```
132
-
133
- **Parsed Output::**
134
-
135
- ```
136
- [Main] title: Work History
137
- subtitle: "A summary of my roles."
138
-
139
- [Item] title: Google
140
- subtitle: 2020-Present
141
-
142
- [Item] title: Facebook
143
- subtitle: 2018-2020
144
- ```
145
-
146
- ### 4. The "Deep Header" Pattern
147
-
148
- - **Structure:** `H3` → `H1` → `H2` -> `H3`
149
-
150
- - **Use Case:** Complex Hero sections with pre-titles and multiple subtitles.
151
-
152
- - **Behavior:** The headings are strictly sequential or hierarchical components of a single block.
153
-
154
- - **Result:** **Merge then Split**. Treats the hierarchy as a single deep header block.
155
-
156
- **Input Data Structure:**
157
-
158
- ```
159
- {
160
- "type": "doc",
161
- "content": [
162
- { "type": "heading", "attrs": { "level": 3 }, "content": [{ "type": "text", "text": "WELCOME" }] },
163
- { "type": "heading", "attrs": { "level": 1 }, "content": [{ "type": "text", "text": "Main Title" }] },
164
- { "type": "heading", "attrs": { "level": 2 }, "content": [{ "type": "text", "text": "Subtitle" }] },
165
- { "type": "heading", "attrs": { "level": 3 }, "content": [{ "type": "text", "text": "Subsubtitle" }] },
166
- { "type": "paragraph", "content": [{ "type": "text", "text": "Content." }] }
167
- ]
168
- }
169
-
170
- ```
171
-
172
- **Parsed Output::**
173
-
174
- ```
175
- [Main] title: Main Title
176
- pretitle: WELCOME
177
- subtitle: Subtitle
178
- subtitle2: Subsubtitle
179
- ```