@valbuild/react 0.17.0 → 0.19.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.
@@ -1,177 +1,193 @@
1
+ /* eslint-disable @typescript-eslint/ban-types */
1
2
  import {
2
- HeadingNode,
3
- ListItemNode,
4
- ListNode,
5
- ParagraphNode,
6
3
  RichText,
4
+ RichTextNode,
5
+ AnyRichTextOptions,
7
6
  SourcePath,
8
- TextNode,
7
+ RichTextOptions,
9
8
  } from "@valbuild/core";
10
- import { createElement } from "react";
11
- import parse from "style-to-object";
9
+ import React from "react";
12
10
 
13
- export function ValRichText({ children }: { children: RichText }) {
14
- const root: RichText = children;
15
- const path = root.valPath;
16
- return (
17
- <div data-val-path={path}>
18
- {root.children.map((child, i) => {
19
- const childType = child.type;
20
- const childPath = `${path}.${i}` as SourcePath;
21
- switch (childType) {
22
- case "heading":
23
- return (
24
- <HeadingNodeComponent
25
- key={childPath}
26
- path={childPath}
27
- node={child}
28
- />
29
- );
30
- case "paragraph":
31
- return (
32
- <ParagraphNodeComponent
33
- key={childPath}
34
- path={childPath}
35
- node={child}
36
- />
37
- );
38
- case "list":
39
- return (
40
- <ListNodeComponent
41
- key={childPath}
42
- path={childPath}
43
- node={child}
44
- />
45
- );
46
- default:
47
- throw Error("Unknown root node type: " + childType);
48
- }
49
- })}
50
- </div>
51
- );
52
- }
11
+ // Pick is used to make sure we do not add a tag or class that is not in options:
12
+ type Tags = keyof Pick<RichTextOptions, "img" | "ul" | "ol">;
13
+ type Classes = keyof Pick<RichTextOptions, "bold" | "italic" | "lineThrough">;
53
14
 
