@uniweb/semantic-parser 1.1.8 → 1.1.10

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 CHANGED
@@ -95,12 +95,15 @@ The `data` object holds all structured content:
95
95
  data: {
96
96
  "nav-links": [...], // From ```yaml:nav-links
97
97
  "config": {...}, // From ```yaml:config
98
- "form": {...}, // From FormBlock editor widget or ```yaml:form
98
+ "stats": [...], // From FormBlock (activeSchemaId='stats') or ```yaml:stats
99
99
  "person": [...], // From card-group with cardType="person"
100
100
  "event": [...] // From card-group with cardType="event"
101
101
  }
102
102
  ```
103
103
 
104
+ FormBlock data is routed to `data[activeSchemaId]`. A legacy
105
+ FormBlock without a `schemaId` still lands at `data.form`.
106
+
104
107
  ### Main Content Identification
105
108
 
106
109
  The `identifyMainContent()` function (src/processors/groups.js:282) determines if the first group should be treated as main content:
@@ -121,7 +124,7 @@ The sequence processor identifies several special element types by inspecting pa
121
124
 
122
125
  Editor-specific nodes are mapped to standard entities:
123
126
  - `button` node → `links[]` with `role: "button"` and `variant` attribute
124
- - `FormBlock` → `data.form`
127
+ - `FormBlock` → `data[activeSchemaId]` (fallback: `data.form` when no schemaId)
125
128
  - `card-group` → `data[cardType]` arrays (e.g., `data.person`, `data.event`)
126
129
  - `document-group` → `links[]` with `role: "document"` and `download: true`
127
130
 
package/README.md CHANGED
@@ -77,13 +77,18 @@ result = {
77
77
  icons: [],
78
78
  lists: [],
79
79
  quotes: [],
80
+ insets: [], // Inline @Component references — { refId }
81
+ snippets: [], // Fenced code blocks — { language, code }
80
82
  data: {}, // Structured data (tagged data blocks, forms, cards)
81
83
  headings: [], // Headings after subtitle, in document order
82
84
 
83
85
  // Additional content groups (from headings after content)
86
+ // Each item has the SAME flat structure as the top level — title,
87
+ // pretitle, subtitle, paragraphs, links, images, icons, lists,
88
+ // snippets, data, etc. Use this for cards, features, FAQ entries.
84
89
  items: [
85
- { title: "Feature 1", paragraphs: [...], links: [...] },
86
- { title: "Feature 2", paragraphs: [...], links: [...] }
90
+ { title: "Feature 1", paragraphs: [...], links: [...], /* ...full shape */ },
91
+ { title: "Feature 2", paragraphs: [...], links: [...], /* ...full shape */ }
87
92
  ],
88
93
 
89
94
  // Ordered sequence for document-order rendering
@@ -144,28 +149,53 @@ sequence.forEach(element => {
144
149
 
145
150
  ## Content Grouping
146
151
 
147
- The parser supports two grouping modes:
152
+ The parser interprets heading levels **relatively**, not absolutely. There is no requirement that a section start with H1 — what matters is the relationship *between* headings (which is more important, which is deeper, which comes after body content). Starting at H1 is natural and recommended, but the same structure works starting at H2 or H3 — every rule below is about relative levels.
148
153
 
149
- ### Heading-Based Grouping (Default)
154
+ ### How groups are formed
150
155
 
151
- Groups are created based on heading patterns. A new group starts when:
152
- - A heading follows content
153
- - Multiple H1s appear (no main content created)
154
- - The heading level indicates a new section
156
+ A new group is started whenever a heading appears after non-heading content (paragraphs, images, links, lists, etc.). A horizontal rule (`---`) explicitly closes the current group.
155
157
 
156
- **Heading group formation:** Consecutive headings are consumed as a single semantic unit only when each subsequent heading is _exactly one level deeper_ than the previous (e.g., H1→H2→H3). Skipping levels (H1→H3) breaks the group — the deeper heading starts a new group instead of becoming a subtitle. This reflects natural document structure: a large level gap signals items, not a title-subtitle pair.
158
+ When a group begins with one or more headings, the parser consumes a contiguous **heading block** following these rules:
157
159
 
158
- **Pretitle Detection:** Any heading followed by a more important heading is automatically detected as a pretitle:
159
- - H3 before H1 → pretitle ✅
160
- - H2 before H1 → pretitle ✅
161
- - H6 before H5 → pretitle ✅
162
- - H4 before H2 → pretitle ✅
160
+ 1. **Adjacent deeper** — the next heading is exactly one level deeper (e.g., H1→H2, H2→H3). It becomes the `subtitle`. Skipping levels (H1→H3) breaks the block; the deeper heading starts a new group as an item.
161
+ 2. **Pretitle promotion** — if the very first heading is followed by a *more important* (lower-numbered) heading, the first one becomes `pretitle` and the next becomes `title`. This only applies at the start of the block.
162
+ 3. **Same-level continuation** — consecutive headings at the same level merge into an array (multi-line title or subtitle). This stops once a deeper level has been reached: after going deeper, a same-level heading starts a new group instead of merging.
163
163
 
164
- No configuration needed - it just works naturally!
164
+ Anything else (going back up after going deeper, or any other gap) breaks the block.
165
165
 
166
- ### Divider-Based Grouping
166
+ ### Main content vs items
167
167
 
168
- When any horizontal rule (`---`) is present, the entire document uses divider-based grouping. Groups are split explicitly by dividers.
168
+ Once groups are split, the parser decides whether the first group is the section's main content or whether *all* groups should be treated as items. The first group becomes main content when **either**:
169
+
170
+ - It has no heading at all (body content appearing before the first heading), **or**
171
+ - Its heading level is more important (lower-numbered) than the second group's heading level.
172
+
173
+ Otherwise, every group becomes an item and the top-level header fields stay empty. For example, a document that starts with `## Card A` followed by `## Card B` produces two items and no main title — because the first group is not more important than the second. The same is true for two H1s in a row, or two H3s; the rule is about *relative* importance, not a specific level.
174
+
175
+ This is the mechanism that creates repeating content groups (cards, features, FAQ entries) without any extra configuration.
176
+
177
+ ### Pretitle detection
178
+
179
+ A heading followed by a more important heading at the start of a group is detected as `pretitle`:
180
+
181
+ - H3 before H1 → pretitle
182
+ - H2 before H1 → pretitle
183
+ - H6 before H5 → pretitle
184
+ - H4 before H2 → pretitle
185
+
186
+ Pretitle is detected only between the first two headings of a heading block (before the title is set).
187
+
188
+ ### Banner image
189
+
190
+ If a section's very first element is an image (or an image followed immediately by a heading), the parser keeps the image and the following heading in the same group rather than splitting them. This lets a leading image act as a banner for the section's title without becoming an unrelated item.
191
+
192
+ ### Divider-based grouping
193
+
194
+ A horizontal rule (`---`) explicitly closes the current group and starts a new one. Dividers compose with the heading rules above — they don't replace them — and are useful in three situations:
195
+
196
+ - **Resolving ambiguity.** When the heading rules would group content one way but you want it grouped another way. Most common case: forcing items without a subtitle. Without a divider, `# Our Stats` followed by `## 15,000+` makes `## 15,000+` the subtitle (one level deeper). Adding `---` between them closes the title group so `## 15,000+` becomes `items[0].title` instead.
197
+ - **Forcing splits within same-level runs.** Same-level headings that would otherwise merge into a multi-line title/subtitle become separate groups when separated by a divider.
198
+ - **Personal preference.** Even when the heading rules would already produce the desired structure, authors can use `---` as an explicit visual separator between groups. It never changes a structure that's already correct — it just makes the boundary obvious in the markdown source.
169
199
 
