@bendyline/squisq-react 2.5.0 → 2.7.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/README.md CHANGED
@@ -57,6 +57,25 @@ state rather than crashing.
57
57
  | `MediaClipLayer` | Hidden `<audio>`/`<video>` elements for timed media clips |
58
58
  | `JsonView` | Read-only viewer for JSON values bound to a Squisq-annotated schema |
59
59
 
60
+ ### Fenced-code copy control
61
+
62
+ `MarkdownRenderer` and the linear document surfaces keep code-copy UI off by
63
+ default. Opt in with `showCodeCopyButton`. Web hosts can rely on
64
+ `navigator.clipboard`; Electron or native embeddings can provide their own
65
+ clipboard bridge:
66
+
67
+ ````tsx
68
+ <LinearDocView
69
+ markdown={'```\n$ node packages/tooling/dist/cli.mjs components\n```'}
70
+ showCodeCopyButton
71
+ onCopyCode={(code, { language }) => hostClipboard.writeText(code)}
72
+ />
73
+ ````
74
+
75
+ The same two props are available on `DocPlayer` (for linear mode), and on the
76
+ standalone static `mount()` options. The callback receives the exact fence
77
+ contents, without the backtick delimiters.
78
+
60
79
  ## Layers
61
80
 
62
81
  Blocks are composed of typed layers rendered as SVG:
@@ -0,0 +1,62 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { Theme } from '@bendyline/squisq/schemas';
3
+ import { FenceRendererMap } from '@bendyline/squisq/fence';
4
+ import { MarkdownBlockNode, HtmlPolicy } from '@bendyline/squisq/markdown';
5
+
6
+ interface CodeBlockCopyContext {
7
+ /** Authored fence language, when one was supplied. */
8
+ language?: string;
9
+ }
10
+ /**
11
+ * Host-owned clipboard adapter for fenced code blocks.
12
+ *
13
+ * The callback runs directly from the button's click handler so Electron
14
+ * bridges and browser Clipboard APIs retain their user-gesture context.
15
+ */
16
+ type CodeBlockCopyHandler = (code: string, context: CodeBlockCopyContext) => void | Promise<void>;
17
+ interface MarkdownRendererProps {
18
+ /** Block-level AST nodes to render */
19
+ nodes: MarkdownBlockNode[];
20
+ /** Optional CSS class for the wrapper element */
21
+ className?: string;
22
+ /**
23
+ * Raw HTML policy. Defaults to `sanitize`, which removes unsafe tags,
24
+ * event handlers, and executable URL schemes before rendering.
25
+ */
26
+ htmlPolicy?: HtmlPolicy;
27
+ /**
28
+ * Extra URL schemes to allow on links (e.g. a host app's internal
29
+ * navigation scheme it intercepts on click). Executable schemes are
30
+ * never allowed regardless. See {@link SanitizeUrlOptions}.
31
+ */
32
+ linkSchemes?: readonly string[];
33
+ /** Resolved Squisq theme inherited by embedded Mermaid diagrams. */
34
+ theme?: Theme;
35
+ /** Show a subtle Copy button on ordinary fenced code blocks (default: false). */
36
+ showCodeCopyButton?: boolean;
37
+ /**
38
+ * Optional host clipboard adapter. When omitted, enabled copy buttons use
39
+ * `navigator.clipboard.writeText`. Supply this for Electron, native shells,
40
+ * or hosts with their own clipboard permission/error handling.
41
+ */
42
+ onCopyCode?: CodeBlockCopyHandler;
43
+ /**
44
+ * Host fence-renderer registry (`@bendyline/squisq/fence`): fenced code
45
+ * blocks whose language is claimed render through the host's widget
46
+ * instead of the default code block. Wins over the built-in mermaid
47
+ * handling and over an ambient `FenceRendererContext`; a renderer that
48
+ * throws falls back to the plain code block via an error boundary.
49
+ */
50
+ fenceRenderers?: FenceRendererMap;
51
+ }
52
+ /**
53
+ * Renders MarkdownBlockNode[] AST as React HTML elements.
54
+ *
55
+ * @example
56
+ * ```tsx
57
+ * <MarkdownRenderer nodes={block.contents} />
58
+ * ```
59
+ */
60
+ declare function MarkdownRenderer({ nodes, className, htmlPolicy, linkSchemes, theme, showCodeCopyButton, onCopyCode, fenceRenderers, }: MarkdownRendererProps): react_jsx_runtime.JSX.Element | null;
61
+
62
+ export { type CodeBlockCopyHandler as C, MarkdownRenderer as M, type CodeBlockCopyContext as a, type MarkdownRendererProps as b };
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  BlockRenderer,
3
3
  LinearDocView
