@localix-ai/flowdown 0.0.0-stage → 0.5.1

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 (231) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -2
  3. package/dist/browser/file-actions.js +80 -0
  4. package/dist/browser/file-actions.js.map +1 -0
  5. package/dist/core/block-facts.js +72 -0
  6. package/dist/core/block-facts.js.map +1 -0
  7. package/dist/core/blocks.d.ts +25 -0
  8. package/dist/core/blocks.js +464 -0
  9. package/dist/core/blocks.js.map +1 -0
  10. package/dist/core/bounded-cache.js +42 -0
  11. package/dist/core/bounded-cache.js.map +1 -0
  12. package/dist/core/continues.js +15 -0
  13. package/dist/core/continues.js.map +1 -0
  14. package/dist/core/dollars.d.ts +12 -0
  15. package/dist/core/dollars.js +88 -0
  16. package/dist/core/dollars.js.map +1 -0
  17. package/dist/core/fence-scan.js +62 -0
  18. package/dist/core/fence-scan.js.map +1 -0
  19. package/dist/core/file-tree-parse.d.ts +26 -0
  20. package/dist/core/file-tree-parse.js +51 -0
  21. package/dist/core/file-tree-parse.js.map +1 -0
  22. package/dist/core/forms-html.js +63 -0
  23. package/dist/core/forms-html.js.map +1 -0
  24. package/dist/core/forms.d.ts +28 -0
  25. package/dist/core/forms.js +23 -0
  26. package/dist/core/forms.js.map +1 -0
  27. package/dist/core/headings.d.ts +11 -0
  28. package/dist/core/headings.js +74 -0
  29. package/dist/core/headings.js.map +1 -0
  30. package/dist/core/heal.d.ts +11 -0
  31. package/dist/core/heal.js +220 -0
  32. package/dist/core/heal.js.map +1 -0
  33. package/dist/core/load-once.js +23 -0
  34. package/dist/core/load-once.js.map +1 -0
  35. package/dist/core/math-delimiters.js +33 -0
  36. package/dist/core/math-delimiters.js.map +1 -0
  37. package/dist/core/math-syntax.js +56 -0
  38. package/dist/core/math-syntax.js.map +1 -0
  39. package/dist/core/prose.d.ts +10 -0
  40. package/dist/core/prose.js +77 -0
  41. package/dist/core/prose.js.map +1 -0
  42. package/dist/core/remark-answer-blocks.d.ts +5 -0
  43. package/dist/core/remark-answer-blocks.js +172 -0
  44. package/dist/core/remark-answer-blocks.js.map +1 -0
  45. package/dist/core/remark-answer-containers.js +299 -0
  46. package/dist/core/remark-answer-containers.js.map +1 -0
  47. package/dist/core/sanitize-schema.d.ts +18 -0
  48. package/dist/core/sanitize-schema.js +62 -0
  49. package/dist/core/sanitize-schema.js.map +1 -0
  50. package/dist/core/syntax.d.ts +8 -0
  51. package/dist/core/syntax.js +32 -0
  52. package/dist/core/syntax.js.map +1 -0
  53. package/dist/dialect.d.ts +92 -0
  54. package/dist/dialect.js +176 -0
  55. package/dist/dialect.js.map +1 -0
  56. package/dist/document/fence.d.ts +25 -0
  57. package/dist/document/fence.js +102 -0
  58. package/dist/document/fence.js.map +1 -0
  59. package/dist/document/index.d.ts +46 -0
  60. package/dist/document/index.js +66 -0
  61. package/dist/document/index.js.map +1 -0
  62. package/dist/document/table.d.ts +11 -0
  63. package/dist/document/table.js +35 -0
  64. package/dist/document/table.js.map +1 -0
  65. package/dist/dom/index.d.ts +21 -0
  66. package/dist/dom/index.js +141 -0
  67. package/dist/dom/index.js.map +1 -0
  68. package/dist/dom/patch.js +104 -0
  69. package/dist/dom/patch.js.map +1 -0
  70. package/dist/element/index.d.ts +35 -0
  71. package/dist/element/index.js +64 -0
  72. package/dist/element/index.js.map +1 -0
  73. package/dist/engine/blocks.js +785 -0
  74. package/dist/engine/blocks.js.map +1 -0
  75. package/dist/engine/entities.js +83 -0
  76. package/dist/engine/entities.js.map +1 -0
  77. package/dist/engine/entity-core.js +9 -0
  78. package/dist/engine/entity-core.js.map +1 -0
  79. package/dist/engine/entity-table.js +9 -0
  80. package/dist/engine/entity-table.js.map +1 -0
  81. package/dist/engine/html.d.ts +7 -0
  82. package/dist/engine/html.js +207 -0
  83. package/dist/engine/html.js.map +1 -0
  84. package/dist/engine/index.d.ts +7 -0
  85. package/dist/engine/index.js +74 -0
  86. package/dist/engine/index.js.map +1 -0
  87. package/dist/engine/inlines.js +887 -0
  88. package/dist/engine/inlines.js.map +1 -0
  89. package/dist/engine/types.d.ts +109 -0
  90. package/dist/engine/url.d.ts +11 -0
  91. package/dist/engine/url.js +16 -0
  92. package/dist/engine/url.js.map +1 -0
  93. package/dist/engine/visit.d.ts +10 -0
  94. package/dist/engine/visit.js +29 -0
  95. package/dist/engine/visit.js.map +1 -0
  96. package/dist/highlight/highlight.d.ts +4 -0
  97. package/dist/highlight/highlight.js +144 -0
  98. package/dist/highlight/highlight.js.map +1 -0
  99. package/dist/highlight/index.d.ts +4 -0
  100. package/dist/highlight/index.js +18 -0
  101. package/dist/highlight/index.js.map +1 -0
  102. package/dist/html/index.d.ts +4 -0
  103. package/dist/html/index.js +544 -0
  104. package/dist/html/index.js.map +1 -0
  105. package/dist/index.d.ts +27 -0
  106. package/dist/index.js +13 -0
  107. package/dist/math/index.d.ts +17 -0
  108. package/dist/math/index.js +20 -0
  109. package/dist/math/index.js.map +1 -0
  110. package/dist/math/typeset.d.ts +12 -0
  111. package/dist/math/typeset.js +74 -0
  112. package/dist/math/typeset.js.map +1 -0
  113. package/dist/mermaid/diagram.d.ts +8 -0
  114. package/dist/mermaid/diagram.js +114 -0
  115. package/dist/mermaid/diagram.js.map +1 -0
  116. package/dist/mermaid/drawing.js +139 -0
  117. package/dist/mermaid/drawing.js.map +1 -0
  118. package/dist/mermaid/icons.js +16 -0
  119. package/dist/mermaid/icons.js.map +1 -0
  120. package/dist/mermaid/index.d.ts +13 -0
  121. package/dist/mermaid/index.js +3 -0
  122. package/dist/mermaid/loader.d.ts +5 -0
  123. package/dist/mermaid/loader.js +18 -0
  124. package/dist/mermaid/loader.js.map +1 -0
  125. package/dist/mermaid/look.d.ts +16 -0
  126. package/dist/mermaid/look.js +167 -0
  127. package/dist/mermaid/look.js.map +1 -0
  128. package/dist/mermaid/normalize-svg.js +44 -0
  129. package/dist/mermaid/normalize-svg.js.map +1 -0
  130. package/dist/mermaid/renderer.d.ts +22 -0
  131. package/dist/mermaid/renderer.js +132 -0
  132. package/dist/mermaid/renderer.js.map +1 -0
  133. package/dist/mermaid/viewer.js +309 -0
  134. package/dist/mermaid/viewer.js.map +1 -0
  135. package/dist/prompt/index.d.ts +19 -0
  136. package/dist/prompt/index.js +54 -0
  137. package/dist/prompt/index.js.map +1 -0
  138. package/dist/react/callout.d.ts +9 -0
  139. package/dist/react/callout.js +12 -0
  140. package/dist/react/callout.js.map +1 -0
  141. package/dist/react/code-block.d.ts +18 -0
  142. package/dist/react/code-block.js +94 -0
  143. package/dist/react/code-block.js.map +1 -0
  144. package/dist/react/context.d.ts +18 -0
  145. package/dist/react/context.js +17 -0
  146. package/dist/react/context.js.map +1 -0
  147. package/dist/react/draw.d.ts +2 -0
  148. package/dist/react/draw.js +17 -0
  149. package/dist/react/draw.js.map +1 -0
  150. package/dist/react/file-tree.d.ts +16 -0
  151. package/dist/react/file-tree.js +69 -0
  152. package/dist/react/file-tree.js.map +1 -0
  153. package/dist/react/growing-code.js +36 -0
  154. package/dist/react/growing-code.js.map +1 -0
  155. package/dist/react/helpers.d.ts +6 -0
  156. package/dist/react/helpers.js +17 -0
  157. package/dist/react/helpers.js.map +1 -0
  158. package/dist/react/icons.js +23 -0
  159. package/dist/react/icons.js.map +1 -0
  160. package/dist/react/labels.js +9 -0
  161. package/dist/react/labels.js.map +1 -0
  162. package/dist/react/markdown.d.ts +78 -0
  163. package/dist/react/markdown.js +206 -0
  164. package/dist/react/markdown.js.map +1 -0
  165. package/dist/react/render.d.ts +8 -0
  166. package/dist/react/render.js +41 -0
  167. package/dist/react/render.js.map +1 -0
  168. package/dist/react/scrolls.js +62 -0
  169. package/dist/react/scrolls.js.map +1 -0
  170. package/dist/react/table-rows.js +66 -0
  171. package/dist/react/table-rows.js.map +1 -0
  172. package/dist/react/table.js +22 -0
  173. package/dist/react/table.js.map +1 -0
  174. package/dist/react/tabs.d.ts +25 -0
  175. package/dist/react/tabs.js +58 -0
  176. package/dist/react/tabs.js.map +1 -0
  177. package/dist/react/use-smooth-text.d.ts +6 -0
  178. package/dist/react/use-smooth-text.js +157 -0
  179. package/dist/react/use-smooth-text.js.map +1 -0
  180. package/dist/react/use-stage.d.ts +9 -0
  181. package/dist/react/use-stage.js +57 -0
  182. package/dist/react/use-stage.js.map +1 -0
  183. package/dist/remark.d.ts +20 -0
  184. package/dist/remark.js +10 -0
  185. package/dist/render/factory.d.ts +15 -0
  186. package/dist/render/labels.d.ts +46 -0
  187. package/dist/render/labels.js +46 -0
  188. package/dist/render/labels.js.map +1 -0
  189. package/dist/render/tree.js +314 -0
  190. package/dist/render/tree.js.map +1 -0
  191. package/dist/render/view.d.ts +58 -0
  192. package/dist/render/view.js +306 -0
  193. package/dist/render/view.js.map +1 -0
  194. package/dist/render/views.d.ts +16 -0
  195. package/dist/render/views.js +261 -0
  196. package/dist/render/views.js.map +1 -0
  197. package/dist/render/vnode.d.ts +21 -0
  198. package/dist/render/vnode.js +194 -0
  199. package/dist/render/vnode.js.map +1 -0
  200. package/dist/static/index.d.ts +28 -0
  201. package/dist/static/index.js +27 -0
  202. package/dist/static/index.js.map +1 -0
  203. package/dist/stream.d.ts +12 -0
  204. package/dist/stream.js +3 -0
  205. package/dist/styles.css +1362 -0
  206. package/dist/tui/code.d.ts +5 -0
  207. package/dist/tui/code.js +240 -0
  208. package/dist/tui/code.js.map +1 -0
  209. package/dist/tui/index.d.ts +54 -0
  210. package/dist/tui/index.js +99 -0
  211. package/dist/tui/index.js.map +1 -0
  212. package/dist/tui/math.js +242 -0
  213. package/dist/tui/math.js.map +1 -0
  214. package/dist/tui/printable.d.ts +12 -0
  215. package/dist/tui/printable.js +31 -0
  216. package/dist/tui/printable.js.map +1 -0
  217. package/dist/tui/render.d.ts +12 -0
  218. package/dist/tui/render.js +524 -0
  219. package/dist/tui/render.js.map +1 -0
  220. package/dist/tui/style.d.ts +35 -0
  221. package/dist/tui/style.js +77 -0
  222. package/dist/tui/style.js.map +1 -0
  223. package/dist/tui/table.js +73 -0
  224. package/dist/tui/table.js.map +1 -0
  225. package/dist/tui/width.d.ts +2 -0
  226. package/dist/tui/width.js +133 -0
  227. package/dist/tui/width.js.map +1 -0
  228. package/dist/tui/wrap.d.ts +8 -0
  229. package/dist/tui/wrap.js +191 -0
  230. package/dist/tui/wrap.js.map +1 -0
  231. package/package.json +128 -4
