@particle-academy/react-fancy 5.66.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.
Files changed (70) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/dist/ContentRenderer.types-RO_l7_6N.d.cts +137 -0
  3. package/dist/ContentRenderer.types-RO_l7_6N.d.ts +137 -0
  4. package/dist/button.cjs +3 -4
  5. package/dist/button.js +1 -2
  6. package/dist/{chunk-J44EV6SP.js → chunk-23AVEYUP.js} +3 -3
  7. package/dist/{chunk-J44EV6SP.js.map → chunk-23AVEYUP.js.map} +1 -1
  8. package/dist/{chunk-L24LUSEE.js → chunk-2LQR4EAN.js} +553 -35
  9. package/dist/chunk-2LQR4EAN.js.map +1 -0
  10. package/dist/{chunk-C5YX5XVL.cjs → chunk-3U7PUZLI.cjs} +214 -4
  11. package/dist/chunk-3U7PUZLI.cjs.map +1 -0
  12. package/dist/{chunk-MCQJFMDP.cjs → chunk-464QSZ4A.cjs} +553 -35
  13. package/dist/chunk-464QSZ4A.cjs.map +1 -0
  14. package/dist/{chunk-QP35Z2WB.js → chunk-BL5IKSB3.js} +3 -3
  15. package/dist/{chunk-QP35Z2WB.js.map → chunk-BL5IKSB3.js.map} +1 -1
  16. package/dist/{chunk-4EUDW2WU.js → chunk-DCAD2JPP.js} +3 -3
  17. package/dist/{chunk-4EUDW2WU.js.map → chunk-DCAD2JPP.js.map} +1 -1
  18. package/dist/{chunk-ATIDUWY6.cjs → chunk-E4DXICU3.cjs} +19 -13
  19. package/dist/chunk-E4DXICU3.cjs.map +1 -0
  20. package/dist/{chunk-TLRRMYNV.js → chunk-OXERDFFF.js} +212 -4
  21. package/dist/chunk-OXERDFFF.js.map +1 -0
  22. package/dist/{chunk-SYEHWUM5.cjs → chunk-RALJMI7C.cjs} +4 -4
  23. package/dist/{chunk-SYEHWUM5.cjs.map → chunk-RALJMI7C.cjs.map} +1 -1
  24. package/dist/{chunk-UM4JXBRZ.cjs → chunk-S3NB6LDV.cjs} +4 -4
  25. package/dist/{chunk-UM4JXBRZ.cjs.map → chunk-S3NB6LDV.cjs.map} +1 -1
  26. package/dist/{chunk-MCQWTOTV.cjs → chunk-SAG6DHQK.cjs} +6 -6
  27. package/dist/{chunk-MCQWTOTV.cjs.map → chunk-SAG6DHQK.cjs.map} +1 -1
  28. package/dist/{chunk-CAUEKGMR.js → chunk-TQ77ZWBD.js} +11 -5
  29. package/dist/chunk-TQ77ZWBD.js.map +1 -0
  30. package/dist/{chunk-7HXFEYOV.cjs → chunk-U5LKNGHU.cjs} +15 -15
  31. package/dist/{chunk-7HXFEYOV.cjs.map → chunk-U5LKNGHU.cjs.map} +1 -1
  32. package/dist/{chunk-M52S3TFI.js → chunk-WFR7HKJL.js} +3 -3
  33. package/dist/{chunk-M52S3TFI.js.map → chunk-WFR7HKJL.js.map} +1 -1
  34. package/dist/content-renderer.cjs +7 -5
  35. package/dist/content-renderer.d.cts +4 -21
  36. package/dist/content-renderer.d.ts +4 -21
  37. package/dist/content-renderer.js +4 -2
  38. package/dist/editor.cjs +6 -5
  39. package/dist/editor.d.cts +19 -2
  40. package/dist/editor.d.ts +19 -2
  41. package/dist/editor.js +4 -3
  42. package/dist/index.cjs +37 -38
  43. package/dist/index.cjs.map +1 -1
  44. package/dist/index.d.cts +2 -2
  45. package/dist/index.d.ts +2 -2
  46. package/dist/index.js +7 -8
  47. package/dist/index.js.map +1 -1
  48. package/dist/json-editor.cjs +13 -14
  49. package/dist/json-editor.js +2 -3
  50. package/dist/magic-wand.cjs +3 -4
  51. package/dist/magic-wand.js +2 -3
  52. package/dist/prompt-input.cjs +5 -6
  53. package/dist/prompt-input.js +2 -3
  54. package/dist/reason-tag.cjs +3 -4
  55. package/dist/reason-tag.js +2 -3
  56. package/docs/ContentRenderer.md +192 -3
  57. package/docs/Editor.md +38 -0
  58. package/package.json +1 -1
  59. package/dist/chunk-6TBRBN7H.js +0 -212
  60. package/dist/chunk-6TBRBN7H.js.map +0 -1
  61. package/dist/chunk-ATIDUWY6.cjs.map +0 -1
  62. package/dist/chunk-C5YX5XVL.cjs.map +0 -1
  63. package/dist/chunk-CAUEKGMR.js.map +0 -1
  64. package/dist/chunk-L24LUSEE.js.map +0 -1
  65. package/dist/chunk-MCQJFMDP.cjs.map +0 -1
  66. package/dist/chunk-TLRRMYNV.js.map +0 -1
  67. package/dist/chunk-X2FC7ODX.cjs +0 -215
  68. package/dist/chunk-X2FC7ODX.cjs.map +0 -1
  69. package/dist/extensions-6PVd3-vB.d.cts +0 -38
  70. package/dist/extensions-6PVd3-vB.d.ts +0 -38
