@ai-react-markdown/mantine 2.11.0 → 2.13.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.
package/dist/index.js CHANGED
@@ -24,8 +24,14 @@ var MantineAIMDefaultExtraStyles = ({ children }) => {
24
24
  var DefaultExtraStyles_default = MantineAIMDefaultExtraStyles;
25
25
 
26
26
  // src/components/customized/PreCode.tsx
27
- import { memo as memo3, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
28
- import { CodeHighlight, CodeHighlightTabs as CodeHighlightTabs2 } from "@mantine/code-highlight";
27
+ import { createContext, memo as memo3, useContext, useEffect as useEffect3, useMemo as useMemo2, useRef as useRef2, useState as useState3 } from "react";
28
+ import {
29
+ CodeHighlight,
30
+ CodeHighlightTabs as CodeHighlightTabs2,
31
+ CodeHighlightControl as CodeHighlightControl2,
32
+ CodeHighlightAdapterProvider,
33
+ useHighlight
34
+ } from "@mantine/code-highlight";
29
35
  import { useAIMarkdownState as useAIMarkdownState2, useAIMarkdownTheme as useAIMarkdownTheme2 } from "@ai-react-markdown/core";
30
36
 
31
37
  // src/hooks/useMantineCodeBlockOptions.ts
@@ -35,7 +41,10 @@ import { useAIMarkdownBehaviors } from "@ai-react-markdown/core";
35
41
  // src/defs.tsx
36
42
  var defaultMantineCodeBlockOptions = Object.freeze({
37
43
  defaultExpanded: true,
38
- autoDetectUnknownLanguage: false
44
+ autoDetectUnknownLanguage: false,
45
+ formatJson: true,
46
+ expandNestedJson: true,
47
+ highlightIntervalMs: 50
39
48
  });
40
49
 
41
50
  // src/hooks/useMantineCodeBlockOptions.ts
@@ -48,10 +57,50 @@ function useMantineCodeBlockOptions() {
48
57
  const value = group?.[key];
49
58
  if (value !== void 0) resolved[key] = value;
50
59
  }
60
+ if (!Number.isFinite(resolved.highlightIntervalMs) || resolved.highlightIntervalMs < 0) {
61
+ resolved.highlightIntervalMs = defaultMantineCodeBlockOptions.highlightIntervalMs;
62
+ }
51
63
  return resolved;
52
64
  }, [group]);
53
65
  }
54
66
 
67
+ // src/components/customized/MermaidCode/renderQueue.ts
68
+ function createRenderQueue() {
69
+ const pending = /* @__PURE__ */ new Map();
70
+ let running = false;
71
+ async function drain() {
72
+ if (running) return;
73
+ running = true;
74
+ try {
75
+ while (pending.size) {
76
+ const [owner, job] = pending.entries().next().value;
77
+ pending.delete(owner);
78
+ try {
79
+ await job.run();
80
+ job.resolve();
81
+ } catch (error) {
82
+ job.reject(error);
83
+ }
84
+ }
85
+ } finally {
86
+ running = false;
87
+ }
88
+ }
89
+ return {
90
+ enqueue(owner, run) {
91
+ pending.get(owner)?.resolve();
92
+ const promise = new Promise((resolve, reject) => pending.set(owner, { run, resolve, reject }));
93
+ void drain();
94
+ return promise;
95
+ },
96
+ cancel(owner) {
97
+ pending.get(owner)?.resolve();
98
+ pending.delete(owner);
99
+ }
100
+ };
101
+ }
102
+ var mermaidRenderQueue = createRenderQueue();
103
+
55
104
  // src/components/customized/MermaidCode/index.tsx
56
105
  import { memo as memo2, useEffect, useRef, useState, useCallback } from "react";
57
106
  import { CodeHighlightControl, CodeHighlightTabs } from "@mantine/code-highlight";
