@particle-academy/react-fancy 5.65.0 → 5.67.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 (106) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/dist/{PastePills.types-CDyotkUb.d.cts → PastePills.types-CpXzsuwJ.d.cts} +2 -0
  3. package/dist/{PastePills.types-CDyotkUb.d.ts → PastePills.types-CpXzsuwJ.d.ts} +2 -0
  4. package/dist/{PastedFilesInput-CrgNoEBd.d.ts → PastedFilesInput-Bt3KSTVN.d.ts} +1 -1
  5. package/dist/{PastedFilesInput-DQ1eZaHk.d.cts → PastedFilesInput-CUQeOeOA.d.cts} +1 -1
  6. package/dist/button.cjs +3 -4
  7. package/dist/button.js +1 -2
  8. package/dist/{chunk-COXHFZR6.js → chunk-23AVEYUP.js} +14 -8
  9. package/dist/chunk-23AVEYUP.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-ATIDUWY6.cjs → chunk-4S5DFN5N.cjs} +12 -12
  13. package/dist/{chunk-ATIDUWY6.cjs.map → chunk-4S5DFN5N.cjs.map} +1 -1
  14. package/dist/{chunk-CMC2TOEQ.cjs → chunk-6CAXLP7D.cjs} +6 -6
  15. package/dist/{chunk-CMC2TOEQ.cjs.map → chunk-6CAXLP7D.cjs.map} +1 -1
  16. package/dist/{chunk-TQGRXJ74.cjs → chunk-7FYRFNPX.cjs} +9 -9
  17. package/dist/{chunk-TQGRXJ74.cjs.map → chunk-7FYRFNPX.cjs.map} +1 -1
  18. package/dist/{chunk-MVVHUTXA.js → chunk-B4LVQ4QI.js} +3 -3
  19. package/dist/{chunk-MVVHUTXA.js.map → chunk-B4LVQ4QI.js.map} +1 -1
  20. package/dist/{chunk-QP35Z2WB.js → chunk-BL5IKSB3.js} +3 -3
  21. package/dist/{chunk-QP35Z2WB.js.map → chunk-BL5IKSB3.js.map} +1 -1
  22. package/dist/{chunk-SZH7TFME.js → chunk-DCAD2JPP.js} +4 -4
  23. package/dist/{chunk-SZH7TFME.js.map → chunk-DCAD2JPP.js.map} +1 -1
  24. package/dist/{chunk-MCQJFMDP.cjs → chunk-E5UU666U.cjs} +478 -36
  25. package/dist/chunk-E5UU666U.cjs.map +1 -0
  26. package/dist/{chunk-L24LUSEE.js → chunk-FXOT4P4P.js} +478 -36
  27. package/dist/chunk-FXOT4P4P.js.map +1 -0
  28. package/dist/{chunk-CAUEKGMR.js → chunk-HEY4SMS2.js} +4 -4
  29. package/dist/{chunk-CAUEKGMR.js.map → chunk-HEY4SMS2.js.map} +1 -1
  30. package/dist/{chunk-QNQNGARW.cjs → chunk-HFK5SBNM.cjs} +27 -9
  31. package/dist/chunk-HFK5SBNM.cjs.map +1 -0
  32. package/dist/{chunk-TLRRMYNV.js → chunk-OXERDFFF.js} +212 -4
  33. package/dist/chunk-OXERDFFF.js.map +1 -0
  34. package/dist/{chunk-ELFTEJBT.js → chunk-QSGAFKK2.js} +3 -3
  35. package/dist/{chunk-ELFTEJBT.js.map → chunk-QSGAFKK2.js.map} +1 -1
  36. package/dist/{chunk-SYEHWUM5.cjs → chunk-RALJMI7C.cjs} +4 -4
  37. package/dist/{chunk-SYEHWUM5.cjs.map → chunk-RALJMI7C.cjs.map} +1 -1
  38. package/dist/{chunk-UM4JXBRZ.cjs → chunk-S3NB6LDV.cjs} +4 -4
  39. package/dist/{chunk-UM4JXBRZ.cjs.map → chunk-S3NB6LDV.cjs.map} +1 -1
  40. package/dist/{chunk-C463C4MM.cjs → chunk-SAG6DHQK.cjs} +22 -16
  41. package/dist/chunk-SAG6DHQK.cjs.map +1 -0
  42. package/dist/{chunk-4XHG6Q42.cjs → chunk-U5LKNGHU.cjs} +21 -21
  43. package/dist/{chunk-4XHG6Q42.cjs.map → chunk-U5LKNGHU.cjs.map} +1 -1
  44. package/dist/{chunk-M52S3TFI.js → chunk-WFR7HKJL.js} +3 -3
  45. package/dist/{chunk-M52S3TFI.js.map → chunk-WFR7HKJL.js.map} +1 -1
  46. package/dist/{chunk-NILA2LZY.js → chunk-YIKXJWO4.js} +27 -9
  47. package/dist/chunk-YIKXJWO4.js.map +1 -0
  48. package/dist/composer.cjs +4 -4
  49. package/dist/composer.d.cts +2 -2
  50. package/dist/composer.d.ts +2 -2
  51. package/dist/composer.js +2 -2
  52. package/dist/content-renderer.cjs +7 -5
  53. package/dist/content-renderer.d.cts +56 -4
  54. package/dist/content-renderer.d.ts +56 -4
  55. package/dist/content-renderer.js +4 -2
  56. package/dist/editor.cjs +6 -5
  57. package/dist/editor.d.cts +1 -1
  58. package/dist/editor.d.ts +1 -1
  59. package/dist/editor.js +4 -3
  60. package/dist/{extensions-6PVd3-vB.d.cts → extensions-DiByZIpD.d.cts} +26 -3
  61. package/dist/{extensions-6PVd3-vB.d.ts → extensions-DiByZIpD.d.ts} +26 -3
  62. package/dist/{index-CCJVT9Hu.d.cts → index-DWtSViJ3.d.cts} +7 -1
  63. package/dist/{index-DRlltbdV.d.ts → index-XR5t8bTq.d.ts} +7 -1
  64. package/dist/index.cjs +47 -48
  65. package/dist/index.cjs.map +1 -1
  66. package/dist/index.d.cts +5 -5
  67. package/dist/index.d.ts +5 -5
  68. package/dist/index.js +10 -11
  69. package/dist/index.js.map +1 -1
  70. package/dist/input.cjs +3 -3
  71. package/dist/input.d.cts +1 -1
  72. package/dist/input.d.ts +1 -1
  73. package/dist/input.js +2 -2
  74. package/dist/inputs.cjs +3 -3
  75. package/dist/inputs.d.cts +1 -1
  76. package/dist/inputs.d.ts +1 -1
  77. package/dist/inputs.js +2 -2
  78. package/dist/json-editor.cjs +15 -16
  79. package/dist/json-editor.js +4 -5
  80. package/dist/magic-wand.cjs +3 -4
  81. package/dist/magic-wand.js +2 -3
  82. package/dist/paste-pills.cjs +14 -14
  83. package/dist/paste-pills.d.cts +3 -3
  84. package/dist/paste-pills.d.ts +3 -3
  85. package/dist/paste-pills.js +1 -1
  86. package/dist/prompt-input.cjs +6 -7
  87. package/dist/prompt-input.d.cts +2 -2
  88. package/dist/prompt-input.d.ts +2 -2
  89. package/dist/prompt-input.js +3 -4
  90. package/dist/reason-tag.cjs +3 -4
  91. package/dist/reason-tag.js +2 -3
  92. package/docs/ContentRenderer.md +138 -3
  93. package/docs/PromptInput.md +24 -1
  94. package/package.json +1 -1
  95. package/dist/chunk-6TBRBN7H.js +0 -212
  96. package/dist/chunk-6TBRBN7H.js.map +0 -1
  97. package/dist/chunk-C463C4MM.cjs.map +0 -1
  98. package/dist/chunk-C5YX5XVL.cjs.map +0 -1
  99. package/dist/chunk-COXHFZR6.js.map +0 -1
  100. package/dist/chunk-L24LUSEE.js.map +0 -1
  101. package/dist/chunk-MCQJFMDP.cjs.map +0 -1
  102. package/dist/chunk-NILA2LZY.js.map +0 -1
  103. package/dist/chunk-QNQNGARW.cjs.map +0 -1
  104. package/dist/chunk-TLRRMYNV.js.map +0 -1
  105. package/dist/chunk-X2FC7ODX.cjs +0 -215
  106. package/dist/chunk-X2FC7ODX.cjs.map +0 -1
