@uniweb/semantic-parser 1.12.1 → 1.13.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/README.md +7 -0
- package/package.json +1 -1
- package/src/processors/groups.js +31 -5
- package/src/processors/sequence.js +48 -17
package/README.md
CHANGED
|
@@ -298,6 +298,13 @@ A renderer that reads `text` as HTML and does not know a marker draws
|
|
|
298
298
|
nothing for it, as for any empty element it does not know. kit's `<Render>`
|
|
299
299
|
draws an icon with `<Icon>` and a picture with `<Image>`.
|
|
300
300
|
|
|
301
|
+
An icon **before any text** is the element's own icon, not part of its prose:
|
|
302
|
+
`` on a line of its own, `-  Web App`, or
|
|
303
|
+
`##  Fast`. The flat fields (`title`, `paragraphs`, a list item's)
|
|
304
|
+
leave out its marker, since a component draws that icon from `icons`, beside
|
|
305
|
+
the words. So a line holding only icons adds no paragraph. The `sequence`
|
|
306
|
+
keeps every marker, for a renderer that draws each icon where it sat.
|
|
307
|
+
|
|
301
308
|
## Use Cases
|
|
302
309
|
|
|
303
310
|
- **Component-based websites**: Extract structured data for React/Vue components
|
package/package.json
CHANGED
package/src/processors/groups.js
CHANGED
|
@@ -286,6 +286,29 @@ function readStack(sequence, startIdx, options = {}) {
|
|
|
286
286
|
return { pre, title, sub, length: i - startIdx };
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
/**
|
|
290
|
+
* An element's text as a flat field holds it, without the markers of the icons before its words.
|
|
291
|
+
*
|
|
292
|
+
* ⭐ An icon before any text is the element's own icon, not part of its prose: `` on
|
|
293
|
+
* a line of its own, `-  Web App`, `##  Fast`. The flat fields (`title`,
|
|
294
|
+
* `paragraphs`, a list item's) hold the text a component draws beside that icon, which it finds
|
|
295
|
+
* in `icons`. So the markers before the words go, and a line holding only icons leaves no
|
|
296
|
+
* paragraph. An icon inside the prose keeps its marker, with its ordinal unchanged, since `icons`
|
|
297
|
+
* still lists the leading ones. The `sequence` keeps every marker: kit's `Render` draws each icon
|
|
298
|
+
* where it sat [Diego, 2026-10-24].
|
|
299
|
+
*
|
|
300
|
+
* ⛔ From 2026-08-12 to 2026-10-24 the flat fields kept the leading markers. An icon-only line
|
|
301
|
+
* became a paragraph of nothing but `<uniweb-icon data-index="0"></uniweb-icon>`, which moved
|
|
302
|
+
* `paragraphs[0]` off the text, and a component drawing a paragraph as text showed the markup.
|
|
303
|
+
*
|
|
304
|
+
* @param {*} text
|
|
305
|
+
* @returns {*} the text without its leading icon markers; anything that is no string, as it came
|
|
306
|
+
*/
|
|
307
|
+
function flatText(text) {
|
|
308
|
+
if (typeof text !== "string") return text;
|
|
309
|
+
return text.replace(/^(?:\s*<uniweb-icon data-index="\d+"><\/uniweb-icon>)+\s*/, "");
|
|
310
|
+
}
|
|
311
|
+
|
|
289
312
|
/** Collapse a slot's texts: none → '', one → string, several → array. */
|
|
290
313
|
function flattenSlot(texts) {
|
|
291
314
|
if (texts.length === 0) return "";
|
|
@@ -346,9 +369,9 @@ function processGroupContent(elements, options = {}) {
|
|
|
346
369
|
let start = 0;
|
|
347
370
|
if (elements[0]?.type === "heading") {
|
|
348
371
|
const stack = readStack(elements, 0, options);
|
|
349
|
-
header.pretitle = flattenSlot(stack.pre.map((el) => el.text));
|
|
350
|
-
header.title = flattenSlot(stack.title.map((el) => el.text));
|
|
351
|
-
header.subtitle = flattenSlot(stack.sub.map((el) => el.text));
|
|
372
|
+
header.pretitle = flattenSlot(stack.pre.map((el) => flatText(el.text)));
|
|
373
|
+
header.title = flattenSlot(stack.title.map((el) => flatText(el.text)));
|
|
374
|
+
header.subtitle = flattenSlot(stack.sub.map((el) => flatText(el.text)));
|
|
352
375
|
metadata.level = stack.title[0]?.level ?? null;
|
|
353
376
|
for (const el of [...stack.pre, ...stack.title, ...stack.sub]) {
|
|
354
377
|
if (el.children && Array.isArray(el.children))
|
|
@@ -366,7 +389,7 @@ function processGroupContent(elements, options = {}) {
|
|
|
366
389
|
// array — section content is split into groups before this runs.
|
|
367
390
|
if (element.children && Array.isArray(element.children))
|
|
368
391
|
processInlineElements(element.children, body);
|
|
369
|
-
body.headings.push(element.text);
|
|
392
|
+
body.headings.push(flatText(element.text));
|
|
370
393
|
} else if (element.type === "list") {
|
|
371
394
|
const listItems = element.children;
|
|
372
395
|
|
|
@@ -383,7 +406,10 @@ function processGroupContent(elements, options = {}) {
|
|
|
383
406
|
if (element.children && Array.isArray(element.children))
|
|
384
407
|
processInlineElements(element.children, body);
|
|
385
408
|
|
|
386
|
-
|
|
409
|
+
{
|
|
410
|
+
const flat = flatText(element.text);
|
|
411
|
+
if (flat) body.paragraphs.push(flat);
|
|
412
|
+
}
|
|
387
413
|
break;
|
|
388
414
|
|
|
389
415
|
case "image":
|
|
@@ -79,7 +79,7 @@ function createSequenceElement(node, options = {}) {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
// Check for paragraph containing only multiple links (no other text)
|
|
82
|
-
const multipleLinks = isOnlyLinks(node);
|
|
82
|
+
const multipleLinks = isOnlyLinks(node, options);
|
|
83
83
|
if (multipleLinks && multipleLinks.every((l) => isSafeLink(l.attrs?.href))) {
|
|
84
84
|
return multipleLinks; // Returns array of link elements
|
|
85
85
|
}
|
|
@@ -612,8 +612,20 @@ function processInlineElements(content, options = {}) {
|
|
|
612
612
|
if (!content) return [];
|
|
613
613
|
|
|
614
614
|
const items = [];
|
|
615
|
+
// The link the previous run belongs to, while the runs continue it: a label
|
|
616
|
+
// in several styles (`[the *full* guide](/guide)`) is one link, as its anchor
|
|
617
|
+
// is. ⛔ Until 2026-10-19 each run was a link here, so `content.links` held
|
|
618
|
+
// "the ", "full" and " guide".
|
|
619
|
+
let open = null;
|
|
615
620
|
|
|
616
621
|
for (const item of content) {
|
|
622
|
+
const runLink = item.type === "text" ? linkMarkOf(item.marks) : null;
|
|
623
|
+
if (runLink && open && sameLink(open.mark, runLink)) {
|
|
624
|
+
open.link.attrs.label += item.text || "";
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
open = null;
|
|
628
|
+
|
|
617
629
|
if (isIconNode(item)) {
|
|
618
630
|
items.push({
|
|
619
631
|
type: "icon",
|
|
@@ -651,18 +663,17 @@ function processInlineElements(content, options = {}) {
|
|
|
651
663
|
refId: item.attrs?.refId,
|
|
652
664
|
embedKind: item.attrs?.embedKind || "visual",
|
|
653
665
|
});
|
|
654
|
-
} else if (
|
|
666
|
+
} else if (runLink && isSafeLink(runLink.attrs?.href)) {
|
|
655
667
|
// Extract links from text nodes with link marks
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
}
|
|
668
|
+
const link = {
|
|
669
|
+
type: "link",
|
|
670
|
+
attrs: {
|
|
671
|
+
...runLink.attrs, // Preserve all link attributes (role, target, etc.)
|
|
672
|
+
label: item.text || "",
|
|
673
|
+
},
|
|
674
|
+
};
|
|
675
|
+
items.push(link);
|
|
676
|
+
open = { mark: runLink, link };
|
|
666
677
|
}
|
|
667
678
|
}
|
|
668
679
|
|
|
@@ -1045,9 +1056,10 @@ function isLink(item, options = {}) {
|
|
|
1045
1056
|
* Markdown treats these as a single paragraph, but semantically they're separate links.
|
|
1046
1057
|
*
|
|
1047
1058
|
* @param {Object} item - Sequence item (paragraph)
|
|
1059
|
+
* @param {Object} [options] - Parse options, for the one-link case's `children`
|
|
1048
1060
|
* @returns {Array|false} Array of link objects or false
|
|
1049
1061
|
*/
|
|
1050
|
-
function isOnlyLinks(item) {
|
|
1062
|
+
function isOnlyLinks(item, options = {}) {
|
|
1051
1063
|
if (item.type !== "paragraph") return false;
|
|
1052
1064
|
|
|
1053
1065
|
const content = item?.content || [];
|
|
@@ -1076,22 +1088,41 @@ function isOnlyLinks(item) {
|
|
|
1076
1088
|
// break becomes, an icon — keeps them apart. ⛔ Until 2026-10-19 every run
|
|
1077
1089
|
// was a link of its own, so a button labelled in two styles became two
|
|
1078
1090
|
// buttons, "Go" and " now".
|
|
1079
|
-
// Icons in this paragraph go to body.icons separately (no association)
|
|
1080
1091
|
const groups = [];
|
|
1081
1092
|
let current = null;
|
|
1082
|
-
|
|
1093
|
+
content.forEach((c, i) => {
|
|
1083
1094
|
const mark = c.type === "text" ? linkMarkOf(c.marks) : null;
|
|
1084
1095
|
if (mark && current && sameLink(current.mark, mark)) {
|
|
1085
1096
|
current.label += c.text || "";
|
|
1097
|
+
current.last = i;
|
|
1086
1098
|
} else if (mark) {
|
|
1087
|
-
current = { mark, label: c.text || "" };
|
|
1099
|
+
current = { mark, label: c.text || "", first: i, last: i };
|
|
1088
1100
|
groups.push(current);
|
|
1089
1101
|
} else {
|
|
1090
1102
|
current = null;
|
|
1091
1103
|
}
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
// One link in several runs is a paragraph that is just a link, and takes the
|
|
1107
|
+
// shape `isLink` gives one — its icons, the last before it and the first
|
|
1108
|
+
// after, and its inline elements. ⛔ Until 2026-10-19 its icons were dropped:
|
|
1109
|
+
// ` [**Go** now](/go){button}` lost the star.
|
|
1110
|
+
if (groups.length === 1) {
|
|
1111
|
+
const [{ mark, label, first, last }] = groups;
|
|
1112
|
+
const icons = content.map((c, i) => (isIconNode(c) ? { i, attrs: parseIcon(c.attrs) } : null)).filter(Boolean);
|
|
1113
|
+
return [{
|
|
1114
|
+
type: "link",
|
|
1115
|
+
attrs: {
|
|
1116
|
+
...mark?.attrs,
|
|
1117
|
+
label,
|
|
1118
|
+
iconBefore: icons.filter(({ i }) => i < first).pop()?.attrs || null,
|
|
1119
|
+
iconAfter: icons.find(({ i }) => i > last)?.attrs || null,
|
|
1120
|
+
children: processInlineElements(content, options),
|
|
1121
|
+
},
|
|
1122
|
+
}];
|
|
1092
1123
|
}
|
|
1093
1124
|
|
|
1094
|
-
//
|
|
1125
|
+
// Several links. An icon among them belongs to no one link, and is not carried.
|
|
1095
1126
|
return groups.map(({ mark, label }) => ({
|
|
1096
1127
|
type: "link",
|
|
1097
1128
|
attrs: {
|