170
200
  ## Text Formatting
171
201
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/semantic-parser",
3
- "version": "1.1.8",
3
+ "version": "1.1.10",
4
4
  "description": "Semantic parser for ProseMirror/TipTap content structures",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -364,17 +364,10 @@ function processGroupContent(elements) {
364
364
  break;
365
365
 
366
366
  case "codeBlock":
367
- const tag = element.attrs?.tag;
368
- if (tag) {
369
- // Tagged block where parsing failed at build time — store as data
370
- body.data[tag] = element.text;
371
- } else {
372
- // Untagged code block — collect as a snippet
373
- body.snippets.push({
374
- language: element.attrs?.language || '',
375
- code: typeof element.text === 'string' ? element.text : '',
376
- });
377
- }
367
+ body.snippets.push({
368
+ language: element.attrs?.language || '',
369
+ code: typeof element.text === 'string' ? element.text : '',
370
+ });
378
371
  break;
379
372
 
380
373
  case "inset":
@@ -382,8 +375,16 @@ function processGroupContent(elements) {
382
375
  break;
383
376
 
384
377
  case "form":
385
- // Map FormBlock to data.form
386
- body.data.form = element.data || element.attrs;
378
+ // Route FormBlock data by the schema id it targets, so it
379
+ // shares a namespace with tagged markdown data blocks
380
+ // (```yaml:<id>``` also lands at data[<id>]). When no
381
+ // schemaId is present (legacy content), fall back to the
382
+ // literal key "form" — eventual deprecation.
383
+ {
384
+ const payload = element.data || element.attrs;
385
+ const key = element.schemaId || "form";
386
+ body.data[key] = payload;
387
+ }
387
388
  break;
388
389
 
389
390
  case "card-group":
@@ -145,13 +145,22 @@ function createSequenceElement(node, options = {}) {
145
145
  tag: attrs.tag,
146
146
  };
147
147
 
148
- case "codeBlock":
148
+ case "codeBlock": {
149
149
  const codeText = getTextContent(content, options);
150
+ // Tagged code blocks are semantically data blocks, not code
151
+ if (attrs?.tag) {
152
+ return {
153
+ type: "dataBlock",
154
+ tag: attrs.tag,
155
+ data: getCodeBlockData(codeText, attrs),
156
+ };
157
+ }
150
158
  return {
151
159
  type: "codeBlock",
152
- text: getCodeBlockData(codeText, attrs),
160
+ text: codeText,
153
161
  attrs,
154
162
  };
163
+ }
155
164
 
156
165
  case "inset_placeholder":
157
166
  return {
@@ -232,6 +241,7 @@ function createSequenceElement(node, options = {}) {
232
241
 
233
242
  return {
234
243
  type: "form",
244
+ schemaId: attrs?.activeSchemaId || null,
235
245
  data: formData,
236
246
  attrs,
237
247
  };