@dxos/react-ui-markdown 0.8.4-main.74a063c4e0 → 0.8.4-main.765dc60934

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 (50) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/browser/index.mjs +617 -24
  3. package/dist/lib/browser/index.mjs.map +4 -4
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/types/src/MarkdownStream/MarkdownStream.d.ts +101 -0
  6. package/dist/types/src/MarkdownStream/MarkdownStream.d.ts.map +1 -0
  7. package/dist/types/src/MarkdownStream/MarkdownStream.stories.d.ts +23 -0
  8. package/dist/types/src/MarkdownStream/MarkdownStream.stories.d.ts.map +1 -0
  9. package/dist/types/src/MarkdownStream/footer.d.ts +23 -0
  10. package/dist/types/src/MarkdownStream/footer.d.ts.map +1 -0
  11. package/dist/types/src/MarkdownStream/index.d.ts +4 -0
  12. package/dist/types/src/MarkdownStream/index.d.ts.map +1 -0
  13. package/dist/types/src/MarkdownStream/stream.d.ts +39 -0
  14. package/dist/types/src/MarkdownStream/stream.d.ts.map +1 -0
  15. package/dist/types/src/MarkdownStream/stream.test.d.ts +2 -0
  16. package/dist/types/src/MarkdownStream/stream.test.d.ts.map +1 -0
  17. package/dist/types/src/MarkdownStream/testing/index.d.ts +2 -0
  18. package/dist/types/src/MarkdownStream/testing/index.d.ts.map +1 -0
  19. package/dist/types/src/MarkdownStream/testing/testing.d.ts +16 -0
  20. package/dist/types/src/MarkdownStream/testing/testing.d.ts.map +1 -0
  21. package/dist/types/src/{MarkdownViewer/MarkdownViewer.d.ts → MarkdownView/MarkdownView.d.ts} +3 -3
  22. package/dist/types/src/MarkdownView/MarkdownView.d.ts.map +1 -0
  23. package/dist/types/src/{MarkdownViewer/MarkdownViewer.stories.d.ts → MarkdownView/MarkdownView.stories.d.ts} +4 -4
  24. package/dist/types/src/MarkdownView/MarkdownView.stories.d.ts.map +1 -0
  25. package/dist/types/src/MarkdownView/Media.d.ts +35 -0
  26. package/dist/types/src/MarkdownView/Media.d.ts.map +1 -0
  27. package/dist/types/src/MarkdownView/index.d.ts +3 -0
  28. package/dist/types/src/MarkdownView/index.d.ts.map +1 -0
  29. package/dist/types/src/index.d.ts +2 -1
  30. package/dist/types/src/index.d.ts.map +1 -1
  31. package/dist/types/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +28 -24
  33. package/src/MarkdownStream/MarkdownStream.stories.tsx +215 -0
  34. package/src/MarkdownStream/MarkdownStream.tsx +442 -0
  35. package/src/MarkdownStream/footer.ts +119 -0
  36. package/src/MarkdownStream/index.ts +8 -0
  37. package/src/MarkdownStream/stream.test.ts +126 -0
  38. package/src/MarkdownStream/stream.ts +229 -0
  39. package/src/{MarkdownViewer → MarkdownStream/testing}/index.ts +1 -1
  40. package/src/MarkdownStream/testing/testing.ts +56 -0
  41. package/src/MarkdownStream/testing/text.md +67 -0
  42. package/src/{MarkdownViewer/MarkdownViewer.stories.tsx → MarkdownView/MarkdownView.stories.tsx} +6 -6
  43. package/src/{MarkdownViewer/MarkdownViewer.tsx → MarkdownView/MarkdownView.tsx} +32 -9
  44. package/src/MarkdownView/Media.tsx +80 -0
  45. package/src/MarkdownView/index.ts +6 -0
  46. package/src/index.ts +2 -1
  47. package/dist/types/src/MarkdownViewer/MarkdownViewer.d.ts.map +0 -1
  48. package/dist/types/src/MarkdownViewer/MarkdownViewer.stories.d.ts.map +0 -1
  49. package/dist/types/src/MarkdownViewer/index.d.ts +0 -2
  50. package/dist/types/src/MarkdownViewer/index.d.ts.map +0 -1
