@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.
@@ -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
- children: [
23
- { tag: "h1", children: ["Title 1"] },
24
- { tag: "h2", children: ["Title 2"] },
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
+ }
@@ -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
- }