@uniweb/semantic-parser 1.9.0 → 1.10.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/AGENTS.md CHANGED
@@ -165,7 +165,7 @@ Key patterns — the staircase rule (each heading relates to the one before it):
165
165
  - **Subtitle**: one step below the title, directly adjacent — and each further one-step descent (or same-size repeat) is another subtitle line. String or array.
166
166
  - **Items**: a heading two or more steps below the previous one, a step back up, or any heading after body content starts a new group.
167
167
  - **Banner Pattern**: an image (with banner role or followed by a heading) at the start of the first group stays with the headline.
168
- - **Dividers**: a `horizontalRule` explicitly closes the current group; it composes with the heading rules rather than replacing them.
168
+ - **Dividers**: a `divider` (`---` in markdown) explicitly closes the current group; it composes with the heading rules rather than replacing them.
169
169
  - **Main Content**: the first group is main if it's the only group, has a more important heading level than the second group, or has no heading at all.
170
170
  - **Body Headings**: `body.headings` fills only from nested content (blockquote children, list items) — a section's headline never spills there.
171
171
 
package/README.md CHANGED
@@ -280,6 +280,24 @@ or `[#id]{k=v}` references. Two artifacts ship to consumers:
280
280
  The two views stay in lockstep — both record every inline inset the
281
281
  parser sees, in source order.
282
282
 
283
+ ### Inline Icons and Pictures
284
+
285
+ An icon or a picture beside text stays in its paragraph, and it is carried
286
+ the same two ways:
287
+
288
+ 1. **`children` carries an `icon` or an `image` entry** — the icon's library
289
+ and name, the picture's resolved `url` and its `alt`.
290
+
291
+ 2. **`text` carries a marker at its position** —
292
+ `<uniweb-icon data-index="N"></uniweb-icon>` or
293
+ `<uniweb-image data-index="N"></uniweb-image>`, where `N` is its ordinal
294
+ among the paragraph's icons, or among its pictures
295
+ (`children.filter(c => c.type === 'image')[N]`).
296
+
297
+ A renderer that reads `text` as HTML and does not know a marker draws
298
+ nothing for it, as for any empty element it does not know. kit's `<Render>`
299
+ draws an icon with `<Icon>` and a picture with `<Image>`.
300
+
283
301
  ## Use Cases
284
302
 
285
303
  - **Component-based websites**: Extract structured data for React/Vue components
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/semantic-parser",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "Semantic parser for ProseMirror/TipTap content structures",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -500,19 +500,10 @@ function processGroupContent(elements, options = {}) {
500
500
  body.media.push({ refId: element.refId, kind: "inset" });
501
501
  break;
502
502
 
503
- case "form":
504
- // Route a form's data by the schema id it targets, so it
505
- // shares a namespace with tagged markdown data blocks
506
- // (```yaml:<id>``` also lands at data[<id>]). When no
507
- // schemaId is present (legacy content), fall back to the
508
- // literal key "form" — eventual deprecation.
509
- {
510
- const payload = element.data || element.attrs;
511
- const key = element.schemaId || "form";
512
- body.data[key] = payload;
513
- }
514
- break;
515
-
503
+ // ⛔ A `form` element was routed here until 2026-10-18 — the
504
+ // editor's form node, its data at `data[<schema id>]`, else at
505
+ // `data.form`. The node is read nowhere now: a form's data is a
506
+ // tagged data block, which lands at `data[<tag>]` above.
516
507
  }
517
508
  }
518
509
  }
@@ -22,6 +22,17 @@ function isIconNode(node) {
22
22
  return node.type === "image" && node.attrs?.role === "icon";
23
23
  }
24
24
 
