@dxos/react-ui-markdown 0.8.4-main.3c1ae3b → 0.8.4-main.3fbcb4aa9b

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