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