@uniweb/semantic-parser 1.1.9 → 1.1.11

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.9",
3
+ "version": "1.1.11",
4
4
  "description": "Semantic parser for ProseMirror/TipTap content structures",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -5,7 +5,7 @@
5
5
  */
6
6
  function flattenGroup(group) {
7
7
  if (!group) return null;
8
- return {
8
+ const flat = {
9
9
  title: group.header.title || '',
10
10
  pretitle: group.header.pretitle || '',
11
11
  subtitle: group.header.subtitle || '',
@@ -21,6 +21,11 @@ function flattenGroup(group) {
21
21
  quotes: group.body.quotes || [],
22
22
  headings: group.body.headings || [],
23
23
  };
24
+ // Only attach rare collections when present so we don't pay the cost
25
+ // on every page (math is <1% of content in practice; authors who need
26
+ // ordering should use content.sequence anyway).
27
+ if (group.body.math && group.body.math.length) flat.math = group.body.math;
28
+ return flat;
24
29
  }
25
30
 
26
31
  /**
@@ -240,6 +245,9 @@ function processGroupContent(elements) {
240
245
  data: {},
241
246
  quotes: [],
242
247
  headings: [],
248
+ // `math` is lazily added in the math_display case when encountered —
249
+ // avoids allocating an empty array on every group (math is rare; most
250
+ // consumers that care about ordering should use content.sequence).
243
251
  };
244
252
 
245
253
  const metadata = {
@@ -370,13 +378,38 @@ function processGroupContent(elements) {
370
378
  });
371
379
  break;
372
380
 
381
+ case "math_display":
382
+ // Block-level math from $$...$$ on its own line or ```math
383
+ // fence. The mathml string is pre-compiled at parse time;
384
+ // foundations render it by setting dangerouslySetInnerHTML
385
+ // on the mathml field. The optional id enables numbered
386
+ // cross-references (see @uniweb/scholar/math).
387
+ // For ordered rendering alongside paragraphs, prefer
388
+ // `content.sequence` — this flat collection is convenience
389
+ // sugar when order doesn't matter. Allocate lazily so
390
+ // pages without math pay nothing.
391
+ (body.math ||= []).push({
392
+ id: element.id || null,
393
+ latex: element.latex || '',
394
+ mathml: element.mathml || '',
395
+ });
396
+ break;
397
+
373
398
  case "inset":
374
399
  body.insets.push({ refId: element.refId });
375
400
  break;
376
401
 
377
402
  case "form":
378
- // Map FormBlock to data.form
379
- body.data.form = element.data || element.attrs;
403
+ // Route FormBlock data by the schema id it targets, so it
404
+ // shares a namespace with tagged markdown data blocks
405
+ // (```yaml:<id>``` also lands at data[<id>]). When no
406
+ // schemaId is present (legacy content), fall back to the
407
+ // literal key "form" — eventual deprecation.
408
+ {
409
+ const payload = element.data || element.attrs;
410
+ const key = element.schemaId || "form";
411
+ body.data[key] = payload;
412
+ }
380
413
  break;
381
414
 
382
415
  case "card-group":
@@ -167,16 +167,33 @@ function createSequenceElement(node, options = {}) {
167
167
  type: "inset",
168
168
  refId: attrs.refId,
169
169
  };
170
+
171
+ case "math_display":
172
+ // Block-level math. The mathml string is pre-compiled at parse
173
+ // time (content-reader) and renders natively via kit's HTML
174
+ // renderers. Foundations can read el.mathml from content.sequence.
175
+ // The optional id enables numbered cross-references via
176
+ // @uniweb/scholar/math's <EquationRef>.
177
+ return {
178
+ type: "math_display",
179
+ id: node.attrs?.id || null,
180
+ latex: node.attrs?.latex || "",
181
+ mathml: node.attrs?.mathml || "",
182
+ attrs,
183
+ };
170
184
  case "ImageBlock":
171
185
  return {
172
186
  type: "image",
173
187
  attrs: parseImgBlock(attrs),
174
188
  };
175
189
  case "image":
176
- // Standard ProseMirror image node - wrap attrs like ImageBlock
190
+ // Standard ProseMirror image node - resolve attrs the same way as
191
+ // ImageBlock so info.identifier lands as a CDN URL. CLI-deployed
192
+ // content uses this node type; without parseImgBlock the url
193
+ // wouldn't be populated and components render an empty slot.
177
194
  return {
178
195
  type: "image",
179
- attrs: attrs || {},
196
+ attrs: parseImgBlock(attrs || {}),
180
197
  };
181
198
  case "Video":
182
199
  return {
@@ -241,6 +258,7 @@ function createSequenceElement(node, options = {}) {
241
258
 
242
259
  return {
243
260
  type: "form",
261
+ schemaId: attrs?.activeSchemaId || null,
244
262
  data: formData,
245
263
  attrs,
246
264
  };
@@ -380,6 +398,12 @@ function getTextContent(content, options = {}) {
380
398
  }
381
399
 
382
400
  return prev + styledText;
401
+ } else if (type === "math_inline") {
402
+ // Pre-compiled MathML string rides straight into the
403
+ // paragraph's HTML and reaches kit's Text via
404
+ // dangerouslySetInnerHTML. The HTML5 parser handles <math>
405
+ // as MathML natively — zero runtime math library needed.
406
+ return prev + (curr.attrs?.mathml || "");
383
407
  } else if (type === "hardBreak") {
384
408
  return prev + "<br>";
385
409
  } else {