pulse-js-framework 1.7.24 → 1.7.26
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/transformer/expressions.js +10 -1
- package/compiler/transformer/view.js +188 -49
- 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
|
|
@@ -25,10 +25,14 @@ export function transformExpression(transformer, node) {
|
|
|
25
25
|
|
|
26
26
|
switch (node.type) {
|
|
27
27
|
case NodeType.Identifier:
|
|
28
|
+
// Props take precedence over state (props are destructured in render scope)
|
|
29
|
+
if (transformer.propVars.has(node.name)) {
|
|
30
|
+
return node.name;
|
|
31
|
+
}
|
|
28
32
|
if (transformer.stateVars.has(node.name)) {
|
|
29
33
|
return `${node.name}.get()`;
|
|
30
34
|
}
|
|
31
|
-
//
|
|
35
|
+
// Other identifiers (actions, imports, etc.) accessed directly
|
|
32
36
|
return node.name;
|
|
33
37
|
|
|
34
38
|
case NodeType.Literal:
|
|
@@ -153,8 +157,13 @@ export function transformExpression(transformer, node) {
|
|
|
153
157
|
*/
|
|
154
158
|
export function transformExpressionString(transformer, exprStr) {
|
|
155
159
|
// Simple transformation: wrap state vars with .get()
|
|
160
|
+
// Props take precedence - don't wrap props with .get()
|
|
156
161
|
let result = exprStr;
|
|
157
162
|
for (const stateVar of transformer.stateVars) {
|
|
163
|
+
// Skip if this var name is also a prop (props shadow state in render scope)
|
|
164
|
+
if (transformer.propVars.has(stateVar)) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
158
167
|
result = result.replace(
|
|
159
168
|
new RegExp(`\\b${stateVar}\\b`, 'g'),
|
|
160
169
|
`${stateVar}.get()`
|
|
@@ -298,34 +298,165 @@ export function transformFocusTrapDirective(transformer, node, indent) {
|
|
|
298
298
|
return `{ ${optionsCode} }`;
|
|
299
299
|
}
|
|
300
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Parse a balanced expression starting from an opening brace
|
|
303
|
+
* Handles nested braces and string literals correctly
|
|
304
|
+
* @param {string} str - The string to parse
|
|
305
|
+
* @param {number} start - Index of the opening brace
|
|
306
|
+
* @returns {Object} { expr: string, end: number } or null if invalid
|
|
307
|
+
*/
|
|
308
|
+
function parseBalancedExpression(str, start) {
|
|
309
|
+
if (str[start] !== '{') return null;
|
|
310
|
+
|
|
311
|
+
let depth = 0;
|
|
312
|
+
let inString = false;
|
|
313
|
+
let stringChar = '';
|
|
314
|
+
let i = start;
|
|
315
|
+
|
|
316
|
+
while (i < str.length) {
|
|
317
|
+
const char = str[i];
|
|
318
|
+
const prevChar = i > 0 ? str[i - 1] : '';
|
|
319
|
+
|
|
320
|
+
// Handle string literals
|
|
321
|
+
if (!inString && (char === '"' || char === "'" || char === '`')) {
|
|
322
|
+
inString = true;
|
|
323
|
+
stringChar = char;
|
|
324
|
+
} else if (inString && char === stringChar && prevChar !== '\\') {
|
|
325
|
+
inString = false;
|
|
326
|
+
stringChar = '';
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Count braces only outside strings
|
|
330
|
+
if (!inString) {
|
|
331
|
+
if (char === '{') {
|
|
332
|
+
depth++;
|
|
333
|
+
} else if (char === '}') {
|
|
334
|
+
depth--;
|
|
335
|
+
if (depth === 0) {
|
|
336
|
+
// Found the matching closing brace
|
|
337
|
+
return {
|
|
338
|
+
expr: str.slice(start + 1, i),
|
|
339
|
+
end: i
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
i++;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return null; // Unbalanced braces
|
|
349
|
+
}
|
|
350
|
+
|
|
301
351
|
/**
|
|
302
352
|
* Extract dynamic attributes from a selector
|
|
303
353
|
* Returns { cleanSelector, dynamicAttrs } where dynamicAttrs is an array of { name, expr }
|
|
354
|
+
* Handles complex expressions including ternaries, nested braces, and string literals
|
|
304
355
|
* @param {string} selector - CSS selector with potential dynamic attributes
|
|
305
356
|
* @returns {Object} { cleanSelector, dynamicAttrs }
|
|
306
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
|
+
*/
|
|
307
366
|
function extractDynamicAttributes(selector) {
|
|
308
367
|
const dynamicAttrs = [];
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
368
|
+
const staticAttrs = [];
|
|
369
|
+
let cleanSelector = '';
|
|
370
|
+
let i = 0;
|
|
371
|
+
|
|
372
|
+
while (i < selector.length) {
|
|
373
|
+
// Look for attribute start: [
|
|
374
|
+
if (selector[i] === '[') {
|
|
375
|
+
i++; // Skip [
|
|
376
|
+
|
|
377
|
+
// Parse attribute name
|
|
378
|
+
let attrName = '';
|
|
379
|
+
while (i < selector.length && /[a-zA-Z0-9-_]/.test(selector[i])) {
|
|
380
|
+
attrName += selector[i];
|
|
381
|
+
i++;
|
|
382
|
+
}
|
|
321
383
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
384
|
+
// Skip whitespace
|
|
385
|
+
while (i < selector.length && /\s/.test(selector[i])) i++;
|
|
386
|
+
|
|
387
|
+
// Check for =
|
|
388
|
+
if (i < selector.length && selector[i] === '=') {
|
|
389
|
+
i++; // Skip =
|
|
390
|
+
|
|
391
|
+
// Skip whitespace
|
|
392
|
+
while (i < selector.length && /\s/.test(selector[i])) i++;
|
|
393
|
+
|
|
394
|
+
// Determine quote character (or none)
|
|
395
|
+
let quoteChar = null;
|
|
396
|
+
if (selector[i] === '"' || selector[i] === "'") {
|
|
397
|
+
quoteChar = selector[i];
|
|
398
|
+
i++;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Check for dynamic expression {
|
|
402
|
+
if (selector[i] === '{') {
|
|
403
|
+
const result = parseBalancedExpression(selector, i);
|
|
404
|
+
if (result) {
|
|
405
|
+
dynamicAttrs.push({ name: attrName, expr: result.expr });
|
|
406
|
+
i = result.end + 1; // Skip past closing }
|
|
407
|
+
|
|
408
|
+
// Skip optional closing quote
|
|
409
|
+
if (quoteChar && selector[i] === quoteChar) i++;
|
|
410
|
+
|
|
411
|
+
// Skip closing ]
|
|
412
|
+
if (selector[i] === ']') i++;
|
|
413
|
+
|
|
414
|
+
// Don't add this attribute to cleanSelector
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
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;
|
|
452
|
+
}
|
|
453
|
+
} else {
|
|
454
|
+
cleanSelector += selector[i];
|
|
455
|
+
i++;
|
|
456
|
+
}
|
|
325
457
|
}
|
|
326
|
-
cleanSelector = cleanSelector.replace(attrPatternQuoted, '');
|
|
327
458
|
|
|
328
|
-
return { cleanSelector, dynamicAttrs };
|
|
459
|
+
return { cleanSelector, staticAttrs, dynamicAttrs };
|
|
329
460
|
}
|
|
330
461
|
|
|
331
462
|
/**
|
|
@@ -350,8 +481,8 @@ export function transformElement(transformer, node, indent) {
|
|
|
350
481
|
return transformComponentCall(transformer, node, indent);
|
|
351
482
|
}
|
|
352
483
|
|
|
353
|
-
// Extract
|
|
354
|
-
let { cleanSelector, dynamicAttrs } = extractDynamicAttributes(node.selector);
|
|
484
|
+
// Extract all attributes from selector (static and dynamic)
|
|
485
|
+
let { cleanSelector, staticAttrs, dynamicAttrs } = extractDynamicAttributes(node.selector);
|
|
355
486
|
|
|
356
487
|
// Add scoped class to selector if CSS scoping is enabled
|
|
357
488
|
let selector = cleanSelector;
|
|
@@ -388,51 +519,46 @@ export function transformElement(transformer, node, indent) {
|
|
|
388
519
|
transformer.usesA11y.trapFocus = true;
|
|
389
520
|
}
|
|
390
521
|
|
|
391
|
-
//
|
|
392
|
-
const
|
|
522
|
+
// Collect all static attributes for the el() attrs object
|
|
523
|
+
const allStaticAttrs = [];
|
|
393
524
|
|
|
394
|
-
//
|
|
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
|
|
395
538
|
for (const directive of a11yDirectives) {
|
|
396
539
|
const attrs = buildA11yAttributes(transformer, directive);
|
|
397
540
|
for (const [key, value] of Object.entries(attrs)) {
|
|
398
541
|
const valueCode = typeof value === 'string' ? `'${value}'` : value;
|
|
399
|
-
|
|
542
|
+
allStaticAttrs.push(`'${key}': ${valueCode}`);
|
|
400
543
|
}
|
|
401
544
|
}
|
|
402
545
|
|
|
403
546
|
// Process @live directives (add aria-live and aria-atomic)
|
|
404
547
|
for (const directive of liveDirectives) {
|
|
405
548
|
const priority = directive.priority || 'polite';
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// Build selector with inline ARIA attributes
|
|
411
|
-
let enhancedSelector = selector;
|
|
412
|
-
if (ariaAttrs.length > 0) {
|
|
413
|
-
// Convert ARIA attrs to selector attribute syntax where possible
|
|
414
|
-
// For dynamic values, we'll need to use setAriaAttributes
|
|
415
|
-
const staticAttrs = [];
|
|
416
|
-
const dynamicAttrs = [];
|
|
417
|
-
|
|
418
|
-
for (const attr of ariaAttrs) {
|
|
419
|
-
const match = attr.match(/^'([^']+)':\s*'([^']+)'$/);
|
|
420
|
-
if (match) {
|
|
421
|
-
// Static attribute - can embed in selector
|
|
422
|
-
staticAttrs.push(`[${match[1]}=${match[2]}]`);
|
|
423
|
-
} else {
|
|
424
|
-
dynamicAttrs.push(attr);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// Add static ARIA attributes to selector
|
|
429
|
-
enhancedSelector = selector + staticAttrs.join('');
|
|
549
|
+
allStaticAttrs.push(`'aria-live': '${priority}'`);
|
|
550
|
+
allStaticAttrs.push(`'aria-atomic': 'true'`);
|
|
430
551
|
}
|
|
431
552
|
|
|
432
553
|
// Start with el() call - escape single quotes in selector
|
|
433
|
-
const escapedSelector =
|
|
554
|
+
const escapedSelector = selector.replace(/'/g, "\\'");
|
|
434
555
|
parts.push(`${pad}el('${escapedSelector}'`);
|
|
435
556
|
|
|
557
|
+
// Add attributes object if we have any static attributes
|
|
558
|
+
if (allStaticAttrs.length > 0) {
|
|
559
|
+
parts.push(`, { ${allStaticAttrs.join(', ')} }`);
|
|
560
|
+
}
|
|
561
|
+
|
|
436
562
|
// Add text content
|
|
437
563
|
if (node.textContent.length > 0) {
|
|
438
564
|
for (const text of node.textContent) {
|
|
@@ -577,6 +703,18 @@ export function transformComponentCall(transformer, node, indent) {
|
|
|
577
703
|
* @param {number} indent - Indentation level
|
|
578
704
|
* @returns {string} JavaScript code
|
|
579
705
|
*/
|
|
706
|
+
/**
|
|
707
|
+
* Escape a string for use in a template literal
|
|
708
|
+
* @param {string} str - String to escape
|
|
709
|
+
* @returns {string} Escaped string
|
|
710
|
+
*/
|
|
711
|
+
function escapeTemplateString(str) {
|
|
712
|
+
return str
|
|
713
|
+
.replace(/\\/g, '\\\\') // Escape backslashes first
|
|
714
|
+
.replace(/`/g, '\\`') // Escape backticks
|
|
715
|
+
.replace(/\$/g, '\\$'); // Escape dollar signs to prevent ${} interpretation
|
|
716
|
+
}
|
|
717
|
+
|
|
580
718
|
export function transformTextNode(transformer, node, indent) {
|
|
581
719
|
const pad = ' '.repeat(indent);
|
|
582
720
|
const parts = node.parts;
|
|
@@ -589,7 +727,8 @@ export function transformTextNode(transformer, node, indent) {
|
|
|
589
727
|
// Has interpolations - use text() with a function
|
|
590
728
|
const textParts = parts.map(part => {
|
|
591
729
|
if (typeof part === 'string') {
|
|
592
|
-
|
|
730
|
+
// Escape for template literal (not JSON.stringify which adds quotes)
|
|
731
|
+
return escapeTemplateString(part);
|
|
593
732
|
}
|
|
594
733
|
// Interpolation
|
|
595
734
|
const expr = transformExpressionString(transformer, part.expression);
|