@uniweb/semantic-parser 1.12.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/semantic-parser",
3
- "version": "1.12.1",
3
+ "version": "1.12.2",
4
4
  "description": "Semantic parser for ProseMirror/TipTap content structures",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -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 (item.type === "text" && item.marks) {
666
+ } else if (runLink && isSafeLink(runLink.attrs?.href)) {
655
667
  // Extract links from text nodes with link marks
656
- const linkMark = linkMarkOf(item.marks);
657
- if (linkMark && isSafeLink(linkMark.attrs?.href)) {
658
- items.push({
659
- type: "link",
660
- attrs: {
661
- ...linkMark.attrs, // Preserve all link attributes (role, target, etc.)
662
- label: item.text || "",
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
- for (const c of content) {
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
+ // `![](lu-star) [**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
- // Extract links with all their attributes
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: {