@ldclabs/kip-lang 0.1.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.
@@ -0,0 +1,678 @@
1
+ import { tokenize } from './lexer.js';
2
+ import { parse } from './parser.js';
3
+ import { TokenType } from './token.js';
4
+ export function format(source, options) {
5
+ const opts = {
6
+ indentSize: options?.indentSize ?? 4,
7
+ sortAttributes: options?.sortAttributes ?? true
8
+ };
9
+ const tokens = tokenize(source);
10
+ const { ast } = parse(source);
11
+ const formatter = new Formatter(opts, tokens);
12
+ return formatter.formatProgram(ast);
13
+ }
14
+ class Formatter {
15
+ opts;
16
+ comments;
17
+ commentIdx = 0;
18
+ output = '';
19
+ indentLevel = 0;
20
+ constructor(opts, tokens) {
21
+ this.opts = opts;
22
+ this.comments = tokens
23
+ .filter((t) => t.type === TokenType.Comment)
24
+ .map((t) => ({ line: t.line, column: t.column, text: t.value }));
25
+ }
26
+ /** Emit all comments whose source line < beforeLine */
27
+ emitCommentsBefore(beforeLine) {
28
+ while (this.commentIdx < this.comments.length &&
29
+ this.comments[this.commentIdx].line < beforeLine) {
30
+ this.writeIndent();
31
+ this.write(this.comments[this.commentIdx].text);
32
+ this.newline();
33
+ this.commentIdx++;
34
+ }
35
+ }
36
+ /** Emit all remaining comments */
37
+ emitRemainingComments() {
38
+ while (this.commentIdx < this.comments.length) {
39
+ this.writeIndent();
40
+ this.write(this.comments[this.commentIdx].text);
41
+ this.newline();
42
+ this.commentIdx++;
43
+ }
44
+ }
45
+ formatProgram(program) {
46
+ this.output = '';
47
+ for (let i = 0; i < program.statements.length; i++) {
48
+ const stmt = program.statements[i];
49
+ if (i > 0)
50
+ this.newline();
51
+ // Emit comments that appear before this statement
52
+ this.emitCommentsBefore(stmt.range.start.line);
53
+ this.formatStatement(stmt);
54
+ this.newline();
55
+ }
56
+ // Trailing comments after last statement
57
+ this.emitRemainingComments();
58
+ return this.output.trimEnd() + '\n';
59
+ }
60
+ // ────────────────────────────────────────────────────────────────────
61
+ // Statements
62
+ // ────────────────────────────────────────────────────────────────────
63
+ formatStatement(stmt) {
64
+ switch (stmt.kind) {
65
+ case 'FindStatement':
66
+ return this.formatFind(stmt);
67
+ case 'UpsertStatement':
68
+ return this.formatUpsert(stmt);
69
+ case 'DeleteStatement':
70
+ return this.formatDelete(stmt);
71
+ case 'DescribeStatement':
72
+ return this.formatDescribe(stmt);
73
+ case 'SearchStatement':
74
+ return this.formatSearch(stmt);
75
+ }
76
+ }
77
+ // ── FIND ───────────────────────────────────────────────────────────
78
+ formatFind(stmt) {
79
+ this.writeIndent();
80
+ this.write('FIND(');
81
+ if (stmt.projections.length <= 2 &&
82
+ this.allSimpleExpressions(stmt.projections)) {
83
+ // Inline
84
+ this.write(stmt.projections.map((p) => this.exprToString(p, 0)).join(', '));
85
+ }
86
+ else {
87
+ this.newline();
88
+ this.indentLevel++;
89
+ for (let i = 0; i < stmt.projections.length; i++) {
90
+ this.writeIndent();
91
+ this.write(this.exprToString(stmt.projections[i], 0));
92
+ if (i < stmt.projections.length - 1) {
93
+ this.write(',');
94
+ }
95
+ this.newline();
96
+ }
97
+ this.indentLevel--;
98
+ this.writeIndent();
99
+ }
100
+ this.write(')');
101
+ this.newline();
102
+ if (stmt.where) {
103
+ this.formatWhere(stmt.where);
104
+ }
105
+ if (stmt.orderBy) {
106
+ this.formatOrderBy(stmt.orderBy);
107
+ }
108
+ if (stmt.limit) {
109
+ this.formatLimit(stmt.limit);
110
+ }
111
+ if (stmt.cursor) {
112
+ this.formatCursor(stmt.cursor);
113
+ }
114
+ }
115
+ // ── UPSERT ─────────────────────────────────────────────────────────
116
+ formatUpsert(stmt) {
117
+ this.writeIndent();
118
+ this.write('UPSERT {');
119
+ this.newline();
120
+ this.indentLevel++;
121
+ for (let i = 0; i < stmt.blocks.length; i++) {
122
+ const block = stmt.blocks[i];
123
+ if (i > 0)
124
+ this.newline();
125
+ // Emit comments between blocks
126
+ this.emitCommentsBefore(block.range.start.line);
127
+ this.formatUpsertBlock(block);
128
+ }
129
+ this.indentLevel--;
130
+ this.writeIndent();
131
+ this.write('}');
132
+ this.newline();
133
+ if (stmt.metadata) {
134
+ this.formatWithMetadata(stmt.metadata);
135
+ }
136
+ }
137
+ formatUpsertBlock(block) {
138
+ if (block.kind === 'ConceptBlock') {
139
+ this.formatConceptBlock(block);
140
+ }
141
+ else {
142
+ this.formatPropositionBlockDef(block);
143
+ }
144
+ }
145
+ formatConceptBlock(block) {
146
+ this.writeIndent();
147
+ this.write(`CONCEPT ${block.handle} {`);
148
+ this.newline();
149
+ this.indentLevel++;
150
+ // Matcher: {type: "...", name: "..."}
151
+ this.writeIndent();
152
+ this.write('{');
153
+ this.write(block.matcher.entries
154
+ .map((e) => `${this.keyToString(e)}: ${this.exprToString(e.value, 0)}`)
155
+ .join(', '));
156
+ this.write('}');
157
+ this.newline();
158
+ if (block.setAttributes) {
159
+ this.formatSetAttributes(block.setAttributes);
160
+ }
161
+ if (block.setPropositions) {
162
+ this.formatSetPropositions(block.setPropositions);
163
+ }
164
+ this.indentLevel--;
165
+ this.writeIndent();
166
+ this.write('}');
167
+ this.newline();
168
+ if (block.metadata) {
169
+ this.formatWithMetadata(block.metadata);
170
+ }
171
+ }
172
+ formatPropositionBlockDef(block) {
173
+ this.writeIndent();
174
+ this.write('PROPOSITION');
175
+ if (block.handle)
176
+ this.write(` ${block.handle}`);
177
+ this.write(' {');
178
+ this.newline();
179
+ this.indentLevel++;
180
+ this.writeIndent();
181
+ this.write(`(${this.endpointToString(block.subject)}, ${this.predicateToString(block.predicate)}, ${this.endpointToString(block.object)})`);
182
+ this.newline();
183
+ if (block.setAttributes) {
184
+ this.formatSetAttributes(block.setAttributes);
185
+ }
186
+ this.indentLevel--;
187
+ this.writeIndent();
188
+ this.write('}');
189
+ this.newline();
190
+ if (block.metadata) {
191
+ this.formatWithMetadata(block.metadata);
192
+ }
193
+ }
194
+ formatSetAttributes(sa) {
195
+ this.writeIndent();
196
+ this.write('SET ATTRIBUTES {');
197
+ const entries = this.opts.sortAttributes
198
+ ? this.sortObjectEntries(sa.entries)
199
+ : sa.entries;
200
+ if (entries.length === 0) {
201
+ this.write('}');
202
+ this.newline();
203
+ return;
204
+ }
205
+ // Check if all values are simple (single-line)
206
+ const allSimple = entries.every((e) => this.isSimpleValue(e.value));
207
+ if (allSimple && entries.length <= 3) {
208
+ // Inline format for very simple cases
209
+ this.write(' ');
210
+ this.write(entries
211
+ .map((e) => `${this.keyToString(e)}: ${this.exprToString(e.value, 0)}`)
212
+ .join(', '));
213
+ this.write(' }');
214
+ this.newline();
215
+ return;
216
+ }
217
+ this.newline();
218
+ this.indentLevel++;
219
+ for (let i = 0; i < entries.length; i++) {
220
+ this.formatObjectEntry(entries[i]);
221
+ if (i < entries.length - 1) {
222
+ this.write(',');
223
+ }
224
+ this.newline();
225
+ }
226
+ this.indentLevel--;
227
+ this.writeIndent();
228
+ this.write('}');
229
+ this.newline();
230
+ }
231
+ formatSetPropositions(sp) {
232
+ this.writeIndent();
233
+ this.write('SET PROPOSITIONS {');
234
+ this.newline();
235
+ this.indentLevel++;
236
+ for (const item of sp.items) {
237
+ this.formatPropositionItem(item);
238
+ }
239
+ this.indentLevel--;
240
+ this.writeIndent();
241
+ this.write('}');
242
+ this.newline();
243
+ }
244
+ formatPropositionItem(item) {
245
+ this.writeIndent();
246
+ this.write(`("${this.escapeString(item.predicate)}", ${this.endpointToString(item.target)})`);
247
+ if (item.metadata) {
248
+ this.write(' ');
249
+ this.write('WITH METADATA { ');
250
+ this.write(item.metadata.entries
251
+ .map((e) => `${this.keyToString(e)}: ${this.exprToString(e.value, 0)}`)
252
+ .join(', '));
253
+ this.write(' }');
254
+ }
255
+ this.newline();
256
+ }
257
+ formatWithMetadata(wm) {
258
+ this.writeIndent();
259
+ this.write('WITH METADATA {');
260
+ if (wm.entries.length === 0) {
261
+ this.write('}');
262
+ this.newline();
263
+ return;
264
+ }
265
+ const allSimple = wm.entries.every((e) => this.isSimpleValue(e.value));
266
+ if (allSimple && wm.entries.length <= 5) {
267
+ // Inline
268
+ this.newline();
269
+ this.indentLevel++;
270
+ for (let i = 0; i < wm.entries.length; i++) {
271
+ this.writeIndent();
272
+ this.write(`${this.keyToString(wm.entries[i])}: ${this.exprToString(wm.entries[i].value, 0)}`);
273
+ if (i < wm.entries.length - 1) {
274
+ this.write(',');
275
+ }
276
+ this.newline();
277
+ }
278
+ this.indentLevel--;
279
+ this.writeIndent();
280
+ this.write('}');
281
+ this.newline();
282
+ return;
283
+ }
284
+ this.newline();
285
+ this.indentLevel++;
286
+ for (let i = 0; i < wm.entries.length; i++) {
287
+ this.formatObjectEntry(wm.entries[i]);
288
+ if (i < wm.entries.length - 1) {
289
+ this.write(',');
290
+ }
291
+ this.newline();
292
+ }
293
+ this.indentLevel--;
294
+ this.writeIndent();
295
+ this.write('}');
296
+ this.newline();
297
+ }
298
+ // ── DELETE ─────────────────────────────────────────────────────────
299
+ formatDelete(stmt) {
300
+ this.writeIndent();
301
+ if (stmt.deleteType === 'ATTRIBUTES' || stmt.deleteType === 'METADATA') {
302
+ this.write(`DELETE ${stmt.deleteType} {`);
303
+ if (stmt.keys) {
304
+ this.write(stmt.keys.map((k) => `"${this.escapeString(k)}"`).join(', '));
305
+ }
306
+ this.write(`} FROM ${stmt.target}`);
307
+ }
308
+ else if (stmt.deleteType === 'PROPOSITIONS') {
309
+ this.write(`DELETE PROPOSITIONS ${stmt.target}`);
310
+ }
311
+ else {
312
+ this.write(`DELETE CONCEPT ${stmt.target}`);
313
+ if (stmt.detach)
314
+ this.write(' DETACH');
315
+ }
316
+ this.newline();
317
+ this.formatWhere(stmt.where);
318
+ }
319
+ // ── DESCRIBE ───────────────────────────────────────────────────────
320
+ formatDescribe(stmt) {
321
+ this.writeIndent();
322
+ switch (stmt.describeType) {
323
+ case 'PRIMER':
324
+ this.write('DESCRIBE PRIMER');
325
+ break;
326
+ case 'DOMAINS':
327
+ this.write('DESCRIBE DOMAINS');
328
+ break;
329
+ case 'CONCEPT_TYPES':
330
+ this.write('DESCRIBE CONCEPT TYPES');
331
+ break;
332
+ case 'CONCEPT_TYPE':
333
+ this.write(`DESCRIBE CONCEPT TYPE "${this.escapeString(stmt.typeName ?? '')}"`);
334
+ break;
335
+ case 'PROPOSITION_TYPES':
336
+ this.write('DESCRIBE PROPOSITION TYPES');
337
+ break;
338
+ case 'PROPOSITION_TYPE':
339
+ this.write(`DESCRIBE PROPOSITION TYPE "${this.escapeString(stmt.typeName ?? '')}"`);
340
+ break;
341
+ }
342
+ if (stmt.limit) {
343
+ this.write(` LIMIT ${this.limitValueToString(stmt.limit)}`);
344
+ }
345
+ if (stmt.cursor) {
346
+ this.write(` CURSOR ${this.cursorValueToString(stmt.cursor)}`);
347
+ }
348
+ }
349
+ // ── SEARCH ─────────────────────────────────────────────────────────
350
+ formatSearch(stmt) {
351
+ this.writeIndent();
352
+ this.write(`SEARCH ${stmt.searchTarget} "${this.escapeString(stmt.term)}"`);
353
+ if (stmt.withType) {
354
+ this.write(` WITH TYPE "${this.escapeString(stmt.withType)}"`);
355
+ }
356
+ if (stmt.limit) {
357
+ this.write(` LIMIT ${this.limitValueToString(stmt.limit)}`);
358
+ }
359
+ }
360
+ // ── WHERE ──────────────────────────────────────────────────────────
361
+ formatWhere(where) {
362
+ this.writeIndent();
363
+ this.write('WHERE {');
364
+ this.newline();
365
+ this.indentLevel++;
366
+ for (const p of where.patterns) {
367
+ this.formatWherePattern(p);
368
+ }
369
+ this.indentLevel--;
370
+ this.writeIndent();
371
+ this.write('}');
372
+ this.newline();
373
+ }
374
+ formatWherePattern(p) {
375
+ switch (p.kind) {
376
+ case 'ConceptPattern':
377
+ return this.formatConceptPattern(p);
378
+ case 'PropositionPattern':
379
+ return this.formatPropositionPattern(p);
380
+ case 'FilterClause':
381
+ return this.formatFilterClause(p);
382
+ case 'NotClause':
383
+ return this.formatNotClause(p);
384
+ case 'OptionalClause':
385
+ return this.formatOptionalClause(p);
386
+ case 'UnionClause':
387
+ return this.formatUnionClause(p);
388
+ }
389
+ }
390
+ formatConceptPattern(p) {
391
+ this.writeIndent();
392
+ if (p.variable)
393
+ this.write(`${p.variable} `);
394
+ this.write('{');
395
+ this.write(p.matcher.entries
396
+ .map((e) => `${this.keyToString(e)}: ${this.exprToString(e.value, 0)}`)
397
+ .join(', '));
398
+ this.write('}');
399
+ this.newline();
400
+ }
401
+ formatPropositionPattern(p) {
402
+ this.writeIndent();
403
+ if (p.variable)
404
+ this.write(`${p.variable} `);
405
+ this.write(`(${this.endpointToString(p.subject)}, ${this.predicateToString(p.predicate)}, ${this.endpointToString(p.object)})`);
406
+ this.newline();
407
+ }
408
+ formatFilterClause(f) {
409
+ this.writeIndent();
410
+ this.write(`FILTER(${this.exprToString(f.expression, 0)})`);
411
+ this.newline();
412
+ }
413
+ formatNotClause(n) {
414
+ this.writeIndent();
415
+ this.write('NOT {');
416
+ this.newline();
417
+ this.indentLevel++;
418
+ for (const p of n.patterns) {
419
+ this.formatWherePattern(p);
420
+ }
421
+ this.indentLevel--;
422
+ this.writeIndent();
423
+ this.write('}');
424
+ this.newline();
425
+ }
426
+ formatOptionalClause(o) {
427
+ this.newline();
428
+ this.writeIndent();
429
+ this.write('OPTIONAL {');
430
+ this.newline();
431
+ this.indentLevel++;
432
+ for (const p of o.patterns) {
433
+ this.formatWherePattern(p);
434
+ }
435
+ this.indentLevel--;
436
+ this.writeIndent();
437
+ this.write('}');
438
+ this.newline();
439
+ }
440
+ formatUnionClause(u) {
441
+ this.newline();
442
+ this.writeIndent();
443
+ this.write('UNION {');
444
+ this.newline();
445
+ this.indentLevel++;
446
+ for (const p of u.patterns) {
447
+ this.formatWherePattern(p);
448
+ }
449
+ this.indentLevel--;
450
+ this.writeIndent();
451
+ this.write('}');
452
+ this.newline();
453
+ }
454
+ formatOrderBy(ob) {
455
+ this.writeIndent();
456
+ this.write(`ORDER BY ${this.exprToString(ob.expression, 0)} ${ob.direction}`);
457
+ this.newline();
458
+ }
459
+ formatLimit(lim) {
460
+ this.writeIndent();
461
+ this.write(`LIMIT ${this.limitValueToString(lim)}`);
462
+ this.newline();
463
+ }
464
+ formatCursor(cur) {
465
+ this.writeIndent();
466
+ this.write(`CURSOR ${this.cursorValueToString(cur)}`);
467
+ this.newline();
468
+ }
469
+ // ────────────────────────────────────────────────────────────────────
470
+ // Expression → string (depth-aware for nested indentation)
471
+ // ────────────────────────────────────────────────────────────────────
472
+ exprToString(expr, depth) {
473
+ switch (expr.kind) {
474
+ case 'VariableRef':
475
+ return expr.name;
476
+ case 'ParameterRef':
477
+ return expr.name;
478
+ case 'DotExpression':
479
+ return `${this.exprToString(expr.object, depth)}.${expr.property}`;
480
+ case 'StringLiteral':
481
+ return expr.value;
482
+ case 'NumberLiteral':
483
+ return expr.raw;
484
+ case 'BooleanLiteral':
485
+ return String(expr.value);
486
+ case 'NullLiteral':
487
+ return 'null';
488
+ case 'ArrayLiteral':
489
+ return this.arrayToString(expr, depth);
490
+ case 'ObjectLiteral':
491
+ return this.objectToString(expr, depth);
492
+ case 'BinaryExpression':
493
+ return `${this.exprToString(expr.left, depth)} ${expr.operator} ${this.exprToString(expr.right, depth)}`;
494
+ case 'UnaryExpression':
495
+ return `${expr.operator}${this.exprToString(expr.operand, depth)}`;
496
+ case 'FunctionCallExpr':
497
+ return `${expr.name}(${expr.args.map((a) => this.exprToString(a, depth)).join(', ')})`;
498
+ }
499
+ }
500
+ arrayToString(arr, depth) {
501
+ if (arr.elements.length === 0)
502
+ return '[]';
503
+ const inner = arr.elements.map((e) => this.exprToString(e, depth + 1));
504
+ const singleLine = `[${inner.join(', ')}]`;
505
+ if (singleLine.length <= 80 && !inner.some((s) => s.includes('\n'))) {
506
+ return singleLine;
507
+ }
508
+ const baseIndent = this.indentAt(this.indentLevel + depth);
509
+ const innerIndent = this.indentAt(this.indentLevel + depth + 1);
510
+ return `[\n${inner.map((s) => `${innerIndent}${s}`).join(',\n')}\n${baseIndent}]`;
511
+ }
512
+ objectToString(obj, depth) {
513
+ if (obj.entries.length === 0)
514
+ return '{}';
515
+ const entries = obj.entries.map((e) => `${this.keyToString(e)}: ${this.exprToString(e.value, depth + 1)}`);
516
+ const singleLine = `{${entries.join(', ')}}`;
517
+ if (singleLine.length <= 80 && !entries.some((s) => s.includes('\n'))) {
518
+ return singleLine;
519
+ }
520
+ const baseIndent = this.indentAt(this.indentLevel + depth);
521
+ const innerIndent = this.indentAt(this.indentLevel + depth + 1);
522
+ return `{\n${entries.map((s) => `${innerIndent}${s}`).join(',\n')}\n${baseIndent}}`;
523
+ }
524
+ formatObjectEntry(entry) {
525
+ this.writeIndent();
526
+ const valStr = this.exprToString(entry.value, 0);
527
+ this.write(`${this.keyToString(entry)}: ${valStr}`);
528
+ }
529
+ // ────────────────────────────────────────────────────────────────────
530
+ // Helpers
531
+ // ────────────────────────────────────────────────────────────────────
532
+ keyToString(entry) {
533
+ if (entry.isQuoted) {
534
+ return `"${this.escapeString(entry.key)}"`;
535
+ }
536
+ return entry.key;
537
+ }
538
+ endpointToString(ep) {
539
+ switch (ep.kind) {
540
+ case 'VariableRef':
541
+ return ep.name;
542
+ case 'ConceptPattern': {
543
+ const entries = ep.matcher.entries
544
+ .map((e) => `${this.keyToString(e)}: ${this.exprToString(e.value, 0)}`)
545
+ .join(', ');
546
+ return `{${entries}}`;
547
+ }
548
+ case 'PropositionPattern': {
549
+ const s = this.endpointToString(ep.subject);
550
+ const p = this.predicateToString(ep.predicate);
551
+ const o = this.endpointToString(ep.object);
552
+ return `(${s}, ${p}, ${o})`;
553
+ }
554
+ default:
555
+ return '?unknown';
556
+ }
557
+ }
558
+ predicateToString(pred) {
559
+ if (pred.kind === 'PredicateLiteral') {
560
+ let s = `"${this.escapeString(pred.value)}"`;
561
+ if (pred.hopRange) {
562
+ if (pred.hopRange.max === undefined) {
563
+ s += `{${pred.hopRange.min},}`;
564
+ }
565
+ else if (pred.hopRange.max === pred.hopRange.min) {
566
+ s += `{${pred.hopRange.min}}`;
567
+ }
568
+ else {
569
+ s += `{${pred.hopRange.min},${pred.hopRange.max}}`;
570
+ }
571
+ }
572
+ return s;
573
+ }
574
+ // Alternation
575
+ return pred.predicates.map((p) => this.predicateToString(p)).join(' | ');
576
+ }
577
+ limitValueToString(lim) {
578
+ if (lim.value.kind === 'NumberLiteral')
579
+ return lim.value.raw;
580
+ return lim.value.name;
581
+ }
582
+ cursorValueToString(cur) {
583
+ if (cur.value.kind === 'StringLiteral')
584
+ return cur.value.value;
585
+ return cur.value.name;
586
+ }
587
+ sortObjectEntries(entries) {
588
+ // Sort: required-looking fields first, then alphabetical
589
+ return [...entries].sort((a, b) => a.key.localeCompare(b.key));
590
+ }
591
+ isSimpleValue(expr) {
592
+ return (expr.kind === 'StringLiteral' ||
593
+ expr.kind === 'NumberLiteral' ||
594
+ expr.kind === 'BooleanLiteral' ||
595
+ expr.kind === 'NullLiteral' ||
596
+ expr.kind === 'VariableRef' ||
597
+ expr.kind === 'ParameterRef');
598
+ }
599
+ allSimpleExpressions(exprs) {
600
+ return exprs.every((e) => e.kind === 'VariableRef' ||
601
+ e.kind === 'DotExpression' ||
602
+ e.kind === 'FunctionCallExpr');
603
+ }
604
+ formatLeadingComments(comments) {
605
+ if (!comments)
606
+ return;
607
+ for (const c of comments) {
608
+ this.writeIndent();
609
+ this.write(c);
610
+ this.newline();
611
+ }
612
+ }
613
+ escapeString(s) {
614
+ return s
615
+ .replace(/\\/g, '\\\\')
616
+ .replace(/"/g, '\\"')
617
+ .replace(/\n/g, '\\n')
618
+ .replace(/\t/g, '\\t')
619
+ .replace(/\r/g, '\\r');
620
+ }
621
+ indentAt(level) {
622
+ return ' '.repeat(level * this.opts.indentSize);
623
+ }
624
+ indent() {
625
+ return ' '.repeat(this.indentLevel * this.opts.indentSize);
626
+ }
627
+ write(s) {
628
+ this.output += s;
629
+ }
630
+ writeIndent() {
631
+ this.output += this.indent();
632
+ }
633
+ newline() {
634
+ this.output += '\n';
635
+ }
636
+ }
637
+ /**
638
+ * A simpler comment-preserving formatter that works at the token level.
639
+ * It re-parses the source and reconstructs it line by line, preserving
640
+ * all comments in their relative positions while normalizing indentation.
641
+ */
642
+ export function formatPreservingComments(source, options) {
643
+ const opts = {
644
+ indentSize: options?.indentSize ?? 4,
645
+ sortAttributes: options?.sortAttributes ?? true
646
+ };
647
+ const tokens = tokenize(source);
648
+ const lines = source.split('\n');
649
+ const result = [];
650
+ let indent = 0;
651
+ const indentStr = ' '.repeat(opts.indentSize);
652
+ for (let i = 0; i < lines.length; i++) {
653
+ const trimmed = lines[i].trim();
654
+ if (trimmed === '') {
655
+ result.push('');
656
+ continue;
657
+ }
658
+ // Decrease indent before closing braces/parens
659
+ if (/^\s*[}\])]+/.test(trimmed)) {
660
+ const closers = trimmed.match(/^[}\])]+/);
661
+ if (closers) {
662
+ indent = Math.max(0, indent - closers[0].length);
663
+ }
664
+ }
665
+ result.push(indentStr.repeat(indent) + trimmed);
666
+ // Increase indent after opening braces
667
+ const opens = (trimmed.match(/[{([\[]/g) || []).length;
668
+ const closes = (trimmed.match(/[})\]]/g) || []).length;
669
+ indent = Math.max(0, indent + opens - closes);
670
+ }
671
+ // Remove trailing blank lines, ensure single trailing newline
672
+ while (result.length > 0 && result[result.length - 1].trim() === '') {
673
+ result.pop();
674
+ }
675
+ result.push('');
676
+ return result.join('\n');
677
+ }
678
+ //# sourceMappingURL=formatter.js.map