@@ -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,493 @@ function extensionEditorClasses(extensions) {
811
832
  ].join(" ");
812
833
  }).join(" ");
813
834
  }
814
- function RenderedContent({
815
- html,
816
- extensions: instanceExtensions,
817
- unsafe = false
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 };
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 };
931
+ }
932
+ function splitMarkdownBlocks(src, tags) {
933
+ const names = tags.map((t) => t.toLowerCase());
934
+ if (names.length === 0) return src ? [{ type: "markdown", source: src }] : [];
935
+ const { tokens } = scan(src, names, true);
936
+ const blocks = [];
937
+ let cursor = 0;
938
+ let i = 0;
939
+ while (i < tokens.length) {
940
+ const open = tokens[i];
941
+ if (open.kind !== "open" || !open.lineStart || open.start < cursor) {
942
+ i++;
943
+ continue;
944
+ }
945
+ let depth = 0;
946
+ let closeIdx = -1;
947
+ for (let j = i; j < tokens.length; j++) {
948
+ const t = tokens[j];
949
+ if (t.name !== open.name) continue;
950
+ depth += t.kind === "open" ? 1 : -1;
951
+ if (depth === 0) {
952
+ closeIdx = j;
953
+ break;
954
+ }
955
+ }
956
+ if (closeIdx < 0) {
957
+ i++;
958
+ continue;
959
+ }
960
+ const close = tokens[closeIdx];
961
+ if (open.start > cursor) blocks.push({ type: "markdown", source: src.slice(cursor, open.start) });
962
+ blocks.push({ type: "tag", tag: open.name, attrs: open.attrs, body: src.slice(open.end, close.start) });
963
+ cursor = close.end;
964
+ i = closeIdx + 1;
965
+ }
966
+ if (cursor < src.length) blocks.push({ type: "markdown", source: src.slice(cursor) });
967
+ return blocks;
968
+ }
969
+ function streamingCut(src, tags, markdown) {
970
+ const names = tags.map((t) => t.toLowerCase());
971
+ const { tokens, openFenceStart } = scan(src, names, markdown);
972
+ let cut = src.length;
973
+ if (openFenceStart >= 0) cut = Math.min(cut, openFenceStart);
974
+ const stack = [];
975
+ for (const t of tokens) {
976
+ if (t.start >= cut) break;
977
+ if (t.kind === "open") {
978
+ stack.push(t);
979
+ continue;
980
+ }
981
+ for (let k = stack.length - 1; k >= 0; k--) {
982
+ if (stack[k].name === t.name) {
983
+ stack.length = k;
984
+ break;
985
+ }
986
+ }
987
+ }
988
+ if (stack.length) cut = Math.min(cut, stack[0].start);
989
+ const lastLineStart = src.lastIndexOf("\n", cut - 1) + 1;
990
+ const lastLine = src.slice(lastLineStart, cut);
991
+ if (markdown && /^ {0,3}(`{1,2}|~{1,2})$/.test(lastLine)) {
992
+ return Math.min(cut, lastLineStart);
993
+ }
994
+ if (names.length) {
995
+ const masked = markdown ? maskInlineCode(lastLine) : lastLine;
996
+ const lt = masked.lastIndexOf("<");
997
+ if (lt >= 0) {
998
+ const m = /^<(\/?)([A-Za-z][\w-]*)?([^>]*)$/.exec(masked.slice(lt));
999
+ if (m) {
1000
+ const name = (m[2] ?? "").toLowerCase();
1001
+ const rest = m[3];
1002
+ let partial = false;
1003
+ if (!name) partial = rest === "";
1004
+ else if (names.includes(name)) partial = rest === "" || /^\s/.test(rest);
1005
+ else partial = rest === "" && names.some((n) => n.startsWith(name));
1006
+ if (partial) cut = Math.min(cut, lastLineStart + lt);
1007
+ }
1008
+ }
1009
+ }
1010
+ return cut;
1011
+ }
1012
+ var FEEDBACK_MS = 2e3;
1013
+ var WRITE_TIMEOUT_MS = 5e3;
1014
+ var CopyContext = createContext(null);
1015
+ async function defaultClipboardWriter(payload) {
1016
+ const clip = typeof navigator !== "undefined" ? navigator.clipboard : void 0;
1017
+ if (!clip) throw new Error("Clipboard API unavailable");
1018
+ const Item = globalThis.ClipboardItem;
1019
+ if (payload.html && Item && typeof clip.write === "function") {
1020
+ try {
1021
+ await clip.write([
1022
+ new Item({
1023
+ "text/html": new Blob([payload.html], { type: "text/html" }),
1024
+ "text/plain": new Blob([payload.text], { type: "text/plain" })
1025
+ })
1026
+ ]);
1027
+ return;
1028
+ } catch {
1029
+ }
1030
+ }
1031
+ if (typeof clip.writeText !== "function") throw new Error("Clipboard writeText unavailable");
1032
+ await clip.writeText(payload.text);
1033
+ }
1034
+ async function runWriter(writer, payload) {
1035
+ let timer;
1036
+ try {
1037
+ const result = await Promise.race([
1038
+ Promise.resolve().then(() => writer(payload)),
1039
+ new Promise((resolve) => {
1040
+ timer = setTimeout(() => resolve(false), WRITE_TIMEOUT_MS);
1041
+ })
1042
+ ]);
1043
+ return result !== false;
1044
+ } catch {
1045
+ return false;
1046
+ } finally {
1047
+ if (timer) clearTimeout(timer);
1048
+ }
1049
+ }
1050
+ function codeText(pre, fromMarkdown) {
1051
+ const text = (pre.querySelector("code") ?? pre).textContent ?? "";
1052
+ return fromMarkdown && text.endsWith("\n") ? text.slice(0, -1) : text;
1053
+ }
1054
+ function tableTsv(table) {
1055
+ const rows = [];
1056
+ for (const tr of Array.from(table.querySelectorAll("tr"))) {
1057
+ if (tr.closest("table") !== table) continue;
1058
+ const cells = Array.from(tr.children).filter((c) => c.tagName === "TD" || c.tagName === "TH");
1059
+ rows.push(cells.map((c) => cellText(c)).join(" "));
1060
+ }
1061
+ return rows.join("\n");
1062
+ }
1063
+ function cellText(cell) {
1064
+ const clone = cell.cloneNode(true);
1065
+ for (const br of Array.from(clone.querySelectorAll("br"))) br.replaceWith(" ");
1066
+ return (clone.textContent ?? "").replace(/[\t\r\n]+/g, " ").replace(/ {2,}/g, " ").trim();
1067
+ }
1068
+ function HtmlChunk({ html, fromMarkdown }) {
1069
+ const ref = useRef(null);
1070
+ const ctx = useContext(CopyContext);
1071
+ const [targets, setTargets] = useState([]);
1072
+ const code = ctx?.code ?? false;
1073
+ const table = ctx?.table ?? false;
1074
+ const inner = useMemo(() => ({ __html: html }), [html]);
1075
+ useLayoutEffect(() => {
1076
+ const root = ref.current;
1077
+ if (!root || !code && !table) {
1078
+ setTargets([]);
1079
+ return;
1080
+ }
1081
+ const found = [];
1082
+ const selector = [code && "pre", table && "table"].filter(Boolean).join(",");
1083
+ for (const el of Array.from(root.querySelectorAll(selector))) {
1084
+ const kind = el.tagName === "PRE" ? "code" : "table";
1085
+ const wrapper = document.createElement("div");
1086
+ wrapper.setAttribute("data-react-fancy-content-copy-block", kind);
1087
+ wrapper.className = kind === "code" ? "group relative" : "group relative flex flex-col";
1088
+ const host = document.createElement("div");
1089
+ host.setAttribute("data-react-fancy-content-copy-host", "");
1090
+ host.className = kind === "code" ? "absolute right-2 top-2 z-10" : "flex justify-end";
1091
+ el.replaceWith(wrapper);
1092
+ if (kind === "table") wrapper.append(host, el);
1093
+ else wrapper.append(el, host);
1094
+ found.push({ kind, el, host });
1095
+ }
1096
+ setTargets(found);
1097
+ return () => {
1098
+ for (const t of found) {
1099
+ const wrapper = t.host.parentElement;
1100
+ if (wrapper?.parentNode) wrapper.replaceWith(t.el);
1101
+ }
1102
+ };
1103
+ }, [html, code, table]);
1104
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1105
+ /* @__PURE__ */ jsx("div", { ref, dangerouslySetInnerHTML: inner }),
1106
+ targets.map(
1107
+ (t, i) => createPortal(
1108
+ /* @__PURE__ */ jsx(CopyButton, { kind: t.kind, target: t.el, fromMarkdown }),
1109
+ t.host,
1110
+ `${i}`
1111
+ )
1112
+ )
1113
+ ] });
1114
+ }
1115
+ function CopyButton({
1116
+ kind,
1117
+ target,
1118
+ fromMarkdown
818
1119
  }) {
819
- const extensions = useMemo(
820
- () => mergeExtensions(instanceExtensions),
821
- [instanceExtensions]
1120
+ const ctx = useContext(CopyContext);
1121
+ const [state, setState] = useState("idle");
1122
+ const timer = useRef(void 0);
1123
+ const btn = useRef(null);
1124
+ useEffect(() => () => clearTimeout(timer.current), []);
1125
+ const onClick = useCallback(async () => {
1126
+ if (!ctx || !btn.current) return;
1127
+ const text = kind === "code" ? codeText(target, fromMarkdown) : tableTsv(target);
1128
+ const ok = await ctx.copy({ kind, text, html: target.outerHTML }, btn.current);
1129
+ setState(ok ? "copied" : "failed");
1130
+ clearTimeout(timer.current);
1131
+ timer.current = setTimeout(() => setState("idle"), FEEDBACK_MS);
1132
+ }, [ctx, kind, target, fromMarkdown]);
1133
+ const idleLabel = kind === "code" ? "Copy code" : "Copy table";
1134
+ const label = state === "copied" ? "Copied" : state === "failed" ? "Copy failed" : idleLabel;
1135
+ return /* @__PURE__ */ jsx(
1136
+ Button,
1137
+ {
1138
+ ref: btn,
1139
+ type: "button",
1140
+ size: "xs",
1141
+ variant: "ghost",
1142
+ icon: state === "copied" ? "check" : state === "failed" ? "x" : "copy",
1143
+ "aria-label": label,
1144
+ title: label,
1145
+ "data-react-fancy-content-copy": kind,
1146
+ "data-copy-state": state,
1147
+ onClick,
1148
+ 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"
1149
+ }
822
1150
  );
823
- const segments = useMemo(
824
- () => parseSegments(html, extensions),
825
- [html, extensions]
1151
+ }
1152
+ function useCopyPipeline({
1153
+ writer,
1154
+ onCopy,
1155
+ rootRef,
1156
+ announce
1157
+ }) {
1158
+ const latest = useRef({ writer, onCopy, announce });
1159
+ latest.current = { writer, onCopy, announce };
1160
+ return useCallback(
1161
+ async (payload, button) => {
1162
+ const { writer: w, onCopy: report, announce: say } = latest.current;
1163
+ const ok = await runWriter(w ?? defaultClipboardWriter, payload);
1164
+ say(ok ? "Copied" : "Copy failed");
1165
+ const all = rootRef.current ? Array.from(rootRef.current.querySelectorAll("[data-react-fancy-content-copy]")) : [];
1166
+ report?.({ ...payload, index: all.indexOf(button), ok });
1167
+ return ok;
1168
+ },
1169
+ [rootRef]
826
1170
  );
1171
+ }
1172
+ var MAX_DEPTH = 8;
1173
+ function ContentBody({ source, format, extensions, unsafe, depth = 0 }) {
1174
+ const active = depth >= MAX_DEPTH ? [] : extensions;
1175
+ const blocks = useMemo(() => {
1176
+ if (format !== "markdown") return null;
1177
+ const mdTags = active.filter((e) => e.markdown).map((e) => e.tag);
1178
+ return splitMarkdownBlocks(source, mdTags);
1179
+ }, [source, format, active]);
1180
+ if (!blocks) {
1181
+ const html = unsafe ? source : sanitizeHtml(source);
1182
+ return /* @__PURE__ */ jsx(RenderedContent, { html, extensions: active, unsafe, fromMarkdown: false, depth });
1183
+ }
1184
+ return /* @__PURE__ */ jsx(Fragment, { children: blocks.map((block, i) => {
1185
+ if (block.type === "markdown") {
1186
+ const raw = markdownToRawHtml(block.source);
1187
+ const html = unsafe ? raw : sanitizeHtml(raw);
1188
+ return /* @__PURE__ */ jsx(
1189
+ RenderedContent,
1190
+ {
1191
+ html,
1192
+ extensions: active,
1193
+ unsafe,
1194
+ fromMarkdown: true,
1195
+ depth
1196
+ },
1197
+ i
1198
+ );
1199
+ }
1200
+ const ext = active.find((e) => e.tag.toLowerCase() === block.tag);
1201
+ if (!ext) return null;
1202
+ const Component = ext.component;
1203
+ const Wrapper = ext.block !== false ? "div" : "span";
1204
+ return /* @__PURE__ */ jsx(Wrapper, { "data-render-extension": block.tag, children: /* @__PURE__ */ jsx(Component, { content: block.body, attributes: tagAttributes(block.attrs, unsafe), children: /* @__PURE__ */ jsx(
1205
+ ContentBody,
1206
+ {
1207
+ source: block.body,
1208
+ format: "markdown",
1209
+ extensions: active,
1210
+ unsafe,
1211
+ depth: depth + 1
1212
+ }
1213
+ ) }) }, i);
1214
+ }) });
1215
+ }
1216
+ function RenderedContent({ html, extensions, unsafe, fromMarkdown, depth }) {
1217
+ const segments = useMemo(() => parseSegments(html, extensions), [html, extensions]);
827
1218
  const renderHtml = (content) => unsafe ? content : sanitizeHtml(content);
1219
+ if (segments.length === 0) return null;
828
1220
  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;
1221
+ return /* @__PURE__ */ jsx(HtmlChunk, { html: renderHtml(segments[0].content), fromMarkdown });
833
1222
  }
834
1223
  return /* @__PURE__ */ jsx(Fragment, { children: segments.map((segment, i) => {
835
1224
  if (segment.type === "html") {
836
- return segment.content ? /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderHtml(segment.content) } }, i) : null;
1225
+ return segment.content ? /* @__PURE__ */ jsx(HtmlChunk, { html: renderHtml(segment.content), fromMarkdown }, i) : null;
837
1226
  }
838
- const ext = extensions.find(
839
- (e) => e.tag.toLowerCase() === segment.tag
840
- );
1227
+ const ext = extensions.find((e) => e.tag.toLowerCase() === segment.tag);
841
1228
  if (!ext) return null;
842
1229
  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);
1230
+ const Wrapper = ext.block !== false ? "div" : "span";
1231
+ const children = ext.markdown ? /* @__PURE__ */ jsx(
1232
+ ContentBody,
1233
+ {
1234
+ source: segment.content,
1235
+ format: "html",
1236
+ extensions,
1237
+ unsafe,
1238
+ depth: depth + 1
1239
+ }
1240
+ ) : void 0;
1241
+ return /* @__PURE__ */ jsx(Wrapper, { "data-render-extension": segment.tag, children: /* @__PURE__ */ jsx(Component, { content: segment.content, attributes: segment.attributes, children }) }, i);
846
1242
  }) });
847
1243
  }
848
1244
  RenderedContent.displayName = "RenderedContent";
1245
+ ContentBody.displayName = "ContentBody";
849
1246
  function ContentRenderer({
850
1247
  value,
851
1248
  format = "auto",
852
1249
  lineSpacing = 1.6,
853
1250
  className,
854
1251
  extensions: instanceExtensions,
855
- unsafe = false
1252
+ unsafe = false,
1253
+ copyButtons = true,
1254
+ onCopy,
1255
+ clipboardWriter,
1256
+ streaming = false
856
1257
  }) {
857
1258
  const extensions = useMemo(
858
1259
  () => mergeExtensions(instanceExtensions),
859
1260
  [instanceExtensions]
860
1261
  );
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(
1262
+ const safe = value ?? "";
1263
+ const resolvedFormat = format === "auto" ? detectFormat(safe) : format;
1264
+ const source = useMemo(() => {
1265
+ if (!streaming) return safe;
1266
+ const cut = streamingCut(
1267
+ safe,
1268
+ extensions.map((e) => e.tag),
1269
+ resolvedFormat === "markdown"
1270
+ );
1271
+ return safe.slice(0, cut);
1272
+ }, [safe, streaming, extensions, resolvedFormat]);
1273
+ const codeOn = copyButtons === true || typeof copyButtons === "object" && copyButtons.code !== false;
1274
+ const tableOn = copyButtons === true || typeof copyButtons === "object" && copyButtons.table !== false;
1275
+ const copyOn = codeOn || tableOn;
1276
+ const rootRef = useRef(null);
1277
+ const [announcement, setAnnouncement] = useState("");
1278
+ const announce = useCallback((message) => {
1279
+ setAnnouncement("");
1280
+ queueMicrotask(() => setAnnouncement(message));
1281
+ }, []);
1282
+ const copy = useCopyPipeline({ writer: clipboardWriter, onCopy, rootRef, announce });
1283
+ const copyContext = useMemo(
1284
+ () => ({ code: codeOn, table: tableOn, copy }),
1285
+ [codeOn, tableOn, copy]
1286
+ );
1287
+ return /* @__PURE__ */ jsxs(
869
1288
  "div",
870
1289
  {
1290
+ ref: rootRef,
871
1291
  "data-react-fancy-content-renderer": "",
1292
+ "data-streaming": streaming ? "true" : void 0,
872
1293
  style: { lineHeight: lineSpacing },
873
1294
  className: cn("text-sm", proseClasses, className),
874
- children: hasExtensions ? /* @__PURE__ */ jsx(RenderedContent, { html, extensions, unsafe }) : /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: html } })
1295
+ children: [
1296
+ /* @__PURE__ */ jsx(CopyContext.Provider, { value: copyContext, children: /* @__PURE__ */ jsx(
1297
+ ContentBody,
1298
+ {
1299
+ source,
1300
+ format: resolvedFormat,
1301
+ extensions,
1302
+ unsafe
1303
+ }
1304
+ ) }),
1305
+ copyOn && /* @__PURE__ */ jsx(
1306
+ "span",
1307
+ {
1308
+ role: "status",
1309
+ "aria-live": "polite",
1310
+ "aria-atomic": "true",
1311
+ "data-react-fancy-content-renderer-live": "",
1312
+ className: "sr-only",
1313
+ children: announcement
1314
+ }
1315
+ )
1316
+ ]
875
1317
  }
876
1318
  );
877
1319
  }
878
1320
  ContentRenderer.displayName = "ContentRenderer";
879
1321
 
880
1322
  export { ContentRenderer, detectFormat, extensionEditorClasses, htmlToMarkdown, markdownToEditorHtml, mergeExtensions, proseClasses, registerExtension, registerExtensions, renderMarkdown };
881
- //# sourceMappingURL=chunk-L24LUSEE.js.map
882
- //# sourceMappingURL=chunk-L24LUSEE.js.map
1323
+ //# sourceMappingURL=chunk-FXOT4P4P.js.map
1324
+ //# sourceMappingURL=chunk-FXOT4P4P.js.map