@@ -1,10 +1,31 @@
1
- import { sanitizeHtml } from './chunk-6TBRBN7H.js';
1
+ import { sanitizeHtml, sanitizeHref, Button } from './chunk-OXERDFFF.js';
2
2
  import { cn } from './chunk-MMW2JGKZ.js';
3
3
  import { marked } from 'marked';
4
- import { useMemo } from 'react';
5
- import { jsx, Fragment } from 'react/jsx-runtime';
4
+ import { createContext, useMemo, useRef, useState, useCallback, useContext, useLayoutEffect, useEffect } from 'react';
5
+ import { createPortal } from 'react-dom';
6
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
6
7
 
7
8
  // src/components/ContentRenderer/extensions.ts
9
+ var URL_ATTRS = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "xlink:href"]);
10
+ function safeAttributes(attrs) {
11
+ const out = {};
12
+ for (const [name, value] of Object.entries(attrs)) {
13
+ const lower = name.toLowerCase();
14
+ if (lower.startsWith("on") || lower === "srcdoc") continue;
15
+ if (URL_ATTRS.has(lower)) {
16
+ const safe = sanitizeHref(value);
17
+ if (safe === void 0) continue;
18
+ out[name] = safe;
19
+ continue;
20
+ }
21
+ out[name] = value;
22
+ }
23
+ return out;
24
+ }
25
+ function tagAttributes(raw, unsafe) {
26
+ const attrs = raw ? parseAttributes(raw) : {};
27
+ return unsafe ? attrs : safeAttributes(attrs);
28
+ }
8
29
  var globalRegistry = [];
9
30
  function registerExtension(extension) {
10
31
  const idx = globalRegistry.findIndex(
@@ -811,72 +832,569 @@ function extensionEditorClasses(extensions) {
811
832
  ].join(" ");
812
833
  }).join(" ");
813
834
  }
