@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 +1 -1
- package/README.md +18 -0
- package/package.json +1 -1
- package/src/processors/groups.js +4 -13
- package/src/processors/sequence.js +47 -47
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 `
|
|
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
package/src/processors/groups.js
CHANGED
|
@@ -500,19 +500,10 @@ function processGroupContent(elements, options = {}) {
|
|
|
500
500
|
body.media.push({ refId: element.refId, kind: "inset" });
|
|
501
501
|
break;
|
|
502
502
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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:
|
|
458
|
-
// This ensures proper nesting
|
|
459
|
-
|
|
460
|
-
// textStyle (color)
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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 
|
|
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;
|