@uniweb/semantic-parser 1.12.0 → 1.12.2
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/package.json +1 -1
- package/src/processors/sequence.js +105 -30
package/package.json
CHANGED
|
@@ -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
|
}
|
|
@@ -161,7 +161,7 @@ function createSequenceElement(node, options = {}) {
|
|
|
161
161
|
};
|
|
162
162
|
|
|
163
163
|
case "codeBlock": {
|
|
164
|
-
const codeText =
|
|
164
|
+
const codeText = codeTextOf(content);
|
|
165
165
|
// Tagged code blocks are semantically data blocks, not code — the
|
|
166
166
|
// tag is what routes them to `data[tag]`, so the routing holds even
|
|
167
167
|
// though the payload here is raw text.
|
|
@@ -374,22 +374,43 @@ function sameLink(a, b) {
|
|
|
374
374
|
/**
|
|
375
375
|
* Wrap already-styled inline content in an anchor.
|
|
376
376
|
*/
|
|
377
|
+
/**
|
|
378
|
+
* A code block's code, as written — the characters a reader sees, not HTML.
|
|
379
|
+
*
|
|
380
|
+
* Every consumer draws it as text: kit's `Code` (and the highlighter, which
|
|
381
|
+
* escapes as it colors), press's `CodeBlock`, a foundation's `{snippet.code}`.
|
|
382
|
+
* ⛔ Until 2026-10-19 it went through `getTextContent`, which escapes text for
|
|
383
|
+
* the HTML strings a paragraph carries, so a `<` arrived as `<`, was escaped
|
|
384
|
+
* again where it was drawn, and a page showed `<p>` for `<p>`.
|
|
385
|
+
*/
|
|
386
|
+
function codeTextOf(content) {
|
|
387
|
+
return (content || [])
|
|
388
|
+
.map((node) => (node?.type === "text" ? node.text || "" : node?.type === "hardBreak" ? "\n" : ""))
|
|
389
|
+
.join("");
|
|
390
|
+
}
|
|
391
|
+
|
|
377
392
|
function wrapLink(inner, linkMark) {
|
|
378
|
-
return anchor(linkMark.attrs.href, linkMark.attrs.target, inner);
|
|
393
|
+
return anchor(linkMark.attrs.href, linkMark.attrs.target, inner, linkMark.attrs.role);
|
|
379
394
|
}
|
|
380
395
|
|
|
381
396
|
/**
|
|
382
397
|
* An anchor around already-escaped inner HTML. A link whose URL may not be
|
|
383
398
|
* followed (`javascript:`, `data:`, any scheme `isSafeLink` refuses) is not
|
|
384
399
|
* drawn as a link: its label stays, as text.
|
|
400
|
+
*
|
|
401
|
+
* Its `role` — `button` for a `[Go](/x){button}`, or an authored hint such as
|
|
402
|
+
* `nav` — rides as `data-role`, a style name or nothing, so a stylesheet can
|
|
403
|
+
* tell a button from a link. ⛔ Until 2026-10-19 the role was dropped here, and
|
|
404
|
+
* a button reached the page as a plain link.
|
|
385
405
|
*/
|
|
386
|
-
function anchor(href, target, inner) {
|
|
406
|
+
function anchor(href, target, inner, role) {
|
|
387
407
|
if (typeof href !== "string" || !isSafeLink(href)) return inner;
|
|
388
408
|
const t = isLinkTarget(target) ? target : "_self";
|
|
389
409
|
const extension = href.split(".").pop()?.toLowerCase();
|
|
390
410
|
const isFileLink = FILE_LINK_EXTENSIONS.includes(extension);
|
|
411
|
+
const roleAttr = isStyleName(role) ? ` data-role="${escapeAttr(role)}"` : "";
|
|
391
412
|
|
|
392
|
-
return `<a href="${escapeAttr(href)}" target="${t}"${isFileLink ? " download" : ""}>${inner}</a>`;
|
|
413
|
+
return `<a href="${escapeAttr(href)}" target="${t}"${isFileLink ? " download" : ""}${roleAttr}>${inner}</a>`;
|
|
393
414
|
}
|
|
394
415
|
|
|
395
416
|
function getTextContent(content, options = {}) {
|
|
@@ -453,8 +474,8 @@ function renderInlineNode(curr, iconOrdinals, pictureOrdinals) {
|
|
|
453
474
|
if (type === "text") {
|
|
454
475
|
let styledText = escapeText(text || "");
|
|
455
476
|
|
|
456
|
-
// Apply marks in order: span, code, bold, italic — the link
|
|
457
|
-
// applied per group by getTextContent. This ensures proper nesting.
|
|
477
|
+
// Apply marks in order: span, code, bold, italic, strike — the link
|
|
478
|
+
// is applied per group by getTextContent. This ensures proper nesting.
|
|
458
479
|
//
|
|
459
480
|
// ⛔ Until 2026-10-18 the editor's `textStyle` (a theme color) and
|
|
460
481
|
// `highlight` marks were applied first. The editor saves both as
|
|
@@ -513,6 +534,12 @@ function renderInlineNode(curr, iconOrdinals, pictureOrdinals) {
|
|
|
513
534
|
styledText = `<em>${styledText}</em>`;
|
|
514
535
|
}
|
|
515
536
|
|
|
537
|
+
// strike — `~~text~~`. ⛔ Until 2026-10-19 nothing applied the
|
|
538
|
+
// mark, so struck text reached the page as ordinary text.
|
|
539
|
+
if (marks.some((mark) => mark.type === "strike")) {
|
|
540
|
+
styledText = `<s>${styledText}</s>`;
|
|
541
|
+
}
|
|
542
|
+
|
|
516
543
|
// The link mark is deliberately NOT applied here — it is the
|
|
517
544
|
// outermost wrapper and may span several nodes, so
|
|
518
545
|
// getTextContent applies it once per group.
|
|
@@ -585,8 +612,20 @@ function processInlineElements(content, options = {}) {
|
|
|
585
612
|
if (!content) return [];
|
|
586
613
|
|
|
587
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;
|
|
588
620
|
|
|
589
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
|
+
|
|
590
629
|
if (isIconNode(item)) {
|
|
591
630
|
items.push({
|
|
592
631
|
type: "icon",
|
|
@@ -624,18 +663,17 @@ function processInlineElements(content, options = {}) {
|
|
|
624
663
|
refId: item.attrs?.refId,
|
|
625
664
|
embedKind: item.attrs?.embedKind || "visual",
|
|
626
665
|
});
|
|
627
|
-
} else if (
|
|
666
|
+
} else if (runLink && isSafeLink(runLink.attrs?.href)) {
|
|
628
667
|
// Extract links from text nodes with link marks
|
|
629
|
-
const
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
}
|
|
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 };
|
|
639
677
|
}
|
|
640
678
|
}
|
|
641
679
|
|
|
@@ -1018,9 +1056,10 @@ function isLink(item, options = {}) {
|
|
|
1018
1056
|
* Markdown treats these as a single paragraph, but semantically they're separate links.
|
|
1019
1057
|
*
|
|
1020
1058
|
* @param {Object} item - Sequence item (paragraph)
|
|
1059
|
+
* @param {Object} [options] - Parse options, for the one-link case's `children`
|
|
1021
1060
|
* @returns {Array|false} Array of link objects or false
|
|
1022
1061
|
*/
|
|
1023
|
-
function isOnlyLinks(item) {
|
|
1062
|
+
function isOnlyLinks(item, options = {}) {
|
|
1024
1063
|
if (item.type !== "paragraph") return false;
|
|
1025
1064
|
|
|
1026
1065
|
const content = item?.content || [];
|
|
@@ -1043,18 +1082,54 @@ function isOnlyLinks(item) {
|
|
|
1043
1082
|
|
|
1044
1083
|
if (!allLinks) return false;
|
|
1045
1084
|
|
|
1046
|
-
//
|
|
1047
|
-
//
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1085
|
+
// One link per anchor, as `getTextContent` draws them: consecutive runs that
|
|
1086
|
+
// carry the same link are ONE link whose label has several runs
|
|
1087
|
+
// (`[**Go** now](/go)`), and anything between two links — the space a line
|
|
1088
|
+
// break becomes, an icon — keeps them apart. ⛔ Until 2026-10-19 every run
|
|
1089
|
+
// was a link of its own, so a button labelled in two styles became two
|
|
1090
|
+
// buttons, "Go" and " now".
|
|
1091
|
+
const groups = [];
|
|
1092
|
+
let current = null;
|
|
1093
|
+
content.forEach((c, i) => {
|
|
1094
|
+
const mark = c.type === "text" ? linkMarkOf(c.marks) : null;
|
|
1095
|
+
if (mark && current && sameLink(current.mark, mark)) {
|
|
1096
|
+
current.label += c.text || "";
|
|
1097
|
+
current.last = i;
|
|
1098
|
+
} else if (mark) {
|
|
1099
|
+
current = { mark, label: c.text || "", first: i, last: i };
|
|
1100
|
+
groups.push(current);
|
|
1101
|
+
} else {
|
|
1102
|
+
current = null;
|
|
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 [{
|
|
1051
1114
|
type: "link",
|
|
1052
1115
|
attrs: {
|
|
1053
|
-
...
|
|
1054
|
-
label
|
|
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),
|
|
1055
1121
|
},
|
|
1056
|
-
};
|
|
1057
|
-
}
|
|
1122
|
+
}];
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// Several links. An icon among them belongs to no one link, and is not carried.
|
|
1126
|
+
return groups.map(({ mark, label }) => ({
|
|
1127
|
+
type: "link",
|
|
1128
|
+
attrs: {
|
|
1129
|
+
...mark?.attrs, // Preserve all link attributes (role, target, etc.)
|
|
1130
|
+
label,
|
|
1131
|
+
},
|
|
1132
|
+
}));
|
|
1058
1133
|
}
|
|
1059
1134
|
|
|
1060
1135
|
// method to check if given item has multiple content parts and each of them has the same link attrs with different inline style (plain, em, strong, u)
|
|
@@ -1085,7 +1160,7 @@ function isStyledLink(item, options = {}) {
|
|
|
1085
1160
|
)
|
|
1086
1161
|
return false;
|
|
1087
1162
|
|
|
1088
|
-
const { href, target } = firstLinkMark.attrs;
|
|
1163
|
+
const { href, target, role } = firstLinkMark.attrs;
|
|
1089
1164
|
|
|
1090
1165
|
const cleanedContent = content.map((c) => {
|
|
1091
1166
|
// remove link marks from content items
|
|
@@ -1104,7 +1179,7 @@ function isStyledLink(item, options = {}) {
|
|
|
1104
1179
|
return {
|
|
1105
1180
|
type: "paragraph",
|
|
1106
1181
|
children: processInlineElements(item.content, options),
|
|
1107
|
-
text: anchor(href, target, textContent),
|
|
1182
|
+
text: anchor(href, target, textContent, role),
|
|
1108
1183
|
attrs: item.attrs,
|
|
1109
1184
|
};
|
|
1110
1185
|
}
|