jamdesk 1.1.95 → 1.1.97
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jamdesk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.97",
|
|
4
4
|
"description": "CLI for Jamdesk — build, preview, and deploy documentation sites from MDX. Dev server with hot reload, 50+ components, OpenAPI support, AI search, and Mintlify migration",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jamdesk",
|
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import dynamic from 'next/dynamic';
|
|
4
|
+
import { useMemo, useRef, useState } from 'react';
|
|
5
|
+
import { readCachedHeight } from './mermaidCache';
|
|
6
|
+
import { useIsomorphicLayoutEffect } from '../../hooks/useIsomorphicLayoutEffect';
|
|
4
7
|
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
// Neutral floor reserved in the SSR markup itself (present on the browser's
|
|
9
|
+
// first paint, before any JS) so a cache miss / pre-hydration window shows
|
|
10
|
+
// correctly-sized empty space instead of a collapse or a grey pulse. Roughly
|
|
11
|
+
// the prior skeleton footprint; a cache hit refines it to the exact diagram
|
|
12
|
+
// height pre-paint, and once the diagram renders the floor is released
|
|
13
|
+
// entirely (see the layout effect below).
|
|
14
|
+
const DEFAULT_MERMAID_RESERVE_PX = 192;
|
|
15
|
+
|
|
16
|
+
// Sentinel: floor released, MermaidInner's own min-height governs the box.
|
|
17
|
+
const FLOOR_RELEASED_PX = 0;
|
|
18
|
+
|
|
19
|
+
// A rendered diagram (or the error box) is always taller than this; the empty
|
|
20
|
+
// pre-render container is 0px. Distinguishes "content has painted" from "still
|
|
21
|
+
// reserving the gap" so the floor is released only once it is dead weight.
|
|
22
|
+
const RENDERED_FLOOR_RELEASE_PX = 8;
|
|
16
23
|
|
|
17
|
-
// Dynamic import - ssr: false because mermaid requires browser APIs
|
|
18
24
|
const MermaidInner = dynamic(
|
|
19
25
|
() => import('./MermaidInner').then(mod => ({ default: mod.MermaidInner })),
|
|
20
|
-
{
|
|
21
|
-
ssr: false,
|
|
22
|
-
loading: () => <MermaidSkeleton />
|
|
23
|
-
}
|
|
26
|
+
{ ssr: false, loading: () => null }
|
|
24
27
|
);
|
|
25
28
|
|
|
26
29
|
interface MermaidProps {
|
|
@@ -31,5 +34,74 @@ interface MermaidProps {
|
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
export function Mermaid({ children, className, minWidth }: MermaidProps) {
|
|
34
|
-
|
|
37
|
+
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
38
|
+
|
|
39
|
+
// Server + first (hydration) render emit the constant default — identical
|
|
40
|
+
// markup, so no hydration mismatch. The sessionStorage read happens only
|
|
41
|
+
// in the post-hydration layout effect, never in this initializer.
|
|
42
|
+
const [reservedPx, setReservedPx] = useState(DEFAULT_MERMAID_RESERVE_PX);
|
|
43
|
+
|
|
44
|
+
// The reserved min-height only bridges the pre-render gap; it must never
|
|
45
|
+
// outlive the diagram or it leaves dead space under any diagram shorter
|
|
46
|
+
// than the floor (the cache-miss path never refines it down).
|
|
47
|
+
useIsomorphicLayoutEffect(() => {
|
|
48
|
+
const wrapper = wrapperRef.current;
|
|
49
|
+
|
|
50
|
+
// Relies on MermaidInner rendering exactly one HTMLElement root (the
|
|
51
|
+
// diagram container or the error box); if that contract changes, this
|
|
52
|
+
// height probe silently targets the wrong node.
|
|
53
|
+
const releaseIfRendered = (): boolean => {
|
|
54
|
+
const content = wrapper?.firstElementChild as HTMLElement | null;
|
|
55
|
+
if (content && content.clientHeight > RENDERED_FLOOR_RELEASE_PX) {
|
|
56
|
+
setReservedPx(FLOOR_RELEASED_PX);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Already painted (warm soft-nav: SVG hydrated synchronously from cache) —
|
|
63
|
+
// the floor is already dead weight, skip the cache refine entirely.
|
|
64
|
+
if (releaseIfRendered()) return;
|
|
65
|
+
|
|
66
|
+
// Not yet painted: cache hit → reserve the exact height pre-paint (zero
|
|
67
|
+
// shift); cache miss / new diagram → hold the default floor for its gap.
|
|
68
|
+
const cached = readCachedHeight(children);
|
|
69
|
+
setReservedPx(cached > 0 ? cached : DEFAULT_MERMAID_RESERVE_PX);
|
|
70
|
+
|
|
71
|
+
if (!wrapper || typeof MutationObserver === 'undefined') return;
|
|
72
|
+
// childList only — no `attributes`: MermaidInner's post-commit style pass
|
|
73
|
+
// mutates SVG child styles heavily; observing attributes would fire a
|
|
74
|
+
// callback storm. The SVG-insert is a childList mutation, all we need.
|
|
75
|
+
const mo = new MutationObserver(() => {
|
|
76
|
+
if (releaseIfRendered()) mo.disconnect();
|
|
77
|
+
});
|
|
78
|
+
mo.observe(wrapper, { childList: true, subtree: true });
|
|
79
|
+
return () => mo.disconnect();
|
|
80
|
+
}, [children]);
|
|
81
|
+
|
|
82
|
+
// Stable element identity across reservedPx changes. The pre-paint
|
|
83
|
+
// setReservedPx above re-renders this wrapper; without this memo that
|
|
84
|
+
// re-render also re-renders the dynamic MermaidInner, making React
|
|
85
|
+
// re-commit its raw-HTML injection and discard the DOM that MermaidInner
|
|
86
|
+
// imperatively themed post-commit. That theming is reapplied only on a
|
|
87
|
+
// genuine content change, not on this re-commit, so on soft-navigation the
|
|
88
|
+
// diagram paints raw mermaid "neutral" colors instead of the active theme.
|
|
89
|
+
// Memoizing the element makes the wrapper own min-height ONLY.
|
|
90
|
+
// Soundness depends on MermaidInner being a stable reference: the dynamic()
|
|
91
|
+
// call MUST stay module-scoped (above) — moving it into this component
|
|
92
|
+
// would memoize an element closed over a stale component identity.
|
|
93
|
+
const inner = useMemo(
|
|
94
|
+
() => (
|
|
95
|
+
<MermaidInner className={className} minWidth={minWidth}>
|
|
96
|
+
{children}
|
|
97
|
+
</MermaidInner>
|
|
98
|
+
),
|
|
99
|
+
[children, className, minWidth]
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div ref={wrapperRef} data-testid="mermaid-wrapper" style={{ minHeight: reservedPx }}>
|
|
104
|
+
{inner}
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
35
107
|
}
|
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
4
4
|
import mermaid from 'mermaid';
|
|
5
|
+
import {
|
|
6
|
+
CACHE_KEY_PREFIX,
|
|
7
|
+
hashDiagram,
|
|
8
|
+
readCache,
|
|
9
|
+
writeCache,
|
|
10
|
+
readSvgHeight,
|
|
11
|
+
type CachedDiagram,
|
|
12
|
+
} from './mermaidCache';
|
|
5
13
|
|
|
6
14
|
mermaid.initialize({
|
|
7
15
|
startOnLoad: false,
|
|
@@ -12,73 +20,15 @@ mermaid.initialize({
|
|
|
12
20
|
},
|
|
13
21
|
});
|
|
14
22
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface CachedDiagram {
|
|
27
|
-
svg: string;
|
|
28
|
-
height: number;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Coarse tamper check, NOT a sanitizer. mermaid's securityLevel:'strict' is
|
|
32
|
-
// the real defense (it sanitizes at render). This only guards the cache-read
|
|
33
|
-
// path, which re-injects stored bytes via React's raw inner-HTML prop without
|
|
34
|
-
// re-sanitizing — so a tampered same-origin sessionStorage entry is rejected
|
|
35
|
-
// here, forcing a fresh re-sanitized render. Catches the obvious signatures
|
|
36
|
-
// only (non-<svg> root, <script>, inline on*= handlers).
|
|
37
|
-
function looksTampered(svg: string): boolean {
|
|
38
|
-
return (
|
|
39
|
-
!/^\s*<svg/.test(svg) ||
|
|
40
|
-
/<script\b/i.test(svg) ||
|
|
41
|
-
/<[^>]+\son[a-z]+\s*=/i.test(svg)
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function readCache(source: string): CachedDiagram | null {
|
|
46
|
-
try {
|
|
47
|
-
const raw = sessionStorage.getItem(CACHE_KEY_PREFIX + hashDiagram(source));
|
|
48
|
-
if (!raw) return null;
|
|
49
|
-
const parsed = JSON.parse(raw) as Partial<CachedDiagram>;
|
|
50
|
-
if (typeof parsed?.svg !== 'string') return null;
|
|
51
|
-
if (looksTampered(parsed.svg)) return null;
|
|
52
|
-
// Normalize the shape so the return honestly matches CachedDiagram:
|
|
53
|
-
// legacy v1 entries predate height tracking, and a tampered/foreign
|
|
54
|
-
// entry could carry a non-numeric height. Coerce both to a number.
|
|
55
|
-
return {
|
|
56
|
-
svg: parsed.svg,
|
|
57
|
-
height: typeof parsed.height === 'number' ? parsed.height : 0,
|
|
58
|
-
};
|
|
59
|
-
} catch {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function writeCache(source: string, entry: CachedDiagram): void {
|
|
65
|
-
try {
|
|
66
|
-
sessionStorage.setItem(CACHE_KEY_PREFIX + hashDiagram(source), JSON.stringify(entry));
|
|
67
|
-
} catch {}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Real mermaid output has no `height` attribute (it sizes via viewBox +
|
|
71
|
-
// max-width), so fall back to the viewBox height; an explicit `height` still
|
|
72
|
-
// wins when present (other/older renderers). Markup-parsing instead of
|
|
73
|
-
// getBoundingClientRect keeps this jsdom-safe and reflow-free on the hot path.
|
|
74
|
-
export function readSvgHeight(svgMarkup: string): number {
|
|
75
|
-
const attr = svgMarkup.match(/<svg\b[^>]*\bheight=["']([\d.]+)(?:px)?["']/i);
|
|
76
|
-
if (attr) return parseFloat(attr[1]);
|
|
77
|
-
const viewBox = svgMarkup.match(
|
|
78
|
-
/<svg\b[^>]*\bviewBox=["']\s*[\d.+-]+\s+[\d.+-]+\s+[\d.+-]+\s+([\d.]+)/i
|
|
79
|
-
);
|
|
80
|
-
return viewBox ? parseFloat(viewBox[1]) : 0;
|
|
81
|
-
}
|
|
23
|
+
// readCachedHeight intentionally not re-exported; Mermaid.tsx imports it directly from mermaidCache
|
|
24
|
+
export {
|
|
25
|
+
CACHE_KEY_PREFIX,
|
|
26
|
+
hashDiagram,
|
|
27
|
+
readCache,
|
|
28
|
+
writeCache,
|
|
29
|
+
readSvgHeight,
|
|
30
|
+
};
|
|
31
|
+
export type { CachedDiagram };
|
|
82
32
|
|
|
83
33
|
interface MermaidInnerProps {
|
|
84
34
|
children: string;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export const CACHE_KEY_PREFIX = 'mermaid:v1:';
|
|
2
|
+
|
|
3
|
+
// djb2; collisions are theoretically possible but the input space is tiny for a docs site.
|
|
4
|
+
export function hashDiagram(source: string): string {
|
|
5
|
+
let h = 5381;
|
|
6
|
+
for (let i = 0; i < source.length; i++) {
|
|
7
|
+
h = ((h << 5) + h) ^ source.charCodeAt(i);
|
|
8
|
+
}
|
|
9
|
+
return (h >>> 0).toString(36);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CachedDiagram {
|
|
13
|
+
svg: string;
|
|
14
|
+
height: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Coarse tamper check, NOT a sanitizer. mermaid's securityLevel:'strict' is
|
|
18
|
+
// the real defense (it sanitizes at render). This only guards the cache-read
|
|
19
|
+
// path, which re-injects stored bytes via React's raw inner-HTML prop without
|
|
20
|
+
// re-sanitizing — so a tampered same-origin sessionStorage entry is rejected
|
|
21
|
+
// here, forcing a fresh re-sanitized render. Catches the obvious signatures
|
|
22
|
+
// only (non-<svg> root, <script>, inline on*= handlers).
|
|
23
|
+
function looksTampered(svg: string): boolean {
|
|
24
|
+
return (
|
|
25
|
+
!/^\s*<svg/.test(svg) ||
|
|
26
|
+
/<script\b/i.test(svg) ||
|
|
27
|
+
/<[^>]+\son[a-z]+\s*=/i.test(svg)
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function readCache(source: string): CachedDiagram | null {
|
|
32
|
+
try {
|
|
33
|
+
const raw = sessionStorage.getItem(CACHE_KEY_PREFIX + hashDiagram(source));
|
|
34
|
+
if (!raw) return null;
|
|
35
|
+
const parsed = JSON.parse(raw) as Partial<CachedDiagram>;
|
|
36
|
+
if (typeof parsed?.svg !== 'string') return null;
|
|
37
|
+
if (looksTampered(parsed.svg)) return null;
|
|
38
|
+
// Normalize the shape so the return honestly matches CachedDiagram:
|
|
39
|
+
// legacy v1 entries predate height tracking, and a tampered/foreign
|
|
40
|
+
// entry could carry a non-numeric height. Coerce both to a number.
|
|
41
|
+
return {
|
|
42
|
+
svg: parsed.svg,
|
|
43
|
+
height: typeof parsed.height === 'number' ? parsed.height : 0,
|
|
44
|
+
};
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function writeCache(source: string, entry: CachedDiagram): void {
|
|
51
|
+
try {
|
|
52
|
+
sessionStorage.setItem(CACHE_KEY_PREFIX + hashDiagram(source), JSON.stringify(entry));
|
|
53
|
+
} catch {}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Real mermaid output has no `height` attribute (it sizes via viewBox +
|
|
57
|
+
// max-width), so fall back to the viewBox height; an explicit `height` still
|
|
58
|
+
// wins when present (other/older renderers). Markup-parsing instead of
|
|
59
|
+
// getBoundingClientRect keeps this jsdom-safe and reflow-free on the hot path.
|
|
60
|
+
export function readSvgHeight(svgMarkup: string): number {
|
|
61
|
+
const attr = svgMarkup.match(/<svg\b[^>]*\bheight=["']([\d.]+)(?:px)?["']/i);
|
|
62
|
+
if (attr) return parseFloat(attr[1]);
|
|
63
|
+
const viewBox = svgMarkup.match(
|
|
64
|
+
/<svg\b[^>]*\bviewBox=["']\s*[\d.+-]+\s+[\d.+-]+\s+[\d.+-]+\s+([\d.]+)/i
|
|
65
|
+
);
|
|
66
|
+
return viewBox ? parseFloat(viewBox[1]) : 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Cached pixel height for a diagram, or 0 when not cached. Lets the
|
|
70
|
+
// always-loaded Mermaid.tsx shell refine its reserved space to the exact
|
|
71
|
+
// diagram height before the mermaid chunk loads, without reaching into the
|
|
72
|
+
// cache entry shape itself.
|
|
73
|
+
export function readCachedHeight(source: string): number {
|
|
74
|
+
return readCache(source)?.height ?? 0;
|
|
75
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { useEffect, useLayoutEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
// useLayoutEffect on the client (runs after commit, synchronously before
|
|
4
|
+
// paint), useEffect on the server (no-op) — avoids React's SSR warning and
|
|
5
|
+
// keeps the rules-of-hooks lint happy (single named hook, no conditional
|
|
6
|
+
// hook-through-a-variable at the call site).
|
|
7
|
+
export const useIsomorphicLayoutEffect =
|
|
8
|
+
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
|