@particle-academy/react-fancy 5.67.0 → 5.68.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.
@@ -878,7 +878,7 @@ function maskInlineCode(line) {
878
878
  function scan(src, names, trackFences) {
879
879
  const tokens = [];
880
880
  let openFenceStart = -1;
881
- if (names.length === 0 && !trackFences) return { tokens, openFenceStart };
881
+ if (names.length === 0 && !trackFences) return { tokens, openFenceStart, openFence: null };
882
882
  const tagRe = names.length ? new RegExp(`<(\\/?)(${names.map(escapeRegExp).join("|")})(?=[\\s>/])([^>]*)>`, "gi") : null;
883
883
  let fence = null;
884
884
  let pos = 0;
@@ -929,7 +929,39 @@ function scan(src, names, trackFences) {
929
929
  if (nl < 0) break;
930
930
  pos = lineEnd + 1;
931
931
  }
932
- return { tokens, openFenceStart };
932
+ return { tokens, openFenceStart, openFence: fence };
933
+ }
934
+ function partialTagAt(lastLine, names, markdown) {
935
+ if (!names.length) return -1;
936
+ const masked = markdown ? maskInlineCode(lastLine) : lastLine;
937
+ const lt = masked.lastIndexOf("<");
938
+ if (lt < 0) return -1;
939
+ const m = /^<(\/?)([A-Za-z][\w-]*)?([^>]*)$/.exec(masked.slice(lt));
940
+ if (!m) return -1;
941
+ const name = (m[2] ?? "").toLowerCase();
942
+ const rest = m[3];
943
+ let partial = false;
944
+ if (!name) partial = rest === "";
945
+ else if (names.includes(name)) partial = rest === "" || /^\s/.test(rest);
946
+ else partial = rest === "" && names.some((n) => n.startsWith(name));
947
+ return partial ? lt : -1;
948
+ }
949
+ function openTagsBefore(tokens, cut) {
950
+ const stack = [];
951
+ for (const t of tokens) {
952
+ if (t.start >= cut) break;
953
+ if (t.kind === "open") {
954
+ stack.push(t);
955
+ continue;
956
+ }
957
+ for (let k = stack.length - 1; k >= 0; k--) {
958
+ if (stack[k].name === t.name) {
959
+ stack.length = k;
960
+ break;
961
+ }
962
+ }
963
+ }
964
+ return stack;
933
965
  }
934
966
  function splitMarkdownBlocks(src, tags) {
935
967
  const names = tags.map((t) => t.toLowerCase());
@@ -973,43 +1005,45 @@ function streamingCut(src, tags, markdown) {
973
1005
  const { tokens, openFenceStart } = scan(src, names, markdown);
974
1006
  let cut = src.length;
975
1007
  if (openFenceStart >= 0) cut = Math.min(cut, openFenceStart);
976
- const stack = [];
977
- for (const t of tokens) {
978
- if (t.start >= cut) break;
979
- if (t.kind === "open") {
980
- stack.push(t);
981
- continue;
982
- }
983
- for (let k = stack.length - 1; k >= 0; k--) {
984
- if (stack[k].name === t.name) {
985
- stack.length = k;
986
- break;
987
- }
988
- }
989
- }
1008
+ const stack = openTagsBefore(tokens, cut);
990
1009
  if (stack.length) cut = Math.min(cut, stack[0].start);
991
1010
  const lastLineStart = src.lastIndexOf("\n", cut - 1) + 1;
992
1011
  const lastLine = src.slice(lastLineStart, cut);
993
1012
  if (markdown && /^ {0,3}(`{1,2}|~{1,2})$/.test(lastLine)) {
994
1013
  return Math.min(cut, lastLineStart);
995
1014
  }
996
- if (names.length) {
997
- const masked = markdown ? maskInlineCode(lastLine) : lastLine;
998
- const lt = masked.lastIndexOf("<");
999
- if (lt >= 0) {
1000
- const m = /^<(\/?)([A-Za-z][\w-]*)?([^>]*)$/.exec(masked.slice(lt));
1001
- if (m) {
1002
- const name = (m[2] ?? "").toLowerCase();
1003
- const rest = m[3];
1004
- let partial = false;
1005
- if (!name) partial = rest === "";
1006
- else if (names.includes(name)) partial = rest === "" || /^\s/.test(rest);
1007
- else partial = rest === "" && names.some((n) => n.startsWith(name));
1008
- if (partial) cut = Math.min(cut, lastLineStart + lt);
1009
- }
1015
+ const lt = partialTagAt(lastLine, names, markdown);
1016
+ if (lt >= 0) cut = Math.min(cut, lastLineStart + lt);
1017
+ return cut;
1018
+ }
1019
+ function streamingProgressive(src, tags, markdown) {
1020
+ const names = tags.map((t) => t.toLowerCase());
1021
+ const { tokens, openFenceStart, openFence } = scan(src, names, markdown);
1022
+ let cut = src.length;
1023
+ let fence = openFenceStart >= 0 ? openFence : null;
1024
+ const lastLineStart = src.lastIndexOf("\n", cut - 1) + 1;
1025
+ const lastLine = src.slice(lastLineStart);
1026
+ if (fence) {
1027
+ if (openFenceStart === lastLineStart) {
1028
+ cut = openFenceStart;
1029
+ fence = null;
1030
+ } else {
1031
+ const m = /^ {0,3}(`+|~+)$/.exec(lastLine);
1032
+ if (m && m[1][0] === fence.char && m[1].length < fence.len) cut = lastLineStart;
1010
1033
  }
1034
+ } else if (markdown && /^ {0,3}(`{1,2}|~{1,2})$/.test(lastLine)) {
1035
+ cut = lastLineStart;
1036
+ } else {
1037
+ const lt = partialTagAt(lastLine, names, markdown);
1038
+ if (lt >= 0) cut = lastLineStart + lt;
1011
1039
  }
1012
- return cut;
1040
+ let out = src.slice(0, cut);
1041
+ if (fence) {
1042
+ out += (out.endsWith("\n") ? "" : "\n") + fence.char.repeat(fence.len) + "\n";
1043
+ }
1044
+ const stack = openTagsBefore(tokens, fence ? openFenceStart : cut);
1045
+ for (let k = stack.length - 1; k >= 0; k--) out += `</${stack[k].name}>`;
1046
+ return { source: out, openFence: fence !== null };
1013
1047
  }
1014
1048
  var FEEDBACK_MS = 2e3;
1015
1049
  var WRITE_TIMEOUT_MS = 5e3;
@@ -1067,12 +1101,17 @@ function cellText(cell) {
1067
1101
  for (const br of Array.from(clone.querySelectorAll("br"))) br.replaceWith(" ");
1068
1102
  return (clone.textContent ?? "").replace(/[\t\r\n]+/g, " ").replace(/ {2,}/g, " ").trim();
1069
1103
  }
1070
- function HtmlChunk({ html, fromMarkdown }) {
1104
+ function HtmlChunk({
1105
+ html,
1106
+ fromMarkdown,
1107
+ tail = false
1108
+ }) {
1071
1109
  const ref = react.useRef(null);
1072
1110
  const ctx = react.useContext(CopyContext);
1073
1111
  const [targets, setTargets] = react.useState([]);
1074
1112
  const code = ctx?.code ?? false;
1075
1113
  const table = ctx?.table ?? false;
1114
+ const holdLastCode = tail && (ctx?.holdTailCode ?? false);
1076
1115
  const inner = react.useMemo(() => ({ __html: html }), [html]);
1077
1116
  react.useLayoutEffect(() => {
1078
1117
  const root = ref.current;
@@ -1082,7 +1121,9 @@ function HtmlChunk({ html, fromMarkdown }) {
1082
1121
  }
1083
1122
  const found = [];
1084
1123
  const selector = [code && "pre", table && "table"].filter(Boolean).join(",");
1124
+ const inProgress = holdLastCode ? Array.from(root.querySelectorAll("pre")).pop() : void 0;
1085
1125
  for (const el of Array.from(root.querySelectorAll(selector))) {
1126
+ if (el === inProgress) continue;
1086
1127
  const kind = el.tagName === "PRE" ? "code" : "table";
1087
1128
  const wrapper = document.createElement("div");
1088
1129
  wrapper.setAttribute("data-react-fancy-content-copy-block", kind);
@@ -1102,7 +1143,7 @@ function HtmlChunk({ html, fromMarkdown }) {
1102
1143
  if (wrapper?.parentNode) wrapper.replaceWith(t.el);
1103
1144
  }
1104
1145
  };
1105
- }, [html, code, table]);
1146
+ }, [html, code, table, holdLastCode]);
1106
1147
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
1107
1148
  /* @__PURE__ */ jsxRuntime.jsx("div", { ref, dangerouslySetInnerHTML: inner }),
1108
1149
  targets.map(
@@ -1172,7 +1213,11 @@ function useCopyPipeline({
1172
1213
  );
1173
1214
  }
1174
1215
  var MAX_DEPTH = 8;
1175
- function ContentBody({ source, format, extensions, unsafe, depth = 0 }) {
1216
+ function lastIndexWith(items, renders) {
1217
+ for (let i = items.length - 1; i >= 0; i--) if (renders(items[i])) return i;
1218
+ return -1;
1219
+ }
1220
+ function ContentBody({ source, format, extensions, unsafe, depth = 0, tail = false }) {
1176
1221
  const active = depth >= MAX_DEPTH ? [] : extensions;
1177
1222
  const blocks = react.useMemo(() => {
1178
1223
  if (format !== "markdown") return null;
@@ -1181,8 +1226,19 @@ function ContentBody({ source, format, extensions, unsafe, depth = 0 }) {
1181
1226
  }, [source, format, active]);
1182
1227
  if (!blocks) {
1183
1228
  const html = unsafe ? source : chunk3U7PUZLI_cjs.sanitizeHtml(source);
1184
- return /* @__PURE__ */ jsxRuntime.jsx(RenderedContent, { html, extensions: active, unsafe, fromMarkdown: false, depth });
1229
+ return /* @__PURE__ */ jsxRuntime.jsx(
1230
+ RenderedContent,
1231
+ {
1232
+ html,
1233
+ extensions: active,
1234
+ unsafe,
1235
+ fromMarkdown: false,
1236
+ depth,
1237
+ tail
1238
+ }
1239
+ );
1185
1240
  }
1241
+ const last = tail ? lastIndexWith(blocks, (b) => b.type === "tag" || b.source.trim() !== "") : -1;
1186
1242
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: blocks.map((block, i) => {
1187
1243
  if (block.type === "markdown") {
1188
1244
  const raw = markdownToRawHtml(block.source);
@@ -1194,7 +1250,8 @@ function ContentBody({ source, format, extensions, unsafe, depth = 0 }) {
1194
1250
  extensions: active,
1195
1251
  unsafe,
1196
1252
  fromMarkdown: true,
1197
- depth
1253
+ depth,
1254
+ tail: i === last
1198
1255
  },
1199
1256
  i
1200
1257
  );
@@ -1210,21 +1267,38 @@ function ContentBody({ source, format, extensions, unsafe, depth = 0 }) {
1210
1267
  format: "markdown",
1211
1268
  extensions: active,
1212
1269
  unsafe,
1213
- depth: depth + 1
1270
+ depth: depth + 1,
1271
+ tail: i === last
1214
1272
  }
1215
1273
  ) }) }, i);
1216
1274
  }) });
1217
1275
  }
1218
- function RenderedContent({ html, extensions, unsafe, fromMarkdown, depth }) {
1276
+ function RenderedContent({
1277
+ html,
1278
+ extensions,
1279
+ unsafe,
1280
+ fromMarkdown,
1281
+ depth,
1282
+ tail = false
1283
+ }) {
1219
1284
  const segments = react.useMemo(() => parseSegments(html, extensions), [html, extensions]);
1220
1285
  const renderHtml = (content) => unsafe ? content : chunk3U7PUZLI_cjs.sanitizeHtml(content);
1221
1286
  if (segments.length === 0) return null;
1222
1287
  if (segments.length === 1 && segments[0].type === "html") {
1223
- return /* @__PURE__ */ jsxRuntime.jsx(HtmlChunk, { html: renderHtml(segments[0].content), fromMarkdown });
1288
+ return /* @__PURE__ */ jsxRuntime.jsx(HtmlChunk, { html: renderHtml(segments[0].content), fromMarkdown, tail });
1224
1289
  }
1290
+ const last = tail ? lastIndexWith(segments, (s) => s.type === "extension" || s.content.trim() !== "") : -1;
1225
1291
  return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children: segments.map((segment, i) => {
1226
1292
  if (segment.type === "html") {
1227
- return segment.content ? /* @__PURE__ */ jsxRuntime.jsx(HtmlChunk, { html: renderHtml(segment.content), fromMarkdown }, i) : null;
1293
+ return segment.content ? /* @__PURE__ */ jsxRuntime.jsx(
1294
+ HtmlChunk,
1295
+ {
1296
+ html: renderHtml(segment.content),
1297
+ fromMarkdown,
1298
+ tail: i === last
1299
+ },
1300
+ i
1301
+ ) : null;
1228
1302
  }
1229
1303
  const ext = extensions.find((e) => e.tag.toLowerCase() === segment.tag);
1230
1304
  if (!ext) return null;
@@ -1237,7 +1311,8 @@ function RenderedContent({ html, extensions, unsafe, fromMarkdown, depth }) {
1237
1311
  format: "html",
1238
1312
  extensions,
1239
1313
  unsafe,
1240
- depth: depth + 1
1314
+ depth: depth + 1,
1315
+ tail: i === last
1241
1316
  }
1242
1317
  ) : void 0;
1243
1318
  return /* @__PURE__ */ jsxRuntime.jsx(Wrapper, { "data-render-extension": segment.tag, children: /* @__PURE__ */ jsxRuntime.jsx(Component, { content: segment.content, attributes: segment.attributes, children }) }, i);
@@ -1263,15 +1338,14 @@ function ContentRenderer({
1263
1338
  );
1264
1339
  const safe = value ?? "";
1265
1340
  const resolvedFormat = format === "auto" ? detectFormat(safe) : format;
1266
- const source = react.useMemo(() => {
1267
- if (!streaming) return safe;
1268
- const cut = streamingCut(
1269
- safe,
1270
- extensions.map((e) => e.tag),
1271
- resolvedFormat === "markdown"
1272
- );
1273
- return safe.slice(0, cut);
1274
- }, [safe, streaming, extensions, resolvedFormat]);
1341
+ const progressive = streaming === "progressive";
1342
+ const { source, openFence } = react.useMemo(() => {
1343
+ if (!streaming) return { source: safe, openFence: false };
1344
+ const tags = extensions.map((e) => e.tag);
1345
+ const markdown = resolvedFormat === "markdown";
1346
+ if (progressive) return streamingProgressive(safe, tags, markdown);
1347
+ return { source: safe.slice(0, streamingCut(safe, tags, markdown)), openFence: false };
1348
+ }, [safe, streaming, progressive, extensions, resolvedFormat]);
1275
1349
  const codeOn = copyButtons === true || typeof copyButtons === "object" && copyButtons.code !== false;
1276
1350
  const tableOn = copyButtons === true || typeof copyButtons === "object" && copyButtons.table !== false;
1277
1351
  const copyOn = codeOn || tableOn;
@@ -1283,8 +1357,8 @@ function ContentRenderer({
1283
1357
  }, []);
1284
1358
  const copy = useCopyPipeline({ writer: clipboardWriter, onCopy, rootRef, announce });
1285
1359
  const copyContext = react.useMemo(
1286
- () => ({ code: codeOn, table: tableOn, copy }),
1287
- [codeOn, tableOn, copy]
1360
+ () => ({ code: codeOn, table: tableOn, copy, holdTailCode: openFence }),
1361
+ [codeOn, tableOn, copy, openFence]
1288
1362
  );
1289
1363
  return /* @__PURE__ */ jsxRuntime.jsxs(
1290
1364
  "div",
@@ -1292,6 +1366,7 @@ function ContentRenderer({
1292
1366
  ref: rootRef,
1293
1367
  "data-react-fancy-content-renderer": "",
1294
1368
  "data-streaming": streaming ? "true" : void 0,
1369
+ "data-streaming-mode": progressive ? "progressive" : void 0,
1295
1370
  style: { lineHeight: lineSpacing },
1296
1371
  className: chunkZU5Q77OT_cjs.cn("text-sm", proseClasses, className),
1297
1372
  children: [
@@ -1301,7 +1376,8 @@ function ContentRenderer({
1301
1376
  source,
1302
1377
  format: resolvedFormat,
1303
1378
  extensions,
1304
- unsafe
1379
+ unsafe,
1380
+ tail: true
1305
1381
  }
1306
1382
  ) }),
1307
1383
  copyOn && /* @__PURE__ */ jsxRuntime.jsx(
@@ -1331,5 +1407,5 @@ exports.proseClasses = proseClasses;
1331
1407
  exports.registerExtension = registerExtension;
1332
1408
  exports.registerExtensions = registerExtensions;
1333
1409
  exports.renderMarkdown = renderMarkdown;
1334
- //# sourceMappingURL=chunk-E5UU666U.cjs.map
1335
- //# sourceMappingURL=chunk-E5UU666U.cjs.map
1410
+ //# sourceMappingURL=chunk-464QSZ4A.cjs.map
1411
+ //# sourceMappingURL=chunk-464QSZ4A.cjs.map