@@ -173,14 +222,7 @@ var MantineAIMMermaidCode = memo2((props) => {
173
222
  if (!ref.current || cancelled || renderVersion !== renderVersionRef.current) {
174
223
  return;
175
224
  }
176
- let rendered = await mermaid.render(generateMermaidUUID(), props.code);
177
- if (initializedTheme !== (isDark ? "dark" : "light")) {
178
- if (!ref.current || cancelled || renderVersion !== renderVersionRef.current) {
179
- return;
180
- }
181
- ensureMermaidInitialized(mermaid, isDark);
182
- rendered = await mermaid.render(generateMermaidUUID(), props.code);
183
- }
225
+ const rendered = await mermaid.render(generateMermaidUUID(), props.code);
184
226
  const { svg, bindFunctions, diagramType } = rendered;
185
227
  if (!ref.current || cancelled || renderVersion !== renderVersionRef.current) {
186
228
  return;
@@ -208,9 +250,11 @@ var MantineAIMMermaidCode = memo2((props) => {
208
250
  applyView({ kind: "error" });
209
251
  }
210
252
  };
211
- void renderMermaid();
253
+ const owner = renderVersionRef;
254
+ void mermaidRenderQueue.enqueue(owner, renderMermaid);
212
255
  return () => {
213
256
  cancelled = true;
257
+ mermaidRenderQueue.cancel(owner);
214
258
  if (retryTimer !== void 0) clearTimeout(retryTimer);
215
259
  };
216
260
  }, [props.code, isDark, showOriginalCode, streaming, loadAttempt]);
@@ -361,7 +405,141 @@ MantineAIMMermaidCode.displayName = "MantineAIMMermaidCode";
361
405
  var MermaidCode_default = MantineAIMMermaidCode;
362
406
 
363
407
  // src/components/customized/PreCode.tsx
364
- import { jsx as jsx4 } from "react/jsx-runtime";
408
+ import { CopyButton as CopyButton2 } from "@mantine/core";
409
+
410
+ // src/components/customized/formatJson.ts
411
+ function prettyPrintJson(text, expandNested = true) {
412
+ try {
413
+ return format(text, expandNested, 0);
414
+ } catch {
415
+ return text;
416
+ }
417
+ }
418
+ function format(text, expandNested, baseIndent) {
419
+ JSON.parse(text);
420
+ const tokens = text.match(/"(?:\\[\s\S]|[^"\\])*"|-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?|true|false|null|[{}[\],:]/g) ?? [];
421
+ const out = [];
422
+ let indent = baseIndent;
423
+ const newline = () => out.push("\n", " ".repeat(indent));
424
+ for (let i = 0; i < tokens.length; i++) {
425
+ const token = tokens[i];
426
+ if (token === "{" || token === "[") {
427
+ out.push(token);
428
+ if (tokens[i + 1] !== (token === "{" ? "}" : "]")) {
429
+ indent++;
430
+ newline();
431
+ }
432
+ } else if (token === "}" || token === "]") {
433
+ if (tokens[i - 1] !== (token === "}" ? "{" : "[")) {
434
+ indent--;
435
+ newline();
436
+ }
437
+ out.push(token);
438
+ } else if (token === ",") {
439
+ out.push(",");
440
+ newline();
441
+ } else if (token === ":") {
442
+ out.push(": ");
443
+ } else if (token.startsWith('"')) {
444
+ const value = JSON.parse(token);
445
+ const trimmed = value.trim();
446
+ if (expandNested && tokens[i + 1] !== ":" && /^[{[]/.test(trimmed) && indent < 100) {
447
+ try {
448
+ out.push(format(trimmed, true, indent));
449
+ continue;
450
+ } catch {
451
+ }
452
+ }
453
+ out.push(JSON.stringify(value));
454
+ } else {
455
+ out.push(token);
456
+ }
457
+ }
458
+ return out.join("");
459
+ }
460
+
461
+ // src/components/customized/jsonCompleteness.ts
462
+ function createJsonCompletenessScanner() {
463
+ let previous = "";
464
+ let depth = 0;
465
+ let quoted = false;
466
+ let escaped = false;
467
+ let invalid = false;
468
+ let last = "";
469
+ return (text) => {
470
+ let from = previous.length;
471
+ if (!text.startsWith(previous)) {
472
+ from = 0;
473
+ depth = 0;
474
+ quoted = escaped = invalid = false;
475
+ last = "";
476
+ }
477
+ previous = text;
478
+ for (let i = from; i < text.length; i++) {
479
+ const c = text[i];
480
+ if (!/\s/.test(c)) last = c;
481
+ if (quoted) {
482
+ if (escaped) escaped = false;
483
+ else if (c === "\\") escaped = true;
484
+ else if (c === '"') quoted = false;
485
+ } else if (c === '"') quoted = true;
486
+ else if (c === "{" || c === "[") depth++;
487
+ else if (c === "}" || c === "]") {
488
+ depth--;
489
+ if (depth < 0) invalid = true;
490
+ }
491
+ }
492
+ return !invalid && !quoted && depth === 0 && (last === "}" || last === "]");
493
+ };
494
+ }
495
+
496
+ // src/components/customized/useCodeFrame.ts
497
+ import { useEffect as useEffect2, useState as useState2 } from "react";
498
+
499
+ // src/components/customized/codeFrame.ts
500
+ function createCodeFrame(initial, publish) {
501
+ let shown = initial;
502
+ let latest = initial;
503
+ let delay = 0;
504
+ let timer;
505
+ const cancel = () => {
506
+ if (timer !== void 0) clearTimeout(timer);
507
+ timer = void 0;
508
+ };
509
+ const flush = () => {
510
+ cancel();
511
+ if (shown.code === latest.code && shown.language === latest.language) return;
512
+ shown = latest;
513
+ publish(shown);
514
+ };
515
+ return {
516
+ update(next, streaming, interval) {
517
+ const replaced = next.language !== latest.language || !next.code.startsWith(latest.code);
518
+ latest = next;
519
+ if (delay !== interval) cancel();
520
+ delay = interval;
521
+ if (!streaming || interval === 0 || replaced) return flush();
522
+ if (shown.code === next.code && shown.language === next.language) return cancel();
523
+ timer ??= setTimeout(flush, interval);
524
+ },
525
+ // Cancellation is reversible: Strict Mode replays the update effect.
526
+ dispose: cancel
527
+ };
528
+ }
529
+
530
+ // src/components/customized/useCodeFrame.ts
531
+ function useCodeFrame(code, language, streaming, interval) {
532
+ const [frame, setFrame] = useState2(() => ({ code, language }));
533
+ const [controller] = useState2(() => createCodeFrame({ code, language }, setFrame));
534
+ useEffect2(() => {
535
+ controller.update({ code, language }, streaming, interval);
536
+ }, [controller, code, language, streaming, interval]);
537
+ useEffect2(() => () => controller.dispose(), [controller]);
538
+ return !streaming || interval === 0 || language !== frame.language || !code.startsWith(frame.code) ? code : frame.code;
539
+ }
540
+
541
+ // src/components/customized/PreCode.tsx
542
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
365
543
  var hljsAutoPromise = null;
366
544
  var loadHljsForAutoDetect = () => {
367
545
  hljsAutoPromise ??= import("highlight.js").then(
@@ -377,14 +555,20 @@ var AUTODETECT_MIN_CHARS = 32;
377
555
  var HLJS_LOAD_RETRIES = 3;
378
556
  var HLJS_LOAD_RETRY_MS = 1500;
379
557
  function useAutoDetectedLanguage(codeText, enabled, streaming) {
380
- const [detected, setDetected] = useState2(
558
+ const [detected, setDetected] = useState3(
381
559
  null
382
560
  );
383
- const [loadAttempt, setLoadAttempt] = useState2(0);
561
+ const [loadAttempt, setLoadAttempt] = useState3(0);
384
562
  const loadFailuresRef = useRef2(0);
385
- useEffect2(() => {
386
- if (!enabled) return;
387
- const prior = detected !== null && codeText.length < detected.atLength ? null : detected;
563
+ const previousTextRef = useRef2(codeText);
564
+ useEffect3(() => {
565
+ const appended = codeText.startsWith(previousTextRef.current);
566
+ previousTextRef.current = codeText;
567
+ if (!enabled) {
568
+ if (detected) setDetected(null);
569
+ return;
570
+ }
571
+ const prior = !appended || detected !== null && codeText.length < detected.atLength ? null : detected;
388
572
  if (prior !== detected) setDetected(null);
389
573
  if (codeText.length < AUTODETECT_MIN_CHARS) return;
390
574
  const due = prior === null || // Not streaming: the verdict must be for THIS text (end-of-stream, or a
@@ -423,55 +607,92 @@ function preloadMantineCodeAssets() {
423
607
  () => void 0
424
608
  );
425
609
  }
426
- function prettyPrintJson(text) {
427
- let parsed;
428
- try {
429
- parsed = JSON.parse(text);
430
- } catch {
431
- return text;
432
- }
433
- return JSON.stringify(expandNestedJson(parsed), null, 2);
610
+ var RawCodeContext = createContext("");
611
+ function RawCodeCopy() {
612
+ const code = useContext(RawCodeContext);
613
+ return /* @__PURE__ */ jsx4(CopyButton2, { value: code, children: ({ copied, copy }) => /* @__PURE__ */ jsx4(
614
+ CodeHighlightControl2,
615
+ {
616
+ tooltipLabel: copied ? "Copied" : "Copy",
617
+ "aria-label": copied ? "Copied" : "Copy code",
618
+ onClick: copy,
619
+ children: copied ? "\u2713" : /* @__PURE__ */ jsxs2(
620
+ "svg",
621
+ {
622
+ width: "16",
623
+ height: "16",
624
+ viewBox: "0 0 24 24",
625
+ fill: "none",
626
+ stroke: "currentColor",
627
+ strokeWidth: "1.5",
628
+ "aria-hidden": "true",
629
+ children: [
630
+ /* @__PURE__ */ jsx4("rect", { x: "8", y: "8", width: "12", height: "12", rx: "2" }),
631
+ /* @__PURE__ */ jsx4("path", { d: "M16 8V4H4v12h4" })
632
+ ]
633
+ }
634
+ )
635
+ }
636
+ ) });
434
637
  }
435
- function expandNestedJson(value) {
436
- if (typeof value === "string") {
437
- const t = value.trim();
438
- if (t.length > 1 && (t[0] === "{" || t[0] === "[")) {
439
- try {
440
- const inner = JSON.parse(t);
441
- if (inner !== null && typeof inner === "object") return expandNestedJson(inner);
442
- } catch {
638
+ function createCachedHighlightAdapter(highlight) {
639
+ let previous;
640
+ let result;
641
+ return {
642
+ getHighlighter: () => (input) => {
643
+ if (!previous || input.code !== previous.code || input.language !== previous.language || input.colorScheme !== previous.colorScheme) {
644
+ result = highlight(input);
645
+ previous = input;
443
646
  }
647
+ return result;
444
648
  }
445
- return value;
446
- }
447
- if (Array.isArray(value)) return value.map(expandNestedJson);
448
- if (value !== null && typeof value === "object") {
449
- const out = /* @__PURE__ */ Object.create(null);
450
- for (const [k, v] of Object.entries(value)) out[k] = expandNestedJson(v);
451
- return out;
452
- }
453
- return value;
649
+ };
454
650
  }
455
- function jsonLooksComplete(text) {
456
- if (!/[}\]]\s*$/.test(text)) return false;
457
- let depth = 0;
458
- let inString = false;
459
- for (let i = 0; i < text.length; i++) {
460
- const c = text.charCodeAt(i);
461
- if (inString) {
462
- if (c === 92) i += 1;
463
- else if (c === 34) inString = false;
464
- } else if (c === 34) {
465
- inString = true;
466
- } else if (c === 123 || c === 91) {
467
- depth += 1;
468
- } else if (c === 125 || c === 93) {
469
- depth -= 1;
470
- if (depth < 0) return false;
651
+ var OrdinaryCodeHighlight = memo3(function OrdinaryCodeHighlight2({
652
+ code,
653
+ language,
654
+ fileName,
655
+ fontSize,
656
+ defaultExpanded
657
+ }) {
658
+ const highlight = useHighlight();
659
+ const adapter = useMemo2(() => createCachedHighlightAdapter(highlight), [highlight]);
660
+ return /* @__PURE__ */ jsx4(CodeHighlightAdapterProvider, { adapter, children: fileName === "unknown" ? /* @__PURE__ */ jsx4(
661
+ CodeHighlight,
662
+ {
663
+ mb: 15,
664
+ fz: fontSize,
665
+ w: "100%",
666
+ code,
667
+ withBorder: true,
668
+ withExpandButton: true,
669
+ defaultExpanded,
670
+ maxCollapsedHeight: "320px",
671
+ withCopyButton: false,
672
+ controls: [/* @__PURE__ */ jsx4(RawCodeCopy, {}, "copy")]
471
673
  }
472
- }
473
- return depth === 0 && !inString;
474
- }
674
+ ) : /* @__PURE__ */ jsx4(
675
+ CodeHighlightTabs2,
676
+ {
677
+ mb: 15,
678
+ fz: fontSize,
679
+ w: "100%",
680
+ code: [
681
+ {
682
+ fileName,
683
+ code,
684
+ language
685
+ }
686
+ ],
687
+ withBorder: true,
688
+ withExpandButton: true,
689
+ defaultExpanded,
690
+ maxCollapsedHeight: "320px",
691
+ withCopyButton: false,
692
+ controls: [/* @__PURE__ */ jsx4(RawCodeCopy, {}, "copy")]
693
+ }
694
+ ) });
695
+ });
475
696
  var SpecialCodeLanguage = /* @__PURE__ */ ((SpecialCodeLanguage2) => {
476
697
  SpecialCodeLanguage2["Mermaid"] = "mermaid";
477
698
  return SpecialCodeLanguage2;
@@ -481,7 +702,7 @@ var MantineAIMPreCode = memo3(
481
702
  (props) => {
482
703
  const { fontSize } = useAIMarkdownTheme2();
483
704
  const { streaming } = useAIMarkdownState2();
484
- const { autoDetectUnknownLanguage, defaultExpanded } = useMantineCodeBlockOptions();
705
+ const { autoDetectUnknownLanguage, defaultExpanded, formatJson, expandNestedJson, highlightIntervalMs } = useMantineCodeBlockOptions();
485
706
  const detectedLanguage = useAutoDetectedLanguage(
486
707
  props.codeText,
487
708
  autoDetectUnknownLanguage && !props.existLanguage,
@@ -493,44 +714,25 @@ var MantineAIMPreCode = memo3(
493
714
  [codeLanguage]
494
715
  );
495
716
  const isSpecialCodeBlock = SPECIAL_LANGUAGES.has(codeLanguage);
717
+ const [scanJson] = useState3(createJsonCompletenessScanner);
718
+ const jsonComplete = useMemo2(
719
+ () => usedCodeLanguage === "json" && formatJson && streaming ? scanJson(props.codeText) : false,
720
+ [usedCodeLanguage, formatJson, streaming, scanJson, props.codeText]
721
+ );
496
722
  const normalCodeBlockContent = useMemo2(() => {
497
723
  if (isSpecialCodeBlock) return null;
498
724
  let usedCodeStr = props.codeText;
499
- if (usedCodeStr && usedCodeLanguage === "json" && (!streaming || jsonLooksComplete(usedCodeStr))) {
500
- usedCodeStr = prettyPrintJson(usedCodeStr);
725
+ if (formatJson && usedCodeStr && usedCodeLanguage === "json" && (!streaming || jsonComplete)) {
726
+ usedCodeStr = prettyPrintJson(usedCodeStr, expandNestedJson);
501
727
  }
502
- return usedFileName === "unknown" ? /* @__PURE__ */ jsx4(
503
- CodeHighlight,
504
- {
505
- mb: 15,
506
- fz: fontSize,
507
- w: "100%",
508
- code: usedCodeStr,
509
- withBorder: true,
510
- withExpandButton: true,
511
- defaultExpanded,
512
- maxCollapsedHeight: "320px"
513
- }
514
- ) : /* @__PURE__ */ jsx4(
515
- CodeHighlightTabs2,
516
- {
517
- mb: 15,
518
- fz: fontSize,
519
- w: "100%",
520
- code: [
521
- {
522
- fileName: usedFileName,
523
- code: usedCodeStr,
524
- language: usedCodeLanguage
525
- }
526
- ],
527
- withBorder: true,
528
- withExpandButton: true,
529
- defaultExpanded,
530
- maxCollapsedHeight: "320px"
531
- }
532
- );
533
- }, [isSpecialCodeBlock, props.codeText, usedCodeLanguage, usedFileName, fontSize, defaultExpanded, streaming]);
728
+ return usedCodeStr;
729
+ }, [isSpecialCodeBlock, props.codeText, usedCodeLanguage, streaming, formatJson, expandNestedJson, jsonComplete]);
730
+ const displayedCode = useCodeFrame(
731
+ normalCodeBlockContent ?? "",
732
+ usedCodeLanguage,
733
+ streaming && !isSpecialCodeBlock,
734
+ highlightIntervalMs
735
+ );
534
736
  const specialCodeBlockContent = useMemo2(() => {
535
737
  switch (codeLanguage) {
536
738
  case "mermaid" /* Mermaid */:
@@ -539,7 +741,16 @@ var MantineAIMPreCode = memo3(
539
741
  return null;
540
742
  }
541
743
  }, [codeLanguage, props.codeText]);
542
- return isSpecialCodeBlock ? specialCodeBlockContent : normalCodeBlockContent;
744
+ return isSpecialCodeBlock ? specialCodeBlockContent : /* @__PURE__ */ jsx4(RawCodeContext.Provider, { value: props.codeText, children: /* @__PURE__ */ jsx4(
745
+ OrdinaryCodeHighlight,
746
+ {
747
+ code: displayedCode,
748
+ language: usedCodeLanguage,
749
+ fileName: usedFileName,
750
+ fontSize,
751
+ defaultExpanded
752
+ }
753
+ ) });
543
754
  }
544
755
  );
545
756
  MantineAIMPreCode.displayName = "MantineAIMPreCode";
@@ -555,13 +766,14 @@ var MANTINE_STABILITY_TABLE = {
555
766
  var DefaultCustomComponents = {
556
767
  pre: ({ node, ...usefulProps }) => {
557
768
  const code = node?.children[0];
558
- if (!code || code.type !== "element" || code.tagName !== "code" || !code.position) {
769
+ if (!code || code.type !== "element" || code.tagName !== "code" || !code.position || node?.children.length !== 1 || code.children.some((child) => !("value" in child) || typeof child.value !== "string") || Object.keys(code.properties ?? {}).some((key2) => key2 !== "className") || Object.keys(node.properties ?? {}).length > 0) {
559
770
  return /* @__PURE__ */ jsx5("pre", { ...usefulProps });
560
771
  }
561
772
  const key = `pre-code-${node?.position?.start?.offset || 0}`;
562
773
  const classNames = code.properties?.className;
563
774
  const classList = Array.isArray(classNames) ? classNames.filter((c) => typeof c === "string") : typeof classNames === "string" ? classNames.split(/\s+/) : [];
564
775
  const detectedLanguage = classList.find((className) => className.startsWith("language-"))?.substring("language-".length);
776
+ if (classList.some((className) => !className.startsWith("language-"))) return /* @__PURE__ */ jsx5("pre", { ...usefulProps });
565
777
  const codeText = code.children.map((child) => child.value ?? "").join("");
566
778
  return /* @__PURE__ */ jsx5(PreCode_default, { codeText, existLanguage: detectedLanguage }, key);
567
779
  }