@readme/markdown 13.7.1 → 13.7.3

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.
@@ -1,7 +1,7 @@
1
1
  import type { Mermaid } from 'mermaid';
2
2
 
3
3
  import syntaxHighlighterUtils from '@readme/syntax-highlighter/utils';
4
- import React, { useContext, useEffect } from 'react';
4
+ import React, { useContext, useEffect, useRef } from 'react';
5
5
 
6
6
  import ThemeContext from '../../contexts/Theme';
7
7
  import useHydrated from '../../hooks/useHydrated';
@@ -10,6 +10,47 @@ let mermaid: Mermaid;
10
10
 
11
11
  const { uppercase } = syntaxHighlighterUtils;
12
12
 
13
+ // Module-level queue that batches mermaid nodes across all CodeTabs instances into a
14
+ // single mermaid.run() call. This is necessary because mermaid generates SVG element IDs
15
+ // using Date.now(), which collides when multiple diagrams render in the same millisecond.
16
+ // Colliding IDs cause diagrams to overlap or break layout.
17
+ //
18
+ // Why not use `deterministicIDSeed` with a unique ID per diagram? Mermaid's implementation
19
+ // only uses seed.length (not the seed value) to compute the starting ID, so every UUID
20
+ // (36 chars) produces the same `mermaid-36` prefix — the collision remains.
21
+ // See: https://github.com/mermaid-js/mermaid/blob/mermaid%4011.12.0/packages/mermaid/src/utils.ts#L755-L761
22
+ //
23
+ // These vars must be module-scoped (not per-instance refs) because the batching requires
24
+ // cross-instance coordination. They are short-lived: the queue drains on the next macrotask
25
+ // and cleanup clears everything on unmount.
26
+ let mermaidQueue: HTMLPreElement[] = [];
27
+ let mermaidFlushTimer: ReturnType<typeof setTimeout> | null = null;
28
+ let currentTheme: string | undefined;
29
+
30
+ function queueMermaidNode(node: HTMLPreElement, theme: string) {
31
+ mermaidQueue.push(node);
32
+ currentTheme = theme;
33
+
34
+ if (!mermaidFlushTimer) {
35
+ // setTimeout(0) defers to a macrotask, after all useEffects have queued their nodes
36
+ mermaidFlushTimer = setTimeout(async () => {
37
+ const nodes = [...mermaidQueue];
38
+ mermaidQueue = [];
39
+ mermaidFlushTimer = null;
40
+
41
+ const module = await import('mermaid');
42
+ mermaid = module.default;
43
+ mermaid.initialize({
44
+ startOnLoad: false,
45
+ theme: currentTheme === 'dark' ? 'dark' : 'default',
46
+ deterministicIds: true,
47
+ });
48
+
49
+ await mermaid.run({ nodes });
50
+ }, 0);
51
+ }
52
+ }
53
+
13
54
  interface Props {
14
55
  children: JSX.Element | JSX.Element[];
15
56
  }