54
- function TextNodeComponent({ node }: { node: TextNode }) {
55
- const styleProps = node.style ? parse(node.style) ?? {} : {};
56
- // TODO: Ugly! We should do this before serializing instead
57
- if (styleProps["font-family"]) {
58
- styleProps["fontFamily"] = styleProps["font-family"];
59
- delete styleProps["font-family"];
60
- }
61
- if (styleProps["font-size"]) {
62
- styleProps["fontSize"] = styleProps["font-size"];
63
- delete styleProps["font-size"];
64
- }
65
- const bitmask = node.format?.toString(2);
66
- const bitmaskOffset = bitmask ? bitmask.length - 1 : 0;
67
- function isBitOne(bit: number) {
68
- if (!bitmask) {
69
- return false;
70
- }
71
- return (
72
- bitmask.length >= bitmaskOffset - bit &&
73
- bitmask[bitmaskOffset - bit] === "1"
74
- );
75
- }
76
- if (isBitOne(0)) {
77
- styleProps["fontWeight"] = "bold";
78
- }
79
- if (isBitOne(1)) {
80
- styleProps["fontStyle"] = "italic";
81
- }
82
- if (isBitOne(2)) {
83
- if (!styleProps["textDecoration"]) {
84
- styleProps["textDecoration"] = "line-through";
85
- } else {
86
- styleProps["textDecoration"] += " line-through";
15
+ type ThemeOptions<O extends RichTextOptions> = {
16
+ tags: (O["headings"] extends Array<unknown>
17
+ ? {
18
+ [Key in O["headings"][number]]: string;
19
+ }
20
+ : {}) & {
21
+ [Key in Tags & keyof O as O[Key] extends true
22
+ ? Key extends "ul" | "ol"
23
+ ? Key | "li"
24
+ : Key
25
+ : never]: string;
26
+ } & { p?: string };
27
+ classes: {
28
+ [Key in Classes & keyof O as O[Key] extends true ? Key : never]: string;
29
+ } & (O["img"] extends true ? { imgContainer?: string } : {});
30
+ };
31
+
32
+ export function ValRichText<O extends RichTextOptions>({
33
+ theme,
34
+ children,
35
+ }: {
36
+ theme: ThemeOptions<O>;
37
+ children: RichText<O>;
38
+ }) {
39
+ const root = children as RichText<AnyRichTextOptions> & {
40
+ valPath: SourcePath;
41
+ };
42
+ function withRenderTag(
43
+ clazz: keyof ThemeOptions<AnyRichTextOptions>["tags"],
44
+ current?: string
45
+ ) {
46
+ const renderClass = (theme as ThemeOptions<AnyRichTextOptions>).tags[clazz];
47
+ if (renderClass && current) {
48
+ return [current, renderClass].join(" ");
49
+ }
50
+ if (renderClass) {
51
+ return renderClass;
87
52
  }
53
+ return current;
88
54
  }
89
- if (isBitOne(3)) {
90
- if (!styleProps["textDecoration"]) {
91
- styleProps["textDecoration"] = "underline";
92
- } else {
93
- styleProps["textDecoration"] += " underline";
55
+ function withRenderClass(
56
+ clazz: keyof ThemeOptions<AnyRichTextOptions>["classes"],
57
+ current?: string
58
+ ) {
59
+ const renderClass = (theme as ThemeOptions<AnyRichTextOptions>).classes[
60
+ clazz
61
+ ];
62
+ if (renderClass && current) {
63
+ return [current, renderClass].join(" ");
94
64
  }
65
+ if (renderClass) {
66
+ return renderClass;
67
+ }
68
+ return current;
95
69
  }
96
- return <span style={styleProps}>{node.text}</span>;
97
- }
98
-
99
- function HeadingNodeComponent({
100
- node,
101
- path,
102
- }: {
103
- path: SourcePath;
104
- node: HeadingNode;
105
- }) {
106
- return createElement(
107
- node.tag,
108
- {},
109
- node.children.map((child, i) => {
110
- const childPath = `${path}.${i}` as SourcePath;
111
- return <TextNodeComponent key={childPath} node={child} />;
112
- })
113
- );
114
- }
115
70
 
116
- function ParagraphNodeComponent({
117
- node,
118
- path,
119
- }: {
120
- path: SourcePath;
121
- node: ParagraphNode;
122
- }) {
123
- return (
124
- <p>
125
- {node.children.map((child, i) => {
126
- const childPath = `${path}.${i}` as SourcePath;
127
- switch (child.type) {
128
- case "text":
129
- return <TextNodeComponent key={childPath} node={child} />;
130
- default:
131
- throw Error("Unknown paragraph node type: " + child?.type);
132
- }
133
- })}
134
- </p>
135
- );
136
- }
137
-
138
- function ListNodeComponent({
139
- node,
140
- path,
141
- }: {
142
- path: SourcePath;
143
- node: ListNode;
144
- }) {
145
- return createElement(
146
- node.tag,
147
- {},
148
- node.children.map((child, i) => {
149
- const childPath = `${path}.${i}` as SourcePath;
71
+ function toReact(
72
+ node: RichTextNode<AnyRichTextOptions>,
73
+ key: number | string
74
+ ): React.ReactNode {
75
+ if (typeof node === "string") {
76
+ return node;
77
+ }
78
+ if (node.tag === "p") {
150
79
  return (
151
- <ListItemComponent key={childPath} path={childPath} node={child} />
80
+ <p className={withRenderTag("p")} key={key}>
81
+ {node.children.map(toReact)}
82
+ </p>
152
83
  );
153
- })
154
- );
155
- }
84
+ }
85
+ if (node.tag === "img") {
86
+ return (
87
+ <div className={withRenderClass("imgContainer")} key={key}>
88
+ <img className={withRenderTag("img")} src={node.src} />
89
+ </div>
90
+ );
91
+ }
92
+ // if (node.tag === "blockquote") {
93
+ // return <blockquote key={key}>{node.children.map(toReact)}</blockquote>;
94
+ // }
95
+ if (node.tag === "ul") {
96
+ return (
97
+ <ul className={withRenderTag("ul")} key={key}>
98
+ {node.children.map(toReact)}
99
+ </ul>
100
+ );
101
+ }
102
+ if (node.tag === "ol") {
103
+ return (
104
+ <ol className={withRenderTag("ol")} key={key}>
105
+ {node.children.map(toReact)}
106
+ </ol>
107
+ );
108
+ }
109
+ if (node.tag === "li") {
110
+ return (
111
+ <li className={withRenderTag("li")} key={key}>
112
+ {node.children.map(toReact)}
113
+ </li>
114
+ );
115
+ }
116
+ if (node.tag === "span") {
117
+ return (
118
+ <span
119
+ key={key}
120
+ className={node.classes
121
+ .map((nodeClass) => {
122
+ switch (nodeClass) {
123
+ case "bold":
124
+ return withRenderClass("bold");
125
+ case "line-through":
126
+ return withRenderClass("lineThrough");
127
+ case "italic":
128
+ return withRenderClass("italic");
129
+ }
130
+ })
131
+ .join(" ")}
132
+ >
133
+ {node.children.map(toReact)}
134
+ </span>
135
+ );
136
+ }
137
+ if (node.tag === "h1") {
138
+ return (
139
+ <h1 className={withRenderTag("h1")} key={key}>
140
+ {node.children.map(toReact)}
141
+ </h1>
142
+ );
143
+ }
144
+ if (node.tag === "h2") {
145
+ return (
146
+ <h2 className={withRenderTag("h2")} key={key}>
147
+ {node.children.map(toReact)}
148
+ </h2>
149
+ );
150
+ }
151
+ if (node.tag === "h3") {
152
+ return (
153
+ <h3 className={withRenderTag("h3")} key={key}>
154
+ {node.children.map(toReact)}
155
+ </h3>
156
+ );
157
+ }
158
+ if (node.tag === "h4") {
159
+ return (
160
+ <h4 className={withRenderTag("h4")} key={key}>
161
+ {node.children.map(toReact)}
162
+ </h4>
163
+ );
164
+ }
165
+ if (node.tag === "h5") {
166
+ return (
167
+ <h5 className={withRenderTag("h5")} key={key}>
168
+ {node.children.map(toReact)}
169
+ </h5>
170
+ );
171
+ }
172
+ if (node.tag === "h6") {
173
+ return (
174
+ <h6 className={withRenderTag("h6")} key={key}>
175
+ {node.children.map(toReact)}
176
+ </h6>
177
+ );
178
+ }
179
+ console.error("Unknown tag", node.tag);
180
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
181
+ const anyNode = node as any;
182
+ if (!anyNode?.tag) {
183
+ return null;
184
+ }
185
+ return React.createElement(anyNode.tag, {
186
+ key,
187
+ className: anyNode.class?.join(" "),
188
+ children: anyNode.children?.map(toReact),
189
+ });
190
+ }
156
191
 
157
- function ListItemComponent({
158
- node,
159
- path,
160
- }: {
161
- path: SourcePath;
162
- node: ListItemNode;
163
- }) {
164
- return (
165
- <li>
166
- {node.children.map((child, i) => {
167
- const childPath = `${path}.${i}` as SourcePath;
168
- switch (child.type) {
169
- case "text":
170
- return <TextNodeComponent key={childPath} node={child} />;
171
- default:
172
- throw Error("Unknown list item node type: " + child?.type);
173
- }
174
- })}
175
- </li>
176
- );
192
+ return <div data-val-path={root.valPath}>{root.children.map(toReact)}</div>;
177
193
  }
package/src/ValUI.tsx CHANGED
@@ -44,7 +44,14 @@ export default function ValUI() {
44
44
  }
45
45
  return (
46
46
  <>
47
- <ShadowRoot>
47
+ <ShadowRoot
48
+ style={{
49
+ position: "absolute",
50
+ top: 0,
51
+ left: 0,
52
+ zIndex: 8999, // 1 less than the NextJS error z-index: 9000
53
+ }}
54
+ >
48
55
  {/* TODO: */}
49
56
  <link rel="preconnect" href="https://fonts.googleapis.com" />
50
57
  <link
@@ -15,8 +15,7 @@ const isIntrinsicElement = (type: any) => {
15
15
 
16
16
  const addValPathIfFound = (type: any, props: any) => {
17
17
  const valSources: any = [];
18
-
19
- if (isIntrinsicElement(type) && props && typeof props === "object") {
18
+ if (props && typeof props === "object") {
20
19
  for (const [key, value] of Object.entries(props)) {
21
20
  if (typeof value === "string" && value.match(VERCEL_STEGA_REGEX)) {
22
21
  const encodedBits = vercelStegaDecode(value);
@@ -31,7 +30,9 @@ const addValPathIfFound = (type: any, props: any) => {
31
30
  const valPath = encodedBits?.data?.valPath;
32
31
  if (valPath) {
33
32
  valSources.push(valPath);
34
- props[key] = vercelStegaSplit(value).cleaned;
33
+ props[key] = isIntrinsicElement(type)
34
+ ? vercelStegaSplit(value).cleaned
35
+ : value;
35
36
  props[`data-val-attr-${key}`] = valPath;
36
37
  }
37
38
  }
@@ -1,8 +1,8 @@
1
1
  // NOTE: the exports of this file needs to be kept in sync with ValQuickJSRuntime
2
2
  export { autoTagJSX } from "./autoTagJSX";
3
3
  export {
4
- transform,
4
+ stegaEncode,
5
5
  getModuleIds,
6
6
  type ValEncodedString,
7
7
  type StegaOfSource,
8
- } from "./transform";
8
+ } from "./stegaEncode";
@@ -1,4 +1,4 @@
1
- import { getModuleIds, transform } from "./transform";
1
+ import { getModuleIds, stegaEncode } from "./stegaEncode";
2
2
  import { initVal } from "@valbuild/core";
3
3
  import { vercelStegaDecode, vercelStegaSplit } from "@vercel/stega";
4
4
 
@@ -9,7 +9,7 @@ describe("stega transform", () => {
9
9
  const schema = s.array(
10
10
  s.object({
11
11
  image: s.image(),
12
- text: s.richtext(),
12
+ text: s.richtext({}),
13
13
  n: s.number(),
14
14
  b: s.boolean(),
15
15
  })
@@ -22,7 +22,7 @@ describe("stega transform", () => {
22
22
  width: 100,
23
23
  height: 100,
24
24
  }),
25
- text: val.richtext("Test1"),
25
+ text: val.richtext`Test`,
26
26
  n: 1,
27
27
  b: true,
28
28
  },
@@ -32,12 +32,12 @@ describe("stega transform", () => {
32
32
  width: 100,
33
33
  height: 100,
34
34
  }),
35
- text: val.richtext("Test2"),
35
+ text: val.richtext`Test`,
36
36
  n: 2,
37
37
  b: false,
38
38
  },
39
39
  ]);
40
- const transformed = transform(valModule, {});
40
+ const transformed = stegaEncode(valModule, {});
41
41
 
42
42
  expect(transformed).toHaveLength(2);
43
43
 
@@ -81,7 +81,7 @@ describe("stega transform", () => {
81
81
 
82
82
  test("basic transform with get modules", () => {
83
83
  const schema = s.array(s.string());
84
- const transformed = transform(
84
+ const transformed = stegaEncode(
85
85
  val.content("/test1", schema, ["one", "two"]),
86
86
  {
87
87
  getModule: (moduleId) => {
@@ -101,9 +101,18 @@ describe("stega transform", () => {
101
101
  });
102
102
  });
103
103
 
104
+ test("Dont stegaEncode raw strings schema", () => {
105
+ const schema = s.object({ str: s.string(), rawStr: s.string().raw() });
106
+ const transformed = stegaEncode(
107
+ val.content("/test1", schema, { str: "one", rawStr: "two" }),
108
+ {}
109
+ );
110
+ //expect(transformed.str).toStrictEqual("one");
111
+ expect(transformed.rawStr).toStrictEqual("two");
112
+ });
104
113
  test("transform with get modules", () => {
105
114
  const schema = s.array(s.string());
106
- const transformed = transform(
115
+ const transformed = stegaEncode(
107
116
  {
108
117
  foo: [
109
118
  { test: val.content("/test1", schema, ["one", "two"]) },
@@ -28,8 +28,8 @@ export type ValEncodedString = string & {
28
28
 
29
29
  export type StegaOfSource<T extends Source> = Json extends T
30
30
  ? Json
31
- : T extends RichTextSource
32
- ? RichText
31
+ : T extends RichTextSource<infer O>
32
+ ? RichText<O>
33
33
  : T extends FileSource
34
34
  ? { url: ValEncodedString }
35
35
  : T extends SourceObject
@@ -44,34 +44,41 @@ export type StegaOfSource<T extends Source> = Json extends T
44
44
  ? T
45
45
  : never;
46
46
 
47
- export function transform(
47
+ export function stegaEncode(
48
48
  input: any,
49
49
  opts: {
50
50
  getModule?: (moduleId: string) => any;
51
51
  disabled?: boolean;
52
52
  }
53
53
  ): any {
54
- function rec(sourceOrSelector: any, path?: any): any {
54
+ function rec(
55
+ sourceOrSelector: any,
56
+ recOpts?: { path: any; schema: any }
57
+ ): any {
55
58
  if (typeof sourceOrSelector === "object") {
59
+ if (!sourceOrSelector) {
60
+ return null;
61
+ }
56
62
  const selectorPath = Internal.getValPath(sourceOrSelector);
57
63
  if (selectorPath) {
64
+ const newSchema = Internal.getSchema(sourceOrSelector);
58
65
  return rec(
59
66
  (opts.getModule && opts.getModule(selectorPath)) ||
60
67
  Internal.getSource(sourceOrSelector),
61
- opts.disabled ? undefined : selectorPath
68
+ opts.disabled ? undefined : { path: selectorPath, schema: newSchema }
62
69
  );
63
70
  }
64
71
 
65
- if (!sourceOrSelector) {
66
- return null;
67
- }
68
-
69
72
  if (VAL_EXTENSION in sourceOrSelector) {
70
73
  if (sourceOrSelector[VAL_EXTENSION] === "richtext") {
71
- return {
72
- ...sourceOrSelector,
73
- valPath: path,
74
- };
74
+ if (recOpts?.path) {
75
+ return {
76
+ ...Internal.convertRichTextSource(sourceOrSelector),
77
+ valPath: recOpts.path,
78
+ };
79
+ }
80
+
81
+ return Internal.convertRichTextSource(sourceOrSelector);
75
82
  }
76
83
 
77
84
  if (
@@ -81,7 +88,7 @@ export function transform(
81
88
  const fileSelector = Internal.convertFileSource(sourceOrSelector);
82
89
  return {
83
90
  ...fileSelector,
84
- url: rec(fileSelector.url, path),
91
+ url: rec(fileSelector.url, recOpts),
85
92
  };
86
93
  }
87
94
  console.error(
@@ -92,7 +99,13 @@ export function transform(
92
99
 
93
100
  if (Array.isArray(sourceOrSelector)) {
94
101
  return sourceOrSelector.map((el, i) =>
95
- rec(el, path && Internal.createValPathOfItem(path, i))
102
+ rec(
103
+ el,
104
+ recOpts && {
105
+ path: Internal.createValPathOfItem(recOpts.path, i),
106
+ schema: recOpts.schema.item,
107
+ }
108
+ )
96
109
  );
97
110
  }
98
111
 
@@ -101,7 +114,12 @@ export function transform(
101
114
  for (const [key, value] of Object.entries(sourceOrSelector)) {
102
115
  res[key] = rec(
103
116
  value,
104
- path && Internal.createValPathOfItem(path, key)
117
+ recOpts && {
118
+ path: Internal.createValPathOfItem(recOpts.path, key),
119
+ schema:
120
+ recOpts.schema.item || // Record
121
+ recOpts.schema.items[key], // Object
122
+ }
105
123
  );
106
124
  }
107
125
  return res;
@@ -117,11 +135,17 @@ export function transform(
117
135
  }
118
136
 
119
137
  if (typeof sourceOrSelector === "string") {
138
+ if (!recOpts) {
139
+ return sourceOrSelector;
140
+ }
141
+ if (recOpts.schema.isRaw) {
142
+ return sourceOrSelector;
143
+ }
120
144
  return vercelStegaCombine(
121
145
  sourceOrSelector,
122
146
  {
123
147
  origin: "val.build",
124
- data: { valPath: path },
148
+ data: { valPath: recOpts.path },
125
149
  },
126
150
  false // auto detection on urls and dates is disabled, isDate could be used but it is also disabled (users should use a date schema instead): isDate(sourceOrSelector) // skip = true if isDate
127
151
  );
@@ -146,16 +170,15 @@ export function getModuleIds(input: any): string[] {
146
170
  const modules: Set<string> = new Set();
147
171
  function rec(sourceOrSelector: any): undefined {
148
172
  if (typeof sourceOrSelector === "object") {
173
+ if (!sourceOrSelector) {
174
+ return;
175
+ }
149
176
  const selectorPath = Internal.getValPath(sourceOrSelector);
150
177
  if (selectorPath) {
151
178
  modules.add(selectorPath);
152
179
  return;
153
180
  }
154
181
 
155
- if (!sourceOrSelector) {
156
- return;
157
- }
158
-
159
182
  if (VAL_EXTENSION in sourceOrSelector) {
160
183
  if (sourceOrSelector[VAL_EXTENSION] === "richtext") {
161
184
  return;