814
- function RenderedContent({
835
+
836
+ // src/components/ContentRenderer/markdown-blocks.ts
837
+ var FENCE_OPEN = /^ {0,3}(`{3,}|~{3,})(.*)$/;
838
+ function escapeRegExp(s) {
839
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
840
+ }
841
+ function maskInlineCode(line) {
842
+ let out = "";
843
+ let i = 0;
844
+ while (i < line.length) {
845
+ if (line[i] !== "`") {
846
+ out += line[i];
847
+ i++;
848
+ continue;
849
+ }
850
+ let n = 0;
851
+ while (line[i + n] === "`") n++;
852
+ const fence = "`".repeat(n);
853
+ let search = i + n;
854
+ let close = -1;
855
+ while (search < line.length) {
856
+ const at = line.indexOf(fence, search);
857
+ if (at < 0) break;
858
+ let len = 0;
859
+ while (line[at + len] === "`") len++;
860
+ if (len === n) {
861
+ close = at;
862
+ break;
863
+ }
864
+ search = at + len;
865
+ }
866
+ if (close < 0) {
867
+ out += fence;
868
+ i += n;
869
+ continue;
870
+ }
871
+ out += " ".repeat(close + n - i);
872
+ i = close + n;
873
+ }
874
+ return out;
875
+ }
876
+ function scan(src, names, trackFences) {
877
+ const tokens = [];
878
+ let openFenceStart = -1;
879
+ if (names.length === 0 && !trackFences) return { tokens, openFenceStart, openFence: null };
880
+ const tagRe = names.length ? new RegExp(`<(\\/?)(${names.map(escapeRegExp).join("|")})(?=[\\s>/])([^>]*)>`, "gi") : null;
881
+ let fence = null;
882
+ let pos = 0;
883
+ while (pos <= src.length) {
884
+ const nl = src.indexOf("\n", pos);
885
+ const lineEnd = nl < 0 ? src.length : nl;
886
+ const line = src.slice(pos, lineEnd);
887
+ if (trackFences) {
888
+ if (fence) {
889
+ const close = new RegExp(`^ {0,3}${fence.char === "`" ? "`" : "~"}{${fence.len},}\\s*$`);
890
+ if (close.test(line)) {
891
+ fence = null;
892
+ openFenceStart = -1;
893
+ }
894
+ pos = lineEnd + 1;
895
+ if (nl < 0) break;
896
+ continue;
897
+ }
898
+ const m = FENCE_OPEN.exec(line);
899
+ if (m && !(m[1][0] === "`" && m[2].includes("`"))) {
900
+ fence = { char: m[1][0], len: m[1].length };
901
+ openFenceStart = pos;
902
+ pos = lineEnd + 1;
903
+ if (nl < 0) break;
904
+ continue;
905
+ }
906
+ }
907
+ if (tagRe) {
908
+ const masked = trackFences ? maskInlineCode(line) : line;
909
+ const indent2 = /^ {0,3}/.exec(masked)[0].length;
910
+ tagRe.lastIndex = 0;
911
+ let t;
912
+ while ((t = tagRe.exec(masked)) !== null) {
913
+ const isClose = t[1] === "/";
914
+ const attrs = t[3];
915
+ if (isClose && attrs.trim() !== "") continue;
916
+ if (!isClose && attrs.trimEnd().endsWith("/")) continue;
917
+ tokens.push({
918
+ kind: isClose ? "close" : "open",
919
+ name: t[2].toLowerCase(),
920
+ start: pos + t.index,
921
+ end: pos + t.index + t[0].length,
922
+ attrs: isClose ? "" : attrs.trim(),
923
+ lineStart: t.index === indent2
924
+ });
925
+ }
926
+ }
927
+ if (nl < 0) break;
928
+ pos = lineEnd + 1;
929
+ }
930
+ return { tokens, openFenceStart, openFence: fence };
931
+ }
932
+ function partialTagAt(lastLine, names, markdown) {
933
+ if (!names.length) return -1;
934
+ const masked = markdown ? maskInlineCode(lastLine) : lastLine;
935
+ const lt = masked.lastIndexOf("<");
936
+ if (lt < 0) return -1;
937
+ const m = /^<(\/?)([A-Za-z][\w-]*)?([^>]*)$/.exec(masked.slice(lt));
938
+ if (!m) return -1;
939
+ const name = (m[2] ?? "").toLowerCase();
940
+ const rest = m[3];
941
+ let partial = false;
942
+ if (!name) partial = rest === "";
943
+ else if (names.includes(name)) partial = rest === "" || /^\s/.test(rest);
944
+ else partial = rest === "" && names.some((n) => n.startsWith(name));
945
+ return partial ? lt : -1;
946
+ }
947
+ function openTagsBefore(tokens, cut) {
948
+ const stack = [];
949
+ for (const t of tokens) {
950
+ if (t.start >= cut) break;
951
+ if (t.kind === "open") {
952
+ stack.push(t);
953
+ continue;
954
+ }
955
+ for (let k = stack.length - 1; k >= 0; k--) {
956
+ if (stack[k].name === t.name) {
957
+ stack.length = k;
958
+ break;
959
+ }
960
+ }
961
+ }
962
+ return stack;
963
+ }
964
+ function splitMarkdownBlocks(src, tags) {
965
+ const names = tags.map((t) => t.toLowerCase());
966
+ if (names.length === 0) return src ? [{ type: "markdown", source: src }] : [];
967
+ const { tokens } = scan(src, names, true);
968
+ const blocks = [];
969
+ let cursor = 0;
970
+ let i = 0;
971
+ while (i < tokens.length) {
972
+ const open = tokens[i];
973
+ if (open.kind !== "open" || !open.lineStart || open.start < cursor) {
974
+ i++;
975
+ continue;
976
+ }
977
+ let depth = 0;
978
+ let closeIdx = -1;
979
+ for (let j = i; j < tokens.length; j++) {
980
+ const t = tokens[j];
981
+ if (t.name !== open.name) continue;
982
+ depth += t.kind === "open" ? 1 : -1;
983
+ if (depth === 0) {
984
+ closeIdx = j;
985
+ break;
986
+ }
987
+ }
988
+ if (closeIdx < 0) {
989
+ i++;
990
+ continue;
991
+ }
992
+ const close = tokens[closeIdx];
993
+ if (open.start > cursor) blocks.push({ type: "markdown", source: src.slice(cursor, open.start) });
994
+ blocks.push({ type: "tag", tag: open.name, attrs: open.attrs, body: src.slice(open.end, close.start) });
995
+ cursor = close.end;
996
+ i = closeIdx + 1;
997
+ }
998
+ if (cursor < src.length) blocks.push({ type: "markdown", source: src.slice(cursor) });
999
+ return blocks;
1000
+ }
1001
+ function streamingCut(src, tags, markdown) {
1002
+ const names = tags.map((t) => t.toLowerCase());
1003
+ const { tokens, openFenceStart } = scan(src, names, markdown);
1004
+ let cut = src.length;
1005
+ if (openFenceStart >= 0) cut = Math.min(cut, openFenceStart);
1006
+ const stack = openTagsBefore(tokens, cut);
1007
+ if (stack.length) cut = Math.min(cut, stack[0].start);
1008
+ const lastLineStart = src.lastIndexOf("\n", cut - 1) + 1;
1009
+ const lastLine = src.slice(lastLineStart, cut);
1010
+ if (markdown && /^ {0,3}(`{1,2}|~{1,2})$/.test(lastLine)) {
1011
+ return Math.min(cut, lastLineStart);
1012
+ }
1013
+ const lt = partialTagAt(lastLine, names, markdown);
1014
+ if (lt >= 0) cut = Math.min(cut, lastLineStart + lt);
1015
+ return cut;
1016
+ }
1017
+ function streamingProgressive(src, tags, markdown) {
1018
+ const names = tags.map((t) => t.toLowerCase());
1019
+ const { tokens, openFenceStart, openFence } = scan(src, names, markdown);
1020
+ let cut = src.length;
1021
+ let fence = openFenceStart >= 0 ? openFence : null;
1022
+ const lastLineStart = src.lastIndexOf("\n", cut - 1) + 1;
1023
+ const lastLine = src.slice(lastLineStart);
1024
+ if (fence) {
1025
+ if (openFenceStart === lastLineStart) {
1026
+ cut = openFenceStart;
1027
+ fence = null;
1028
+ } else {
1029
+ const m = /^ {0,3}(`+|~+)$/.exec(lastLine);
1030
+ if (m && m[1][0] === fence.char && m[1].length < fence.len) cut = lastLineStart;
1031
+ }
1032
+ } else if (markdown && /^ {0,3}(`{1,2}|~{1,2})$/.test(lastLine)) {
1033
+ cut = lastLineStart;
1034
+ } else {
1035
+ const lt = partialTagAt(lastLine, names, markdown);
1036
+ if (lt >= 0) cut = lastLineStart + lt;
1037
+ }
1038
+ let out = src.slice(0, cut);
1039
+ if (fence) {
1040
+ out += (out.endsWith("\n") ? "" : "\n") + fence.char.repeat(fence.len) + "\n";
1041
+ }
1042
+ const stack = openTagsBefore(tokens, fence ? openFenceStart : cut);
1043
+ for (let k = stack.length - 1; k >= 0; k--) out += `</${stack[k].name}>`;
1044
+ return { source: out, openFence: fence !== null };
1045
+ }
1046
+ var FEEDBACK_MS = 2e3;
1047
+ var WRITE_TIMEOUT_MS = 5e3;
1048
+ var CopyContext = createContext(null);
1049
+ async function defaultClipboardWriter(payload) {
1050
+ const clip = typeof navigator !== "undefined" ? navigator.clipboard : void 0;
1051
+ if (!clip) throw new Error("Clipboard API unavailable");
1052
+ const Item = globalThis.ClipboardItem;
1053
+ if (payload.html && Item && typeof clip.write === "function") {
1054
+ try {
1055
+ await clip.write([
1056
+ new Item({
1057
+ "text/html": new Blob([payload.html], { type: "text/html" }),
1058
+ "text/plain": new Blob([payload.text], { type: "text/plain" })
1059
+ })
1060
+ ]);
1061
+ return;
1062
+ } catch {
1063
+ }
1064
+ }
1065
+ if (typeof clip.writeText !== "function") throw new Error("Clipboard writeText unavailable");
1066
+ await clip.writeText(payload.text);
1067
+ }
1068
+ async function runWriter(writer, payload) {
1069
+ let timer;
1070
+ try {
1071
+ const result = await Promise.race([
1072
+ Promise.resolve().then(() => writer(payload)),
1073
+ new Promise((resolve) => {
1074
+ timer = setTimeout(() => resolve(false), WRITE_TIMEOUT_MS);
1075
+ })
1076
+ ]);
1077
+ return result !== false;
1078
+ } catch {
1079
+ return false;
1080
+ } finally {
1081
+ if (timer) clearTimeout(timer);
1082
+ }
1083
+ }
1084
+ function codeText(pre, fromMarkdown) {
1085
+ const text = (pre.querySelector("code") ?? pre).textContent ?? "";
1086
+ return fromMarkdown && text.endsWith("\n") ? text.slice(0, -1) : text;
1087
+ }
1088
+ function tableTsv(table) {
1089
+ const rows = [];
1090
+ for (const tr of Array.from(table.querySelectorAll("tr"))) {
1091
+ if (tr.closest("table") !== table) continue;
1092
+ const cells = Array.from(tr.children).filter((c) => c.tagName === "TD" || c.tagName === "TH");
1093
+ rows.push(cells.map((c) => cellText(c)).join(" "));
1094
+ }
1095
+ return rows.join("\n");
1096
+ }
1097
+ function cellText(cell) {
1098
+ const clone = cell.cloneNode(true);
1099
+ for (const br of Array.from(clone.querySelectorAll("br"))) br.replaceWith(" ");
1100
+ return (clone.textContent ?? "").replace(/[\t\r\n]+/g, " ").replace(/ {2,}/g, " ").trim();
1101
+ }
1102
+ function HtmlChunk({
815
1103
  html,
816
- extensions: instanceExtensions,
817
- unsafe = false
1104
+ fromMarkdown,
1105
+ tail = false
818
1106
  }) {
819
- const extensions = useMemo(
820
- () => mergeExtensions(instanceExtensions),
821
- [instanceExtensions]
1107
+ const ref = useRef(null);
1108
+ const ctx = useContext(CopyContext);
1109
+ const [targets, setTargets] = useState([]);
1110
+ const code = ctx?.code ?? false;
1111
+ const table = ctx?.table ?? false;
1112
+ const holdLastCode = tail && (ctx?.holdTailCode ?? false);
1113
+ const inner = useMemo(() => ({ __html: html }), [html]);
1114
+ useLayoutEffect(() => {
1115
+ const root = ref.current;
1116
+ if (!root || !code && !table) {
1117
+ setTargets([]);
1118
+ return;
1119
+ }
1120
+ const found = [];
1121
+ const selector = [code && "pre", table && "table"].filter(Boolean).join(",");
1122
+ const inProgress = holdLastCode ? Array.from(root.querySelectorAll("pre")).pop() : void 0;
1123
+ for (const el of Array.from(root.querySelectorAll(selector))) {
1124
+ if (el === inProgress) continue;
1125
+ const kind = el.tagName === "PRE" ? "code" : "table";
1126
+ const wrapper = document.createElement("div");
1127
+ wrapper.setAttribute("data-react-fancy-content-copy-block", kind);
1128
+ wrapper.className = kind === "code" ? "group relative" : "group relative flex flex-col";
1129
+ const host = document.createElement("div");
1130
+ host.setAttribute("data-react-fancy-content-copy-host", "");
1131
+ host.className = kind === "code" ? "absolute right-2 top-2 z-10" : "flex justify-end";
1132
+ el.replaceWith(wrapper);
1133
+ if (kind === "table") wrapper.append(host, el);
1134
+ else wrapper.append(el, host);
1135
+ found.push({ kind, el, host });
1136
+ }
1137
+ setTargets(found);
1138
+ return () => {
1139
+ for (const t of found) {
1140
+ const wrapper = t.host.parentElement;
1141
+ if (wrapper?.parentNode) wrapper.replaceWith(t.el);
1142
+ }
1143
+ };
1144
+ }, [html, code, table, holdLastCode]);
1145
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1146
+ /* @__PURE__ */ jsx("div", { ref, dangerouslySetInnerHTML: inner }),
1147
+ targets.map(
1148
+ (t, i) => createPortal(
1149
+ /* @__PURE__ */ jsx(CopyButton, { kind: t.kind, target: t.el, fromMarkdown }),
1150
+ t.host,
1151
+ `${i}`
1152
+ )
1153
+ )
1154
+ ] });
1155
+ }
1156
+ function CopyButton({
1157
+ kind,
1158
+ target,
1159
+ fromMarkdown
1160
+ }) {
1161
+ const ctx = useContext(CopyContext);
1162
+ const [state, setState] = useState("idle");
1163
+ const timer = useRef(void 0);
1164
+ const btn = useRef(null);
1165
+ useEffect(() => () => clearTimeout(timer.current), []);
1166
+ const onClick = useCallback(async () => {
1167
+ if (!ctx || !btn.current) return;
1168
+ const text = kind === "code" ? codeText(target, fromMarkdown) : tableTsv(target);
1169
+ const ok = await ctx.copy({ kind, text, html: target.outerHTML }, btn.current);
1170
+ setState(ok ? "copied" : "failed");
1171
+ clearTimeout(timer.current);
1172
+ timer.current = setTimeout(() => setState("idle"), FEEDBACK_MS);
1173
+ }, [ctx, kind, target, fromMarkdown]);
1174
+ const idleLabel = kind === "code" ? "Copy code" : "Copy table";
1175
+ const label = state === "copied" ? "Copied" : state === "failed" ? "Copy failed" : idleLabel;
1176
+ return /* @__PURE__ */ jsx(
1177
+ Button,
1178
+ {
1179
+ ref: btn,
1180
+ type: "button",
1181
+ size: "xs",
1182
+ variant: "ghost",
1183
+ icon: state === "copied" ? "check" : state === "failed" ? "x" : "copy",
1184
+ "aria-label": label,
1185
+ title: label,
1186
+ "data-react-fancy-content-copy": kind,
1187
+ "data-copy-state": state,
1188
+ onClick,
1189
+ className: kind === "code" ? "text-zinc-300 hover:bg-zinc-800 hover:text-white opacity-70 group-hover:opacity-100 focus-visible:opacity-100" : "opacity-70 group-hover:opacity-100 focus-visible:opacity-100"
1190
+ }
822
1191
  );
823
- const segments = useMemo(
824
- () => parseSegments(html, extensions),
825
- [html, extensions]
1192
+ }
1193
+ function useCopyPipeline({
1194
+ writer,
1195
+ onCopy,
1196
+ rootRef,
1197
+ announce
1198
+ }) {
1199
+ const latest = useRef({ writer, onCopy, announce });
1200
+ latest.current = { writer, onCopy, announce };
1201
+ return useCallback(
1202
+ async (payload, button) => {
1203
+ const { writer: w, onCopy: report, announce: say } = latest.current;
1204
+ const ok = await runWriter(w ?? defaultClipboardWriter, payload);
1205
+ say(ok ? "Copied" : "Copy failed");
1206
+ const all = rootRef.current ? Array.from(rootRef.current.querySelectorAll("[data-react-fancy-content-copy]")) : [];
1207
+ report?.({ ...payload, index: all.indexOf(button), ok });
1208
+ return ok;
1209
+ },
1210
+ [rootRef]
826
1211
  );
1212
+ }
1213
+ var MAX_DEPTH = 8;
1214
+ function lastIndexWith(items, renders) {
1215
+ for (let i = items.length - 1; i >= 0; i--) if (renders(items[i])) return i;
1216
+ return -1;
1217
+ }
1218
+ function ContentBody({ source, format, extensions, unsafe, depth = 0, tail = false }) {
1219
+ const active = depth >= MAX_DEPTH ? [] : extensions;
1220
+ const blocks = useMemo(() => {
1221
+ if (format !== "markdown") return null;
1222
+ const mdTags = active.filter((e) => e.markdown).map((e) => e.tag);
1223
+ return splitMarkdownBlocks(source, mdTags);
1224
+ }, [source, format, active]);
1225
+ if (!blocks) {
1226
+ const html = unsafe ? source : sanitizeHtml(source);
1227
+ return /* @__PURE__ */ jsx(
1228
+ RenderedContent,
1229
+ {
1230
+ html,
1231
+ extensions: active,
1232
+ unsafe,
1233
+ fromMarkdown: false,
1234
+ depth,
1235
+ tail
1236
+ }
1237
+ );
1238
+ }
1239
+ const last = tail ? lastIndexWith(blocks, (b) => b.type === "tag" || b.source.trim() !== "") : -1;
1240
+ return /* @__PURE__ */ jsx(Fragment, { children: blocks.map((block, i) => {
1241
+ if (block.type === "markdown") {
1242
+ const raw = markdownToRawHtml(block.source);
1243
+ const html = unsafe ? raw : sanitizeHtml(raw);
1244
+ return /* @__PURE__ */ jsx(
1245
+ RenderedContent,
1246
+ {
1247
+ html,
1248
+ extensions: active,
1249
+ unsafe,
1250
+ fromMarkdown: true,
1251
+ depth,
1252
+ tail: i === last
1253
+ },
1254
+ i
1255
+ );
1256
+ }
1257
+ const ext = active.find((e) => e.tag.toLowerCase() === block.tag);
1258
+ if (!ext) return null;
1259
+ const Component = ext.component;
1260
+ const Wrapper = ext.block !== false ? "div" : "span";
1261
+ return /* @__PURE__ */ jsx(Wrapper, { "data-render-extension": block.tag, children: /* @__PURE__ */ jsx(Component, { content: block.body, attributes: tagAttributes(block.attrs, unsafe), children: /* @__PURE__ */ jsx(
1262
+ ContentBody,
1263
+ {
1264
+ source: block.body,
1265
+ format: "markdown",
1266
+ extensions: active,
1267
+ unsafe,
1268
+ depth: depth + 1,
1269
+ tail: i === last
1270
+ }
1271
+ ) }) }, i);
1272
+ }) });
1273
+ }
1274
+ function RenderedContent({
1275
+ html,
1276
+ extensions,
1277
+ unsafe,
1278
+ fromMarkdown,
1279
+ depth,
1280
+ tail = false
1281
+ }) {
1282
+ const segments = useMemo(() => parseSegments(html, extensions), [html, extensions]);
827
1283
  const renderHtml = (content) => unsafe ? content : sanitizeHtml(content);
1284
+ if (segments.length === 0) return null;
828
1285
  if (segments.length === 1 && segments[0].type === "html") {
829
- return /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderHtml(segments[0].content) } });
830
- }
831
- if (segments.length === 0) {
832
- return null;
1286
+ return /* @__PURE__ */ jsx(HtmlChunk, { html: renderHtml(segments[0].content), fromMarkdown, tail });
833
1287
  }
1288
+ const last = tail ? lastIndexWith(segments, (s) => s.type === "extension" || s.content.trim() !== "") : -1;
834
1289
  return /* @__PURE__ */ jsx(Fragment, { children: segments.map((segment, i) => {
835
1290
  if (segment.type === "html") {
836
- return segment.content ? /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderHtml(segment.content) } }, i) : null;
1291
+ return segment.content ? /* @__PURE__ */ jsx(
1292
+ HtmlChunk,
1293
+ {
1294
+ html: renderHtml(segment.content),
1295
+ fromMarkdown,
1296
+ tail: i === last
1297
+ },
1298
+ i
1299
+ ) : null;
837
1300
  }
838
- const ext = extensions.find(
839
- (e) => e.tag.toLowerCase() === segment.tag
840
- );
1301
+ const ext = extensions.find((e) => e.tag.toLowerCase() === segment.tag);
841
1302
  if (!ext) return null;
842
1303
  const Component = ext.component;
843
- const isBlock = ext.block !== false;
844
- const Wrapper = isBlock ? "div" : "span";
845
- return /* @__PURE__ */ jsx(Wrapper, { "data-render-extension": segment.tag, children: /* @__PURE__ */ jsx(Component, { content: segment.content, attributes: segment.attributes }) }, i);
1304
+ const Wrapper = ext.block !== false ? "div" : "span";
1305
+ const children = ext.markdown ? /* @__PURE__ */ jsx(
1306
+ ContentBody,
1307
+ {
1308
+ source: segment.content,
1309
+ format: "html",
1310
+ extensions,
1311
+ unsafe,
1312
+ depth: depth + 1,
1313
+ tail: i === last
1314
+ }
1315
+ ) : void 0;
1316
+ return /* @__PURE__ */ jsx(Wrapper, { "data-render-extension": segment.tag, children: /* @__PURE__ */ jsx(Component, { content: segment.content, attributes: segment.attributes, children }) }, i);
846
1317
  }) });
847
1318
  }
848
1319
  RenderedContent.displayName = "RenderedContent";
1320
+ ContentBody.displayName = "ContentBody";
849
1321
  function ContentRenderer({
850
1322
  value,
851
1323
  format = "auto",
852
1324
  lineSpacing = 1.6,
853
1325
  className,
854
1326
  extensions: instanceExtensions,
855
- unsafe = false
1327
+ unsafe = false,
1328
+ copyButtons = true,
1329
+ onCopy,
1330
+ clipboardWriter,
1331
+ streaming = false
856
1332
  }) {
857
1333
  const extensions = useMemo(
858
1334
  () => mergeExtensions(instanceExtensions),
859
1335
  [instanceExtensions]
860
1336
  );
861
- const html = useMemo(() => {
862
- const safe = value ?? "";
863
- const resolvedFormat = format === "auto" ? detectFormat(safe) : format;
864
- const raw = resolvedFormat === "markdown" ? markdownToRawHtml(safe) : safe;
865
- return unsafe ? raw : sanitizeHtml(raw);
866
- }, [value, format, unsafe]);
867
- const hasExtensions = extensions.length > 0;
868
- return /* @__PURE__ */ jsx(
1337
+ const safe = value ?? "";
1338
+ const resolvedFormat = format === "auto" ? detectFormat(safe) : format;
1339
+ const progressive = streaming === "progressive";
1340
+ const { source, openFence } = useMemo(() => {
1341
+ if (!streaming) return { source: safe, openFence: false };
1342
+ const tags = extensions.map((e) => e.tag);
1343
+ const markdown = resolvedFormat === "markdown";
1344
+ if (progressive) return streamingProgressive(safe, tags, markdown);
1345
+ return { source: safe.slice(0, streamingCut(safe, tags, markdown)), openFence: false };
1346
+ }, [safe, streaming, progressive, extensions, resolvedFormat]);
1347
+ const codeOn = copyButtons === true || typeof copyButtons === "object" && copyButtons.code !== false;
1348
+ const tableOn = copyButtons === true || typeof copyButtons === "object" && copyButtons.table !== false;
1349
+ const copyOn = codeOn || tableOn;
1350
+ const rootRef = useRef(null);
1351
+ const [announcement, setAnnouncement] = useState("");
1352
+ const announce = useCallback((message) => {
1353
+ setAnnouncement("");
1354
+ queueMicrotask(() => setAnnouncement(message));
1355
+ }, []);
1356
+ const copy = useCopyPipeline({ writer: clipboardWriter, onCopy, rootRef, announce });
1357
+ const copyContext = useMemo(
1358
+ () => ({ code: codeOn, table: tableOn, copy, holdTailCode: openFence }),
1359
+ [codeOn, tableOn, copy, openFence]
1360
+ );
1361
+ return /* @__PURE__ */ jsxs(
869
1362
  "div",
870
1363
  {
1364
+ ref: rootRef,
871
1365
  "data-react-fancy-content-renderer": "",
1366
+ "data-streaming": streaming ? "true" : void 0,
1367
+ "data-streaming-mode": progressive ? "progressive" : void 0,
872
1368
  style: { lineHeight: lineSpacing },
873
1369
  className: cn("text-sm", proseClasses, className),
874
- children: hasExtensions ? /* @__PURE__ */ jsx(RenderedContent, { html, extensions, unsafe }) : /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: html } })
1370
+ children: [
1371
+ /* @__PURE__ */ jsx(CopyContext.Provider, { value: copyContext, children: /* @__PURE__ */ jsx(
1372
+ ContentBody,
1373
+ {
1374
+ source,
1375
+ format: resolvedFormat,
1376
+ extensions,
1377
+ unsafe,
1378
+ tail: true
1379
+ }
1380
+ ) }),
1381
+ copyOn && /* @__PURE__ */ jsx(
1382
+ "span",
1383
+ {
1384
+ role: "status",
1385
+ "aria-live": "polite",
1386
+ "aria-atomic": "true",
1387
+ "data-react-fancy-content-renderer-live": "",
1388
+ className: "sr-only",
1389
+ children: announcement
1390
+ }
1391
+ )
1392
+ ]
875
1393
  }
876
1394
  );
877
1395
  }
878
1396
  ContentRenderer.displayName = "ContentRenderer";
879
1397
 
880
1398
  export { ContentRenderer, detectFormat, extensionEditorClasses, htmlToMarkdown, markdownToEditorHtml, mergeExtensions, proseClasses, registerExtension, registerExtensions, renderMarkdown };
881
- //# sourceMappingURL=chunk-L24LUSEE.js.map
882
- //# sourceMappingURL=chunk-L24LUSEE.js.map
1399
+ //# sourceMappingURL=chunk-2LQR4EAN.js.map
1400
+ //# sourceMappingURL=chunk-2LQR4EAN.js.map