@bytefaceinc/web-lang 0.1.1
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/LICENSE +21 -0
- package/README.md +277 -0
- package/bin/web.js +10 -0
- package/compiler.js +4602 -0
- package/docs/cli.md +657 -0
- package/docs/compiler.md +1433 -0
- package/docs/error-handling.md +863 -0
- package/docs/getting-started.md +805 -0
- package/docs/language-guide.md +945 -0
- package/lib/cli/commands/compile.js +127 -0
- package/lib/cli/commands/init.js +172 -0
- package/lib/cli/commands/screenshot.js +257 -0
- package/lib/cli/commands/watch.js +458 -0
- package/lib/cli/compile-service.js +19 -0
- package/lib/cli/compile-worker.js +32 -0
- package/lib/cli/compiler-runner.js +37 -0
- package/lib/cli/index.js +154 -0
- package/lib/cli/init-service.js +204 -0
- package/lib/cli/screenshot-artifacts.js +81 -0
- package/lib/cli/screenshot-service.js +153 -0
- package/lib/cli/shared.js +261 -0
- package/lib/cli/targets.js +199 -0
- package/lib/cli/watch-settings.js +37 -0
- package/package.json +50 -0
package/compiler.js
ADDED
|
@@ -0,0 +1,4602 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* WEB compiler
|
|
6
|
+
* ------------
|
|
7
|
+
* This standalone Node.js compiler parses WEB source, builds a tiny AST plus
|
|
8
|
+
* symbol table, and emits HTML and CSS. Script mode preserves the original
|
|
9
|
+
* `layout.web` -> `index.html` + `styles.css` flow, while the exported API can
|
|
10
|
+
* compile any `.web` file.
|
|
11
|
+
*
|
|
12
|
+
* The implementation is intentionally explicit and heavily commented so the
|
|
13
|
+
* parsing and code generation steps are easy to follow.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const vm = require('vm');
|
|
19
|
+
|
|
20
|
+
const DEFAULT_INPUT_FILE = 'layout.web';
|
|
21
|
+
const DEFAULT_HTML_OUTPUT_FILE = 'index.html';
|
|
22
|
+
const DEFAULT_CSS_OUTPUT_FILE = 'styles.css';
|
|
23
|
+
const DEFAULT_DOCUMENT_TITLE = 'WEB Output';
|
|
24
|
+
const JAVASCRIPT_INJECTION_TIMEOUT_MS = 1000;
|
|
25
|
+
const VOID_HTML_TAGS = new Set(['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
|
|
26
|
+
|
|
27
|
+
class CompilerError extends Error {
|
|
28
|
+
constructor(message, line, column, options = {}) {
|
|
29
|
+
super(line && column ? `${message} (line ${line}, column ${column})` : message);
|
|
30
|
+
this.name = 'CompilerError';
|
|
31
|
+
this.rawMessage = message;
|
|
32
|
+
this.line = line;
|
|
33
|
+
this.column = column;
|
|
34
|
+
this.actual = options.actual || null;
|
|
35
|
+
this.rule = options.rule || null;
|
|
36
|
+
this.hint = options.hint || null;
|
|
37
|
+
this.example = options.example || null;
|
|
38
|
+
this.phase = options.phase || null;
|
|
39
|
+
this.sourceName = options.sourceName || null;
|
|
40
|
+
this.isEnhanced = false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function variableDefineBlockMessage() {
|
|
45
|
+
return 'Variables must be declared inside a top-level define { ... } block before any rules. Example: define { @width = "10px"; }';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function typeDefineBlockMessage() {
|
|
49
|
+
return 'Type declarations must be declared inside a top-level define { ... } block before any rules. Example: define { Button heroButton; }';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function styleInheritanceDefineBlockMessage() {
|
|
53
|
+
return 'Style inheritance must be declared inside a top-level define { ... } block before any rules. Example: define { heroButton extends buttonFrame; }';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function defineBlockPlacementMessage() {
|
|
57
|
+
return 'The define { ... } block must appear once at the top of the file before any rules.';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function attrsBlockPlacementMessage() {
|
|
61
|
+
return '::attrs blocks must be nested directly inside an element scope. Use them inside a normal DOM block, not inside styles, pseudos, or media queries.';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function attrsBlockContentMessage() {
|
|
65
|
+
return '::attrs blocks only support direct HTML attribute assignments like href = "/docs";';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function scriptBlockPlacementMessage() {
|
|
69
|
+
return '::script blocks must be declared at the top level outside element, style, pseudo, and media scopes.';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function scriptBlockContentMessage() {
|
|
73
|
+
return '::script blocks only support direct attribute assignments and a single code { ... } block.';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function headBlockPlacementMessage() {
|
|
77
|
+
return '::head blocks must be declared at the top level outside element, style, pseudo, and media scopes.';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function headBlockContentMessage() {
|
|
81
|
+
return '::head blocks only support meta { ... } and link { ... } entries.';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function headEntryContentMessage(tagName) {
|
|
85
|
+
return `${tagName} blocks inside ::head only support direct HTML attribute assignments like name = "description";`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function globalStyleScopePlacementMessage() {
|
|
89
|
+
return 'Global style blocks using "*" or "html" must be declared at the top level or inside a top-level ::media block without a parent selector.';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function runCompilerStage(phase, source, sourceName, work) {
|
|
93
|
+
try {
|
|
94
|
+
return work();
|
|
95
|
+
} catch (error) {
|
|
96
|
+
throw enhanceCompilerError(error, { source, sourceName, phase });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function enhanceCompilerError(error, options = {}) {
|
|
101
|
+
if (!(error instanceof CompilerError) || error.isEnhanced) {
|
|
102
|
+
return error;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const sourceName = options.sourceName || error.sourceName || DEFAULT_INPUT_FILE;
|
|
106
|
+
const phase = options.phase || error.phase || 'compilation';
|
|
107
|
+
const guidance = inferCompilerErrorGuidance(error, options.source);
|
|
108
|
+
const locationLabel = error.line && error.column
|
|
109
|
+
? `${sourceName}:${error.line}:${error.column}`
|
|
110
|
+
: sourceName;
|
|
111
|
+
const foundLabel = error.actual || describeSyntaxAtLocation(options.source, error.line, error.column);
|
|
112
|
+
const frame = buildCompilerCodeFrame(options.source, error.line, error.column);
|
|
113
|
+
const messageLines = [
|
|
114
|
+
formatCompilerErrorSummary(error),
|
|
115
|
+
`Source: ${locationLabel}`,
|
|
116
|
+
`Phase: ${phase}`,
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
if (guidance.rule) {
|
|
120
|
+
messageLines.push(`Violation: ${guidance.rule}`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (foundLabel) {
|
|
124
|
+
messageLines.push(`Found: ${foundLabel}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (guidance.hint) {
|
|
128
|
+
messageLines.push(`How to fix: ${guidance.hint}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (guidance.example) {
|
|
132
|
+
messageLines.push(`Example: ${guidance.example}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (frame) {
|
|
136
|
+
messageLines.push('Context:');
|
|
137
|
+
messageLines.push(frame);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
error.message = messageLines.join('\n');
|
|
141
|
+
error.phase = phase;
|
|
142
|
+
error.sourceName = sourceName;
|
|
143
|
+
error.isEnhanced = true;
|
|
144
|
+
return error;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function formatCompilerErrorSummary(error) {
|
|
148
|
+
if (error.line && error.column) {
|
|
149
|
+
return `${error.rawMessage || error.message} (line ${error.line}, column ${error.column})`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return error.rawMessage || error.message;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function inferCompilerErrorGuidance(error, source) {
|
|
156
|
+
const message = error.rawMessage || error.message;
|
|
157
|
+
|
|
158
|
+
if (message.startsWith('Unexpected character')) {
|
|
159
|
+
return {
|
|
160
|
+
rule: 'Only valid WEB syntax tokens may appear here. Bare punctuation such as `#`, `?`, or stray symbols are not valid values by themselves.',
|
|
161
|
+
hint: 'Remove the unsupported character, or if you meant a literal value, wrap it in quotes or a template literal first.',
|
|
162
|
+
example: 'heroCard.backgroundColor = "#1f2a44";',
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (message.startsWith('Unexpected value token')) {
|
|
167
|
+
return {
|
|
168
|
+
rule: 'Assignment values in WEB must be a string, template literal, number, percentage, identifier, variable reference, or `js` literal.',
|
|
169
|
+
hint: 'Replace the highlighted token with a supported value form and keep the full assignment on one complete statement.',
|
|
170
|
+
example: 'heroTitle.textContent = "Hello";',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (message.includes('Expected ";" after')) {
|
|
175
|
+
return {
|
|
176
|
+
rule: 'WEB terminates declarations, assignments, and attribute entries with a semicolon.',
|
|
177
|
+
hint: 'Add `;` at the end of the current statement before starting the next statement or closing the block.',
|
|
178
|
+
example: 'heroTitle.textContent = "Hello";',
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (message.includes('Expected "=" after')) {
|
|
183
|
+
return {
|
|
184
|
+
rule: 'WEB uses `=` between the left-hand side and the assigned value.',
|
|
185
|
+
hint: 'Insert `=` between the property, variable, or attribute name and the value.',
|
|
186
|
+
example: 'heroTitle.color = "#223344";',
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (message.includes('Expected "{" after')) {
|
|
191
|
+
return {
|
|
192
|
+
rule: 'Block headers in WEB must be followed immediately by an opening `{`.',
|
|
193
|
+
hint: 'Add `{` after the block header so the compiler knows where the block body begins.',
|
|
194
|
+
example: 'heroSection {\n padding = "2rem";\n}',
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (message.includes('Expected "}" after') || message.startsWith('Unterminated ')) {
|
|
199
|
+
return {
|
|
200
|
+
rule: 'Every WEB block opened with `{` must be closed with a matching `}`.',
|
|
201
|
+
hint: 'Close the highlighted block and check that nested blocks are balanced in the correct order.',
|
|
202
|
+
example: 'heroSection {\n padding = "2rem";\n}',
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (message === 'Expected an identifier after "."') {
|
|
207
|
+
return {
|
|
208
|
+
rule: 'Dot notation paths in WEB must continue with a valid identifier after each `.`.',
|
|
209
|
+
hint: 'Replace the invalid segment with a normal identifier such as `heroTitle` or remove the extra dot.',
|
|
210
|
+
example: 'pageMain.heroSection.heroTitle.textContent = "Hello";',
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (message === 'Expected a variable name after "@"') {
|
|
215
|
+
return {
|
|
216
|
+
rule: 'Variables start with `@` and then a valid identifier name.',
|
|
217
|
+
hint: 'Put a letter, `_`, or `$` immediately after `@`, then continue with letters, digits, `_`, or `$`.',
|
|
218
|
+
example: 'define {\n @pageWidth = "960px";\n}',
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (message === 'Invalid pseudo block header' || message.includes('Expected a pseudo selector, media query, or keyframes name after "::"')) {
|
|
223
|
+
return {
|
|
224
|
+
rule: 'A `::` block must name a supported WEB construct such as `::hover`, `::before`, `::media (...)`, `::attrs`, `::script`, `::head`, or `::keyframes name`.',
|
|
225
|
+
hint: 'Rename the pseudo block to a supported form and keep any required selector or query text directly after the header.',
|
|
226
|
+
example: 'buttonPrimary {\n ::hover {\n transform = "translateY(-2px)";\n }\n}',
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (message.startsWith('Expected a keyframe stage such as')) {
|
|
231
|
+
return {
|
|
232
|
+
rule: 'Keyframe stages must be `from`, `to`, or a percentage like `50%`.',
|
|
233
|
+
hint: 'Replace the highlighted token with a valid keyframe stage label before opening the stage block.',
|
|
234
|
+
example: '::keyframes fadeIn {\n from {\n opacity = 0;\n }\n to {\n opacity = 1;\n }\n}',
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (message === 'Unterminated string literal') {
|
|
239
|
+
return {
|
|
240
|
+
rule: 'Quoted WEB strings must end with the same quote character used to open them.',
|
|
241
|
+
hint: 'Add the missing closing quote before the statement ends.',
|
|
242
|
+
example: 'heroTitle.textContent = "Welcome";',
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (message === 'Unterminated template literal') {
|
|
247
|
+
return {
|
|
248
|
+
rule: 'Template literals in WEB must close with a backtick.',
|
|
249
|
+
hint: 'Add the closing backtick before the semicolon or block boundary.',
|
|
250
|
+
example: 'card.raw = `\n &:hover { transform: scale(1.02); }\n`;',
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (message === 'Unterminated JavaScript inject literal') {
|
|
255
|
+
return {
|
|
256
|
+
rule: 'Compile-time JavaScript literals must close with a backtick after `js`.',
|
|
257
|
+
hint: 'Finish the `js` literal with a closing backtick and then complete the enclosing WEB statement.',
|
|
258
|
+
example: 'heroCopy.innerHTML = js`return "<strong>Hello</strong>";`;',
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (message === 'Only one top-level define { ... } block is allowed.' || message === defineBlockPlacementMessage()) {
|
|
263
|
+
return {
|
|
264
|
+
rule: 'WEB allows at most one top-level `define { ... }` block, and it must appear before normal rules.',
|
|
265
|
+
hint: 'Merge additional declarations into the existing top-level define block instead of creating another one later in the file.',
|
|
266
|
+
example: 'define {\n @space = "1rem";\n Button primaryButton;\n}',
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (message === variableDefineBlockMessage() || message === typeDefineBlockMessage() || message === styleInheritanceDefineBlockMessage()) {
|
|
271
|
+
return {
|
|
272
|
+
rule: 'Variables, type declarations, and style inheritance declarations are only valid inside the top-level `define { ... }` block.',
|
|
273
|
+
hint: 'Move the highlighted declaration into the single top-level define block before any element rules.',
|
|
274
|
+
example: 'define {\n @space = "1rem";\n Button primaryButton;\n primaryButton extends buttonBase;\n}',
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (message.startsWith('Unknown variable')) {
|
|
279
|
+
return {
|
|
280
|
+
rule: 'Every variable reference must resolve to a declaration inside the top-level `define { ... }` block.',
|
|
281
|
+
hint: 'Declare the variable before use, or fix the variable name so it matches an existing declaration exactly.',
|
|
282
|
+
example: 'define {\n @accent = "#94a7ca";\n}',
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (message.startsWith('Circular variable reference') || message.startsWith('Circular type inheritance') || message.startsWith('Circular style inheritance')) {
|
|
287
|
+
return {
|
|
288
|
+
rule: 'Inheritance and variable references must form an acyclic graph so the compiler can resolve them deterministically.',
|
|
289
|
+
hint: 'Break the cycle by removing or renaming one link in the chain so resolution flows in one direction.',
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (message === attrsBlockContentMessage()) {
|
|
294
|
+
return {
|
|
295
|
+
rule: 'Inside `::attrs`, only direct HTML attribute assignments are valid.',
|
|
296
|
+
hint: 'Use only `attributeName = value;` entries inside `::attrs`, and move style/content assignments back to the normal element scope.',
|
|
297
|
+
example: '::attrs {\n href = "/docs";\n ariaLabel = "Open docs";\n}',
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (message === headBlockContentMessage() || message === scriptBlockContentMessage()) {
|
|
302
|
+
return {
|
|
303
|
+
rule: 'This special block only supports a narrow set of child entries defined by the WEB grammar.',
|
|
304
|
+
hint: 'Use the allowed entry types for that block and move unrelated statements elsewhere.',
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const sourceHint = source && error.line ? ` Review the highlighted line and make sure the current statement is complete before the next token begins.` : '';
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
rule: 'The highlighted code does not match the WEB grammar or semantic rules expected at this point in compilation.',
|
|
312
|
+
hint: `Adjust the highlighted statement so it follows the WEB syntax required by the surrounding block.${sourceHint}`,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function describeSyntaxAtLocation(source, line, column) {
|
|
317
|
+
if (!source || !line || !column) {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const lines = source.split(/\r?\n/);
|
|
322
|
+
const lineText = lines[line - 1];
|
|
323
|
+
|
|
324
|
+
if (typeof lineText !== 'string') {
|
|
325
|
+
return 'end of file';
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (column - 1 >= lineText.length) {
|
|
329
|
+
return line === lines.length ? 'end of file' : 'end of line';
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const slice = lineText.slice(column - 1);
|
|
333
|
+
const tokenMatch = slice.match(/^\S+/);
|
|
334
|
+
|
|
335
|
+
if (tokenMatch) {
|
|
336
|
+
return `token ${JSON.stringify(truncateDiagnosticText(tokenMatch[0], 48))}`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return `character ${JSON.stringify(lineText[column - 1])}`;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function buildCompilerCodeFrame(source, line, column, contextRadius = 1) {
|
|
343
|
+
if (!source || !line || !column) {
|
|
344
|
+
return '';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const lines = source.split(/\r?\n/);
|
|
348
|
+
const targetLine = lines[line - 1];
|
|
349
|
+
|
|
350
|
+
if (typeof targetLine !== 'string') {
|
|
351
|
+
return '';
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const startLine = Math.max(1, line - contextRadius);
|
|
355
|
+
const endLine = Math.min(lines.length, line + contextRadius);
|
|
356
|
+
const lineNumberWidth = String(endLine).length;
|
|
357
|
+
const frameLines = [];
|
|
358
|
+
|
|
359
|
+
for (let currentLine = startLine; currentLine <= endLine; currentLine += 1) {
|
|
360
|
+
const rawText = lines[currentLine - 1];
|
|
361
|
+
const expandedText = expandTabs(rawText);
|
|
362
|
+
const marker = currentLine === line ? '>' : ' ';
|
|
363
|
+
frameLines.push(`${marker} ${String(currentLine).padStart(lineNumberWidth)} | ${expandedText}`);
|
|
364
|
+
|
|
365
|
+
if (currentLine === line) {
|
|
366
|
+
const visualColumn = computeVisualColumn(rawText, column);
|
|
367
|
+
frameLines.push(` ${' '.repeat(lineNumberWidth)} | ${' '.repeat(Math.max(0, visualColumn - 1))}^`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return frameLines.join('\n');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function expandTabs(text, tabSize = 2) {
|
|
375
|
+
return String(text).replace(/\t/g, ' '.repeat(tabSize));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function computeVisualColumn(text, column, tabSize = 2) {
|
|
379
|
+
let visualColumn = 1;
|
|
380
|
+
|
|
381
|
+
for (let index = 0; index < Math.max(0, column - 1) && index < text.length; index += 1) {
|
|
382
|
+
visualColumn += text[index] === '\t' ? tabSize : 1;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return visualColumn;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function truncateDiagnosticText(text, maxLength = 48) {
|
|
389
|
+
if (text.length <= maxLength) {
|
|
390
|
+
return text;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return `${text.slice(0, Math.max(0, maxLength - 3))}...`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function describeTokenForDiagnostic(token) {
|
|
397
|
+
if (!token || token.type === 'EOF') {
|
|
398
|
+
return 'end of file';
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (token.value === null || token.value === undefined || token.value === '') {
|
|
402
|
+
return `token ${token.type}`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return `token ${token.type} ${JSON.stringify(truncateDiagnosticText(String(token.value)))}`;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function main() {
|
|
409
|
+
const workingDirectory = process.cwd();
|
|
410
|
+
const inputPath = path.join(workingDirectory, DEFAULT_INPUT_FILE);
|
|
411
|
+
const htmlPath = path.join(workingDirectory, DEFAULT_HTML_OUTPUT_FILE);
|
|
412
|
+
const cssPath = path.join(workingDirectory, DEFAULT_CSS_OUTPUT_FILE);
|
|
413
|
+
|
|
414
|
+
compileFile(inputPath, {
|
|
415
|
+
htmlPath,
|
|
416
|
+
cssPath,
|
|
417
|
+
stylesheetHref: DEFAULT_CSS_OUTPUT_FILE,
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
console.log(`Compiled ${DEFAULT_INPUT_FILE} -> ${DEFAULT_HTML_OUTPUT_FILE}, ${DEFAULT_CSS_OUTPUT_FILE}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function compileSource(source, options = {}) {
|
|
424
|
+
const sourceName = options.sourceName || DEFAULT_INPUT_FILE;
|
|
425
|
+
const lexer = new Lexer(source);
|
|
426
|
+
const tokens = runCompilerStage('lexing', source, sourceName, () => lexer.tokenize());
|
|
427
|
+
const parser = new Parser(tokens);
|
|
428
|
+
const ast = runCompilerStage('parsing', source, sourceName, () => parser.parseProgram());
|
|
429
|
+
const variableTable = runCompilerStage('semantic analysis', source, sourceName, () => buildVariableTable(ast));
|
|
430
|
+
const resolveContext = {
|
|
431
|
+
variableTable,
|
|
432
|
+
sourceName,
|
|
433
|
+
};
|
|
434
|
+
const symbolTable = runCompilerStage('semantic analysis', source, sourceName, () => buildSymbolTable(ast));
|
|
435
|
+
const styleInheritanceTable = runCompilerStage('semantic analysis', source, sourceName, () => buildStyleInheritanceTable(ast));
|
|
436
|
+
const headEntries = runCompilerStage('HTML preparation', source, sourceName, () => buildHeadEntries(ast, resolveContext));
|
|
437
|
+
const scriptBlocks = runCompilerStage('HTML preparation', source, sourceName, () => buildScriptBlocks(ast, resolveContext));
|
|
438
|
+
const documentModel = runCompilerStage('HTML preparation', source, sourceName, () => buildDocumentModel(ast, symbolTable, resolveContext));
|
|
439
|
+
const styleModel = runCompilerStage('CSS preparation', source, sourceName, () => buildStyleModel(ast, resolveContext, styleInheritanceTable));
|
|
440
|
+
const htmlOutput = runCompilerStage('HTML generation', source, sourceName, () => generateHtml(documentModel, symbolTable, headEntries, scriptBlocks, {
|
|
441
|
+
title: options.title || DEFAULT_DOCUMENT_TITLE,
|
|
442
|
+
stylesheetHref: options.stylesheetHref || DEFAULT_CSS_OUTPUT_FILE,
|
|
443
|
+
}));
|
|
444
|
+
const cssOutput = runCompilerStage('CSS generation', source, sourceName, () => generateCss(styleModel));
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
htmlOutput,
|
|
448
|
+
cssOutput,
|
|
449
|
+
stats: collectCompileStats(tokens, ast, documentModel, styleModel, headEntries, scriptBlocks),
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function compileFile(inputPath, options = {}) {
|
|
454
|
+
const resolvedInputPath = path.resolve(inputPath);
|
|
455
|
+
|
|
456
|
+
if (!fs.existsSync(resolvedInputPath)) {
|
|
457
|
+
throw new CompilerError(`Missing input file: ${inputPath}`);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const inputStats = fs.statSync(resolvedInputPath);
|
|
461
|
+
|
|
462
|
+
if (inputStats.isDirectory()) {
|
|
463
|
+
throw new CompilerError(`Expected a .web file but found a directory: ${inputPath}`);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (!inputStats.isFile()) {
|
|
467
|
+
throw new CompilerError(`Unsupported input path: ${inputPath}`);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (path.extname(resolvedInputPath).toLowerCase() !== '.web') {
|
|
471
|
+
throw new CompilerError(`Unsupported input file: ${path.basename(resolvedInputPath)}. Expected a .web file.`);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const outputPaths = resolveCompileOutputPaths(resolvedInputPath, options);
|
|
475
|
+
const source = fs.readFileSync(resolvedInputPath, 'utf8');
|
|
476
|
+
const result = compileSource(source, {
|
|
477
|
+
sourceName: options.sourceName || path.basename(resolvedInputPath),
|
|
478
|
+
title: options.title,
|
|
479
|
+
stylesheetHref: options.stylesheetHref || path.basename(outputPaths.cssPath),
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
if (options.write !== false) {
|
|
483
|
+
fs.writeFileSync(outputPaths.htmlPath, result.htmlOutput, 'utf8');
|
|
484
|
+
fs.writeFileSync(outputPaths.cssPath, result.cssOutput, 'utf8');
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return {
|
|
488
|
+
...result,
|
|
489
|
+
inputPath: resolvedInputPath,
|
|
490
|
+
htmlPath: outputPaths.htmlPath,
|
|
491
|
+
cssPath: outputPaths.cssPath,
|
|
492
|
+
sourceBytes: Buffer.byteLength(source),
|
|
493
|
+
htmlBytes: Buffer.byteLength(result.htmlOutput),
|
|
494
|
+
cssBytes: Buffer.byteLength(result.cssOutput),
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function resolveCompileOutputPaths(inputPath, options = {}) {
|
|
499
|
+
const inputDirectory = path.dirname(inputPath);
|
|
500
|
+
const inputName = path.basename(inputPath, path.extname(inputPath));
|
|
501
|
+
|
|
502
|
+
return {
|
|
503
|
+
htmlPath: path.resolve(options.htmlPath || path.join(inputDirectory, `${inputName}.html`)),
|
|
504
|
+
cssPath: path.resolve(options.cssPath || path.join(inputDirectory, `${inputName}.css`)),
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function collectCompileStats(tokens, ast, documentModel, styleModel, headEntries, scriptBlocks) {
|
|
509
|
+
return {
|
|
510
|
+
tokenCount: tokens.length,
|
|
511
|
+
statementCount: ast.body.length,
|
|
512
|
+
domNodeCount: countRenderedHtmlNodes(documentModel),
|
|
513
|
+
cssRuleCount: styleModel.entries.filter((entry) => entry.type === 'rule').length,
|
|
514
|
+
keyframesCount: styleModel.entries.filter((entry) => entry.type === 'keyframes').length,
|
|
515
|
+
headEntryCount: headEntries.length,
|
|
516
|
+
scriptBlockCount: scriptBlocks.length,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function countRenderedHtmlNodes(node) {
|
|
521
|
+
let total = 0;
|
|
522
|
+
|
|
523
|
+
for (const child of node.children) {
|
|
524
|
+
if (!child.renderInHtml) {
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
total += 1 + countRenderedHtmlNodes(child);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return total;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* LEXER
|
|
536
|
+
* -----
|
|
537
|
+
* WEB is deliberately small, so the lexer only needs a handful of token types:
|
|
538
|
+
* identifiers, variables, literals, dots, equals, braces, and semicolons.
|
|
539
|
+
*
|
|
540
|
+
* It also strips line comments (`// comment`) so the parser only sees the
|
|
541
|
+
* meaningful syntax.
|
|
542
|
+
*/
|
|
543
|
+
class Lexer {
|
|
544
|
+
constructor(source) {
|
|
545
|
+
this.source = source;
|
|
546
|
+
this.index = 0;
|
|
547
|
+
this.line = 1;
|
|
548
|
+
this.column = 1;
|
|
549
|
+
this.pendingScriptBlock = false;
|
|
550
|
+
this.scriptBlockDepth = 0;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
tokenize() {
|
|
554
|
+
const tokens = [];
|
|
555
|
+
|
|
556
|
+
while (!this.isAtEnd()) {
|
|
557
|
+
this.skipWhitespaceAndComments();
|
|
558
|
+
|
|
559
|
+
if (this.isAtEnd()) {
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const startLine = this.line;
|
|
564
|
+
const startColumn = this.column;
|
|
565
|
+
const char = this.peek();
|
|
566
|
+
|
|
567
|
+
if (this.scriptBlockDepth > 0 && this.startsScriptCodeBlock()) {
|
|
568
|
+
tokens.push(this.readScriptCodeBlock(startLine, startColumn));
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (this.startsJavaScriptLiteral()) {
|
|
573
|
+
tokens.push(this.readJavaScriptLiteral(startLine, startColumn));
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (char === ':' && this.peek(1) === ':') {
|
|
578
|
+
tokens.push(this.readPseudoHeader(startLine, startColumn));
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
if (char === '@') {
|
|
583
|
+
tokens.push(this.readVariableIdentifier(startLine, startColumn));
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (isIdentifierStart(char)) {
|
|
588
|
+
tokens.push(this.readIdentifier(startLine, startColumn));
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (char === '-' && isDigit(this.peek(1))) {
|
|
593
|
+
tokens.push(this.readNumber(startLine, startColumn));
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (isDigit(char)) {
|
|
598
|
+
tokens.push(this.readNumber(startLine, startColumn));
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (char === '"' || char === '\'') {
|
|
603
|
+
tokens.push(this.readString(startLine, startColumn));
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
if (char === '`') {
|
|
608
|
+
tokens.push(this.readTemplateLiteral(startLine, startColumn));
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
switch (char) {
|
|
613
|
+
case '*':
|
|
614
|
+
tokens.push(this.makeToken('STAR', '*', startLine, startColumn));
|
|
615
|
+
this.advance();
|
|
616
|
+
break;
|
|
617
|
+
case '.':
|
|
618
|
+
tokens.push(this.makeToken('DOT', '.', startLine, startColumn));
|
|
619
|
+
this.advance();
|
|
620
|
+
break;
|
|
621
|
+
case '{':
|
|
622
|
+
tokens.push(this.makeToken('LBRACE', '{', startLine, startColumn));
|
|
623
|
+
this.advance();
|
|
624
|
+
if (this.pendingScriptBlock) {
|
|
625
|
+
this.scriptBlockDepth += 1;
|
|
626
|
+
this.pendingScriptBlock = false;
|
|
627
|
+
} else if (this.scriptBlockDepth > 0) {
|
|
628
|
+
this.scriptBlockDepth += 1;
|
|
629
|
+
}
|
|
630
|
+
break;
|
|
631
|
+
case '}':
|
|
632
|
+
tokens.push(this.makeToken('RBRACE', '}', startLine, startColumn));
|
|
633
|
+
this.advance();
|
|
634
|
+
if (this.scriptBlockDepth > 0) {
|
|
635
|
+
this.scriptBlockDepth -= 1;
|
|
636
|
+
}
|
|
637
|
+
break;
|
|
638
|
+
case '=':
|
|
639
|
+
tokens.push(this.makeToken('EQUALS', '=', startLine, startColumn));
|
|
640
|
+
this.advance();
|
|
641
|
+
break;
|
|
642
|
+
case ';':
|
|
643
|
+
tokens.push(this.makeToken('SEMICOLON', ';', startLine, startColumn));
|
|
644
|
+
this.advance();
|
|
645
|
+
break;
|
|
646
|
+
default:
|
|
647
|
+
throw new CompilerError(`Unexpected character "${char}"`, startLine, startColumn, {
|
|
648
|
+
actual: `character ${JSON.stringify(char)}`,
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
tokens.push(this.makeToken('EOF', null, this.line, this.column));
|
|
654
|
+
return tokens;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
skipWhitespaceAndComments() {
|
|
658
|
+
while (!this.isAtEnd()) {
|
|
659
|
+
const char = this.peek();
|
|
660
|
+
const next = this.peek(1);
|
|
661
|
+
|
|
662
|
+
if (char === '/' && next === '/') {
|
|
663
|
+
while (!this.isAtEnd() && this.peek() !== '\n') {
|
|
664
|
+
this.advance();
|
|
665
|
+
}
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
if (char === '/' && next === '*') {
|
|
670
|
+
this.advance();
|
|
671
|
+
this.advance();
|
|
672
|
+
|
|
673
|
+
while (!this.isAtEnd() && !(this.peek() === '*' && this.peek(1) === '/')) {
|
|
674
|
+
this.advance();
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
if (this.isAtEnd()) {
|
|
678
|
+
throw new CompilerError('Unterminated block comment', this.line, this.column);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
this.advance();
|
|
682
|
+
this.advance();
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (/\s/.test(char)) {
|
|
687
|
+
this.advance();
|
|
688
|
+
continue;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
readIdentifier(line, column) {
|
|
696
|
+
let value = '';
|
|
697
|
+
|
|
698
|
+
while (!this.isAtEnd() && isIdentifierPart(this.peek())) {
|
|
699
|
+
value += this.advance();
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return this.makeToken('IDENTIFIER', value, line, column);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
readVariableIdentifier(line, column) {
|
|
706
|
+
let value = this.advance();
|
|
707
|
+
|
|
708
|
+
if (!isIdentifierStart(this.peek())) {
|
|
709
|
+
throw new CompilerError('Expected a variable name after "@"', line, column);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
while (!this.isAtEnd() && isIdentifierPart(this.peek())) {
|
|
713
|
+
value += this.advance();
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
return this.makeToken('VARIABLE', value, line, column);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
readPseudoHeader(line, column) {
|
|
720
|
+
this.advance(); // :
|
|
721
|
+
this.advance(); // :
|
|
722
|
+
|
|
723
|
+
let value = '';
|
|
724
|
+
|
|
725
|
+
while (!this.isAtEnd()) {
|
|
726
|
+
const char = this.peek();
|
|
727
|
+
|
|
728
|
+
if (char === '{') {
|
|
729
|
+
const trimmed = value.trim();
|
|
730
|
+
|
|
731
|
+
if (!trimmed) {
|
|
732
|
+
throw new CompilerError('Expected a pseudo selector, media query, or keyframes name after "::"', line, column);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const keywordMatch = trimmed.match(/^([A-Za-z][A-Za-z0-9_-]*)([\s\S]*)$/);
|
|
736
|
+
|
|
737
|
+
if (keywordMatch) {
|
|
738
|
+
const keyword = camelToKebab(keywordMatch[1]);
|
|
739
|
+
const remainder = keywordMatch[2].trim();
|
|
740
|
+
|
|
741
|
+
if (keyword === 'script' && !remainder) {
|
|
742
|
+
this.pendingScriptBlock = true;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
return this.makeToken('PSEUDO', trimmed, line, column);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
value += this.advance();
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
throw new CompilerError('Unterminated pseudo block header', line, column);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
startsJavaScriptLiteral() {
|
|
756
|
+
return this.peek() === 'j' && this.peek(1) === 's' && this.peek(2) === '`';
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
readJavaScriptLiteral(line, column) {
|
|
760
|
+
this.advance(); // j
|
|
761
|
+
this.advance(); // s
|
|
762
|
+
this.advance(); // opening backtick
|
|
763
|
+
|
|
764
|
+
let value = '';
|
|
765
|
+
|
|
766
|
+
while (!this.isAtEnd()) {
|
|
767
|
+
const char = this.peek();
|
|
768
|
+
const next = this.peek(1);
|
|
769
|
+
|
|
770
|
+
if (char === '`') {
|
|
771
|
+
if (shouldStartNestedJavaScriptTemplate(value)) {
|
|
772
|
+
value += this.readNestedJavaScriptTemplate(line, column);
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
this.advance();
|
|
777
|
+
return this.makeToken('JAVASCRIPT', value, line, column);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
if (char === '"' || char === '\'') {
|
|
781
|
+
value += this.readJavaScriptQuotedString(char, line, column);
|
|
782
|
+
continue;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (char === '/' && next === '/') {
|
|
786
|
+
value += this.readJavaScriptLineComment();
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (char === '/' && next === '*') {
|
|
791
|
+
value += this.readJavaScriptBlockComment(line, column);
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
value += this.advance();
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
throw new CompilerError('Unterminated JavaScript inject literal', line, column);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
readNumber(line, column) {
|
|
802
|
+
let value = '';
|
|
803
|
+
|
|
804
|
+
if (this.peek() === '-') {
|
|
805
|
+
value += this.advance();
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
while (!this.isAtEnd() && isDigit(this.peek())) {
|
|
809
|
+
value += this.advance();
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (this.peek() === '.' && isDigit(this.peek(1))) {
|
|
813
|
+
value += this.advance();
|
|
814
|
+
while (!this.isAtEnd() && isDigit(this.peek())) {
|
|
815
|
+
value += this.advance();
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
if (this.peek() === '%') {
|
|
820
|
+
value += this.advance();
|
|
821
|
+
return this.makeToken('PERCENTAGE', value, line, column);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return this.makeToken('NUMBER', value, line, column);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
readString(line, column) {
|
|
828
|
+
const quote = this.advance();
|
|
829
|
+
let value = '';
|
|
830
|
+
|
|
831
|
+
while (!this.isAtEnd()) {
|
|
832
|
+
const char = this.advance();
|
|
833
|
+
|
|
834
|
+
if (char === quote) {
|
|
835
|
+
return this.makeToken('STRING', value, line, column);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if (char === '\\') {
|
|
839
|
+
value += this.readEscapeSequence(line, column);
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
value += char;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
throw new CompilerError('Unterminated string literal', line, column);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
readTemplateLiteral(line, column) {
|
|
850
|
+
this.advance(); // opening backtick
|
|
851
|
+
let value = '';
|
|
852
|
+
|
|
853
|
+
while (!this.isAtEnd()) {
|
|
854
|
+
const char = this.advance();
|
|
855
|
+
|
|
856
|
+
if (char === '`') {
|
|
857
|
+
return this.makeToken('TEMPLATE', value, line, column);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if (char === '\\') {
|
|
861
|
+
value += this.readEscapeSequence(line, column, true);
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
value += char;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
throw new CompilerError('Unterminated template literal', line, column);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
readNestedJavaScriptTemplate(line, column) {
|
|
872
|
+
let value = this.advance(); // opening backtick
|
|
873
|
+
|
|
874
|
+
while (!this.isAtEnd()) {
|
|
875
|
+
const char = this.peek();
|
|
876
|
+
const next = this.peek(1);
|
|
877
|
+
|
|
878
|
+
if (char === '\\') {
|
|
879
|
+
value += this.advance();
|
|
880
|
+
if (!this.isAtEnd()) {
|
|
881
|
+
value += this.advance();
|
|
882
|
+
}
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (char === '`') {
|
|
887
|
+
value += this.advance();
|
|
888
|
+
return value;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
if (char === '$' && next === '{') {
|
|
892
|
+
value += this.advance();
|
|
893
|
+
value += this.advance();
|
|
894
|
+
value += this.readJavaScriptInterpolation(line, column);
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
value += this.advance();
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
throw new CompilerError('Unterminated nested JavaScript template literal', line, column);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
readJavaScriptInterpolation(line, column) {
|
|
905
|
+
let value = '';
|
|
906
|
+
let depth = 1;
|
|
907
|
+
|
|
908
|
+
while (!this.isAtEnd() && depth > 0) {
|
|
909
|
+
const char = this.peek();
|
|
910
|
+
const next = this.peek(1);
|
|
911
|
+
|
|
912
|
+
if (char === '"' || char === '\'') {
|
|
913
|
+
value += this.readJavaScriptQuotedString(char, line, column);
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (char === '/' && next === '/') {
|
|
918
|
+
value += this.readJavaScriptLineComment();
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if (char === '/' && next === '*') {
|
|
923
|
+
value += this.readJavaScriptBlockComment(line, column);
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
if (char === '`') {
|
|
928
|
+
value += this.readNestedJavaScriptTemplate(line, column);
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
if (char === '{') {
|
|
933
|
+
value += this.advance();
|
|
934
|
+
depth += 1;
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
if (char === '}') {
|
|
939
|
+
value += this.advance();
|
|
940
|
+
depth -= 1;
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
value += this.advance();
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
if (depth !== 0) {
|
|
948
|
+
throw new CompilerError('Unterminated JavaScript template interpolation', line, column);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
return value;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
readJavaScriptQuotedString(quote, line, column) {
|
|
955
|
+
let value = this.advance(); // opening quote
|
|
956
|
+
|
|
957
|
+
while (!this.isAtEnd()) {
|
|
958
|
+
const char = this.advance();
|
|
959
|
+
value += char;
|
|
960
|
+
|
|
961
|
+
if (char === '\\') {
|
|
962
|
+
if (!this.isAtEnd()) {
|
|
963
|
+
value += this.advance();
|
|
964
|
+
}
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
if (char === quote) {
|
|
969
|
+
return value;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
throw new CompilerError('Unterminated JavaScript string literal inside js`...`', line, column);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
readJavaScriptLineComment() {
|
|
977
|
+
let value = this.advance(); // /
|
|
978
|
+
value += this.advance(); // /
|
|
979
|
+
|
|
980
|
+
while (!this.isAtEnd() && this.peek() !== '\n') {
|
|
981
|
+
value += this.advance();
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
return value;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
readJavaScriptBlockComment(line, column) {
|
|
988
|
+
let value = this.advance(); // /
|
|
989
|
+
value += this.advance(); // *
|
|
990
|
+
|
|
991
|
+
while (!this.isAtEnd()) {
|
|
992
|
+
const char = this.advance();
|
|
993
|
+
value += char;
|
|
994
|
+
|
|
995
|
+
if (char === '*' && this.peek() === '/') {
|
|
996
|
+
value += this.advance();
|
|
997
|
+
return value;
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
throw new CompilerError('Unterminated JavaScript block comment inside js`...`', line, column);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
startsScriptCodeBlock() {
|
|
1005
|
+
if (!isIdentifierStart(this.peek())) {
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
let index = this.index;
|
|
1010
|
+
let value = '';
|
|
1011
|
+
|
|
1012
|
+
while (index < this.source.length && isIdentifierPart(this.source[index])) {
|
|
1013
|
+
value += this.source[index];
|
|
1014
|
+
index += 1;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
if (value !== 'code') {
|
|
1018
|
+
return false;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
index = skipWhitespaceAndCommentsFromSource(this.source, index);
|
|
1022
|
+
return this.source[index] === '{';
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
readScriptCodeBlock(line, column) {
|
|
1026
|
+
const identifierToken = this.readIdentifier(line, column);
|
|
1027
|
+
|
|
1028
|
+
if (identifierToken.value !== 'code') {
|
|
1029
|
+
throw new CompilerError('Expected "code" inside ::script', line, column);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
this.skipWhitespaceAndComments();
|
|
1033
|
+
|
|
1034
|
+
if (this.peek() !== '{') {
|
|
1035
|
+
throw new CompilerError('Expected "{" after code inside ::script', this.line, this.column);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
this.advance();
|
|
1039
|
+
const value = this.readRawScriptCodeBody(line, column);
|
|
1040
|
+
return this.makeToken('SCRIPT_CODE', value, line, column);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
readRawScriptCodeBody(line, column) {
|
|
1044
|
+
let value = '';
|
|
1045
|
+
let depth = 1;
|
|
1046
|
+
|
|
1047
|
+
while (!this.isAtEnd() && depth > 0) {
|
|
1048
|
+
const char = this.peek();
|
|
1049
|
+
const next = this.peek(1);
|
|
1050
|
+
|
|
1051
|
+
if (char === '"' || char === '\'') {
|
|
1052
|
+
value += this.readJavaScriptQuotedString(char, line, column);
|
|
1053
|
+
continue;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
if (char === '/' && next === '/') {
|
|
1057
|
+
value += this.readJavaScriptLineComment();
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
if (char === '/' && next === '*') {
|
|
1062
|
+
value += this.readJavaScriptBlockComment(line, column);
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
if (char === '`') {
|
|
1067
|
+
value += this.readNestedJavaScriptTemplate(line, column);
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if (char === '{') {
|
|
1072
|
+
value += this.advance();
|
|
1073
|
+
depth += 1;
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
if (char === '}') {
|
|
1078
|
+
this.advance();
|
|
1079
|
+
depth -= 1;
|
|
1080
|
+
|
|
1081
|
+
if (depth === 0) {
|
|
1082
|
+
return value;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
value += '}';
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
value += this.advance();
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
throw new CompilerError('Unterminated code block inside ::script', line, column);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
readEscapeSequence(line, column, allowBacktick = false) {
|
|
1096
|
+
if (this.isAtEnd()) {
|
|
1097
|
+
throw new CompilerError('Incomplete escape sequence', line, column);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const escaped = this.advance();
|
|
1101
|
+
switch (escaped) {
|
|
1102
|
+
case 'n':
|
|
1103
|
+
return '\n';
|
|
1104
|
+
case 'r':
|
|
1105
|
+
return '\r';
|
|
1106
|
+
case 't':
|
|
1107
|
+
return '\t';
|
|
1108
|
+
case '\\':
|
|
1109
|
+
return '\\';
|
|
1110
|
+
case '"':
|
|
1111
|
+
return '"';
|
|
1112
|
+
case '\'':
|
|
1113
|
+
return '\'';
|
|
1114
|
+
case '`':
|
|
1115
|
+
if (allowBacktick) {
|
|
1116
|
+
return '`';
|
|
1117
|
+
}
|
|
1118
|
+
return '`';
|
|
1119
|
+
default:
|
|
1120
|
+
return escaped;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
makeToken(type, value, line, column) {
|
|
1125
|
+
return { type, value, line, column };
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
peek(offset = 0) {
|
|
1129
|
+
return this.source[this.index + offset];
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
advance() {
|
|
1133
|
+
const char = this.source[this.index++];
|
|
1134
|
+
|
|
1135
|
+
if (char === '\n') {
|
|
1136
|
+
this.line += 1;
|
|
1137
|
+
this.column = 1;
|
|
1138
|
+
} else {
|
|
1139
|
+
this.column += 1;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
return char;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
isAtEnd() {
|
|
1146
|
+
return this.index >= this.source.length;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* PARSER
|
|
1152
|
+
* ------
|
|
1153
|
+
* The parser turns the token stream into an AST. WEB supports:
|
|
1154
|
+
*
|
|
1155
|
+
* 1. Top-level definitions:
|
|
1156
|
+
* define {
|
|
1157
|
+
* @space = "1rem";
|
|
1158
|
+
* Button customButton;
|
|
1159
|
+
* primaryButton extends customButton;
|
|
1160
|
+
* }
|
|
1161
|
+
* 2. Direct assignments: `heroSection.customButton.padding = "1rem";`
|
|
1162
|
+
* 3. Scoped blocks:
|
|
1163
|
+
* heroSection {
|
|
1164
|
+
* display = "grid";
|
|
1165
|
+
* customButton {
|
|
1166
|
+
* padding = "1rem";
|
|
1167
|
+
* }
|
|
1168
|
+
* }
|
|
1169
|
+
* 4. CSS-only style scopes:
|
|
1170
|
+
* featureGrid {
|
|
1171
|
+
* styles {
|
|
1172
|
+
* featureCard {
|
|
1173
|
+
* padding = "1rem";
|
|
1174
|
+
* }
|
|
1175
|
+
* }
|
|
1176
|
+
* }
|
|
1177
|
+
* 5. Pseudo / media / attrs / keyframes blocks:
|
|
1178
|
+
* featureCard {
|
|
1179
|
+
* ::hover {
|
|
1180
|
+
* background = "red";
|
|
1181
|
+
* }
|
|
1182
|
+
* ::attrs {
|
|
1183
|
+
* ariaLabel = "Open feature";
|
|
1184
|
+
* }
|
|
1185
|
+
* }
|
|
1186
|
+
*
|
|
1187
|
+
* Blocks are flattened into ordinary rule statements so later compiler stages
|
|
1188
|
+
* can reason about HTML structure and CSS output separately.
|
|
1189
|
+
*/
|
|
1190
|
+
class Parser {
|
|
1191
|
+
constructor(tokens) {
|
|
1192
|
+
this.tokens = tokens;
|
|
1193
|
+
this.current = 0;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
parseProgram() {
|
|
1197
|
+
const body = [];
|
|
1198
|
+
let sawRule = false;
|
|
1199
|
+
let sawDefineBlock = false;
|
|
1200
|
+
|
|
1201
|
+
while (!this.check('EOF')) {
|
|
1202
|
+
if (!sawRule && !sawDefineBlock && this.isDefineBlockStart()) {
|
|
1203
|
+
body.push(...this.parseDefineBlock());
|
|
1204
|
+
sawDefineBlock = true;
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
if (this.isDefineBlockStart()) {
|
|
1209
|
+
const token = this.peek();
|
|
1210
|
+
const message = sawRule
|
|
1211
|
+
? defineBlockPlacementMessage()
|
|
1212
|
+
: 'Only one top-level define { ... } block is allowed.';
|
|
1213
|
+
throw new CompilerError(message, token.line, token.column);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
if (this.isVariableDeclarationStart()) {
|
|
1217
|
+
const token = this.peek();
|
|
1218
|
+
throw new CompilerError(variableDefineBlockMessage(), token.line, token.column);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
if (this.isDeclarationStart()) {
|
|
1222
|
+
const token = this.peek();
|
|
1223
|
+
throw new CompilerError(typeDefineBlockMessage(), token.line, token.column);
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
if (this.isStyleInheritanceDeclarationStart()) {
|
|
1227
|
+
const token = this.peek();
|
|
1228
|
+
throw new CompilerError(styleInheritanceDefineBlockMessage(), token.line, token.column);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
const statements = this.parseStatement([], 'dom', [], []);
|
|
1232
|
+
body.push(...statements);
|
|
1233
|
+
|
|
1234
|
+
if (statements.some((statement) => !isTopLevelDeclarationStatement(statement))) {
|
|
1235
|
+
sawRule = true;
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
return { type: 'Program', body };
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
parseStatement(basePath, scopeKind = 'dom', selectorParts = [], atRules = []) {
|
|
1243
|
+
if (this.isDefineBlockStart()) {
|
|
1244
|
+
const token = this.peek();
|
|
1245
|
+
throw new CompilerError(defineBlockPlacementMessage(), token.line, token.column);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
if (this.isVariableDeclarationStart()) {
|
|
1249
|
+
const token = this.peek();
|
|
1250
|
+
throw new CompilerError(variableDefineBlockMessage(), token.line, token.column);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
if (this.isDeclarationStart()) {
|
|
1254
|
+
const token = this.peek();
|
|
1255
|
+
throw new CompilerError(typeDefineBlockMessage(), token.line, token.column);
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
if (this.isStyleInheritanceDeclarationStart()) {
|
|
1259
|
+
const token = this.peek();
|
|
1260
|
+
throw new CompilerError(styleInheritanceDefineBlockMessage(), token.line, token.column);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
if (this.isPseudoBlockStart()) {
|
|
1264
|
+
return this.parsePseudoBlock(basePath, scopeKind, selectorParts, atRules);
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
if (this.isGlobalStyleScopeStart()) {
|
|
1268
|
+
return this.parseGlobalStyleScope(basePath, scopeKind, selectorParts, atRules);
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
if (this.isStyleScopeStart()) {
|
|
1272
|
+
return this.parseStyleScope(basePath, selectorParts, atRules);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
return this.parseScopedEntry(basePath, scopeKind, selectorParts, atRules);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
isVariableDeclarationStart() {
|
|
1279
|
+
return this.check('VARIABLE') && this.checkNext('EQUALS');
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
isDefineBlockStart() {
|
|
1283
|
+
return this.check('IDENTIFIER') && this.peek().value === 'define' && this.checkNext('LBRACE');
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
isDeclarationStart() {
|
|
1287
|
+
return (
|
|
1288
|
+
this.check('IDENTIFIER') &&
|
|
1289
|
+
this.checkNext('IDENTIFIER') &&
|
|
1290
|
+
this.checkAhead(2, 'SEMICOLON')
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
isStyleInheritanceDeclarationStart() {
|
|
1295
|
+
return (
|
|
1296
|
+
this.check('IDENTIFIER') &&
|
|
1297
|
+
this.checkNext('IDENTIFIER') &&
|
|
1298
|
+
this.peek(1).value === 'extends' &&
|
|
1299
|
+
this.checkAhead(2, 'IDENTIFIER') &&
|
|
1300
|
+
this.checkAhead(3, 'SEMICOLON')
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
isStyleScopeStart() {
|
|
1305
|
+
return this.check('IDENTIFIER') && this.peek().value === 'styles' && this.checkNext('LBRACE');
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
isPseudoBlockStart() {
|
|
1309
|
+
return this.check('PSEUDO');
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
isGlobalStyleScopeStart() {
|
|
1313
|
+
return (
|
|
1314
|
+
(this.check('STAR') || (this.check('IDENTIFIER') && this.peek().value === 'html')) &&
|
|
1315
|
+
this.checkNext('LBRACE')
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
parseDeclaration() {
|
|
1320
|
+
const baseType = this.consume('IDENTIFIER', 'Expected a base type in declaration');
|
|
1321
|
+
const typeName = this.consume('IDENTIFIER', 'Expected a type name in declaration');
|
|
1322
|
+
this.consume('SEMICOLON', 'Expected ";" after declaration');
|
|
1323
|
+
|
|
1324
|
+
return {
|
|
1325
|
+
type: 'Declaration',
|
|
1326
|
+
baseType: baseType.value,
|
|
1327
|
+
typeName: typeName.value,
|
|
1328
|
+
line: baseType.line,
|
|
1329
|
+
column: baseType.column,
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
parseDefineBlock() {
|
|
1334
|
+
this.consume('IDENTIFIER', 'Expected "define"');
|
|
1335
|
+
this.consume('LBRACE', 'Expected "{" after "define"');
|
|
1336
|
+
const body = [];
|
|
1337
|
+
|
|
1338
|
+
while (!this.check('RBRACE') && !this.check('EOF')) {
|
|
1339
|
+
if (this.isVariableDeclarationStart()) {
|
|
1340
|
+
body.push(this.parseVariableDeclaration());
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
if (this.isDeclarationStart()) {
|
|
1345
|
+
body.push(this.parseDeclaration());
|
|
1346
|
+
continue;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
if (this.isStyleInheritanceDeclarationStart()) {
|
|
1350
|
+
body.push(this.parseStyleInheritanceDeclaration());
|
|
1351
|
+
continue;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
const token = this.peek();
|
|
1355
|
+
throw new CompilerError(
|
|
1356
|
+
'define { ... } only supports variables, type declarations, and style inheritance. Move DOM and CSS rules below the define block.',
|
|
1357
|
+
token.line,
|
|
1358
|
+
token.column
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
if (this.check('EOF')) {
|
|
1363
|
+
const token = this.previous() || this.peek();
|
|
1364
|
+
throw new CompilerError('Unterminated define block', token.line, token.column);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
this.consume('RBRACE', 'Expected "}" after define block');
|
|
1368
|
+
return body;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
parseStyleInheritanceDeclaration() {
|
|
1372
|
+
const derivedName = this.consume('IDENTIFIER', 'Expected a derived style name');
|
|
1373
|
+
const extendsKeyword = this.consume('IDENTIFIER', 'Expected "extends" in style inheritance declaration');
|
|
1374
|
+
|
|
1375
|
+
if (extendsKeyword.value !== 'extends') {
|
|
1376
|
+
throw new CompilerError('Expected "extends" in style inheritance declaration', extendsKeyword.line, extendsKeyword.column);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
const baseName = this.consume('IDENTIFIER', 'Expected a base style name after "extends"');
|
|
1380
|
+
this.consume('SEMICOLON', 'Expected ";" after style inheritance declaration');
|
|
1381
|
+
|
|
1382
|
+
return {
|
|
1383
|
+
type: 'StyleInheritanceDeclaration',
|
|
1384
|
+
derivedName: derivedName.value,
|
|
1385
|
+
baseName: baseName.value,
|
|
1386
|
+
line: derivedName.line,
|
|
1387
|
+
column: derivedName.column,
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
parseVariableDeclaration() {
|
|
1392
|
+
const variableName = this.consume('VARIABLE', 'Expected a variable name');
|
|
1393
|
+
this.consume('EQUALS', 'Expected "=" after variable name');
|
|
1394
|
+
const value = this.parseValue();
|
|
1395
|
+
|
|
1396
|
+
if (value.type === 'JavaScriptLiteral') {
|
|
1397
|
+
throw new CompilerError(
|
|
1398
|
+
'Variables only support simple values and cannot use js`...`',
|
|
1399
|
+
variableName.line,
|
|
1400
|
+
variableName.column
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
this.consume('SEMICOLON', 'Expected ";" after variable declaration');
|
|
1405
|
+
|
|
1406
|
+
return {
|
|
1407
|
+
type: 'VariableDeclaration',
|
|
1408
|
+
name: variableName.value,
|
|
1409
|
+
value,
|
|
1410
|
+
line: variableName.line,
|
|
1411
|
+
column: variableName.column,
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
parseStyleScope(basePath, selectorParts, atRules) {
|
|
1416
|
+
this.consume('IDENTIFIER', 'Expected "styles"');
|
|
1417
|
+
this.consume('LBRACE', 'Expected "{" after "styles"');
|
|
1418
|
+
return this.parseBlock(basePath, 'style', selectorParts, atRules);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
parseGlobalStyleScope(basePath, scopeKind, selectorParts, atRules) {
|
|
1422
|
+
const selectorToken = this.advance();
|
|
1423
|
+
const selectorName = selectorToken.value;
|
|
1424
|
+
|
|
1425
|
+
if (scopeKind !== 'dom' || basePath.length > 0 || selectorParts.length > 0) {
|
|
1426
|
+
throw new CompilerError(globalStyleScopePlacementMessage(), selectorToken.line, selectorToken.column);
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
this.consume('LBRACE', `Expected "{" after "${selectorName}"`);
|
|
1430
|
+
|
|
1431
|
+
return this.parseBlock(
|
|
1432
|
+
[selectorName],
|
|
1433
|
+
'style',
|
|
1434
|
+
[{ type: 'global', value: selectorName }],
|
|
1435
|
+
atRules
|
|
1436
|
+
);
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
parsePseudoBlock(basePath, scopeKind, selectorParts, atRules) {
|
|
1440
|
+
const headerToken = this.consume('PSEUDO', 'Expected a pseudo block');
|
|
1441
|
+
const header = parsePseudoBlockHeader(headerToken.value, headerToken.line, headerToken.column);
|
|
1442
|
+
this.consume('LBRACE', 'Expected "{" after pseudo block header');
|
|
1443
|
+
|
|
1444
|
+
if (header.kind === 'script') {
|
|
1445
|
+
if (basePath.length > 0 || selectorParts.length > 0 || atRules.length > 0 || scopeKind !== 'dom') {
|
|
1446
|
+
throw new CompilerError(scriptBlockPlacementMessage(), headerToken.line, headerToken.column);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
return [this.parseScriptBlock(headerToken)];
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
if (header.kind === 'head') {
|
|
1453
|
+
if (basePath.length > 0 || selectorParts.length > 0 || atRules.length > 0 || scopeKind !== 'dom') {
|
|
1454
|
+
throw new CompilerError(headBlockPlacementMessage(), headerToken.line, headerToken.column);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
return [this.parseHeadBlock(headerToken)];
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
if (header.kind === 'attrs') {
|
|
1461
|
+
return this.parseAttributeScope(basePath, scopeKind, selectorParts, atRules, headerToken);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
if (header.kind === 'selector') {
|
|
1465
|
+
if (selectorParts.length === 0) {
|
|
1466
|
+
throw new CompilerError(
|
|
1467
|
+
'Pseudo selector blocks must be nested inside an element or style scope',
|
|
1468
|
+
headerToken.line,
|
|
1469
|
+
headerToken.column
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
const nextSelectorParts = appendPseudoSelectorPart(selectorParts, header.selector, headerToken);
|
|
1474
|
+
return this.parseBlock(basePath, scopeKind, nextSelectorParts, atRules);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
if (header.kind === 'media') {
|
|
1478
|
+
return this.parseBlock(basePath, scopeKind, selectorParts, [...atRules, `@media ${header.query}`]);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
if (basePath.length > 0 || selectorParts.length > 0 || atRules.length > 0 || scopeKind !== 'dom') {
|
|
1482
|
+
throw new CompilerError(
|
|
1483
|
+
'::keyframes blocks must be declared at the top level outside element and style scopes',
|
|
1484
|
+
headerToken.line,
|
|
1485
|
+
headerToken.column
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
return [this.parseKeyframesRule(header.name, headerToken)];
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
parseScriptBlock(headerToken) {
|
|
1493
|
+
const attributes = [];
|
|
1494
|
+
let code = null;
|
|
1495
|
+
|
|
1496
|
+
while (!this.check('RBRACE') && !this.check('EOF')) {
|
|
1497
|
+
if (this.match('SCRIPT_CODE')) {
|
|
1498
|
+
if (code !== null) {
|
|
1499
|
+
const token = this.previous();
|
|
1500
|
+
throw new CompilerError('::script blocks only support one code { ... } block.', token.line, token.column);
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
code = this.previous().value;
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
attributes.push(this.parseScriptAttribute());
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
if (this.check('EOF')) {
|
|
1511
|
+
const token = this.previous() || this.peek();
|
|
1512
|
+
throw new CompilerError('Unterminated ::script block', token.line, token.column);
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
this.consume('RBRACE', 'Expected "}" after ::script block');
|
|
1516
|
+
|
|
1517
|
+
return {
|
|
1518
|
+
type: 'ScriptBlock',
|
|
1519
|
+
attributes,
|
|
1520
|
+
code,
|
|
1521
|
+
line: headerToken.line,
|
|
1522
|
+
column: headerToken.column,
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
parseHeadBlock(headerToken) {
|
|
1527
|
+
const entries = [];
|
|
1528
|
+
|
|
1529
|
+
while (!this.check('RBRACE') && !this.check('EOF')) {
|
|
1530
|
+
entries.push(this.parseHeadEntry());
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
if (this.check('EOF')) {
|
|
1534
|
+
const token = this.previous() || this.peek();
|
|
1535
|
+
throw new CompilerError('Unterminated ::head block', token.line, token.column);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
this.consume('RBRACE', 'Expected "}" after ::head block');
|
|
1539
|
+
|
|
1540
|
+
return {
|
|
1541
|
+
type: 'HeadBlock',
|
|
1542
|
+
entries,
|
|
1543
|
+
line: headerToken.line,
|
|
1544
|
+
column: headerToken.column,
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
parseHeadEntry() {
|
|
1549
|
+
if (
|
|
1550
|
+
this.isDefineBlockStart() ||
|
|
1551
|
+
this.isVariableDeclarationStart() ||
|
|
1552
|
+
this.isDeclarationStart() ||
|
|
1553
|
+
this.isStyleInheritanceDeclarationStart() ||
|
|
1554
|
+
this.isPseudoBlockStart() ||
|
|
1555
|
+
this.isStyleScopeStart() ||
|
|
1556
|
+
this.isGlobalStyleScopeStart() ||
|
|
1557
|
+
!this.check('IDENTIFIER')
|
|
1558
|
+
) {
|
|
1559
|
+
const token = this.peek();
|
|
1560
|
+
throw new CompilerError(headBlockContentMessage(), token.line, token.column);
|
|
1561
|
+
}
|
|
1562
|
+
|
|
1563
|
+
const tagToken = this.consume('IDENTIFIER', 'Expected "meta" or "link" inside ::head');
|
|
1564
|
+
const normalizedTagName = camelToKebab(tagToken.value);
|
|
1565
|
+
|
|
1566
|
+
if (normalizedTagName !== 'meta' && normalizedTagName !== 'link') {
|
|
1567
|
+
throw new CompilerError(headBlockContentMessage(), tagToken.line, tagToken.column);
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
this.consume('LBRACE', `Expected "{" after "${tagToken.value}" inside ::head`);
|
|
1571
|
+
|
|
1572
|
+
const attributes = [];
|
|
1573
|
+
|
|
1574
|
+
while (!this.check('RBRACE') && !this.check('EOF')) {
|
|
1575
|
+
attributes.push(this.parseLooseHtmlAttributeAssignment({
|
|
1576
|
+
invalidMessage: headEntryContentMessage(normalizedTagName),
|
|
1577
|
+
expectedNameMessage: `Expected an HTML attribute name inside ${normalizedTagName} { ... }`,
|
|
1578
|
+
equalsMessage: `Expected "=" after HTML attribute name inside ${normalizedTagName} { ... }`,
|
|
1579
|
+
semicolonMessage: `Expected ";" after HTML attribute assignment inside ${normalizedTagName} { ... }`,
|
|
1580
|
+
}));
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
if (this.check('EOF')) {
|
|
1584
|
+
const token = this.previous() || this.peek();
|
|
1585
|
+
throw new CompilerError(`Unterminated ${normalizedTagName} block inside ::head`, token.line, token.column);
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
this.consume('RBRACE', `Expected "}" after ${normalizedTagName} block inside ::head`);
|
|
1589
|
+
|
|
1590
|
+
return {
|
|
1591
|
+
tagName: normalizedTagName,
|
|
1592
|
+
attributes,
|
|
1593
|
+
line: tagToken.line,
|
|
1594
|
+
column: tagToken.column,
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
parseScriptAttribute() {
|
|
1599
|
+
return this.parseLooseHtmlAttributeAssignment({
|
|
1600
|
+
invalidMessage: scriptBlockContentMessage(),
|
|
1601
|
+
expectedNameMessage: 'Expected an HTML attribute name inside ::script',
|
|
1602
|
+
equalsMessage: 'Expected "=" after HTML attribute name inside ::script',
|
|
1603
|
+
semicolonMessage: 'Expected ";" after ::script attribute assignment',
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
parseLooseHtmlAttributeAssignment(messages) {
|
|
1608
|
+
if (
|
|
1609
|
+
this.isDefineBlockStart() ||
|
|
1610
|
+
this.isVariableDeclarationStart() ||
|
|
1611
|
+
this.isDeclarationStart() ||
|
|
1612
|
+
this.isStyleInheritanceDeclarationStart() ||
|
|
1613
|
+
this.isPseudoBlockStart() ||
|
|
1614
|
+
this.isStyleScopeStart() ||
|
|
1615
|
+
this.isGlobalStyleScopeStart() ||
|
|
1616
|
+
!this.check('IDENTIFIER')
|
|
1617
|
+
) {
|
|
1618
|
+
const token = this.peek();
|
|
1619
|
+
throw new CompilerError(messages.invalidMessage, token.line, token.column);
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
const attributeName = this.consume('IDENTIFIER', messages.expectedNameMessage);
|
|
1623
|
+
|
|
1624
|
+
if (this.check('DOT') || this.check('LBRACE')) {
|
|
1625
|
+
const token = this.peek();
|
|
1626
|
+
throw new CompilerError(messages.invalidMessage, token.line, token.column);
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
this.consume('EQUALS', messages.equalsMessage);
|
|
1630
|
+
const value = this.parseValue();
|
|
1631
|
+
this.consume('SEMICOLON', messages.semicolonMessage);
|
|
1632
|
+
|
|
1633
|
+
return {
|
|
1634
|
+
name: attributeName.value,
|
|
1635
|
+
value,
|
|
1636
|
+
line: attributeName.line,
|
|
1637
|
+
column: attributeName.column,
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
parseAttributeScope(basePath, scopeKind, selectorParts, atRules, headerToken) {
|
|
1642
|
+
if (
|
|
1643
|
+
scopeKind !== 'dom' ||
|
|
1644
|
+
basePath.length === 0 ||
|
|
1645
|
+
atRules.length > 0 ||
|
|
1646
|
+
selectorParts.length === 0 ||
|
|
1647
|
+
selectorParts.some((part) => part.type !== 'segment')
|
|
1648
|
+
) {
|
|
1649
|
+
throw new CompilerError(attrsBlockPlacementMessage(), headerToken.line, headerToken.column);
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
const body = [];
|
|
1653
|
+
|
|
1654
|
+
while (!this.check('RBRACE') && !this.check('EOF')) {
|
|
1655
|
+
body.push(this.parseAttributeAssignment(basePath));
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
if (this.check('EOF')) {
|
|
1659
|
+
const token = this.previous() || this.peek();
|
|
1660
|
+
throw new CompilerError(`Unterminated ::attrs block for "${basePath.join('.')}"`, token.line, token.column);
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
this.consume('RBRACE', 'Expected "}" after ::attrs block');
|
|
1664
|
+
return body;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
parseAttributeAssignment(basePath) {
|
|
1668
|
+
if (
|
|
1669
|
+
this.isDefineBlockStart() ||
|
|
1670
|
+
this.isVariableDeclarationStart() ||
|
|
1671
|
+
this.isDeclarationStart() ||
|
|
1672
|
+
this.isStyleInheritanceDeclarationStart() ||
|
|
1673
|
+
this.isPseudoBlockStart() ||
|
|
1674
|
+
this.isStyleScopeStart() ||
|
|
1675
|
+
!this.check('IDENTIFIER')
|
|
1676
|
+
) {
|
|
1677
|
+
const token = this.peek();
|
|
1678
|
+
throw new CompilerError(attrsBlockContentMessage(), token.line, token.column);
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
const attributeName = this.consume('IDENTIFIER', 'Expected an HTML attribute name inside ::attrs');
|
|
1682
|
+
|
|
1683
|
+
if (this.check('DOT') || this.check('LBRACE')) {
|
|
1684
|
+
const token = this.peek();
|
|
1685
|
+
throw new CompilerError(attrsBlockContentMessage(), token.line, token.column);
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
this.consume('EQUALS', 'Expected "=" after HTML attribute name inside ::attrs');
|
|
1689
|
+
const value = this.parseValue();
|
|
1690
|
+
this.consume('SEMICOLON', 'Expected ";" after HTML attribute assignment');
|
|
1691
|
+
|
|
1692
|
+
return [{
|
|
1693
|
+
type: 'Assignment',
|
|
1694
|
+
path: [...basePath, attributeName.value],
|
|
1695
|
+
scopeKind: 'attr',
|
|
1696
|
+
selectorParts: [],
|
|
1697
|
+
atRules: [],
|
|
1698
|
+
value,
|
|
1699
|
+
line: attributeName.line,
|
|
1700
|
+
column: attributeName.column,
|
|
1701
|
+
}][0];
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
parseScopedEntry(basePath, scopeKind = 'dom', selectorParts = [], atRules = []) {
|
|
1705
|
+
const pathResult = this.parsePath('Expected an identifier at the start of a statement');
|
|
1706
|
+
const fullPath = [...basePath, ...pathResult.segments];
|
|
1707
|
+
const blockSelectorParts = appendSelectorSegments(selectorParts, pathResult.segments);
|
|
1708
|
+
|
|
1709
|
+
if (this.match('LBRACE')) {
|
|
1710
|
+
return this.parseBlock(fullPath, scopeKind, blockSelectorParts, atRules);
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
this.consume('EQUALS', 'Expected "=" or "{" after path');
|
|
1714
|
+
const value = this.parseValue();
|
|
1715
|
+
this.consume('SEMICOLON', 'Expected ";" after assignment');
|
|
1716
|
+
|
|
1717
|
+
return [{
|
|
1718
|
+
type: 'Assignment',
|
|
1719
|
+
path: fullPath,
|
|
1720
|
+
scopeKind,
|
|
1721
|
+
selectorParts: appendSelectorSegments(selectorParts, pathResult.segments.slice(0, -1)),
|
|
1722
|
+
atRules: [...atRules],
|
|
1723
|
+
value,
|
|
1724
|
+
line: pathResult.first.line,
|
|
1725
|
+
column: pathResult.first.column,
|
|
1726
|
+
}];
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
parseBlock(scopePath, scopeKind = 'dom', selectorParts = [], atRules = []) {
|
|
1730
|
+
const body = [];
|
|
1731
|
+
|
|
1732
|
+
while (!this.check('RBRACE') && !this.check('EOF')) {
|
|
1733
|
+
body.push(...this.parseStatement(scopePath, scopeKind, selectorParts, atRules));
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
if (this.check('EOF')) {
|
|
1737
|
+
const token = this.previous() || this.peek();
|
|
1738
|
+
throw new CompilerError(
|
|
1739
|
+
`Unterminated block for "${scopePath.join('.')}"`,
|
|
1740
|
+
token.line,
|
|
1741
|
+
token.column
|
|
1742
|
+
);
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
this.consume('RBRACE', 'Expected "}" after block');
|
|
1746
|
+
return body;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
parseKeyframesRule(name, headerToken) {
|
|
1750
|
+
const stages = [];
|
|
1751
|
+
|
|
1752
|
+
while (!this.check('RBRACE') && !this.check('EOF')) {
|
|
1753
|
+
stages.push(this.parseKeyframesStage());
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
if (this.check('EOF')) {
|
|
1757
|
+
const token = this.previous() || this.peek();
|
|
1758
|
+
throw new CompilerError(`Unterminated ::keyframes block for "${name}"`, token.line, token.column);
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
this.consume('RBRACE', 'Expected "}" after ::keyframes block');
|
|
1762
|
+
|
|
1763
|
+
return {
|
|
1764
|
+
type: 'KeyframesRule',
|
|
1765
|
+
name,
|
|
1766
|
+
stages,
|
|
1767
|
+
line: headerToken.line,
|
|
1768
|
+
column: headerToken.column,
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
parseKeyframesStage() {
|
|
1773
|
+
const selectorToken = this.advanceKeyframeStageToken();
|
|
1774
|
+
const selector = normalizeKeyframeStageSelector(selectorToken);
|
|
1775
|
+
this.consume('LBRACE', 'Expected "{" after keyframe stage');
|
|
1776
|
+
|
|
1777
|
+
const declarations = [];
|
|
1778
|
+
|
|
1779
|
+
while (!this.check('RBRACE') && !this.check('EOF')) {
|
|
1780
|
+
const propertyToken = this.consume('IDENTIFIER', 'Expected a CSS property inside a keyframe stage');
|
|
1781
|
+
const propertyName = propertyToken.value;
|
|
1782
|
+
|
|
1783
|
+
if (propertyName === 'raw') {
|
|
1784
|
+
throw new CompilerError(
|
|
1785
|
+
'Keyframe stages only support CSS property assignments; raw is not supported here',
|
|
1786
|
+
propertyToken.line,
|
|
1787
|
+
propertyToken.column
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
if (propertyName === 'textContent' || propertyName === 'innerHTML') {
|
|
1792
|
+
throw new CompilerError(
|
|
1793
|
+
'Keyframe stages only support CSS property assignments',
|
|
1794
|
+
propertyToken.line,
|
|
1795
|
+
propertyToken.column
|
|
1796
|
+
);
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
this.consume('EQUALS', 'Expected "=" after keyframe property name');
|
|
1800
|
+
const value = this.parseValue();
|
|
1801
|
+
this.consume('SEMICOLON', 'Expected ";" after keyframe declaration');
|
|
1802
|
+
|
|
1803
|
+
declarations.push({
|
|
1804
|
+
propertyName,
|
|
1805
|
+
value,
|
|
1806
|
+
line: propertyToken.line,
|
|
1807
|
+
column: propertyToken.column,
|
|
1808
|
+
});
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
if (this.check('EOF')) {
|
|
1812
|
+
const token = this.previous() || this.peek();
|
|
1813
|
+
throw new CompilerError(`Unterminated keyframe stage "${selector}"`, token.line, token.column);
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
this.consume('RBRACE', 'Expected "}" after keyframe stage');
|
|
1817
|
+
|
|
1818
|
+
return {
|
|
1819
|
+
selector,
|
|
1820
|
+
declarations,
|
|
1821
|
+
line: selectorToken.line,
|
|
1822
|
+
column: selectorToken.column,
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
advanceKeyframeStageToken() {
|
|
1827
|
+
if (this.check('IDENTIFIER') || this.check('PERCENTAGE')) {
|
|
1828
|
+
return this.advance();
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
const token = this.peek();
|
|
1832
|
+
throw new CompilerError(
|
|
1833
|
+
'Expected a keyframe stage such as "from", "to", or "50%"',
|
|
1834
|
+
token.line,
|
|
1835
|
+
token.column,
|
|
1836
|
+
{ actual: describeTokenForDiagnostic(token) }
|
|
1837
|
+
);
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
parsePath(message) {
|
|
1841
|
+
const segments = [];
|
|
1842
|
+
const first = this.consume('IDENTIFIER', message);
|
|
1843
|
+
segments.push(first.value);
|
|
1844
|
+
|
|
1845
|
+
while (this.match('DOT')) {
|
|
1846
|
+
const segment = this.consume('IDENTIFIER', 'Expected an identifier after "."');
|
|
1847
|
+
segments.push(segment.value);
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
return { segments, first };
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
parseValue() {
|
|
1854
|
+
if (this.match('JAVASCRIPT')) {
|
|
1855
|
+
return { type: 'JavaScriptLiteral', value: this.previous().value };
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1858
|
+
if (this.match('VARIABLE')) {
|
|
1859
|
+
const token = this.previous();
|
|
1860
|
+
return {
|
|
1861
|
+
type: 'VariableReferenceLiteral',
|
|
1862
|
+
value: token.value,
|
|
1863
|
+
line: token.line,
|
|
1864
|
+
column: token.column,
|
|
1865
|
+
};
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
if (this.match('STRING')) {
|
|
1869
|
+
return { type: 'StringLiteral', value: this.previous().value };
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
if (this.match('TEMPLATE')) {
|
|
1873
|
+
return { type: 'TemplateLiteral', value: this.previous().value };
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
if (this.match('PERCENTAGE')) {
|
|
1877
|
+
return { type: 'PercentageLiteral', value: this.previous().value };
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
if (this.match('NUMBER')) {
|
|
1881
|
+
return { type: 'NumberLiteral', value: this.previous().value };
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
if (this.match('IDENTIFIER')) {
|
|
1885
|
+
return { type: 'IdentifierLiteral', value: this.previous().value };
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
const token = this.peek();
|
|
1889
|
+
throw new CompilerError(`Unexpected value token "${token.type}"`, token.line, token.column, {
|
|
1890
|
+
actual: describeTokenForDiagnostic(token),
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
match(type) {
|
|
1895
|
+
if (this.check(type)) {
|
|
1896
|
+
this.advance();
|
|
1897
|
+
return true;
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
return false;
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
consume(type, message) {
|
|
1904
|
+
if (this.check(type)) {
|
|
1905
|
+
return this.advance();
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
const token = this.peek();
|
|
1909
|
+
throw new CompilerError(message, token.line, token.column, {
|
|
1910
|
+
actual: describeTokenForDiagnostic(token),
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
check(type) {
|
|
1915
|
+
return this.peek().type === type;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
checkNext(type) {
|
|
1919
|
+
return this.peek(1).type === type;
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
checkAhead(offset, type) {
|
|
1923
|
+
return this.peek(offset).type === type;
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
advance() {
|
|
1927
|
+
if (!this.isAtEnd()) {
|
|
1928
|
+
this.current += 1;
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
return this.previous();
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
isAtEnd() {
|
|
1935
|
+
return this.peek().type === 'EOF';
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
peek(offset = 0) {
|
|
1939
|
+
return this.tokens[this.current + offset];
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
previous() {
|
|
1943
|
+
return this.tokens[this.current - 1];
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
function isTopLevelDeclarationStatement(statement) {
|
|
1948
|
+
return (
|
|
1949
|
+
statement.type === 'VariableDeclaration' ||
|
|
1950
|
+
statement.type === 'Declaration' ||
|
|
1951
|
+
statement.type === 'StyleInheritanceDeclaration'
|
|
1952
|
+
);
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
const CSS_PSEUDO_ELEMENTS = new Set([
|
|
1956
|
+
'after',
|
|
1957
|
+
'backdrop',
|
|
1958
|
+
'before',
|
|
1959
|
+
'cue',
|
|
1960
|
+
'cue-region',
|
|
1961
|
+
'file-selector-button',
|
|
1962
|
+
'first-letter',
|
|
1963
|
+
'first-line',
|
|
1964
|
+
'grammar-error',
|
|
1965
|
+
'marker',
|
|
1966
|
+
'part',
|
|
1967
|
+
'placeholder',
|
|
1968
|
+
'selection',
|
|
1969
|
+
'slotted',
|
|
1970
|
+
'spelling-error',
|
|
1971
|
+
'target-text',
|
|
1972
|
+
]);
|
|
1973
|
+
|
|
1974
|
+
function parsePseudoBlockHeader(value, line, column) {
|
|
1975
|
+
const trimmed = value.trim();
|
|
1976
|
+
const keywordMatch = trimmed.match(/^([A-Za-z][A-Za-z0-9_-]*)([\s\S]*)$/);
|
|
1977
|
+
|
|
1978
|
+
if (!keywordMatch) {
|
|
1979
|
+
throw new CompilerError('Invalid pseudo block header', line, column);
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
const keyword = camelToKebab(keywordMatch[1]);
|
|
1983
|
+
const remainder = keywordMatch[2].trim();
|
|
1984
|
+
|
|
1985
|
+
if (keyword === 'media') {
|
|
1986
|
+
if (!remainder) {
|
|
1987
|
+
throw new CompilerError('Expected a media query after "::media"', line, column);
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
return {
|
|
1991
|
+
kind: 'media',
|
|
1992
|
+
query: remainder,
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
if (keyword === 'attrs') {
|
|
1997
|
+
if (remainder) {
|
|
1998
|
+
throw new CompilerError('::attrs does not accept a selector, query, or name', line, column);
|
|
1999
|
+
}
|
|
2000
|
+
|
|
2001
|
+
return {
|
|
2002
|
+
kind: 'attrs',
|
|
2003
|
+
};
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
if (keyword === 'script') {
|
|
2007
|
+
if (remainder) {
|
|
2008
|
+
throw new CompilerError('::script does not accept a selector, query, or name', line, column);
|
|
2009
|
+
}
|
|
2010
|
+
|
|
2011
|
+
return {
|
|
2012
|
+
kind: 'script',
|
|
2013
|
+
};
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
if (keyword === 'head') {
|
|
2017
|
+
if (remainder) {
|
|
2018
|
+
throw new CompilerError('::head does not accept a selector, query, or name', line, column);
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
return {
|
|
2022
|
+
kind: 'head',
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
if (keyword === 'keyframes') {
|
|
2027
|
+
if (!remainder) {
|
|
2028
|
+
throw new CompilerError('Expected a keyframes name after "::keyframes"', line, column);
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
return {
|
|
2032
|
+
kind: 'keyframes',
|
|
2033
|
+
name: remainder,
|
|
2034
|
+
};
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
const parenIndex = trimmed.indexOf('(');
|
|
2038
|
+
const name = parenIndex === -1 ? trimmed : trimmed.slice(0, parenIndex).trim();
|
|
2039
|
+
const suffix = parenIndex === -1 ? '' : trimmed.slice(parenIndex).trim();
|
|
2040
|
+
const normalizedName = camelToKebab(name);
|
|
2041
|
+
const prefix = CSS_PSEUDO_ELEMENTS.has(normalizedName) ? '::' : ':';
|
|
2042
|
+
|
|
2043
|
+
return {
|
|
2044
|
+
kind: 'selector',
|
|
2045
|
+
selector: `${prefix}${normalizedName}${suffix}`,
|
|
2046
|
+
};
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
function appendSelectorSegments(selectorParts, segments) {
|
|
2050
|
+
const additions = segments.map((segment) => ({
|
|
2051
|
+
type: 'segment',
|
|
2052
|
+
value: segment,
|
|
2053
|
+
}));
|
|
2054
|
+
|
|
2055
|
+
return [...selectorParts, ...additions];
|
|
2056
|
+
}
|
|
2057
|
+
|
|
2058
|
+
function appendPseudoSelectorPart(selectorParts, selector, token) {
|
|
2059
|
+
if (selectorParts.length === 0) {
|
|
2060
|
+
throw new CompilerError(
|
|
2061
|
+
'Pseudo selector blocks must be nested inside an element or style scope',
|
|
2062
|
+
token.line,
|
|
2063
|
+
token.column
|
|
2064
|
+
);
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
return [
|
|
2068
|
+
...selectorParts,
|
|
2069
|
+
{
|
|
2070
|
+
type: 'pseudo',
|
|
2071
|
+
value: selector,
|
|
2072
|
+
},
|
|
2073
|
+
];
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
function normalizeKeyframeStageSelector(token) {
|
|
2077
|
+
if (token.type === 'PERCENTAGE') {
|
|
2078
|
+
return token.value;
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
const normalized = camelToKebab(token.value);
|
|
2082
|
+
|
|
2083
|
+
if (normalized === 'from' || normalized === 'to') {
|
|
2084
|
+
return normalized;
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
throw new CompilerError(
|
|
2088
|
+
'Keyframe stages must use "from", "to", or percentages like "50%"',
|
|
2089
|
+
token.line,
|
|
2090
|
+
token.column
|
|
2091
|
+
);
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
/**
|
|
2095
|
+
* SEMANTIC ANALYSIS
|
|
2096
|
+
* -----------------
|
|
2097
|
+
* Type declarations form a tiny symbol table so WEB types can inherit from
|
|
2098
|
+
* previously declared custom types:
|
|
2099
|
+
*
|
|
2100
|
+
* Button baseButton;
|
|
2101
|
+
* baseButton ctaButton;
|
|
2102
|
+
*
|
|
2103
|
+
* The symbol table resolves that `ctaButton` ultimately inherits from
|
|
2104
|
+
* `Button`, which lets the HTML generator infer the correct `<button>` tag.
|
|
2105
|
+
*/
|
|
2106
|
+
function buildSymbolTable(ast) {
|
|
2107
|
+
const declarations = new Map();
|
|
2108
|
+
|
|
2109
|
+
for (const statement of ast.body) {
|
|
2110
|
+
if (statement.type !== 'Declaration') {
|
|
2111
|
+
continue;
|
|
2112
|
+
}
|
|
2113
|
+
|
|
2114
|
+
if (declarations.has(statement.typeName)) {
|
|
2115
|
+
throw new CompilerError(
|
|
2116
|
+
`Duplicate type declaration for "${statement.typeName}"`,
|
|
2117
|
+
statement.line,
|
|
2118
|
+
statement.column
|
|
2119
|
+
);
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
declarations.set(statement.typeName, {
|
|
2123
|
+
name: statement.typeName,
|
|
2124
|
+
baseType: statement.baseType,
|
|
2125
|
+
resolvedBaseType: null,
|
|
2126
|
+
declaration: statement,
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
const resolving = new Set();
|
|
2131
|
+
|
|
2132
|
+
function resolveBaseType(typeName) {
|
|
2133
|
+
const entry = declarations.get(typeName);
|
|
2134
|
+
|
|
2135
|
+
// If the type isn't declared, treat it as a built-in / external base.
|
|
2136
|
+
// That keeps the compiler permissive and lets unknown names fall back to
|
|
2137
|
+
// the default tag inference logic.
|
|
2138
|
+
if (!entry) {
|
|
2139
|
+
return typeName;
|
|
2140
|
+
}
|
|
2141
|
+
|
|
2142
|
+
if (entry.resolvedBaseType) {
|
|
2143
|
+
return entry.resolvedBaseType;
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
if (resolving.has(typeName)) {
|
|
2147
|
+
const { line, column } = entry.declaration;
|
|
2148
|
+
throw new CompilerError(`Circular type inheritance involving "${typeName}"`, line, column);
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
resolving.add(typeName);
|
|
2152
|
+
entry.resolvedBaseType = resolveBaseType(entry.baseType);
|
|
2153
|
+
resolving.delete(typeName);
|
|
2154
|
+
|
|
2155
|
+
return entry.resolvedBaseType;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
for (const typeName of declarations.keys()) {
|
|
2159
|
+
resolveBaseType(typeName);
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
return {
|
|
2163
|
+
has(typeName) {
|
|
2164
|
+
return declarations.has(typeName);
|
|
2165
|
+
},
|
|
2166
|
+
|
|
2167
|
+
get(typeName) {
|
|
2168
|
+
return declarations.get(typeName) || null;
|
|
2169
|
+
},
|
|
2170
|
+
|
|
2171
|
+
resolveBaseType(typeName) {
|
|
2172
|
+
return resolveBaseType(typeName);
|
|
2173
|
+
},
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
/**
|
|
2178
|
+
* STYLE INHERITANCE ANALYSIS
|
|
2179
|
+
* --------------------------
|
|
2180
|
+
* Style inheritance declarations are independent from semantic type
|
|
2181
|
+
* declarations:
|
|
2182
|
+
*
|
|
2183
|
+
* storeCard extends card;
|
|
2184
|
+
*
|
|
2185
|
+
* This affects CSS selector inheritance only. It does not change which HTML
|
|
2186
|
+
* tag a node renders as.
|
|
2187
|
+
*/
|
|
2188
|
+
function buildStyleInheritanceTable(ast) {
|
|
2189
|
+
const declarations = new Map();
|
|
2190
|
+
const directDescendants = new Map();
|
|
2191
|
+
let orderCounter = 0;
|
|
2192
|
+
|
|
2193
|
+
for (const statement of ast.body) {
|
|
2194
|
+
if (statement.type !== 'StyleInheritanceDeclaration') {
|
|
2195
|
+
continue;
|
|
2196
|
+
}
|
|
2197
|
+
|
|
2198
|
+
if (declarations.has(statement.derivedName)) {
|
|
2199
|
+
throw new CompilerError(
|
|
2200
|
+
`Duplicate style inheritance declaration for "${statement.derivedName}"`,
|
|
2201
|
+
statement.line,
|
|
2202
|
+
statement.column
|
|
2203
|
+
);
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
const entry = {
|
|
2207
|
+
name: statement.derivedName,
|
|
2208
|
+
baseName: statement.baseName,
|
|
2209
|
+
declaration: statement,
|
|
2210
|
+
order: orderCounter++,
|
|
2211
|
+
resolvedBaseChain: null,
|
|
2212
|
+
};
|
|
2213
|
+
|
|
2214
|
+
declarations.set(statement.derivedName, entry);
|
|
2215
|
+
|
|
2216
|
+
if (!directDescendants.has(statement.baseName)) {
|
|
2217
|
+
directDescendants.set(statement.baseName, []);
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
directDescendants.get(statement.baseName).push(entry);
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
const resolving = new Set();
|
|
2224
|
+
|
|
2225
|
+
function resolveBaseChain(name) {
|
|
2226
|
+
const entry = declarations.get(name);
|
|
2227
|
+
|
|
2228
|
+
if (!entry) {
|
|
2229
|
+
return [];
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
if (entry.resolvedBaseChain) {
|
|
2233
|
+
return entry.resolvedBaseChain;
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
if (resolving.has(name)) {
|
|
2237
|
+
const { line, column } = entry.declaration;
|
|
2238
|
+
throw new CompilerError(`Circular style inheritance involving "${name}"`, line, column);
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
resolving.add(name);
|
|
2242
|
+
entry.resolvedBaseChain = [entry.baseName, ...resolveBaseChain(entry.baseName)];
|
|
2243
|
+
resolving.delete(name);
|
|
2244
|
+
|
|
2245
|
+
return entry.resolvedBaseChain;
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
for (const name of declarations.keys()) {
|
|
2249
|
+
resolveBaseChain(name);
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
const descendantsCache = new Map();
|
|
2253
|
+
|
|
2254
|
+
function getDescendants(name) {
|
|
2255
|
+
if (descendantsCache.has(name)) {
|
|
2256
|
+
return descendantsCache.get(name);
|
|
2257
|
+
}
|
|
2258
|
+
|
|
2259
|
+
const direct = directDescendants.get(name) || [];
|
|
2260
|
+
const descendants = [];
|
|
2261
|
+
|
|
2262
|
+
for (const entry of direct) {
|
|
2263
|
+
descendants.push({
|
|
2264
|
+
name: entry.name,
|
|
2265
|
+
distance: 1,
|
|
2266
|
+
order: entry.order,
|
|
2267
|
+
});
|
|
2268
|
+
|
|
2269
|
+
for (const descendant of getDescendants(entry.name)) {
|
|
2270
|
+
descendants.push({
|
|
2271
|
+
name: descendant.name,
|
|
2272
|
+
distance: descendant.distance + 1,
|
|
2273
|
+
order: descendant.order,
|
|
2274
|
+
});
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
descendantsCache.set(name, descendants);
|
|
2279
|
+
return descendants;
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
return {
|
|
2283
|
+
has(name) {
|
|
2284
|
+
return declarations.has(name);
|
|
2285
|
+
},
|
|
2286
|
+
|
|
2287
|
+
get(name) {
|
|
2288
|
+
return declarations.get(name) || null;
|
|
2289
|
+
},
|
|
2290
|
+
|
|
2291
|
+
getBaseChain(name) {
|
|
2292
|
+
return resolveBaseChain(name);
|
|
2293
|
+
},
|
|
2294
|
+
|
|
2295
|
+
getExpansionOptions(name) {
|
|
2296
|
+
return [
|
|
2297
|
+
{
|
|
2298
|
+
name,
|
|
2299
|
+
distance: 0,
|
|
2300
|
+
order: -1,
|
|
2301
|
+
},
|
|
2302
|
+
...getDescendants(name),
|
|
2303
|
+
];
|
|
2304
|
+
},
|
|
2305
|
+
|
|
2306
|
+
hasDeclarations() {
|
|
2307
|
+
return declarations.size > 0;
|
|
2308
|
+
},
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
function buildVariableTable(ast) {
|
|
2313
|
+
const declarations = new Map();
|
|
2314
|
+
|
|
2315
|
+
for (const statement of ast.body) {
|
|
2316
|
+
if (statement.type !== 'VariableDeclaration') {
|
|
2317
|
+
continue;
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
if (declarations.has(statement.name)) {
|
|
2321
|
+
throw new CompilerError(
|
|
2322
|
+
`Duplicate variable declaration for "${statement.name}"`,
|
|
2323
|
+
statement.line,
|
|
2324
|
+
statement.column
|
|
2325
|
+
);
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
declarations.set(statement.name, {
|
|
2329
|
+
name: statement.name,
|
|
2330
|
+
value: statement.value,
|
|
2331
|
+
line: statement.line,
|
|
2332
|
+
column: statement.column,
|
|
2333
|
+
resolvedValue: undefined,
|
|
2334
|
+
});
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
const resolving = new Set();
|
|
2338
|
+
|
|
2339
|
+
function resolveVariableValue(literal) {
|
|
2340
|
+
switch (literal.type) {
|
|
2341
|
+
case 'StringLiteral':
|
|
2342
|
+
case 'TemplateLiteral':
|
|
2343
|
+
case 'IdentifierLiteral':
|
|
2344
|
+
case 'PercentageLiteral':
|
|
2345
|
+
return literal.value;
|
|
2346
|
+
case 'NumberLiteral':
|
|
2347
|
+
return String(literal.value);
|
|
2348
|
+
case 'VariableReferenceLiteral':
|
|
2349
|
+
return resolve(literal.value, literal.line, literal.column);
|
|
2350
|
+
default:
|
|
2351
|
+
throw new CompilerError(`Unsupported variable value type "${literal.type}"`);
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
function resolve(name, useLine, useColumn) {
|
|
2356
|
+
const entry = declarations.get(name);
|
|
2357
|
+
|
|
2358
|
+
if (!entry) {
|
|
2359
|
+
throw new CompilerError(`Unknown variable "${name}"`, useLine, useColumn);
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
if (entry.resolvedValue !== undefined) {
|
|
2363
|
+
return entry.resolvedValue;
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
if (resolving.has(name)) {
|
|
2367
|
+
throw new CompilerError(`Circular variable reference involving "${name}"`, entry.line, entry.column);
|
|
2368
|
+
}
|
|
2369
|
+
|
|
2370
|
+
resolving.add(name);
|
|
2371
|
+
entry.resolvedValue = resolveVariableValue(entry.value);
|
|
2372
|
+
resolving.delete(name);
|
|
2373
|
+
|
|
2374
|
+
return entry.resolvedValue;
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
for (const name of declarations.keys()) {
|
|
2378
|
+
resolve(name);
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
return {
|
|
2382
|
+
has(name) {
|
|
2383
|
+
return declarations.has(name);
|
|
2384
|
+
},
|
|
2385
|
+
|
|
2386
|
+
get(name) {
|
|
2387
|
+
return declarations.get(name) || null;
|
|
2388
|
+
},
|
|
2389
|
+
|
|
2390
|
+
resolve(name, line, column) {
|
|
2391
|
+
return resolve(name, line, column);
|
|
2392
|
+
},
|
|
2393
|
+
};
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
function assignmentAffectsHtmlStructure(statement) {
|
|
2397
|
+
return (
|
|
2398
|
+
statement.type === 'Assignment' &&
|
|
2399
|
+
(statement.scopeKind === 'dom' || statement.scopeKind === 'attr') &&
|
|
2400
|
+
!assignmentUsesCssOnlyContext(statement)
|
|
2401
|
+
);
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
function assignmentUsesCssOnlyContext(statement) {
|
|
2405
|
+
return statement.scopeKind === 'style' || statement.atRules.length > 0 || statementHasPseudoSelector(statement);
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
function statementHasPseudoSelector(statement) {
|
|
2409
|
+
return statement.selectorParts.some((part) => part.type === 'pseudo');
|
|
2410
|
+
}
|
|
2411
|
+
|
|
2412
|
+
/**
|
|
2413
|
+
* DOCUMENT MODEL
|
|
2414
|
+
* --------------
|
|
2415
|
+
* WEB describes the DOM through flat dot-notation assignments, so this step
|
|
2416
|
+
* rebuilds the nested tree structure:
|
|
2417
|
+
*
|
|
2418
|
+
* heroSection.customButton.textContent = "Buy";
|
|
2419
|
+
*
|
|
2420
|
+
* becomes:
|
|
2421
|
+
* heroSection (root)
|
|
2422
|
+
* customButton (child of heroSection)
|
|
2423
|
+
*
|
|
2424
|
+
* Each unique path in the source maps to one element instance in the output DOM.
|
|
2425
|
+
*/
|
|
2426
|
+
function buildDocumentModel(ast, symbolTable, resolveContext) {
|
|
2427
|
+
let orderCounter = 0;
|
|
2428
|
+
|
|
2429
|
+
const root = createNode('__root__', [], orderCounter++, symbolTable, true);
|
|
2430
|
+
|
|
2431
|
+
for (const statement of ast.body) {
|
|
2432
|
+
if (statement.type !== 'Assignment' || !assignmentAffectsHtmlStructure(statement)) {
|
|
2433
|
+
continue;
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
if (statement.path.length < 2) {
|
|
2437
|
+
throw new CompilerError(
|
|
2438
|
+
'Assignments must target an element path and a property',
|
|
2439
|
+
statement.line,
|
|
2440
|
+
statement.column
|
|
2441
|
+
);
|
|
2442
|
+
}
|
|
2443
|
+
|
|
2444
|
+
const propertyName = statement.path[statement.path.length - 1];
|
|
2445
|
+
const targetPath = statement.path.slice(0, -1);
|
|
2446
|
+
const node = getOrCreateNode(root, targetPath, symbolTable, () => orderCounter++, true);
|
|
2447
|
+
applyAssignmentToNode(node, propertyName, statement.value, statement, symbolTable, resolveContext, statement.scopeKind);
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
return root;
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
function buildHeadEntries(ast, resolveContext) {
|
|
2454
|
+
return ast.body.flatMap((statement) => {
|
|
2455
|
+
if (statement.type !== 'HeadBlock') {
|
|
2456
|
+
return [];
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
return statement.entries.map((entry) => ({
|
|
2460
|
+
tagName: entry.tagName,
|
|
2461
|
+
attributes: buildLooseHtmlAttributeMap(entry.attributes, resolveContext),
|
|
2462
|
+
line: entry.line,
|
|
2463
|
+
column: entry.column,
|
|
2464
|
+
}));
|
|
2465
|
+
});
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
function buildScriptBlocks(ast, resolveContext) {
|
|
2469
|
+
return ast.body
|
|
2470
|
+
.filter((statement) => statement.type === 'ScriptBlock')
|
|
2471
|
+
.map((statement) => ({
|
|
2472
|
+
attributes: buildLooseHtmlAttributeMap(statement.attributes, resolveContext),
|
|
2473
|
+
code: statement.code || '',
|
|
2474
|
+
line: statement.line,
|
|
2475
|
+
column: statement.column,
|
|
2476
|
+
}));
|
|
2477
|
+
}
|
|
2478
|
+
|
|
2479
|
+
function buildLooseHtmlAttributeMap(attributesList, resolveContext) {
|
|
2480
|
+
const attributes = new Map();
|
|
2481
|
+
|
|
2482
|
+
for (const attribute of attributesList) {
|
|
2483
|
+
const normalizedName = normalizeLooseHtmlAttributeName(attribute.name);
|
|
2484
|
+
|
|
2485
|
+
if (attributes.has(normalizedName)) {
|
|
2486
|
+
attributes.delete(normalizedName);
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
attributes.set(normalizedName, literalToLooseHtmlAttributeValue(attribute.value, resolveContext));
|
|
2490
|
+
}
|
|
2491
|
+
|
|
2492
|
+
return attributes;
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
function createNode(name, pathSegments, order, symbolTable, renderInHtml = false) {
|
|
2496
|
+
return {
|
|
2497
|
+
name,
|
|
2498
|
+
pathSegments,
|
|
2499
|
+
order,
|
|
2500
|
+
baseType: symbolTable.resolveBaseType(name),
|
|
2501
|
+
renderInHtml,
|
|
2502
|
+
children: [],
|
|
2503
|
+
childMap: new Map(),
|
|
2504
|
+
attributes: new Map(),
|
|
2505
|
+
styles: new Map(),
|
|
2506
|
+
rawBlocks: [],
|
|
2507
|
+
content: null,
|
|
2508
|
+
};
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
function getOrCreateNode(root, pathSegments, symbolTable, nextOrder, renderInHtml = false) {
|
|
2512
|
+
let current = root;
|
|
2513
|
+
const accumulatedPath = [];
|
|
2514
|
+
|
|
2515
|
+
for (const segment of pathSegments) {
|
|
2516
|
+
accumulatedPath.push(segment);
|
|
2517
|
+
|
|
2518
|
+
if (!current.childMap.has(segment)) {
|
|
2519
|
+
const child = createNode(segment, [...accumulatedPath], nextOrder(), symbolTable, renderInHtml);
|
|
2520
|
+
current.childMap.set(segment, child);
|
|
2521
|
+
current.children.push(child);
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
current = current.childMap.get(segment);
|
|
2525
|
+
current.renderInHtml = current.renderInHtml || renderInHtml;
|
|
2526
|
+
}
|
|
2527
|
+
|
|
2528
|
+
return current;
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
function applyAssignmentToNode(node, propertyName, value, statement, symbolTable, resolveContext, scopeKind = 'dom') {
|
|
2532
|
+
if (scopeKind === 'attr') {
|
|
2533
|
+
if (propertyName === 'textContent' || propertyName === 'innerHTML' || propertyName === 'raw') {
|
|
2534
|
+
throw new CompilerError(attrsBlockContentMessage(), statement.line, statement.column);
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
const normalizedAttributeName = normalizeHtmlAttributeName(propertyName, statement.line, statement.column);
|
|
2538
|
+
|
|
2539
|
+
if (node.attributes.has(normalizedAttributeName)) {
|
|
2540
|
+
node.attributes.delete(normalizedAttributeName);
|
|
2541
|
+
}
|
|
2542
|
+
|
|
2543
|
+
node.attributes.set(normalizedAttributeName, literalToAttributeValue(value, resolveContext));
|
|
2544
|
+
return;
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
if (scopeKind === 'style' && (propertyName === 'textContent' || propertyName === 'innerHTML')) {
|
|
2548
|
+
throw new CompilerError(
|
|
2549
|
+
'styles blocks only support CSS properties and raw assignments',
|
|
2550
|
+
statement.line,
|
|
2551
|
+
statement.column
|
|
2552
|
+
);
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
if (propertyName === 'textContent') {
|
|
2556
|
+
const content = literalToContentValue(value, 'text', symbolTable, resolveContext);
|
|
2557
|
+
node.content = {
|
|
2558
|
+
kind: content.kind,
|
|
2559
|
+
value: content.value,
|
|
2560
|
+
line: statement.line,
|
|
2561
|
+
column: statement.column,
|
|
2562
|
+
};
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
if (propertyName === 'innerHTML') {
|
|
2567
|
+
const content = literalToContentValue(value, 'html', symbolTable, resolveContext);
|
|
2568
|
+
node.content = {
|
|
2569
|
+
kind: content.kind,
|
|
2570
|
+
value: content.value,
|
|
2571
|
+
line: statement.line,
|
|
2572
|
+
column: statement.column,
|
|
2573
|
+
};
|
|
2574
|
+
return;
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
if (propertyName === 'raw') {
|
|
2578
|
+
node.rawBlocks.push(literalToRawCss(value, resolveContext));
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
// Later assignments for the same property win, so we replace the existing
|
|
2583
|
+
// Map entry to preserve the most recent source order.
|
|
2584
|
+
if (node.styles.has(propertyName)) {
|
|
2585
|
+
node.styles.delete(propertyName);
|
|
2586
|
+
}
|
|
2587
|
+
|
|
2588
|
+
node.styles.set(propertyName, {
|
|
2589
|
+
property: camelToKebab(propertyName),
|
|
2590
|
+
value: literalToCssValue(value, resolveContext),
|
|
2591
|
+
});
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
/**
|
|
2595
|
+
* HTML GENERATION
|
|
2596
|
+
* ---------------
|
|
2597
|
+
* Nodes become nested HTML elements. The tag is inferred from the resolved
|
|
2598
|
+
* base type. The compiler currently supports a broader built-in tag map for
|
|
2599
|
+
* common structure, text, figure/media, form, disclosure, list, and table
|
|
2600
|
+
* elements.
|
|
2601
|
+
*
|
|
2602
|
+
* `textContent` is escaped before insertion, while `innerHTML` is copied
|
|
2603
|
+
* directly into the element body. Top-level ::head blocks populate <head>,
|
|
2604
|
+
* and top-level ::script blocks are rendered near the end of <body>. Neither
|
|
2605
|
+
* participates in CSS generation.
|
|
2606
|
+
*/
|
|
2607
|
+
function generateHtml(root, symbolTable, headEntries = [], scriptBlocks = [], options = {}) {
|
|
2608
|
+
const documentTitle = options.title || DEFAULT_DOCUMENT_TITLE;
|
|
2609
|
+
const stylesheetHref = options.stylesheetHref || DEFAULT_CSS_OUTPUT_FILE;
|
|
2610
|
+
const lines = [
|
|
2611
|
+
'<!DOCTYPE html>',
|
|
2612
|
+
'<html lang="en">',
|
|
2613
|
+
' <head>',
|
|
2614
|
+
' <meta charset="UTF-8" />',
|
|
2615
|
+
' <meta name="viewport" content="width=device-width, initial-scale=1.0" />',
|
|
2616
|
+
` <title>${escapeHtml(documentTitle)}</title>`,
|
|
2617
|
+
` <link rel="stylesheet" href="${escapeAttribute(stylesheetHref)}" />`,
|
|
2618
|
+
];
|
|
2619
|
+
|
|
2620
|
+
for (const headEntry of headEntries) {
|
|
2621
|
+
lines.push(renderHeadEntry(headEntry, 2));
|
|
2622
|
+
}
|
|
2623
|
+
|
|
2624
|
+
lines.push(' </head>');
|
|
2625
|
+
lines.push(' <body>');
|
|
2626
|
+
|
|
2627
|
+
for (const child of root.children) {
|
|
2628
|
+
if (!child.renderInHtml) {
|
|
2629
|
+
continue;
|
|
2630
|
+
}
|
|
2631
|
+
|
|
2632
|
+
lines.push(renderHtmlNode(child, symbolTable, 2));
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
for (const scriptBlock of scriptBlocks) {
|
|
2636
|
+
lines.push(renderScriptBlock(scriptBlock, 2));
|
|
2637
|
+
}
|
|
2638
|
+
|
|
2639
|
+
lines.push(' </body>');
|
|
2640
|
+
lines.push('</html>');
|
|
2641
|
+
|
|
2642
|
+
return `${lines.join('\n')}\n`;
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
function renderHeadEntry(entry, indentLevel) {
|
|
2646
|
+
const indent = ' '.repeat(indentLevel);
|
|
2647
|
+
const tagName = entry.tagName;
|
|
2648
|
+
const openTag = `${indent}<${tagName}${renderAttributeEntries(entry.attributes)}>`;
|
|
2649
|
+
|
|
2650
|
+
if (VOID_HTML_TAGS.has(tagName)) {
|
|
2651
|
+
return openTag;
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
return `${openTag}</${tagName}>`;
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
function renderHtmlNode(node, symbolTable, indentLevel) {
|
|
2658
|
+
const indent = ' '.repeat(indentLevel);
|
|
2659
|
+
const contentIndent = ' '.repeat(indentLevel + 1);
|
|
2660
|
+
const tagName = inferHtmlTag(node.name, symbolTable);
|
|
2661
|
+
const voidElement = VOID_HTML_TAGS.has(tagName);
|
|
2662
|
+
const openTag = `${indent}<${tagName}${renderHtmlNodeAttributes(node)}>`;
|
|
2663
|
+
const closeTag = `${indent}</${tagName}>`;
|
|
2664
|
+
|
|
2665
|
+
if (voidElement) {
|
|
2666
|
+
if (node.content || node.children.length > 0) {
|
|
2667
|
+
throw new CompilerError(`Void element <${tagName}> cannot contain content or children for "${node.name}"`);
|
|
2668
|
+
}
|
|
2669
|
+
|
|
2670
|
+
return openTag;
|
|
2671
|
+
}
|
|
2672
|
+
|
|
2673
|
+
const innerParts = [];
|
|
2674
|
+
|
|
2675
|
+
if (node.content) {
|
|
2676
|
+
if (node.content.kind === 'text') {
|
|
2677
|
+
innerParts.push(...prefixLines(escapeHtml(node.content.value), contentIndent));
|
|
2678
|
+
} else {
|
|
2679
|
+
innerParts.push(...prefixLines(node.content.value, contentIndent));
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
for (const child of node.children) {
|
|
2684
|
+
if (!child.renderInHtml) {
|
|
2685
|
+
continue;
|
|
2686
|
+
}
|
|
2687
|
+
|
|
2688
|
+
innerParts.push(renderHtmlNode(child, symbolTable, indentLevel + 1));
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
if (innerParts.length === 0) {
|
|
2692
|
+
return `${openTag}</${tagName}>`;
|
|
2693
|
+
}
|
|
2694
|
+
|
|
2695
|
+
if (innerParts.length === 1 && !innerParts[0].includes('\n') && node.children.length === 0) {
|
|
2696
|
+
return `${openTag}${innerParts[0].trimStart()}</${tagName}>`;
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
return [openTag, ...innerParts, closeTag].join('\n');
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
function renderScriptBlock(scriptBlock, indentLevel) {
|
|
2703
|
+
const indent = ' '.repeat(indentLevel);
|
|
2704
|
+
const openTag = `${indent}<script${renderAttributeEntries(scriptBlock.attributes)}>`;
|
|
2705
|
+
const code = scriptBlock.code.trimEnd();
|
|
2706
|
+
|
|
2707
|
+
if (!code) {
|
|
2708
|
+
return `${openTag}</script>`;
|
|
2709
|
+
}
|
|
2710
|
+
|
|
2711
|
+
if (!code.includes('\n')) {
|
|
2712
|
+
return `${openTag}${code}</script>`;
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
return `${openTag}${code}\n${indent}</script>`;
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
function inferHtmlTag(typeName, symbolTable) {
|
|
2719
|
+
const resolvedBaseType = symbolTable.resolveBaseType(typeName);
|
|
2720
|
+
const normalized = resolvedBaseType.toLowerCase();
|
|
2721
|
+
const words = splitTypeWords(resolvedBaseType);
|
|
2722
|
+
const hasWord = (word) => words.includes(word);
|
|
2723
|
+
const headingLevel = inferHeadingLevel(normalized, words);
|
|
2724
|
+
|
|
2725
|
+
if (headingLevel) {
|
|
2726
|
+
return `h${headingLevel}`;
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
if (normalized === 'button' || hasWord('button')) {
|
|
2730
|
+
return 'button';
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
if (normalized === 'br' || (hasWord('line') && hasWord('break'))) {
|
|
2734
|
+
return 'br';
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
if (normalized === 'code' || hasWord('code')) {
|
|
2738
|
+
return 'code';
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
if (normalized === 'pre' || normalized === 'preformatted' || (hasWord('pre') || hasWord('preformatted'))) {
|
|
2742
|
+
return 'pre';
|
|
2743
|
+
}
|
|
2744
|
+
|
|
2745
|
+
if (normalized === 'strong' || hasWord('strong')) {
|
|
2746
|
+
return 'strong';
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
if (normalized === 'em' || hasWord('emphasis') || hasWord('em')) {
|
|
2750
|
+
return 'em';
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
if (normalized === 'textarea' || (hasWord('text') && hasWord('area'))) {
|
|
2754
|
+
return 'textarea';
|
|
2755
|
+
}
|
|
2756
|
+
|
|
2757
|
+
if (normalized === 'thead' || (hasWord('table') && hasWord('head'))) {
|
|
2758
|
+
return 'thead';
|
|
2759
|
+
}
|
|
2760
|
+
|
|
2761
|
+
if (normalized === 'tbody' || (hasWord('table') && hasWord('body'))) {
|
|
2762
|
+
return 'tbody';
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
if (normalized === 'tfoot' || (hasWord('table') && (hasWord('foot') || hasWord('footer')))) {
|
|
2766
|
+
return 'tfoot';
|
|
2767
|
+
}
|
|
2768
|
+
|
|
2769
|
+
if (normalized === 'tr' || (hasWord('table') && hasWord('row'))) {
|
|
2770
|
+
return 'tr';
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
if (normalized === 'th' || ((hasWord('header') || hasWord('head')) && hasWord('cell'))) {
|
|
2774
|
+
return 'th';
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
if (normalized === 'td' || (hasWord('table') && hasWord('cell'))) {
|
|
2778
|
+
return 'td';
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
if (normalized === 'ul' || ((hasWord('unordered') || hasWord('bullet')) && hasWord('list'))) {
|
|
2782
|
+
return 'ul';
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
if (normalized === 'ol' || ((hasWord('ordered') || hasWord('numbered')) && hasWord('list'))) {
|
|
2786
|
+
return 'ol';
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2789
|
+
if (normalized === 'li' || (hasWord('list') && hasWord('item'))) {
|
|
2790
|
+
return 'li';
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
if (normalized === 'table' || hasWord('table')) {
|
|
2794
|
+
return 'table';
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
if (normalized === 'img' || hasWord('image') || hasWord('img')) {
|
|
2798
|
+
return 'img';
|
|
2799
|
+
}
|
|
2800
|
+
|
|
2801
|
+
if (normalized === 'source' || hasWord('source')) {
|
|
2802
|
+
return 'source';
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
if (normalized === 'picture' || hasWord('picture')) {
|
|
2806
|
+
return 'picture';
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
if (normalized === 'video' || hasWord('video')) {
|
|
2810
|
+
return 'video';
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2813
|
+
if (normalized === 'audio' || hasWord('audio')) {
|
|
2814
|
+
return 'audio';
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
if (normalized === 'input' || hasWord('input')) {
|
|
2818
|
+
return 'input';
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
if (normalized === 'select' || hasWord('select')) {
|
|
2822
|
+
return 'select';
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
if (normalized === 'option' || hasWord('option')) {
|
|
2826
|
+
return 'option';
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
if (normalized === 'form' || hasWord('form')) {
|
|
2830
|
+
return 'form';
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
if (normalized === 'label' || hasWord('label')) {
|
|
2834
|
+
return 'label';
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
if (normalized === 'dialog' || hasWord('dialog')) {
|
|
2838
|
+
return 'dialog';
|
|
2839
|
+
}
|
|
2840
|
+
|
|
2841
|
+
if (normalized === 'details' || hasWord('details')) {
|
|
2842
|
+
return 'details';
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2845
|
+
if (normalized === 'summary' || hasWord('summary')) {
|
|
2846
|
+
return 'summary';
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2849
|
+
if (normalized === 'a' || hasWord('link') || hasWord('anchor')) {
|
|
2850
|
+
return 'a';
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
if (normalized === 'nav' || hasWord('nav') || hasWord('navigation')) {
|
|
2854
|
+
return 'nav';
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
if (normalized === 'section' || hasWord('section')) {
|
|
2858
|
+
return 'section';
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
if (normalized === 'main' || hasWord('main')) {
|
|
2862
|
+
return 'main';
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
if (normalized === 'header' || hasWord('header')) {
|
|
2866
|
+
return 'header';
|
|
2867
|
+
}
|
|
2868
|
+
|
|
2869
|
+
if (normalized === 'footer' || hasWord('footer')) {
|
|
2870
|
+
return 'footer';
|
|
2871
|
+
}
|
|
2872
|
+
|
|
2873
|
+
if (normalized === 'article' || hasWord('article')) {
|
|
2874
|
+
return 'article';
|
|
2875
|
+
}
|
|
2876
|
+
|
|
2877
|
+
if (normalized === 'aside' || hasWord('aside')) {
|
|
2878
|
+
return 'aside';
|
|
2879
|
+
}
|
|
2880
|
+
|
|
2881
|
+
if (
|
|
2882
|
+
normalized === 'figcaption' ||
|
|
2883
|
+
normalized === 'figurecaption' ||
|
|
2884
|
+
(hasWord('fig') && hasWord('caption')) ||
|
|
2885
|
+
(hasWord('figure') && hasWord('caption'))
|
|
2886
|
+
) {
|
|
2887
|
+
return 'figcaption';
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
if (normalized === 'figure' || hasWord('figure')) {
|
|
2891
|
+
return 'figure';
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
if (normalized === 'paragraph' || hasWord('paragraph')) {
|
|
2895
|
+
return 'p';
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
if (normalized === 'span' || hasWord('span')) {
|
|
2899
|
+
return 'span';
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2902
|
+
if (normalized === 'list' || hasWord('list')) {
|
|
2903
|
+
return 'ul';
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2906
|
+
if (normalized === 'text' || hasWord('text')) {
|
|
2907
|
+
return 'p';
|
|
2908
|
+
}
|
|
2909
|
+
|
|
2910
|
+
return 'div';
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
function inferHeadingLevel(normalized, words) {
|
|
2914
|
+
const compact = words.join('');
|
|
2915
|
+
|
|
2916
|
+
for (let level = 6; level >= 1; level -= 1) {
|
|
2917
|
+
if (
|
|
2918
|
+
normalized === `h${level}` ||
|
|
2919
|
+
compact === `heading${level}` ||
|
|
2920
|
+
(words.includes('heading') && words.includes(String(level)))
|
|
2921
|
+
) {
|
|
2922
|
+
return level;
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
if (normalized === 'heading' || words.includes('heading')) {
|
|
2927
|
+
return 1;
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
return null;
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
/**
|
|
2934
|
+
* CSS GENERATION
|
|
2935
|
+
* --------------
|
|
2936
|
+
* CSS is collected from assignments into an explicit rule table so WEB can
|
|
2937
|
+
* support:
|
|
2938
|
+
*
|
|
2939
|
+
* - ordinary descendant selectors
|
|
2940
|
+
* - `styles { ... }` blocks
|
|
2941
|
+
* - pseudo selector blocks like `::hover`
|
|
2942
|
+
* - media-query blocks like `::media (...)`
|
|
2943
|
+
* - top-level `::keyframes`
|
|
2944
|
+
*
|
|
2945
|
+
* Regular style assignments become declaration blocks. `raw` template literal
|
|
2946
|
+
* content is still expanded so nested selectors like `&:hover` remain valid.
|
|
2947
|
+
*/
|
|
2948
|
+
function buildStyleModel(ast, resolveContext, styleInheritanceTable) {
|
|
2949
|
+
let orderCounter = 0;
|
|
2950
|
+
const entries = [];
|
|
2951
|
+
const ruleMap = new Map();
|
|
2952
|
+
const keyframesMap = new Map();
|
|
2953
|
+
|
|
2954
|
+
for (const statement of ast.body) {
|
|
2955
|
+
if (statement.type === 'Assignment') {
|
|
2956
|
+
if (statement.path.length < 2) {
|
|
2957
|
+
throw new CompilerError(
|
|
2958
|
+
'Assignments must target an element path and a property',
|
|
2959
|
+
statement.line,
|
|
2960
|
+
statement.column
|
|
2961
|
+
);
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
if (statement.scopeKind === 'attr') {
|
|
2965
|
+
continue;
|
|
2966
|
+
}
|
|
2967
|
+
|
|
2968
|
+
const propertyName = statement.path[statement.path.length - 1];
|
|
2969
|
+
|
|
2970
|
+
if (assignmentUsesCssOnlyContext(statement) && (propertyName === 'textContent' || propertyName === 'innerHTML')) {
|
|
2971
|
+
const message = statement.scopeKind === 'style'
|
|
2972
|
+
? 'styles blocks only support CSS properties and raw assignments'
|
|
2973
|
+
: 'Pseudo blocks and media queries only support CSS properties and raw assignments';
|
|
2974
|
+
throw new CompilerError(message, statement.line, statement.column);
|
|
2975
|
+
}
|
|
2976
|
+
|
|
2977
|
+
if (propertyName === 'textContent' || propertyName === 'innerHTML') {
|
|
2978
|
+
continue;
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
const selector = selectorPartsToString(statement.selectorParts);
|
|
2982
|
+
const rule = getOrCreateStyleRule(
|
|
2983
|
+
entries,
|
|
2984
|
+
ruleMap,
|
|
2985
|
+
selector,
|
|
2986
|
+
statement.selectorParts,
|
|
2987
|
+
statement.atRules,
|
|
2988
|
+
() => orderCounter++
|
|
2989
|
+
);
|
|
2990
|
+
|
|
2991
|
+
if (propertyName === 'raw') {
|
|
2992
|
+
rule.rawBlocks.push(literalToRawCss(statement.value, resolveContext));
|
|
2993
|
+
continue;
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
if (rule.styles.has(propertyName)) {
|
|
2997
|
+
rule.styles.delete(propertyName);
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
rule.styles.set(propertyName, {
|
|
3001
|
+
property: camelToKebab(propertyName),
|
|
3002
|
+
value: literalToCssValue(statement.value, resolveContext),
|
|
3003
|
+
});
|
|
3004
|
+
|
|
3005
|
+
continue;
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
if (statement.type === 'KeyframesRule') {
|
|
3009
|
+
if (keyframesMap.has(statement.name)) {
|
|
3010
|
+
throw new CompilerError(
|
|
3011
|
+
`Duplicate ::keyframes declaration for "${statement.name}"`,
|
|
3012
|
+
statement.line,
|
|
3013
|
+
statement.column
|
|
3014
|
+
);
|
|
3015
|
+
}
|
|
3016
|
+
|
|
3017
|
+
const entry = {
|
|
3018
|
+
type: 'keyframes',
|
|
3019
|
+
name: statement.name,
|
|
3020
|
+
order: orderCounter++,
|
|
3021
|
+
stages: [],
|
|
3022
|
+
stageMap: new Map(),
|
|
3023
|
+
};
|
|
3024
|
+
|
|
3025
|
+
keyframesMap.set(statement.name, entry);
|
|
3026
|
+
entries.push(entry);
|
|
3027
|
+
|
|
3028
|
+
for (const stage of statement.stages) {
|
|
3029
|
+
let stageEntry = entry.stageMap.get(stage.selector);
|
|
3030
|
+
|
|
3031
|
+
if (!stageEntry) {
|
|
3032
|
+
stageEntry = {
|
|
3033
|
+
selector: stage.selector,
|
|
3034
|
+
order: entry.stages.length,
|
|
3035
|
+
styles: new Map(),
|
|
3036
|
+
};
|
|
3037
|
+
entry.stageMap.set(stage.selector, stageEntry);
|
|
3038
|
+
entry.stages.push(stageEntry);
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
for (const declaration of stage.declarations) {
|
|
3042
|
+
if (stageEntry.styles.has(declaration.propertyName)) {
|
|
3043
|
+
stageEntry.styles.delete(declaration.propertyName);
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
stageEntry.styles.set(declaration.propertyName, {
|
|
3047
|
+
property: camelToKebab(declaration.propertyName),
|
|
3048
|
+
value: literalToCssValue(declaration.value, resolveContext),
|
|
3049
|
+
});
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
}
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
return {
|
|
3056
|
+
entries: applyStyleInheritanceToStyleEntries(entries, styleInheritanceTable),
|
|
3057
|
+
};
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
function getOrCreateStyleRule(entries, ruleMap, selector, selectorParts, atRules, nextOrder) {
|
|
3061
|
+
const key = JSON.stringify({ selector, atRules });
|
|
3062
|
+
let entry = ruleMap.get(key);
|
|
3063
|
+
|
|
3064
|
+
if (!entry) {
|
|
3065
|
+
entry = {
|
|
3066
|
+
type: 'rule',
|
|
3067
|
+
selector,
|
|
3068
|
+
selectorParts: cloneSelectorParts(selectorParts),
|
|
3069
|
+
atRules: [...atRules],
|
|
3070
|
+
order: nextOrder(),
|
|
3071
|
+
styles: new Map(),
|
|
3072
|
+
rawBlocks: [],
|
|
3073
|
+
};
|
|
3074
|
+
ruleMap.set(key, entry);
|
|
3075
|
+
entries.push(entry);
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
return entry;
|
|
3079
|
+
}
|
|
3080
|
+
|
|
3081
|
+
function applyStyleInheritanceToStyleEntries(entries, styleInheritanceTable) {
|
|
3082
|
+
if (!styleInheritanceTable || !styleInheritanceTable.hasDeclarations()) {
|
|
3083
|
+
return entries;
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
const ruleTargets = new Map();
|
|
3087
|
+
const finalEntries = [];
|
|
3088
|
+
|
|
3089
|
+
for (const entry of entries) {
|
|
3090
|
+
if (entry.type !== 'rule') {
|
|
3091
|
+
finalEntries.push(entry);
|
|
3092
|
+
continue;
|
|
3093
|
+
}
|
|
3094
|
+
|
|
3095
|
+
for (const variant of expandInheritedSelectorVariants(entry.selectorParts, styleInheritanceTable)) {
|
|
3096
|
+
const selector = selectorPartsToString(variant.selectorParts);
|
|
3097
|
+
const key = JSON.stringify({ selector, atRules: entry.atRules });
|
|
3098
|
+
let target = ruleTargets.get(key);
|
|
3099
|
+
|
|
3100
|
+
if (!target) {
|
|
3101
|
+
target = {
|
|
3102
|
+
type: 'rule',
|
|
3103
|
+
selector,
|
|
3104
|
+
selectorParts: cloneSelectorParts(variant.selectorParts),
|
|
3105
|
+
atRules: [...entry.atRules],
|
|
3106
|
+
order: entry.order,
|
|
3107
|
+
contributors: [],
|
|
3108
|
+
};
|
|
3109
|
+
ruleTargets.set(key, target);
|
|
3110
|
+
}
|
|
3111
|
+
|
|
3112
|
+
target.order = Math.max(target.order, entry.order);
|
|
3113
|
+
target.contributors.push({
|
|
3114
|
+
entry,
|
|
3115
|
+
distance: variant.distance,
|
|
3116
|
+
});
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
for (const target of ruleTargets.values()) {
|
|
3121
|
+
const finalRule = {
|
|
3122
|
+
type: 'rule',
|
|
3123
|
+
selector: target.selector,
|
|
3124
|
+
selectorParts: cloneSelectorParts(target.selectorParts),
|
|
3125
|
+
atRules: [...target.atRules],
|
|
3126
|
+
order: target.order,
|
|
3127
|
+
styles: new Map(),
|
|
3128
|
+
rawBlocks: [],
|
|
3129
|
+
};
|
|
3130
|
+
|
|
3131
|
+
const contributors = [...target.contributors].sort(compareStyleInheritanceContributors);
|
|
3132
|
+
|
|
3133
|
+
for (const contributor of contributors) {
|
|
3134
|
+
for (const [propertyName, style] of contributor.entry.styles.entries()) {
|
|
3135
|
+
finalRule.styles.set(propertyName, {
|
|
3136
|
+
property: style.property,
|
|
3137
|
+
value: style.value,
|
|
3138
|
+
});
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
finalRule.rawBlocks.push(...contributor.entry.rawBlocks);
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
finalEntries.push(finalRule);
|
|
3145
|
+
}
|
|
3146
|
+
|
|
3147
|
+
return finalEntries.sort(compareStyleEntries);
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
function expandInheritedSelectorVariants(selectorParts, styleInheritanceTable) {
|
|
3151
|
+
const variants = [];
|
|
3152
|
+
|
|
3153
|
+
function visit(index, builtParts, distance) {
|
|
3154
|
+
if (index >= selectorParts.length) {
|
|
3155
|
+
variants.push({
|
|
3156
|
+
selectorParts: builtParts,
|
|
3157
|
+
distance,
|
|
3158
|
+
});
|
|
3159
|
+
return;
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
const part = selectorParts[index];
|
|
3163
|
+
|
|
3164
|
+
if (part.type !== 'segment') {
|
|
3165
|
+
visit(index + 1, [...builtParts, { ...part }], distance);
|
|
3166
|
+
return;
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
const options = styleInheritanceTable.getExpansionOptions(part.value);
|
|
3170
|
+
|
|
3171
|
+
for (const option of options) {
|
|
3172
|
+
visit(
|
|
3173
|
+
index + 1,
|
|
3174
|
+
[...builtParts, { type: 'segment', value: option.name }],
|
|
3175
|
+
distance + option.distance
|
|
3176
|
+
);
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
|
|
3180
|
+
visit(0, [], 0);
|
|
3181
|
+
return variants;
|
|
3182
|
+
}
|
|
3183
|
+
|
|
3184
|
+
function compareStyleInheritanceContributors(left, right) {
|
|
3185
|
+
if (left.distance !== right.distance) {
|
|
3186
|
+
return right.distance - left.distance;
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
return left.entry.order - right.entry.order;
|
|
3190
|
+
}
|
|
3191
|
+
|
|
3192
|
+
function compareStyleEntries(left, right) {
|
|
3193
|
+
if (left.order !== right.order) {
|
|
3194
|
+
return left.order - right.order;
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3197
|
+
if (left.type !== right.type) {
|
|
3198
|
+
return left.type === 'rule' ? -1 : 1;
|
|
3199
|
+
}
|
|
3200
|
+
|
|
3201
|
+
if (left.type === 'rule' && right.type === 'rule') {
|
|
3202
|
+
return left.selector.localeCompare(right.selector);
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
if (left.type === 'keyframes' && right.type === 'keyframes') {
|
|
3206
|
+
return left.name.localeCompare(right.name);
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
return 0;
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
function cloneSelectorParts(parts) {
|
|
3213
|
+
return parts.map((part) => ({ ...part }));
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3216
|
+
function generateCss(styleModel) {
|
|
3217
|
+
const blocks = [];
|
|
3218
|
+
|
|
3219
|
+
for (const entry of styleModel.entries) {
|
|
3220
|
+
if (entry.type === 'rule') {
|
|
3221
|
+
const renderedRule = renderStyleRuleEntry(entry);
|
|
3222
|
+
if (renderedRule.trim()) {
|
|
3223
|
+
blocks.push(renderedRule.trim());
|
|
3224
|
+
}
|
|
3225
|
+
continue;
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
if (entry.type === 'keyframes') {
|
|
3229
|
+
const renderedKeyframes = renderKeyframesEntry(entry);
|
|
3230
|
+
if (renderedKeyframes.trim()) {
|
|
3231
|
+
blocks.push(renderedKeyframes.trim());
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
|
|
3236
|
+
if (blocks.length === 0) {
|
|
3237
|
+
return '';
|
|
3238
|
+
}
|
|
3239
|
+
|
|
3240
|
+
return `${blocks.join('\n\n')}\n`;
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
function renderStyleRuleEntry(entry) {
|
|
3244
|
+
const fragments = [];
|
|
3245
|
+
|
|
3246
|
+
if (entry.styles.size > 0) {
|
|
3247
|
+
const declarations = [];
|
|
3248
|
+
|
|
3249
|
+
for (const style of entry.styles.values()) {
|
|
3250
|
+
declarations.push(`${style.property}: ${style.value};`);
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
fragments.push(formatCssRule(entry.selector, declarations));
|
|
3254
|
+
}
|
|
3255
|
+
|
|
3256
|
+
for (const rawBlock of entry.rawBlocks) {
|
|
3257
|
+
const expanded = expandRawCss(rawBlock, entry.selector);
|
|
3258
|
+
if (expanded.trim()) {
|
|
3259
|
+
fragments.push(expanded.trim());
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3263
|
+
if (fragments.length === 0) {
|
|
3264
|
+
return '';
|
|
3265
|
+
}
|
|
3266
|
+
|
|
3267
|
+
return wrapCssInAtRules(fragments.join('\n\n'), entry.atRules);
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
function renderKeyframesEntry(entry) {
|
|
3271
|
+
const stageBlocks = entry.stages.map((stage) => renderKeyframeStage(stage));
|
|
3272
|
+
const keyframesBlock = [
|
|
3273
|
+
`@keyframes ${entry.name} {`,
|
|
3274
|
+
...stageBlocks,
|
|
3275
|
+
'}',
|
|
3276
|
+
].join('\n');
|
|
3277
|
+
|
|
3278
|
+
return keyframesBlock;
|
|
3279
|
+
}
|
|
3280
|
+
|
|
3281
|
+
function renderKeyframeStage(stage) {
|
|
3282
|
+
const declarations = [];
|
|
3283
|
+
|
|
3284
|
+
for (const style of stage.styles.values()) {
|
|
3285
|
+
declarations.push(` ${style.property}: ${style.value};`);
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
return [
|
|
3289
|
+
` ${stage.selector} {`,
|
|
3290
|
+
...declarations,
|
|
3291
|
+
' }',
|
|
3292
|
+
].join('\n');
|
|
3293
|
+
}
|
|
3294
|
+
|
|
3295
|
+
function wrapCssInAtRules(cssText, atRules) {
|
|
3296
|
+
let output = cssText.trim();
|
|
3297
|
+
|
|
3298
|
+
for (let index = atRules.length - 1; index >= 0; index -= 1) {
|
|
3299
|
+
output = [
|
|
3300
|
+
`${atRules[index]} {`,
|
|
3301
|
+
indentMultiline(output, 1),
|
|
3302
|
+
'}',
|
|
3303
|
+
].join('\n');
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
return output;
|
|
3307
|
+
}
|
|
3308
|
+
|
|
3309
|
+
function selectorPartsToString(parts) {
|
|
3310
|
+
let selector = '';
|
|
3311
|
+
|
|
3312
|
+
for (const part of parts) {
|
|
3313
|
+
if (part.type === 'global') {
|
|
3314
|
+
selector += `${selector ? ' ' : ''}${part.value}`;
|
|
3315
|
+
continue;
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
if (part.type === 'segment') {
|
|
3319
|
+
selector += `${selector ? ' ' : ''}.${part.value}`;
|
|
3320
|
+
continue;
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
if (part.type === 'pseudo') {
|
|
3324
|
+
selector += part.value;
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
return selector;
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
/**
|
|
3332
|
+
* RAW CSS EXPANSION
|
|
3333
|
+
* -----------------
|
|
3334
|
+
* The WEB escape hatch accepts template literals that may contain:
|
|
3335
|
+
* - plain CSS declarations
|
|
3336
|
+
* - nested selector blocks using `&`
|
|
3337
|
+
* - at-rules like `@media`
|
|
3338
|
+
*
|
|
3339
|
+
* Example input:
|
|
3340
|
+
* raw = `
|
|
3341
|
+
* transition: all 0.3s ease;
|
|
3342
|
+
* &:hover { transform: scale(1.05); }
|
|
3343
|
+
* `;
|
|
3344
|
+
*
|
|
3345
|
+
* Output:
|
|
3346
|
+
* .heroSection .customButton {
|
|
3347
|
+
* transition: all 0.3s ease;
|
|
3348
|
+
* }
|
|
3349
|
+
*
|
|
3350
|
+
* .heroSection .customButton:hover {
|
|
3351
|
+
* transform: scale(1.05);
|
|
3352
|
+
* }
|
|
3353
|
+
*/
|
|
3354
|
+
function expandRawCss(rawCss, selector, indentLevel = 0) {
|
|
3355
|
+
const parts = parseCssBody(rawCss);
|
|
3356
|
+
const declarationLines = [];
|
|
3357
|
+
const emittedBlocks = [];
|
|
3358
|
+
|
|
3359
|
+
for (const part of parts) {
|
|
3360
|
+
if (part.type === 'declaration') {
|
|
3361
|
+
declarationLines.push(normalizeCssStatement(part.text));
|
|
3362
|
+
continue;
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3365
|
+
if (part.type === 'statement') {
|
|
3366
|
+
emittedBlocks.push(`${indent(indentLevel)}${normalizeCssStatement(part.text)}`);
|
|
3367
|
+
continue;
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3370
|
+
if (part.type === 'rule') {
|
|
3371
|
+
const nestedSelector = resolveNestedSelector(selector, part.prelude);
|
|
3372
|
+
const nestedBlock = expandRawCss(part.body, nestedSelector, indentLevel);
|
|
3373
|
+
if (nestedBlock.trim()) {
|
|
3374
|
+
emittedBlocks.push(nestedBlock.trimEnd());
|
|
3375
|
+
}
|
|
3376
|
+
continue;
|
|
3377
|
+
}
|
|
3378
|
+
|
|
3379
|
+
if (part.type === 'at-rule') {
|
|
3380
|
+
emittedBlocks.push(renderAtRule(selector, part.prelude, part.body, indentLevel));
|
|
3381
|
+
}
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
const output = [];
|
|
3385
|
+
|
|
3386
|
+
if (declarationLines.length > 0) {
|
|
3387
|
+
output.push(formatCssRule(selector, declarationLines, indentLevel));
|
|
3388
|
+
}
|
|
3389
|
+
|
|
3390
|
+
for (const block of emittedBlocks) {
|
|
3391
|
+
if (block.trim()) {
|
|
3392
|
+
output.push(block);
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
|
|
3396
|
+
return output.join('\n\n');
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
function parseCssBody(source) {
|
|
3400
|
+
const parts = [];
|
|
3401
|
+
let buffer = '';
|
|
3402
|
+
let index = 0;
|
|
3403
|
+
|
|
3404
|
+
while (index < source.length) {
|
|
3405
|
+
const char = source[index];
|
|
3406
|
+
const next = source[index + 1];
|
|
3407
|
+
|
|
3408
|
+
if (char === '/' && next === '*') {
|
|
3409
|
+
index = skipBlockComment(source, index);
|
|
3410
|
+
continue;
|
|
3411
|
+
}
|
|
3412
|
+
|
|
3413
|
+
if (char === '"' || char === '\'' || char === '`') {
|
|
3414
|
+
const consumed = consumeQuotedText(source, index);
|
|
3415
|
+
buffer += consumed.text;
|
|
3416
|
+
index = consumed.nextIndex;
|
|
3417
|
+
continue;
|
|
3418
|
+
}
|
|
3419
|
+
|
|
3420
|
+
if (char === ';') {
|
|
3421
|
+
const text = buffer.trim();
|
|
3422
|
+
if (text) {
|
|
3423
|
+
parts.push({
|
|
3424
|
+
type: text.startsWith('@') ? 'statement' : 'declaration',
|
|
3425
|
+
text,
|
|
3426
|
+
});
|
|
3427
|
+
}
|
|
3428
|
+
buffer = '';
|
|
3429
|
+
index += 1;
|
|
3430
|
+
continue;
|
|
3431
|
+
}
|
|
3432
|
+
|
|
3433
|
+
if (char === '{') {
|
|
3434
|
+
const prelude = buffer.trim();
|
|
3435
|
+
const consumed = consumeBalancedBlock(source, index);
|
|
3436
|
+
|
|
3437
|
+
if (prelude) {
|
|
3438
|
+
parts.push({
|
|
3439
|
+
type: prelude.startsWith('@') ? 'at-rule' : 'rule',
|
|
3440
|
+
prelude,
|
|
3441
|
+
body: consumed.body,
|
|
3442
|
+
});
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
buffer = '';
|
|
3446
|
+
index = consumed.nextIndex;
|
|
3447
|
+
continue;
|
|
3448
|
+
}
|
|
3449
|
+
|
|
3450
|
+
buffer += char;
|
|
3451
|
+
index += 1;
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
const trailing = buffer.trim();
|
|
3455
|
+
if (trailing) {
|
|
3456
|
+
parts.push({
|
|
3457
|
+
type: trailing.startsWith('@') ? 'statement' : 'declaration',
|
|
3458
|
+
text: trailing,
|
|
3459
|
+
});
|
|
3460
|
+
}
|
|
3461
|
+
|
|
3462
|
+
return parts;
|
|
3463
|
+
}
|
|
3464
|
+
|
|
3465
|
+
function consumeBalancedBlock(source, startIndex) {
|
|
3466
|
+
let depth = 0;
|
|
3467
|
+
let index = startIndex;
|
|
3468
|
+
let bodyStart = -1;
|
|
3469
|
+
|
|
3470
|
+
while (index < source.length) {
|
|
3471
|
+
const char = source[index];
|
|
3472
|
+
const next = source[index + 1];
|
|
3473
|
+
|
|
3474
|
+
if (char === '/' && next === '*') {
|
|
3475
|
+
index = skipBlockComment(source, index);
|
|
3476
|
+
continue;
|
|
3477
|
+
}
|
|
3478
|
+
|
|
3479
|
+
if (char === '"' || char === '\'' || char === '`') {
|
|
3480
|
+
const consumed = consumeQuotedText(source, index);
|
|
3481
|
+
index = consumed.nextIndex;
|
|
3482
|
+
continue;
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
if (char === '{') {
|
|
3486
|
+
depth += 1;
|
|
3487
|
+
if (depth === 1) {
|
|
3488
|
+
bodyStart = index + 1;
|
|
3489
|
+
}
|
|
3490
|
+
index += 1;
|
|
3491
|
+
continue;
|
|
3492
|
+
}
|
|
3493
|
+
|
|
3494
|
+
if (char === '}') {
|
|
3495
|
+
depth -= 1;
|
|
3496
|
+
index += 1;
|
|
3497
|
+
|
|
3498
|
+
if (depth === 0) {
|
|
3499
|
+
return {
|
|
3500
|
+
body: source.slice(bodyStart, index - 1),
|
|
3501
|
+
nextIndex: index,
|
|
3502
|
+
};
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3505
|
+
continue;
|
|
3506
|
+
}
|
|
3507
|
+
|
|
3508
|
+
index += 1;
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3511
|
+
throw new CompilerError('Unterminated CSS block in raw template literal');
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
function consumeQuotedText(source, startIndex) {
|
|
3515
|
+
const quote = source[startIndex];
|
|
3516
|
+
let index = startIndex + 1;
|
|
3517
|
+
|
|
3518
|
+
while (index < source.length) {
|
|
3519
|
+
const char = source[index];
|
|
3520
|
+
|
|
3521
|
+
if (char === '\\') {
|
|
3522
|
+
index += 2;
|
|
3523
|
+
continue;
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
if (char === quote) {
|
|
3527
|
+
return {
|
|
3528
|
+
text: source.slice(startIndex, index + 1),
|
|
3529
|
+
nextIndex: index + 1,
|
|
3530
|
+
};
|
|
3531
|
+
}
|
|
3532
|
+
|
|
3533
|
+
index += 1;
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
throw new CompilerError('Unterminated quoted text in raw CSS');
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
function skipBlockComment(source, startIndex) {
|
|
3540
|
+
let index = startIndex + 2;
|
|
3541
|
+
|
|
3542
|
+
while (index < source.length && !(source[index] === '*' && source[index + 1] === '/')) {
|
|
3543
|
+
index += 1;
|
|
3544
|
+
}
|
|
3545
|
+
|
|
3546
|
+
if (index >= source.length) {
|
|
3547
|
+
throw new CompilerError('Unterminated CSS block comment in raw template literal');
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
return index + 2;
|
|
3551
|
+
}
|
|
3552
|
+
|
|
3553
|
+
function skipWhitespaceAndCommentsFromSource(source, startIndex) {
|
|
3554
|
+
let index = startIndex;
|
|
3555
|
+
|
|
3556
|
+
while (index < source.length) {
|
|
3557
|
+
const char = source[index];
|
|
3558
|
+
const next = source[index + 1];
|
|
3559
|
+
|
|
3560
|
+
if (/\s/.test(char)) {
|
|
3561
|
+
index += 1;
|
|
3562
|
+
continue;
|
|
3563
|
+
}
|
|
3564
|
+
|
|
3565
|
+
if (char === '/' && next === '/') {
|
|
3566
|
+
index += 2;
|
|
3567
|
+
|
|
3568
|
+
while (index < source.length && source[index] !== '\n') {
|
|
3569
|
+
index += 1;
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
continue;
|
|
3573
|
+
}
|
|
3574
|
+
|
|
3575
|
+
if (char === '/' && next === '*') {
|
|
3576
|
+
index += 2;
|
|
3577
|
+
|
|
3578
|
+
while (index < source.length && !(source[index] === '*' && source[index + 1] === '/')) {
|
|
3579
|
+
index += 1;
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3582
|
+
if (index >= source.length) {
|
|
3583
|
+
return source.length;
|
|
3584
|
+
}
|
|
3585
|
+
|
|
3586
|
+
index += 2;
|
|
3587
|
+
continue;
|
|
3588
|
+
}
|
|
3589
|
+
|
|
3590
|
+
break;
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
return index;
|
|
3594
|
+
}
|
|
3595
|
+
|
|
3596
|
+
function renderAtRule(selector, prelude, body, indentLevel) {
|
|
3597
|
+
if (isPassthroughAtRule(prelude)) {
|
|
3598
|
+
return [
|
|
3599
|
+
`${indent(indentLevel)}${prelude} {`,
|
|
3600
|
+
indentMultiline(body.trim(), indentLevel + 1),
|
|
3601
|
+
`${indent(indentLevel)}}`,
|
|
3602
|
+
].join('\n');
|
|
3603
|
+
}
|
|
3604
|
+
|
|
3605
|
+
const expandedBody = expandRawCss(body, selector, indentLevel + 1);
|
|
3606
|
+
return [
|
|
3607
|
+
`${indent(indentLevel)}${prelude} {`,
|
|
3608
|
+
expandedBody ? expandedBody : indent(indentLevel + 1),
|
|
3609
|
+
`${indent(indentLevel)}}`,
|
|
3610
|
+
].join('\n');
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
function isPassthroughAtRule(prelude) {
|
|
3614
|
+
return /^@(keyframes|font-face|property|counter-style|page)\b/i.test(prelude);
|
|
3615
|
+
}
|
|
3616
|
+
|
|
3617
|
+
function resolveNestedSelector(parentSelector, nestedSelectorText) {
|
|
3618
|
+
const parts = splitSelectorList(nestedSelectorText);
|
|
3619
|
+
|
|
3620
|
+
return parts
|
|
3621
|
+
.map((part) => {
|
|
3622
|
+
const selector = part.trim();
|
|
3623
|
+
|
|
3624
|
+
if (!selector) {
|
|
3625
|
+
return parentSelector;
|
|
3626
|
+
}
|
|
3627
|
+
|
|
3628
|
+
if (selector.includes('&')) {
|
|
3629
|
+
return selector.replace(/&/g, parentSelector);
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
if (/^[:\[]/.test(selector)) {
|
|
3633
|
+
return `${parentSelector}${selector}`;
|
|
3634
|
+
}
|
|
3635
|
+
|
|
3636
|
+
return `${parentSelector} ${selector}`;
|
|
3637
|
+
})
|
|
3638
|
+
.join(', ');
|
|
3639
|
+
}
|
|
3640
|
+
|
|
3641
|
+
function splitSelectorList(text) {
|
|
3642
|
+
const parts = [];
|
|
3643
|
+
let buffer = '';
|
|
3644
|
+
let parenDepth = 0;
|
|
3645
|
+
let bracketDepth = 0;
|
|
3646
|
+
|
|
3647
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
3648
|
+
const char = text[index];
|
|
3649
|
+
|
|
3650
|
+
if (char === '"' || char === '\'' || char === '`') {
|
|
3651
|
+
const consumed = consumeQuotedText(text, index);
|
|
3652
|
+
buffer += consumed.text;
|
|
3653
|
+
index = consumed.nextIndex - 1;
|
|
3654
|
+
continue;
|
|
3655
|
+
}
|
|
3656
|
+
|
|
3657
|
+
if (char === '(') {
|
|
3658
|
+
parenDepth += 1;
|
|
3659
|
+
buffer += char;
|
|
3660
|
+
continue;
|
|
3661
|
+
}
|
|
3662
|
+
|
|
3663
|
+
if (char === ')') {
|
|
3664
|
+
parenDepth -= 1;
|
|
3665
|
+
buffer += char;
|
|
3666
|
+
continue;
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
if (char === '[') {
|
|
3670
|
+
bracketDepth += 1;
|
|
3671
|
+
buffer += char;
|
|
3672
|
+
continue;
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3675
|
+
if (char === ']') {
|
|
3676
|
+
bracketDepth -= 1;
|
|
3677
|
+
buffer += char;
|
|
3678
|
+
continue;
|
|
3679
|
+
}
|
|
3680
|
+
|
|
3681
|
+
if (char === ',' && parenDepth === 0 && bracketDepth === 0) {
|
|
3682
|
+
parts.push(buffer);
|
|
3683
|
+
buffer = '';
|
|
3684
|
+
continue;
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
buffer += char;
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3690
|
+
if (buffer) {
|
|
3691
|
+
parts.push(buffer);
|
|
3692
|
+
}
|
|
3693
|
+
|
|
3694
|
+
return parts;
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3697
|
+
function formatCssRule(selector, declarations, indentLevel = 0) {
|
|
3698
|
+
return [
|
|
3699
|
+
`${indent(indentLevel)}${selector} {`,
|
|
3700
|
+
...declarations.map((line) => `${indent(indentLevel + 1)}${normalizeCssStatement(line)}`),
|
|
3701
|
+
`${indent(indentLevel)}}`,
|
|
3702
|
+
].join('\n');
|
|
3703
|
+
}
|
|
3704
|
+
|
|
3705
|
+
function normalizeCssStatement(statement) {
|
|
3706
|
+
const trimmed = statement.trim();
|
|
3707
|
+
return trimmed.endsWith(';') ? trimmed : `${trimmed};`;
|
|
3708
|
+
}
|
|
3709
|
+
|
|
3710
|
+
/**
|
|
3711
|
+
* GENERAL HELPERS
|
|
3712
|
+
*/
|
|
3713
|
+
function literalToContentValue(literal, preferredKind, symbolTable, resolveContext) {
|
|
3714
|
+
const resolved = resolveLiteral(literal, {
|
|
3715
|
+
preserveStructured: preferredKind === 'html',
|
|
3716
|
+
...resolveContext,
|
|
3717
|
+
});
|
|
3718
|
+
|
|
3719
|
+
if (resolved.isError) {
|
|
3720
|
+
return {
|
|
3721
|
+
kind: 'text',
|
|
3722
|
+
value: resolved.value,
|
|
3723
|
+
};
|
|
3724
|
+
}
|
|
3725
|
+
|
|
3726
|
+
if (preferredKind === 'html' && isJavaScriptFragmentValue(resolved.value)) {
|
|
3727
|
+
try {
|
|
3728
|
+
return {
|
|
3729
|
+
kind: 'html',
|
|
3730
|
+
value: renderJavaScriptFragmentValue(resolved.value, symbolTable),
|
|
3731
|
+
};
|
|
3732
|
+
} catch (error) {
|
|
3733
|
+
return {
|
|
3734
|
+
kind: 'text',
|
|
3735
|
+
value: formatJavaScriptInjectionError(error instanceof Error ? error.message : String(error)),
|
|
3736
|
+
};
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
|
|
3740
|
+
return {
|
|
3741
|
+
kind: preferredKind,
|
|
3742
|
+
value: String(resolved.value),
|
|
3743
|
+
};
|
|
3744
|
+
}
|
|
3745
|
+
|
|
3746
|
+
function literalToAttributeValue(literal, resolveContext) {
|
|
3747
|
+
const resolved = resolveLiteral(literal, resolveContext);
|
|
3748
|
+
|
|
3749
|
+
if (resolved.isError) {
|
|
3750
|
+
return resolved.value;
|
|
3751
|
+
}
|
|
3752
|
+
|
|
3753
|
+
if (resolved.value === true) {
|
|
3754
|
+
return true;
|
|
3755
|
+
}
|
|
3756
|
+
|
|
3757
|
+
if (resolved.value === false || resolved.value === null || resolved.value === undefined) {
|
|
3758
|
+
return null;
|
|
3759
|
+
}
|
|
3760
|
+
|
|
3761
|
+
if (resolved.value === 'true') {
|
|
3762
|
+
return true;
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
if (resolved.value === 'false' || resolved.value === 'null' || resolved.value === 'undefined') {
|
|
3766
|
+
return null;
|
|
3767
|
+
}
|
|
3768
|
+
|
|
3769
|
+
return String(resolved.value);
|
|
3770
|
+
}
|
|
3771
|
+
|
|
3772
|
+
function literalToLooseHtmlAttributeValue(literal, resolveContext) {
|
|
3773
|
+
const resolved = resolveLiteral(literal, resolveContext);
|
|
3774
|
+
|
|
3775
|
+
if (resolved.isError) {
|
|
3776
|
+
return resolved.value;
|
|
3777
|
+
}
|
|
3778
|
+
|
|
3779
|
+
if (literal.type === 'IdentifierLiteral') {
|
|
3780
|
+
if (resolved.value === 'true') {
|
|
3781
|
+
return true;
|
|
3782
|
+
}
|
|
3783
|
+
|
|
3784
|
+
if (resolved.value === 'false' || resolved.value === 'null' || resolved.value === 'undefined') {
|
|
3785
|
+
return null;
|
|
3786
|
+
}
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3789
|
+
if (resolved.value === true) {
|
|
3790
|
+
return true;
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3793
|
+
if (resolved.value === false || resolved.value === null || resolved.value === undefined) {
|
|
3794
|
+
return null;
|
|
3795
|
+
}
|
|
3796
|
+
|
|
3797
|
+
return String(resolved.value);
|
|
3798
|
+
}
|
|
3799
|
+
|
|
3800
|
+
function literalToCssValue(literal, resolveContext) {
|
|
3801
|
+
const resolved = resolveLiteral(literal, resolveContext);
|
|
3802
|
+
return resolved.isError ? quoteCssString(resolved.value) : resolved.value;
|
|
3803
|
+
}
|
|
3804
|
+
|
|
3805
|
+
function literalToString(literal, resolveContext) {
|
|
3806
|
+
return resolveLiteral(literal, resolveContext).value;
|
|
3807
|
+
}
|
|
3808
|
+
|
|
3809
|
+
function literalToRawCss(literal, resolveContext) {
|
|
3810
|
+
const resolved = resolveLiteral(literal, resolveContext);
|
|
3811
|
+
return resolved.isError ? formatCssComment(resolved.value) : resolved.value;
|
|
3812
|
+
}
|
|
3813
|
+
|
|
3814
|
+
function resolveLiteral(literal, options = {}) {
|
|
3815
|
+
switch (literal.type) {
|
|
3816
|
+
case 'StringLiteral':
|
|
3817
|
+
case 'TemplateLiteral':
|
|
3818
|
+
case 'IdentifierLiteral':
|
|
3819
|
+
case 'PercentageLiteral':
|
|
3820
|
+
return { value: literal.value, isError: false };
|
|
3821
|
+
case 'NumberLiteral':
|
|
3822
|
+
return { value: String(literal.value), isError: false };
|
|
3823
|
+
case 'VariableReferenceLiteral':
|
|
3824
|
+
if (!options.variableTable) {
|
|
3825
|
+
throw new CompilerError(`Unknown variable "${literal.value}"`, literal.line, literal.column);
|
|
3826
|
+
}
|
|
3827
|
+
|
|
3828
|
+
return {
|
|
3829
|
+
value: options.variableTable.resolve(literal.value, literal.line, literal.column),
|
|
3830
|
+
isError: false,
|
|
3831
|
+
};
|
|
3832
|
+
case 'JavaScriptLiteral':
|
|
3833
|
+
return evaluateJavaScriptLiteral(literal.value, options);
|
|
3834
|
+
default:
|
|
3835
|
+
throw new CompilerError(`Unsupported literal type "${literal.type}"`);
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3838
|
+
|
|
3839
|
+
function evaluateJavaScriptLiteral(source, options = {}) {
|
|
3840
|
+
try {
|
|
3841
|
+
const script = new vm.Script(buildJavaScriptInjectionWrapper(source), {
|
|
3842
|
+
displayErrors: true,
|
|
3843
|
+
filename: `${options.sourceName || DEFAULT_INPUT_FILE}:js`,
|
|
3844
|
+
});
|
|
3845
|
+
const result = script.runInNewContext(createJavaScriptInjectionSandbox(), {
|
|
3846
|
+
timeout: JAVASCRIPT_INJECTION_TIMEOUT_MS,
|
|
3847
|
+
});
|
|
3848
|
+
|
|
3849
|
+
if (result && typeof result === 'object' && result.__webJsError) {
|
|
3850
|
+
return {
|
|
3851
|
+
value: formatJavaScriptInjectionError(result.message),
|
|
3852
|
+
isError: true,
|
|
3853
|
+
};
|
|
3854
|
+
}
|
|
3855
|
+
|
|
3856
|
+
return {
|
|
3857
|
+
value: normalizeJavaScriptInjectionResult(result, options),
|
|
3858
|
+
isError: false,
|
|
3859
|
+
};
|
|
3860
|
+
} catch (error) {
|
|
3861
|
+
return {
|
|
3862
|
+
value: formatJavaScriptInjectionError(error instanceof Error ? error.message : String(error)),
|
|
3863
|
+
isError: true,
|
|
3864
|
+
};
|
|
3865
|
+
}
|
|
3866
|
+
}
|
|
3867
|
+
|
|
3868
|
+
function buildJavaScriptInjectionWrapper(source) {
|
|
3869
|
+
const fallbackVariableName = inferLastDeclaredVariableName(source);
|
|
3870
|
+
const fallbackLines = [
|
|
3871
|
+
" if (__webOutput !== '') {",
|
|
3872
|
+
' return __webOutput;',
|
|
3873
|
+
' }',
|
|
3874
|
+
];
|
|
3875
|
+
|
|
3876
|
+
if (fallbackVariableName) {
|
|
3877
|
+
fallbackLines.push(
|
|
3878
|
+
` if (typeof ${fallbackVariableName} !== 'undefined') {`,
|
|
3879
|
+
` return ${fallbackVariableName};`,
|
|
3880
|
+
' }'
|
|
3881
|
+
);
|
|
3882
|
+
}
|
|
3883
|
+
|
|
3884
|
+
fallbackLines.push(" return '';");
|
|
3885
|
+
|
|
3886
|
+
return [
|
|
3887
|
+
'(function () {',
|
|
3888
|
+
" let __webOutput = '';",
|
|
3889
|
+
' const emit = function (value) {',
|
|
3890
|
+
" if (value === undefined || value === null) {",
|
|
3891
|
+
' return __webOutput;',
|
|
3892
|
+
' }',
|
|
3893
|
+
' __webOutput += String(value);',
|
|
3894
|
+
' return __webOutput;',
|
|
3895
|
+
' };',
|
|
3896
|
+
' try {',
|
|
3897
|
+
' const __webUserFunction = function () {',
|
|
3898
|
+
indentMultiline(source, 3),
|
|
3899
|
+
...fallbackLines,
|
|
3900
|
+
' };',
|
|
3901
|
+
' const __webResult = __webUserFunction();',
|
|
3902
|
+
" if ((__webResult === undefined || __webResult === null) && __webOutput !== '') {",
|
|
3903
|
+
' return __webOutput;',
|
|
3904
|
+
' }',
|
|
3905
|
+
' return __webResult;',
|
|
3906
|
+
' } catch (error) {',
|
|
3907
|
+
' return {',
|
|
3908
|
+
' __webJsError: true,',
|
|
3909
|
+
" message: error && error.message ? error.message : String(error),",
|
|
3910
|
+
' };',
|
|
3911
|
+
' }',
|
|
3912
|
+
'})()',
|
|
3913
|
+
].join('\n');
|
|
3914
|
+
}
|
|
3915
|
+
|
|
3916
|
+
function createJavaScriptInjectionSandbox() {
|
|
3917
|
+
return {
|
|
3918
|
+
console: {
|
|
3919
|
+
log() {},
|
|
3920
|
+
info() {},
|
|
3921
|
+
warn() {},
|
|
3922
|
+
error() {},
|
|
3923
|
+
},
|
|
3924
|
+
node: createJavaScriptFragmentNode,
|
|
3925
|
+
el: createJavaScriptFragmentNode,
|
|
3926
|
+
fragment: createJavaScriptFragment,
|
|
3927
|
+
text: createJavaScriptFragmentText,
|
|
3928
|
+
html: createJavaScriptFragmentHtml,
|
|
3929
|
+
};
|
|
3930
|
+
}
|
|
3931
|
+
|
|
3932
|
+
function normalizeJavaScriptInjectionResult(result, options = {}) {
|
|
3933
|
+
if (result === undefined || result === null) {
|
|
3934
|
+
return '';
|
|
3935
|
+
}
|
|
3936
|
+
|
|
3937
|
+
if (options.preserveStructured && isJavaScriptFragmentValue(result)) {
|
|
3938
|
+
return result;
|
|
3939
|
+
}
|
|
3940
|
+
|
|
3941
|
+
if (typeof result === 'string') {
|
|
3942
|
+
return result;
|
|
3943
|
+
}
|
|
3944
|
+
|
|
3945
|
+
if (typeof result === 'object') {
|
|
3946
|
+
try {
|
|
3947
|
+
return JSON.stringify(result);
|
|
3948
|
+
} catch (error) {
|
|
3949
|
+
return String(result);
|
|
3950
|
+
}
|
|
3951
|
+
}
|
|
3952
|
+
|
|
3953
|
+
return String(result);
|
|
3954
|
+
}
|
|
3955
|
+
|
|
3956
|
+
function createJavaScriptFragmentNode(typeName, classNameOrOptions, maybeOptions) {
|
|
3957
|
+
if (typeof typeName !== 'string' || typeName.trim() === '') {
|
|
3958
|
+
throw new Error('node() requires a non-empty type name');
|
|
3959
|
+
}
|
|
3960
|
+
|
|
3961
|
+
let className = '';
|
|
3962
|
+
let options = {};
|
|
3963
|
+
|
|
3964
|
+
if (classNameOrOptions === undefined || classNameOrOptions === null || isPlainObject(classNameOrOptions)) {
|
|
3965
|
+
options = classNameOrOptions || {};
|
|
3966
|
+
} else {
|
|
3967
|
+
className = normalizeJavaScriptFragmentClassName(classNameOrOptions);
|
|
3968
|
+
options = maybeOptions || {};
|
|
3969
|
+
}
|
|
3970
|
+
|
|
3971
|
+
if (!isPlainObject(options)) {
|
|
3972
|
+
throw new Error('node() options must be an object');
|
|
3973
|
+
}
|
|
3974
|
+
|
|
3975
|
+
if (options.className !== undefined) {
|
|
3976
|
+
const optionClassName = normalizeJavaScriptFragmentClassName(options.className);
|
|
3977
|
+
className = className ? `${className} ${optionClassName}` : optionClassName;
|
|
3978
|
+
}
|
|
3979
|
+
|
|
3980
|
+
return {
|
|
3981
|
+
__webKind: 'node',
|
|
3982
|
+
typeName: typeName.trim(),
|
|
3983
|
+
className,
|
|
3984
|
+
style: normalizeJavaScriptFragmentStyle(options.style),
|
|
3985
|
+
attributes: normalizeJavaScriptFragmentAttributes(options.attributes),
|
|
3986
|
+
textContent: options.textContent === undefined || options.textContent === null ? null : String(options.textContent),
|
|
3987
|
+
innerHTML: options.innerHTML === undefined || options.innerHTML === null ? null : String(options.innerHTML),
|
|
3988
|
+
children: normalizeJavaScriptFragmentChildren(options.children),
|
|
3989
|
+
};
|
|
3990
|
+
}
|
|
3991
|
+
|
|
3992
|
+
function createJavaScriptFragment(children) {
|
|
3993
|
+
return {
|
|
3994
|
+
__webKind: 'fragment',
|
|
3995
|
+
children: normalizeJavaScriptFragmentChildren(children),
|
|
3996
|
+
};
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3999
|
+
function createJavaScriptFragmentText(value) {
|
|
4000
|
+
return {
|
|
4001
|
+
__webKind: 'text',
|
|
4002
|
+
value: value === undefined || value === null ? '' : String(value),
|
|
4003
|
+
};
|
|
4004
|
+
}
|
|
4005
|
+
|
|
4006
|
+
function createJavaScriptFragmentHtml(value) {
|
|
4007
|
+
return {
|
|
4008
|
+
__webKind: 'html',
|
|
4009
|
+
value: value === undefined || value === null ? '' : String(value),
|
|
4010
|
+
};
|
|
4011
|
+
}
|
|
4012
|
+
|
|
4013
|
+
function normalizeJavaScriptFragmentClassName(value) {
|
|
4014
|
+
if (Array.isArray(value)) {
|
|
4015
|
+
return value
|
|
4016
|
+
.flatMap((entry) => String(entry).trim().split(/\s+/))
|
|
4017
|
+
.filter(Boolean)
|
|
4018
|
+
.join(' ');
|
|
4019
|
+
}
|
|
4020
|
+
|
|
4021
|
+
return String(value)
|
|
4022
|
+
.trim()
|
|
4023
|
+
.split(/\s+/)
|
|
4024
|
+
.filter(Boolean)
|
|
4025
|
+
.join(' ');
|
|
4026
|
+
}
|
|
4027
|
+
|
|
4028
|
+
function normalizeJavaScriptFragmentChildren(children) {
|
|
4029
|
+
if (children === undefined || children === null) {
|
|
4030
|
+
return [];
|
|
4031
|
+
}
|
|
4032
|
+
|
|
4033
|
+
if (Array.isArray(children)) {
|
|
4034
|
+
return children.flatMap((child) => normalizeJavaScriptFragmentChildren(child));
|
|
4035
|
+
}
|
|
4036
|
+
|
|
4037
|
+
return [children];
|
|
4038
|
+
}
|
|
4039
|
+
|
|
4040
|
+
function normalizeJavaScriptFragmentStyle(style) {
|
|
4041
|
+
if (style === undefined || style === null || style === '') {
|
|
4042
|
+
return null;
|
|
4043
|
+
}
|
|
4044
|
+
|
|
4045
|
+
if (typeof style === 'string') {
|
|
4046
|
+
return style.trim();
|
|
4047
|
+
}
|
|
4048
|
+
|
|
4049
|
+
if (!isPlainObject(style)) {
|
|
4050
|
+
throw new Error('node() style must be a string or object');
|
|
4051
|
+
}
|
|
4052
|
+
|
|
4053
|
+
return Object.entries(style)
|
|
4054
|
+
.filter(([, value]) => value !== undefined && value !== null && value !== false)
|
|
4055
|
+
.map(([property, value]) => `${camelToKebab(property)}: ${String(value)};`)
|
|
4056
|
+
.join(' ');
|
|
4057
|
+
}
|
|
4058
|
+
|
|
4059
|
+
function normalizeJavaScriptFragmentAttributes(attributes) {
|
|
4060
|
+
if (attributes === undefined || attributes === null) {
|
|
4061
|
+
return [];
|
|
4062
|
+
}
|
|
4063
|
+
|
|
4064
|
+
if (!isPlainObject(attributes)) {
|
|
4065
|
+
throw new Error('node() attributes must be an object');
|
|
4066
|
+
}
|
|
4067
|
+
|
|
4068
|
+
return Object.entries(attributes);
|
|
4069
|
+
}
|
|
4070
|
+
|
|
4071
|
+
function isJavaScriptFragmentValue(value) {
|
|
4072
|
+
if (value === undefined || value === null) {
|
|
4073
|
+
return false;
|
|
4074
|
+
}
|
|
4075
|
+
|
|
4076
|
+
if (Array.isArray(value)) {
|
|
4077
|
+
return value.every((entry) => isJavaScriptFragmentChild(entry));
|
|
4078
|
+
}
|
|
4079
|
+
|
|
4080
|
+
return isPlainObject(value) && ['node', 'fragment', 'text', 'html'].includes(value.__webKind);
|
|
4081
|
+
}
|
|
4082
|
+
|
|
4083
|
+
function isJavaScriptFragmentChild(value) {
|
|
4084
|
+
if (value === undefined || value === null) {
|
|
4085
|
+
return true;
|
|
4086
|
+
}
|
|
4087
|
+
|
|
4088
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
4089
|
+
return true;
|
|
4090
|
+
}
|
|
4091
|
+
|
|
4092
|
+
if (Array.isArray(value)) {
|
|
4093
|
+
return value.every((entry) => isJavaScriptFragmentChild(entry));
|
|
4094
|
+
}
|
|
4095
|
+
|
|
4096
|
+
return isPlainObject(value) && ['node', 'fragment', 'text', 'html'].includes(value.__webKind);
|
|
4097
|
+
}
|
|
4098
|
+
|
|
4099
|
+
function renderJavaScriptFragmentValue(value, symbolTable, indentLevel = 0) {
|
|
4100
|
+
if (value === undefined || value === null) {
|
|
4101
|
+
return '';
|
|
4102
|
+
}
|
|
4103
|
+
|
|
4104
|
+
if (Array.isArray(value)) {
|
|
4105
|
+
const parts = value
|
|
4106
|
+
.map((child) => renderJavaScriptFragmentValue(child, symbolTable, indentLevel))
|
|
4107
|
+
.filter(Boolean);
|
|
4108
|
+
return parts.join('\n');
|
|
4109
|
+
}
|
|
4110
|
+
|
|
4111
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
4112
|
+
return escapeHtml(String(value));
|
|
4113
|
+
}
|
|
4114
|
+
|
|
4115
|
+
if (!isPlainObject(value)) {
|
|
4116
|
+
throw new Error('Unsupported js fragment value');
|
|
4117
|
+
}
|
|
4118
|
+
|
|
4119
|
+
if (value.__webKind === 'fragment') {
|
|
4120
|
+
return renderJavaScriptFragmentValue(value.children, symbolTable, indentLevel);
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
if (value.__webKind === 'text') {
|
|
4124
|
+
return escapeHtml(value.value);
|
|
4125
|
+
}
|
|
4126
|
+
|
|
4127
|
+
if (value.__webKind === 'html') {
|
|
4128
|
+
return value.value;
|
|
4129
|
+
}
|
|
4130
|
+
|
|
4131
|
+
if (value.__webKind !== 'node') {
|
|
4132
|
+
throw new Error(`Unsupported js fragment kind "${value.__webKind}"`);
|
|
4133
|
+
}
|
|
4134
|
+
|
|
4135
|
+
return renderJavaScriptFragmentNode(value, symbolTable, indentLevel);
|
|
4136
|
+
}
|
|
4137
|
+
|
|
4138
|
+
function renderJavaScriptFragmentNode(node, symbolTable, indentLevel) {
|
|
4139
|
+
const indentText = indent(indentLevel);
|
|
4140
|
+
const contentIndent = indent(indentLevel + 1);
|
|
4141
|
+
const tagName = inferHtmlTag(node.typeName, symbolTable);
|
|
4142
|
+
const attributeText = renderJavaScriptFragmentAttributes(node);
|
|
4143
|
+
const openTag = `${indentText}<${tagName}${attributeText}>`;
|
|
4144
|
+
|
|
4145
|
+
if (VOID_HTML_TAGS.has(tagName)) {
|
|
4146
|
+
if (node.textContent !== null || node.innerHTML !== null || node.children.length > 0) {
|
|
4147
|
+
throw new Error(`Void element <${tagName}> cannot contain content in js fragments`);
|
|
4148
|
+
}
|
|
4149
|
+
|
|
4150
|
+
return openTag;
|
|
4151
|
+
}
|
|
4152
|
+
|
|
4153
|
+
const innerParts = [];
|
|
4154
|
+
|
|
4155
|
+
if (node.textContent !== null) {
|
|
4156
|
+
innerParts.push(...prefixLines(escapeHtml(node.textContent), contentIndent));
|
|
4157
|
+
}
|
|
4158
|
+
|
|
4159
|
+
if (node.innerHTML !== null) {
|
|
4160
|
+
innerParts.push(...prefixLines(node.innerHTML, contentIndent));
|
|
4161
|
+
}
|
|
4162
|
+
|
|
4163
|
+
for (const child of node.children) {
|
|
4164
|
+
const renderedChild = renderJavaScriptFragmentValue(child, symbolTable, indentLevel + 1);
|
|
4165
|
+
if (renderedChild) {
|
|
4166
|
+
innerParts.push(renderedChild);
|
|
4167
|
+
}
|
|
4168
|
+
}
|
|
4169
|
+
|
|
4170
|
+
if (innerParts.length === 0) {
|
|
4171
|
+
return `${openTag}</${tagName}>`;
|
|
4172
|
+
}
|
|
4173
|
+
|
|
4174
|
+
if (innerParts.length === 1 && !innerParts[0].includes('\n')) {
|
|
4175
|
+
return `${openTag}${innerParts[0].trimStart()}</${tagName}>`;
|
|
4176
|
+
}
|
|
4177
|
+
|
|
4178
|
+
return [openTag, ...innerParts, `${indentText}</${tagName}>`].join('\n');
|
|
4179
|
+
}
|
|
4180
|
+
|
|
4181
|
+
function renderJavaScriptFragmentAttributes(node) {
|
|
4182
|
+
const attributes = [];
|
|
4183
|
+
|
|
4184
|
+
if (node.className) {
|
|
4185
|
+
attributes.push(['class', node.className]);
|
|
4186
|
+
}
|
|
4187
|
+
|
|
4188
|
+
if (node.style) {
|
|
4189
|
+
attributes.push(['style', node.style]);
|
|
4190
|
+
}
|
|
4191
|
+
|
|
4192
|
+
for (const entry of node.attributes) {
|
|
4193
|
+
attributes.push(entry);
|
|
4194
|
+
}
|
|
4195
|
+
|
|
4196
|
+
return renderAttributeEntries(attributes);
|
|
4197
|
+
}
|
|
4198
|
+
|
|
4199
|
+
function renderHtmlNodeAttributes(node) {
|
|
4200
|
+
return renderAttributeEntries([
|
|
4201
|
+
['class', node.name],
|
|
4202
|
+
...node.attributes.entries(),
|
|
4203
|
+
]);
|
|
4204
|
+
}
|
|
4205
|
+
|
|
4206
|
+
function normalizeHtmlAttributeName(value, line, column) {
|
|
4207
|
+
const normalized = camelToKebab(value);
|
|
4208
|
+
|
|
4209
|
+
if (normalized === 'class' || normalized === 'style') {
|
|
4210
|
+
throw new CompilerError(
|
|
4211
|
+
'::attrs cannot set "class" or "style". WEB manages class names automatically, and visual styling should use normal WEB properties.',
|
|
4212
|
+
line,
|
|
4213
|
+
column
|
|
4214
|
+
);
|
|
4215
|
+
}
|
|
4216
|
+
|
|
4217
|
+
return normalized;
|
|
4218
|
+
}
|
|
4219
|
+
|
|
4220
|
+
function normalizeLooseHtmlAttributeName(value) {
|
|
4221
|
+
return camelToKebab(value);
|
|
4222
|
+
}
|
|
4223
|
+
|
|
4224
|
+
function renderAttributeEntries(attributeEntries) {
|
|
4225
|
+
const attributes = [];
|
|
4226
|
+
|
|
4227
|
+
for (const [name, value] of attributeEntries) {
|
|
4228
|
+
if (value === undefined || value === null || value === false) {
|
|
4229
|
+
continue;
|
|
4230
|
+
}
|
|
4231
|
+
|
|
4232
|
+
if (value === true) {
|
|
4233
|
+
attributes.push(`${name}`);
|
|
4234
|
+
continue;
|
|
4235
|
+
}
|
|
4236
|
+
|
|
4237
|
+
attributes.push(`${name}="${escapeAttribute(String(value))}"`);
|
|
4238
|
+
}
|
|
4239
|
+
|
|
4240
|
+
return attributes.length > 0 ? ` ${attributes.join(' ')}` : '';
|
|
4241
|
+
}
|
|
4242
|
+
|
|
4243
|
+
function formatJavaScriptInjectionError(message) {
|
|
4244
|
+
return `JavaScript error: ${message}`;
|
|
4245
|
+
}
|
|
4246
|
+
|
|
4247
|
+
function inferLastDeclaredVariableName(source) {
|
|
4248
|
+
let index = 0;
|
|
4249
|
+
let braceDepth = 0;
|
|
4250
|
+
let parenDepth = 0;
|
|
4251
|
+
let bracketDepth = 0;
|
|
4252
|
+
let lastMatch = null;
|
|
4253
|
+
|
|
4254
|
+
while (index < source.length) {
|
|
4255
|
+
const char = source[index];
|
|
4256
|
+
const next = source[index + 1];
|
|
4257
|
+
|
|
4258
|
+
if (char === '"' || char === '\'') {
|
|
4259
|
+
index = skipJavaScriptQuotedTextSource(source, index);
|
|
4260
|
+
continue;
|
|
4261
|
+
}
|
|
4262
|
+
|
|
4263
|
+
if (char === '`') {
|
|
4264
|
+
index = skipJavaScriptTemplateLiteralSource(source, index);
|
|
4265
|
+
continue;
|
|
4266
|
+
}
|
|
4267
|
+
|
|
4268
|
+
if (char === '/' && next === '/') {
|
|
4269
|
+
index = skipJavaScriptLineCommentSource(source, index);
|
|
4270
|
+
continue;
|
|
4271
|
+
}
|
|
4272
|
+
|
|
4273
|
+
if (char === '/' && next === '*') {
|
|
4274
|
+
index = skipJavaScriptBlockCommentSource(source, index);
|
|
4275
|
+
continue;
|
|
4276
|
+
}
|
|
4277
|
+
|
|
4278
|
+
if (char === '{') {
|
|
4279
|
+
braceDepth += 1;
|
|
4280
|
+
index += 1;
|
|
4281
|
+
continue;
|
|
4282
|
+
}
|
|
4283
|
+
|
|
4284
|
+
if (char === '}') {
|
|
4285
|
+
braceDepth = Math.max(0, braceDepth - 1);
|
|
4286
|
+
index += 1;
|
|
4287
|
+
continue;
|
|
4288
|
+
}
|
|
4289
|
+
|
|
4290
|
+
if (char === '(') {
|
|
4291
|
+
parenDepth += 1;
|
|
4292
|
+
index += 1;
|
|
4293
|
+
continue;
|
|
4294
|
+
}
|
|
4295
|
+
|
|
4296
|
+
if (char === ')') {
|
|
4297
|
+
parenDepth = Math.max(0, parenDepth - 1);
|
|
4298
|
+
index += 1;
|
|
4299
|
+
continue;
|
|
4300
|
+
}
|
|
4301
|
+
|
|
4302
|
+
if (char === '[') {
|
|
4303
|
+
bracketDepth += 1;
|
|
4304
|
+
index += 1;
|
|
4305
|
+
continue;
|
|
4306
|
+
}
|
|
4307
|
+
|
|
4308
|
+
if (char === ']') {
|
|
4309
|
+
bracketDepth = Math.max(0, bracketDepth - 1);
|
|
4310
|
+
index += 1;
|
|
4311
|
+
continue;
|
|
4312
|
+
}
|
|
4313
|
+
|
|
4314
|
+
if (
|
|
4315
|
+
braceDepth === 0 &&
|
|
4316
|
+
parenDepth === 0 &&
|
|
4317
|
+
bracketDepth === 0 &&
|
|
4318
|
+
isIdentifierStart(char)
|
|
4319
|
+
) {
|
|
4320
|
+
const wordResult = readIdentifierFromSource(source, index);
|
|
4321
|
+
const keyword = wordResult.value;
|
|
4322
|
+
index = wordResult.nextIndex;
|
|
4323
|
+
|
|
4324
|
+
if (keyword === 'let' || keyword === 'const' || keyword === 'var') {
|
|
4325
|
+
index = skipJavaScriptWhitespaceSource(source, index);
|
|
4326
|
+
|
|
4327
|
+
if (isIdentifierStart(source[index])) {
|
|
4328
|
+
const identifierResult = readIdentifierFromSource(source, index);
|
|
4329
|
+
lastMatch = identifierResult.value;
|
|
4330
|
+
index = identifierResult.nextIndex;
|
|
4331
|
+
}
|
|
4332
|
+
}
|
|
4333
|
+
|
|
4334
|
+
continue;
|
|
4335
|
+
}
|
|
4336
|
+
|
|
4337
|
+
index += 1;
|
|
4338
|
+
}
|
|
4339
|
+
|
|
4340
|
+
return lastMatch;
|
|
4341
|
+
}
|
|
4342
|
+
|
|
4343
|
+
function camelToKebab(value) {
|
|
4344
|
+
return value.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
|
|
4345
|
+
}
|
|
4346
|
+
|
|
4347
|
+
function isPlainObject(value) {
|
|
4348
|
+
return Object.prototype.toString.call(value) === '[object Object]';
|
|
4349
|
+
}
|
|
4350
|
+
|
|
4351
|
+
function splitTypeWords(value) {
|
|
4352
|
+
return String(value)
|
|
4353
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
4354
|
+
.replace(/([A-Z])([A-Z][a-z])/g, '$1 $2')
|
|
4355
|
+
.replace(/[_-]+/g, ' ')
|
|
4356
|
+
.trim()
|
|
4357
|
+
.split(/\s+/)
|
|
4358
|
+
.flatMap((part) => part.split(/(?<=\D)(?=\d)|(?<=\d)(?=\D)/))
|
|
4359
|
+
.map((part) => part.toLowerCase())
|
|
4360
|
+
.filter(Boolean);
|
|
4361
|
+
}
|
|
4362
|
+
|
|
4363
|
+
function escapeHtml(value) {
|
|
4364
|
+
return String(value)
|
|
4365
|
+
.replace(/&/g, '&')
|
|
4366
|
+
.replace(/</g, '<')
|
|
4367
|
+
.replace(/>/g, '>')
|
|
4368
|
+
.replace(/"/g, '"')
|
|
4369
|
+
.replace(/'/g, ''');
|
|
4370
|
+
}
|
|
4371
|
+
|
|
4372
|
+
function escapeAttribute(value) {
|
|
4373
|
+
return escapeHtml(value);
|
|
4374
|
+
}
|
|
4375
|
+
|
|
4376
|
+
function prefixLines(text, linePrefix) {
|
|
4377
|
+
return String(text)
|
|
4378
|
+
.split('\n')
|
|
4379
|
+
.map((line) => `${linePrefix}${line}`);
|
|
4380
|
+
}
|
|
4381
|
+
|
|
4382
|
+
function quoteCssString(value) {
|
|
4383
|
+
return JSON.stringify(String(value));
|
|
4384
|
+
}
|
|
4385
|
+
|
|
4386
|
+
function formatCssComment(value) {
|
|
4387
|
+
const sanitized = String(value).replace(/\*\//g, '* /');
|
|
4388
|
+
return `/* ${sanitized} */`;
|
|
4389
|
+
}
|
|
4390
|
+
|
|
4391
|
+
function skipJavaScriptWhitespaceSource(source, startIndex) {
|
|
4392
|
+
let index = startIndex;
|
|
4393
|
+
|
|
4394
|
+
while (index < source.length && /\s/.test(source[index])) {
|
|
4395
|
+
index += 1;
|
|
4396
|
+
}
|
|
4397
|
+
|
|
4398
|
+
return index;
|
|
4399
|
+
}
|
|
4400
|
+
|
|
4401
|
+
function readIdentifierFromSource(source, startIndex) {
|
|
4402
|
+
let index = startIndex;
|
|
4403
|
+
let value = '';
|
|
4404
|
+
|
|
4405
|
+
while (index < source.length && isIdentifierPart(source[index])) {
|
|
4406
|
+
value += source[index];
|
|
4407
|
+
index += 1;
|
|
4408
|
+
}
|
|
4409
|
+
|
|
4410
|
+
return {
|
|
4411
|
+
value,
|
|
4412
|
+
nextIndex: index,
|
|
4413
|
+
};
|
|
4414
|
+
}
|
|
4415
|
+
|
|
4416
|
+
function skipJavaScriptQuotedTextSource(source, startIndex) {
|
|
4417
|
+
const quote = source[startIndex];
|
|
4418
|
+
let index = startIndex + 1;
|
|
4419
|
+
|
|
4420
|
+
while (index < source.length) {
|
|
4421
|
+
const char = source[index];
|
|
4422
|
+
|
|
4423
|
+
if (char === '\\') {
|
|
4424
|
+
index += 2;
|
|
4425
|
+
continue;
|
|
4426
|
+
}
|
|
4427
|
+
|
|
4428
|
+
if (char === quote) {
|
|
4429
|
+
return index + 1;
|
|
4430
|
+
}
|
|
4431
|
+
|
|
4432
|
+
index += 1;
|
|
4433
|
+
}
|
|
4434
|
+
|
|
4435
|
+
return source.length;
|
|
4436
|
+
}
|
|
4437
|
+
|
|
4438
|
+
function skipJavaScriptTemplateLiteralSource(source, startIndex) {
|
|
4439
|
+
let index = startIndex + 1;
|
|
4440
|
+
|
|
4441
|
+
while (index < source.length) {
|
|
4442
|
+
const char = source[index];
|
|
4443
|
+
const next = source[index + 1];
|
|
4444
|
+
|
|
4445
|
+
if (char === '\\') {
|
|
4446
|
+
index += 2;
|
|
4447
|
+
continue;
|
|
4448
|
+
}
|
|
4449
|
+
|
|
4450
|
+
if (char === '`') {
|
|
4451
|
+
return index + 1;
|
|
4452
|
+
}
|
|
4453
|
+
|
|
4454
|
+
if (char === '$' && next === '{') {
|
|
4455
|
+
index = skipJavaScriptTemplateInterpolationSource(source, index + 2);
|
|
4456
|
+
continue;
|
|
4457
|
+
}
|
|
4458
|
+
|
|
4459
|
+
index += 1;
|
|
4460
|
+
}
|
|
4461
|
+
|
|
4462
|
+
return source.length;
|
|
4463
|
+
}
|
|
4464
|
+
|
|
4465
|
+
function skipJavaScriptTemplateInterpolationSource(source, startIndex) {
|
|
4466
|
+
let index = startIndex;
|
|
4467
|
+
let depth = 1;
|
|
4468
|
+
|
|
4469
|
+
while (index < source.length && depth > 0) {
|
|
4470
|
+
const char = source[index];
|
|
4471
|
+
const next = source[index + 1];
|
|
4472
|
+
|
|
4473
|
+
if (char === '"' || char === '\'') {
|
|
4474
|
+
index = skipJavaScriptQuotedTextSource(source, index);
|
|
4475
|
+
continue;
|
|
4476
|
+
}
|
|
4477
|
+
|
|
4478
|
+
if (char === '`') {
|
|
4479
|
+
index = skipJavaScriptTemplateLiteralSource(source, index);
|
|
4480
|
+
continue;
|
|
4481
|
+
}
|
|
4482
|
+
|
|
4483
|
+
if (char === '/' && next === '/') {
|
|
4484
|
+
index = skipJavaScriptLineCommentSource(source, index);
|
|
4485
|
+
continue;
|
|
4486
|
+
}
|
|
4487
|
+
|
|
4488
|
+
if (char === '/' && next === '*') {
|
|
4489
|
+
index = skipJavaScriptBlockCommentSource(source, index);
|
|
4490
|
+
continue;
|
|
4491
|
+
}
|
|
4492
|
+
|
|
4493
|
+
if (char === '{') {
|
|
4494
|
+
depth += 1;
|
|
4495
|
+
index += 1;
|
|
4496
|
+
continue;
|
|
4497
|
+
}
|
|
4498
|
+
|
|
4499
|
+
if (char === '}') {
|
|
4500
|
+
depth -= 1;
|
|
4501
|
+
index += 1;
|
|
4502
|
+
continue;
|
|
4503
|
+
}
|
|
4504
|
+
|
|
4505
|
+
index += 1;
|
|
4506
|
+
}
|
|
4507
|
+
|
|
4508
|
+
return index;
|
|
4509
|
+
}
|
|
4510
|
+
|
|
4511
|
+
function skipJavaScriptLineCommentSource(source, startIndex) {
|
|
4512
|
+
let index = startIndex + 2;
|
|
4513
|
+
|
|
4514
|
+
while (index < source.length && source[index] !== '\n') {
|
|
4515
|
+
index += 1;
|
|
4516
|
+
}
|
|
4517
|
+
|
|
4518
|
+
return index;
|
|
4519
|
+
}
|
|
4520
|
+
|
|
4521
|
+
function skipJavaScriptBlockCommentSource(source, startIndex) {
|
|
4522
|
+
let index = startIndex + 2;
|
|
4523
|
+
|
|
4524
|
+
while (index < source.length && !(source[index] === '*' && source[index + 1] === '/')) {
|
|
4525
|
+
index += 1;
|
|
4526
|
+
}
|
|
4527
|
+
|
|
4528
|
+
return index >= source.length ? source.length : index + 2;
|
|
4529
|
+
}
|
|
4530
|
+
|
|
4531
|
+
function indent(level) {
|
|
4532
|
+
return ' '.repeat(level);
|
|
4533
|
+
}
|
|
4534
|
+
|
|
4535
|
+
function indentMultiline(text, level) {
|
|
4536
|
+
if (!text) {
|
|
4537
|
+
return indent(level);
|
|
4538
|
+
}
|
|
4539
|
+
|
|
4540
|
+
return text
|
|
4541
|
+
.split('\n')
|
|
4542
|
+
.map((line) => `${indent(level)}${line}`)
|
|
4543
|
+
.join('\n');
|
|
4544
|
+
}
|
|
4545
|
+
|
|
4546
|
+
function isIdentifierStart(char) {
|
|
4547
|
+
return typeof char === 'string' && /^[A-Za-z_$]$/.test(char);
|
|
4548
|
+
}
|
|
4549
|
+
|
|
4550
|
+
function isIdentifierPart(char) {
|
|
4551
|
+
return typeof char === 'string' && /^[A-Za-z0-9_$]$/.test(char);
|
|
4552
|
+
}
|
|
4553
|
+
|
|
4554
|
+
function isDigit(char) {
|
|
4555
|
+
return typeof char === 'string' && /^[0-9]$/.test(char);
|
|
4556
|
+
}
|
|
4557
|
+
|
|
4558
|
+
function shouldStartNestedJavaScriptTemplate(currentValue) {
|
|
4559
|
+
const lastSignificantChar = getLastSignificantChar(currentValue);
|
|
4560
|
+
|
|
4561
|
+
if (!lastSignificantChar) {
|
|
4562
|
+
return false;
|
|
4563
|
+
}
|
|
4564
|
+
|
|
4565
|
+
if ('=([{,:?+-*/%!&|^~<>'.includes(lastSignificantChar)) {
|
|
4566
|
+
return true;
|
|
4567
|
+
}
|
|
4568
|
+
|
|
4569
|
+
const trailingWord = getTrailingJavaScriptWord(currentValue);
|
|
4570
|
+
return ['return', 'throw', 'case', 'yield', 'await', 'typeof', 'void', 'new', 'delete'].includes(trailingWord);
|
|
4571
|
+
}
|
|
4572
|
+
|
|
4573
|
+
function getLastSignificantChar(value) {
|
|
4574
|
+
for (let index = value.length - 1; index >= 0; index -= 1) {
|
|
4575
|
+
if (!/\s/.test(value[index])) {
|
|
4576
|
+
return value[index];
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4579
|
+
|
|
4580
|
+
return null;
|
|
4581
|
+
}
|
|
4582
|
+
|
|
4583
|
+
function getTrailingJavaScriptWord(value) {
|
|
4584
|
+
const match = value.match(/([A-Za-z_$][A-Za-z0-9_$]*)\s*$/);
|
|
4585
|
+
return match ? match[1] : '';
|
|
4586
|
+
}
|
|
4587
|
+
|
|
4588
|
+
module.exports = {
|
|
4589
|
+
CompilerError,
|
|
4590
|
+
compileSource,
|
|
4591
|
+
compileFile,
|
|
4592
|
+
resolveCompileOutputPaths,
|
|
4593
|
+
};
|
|
4594
|
+
|
|
4595
|
+
if (require.main === module) {
|
|
4596
|
+
try {
|
|
4597
|
+
main();
|
|
4598
|
+
} catch (error) {
|
|
4599
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
4600
|
+
process.exit(1);
|
|
4601
|
+
}
|
|
4602
|
+
}
|