@dom-expressions/tagged-jsx 0.50.0-next.15

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.
@@ -0,0 +1,260 @@
1
+ export const OPEN_TAG_TOKEN = 0;
2
+ export const CLOSE_TAG_TOKEN = 1;
3
+ export const SLASH_TOKEN = 2;
4
+ export const IDENTIFIER_TOKEN = 3;
5
+ export const EQUALS_TOKEN = 4;
6
+ export const STRING_TOKEN = 5;
7
+ export const TEXT_TOKEN = 6;
8
+ export const EXPRESSION_TOKEN = 7;
9
+ export const SPREAD_TOKEN = 8;
10
+
11
+ const isIdentifierChar = (code: number): boolean => {
12
+ return (
13
+ isIdentifierStart(code) ||
14
+ (code >= 48 && code <= 58) || // 0-9, :
15
+ code === 46 || // .
16
+ code === 45 // -
17
+ );
18
+ };
19
+
20
+ const isIdentifierStart = (code: number): boolean => {
21
+ return (
22
+ (code >= 65 && code <= 90) || // A-Z
23
+ (code >= 97 && code <= 122) || // a-z
24
+ code === 95 || // _
25
+ code === 36 // $
26
+ );
27
+ };
28
+
29
+ const isWhitespace = (code: number): boolean => {
30
+ return (code >= 9 && code <= 13) || code === 32; // \t \n \v \f \r space
31
+ };
32
+
33
+ export interface OpenTagToken {
34
+ type: typeof OPEN_TAG_TOKEN;
35
+ // value: "<";
36
+ }
37
+
38
+ export interface CloseTagToken {
39
+ type: typeof CLOSE_TAG_TOKEN;
40
+ // value: ">";
41
+ }
42
+
43
+ export interface SlashToken {
44
+ type: typeof SLASH_TOKEN;
45
+ // value: "/";
46
+ }
47
+
48
+ export interface IdentifierToken {
49
+ type: typeof IDENTIFIER_TOKEN;
50
+ value: string;
51
+ }
52
+
53
+ export interface EqualsToken {
54
+ type: typeof EQUALS_TOKEN;
55
+ // value: "=";
56
+ }
57
+
58
+ export interface StringToken {
59
+ type: typeof STRING_TOKEN;
60
+ value: string;
61
+ quote: "'" | '"';
62
+ }
63
+
64
+ export interface TextToken {
65
+ type: typeof TEXT_TOKEN;
66
+ value: string;
67
+ }
68
+
69
+ export interface SpreadToken {
70
+ type: typeof SPREAD_TOKEN;
71
+ // value: "..."
72
+ }
73
+
74
+ export interface ExpressionToken {
75
+ type: typeof EXPRESSION_TOKEN;
76
+ value: number;
77
+ }
78
+
79
+ export type Token =
80
+ | OpenTagToken
81
+ | CloseTagToken
82
+ | SlashToken
83
+ | IdentifierToken
84
+ | EqualsToken
85
+ | StringToken
86
+ | TextToken
87
+ | ExpressionToken
88
+ // | QuoteToken
89
+ | SpreadToken;
90
+
91
+ // Add a new state for elements that contain raw text only
92
+ const STATE_TEXT = 0;
93
+ const STATE_TAG = 1;
94
+ const STATE_RAW_TEXT = 2;
95
+ const STATE_COMMENT = 3;
96
+ const STATE_LINE_COMMENT = 4;
97
+ const STATE_BLOCK_COMMENT = 5;
98
+
99
+ export const tokenize = (
100
+ strings: TemplateStringsArray | string[],
101
+ rawTextElements: Set<string>
102
+ ): Token[] => {
103
+ const tokens: Token[] = [];
104
+ let state = STATE_TEXT;
105
+ let lastTagName = "";
106
+ let cursor = 0;
107
+
108
+ for (let i = 0; i < strings.length; i++) {
109
+ const str = strings[i];
110
+ const len = str.length;
111
+ cursor = 0;
112
+
113
+ while (cursor < len) {
114
+ switch (state) {
115
+ case STATE_TEXT: {
116
+ lastTagName = "";
117
+ const nextTag = str.indexOf("<", cursor);
118
+ if (nextTag === -1) {
119
+ if (cursor < len) tokens.push({ type: TEXT_TOKEN, value: str.slice(cursor) });
120
+ cursor = len;
121
+ } else {
122
+ if (nextTag > cursor)
123
+ tokens.push({
124
+ type: TEXT_TOKEN,
125
+ value: str.slice(cursor, nextTag)
126
+ });
127
+
128
+ if (str[nextTag + 1] === "!" && str[nextTag + 2] === "-" && str[nextTag + 3] === "-") {
129
+ state = STATE_COMMENT;
130
+ cursor = nextTag + 4;
131
+ } else {
132
+ tokens.push({ type: OPEN_TAG_TOKEN });
133
+ state = STATE_TAG;
134
+ cursor = nextTag + 1;
135
+ }
136
+ }
137
+ break;
138
+ }
139
+ case STATE_TAG: {
140
+ const code = str.charCodeAt(cursor);
141
+
142
+ if (isWhitespace(code)) {
143
+ cursor++;
144
+ } else if (code === 62) {
145
+ // ">"
146
+ if (
147
+ rawTextElements.has(lastTagName) &&
148
+ tokens[tokens.length - 2]?.type !== SLASH_TOKEN
149
+ ) {
150
+ state = STATE_RAW_TEXT;
151
+ } else {
152
+ state = STATE_TEXT;
153
+ lastTagName = "";
154
+ }
155
+ tokens.push({ type: CLOSE_TAG_TOKEN });
156
+
157
+ cursor++;
158
+ } else if (code === 61) {
159
+ // "="
160
+ tokens.push({ type: EQUALS_TOKEN });
161
+ cursor++;
162
+ } else if (code === 47) {
163
+ // "/"
164
+ const next = str.charCodeAt(cursor + 1);
165
+ const nextNonWhitespace = str.slice(cursor + 2).search(/\S/);
166
+ const isShorthandClosingTag =
167
+ next === 47 &&
168
+ tokens[tokens.length - 1]?.type === OPEN_TAG_TOKEN &&
169
+ nextNonWhitespace !== -1 &&
170
+ str[cursor + 2 + nextNonWhitespace] === ">";
171
+
172
+ if (next === 47 && !isShorthandClosingTag) {
173
+ state = STATE_LINE_COMMENT;
174
+ } else if (next === 42) {
175
+ state = STATE_BLOCK_COMMENT;
176
+ } else {
177
+ tokens.push({ type: SLASH_TOKEN });
178
+ cursor++;
179
+ }
180
+ } else if (code === 34 || code === 39) {
181
+ const char = str[cursor] as "'" | '"';
182
+ const endQuoteIndex = str.indexOf(char, cursor + 1);
183
+
184
+ if (endQuoteIndex === -1) {
185
+ throw new Error(`Unterminated string at ${i}:${cursor}`);
186
+ }
187
+ tokens.push({
188
+ type: STRING_TOKEN,
189
+ value: str.slice(cursor + 1, endQuoteIndex),
190
+ quote: char
191
+ });
192
+ cursor = endQuoteIndex + 1;
193
+ } else if (isIdentifierStart(code)) {
194
+ const start = cursor;
195
+ while (cursor < len && isIdentifierChar(str.charCodeAt(cursor))) cursor++;
196
+ const value = str.slice(start, cursor);
197
+ if (lastTagName === "") {
198
+ lastTagName = value;
199
+ }
200
+ tokens.push({ type: IDENTIFIER_TOKEN, value });
201
+ } else if (code === 46 && str[cursor + 1] === "." && str[cursor + 2] === ".") {
202
+ // "."
203
+ tokens.push({ type: SPREAD_TOKEN });
204
+ cursor += 3;
205
+ } else {
206
+ throw new Error(`Unexpected Character: ${str[cursor]} at ${i}:${cursor}`);
207
+ }
208
+ break;
209
+ }
210
+ case STATE_RAW_TEXT: {
211
+ // Case-sensitive search for the specific closing tag with optional whitespace in between, e.g. < / textarea >
212
+ const closeTagRegex = new RegExp(`<\\s*/\\s*${lastTagName}\\s*>`, "g");
213
+ closeTagRegex.lastIndex = cursor;
214
+ const match = closeTagRegex.exec(str);
215
+
216
+ if (match) {
217
+ const endOfRawIdx = match.index;
218
+ if (endOfRawIdx > cursor) {
219
+ tokens.push({
220
+ type: TEXT_TOKEN,
221
+ value: str.slice(cursor, endOfRawIdx)
222
+ });
223
+ }
224
+ state = STATE_TEXT;
225
+ cursor = endOfRawIdx;
226
+ lastTagName = "";
227
+ } else {
228
+ tokens.push({ type: TEXT_TOKEN, value: str.slice(cursor) });
229
+ cursor = len;
230
+ }
231
+ break;
232
+ }
233
+ case STATE_COMMENT:
234
+ case STATE_LINE_COMMENT:
235
+ case STATE_BLOCK_COMMENT: {
236
+ const commentEnd =
237
+ state === STATE_LINE_COMMENT ? "\n" : state === STATE_BLOCK_COMMENT ? "*/" : "-->";
238
+ const commentEndIndex = str.indexOf(commentEnd, cursor);
239
+
240
+ if (commentEndIndex === -1) {
241
+ // If we don't find the closer in this string chunk, consume the rest and stay in the comment.
242
+ cursor = len;
243
+ } else {
244
+ state = state === STATE_COMMENT ? STATE_TEXT : STATE_TAG;
245
+ cursor = commentEndIndex + commentEnd.length;
246
+ }
247
+ break;
248
+ }
249
+ }
250
+ }
251
+
252
+ if (i < strings.length - 1) {
253
+ if (state === STATE_TEXT || state === STATE_TAG || state === STATE_RAW_TEXT) {
254
+ tokens.push({ type: EXPRESSION_TOKEN, value: i });
255
+ }
256
+ }
257
+ }
258
+
259
+ return tokens;
260
+ };
package/src/types.ts ADDED
@@ -0,0 +1,69 @@
1
+ import { type JSX } from "../../runtime/src/jsx";
2
+
3
+ export type FunctionComponent = (...args: any[]) => any;
4
+ /**
5
+ * Component registry type
6
+ * @example
7
+ * ```tsx
8
+ * const components: ComponentRegistry = {
9
+ * MyComponent: (props) => <div>Hello {props.name}</div>
10
+ * }
11
+ * ```
12
+ */
13
+ export type ComponentRegistry = Record<string, FunctionComponent>;
14
+
15
+ /**
16
+ * Tagged JSX instance type
17
+ * @template T Component registry
18
+ */
19
+ export type TaggedJSXInstance<T extends ComponentRegistry> = {
20
+ /**
21
+ * Tagged JSX template function
22
+ * @example
23
+ * ```tsx
24
+ * const myTemplate = html`<div>Hello World</div>`
25
+ * ```
26
+ */
27
+ (strings: TemplateStringsArray, ...values: any[]): JSX.Element;
28
+
29
+ /**
30
+ * Self reference to tagged JSX instance for tooling
31
+ * @example
32
+ * ```tsx
33
+ * const MyComponent: FunctionComponent = (props) => {
34
+ * // Use html to create a template inside a component
35
+ * return myTaggedJSX.jsx`<div>Hello ${props.name}</div>`
36
+ * ```
37
+ */
38
+ jsx: TaggedJSXInstance<T>;
39
+
40
+ /**
41
+ * Create a new tagged JSX instance with additional components added to the registry
42
+ * @param components New components to add to the registry
43
+ * @example
44
+ * ```tsx
45
+ * const MyComponent: FunctionComponent = (props) => <div>Hello {props.name}</div>
46
+ * const myTaggedJSX = html.define({MyComponent})
47
+ * const myTemplate = myTaggedJSX`<MyComponent name="World" />`
48
+ * ```
49
+ */
50
+ define<TNew extends ComponentRegistry>(components: TNew): TaggedJSXInstance<T & TNew>;
51
+
52
+ /**
53
+ * Component registry
54
+ */
55
+ components: T;
56
+ };
57
+
58
+ type MountableElement = Element | Document | ShadowRoot | DocumentFragment | Node;
59
+
60
+ export interface Runtime {
61
+ insert(parent: MountableElement, accessor: any, marker?: Node | null, init?: any): any;
62
+ spread<T>(node: Element, accessor: (() => T) | T, skipChildren?: boolean): void;
63
+ createComponent(Comp: (props: any) => any, props: any): any;
64
+ mergeProps(...sources: unknown[]): any;
65
+ SVGElements: Set<string>;
66
+ VoidElements: Set<string>;
67
+ RawTextElements: Set<string>;
68
+ MathMLElements: Set<string>;
69
+ }
package/tests/core.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { untrack } from "@solidjs/signals";
2
+
3
+ export const sharedConfig = {};
4
+
5
+ export function createComponent(Comp: any, props: any) {
6
+ if (Comp.prototype && Comp.prototype.isClassComponent) {
7
+ return untrack(() => {
8
+ const comp = new Comp(props);
9
+ return comp.render(props);
10
+ });
11
+ }
12
+ return untrack(() => Comp(props));
13
+ }
14
+
15
+ export {
16
+ createRoot as root,
17
+ createRenderEffect as effect,
18
+ createMemo as memo,
19
+ getOwner,
20
+ runWithOwner,
21
+ untrack,
22
+ merge as mergeProps,
23
+ flatten,
24
+ flush
25
+ } from "@solidjs/signals";
26
+
27
+ export { RawTextElements, VoidElements } from "../../runtime/src/constants";