@ai-matrx/rich-content 0.2.89 → 0.2.90

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.90
4
+
5
+ Automatic changed-only republish (docs/metadata drift since the last tag — see
6
+ `git diff npm/rich-content/v0.2.89..npm/rich-content/v0.2.90 -- apps/shared/rich-content`).
7
+ No source changes intended and no consumer action required.
8
+
3
9
  ## 0.2.89
4
10
 
5
11
  Automatic changed-only republish (docs/metadata drift since the last tag — see
@@ -78,15 +78,7 @@ export interface SplitterBlock {
78
78
  * Kept as an alias so existing imports continue to compile during migration.
79
79
  */
80
80
  export type ContentBlock = SplitterBlock;
81
- /**
82
- * Detection is recursive across arrival containers: a complete object that
83
- * directly declares `__kind` becomes its own JSON/Content-IR block even when
84
- * Markdown first captured it inside prose, a non-JSON fence, or XML. Every
85
- * byte around recovered objects stays in document order as its original block
86
- * type. Artifact wrappers already carry their body envelope for selectors and
87
- * retain their identity/chrome; they are the one container that must not be
88
- * exploded into anonymous fragments.
89
- */
81
+ export declare function wrapperSourceOf(block: SplitterBlock): string | undefined;
90
82
  export declare function recoverEmbeddedKindJsonBlocksWith(input: SplitterBlock[], envelopes: SplitterEnvelopes): SplitterBlock[];
91
83
  /**
92
84
  * Code-fence languages that promote to a first-class block type (the fence
@@ -43,6 +43,7 @@ import {
43
43
  XmlContainerTracker
44
44
  } from "@ai-matrx/content-ir/source";
45
45
  import { indexOutsideInlineCode } from "@ai-matrx/content-ir/source";
46
+ import { recoverPartialKindTailWith, splitAdjacentJsonRootsWith } from "./partial-kind-tail.js";
46
47
  import {
47
48
  findBalancedXmlClose,
48
49
  initialXmlBalance
@@ -90,6 +91,15 @@ function isJsonFenceBlock(block) {
90
91
  function isQuotedSourceFenceBlock(block) {
91
92
  return block.type === "code" && block.metadata?.genericXmlContainer !== true && !isJsonFenceLanguage(block.language);
92
93
  }
94
+ function withFenceSurface(metadata, language, content, envelopes) {
95
+ if (!language) return metadata;
96
+ const envelope = envelopes.fenceRegion(language, content);
97
+ return envelope ? { ...metadata ?? {}, [IR_ENVELOPE_KEY]: envelope } : metadata;
98
+ }
99
+ const wrapperSources = /* @__PURE__ */ new WeakMap();
100
+ function wrapperSourceOf(block) {
101
+ return wrapperSources.get(block);
102
+ }
93
103
  function recoverEmbeddedKindJsonBlocksWith(input, envelopes) {
94
104
  const recovered = [];
95
105
  for (const block of input) {
@@ -116,6 +126,8 @@ function recoverEmbeddedKindJsonBlocksWith(input, envelopes) {
116
126
  recovered.push(block);
117
127
  continue;
118
128
  }
129
+ const isWrapperSplit = pieces.some((piece) => piece.type === "residual");
130
+ const firstPiece = recovered.length;
119
131
  let followsKind = false;
120
132
  for (const piece of pieces) {
121
133
  if (piece.type === "chrome") continue;
@@ -162,6 +174,9 @@ function recoverEmbeddedKindJsonBlocksWith(input, envelopes) {
162
174
  metadata: genericXml ? { genericXmlContainer: true } : void 0
163
175
  });
164
176
  }
177
+ if (isWrapperSplit) {
178
+ for (const piece of recovered.slice(firstPiece)) wrapperSources.set(piece, block.content);
179
+ }
165
180
  }
166
181
  return recovered;
167
182
  }
@@ -890,8 +905,11 @@ function extractCodeBlock(language, openTicks, startIndex, lines, fenceChar = "`
890
905
  { char: fenceChar, ticks: openTicks, lang: language ?? "" }
891
906
  );
892
907
  if (!end.closed) {
908
+ const body2 = lines.slice(startIndex);
909
+ const tail = (body2[body2.length - 1] ?? "").trim();
910
+ if (body2.length > 0 && tail && tail.length < openTicks && [...tail].every((c) => c === fenceChar)) body2.pop();
893
911
  return {
894
- content: lines.slice(startIndex).join("\n"),
912
+ content: body2.join("\n"),
895
913
  nextIndex: lines.length + 1,
896
914
  metadata: { language },
897
915
  closedCleanly: false
@@ -1288,18 +1306,29 @@ const splitContentIntoBlocksWith = (mdContent, envelopes) => {
1288
1306
  type: "code",
1289
1307
  content: extraction.content,
1290
1308
  language: codeCheck.language,
1291
- metadata: envelopes.withIrEnvelope(
1309
+ metadata: withFenceSurface(
1310
+ envelopes.withIrEnvelope(
1311
+ extraction.content,
1312
+ codeCheck.meta ? { [FENCE_META_KEY]: codeCheck.meta } : void 0
1313
+ ),
1314
+ extraction.closedCleanly ? normalizedLanguage : void 0,
1292
1315
  extraction.content,
1293
- codeCheck.meta ? { [FENCE_META_KEY]: codeCheck.meta } : void 0
1316
+ envelopes
1294
1317
  )
1295
1318
  });
1296
1319
  }
1297
1320
  } else {
1321
+ const codeMetadata = withFenceSurface(
1322
+ codeCheck.meta ? { [FENCE_META_KEY]: codeCheck.meta } : void 0,
1323
+ extraction.closedCleanly ? normalizedLanguage : void 0,
1324
+ extraction.content,
1325
+ envelopes
1326
+ );
1298
1327
  blocks.push({
1299
1328
  type: "code",
1300
1329
  content: extraction.content,
1301
1330
  language: codeCheck.language,
1302
- ...codeCheck.meta ? { metadata: { [FENCE_META_KEY]: codeCheck.meta } } : {}
1331
+ ...codeMetadata ? { metadata: codeMetadata } : {}
1303
1332
  });
1304
1333
  }
1305
1334
  i = extraction.nextIndex;
@@ -1341,7 +1370,7 @@ const splitContentIntoBlocksWith = (mdContent, envelopes) => {
1341
1370
  i,
1342
1371
  lines
1343
1372
  );
1344
- let xmlMetadata = extraction.metadata;
1373
+ let xmlMetadata = extraction.metadata ? { rawXml: extraction.content, ...extraction.metadata } : extraction.metadata;
1345
1374
  if (xmlMetadata?.isComplete === true) {
1346
1375
  const xmlTag = xmlTagNameOf(xmlMatch.matchedTag);
1347
1376
  const attributed = xmlMatch.matchedTag !== `<${xmlTag}>`;
@@ -1776,7 +1805,10 @@ ${extraction.content}
1776
1805
  if (currentText.trim()) {
1777
1806
  blocks.push({ type: "text", content: currentText.trimEnd() });
1778
1807
  }
1779
- return recoverEmbeddedKindJsonBlocksWith(blocks, envelopes);
1808
+ return recoverPartialKindTailWith(
1809
+ recoverEmbeddedKindJsonBlocksWith(splitAdjacentJsonRootsWith(blocks, envelopes), envelopes),
1810
+ envelopes
1811
+ );
1780
1812
  };
1781
1813
  export {
1782
1814
  AUDIO_URL_EXT,
@@ -1795,5 +1827,6 @@ export {
1795
1827
  parseXmlAttributes,
1796
1828
  recoverEmbeddedKindJsonBlocksWith,
1797
1829
  splitContentIntoBlocksWith,
1798
- startUnrecognizedXmlContainer
1830
+ startUnrecognizedXmlContainer,
1831
+ wrapperSourceOf
1799
1832
  };
@@ -0,0 +1,22 @@
1
+ /**
2
+ * THE OPEN KIND AT THE END OF THE TEXT — the splitter's reading of a text that stops inside a kind.
3
+ *
4
+ * `recoverEmbeddedKindJsonBlocksWith` lifts every COMPLETE kind object out of its container
5
+ * (prose, an XML tag, a reasoning region, a JSON wrapper). A text that ends inside a kind — every
6
+ * frame of a stream, or a truncated answer on reload — holds an object that never closes, so
7
+ * that pass cannot see it and the kind would print as prose, sit inside an XML card, or draw as
8
+ * a raw JSON card (the never-raw law, Arman 2026-09-30). This pass reads the same thing for the
9
+ * open object: the last block is cut where the still-open root object (or array of objects)
10
+ * starts, the moment a `__kind` key is visible in it. Complete elements of an open array of
11
+ * kinds become their own kinds (A6); its array punctuation draws nothing.
12
+ *
13
+ * One reading for both paths: the live stream and a reload of the same text call this the same
14
+ * way, so a stream never draws a layout its own reload does not.
15
+ */
16
+ import type { SplitterBlock, SplitterEnvelopes } from "./content-splitter-core.js";
17
+ export declare function recoverPartialKindTailWith(blocks: SplitterBlock[], envelopes: SplitterEnvelopes): SplitterBlock[];
18
+ /**
19
+ * Root objects written back to back (`{…}{…}{…}`, a provider concatenating answers): each root is
20
+ * its own JSON region — one block per root, the reading the live stream always drew.
21
+ */
22
+ export declare function splitAdjacentJsonRootsWith(blocks: SplitterBlock[], envelopes: SplitterEnvelopes): SplitterBlock[];
@@ -0,0 +1,209 @@
1
+ import {
2
+ bareRegionBreaksIntoProse,
3
+ hasKindKey,
4
+ jsonKindSignal,
5
+ kindObjectProseBreak,
6
+ normalizeRecoveredContainerPiece
7
+ } from "@ai-matrx/content-ir/surfaces";
8
+ import { hasUnclosedBacktickRun } from "@ai-matrx/content-ir/source";
9
+ const unescapeKey = (source) => source.replace(/\\_/g, "_");
10
+ const isKindSource = (source) => hasKindKey(source) || hasKindKey(unescapeKey(source));
11
+ const QUOTE_MARKERS = /\n[ \t]{0,3}(?:> ?)+/g;
12
+ const unquote = (source) => source.replace(QUOTE_MARKERS, "\n");
13
+ const NEVER_CUT = /* @__PURE__ */ new Set(["artifact", "matrx", "matrx_file", "table", "tree", "image", "video", "audio"]);
14
+ const JSON_LANGUAGES = /* @__PURE__ */ new Set(["", "json", "jsonc", "json5"]);
15
+ function scan(text, from) {
16
+ const stack = [];
17
+ let inString = false;
18
+ let escaped = false;
19
+ const isArray = text[from] === "[";
20
+ const elements = [];
21
+ let elementStart = -1;
22
+ for (let i = from; i < text.length; i++) {
23
+ const ch = text[i];
24
+ if (inString) {
25
+ if (escaped) escaped = false;
26
+ else if (ch === "\\") escaped = true;
27
+ else if (ch === '"') inString = false;
28
+ continue;
29
+ }
30
+ if (isArray && stack.length === 1 && elementStart === -1 && !/[\s,\]]/.test(ch)) elementStart = i;
31
+ if (ch === '"') inString = true;
32
+ else if (ch === "{" || ch === "[") stack.push(ch);
33
+ else if (ch === "}" || ch === "]") {
34
+ stack.pop();
35
+ if (stack.length === 0) return { closedAt: i + 1 };
36
+ if (isArray && stack.length === 1 && elementStart !== -1) {
37
+ elements.push({ start: elementStart, end: i + 1, complete: true });
38
+ elementStart = -1;
39
+ }
40
+ } else if (isArray && stack.length === 1 && ch === ",") {
41
+ if (elementStart !== -1) elements.push({ start: elementStart, end: i, complete: true });
42
+ elementStart = -1;
43
+ }
44
+ }
45
+ if (isArray && elementStart !== -1) elements.push({ start: elementStart, end: text.length, complete: false });
46
+ return { open: { start: from, elements, isArray } };
47
+ }
48
+ function openRootOf(text, prose, xml) {
49
+ let i = 0;
50
+ while (i < text.length) {
51
+ const ch = text[i];
52
+ if (ch !== "{" && ch !== "[") {
53
+ i++;
54
+ continue;
55
+ }
56
+ if (prose) {
57
+ const lineStart = text.lastIndexOf("\n", i - 1) + 1;
58
+ if (hasUnclosedBacktickRun(text.slice(lineStart, i))) {
59
+ i++;
60
+ continue;
61
+ }
62
+ }
63
+ if (xml && text.lastIndexOf("<", i) > text.lastIndexOf(">", i)) {
64
+ i++;
65
+ continue;
66
+ }
67
+ if (ch === "{" && !/^\{\s*(?:"|\}|$)/.test(unquote(text.slice(i)))) {
68
+ i++;
69
+ continue;
70
+ }
71
+ if (ch === "[" && !/^\[\s*(?:\{|$)/.test(text.slice(i))) {
72
+ i++;
73
+ continue;
74
+ }
75
+ const result = scan(text, i);
76
+ if ("open" in result) return result.open;
77
+ i = result.closedAt;
78
+ }
79
+ return null;
80
+ }
81
+ function isCuttable(block) {
82
+ if (NEVER_CUT.has(block.type)) return false;
83
+ if (block.type === "code") {
84
+ if (block.metadata?.genericXmlContainer === true) return true;
85
+ return JSON_LANGUAGES.has((block.language ?? "").toLowerCase());
86
+ }
87
+ return true;
88
+ }
89
+ const TRAILING_JSON_FENCE_OPENER = /\n?[ \t]*(?:`{3,}|~{3,})[ \t]*(?:json5?|jsonc)?[ \t]*\n?[ \t]*$/i;
90
+ function recoverPartialKindTail(blocks, envelopes) {
91
+ const last = blocks[blocks.length - 1];
92
+ if (!last || !isCuttable(last)) return blocks;
93
+ const content = last.content;
94
+ const isJsonCode = last.type === "code" && last.metadata?.genericXmlContainer !== true;
95
+ if (isJsonCode && blocks.length > 1 && content.trim() && /^[\s,\]]+$/.test(content)) return blocks.slice(0, -1);
96
+ if (!content.includes("{")) return blocks;
97
+ const isCodeJson = last.type === "code" && last.metadata?.genericXmlContainer !== true;
98
+ const xml = last.type !== "text" && content.trimStart().startsWith("<");
99
+ const root = openRootOf(content, last.type === "text", xml);
100
+ if (!root) return blocks;
101
+ const lead = content.slice(0, root.start);
102
+ if (isCodeJson && !root.isArray) {
103
+ if (!lead.trim() || !/^[\s,[\]]*$/.test(lead)) return blocks;
104
+ return [
105
+ ...blocks.slice(0, -1),
106
+ { type: "code", content: content.slice(root.start), language: "json", metadata: { isComplete: false } }
107
+ ];
108
+ }
109
+ const rootText = last.type === "text" ? unquote(content.slice(root.start)) : content.slice(root.start);
110
+ if (kindObjectProseBreak(rootText.trimStart()) !== null || bareRegionBreaksIntoProse(rootText)) return blocks;
111
+ const opensLine = !lead || lead.endsWith("\n");
112
+ const bareRegion = last.type === "text" && opensLine && (/^(?:\[\s*)?\{\s*"[^"]*"\s*:/.test(rootText) || /^(?:\[\s*)?\{\s*(?:"[^"]*"?\s*)?$/.test(rootText)) && !bareRegionBreaksIntoProse(rootText);
113
+ const headOf = () => {
114
+ const before2 = lead.trimEnd();
115
+ return before2.trim() ? [{ ...last, content: before2 }] : [];
116
+ };
117
+ const splitAsRegion = () => [
118
+ ...blocks.slice(0, -1),
119
+ ...headOf(),
120
+ { type: "code", content: rootText, language: "json", metadata: { isComplete: false } }
121
+ ];
122
+ const pieces = [];
123
+ if (root.isArray) {
124
+ for (const element of root.elements) {
125
+ const source = content.slice(element.start, element.end).trim();
126
+ if (!source.startsWith("{") || !isKindSource(source)) {
127
+ if (element.complete) return bareRegion ? splitAsRegion() : blocks;
128
+ if (jsonKindSignal(source) === "not_kind") return bareRegion ? splitAsRegion() : blocks;
129
+ }
130
+ pieces.push(
131
+ element.complete ? { type: "code", content: source, language: "json", metadata: envelopes.withIrEnvelope(source, void 0) } : { type: "code", content: source, language: "json", metadata: { isComplete: false } }
132
+ );
133
+ }
134
+ if (pieces.length === 0) return blocks;
135
+ } else {
136
+ const raw = last.type === "text" ? unquote(content.slice(root.start)) : content.slice(root.start);
137
+ if (!isKindSource(raw)) return bareRegion ? splitAsRegion() : blocks;
138
+ const source = /"[^"]*\\_/.test(raw) ? unescapeKey(raw) : raw;
139
+ pieces.push({ type: "code", content: source, language: "json", metadata: { isComplete: false } });
140
+ }
141
+ let before = content.slice(0, root.start);
142
+ if (last.type !== "text") before = before.replace(TRAILING_JSON_FENCE_OPENER, "");
143
+ const container = isCodeJson ? "" : normalizeRecoveredContainerPiece(before, last.type, false);
144
+ const head = container && container.trim() ? [{ ...last, content: last.type === "text" ? container.trimEnd() : container }] : [];
145
+ return withoutArrayChrome([...blocks.slice(0, -1), ...head, ...pieces]);
146
+ }
147
+ function recoverPartialKindTailWith(blocks, envelopes) {
148
+ const out = recoverPartialKindTail(blocks, envelopes);
149
+ const cleaned = withoutArrayChrome(out);
150
+ return cleaned.length === out.length ? out : cleaned;
151
+ }
152
+ const isJsonBlock = (block) => block?.type === "code" && block.language === "json";
153
+ function withoutArrayChrome(blocks) {
154
+ return blocks.filter(
155
+ (block, i) => !((block.type === "text" || isJsonBlock(block)) && /^[\s[\],]+$/.test(block.content) && (isJsonBlock(blocks[i - 1]) || isJsonBlock(blocks[i + 1])))
156
+ );
157
+ }
158
+ function splitAdjacentJsonRootsWith(blocks, envelopes) {
159
+ let changed = false;
160
+ const out = [];
161
+ for (const block of blocks) {
162
+ const roots = block.type === "text" ? adjacentRoots(block.content) : null;
163
+ if (!roots) {
164
+ out.push(block);
165
+ continue;
166
+ }
167
+ changed = true;
168
+ roots.forEach((root, n) => {
169
+ const open = n === roots.length - 1 && scan(root, 0) && !("closedAt" in scan(root, 0));
170
+ out.push({
171
+ type: "code",
172
+ content: root,
173
+ language: "json",
174
+ metadata: open ? { isComplete: false } : envelopes.withIrEnvelope(root, void 0)
175
+ });
176
+ });
177
+ }
178
+ return changed ? out : blocks;
179
+ }
180
+ function adjacentRoots(content) {
181
+ const text = content.trim();
182
+ if (!text.startsWith("{")) return null;
183
+ const roots = [];
184
+ let i = 0;
185
+ while (i < text.length) {
186
+ if (text[i] !== "{") return null;
187
+ const result = scan(text, i);
188
+ if (!("closedAt" in result)) {
189
+ if (roots.length === 0) return null;
190
+ roots.push(text.slice(i));
191
+ return roots;
192
+ }
193
+ const root = text.slice(i, result.closedAt);
194
+ try {
195
+ const value = JSON.parse(root);
196
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
197
+ } catch {
198
+ return null;
199
+ }
200
+ roots.push(root);
201
+ i = result.closedAt;
202
+ while (i < text.length && /\s/.test(text[i])) i++;
203
+ }
204
+ return roots.length >= 2 ? roots : null;
205
+ }
206
+ export {
207
+ recoverPartialKindTailWith,
208
+ splitAdjacentJsonRootsWith
209
+ };
@@ -833,7 +833,7 @@ const THEMES = {
833
833
  headerText: "text-neutral-900 dark:text-neutral-100",
834
834
  row: {
835
835
  even: "bg-white dark:bg-neutral-800",
836
- odd: "bg-neutral-50 dark:bg-neutral-850",
836
+ odd: "bg-neutral-50 dark:bg-neutral-900",
837
837
  hover: "hover:bg-neutral-100 dark:hover:bg-neutral-700"
838
838
  },
839
839
  border: "border-neutral-200 dark:border-neutral-700",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-matrx/rich-content",
3
- "version": "0.2.89",
3
+ "version": "0.2.90",
4
4
  "description": "The AI Matrx formatted-text engine: the one markdown renderer, streaming markdown, the block splitter and renderer, the generic blocks, <RichContent> levels, the rich-document action registry and menus, copy, and the selection toolbar — as portable React.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -131,19 +131,19 @@
131
131
  "unist-util-visit-parents": "^6.0.2",
132
132
  "yaml": "^2.9.1",
133
133
  "@ai-matrx/agents": "latest",
134
+ "@ai-matrx/alchemy": "latest",
135
+ "@ai-matrx/canvas": "latest",
134
136
  "@ai-matrx/content-ir": "latest",
135
137
  "@ai-matrx/content-ir-react": "latest",
136
138
  "@ai-matrx/data": "latest",
137
- "@ai-matrx/alchemy": "latest",
138
- "@ai-matrx/canvas": "latest",
139
- "@ai-matrx/icons": "latest",
139
+ "@ai-matrx/design-system": "latest",
140
140
  "@ai-matrx/diff": "latest",
141
+ "@ai-matrx/icons": "latest",
141
142
  "@ai-matrx/kit": "latest",
142
- "@ai-matrx/design-system": "latest",
143
+ "@ai-matrx/media": "latest",
143
144
  "@ai-matrx/print": "latest",
144
- "@ai-matrx/records-ui": "latest",
145
145
  "@ai-matrx/records": "latest",
146
- "@ai-matrx/media": "latest"
146
+ "@ai-matrx/records-ui": "latest"
147
147
  },
148
148
  "peerDependencies": {
149
149
  "react": ">=19.0.0",
@@ -204,5 +204,5 @@
204
204
  "verify:modules-tarball": "node ./scripts/verify-modules-tarball.mjs",
205
205
  "verify:tarball": "node ./scripts/verify-tarball.mjs"
206
206
  },
207
- "gitHead": "f454ad2b6bb89fd398ef3fb05603ded30ca95dc0"
207
+ "gitHead": "cbad1e6f44472b071ce2b4925c0d19d425756a36"
208
208
  }