solid-tag-runtime 0.0.15 → 0.0.18
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/ARCHITECTURE.md +131 -4
- package/README.md +47 -3
- package/bench/observer.mjs +103 -0
- package/docs/api/html.md +33 -2
- package/docs/getting-started.md +18 -3
- package/docs/html-runtime.md +41 -5
- package/docs/lifecycle-events.md +6 -1
- package/docs/rendering.md +4 -0
- package/docs/solid-render.md +227 -29
- package/docs/solid-runtime-setup.md +1 -1
- package/examples/observer-strategies.js +19 -0
- package/examples/render.html +7 -2
- package/html.d.ts +29 -0
- package/package.json +5 -3
- package/src/html/data-literal.js +270 -0
- package/src/html.js +546 -97
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a safe JSON5-style data literal without evaluating JavaScript.
|
|
3
|
+
*
|
|
4
|
+
* Supported conveniences intentionally cover declarative HTML data:
|
|
5
|
+
* - single/double quoted strings
|
|
6
|
+
* - unquoted object keys
|
|
7
|
+
* - line/block comments
|
|
8
|
+
* - trailing commas
|
|
9
|
+
* - arrays/objects
|
|
10
|
+
* - booleans/null/numbers
|
|
11
|
+
*
|
|
12
|
+
* Identifiers are accepted only as object keys or the literals true/false/null.
|
|
13
|
+
* Calls, property access, functions, constructors, variables, and expressions
|
|
14
|
+
* are rejected because this parser never executes JavaScript.
|
|
15
|
+
*/
|
|
16
|
+
export function parseHTMLDataLiteral(source) {
|
|
17
|
+
const input = String(source ?? "");
|
|
18
|
+
let index = 0;
|
|
19
|
+
|
|
20
|
+
const error = message => {
|
|
21
|
+
const position = index;
|
|
22
|
+
const context = input.slice(Math.max(0, position - 16), Math.min(input.length, position + 16));
|
|
23
|
+
const failure = new SyntaxError(`${message} at position ${position}${context ? ` near ${JSON.stringify(context)}` : ""}.`);
|
|
24
|
+
failure.position = position;
|
|
25
|
+
throw failure;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const peek = () => input[index];
|
|
29
|
+
const consume = expected => {
|
|
30
|
+
if (input[index] !== expected) error(`Expected ${JSON.stringify(expected)}`);
|
|
31
|
+
index += 1;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function skipSpaceAndComments() {
|
|
35
|
+
while (index < input.length) {
|
|
36
|
+
const char = input[index];
|
|
37
|
+
if (/\s/.test(char)) {
|
|
38
|
+
index += 1;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (char === "/" && input[index + 1] === "/") {
|
|
42
|
+
index += 2;
|
|
43
|
+
while (index < input.length && input[index] !== "\n" && input[index] !== "\r") index += 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (char === "/" && input[index + 1] === "*") {
|
|
47
|
+
index += 2;
|
|
48
|
+
const end = input.indexOf("*/", index);
|
|
49
|
+
if (end < 0) error("Unterminated block comment");
|
|
50
|
+
index = end + 2;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function parseString() {
|
|
58
|
+
const quote = input[index++];
|
|
59
|
+
let result = "";
|
|
60
|
+
while (index < input.length) {
|
|
61
|
+
const char = input[index++];
|
|
62
|
+
if (char === quote) return result;
|
|
63
|
+
if (char === "\n" || char === "\r") error("Unterminated string literal");
|
|
64
|
+
if (char !== "\\") {
|
|
65
|
+
result += char;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (index >= input.length) error("Unterminated escape sequence");
|
|
69
|
+
const escape = input[index++];
|
|
70
|
+
switch (escape) {
|
|
71
|
+
case "'": result += "'"; break;
|
|
72
|
+
case '"': result += '"'; break;
|
|
73
|
+
case "\\": result += "\\"; break;
|
|
74
|
+
case "/": result += "/"; break;
|
|
75
|
+
case "b": result += "\b"; break;
|
|
76
|
+
case "f": result += "\f"; break;
|
|
77
|
+
case "n": result += "\n"; break;
|
|
78
|
+
case "r": result += "\r"; break;
|
|
79
|
+
case "t": result += "\t"; break;
|
|
80
|
+
case "v": result += "\v"; break;
|
|
81
|
+
case "0": {
|
|
82
|
+
if (/[0-9]/.test(input[index] ?? "")) error("Legacy octal escapes are not supported");
|
|
83
|
+
result += "\0";
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
case "x": {
|
|
87
|
+
const hex = input.slice(index, index + 2);
|
|
88
|
+
if (!/^[0-9a-fA-F]{2}$/.test(hex)) error("Invalid hexadecimal escape");
|
|
89
|
+
result += String.fromCharCode(Number.parseInt(hex, 16));
|
|
90
|
+
index += 2;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
case "u": {
|
|
94
|
+
if (input[index] === "{") {
|
|
95
|
+
index += 1;
|
|
96
|
+
const close = input.indexOf("}", index);
|
|
97
|
+
if (close < 0) error("Invalid Unicode escape");
|
|
98
|
+
const hex = input.slice(index, close);
|
|
99
|
+
if (!/^[0-9a-fA-F]{1,6}$/.test(hex)) error("Invalid Unicode escape");
|
|
100
|
+
result += String.fromCodePoint(Number.parseInt(hex, 16));
|
|
101
|
+
index = close + 1;
|
|
102
|
+
} else {
|
|
103
|
+
const hex = input.slice(index, index + 4);
|
|
104
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) error("Invalid Unicode escape");
|
|
105
|
+
result += String.fromCharCode(Number.parseInt(hex, 16));
|
|
106
|
+
index += 4;
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
case "\n": break;
|
|
111
|
+
case "\r":
|
|
112
|
+
if (input[index] === "\n") index += 1;
|
|
113
|
+
break;
|
|
114
|
+
default:
|
|
115
|
+
result += escape;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
error("Unterminated string literal");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseIdentifier() {
|
|
122
|
+
const start = index;
|
|
123
|
+
if (!/[A-Za-z_$]/.test(input[index] ?? "")) error("Expected identifier");
|
|
124
|
+
index += 1;
|
|
125
|
+
while (/[A-Za-z0-9_$]/.test(input[index] ?? "")) index += 1;
|
|
126
|
+
return input.slice(start, index);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parseNumber() {
|
|
130
|
+
const start = index;
|
|
131
|
+
if (input[index] === "+" || input[index] === "-") index += 1;
|
|
132
|
+
|
|
133
|
+
if (input[index] === "0" && /[xX]/.test(input[index + 1] ?? "")) {
|
|
134
|
+
index += 2;
|
|
135
|
+
const hexStart = index;
|
|
136
|
+
while (/[0-9a-fA-F]/.test(input[index] ?? "")) index += 1;
|
|
137
|
+
if (index === hexStart) error("Invalid hexadecimal number");
|
|
138
|
+
const raw = input.slice(start, index);
|
|
139
|
+
const sign = raw.startsWith("-") ? -1 : 1;
|
|
140
|
+
const normalized = raw.replace(/^[+-]?0[xX]/, "");
|
|
141
|
+
return sign * Number.parseInt(normalized, 16);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let digits = 0;
|
|
145
|
+
while (/[0-9]/.test(input[index] ?? "")) {
|
|
146
|
+
digits += 1;
|
|
147
|
+
index += 1;
|
|
148
|
+
}
|
|
149
|
+
if (input[index] === ".") {
|
|
150
|
+
index += 1;
|
|
151
|
+
while (/[0-9]/.test(input[index] ?? "")) {
|
|
152
|
+
digits += 1;
|
|
153
|
+
index += 1;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (digits === 0) error("Invalid number");
|
|
157
|
+
if (/[eE]/.test(input[index] ?? "")) {
|
|
158
|
+
index += 1;
|
|
159
|
+
if (input[index] === "+" || input[index] === "-") index += 1;
|
|
160
|
+
const exponentStart = index;
|
|
161
|
+
while (/[0-9]/.test(input[index] ?? "")) index += 1;
|
|
162
|
+
if (index === exponentStart) error("Invalid exponent");
|
|
163
|
+
}
|
|
164
|
+
const raw = input.slice(start, index);
|
|
165
|
+
const value = Number(raw);
|
|
166
|
+
if (!Number.isFinite(value)) error("Invalid finite number");
|
|
167
|
+
return value;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function defineDataProperty(target, key, value) {
|
|
171
|
+
Object.defineProperty(target, key, {
|
|
172
|
+
configurable: true,
|
|
173
|
+
enumerable: true,
|
|
174
|
+
writable: true,
|
|
175
|
+
value,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function parseObject() {
|
|
180
|
+
consume("{");
|
|
181
|
+
skipSpaceAndComments();
|
|
182
|
+
const object = {};
|
|
183
|
+
if (peek() === "}") {
|
|
184
|
+
index += 1;
|
|
185
|
+
return object;
|
|
186
|
+
}
|
|
187
|
+
while (index < input.length) {
|
|
188
|
+
skipSpaceAndComments();
|
|
189
|
+
let key;
|
|
190
|
+
const char = peek();
|
|
191
|
+
if (char === "'" || char === '"') key = parseString();
|
|
192
|
+
else if (/[A-Za-z_$]/.test(char ?? "")) key = parseIdentifier();
|
|
193
|
+
else if (/[+\-.0-9]/.test(char ?? "")) key = String(parseNumber());
|
|
194
|
+
else error("Expected object key");
|
|
195
|
+
|
|
196
|
+
skipSpaceAndComments();
|
|
197
|
+
consume(":");
|
|
198
|
+
skipSpaceAndComments();
|
|
199
|
+
defineDataProperty(object, key, parseValue());
|
|
200
|
+
skipSpaceAndComments();
|
|
201
|
+
if (peek() === "}") {
|
|
202
|
+
index += 1;
|
|
203
|
+
return object;
|
|
204
|
+
}
|
|
205
|
+
consume(",");
|
|
206
|
+
skipSpaceAndComments();
|
|
207
|
+
if (peek() === "}") {
|
|
208
|
+
index += 1;
|
|
209
|
+
return object;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
error("Unterminated object literal");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function parseArray() {
|
|
216
|
+
consume("[");
|
|
217
|
+
skipSpaceAndComments();
|
|
218
|
+
const array = [];
|
|
219
|
+
if (peek() === "]") {
|
|
220
|
+
index += 1;
|
|
221
|
+
return array;
|
|
222
|
+
}
|
|
223
|
+
while (index < input.length) {
|
|
224
|
+
array.push(parseValue());
|
|
225
|
+
skipSpaceAndComments();
|
|
226
|
+
if (peek() === "]") {
|
|
227
|
+
index += 1;
|
|
228
|
+
return array;
|
|
229
|
+
}
|
|
230
|
+
consume(",");
|
|
231
|
+
skipSpaceAndComments();
|
|
232
|
+
if (peek() === "]") {
|
|
233
|
+
index += 1;
|
|
234
|
+
return array;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
error("Unterminated array literal");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function parseValue() {
|
|
241
|
+
skipSpaceAndComments();
|
|
242
|
+
const char = peek();
|
|
243
|
+
if (char === "{") return parseObject();
|
|
244
|
+
if (char === "[") return parseArray();
|
|
245
|
+
if (char === "'" || char === '"') return parseString();
|
|
246
|
+
if (/[+\-.0-9]/.test(char ?? "")) return parseNumber();
|
|
247
|
+
if (/[A-Za-z_$]/.test(char ?? "")) {
|
|
248
|
+
const identifier = parseIdentifier();
|
|
249
|
+
if (identifier === "true") return true;
|
|
250
|
+
if (identifier === "false") return false;
|
|
251
|
+
if (identifier === "null") return null;
|
|
252
|
+
error(`Unsupported identifier ${JSON.stringify(identifier)}; declarative values cannot execute JavaScript`);
|
|
253
|
+
}
|
|
254
|
+
error("Expected data literal");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
skipSpaceAndComments();
|
|
258
|
+
const value = parseValue();
|
|
259
|
+
skipSpaceAndComments();
|
|
260
|
+
if (index !== input.length) error("Unexpected trailing input");
|
|
261
|
+
return value;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function parseHTMLPropsObject(source) {
|
|
265
|
+
const value = parseHTMLDataLiteral(source);
|
|
266
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
267
|
+
throw new TypeError("The `props` attribute must contain a top-level object literal.");
|
|
268
|
+
}
|
|
269
|
+
return value;
|
|
270
|
+
}
|