@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
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
RichTextEditorProps,
|
|
4
4
|
} from "../components/RichTextEditor/RichTextEditor";
|
|
5
5
|
import { Meta, Story } from "@storybook/react";
|
|
6
|
-
import { AnyRichTextOptions, RichTextSource } from "@valbuild/core";
|
|
7
6
|
|
|
8
7
|
export default {
|
|
9
8
|
title: "RichTextEditor",
|
|
@@ -19,50 +18,7 @@ export const DropdownStory = Template.bind({});
|
|
|
19
18
|
DropdownStory.args = {
|
|
20
19
|
richtext: {
|
|
21
20
|
_type: "richtext",
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
{ tag: "h3", children: ["Title 3"] },
|
|
26
|
-
{ tag: "h4", children: ["Title 4"] },
|
|
27
|
-
{ tag: "h5", children: ["Title 5"] },
|
|
28
|
-
{ tag: "h6", children: ["Title 6"] },
|
|
29
|
-
{
|
|
30
|
-
tag: "p",
|
|
31
|
-
children: [
|
|
32
|
-
{
|
|
33
|
-
tag: "span",
|
|
34
|
-
classes: ["bold", "italic", "line-through"],
|
|
35
|
-
children: ["Formatted span"],
|
|
36
|
-
},
|
|
37
|
-
],
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
tag: "ul",
|
|
41
|
-
children: [
|
|
42
|
-
{
|
|
43
|
-
tag: "li",
|
|
44
|
-
children: [
|
|
45
|
-
{
|
|
46
|
-
tag: "ol",
|
|
47
|
-
dir: "rtl",
|
|
48
|
-
children: [
|
|
49
|
-
{
|
|
50
|
-
tag: "li",
|
|
51
|
-
children: [
|
|
52
|
-
{
|
|
53
|
-
tag: "span",
|
|
54
|
-
classes: ["italic"],
|
|
55
|
-
children: ["number 1.1"],
|
|
56
|
-
},
|
|
57
|
-
],
|
|
58
|
-
},
|
|
59
|
-
{ tag: "li", children: ["number 1.2"] },
|
|
60
|
-
],
|
|
61
|
-
},
|
|
62
|
-
],
|
|
63
|
-
},
|
|
64
|
-
],
|
|
65
|
-
},
|
|
66
|
-
],
|
|
67
|
-
} as RichTextSource<AnyRichTextOptions>,
|
|
21
|
+
templateStrings: ["# Title 1"],
|
|
22
|
+
exprs: [],
|
|
23
|
+
},
|
|
68
24
|
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const MIME_TYPE_REGEX =
|
|
2
|
+
/^data:(image\/(png|jpeg|jpg|gif|webp|bmp|tiff|ico|svg\+xml));base64,/;
|
|
3
|
+
|
|
4
|
+
export function getMimeType(base64Url: string): string | undefined {
|
|
5
|
+
const match = MIME_TYPE_REGEX.exec(base64Url);
|
|
6
|
+
if (match && match[1]) {
|
|
7
|
+
return match[1];
|
|
8
|
+
}
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function mimeTypeToFileExt(mimeType: string) {
|
|
13
|
+
if (mimeType === "image/svg+xml") {
|
|
14
|
+
return "svg";
|
|
15
|
+
}
|
|
16
|
+
if (mimeType === "image/vnd.microsoft.icon") {
|
|
17
|
+
return "ico";
|
|
18
|
+
}
|
|
19
|
+
if (mimeType.startsWith("image/")) {
|
|
20
|
+
return mimeType.slice("image/".length);
|
|
21
|
+
}
|
|
22
|
+
return mimeType;
|
|
23
|
+
}
|
package/src/utils/readImage.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Internal } from "@valbuild/core";
|
|
2
2
|
import { ChangeEvent } from "react";
|
|
3
|
+
import { getMimeType, mimeTypeToFileExt } from "./imageMimeType";
|
|
3
4
|
|
|
4
5
|
const textEncoder = new TextEncoder();
|
|
5
6
|
|
|
@@ -24,7 +25,6 @@ export function readImage(ev: ChangeEvent<HTMLInputElement>) {
|
|
|
24
25
|
);
|
|
25
26
|
if (image.naturalWidth && image.naturalHeight) {
|
|
26
27
|
const mimeType = getMimeType(result);
|
|
27
|
-
console.log(result.slice(0, 30), mimeType);
|
|
28
28
|
resolve({
|
|
29
29
|
src: result,
|
|
30
30
|
width: image.naturalWidth,
|
|
@@ -52,27 +52,3 @@ export function readImage(ev: ChangeEvent<HTMLInputElement>) {
|
|
|
52
52
|
}
|
|
53
53
|
});
|
|
54
54
|
}
|
|
55
|
-
|
|
56
|
-
const MIME_TYPE_REGEX =
|
|
57
|
-
/^data:(image\/(png|jpeg|jpg|gif|webp|bmp|tiff|ico|svg\+xml));base64,/;
|
|
58
|
-
|
|
59
|
-
function getMimeType(base64Url: string): string | undefined {
|
|
60
|
-
const match = MIME_TYPE_REGEX.exec(base64Url);
|
|
61
|
-
if (match && match[1]) {
|
|
62
|
-
return match[1];
|
|
63
|
-
}
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function mimeTypeToFileExt(mimeType: string) {
|
|
68
|
-
if (mimeType === "image/svg+xml") {
|
|
69
|
-
return "svg";
|
|
70
|
-
}
|
|
71
|
-
if (mimeType === "image/vnd.microsoft.icon") {
|
|
72
|
-
return "ico";
|
|
73
|
-
}
|
|
74
|
-
if (mimeType.startsWith("image/")) {
|
|
75
|
-
return mimeType.slice("image/".length);
|
|
76
|
-
}
|
|
77
|
-
return mimeType;
|
|
78
|
-
}
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import { AnyRichTextOptions, RichText, RichTextSource } from "@valbuild/core";
|
|
2
|
-
import {
|
|
3
|
-
fromLexical,
|
|
4
|
-
fromLexicalFormat,
|
|
5
|
-
toLexical,
|
|
6
|
-
toLexicalFormat,
|
|
7
|
-
} from "./conversion";
|
|
8
|
-
|
|
9
|
-
describe("richtext conversion", () => {
|
|
10
|
-
test("format conversion", () => {
|
|
11
|
-
//
|
|
12
|
-
expect(toLexicalFormat([])).toStrictEqual(0);
|
|
13
|
-
expect(toLexicalFormat(["bold"])).toStrictEqual(1);
|
|
14
|
-
expect(toLexicalFormat(["italic"])).toStrictEqual(2);
|
|
15
|
-
expect(toLexicalFormat(["bold", "italic"])).toStrictEqual(3);
|
|
16
|
-
expect(toLexicalFormat(["line-through"])).toStrictEqual(4);
|
|
17
|
-
expect(toLexicalFormat(["bold", "line-through"])).toStrictEqual(5);
|
|
18
|
-
expect(toLexicalFormat(["italic", "line-through"])).toStrictEqual(6);
|
|
19
|
-
expect(toLexicalFormat(["bold", "italic", "line-through"])).toStrictEqual(
|
|
20
|
-
7
|
|
21
|
-
);
|
|
22
|
-
//
|
|
23
|
-
expect(fromLexicalFormat(0)).toStrictEqual([]);
|
|
24
|
-
expect(fromLexicalFormat(1)).toStrictEqual(["bold"]);
|
|
25
|
-
expect(fromLexicalFormat(2)).toStrictEqual(["italic"]);
|
|
26
|
-
expect(fromLexicalFormat(3)).toStrictEqual(["bold", "italic"]);
|
|
27
|
-
expect(fromLexicalFormat(4)).toStrictEqual(["line-through"]);
|
|
28
|
-
expect(fromLexicalFormat(5)).toStrictEqual(["bold", "line-through"]);
|
|
29
|
-
expect(fromLexicalFormat(6)).toStrictEqual(["italic", "line-through"]);
|
|
30
|
-
expect(fromLexicalFormat(7)).toStrictEqual([
|
|
31
|
-
"bold",
|
|
32
|
-
"italic",
|
|
33
|
-
"line-through",
|
|
34
|
-
]);
|
|
35
|
-
});
|
|
36
|
-
test("basic lexical text conversion to <-> from", () => {
|
|
37
|
-
const input: RichTextSource<AnyRichTextOptions> = {
|
|
38
|
-
_type: "richtext",
|
|
39
|
-
children: [
|
|
40
|
-
{ tag: "h1", children: ["Title 1"] },
|
|
41
|
-
{ tag: "h2", children: ["Title 2"] },
|
|
42
|
-
{ tag: "h3", children: ["Title 3"] },
|
|
43
|
-
{ tag: "h4", children: ["Title 4"] },
|
|
44
|
-
{ tag: "h5", children: ["Title 5"] },
|
|
45
|
-
{ tag: "h6", children: ["Title 6"] },
|
|
46
|
-
{
|
|
47
|
-
tag: "p",
|
|
48
|
-
children: [
|
|
49
|
-
{
|
|
50
|
-
tag: "span",
|
|
51
|
-
classes: ["bold", "italic", "line-through"],
|
|
52
|
-
children: ["Formatted span"],
|
|
53
|
-
},
|
|
54
|
-
],
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
tag: "ul",
|
|
58
|
-
children: [
|
|
59
|
-
{
|
|
60
|
-
tag: "li",
|
|
61
|
-
children: [
|
|
62
|
-
{
|
|
63
|
-
tag: "ol",
|
|
64
|
-
dir: "rtl",
|
|
65
|
-
children: [
|
|
66
|
-
{
|
|
67
|
-
tag: "li",
|
|
68
|
-
children: [
|
|
69
|
-
{
|
|
70
|
-
tag: "span",
|
|
71
|
-
classes: ["italic"],
|
|
72
|
-
children: ["number 1.1"],
|
|
73
|
-
},
|
|
74
|
-
],
|
|
75
|
-
},
|
|
76
|
-
{ tag: "li", children: ["number 1.2"] },
|
|
77
|
-
],
|
|
78
|
-
},
|
|
79
|
-
],
|
|
80
|
-
},
|
|
81
|
-
],
|
|
82
|
-
},
|
|
83
|
-
],
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
expect(fromLexical(toLexical(input)).node).toStrictEqual(input);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
// Uncertain whether Val RichText text nodes should allow nested spans - remove this test if that is not the case anymore
|
|
90
|
-
test("merged lexical text nodes to <-> from", () => {
|
|
91
|
-
const input: RichTextSource<AnyRichTextOptions> = {
|
|
92
|
-
_type: "richtext",
|
|
93
|
-
children: [
|
|
94
|
-
{
|
|
95
|
-
tag: "p",
|
|
96
|
-
children: [
|
|
97
|
-
{
|
|
98
|
-
tag: "span",
|
|
99
|
-
classes: ["bold", "line-through"],
|
|
100
|
-
children: [
|
|
101
|
-
{
|
|
102
|
-
tag: "span",
|
|
103
|
-
classes: ["italic"],
|
|
104
|
-
children: ["Formatted nested span"],
|
|
105
|
-
},
|
|
106
|
-
],
|
|
107
|
-
},
|
|
108
|
-
],
|
|
109
|
-
},
|
|
110
|
-
],
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
// See inline comments for what changed between input / output
|
|
114
|
-
const output: RichText<AnyRichTextOptions> = {
|
|
115
|
-
_type: "richtext",
|
|
116
|
-
children: [
|
|
117
|
-
{
|
|
118
|
-
tag: "p",
|
|
119
|
-
children: [
|
|
120
|
-
{
|
|
121
|
-
tag: "span",
|
|
122
|
-
classes: ["bold", "italic", "line-through"], // NOTE: classes was merged
|
|
123
|
-
children: ["Formatted nested span"],
|
|
124
|
-
},
|
|
125
|
-
],
|
|
126
|
-
},
|
|
127
|
-
],
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
expect(fromLexical(toLexical(input)).node).toStrictEqual(output);
|
|
131
|
-
});
|
|
132
|
-
});
|
|
@@ -1,389 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
AnyRichTextOptions,
|
|
3
|
-
HeadingNode as ValHeadingNode,
|
|
4
|
-
ListItemNode as ValListItemNode,
|
|
5
|
-
SpanNode as ValSpanNode,
|
|
6
|
-
UnorderedListNode as ValUnorderedListNode,
|
|
7
|
-
OrderedListNode as ValOrderedListNode,
|
|
8
|
-
ParagraphNode as ValParagraphNode,
|
|
9
|
-
RichTextSourceNode as ValRichTextSourceNode,
|
|
10
|
-
RichText,
|
|
11
|
-
RootNode,
|
|
12
|
-
VAL_EXTENSION,
|
|
13
|
-
Internal,
|
|
14
|
-
FILE_REF_PROP,
|
|
15
|
-
RichTextSource,
|
|
16
|
-
FileSource,
|
|
17
|
-
} from "@valbuild/core";
|
|
18
|
-
import { ImagePayload } from "./Nodes/ImageNode";
|
|
19
|
-
|
|
20
|
-
/// Serialized Lexical Nodes:
|
|
21
|
-
// TODO: replace with Lexical libs types - not currently exported?
|
|
22
|
-
|
|
23
|
-
type LexicalTextNode = CommonLexicalProps & {
|
|
24
|
-
type: "text";
|
|
25
|
-
text: string;
|
|
26
|
-
format: "" | number;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
type LexicalParagraphNode = CommonLexicalProps & {
|
|
30
|
-
type: "paragraph";
|
|
31
|
-
children: LexicalNode[];
|
|
32
|
-
};
|
|
33
|
-
type LexicalHeadingNode = CommonLexicalProps & {
|
|
34
|
-
type: "heading";
|
|
35
|
-
tag: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
|
|
36
|
-
children: LexicalNode[];
|
|
37
|
-
};
|
|
38
|
-
type LexicalListItemNode = CommonLexicalProps & {
|
|
39
|
-
type: "listitem";
|
|
40
|
-
children: LexicalNode[];
|
|
41
|
-
};
|
|
42
|
-
type LexicalListNode = CommonLexicalProps & {
|
|
43
|
-
type: "list";
|
|
44
|
-
listType: "bullet" | "number" | "checked";
|
|
45
|
-
direction: "ltr" | "rtl" | null;
|
|
46
|
-
children: LexicalNode[];
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
type LexicalImageNode = CommonLexicalProps & {
|
|
50
|
-
type: "image";
|
|
51
|
-
} & ImagePayload;
|
|
52
|
-
|
|
53
|
-
type LexicalNode =
|
|
54
|
-
| LexicalTextNode
|
|
55
|
-
| LexicalParagraphNode
|
|
56
|
-
| LexicalHeadingNode
|
|
57
|
-
| LexicalListItemNode
|
|
58
|
-
| LexicalListNode
|
|
59
|
-
| LexicalImageNode;
|
|
60
|
-
|
|
61
|
-
export type LexicalRootNode = {
|
|
62
|
-
type: "root";
|
|
63
|
-
children: LexicalNode[];
|
|
64
|
-
version: 1;
|
|
65
|
-
format: "left" | "start" | "center" | "right" | "end" | "justify" | "";
|
|
66
|
-
direction: null | "ltr" | "rtl";
|
|
67
|
-
} & CommonLexicalProps;
|
|
68
|
-
|
|
69
|
-
const COMMON_LEXICAL_PROPS = {
|
|
70
|
-
version: 1,
|
|
71
|
-
format: "" as number | "",
|
|
72
|
-
indent: 0,
|
|
73
|
-
direction: null as null | "ltr" | "rtl",
|
|
74
|
-
} as const;
|
|
75
|
-
|
|
76
|
-
type CommonLexicalProps = typeof COMMON_LEXICAL_PROPS;
|
|
77
|
-
|
|
78
|
-
export function toLexicalNode(
|
|
79
|
-
node: ValRichTextSourceNode<AnyRichTextOptions>
|
|
80
|
-
): LexicalNode {
|
|
81
|
-
if (typeof node === "string") {
|
|
82
|
-
return {
|
|
83
|
-
...COMMON_LEXICAL_PROPS,
|
|
84
|
-
type: "text",
|
|
85
|
-
format: "",
|
|
86
|
-
text: node,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
if ("tag" in node) {
|
|
90
|
-
switch (node.tag) {
|
|
91
|
-
case "h1":
|
|
92
|
-
return toLexicalHeadingNode(node);
|
|
93
|
-
case "h2":
|
|
94
|
-
return toLexicalHeadingNode(node);
|
|
95
|
-
case "h3":
|
|
96
|
-
return toLexicalHeadingNode(node);
|
|
97
|
-
case "h4":
|
|
98
|
-
return toLexicalHeadingNode(node);
|
|
99
|
-
case "h5":
|
|
100
|
-
return toLexicalHeadingNode(node);
|
|
101
|
-
case "h6":
|
|
102
|
-
return toLexicalHeadingNode(node);
|
|
103
|
-
case "li":
|
|
104
|
-
return toLexicalListItemNode(node);
|
|
105
|
-
case "p":
|
|
106
|
-
return toLexicalParagraphNode(node);
|
|
107
|
-
case "ul":
|
|
108
|
-
return toLexicalListNode(node);
|
|
109
|
-
case "ol":
|
|
110
|
-
return toLexicalListNode(node);
|
|
111
|
-
case "span":
|
|
112
|
-
return toLexicalTextNode(node);
|
|
113
|
-
default:
|
|
114
|
-
throw Error("Unexpected node tag: " + JSON.stringify(node, null, 2));
|
|
115
|
-
}
|
|
116
|
-
} else if (VAL_EXTENSION in node) {
|
|
117
|
-
switch (node[VAL_EXTENSION]) {
|
|
118
|
-
case "file":
|
|
119
|
-
return toLexicalImageNode(node);
|
|
120
|
-
default:
|
|
121
|
-
throw Error(
|
|
122
|
-
"Unexpected val extension: " + JSON.stringify(node, null, 2)
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
} else {
|
|
126
|
-
throw Error("Unexpected node: " + JSON.stringify(node, null, 2));
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function toLexicalImageNode(
|
|
131
|
-
node: FileSource<{ width: number; height: number; sha256: string }>
|
|
132
|
-
): LexicalImageNode {
|
|
133
|
-
const url = Internal.convertFileSource(node).url;
|
|
134
|
-
const fileExt = getFileExtFromUrl(url); // TODO: add file extension to metadata and use this only as fallback
|
|
135
|
-
return {
|
|
136
|
-
...COMMON_LEXICAL_PROPS,
|
|
137
|
-
type: "image",
|
|
138
|
-
src: url,
|
|
139
|
-
width: node.metadata?.width,
|
|
140
|
-
height: node.metadata?.height,
|
|
141
|
-
sha256: node.metadata?.sha256,
|
|
142
|
-
fileExt,
|
|
143
|
-
// TODO: altText
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const URL_FILE_EXT_REGEX = /.*\/(.*)\?/;
|
|
148
|
-
function getFileExtFromUrl(url: string): string | undefined {
|
|
149
|
-
const match = url.match(URL_FILE_EXT_REGEX);
|
|
150
|
-
if (match) {
|
|
151
|
-
const fileExtension = match[1].split(".").slice(-1)[0];
|
|
152
|
-
return fileExtension;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export function toLexical(
|
|
157
|
-
richtext: RichTextSource<AnyRichTextOptions>
|
|
158
|
-
): LexicalRootNode {
|
|
159
|
-
return {
|
|
160
|
-
...COMMON_LEXICAL_PROPS,
|
|
161
|
-
format: "",
|
|
162
|
-
type: "root",
|
|
163
|
-
children: richtext.children.map(toLexicalNode),
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function toLexicalHeadingNode(
|
|
168
|
-
heading: ValHeadingNode<AnyRichTextOptions>
|
|
169
|
-
): LexicalHeadingNode {
|
|
170
|
-
return {
|
|
171
|
-
...COMMON_LEXICAL_PROPS,
|
|
172
|
-
type: "heading",
|
|
173
|
-
tag: heading.tag,
|
|
174
|
-
children: heading.children.map(toLexicalNode),
|
|
175
|
-
};
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function toLexicalParagraphNode(
|
|
179
|
-
paragraph: ValParagraphNode<AnyRichTextOptions>
|
|
180
|
-
): LexicalParagraphNode {
|
|
181
|
-
return {
|
|
182
|
-
...COMMON_LEXICAL_PROPS,
|
|
183
|
-
type: "paragraph",
|
|
184
|
-
children: paragraph.children.map(toLexicalNode),
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function toLexicalListItemNode(
|
|
189
|
-
listItem: ValListItemNode<AnyRichTextOptions>
|
|
190
|
-
): LexicalListItemNode {
|
|
191
|
-
return {
|
|
192
|
-
...COMMON_LEXICAL_PROPS,
|
|
193
|
-
type: "listitem",
|
|
194
|
-
children: listItem.children.map(toLexicalNode),
|
|
195
|
-
};
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function toLexicalListNode(
|
|
199
|
-
list:
|
|
200
|
-
| ValUnorderedListNode<AnyRichTextOptions>
|
|
201
|
-
| ValOrderedListNode<AnyRichTextOptions>
|
|
202
|
-
): LexicalListNode {
|
|
203
|
-
return {
|
|
204
|
-
...COMMON_LEXICAL_PROPS,
|
|
205
|
-
type: "list",
|
|
206
|
-
listType: list.tag === "ol" ? "number" : "bullet",
|
|
207
|
-
children: list.children.map(toLexicalNode),
|
|
208
|
-
...(list.dir ? { direction: list.dir } : { direction: null }),
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const FORMAT_MAPPING = {
|
|
213
|
-
bold: 1, // 0001
|
|
214
|
-
italic: 2, // 0010
|
|
215
|
-
"line-through": 4, // 0100
|
|
216
|
-
// underline: 8, // 1000
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
export function toLexicalFormat(
|
|
220
|
-
classes: (keyof typeof FORMAT_MAPPING)[]
|
|
221
|
-
): number {
|
|
222
|
-
return classes.reduce(
|
|
223
|
-
(prev, curr) => prev | /* bitwise or */ FORMAT_MAPPING[curr],
|
|
224
|
-
0
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
export function fromLexicalFormat(
|
|
229
|
-
format: number
|
|
230
|
-
): (keyof typeof FORMAT_MAPPING)[] {
|
|
231
|
-
return Object.entries(FORMAT_MAPPING).flatMap(([key, value]) => {
|
|
232
|
-
if ((value & /* bitwise and */ format) === value) {
|
|
233
|
-
return [key as keyof typeof FORMAT_MAPPING];
|
|
234
|
-
}
|
|
235
|
-
return [];
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function toLexicalTextNode(
|
|
240
|
-
spanNode: ValSpanNode<AnyRichTextOptions>
|
|
241
|
-
): LexicalTextNode {
|
|
242
|
-
const child = spanNode.children[0];
|
|
243
|
-
if (typeof child === "string") {
|
|
244
|
-
return {
|
|
245
|
-
...COMMON_LEXICAL_PROPS,
|
|
246
|
-
type: "text",
|
|
247
|
-
text: child,
|
|
248
|
-
format: toLexicalFormat(spanNode.classes),
|
|
249
|
-
};
|
|
250
|
-
} else {
|
|
251
|
-
// recurse the spans and merge their classes
|
|
252
|
-
return toLexicalTextNode({
|
|
253
|
-
...child,
|
|
254
|
-
classes: spanNode.classes.concat(child.classes),
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
export function fromLexical(node: LexicalRootNode): {
|
|
260
|
-
node: RichText<AnyRichTextOptions>;
|
|
261
|
-
files: Record<string, string>;
|
|
262
|
-
} {
|
|
263
|
-
const files = {};
|
|
264
|
-
return {
|
|
265
|
-
node: {
|
|
266
|
-
_type: "richtext",
|
|
267
|
-
children: node.children.map((node) =>
|
|
268
|
-
fromLexicalNode(node, files)
|
|
269
|
-
) as RootNode<AnyRichTextOptions>[], // TODO: validate
|
|
270
|
-
},
|
|
271
|
-
files,
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
export function fromLexicalNode(
|
|
276
|
-
node: LexicalNode,
|
|
277
|
-
files: Record<string, string>
|
|
278
|
-
) {
|
|
279
|
-
switch (node.type) {
|
|
280
|
-
case "heading":
|
|
281
|
-
return fromLexicalHeadingNode(node, files);
|
|
282
|
-
case "paragraph":
|
|
283
|
-
return fromLexicalParagraphNode(node, files);
|
|
284
|
-
case "text":
|
|
285
|
-
return fromLexicalTextNode(node);
|
|
286
|
-
case "list":
|
|
287
|
-
return fromLexicalListNode(node, files);
|
|
288
|
-
case "listitem":
|
|
289
|
-
return fromLexicalListItemNode(node, files);
|
|
290
|
-
case "image":
|
|
291
|
-
return fromLexicalImageNode(node, files);
|
|
292
|
-
default:
|
|
293
|
-
throw Error(`Unknown lexical node: ${JSON.stringify(node)}`);
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function fromLexicalImageNode(
|
|
298
|
-
node: LexicalImageNode,
|
|
299
|
-
files: Record<string, string>
|
|
300
|
-
) {
|
|
301
|
-
if (node.src.startsWith("data:")) {
|
|
302
|
-
const filePath = `/public/${node.sha256}.${node.fileExt}`;
|
|
303
|
-
files[filePath] = node.src;
|
|
304
|
-
return {
|
|
305
|
-
[VAL_EXTENSION]: "file",
|
|
306
|
-
[FILE_REF_PROP]: filePath,
|
|
307
|
-
metadata: {
|
|
308
|
-
width: node.width,
|
|
309
|
-
height: node.width,
|
|
310
|
-
sha256: node.sha256,
|
|
311
|
-
},
|
|
312
|
-
};
|
|
313
|
-
} else {
|
|
314
|
-
return {
|
|
315
|
-
[VAL_EXTENSION]: "file",
|
|
316
|
-
[FILE_REF_PROP]: `/public${node.src.split("?")[0]}`,
|
|
317
|
-
metadata: {
|
|
318
|
-
width: node.width,
|
|
319
|
-
height: node.width,
|
|
320
|
-
sha256: node.sha256,
|
|
321
|
-
},
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
function fromLexicalTextNode(
|
|
327
|
-
textNode: LexicalTextNode
|
|
328
|
-
): ValSpanNode<AnyRichTextOptions> | string {
|
|
329
|
-
if (textNode.format === "" || textNode.format === 0) {
|
|
330
|
-
return textNode.text;
|
|
331
|
-
}
|
|
332
|
-
return {
|
|
333
|
-
tag: "span",
|
|
334
|
-
classes: fromLexicalFormat(textNode.format),
|
|
335
|
-
children: [textNode.text],
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function fromLexicalHeadingNode(
|
|
340
|
-
headingNode: LexicalHeadingNode,
|
|
341
|
-
files: Record<string, string>
|
|
342
|
-
): ValHeadingNode<AnyRichTextOptions> {
|
|
343
|
-
return {
|
|
344
|
-
tag: headingNode.tag,
|
|
345
|
-
children: headingNode.children.map((node) =>
|
|
346
|
-
fromLexicalNode(node, files)
|
|
347
|
-
) as ValHeadingNode<AnyRichTextOptions>["children"], // TODO: validate children
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function fromLexicalParagraphNode(
|
|
352
|
-
paragraphNode: LexicalParagraphNode,
|
|
353
|
-
files: Record<string, string>
|
|
354
|
-
): ValParagraphNode<AnyRichTextOptions> {
|
|
355
|
-
return {
|
|
356
|
-
tag: "p",
|
|
357
|
-
children: paragraphNode.children.map((node) =>
|
|
358
|
-
fromLexicalNode(node, files)
|
|
359
|
-
) as ValParagraphNode<AnyRichTextOptions>["children"], // TODO: validate children
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
function fromLexicalListNode(
|
|
364
|
-
listNode: LexicalListNode,
|
|
365
|
-
files: Record<string, string>
|
|
366
|
-
):
|
|
367
|
-
| ValOrderedListNode<AnyRichTextOptions>
|
|
368
|
-
| ValUnorderedListNode<AnyRichTextOptions> {
|
|
369
|
-
return {
|
|
370
|
-
...(listNode.direction ? { dir: listNode.direction } : {}),
|
|
371
|
-
tag: listNode.listType === "number" ? "ol" : "ul",
|
|
372
|
-
children: listNode.children.map((node) => fromLexicalNode(node, files)) as (
|
|
373
|
-
| ValOrderedListNode<AnyRichTextOptions>
|
|
374
|
-
| ValUnorderedListNode<AnyRichTextOptions>
|
|
375
|
-
)["children"], // TODO: validate children
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function fromLexicalListItemNode(
|
|
380
|
-
listItemNode: LexicalListItemNode,
|
|
381
|
-
files: Record<string, string>
|
|
382
|
-
): ValListItemNode<AnyRichTextOptions> {
|
|
383
|
-
return {
|
|
384
|
-
tag: "li",
|
|
385
|
-
children: listItemNode.children.map((node) =>
|
|
386
|
-
fromLexicalNode(node, files)
|
|
387
|
-
) as ValListItemNode<AnyRichTextOptions>["children"],
|
|
388
|
-
};
|
|
389
|
-
}
|