@dxos/react-ui-markdown 0.8.4-main.9be5663bfe → 0.8.4-main.abd8ff62ef

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 +541 -10
  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 +26 -22
  30. package/src/{MarkdownViewer/MarkdownViewer.stories.tsx → MarkdownBlock/MarkdownBlock.stories.tsx} +6 -6
  31. package/src/{MarkdownViewer/MarkdownViewer.tsx → MarkdownBlock/MarkdownBlock.tsx} +14 -9
  32. package/src/{MarkdownViewer → MarkdownBlock}/index.ts +1 -1
  33. package/src/MarkdownStream/MarkdownStream.stories.tsx +215 -0
  34. package/src/MarkdownStream/MarkdownStream.tsx +446 -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
@@ -0,0 +1,229 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+ import * as Stream from 'effect/Stream';
7
+
8
+ import { Obj } from '@dxos/echo';
9
+
10
+ export const renderObjectLink = (obj: Obj.Unknown, block?: boolean) =>
11
+ `${block ? '!' : ''}[${Obj.getLabel(obj)}](${Obj.getDXN(obj).toString()})`;
12
+
13
+ export type StreamerOptions = {
14
+ /**
15
+ * How to subdivide plain-text spans before emitting them downstream.
16
+ * - `'span'` (default) — keep entire spans intact; one CM dispatch per chunk that arrived at the source.
17
+ * - `'word'` — split text spans at whitespace boundaries (whitespace runs become their own tokens).
18
+ * - `'character'` — split text spans into individual characters.
19
+ * XML/HTML fragments (`<tag>…</tag>`, self-closing tags, hyphenated custom elements) are always
20
+ * emitted as one token regardless of `chunkSize`, otherwise widget mounting would see partial markup.
21
+ */
22
+ chunkSize?: 'character' | 'word' | 'span';
23
+ /**
24
+ * Inter-token delay in ms. Default `0`. Useful for slowing down rapid bursts so the
25
+ * downstream renderer (CodeMirror) can show a visible streaming cadence even when the
26
+ * source emits large chunks at once.
27
+ */
28
+ delayMs?: number;
29
+ };
30
+
31
+ /**
32
+ * Streams tokens to the consumer, keeping XML/HTML fragments intact.
33
+ *
34
+ * The cadence is controlled by `options.chunkSize`. `'span'` (default) preserves the
35
+ * one-token-per-source-chunk behaviour the function had originally. Use `'word'` or
36
+ * `'character'` to decouple the visible cadence from the source's chunk size — useful when
37
+ * the AI service emits large partial blocks but you want a smoother typewriter effect.
38
+ */
39
+ export const createStreamer = (
40
+ source: Stream.Stream<string>,
41
+ { chunkSize = 'span', delayMs = 0 }: StreamerOptions = {},
42
+ ) => {
43
+ const subdivide =
44
+ chunkSize === 'span'
45
+ ? (token: string) => [token]
46
+ : (token: string) => (isXmlFragment(token) ? [token] : splitTextSpan(token, chunkSize));
47
+
48
+ let stream: Stream.Stream<string> = source.pipe(
49
+ Stream.flatMap((chunk) => Stream.fromIterable(splitFragments(chunk).flatMap(subdivide))),
50
+ );
51
+ if (delayMs > 0) {
52
+ stream = stream.pipe(Stream.tap(() => Effect.sleep(`${delayMs} millis`)));
53
+ }
54
+ return stream;
55
+ };
56
+
57
+ /** A token starts with `<` if and only if it is an XML/HTML fragment produced by `splitFragments`. */
58
+ const isXmlFragment = (token: string): boolean => token.startsWith('<');
59
+
60
+ /**
61
+ * Subdivide a non-XML text span into smaller tokens. `'word'` splits into runs of non-whitespace
62
+ * and runs of whitespace as separate tokens (so the renderer can display whitespace cadence too).
63
+ */
64
+ const splitTextSpan = (span: string, chunkSize: 'character' | 'word'): string[] => {
65
+ if (chunkSize === 'character') {
66
+ return [...span];
67
+ }
68
+ return span.match(/\s+|\S+/g) ?? [span];
69
+ };
70
+
71
+ /** Matches opening tag names, including custom elements with hyphens (e.g. dom-widget). */
72
+ const OPENING_TAG_NAME = /^<([a-zA-Z][\w-]*)(?:\s[^>]*)?>/;
73
+
74
+ /**
75
+ * Splits text into chunks, preserving XML/HTML fragments.
76
+ */
77
+ export const splitFragments = (text: string): string[] => {
78
+ // First tokenize with tags to get tags as complete tokens.
79
+ const initialTokens = splitSpans(text);
80
+ const tokens: string[] = [];
81
+
82
+ let i = 0;
83
+ while (i < initialTokens.length) {
84
+ const token = initialTokens[i];
85
+
86
+ // Check if this is an opening tag.
87
+ if (token.startsWith('<') && !token.startsWith('</') && !token.endsWith('/>')) {
88
+ const tagMatch = token.match(OPENING_TAG_NAME);
89
+ if (tagMatch) {
90
+ const tagName = tagMatch[1];
91
+ const closingTag = `</${tagName}>`;
92
+
93
+ // Collect tokens until we find the closing tag.
94
+ let fragment = token;
95
+ let foundClosing = false;
96
+ let j = i + 1;
97
+ while (j < initialTokens.length) {
98
+ fragment += initialTokens[j];
99
+ if (initialTokens[j] === closingTag) {
100
+ foundClosing = true;
101
+ break;
102
+ }
103
+ j++;
104
+ }
105
+
106
+ if (foundClosing) {
107
+ // Return the complete element as one token.
108
+ tokens.push(fragment);
109
+ i = j + 1;
110
+ } else {
111
+ // No closing tag found, just add the opening tag.
112
+ tokens.push(token);
113
+ i++;
114
+ }
115
+ } else {
116
+ // Not a valid opening tag.
117
+ tokens.push(token);
118
+ i++;
119
+ }
120
+ } else {
121
+ // Not an opening tag (could be closing tag, self-closing, or regular character).
122
+ tokens.push(token);
123
+ i++;
124
+ }
125
+ }
126
+
127
+ return tokens;
128
+ };
129
+
130
+ // TODO(burdon): Split into paragraphs and tags.
131
+ // export const tokenizeWithTags = (text: string): string[] => {
132
+ // const tokens: string[] = [];
133
+ // let i = 0;
134
+ // while (i < text.length) {
135
+ // if (text[i] === '<') {
136
+ // // Find the closing bracket.
137
+ // const closeIndex = text.indexOf('>', i);
138
+ // if (closeIndex !== -1) {
139
+ // // Include the complete tag.
140
+ // tokens.push(text.slice(i, closeIndex + 1));
141
+ // i = closeIndex + 1;
142
+ // } else {
143
+ // // No closing bracket found, return the entire remaining fragment.
144
+ // tokens.push(text.slice(i));
145
+ // break;
146
+ // }
147
+ // } else {
148
+ // // Regular character.
149
+ // tokens.push(text[i]);
150
+ // i++;
151
+ // }
152
+ // }
153
+ // return tokens;
154
+ // };
155
+
156
+ export const splitSpans = (text: string): string[] => {
157
+ const spans: string[] = [];
158
+ let currentText = '';
159
+
160
+ let i = 0;
161
+ while (i < text.length) {
162
+ if (text[i] === '<') {
163
+ // If we have accumulated text, split it into sentences and push them.
164
+ if (currentText) {
165
+ // const sentences = splitSentences(currentText);
166
+ // spans.push(...sentences);
167
+ spans.push(currentText);
168
+ currentText = '';
169
+ }
170
+
171
+ // Find the closing bracket.
172
+ const closeIndex = text.indexOf('>', i);
173
+ if (closeIndex !== -1) {
174
+ // Include the complete tag.
175
+ spans.push(text.slice(i, closeIndex + 1));
176
+ i = closeIndex + 1;
177
+ } else {
178
+ // No closing bracket found, treat the rest as text.
179
+ currentText = text.slice(i);
180
+ break;
181
+ }
182
+ } else {
183
+ // Accumulate regular text.
184
+ currentText += text[i];
185
+ i++;
186
+ }
187
+ }
188
+
189
+ // Push any remaining text split into sentences.
190
+ if (currentText) {
191
+ spans.push(currentText);
192
+ // const sentences = splitSentences(currentText);
193
+ // spans.push(...sentences);
194
+ }
195
+
196
+ return spans;
197
+ };
198
+
199
+ /**
200
+ * Split text into sentences, preserving the sentence-ending punctuation.
201
+ */
202
+ export const splitSentences = (text: string): string[] => {
203
+ // Match sentences ending with ., !, or ? followed by space or end of string.
204
+ // This regex captures the sentence including its ending punctuation.
205
+ const sentenceRegex = /[^.!?]*[.!?]+(?:\s+|$)/g;
206
+ const sentences: string[] = [];
207
+ let lastIndex = 0;
208
+ let match;
209
+
210
+ while ((match = sentenceRegex.exec(text)) !== null) {
211
+ sentences.push(match[0]);
212
+ lastIndex = match.index + match[0].length;
213
+ }
214
+
215
+ // If there's remaining text that doesn't end with punctuation, include it.
216
+ if (lastIndex < text.length) {
217
+ const remaining = text.slice(lastIndex);
218
+ if (remaining) {
219
+ sentences.push(remaining);
220
+ }
221
+ }
222
+
223
+ // If no sentences were found, return the original text.
224
+ if (sentences.length === 0 && text) {
225
+ sentences.push(text);
226
+ }
227
+
228
+ return sentences;
229
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './testing';
@@ -0,0 +1,56 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ /**
6
+ * Options for the streaming text generator.
7
+ */
8
+ export type TextStreamOptions = {
9
+ /** Delay between chunks in ms. */
10
+ chunkDelay?: number;
11
+ /** Variance in timing (0-1). */
12
+ variance?: number;
13
+ /** Number of words per chunk. */
14
+ wordsPerChunk?: number;
15
+ };
16
+
17
+ /**
18
+ * Simulates word-by-word streaming (more natural for LLMs).
19
+ */
20
+ export async function* textStream(
21
+ text: string,
22
+ options: TextStreamOptions = {},
23
+ ): AsyncGenerator<string, void, unknown> {
24
+ const { chunkDelay = 100, variance = 0.3, wordsPerChunk = 3 } = options;
25
+
26
+ // Split into words while preserving whitespace.
27
+ const words = text.match(/\S+|\s+/g) || [];
28
+
29
+ let i = 0;
30
+ while (i < words.length) {
31
+ // Collect multiple words for this chunk.
32
+ const chunkWords: string[] = [];
33
+
34
+ // Add words up to wordsPerChunk (counting only non-whitespace as words).
35
+ let wordCount = 0;
36
+ while (i < words.length && wordCount < wordsPerChunk) {
37
+ const word = words[i];
38
+ chunkWords.push(word);
39
+
40
+ // Only count non-whitespace as words.
41
+ if (word.trim()) {
42
+ wordCount++;
43
+ }
44
+ i++;
45
+ }
46
+
47
+ // Yield the chunk.
48
+ const chunk = chunkWords.join('');
49
+ yield chunk;
50
+
51
+ // Calculate delay based on chunk length.
52
+ const varianceMultiplier = 1 + (Math.random() - 0.5) * variance * 2;
53
+ const delay = chunkDelay * varianceMultiplier;
54
+ await new Promise((resolve) => setTimeout(resolve, delay));
55
+ }
56
+ }
@@ -0,0 +1,67 @@
1
+ ## Markdown
2
+
3
+ Markdown is a lightweight markup language used to format plain text in a simple and readable way. It allows you to create structured documents using conventions for headings, lists, emphasis (bold/italic), links, images, code, blockquotes, tables, and horizontal rules.
4
+
5
+ It’s widely used in:
6
+
7
+ - documentation
8
+ - note-taking
9
+ - online writing
10
+
11
+ There are task lists also:
12
+
13
+ - [ ] Not done
14
+ - [x] Done
15
+
16
+ ## Benefits
17
+
18
+ Markdown is designed to be human-readable, meaning that even without rendering, the text remains understandable. It’s highly portable and supported across many platforms like GitHub, documentation tools, blogging systems, and note-taking apps.
19
+
20
+ JSON fenced code:
21
+
22
+ ```json
23
+ {
24
+ "hello": "world",
25
+ "items": [1, 2, 3, 4, 5]
26
+ }
27
+ ```
28
+
29
+ We can also have <xml>content</xml>.
30
+
31
+ ## Structured data
32
+
33
+ Table:
34
+
35
+ | Column 1 | Column 2 |
36
+ | -------- | -------- |
37
+ | Item 1 | Item 2 |
38
+ | Item 3 | Item 4 |
39
+ | Item 5 | Item 6 |
40
+
41
+ There are also extended flavors of Markdown (like GitHub Flavored Markdown) that add features such as checkboxes, footnotes, and task lists, expanding its capabilities for more complex documents.
42
+
43
+ Markdown’s simplicity makes it ideal for writing structured content quickly while keeping the source clean and readable.
44
+
45
+ If you want, I can also break down how Markdown parsing actually works behind the scenes, which explains how these plain-text symbols get converted to formatted output. Do you want me to do that?
46
+
47
+ ## Summary
48
+
49
+ Markdown is a quiet kind of craft,
50
+ Hashes, dashes, lines that draft.
51
+ Asterisks to tilt a phrase,
52
+ Backticks cage a block of blaze.
53
+
54
+ A link is just a name and route,
55
+ No lacquered gloss to dress it out.
56
+ It travels clean from eye to page,
57
+ A small instruction on the stage.
58
+
59
+ Lists march down in measured rows,
60
+ Indented thoughts in steady prose.
61
+ Blockquotes lean and speak aside,
62
+ A softer voice, but not denied.
63
+
64
+ No ornate fonts, no tricks of light,
65
+ Just structure drawn in plainest white.
66
+ Yet from that spare, unmoving art,
67
+ A document can find its heart.
package/src/index.ts CHANGED
@@ -2,4 +2,5 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- export * from './MarkdownViewer';
5
+ export * from './MarkdownBlock';
6
+ export * from './MarkdownStream';
@@ -1 +0,0 @@
1
- {"version":3,"file":"MarkdownViewer.d.ts","sourceRoot":"","sources":["../../../../src/MarkdownViewer/MarkdownViewer.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,EAAE,KAAK,iBAAiB,EAAE,MAAM,OAAO,CAAC;AACtD,OAAsB,EAAE,KAAK,OAAO,IAAI,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAGrF,OAAO,EAAE,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAItD,MAAM,MAAM,mBAAmB,GAAG,eAAe,CAC/C,iBAAiB,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,oBAAoB,CAAC,YAAY,CAAC,CAAC;CACjD,CAAC,CACH,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,+CAAoD,mBAAmB,sBASrG,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"MarkdownViewer.stories.d.ts","sourceRoot":"","sources":["../../../../src/MarkdownViewer/MarkdownViewer.stories.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAa,KAAK,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AAMjE,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAIlD,QAAA,MAAM,IAAI;;;;CAI6B,CAAC;AAExC,eAAe,IAAI,CAAC;AAEpB,KAAK,KAAK,GAAG,QAAQ,CAAC,OAAO,cAAc,CAAC,CAAC;AA+C7C,eAAO,MAAM,OAAO,EAAE,KAKrB,CAAC"}
@@ -1,2 +0,0 @@
1
- export * from './MarkdownViewer';
2
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/MarkdownViewer/index.ts"],"names":[],"mappings":"AAIA,cAAc,kBAAkB,CAAC"}