pulse-js-framework 1.4.10 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Transformer Style Module
3
+ * Handles style block transformation with CSS scoping
4
+ * @module pulse-js-framework/compiler/transformer/style
5
+ */
6
+
7
+ /**
8
+ * Transform style block with optional scoping
9
+ * @param {Object} transformer - Transformer instance
10
+ * @param {Object} styleBlock - Style block from AST
11
+ * @returns {string} JavaScript code
12
+ */
13
+ export function transformStyle(transformer, styleBlock) {
14
+ const lines = ['// Styles'];
15
+
16
+ if (transformer.scopeId) {
17
+ lines.push(`const SCOPE_ID = '${transformer.scopeId}';`);
18
+ }
19
+
20
+ lines.push('const styles = `');
21
+
22
+ for (const rule of styleBlock.rules) {
23
+ lines.push(transformStyleRule(transformer, rule, 0));
24
+ }
25
+
26
+ lines.push('`;');
27
+ lines.push('');
28
+ lines.push('// Inject styles');
29
+ lines.push('const styleEl = document.createElement("style");');
30
+
31
+ if (transformer.scopeId) {
32
+ lines.push(`styleEl.setAttribute('data-p-scope', SCOPE_ID);`);
33
+ }
34
+
35
+ lines.push('styleEl.textContent = styles;');
36
+ lines.push('document.head.appendChild(styleEl);');
37
+
38
+ return lines.join('\n');
39
+ }
40
+
41
+ /**
42
+ * Transform style rule with optional scoping
43
+ * @param {Object} transformer - Transformer instance
44
+ * @param {Object} rule - CSS rule from AST
45
+ * @param {number} indent - Indentation level
46
+ * @returns {string} CSS code
47
+ */
48
+ export function transformStyleRule(transformer, rule, indent) {
49
+ const pad = ' '.repeat(indent);
50
+ const lines = [];
51
+
52
+ // Apply scope to selector if enabled
53
+ let selector = rule.selector;
54
+ if (transformer.scopeId) {
55
+ selector = scopeStyleSelector(transformer, selector);
56
+ }
57
+
58
+ lines.push(`${pad}${selector} {`);
59
+
60
+ for (const prop of rule.properties) {
61
+ lines.push(`${pad} ${prop.name}: ${prop.value};`);
62
+ }
63
+
64
+ for (const nested of rule.nestedRules) {
65
+ // For nested rules, combine selectors (simplified nesting)
66
+ const nestedLines = transformStyleRule(transformer, nested, indent + 1);
67
+ lines.push(nestedLines);
68
+ }
69
+
70
+ lines.push(`${pad}}`);
71
+ return lines.join('\n');
72
+ }
73
+
74
+ /**
75
+ * Add scope to CSS selector
76
+ * .container -> .container.p123abc
77
+ * div -> div.p123abc
78
+ * .a .b -> .a.p123abc .b.p123abc
79
+ * @media (max-width: 900px) -> @media (max-width: 900px) (unchanged)
80
+ * :root, body, *, html -> unchanged (global selectors)
81
+ * @param {Object} transformer - Transformer instance
82
+ * @param {string} selector - CSS selector
83
+ * @returns {string} Scoped selector
84
+ */
85
+ export function scopeStyleSelector(transformer, selector) {
86
+ if (!transformer.scopeId) return selector;
87
+
88
+ // Don't scope at-rules (media queries, keyframes, etc.)
89
+ if (selector.startsWith('@')) {
90
+ return selector;
91
+ }
92
+
93
+ // Global selectors that should not be scoped
94
+ const globalSelectors = new Set([':root', 'body', 'html', '*']);
95
+
96
+ // Check if entire selector is a global selector (possibly with classes like body.dark)
97
+ const trimmed = selector.trim();
98
+ const baseSelector = trimmed.split(/[.#\[:\s]/)[0];
99
+ if (globalSelectors.has(baseSelector) || globalSelectors.has(trimmed)) {
100
+ return selector;
101
+ }
102
+
103
+ // Split by comma for multiple selectors
104
+ return selector.split(',').map(part => {
105
+ part = part.trim();
106
+
107
+ // Split by space for descendant selectors
108
+ return part.split(/\s+/).map(segment => {
109
+ // Check if this segment is a global selector
110
+ const segmentBase = segment.split(/[.#\[]/)[0];
111
+ if (globalSelectors.has(segmentBase) || globalSelectors.has(segment)) {
112
+ return segment;
113
+ }
114
+
115
+ // Skip pseudo-elements and pseudo-classes at the end
116
+ const pseudoMatch = segment.match(/^([^:]+)(:.+)?$/);
117
+ if (pseudoMatch) {
118
+ const base = pseudoMatch[1];
119
+ const pseudo = pseudoMatch[2] || '';
120
+
121
+ // Skip if it's just a pseudo selector (like :root)
122
+ if (!base || globalSelectors.has(`:${pseudo.slice(1)}`)) return segment;
123
+
124
+ // Add scope class
125
+ return `${base}.${transformer.scopeId}${pseudo}`;
126
+ }
127
+ return `${segment}.${transformer.scopeId}`;
128
+ }).join(' ');
129
+ }).join(', ');
130
+ }
@@ -0,0 +1,428 @@
1
+ /**
2
+ * Transformer View Module
3
+ * Handles view block and element transformations
4
+ * @module pulse-js-framework/compiler/transformer/view
5
+ */
6
+
7
+ import { NodeType } from '../parser.js';
8
+ import { transformValue } from './state.js';
9
+ import { transformExpression, transformExpressionString } from './expressions.js';
10
+
11
+ /** View node transformers lookup table */
12
+ export const VIEW_NODE_HANDLERS = {
13
+ [NodeType.Element]: 'transformElement',
14
+ [NodeType.TextNode]: 'transformTextNode',
15
+ [NodeType.IfDirective]: 'transformIfDirective',
16
+ [NodeType.EachDirective]: 'transformEachDirective',
17
+ [NodeType.EventDirective]: 'transformEventDirective',
18
+ [NodeType.SlotElement]: 'transformSlot',
19
+ [NodeType.LinkDirective]: 'transformLinkDirective',
20
+ [NodeType.OutletDirective]: 'transformOutletDirective',
21
+ [NodeType.NavigateDirective]: 'transformNavigateDirective'
22
+ };
23
+
24
+ /**
25
+ * Transform view block
26
+ * @param {Object} transformer - Transformer instance
27
+ * @param {Object} viewBlock - View block from AST
28
+ * @returns {string} JavaScript code
29
+ */
30
+ export function transformView(transformer, viewBlock) {
31
+ const lines = ['// View'];
32
+
33
+ // Generate render function with props parameter
34
+ lines.push('function render({ props = {}, slots = {} } = {}) {');
35
+
36
+ // Destructure props with defaults if component has props
37
+ if (transformer.propVars.size > 0) {
38
+ const propsDestructure = [...transformer.propVars].map(name => {
39
+ const defaultValue = transformer.propDefaults.get(name);
40
+ const defaultCode = defaultValue ? transformValue(transformer, defaultValue) : 'undefined';
41
+ return `${name} = ${defaultCode}`;
42
+ }).join(', ');
43
+ lines.push(` const { ${propsDestructure} } = props;`);
44
+ }
45
+
46
+ lines.push(' return (');
47
+
48
+ const children = viewBlock.children.map(child =>
49
+ transformViewNode(transformer, child, 4)
50
+ );
51
+
52
+ if (children.length === 1) {
53
+ lines.push(children[0]);
54
+ } else {
55
+ lines.push(' [');
56
+ lines.push(children.map(c => ' ' + c.trim()).join(',\n'));
57
+ lines.push(' ]');
58
+ }
59
+
60
+ lines.push(' );');
61
+ lines.push('}');
62
+
63
+ return lines.join('\n');
64
+ }
65
+
66
+ /**
67
+ * Transform a view node (element, directive, slot, text)
68
+ * @param {Object} transformer - Transformer instance
69
+ * @param {Object} node - AST node
70
+ * @param {number} indent - Indentation level
71
+ * @returns {string} JavaScript code
72
+ */
73
+ export function transformViewNode(transformer, node, indent = 0) {
74
+ const handler = VIEW_NODE_HANDLERS[node.type];
75
+ if (handler) {
76
+ // Call the appropriate handler function
77
+ switch (node.type) {
78
+ case NodeType.Element:
79
+ return transformElement(transformer, node, indent);
80
+ case NodeType.TextNode:
81
+ return transformTextNode(transformer, node, indent);
82
+ case NodeType.IfDirective:
83
+ return transformIfDirective(transformer, node, indent);
84
+ case NodeType.EachDirective:
85
+ return transformEachDirective(transformer, node, indent);
86
+ case NodeType.EventDirective:
87
+ return transformEventDirective(transformer, node, indent);
88
+ case NodeType.SlotElement:
89
+ return transformSlot(transformer, node, indent);
90
+ case NodeType.LinkDirective:
91
+ return transformLinkDirective(transformer, node, indent);
92
+ case NodeType.OutletDirective:
93
+ return transformOutletDirective(transformer, node, indent);
94
+ case NodeType.NavigateDirective:
95
+ return transformNavigateDirective(transformer, node, indent);
96
+ default:
97
+ return `${' '.repeat(indent)}/* unknown node: ${node.type} */`;
98
+ }
99
+ }
100
+ return `${' '.repeat(indent)}/* unknown node: ${node.type} */`;
101
+ }
102
+
103
+ /**
104
+ * Transform slot element
105
+ * @param {Object} transformer - Transformer instance
106
+ * @param {Object} node - Slot node
107
+ * @param {number} indent - Indentation level
108
+ * @returns {string} JavaScript code
109
+ */
110
+ export function transformSlot(transformer, node, indent) {
111
+ const pad = ' '.repeat(indent);
112
+ const slotName = node.name || 'default';
113
+
114
+ // If there's fallback content
115
+ if (node.fallback && node.fallback.length > 0) {
116
+ const fallbackCode = node.fallback.map(child =>
117
+ transformViewNode(transformer, child, indent + 2)
118
+ ).join(',\n');
119
+
120
+ return `${pad}(slots?.${slotName} ? slots.${slotName}() : (\n${fallbackCode}\n${pad}))`;
121
+ }
122
+
123
+ // Simple slot reference
124
+ return `${pad}(slots?.${slotName} ? slots.${slotName}() : null)`;
125
+ }
126
+
127
+ /**
128
+ * Transform @link directive
129
+ * @param {Object} transformer - Transformer instance
130
+ * @param {Object} node - Link directive node
131
+ * @param {number} indent - Indentation level
132
+ * @returns {string} JavaScript code
133
+ */
134
+ export function transformLinkDirective(transformer, node, indent) {
135
+ const pad = ' '.repeat(indent);
136
+ const path = transformExpression(transformer, node.path);
137
+
138
+ let content;
139
+ if (Array.isArray(node.content)) {
140
+ content = node.content.map(c => transformViewNode(transformer, c, 0)).join(', ');
141
+ } else if (node.content) {
142
+ content = transformTextNode(transformer, node.content, 0).trim();
143
+ } else {
144
+ content = '""';
145
+ }
146
+
147
+ let options = '{}';
148
+ if (node.options) {
149
+ options = transformExpression(transformer, node.options);
150
+ }
151
+
152
+ return `${pad}router.link(${path}, ${content}, ${options})`;
153
+ }
154
+
155
+ /**
156
+ * Transform @outlet directive
157
+ * @param {Object} transformer - Transformer instance
158
+ * @param {Object} node - Outlet directive node
159
+ * @param {number} indent - Indentation level
160
+ * @returns {string} JavaScript code
161
+ */
162
+ export function transformOutletDirective(transformer, node, indent) {
163
+ const pad = ' '.repeat(indent);
164
+ const container = node.container ? `'${node.container}'` : "'#app'";
165
+ return `${pad}router.outlet(${container})`;
166
+ }
167
+
168
+ /**
169
+ * Transform @navigate directive (used in event handlers)
170
+ * @param {Object} transformer - Transformer instance
171
+ * @param {Object} node - Navigate directive node
172
+ * @param {number} indent - Indentation level
173
+ * @returns {string} JavaScript code
174
+ */
175
+ export function transformNavigateDirective(transformer, node, indent) {
176
+ const pad = ' '.repeat(indent);
177
+
178
+ // Handle @back and @forward
179
+ if (node.action === 'back') {
180
+ return `${pad}router.back()`;
181
+ }
182
+ if (node.action === 'forward') {
183
+ return `${pad}router.forward()`;
184
+ }
185
+
186
+ // Regular @navigate(path)
187
+ const path = transformExpression(transformer, node.path);
188
+ let options = '';
189
+ if (node.options) {
190
+ options = ', ' + transformExpression(transformer, node.options);
191
+ }
192
+ return `${pad}router.navigate(${path}${options})`;
193
+ }
194
+
195
+ /**
196
+ * Transform element
197
+ * @param {Object} transformer - Transformer instance
198
+ * @param {Object} node - Element node
199
+ * @param {number} indent - Indentation level
200
+ * @returns {string} JavaScript code
201
+ */
202
+ export function transformElement(transformer, node, indent) {
203
+ const pad = ' '.repeat(indent);
204
+ const parts = [];
205
+
206
+ // Check if this is a component (starts with uppercase)
207
+ const selectorParts = node.selector.match(/^([a-zA-Z][a-zA-Z0-9]*)/);
208
+ const tagName = selectorParts ? selectorParts[1] : '';
209
+ const isComponent = tagName && /^[A-Z]/.test(tagName) &&
210
+ transformer.importedComponents.has(tagName);
211
+
212
+ if (isComponent) {
213
+ // Render as component call
214
+ return transformComponentCall(transformer, node, indent);
215
+ }
216
+
217
+ // Add scoped class to selector if CSS scoping is enabled
218
+ let selector = node.selector;
219
+ if (transformer.scopeId && selector) {
220
+ selector = addScopeToSelector(transformer, selector);
221
+ }
222
+
223
+ // Start with el() call
224
+ parts.push(`${pad}el('${selector}'`);
225
+
226
+ // Add event handlers as on() chain
227
+ const eventHandlers = node.directives.filter(d => d.type === NodeType.EventDirective);
228
+
229
+ // Add text content
230
+ if (node.textContent.length > 0) {
231
+ for (const text of node.textContent) {
232
+ const textCode = transformTextNode(transformer, text, 0);
233
+ parts.push(`,\n${pad} ${textCode.trim()}`);
234
+ }
235
+ }
236
+
237
+ // Add children
238
+ if (node.children.length > 0) {
239
+ for (const child of node.children) {
240
+ const childCode = transformViewNode(transformer, child, indent + 2);
241
+ parts.push(`,\n${childCode}`);
242
+ }
243
+ }
244
+
245
+ parts.push(')');
246
+
247
+ // Chain event handlers
248
+ let result = parts.join('');
249
+ for (const handler of eventHandlers) {
250
+ const handlerCode = transformExpression(transformer, handler.handler);
251
+ result = `on(${result}, '${handler.event}', () => { ${handlerCode}; })`;
252
+ }
253
+
254
+ return result;
255
+ }
256
+
257
+ /**
258
+ * Add scope class to a CSS selector
259
+ * @param {Object} transformer - Transformer instance
260
+ * @param {string} selector - CSS selector
261
+ * @returns {string} Scoped selector
262
+ */
263
+ export function addScopeToSelector(transformer, selector) {
264
+ // If selector has classes, add scope class after the first class
265
+ // Otherwise add it at the end
266
+ if (selector.includes('.')) {
267
+ // Add scope after tag name and before first class
268
+ return selector.replace(/^([a-zA-Z0-9-]*)/, `$1.${transformer.scopeId}`);
269
+ }
270
+ // Just a tag name, add scope class
271
+ return `${selector}.${transformer.scopeId}`;
272
+ }
273
+
274
+ /**
275
+ * Transform a component call (imported component)
276
+ * @param {Object} transformer - Transformer instance
277
+ * @param {Object} node - Element node (component)
278
+ * @param {number} indent - Indentation level
279
+ * @returns {string} JavaScript code
280
+ */
281
+ export function transformComponentCall(transformer, node, indent) {
282
+ const pad = ' '.repeat(indent);
283
+ const selectorParts = node.selector.match(/^([a-zA-Z][a-zA-Z0-9]*)/);
284
+ const componentName = selectorParts[1];
285
+
286
+ // Extract slots from children
287
+ const slots = {};
288
+
289
+ // Children become the default slot
290
+ if (node.children.length > 0 || node.textContent.length > 0) {
291
+ const slotContent = [];
292
+ for (const text of node.textContent) {
293
+ slotContent.push(transformTextNode(transformer, text, 0).trim());
294
+ }
295
+ for (const child of node.children) {
296
+ slotContent.push(transformViewNode(transformer, child, 0).trim());
297
+ }
298
+ slots['default'] = slotContent;
299
+ }
300
+
301
+ // Build component call
302
+ let code = `${pad}${componentName}.render({ `;
303
+
304
+ const renderArgs = [];
305
+
306
+ // Add props if any
307
+ if (node.props && node.props.length > 0) {
308
+ const propsCode = node.props.map(prop => {
309
+ const valueCode = transformExpression(transformer, prop.value);
310
+ return `${prop.name}: ${valueCode}`;
311
+ }).join(', ');
312
+ renderArgs.push(`props: { ${propsCode} }`);
313
+ }
314
+
315
+ // Add slots if any
316
+ if (Object.keys(slots).length > 0) {
317
+ const slotCode = Object.entries(slots).map(([name, content]) => {
318
+ return `${name}: () => ${content.length === 1 ? content[0] : `[${content.join(', ')}]`}`;
319
+ }).join(', ');
320
+ renderArgs.push(`slots: { ${slotCode} }`);
321
+ }
322
+
323
+ code += renderArgs.join(', ');
324
+ code += ' })';
325
+ return code;
326
+ }
327
+
328
+ /**
329
+ * Transform text node
330
+ * @param {Object} transformer - Transformer instance
331
+ * @param {Object} node - Text node
332
+ * @param {number} indent - Indentation level
333
+ * @returns {string} JavaScript code
334
+ */
335
+ export function transformTextNode(transformer, node, indent) {
336
+ const pad = ' '.repeat(indent);
337
+ const parts = node.parts;
338
+
339
+ if (parts.length === 1 && typeof parts[0] === 'string') {
340
+ // Simple static text
341
+ return `${pad}${JSON.stringify(parts[0])}`;
342
+ }
343
+
344
+ // Has interpolations - use text() with a function
345
+ const textParts = parts.map(part => {
346
+ if (typeof part === 'string') {
347
+ return JSON.stringify(part);
348
+ }
349
+ // Interpolation
350
+ const expr = transformExpressionString(transformer, part.expression);
351
+ return `\${${expr}}`;
352
+ });
353
+
354
+ return `${pad}text(() => \`${textParts.join('')}\`)`;
355
+ }
356
+
357
+ /**
358
+ * Transform @if directive
359
+ * @param {Object} transformer - Transformer instance
360
+ * @param {Object} node - If directive node
361
+ * @param {number} indent - Indentation level
362
+ * @returns {string} JavaScript code
363
+ */
364
+ export function transformIfDirective(transformer, node, indent) {
365
+ 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
+
372
+ let code = `${pad}when(\n`;
373
+ code += `${pad} () => ${condition},\n`;
374
+ code += `${pad} () => (\n${consequent}\n${pad} )`;
375
+
376
+ if (node.alternate) {
377
+ const alternate = node.alternate.map(c =>
378
+ transformViewNode(transformer, c, indent + 2)
379
+ ).join(',\n');
380
+ code += `,\n${pad} () => (\n${alternate}\n${pad} )`;
381
+ }
382
+
383
+ code += `\n${pad})`;
384
+ return code;
385
+ }
386
+
387
+ /**
388
+ * Transform @each directive
389
+ * @param {Object} transformer - Transformer instance
390
+ * @param {Object} node - Each directive node
391
+ * @param {number} indent - Indentation level
392
+ * @returns {string} JavaScript code
393
+ */
394
+ export function transformEachDirective(transformer, node, indent) {
395
+ const pad = ' '.repeat(indent);
396
+ const iterable = transformExpression(transformer, node.iterable);
397
+
398
+ const template = node.template.map(t =>
399
+ transformViewNode(transformer, t, indent + 2)
400
+ ).join(',\n');
401
+
402
+ return `${pad}list(\n` +
403
+ `${pad} () => ${iterable},\n` +
404
+ `${pad} (${node.itemName}, _index) => (\n${template}\n${pad} )\n` +
405
+ `${pad})`;
406
+ }
407
+
408
+ /**
409
+ * Transform event directive
410
+ * @param {Object} transformer - Transformer instance
411
+ * @param {Object} node - Event directive node
412
+ * @param {number} indent - Indentation level
413
+ * @returns {string} JavaScript code
414
+ */
415
+ export function transformEventDirective(transformer, node, indent) {
416
+ const pad = ' '.repeat(indent);
417
+ const handler = transformExpression(transformer, node.handler);
418
+
419
+ if (node.children && node.children.length > 0) {
420
+ const children = node.children.map(c =>
421
+ transformViewNode(transformer, c, indent + 2)
422
+ ).join(',\n');
423
+
424
+ return `${pad}on(el('div',\n${children}\n${pad}), '${node.event}', () => { ${handler}; })`;
425
+ }
426
+
427
+ return `/* event: ${node.event} -> ${handler} */`;
428
+ }