25
+ /**
26
+ * Is an inline node a picture — an `image` that is not an icon, a video or a
27
+ * file? These are the nodes processInlineElements() delivers as `image`
28
+ * children, so the marker's ordinal (getTextContent) counts exactly them.
29
+ */
30
+ function isInlinePicture(node) {
31
+ if (node?.type !== "image" || isIconNode(node)) return false;
32
+ const role = node.attrs?.role;
33
+ return role !== "video" && role !== "pdf";
34
+ }
35
+
25
36
  /**
26
37
  * Process a ProseMirror/TipTap document into a flat sequence
27
38
  * @param {Object} doc ProseMirror document
@@ -288,44 +299,26 @@ function createSequenceElement(node, options = {}) {
288
299
  return { type: "table", rows, attrs };
289
300
  }
290
301
 
291
- // `divider` is what content-reader emits; `horizontalRule` and
292
- // `DividerBlock` are the editor's spellings. All three are one element.
293
- //
294
302
  // The reader's own name was NOT listed here until 2026-07-30 — markdown
295
303
  // dividers reached consumers only by falling through `default:`, which
296
304
  // happens to produce `{ type: 'divider', content: '' }` and happens to
297
305
  // match what everything downstream keys on. It worked by coincidence,
298
306
  // and the coincidence would have broken the moment `default:` changed.
307
+ //
308
+ // ⭐ The vocabulary's names are the only ones read. ⛔ Until 2026-10-18 the
309
+ // editor's spellings were read too: `DividerBlock` and `horizontalRule` as
310
+ // a divider, and its form node — `StructuredContent`, and `FormBlock`, its
311
+ // name before 2026-07-31 — as a `form` element whose data landed at
312
+ // `content.data[<schema id>]`. The editor writes `divider` and `dataBlock`
313
+ // itself now, and no writer produces the others, as none does its
314
+ // `card-group`, `document-group`, `ImageBlock`, `Video`, `UniwebIcon`,
315
+ // `Icon` and `button` nodes.
299
316
  case "divider":
300
- case "DividerBlock":
301
- case "horizontalRule":
302
317
  return {
303
318
  type: "divider",
304
319
  attrs,
305
320
  };
306
321
 
307
- // The editor's form node. ⛔ Until 2026-10-18 its name before
308
- // 2026-07-31, `FormBlock`, was read too — and the editor's `card-group`,
309
- // `document-group`, `ImageBlock`, `Video`, `UniwebIcon`, `Icon` and
310
- // `button` nodes, which no writer produces now.
311
- case "StructuredContent":
312
- // Parse form data (can be JSON string or object)
313
- let formData = attrs?.data;
314
- if (typeof formData === "string") {
315
- try {
316
- formData = JSON.parse(formData);
317
- } catch (err) {
318
- // Keep as string
319
- }
320
- }
321
-
322
- return {
323
- type: "form",
324
- schemaId: attrs?.activeSchemaId || null,
325
- data: formData,
326
- attrs,
327
- };
328
-
329
322
  default:
330
323
  return {
331
324
  type: node.type,
@@ -424,16 +417,22 @@ function getTextContent(content, options = {}) {
424
417
  // one with `children.filter(c => c.type === 'icon')[index]`. Referencing by
425
418
  // ordinal rather than serializing the attrs keeps an inline `svg` blob out
426
419
  // of the text stream and leaves `children` the single source of truth.
420
+ //
421
+ // A picture beside text is marked the same way, by its ordinal among the
422
+ // pictures of this array — `children.filter(c => c.type === 'image')[index]`.
427
423
  const iconOrdinals = new Map();
424
+ const pictureOrdinals = new Map();
428
425
  let nextIcon = 0;
426
+ let nextPicture = 0;
429
427
  for (const node of content) {
430
428
  if (isIconNode(node)) iconOrdinals.set(node, nextIcon++);
429
+ else if (isInlinePicture(node)) pictureOrdinals.set(node, nextPicture++);
431
430
  }
432
431
 
433
432
  return groups
434
433
  .reduce((prev, group) => {
435
434
  const inner = group.nodes.reduce(
436
- (acc, node) => acc + renderInlineNode(node, iconOrdinals),
435
+ (acc, node) => acc + renderInlineNode(node, iconOrdinals, pictureOrdinals),
437
436
  "",
438
437
  );
439
438
  if (!inner && !group.link) return prev;
@@ -446,7 +445,7 @@ function getTextContent(content, options = {}) {
446
445
  * Render one inline node to HTML, applying every mark EXCEPT link — the
447
446
  * anchor is applied per group by getTextContent, since it may span nodes.
448
447
  */
449
- function renderInlineNode(curr, iconOrdinals) {
448
+ function renderInlineNode(curr, iconOrdinals, pictureOrdinals) {
450
449
  return [curr]
451
450
  .reduce((prev, curr) => {
452
451
  const { type, marks = [], text } = curr;
@@ -454,24 +453,13 @@ function renderInlineNode(curr, iconOrdinals) {
454
453
  if (type === "text") {
455
454
  let styledText = escapeText(text || "");
456
455
 
457
- // Apply marks in order: textStyle, highlight, bold, italic, link
458
- // This ensures proper nesting
459
-
460
- // textStyle (color)
461
- if (marks.some((mark) => mark.type === "textStyle")) {
462
- const color = marks.find(
463
- (mark) => mark.type === "textStyle"
464
- )?.attrs?.color;
465
- // A theme token's name, or nothing — it lands inside `var(--…)`.
466
- if (typeof color === "string" && /^[a-zA-Z0-9-]+$/.test(color)) {
467
- styledText = `<span style="color: var(--${color})">${styledText}</span>`;
468
- }
469
- }
470
-
471
- // highlight
472
- if (marks.some((mark) => mark.type === "highlight")) {
473
- styledText = `<span style="background-color: var(--highlight)">${styledText}</span>`;
474
- }
456
+ // Apply marks in order: span, code, bold, italic — the link is
457
+ // applied per group by getTextContent. This ensures proper nesting.
458
+ //
459
+ // ⛔ Until 2026-10-18 the editor's `textStyle` (a theme color) and
460
+ // `highlight` marks were applied first. The editor saves both as
461
+ // the vocabulary's `span` — `[x]{accent}`, `[x]{highlight}` — so
462
+ // neither is read now.
475
463
 
476
464
  // span (bracketed spans with class/id/attributes)
477
465
  if (marks.some((mark) => mark.type === "span")) {
@@ -574,6 +562,18 @@ function renderInlineNode(curr, iconOrdinals) {
574
562
  const index = iconOrdinals?.get(curr);
575
563
  if (index === undefined) return prev;
576
564
  return prev + `<uniweb-icon data-index="${index}"></uniweb-icon>`;
565
+ } else if (isInlinePicture(curr)) {
566
+ // A picture sitting IN the prose — `as the figure ![](./cells.jpg)
567
+ // shows` — marked for the icon's reason: a renderer draws it with a
568
+ // component (kit's <Image>), from the entry processInlineElements()
569
+ // puts in `children`, its address resolved there.
570
+ //
571
+ // ⛔ Until 2026-10-18 it fell through to the catch-all below, as an
572
+ // icon did before 2026-08-12: the text kept a gap, the picture stayed
573
+ // in `children` and `content.images`, and a walker drew nothing.
574
+ const index = pictureOrdinals?.get(curr);
575
+ if (index === undefined) return prev;
576
+ return prev + `<uniweb-image data-index="${index}"></uniweb-image>`;
577
577
  } else {
578
578
  // console.warn(`unhandled text content type: ${type}`, curr);
579
579
  return prev;