pulse-js-framework 1.0.0 → 1.4.0
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 +414 -182
- package/cli/analyze.js +499 -0
- package/cli/build.js +341 -199
- package/cli/format.js +704 -0
- package/cli/index.js +398 -324
- package/cli/lint.js +642 -0
- package/cli/mobile.js +1473 -0
- package/cli/utils/file-utils.js +298 -0
- package/compiler/lexer.js +766 -581
- package/compiler/parser.js +1797 -900
- package/compiler/transformer.js +1332 -552
- package/index.js +1 -1
- package/mobile/bridge/pulse-native.js +420 -0
- package/package.json +68 -58
- package/runtime/dom.js +363 -33
- package/runtime/index.js +2 -0
- package/runtime/native.js +368 -0
- package/runtime/pulse.js +247 -13
- package/runtime/router.js +596 -392
package/cli/format.js
ADDED
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse CLI - Format Command
|
|
3
|
+
* Formats .pulse files consistently
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
7
|
+
import { findPulseFiles, parseArgs, relativePath } from './utils/file-utils.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Default format options
|
|
11
|
+
*/
|
|
12
|
+
export const FormatOptions = {
|
|
13
|
+
indentSize: 2,
|
|
14
|
+
maxLineLength: 100,
|
|
15
|
+
sortImports: true,
|
|
16
|
+
insertFinalNewline: true
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Pulse code formatter (AST-based)
|
|
21
|
+
*/
|
|
22
|
+
export class PulseFormatter {
|
|
23
|
+
constructor(ast, options = {}) {
|
|
24
|
+
this.ast = ast;
|
|
25
|
+
this.options = { ...FormatOptions, ...options };
|
|
26
|
+
this.indent = 0;
|
|
27
|
+
this.output = [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Format the AST to string
|
|
32
|
+
*/
|
|
33
|
+
format() {
|
|
34
|
+
// Imports (sorted if option enabled)
|
|
35
|
+
this.formatImports();
|
|
36
|
+
|
|
37
|
+
// Page declaration
|
|
38
|
+
if (this.ast.page) {
|
|
39
|
+
this.emit(`@page ${this.ast.page.name}`);
|
|
40
|
+
this.emit('');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Route declaration
|
|
44
|
+
if (this.ast.route) {
|
|
45
|
+
this.emit(`@route "${this.ast.route.path}"`);
|
|
46
|
+
this.emit('');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// State block
|
|
50
|
+
if (this.ast.state) {
|
|
51
|
+
this.formatStateBlock();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// View block
|
|
55
|
+
if (this.ast.view) {
|
|
56
|
+
this.formatViewBlock();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Actions block
|
|
60
|
+
if (this.ast.actions) {
|
|
61
|
+
this.formatActionsBlock();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Style block
|
|
65
|
+
if (this.ast.style) {
|
|
66
|
+
this.formatStyleBlock();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let result = this.output.join('\n');
|
|
70
|
+
|
|
71
|
+
// Ensure final newline
|
|
72
|
+
if (this.options.insertFinalNewline && !result.endsWith('\n')) {
|
|
73
|
+
result += '\n';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Format import statements
|
|
81
|
+
*/
|
|
82
|
+
formatImports() {
|
|
83
|
+
let imports = [...(this.ast.imports || [])];
|
|
84
|
+
|
|
85
|
+
if (imports.length === 0) return;
|
|
86
|
+
|
|
87
|
+
if (this.options.sortImports) {
|
|
88
|
+
imports.sort((a, b) => a.source.localeCompare(b.source));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const imp of imports) {
|
|
92
|
+
this.emit(this.formatImportStatement(imp));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.emit('');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Format a single import statement
|
|
100
|
+
*/
|
|
101
|
+
formatImportStatement(imp) {
|
|
102
|
+
const specifiers = imp.specifiers || [];
|
|
103
|
+
|
|
104
|
+
if (specifiers.length === 0) {
|
|
105
|
+
return `import '${imp.source}'`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Default import
|
|
109
|
+
const defaultSpec = specifiers.find(s => s.type === 'default');
|
|
110
|
+
// Named imports
|
|
111
|
+
const namedSpecs = specifiers.filter(s => s.type === 'named');
|
|
112
|
+
// Namespace import
|
|
113
|
+
const namespaceSpec = specifiers.find(s => s.type === 'namespace');
|
|
114
|
+
|
|
115
|
+
let parts = ['import'];
|
|
116
|
+
|
|
117
|
+
if (defaultSpec) {
|
|
118
|
+
parts.push(defaultSpec.local);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (namespaceSpec) {
|
|
122
|
+
if (defaultSpec) parts.push(',');
|
|
123
|
+
parts.push(`* as ${namespaceSpec.local}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (namedSpecs.length > 0) {
|
|
127
|
+
if (defaultSpec || namespaceSpec) parts.push(',');
|
|
128
|
+
|
|
129
|
+
const namedParts = namedSpecs.map(s => {
|
|
130
|
+
if (s.imported && s.imported !== s.local) {
|
|
131
|
+
return `${s.imported} as ${s.local}`;
|
|
132
|
+
}
|
|
133
|
+
return s.local;
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
parts.push(`{ ${namedParts.join(', ')} }`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
parts.push('from');
|
|
140
|
+
parts.push(`'${imp.source}'`);
|
|
141
|
+
|
|
142
|
+
return parts.join(' ');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Format state block
|
|
147
|
+
*/
|
|
148
|
+
formatStateBlock() {
|
|
149
|
+
this.emit('state {');
|
|
150
|
+
this.indent++;
|
|
151
|
+
|
|
152
|
+
const properties = this.ast.state.properties || [];
|
|
153
|
+
for (const prop of properties) {
|
|
154
|
+
const value = this.formatValue(prop.value);
|
|
155
|
+
this.emit(`${prop.name}: ${value}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
this.indent--;
|
|
159
|
+
this.emit('}');
|
|
160
|
+
this.emit('');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Format a value (literal, object, array)
|
|
165
|
+
*/
|
|
166
|
+
formatValue(node) {
|
|
167
|
+
if (node === null || node === undefined) {
|
|
168
|
+
return 'null';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (typeof node === 'string') {
|
|
172
|
+
return `"${this.escapeString(node)}"`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (typeof node === 'number' || typeof node === 'boolean') {
|
|
176
|
+
return String(node);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (node.type === 'Literal') {
|
|
180
|
+
if (typeof node.value === 'string') {
|
|
181
|
+
return `"${this.escapeString(node.value)}"`;
|
|
182
|
+
}
|
|
183
|
+
return String(node.value);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (node.type === 'Identifier') {
|
|
187
|
+
return node.name;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (node.type === 'ArrayLiteral' || Array.isArray(node.elements)) {
|
|
191
|
+
const elements = node.elements || [];
|
|
192
|
+
if (elements.length === 0) return '[]';
|
|
193
|
+
|
|
194
|
+
const formatted = elements.map(el => this.formatValue(el));
|
|
195
|
+
const inline = `[${formatted.join(', ')}]`;
|
|
196
|
+
|
|
197
|
+
if (inline.length <= 60) {
|
|
198
|
+
return inline;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Multi-line array
|
|
202
|
+
return `[\n${formatted.map(f => this.getIndent(1) + f).join(',\n')}\n${this.getIndent()}]`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (node.type === 'ObjectLiteral' || node.properties) {
|
|
206
|
+
const properties = node.properties || [];
|
|
207
|
+
if (properties.length === 0) return '{}';
|
|
208
|
+
|
|
209
|
+
const formatted = properties.map(p => `${p.name}: ${this.formatValue(p.value)}`);
|
|
210
|
+
const inline = `{ ${formatted.join(', ')} }`;
|
|
211
|
+
|
|
212
|
+
if (inline.length <= 60) {
|
|
213
|
+
return inline;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Multi-line object
|
|
217
|
+
return `{\n${formatted.map(f => this.getIndent(1) + f).join(',\n')}\n${this.getIndent()}}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Default - return as-is
|
|
221
|
+
return String(node.value || node);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Format view block
|
|
226
|
+
*/
|
|
227
|
+
formatViewBlock() {
|
|
228
|
+
this.emit('view {');
|
|
229
|
+
this.indent++;
|
|
230
|
+
|
|
231
|
+
const children = this.ast.view.children || [];
|
|
232
|
+
for (const child of children) {
|
|
233
|
+
this.formatViewNode(child);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
this.indent--;
|
|
237
|
+
this.emit('}');
|
|
238
|
+
this.emit('');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Format a single view node
|
|
243
|
+
*/
|
|
244
|
+
formatViewNode(node) {
|
|
245
|
+
if (!node) return;
|
|
246
|
+
|
|
247
|
+
switch (node.type) {
|
|
248
|
+
case 'Element':
|
|
249
|
+
this.formatElement(node);
|
|
250
|
+
break;
|
|
251
|
+
|
|
252
|
+
case 'TextNode':
|
|
253
|
+
this.formatTextNode(node);
|
|
254
|
+
break;
|
|
255
|
+
|
|
256
|
+
case 'IfDirective':
|
|
257
|
+
this.formatIfDirective(node);
|
|
258
|
+
break;
|
|
259
|
+
|
|
260
|
+
case 'EachDirective':
|
|
261
|
+
this.formatEachDirective(node);
|
|
262
|
+
break;
|
|
263
|
+
|
|
264
|
+
case 'SlotElement':
|
|
265
|
+
this.formatSlot(node);
|
|
266
|
+
break;
|
|
267
|
+
|
|
268
|
+
default:
|
|
269
|
+
// Unknown node type - skip
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Format an expression to string
|
|
276
|
+
*/
|
|
277
|
+
formatExpression(expr) {
|
|
278
|
+
if (!expr) return '';
|
|
279
|
+
if (typeof expr === 'string') return expr;
|
|
280
|
+
|
|
281
|
+
// Handle AST expression nodes
|
|
282
|
+
switch (expr.type) {
|
|
283
|
+
case 'Identifier':
|
|
284
|
+
return expr.name;
|
|
285
|
+
case 'Literal':
|
|
286
|
+
return typeof expr.value === 'string' ? `"${expr.value}"` : String(expr.value);
|
|
287
|
+
case 'MemberExpression':
|
|
288
|
+
return `${this.formatExpression(expr.object)}.${this.formatExpression(expr.property)}`;
|
|
289
|
+
case 'CallExpression':
|
|
290
|
+
const callee = this.formatExpression(expr.callee);
|
|
291
|
+
const args = (expr.arguments || []).map(a => this.formatExpression(a)).join(', ');
|
|
292
|
+
return `${callee}(${args})`;
|
|
293
|
+
case 'BinaryExpression':
|
|
294
|
+
return `${this.formatExpression(expr.left)} ${expr.operator} ${this.formatExpression(expr.right)}`;
|
|
295
|
+
case 'UpdateExpression':
|
|
296
|
+
return expr.prefix
|
|
297
|
+
? `${expr.operator}${this.formatExpression(expr.argument)}`
|
|
298
|
+
: `${this.formatExpression(expr.argument)}${expr.operator}`;
|
|
299
|
+
case 'UnaryExpression':
|
|
300
|
+
return `${expr.operator}${this.formatExpression(expr.argument)}`;
|
|
301
|
+
default:
|
|
302
|
+
// Fallback to raw if available
|
|
303
|
+
if (expr.raw) return expr.raw;
|
|
304
|
+
return String(expr);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Format an element node
|
|
310
|
+
*/
|
|
311
|
+
formatElement(node) {
|
|
312
|
+
let line = node.selector || node.tag || 'div';
|
|
313
|
+
|
|
314
|
+
// Add inline directives
|
|
315
|
+
for (const dir of node.directives || []) {
|
|
316
|
+
const handler = this.formatExpression(dir.handler || dir.expression || '');
|
|
317
|
+
line += ` @${dir.event || dir.name}(${handler})`;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Get text content
|
|
321
|
+
const textContent = this.getTextContent(node);
|
|
322
|
+
const children = node.children || [];
|
|
323
|
+
const hasChildren = children.length > 0;
|
|
324
|
+
|
|
325
|
+
if (textContent && !hasChildren) {
|
|
326
|
+
// Inline text content
|
|
327
|
+
line += ` "${this.escapeString(textContent)}"`;
|
|
328
|
+
this.emit(line);
|
|
329
|
+
} else if (hasChildren) {
|
|
330
|
+
// Block with children
|
|
331
|
+
this.emit(`${line} {`);
|
|
332
|
+
this.indent++;
|
|
333
|
+
|
|
334
|
+
// Add text content as first child if present
|
|
335
|
+
if (textContent) {
|
|
336
|
+
this.emit(`"${this.escapeString(textContent)}"`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
for (const child of children) {
|
|
340
|
+
this.formatViewNode(child);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
this.indent--;
|
|
344
|
+
this.emit('}');
|
|
345
|
+
} else {
|
|
346
|
+
// Empty element
|
|
347
|
+
this.emit(line);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Format a part of an interpolated string
|
|
353
|
+
*/
|
|
354
|
+
formatPart(p) {
|
|
355
|
+
if (typeof p === 'string') return p;
|
|
356
|
+
// Handle Interpolation AST node (type: 'Interpolation', expression: string)
|
|
357
|
+
if (p.type === 'Interpolation') {
|
|
358
|
+
const expr = typeof p.expression === 'string' ? p.expression : this.formatExpression(p.expression);
|
|
359
|
+
return `{${expr}}`;
|
|
360
|
+
}
|
|
361
|
+
// Handle plain object with expr property
|
|
362
|
+
if (p.expr) return `{${p.expr}}`;
|
|
363
|
+
if (p.expression) return `{${p.expression}}`;
|
|
364
|
+
return String(p);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Get text content from element
|
|
369
|
+
*/
|
|
370
|
+
getTextContent(node) {
|
|
371
|
+
const textContent = node.textContent || [];
|
|
372
|
+
|
|
373
|
+
if (textContent.length === 0) return '';
|
|
374
|
+
|
|
375
|
+
return textContent.map(t => {
|
|
376
|
+
if (typeof t === 'string') return t;
|
|
377
|
+
// Handle TextNode with parts (interpolated strings)
|
|
378
|
+
if (t.type === 'TextNode' && t.parts) {
|
|
379
|
+
return t.parts.map(p => this.formatPart(p)).join('');
|
|
380
|
+
}
|
|
381
|
+
if (t.type === 'Interpolation') {
|
|
382
|
+
const expr = typeof t.expression === 'string' ? t.expression : this.formatExpression(t.expression);
|
|
383
|
+
return `{${expr}}`;
|
|
384
|
+
}
|
|
385
|
+
if (t.type === 'TextNode') return t.value || '';
|
|
386
|
+
return String(t.value || t);
|
|
387
|
+
}).join('');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Format a text node
|
|
392
|
+
*/
|
|
393
|
+
formatTextNode(node) {
|
|
394
|
+
const value = node.value || '';
|
|
395
|
+
this.emit(`"${this.escapeString(value)}"`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Format if directive
|
|
400
|
+
*/
|
|
401
|
+
formatIfDirective(node) {
|
|
402
|
+
const condition = node.condition || 'true';
|
|
403
|
+
this.emit(`@if (${condition}) {`);
|
|
404
|
+
this.indent++;
|
|
405
|
+
|
|
406
|
+
const consequent = node.consequent?.children || node.consequent || [];
|
|
407
|
+
for (const child of consequent) {
|
|
408
|
+
this.formatViewNode(child);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
this.indent--;
|
|
412
|
+
|
|
413
|
+
if (node.alternate) {
|
|
414
|
+
this.emit('} @else {');
|
|
415
|
+
this.indent++;
|
|
416
|
+
|
|
417
|
+
const alternate = node.alternate?.children || node.alternate || [];
|
|
418
|
+
for (const child of alternate) {
|
|
419
|
+
this.formatViewNode(child);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
this.indent--;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
this.emit('}');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Format each directive
|
|
430
|
+
*/
|
|
431
|
+
formatEachDirective(node) {
|
|
432
|
+
const item = node.item || 'item';
|
|
433
|
+
const iterable = node.iterable || 'items';
|
|
434
|
+
this.emit(`@each (${item} in ${iterable}) {`);
|
|
435
|
+
this.indent++;
|
|
436
|
+
|
|
437
|
+
const body = node.body?.children || node.body || [];
|
|
438
|
+
for (const child of body) {
|
|
439
|
+
this.formatViewNode(child);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
this.indent--;
|
|
443
|
+
this.emit('}');
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Format slot element
|
|
448
|
+
*/
|
|
449
|
+
formatSlot(node) {
|
|
450
|
+
const name = node.name;
|
|
451
|
+
const hasFallback = node.fallback && node.fallback.length > 0;
|
|
452
|
+
|
|
453
|
+
if (name === 'default' && !hasFallback) {
|
|
454
|
+
this.emit('slot');
|
|
455
|
+
} else if (!hasFallback) {
|
|
456
|
+
this.emit(`slot "${name}"`);
|
|
457
|
+
} else {
|
|
458
|
+
this.emit(`slot "${name}" {`);
|
|
459
|
+
this.indent++;
|
|
460
|
+
|
|
461
|
+
for (const child of node.fallback) {
|
|
462
|
+
this.formatViewNode(child);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
this.indent--;
|
|
466
|
+
this.emit('}');
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Format actions block
|
|
472
|
+
*/
|
|
473
|
+
formatActionsBlock() {
|
|
474
|
+
this.emit('actions {');
|
|
475
|
+
this.indent++;
|
|
476
|
+
|
|
477
|
+
const functions = this.ast.actions.functions || [];
|
|
478
|
+
for (let i = 0; i < functions.length; i++) {
|
|
479
|
+
const fn = functions[i];
|
|
480
|
+
this.formatFunction(fn);
|
|
481
|
+
|
|
482
|
+
// Add blank line between functions
|
|
483
|
+
if (i < functions.length - 1) {
|
|
484
|
+
this.emit('');
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
this.indent--;
|
|
489
|
+
this.emit('}');
|
|
490
|
+
this.emit('');
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Format a function declaration
|
|
495
|
+
*/
|
|
496
|
+
formatFunction(fn) {
|
|
497
|
+
const async = fn.async ? 'async ' : '';
|
|
498
|
+
const params = (fn.params || []).join(', ');
|
|
499
|
+
const body = fn.body || fn.rawBody || '';
|
|
500
|
+
|
|
501
|
+
// Simple one-liner
|
|
502
|
+
if (!body.includes('\n') && body.length < 50) {
|
|
503
|
+
this.emit(`${async}${fn.name}(${params}) { ${body.trim()} }`);
|
|
504
|
+
} else {
|
|
505
|
+
this.emit(`${async}${fn.name}(${params}) {`);
|
|
506
|
+
this.indent++;
|
|
507
|
+
|
|
508
|
+
// Format body with proper indentation
|
|
509
|
+
const lines = body.split('\n').filter(l => l.trim());
|
|
510
|
+
for (const line of lines) {
|
|
511
|
+
this.emit(line.trim());
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
this.indent--;
|
|
515
|
+
this.emit('}');
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Format style block
|
|
521
|
+
*/
|
|
522
|
+
formatStyleBlock() {
|
|
523
|
+
this.emit('style {');
|
|
524
|
+
this.indent++;
|
|
525
|
+
|
|
526
|
+
const rules = this.ast.style.rules || [];
|
|
527
|
+
for (let i = 0; i < rules.length; i++) {
|
|
528
|
+
this.formatStyleRule(rules[i]);
|
|
529
|
+
|
|
530
|
+
// Add blank line between top-level rules
|
|
531
|
+
if (i < rules.length - 1) {
|
|
532
|
+
this.emit('');
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
this.indent--;
|
|
537
|
+
this.emit('}');
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Format a CSS rule
|
|
542
|
+
*/
|
|
543
|
+
formatStyleRule(rule) {
|
|
544
|
+
const selector = rule.selector || '';
|
|
545
|
+
const properties = rule.properties || [];
|
|
546
|
+
const nestedRules = rule.rules || [];
|
|
547
|
+
|
|
548
|
+
if (properties.length === 0 && nestedRules.length === 0) {
|
|
549
|
+
this.emit(`${selector} {}`);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
this.emit(`${selector} {`);
|
|
554
|
+
this.indent++;
|
|
555
|
+
|
|
556
|
+
// Properties
|
|
557
|
+
for (const prop of properties) {
|
|
558
|
+
this.emit(`${prop.name}: ${prop.value}`);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Nested rules
|
|
562
|
+
for (const nested of nestedRules) {
|
|
563
|
+
if (properties.length > 0) {
|
|
564
|
+
this.emit('');
|
|
565
|
+
}
|
|
566
|
+
this.formatStyleRule(nested);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
this.indent--;
|
|
570
|
+
this.emit('}');
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Emit a line with current indentation
|
|
575
|
+
*/
|
|
576
|
+
emit(text) {
|
|
577
|
+
if (text === '') {
|
|
578
|
+
this.output.push('');
|
|
579
|
+
} else {
|
|
580
|
+
this.output.push(this.getIndent() + text);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Get indentation string
|
|
586
|
+
*/
|
|
587
|
+
getIndent(extra = 0) {
|
|
588
|
+
return ' '.repeat((this.indent + extra) * this.options.indentSize);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Escape string for output
|
|
593
|
+
*/
|
|
594
|
+
escapeString(str) {
|
|
595
|
+
return str
|
|
596
|
+
.replace(/\\/g, '\\\\')
|
|
597
|
+
.replace(/"/g, '\\"')
|
|
598
|
+
.replace(/\n/g, '\\n')
|
|
599
|
+
.replace(/\r/g, '\\r')
|
|
600
|
+
.replace(/\t/g, '\\t');
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Format a single file
|
|
606
|
+
*/
|
|
607
|
+
export async function formatFile(filePath, options = {}) {
|
|
608
|
+
const { parse } = await import('../compiler/index.js');
|
|
609
|
+
|
|
610
|
+
const source = readFileSync(filePath, 'utf-8');
|
|
611
|
+
|
|
612
|
+
// Parse the file
|
|
613
|
+
let ast;
|
|
614
|
+
try {
|
|
615
|
+
ast = parse(source);
|
|
616
|
+
} catch (e) {
|
|
617
|
+
return {
|
|
618
|
+
file: filePath,
|
|
619
|
+
error: e.message,
|
|
620
|
+
formatted: null
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Format
|
|
625
|
+
const formatter = new PulseFormatter(ast, options);
|
|
626
|
+
const formatted = formatter.format();
|
|
627
|
+
|
|
628
|
+
return {
|
|
629
|
+
file: filePath,
|
|
630
|
+
original: source,
|
|
631
|
+
formatted,
|
|
632
|
+
changed: source !== formatted
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Main format command handler
|
|
638
|
+
*/
|
|
639
|
+
export async function runFormat(args) {
|
|
640
|
+
const { options, patterns } = parseArgs(args);
|
|
641
|
+
const check = options.check || false;
|
|
642
|
+
const write = !check; // Default to write unless --check is specified
|
|
643
|
+
|
|
644
|
+
// Find files to format
|
|
645
|
+
const files = findPulseFiles(patterns);
|
|
646
|
+
|
|
647
|
+
if (files.length === 0) {
|
|
648
|
+
console.log('No .pulse files found to format.');
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
console.log(`${check ? 'Checking' : 'Formatting'} ${files.length} file(s)...\n`);
|
|
653
|
+
|
|
654
|
+
let changedCount = 0;
|
|
655
|
+
let errorCount = 0;
|
|
656
|
+
|
|
657
|
+
for (const file of files) {
|
|
658
|
+
const result = await formatFile(file, options);
|
|
659
|
+
const relPath = relativePath(file);
|
|
660
|
+
|
|
661
|
+
if (result.error) {
|
|
662
|
+
console.log(` ${relPath} - ERROR: ${result.error}`);
|
|
663
|
+
errorCount++;
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
if (result.changed) {
|
|
668
|
+
changedCount++;
|
|
669
|
+
|
|
670
|
+
if (check) {
|
|
671
|
+
console.log(` ${relPath} - needs formatting`);
|
|
672
|
+
} else if (write) {
|
|
673
|
+
writeFileSync(file, result.formatted);
|
|
674
|
+
console.log(` ${relPath} - formatted`);
|
|
675
|
+
}
|
|
676
|
+
} else {
|
|
677
|
+
if (!check) {
|
|
678
|
+
console.log(` ${relPath} - unchanged`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Summary
|
|
684
|
+
console.log('\n' + '─'.repeat(60));
|
|
685
|
+
|
|
686
|
+
if (errorCount > 0) {
|
|
687
|
+
console.log(`✗ ${errorCount} file(s) had errors`);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (check) {
|
|
691
|
+
if (changedCount > 0) {
|
|
692
|
+
console.log(`✗ ${changedCount} file(s) need formatting`);
|
|
693
|
+
process.exit(1);
|
|
694
|
+
} else {
|
|
695
|
+
console.log(`✓ All ${files.length} file(s) are properly formatted`);
|
|
696
|
+
}
|
|
697
|
+
} else {
|
|
698
|
+
if (changedCount > 0) {
|
|
699
|
+
console.log(`✓ ${changedCount} file(s) formatted`);
|
|
700
|
+
} else {
|
|
701
|
+
console.log(`✓ All ${files.length} file(s) were already formatted`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|