@@ -0,0 +1,309 @@
1
+ "use client";
2
+ import { useLabels } from "../react/labels.js";
3
+ import { CloseIcon, FitIcon, MinusIcon, PlusIcon } from "./icons.js";
4
+ import { useEffect, useMemo, useRef, useState } from "react";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ // src/mermaid/viewer.tsx
7
+ /** A button or key zooms by this much; the wheel by its own delta. */
8
+ const STEP = 1.25;
9
+ /** An arrow key pans by this many pixels. */
10
+ const PAN = 48;
11
+ /** Room left around a fitted diagram. */
12
+ const PADDING = 32;
13
+ /**
14
+ * A diagram's size in its own units, from the opening tag alone: its `viewBox`, else its `width`
15
+ * and `height`. Mermaid writes `width="100%"`, so a percentage counts as unknown.
16
+ */
17
+ function diagramBounds(svg) {
18
+ const root = /<svg\b[^>]*>/i.exec(svg)?.[0] ?? "";
19
+ const attribute = (name) => new RegExp(`\\s${name}\\s*=\\s*(["'])(.*?)\\1`, "i").exec(root)?.[2];
20
+ const length = (name) => {
21
+ const value = attribute(name);
22
+ return value?.includes("%") ? void 0 : Number.parseFloat(value ?? "");
23
+ };
24
+ const box = attribute("viewBox")?.trim().split(/[\s,]+/).map(Number);
25
+ const positive = (value, fallback) => value !== void 0 && Number.isFinite(value) && value > 0 ? value : fallback;
26
+ return {
27
+ width: positive(box?.[2] ?? length("width"), 800),
28
+ height: positive(box?.[3] ?? length("height"), 600)
29
+ };
30
+ }
31
+ /**
32
+ * The scale that shows the whole diagram on the stage, kept as the stage resizes. A small diagram
33
+ * is enlarged up to twice its size; a stage not laid out yet (a test DOM) shows it at its own.
34
+ */
35
+ function useFitScale(stage, size) {
36
+ const [fit, setFit] = useState(1);
37
+ useEffect(() => {
38
+ const element = stage.current;
39
+ if (!element) return;
40
+ const measure = () => {
41
+ const { width, height } = element.getBoundingClientRect();
42
+ if (width <= PADDING || height <= PADDING) return;
43
+ setFit(Math.min((width - PADDING) / size.width, (height - PADDING) / size.height, 2));
44
+ };
45
+ measure();
46
+ if (typeof ResizeObserver === "undefined") return;
47
+ const observer = new ResizeObserver(measure);
48
+ observer.observe(element);
49
+ return () => observer.disconnect();
50
+ }, [stage, size]);
51
+ return fit;
52
+ }
53
+ /** `from` zoomed by `factor` around `point` (relative to the stage's centre), within the limits. */
54
+ function zoomed(from, factor, point, fit) {
55
+ const scale = Math.min(Math.max(fit, 1) * 8, Math.max(Math.min(fit, 1) / 4, from.scale * factor));
56
+ const k = scale / from.scale;
57
+ return {
58
+ scale,
59
+ x: point.x - (point.x - from.x) * k,
60
+ y: point.y - (point.y - from.y) * k
61
+ };
62
+ }
63
+ const centre = {
64
+ x: 0,
65
+ y: 0
66
+ };
67
+ /** The first two pointers' midpoint and distance apart; the distance is 0 for one pointer. */
68
+ function spread(pointers) {
69
+ const [first, second] = [...pointers.values()];
70
+ if (!first) return {
71
+ start: centre,
72
+ distance: 0
73
+ };
74
+ if (!second) return {
75
+ start: first,
76
+ distance: 0
77
+ };
78
+ return {
79
+ start: {
80
+ x: (first.x + second.x) / 2,
81
+ y: (first.y + second.y) / 2
82
+ },
83
+ distance: Math.hypot(second.x - first.x, second.y - first.y)
84
+ };
85
+ }
86
+ const ZOOM_KEYS = /* @__PURE__ */ new Map([
87
+ ["+", STEP],
88
+ ["=", STEP],
89
+ ["-", 1 / STEP],
90
+ ["_", 1 / STEP]
91
+ ]);
92
+ /** The picture follows the key: the right arrow shows what is to the right. */
93
+ const PAN_KEYS = /* @__PURE__ */ new Map([
94
+ ["ArrowLeft", {
95
+ x: PAN,
96
+ y: 0
97
+ }],
98
+ ["ArrowRight", {
99
+ x: -48,
100
+ y: 0
101
+ }],
102
+ ["ArrowUp", {
103
+ x: 0,
104
+ y: PAN
105
+ }],
106
+ ["ArrowDown", {
107
+ x: 0,
108
+ y: -48
109
+ }]
110
+ ]);
111
+ function DiagramViewer({ svg, onClose }) {
112
+ const labels = useLabels();
113
+ const dialog = useRef(null);
114
+ const stage = useRef(null);
115
+ const size = useMemo(() => diagramBounds(svg), [svg]);
116
+ const fit = useFitScale(stage, size);
117
+ const [moved, setMoved] = useState(null);
118
+ const [moving, setMoving] = useState(false);
119
+ const view = moved ?? {
120
+ scale: fit,
121
+ x: 0,
122
+ y: 0
123
+ };
124
+ const pointers = useRef(/* @__PURE__ */ new Map());
125
+ const gesture = useRef(null);
126
+ useEffect(() => {
127
+ const element = dialog.current;
128
+ if (!element) return;
129
+ if (!element.open) element.showModal();
130
+ stage.current?.focus();
131
+ }, []);
132
+ useEffect(() => {
133
+ const element = stage.current;
134
+ if (!element) return;
135
+ const wheel = (event) => {
136
+ event.preventDefault();
137
+ const box = element.getBoundingClientRect();
138
+ const point = {
139
+ x: event.clientX - box.left - box.width / 2,
140
+ y: event.clientY - box.top - box.height / 2
141
+ };
142
+ const delta = Math.max(-100, Math.min(100, event.deltaY));
143
+ const factor = Math.exp(-delta * (event.ctrlKey ? .01 : .002));
144
+ setMoving(true);
145
+ setMoved((current) => zoomed(current ?? {
146
+ scale: fit,
147
+ x: 0,
148
+ y: 0
149
+ }, factor, point, fit));
150
+ };
151
+ element.addEventListener("wheel", wheel, { passive: false });
152
+ return () => element.removeEventListener("wheel", wheel);
153
+ }, [fit]);
154
+ const zoomBy = (factor) => {
155
+ setMoving(false);
156
+ setMoved(zoomed(view, factor, centre, fit));
157
+ };
158
+ const actualSize = () => {
159
+ setMoving(false);
160
+ setMoved(zoomed(view, 1 / view.scale, centre, fit));
161
+ };
162
+ const fitToStage = () => {
163
+ setMoving(false);
164
+ setMoved(null);
165
+ };
166
+ /** Starts a drag or a pinch from the pointers down now, from the view shown now. */
167
+ const begin = (from) => {
168
+ const now = spread(pointers.current);
169
+ gesture.current = pointers.current.size ? {
170
+ from,
171
+ ...now
172
+ } : null;
173
+ setMoving(pointers.current.size > 0);
174
+ };
175
+ const onPointerDown = (event) => {
176
+ if (event.pointerType === "mouse" && event.button !== 0) return;
177
+ event.currentTarget.setPointerCapture?.(event.pointerId);
178
+ pointers.current.set(event.pointerId, {
179
+ x: event.clientX,
180
+ y: event.clientY
181
+ });
182
+ begin(view);
183
+ };
184
+ const onPointerMove = (event) => {
185
+ const start = gesture.current;
186
+ if (!start || !pointers.current.has(event.pointerId)) return;
187
+ pointers.current.set(event.pointerId, {
188
+ x: event.clientX,
189
+ y: event.clientY
190
+ });
191
+ const now = spread(pointers.current);
192
+ const box = event.currentTarget.getBoundingClientRect();
193
+ const anchor = {
194
+ x: start.start.x - box.left - box.width / 2,
195
+ y: start.start.y - box.top - box.height / 2
196
+ };
197
+ const factor = start.distance > 0 && now.distance > 0 ? now.distance / start.distance : 1;
198
+ const next = zoomed(start.from, factor, anchor, fit);
199
+ setMoved({
200
+ scale: next.scale,
201
+ x: next.x + now.start.x - start.start.x,
202
+ y: next.y + now.start.y - start.start.y
203
+ });
204
+ };
205
+ const onPointerUp = (event) => {
206
+ if (!pointers.current.delete(event.pointerId)) return;
207
+ begin(view);
208
+ };
209
+ const onKeyDown = (event) => {
210
+ if (event.key === "Escape") {
211
+ event.preventDefault();
212
+ dialog.current?.close();
213
+ return;
214
+ }
215
+ if (event.altKey || event.metaKey || event.ctrlKey) return;
216
+ const factor = ZOOM_KEYS.get(event.key);
217
+ const pan = PAN_KEYS.get(event.key);
218
+ if (factor) zoomBy(factor);
219
+ else if (event.key === "0") fitToStage();
220
+ else if (pan) {
221
+ setMoving(false);
222
+ setMoved({
223
+ ...view,
224
+ x: view.x + pan.x,
225
+ y: view.y + pan.y
226
+ });
227
+ } else return;
228
+ event.preventDefault();
229
+ };
230
+ const percent = `${Math.round(view.scale * 100)}%`;
231
+ return /* @__PURE__ */ jsxs("dialog", {
232
+ ref: dialog,
233
+ className: "fd-diagram-viewer",
234
+ "aria-label": labels.diagram,
235
+ onClose,
236
+ onKeyDown,
237
+ onClick: (event) => {
238
+ if (event.target === event.currentTarget) event.currentTarget.close();
239
+ },
240
+ children: [/* @__PURE__ */ jsxs("div", {
241
+ className: "fd-diagram-viewer-bar",
242
+ children: [
243
+ /* @__PURE__ */ jsx("button", {
244
+ type: "button",
245
+ className: "fd-diagram-action",
246
+ "aria-label": labels.zoomOut,
247
+ title: labels.zoomOut,
248
+ onClick: () => zoomBy(1 / STEP),
249
+ children: /* @__PURE__ */ jsx(MinusIcon, {})
250
+ }),
251
+ /* @__PURE__ */ jsx("button", {
252
+ type: "button",
253
+ className: "fd-diagram-action fd-diagram-zoom",
254
+ "aria-label": `${labels.actualSize} (${percent})`,
255
+ title: labels.actualSize,
256
+ onClick: actualSize,
257
+ children: percent
258
+ }),
259
+ /* @__PURE__ */ jsx("button", {
260
+ type: "button",
261
+ className: "fd-diagram-action",
262
+ "aria-label": labels.zoomIn,
263
+ title: labels.zoomIn,
264
+ onClick: () => zoomBy(STEP),
265
+ children: /* @__PURE__ */ jsx(PlusIcon, {})
266
+ }),
267
+ /* @__PURE__ */ jsx("button", {
268
+ type: "button",
269
+ className: "fd-diagram-action",
270
+ "aria-label": labels.fit,
271
+ title: labels.fit,
272
+ onClick: fitToStage,
273
+ children: /* @__PURE__ */ jsx(FitIcon, {})
274
+ }),
275
+ /* @__PURE__ */ jsx("button", {
276
+ type: "button",
277
+ className: "fd-diagram-action fd-diagram-close",
278
+ "aria-label": labels.close,
279
+ title: labels.close,
280
+ onClick: () => dialog.current?.close(),
281
+ children: /* @__PURE__ */ jsx(CloseIcon, {})
282
+ })
283
+ ]
284
+ }), /* @__PURE__ */ jsx("div", {
285
+ ref: stage,
286
+ className: "fd-diagram-stage",
287
+ role: "application",
288
+ "aria-label": labels.diagram,
289
+ tabIndex: 0,
290
+ "data-moving": moving || void 0,
291
+ onPointerDown,
292
+ onPointerMove,
293
+ onPointerUp,
294
+ onPointerCancel: onPointerUp,
295
+ children: /* @__PURE__ */ jsx("div", {
296
+ className: "fd-diagram-viewer-canvas",
297
+ style: {
298
+ width: size.width,
299
+ height: size.height,
300
+ transform: `translate(${view.x}px, ${view.y}px) scale(${view.scale})`
301
+ },
302
+ dangerouslySetInnerHTML: { __html: svg }
303
+ })
304
+ })]
305
+ });
306
+ }
307
+ export { DiagramViewer };
308
+
309
+ //# sourceMappingURL=viewer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"viewer.js","names":[],"sources":["../../src/mermaid/viewer.tsx"],"sourcesContent":["'use client'\nimport {\n useEffect,\n useMemo,\n useRef,\n useState,\n type KeyboardEvent,\n type PointerEvent,\n type RefObject,\n} from 'react'\nimport { useLabels } from '../react/labels'\nimport { CloseIcon, FitIcon, MinusIcon, PlusIcon } from './icons'\n\n/**\n * The enlarged diagram: a modal `<dialog>` over the page, with the picture on a stage that pans\n * and zooms. Plain state and a CSS transform; no gesture library. Loaded only when a diagram is\n * first enlarged (`diagram.tsx`).\n */\n\n/** Where the picture sits: its scale (1 is its own size) and its offset from the stage's centre. */\ninterface View {\n scale: number\n x: number\n y: number\n}\ninterface Point {\n x: number\n y: number\n}\n/** A drag or a pinch in progress, measured from where it started. */\ninterface Gesture {\n from: View\n start: Point\n distance: number\n}\n\n/** A button or key zooms by this much; the wheel by its own delta. */\nconst STEP = 1.25\n/** An arrow key pans by this many pixels. */\nconst PAN = 48\n/** Room left around a fitted diagram. */\nconst PADDING = 32\n\n/**\n * A diagram's size in its own units, from the opening tag alone: its `viewBox`, else its `width`\n * and `height`. Mermaid writes `width=\"100%\"`, so a percentage counts as unknown.\n */\nfunction diagramBounds(svg: string) {\n const root = /<svg\\b[^>]*>/i.exec(svg)?.[0] ?? ''\n const attribute = (name: string) =>\n new RegExp(`\\\\s${name}\\\\s*=\\\\s*([\"'])(.*?)\\\\1`, 'i').exec(root)?.[2]\n const length = (name: string) => {\n const value = attribute(name)\n return value?.includes('%') ? undefined : Number.parseFloat(value ?? '')\n }\n const box = attribute('viewBox')\n ?.trim()\n .split(/[\\s,]+/)\n .map(Number)\n const positive = (value: number | undefined, fallback: number) =>\n value !== undefined && Number.isFinite(value) && value > 0 ? value : fallback\n return {\n width: positive(box?.[2] ?? length('width'), 800),\n height: positive(box?.[3] ?? length('height'), 600),\n }\n}\n\n/**\n * The scale that shows the whole diagram on the stage, kept as the stage resizes. A small diagram\n * is enlarged up to twice its size; a stage not laid out yet (a test DOM) shows it at its own.\n */\nfunction useFitScale(\n stage: RefObject<HTMLDivElement | null>,\n size: { width: number; height: number },\n) {\n const [fit, setFit] = useState(1)\n useEffect(() => {\n const element = stage.current\n if (!element) return\n const measure = () => {\n const { width, height } = element.getBoundingClientRect()\n if (width <= PADDING || height <= PADDING) return\n setFit(Math.min((width - PADDING) / size.width, (height - PADDING) / size.height, 2))\n }\n measure()\n if (typeof ResizeObserver === 'undefined') return\n const observer = new ResizeObserver(measure)\n observer.observe(element)\n return () => observer.disconnect()\n }, [stage, size])\n return fit\n}\n\n/** `from` zoomed by `factor` around `point` (relative to the stage's centre), within the limits. */\nfunction zoomed(from: View, factor: number, point: Point, fit: number): View {\n const scale = Math.min(Math.max(fit, 1) * 8, Math.max(Math.min(fit, 1) / 4, from.scale * factor))\n const k = scale / from.scale\n return { scale, x: point.x - (point.x - from.x) * k, y: point.y - (point.y - from.y) * k }\n}\n\nconst centre: Point = { x: 0, y: 0 }\n\n/** The first two pointers' midpoint and distance apart; the distance is 0 for one pointer. */\nfunction spread(pointers: Map<number, Point>): { start: Point; distance: number } {\n const [first, second] = [...pointers.values()]\n if (!first) return { start: centre, distance: 0 }\n if (!second) return { start: first, distance: 0 }\n return {\n start: { x: (first.x + second.x) / 2, y: (first.y + second.y) / 2 },\n distance: Math.hypot(second.x - first.x, second.y - first.y),\n }\n}\n\nconst ZOOM_KEYS = new Map([\n ['+', STEP],\n ['=', STEP],\n ['-', 1 / STEP],\n ['_', 1 / STEP],\n])\n/** The picture follows the key: the right arrow shows what is to the right. */\nconst PAN_KEYS = new Map<string, Point>([\n ['ArrowLeft', { x: PAN, y: 0 }],\n ['ArrowRight', { x: -PAN, y: 0 }],\n ['ArrowUp', { x: 0, y: PAN }],\n ['ArrowDown', { x: 0, y: -PAN }],\n])\n\nexport function DiagramViewer({ svg, onClose }: { svg: string; onClose: () => void }) {\n const labels = useLabels()\n const dialog = useRef<HTMLDialogElement>(null)\n const stage = useRef<HTMLDivElement>(null)\n const size = useMemo(() => diagramBounds(svg), [svg])\n const fit = useFitScale(stage, size)\n // Null until the reader moves the picture: it stays fitted while the window resizes.\n const [moved, setMoved] = useState<View | null>(null)\n // A drag or pinch follows the finger; only a button or a key glides (`data-moving` off).\n const [moving, setMoving] = useState(false)\n const view = moved ?? { scale: fit, x: 0, y: 0 }\n const pointers = useRef(new Map<number, Point>())\n const gesture = useRef<Gesture | null>(null)\n\n useEffect(() => {\n const element = dialog.current\n if (!element) return\n if (!element.open) element.showModal()\n stage.current?.focus()\n // No close on cleanup: a dialog leaves the top layer with the page when it unmounts, and a\n // close here would end the viewer in Strict Mode, whose second mount follows a cleanup.\n }, [])\n\n // The wheel zooms at the pointer; a trackpad pinch arrives as the wheel with Ctrl held. A\n // listener of its own, because React's is passive and the page behind must not scroll.\n useEffect(() => {\n const element = stage.current\n if (!element) return\n const wheel = (event: WheelEvent) => {\n event.preventDefault()\n const box = element.getBoundingClientRect()\n const point = {\n x: event.clientX - box.left - box.width / 2,\n y: event.clientY - box.top - box.height / 2,\n }\n const delta = Math.max(-100, Math.min(100, event.deltaY))\n const factor = Math.exp(-delta * (event.ctrlKey ? 0.01 : 0.002))\n setMoving(true)\n setMoved((current) => zoomed(current ?? { scale: fit, x: 0, y: 0 }, factor, point, fit))\n }\n element.addEventListener('wheel', wheel, { passive: false })\n return () => element.removeEventListener('wheel', wheel)\n }, [fit])\n\n const zoomBy = (factor: number) => {\n setMoving(false)\n setMoved(zoomed(view, factor, centre, fit))\n }\n const actualSize = () => {\n setMoving(false)\n setMoved(zoomed(view, 1 / view.scale, centre, fit))\n }\n const fitToStage = () => {\n setMoving(false)\n setMoved(null)\n }\n\n /** Starts a drag or a pinch from the pointers down now, from the view shown now. */\n const begin = (from: View) => {\n const now = spread(pointers.current)\n gesture.current = pointers.current.size ? { from, ...now } : null\n setMoving(pointers.current.size > 0)\n }\n const onPointerDown = (event: PointerEvent<HTMLDivElement>) => {\n if (event.pointerType === 'mouse' && event.button !== 0) return\n event.currentTarget.setPointerCapture?.(event.pointerId)\n pointers.current.set(event.pointerId, { x: event.clientX, y: event.clientY })\n begin(view)\n }\n const onPointerMove = (event: PointerEvent<HTMLDivElement>) => {\n const start = gesture.current\n if (!start || !pointers.current.has(event.pointerId)) return\n pointers.current.set(event.pointerId, { x: event.clientX, y: event.clientY })\n const now = spread(pointers.current)\n const box = event.currentTarget.getBoundingClientRect()\n const anchor = {\n x: start.start.x - box.left - box.width / 2,\n y: start.start.y - box.top - box.height / 2,\n }\n const factor = start.distance > 0 && now.distance > 0 ? now.distance / start.distance : 1\n const next = zoomed(start.from, factor, anchor, fit)\n setMoved({\n scale: next.scale,\n x: next.x + now.start.x - start.start.x,\n y: next.y + now.start.y - start.start.y,\n })\n }\n const onPointerUp = (event: PointerEvent<HTMLDivElement>) => {\n if (!pointers.current.delete(event.pointerId)) return\n // A finger lifted from a pinch: the one left drags on from here.\n begin(view)\n }\n\n const onKeyDown = (event: KeyboardEvent<HTMLDialogElement>) => {\n if (event.key === 'Escape') {\n // Closed here rather than by the dialog's own cancel, so every browser closes the same way.\n event.preventDefault()\n dialog.current?.close()\n return\n }\n if (event.altKey || event.metaKey || event.ctrlKey) return\n const factor = ZOOM_KEYS.get(event.key)\n const pan = PAN_KEYS.get(event.key)\n if (factor) zoomBy(factor)\n else if (event.key === '0') fitToStage()\n else if (pan) {\n setMoving(false)\n setMoved({ ...view, x: view.x + pan.x, y: view.y + pan.y })\n } else return\n event.preventDefault()\n }\n\n const percent = `${Math.round(view.scale * 100)}%`\n return (\n // The dialog's keys and its backdrop click; every control in it is a button of its own.\n // oxlint-disable-next-line jsx-a11y/no-noninteractive-element-interactions\n <dialog\n ref={dialog}\n className=\"fd-diagram-viewer\"\n aria-label={labels.diagram}\n onClose={onClose}\n onKeyDown={onKeyDown}\n // A click on the backdrop lands on the dialog itself: it closes, as a click outside does.\n onClick={(event) => {\n if (event.target === event.currentTarget) event.currentTarget.close()\n }}\n >\n <div className=\"fd-diagram-viewer-bar\">\n <button\n type=\"button\"\n className=\"fd-diagram-action\"\n aria-label={labels.zoomOut}\n title={labels.zoomOut}\n onClick={() => zoomBy(1 / STEP)}\n >\n <MinusIcon />\n </button>\n <button\n type=\"button\"\n className=\"fd-diagram-action fd-diagram-zoom\"\n aria-label={`${labels.actualSize} (${percent})`}\n title={labels.actualSize}\n onClick={actualSize}\n >\n {percent}\n </button>\n <button\n type=\"button\"\n className=\"fd-diagram-action\"\n aria-label={labels.zoomIn}\n title={labels.zoomIn}\n onClick={() => zoomBy(STEP)}\n >\n <PlusIcon />\n </button>\n <button\n type=\"button\"\n className=\"fd-diagram-action\"\n aria-label={labels.fit}\n title={labels.fit}\n onClick={fitToStage}\n >\n <FitIcon />\n </button>\n <button\n type=\"button\"\n className=\"fd-diagram-action fd-diagram-close\"\n aria-label={labels.close}\n title={labels.close}\n onClick={() => dialog.current?.close()}\n >\n <CloseIcon />\n </button>\n </div>\n {/* A pan and zoom surface: it takes the focus so the keys reach it, and handles its own. */}\n <div\n ref={stage}\n className=\"fd-diagram-stage\"\n role=\"application\"\n aria-label={labels.diagram}\n // oxlint-disable-next-line jsx-a11y/no-noninteractive-tabindex\n tabIndex={0}\n data-moving={moving || undefined}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={onPointerUp}\n onPointerCancel={onPointerUp}\n >\n <div\n className=\"fd-diagram-viewer-canvas\"\n style={{\n width: size.width,\n height: size.height,\n transform: `translate(${view.x}px, ${view.y}px) scale(${view.scale})`,\n }}\n // The same SVG the page shows, from Mermaid in strict mode.\n dangerouslySetInnerHTML={{ __html: svg }}\n />\n </div>\n </dialog>\n )\n}\n"],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import { ANSWER_FORM_EXAMPLES, CALLOUT_KINDS, CalloutKind, DIAGRAM_FENCE_LANGUAGE, DIAGRAM_KINDS, TREE_FENCE_LANGUAGE } from "../dialect.js";
2
+ export interface AnswerFormatOptions {
3
+ /** The section's heading line. */
4
+ header?: string;
5
+ /** Mention `$…$` math. Off by default, like `<Markdown math>`: turn both on together. */
6
+ math?: boolean;
7
+ /** Teach ```mermaid diagrams. Off by default, like `<Markdown mermaid>`: turn both on together. */
8
+ diagrams?: boolean;
9
+ }
10
+ export declare const ANSWER_FORMAT_HEADER = "# Answer formatting";
11
+ /**
12
+ * One line for the rules at the top of a system prompt. Described only in a section further down,
13
+ * the forms read as an optional reference: a model still answered "plan by day" with a numbered
14
+ * list and "worth it?" with prose. Stated among the leading rules, they are part of answering.
15
+ */
16
+ export declare function answerFormsPrinciple(header?: string): string;
17
+ /** The section that teaches the dialect, ready to place in a system prompt. */
18
+ export declare function answerFormatPrompt({ header, math, diagrams }?: AnswerFormatOptions): string;
19
+ export { ANSWER_FORM_EXAMPLES, CALLOUT_KINDS, type CalloutKind, DIAGRAM_FENCE_LANGUAGE, DIAGRAM_KINDS, TREE_FENCE_LANGUAGE };
@@ -0,0 +1,54 @@
1
+ import { ANSWER_FORM_EXAMPLES, CALLOUT_KINDS, DIAGRAM_FENCE_LANGUAGE, DIAGRAM_KINDS, TREE_FENCE_LANGUAGE } from "../dialect.js";
2
+ // src/prompt/index.ts
3
+ /**
4
+ * `@localix-ai/flowdown/prompt`: the instructions that teach a model to write the dialect.
5
+ *
6
+ * A model writes the forms it was told about and no others: without these lines, a renderer's
7
+ * steps, tabs and callouts go unused, and a reply's shape is whatever the model defaults to
8
+ * (headings on a two-line answer, bullet lists where a comparison belonged, a warning buried
9
+ * mid-paragraph). Every marker and example here is read from `dialect.ts`, the file the renderer
10
+ * reads too, and the tests render each example: the prompt cannot teach a form that would print as
11
+ * raw text.
12
+ *
13
+ * Plain text, no dependencies, safe on a server. Every character rides every request's prompt, so
14
+ * a rule is one line, and only the forms a model cannot know are shown in full.
15
+ */
16
+ const ANSWER_FORMAT_HEADER = "# Answer formatting";
17
+ /** An example under its rule line, two spaces in, blank lines kept blank. */
18
+ const shown = (lines) => lines.map((line) => line ? ` ${line}` : "").join("\n");
19
+ /**
20
+ * One line for the rules at the top of a system prompt. Described only in a section further down,
21
+ * the forms read as an optional reference: a model still answered "plan by day" with a numbered
22
+ * list and "worth it?" with prose. Stated among the leading rules, they are part of answering.
23
+ */
24
+ function answerFormsPrinciple(header = ANSWER_FORMAT_HEADER) {
25
+ return `- Shape every reply with the forms in "${header.replace(/^#+\s*/, "")}": the page renders them, and a plain list or table where a form fits is a formatting defect.`;
26
+ }
27
+ /** The section that teaches the dialect, ready to place in a system prompt. */
28
+ function answerFormatPrompt({ header = ANSWER_FORMAT_HEADER, math = false, diagrams = false } = {}) {
29
+ const markers = CALLOUT_KINDS.map((kind) => `[!${kind.toUpperCase()}]`).join(" ");
30
+ const { comparison, verdict, steps, stats, details, sources } = ANSWER_FORM_EXAMPLES;
31
+ const lines = [
32
+ header,
33
+ answerFormsPrinciple(header),
34
+ `Replies render as GitHub Markdown${math ? " with $…$ math" : ""} plus the forms below; the user's style preferences override them.`,
35
+ "Match the content to the first form that fits; a table or list is the fallback. Write a marker as its own quote line, the content unquoted after it.",
36
+ "- Lead with the answer. Short replies use prose; long ones use `##` per thread, short paragraphs and selective bold.",
37
+ `- Options compared (X vs Y, which to use) or variants (per OS, language): a tab per option, \`#### Label\`; a compared option holds its sides. After them a \`---\`, then one line on when to pick which:\n${shown(comparison)}`,
38
+ `- One option weighed (should I, is it worth it): its sides, then the verdict:\n${shown(verdict)}`,
39
+ `- Stages in order (a plan, recovery, schedule, by day, a procedure): steps, never a numbered list:\n${shown(steps)}`,
40
+ `- Headline figures (speedups, sizes, key numbers): stats:\n${shown(stats)}`,
41
+ "- Exact values across options (specs, prices, limits): a table.",
42
+ `- A risk or key aside: one or two callouts, quotes starting \`> [!KIND]\` (${markers}); text after the marker titles it.`,
43
+ `- Long secondary material (a full log, raw output, a derivation): folded, its content quoted:\n${shown(details)}`,
44
+ `- Pages the reply draws on: sources, last:\n${shown(sources)}`,
45
+ `- A directory layout: a \`\`\`${TREE_FENCE_LANGUAGE} fence of \`├──\`/\`└──\` lines. Things to do: \`- [ ] item\`.`,
46
+ "- Code for a file names it: ```ts title=\"src/app.ts\". A change to code: a ```diff fence."
47
+ ];
48
+ if (diagrams) lines.push(`- Draw what a picture explains better in a \`\`\`${DIAGRAM_FENCE_LANGUAGE} fence (${DIAGRAM_KINDS.join(", ")}); quote labels with punctuation.`, "- Draw proactively for branching flows, structure and data; prefer one useful picture over ASCII art. Use supplied data; label illustrative data. Never claim an unsupported chart type.");
49
+ lines.push("- Quote attribution: `> — Author`. Image caption: `![alt](url \"Caption\")`.");
50
+ return lines.join("\n");
51
+ }
52
+ export { ANSWER_FORMAT_HEADER, ANSWER_FORM_EXAMPLES, CALLOUT_KINDS, DIAGRAM_FENCE_LANGUAGE, DIAGRAM_KINDS, TREE_FENCE_LANGUAGE, answerFormatPrompt, answerFormsPrinciple };
53
+
54
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/prompt/index.ts"],"sourcesContent":["/**\n * `@localix-ai/flowdown/prompt`: the instructions that teach a model to write the dialect.\n *\n * A model writes the forms it was told about and no others: without these lines, a renderer's\n * steps, tabs and callouts go unused, and a reply's shape is whatever the model defaults to\n * (headings on a two-line answer, bullet lists where a comparison belonged, a warning buried\n * mid-paragraph). Every marker and example here is read from `dialect.ts`, the file the renderer\n * reads too, and the tests render each example: the prompt cannot teach a form that would print as\n * raw text.\n *\n * Plain text, no dependencies, safe on a server. Every character rides every request's prompt, so\n * a rule is one line, and only the forms a model cannot know are shown in full.\n */\nimport {\n ANSWER_FORM_EXAMPLES,\n CALLOUT_KINDS,\n DIAGRAM_FENCE_LANGUAGE,\n DIAGRAM_KINDS,\n TREE_FENCE_LANGUAGE,\n} from '../dialect'\n\n// The names a prompt of your own builds its lines from, the same ones the renderer reads.\nexport {\n ANSWER_FORM_EXAMPLES,\n CALLOUT_KINDS,\n DIAGRAM_FENCE_LANGUAGE,\n DIAGRAM_KINDS,\n TREE_FENCE_LANGUAGE,\n type CalloutKind,\n} from '../dialect'\n\nexport interface AnswerFormatOptions {\n /** The section's heading line. */\n header?: string\n /** Mention `$…$` math. Off by default, like `<Markdown math>`: turn both on together. */\n math?: boolean\n /** Teach ```mermaid diagrams. Off by default, like `<Markdown mermaid>`: turn both on together. */\n diagrams?: boolean\n}\n\nexport const ANSWER_FORMAT_HEADER = '# Answer formatting'\n\n/** An example under its rule line, two spaces in, blank lines kept blank. */\nconst shown = (lines: readonly string[]) =>\n lines.map((line) => (line ? ` ${line}` : '')).join('\\n')\n\n/**\n * One line for the rules at the top of a system prompt. Described only in a section further down,\n * the forms read as an optional reference: a model still answered \"plan by day\" with a numbered\n * list and \"worth it?\" with prose. Stated among the leading rules, they are part of answering.\n */\nexport function answerFormsPrinciple(header = ANSWER_FORMAT_HEADER): string {\n return `- Shape every reply with the forms in \"${header.replace(/^#+\\s*/, '')}\": the page renders them, and a plain list or table where a form fits is a formatting defect.`\n}\n\n/** The section that teaches the dialect, ready to place in a system prompt. */\nexport function answerFormatPrompt({\n header = ANSWER_FORMAT_HEADER,\n math = false,\n diagrams = false,\n}: AnswerFormatOptions = {}): string {\n const markers = CALLOUT_KINDS.map((kind) => `[!${kind.toUpperCase()}]`).join(' ')\n const { comparison, verdict, steps, stats, details, sources } = ANSWER_FORM_EXAMPLES\n const lines = [\n header,\n answerFormsPrinciple(header),\n `Replies render as GitHub Markdown${math ? ' with $…$ math' : ''} plus the forms below; the user's style preferences override them.`,\n // A decision list, read top-down, first match wins. A catalogue of forms made \"compare options\n // in a table\" the only rule a \"compare X and Y\" request matched, and every comparison a table.\n 'Match the content to the first form that fits; a table or list is the fallback. Write a marker as its own quote line, the content unquoted after it.',\n '- Lead with the answer. Short replies use prose; long ones use `##` per thread, short paragraphs and selective bold.',\n `- Options compared (X vs Y, which to use) or variants (per OS, language): a tab per option, \\`#### Label\\`; a compared option holds its sides. After them a \\`---\\`, then one line on when to pick which:\\n${shown(comparison)}`,\n `- One option weighed (should I, is it worth it): its sides, then the verdict:\\n${shown(verdict)}`,\n `- Stages in order (a plan, recovery, schedule, by day, a procedure): steps, never a numbered list:\\n${shown(steps)}`,\n `- Headline figures (speedups, sizes, key numbers): stats:\\n${shown(stats)}`,\n '- Exact values across options (specs, prices, limits): a table.',\n `- A risk or key aside: one or two callouts, quotes starting \\`> [!KIND]\\` (${markers}); text after the marker titles it.`,\n `- Long secondary material (a full log, raw output, a derivation): folded, its content quoted:\\n${shown(details)}`,\n `- Pages the reply draws on: sources, last:\\n${shown(sources)}`,\n `- A directory layout: a \\`\\`\\`${TREE_FENCE_LANGUAGE} fence of \\`├──\\`/\\`└──\\` lines. Things to do: \\`- [ ] item\\`.`,\n '- Code for a file names it: ```ts title=\"src/app.ts\". A change to code: a ```diff fence.',\n ]\n if (diagrams) {\n lines.push(\n `- Draw what a picture explains better in a \\`\\`\\`${DIAGRAM_FENCE_LANGUAGE} fence (${DIAGRAM_KINDS.join(', ')}); quote labels with punctuation.`,\n '- Draw proactively for branching flows, structure and data; prefer one useful picture over ASCII art. Use supplied data; label illustrative data. Never claim an unsupported chart type.',\n )\n }\n lines.push('- Quote attribution: `> — Author`. Image caption: `![alt](url \"Caption\")`.')\n return lines.join('\\n')\n}\n"],"mappings":";;;;;;;;;;;;;;;AAwCA,MAAa,uBAAuB;;AAGpC,MAAM,SAAS,UACb,MAAM,KAAK,SAAU,OAAO,KAAK,SAAS,EAAG,CAAC,CAAC,KAAK,IAAI;;;;;;AAO1D,SAAgB,qBAAqB,SAAS,sBAA8B;CAC1E,OAAO,0CAA0C,OAAO,QAAQ,UAAU,EAAE,EAAE;AAChF;;AAGA,SAAgB,mBAAmB,EACjC,SAAS,sBACT,OAAO,OACP,WAAW,UACY,CAAC,GAAW;CACnC,MAAM,UAAU,cAAc,KAAK,SAAS,KAAK,KAAK,YAAY,EAAE,EAAE,CAAC,CAAC,KAAK,GAAG;CAChF,MAAM,EAAE,YAAY,SAAS,OAAO,OAAO,SAAS,YAAY;CAChE,MAAM,QAAQ;EACZ;EACA,qBAAqB,MAAM;EAC3B,oCAAoC,OAAO,mBAAmB,GAAG;EAGjE;EACA;EACA,8MAA8M,MAAM,UAAU;EAC9N,kFAAkF,MAAM,OAAO;EAC/F,uGAAuG,MAAM,KAAK;EAClH,8DAA8D,MAAM,KAAK;EACzE;EACA,8EAA8E,QAAQ;EACtF,kGAAkG,MAAM,OAAO;EAC/G,+CAA+C,MAAM,OAAO;EAC5D,iCAAiC,oBAAoB;EACrD;CACF;CACA,IAAI,UACF,MAAM,KACJ,oDAAoD,uBAAuB,UAAU,cAAc,KAAK,IAAI,EAAE,oCAC9G,0LACF;CAEF,MAAM,KAAK,8EAA4E;CACvF,OAAO,MAAM,KAAK,IAAI;AACxB"}
@@ -0,0 +1,9 @@
1
+ import { ComponentPropsWithoutRef } from "react";
2
+ type CalloutProps = ComponentPropsWithoutRef<'blockquote'> & {
3
+ node?: unknown;
4
+ 'data-callout'?: string;
5
+ 'data-callout-title'?: string;
6
+ };
7
+ /** A quote, or a typed callout (`> [!WARNING]`): what `render/views.ts` says one is made of. */
8
+ export declare function Callout({ children, ...props }: CalloutProps): string | number | bigint | boolean | Iterable<import("react").ReactNode> | Promise<string | number | bigint | boolean | Iterable<import("react").ReactNode> | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | import("react").ReactPortal | null | undefined> | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | import("react").ReactPortal | null | undefined;
9
+ export {};
@@ -0,0 +1,12 @@
1
+ "use client";
2
+ import { callout } from "../render/views.js";
3
+ import { useLabels } from "./labels.js";
4
+ import { react } from "./render.js";
5
+ // src/react/callout.tsx
6
+ /** A quote, or a typed callout (`> [!WARNING]`): what `render/views.ts` says one is made of. */
7
+ function Callout({ children, ...props }) {
8
+ return callout(react, props, children === void 0 ? [] : [children], useLabels());
9
+ }
10
+ export { Callout };
11
+
12
+ //# sourceMappingURL=callout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callout.js","names":[],"sources":["../../src/react/callout.tsx"],"sourcesContent":["'use client'\nimport type { ComponentPropsWithoutRef } from 'react'\nimport type { Props } from '../render/factory'\nimport { callout } from '../render/views'\nimport { useLabels } from './labels'\nimport { react } from './render'\n\ntype CalloutProps = ComponentPropsWithoutRef<'blockquote'> & {\n node?: unknown\n 'data-callout'?: string\n 'data-callout-title'?: string\n}\n\n/** A quote, or a typed callout (`> [!WARNING]`): what `render/views.ts` says one is made of. */\nexport function Callout({ children, ...props }: CalloutProps) {\n return callout(react, props as Props, children === undefined ? [] : [children], useLabels())\n}\n"],"mappings":";;;;;;AAcA,SAAgB,QAAQ,EAAE,UAAU,GAAG,SAAuB;CAC5D,OAAO,QAAQ,OAAO,OAAgB,aAAa,KAAA,IAAY,CAAC,IAAI,CAAC,QAAQ,GAAG,UAAU,CAAC;AAC7F"}
@@ -0,0 +1,18 @@
1
+ import { ComponentPropsWithoutRef, ReactNode } from "react";
2
+ type CodeProps = ComponentPropsWithoutRef<'code'> & {
3
+ node?: unknown;
4
+ };
5
+ type PreProps = ComponentPropsWithoutRef<'pre'> & {
6
+ node?: unknown;
7
+ };
8
+ /**
9
+ * Code, inline or in a fence. A fenced block is highlighted when the page gave `<Markdown>` a
10
+ * highlighter and the fence names a language it knows; otherwise its text is printed as is.
11
+ */
12
+ export declare function Code({ children, ...props }: CodeProps): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | Iterable<ReactNode> | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | import("react").ReactPortal | null | undefined> | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | import("react").ReactPortal | null | undefined;
13
+ /**
14
+ * A fenced code block: its language, a download and a copy button above the code. Replace it with
15
+ * your own through `components.pre`; the `Code` inside still highlights.
16
+ */
17
+ export declare function CodeBlock({ node: _node, children, ...props }: PreProps): import("react").JSX.Element;
18
+ export {};
@@ -0,0 +1,94 @@
1
+ "use client";
2
+ import { codeFileName, copyText, downloadFile } from "../browser/file-actions.js";
3
+ import { code, codeBar, codeFrame, copyButton, downloadButton, readCodeBlock } from "../render/views.js";
4
+ import { useRenderContext } from "./context.js";
5
+ import { useLabels } from "./labels.js";
6
+ import { react } from "./render.js";
7
+ import { useScrolls } from "./scrolls.js";
8
+ import { memo, useEffect, useRef, useState } from "react";
9
+ import { jsx } from "react/jsx-runtime";
10
+ // src/react/code-block.tsx
11
+ /** How long "Copied" stays before the button reads "Copy" again. */
12
+ const COPIED_MS = 1600;
13
+ /**
14
+ * Code, inline or in a fence. A fenced block is highlighted when the page gave `<Markdown>` a
15
+ * highlighter and the fence names a language it knows; otherwise its text is printed as is.
16
+ */
17
+ function Code({ children, ...props }) {
18
+ const { highlight } = useRenderContext();
19
+ return code(react, props, children === void 0 ? [] : [children], highlight);
20
+ }
21
+ function CopyButton({ code: text }) {
22
+ const labels = useLabels();
23
+ const [copies, setCopies] = useState(0);
24
+ useEffect(() => {
25
+ if (copies === 0) return;
26
+ const timer = setTimeout(() => setCopies(0), COPIED_MS);
27
+ return () => clearTimeout(timer);
28
+ }, [copies]);
29
+ const copy = () => {
30
+ copyText(text.current).then((done) => {
31
+ if (done) setCopies((count) => count + 1);
32
+ });
33
+ };
34
+ return copyButton(react, labels, copies > 0, copy);
35
+ }
36
+ /** Saves the code as a file: named as its fence's title, or `code.<ext>` from its language. */
37
+ function DownloadButton({ code: text, name }) {
38
+ return downloadButton(react, useLabels(), () => downloadFile(text.current, name));
39
+ }
40
+ /**
41
+ * A fenced code block: its language, a download and a copy button above the code. Replace it with
42
+ * your own through `components.pre`; the `Code` inside still highlights.
43
+ */
44
+ function CodeBlock({ node: _node, children, ...props }) {
45
+ const { language, title, text } = readCodeBlock(react, children === void 0 ? [] : [children]);
46
+ return /* @__PURE__ */ jsx(CodeFrame, {
47
+ language,
48
+ title,
49
+ text,
50
+ preProps: props,
51
+ children
52
+ });
53
+ }
54
+ /**
55
+ * The bar of a code block. It reads the code only when a button is pressed, so while a fence is
56
+ * being written the bar is drawn once, not on every token.
57
+ */
58
+ const CodeBar = memo(function CodeBar({ language, title, code: text }) {
59
+ return codeBar(react, language, title, [/* @__PURE__ */ jsx(DownloadButton, {
60
+ code: text,
61
+ name: codeFileName(title, language)
62
+ }, "download"), /* @__PURE__ */ jsx(CopyButton, { code: text }, "copy")]);
63
+ });
64
+ /**
65
+ * The frame of a code block: the bar with its language and its download and copy buttons, then the
66
+ * `<pre>`. A fence that names its file (` ```ts title="src/app.ts" `) shows the file first, the
67
+ * language after it; a fence with no language shows none.
68
+ */
69
+ function CodeFrame({ language, title, text, preProps, children }) {
70
+ const labels = useLabels();
71
+ const [attach, scrolls, again] = useScrolls();
72
+ const current = useRef(text);
73
+ useEffect(() => {
74
+ if (current.current === text) return;
75
+ current.current = text;
76
+ again();
77
+ }, [text, again]);
78
+ return codeFrame(react, language, /* @__PURE__ */ jsx(CodeBar, {
79
+ language,
80
+ title,
81
+ code: current
82
+ }), {
83
+ ...preProps,
84
+ ref: attach,
85
+ ...scrolls ? {
86
+ role: "group",
87
+ "aria-label": labels.scrollableCode,
88
+ tabIndex: 0
89
+ } : null
90
+ }, [children]);
91
+ }
92
+ export { Code, CodeBlock, CodeFrame };
93
+
94
+ //# sourceMappingURL=code-block.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"code-block.js","names":[],"sources":["../../src/react/code-block.tsx"],"sourcesContent":["'use client'\nimport {\n memo,\n useEffect,\n useRef,\n useState,\n type ComponentPropsWithoutRef,\n type ReactNode,\n} from 'react'\nimport { codeFileName, copyText, downloadFile } from '../browser/file-actions'\nimport type { Props } from '../render/factory'\nimport {\n code,\n codeBar,\n codeFrame,\n copyButton,\n downloadButton,\n readCodeBlock,\n} from '../render/views'\nimport { useRenderContext } from './context'\nimport { useLabels } from './labels'\nimport { react } from './render'\nimport { useScrolls } from './scrolls'\n\ntype CodeProps = ComponentPropsWithoutRef<'code'> & { node?: unknown }\ntype PreProps = ComponentPropsWithoutRef<'pre'> & { node?: unknown }\n\n/** How long \"Copied\" stays before the button reads \"Copy\" again. */\nconst COPIED_MS = 1_600\n\n/**\n * Code, inline or in a fence. A fenced block is highlighted when the page gave `<Markdown>` a\n * highlighter and the fence names a language it knows; otherwise its text is printed as is.\n */\nexport function Code({ children, ...props }: CodeProps) {\n const { highlight } = useRenderContext()\n return code(react, props as Props, children === undefined ? [] : [children], highlight)\n}\n\n/** The code of a block as last drawn: read when a button is pressed, not when it is drawn. */\ntype CodeText = { readonly current: string }\n\nfunction CopyButton({ code: text }: { code: CodeText }) {\n const labels = useLabels()\n // How many times the code was copied since the button last read \"Copy\": each copy starts the\n // wait over, and the effect's cleanup ends it when the block leaves the page.\n const [copies, setCopies] = useState(0)\n useEffect(() => {\n if (copies === 0) return\n const timer = setTimeout(() => setCopies(0), COPIED_MS)\n return () => clearTimeout(timer)\n }, [copies])\n const copy = () => {\n // The Clipboard API, or the old copy command where the page has none (an insecure origin).\n // When both refuse there is nothing to say: the code is still selectable.\n void copyText(text.current).then((done) => {\n if (done) setCopies((count) => count + 1)\n })\n }\n return copyButton(react, labels, copies > 0, copy)\n}\n\n/** Saves the code as a file: named as its fence's title, or `code.<ext>` from its language. */\nfunction DownloadButton({ code: text, name }: { code: CodeText; name: string }) {\n return downloadButton(react, useLabels(), () => downloadFile(text.current, name))\n}\n\n/**\n * A fenced code block: its language, a download and a copy button above the code. Replace it with\n * your own through `components.pre`; the `Code` inside still highlights.\n */\nexport function CodeBlock({ node: _node, children, ...props }: PreProps) {\n const { language, title, text } = readCodeBlock(react, children === undefined ? [] : [children])\n return (\n <CodeFrame language={language} title={title} text={text} preProps={props}>\n {children}\n </CodeFrame>\n )\n}\n\n/**\n * The bar of a code block. It reads the code only when a button is pressed, so while a fence is\n * being written the bar is drawn once, not on every token.\n */\nconst CodeBar = memo(function CodeBar({\n language,\n title,\n code: text,\n}: {\n language: string | undefined\n title: string | undefined\n code: CodeText\n}) {\n return codeBar(react, language, title, [\n <DownloadButton key=\"download\" code={text} name={codeFileName(title, language)} />,\n <CopyButton key=\"copy\" code={text} />,\n ])\n})\n\n/**\n * The frame of a code block: the bar with its language and its download and copy buttons, then the\n * `<pre>`. A fence that names its file (` ```ts title=\"src/app.ts\" `) shows the file first, the\n * language after it; a fence with no language shows none.\n */\nexport function CodeFrame({\n language,\n title,\n text,\n preProps,\n children,\n}: {\n language: string | undefined\n title?: string | undefined\n text: string\n preProps?: ComponentPropsWithoutRef<'pre'>\n children: ReactNode\n}) {\n const labels = useLabels()\n // A line too long for the reply scrolls inside the `<pre>`: the keyboard must then reach it.\n const [attach, scrolls, again] = useScrolls<HTMLPreElement>()\n const current = useRef(text)\n useEffect(() => {\n // Not on the first drawing: the box was measured as it was attached.\n if (current.current === text) return\n current.current = text\n again()\n }, [text, again])\n return codeFrame(\n react,\n language,\n <CodeBar language={language} title={title} code={current} />,\n {\n ...(preProps as Props | undefined),\n ref: attach,\n ...(scrolls ? { role: 'group', 'aria-label': labels.scrollableCode, tabIndex: 0 } : null),\n },\n [children],\n )\n}\n"],"mappings":";;;;;;;;;;;AA4BA,MAAM,YAAY;;;;;AAMlB,SAAgB,KAAK,EAAE,UAAU,GAAG,SAAoB;CACtD,MAAM,EAAE,cAAc,iBAAiB;CACvC,OAAO,KAAK,OAAO,OAAgB,aAAa,KAAA,IAAY,CAAC,IAAI,CAAC,QAAQ,GAAG,SAAS;AACxF;AAKA,SAAS,WAAW,EAAE,MAAM,QAA4B;CACtD,MAAM,SAAS,UAAU;CAGzB,MAAM,CAAC,QAAQ,aAAa,SAAS,CAAC;CACtC,gBAAgB;EACd,IAAI,WAAW,GAAG;EAClB,MAAM,QAAQ,iBAAiB,UAAU,CAAC,GAAG,SAAS;EACtD,aAAa,aAAa,KAAK;CACjC,GAAG,CAAC,MAAM,CAAC;CACX,MAAM,aAAa;EAGjB,SAAc,KAAK,OAAO,CAAC,CAAC,MAAM,SAAS;GACzC,IAAI,MAAM,WAAW,UAAU,QAAQ,CAAC;EAC1C,CAAC;CACH;CACA,OAAO,WAAW,OAAO,QAAQ,SAAS,GAAG,IAAI;AACnD;;AAGA,SAAS,eAAe,EAAE,MAAM,MAAM,QAA0C;CAC9E,OAAO,eAAe,OAAO,UAAU,SAAS,aAAa,KAAK,SAAS,IAAI,CAAC;AAClF;;;;;AAMA,SAAgB,UAAU,EAAE,MAAM,OAAO,UAAU,GAAG,SAAmB;CACvE,MAAM,EAAE,UAAU,OAAO,SAAS,cAAc,OAAO,aAAa,KAAA,IAAY,CAAC,IAAI,CAAC,QAAQ,CAAC;CAC/F,OACE,oBAAC,WAAD;EAAqB;EAAiB;EAAa;EAAM,UAAU;EAChE;CACQ,CAAA;AAEf;;;;;AAMA,MAAM,UAAU,KAAK,SAAS,QAAQ,EACpC,UACA,OACA,MAAM,QAKL;CACD,OAAO,QAAQ,OAAO,UAAU,OAAO,CACrC,oBAAC,gBAAD;EAA+B,MAAM;EAAM,MAAM,aAAa,OAAO,QAAQ;CAAI,GAA7D,UAA6D,GACjF,oBAAC,YAAD,EAAuB,MAAM,KAAO,GAApB,MAAoB,CACtC,CAAC;AACH,CAAC;;;;;;AAOD,SAAgB,UAAU,EACxB,UACA,OACA,MACA,UACA,YAOC;CACD,MAAM,SAAS,UAAU;CAEzB,MAAM,CAAC,QAAQ,SAAS,SAAS,WAA2B;CAC5D,MAAM,UAAU,OAAO,IAAI;CAC3B,gBAAgB;EAEd,IAAI,QAAQ,YAAY,MAAM;EAC9B,QAAQ,UAAU;EAClB,MAAM;CACR,GAAG,CAAC,MAAM,KAAK,CAAC;CAChB,OAAO,UACL,OACA,UACA,oBAAC,SAAD;EAAmB;EAAiB;EAAO,MAAM;CAAU,CAAA,GAC3D;EACE,GAAI;EACJ,KAAK;EACL,GAAI,UAAU;GAAE,MAAM;GAAS,cAAc,OAAO;GAAgB,UAAU;EAAE,IAAI;CACtF,GACA,CAAC,QAAQ,CACX;AACF"}
@@ -0,0 +1,18 @@
1
+ import { Highlighter } from "../render/views.js";
2
+ import { Components } from "./render.js";
3
+ import { ComponentType } from "react";
4
+ /**
5
+ * What `<Markdown components>` takes: an element for any tag, and a page's own drawing of the two
6
+ * fences that are not code. `diagram` is told whether its fence is the one still being written
7
+ * (`growing`), so it can wait for the whole source; every diagram above it is complete.
8
+ */
9
+ export type MarkdownComponents = Components & {
10
+ diagram?: ComponentType<{
11
+ source: string;
12
+ growing: boolean;
13
+ }>;
14
+ tree?: ComponentType<{
15
+ source: string;
16
+ }>;
17
+ };
18
+ export type { Highlighter };
@@ -0,0 +1,17 @@
1
+ "use client";
2
+ import { createContext, useContext } from "react";
3
+ // src/react/context.tsx
4
+ const RenderContextValue = createContext({
5
+ highlight: null,
6
+ mermaid: null
7
+ });
8
+ const useRenderContext = () => useContext(RenderContextValue);
9
+ /**
10
+ * Whether the block being drawn is the end of a streaming reply that stops inside a fence. Such a
11
+ * fence is still being written: a diagram in it is not drawn yet (every token would be a layout
12
+ * and most of them a syntax error), only shown as the source it is so far.
13
+ */
14
+ const GrowingFence = createContext(false);
15
+ export { GrowingFence, RenderContextValue, useRenderContext };
16
+
17
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","names":[],"sources":["../../src/react/context.tsx"],"sourcesContent":["'use client'\nimport { createContext, useContext, type ComponentType } from 'react'\nimport type { Components } from './render'\nimport type { MermaidRenderer } from '../mermaid/renderer'\nimport type { Highlighter } from '../render/views'\n\nexport type { Highlighter } from '../render/views'\n\n/**\n * What `<Markdown components>` takes: an element for any tag, and a page's own drawing of the two\n * fences that are not code. `diagram` is told whether its fence is the one still being written\n * (`growing`), so it can wait for the whole source; every diagram above it is complete.\n */\nexport type MarkdownComponents = Components & {\n diagram?: ComponentType<{ source: string; growing: boolean }>\n tree?: ComponentType<{ source: string }>\n}\n\n/** What every block of one document shares, beside the labels. */\nexport interface RenderContext {\n highlight: Highlighter | null\n /**\n * What draws a diagram, or null to show it as its source. `'page'` is the page's one renderer,\n * for a page that gave a Mermaid loader: it lives in the chunk a diagram loads, so it is named\n * here, not held.\n */\n mermaid: MermaidRenderer | 'page' | null\n}\n\nexport const RenderContextValue = createContext<RenderContext>({ highlight: null, mermaid: null })\n\nexport const useRenderContext = () => useContext(RenderContextValue)\n\n/**\n * Whether the block being drawn is the end of a streaming reply that stops inside a fence. Such a\n * fence is still being written: a diagram in it is not drawn yet (every token would be a layout\n * and most of them a syntax error), only shown as the source it is so far.\n */\nexport const GrowingFence = createContext(false)\n"],"mappings":";;;AA6BA,MAAa,qBAAqB,cAA6B;CAAE,WAAW;CAAM,SAAS;AAAK,CAAC;AAEjG,MAAa,yBAAyB,WAAW,kBAAkB;;;;;;AAOnE,MAAa,eAAe,cAAc,KAAK"}
@@ -0,0 +1,2 @@
1
+ import "./render.js";
2
+ import "react";