@@ -1,13 +1,53 @@
1
- // src/MarkdownViewer/MarkdownViewer.tsx
2
- import React from "react";
1
+ // src/MarkdownView/MarkdownView.tsx
2
+ import React2 from "react";
3
3
  import ReactMarkdown from "react-markdown";
4
4
  import remarkGfm from "remark-gfm";
5
5
  import { SyntaxHighlighter } from "@dxos/react-ui-syntax-highlighter";
6
+ import { mx as mx2 } from "@dxos/ui-theme";
7
+
8
+ // src/MarkdownView/Media.tsx
9
+ import React from "react";
10
+ import { MediaPlayer, detectMediaKind } from "@dxos/react-ui";
6
11
  import { mx } from "@dxos/ui-theme";
7
- var MarkdownViewer = ({ classNames, children, components, content = "" }) => {
8
- return /* @__PURE__ */ React.createElement("div", {
9
- className: mx(classNames)
10
- }, /* @__PURE__ */ React.createElement(ReactMarkdown, {
12
+ var isEmbedUrl = (src) => detectMediaKind(src) !== void 0;
13
+ var isLegacyIframeUrl = (src) => src.includes("iframe");
14
+ var DEFAULT_IFRAME_SANDBOX = "allow-scripts allow-same-origin allow-presentation";
15
+ var MarkdownMedia = ({ src, alt, classNames, imgClassNames, mediaClassNames }) => {
16
+ if (isEmbedUrl(src)) {
17
+ return /* @__PURE__ */ React.createElement(MediaPlayer, {
18
+ src,
19
+ alt,
20
+ classNames: mx(classNames, mediaClassNames)
21
+ });
22
+ }
23
+ if (isLegacyIframeUrl(src)) {
24
+ return /* @__PURE__ */ React.createElement("iframe", {
25
+ src,
26
+ title: alt ?? "Embedded media",
27
+ loading: "lazy",
28
+ className: mx("border-none", classNames, mediaClassNames),
29
+ sandbox: DEFAULT_IFRAME_SANDBOX,
30
+ referrerPolicy: "no-referrer",
31
+ allow: "accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;",
32
+ allowFullScreen: true
33
+ });
34
+ }
35
+ return /* @__PURE__ */ React.createElement("img", {
36
+ src,
37
+ alt: alt ?? "",
38
+ loading: "lazy",
39
+ className: mx(classNames, imgClassNames),
40
+ onError: (event) => {
41
+ event.currentTarget.style.display = "none";
42
+ }
43
+ });
44
+ };
45
+
46
+ // src/MarkdownView/MarkdownView.tsx
47
+ var MarkdownView = ({ classNames, children, components, content = "" }) => {
48
+ return /* @__PURE__ */ React2.createElement("div", {
49
+ className: mx2(classNames)
50
+ }, /* @__PURE__ */ React2.createElement(ReactMarkdown, {
11
51
  remarkPlugins: [
12
52
  remarkGfm
13
53
  ],
@@ -20,60 +60,613 @@ var MarkdownViewer = ({ classNames, children, components, content = "" }) => {
20
60
  };
21
61
  var defaultComponents = {
22
62
  h1: ({ children }) => {
23
- return /* @__PURE__ */ React.createElement("h1", {
24
- className: "pt-1 pb-1 text-xl"
63
+ return /* @__PURE__ */ React2.createElement("h1", {
64
+ className: "pt-1 pb-1 text-accent-text text-xl"
25
65
  }, children);
26
66
  },
27
67
  h2: ({ children }) => {
28
- return /* @__PURE__ */ React.createElement("h2", {
29
- className: "pt-1 pb-1 text-lg"
68
+ return /* @__PURE__ */ React2.createElement("h2", {
69
+ className: "pt-1 pb-1 text-accent-text text-lg"
30
70
  }, children);
31
71
  },
32
72
  h3: ({ children }) => {
33
- return /* @__PURE__ */ React.createElement("h3", {
34
- className: "pt-1 pb-1 text-base"
73
+ return /* @__PURE__ */ React2.createElement("h3", {
74
+ className: "pt-1 pb-1 text-accent-text text-base"
35
75
  }, children);
36
76
  },
37
- blockquote: ({ children, ...props }) => /* @__PURE__ */ React.createElement("blockquote", {
38
- className: "ps-4 mt-2 mb-2 pt-2 pb-2 border-l-4 border-accent-text text-accent-text",
77
+ h4: ({ children }) => {
78
+ return /* @__PURE__ */ React2.createElement("h4", {
79
+ className: "pt-1 pb-1 uppercase text-base"
80
+ }, children);
81
+ },
82
+ blockquote: ({ children, ...props }) => /* @__PURE__ */ React2.createElement("blockquote", {
83
+ className: "my-2 py-2 ps-4 border-l-4 border-accent-text text-accent-text",
39
84
  ...props
40
85
  }, children),
41
86
  p: ({ children }) => {
42
- return /* @__PURE__ */ React.createElement("div", {
87
+ return /* @__PURE__ */ React2.createElement("div", {
43
88
  className: "pt-1 pb-1"
44
89
  }, children);
45
90
  },
46
- a: ({ children, href, ...props }) => /* @__PURE__ */ React.createElement("a", {
91
+ a: ({ children, href, ...props }) => /* @__PURE__ */ React2.createElement("a", {
47
92
  href,
48
93
  className: "text-primary-500 hover:text-primary-500",
49
94
  target: "_blank",
50
95
  rel: "noopener noreferrer",
51
96
  ...props
52
97
  }, children),
53
- ol: ({ children, ...props }) => /* @__PURE__ */ React.createElement("ol", {
98
+ // Hide broken images: many markdown sources reference remote URLs that
99
+ // 404 or are blocked. Drop the element on load failure rather than
100
+ // leaving the browser's broken-image placeholder.
101
+ //
102
+ // Media URLs (mp4/mp3/etc. or legacy `iframe`-style embeds) are swapped to a
103
+ // native `<video>` / `<audio>` `MediaPlayer` by {@link MarkdownMedia} so
104
+ // playable media in the source document renders inline.
105
+ img: ({ src, alt }) => {
106
+ if (!src) {
107
+ return null;
108
+ }
109
+ return /* @__PURE__ */ React2.createElement(MarkdownMedia, {
110
+ src,
111
+ alt,
112
+ mediaClassNames: "aspect-video w-full"
113
+ });
114
+ },
115
+ ol: ({ children, ...props }) => /* @__PURE__ */ React2.createElement("ol", {
54
116
  className: "pt-1 pb-1 ps-6 leading-tight list-decimal",
55
117
  ...props
56
118
  }, children),
57
- ul: ({ children, ...props }) => /* @__PURE__ */ React.createElement("ul", {
119
+ ul: ({ children, ...props }) => /* @__PURE__ */ React2.createElement("ul", {
58
120
  className: "pt-1 pb-1 ps-6 leading-tight list-disc",
59
121
  ...props
60
122
  }, children),
61
- li: ({ children, ...props }) => /* @__PURE__ */ React.createElement("li", {
123
+ li: ({ children, ...props }) => /* @__PURE__ */ React2.createElement("li", {
62
124
  className: "",
63
125
  ...props
64
126
  }, children),
65
127
  pre: ({ children }) => children,
66
- // TODO(burdon): Copy/paste button.
67
- code: ({ children, className }) => {
128
+ code: ({ children, className, node }) => {
68
129
  const [, language] = /language-(\w+)/.exec(className || "") || [];
69
- return /* @__PURE__ */ React.createElement(SyntaxHighlighter, {
130
+ const inline = !className && node?.position?.start.line === node?.position?.end.line;
131
+ if (inline) {
132
+ return /* @__PURE__ */ React2.createElement("code", {
133
+ className: "rounded-xs bg-group-surface px-1 py-0.5 text-sm text-info-text"
134
+ }, children);
135
+ }
136
+ return /* @__PURE__ */ React2.createElement(SyntaxHighlighter, {
70
137
  language,
71
- classNames: "mt-2 mb-2 border border-separator rounded-xs text-sm bg-group-surface",
138
+ classNames: "mt-2 mb-2 p-2 border border-separator rounded-xs text-sm bg-group-surface",
139
+ copyButton: true,
72
140
  PreTag: "pre"
73
141
  }, children);
74
142
  }
75
143
  };
144
+
145
+ // src/MarkdownStream/stream.ts
146
+ import * as Effect from "effect/Effect";
147
+ import * as Stream from "effect/Stream";
148
+ import { Obj } from "@dxos/echo";
149
+ var renderObjectLink = (obj, block) => `${block ? "!" : ""}[${Obj.getLabel(obj)}](${Obj.getDXN(obj).toString()})`;
150
+ var createStreamer = (source, { chunkSize = "span", delayMs = 0 } = {}) => {
151
+ const subdivide = chunkSize === "span" ? (token) => [
152
+ token
153
+ ] : (token) => isXmlFragment(token) ? [
154
+ token
155
+ ] : splitTextSpan(token, chunkSize);
156
+ let stream = source.pipe(Stream.flatMap((chunk) => Stream.fromIterable(splitFragments(chunk).flatMap(subdivide))));
157
+ if (delayMs > 0) {
158
+ stream = stream.pipe(Stream.tap(() => Effect.sleep(`${delayMs} millis`)));
159
+ }
160
+ return stream;
161
+ };
162
+ var isXmlFragment = (token) => token.startsWith("<");
163
+ var splitTextSpan = (span, chunkSize) => {
164
+ if (chunkSize === "character") {
165
+ return [
166
+ ...span
167
+ ];
168
+ }
169
+ return span.match(/\s+|\S+/g) ?? [
170
+ span
171
+ ];
172
+ };
173
+ var OPENING_TAG_NAME = /^<([a-zA-Z][\w-]*)(?:\s[^>]*)?>/;
174
+ var splitFragments = (text) => {
175
+ const initialTokens = splitSpans(text);
176
+ const tokens = [];
177
+ let i = 0;
178
+ while (i < initialTokens.length) {
179
+ const token = initialTokens[i];
180
+ if (token.startsWith("<") && !token.startsWith("</") && !token.endsWith("/>")) {
181
+ const tagMatch = token.match(OPENING_TAG_NAME);
182
+ if (tagMatch) {
183
+ const tagName = tagMatch[1];
184
+ const closingTag = `</${tagName}>`;
185
+ let fragment = token;
186
+ let foundClosing = false;
187
+ let j = i + 1;
188
+ while (j < initialTokens.length) {
189
+ fragment += initialTokens[j];
190
+ if (initialTokens[j] === closingTag) {
191
+ foundClosing = true;
192
+ break;
193
+ }
194
+ j++;
195
+ }
196
+ if (foundClosing) {
197
+ tokens.push(fragment);
198
+ i = j + 1;
199
+ } else {
200
+ tokens.push(token);
201
+ i++;
202
+ }
203
+ } else {
204
+ tokens.push(token);
205
+ i++;
206
+ }
207
+ } else {
208
+ tokens.push(token);
209
+ i++;
210
+ }
211
+ }
212
+ return tokens;
213
+ };
214
+ var splitSpans = (text) => {
215
+ const spans = [];
216
+ let currentText = "";
217
+ let i = 0;
218
+ while (i < text.length) {
219
+ if (text[i] === "<") {
220
+ if (currentText) {
221
+ spans.push(currentText);
222
+ currentText = "";
223
+ }
224
+ const closeIndex = text.indexOf(">", i);
225
+ if (closeIndex !== -1) {
226
+ spans.push(text.slice(i, closeIndex + 1));
227
+ i = closeIndex + 1;
228
+ } else {
229
+ currentText = text.slice(i);
230
+ break;
231
+ }
232
+ } else {
233
+ currentText += text[i];
234
+ i++;
235
+ }
236
+ }
237
+ if (currentText) {
238
+ spans.push(currentText);
239
+ }
240
+ return spans;
241
+ };
242
+ var splitSentences = (text) => {
243
+ const sentenceRegex = /[^.!?]*[.!?]+(?:\s+|$)/g;
244
+ const sentences = [];
245
+ let lastIndex = 0;
246
+ let match;
247
+ while ((match = sentenceRegex.exec(text)) !== null) {
248
+ sentences.push(match[0]);
249
+ lastIndex = match.index + match[0].length;
250
+ }
251
+ if (lastIndex < text.length) {
252
+ const remaining = text.slice(lastIndex);
253
+ if (remaining) {
254
+ sentences.push(remaining);
255
+ }
256
+ }
257
+ if (sentences.length === 0 && text) {
258
+ sentences.push(text);
259
+ }
260
+ return sentences;
261
+ };
262
+
263
+ // src/MarkdownStream/testing/testing.ts
264
+ async function* textStream(text, options = {}) {
265
+ const { chunkDelay = 100, variance = 0.3, wordsPerChunk = 3 } = options;
266
+ const words = text.match(/\S+|\s+/g) || [];
267
+ let i = 0;
268
+ while (i < words.length) {
269
+ const chunkWords = [];
270
+ let wordCount = 0;
271
+ while (i < words.length && wordCount < wordsPerChunk) {
272
+ const word = words[i];
273
+ chunkWords.push(word);
274
+ if (word.trim()) {
275
+ wordCount++;
276
+ }
277
+ i++;
278
+ }
279
+ const chunk = chunkWords.join("");
280
+ yield chunk;
281
+ const varianceMultiplier = 1 + (Math.random() - 0.5) * variance * 2;
282
+ const delay = chunkDelay * varianceMultiplier;
283
+ await new Promise((resolve) => setTimeout(resolve, delay));
284
+ }
285
+ }
286
+
287
+ // src/MarkdownStream/MarkdownStream.tsx
288
+ import { EditorSelection, Transaction } from "@codemirror/state";
289
+ import * as Effect2 from "effect/Effect";
290
+ import * as Fiber from "effect/Fiber";
291
+ import * as Queue from "effect/Queue";
292
+ import * as Stream2 from "effect/Stream";
293
+ import React3, { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react";
294
+ import { createPortal } from "react-dom";
295
+ import { addEventListener } from "@dxos/async";
296
+ import { runAndForwardErrors } from "@dxos/effect";
297
+ import { ErrorBoundary, useDynamicRef, useStateWithRef, useThemeContext } from "@dxos/react-ui";
298
+ import { useTextEditor } from "@dxos/react-ui-editor";
299
+ import { createBasicExtensions, createThemeExtensions, decorateMarkdown, extendedMarkdown, navigateNextEffect, navigatePreviousEffect, preview, scroller, crawlerLineEffect, fader, typewriter, typewriterBypass, xmlTagContextEffect, xmlTagResetEffect, xmlTagUpdateEffect, xmlTags, documentSlots, xmlFormatting, xmlBlockDecoration } from "@dxos/ui-editor";
300
+ import { mx as mx3 } from "@dxos/ui-theme";
301
+ import { isTruthy } from "@dxos/util";
302
+
303
+ // src/MarkdownStream/footer.ts
304
+ import { StateEffect, StateField } from "@codemirror/state";
305
+ import { Decoration, EditorView, WidgetType } from "@codemirror/view";
306
+ import { Domino } from "@dxos/ui";
307
+ import { typewriterDrainingEffect } from "@dxos/ui-editor";
308
+ var setFooterVisibleEffect = StateEffect.define();
309
+ var footer = (setRoot) => {
310
+ const widget = new FooterWidget(setRoot);
311
+ const buildSet = (length) => Decoration.set([
312
+ Decoration.widget({
313
+ widget,
314
+ block: true,
315
+ side: 1
316
+ }).range(length)
317
+ ]);
318
+ const field = StateField.define({
319
+ create: () => ({
320
+ wanted: false,
321
+ draining: false,
322
+ decorations: Decoration.none
323
+ }),
324
+ update: (state, tr) => {
325
+ let { wanted, draining, decorations } = state;
326
+ for (const effect of tr.effects) {
327
+ if (effect.is(setFooterVisibleEffect)) {
328
+ wanted = effect.value;
329
+ }
330
+ if (effect.is(typewriterDrainingEffect)) {
331
+ draining = effect.value;
332
+ }
333
+ }
334
+ const docLength = tr.state.doc.length;
335
+ const visible = wanted && !draining && docLength > 0;
336
+ const wasVisible = decorations.size > 0;
337
+ if (visible !== wasVisible) {
338
+ decorations = visible ? buildSet(docLength) : Decoration.none;
339
+ } else if (tr.docChanged && decorations.size > 0) {
340
+ decorations = decorations.map(tr.changes);
341
+ }
342
+ return {
343
+ wanted,
344
+ draining,
345
+ decorations
346
+ };
347
+ },
348
+ provide: (f) => EditorView.decorations.from(f, (state) => state.decorations)
349
+ });
350
+ return [
351
+ field
352
+ ];
353
+ };
354
+ var FooterWidget = class extends WidgetType {
355
+ _setRoot;
356
+ constructor(_setRoot) {
357
+ super(), this._setRoot = _setRoot;
358
+ }
359
+ // Singleton equality so CM keeps the same DOM element across decoration rebuilds —
360
+ // the React subtree portaled into it is not unmounted on every doc change.
361
+ eq(_other) {
362
+ return true;
363
+ }
364
+ ignoreEvent() {
365
+ return true;
366
+ }
367
+ toDOM() {
368
+ const inner = Domino.of("div").classNames("cm-stream-footer-content").style({
369
+ position: "absolute",
370
+ left: "0",
371
+ top: "0"
372
+ });
373
+ const el = Domino.of("div").classNames("cm-stream-footer").style({
374
+ position: "relative",
375
+ height: "0"
376
+ }).append(inner);
377
+ this._setRoot(inner.root);
378
+ return el.root;
379
+ }
380
+ destroy() {
381
+ this._setRoot(null);
382
+ }
383
+ };
384
+
385
+ // src/MarkdownStream/MarkdownStream.tsx
386
+ var MarkdownStream = /* @__PURE__ */ forwardRef(({ classNames, debug, content, options, registry, extensions, footer: footer2, onEvent }, forwardedRef) => {
387
+ const contentRef = useRef(content ?? "");
388
+ const [footerRoot, setFooterRoot] = useState(null);
389
+ const { parentRef, view, viewRef, widgets } = useMarkdownStreamTextEditor(contentRef, {
390
+ debug,
391
+ registry,
392
+ options,
393
+ extensions,
394
+ setFooterRoot: footer2 ? setFooterRoot : void 0
395
+ });
396
+ const footerVisible = !!footer2;
397
+ useEffect(() => {
398
+ view?.dispatch({
399
+ effects: setFooterVisibleEffect.of(footerVisible)
400
+ });
401
+ }, [
402
+ view,
403
+ footerVisible
404
+ ]);
405
+ const [queue, setQueue, queueRef] = useStateWithRef(Effect2.runSync(Queue.unbounded()));
406
+ const onReset = useCallback(async (text) => {
407
+ contentRef.current = text;
408
+ if (!viewRef.current) {
409
+ return;
410
+ }
411
+ viewRef.current.dispatch({
412
+ effects: [
413
+ xmlTagContextEffect.of(null),
414
+ xmlTagResetEffect.of(null)
415
+ ],
416
+ changes: [
417
+ {
418
+ from: 0,
419
+ to: viewRef.current.state.doc.length,
420
+ insert: text
421
+ }
422
+ ],
423
+ annotations: typewriterBypass.of(true),
424
+ selection: EditorSelection.cursor(text.length)
425
+ });
426
+ setQueue(Effect2.runSync(Queue.unbounded()));
427
+ }, [
428
+ contentRef,
429
+ viewRef,
430
+ setQueue
431
+ ]);
432
+ useImperativeHandle(forwardedRef, () => createMarkdownStreamController({
433
+ contentRef,
434
+ viewRef,
435
+ queueRef,
436
+ onReset
437
+ }), [
438
+ onReset
439
+ ]);
440
+ useEffect(() => {
441
+ if (!parentRef.current) {
442
+ return;
443
+ }
444
+ return addEventListener(parentRef.current, "click", (event) => {
445
+ const button = event.target.closest('[data-action="submit"]');
446
+ if (button?.getAttribute("data-action") === "submit") {
447
+ onEvent?.({
448
+ type: "submit",
449
+ value: button.getAttribute("data-value")
450
+ });
451
+ }
452
+ });
453
+ }, [
454
+ view,
455
+ parentRef,
456
+ onEvent
457
+ ]);
458
+ useMarkdownStreamQueue(view, queue, {
459
+ chunkSize: options?.streamCadence,
460
+ delayMs: options?.streamDelayMs
461
+ });
462
+ useEffect(() => {
463
+ return () => {
464
+ view?.destroy();
465
+ };
466
+ }, [
467
+ view
468
+ ]);
469
+ return /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement("div", {
470
+ className: mx3("dx-container", classNames),
471
+ ref: parentRef
472
+ }), /* @__PURE__ */ React3.createElement(ErrorBoundary, {
473
+ name: "markdown-stream"
474
+ }, widgets.map(({ Component, root, id, props }) => /* @__PURE__ */ React3.createElement("div", {
475
+ key: id
476
+ }, /* @__PURE__ */ createPortal(/* @__PURE__ */ React3.createElement(Component, {
477
+ view,
478
+ ...props
479
+ }), root))), footerRoot && footerVisible && /* @__PURE__ */ createPortal(footer2, footerRoot)));
480
+ });
481
+ var useMarkdownStreamTextEditor = (currentContent, { debug, registry, options, extensions: extraExtensions, setFooterRoot }) => {
482
+ const { themeMode } = useThemeContext();
483
+ const [widgets, setWidgets] = useState([]);
484
+ const { view, parentRef } = useTextEditor(() => {
485
+ const content = currentContent.current;
486
+ return {
487
+ initialValue: content,
488
+ selection: EditorSelection.cursor(content?.length ?? 0),
489
+ extensions: [
490
+ createBasicExtensions({
491
+ lineWrapping: true,
492
+ readOnly: true
493
+ }),
494
+ createThemeExtensions({
495
+ slots: documentSlots,
496
+ scrollbarThin: true,
497
+ syntaxHighlighting: true,
498
+ themeMode
499
+ }),
500
+ xmlFormatting({
501
+ skip: debug ? [] : [
502
+ "prompt"
503
+ ]
504
+ }),
505
+ !debug && [
506
+ extendedMarkdown({
507
+ registry
508
+ }),
509
+ decorateMarkdown({
510
+ // `dxn:` links/images are reference widgets owned by `preview()` (PreviewInlineWidget /
511
+ // PreviewBlockWidget). Skipping them here avoids `decorateMarkdown` adding a
512
+ // non-functional `LinkButton` anchor on top of the same node — e.g. for
513
+ // `[DXOS](dxn:echo:BNPMIBEDJLRIILYUYZVM6GT64VWI6WPPZ:01KQ889PZBRNHAEECV0ANFAYX7)`.
514
+ skip: (node) => (node.name === "Link" || node.name === "Image") && node.url.startsWith("dxn:")
515
+ }),
516
+ preview(),
517
+ // NOTE: An ancestor element must set `data-hue` so `.dx-panel` resolves to the user's
518
+ // hue tokens (see `packages/ui/ui-theme/src/css/components/panel.css`). Tailwind picks
519
+ // up these utility classes from this source file.
520
+ xmlBlockDecoration({
521
+ tag: "prompt",
522
+ lineClass: "cm-prompt-line my-8",
523
+ contentClass: "cm-prompt-bubble dx-panel px-2 py-1.5 rounded-sm [&_*]:text-inherit!",
524
+ hideTags: true
525
+ }),
526
+ xmlTags({
527
+ registry,
528
+ setWidgets,
529
+ bookmarks: [
530
+ "prompt"
531
+ ]
532
+ }),
533
+ scroller({
534
+ overScroll: 80,
535
+ autoScroll: options?.autoScroll
536
+ }),
537
+ options?.typewriter && typewriter({
538
+ cursor: options?.cursor,
539
+ streamingTags: new Set(Object.entries(registry ?? {}).filter(([, def]) => def.streaming).map(([tag]) => tag))
540
+ }),
541
+ options?.fader && fader(),
542
+ setFooterRoot && footer(setFooterRoot)
543
+ ].filter(isTruthy),
544
+ extraExtensions
545
+ ].filter(isTruthy)
546
+ };
547
+ }, [
548
+ themeMode,
549
+ registry,
550
+ debug,
551
+ options?.autoScroll,
552
+ options?.typewriter,
553
+ options?.cursor,
554
+ options?.fader,
555
+ extraExtensions
556
+ ]);
557
+ const viewRef = useDynamicRef(view);
558
+ return {
559
+ view,
560
+ viewRef,
561
+ parentRef,
562
+ widgets
563
+ };
564
+ };
565
+ var useMarkdownStreamQueue = (view, queue, streamerOptions) => {
566
+ const chunkSize = streamerOptions?.chunkSize;
567
+ const delayMs = streamerOptions?.delayMs;
568
+ useEffect(() => {
569
+ if (!view) {
570
+ return;
571
+ }
572
+ const fork = Stream2.fromQueue(queue).pipe((source) => createStreamer(source, {
573
+ chunkSize,
574
+ delayMs
575
+ }), Stream2.runForEach((text) => Effect2.sync(() => {
576
+ const scrollTop = view.scrollDOM.scrollTop;
577
+ view.dispatch({
578
+ changes: [
579
+ {
580
+ from: view.state.doc.length,
581
+ insert: text
582
+ }
583
+ ],
584
+ annotations: Transaction.remote.of(true),
585
+ scrollIntoView: false
586
+ });
587
+ requestAnimationFrame(() => {
588
+ view.scrollDOM.scrollTop = scrollTop;
589
+ });
590
+ })), Effect2.runFork);
591
+ return () => {
592
+ void runAndForwardErrors(Fiber.interrupt(fork));
593
+ };
594
+ }, [
595
+ view,
596
+ queue,
597
+ chunkSize,
598
+ delayMs
599
+ ]);
600
+ };
601
+ var createMarkdownStreamController = ({ contentRef, viewRef, queueRef, onReset }) => {
602
+ return {
603
+ get length() {
604
+ return viewRef.current?.state.doc.length;
605
+ },
606
+ /** Focus the editor. */
607
+ focus: () => {
608
+ viewRef.current?.focus();
609
+ },
610
+ /** Scroll to bottom. */
611
+ scrollToBottom: (behavior) => {
612
+ viewRef.current?.dispatch({
613
+ effects: crawlerLineEffect.of({
614
+ line: -1,
615
+ behavior
616
+ })
617
+ });
618
+ },
619
+ /** Navigate previous prompt. */
620
+ navigatePrevious: () => {
621
+ viewRef.current?.dispatch({
622
+ effects: navigatePreviousEffect.of()
623
+ });
624
+ },
625
+ /** Navigate next prompt. */
626
+ navigateNext: () => {
627
+ viewRef.current?.dispatch({
628
+ effects: navigateNextEffect.of()
629
+ });
630
+ },
631
+ /** Set the context for widgets (XML tags). */
632
+ setContext: (context) => {
633
+ viewRef.current?.dispatch({
634
+ effects: xmlTagContextEffect.of(context)
635
+ });
636
+ },
637
+ /** Reset document. */
638
+ setContent: onReset,
639
+ /** Append to queue (and stream). */
640
+ append: async (text) => {
641
+ contentRef.current += text;
642
+ if (text.length) {
643
+ const queue = queueRef.current;
644
+ if (queue) {
645
+ await runAndForwardErrors(Queue.offer(queue, text));
646
+ }
647
+ }
648
+ },
649
+ /** Update widget state. */
650
+ updateWidget: (id, value) => {
651
+ viewRef.current?.dispatch({
652
+ effects: xmlTagUpdateEffect.of({
653
+ id,
654
+ value
655
+ })
656
+ });
657
+ }
658
+ };
659
+ };
76
660
  export {
77
- MarkdownViewer
661
+ MarkdownMedia,
662
+ MarkdownStream,
663
+ MarkdownView,
664
+ createStreamer,
665
+ isEmbedUrl,
666
+ renderObjectLink,
667
+ splitFragments,
668
+ splitSentences,
669
+ splitSpans,
670
+ textStream
78
671
  };
79
672
  //# sourceMappingURL=index.mjs.map