4
- } from "./chunk-THDXCSPC.js";
4
+ } from "./chunk-U3HRSQ45.js";
5
5
  import {
6
6
  useAudioSync,
7
7
  useDocPlayback,
8
8
  useMediaSchedule,
9
9
  useViewportOrientation
10
- } from "./chunk-BOZJ655L.js";
10
+ } from "./chunk-MVJQL2W2.js";
11
11
  import {
12
12
  useAutoSurface
13
13
  } from "./chunk-TT6ENR6T.js";
@@ -1859,7 +1859,10 @@ function DocPlayerContent({
1859
1859
  pipShape,
1860
1860
  pipPosition,
1861
1861
  enableSwipe = true,
1862
- globalKeyboardShortcuts = false
1862
+ globalKeyboardShortcuts = false,
1863
+ showCodeCopyButton = false,
1864
+ onCopyCode,
1865
+ fenceRenderers
1863
1866
  }) {
1864
1867
  const isSlideshowMode = displayMode === "slideshow";
1865
1868
  const isLinearMode = displayMode === "linear";
@@ -1876,12 +1879,17 @@ function DocPlayerContent({
1876
1879
  const params = new URLSearchParams(window.location.search);
1877
1880
  return params.get("debug") === "true";
1878
1881
  }, []);
1882
+ const syntheticDuration = useMemo3(
1883
+ () => audioMode === "synthetic" ? getDocPlaybackDuration(doc) : 0,
1884
+ [audioMode, doc]
1885
+ );
1879
1886
  const internalAudio = useAudioSync(
1880
1887
  audioRef,
1881
1888
  doc.audio,
1882
1889
  basePath,
1883
1890
  !externalAudioController,
1884
- audioMode
1891
+ audioMode,
1892
+ syntheticDuration
1885
1893
  );
1886
1894
  const audio = externalAudioController || internalAudio;
1887
1895
  useEffect5(() => {
@@ -2660,7 +2668,10 @@ function DocPlayerContent({
2660
2668
  viewport: activeViewport,
2661
2669
  theme,
2662
2670
  surface,
2663
- animationsEnabled
2671
+ animationsEnabled,
2672
+ showCodeCopyButton,
2673
+ onCopyCode,
2674
+ fenceRenderers
2664
2675
  }
2665
2676
  )
2666
2677
  }
@@ -2675,6 +2686,7 @@ function DocPlayerContent({
2675
2686
  "data-playback-state": isPlaying ? "playing" : "paused",
2676
2687
  "data-swipe-phase": swipe.phase,
2677
2688
  tabIndex: renderMode ? -1 : 0,
2689
+ role: "region",
2678
2690
  "aria-label": "Document player",
2679
2691
  onKeyDown: renderMode ? void 0 : handleKeyDown,
2680
2692
  className: `doc-player${swipeEnabled ? " doc-player--swipe" : ""}${swipe.phase === "dragging" ? " doc-player--grabbing" : ""}`,
@@ -5,6 +5,13 @@ import {
5
5
  useMediaUrl
6
6
  } from "./chunk-LR3AIGDD.js";
7
7
 
8
+ // src/hooks/FenceRendererContext.tsx
9
+ import { createContext, useContext } from "react";
10
+ var FenceRendererContext = createContext(null);
11
+ function useFenceRenderers() {
12
+ return useContext(FenceRendererContext);
13
+ }
14
+
8
15
  // src/InlineVideoPlayer.tsx
9
16
  import { jsx } from "react/jsx-runtime";
10
17
  function InlineVideoPlayer({
@@ -50,13 +57,99 @@ function InlineAudioPlayer({
50
57
  }
51
58
 
52
59
  // src/MarkdownRenderer.tsx
53
- import { Fragment } from "react";
60
+ import { Component, Fragment, useEffect, useRef, useState } from "react";
54
61
  import {
55
62
  sanitizeHtmlNodes,
56
63
  sanitizeUrl
57
64
  } from "@bendyline/squisq/markdown";
58
65
  import { jsx as jsx3, jsxs } from "react/jsx-runtime";
59
66
  var DEFAULT_CTX = { htmlPolicy: "sanitize" };
67
+ var CODE_COPY_LABELS = {
68
+ idle: "Copy",
69
+ copying: "Copying\u2026",
70
+ copied: "Copied",
71
+ failed: "Copy failed"
72
+ };
73
+ async function writeCodeToClipboard(code, language, onCopyCode) {
74
+ if (onCopyCode) {
75
+ await onCopyCode(code, language === void 0 ? {} : { language });
76
+ return;
77
+ }
78
+ if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) {
79
+ throw new Error("Clipboard access is unavailable in this host");
80
+ }
81
+ await navigator.clipboard.writeText(code);
82
+ }
83
+ function CodeBlock({
84
+ value,
85
+ language,
86
+ ctx
87
+ }) {
88
+ const [status, setStatus] = useState("idle");
89
+ const resetTimer = useRef();
90
+ useEffect(
91
+ () => () => {
92
+ if (resetTimer.current !== void 0) clearTimeout(resetTimer.current);
93
+ },
94
+ []
95
+ );
96
+ const pre = /* @__PURE__ */ jsx3("pre", { className: "squisq-md-code-block", children: /* @__PURE__ */ jsx3("code", { className: language ? `language-${language}` : void 0, children: value }) });
97
+ if (!ctx.showCodeCopyButton) return pre;
98
+ const handleCopy = async () => {
99
+ if (status === "copying") return;
100
+ setStatus("copying");
101
+ try {
102
+ await writeCodeToClipboard(value, language ?? void 0, ctx.onCopyCode);
103
+ setStatus("copied");
104
+ } catch {
105
+ setStatus("failed");
106
+ }
107
+ if (resetTimer.current !== void 0) clearTimeout(resetTimer.current);
108
+ resetTimer.current = setTimeout(() => setStatus("idle"), 1600);
109
+ };
110
+ return /* @__PURE__ */ jsxs("div", { className: "squisq-md-code-frame", children: [
111
+ pre,
112
+ /* @__PURE__ */ jsx3(
113
+ "button",
114
+ {
115
+ type: "button",
116
+ className: "squisq-md-code-copy",
117
+ "data-copy-state": status,
118
+ disabled: status === "copying",
119
+ onClick: () => void handleCopy(),
120
+ "aria-label": "Copy code to clipboard",
121
+ children: CODE_COPY_LABELS[status]
122
+ }
123
+ )
124
+ ] });
125
+ }
126
+ var FenceWidgetBoundary = class extends Component {
127
+ constructor() {
128
+ super(...arguments);
129
+ this.state = { failed: false };
130
+ }
131
+ static getDerivedStateFromError() {
132
+ return { failed: true };
133
+ }
134
+ render() {
135
+ return this.state.failed ? this.props.fallback : this.props.children;
136
+ }
137
+ };
138
+ function HostFence({
139
+ node,
140
+ lang,
141
+ ctx
142
+ }) {
143
+ const renderer = ctx.fenceRenderers?.[lang];
144
+ if (!renderer) return null;
145
+ return /* @__PURE__ */ jsx3("div", { className: `squisq-md-fence-widget squisq-md-fence-widget-${lang}`, "data-fence-lang": lang, children: renderer({
146
+ lang,
147
+ ...node.meta != null ? { meta: node.meta } : {},
148
+ value: node.value,
149
+ ...ctx.theme ? { theme: ctx.theme } : {},
150
+ mode: "read"
151
+ }) });
152
+ }
60
153
  function renderInline(nodes, keyPrefix = "", ctx = DEFAULT_CTX) {
61
154
  return nodes.map((node, i) => {
62
155
  const key = `${keyPrefix}i${i}`;
@@ -168,8 +261,19 @@ function renderBlock(node, key, ctx = DEFAULT_CTX) {
168
261
  return /* @__PURE__ */ jsx3("ol", { className: "squisq-md-list squisq-md-ol", start: node.start ?? void 0, children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`, ctx)) }, key);
169
262
  }
170
263
  return /* @__PURE__ */ jsx3("ul", { className: "squisq-md-list squisq-md-ul", children: node.children.map((item, i) => renderListItem(item, `${key}li${i}`, ctx)) }, key);
171
- case "code":
172
- if (node.lang?.trim().toLowerCase() === "mermaid") {
264
+ case "code": {
265
+ const fenceLang = node.lang?.trim().toLowerCase();
266
+ if (fenceLang && ctx.fenceRenderers?.[fenceLang]) {
267
+ return /* @__PURE__ */ jsx3(
268
+ FenceWidgetBoundary,
269
+ {
270
+ fallback: /* @__PURE__ */ jsx3(CodeBlock, { value: node.value, language: node.lang, ctx }),
271
+ children: /* @__PURE__ */ jsx3(HostFence, { node, lang: fenceLang, ctx })
272
+ },
273
+ key
274
+ );
275
+ }
276
+ if (fenceLang === "mermaid") {
173
277
  return /* @__PURE__ */ jsx3(
174
278
  MermaidDiagram,
175
279
  {
@@ -181,7 +285,8 @@ function renderBlock(node, key, ctx = DEFAULT_CTX) {
181
285
  key
182
286
  );
183
287
  }
184
- return /* @__PURE__ */ jsx3("pre", { className: "squisq-md-code-block", children: /* @__PURE__ */ jsx3("code", { className: node.lang ? `language-${node.lang}` : void 0, children: node.value }) }, key);
288
+ return /* @__PURE__ */ jsx3(CodeBlock, { value: node.value, language: node.lang, ctx }, key);
289
+ }
185
290
  case "thematicBreak":
186
291
  return /* @__PURE__ */ jsx3("hr", { className: "squisq-md-hr" }, key);
187
292
  case "table":
@@ -393,13 +498,26 @@ function MarkdownRenderer({
393
498
  className,
394
499
  htmlPolicy = "sanitize",
395
500
  linkSchemes,
396
- theme
501
+ theme,
502
+ showCodeCopyButton = false,
503
+ onCopyCode,
504
+ fenceRenderers
397
505
  }) {
506
+ const ambientFenceRenderers = useFenceRenderers();
398
507
  if (!nodes || nodes.length === 0) return null;
399
- return /* @__PURE__ */ jsx3("div", { className: `squisq-md ${className || ""}`, children: renderBlocks(nodes, "", { htmlPolicy, linkSchemes, theme }) });
508
+ return /* @__PURE__ */ jsx3("div", { className: `squisq-md ${className || ""}`, children: renderBlocks(nodes, "", {
509
+ htmlPolicy,
510
+ linkSchemes,
511
+ theme,
512
+ showCodeCopyButton,
513
+ onCopyCode,
514
+ fenceRenderers: fenceRenderers ?? ambientFenceRenderers ?? void 0
515
+ }) });
400
516
  }
401
517
 
402
518
  export {
519
+ FenceRendererContext,
520
+ useFenceRenderers,
403
521
  InlineVideoPlayer,
404
522
  InlineAudioPlayer,
405
523
  MarkdownRenderer
@@ -16,7 +16,7 @@ function useMediaSchedule(schedule, currentTime) {
16
16
  }
17
17
 
18
18
  // src/hooks/useAudioSync.ts
19
- import { useState, useEffect, useRef, useCallback } from "react";
19
+ import { useState, useEffect, useMemo as useMemo2, useRef, useCallback } from "react";
20
20
  import { fetchResourceBytes, isResourceUrlAllowed } from "@bendyline/squisq/markdown";
21
21
 
22
22
  // src/hooks/AudioController.ts
@@ -61,7 +61,14 @@ function resolveAudioUrl(src, basePath) {
61
61
  if (!basePath) return src;
62
62
  return `${basePath.replace(/\/$/, "")}/${src.replace(/^\//, "")}`;
63
63
  }
64
- function useAudioSync(audioRef, audioTrack, basePath = "", enabled = true, mode = "media") {
64
+ function useAudioSync(audioRef, track, basePath = "", enabled = true, mode = "media", syntheticDuration = 0) {
65
+ const audioTrack = useMemo2(() => {
66
+ if (mode !== "synthetic" || track?.segments?.length || syntheticDuration <= 0) return track;
67
+ return {
68
+ ...track,
69
+ segments: [{ src: "", name: "synthetic", duration: syntheticDuration, startTime: 0 }]
70
+ };
71
+ }, [track, mode, syntheticDuration]);
65
72
  const resourcePolicy = useResourcePolicy();
66
73
  const [currentTime, setCurrentTime] = useState(0);
67
74
  const [isPlaying, setIsPlaying] = useState(false);
@@ -468,7 +475,7 @@ function useAudioSync(audioRef, audioTrack, basePath = "", enabled = true, mode
468
475
  }
469
476
 
470
477
  // src/hooks/useDocPlayback.ts
471
- import { useMemo as useMemo2, useCallback as useCallback2, useRef as useRef2 } from "react";
478
+ import { useMemo as useMemo3, useCallback as useCallback2, useRef as useRef2 } from "react";
472
479
  import {
473
480
  DEFAULT_THEME,
474
481
  getBlockAtTime,
@@ -489,7 +496,7 @@ function useDocPlayback(script, currentTime, options = {}) {
489
496
  onSeek,
490
497
  useAudioSegmentTiming = true
491
498
  } = options;
492
- const blocks = useMemo2(() => {
499
+ const blocks = useMemo3(() => {
493
500
  if (!script?.blocks) {
494
501
  return [];
495
502
  }
@@ -532,20 +539,20 @@ function useDocPlayback(script, currentTime, options = {}) {
532
539
  theme,
533
540
  useAudioSegmentTiming
534
541
  ]);
535
- const currentBlock = useMemo2(() => getBlockAtTime(blocks, currentTime), [blocks, currentTime]);
536
- const currentBlockIndex = useMemo2(
542
+ const currentBlock = useMemo3(() => getBlockAtTime(blocks, currentTime), [blocks, currentTime]);
543
+ const currentBlockIndex = useMemo3(
537
544
  () => currentBlock ? blocks.indexOf(currentBlock) : -1,
538
545
  [blocks, currentBlock]
539
546
  );
540
- const blockTime = useMemo2(() => {
547
+ const blockTime = useMemo3(() => {
541
548
  if (!currentBlock) return 0;
542
549
  return Math.max(0, currentTime - currentBlock.startTime);
543
550
  }, [currentBlock, currentTime]);
544
- const blockProgress = useMemo2(() => {
551
+ const blockProgress = useMemo3(() => {
545
552
  if (!currentBlock || currentBlock.duration === 0) return 0;
546
553
  return Math.min(1, blockTime / currentBlock.duration);
547
554
  }, [currentBlock, blockTime]);
548
- const docProgress = useMemo2(() => {
555
+ const docProgress = useMemo3(() => {
549
556
  if (!script || script.duration === 0) return 0;
550
557
  return Math.min(1, currentTime / script.duration);
551
558
  }, [script, currentTime]);
@@ -612,7 +619,7 @@ function useDocPlayback(script, currentTime, options = {}) {
612
619
  }
613
620
 
614
621
  // src/hooks/useViewportOrientation.ts
615
- import { useState as useState2, useEffect as useEffect2, useMemo as useMemo3 } from "react";
622
+ import { useState as useState2, useEffect as useEffect2, useMemo as useMemo4 } from "react";
616
623
  import {
617
624
  VIEWPORT_PRESETS as VIEWPORT_PRESETS2
618
625
  } from "@bendyline/squisq/doc";
@@ -661,11 +668,11 @@ function useViewportOrientation() {
661
668
  clearTimeout(timeoutId);
662
669
  };
663
670
  }, []);
664
- const orientation = useMemo3(
671
+ const orientation = useMemo4(
665
672
  () => getOrientationFromWindow(windowSize.width, windowSize.height),
666
673
  [windowSize.width, windowSize.height]
667
674
  );
668
- const viewport = useMemo3(() => getViewportForOrientation(orientation), [orientation]);
675
+ const viewport = useMemo4(() => getViewportForOrientation(orientation), [orientation]);
669
676
  return {
670
677
  viewport,
671
678
  orientation,
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-TT6ENR6T.js";
4
4
  import {
5
5
  MarkdownRenderer
6
- } from "./chunk-VMPQEUJH.js";
6
+ } from "./chunk-ESI3P77P.js";
7
7
 
8
8
  // src/jsonView/useJsonViewTokens.ts
9
9
  import { useMemo } from "react";
@@ -325,6 +325,72 @@ function JsonView(props) {
325
325
  ) });
326
326
  }
327
327
 
328
+ // src/jsonView/jsonFormFenceRenderer.tsx
329
+ import { inferSchema } from "@bendyline/squisq/jsonForm";
330
+ import { parseYamlSubset } from "@bendyline/squisq/doc";
331
+ import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
332
+ function parseFenceBody(value) {
333
+ const trimmed = value.trim();
334
+ if (trimmed.startsWith("{")) {
335
+ try {
336
+ const parsed = JSON.parse(trimmed);
337
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
338
+ return { error: "fence body must be a top-level object" };
339
+ }
340
+ return { data: parsed };
341
+ } catch (err) {
342
+ return { error: `invalid JSON: ${err instanceof Error ? err.message : String(err)}` };
343
+ }
344
+ }
345
+ try {
346
+ return { data: parseYamlSubset(value) };
347
+ } catch (err) {
348
+ return { error: `invalid YAML: ${err instanceof Error ? err.message : String(err)}` };
349
+ }
350
+ }
351
+ function JsonFormFence({
352
+ ctx,
353
+ options
354
+ }) {
355
+ const parsed = ctx.data !== void 0 ? { data: ctx.data } : parseFenceBody(ctx.value);
356
+ const data = parsed.data;
357
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
358
+ return /* @__PURE__ */ jsxs2("div", { className: "squisq-json-fence squisq-json-fence--invalid", children: [
359
+ /* @__PURE__ */ jsx4("pre", { className: "squisq-md-code-block", children: /* @__PURE__ */ jsx4("code", { children: ctx.value }) }),
360
+ "error" in parsed && parsed.error && /* @__PURE__ */ jsx4("div", { className: "squisq-json-fence-error", children: parsed.error })
361
+ ] });
362
+ }
363
+ const schema = options.schema ?? inferSchema(data);
364
+ const actions = options.actions ?? [];
365
+ return /* @__PURE__ */ jsxs2("div", { className: `squisq-json-fence${options.className ? ` ${options.className}` : ""}`, children: [
366
+ /* @__PURE__ */ jsx4(
367
+ JsonView,
368
+ {
369
+ schema,
370
+ value: data,
371
+ theme: ctx.theme,
372
+ density: options.density ?? "compact"
373
+ }
374
+ ),
375
+ actions.length > 0 && options.onAction && /* @__PURE__ */ jsx4("div", { className: "squisq-json-fence-actions", role: "group", children: actions.map((action) => /* @__PURE__ */ jsx4(
376
+ "button",
377
+ {
378
+ type: "button",
379
+ className: `squisq-json-fence-action${action.variant === "primary" ? " squisq-json-fence-action--primary" : ""}`,
380
+ onClick: () => void options.onAction?.(action.id, data, ctx),
381
+ children: action.label
382
+ },
383
+ action.id
384
+ )) })
385
+ ] });
386
+ }
387
+ function createJsonFormFenceRenderer(options = {}) {
388
+ return function jsonFormFenceRenderer(ctx) {
389
+ return /* @__PURE__ */ jsx4(JsonFormFence, { ctx, options });
390
+ };
391
+ }
392
+
328
393
  export {
329
- JsonView
394
+ JsonView,
395
+ createJsonFormFenceRenderer
330
396
  };
@@ -16,7 +16,7 @@ import {
16
16
  import {
17
17
  InlineVideoPlayer,
18
18
  MarkdownRenderer
19
- } from "./chunk-VMPQEUJH.js";
19
+ } from "./chunk-ESI3P77P.js";
20
20
  import {
21
21
  useMediaUrl
22
22
  } from "./chunk-LR3AIGDD.js";
@@ -165,7 +165,8 @@ var defaultValue = {
165
165
  viewport: VIEWPORT_PRESETS.landscape,
166
166
  renderContext: { theme: DEFAULT_THEME },
167
167
  animationsEnabled: true,
168
- imageDisplayMode: "inline"
168
+ imageDisplayMode: "inline",
169
+ showCodeCopyButton: false
169
170
  };
170
171
  var PageViewContext = createContext(defaultValue);
171
172
  function usePageView() {
@@ -438,11 +439,22 @@ function CardGridSection({ section }) {
438
439
  ] });
439
440
  }
440
441
  function ItemListSection({ section }) {
441
- const { theme } = usePageView();
442
+ const { theme, showCodeCopyButton, onCopyCode, fenceRenderers, htmlPolicy, linkSchemes } = usePageView();
442
443
  const items = section.slots.items ?? [];
443
444
  return /* @__PURE__ */ jsxs2(Fragment, { children: [
444
445
  section.slots.title && /* @__PURE__ */ jsx3("h2", { className: "squisq-page-section-title squisq-page-items-title", children: section.slots.title }),
445
- /* @__PURE__ */ jsx3("ol", { className: "squisq-page-items", children: items.map((item, i) => /* @__PURE__ */ jsx3("li", { children: item.markdown ? /* @__PURE__ */ jsx3(MarkdownRenderer, { nodes: item.markdown, theme }) : item.body }, i)) })
446
+ /* @__PURE__ */ jsx3("ol", { className: "squisq-page-items", children: items.map((item, i) => /* @__PURE__ */ jsx3("li", { children: item.markdown ? /* @__PURE__ */ jsx3(
447
+ MarkdownRenderer,
448
+ {
449
+ nodes: item.markdown,
450
+ theme,
451
+ showCodeCopyButton,
452
+ onCopyCode,
453
+ fenceRenderers,
454
+ htmlPolicy,
455
+ linkSchemes
456
+ }
457
+ ) : item.body }, i)) })
446
458
  ] });
447
459
  }
448
460
  function TimelineRailSection({ section }) {
@@ -470,12 +482,33 @@ function TableSection({ section }) {
470
482
  ] });
471
483
  }
472
484
  function ProseSection({ section, block }) {
473
- const { theme } = usePageView();
474
- const heading = section.slots.title !== void 0 && block?.sourceHeading ? /* @__PURE__ */ jsx3(MarkdownRenderer, { nodes: [block.sourceHeading], theme }) : null;
485
+ const { theme, showCodeCopyButton, onCopyCode, fenceRenderers, htmlPolicy, linkSchemes } = usePageView();
486
+ const heading = section.slots.title !== void 0 && block?.sourceHeading ? /* @__PURE__ */ jsx3(
487
+ MarkdownRenderer,
488
+ {
489
+ nodes: [block.sourceHeading],
490
+ theme,
491
+ showCodeCopyButton,
492
+ onCopyCode,
493
+ htmlPolicy,
494
+ linkSchemes
495
+ }
496
+ ) : null;
475
497
  const bodyNodes = section.slots.body?.markdown ?? block?.contents;
476
498
  return /* @__PURE__ */ jsxs2("div", { className: "squisq-page-prose", children: [
477
499
  heading,
478
- bodyNodes && bodyNodes.length > 0 && /* @__PURE__ */ jsx3(MarkdownRenderer, { nodes: bodyNodes, theme })
500
+ bodyNodes && bodyNodes.length > 0 && /* @__PURE__ */ jsx3(
501
+ MarkdownRenderer,
502
+ {
503
+ nodes: bodyNodes,
504
+ theme,
505
+ showCodeCopyButton,
506
+ onCopyCode,
507
+ fenceRenderers,
508
+ htmlPolicy,
509
+ linkSchemes
510
+ }
511
+ )
479
512
  ] });
480
513
  }
481
514
  function FooterSection({ section }) {
@@ -537,7 +570,15 @@ function sectionBody(entry, featureFlip) {
537
570
  }
538
571
  }
539
572
  function PageSectionView({ entry, featureFlip, isLeadProse }) {
540
- const { pageStyle, theme } = usePageView();
573
+ const {
574
+ pageStyle,
575
+ theme,
576
+ showCodeCopyButton,
577
+ onCopyCode,
578
+ fenceRenderers,
579
+ htmlPolicy,
580
+ linkSchemes
581
+ } = usePageView();
541
582
  const { section } = entry;
542
583
  const richContent = section.slots.richContent?.markdown;
543
584
  const classes = [
@@ -566,7 +607,18 @@ function PageSectionView({ entry, featureFlip, isLeadProse }) {
566
607
  ...hintAttrs,
567
608
  children: /* @__PURE__ */ jsxs3("div", { className: "squisq-page-section-inner", children: [
568
609
  sectionBody(entry, featureFlip),
569
- richContent && richContent.length > 0 && /* @__PURE__ */ jsx4("div", { className: "squisq-page-rich-content", children: /* @__PURE__ */ jsx4(MarkdownRenderer, { nodes: richContent, theme }) })
610
+ richContent && richContent.length > 0 && /* @__PURE__ */ jsx4("div", { className: "squisq-page-rich-content", children: /* @__PURE__ */ jsx4(
611
+ MarkdownRenderer,
612
+ {
613
+ nodes: richContent,
614
+ theme,
615
+ showCodeCopyButton,
616
+ onCopyCode,
617
+ fenceRenderers,
618
+ htmlPolicy,
619
+ linkSchemes
620
+ }
621
+ ) })
570
622
  ] })
571
623
  }
572
624
  );
@@ -589,6 +641,7 @@ import {
589
641
  DEFAULT_THEME as DEFAULT_THEME2
590
642
  } from "@bendyline/squisq/doc";
591
643
  import { parseMarkdown } from "@bendyline/squisq/markdown";
644
+ import { fenceRendererLangs } from "@bendyline/squisq/fence";
592
645
  import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
593
646
  function LinearDocView({
594
647
  doc,
@@ -603,7 +656,12 @@ function LinearDocView({
603
656
  imageDisplayMode = "inline",
604
657
  globalKeyboardShortcuts = false,
605
658
  showCover = true,
606
- transformPage
659
+ transformPage,
660
+ showCodeCopyButton = false,
661
+ onCopyCode,
662
+ fenceRenderers,
663
+ htmlPolicy,
664
+ linkSchemes
607
665
  }) {
608
666
  const scrollRef = useRef(null);
609
667
  const activeViewport = viewport ?? VIEWPORT_PRESETS2.landscape;
@@ -622,6 +680,7 @@ function LinearDocView({
622
680
  () => resolvePageStyle2(activeTheme, transformPage),
623
681
  [activeTheme, transformPage]
624
682
  );
683
+ const widgetFenceLangs = useMemo2(() => fenceRendererLangs(fenceRenderers), [fenceRenderers]);
625
684
  const sections = useMemo2(() => {
626
685
  if (!resolvedDoc) return [];
627
686
  return materializePageSections(resolvedDoc, {
@@ -629,9 +688,10 @@ function LinearDocView({
629
688
  viewport: activeViewport,
630
689
  customTemplates: resolvedDoc.customTemplates,
631
690
  cover: showCover !== false ? resolvedDoc.startBlock : false,
632
- transformPage
691
+ transformPage,
692
+ ...widgetFenceLangs.length > 0 ? { widgetFenceLangs } : {}
633
693
  });
634
- }, [resolvedDoc, activeTheme, activeViewport, showCover, transformPage]);
694
+ }, [resolvedDoc, activeTheme, activeViewport, showCover, transformPage, widgetFenceLangs]);
635
695
  const renderContext = useMemo2(
636
696
  () => ({
637
697
  theme: activeTheme,
@@ -650,7 +710,12 @@ function LinearDocView({
650
710
  viewport: activeViewport,
651
711
  renderContext,
652
712
  animationsEnabled,
653
- imageDisplayMode
713
+ imageDisplayMode,
714
+ showCodeCopyButton,
715
+ onCopyCode,
716
+ fenceRenderers,
717
+ htmlPolicy,
718
+ linkSchemes
654
719
  }),
655
720
  [
656
721
  activeTheme,
@@ -659,7 +724,12 @@ function LinearDocView({
659
724
  activeViewport,
660
725
  renderContext,
661
726
  animationsEnabled,
662
- imageDisplayMode
727
+ imageDisplayMode,
728
+ showCodeCopyButton,
729
+ onCopyCode,
730
+ fenceRenderers,
731
+ htmlPolicy,
732
+ linkSchemes
663
733
  ]
664
734
  );
665
735
  const { featureOrdinals, leadProseIndex } = useMemo2(() => {