pulse-js-framework 1.7.25 → 1.7.29
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/README.md +26 -0
- package/cli/help.js +14 -3
- package/cli/index.js +188 -2
- package/compiler/lexer.js +159 -16
- package/compiler/transformer/expressions.js +7 -2
- package/compiler/transformer/style.js +185 -20
- package/compiler/transformer/view.js +77 -46
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,6 +48,27 @@ npm install
|
|
|
48
48
|
npm run dev
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
### Or from a template
|
|
52
|
+
|
|
53
|
+
Create projects from built-in example apps:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# E-commerce app (products, cart, checkout)
|
|
57
|
+
npx pulse-js-framework create my-shop --ecommerce
|
|
58
|
+
|
|
59
|
+
# Todo app (filtering, local storage)
|
|
60
|
+
npx pulse-js-framework create my-todos --todo
|
|
61
|
+
|
|
62
|
+
# Blog (posts, sidebar, navigation)
|
|
63
|
+
npx pulse-js-framework create my-blog --blog
|
|
64
|
+
|
|
65
|
+
# Chat app (messages, users, emoji picker)
|
|
66
|
+
npx pulse-js-framework create my-chat --chat
|
|
67
|
+
|
|
68
|
+
# Dashboard (data visualization)
|
|
69
|
+
npx pulse-js-framework create my-dashboard --dashboard
|
|
70
|
+
```
|
|
71
|
+
|
|
51
72
|
### Or use directly
|
|
52
73
|
|
|
53
74
|
```javascript
|
|
@@ -139,6 +160,11 @@ See [Pulse DSL documentation](docs/pulse-dsl.md) for full syntax reference.
|
|
|
139
160
|
# Project Creation
|
|
140
161
|
pulse create <name> # Create new project
|
|
141
162
|
pulse create <name> --typescript # Create TypeScript project
|
|
163
|
+
pulse create <name> --ecommerce # Create from E-Commerce template
|
|
164
|
+
pulse create <name> --todo # Create from Todo App template
|
|
165
|
+
pulse create <name> --blog # Create from Blog template
|
|
166
|
+
pulse create <name> --chat # Create from Chat template
|
|
167
|
+
pulse create <name> --dashboard # Create from Dashboard template
|
|
142
168
|
pulse init --typescript # Initialize in current directory
|
|
143
169
|
|
|
144
170
|
# Development
|
package/cli/help.js
CHANGED
|
@@ -29,18 +29,29 @@ Creates a new Pulse project with a complete starter template including:
|
|
|
29
29
|
- Project structure (src/, public/)
|
|
30
30
|
- Vite configuration for development and building
|
|
31
31
|
- Sample App.pulse component with counter example
|
|
32
|
-
- Package.json with all necessary scripts
|
|
32
|
+
- Package.json with all necessary scripts
|
|
33
|
+
|
|
34
|
+
You can also create projects from built-in example templates using
|
|
35
|
+
the template flags (--ecommerce, --todo, --blog, --chat, --dashboard).`,
|
|
33
36
|
arguments: [
|
|
34
37
|
{ name: '<name>', description: 'Name of the project directory to create' }
|
|
35
38
|
],
|
|
36
39
|
options: [
|
|
37
40
|
{ flag: '--typescript, --ts', description: 'Create a TypeScript project with tsconfig.json' },
|
|
38
|
-
{ flag: '--minimal', description: 'Create minimal project structure without extras' }
|
|
41
|
+
{ flag: '--minimal', description: 'Create minimal project structure without extras' },
|
|
42
|
+
{ flag: '--ecommerce', description: 'Use E-Commerce template (products, cart, checkout)' },
|
|
43
|
+
{ flag: '--todo', description: 'Use Todo App template (filtering, local storage)' },
|
|
44
|
+
{ flag: '--blog', description: 'Use Blog template (posts, sidebar, navigation)' },
|
|
45
|
+
{ flag: '--chat', description: 'Use Chat template (messages, users, emoji picker)' },
|
|
46
|
+
{ flag: '--dashboard', description: 'Use Dashboard template (data visualization)' }
|
|
39
47
|
],
|
|
40
48
|
examples: [
|
|
41
49
|
{ cmd: 'pulse create my-app', desc: 'Create a new JavaScript project' },
|
|
42
50
|
{ cmd: 'pulse create my-app --typescript', desc: 'Create a new TypeScript project' },
|
|
43
|
-
{ cmd: 'pulse create my-app --minimal', desc: 'Create a minimal project' }
|
|
51
|
+
{ cmd: 'pulse create my-app --minimal', desc: 'Create a minimal project' },
|
|
52
|
+
{ cmd: 'pulse create my-shop --ecommerce', desc: 'Create from E-Commerce template' },
|
|
53
|
+
{ cmd: 'pulse create my-todos --todo', desc: 'Create from Todo App template' },
|
|
54
|
+
{ cmd: 'pulse create my-blog --blog', desc: 'Create from Blog template' }
|
|
44
55
|
]
|
|
45
56
|
},
|
|
46
57
|
|
package/cli/index.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
8
|
import { dirname, join, resolve, relative } from 'path';
|
|
9
|
-
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, watch } from 'fs';
|
|
9
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, watch, cpSync, statSync } from 'fs';
|
|
10
10
|
import { log } from './logger.js';
|
|
11
11
|
import { findPulseFiles, parseArgs } from './utils/file-utils.js';
|
|
12
12
|
import { runHelp } from './help.js';
|
|
@@ -18,6 +18,15 @@ const __dirname = dirname(__filename);
|
|
|
18
18
|
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
19
19
|
const VERSION = pkg.version;
|
|
20
20
|
|
|
21
|
+
// Available example templates
|
|
22
|
+
const TEMPLATES = {
|
|
23
|
+
ecommerce: { name: 'E-Commerce', description: 'Shopping cart with products, checkout, and cart management' },
|
|
24
|
+
todo: { name: 'Todo App', description: 'Classic todo list with filtering and local storage' },
|
|
25
|
+
blog: { name: 'Blog', description: 'Blogging platform with posts, sidebar, and navigation' },
|
|
26
|
+
chat: { name: 'Chat', description: 'Real-time chat with messages, users, and emoji picker' },
|
|
27
|
+
dashboard: { name: 'Dashboard', description: 'Analytics dashboard with data visualization' }
|
|
28
|
+
};
|
|
29
|
+
|
|
21
30
|
// Command handlers
|
|
22
31
|
const commands = {
|
|
23
32
|
help: showHelp,
|
|
@@ -191,6 +200,87 @@ function showVersion() {
|
|
|
191
200
|
log.info(`Pulse Framework v${VERSION}`);
|
|
192
201
|
}
|
|
193
202
|
|
|
203
|
+
/**
|
|
204
|
+
* Copy example template to project directory
|
|
205
|
+
* Transforms imports from /runtime/index.js to pulse-js-framework/runtime
|
|
206
|
+
* @param {string} templateName - Name of the template (ecommerce, todo, blog, chat, dashboard)
|
|
207
|
+
* @param {string} projectPath - Destination project path
|
|
208
|
+
* @param {string} projectName - Name of the project
|
|
209
|
+
*/
|
|
210
|
+
function copyExampleTemplate(templateName, projectPath, projectName) {
|
|
211
|
+
const examplesDir = join(__dirname, '..', 'examples');
|
|
212
|
+
const templateDir = join(examplesDir, templateName);
|
|
213
|
+
|
|
214
|
+
if (!existsSync(templateDir)) {
|
|
215
|
+
throw new Error(`Template "${templateName}" not found at ${templateDir}`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Recursively copy directory, transforming JS files
|
|
220
|
+
*/
|
|
221
|
+
function copyDir(src, dest) {
|
|
222
|
+
if (!existsSync(dest)) {
|
|
223
|
+
mkdirSync(dest, { recursive: true });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const entries = readdirSync(src, { withFileTypes: true });
|
|
227
|
+
|
|
228
|
+
for (const entry of entries) {
|
|
229
|
+
const srcPath = join(src, entry.name);
|
|
230
|
+
const destPath = join(dest, entry.name);
|
|
231
|
+
|
|
232
|
+
// Skip node_modules and dist directories
|
|
233
|
+
if (entry.name === 'node_modules' || entry.name === 'dist') {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (entry.isDirectory()) {
|
|
238
|
+
copyDir(srcPath, destPath);
|
|
239
|
+
} else {
|
|
240
|
+
// Read file content
|
|
241
|
+
let content = readFileSync(srcPath, 'utf-8');
|
|
242
|
+
|
|
243
|
+
// Transform imports in JS files
|
|
244
|
+
if (entry.name.endsWith('.js') || entry.name.endsWith('.ts')) {
|
|
245
|
+
// Transform /runtime/index.js imports to pulse-js-framework/runtime
|
|
246
|
+
content = content.replace(
|
|
247
|
+
/from\s+['"]\/runtime\/index\.js['"]/g,
|
|
248
|
+
"from 'pulse-js-framework/runtime'"
|
|
249
|
+
);
|
|
250
|
+
content = content.replace(
|
|
251
|
+
/from\s+['"]\/runtime['"]/g,
|
|
252
|
+
"from 'pulse-js-framework/runtime'"
|
|
253
|
+
);
|
|
254
|
+
// Transform other runtime submodule imports
|
|
255
|
+
content = content.replace(
|
|
256
|
+
/from\s+['"]\/runtime\/([^'"]+)['"]/g,
|
|
257
|
+
"from 'pulse-js-framework/runtime/$1'"
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
writeFileSync(destPath, content);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Copy src directory
|
|
267
|
+
const srcDir = join(templateDir, 'src');
|
|
268
|
+
if (existsSync(srcDir)) {
|
|
269
|
+
copyDir(srcDir, join(projectPath, 'src'));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Copy index.html if exists
|
|
273
|
+
const indexHtml = join(templateDir, 'index.html');
|
|
274
|
+
if (existsSync(indexHtml)) {
|
|
275
|
+
let content = readFileSync(indexHtml, 'utf-8');
|
|
276
|
+
// Update title to project name
|
|
277
|
+
content = content.replace(/<title>[^<]*<\/title>/, `<title>${projectName}</title>`);
|
|
278
|
+
writeFileSync(join(projectPath, 'index.html'), content);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
|
|
194
284
|
/**
|
|
195
285
|
* Create a new project
|
|
196
286
|
*/
|
|
@@ -200,7 +290,16 @@ async function createProject(args) {
|
|
|
200
290
|
|
|
201
291
|
if (!projectName) {
|
|
202
292
|
log.error('Please provide a project name.');
|
|
203
|
-
log.info('Usage: pulse create <project-name>');
|
|
293
|
+
log.info('Usage: pulse create <project-name> [options]');
|
|
294
|
+
log.info('');
|
|
295
|
+
log.info('Options:');
|
|
296
|
+
log.info(' --typescript, --ts Create TypeScript project');
|
|
297
|
+
log.info(' --minimal Create minimal project');
|
|
298
|
+
log.info('');
|
|
299
|
+
log.info('Templates (use existing example apps):');
|
|
300
|
+
for (const [key, info] of Object.entries(TEMPLATES)) {
|
|
301
|
+
log.info(` --${key.padEnd(12)} ${info.description}`);
|
|
302
|
+
}
|
|
204
303
|
process.exit(1);
|
|
205
304
|
}
|
|
206
305
|
|
|
@@ -214,6 +313,93 @@ async function createProject(args) {
|
|
|
214
313
|
const useTypescript = options.typescript || options.ts || false;
|
|
215
314
|
const minimal = options.minimal || false;
|
|
216
315
|
|
|
316
|
+
// Check for template options
|
|
317
|
+
let selectedTemplate = null;
|
|
318
|
+
for (const templateName of Object.keys(TEMPLATES)) {
|
|
319
|
+
if (options[templateName]) {
|
|
320
|
+
selectedTemplate = templateName;
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// If a template is selected, use the template-based creation
|
|
326
|
+
if (selectedTemplate) {
|
|
327
|
+
const templateInfo = TEMPLATES[selectedTemplate];
|
|
328
|
+
log.info(`Creating new Pulse project: ${projectName} (${templateInfo.name} template)`);
|
|
329
|
+
|
|
330
|
+
// Create project directory
|
|
331
|
+
mkdirSync(projectPath);
|
|
332
|
+
mkdirSync(join(projectPath, 'public'));
|
|
333
|
+
|
|
334
|
+
// Copy template files
|
|
335
|
+
try {
|
|
336
|
+
copyExampleTemplate(selectedTemplate, projectPath, projectName);
|
|
337
|
+
log.info(` ✓ Copied ${templateInfo.name} template files`);
|
|
338
|
+
} catch (err) {
|
|
339
|
+
log.error(`Failed to copy template: ${err.message}`);
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Create package.json for template project
|
|
344
|
+
const packageJson = {
|
|
345
|
+
name: projectName,
|
|
346
|
+
version: '0.1.0',
|
|
347
|
+
type: 'module',
|
|
348
|
+
scripts: {
|
|
349
|
+
dev: 'pulse dev',
|
|
350
|
+
build: 'pulse build',
|
|
351
|
+
preview: 'vite preview',
|
|
352
|
+
test: 'pulse test',
|
|
353
|
+
lint: 'pulse lint'
|
|
354
|
+
},
|
|
355
|
+
dependencies: {
|
|
356
|
+
'pulse-js-framework': '^1.0.0'
|
|
357
|
+
},
|
|
358
|
+
devDependencies: {
|
|
359
|
+
vite: '^5.0.0'
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
writeFileSync(
|
|
364
|
+
join(projectPath, 'package.json'),
|
|
365
|
+
JSON.stringify(packageJson, null, 2)
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
// Create vite.config.js
|
|
369
|
+
const viteConfig = `import { defineConfig } from 'vite';
|
|
370
|
+
import pulse from 'pulse-js-framework/vite';
|
|
371
|
+
|
|
372
|
+
export default defineConfig({
|
|
373
|
+
plugins: [pulse()]
|
|
374
|
+
});
|
|
375
|
+
`;
|
|
376
|
+
writeFileSync(join(projectPath, 'vite.config.js'), viteConfig);
|
|
377
|
+
|
|
378
|
+
// Create .gitignore
|
|
379
|
+
const gitignore = `node_modules
|
|
380
|
+
dist
|
|
381
|
+
.DS_Store
|
|
382
|
+
*.local
|
|
383
|
+
`;
|
|
384
|
+
writeFileSync(join(projectPath, '.gitignore'), gitignore);
|
|
385
|
+
|
|
386
|
+
log.info(`
|
|
387
|
+
Project created successfully!
|
|
388
|
+
|
|
389
|
+
Next steps:
|
|
390
|
+
cd ${projectName}
|
|
391
|
+
npm install
|
|
392
|
+
npm run dev
|
|
393
|
+
|
|
394
|
+
Template: ${templateInfo.name}
|
|
395
|
+
${templateInfo.description}
|
|
396
|
+
|
|
397
|
+
Happy coding with Pulse!
|
|
398
|
+
`);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Standard project creation (no template)
|
|
217
403
|
log.info(`Creating new Pulse project: ${projectName}${useTypescript ? ' (TypeScript)' : ''}`);
|
|
218
404
|
|
|
219
405
|
// Create project structure
|
package/compiler/lexer.js
CHANGED
|
@@ -85,13 +85,24 @@ export const TokenType = {
|
|
|
85
85
|
PLUSPLUS: 'PLUSPLUS', // ++
|
|
86
86
|
MINUSMINUS: 'MINUSMINUS', // --
|
|
87
87
|
QUESTION: 'QUESTION', // ?
|
|
88
|
+
NULLISH: 'NULLISH', // ??
|
|
89
|
+
OPTIONAL_CHAIN: 'OPTIONAL_CHAIN', // ?.
|
|
88
90
|
ARROW: 'ARROW', // =>
|
|
89
91
|
SPREAD: 'SPREAD', // ...
|
|
92
|
+
// Logical/Nullish Assignment Operators (ES2021)
|
|
93
|
+
OR_ASSIGN: 'OR_ASSIGN', // ||=
|
|
94
|
+
AND_ASSIGN: 'AND_ASSIGN', // &&=
|
|
95
|
+
NULLISH_ASSIGN: 'NULLISH_ASSIGN', // ??=
|
|
96
|
+
PLUS_ASSIGN: 'PLUS_ASSIGN', // +=
|
|
97
|
+
MINUS_ASSIGN: 'MINUS_ASSIGN', // -=
|
|
98
|
+
STAR_ASSIGN: 'STAR_ASSIGN', // *=
|
|
99
|
+
SLASH_ASSIGN: 'SLASH_ASSIGN', // /=
|
|
90
100
|
|
|
91
101
|
// Literals
|
|
92
102
|
STRING: 'STRING',
|
|
93
103
|
TEMPLATE: 'TEMPLATE', // Template literal `...`
|
|
94
104
|
NUMBER: 'NUMBER',
|
|
105
|
+
BIGINT: 'BIGINT', // BigInt literal 123n
|
|
95
106
|
TRUE: 'TRUE',
|
|
96
107
|
FALSE: 'FALSE',
|
|
97
108
|
NULL: 'NULL',
|
|
@@ -365,22 +376,96 @@ export class Lexer {
|
|
|
365
376
|
|
|
366
377
|
/**
|
|
367
378
|
* Read a number literal
|
|
379
|
+
* Supports:
|
|
380
|
+
* - Integers: 42
|
|
381
|
+
* - Decimals: 3.14
|
|
382
|
+
* - Scientific notation: 1e10, 1.5e-3
|
|
383
|
+
* - Numeric separators (ES2021): 1_000_000, 0xFF_FF_FF
|
|
384
|
+
* - BigInt literals (ES2020): 123n
|
|
385
|
+
* - Hex: 0xFF, Binary: 0b101, Octal: 0o777
|
|
368
386
|
*/
|
|
369
387
|
readNumber() {
|
|
370
388
|
const startLine = this.line;
|
|
371
389
|
const startColumn = this.column;
|
|
372
390
|
let value = '';
|
|
391
|
+
let rawValue = '';
|
|
392
|
+
let isBigInt = false;
|
|
393
|
+
|
|
394
|
+
// Check for hex, binary, or octal prefixes
|
|
395
|
+
if (this.current() === '0') {
|
|
396
|
+
rawValue += this.advance();
|
|
397
|
+
value += '0';
|
|
398
|
+
|
|
399
|
+
if (this.current() === 'x' || this.current() === 'X') {
|
|
400
|
+
// Hexadecimal
|
|
401
|
+
rawValue += this.advance();
|
|
402
|
+
value += 'x';
|
|
403
|
+
while (!this.isEOF() && /[0-9a-fA-F_]/.test(this.current())) {
|
|
404
|
+
const char = this.advance();
|
|
405
|
+
rawValue += char;
|
|
406
|
+
if (char !== '_') value += char; // Skip separators in actual value
|
|
407
|
+
}
|
|
408
|
+
// Check for BigInt suffix
|
|
409
|
+
if (this.current() === 'n') {
|
|
410
|
+
rawValue += this.advance();
|
|
411
|
+
isBigInt = true;
|
|
412
|
+
}
|
|
413
|
+
if (isBigInt) {
|
|
414
|
+
return new Token(TokenType.BIGINT, value + 'n', startLine, startColumn, rawValue);
|
|
415
|
+
}
|
|
416
|
+
return new Token(TokenType.NUMBER, parseInt(value, 16), startLine, startColumn, rawValue);
|
|
417
|
+
} else if (this.current() === 'b' || this.current() === 'B') {
|
|
418
|
+
// Binary
|
|
419
|
+
rawValue += this.advance();
|
|
420
|
+
value += 'b';
|
|
421
|
+
while (!this.isEOF() && /[01_]/.test(this.current())) {
|
|
422
|
+
const char = this.advance();
|
|
423
|
+
rawValue += char;
|
|
424
|
+
if (char !== '_') value += char;
|
|
425
|
+
}
|
|
426
|
+
if (this.current() === 'n') {
|
|
427
|
+
rawValue += this.advance();
|
|
428
|
+
isBigInt = true;
|
|
429
|
+
}
|
|
430
|
+
if (isBigInt) {
|
|
431
|
+
return new Token(TokenType.BIGINT, value + 'n', startLine, startColumn, rawValue);
|
|
432
|
+
}
|
|
433
|
+
return new Token(TokenType.NUMBER, parseInt(value.slice(2), 2), startLine, startColumn, rawValue);
|
|
434
|
+
} else if (this.current() === 'o' || this.current() === 'O') {
|
|
435
|
+
// Octal
|
|
436
|
+
rawValue += this.advance();
|
|
437
|
+
value += 'o';
|
|
438
|
+
while (!this.isEOF() && /[0-7_]/.test(this.current())) {
|
|
439
|
+
const char = this.advance();
|
|
440
|
+
rawValue += char;
|
|
441
|
+
if (char !== '_') value += char;
|
|
442
|
+
}
|
|
443
|
+
if (this.current() === 'n') {
|
|
444
|
+
rawValue += this.advance();
|
|
445
|
+
isBigInt = true;
|
|
446
|
+
}
|
|
447
|
+
if (isBigInt) {
|
|
448
|
+
return new Token(TokenType.BIGINT, value + 'n', startLine, startColumn, rawValue);
|
|
449
|
+
}
|
|
450
|
+
return new Token(TokenType.NUMBER, parseInt(value.slice(2), 8), startLine, startColumn, rawValue);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
373
453
|
|
|
374
|
-
//
|
|
375
|
-
while (!this.isEOF() && /[0-
|
|
376
|
-
|
|
454
|
+
// Regular decimal number (or continuation of '0')
|
|
455
|
+
while (!this.isEOF() && /[0-9_]/.test(this.current())) {
|
|
456
|
+
const char = this.advance();
|
|
457
|
+
rawValue += char;
|
|
458
|
+
if (char !== '_') value += char;
|
|
377
459
|
}
|
|
378
460
|
|
|
379
461
|
// Decimal part
|
|
380
462
|
if (this.current() === '.' && /[0-9]/.test(this.peek())) {
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
463
|
+
rawValue += this.advance();
|
|
464
|
+
value += '.';
|
|
465
|
+
while (!this.isEOF() && /[0-9_]/.test(this.current())) {
|
|
466
|
+
const char = this.advance();
|
|
467
|
+
rawValue += char;
|
|
468
|
+
if (char !== '_') value += char;
|
|
384
469
|
}
|
|
385
470
|
}
|
|
386
471
|
|
|
@@ -394,18 +479,32 @@ export class Lexer {
|
|
|
394
479
|
((nextChar === '+' || nextChar === '-') && /[0-9]/.test(nextNextChar));
|
|
395
480
|
|
|
396
481
|
if (isScientific) {
|
|
397
|
-
|
|
482
|
+
rawValue += this.advance();
|
|
483
|
+
value += 'e';
|
|
398
484
|
if (this.current() === '+' || this.current() === '-') {
|
|
399
|
-
|
|
485
|
+
const sign = this.advance();
|
|
486
|
+
rawValue += sign;
|
|
487
|
+
value += sign;
|
|
400
488
|
}
|
|
401
|
-
while (!this.isEOF() && /[0-
|
|
402
|
-
|
|
489
|
+
while (!this.isEOF() && /[0-9_]/.test(this.current())) {
|
|
490
|
+
const char = this.advance();
|
|
491
|
+
rawValue += char;
|
|
492
|
+
if (char !== '_') value += char;
|
|
403
493
|
}
|
|
404
494
|
}
|
|
405
495
|
// If not scientific notation, leave 'e' for the next token (e.g., 'em' unit)
|
|
406
496
|
}
|
|
407
497
|
|
|
408
|
-
|
|
498
|
+
// Check for BigInt suffix 'n'
|
|
499
|
+
if (this.current() === 'n') {
|
|
500
|
+
rawValue += this.advance();
|
|
501
|
+
isBigInt = true;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (isBigInt) {
|
|
505
|
+
return new Token(TokenType.BIGINT, value + 'n', startLine, startColumn, rawValue);
|
|
506
|
+
}
|
|
507
|
+
return new Token(TokenType.NUMBER, parseFloat(value), startLine, startColumn, rawValue);
|
|
409
508
|
}
|
|
410
509
|
|
|
411
510
|
/**
|
|
@@ -586,6 +685,9 @@ export class Lexer {
|
|
|
586
685
|
if (this.current() === '+') {
|
|
587
686
|
this.advance();
|
|
588
687
|
this.tokens.push(new Token(TokenType.PLUSPLUS, '++', startLine, startColumn));
|
|
688
|
+
} else if (this.current() === '=') {
|
|
689
|
+
this.advance();
|
|
690
|
+
this.tokens.push(new Token(TokenType.PLUS_ASSIGN, '+=', startLine, startColumn));
|
|
589
691
|
} else {
|
|
590
692
|
this.tokens.push(new Token(TokenType.PLUS, '+', startLine, startColumn));
|
|
591
693
|
}
|
|
@@ -595,17 +697,30 @@ export class Lexer {
|
|
|
595
697
|
if (this.current() === '-') {
|
|
596
698
|
this.advance();
|
|
597
699
|
this.tokens.push(new Token(TokenType.MINUSMINUS, '--', startLine, startColumn));
|
|
700
|
+
} else if (this.current() === '=') {
|
|
701
|
+
this.advance();
|
|
702
|
+
this.tokens.push(new Token(TokenType.MINUS_ASSIGN, '-=', startLine, startColumn));
|
|
598
703
|
} else {
|
|
599
704
|
this.tokens.push(new Token(TokenType.MINUS, '-', startLine, startColumn));
|
|
600
705
|
}
|
|
601
706
|
continue;
|
|
602
707
|
case '*':
|
|
603
708
|
this.advance();
|
|
604
|
-
this.
|
|
709
|
+
if (this.current() === '=') {
|
|
710
|
+
this.advance();
|
|
711
|
+
this.tokens.push(new Token(TokenType.STAR_ASSIGN, '*=', startLine, startColumn));
|
|
712
|
+
} else {
|
|
713
|
+
this.tokens.push(new Token(TokenType.STAR, '*', startLine, startColumn));
|
|
714
|
+
}
|
|
605
715
|
continue;
|
|
606
716
|
case '/':
|
|
607
717
|
this.advance();
|
|
608
|
-
this.
|
|
718
|
+
if (this.current() === '=') {
|
|
719
|
+
this.advance();
|
|
720
|
+
this.tokens.push(new Token(TokenType.SLASH_ASSIGN, '/=', startLine, startColumn));
|
|
721
|
+
} else {
|
|
722
|
+
this.tokens.push(new Token(TokenType.SLASH, '/', startLine, startColumn));
|
|
723
|
+
}
|
|
609
724
|
continue;
|
|
610
725
|
case '=':
|
|
611
726
|
this.advance();
|
|
@@ -626,7 +741,25 @@ export class Lexer {
|
|
|
626
741
|
continue;
|
|
627
742
|
case '?':
|
|
628
743
|
this.advance();
|
|
629
|
-
this.
|
|
744
|
+
if (this.current() === '?') {
|
|
745
|
+
this.advance();
|
|
746
|
+
if (this.current() === '=') {
|
|
747
|
+
this.advance();
|
|
748
|
+
this.tokens.push(new Token(TokenType.NULLISH_ASSIGN, '??=', startLine, startColumn));
|
|
749
|
+
} else {
|
|
750
|
+
this.tokens.push(new Token(TokenType.NULLISH, '??', startLine, startColumn));
|
|
751
|
+
}
|
|
752
|
+
} else if (this.current() === '.') {
|
|
753
|
+
// Optional chaining ?. but only if not followed by a digit (to avoid ?.5)
|
|
754
|
+
if (!/[0-9]/.test(this.peek())) {
|
|
755
|
+
this.advance();
|
|
756
|
+
this.tokens.push(new Token(TokenType.OPTIONAL_CHAIN, '?.', startLine, startColumn));
|
|
757
|
+
} else {
|
|
758
|
+
this.tokens.push(new Token(TokenType.QUESTION, '?', startLine, startColumn));
|
|
759
|
+
}
|
|
760
|
+
} else {
|
|
761
|
+
this.tokens.push(new Token(TokenType.QUESTION, '?', startLine, startColumn));
|
|
762
|
+
}
|
|
630
763
|
continue;
|
|
631
764
|
case '%':
|
|
632
765
|
this.advance();
|
|
@@ -668,7 +801,12 @@ export class Lexer {
|
|
|
668
801
|
this.advance();
|
|
669
802
|
if (this.current() === '&') {
|
|
670
803
|
this.advance();
|
|
671
|
-
this.
|
|
804
|
+
if (this.current() === '=') {
|
|
805
|
+
this.advance();
|
|
806
|
+
this.tokens.push(new Token(TokenType.AND_ASSIGN, '&&=', startLine, startColumn));
|
|
807
|
+
} else {
|
|
808
|
+
this.tokens.push(new Token(TokenType.AND, '&&', startLine, startColumn));
|
|
809
|
+
}
|
|
672
810
|
} else {
|
|
673
811
|
// Single & is the CSS parent selector
|
|
674
812
|
this.tokens.push(new Token(TokenType.AMPERSAND, '&', startLine, startColumn));
|
|
@@ -678,7 +816,12 @@ export class Lexer {
|
|
|
678
816
|
this.advance();
|
|
679
817
|
if (this.current() === '|') {
|
|
680
818
|
this.advance();
|
|
681
|
-
this.
|
|
819
|
+
if (this.current() === '=') {
|
|
820
|
+
this.advance();
|
|
821
|
+
this.tokens.push(new Token(TokenType.OR_ASSIGN, '||=', startLine, startColumn));
|
|
822
|
+
} else {
|
|
823
|
+
this.tokens.push(new Token(TokenType.OR, '||', startLine, startColumn));
|
|
824
|
+
}
|
|
682
825
|
}
|
|
683
826
|
continue;
|
|
684
827
|
}
|
|
@@ -169,8 +169,13 @@ export function transformExpressionString(transformer, exprStr) {
|
|
|
169
169
|
`${stateVar}.get()`
|
|
170
170
|
);
|
|
171
171
|
}
|
|
172
|
-
|
|
173
|
-
|
|
172
|
+
|
|
173
|
+
// NOTE: Removed aggressive optional chaining regex that was adding ?.
|
|
174
|
+
// after ALL function calls. This caused false positives like:
|
|
175
|
+
// "User.name" -> "User?.name" in string literals.
|
|
176
|
+
// Optional chaining should be explicitly written by developers, not auto-added.
|
|
177
|
+
// The lexer now properly tokenizes ?. as OPTIONAL_CHAIN for explicit usage.
|
|
178
|
+
|
|
174
179
|
return result;
|
|
175
180
|
}
|
|
176
181
|
|
|
@@ -63,6 +63,49 @@ function isKeyframesRule(selector) {
|
|
|
63
63
|
return selector.trim().startsWith('@keyframes');
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Check if selector is @layer (CSS Cascade Layers)
|
|
68
|
+
* @param {string} selector - CSS selector
|
|
69
|
+
* @returns {boolean}
|
|
70
|
+
*/
|
|
71
|
+
function isLayerRule(selector) {
|
|
72
|
+
return selector.trim().startsWith('@layer');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Check if selector is @supports (CSS Feature Queries)
|
|
77
|
+
* @param {string} selector - CSS selector
|
|
78
|
+
* @returns {boolean}
|
|
79
|
+
*/
|
|
80
|
+
function isSupportsRule(selector) {
|
|
81
|
+
return selector.trim().startsWith('@supports');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if selector is @container (CSS Container Queries)
|
|
86
|
+
* @param {string} selector - CSS selector
|
|
87
|
+
* @returns {boolean}
|
|
88
|
+
*/
|
|
89
|
+
function isContainerRule(selector) {
|
|
90
|
+
return selector.trim().startsWith('@container');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check if selector is a conditional group at-rule that can contain nested rules
|
|
95
|
+
* These include @media, @supports, @container, @layer
|
|
96
|
+
* @param {string} selector - CSS selector
|
|
97
|
+
* @returns {boolean}
|
|
98
|
+
*/
|
|
99
|
+
function isConditionalGroupAtRule(selector) {
|
|
100
|
+
const trimmed = selector.trim();
|
|
101
|
+
return trimmed.startsWith('@media') ||
|
|
102
|
+
trimmed.startsWith('@supports') ||
|
|
103
|
+
trimmed.startsWith('@container') ||
|
|
104
|
+
trimmed.startsWith('@layer') ||
|
|
105
|
+
trimmed.startsWith('@scope') ||
|
|
106
|
+
trimmed.startsWith('@document');
|
|
107
|
+
}
|
|
108
|
+
|
|
66
109
|
/**
|
|
67
110
|
* Check if a selector is a keyframe step (from, to, or percentage)
|
|
68
111
|
* @param {string} selector - CSS selector
|
|
@@ -76,7 +119,7 @@ function isKeyframeStep(selector) {
|
|
|
76
119
|
/**
|
|
77
120
|
* Flatten nested CSS rules by combining selectors
|
|
78
121
|
* Handles CSS nesting by prepending parent selector to nested rules
|
|
79
|
-
* Special handling for @-rules (media queries, keyframes, etc.)
|
|
122
|
+
* Special handling for @-rules (media queries, keyframes, supports, container, layer, etc.)
|
|
80
123
|
* @param {Object} transformer - Transformer instance
|
|
81
124
|
* @param {Object} rule - CSS rule from AST
|
|
82
125
|
* @param {string} parentSelector - Parent selector to prepend (empty for top-level)
|
|
@@ -90,27 +133,96 @@ export function flattenStyleRule(transformer, rule, parentSelector, output, atRu
|
|
|
90
133
|
// Check if this is an @-rule
|
|
91
134
|
if (isAtRule(selector)) {
|
|
92
135
|
const isKeyframes = isKeyframesRule(selector);
|
|
136
|
+
const isLayer = isLayerRule(selector);
|
|
137
|
+
const isConditionalGroup = isConditionalGroupAtRule(selector);
|
|
93
138
|
|
|
94
139
|
// @keyframes should be output as a complete block, not flattened
|
|
95
140
|
if (isKeyframes) {
|
|
96
141
|
const lines = [];
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
142
|
+
// Wrap in existing @-rule if present
|
|
143
|
+
if (atRuleWrapper) {
|
|
144
|
+
lines.push(` ${atRuleWrapper} {`);
|
|
145
|
+
lines.push(` ${selector} {`);
|
|
146
|
+
for (const nested of rule.nestedRules) {
|
|
147
|
+
lines.push(` ${nested.selector} {`);
|
|
148
|
+
for (const prop of nested.properties) {
|
|
149
|
+
lines.push(` ${prop.name}: ${prop.value};`);
|
|
150
|
+
}
|
|
151
|
+
lines.push(' }');
|
|
104
152
|
}
|
|
105
153
|
lines.push(' }');
|
|
154
|
+
lines.push(' }');
|
|
155
|
+
} else {
|
|
156
|
+
lines.push(` ${selector} {`);
|
|
157
|
+
for (const nested of rule.nestedRules) {
|
|
158
|
+
lines.push(` ${nested.selector} {`);
|
|
159
|
+
for (const prop of nested.properties) {
|
|
160
|
+
lines.push(` ${prop.name}: ${prop.value};`);
|
|
161
|
+
}
|
|
162
|
+
lines.push(' }');
|
|
163
|
+
}
|
|
164
|
+
lines.push(' }');
|
|
165
|
+
}
|
|
166
|
+
output.push(lines.join('\n'));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// @layer - output with its content, support both named layers and anonymous layer blocks
|
|
171
|
+
if (isLayer) {
|
|
172
|
+
// Check if it's just a layer statement (@layer name;) or a layer block (@layer name { ... })
|
|
173
|
+
if (rule.nestedRules.length === 0 && rule.properties.length === 0) {
|
|
174
|
+
// Layer order statement: @layer base, components, utilities;
|
|
175
|
+
output.push(` ${selector};`);
|
|
176
|
+
return;
|
|
106
177
|
}
|
|
107
178
|
|
|
108
|
-
|
|
179
|
+
// Layer block with content
|
|
180
|
+
const lines = [];
|
|
181
|
+
|
|
182
|
+
if (atRuleWrapper) {
|
|
183
|
+
lines.push(` ${atRuleWrapper} {`);
|
|
184
|
+
lines.push(` ${selector} {`);
|
|
185
|
+
} else {
|
|
186
|
+
lines.push(` ${selector} {`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Process nested rules within the layer
|
|
190
|
+
const nestedOutput = [];
|
|
191
|
+
for (const nested of rule.nestedRules) {
|
|
192
|
+
flattenStyleRule(transformer, nested, '', nestedOutput, '', false);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Add nested output with proper indentation
|
|
196
|
+
const baseIndent = atRuleWrapper ? ' ' : ' ';
|
|
197
|
+
for (const nestedRule of nestedOutput) {
|
|
198
|
+
// Adjust indentation for nested rules
|
|
199
|
+
const reindented = nestedRule.split('\n').map(line => baseIndent + line.trim()).join('\n');
|
|
200
|
+
lines.push(reindented);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (atRuleWrapper) {
|
|
204
|
+
lines.push(' }');
|
|
205
|
+
lines.push(' }');
|
|
206
|
+
} else {
|
|
207
|
+
lines.push(' }');
|
|
208
|
+
}
|
|
109
209
|
output.push(lines.join('\n'));
|
|
110
210
|
return;
|
|
111
211
|
}
|
|
112
212
|
|
|
113
|
-
//
|
|
213
|
+
// Conditional group @-rules (@media, @supports, @container) wrap their nested rules
|
|
214
|
+
// They can be nested inside each other
|
|
215
|
+
if (isConditionalGroup) {
|
|
216
|
+
// Combine with existing wrapper if present
|
|
217
|
+
const combinedWrapper = atRuleWrapper ? `${atRuleWrapper} { ${selector}` : selector;
|
|
218
|
+
|
|
219
|
+
for (const nested of rule.nestedRules) {
|
|
220
|
+
flattenStyleRule(transformer, nested, parentSelector, output, combinedWrapper, false);
|
|
221
|
+
}
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Other @-rules (unknown) - output as-is with nested content
|
|
114
226
|
for (const nested of rule.nestedRules) {
|
|
115
227
|
flattenStyleRule(transformer, nested, '', output, selector, false);
|
|
116
228
|
}
|
|
@@ -172,6 +284,9 @@ export function flattenStyleRule(transformer, rule, parentSelector, output, atRu
|
|
|
172
284
|
* .container -> .container.p123abc
|
|
173
285
|
* div -> div.p123abc
|
|
174
286
|
* .a .b -> .a.p123abc .b.p123abc
|
|
287
|
+
* .a > .b -> .a.p123abc > .b.p123abc (preserves combinators)
|
|
288
|
+
* .a + .b -> .a.p123abc + .b.p123abc
|
|
289
|
+
* .a ~ .b -> .a.p123abc ~ .b.p123abc
|
|
175
290
|
* @media (max-width: 900px) -> @media (max-width: 900px) (unchanged)
|
|
176
291
|
* :root, body, *, html -> unchanged (global selectors)
|
|
177
292
|
* @param {Object} transformer - Transformer instance
|
|
@@ -196,31 +311,81 @@ export function scopeStyleSelector(transformer, selector) {
|
|
|
196
311
|
return selector;
|
|
197
312
|
}
|
|
198
313
|
|
|
314
|
+
// CSS combinators that should be preserved
|
|
315
|
+
const combinators = new Set(['>', '+', '~']);
|
|
316
|
+
|
|
199
317
|
// Split by comma for multiple selectors
|
|
200
318
|
return selector.split(',').map(part => {
|
|
201
319
|
part = part.trim();
|
|
202
320
|
|
|
203
|
-
// Split by
|
|
204
|
-
|
|
321
|
+
// Split by whitespace but preserve combinators
|
|
322
|
+
// This regex splits on whitespace but keeps combinators as separate tokens
|
|
323
|
+
const tokens = part.split(/(\s*[>+~]\s*|\s+)/).filter(t => t.trim());
|
|
324
|
+
const result = [];
|
|
325
|
+
|
|
326
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
327
|
+
const token = tokens[i].trim();
|
|
328
|
+
|
|
329
|
+
// Check if this is a combinator
|
|
330
|
+
if (combinators.has(token)) {
|
|
331
|
+
result.push(` ${token} `);
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Skip empty tokens
|
|
336
|
+
if (!token) continue;
|
|
337
|
+
|
|
205
338
|
// Check if this segment is a global selector
|
|
206
|
-
const segmentBase =
|
|
207
|
-
if (globalSelectors.has(segmentBase) || globalSelectors.has(
|
|
208
|
-
|
|
339
|
+
const segmentBase = token.split(/[.#\[]/)[0];
|
|
340
|
+
if (globalSelectors.has(segmentBase) || globalSelectors.has(token)) {
|
|
341
|
+
result.push(token);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Handle :has(), :is(), :where(), :not() - scope selectors inside
|
|
346
|
+
if (token.includes(':has(') || token.includes(':is(') ||
|
|
347
|
+
token.includes(':where(') || token.includes(':not(')) {
|
|
348
|
+
result.push(scopePseudoClassSelector(transformer, token));
|
|
349
|
+
continue;
|
|
209
350
|
}
|
|
210
351
|
|
|
211
352
|
// Skip pseudo-elements and pseudo-classes at the end
|
|
212
|
-
const pseudoMatch =
|
|
353
|
+
const pseudoMatch = token.match(/^([^:]+)(:.+)?$/);
|
|
213
354
|
if (pseudoMatch) {
|
|
214
355
|
const base = pseudoMatch[1];
|
|
215
356
|
const pseudo = pseudoMatch[2] || '';
|
|
216
357
|
|
|
217
358
|
// Skip if it's just a pseudo selector (like :root)
|
|
218
|
-
if (!base || globalSelectors.has(`:${pseudo.slice(1)}`))
|
|
359
|
+
if (!base || globalSelectors.has(`:${pseudo.slice(1)}`)) {
|
|
360
|
+
result.push(token);
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
219
363
|
|
|
220
364
|
// Add scope class
|
|
221
|
-
|
|
365
|
+
result.push(`${base}.${transformer.scopeId}${pseudo}`);
|
|
366
|
+
continue;
|
|
222
367
|
}
|
|
223
|
-
|
|
224
|
-
}
|
|
368
|
+
result.push(`${token}.${transformer.scopeId}`);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return result.join('');
|
|
225
372
|
}).join(', ');
|
|
226
373
|
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Scope selectors inside functional pseudo-classes like :has(), :is(), :where(), :not()
|
|
377
|
+
* @param {Object} transformer - Transformer instance
|
|
378
|
+
* @param {string} selector - Selector containing functional pseudo-class
|
|
379
|
+
* @returns {string} Scoped selector
|
|
380
|
+
*/
|
|
381
|
+
function scopePseudoClassSelector(transformer, selector) {
|
|
382
|
+
// Match functional pseudo-classes: :has(), :is(), :where(), :not()
|
|
383
|
+
return selector.replace(
|
|
384
|
+
/:(has|is|where|not)\(([^)]+)\)/g,
|
|
385
|
+
(_match, pseudoClass, inner) => {
|
|
386
|
+
// Recursively scope the inner selector
|
|
387
|
+
const scopedInner = scopeStyleSelector(transformer, inner);
|
|
388
|
+
return `:${pseudoClass}(${scopedInner})`;
|
|
389
|
+
}
|
|
390
|
+
);
|
|
391
|
+
}
|
|
@@ -355,8 +355,17 @@ function parseBalancedExpression(str, start) {
|
|
|
355
355
|
* @param {string} selector - CSS selector with potential dynamic attributes
|
|
356
356
|
* @returns {Object} { cleanSelector, dynamicAttrs }
|
|
357
357
|
*/
|
|
358
|
+
/**
|
|
359
|
+
* Extract all attributes (static and dynamic) from a selector string.
|
|
360
|
+
* Static attributes become part of the attrs object passed to el().
|
|
361
|
+
* Dynamic attributes (with {expr} values) are bound via bind().
|
|
362
|
+
*
|
|
363
|
+
* @param {string} selector - Element selector like "div.class[href=url][value={expr}]"
|
|
364
|
+
* @returns {{cleanSelector: string, staticAttrs: Array<{name: string, value: string}>, dynamicAttrs: Array<{name: string, expr: string}>}}
|
|
365
|
+
*/
|
|
358
366
|
function extractDynamicAttributes(selector) {
|
|
359
367
|
const dynamicAttrs = [];
|
|
368
|
+
const staticAttrs = [];
|
|
360
369
|
let cleanSelector = '';
|
|
361
370
|
let i = 0;
|
|
362
371
|
|
|
@@ -367,7 +376,7 @@ function extractDynamicAttributes(selector) {
|
|
|
367
376
|
|
|
368
377
|
// Parse attribute name
|
|
369
378
|
let attrName = '';
|
|
370
|
-
while (i < selector.length && /[a-zA-Z0-9-]/.test(selector[i])) {
|
|
379
|
+
while (i < selector.length && /[a-zA-Z0-9-_]/.test(selector[i])) {
|
|
371
380
|
attrName += selector[i];
|
|
372
381
|
i++;
|
|
373
382
|
}
|
|
@@ -382,9 +391,12 @@ function extractDynamicAttributes(selector) {
|
|
|
382
391
|
// Skip whitespace
|
|
383
392
|
while (i < selector.length && /\s/.test(selector[i])) i++;
|
|
384
393
|
|
|
385
|
-
//
|
|
386
|
-
|
|
387
|
-
if (
|
|
394
|
+
// Determine quote character (or none)
|
|
395
|
+
let quoteChar = null;
|
|
396
|
+
if (selector[i] === '"' || selector[i] === "'") {
|
|
397
|
+
quoteChar = selector[i];
|
|
398
|
+
i++;
|
|
399
|
+
}
|
|
388
400
|
|
|
389
401
|
// Check for dynamic expression {
|
|
390
402
|
if (selector[i] === '{') {
|
|
@@ -394,7 +406,7 @@ function extractDynamicAttributes(selector) {
|
|
|
394
406
|
i = result.end + 1; // Skip past closing }
|
|
395
407
|
|
|
396
408
|
// Skip optional closing quote
|
|
397
|
-
if (
|
|
409
|
+
if (quoteChar && selector[i] === quoteChar) i++;
|
|
398
410
|
|
|
399
411
|
// Skip closing ]
|
|
400
412
|
if (selector[i] === ']') i++;
|
|
@@ -403,16 +415,40 @@ function extractDynamicAttributes(selector) {
|
|
|
403
415
|
continue;
|
|
404
416
|
}
|
|
405
417
|
}
|
|
406
|
-
}
|
|
407
418
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
419
|
+
// Static attribute - parse the value
|
|
420
|
+
let attrValue = '';
|
|
421
|
+
if (quoteChar) {
|
|
422
|
+
// Quoted value - read until closing quote
|
|
423
|
+
while (i < selector.length && selector[i] !== quoteChar) {
|
|
424
|
+
attrValue += selector[i];
|
|
425
|
+
i++;
|
|
426
|
+
}
|
|
427
|
+
// Skip closing quote
|
|
428
|
+
if (selector[i] === quoteChar) i++;
|
|
429
|
+
} else {
|
|
430
|
+
// Unquoted value - read until ]
|
|
431
|
+
while (i < selector.length && selector[i] !== ']') {
|
|
432
|
+
attrValue += selector[i];
|
|
433
|
+
i++;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Skip closing ]
|
|
438
|
+
if (selector[i] === ']') i++;
|
|
439
|
+
|
|
440
|
+
// Add to static attrs (don't put in selector)
|
|
441
|
+
staticAttrs.push({ name: attrName, value: attrValue });
|
|
442
|
+
continue;
|
|
443
|
+
} else {
|
|
444
|
+
// Boolean attribute (no value) like [disabled]
|
|
445
|
+
// Skip to closing ]
|
|
446
|
+
while (i < selector.length && selector[i] !== ']') i++;
|
|
447
|
+
if (selector[i] === ']') i++;
|
|
448
|
+
|
|
449
|
+
// Add as boolean attribute
|
|
450
|
+
staticAttrs.push({ name: attrName, value: '' });
|
|
451
|
+
continue;
|
|
416
452
|
}
|
|
417
453
|
} else {
|
|
418
454
|
cleanSelector += selector[i];
|
|
@@ -420,7 +456,7 @@ function extractDynamicAttributes(selector) {
|
|
|
420
456
|
}
|
|
421
457
|
}
|
|
422
458
|
|
|
423
|
-
return { cleanSelector, dynamicAttrs };
|
|
459
|
+
return { cleanSelector, staticAttrs, dynamicAttrs };
|
|
424
460
|
}
|
|
425
461
|
|
|
426
462
|
/**
|
|
@@ -445,8 +481,8 @@ export function transformElement(transformer, node, indent) {
|
|
|
445
481
|
return transformComponentCall(transformer, node, indent);
|
|
446
482
|
}
|
|
447
483
|
|
|
448
|
-
// Extract
|
|
449
|
-
let { cleanSelector, dynamicAttrs } = extractDynamicAttributes(node.selector);
|
|
484
|
+
// Extract all attributes from selector (static and dynamic)
|
|
485
|
+
let { cleanSelector, staticAttrs, dynamicAttrs } = extractDynamicAttributes(node.selector);
|
|
450
486
|
|
|
451
487
|
// Add scoped class to selector if CSS scoping is enabled
|
|
452
488
|
let selector = cleanSelector;
|
|
@@ -483,51 +519,46 @@ export function transformElement(transformer, node, indent) {
|
|
|
483
519
|
transformer.usesA11y.trapFocus = true;
|
|
484
520
|
}
|
|
485
521
|
|
|
486
|
-
//
|
|
487
|
-
const
|
|
522
|
+
// Collect all static attributes for the el() attrs object
|
|
523
|
+
const allStaticAttrs = [];
|
|
488
524
|
|
|
489
|
-
//
|
|
525
|
+
// Add attributes extracted from selector
|
|
526
|
+
for (const attr of staticAttrs) {
|
|
527
|
+
// Escape single quotes in values
|
|
528
|
+
const escapedValue = attr.value.replace(/'/g, "\\'");
|
|
529
|
+
if (attr.value === '') {
|
|
530
|
+
// Boolean attribute
|
|
531
|
+
allStaticAttrs.push(`'${attr.name}': true`);
|
|
532
|
+
} else {
|
|
533
|
+
allStaticAttrs.push(`'${attr.name}': '${escapedValue}'`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Process @a11y directives - add to static attrs
|
|
490
538
|
for (const directive of a11yDirectives) {
|
|
491
539
|
const attrs = buildA11yAttributes(transformer, directive);
|
|
492
540
|
for (const [key, value] of Object.entries(attrs)) {
|
|
493
541
|
const valueCode = typeof value === 'string' ? `'${value}'` : value;
|
|
494
|
-
|
|
542
|
+
allStaticAttrs.push(`'${key}': ${valueCode}`);
|
|
495
543
|
}
|
|
496
544
|
}
|
|
497
545
|
|
|
498
546
|
// Process @live directives (add aria-live and aria-atomic)
|
|
499
547
|
for (const directive of liveDirectives) {
|
|
500
548
|
const priority = directive.priority || 'polite';
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// Build selector with inline ARIA attributes
|
|
506
|
-
let enhancedSelector = selector;
|
|
507
|
-
if (ariaAttrs.length > 0) {
|
|
508
|
-
// Convert ARIA attrs to selector attribute syntax where possible
|
|
509
|
-
// For dynamic values, we'll need to use setAriaAttributes
|
|
510
|
-
const staticAttrs = [];
|
|
511
|
-
const dynamicAttrs = [];
|
|
512
|
-
|
|
513
|
-
for (const attr of ariaAttrs) {
|
|
514
|
-
const match = attr.match(/^'([^']+)':\s*'([^']+)'$/);
|
|
515
|
-
if (match) {
|
|
516
|
-
// Static attribute - can embed in selector
|
|
517
|
-
staticAttrs.push(`[${match[1]}=${match[2]}]`);
|
|
518
|
-
} else {
|
|
519
|
-
dynamicAttrs.push(attr);
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// Add static ARIA attributes to selector
|
|
524
|
-
enhancedSelector = selector + staticAttrs.join('');
|
|
549
|
+
allStaticAttrs.push(`'aria-live': '${priority}'`);
|
|
550
|
+
allStaticAttrs.push(`'aria-atomic': 'true'`);
|
|
525
551
|
}
|
|
526
552
|
|
|
527
553
|
// Start with el() call - escape single quotes in selector
|
|
528
|
-
const escapedSelector =
|
|
554
|
+
const escapedSelector = selector.replace(/'/g, "\\'");
|
|
529
555
|
parts.push(`${pad}el('${escapedSelector}'`);
|
|
530
556
|
|
|
557
|
+
// Add attributes object if we have any static attributes
|
|
558
|
+
if (allStaticAttrs.length > 0) {
|
|
559
|
+
parts.push(`, { ${allStaticAttrs.join(', ')} }`);
|
|
560
|
+
}
|
|
561
|
+
|
|
531
562
|
// Add text content
|
|
532
563
|
if (node.textContent.length > 0) {
|
|
533
564
|
for (const text of node.textContent) {
|