@@ -18,6 +59,7 @@ const CodeTabs = (props: Props) => {
18
59
  const { children } = props;
19
60
  const theme = useContext(ThemeContext);
20
61
  const isHydrated = useHydrated();
62
+ const mermaidRef = useRef<HTMLPreElement>(null);
21
63
 
22
64
  // Handle both array (from rehype-react in rendering mdxish) and single element (MDX/JSX runtime) cases
23
65
  // The children here is the individual code block objects
@@ -32,22 +74,21 @@ const CodeTabs = (props: Props) => {
32
74
 
33
75
  const containAtLeastOneMermaid = childrenArray.some(pre => getCodeComponent(pre)?.props?.lang === 'mermaid');
34
76
 
35
- // Render Mermaid diagram
36
77
  useEffect(() => {
37
- // Ensure we only render mermaids when frontend is hydrated to avoid hydration errors
38
- // because mermaid mutates the DOM before react hydrates
39
- if (typeof window !== 'undefined' && containAtLeastOneMermaid && isHydrated) {
40
- import('mermaid').then(module => {
41
- mermaid = module.default;
42
- mermaid.initialize({
43
- startOnLoad: false,
44
- theme: theme === 'dark' ? 'dark' : 'default',
45
- });
46
- mermaid.run({
47
- nodes: document.querySelectorAll('.mermaid-render'),
48
- });
49
- });
78
+ // Wait for hydration so mermaid's DOM mutations don't cause mismatches
79
+ if (typeof window !== 'undefined' && containAtLeastOneMermaid && isHydrated && mermaidRef.current) {
80
+ queueMermaidNode(mermaidRef.current, theme);
50
81
  }
82
+
83
+ return () => {
84
+ // Clear the batch timer on unmount to prevent mermaid from running
85
+ // after the DOM is torn down
86
+ if (mermaidFlushTimer) {
87
+ clearTimeout(mermaidFlushTimer);
88
+ mermaidFlushTimer = null;
89
+ mermaidQueue = [];
90
+ }
91
+ };
51
92
  }, [containAtLeastOneMermaid, theme, isHydrated]);
52
93
 
53
94
  function handleClick({ target }, index: number) {
@@ -67,7 +108,11 @@ const CodeTabs = (props: Props) => {
67
108
  const codeComponent = getCodeComponent(childrenArray[0]);
68
109
  if (codeComponent?.props?.lang === 'mermaid') {
69
110
  const value = codeComponent?.props?.value;
70
- return <pre className="mermaid-render mermaid_single">{value}</pre>;
111
+ return (
112
+ <pre ref={mermaidRef} className="mermaid-render mermaid_single">
113
+ {value}
114
+ </pre>
115
+ );
71
116
  }
72
117
  }
73
118
 
@@ -76,9 +121,7 @@ const CodeTabs = (props: Props) => {
76
121
  <div className="CodeTabs-toolbar">
77
122
  {childrenArray.map((pre, i) => {
78
123
  // the first or only child should be our Code component
79
- const tabCodeComponent = Array.isArray(pre.props?.children)
80
- ? pre.props.children[0]
81
- : pre.props?.children;
124
+ const tabCodeComponent = Array.isArray(pre.props?.children) ? pre.props.children[0] : pre.props?.children;
82
125
  const lang = tabCodeComponent?.props?.lang;
83
126
  const meta = tabCodeComponent?.props?.meta;
84
127
 
@@ -75,10 +75,20 @@ const TailwindStyle = ({ children, darkModeDataAttribute }: Props) => {
75
75
  records.forEach(record => {
76
76
  if (record.type === 'childList') {
77
77
  record.addedNodes.forEach(node => {
78
- if (!(node instanceof HTMLElement) || !node.classList.contains(tailwindPrefix)) return;
79
-
80
- traverse(node, addClasses);
81
- shouldUpdate = true;
78
+ if (!(node instanceof HTMLElement)) return;
79
+
80
+ // Check the added node itself
81
+ if (node.classList.contains(tailwindPrefix)) {
82
+ traverse(node, addClasses);
83
+ shouldUpdate = true;
84
+ }
85
+
86
+ // Also check descendants — React may insert a parent node
87
+ // whose children contain TailwindRoot elements
88
+ node.querySelectorAll(`.${tailwindPrefix}`).forEach(child => {
89
+ traverse(child, addClasses);
90
+ shouldUpdate = true;
91
+ });
82
92
  });
83
93
  } else if (record.type === 'attributes') {
84
94
  if (record.attributeName !== 'class' || !(record.target instanceof HTMLElement)) return;
@@ -87,10 +97,11 @@ const TailwindStyle = ({ children, darkModeDataAttribute }: Props) => {
87
97
  shouldUpdate = true;
88
98
  }
89
99
 
90
- if (shouldUpdate) {
91
- setClasses(Array.from(classesSet.current));
92
- }
93
100
  });
101
+
102
+ if (shouldUpdate) {
103
+ setClasses(Array.from(classesSet.current));
104
+ }
94
105
  });
95
106
 
96
107
  observer.observe(ref.current.parentElement, {
@@ -29,7 +29,7 @@ export declare function mdxishAstProcessor(mdContent: string, opts?: MdxishOpts)
29
29
  parserReadyContent: string;
30
30
  };
31
31
  /**
32
- * Converts an Mdast to a Markdown string.
32
+ * Serializes an Mdast back into a markdown string.
33
33
  */
34
34
  export declare function mdxishMdastToMd(mdast: MdastRoot): string;
35
35
  /**