pulse-js-framework 1.7.9 → 1.7.11
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 +1 -0
- package/cli/lint.js +442 -3
- package/compiler/lexer.js +29 -1
- package/compiler/parser.js +262 -18
- package/compiler/transformer/export.js +41 -2
- package/compiler/transformer/expressions.js +148 -5
- package/compiler/transformer/imports.js +16 -0
- package/compiler/transformer/index.js +46 -0
- package/compiler/transformer/view.js +397 -27
- package/loader/vite-plugin.js +27 -4
- package/package.json +9 -2
- package/runtime/a11y.js +1005 -0
- package/runtime/devtools/a11y-audit.js +442 -0
- package/runtime/devtools/diagnostics.js +403 -0
- package/runtime/devtools/index.js +53 -0
- package/runtime/devtools/time-travel.js +189 -0
- package/runtime/devtools.js +138 -497
- package/runtime/dom-binding.js +39 -4
- package/runtime/dom-element.js +192 -1
- package/runtime/dom.js +8 -2
- package/runtime/index.js +2 -0
- package/runtime/native.js +2 -2
- package/runtime/security.js +461 -0
- package/runtime/utils.js +37 -16
- package/types/a11y.d.ts +336 -0
|
@@ -15,10 +15,15 @@ export const VIEW_NODE_HANDLERS = {
|
|
|
15
15
|
[NodeType.IfDirective]: 'transformIfDirective',
|
|
16
16
|
[NodeType.EachDirective]: 'transformEachDirective',
|
|
17
17
|
[NodeType.EventDirective]: 'transformEventDirective',
|
|
18
|
+
[NodeType.ModelDirective]: 'transformModelDirective',
|
|
18
19
|
[NodeType.SlotElement]: 'transformSlot',
|
|
19
20
|
[NodeType.LinkDirective]: 'transformLinkDirective',
|
|
20
21
|
[NodeType.OutletDirective]: 'transformOutletDirective',
|
|
21
|
-
[NodeType.NavigateDirective]: 'transformNavigateDirective'
|
|
22
|
+
[NodeType.NavigateDirective]: 'transformNavigateDirective',
|
|
23
|
+
// Accessibility directives
|
|
24
|
+
[NodeType.A11yDirective]: 'transformA11yDirective',
|
|
25
|
+
[NodeType.LiveDirective]: 'transformLiveDirective',
|
|
26
|
+
[NodeType.FocusTrapDirective]: 'transformFocusTrapDirective'
|
|
22
27
|
};
|
|
23
28
|
|
|
24
29
|
/**
|
|
@@ -85,6 +90,8 @@ export function transformViewNode(transformer, node, indent = 0) {
|
|
|
85
90
|
return transformEachDirective(transformer, node, indent);
|
|
86
91
|
case NodeType.EventDirective:
|
|
87
92
|
return transformEventDirective(transformer, node, indent);
|
|
93
|
+
case NodeType.ModelDirective:
|
|
94
|
+
return transformModelDirective(transformer, node, indent);
|
|
88
95
|
case NodeType.SlotElement:
|
|
89
96
|
return transformSlot(transformer, node, indent);
|
|
90
97
|
case NodeType.LinkDirective:
|
|
@@ -93,6 +100,12 @@ export function transformViewNode(transformer, node, indent = 0) {
|
|
|
93
100
|
return transformOutletDirective(transformer, node, indent);
|
|
94
101
|
case NodeType.NavigateDirective:
|
|
95
102
|
return transformNavigateDirective(transformer, node, indent);
|
|
103
|
+
case NodeType.A11yDirective:
|
|
104
|
+
return transformA11yDirective(transformer, node, indent);
|
|
105
|
+
case NodeType.LiveDirective:
|
|
106
|
+
return transformLiveDirective(transformer, node, indent);
|
|
107
|
+
case NodeType.FocusTrapDirective:
|
|
108
|
+
return transformFocusTrapDirective(transformer, node, indent);
|
|
96
109
|
default:
|
|
97
110
|
return `${' '.repeat(indent)}/* unknown node: ${node.type} */`;
|
|
98
111
|
}
|
|
@@ -192,6 +205,129 @@ export function transformNavigateDirective(transformer, node, indent) {
|
|
|
192
205
|
return `${pad}router.navigate(${path}${options})`;
|
|
193
206
|
}
|
|
194
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Transform @a11y directive - sets ARIA attributes
|
|
210
|
+
* @param {Object} transformer - Transformer instance
|
|
211
|
+
* @param {Object} node - A11y directive node
|
|
212
|
+
* @param {number} indent - Indentation level
|
|
213
|
+
* @returns {string} JavaScript code
|
|
214
|
+
*/
|
|
215
|
+
export function transformA11yDirective(transformer, node, indent) {
|
|
216
|
+
const pad = ' '.repeat(indent);
|
|
217
|
+
const attrs = node.attrs || {};
|
|
218
|
+
|
|
219
|
+
// Handle @srOnly - create visually hidden element
|
|
220
|
+
if (attrs.srOnly) {
|
|
221
|
+
return `${pad}srOnly(/* content */)`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Build ARIA attributes object
|
|
225
|
+
const ariaAttrs = Object.entries(attrs).map(([key, value]) => {
|
|
226
|
+
// Map short names to aria- attributes (role is not prefixed)
|
|
227
|
+
const ariaKey = key === 'role' ? key : (key.startsWith('aria-') ? key : `aria-${key}`);
|
|
228
|
+
const valueCode = typeof value === 'string' ? `'${value}'` : transformExpression(transformer, value);
|
|
229
|
+
return `'${ariaKey}': ${valueCode}`;
|
|
230
|
+
}).join(', ');
|
|
231
|
+
|
|
232
|
+
return `{ ${ariaAttrs} }`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Build ARIA attributes object from a11y directive
|
|
237
|
+
* @param {Object} transformer - Transformer instance
|
|
238
|
+
* @param {Object} directive - A11y directive node
|
|
239
|
+
* @returns {Object} Object with key-value pairs for ARIA attributes
|
|
240
|
+
*/
|
|
241
|
+
export function buildA11yAttributes(transformer, directive) {
|
|
242
|
+
const attrs = directive.attrs || {};
|
|
243
|
+
const result = {};
|
|
244
|
+
|
|
245
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
246
|
+
if (key === 'srOnly') continue;
|
|
247
|
+
// Map short names to aria- attributes (role is not prefixed)
|
|
248
|
+
const ariaKey = key === 'role' ? key : (key.startsWith('aria-') ? key : `aria-${key}`);
|
|
249
|
+
|
|
250
|
+
// Handle different value types
|
|
251
|
+
if (typeof value === 'string') {
|
|
252
|
+
result[ariaKey] = value;
|
|
253
|
+
} else if (typeof value === 'boolean') {
|
|
254
|
+
result[ariaKey] = String(value); // Convert true/false to "true"/"false"
|
|
255
|
+
} else if (typeof value === 'number') {
|
|
256
|
+
result[ariaKey] = String(value);
|
|
257
|
+
} else {
|
|
258
|
+
result[ariaKey] = transformExpression(transformer, value);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Transform @live directive - creates live region
|
|
267
|
+
* @param {Object} transformer - Transformer instance
|
|
268
|
+
* @param {Object} node - Live directive node
|
|
269
|
+
* @param {number} indent - Indentation level
|
|
270
|
+
* @returns {string} JavaScript code
|
|
271
|
+
*/
|
|
272
|
+
export function transformLiveDirective(transformer, node, indent) {
|
|
273
|
+
const priority = node.priority || 'polite';
|
|
274
|
+
return `'${priority}'`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Transform @focusTrap directive
|
|
279
|
+
* @param {Object} transformer - Transformer instance
|
|
280
|
+
* @param {Object} node - Focus trap directive node
|
|
281
|
+
* @param {number} indent - Indentation level
|
|
282
|
+
* @returns {string} JavaScript code
|
|
283
|
+
*/
|
|
284
|
+
export function transformFocusTrapDirective(transformer, node, indent) {
|
|
285
|
+
const options = node.options || {};
|
|
286
|
+
|
|
287
|
+
if (Object.keys(options).length === 0) {
|
|
288
|
+
return '{}';
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const optionsCode = Object.entries(options).map(([key, value]) => {
|
|
292
|
+
const valueCode = typeof value === 'boolean' ? String(value) :
|
|
293
|
+
typeof value === 'string' ? `'${value}'` :
|
|
294
|
+
transformExpression(transformer, value);
|
|
295
|
+
return `${key}: ${valueCode}`;
|
|
296
|
+
}).join(', ');
|
|
297
|
+
|
|
298
|
+
return `{ ${optionsCode} }`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Extract dynamic attributes from a selector
|
|
303
|
+
* Returns { cleanSelector, dynamicAttrs } where dynamicAttrs is an array of { name, expr }
|
|
304
|
+
* @param {string} selector - CSS selector with potential dynamic attributes
|
|
305
|
+
* @returns {Object} { cleanSelector, dynamicAttrs }
|
|
306
|
+
*/
|
|
307
|
+
function extractDynamicAttributes(selector) {
|
|
308
|
+
const dynamicAttrs = [];
|
|
309
|
+
// Match attributes with {expression} values: [name={expr}] or [name="{expr}"]
|
|
310
|
+
const attrPattern = /\[([a-zA-Z][a-zA-Z0-9-]*)\s*=\s*\{([^}]+)\}\]/g;
|
|
311
|
+
const attrPatternQuoted = /\[([a-zA-Z][a-zA-Z0-9-]*)\s*=\s*"\{([^}]+)\}"\]/g;
|
|
312
|
+
|
|
313
|
+
let cleanSelector = selector;
|
|
314
|
+
|
|
315
|
+
// Extract unquoted dynamic attributes: [value={expr}]
|
|
316
|
+
let match;
|
|
317
|
+
while ((match = attrPattern.exec(selector)) !== null) {
|
|
318
|
+
dynamicAttrs.push({ name: match[1], expr: match[2] });
|
|
319
|
+
}
|
|
320
|
+
cleanSelector = cleanSelector.replace(attrPattern, '');
|
|
321
|
+
|
|
322
|
+
// Extract quoted dynamic attributes: [value="{expr}"]
|
|
323
|
+
while ((match = attrPatternQuoted.exec(selector)) !== null) {
|
|
324
|
+
dynamicAttrs.push({ name: match[1], expr: match[2] });
|
|
325
|
+
}
|
|
326
|
+
cleanSelector = cleanSelector.replace(attrPatternQuoted, '');
|
|
327
|
+
|
|
328
|
+
return { cleanSelector, dynamicAttrs };
|
|
329
|
+
}
|
|
330
|
+
|
|
195
331
|
/**
|
|
196
332
|
* Transform element
|
|
197
333
|
* @param {Object} transformer - Transformer instance
|
|
@@ -214,17 +350,88 @@ export function transformElement(transformer, node, indent) {
|
|
|
214
350
|
return transformComponentCall(transformer, node, indent);
|
|
215
351
|
}
|
|
216
352
|
|
|
353
|
+
// Extract dynamic attributes from selector (e.g., [value={searchQuery}])
|
|
354
|
+
let { cleanSelector, dynamicAttrs } = extractDynamicAttributes(node.selector);
|
|
355
|
+
|
|
217
356
|
// Add scoped class to selector if CSS scoping is enabled
|
|
218
|
-
let selector =
|
|
357
|
+
let selector = cleanSelector;
|
|
219
358
|
if (transformer.scopeId && selector) {
|
|
220
359
|
selector = addScopeToSelector(transformer, selector);
|
|
221
360
|
}
|
|
222
361
|
|
|
223
|
-
//
|
|
224
|
-
parts.push(`${pad}el('${selector}'`);
|
|
225
|
-
|
|
226
|
-
// Add event handlers as on() chain
|
|
362
|
+
// Extract directives by type
|
|
227
363
|
const eventHandlers = node.directives.filter(d => d.type === NodeType.EventDirective);
|
|
364
|
+
const modelDirectives = node.directives.filter(d => d.type === NodeType.ModelDirective);
|
|
365
|
+
const a11yDirectives = node.directives.filter(d => d.type === NodeType.A11yDirective);
|
|
366
|
+
const liveDirectives = node.directives.filter(d => d.type === NodeType.LiveDirective);
|
|
367
|
+
const focusTrapDirectives = node.directives.filter(d => d.type === NodeType.FocusTrapDirective);
|
|
368
|
+
|
|
369
|
+
// Check for @srOnly directive
|
|
370
|
+
const srOnlyDirective = a11yDirectives.find(d => d.attrs && d.attrs.srOnly);
|
|
371
|
+
|
|
372
|
+
// If @srOnly, wrap entire content
|
|
373
|
+
if (srOnlyDirective) {
|
|
374
|
+
transformer.usesA11y.srOnly = true;
|
|
375
|
+
const content = [];
|
|
376
|
+
for (const text of node.textContent) {
|
|
377
|
+
content.push(transformTextNode(transformer, text, 0).trim());
|
|
378
|
+
}
|
|
379
|
+
for (const child of node.children) {
|
|
380
|
+
content.push(transformViewNode(transformer, child, 0).trim());
|
|
381
|
+
}
|
|
382
|
+
const contentCode = content.length === 1 ? content[0] : `[${content.join(', ')}]`;
|
|
383
|
+
return `${pad}srOnly(${contentCode})`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Track focusTrap usage
|
|
387
|
+
if (focusTrapDirectives.length > 0) {
|
|
388
|
+
transformer.usesA11y.trapFocus = true;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Build ARIA attributes from directives
|
|
392
|
+
const ariaAttrs = [];
|
|
393
|
+
|
|
394
|
+
// Process @a11y directives
|
|
395
|
+
for (const directive of a11yDirectives) {
|
|
396
|
+
const attrs = buildA11yAttributes(transformer, directive);
|
|
397
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
398
|
+
const valueCode = typeof value === 'string' ? `'${value}'` : value;
|
|
399
|
+
ariaAttrs.push(`'${key}': ${valueCode}`);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Process @live directives (add aria-live and aria-atomic)
|
|
404
|
+
for (const directive of liveDirectives) {
|
|
405
|
+
const priority = directive.priority || 'polite';
|
|
406
|
+
ariaAttrs.push(`'aria-live': '${priority}'`);
|
|
407
|
+
ariaAttrs.push(`'aria-atomic': 'true'`);
|
|
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('');
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Start with el() call - escape single quotes in selector
|
|
433
|
+
const escapedSelector = enhancedSelector.replace(/'/g, "\\'");
|
|
434
|
+
parts.push(`${pad}el('${escapedSelector}'`);
|
|
228
435
|
|
|
229
436
|
// Add text content
|
|
230
437
|
if (node.textContent.length > 0) {
|
|
@@ -244,11 +451,49 @@ export function transformElement(transformer, node, indent) {
|
|
|
244
451
|
|
|
245
452
|
parts.push(')');
|
|
246
453
|
|
|
247
|
-
// Chain event handlers
|
|
454
|
+
// Chain event handlers with modifiers support
|
|
248
455
|
let result = parts.join('');
|
|
249
456
|
for (const handler of eventHandlers) {
|
|
250
457
|
const handlerCode = transformExpression(transformer, handler.handler);
|
|
251
|
-
|
|
458
|
+
const modifiers = handler.modifiers || [];
|
|
459
|
+
|
|
460
|
+
if (modifiers.length === 0) {
|
|
461
|
+
// Always pass event parameter since handlers commonly use event.target, etc.
|
|
462
|
+
result = `on(${result}, '${handler.event}', (event) => { ${handlerCode}; })`;
|
|
463
|
+
} else {
|
|
464
|
+
const modifiedHandler = generateModifiedHandler(handler.event, handlerCode, modifiers);
|
|
465
|
+
result = `on(${result}, '${handler.event}', ${modifiedHandler})`;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Chain model directives for two-way binding
|
|
470
|
+
for (const directive of modelDirectives) {
|
|
471
|
+
const binding = transformExpression(transformer, directive.binding);
|
|
472
|
+
const modifiers = directive.modifiers || [];
|
|
473
|
+
|
|
474
|
+
// Build options from modifiers
|
|
475
|
+
const options = [];
|
|
476
|
+
if (modifiers.includes('lazy')) options.push('lazy: true');
|
|
477
|
+
if (modifiers.includes('trim')) options.push('trim: true');
|
|
478
|
+
if (modifiers.includes('number')) options.push('number: true');
|
|
479
|
+
|
|
480
|
+
if (options.length > 0) {
|
|
481
|
+
result = `model(${result}, ${binding}, { ${options.join(', ')} })`;
|
|
482
|
+
} else {
|
|
483
|
+
result = `model(${result}, ${binding})`;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Chain focus trap if present
|
|
488
|
+
for (const directive of focusTrapDirectives) {
|
|
489
|
+
const optionsCode = transformFocusTrapDirective(transformer, directive, 0);
|
|
490
|
+
result = `trapFocus(${result}, ${optionsCode})`;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Chain dynamic attribute bindings (e.g., [value={searchQuery}])
|
|
494
|
+
for (const attr of dynamicAttrs) {
|
|
495
|
+
const exprCode = transformExpressionString(transformer, attr.expr);
|
|
496
|
+
result = `bind(${result}, '${attr.name}', () => ${exprCode})`;
|
|
252
497
|
}
|
|
253
498
|
|
|
254
499
|
return result;
|
|
@@ -363,25 +608,59 @@ export function transformTextNode(transformer, node, indent) {
|
|
|
363
608
|
*/
|
|
364
609
|
export function transformIfDirective(transformer, node, indent) {
|
|
365
610
|
const pad = ' '.repeat(indent);
|
|
366
|
-
const condition = transformExpression(transformer, node.condition);
|
|
367
|
-
|
|
368
|
-
const consequent = node.consequent.map(c =>
|
|
369
|
-
transformViewNode(transformer, c, indent + 2)
|
|
370
|
-
).join(',\n');
|
|
371
611
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
612
|
+
// Helper to build nested when() calls for else-if chains
|
|
613
|
+
function buildConditionChain(condition, consequent, elseIfBranches, alternate, depth = 0) {
|
|
614
|
+
const innerPad = ' '.repeat(indent + depth * 2);
|
|
615
|
+
const conditionCode = transformExpression(transformer, condition);
|
|
616
|
+
|
|
617
|
+
// Wrap multiple children in array, single child returns directly
|
|
618
|
+
const consequentItems = consequent.map(c =>
|
|
619
|
+
transformViewNode(transformer, c, indent + depth * 2 + 4)
|
|
620
|
+
);
|
|
621
|
+
const consequentCode = consequentItems.length === 1
|
|
622
|
+
? consequentItems[0]
|
|
623
|
+
: `[\n${consequentItems.join(',\n')}\n${innerPad} ]`;
|
|
624
|
+
|
|
625
|
+
let code = `${innerPad}when(\n`;
|
|
626
|
+
code += `${innerPad} () => ${conditionCode},\n`;
|
|
627
|
+
code += `${innerPad} () => ${consequentCode}`;
|
|
628
|
+
|
|
629
|
+
// Handle else-if branches
|
|
630
|
+
if (elseIfBranches && elseIfBranches.length > 0) {
|
|
631
|
+
const nextBranch = elseIfBranches[0];
|
|
632
|
+
const remainingBranches = elseIfBranches.slice(1);
|
|
633
|
+
|
|
634
|
+
code += `,\n${innerPad} () => (\n`;
|
|
635
|
+
code += buildConditionChain(
|
|
636
|
+
nextBranch.condition,
|
|
637
|
+
nextBranch.consequent,
|
|
638
|
+
remainingBranches,
|
|
639
|
+
alternate,
|
|
640
|
+
depth + 2
|
|
641
|
+
);
|
|
642
|
+
code += `\n${innerPad} )`;
|
|
643
|
+
} else if (alternate) {
|
|
644
|
+
// Final else branch - wrap multiple children in array
|
|
645
|
+
const alternateItems = alternate.map(c =>
|
|
646
|
+
transformViewNode(transformer, c, indent + depth * 2 + 4)
|
|
647
|
+
);
|
|
648
|
+
const alternateCode = alternateItems.length === 1
|
|
649
|
+
? alternateItems[0]
|
|
650
|
+
: `[\n${alternateItems.join(',\n')}\n${innerPad} ]`;
|
|
651
|
+
code += `,\n${innerPad} () => ${alternateCode}`;
|
|
652
|
+
}
|
|
375
653
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
transformViewNode(transformer, c, indent + 2)
|
|
379
|
-
).join(',\n');
|
|
380
|
-
code += `,\n${pad} () => (\n${alternate}\n${pad} )`;
|
|
654
|
+
code += `\n${innerPad})`;
|
|
655
|
+
return code;
|
|
381
656
|
}
|
|
382
657
|
|
|
383
|
-
|
|
384
|
-
|
|
658
|
+
return buildConditionChain(
|
|
659
|
+
node.condition,
|
|
660
|
+
node.consequent,
|
|
661
|
+
node.elseIfBranches || [],
|
|
662
|
+
node.alternate
|
|
663
|
+
);
|
|
385
664
|
}
|
|
386
665
|
|
|
387
666
|
/**
|
|
@@ -399,10 +678,19 @@ export function transformEachDirective(transformer, node, indent) {
|
|
|
399
678
|
transformViewNode(transformer, t, indent + 2)
|
|
400
679
|
).join(',\n');
|
|
401
680
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
681
|
+
// Build list() call with optional key function
|
|
682
|
+
let code = `${pad}list(\n` +
|
|
683
|
+
`${pad} () => ${iterable},\n` +
|
|
684
|
+
`${pad} (${node.itemName}, _index) => (\n${template}\n${pad} )`;
|
|
685
|
+
|
|
686
|
+
// Add key function if provided
|
|
687
|
+
if (node.keyExpr) {
|
|
688
|
+
const keyExprCode = transformExpression(transformer, node.keyExpr);
|
|
689
|
+
code += `,\n${pad} (${node.itemName}) => ${keyExprCode}`;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
code += `\n${pad})`;
|
|
693
|
+
return code;
|
|
406
694
|
}
|
|
407
695
|
|
|
408
696
|
/**
|
|
@@ -426,3 +714,85 @@ export function transformEventDirective(transformer, node, indent) {
|
|
|
426
714
|
|
|
427
715
|
return `/* event: ${node.event} -> ${handler} */`;
|
|
428
716
|
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* Transform @model directive for two-way binding
|
|
720
|
+
* @param {Object} transformer - Transformer instance
|
|
721
|
+
* @param {Object} node - Model directive node
|
|
722
|
+
* @param {number} indent - Indentation level
|
|
723
|
+
* @returns {string} JavaScript code
|
|
724
|
+
*/
|
|
725
|
+
export function transformModelDirective(transformer, node, indent) {
|
|
726
|
+
const pad = ' '.repeat(indent);
|
|
727
|
+
const binding = transformExpression(transformer, node.binding);
|
|
728
|
+
const modifiers = node.modifiers || [];
|
|
729
|
+
|
|
730
|
+
// Build options from modifiers
|
|
731
|
+
const options = [];
|
|
732
|
+
if (modifiers.includes('lazy')) options.push('lazy: true');
|
|
733
|
+
if (modifiers.includes('trim')) options.push('trim: true');
|
|
734
|
+
if (modifiers.includes('number')) options.push('number: true');
|
|
735
|
+
|
|
736
|
+
if (options.length > 0) {
|
|
737
|
+
return `${pad}/* model: ${binding} { ${options.join(', ')} } */`;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return `${pad}/* model: ${binding} */`;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Generate event handler code with modifiers applied
|
|
745
|
+
* @param {string} event - Event name
|
|
746
|
+
* @param {string} handlerCode - Handler expression code
|
|
747
|
+
* @param {string[]} modifiers - Array of modifier names
|
|
748
|
+
* @returns {string} JavaScript handler code
|
|
749
|
+
*/
|
|
750
|
+
function generateModifiedHandler(event, handlerCode, modifiers) {
|
|
751
|
+
// Key modifiers map
|
|
752
|
+
const keyMap = {
|
|
753
|
+
enter: 'Enter', tab: 'Tab', delete: 'Delete', esc: 'Escape', escape: 'Escape',
|
|
754
|
+
space: ' ', up: 'ArrowUp', down: 'ArrowDown', left: 'ArrowLeft', right: 'ArrowRight'
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
// System key modifiers
|
|
758
|
+
const systemModifiers = ['ctrl', 'alt', 'shift', 'meta'];
|
|
759
|
+
|
|
760
|
+
// Build handler code with checks
|
|
761
|
+
const checks = [];
|
|
762
|
+
let hasEventParam = false;
|
|
763
|
+
|
|
764
|
+
for (const mod of modifiers) {
|
|
765
|
+
if (mod === 'prevent') {
|
|
766
|
+
checks.push('event.preventDefault();');
|
|
767
|
+
hasEventParam = true;
|
|
768
|
+
} else if (mod === 'stop') {
|
|
769
|
+
checks.push('event.stopPropagation();');
|
|
770
|
+
hasEventParam = true;
|
|
771
|
+
} else if (mod === 'self') {
|
|
772
|
+
checks.push('if (event.target !== event.currentTarget) return;');
|
|
773
|
+
hasEventParam = true;
|
|
774
|
+
} else if (keyMap[mod]) {
|
|
775
|
+
checks.push(`if (event.key !== '${keyMap[mod]}') return;`);
|
|
776
|
+
hasEventParam = true;
|
|
777
|
+
} else if (systemModifiers.includes(mod)) {
|
|
778
|
+
checks.push(`if (!event.${mod}Key) return;`);
|
|
779
|
+
hasEventParam = true;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Build options for addEventListener
|
|
784
|
+
const options = [];
|
|
785
|
+
if (modifiers.includes('capture')) options.push('capture: true');
|
|
786
|
+
if (modifiers.includes('once')) options.push('once: true');
|
|
787
|
+
if (modifiers.includes('passive')) options.push('passive: true');
|
|
788
|
+
|
|
789
|
+
const checksCode = checks.join(' ');
|
|
790
|
+
// Always pass event parameter since handler code commonly uses event.target, etc.
|
|
791
|
+
const handler = `(event) => { ${checksCode} ${handlerCode}; }`;
|
|
792
|
+
|
|
793
|
+
if (options.length > 0) {
|
|
794
|
+
return `${handler}, { ${options.join(', ')} }`;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return handler;
|
|
798
|
+
}
|
package/loader/vite-plugin.js
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { compile } from '../compiler/index.js';
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
|
+
import { resolve, dirname } from 'path';
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Create Pulse Vite plugin
|
|
@@ -18,14 +20,35 @@ export default function pulsePlugin(options = {}) {
|
|
|
18
20
|
|
|
19
21
|
return {
|
|
20
22
|
name: 'vite-plugin-pulse',
|
|
23
|
+
enforce: 'pre',
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
|
-
* Resolve .pulse files
|
|
26
|
+
* Resolve .pulse files and .js imports that map to .pulse files
|
|
27
|
+
* The compiler transforms .pulse imports to .js, so we need to
|
|
28
|
+
* resolve them back to .pulse for Vite to process them
|
|
24
29
|
*/
|
|
25
|
-
resolveId(id) {
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
resolveId(id, importer) {
|
|
31
|
+
// Direct .pulse imports - resolve to absolute path
|
|
32
|
+
if (id.endsWith('.pulse') && importer) {
|
|
33
|
+
const importerDir = dirname(importer);
|
|
34
|
+
const absolutePath = resolve(importerDir, id);
|
|
35
|
+
if (existsSync(absolutePath)) {
|
|
36
|
+
return absolutePath;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Check if a .js import has a corresponding .pulse file
|
|
41
|
+
// This handles the compiler's transformation of .pulse -> .js imports
|
|
42
|
+
if (id.endsWith('.js') && importer) {
|
|
43
|
+
const pulseId = id.replace(/\.js$/, '.pulse');
|
|
44
|
+
const importerDir = dirname(importer);
|
|
45
|
+
const absolutePulsePath = resolve(importerDir, pulseId);
|
|
46
|
+
|
|
47
|
+
if (existsSync(absolutePulsePath)) {
|
|
48
|
+
return absolutePulsePath;
|
|
49
|
+
}
|
|
28
50
|
}
|
|
51
|
+
|
|
29
52
|
return null;
|
|
30
53
|
},
|
|
31
54
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pulse-js-framework",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.11",
|
|
4
4
|
"description": "A declarative DOM framework with CSS selector-based structure and reactive pulsations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -68,6 +68,10 @@
|
|
|
68
68
|
"types": "./types/http.d.ts",
|
|
69
69
|
"default": "./runtime/http.js"
|
|
70
70
|
},
|
|
71
|
+
"./runtime/a11y": {
|
|
72
|
+
"types": "./types/a11y.d.ts",
|
|
73
|
+
"default": "./runtime/a11y.js"
|
|
74
|
+
},
|
|
71
75
|
"./runtime/devtools": "./runtime/devtools.js",
|
|
72
76
|
"./compiler": {
|
|
73
77
|
"types": "./types/index.d.ts",
|
|
@@ -97,7 +101,7 @@
|
|
|
97
101
|
"LICENSE"
|
|
98
102
|
],
|
|
99
103
|
"scripts": {
|
|
100
|
-
"test": "npm run test:compiler && npm run test:sourcemap && npm run test:pulse && npm run test:dom && npm run test:dom-adapter && npm run test:router && npm run test:store && npm run test:hmr && npm run test:lint && npm run test:format && npm run test:analyze && npm run test:lru-cache && npm run test:utils && npm run test:docs && npm run test:async && npm run test:form && npm run test:http && npm run test:devtools && npm run test:native",
|
|
104
|
+
"test": "npm run test:compiler && npm run test:sourcemap && npm run test:pulse && npm run test:dom && npm run test:dom-adapter && npm run test:router && npm run test:store && npm run test:hmr && npm run test:lint && npm run test:format && npm run test:analyze && npm run test:lru-cache && npm run test:utils && npm run test:docs && npm run test:async && npm run test:form && npm run test:http && npm run test:devtools && npm run test:native && npm run test:a11y && npm run test:logger && npm run test:errors",
|
|
101
105
|
"test:compiler": "node test/compiler.test.js",
|
|
102
106
|
"test:sourcemap": "node test/sourcemap.test.js",
|
|
103
107
|
"test:pulse": "node test/pulse.test.js",
|
|
@@ -117,6 +121,9 @@
|
|
|
117
121
|
"test:http": "node test/http.test.js",
|
|
118
122
|
"test:devtools": "node test/devtools.test.js",
|
|
119
123
|
"test:native": "node test/native.test.js",
|
|
124
|
+
"test:a11y": "node test/a11y.test.js",
|
|
125
|
+
"test:logger": "node test/logger.test.js",
|
|
126
|
+
"test:errors": "node test/errors.test.js",
|
|
120
127
|
"build:netlify": "node scripts/build-netlify.js",
|
|
121
128
|
"version": "node scripts/sync-version.js",
|
|
122
129
|
"docs": "node cli/index.js dev docs"
|