@valbuild/ui 0.20.2 → 0.21.0
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/valbuild-ui.cjs.d.ts +4 -2
- package/dist/valbuild-ui.cjs.js +10915 -9677
- package/dist/valbuild-ui.esm.js +8191 -6953
- package/package.json +4 -2
- package/src/components/RichTextEditor/Nodes/ImageNode.tsx +0 -8
- package/src/components/RichTextEditor/Plugins/LinkEditorPlugin.tsx +58 -0
- package/src/components/RichTextEditor/Plugins/Toolbar.tsx +70 -2
- package/src/components/RichTextEditor/RichTextEditor.tsx +15 -4
- package/src/components/ValOverlay.tsx +39 -27
- package/src/components/ValWindow.stories.tsx +5 -54
- package/src/components/dashboard/ValDashboard.tsx +2 -0
- package/src/exports.ts +1 -0
- package/src/richtext/conversion/conversion.test.ts +146 -0
- package/src/richtext/conversion/lexicalToRichTextSource.test.ts +89 -0
- package/src/richtext/conversion/lexicalToRichTextSource.ts +286 -0
- package/src/richtext/conversion/parseRichTextSource.test.ts +424 -0
- package/src/richtext/conversion/parseRichTextSource.ts +228 -0
- package/src/richtext/conversion/richTextSourceToLexical.test.ts +381 -0
- package/src/richtext/conversion/richTextSourceToLexical.ts +293 -0
- package/src/stories/RichTextEditor.stories.tsx +3 -47
- package/src/utils/imageMimeType.ts +23 -0
- package/src/utils/readImage.ts +1 -25
- package/src/components/RichTextEditor/conversion.test.ts +0 -132
- package/src/components/RichTextEditor/conversion.ts +0 -389
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { initVal } from "@valbuild/core";
|
|
2
|
+
import { parseRichTextSource } from "./parseRichTextSource";
|
|
3
|
+
|
|
4
|
+
const { val } = initVal();
|
|
5
|
+
|
|
6
|
+
//MD to HTML
|
|
7
|
+
describe("richtext", () => {
|
|
8
|
+
test("basic h1", () => {
|
|
9
|
+
const r = val.richtext`# Title 1`;
|
|
10
|
+
expect(parseRichTextSource(r).children).toStrictEqual([
|
|
11
|
+
{ tag: "h1", children: ["Title 1"] },
|
|
12
|
+
]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("basic complete", () => {
|
|
16
|
+
const r = val.richtext`# Title 1
|
|
17
|
+
## Title 2
|
|
18
|
+
|
|
19
|
+
Paragraph 1 2 3 4 5. Words *italic* **bold**
|
|
20
|
+
`;
|
|
21
|
+
expect(parseRichTextSource(r).children).toStrictEqual([
|
|
22
|
+
{ tag: "h1", children: ["Title 1"] },
|
|
23
|
+
{ tag: "h2", children: ["Title 2"] },
|
|
24
|
+
{
|
|
25
|
+
tag: "p",
|
|
26
|
+
children: [
|
|
27
|
+
"Paragraph 1 2 3 4 5. Words ",
|
|
28
|
+
{ tag: "span", classes: ["italic"], children: ["italic"] },
|
|
29
|
+
" ",
|
|
30
|
+
{ tag: "span", classes: ["bold"], children: ["bold"] },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test.skip("strong and emphasis merged spans", () => {
|
|
37
|
+
// TODO: currently we do not merge
|
|
38
|
+
const r = val.richtext`Which classes?
|
|
39
|
+
***All of them!***
|
|
40
|
+
`;
|
|
41
|
+
expect(parseRichTextSource(r).children).toStrictEqual([
|
|
42
|
+
{
|
|
43
|
+
tag: "p",
|
|
44
|
+
children: [
|
|
45
|
+
"Which classes?\n",
|
|
46
|
+
{
|
|
47
|
+
tag: "span",
|
|
48
|
+
classes: ["italic", "bold"],
|
|
49
|
+
children: ["All of them!"],
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("line through", () => {
|
|
57
|
+
// TODO: currently we do not merge
|
|
58
|
+
const r = val.richtext`~~line through~~`;
|
|
59
|
+
expect(parseRichTextSource(r).children).toStrictEqual([
|
|
60
|
+
{
|
|
61
|
+
tag: "p",
|
|
62
|
+
children: [
|
|
63
|
+
{
|
|
64
|
+
tag: "span",
|
|
65
|
+
classes: ["line-through"],
|
|
66
|
+
children: ["line through"],
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("2 paragraphs", () => {
|
|
74
|
+
const r = val.richtext`# Title 1
|
|
75
|
+
|
|
76
|
+
First paragraph
|
|
77
|
+
|
|
78
|
+
Second paragraph
|
|
79
|
+
`;
|
|
80
|
+
expect(parseRichTextSource(r).children).toStrictEqual([
|
|
81
|
+
{ tag: "h1", children: ["Title 1"] },
|
|
82
|
+
{ tag: "p", children: ["First paragraph"] },
|
|
83
|
+
{ tag: "p", children: ["Second paragraph"] },
|
|
84
|
+
]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("basic lists", () => {
|
|
88
|
+
const r = val.richtext`A bullet list:
|
|
89
|
+
|
|
90
|
+
- bullet 1
|
|
91
|
+
- bullet 2
|
|
92
|
+
`;
|
|
93
|
+
expect(parseRichTextSource(r).children).toStrictEqual([
|
|
94
|
+
{ tag: "p", children: ["A bullet list:"] },
|
|
95
|
+
{
|
|
96
|
+
tag: "ul",
|
|
97
|
+
children: [
|
|
98
|
+
{ tag: "li", children: ["bullet 1"] },
|
|
99
|
+
{ tag: "li", children: ["bullet 2"] },
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("lists with line breaks", () => {
|
|
106
|
+
const r = val.richtext`A bullet list:
|
|
107
|
+
|
|
108
|
+
- bullet 1
|
|
109
|
+
- bullet 2
|
|
110
|
+
break this line
|
|
111
|
+
`;
|
|
112
|
+
expect(parseRichTextSource(r).children).toStrictEqual([
|
|
113
|
+
{ tag: "p", children: ["A bullet list:"] },
|
|
114
|
+
{
|
|
115
|
+
tag: "ul",
|
|
116
|
+
children: [
|
|
117
|
+
{
|
|
118
|
+
tag: "li",
|
|
119
|
+
children: ["bullet 1"],
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
tag: "li",
|
|
123
|
+
children: [
|
|
124
|
+
"bullet 2",
|
|
125
|
+
{ tag: "br", children: [] },
|
|
126
|
+
"break this line",
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("special chars", () => {
|
|
135
|
+
const r = val.richtext`# "Title 1"
|
|
136
|
+
|
|
137
|
+
Beautiful "quotes" and 'single quotes'
|
|
138
|
+
|
|
139
|
+
Some crocodiles: < >
|
|
140
|
+
|
|
141
|
+
Ampersand: &
|
|
142
|
+
|
|
143
|
+
`;
|
|
144
|
+
expect(parseRichTextSource(r).children).toStrictEqual([
|
|
145
|
+
{ tag: "h1", children: ['"Title 1"'] },
|
|
146
|
+
{ tag: "p", children: [`Beautiful "quotes" and 'single quotes'`] },
|
|
147
|
+
{ tag: "p", children: ["Some crocodiles: < >"] },
|
|
148
|
+
{ tag: "p", children: ["Ampersand: &"] },
|
|
149
|
+
]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("lists", () => {
|
|
153
|
+
const r = val.richtext`# Title 1
|
|
154
|
+
|
|
155
|
+
A paragraph
|
|
156
|
+
|
|
157
|
+
A bullet list:
|
|
158
|
+
- bullet 1
|
|
159
|
+
- bullet 2
|
|
160
|
+
|
|
161
|
+
A numbered list:
|
|
162
|
+
1. number 1
|
|
163
|
+
2. number 2
|
|
164
|
+
|
|
165
|
+
A nested list:
|
|
166
|
+
- bullet 1:
|
|
167
|
+
1. number 1.1
|
|
168
|
+
2. number 1.2
|
|
169
|
+
- bullet 2:
|
|
170
|
+
- bullet 2.1
|
|
171
|
+
- bullet 2.2
|
|
172
|
+
`;
|
|
173
|
+
expect(parseRichTextSource(r).children).toStrictEqual([
|
|
174
|
+
{ tag: "h1", children: ["Title 1"] },
|
|
175
|
+
{ tag: "p", children: ["A paragraph"] },
|
|
176
|
+
{ tag: "p", children: ["A bullet list:"] },
|
|
177
|
+
{
|
|
178
|
+
tag: "ul",
|
|
179
|
+
children: [
|
|
180
|
+
{ tag: "li", children: ["bullet 1"] },
|
|
181
|
+
{ tag: "li", children: ["bullet 2"] },
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
{ tag: "p", children: ["A numbered list:"] },
|
|
185
|
+
{
|
|
186
|
+
tag: "ol",
|
|
187
|
+
children: [
|
|
188
|
+
{ tag: "li", children: ["number 1"] },
|
|
189
|
+
{ tag: "li", children: ["number 2"] },
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
{ tag: "p", children: ["A nested list:"] },
|
|
193
|
+
{
|
|
194
|
+
tag: "ul",
|
|
195
|
+
children: [
|
|
196
|
+
{
|
|
197
|
+
tag: "li",
|
|
198
|
+
children: [
|
|
199
|
+
"bullet 1:",
|
|
200
|
+
{
|
|
201
|
+
tag: "ol",
|
|
202
|
+
children: [
|
|
203
|
+
{ tag: "li", children: ["number 1.1"] },
|
|
204
|
+
{ tag: "li", children: ["number 1.2"] },
|
|
205
|
+
],
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
tag: "li",
|
|
211
|
+
children: [
|
|
212
|
+
"bullet 2:",
|
|
213
|
+
{
|
|
214
|
+
tag: "ul",
|
|
215
|
+
children: [
|
|
216
|
+
{ tag: "li", children: ["bullet 2.1"] },
|
|
217
|
+
{ tag: "li", children: ["bullet 2.2"] },
|
|
218
|
+
],
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
]);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("image", () => {
|
|
228
|
+
const r = val.richtext`# Title 1
|
|
229
|
+
|
|
230
|
+
Below we have an image block:
|
|
231
|
+
|
|
232
|
+
${val.file("/public/foo.png", {
|
|
233
|
+
width: 100,
|
|
234
|
+
height: 100,
|
|
235
|
+
sha256: "123",
|
|
236
|
+
})}`;
|
|
237
|
+
expect(parseRichTextSource(r).children).toStrictEqual([
|
|
238
|
+
{ tag: "h1", children: ["Title 1"] },
|
|
239
|
+
{ tag: "p", children: ["Below we have an image block:"] },
|
|
240
|
+
{
|
|
241
|
+
tag: "p",
|
|
242
|
+
children: [
|
|
243
|
+
{
|
|
244
|
+
src: "/foo.png?sha256=123",
|
|
245
|
+
tag: "img",
|
|
246
|
+
width: 100,
|
|
247
|
+
height: 100,
|
|
248
|
+
children: [],
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
]);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("markdown link", () => {
|
|
256
|
+
const r = val.richtext`# Title 1
|
|
257
|
+
|
|
258
|
+
Below we have a url: [google](https://google.com)`;
|
|
259
|
+
expect(parseRichTextSource(r).children).toStrictEqual([
|
|
260
|
+
{ tag: "h1", children: ["Title 1"] },
|
|
261
|
+
{
|
|
262
|
+
tag: "p",
|
|
263
|
+
children: [
|
|
264
|
+
"Below we have a url: ",
|
|
265
|
+
{
|
|
266
|
+
href: "https://google.com",
|
|
267
|
+
tag: "a",
|
|
268
|
+
children: ["google"],
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
},
|
|
272
|
+
]);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("block link", () => {
|
|
276
|
+
const r = val.richtext`# Title 1
|
|
277
|
+
|
|
278
|
+
Below we have a url:
|
|
279
|
+
|
|
280
|
+
${val.link("google", { href: "https://google.com" })}`;
|
|
281
|
+
expect(parseRichTextSource(r).children).toStrictEqual([
|
|
282
|
+
{ tag: "h1", children: ["Title 1"] },
|
|
283
|
+
{ tag: "p", children: ["Below we have a url:"] },
|
|
284
|
+
{
|
|
285
|
+
tag: "p",
|
|
286
|
+
children: [
|
|
287
|
+
{
|
|
288
|
+
tag: "a",
|
|
289
|
+
href: "https://google.com",
|
|
290
|
+
children: ["google"],
|
|
291
|
+
},
|
|
292
|
+
],
|
|
293
|
+
},
|
|
294
|
+
]);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("inline link", () => {
|
|
298
|
+
const r = val.richtext`# Title 1
|
|
299
|
+
|
|
300
|
+
Below we have a url: ${val.link("google", { href: "https://google.com" })}`;
|
|
301
|
+
expect(parseRichTextSource(r).children).toStrictEqual([
|
|
302
|
+
{ tag: "h1", children: ["Title 1"] },
|
|
303
|
+
{
|
|
304
|
+
tag: "p",
|
|
305
|
+
children: [
|
|
306
|
+
"Below we have a url: ",
|
|
307
|
+
{
|
|
308
|
+
href: "https://google.com",
|
|
309
|
+
tag: "a",
|
|
310
|
+
children: ["google"],
|
|
311
|
+
},
|
|
312
|
+
],
|
|
313
|
+
},
|
|
314
|
+
]);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("inline link with bold", () => {
|
|
318
|
+
const r = val.richtext`# Title 1
|
|
319
|
+
|
|
320
|
+
Inline link -> ${val.link("**google**", { href: "https://google.com" })}`;
|
|
321
|
+
|
|
322
|
+
// source:
|
|
323
|
+
expect(parseRichTextSource(r).children).toStrictEqual([
|
|
324
|
+
{ tag: "h1", children: ["Title 1"] },
|
|
325
|
+
{
|
|
326
|
+
tag: "p",
|
|
327
|
+
children: [
|
|
328
|
+
"Inline link -> ",
|
|
329
|
+
{
|
|
330
|
+
href: "https://google.com",
|
|
331
|
+
tag: "a",
|
|
332
|
+
children: [
|
|
333
|
+
{
|
|
334
|
+
tag: "span",
|
|
335
|
+
classes: ["bold"],
|
|
336
|
+
children: ["google"],
|
|
337
|
+
},
|
|
338
|
+
],
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
},
|
|
342
|
+
]);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("https:// in link description", () => {
|
|
346
|
+
const r = val.richtext`# Title 1
|
|
347
|
+
|
|
348
|
+
Inline link -> ${val.link("https://google.com", {
|
|
349
|
+
href: "https://google.com",
|
|
350
|
+
})}`;
|
|
351
|
+
|
|
352
|
+
expect(parseRichTextSource(r).children).toStrictEqual([
|
|
353
|
+
{ tag: "h1", children: ["Title 1"] },
|
|
354
|
+
{
|
|
355
|
+
tag: "p",
|
|
356
|
+
children: [
|
|
357
|
+
"Inline link -> ",
|
|
358
|
+
{
|
|
359
|
+
tag: "a",
|
|
360
|
+
href: "https://google.com",
|
|
361
|
+
children: ["https://google.com"],
|
|
362
|
+
},
|
|
363
|
+
],
|
|
364
|
+
},
|
|
365
|
+
]);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("auto link does nothing", () => {
|
|
369
|
+
const r = val.richtext`# Title 1
|
|
370
|
+
|
|
371
|
+
No transform here -> https://google.com
|
|
372
|
+
|
|
373
|
+
Transform this:
|
|
374
|
+
[https://google.com](https://google.com)
|
|
375
|
+
`;
|
|
376
|
+
|
|
377
|
+
expect(parseRichTextSource(r).children).toStrictEqual([
|
|
378
|
+
{ tag: "h1", children: ["Title 1"] },
|
|
379
|
+
{
|
|
380
|
+
tag: "p",
|
|
381
|
+
children: ["No transform here -> ", "https://google.com"],
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
tag: "p",
|
|
385
|
+
children: [
|
|
386
|
+
"Transform this:\n",
|
|
387
|
+
{
|
|
388
|
+
tag: "a",
|
|
389
|
+
href: "https://google.com",
|
|
390
|
+
children: ["https://google.com"],
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
},
|
|
394
|
+
]);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("breaks", () => {
|
|
398
|
+
const r = val.richtext`
|
|
399
|
+
# Title 1
|
|
400
|
+
|
|
401
|
+
Foo
|
|
402
|
+
|
|
403
|
+
<br>
|
|
404
|
+
|
|
405
|
+
Bar
|
|
406
|
+
`;
|
|
407
|
+
// source:
|
|
408
|
+
expect(parseRichTextSource(r).children).toStrictEqual([
|
|
409
|
+
{ tag: "h1", children: ["Title 1"] },
|
|
410
|
+
{
|
|
411
|
+
tag: "p",
|
|
412
|
+
children: ["Foo"],
|
|
413
|
+
},
|
|
414
|
+
{
|
|
415
|
+
tag: "br",
|
|
416
|
+
children: [],
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
tag: "p",
|
|
420
|
+
children: ["Bar"],
|
|
421
|
+
},
|
|
422
|
+
]);
|
|
423
|
+
});
|
|
424
|
+
});
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import * as marked from "marked";
|
|
2
|
+
import {
|
|
3
|
+
OrderedListNode,
|
|
4
|
+
AnyRichTextOptions,
|
|
5
|
+
UnorderedListNode,
|
|
6
|
+
LinkSource,
|
|
7
|
+
RichTextNode,
|
|
8
|
+
SpanNode,
|
|
9
|
+
LinkNode,
|
|
10
|
+
ListItemNode,
|
|
11
|
+
RichTextOptions,
|
|
12
|
+
RichTextSource,
|
|
13
|
+
RichText,
|
|
14
|
+
VAL_EXTENSION,
|
|
15
|
+
FileSource,
|
|
16
|
+
ParagraphNode,
|
|
17
|
+
HeadingNode,
|
|
18
|
+
Internal,
|
|
19
|
+
} from "@valbuild/core";
|
|
20
|
+
|
|
21
|
+
const VAL_START_TAG_PREFIX = '<val value="';
|
|
22
|
+
const VAL_START_TAG_SUFFIX = '">';
|
|
23
|
+
const VAL_END_TAG = "</val>";
|
|
24
|
+
|
|
25
|
+
type AnyListChildren =
|
|
26
|
+
| OrderedListNode<AnyRichTextOptions>["children"]
|
|
27
|
+
| UnorderedListNode<AnyRichTextOptions>["children"];
|
|
28
|
+
|
|
29
|
+
type ImageSource = FileSource<{
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
sha256: string;
|
|
33
|
+
}>;
|
|
34
|
+
|
|
35
|
+
function parseTokens(
|
|
36
|
+
tokens: marked.Token[],
|
|
37
|
+
sourceNodes: (ImageSource | LinkSource)[],
|
|
38
|
+
cursor: number,
|
|
39
|
+
insideList = false
|
|
40
|
+
): { children: RichTextNode<AnyRichTextOptions>[]; cursor: number } {
|
|
41
|
+
const children: RichTextNode<AnyRichTextOptions>[] = [];
|
|
42
|
+
while (cursor < tokens.length) {
|
|
43
|
+
const token = tokens[cursor];
|
|
44
|
+
if (token.type === "heading") {
|
|
45
|
+
children.push({
|
|
46
|
+
tag: `h${token.depth as 1 | 2 | 3 | 4 | 5 | 6}`,
|
|
47
|
+
children: parseTokens(token.tokens ? token.tokens : [], sourceNodes, 0)
|
|
48
|
+
.children as HeadingNode<AnyRichTextOptions>["children"],
|
|
49
|
+
});
|
|
50
|
+
} else if (token.type === "paragraph") {
|
|
51
|
+
children.push({
|
|
52
|
+
tag: "p",
|
|
53
|
+
children: parseTokens(token.tokens ? token.tokens : [], sourceNodes, 0)
|
|
54
|
+
.children as ParagraphNode<AnyRichTextOptions>["children"],
|
|
55
|
+
});
|
|
56
|
+
} else if (token.type === "strong") {
|
|
57
|
+
children.push({
|
|
58
|
+
tag: "span",
|
|
59
|
+
classes: ["bold"],
|
|
60
|
+
children: parseTokens(token.tokens ? token.tokens : [], sourceNodes, 0)
|
|
61
|
+
.children as SpanNode<AnyRichTextOptions>["children"],
|
|
62
|
+
});
|
|
63
|
+
} else if (token.type === "em") {
|
|
64
|
+
children.push({
|
|
65
|
+
tag: "span",
|
|
66
|
+
classes: ["italic"],
|
|
67
|
+
children: parseTokens(token.tokens ? token.tokens : [], sourceNodes, 0)
|
|
68
|
+
.children as SpanNode<AnyRichTextOptions>["children"],
|
|
69
|
+
});
|
|
70
|
+
} else if (token.type === "del") {
|
|
71
|
+
children.push({
|
|
72
|
+
tag: "span",
|
|
73
|
+
classes: ["line-through"],
|
|
74
|
+
children: parseTokens(token.tokens ? token.tokens : [], sourceNodes, 0)
|
|
75
|
+
.children as SpanNode<AnyRichTextOptions>["children"],
|
|
76
|
+
});
|
|
77
|
+
} else if (token.type === "text") {
|
|
78
|
+
if ("tokens" in token && Array.isArray(token.tokens)) {
|
|
79
|
+
children.push(
|
|
80
|
+
...parseTokens(token.tokens, sourceNodes, cursor, insideList).children
|
|
81
|
+
);
|
|
82
|
+
} else {
|
|
83
|
+
if (insideList && typeof token.raw === "string") {
|
|
84
|
+
const lines = token.raw.split("\n");
|
|
85
|
+
const tags: RichTextNode<AnyRichTextOptions>[] = lines.flatMap(
|
|
86
|
+
(line, i) => {
|
|
87
|
+
if (i === lines.length - 1) return [line];
|
|
88
|
+
return [line, { tag: "br", children: [] }];
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
children.push(...tags);
|
|
92
|
+
} else {
|
|
93
|
+
children.push(token.raw);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} else if (token.type === "list") {
|
|
97
|
+
children.push({
|
|
98
|
+
tag: token.ordered ? "ol" : "ul",
|
|
99
|
+
children: parseTokens(token.items, sourceNodes, 0)
|
|
100
|
+
.children as AnyListChildren,
|
|
101
|
+
});
|
|
102
|
+
} else if (token.type === "list_item") {
|
|
103
|
+
children.push({
|
|
104
|
+
tag: "li",
|
|
105
|
+
children: parseTokens(
|
|
106
|
+
token.tokens ? token.tokens : [],
|
|
107
|
+
sourceNodes,
|
|
108
|
+
0,
|
|
109
|
+
true
|
|
110
|
+
).children as ListItemNode<AnyRichTextOptions>["children"],
|
|
111
|
+
});
|
|
112
|
+
} else if (token.type === "space") {
|
|
113
|
+
// do nothing
|
|
114
|
+
} else if (token.type === "html") {
|
|
115
|
+
if (token.text === VAL_END_TAG) {
|
|
116
|
+
return { children, cursor };
|
|
117
|
+
}
|
|
118
|
+
const suffixIndex = token.text.indexOf(VAL_START_TAG_SUFFIX);
|
|
119
|
+
if (token.text.startsWith(VAL_START_TAG_PREFIX) && suffixIndex > -1) {
|
|
120
|
+
const number = Number(
|
|
121
|
+
token.text.slice(VAL_START_TAG_PREFIX.length, suffixIndex)
|
|
122
|
+
);
|
|
123
|
+
if (Number.isNaN(number)) {
|
|
124
|
+
throw Error(
|
|
125
|
+
`Illegal val intermediate node: ${JSON.stringify(token)}`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const { children: subChildren, cursor: subCursor } = parseTokens(
|
|
129
|
+
tokens.map((token) => {
|
|
130
|
+
if (token.type === "link" || token.type === "list") {
|
|
131
|
+
return {
|
|
132
|
+
type: "text",
|
|
133
|
+
raw: token.raw,
|
|
134
|
+
text: token.raw,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return token;
|
|
138
|
+
}),
|
|
139
|
+
sourceNodes,
|
|
140
|
+
cursor + 1
|
|
141
|
+
);
|
|
142
|
+
const sourceNode = sourceNodes[number];
|
|
143
|
+
if (sourceNode._type === "link") {
|
|
144
|
+
children.push({
|
|
145
|
+
tag: "a",
|
|
146
|
+
href: sourceNode.href,
|
|
147
|
+
children: subChildren as LinkNode<AnyRichTextOptions>["children"],
|
|
148
|
+
});
|
|
149
|
+
} else if (sourceNode._type === "file") {
|
|
150
|
+
children.push({
|
|
151
|
+
tag: "img",
|
|
152
|
+
src: Internal.convertFileSource(sourceNode).url,
|
|
153
|
+
width: sourceNode.metadata?.width,
|
|
154
|
+
height: sourceNode.metadata?.height,
|
|
155
|
+
children: [],
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
cursor = subCursor;
|
|
160
|
+
}
|
|
161
|
+
const br_html_regex = /<br\s*\/?>/gi; // matches <br>, <br/>, <br />; case insensitive
|
|
162
|
+
if (token.text.trim().match(br_html_regex)) {
|
|
163
|
+
children.push({
|
|
164
|
+
tag: "br",
|
|
165
|
+
children: [],
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
} else if (token.type === "link") {
|
|
169
|
+
if (token.raw === token.href) {
|
|
170
|
+
// avoid auto-linking (provided by github flavoured markdown, but we want strikethrough so keep it enabled)
|
|
171
|
+
children.push(token.raw);
|
|
172
|
+
} else {
|
|
173
|
+
children.push({
|
|
174
|
+
tag: "a",
|
|
175
|
+
href: token.href,
|
|
176
|
+
children: parseTokens(
|
|
177
|
+
token.tokens ? token.tokens : [],
|
|
178
|
+
sourceNodes,
|
|
179
|
+
0
|
|
180
|
+
).children as LinkNode<AnyRichTextOptions>["children"],
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
console.error(
|
|
185
|
+
`Could not parse markdown: unsupported token type: ${token.type}. Found: ${token.raw}`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
cursor++;
|
|
189
|
+
}
|
|
190
|
+
return { children, cursor };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function parseRichTextSource<O extends RichTextOptions>({
|
|
194
|
+
templateStrings,
|
|
195
|
+
exprs: nodes,
|
|
196
|
+
}: RichTextSource<O>): RichText<O> {
|
|
197
|
+
// TODO: validate that templateStrings does not contain VAL_NODE_PREFIX
|
|
198
|
+
const inputText = templateStrings
|
|
199
|
+
.flatMap((templateString, i) => {
|
|
200
|
+
const node = nodes[i];
|
|
201
|
+
if (node) {
|
|
202
|
+
if (node[VAL_EXTENSION] === "link") {
|
|
203
|
+
return templateString.concat(
|
|
204
|
+
`${VAL_START_TAG_PREFIX}${i}${VAL_START_TAG_SUFFIX}${node.children[0]}${VAL_END_TAG}`
|
|
205
|
+
);
|
|
206
|
+
} else {
|
|
207
|
+
return templateString.concat(
|
|
208
|
+
`${VAL_START_TAG_PREFIX}${i}${VAL_START_TAG_SUFFIX}${VAL_END_TAG}`
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return templateString;
|
|
213
|
+
})
|
|
214
|
+
.join("");
|
|
215
|
+
const tokenList = marked.lexer(inputText, {
|
|
216
|
+
gfm: true,
|
|
217
|
+
});
|
|
218
|
+
const { children, cursor } = parseTokens(tokenList, nodes, 0);
|
|
219
|
+
if (cursor !== tokenList.length) {
|
|
220
|
+
throw Error(
|
|
221
|
+
"Unexpectedly terminated markdown parsing. Possible reason: unclosed html tag?"
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
[VAL_EXTENSION]: "richtext",
|
|
226
|
+
children,
|
|
227
|
+
} as RichText<O>;
|
|
228
|
+
}
|