@purestack/ts-format 0.0.0-stage → 1.1.25

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/dist/files.mjs ADDED
@@ -0,0 +1,1076 @@
1
+ /*! @purestack/ts-format v1.1.25 | (c) 2026 Ahmed Yasin Koculu | MIT License */
2
+
3
+ import { execFile } from "node:child_process";
4
+ import { lstat, readFile, readdir, writeFile } from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { promisify } from "node:util";
7
+ import htmlPlugin from "prettier/plugins/html";
8
+ import postcssPlugin from "prettier/plugins/postcss";
9
+ import prettier from "prettier/standalone";
10
+ import ts from "typescript-parser";
11
+
12
+ //#region packages/ts-format/src/markup.ts
13
+ const VOID_HTML_TAG_NAMES = /* @__PURE__ */ new Set([
14
+ "area",
15
+ "base",
16
+ "br",
17
+ "col",
18
+ "embed",
19
+ "hr",
20
+ "img",
21
+ "input",
22
+ "link",
23
+ "meta",
24
+ "param",
25
+ "source",
26
+ "track",
27
+ "wbr"
28
+ ]);
29
+ function scanTagTokens(markup) {
30
+ const tokens = [];
31
+ for (let index = 0; index < markup.length; index++) {
32
+ if (markup[index] !== "<") continue;
33
+ if (markup.startsWith("<!--", index)) {
34
+ const commentEnd = markup.indexOf("-->", index + 4);
35
+ if (commentEnd === -1) break;
36
+ index = commentEnd + 2;
37
+ continue;
38
+ }
39
+ const isClosing = markup[index + 1] === "/";
40
+ const nameStart = skipWhitespace(markup, index + (isClosing ? 2 : 1));
41
+ const nameEnd = readTagNameEnd(markup, nameStart);
42
+ if (nameEnd === nameStart) continue;
43
+ const tagEnd = findTagEnd(markup, nameEnd);
44
+ if (tagEnd === -1) break;
45
+ const name = markup.slice(nameStart, nameEnd);
46
+ const trailing = markup.slice(nameEnd, tagEnd);
47
+ tokens.push({
48
+ start: index,
49
+ end: tagEnd + 1,
50
+ kind: isClosing ? "closing" : "opening",
51
+ name,
52
+ nameStart,
53
+ nameEnd,
54
+ selfClosing: !isClosing && /\/\s*$/.test(trailing)
55
+ });
56
+ index = tagEnd;
57
+ }
58
+ return tokens;
59
+ }
60
+ function isMdxExpressionStart(source, index) {
61
+ return source[index] === "{" && source[index - 1] !== "{" && source[index + 1] !== "{";
62
+ }
63
+ function findMdxExpressionEnd(source, startIndex) {
64
+ let braceDepth = 0;
65
+ let quote;
66
+ for (let index = startIndex; index < source.length; index++) {
67
+ const current = source[index];
68
+ const previous = source[index - 1];
69
+ if (quote) {
70
+ if (current === quote && previous !== "\\") quote = void 0;
71
+ else if (quote === "`" && current === "$" && source[index + 1] === "{") {
72
+ const templateExpressionEnd = findMdxExpressionEnd(source, index + 1);
73
+ if (templateExpressionEnd === -1) return -1;
74
+ index = templateExpressionEnd - 1;
75
+ }
76
+ continue;
77
+ }
78
+ if (current === "\"" || current === "'" || current === "`") {
79
+ quote = current;
80
+ continue;
81
+ }
82
+ if (current === "{") {
83
+ braceDepth++;
84
+ continue;
85
+ }
86
+ if (current !== "}") continue;
87
+ braceDepth--;
88
+ if (braceDepth === 0) return index + 1;
89
+ if (braceDepth < 0) return -1;
90
+ }
91
+ return -1;
92
+ }
93
+ function findTagEnd(markup, startIndex) {
94
+ let quote;
95
+ for (let index = startIndex; index < markup.length; index++) {
96
+ const current = markup[index];
97
+ if (quote) {
98
+ if (current === quote && markup[index - 1] !== "\\") quote = void 0;
99
+ continue;
100
+ }
101
+ if (current === "\"" || current === "'") {
102
+ quote = current;
103
+ continue;
104
+ }
105
+ if (current === ">") return index;
106
+ }
107
+ return -1;
108
+ }
109
+ function skipWhitespace(markup, startIndex) {
110
+ let index = startIndex;
111
+ while (index < markup.length && /\s/.test(markup[index])) index++;
112
+ return index;
113
+ }
114
+ function readTagNameEnd(markup, startIndex) {
115
+ let index = startIndex;
116
+ while (index < markup.length && /[A-Za-z0-9._:$-]/.test(markup[index])) index++;
117
+ return index;
118
+ }
119
+
120
+ //#endregion
121
+ //#region packages/ts-format/src/expressions.ts
122
+ /**
123
+ * Protects Regor {{ ... }} expressions while the surrounding markup is
124
+ * formatted as HTML. The expression source is restored byte-for-byte.
125
+ */
126
+ function maskRegorExpressions(source) {
127
+ const expressions = [];
128
+ let placeholderContent = "";
129
+ let cursor = 0;
130
+ while (cursor < source.length) {
131
+ const start = source.indexOf("{{", cursor);
132
+ if (start === -1) {
133
+ placeholderContent += source.slice(cursor);
134
+ break;
135
+ }
136
+ placeholderContent += source.slice(cursor, start);
137
+ const end = findRegorExpressionEnd(source, start);
138
+ if (end === -1) {
139
+ placeholderContent += source.slice(start);
140
+ break;
141
+ }
142
+ const placeholder = createPlaceholder(source, expressions.length);
143
+ expressions.push({
144
+ placeholder,
145
+ source: source.slice(start, end)
146
+ });
147
+ placeholderContent += placeholder;
148
+ cursor = end;
149
+ }
150
+ return {
151
+ expressions,
152
+ placeholderContent
153
+ };
154
+ }
155
+ function restoreRegorExpressions(formatted, expressions) {
156
+ let restored = formatted;
157
+ for (const expression of expressions) restored = restored.split(expression.placeholder).join(expression.source);
158
+ return restored;
159
+ }
160
+ function findRegorExpressionEnd(source, start) {
161
+ const end = findMdxExpressionEnd(source, start);
162
+ if (end === -1) return -1;
163
+ return source.slice(end - 2, end) === "}}" ? end : -1;
164
+ }
165
+ function createPlaceholder(source, index) {
166
+ let placeholder = `__PURESTACK_REGOR_EXPR_${index}__`;
167
+ while (source.includes(placeholder)) placeholder = `_${placeholder}_`;
168
+ return placeholder;
169
+ }
170
+
171
+ //#endregion
172
+ //#region packages/ts-format/src/source.ts
173
+ function intersects(left, right) {
174
+ return left.start <= right.end && right.start <= left.end;
175
+ }
176
+ /** Immutable source snapshot. All edit offsets refer to this text. */
177
+ var SourceText = class {
178
+ text;
179
+ filePath;
180
+ options;
181
+ diagnostics = [];
182
+ lines = [];
183
+ eol;
184
+ constructor(text, options = {}) {
185
+ this.text = text;
186
+ this.filePath = options.filePath ?? "source.mdx";
187
+ this.options = {
188
+ printWidth: options.printWidth ?? 80,
189
+ tabSize: options.tabSize ?? 2,
190
+ useTabs: options.useTabs ?? false
191
+ };
192
+ for (const value of [this.options.printWidth, this.options.tabSize]) if (!Number.isInteger(value) || value < 1) throw new Error("Print width and tab size must be positive integers.");
193
+ this.eol = /\r\n/.test(text) ? "\r\n" : "\n";
194
+ let start = 0;
195
+ for (const line of text.split("\n")) {
196
+ const content = line.endsWith("\r") ? line.slice(0, -1) : line;
197
+ this.lines.push({
198
+ text: content,
199
+ range: {
200
+ start,
201
+ end: start + content.length
202
+ }
203
+ });
204
+ start += line.length + 1;
205
+ }
206
+ }
207
+ get lineCount() {
208
+ return this.lines.length;
209
+ }
210
+ lineAt(line) {
211
+ return this.lines[line];
212
+ }
213
+ positionAt(offset) {
214
+ const bounded = Math.max(0, Math.min(offset, this.text.length));
215
+ let low = 0;
216
+ let high = this.lines.length - 1;
217
+ while (low < high) {
218
+ const middle = Math.ceil((low + high) / 2);
219
+ if (this.lines[middle].range.start > bounded) high = middle - 1;
220
+ else low = middle;
221
+ }
222
+ const line = this.lines[low];
223
+ return {
224
+ line: low,
225
+ character: Math.min(bounded, line.range.end) - line.range.start
226
+ };
227
+ }
228
+ offsetAt(position) {
229
+ const line = this.lines[Math.max(0, Math.min(position.line, this.lines.length - 1))];
230
+ return Math.min(line.range.end, line.range.start + Math.max(0, position.character));
231
+ }
232
+ preserveEol(text) {
233
+ return text.replace(/\r?\n/g, this.eol);
234
+ }
235
+ report(range, error) {
236
+ this.diagnostics.push({
237
+ ...range,
238
+ message: error instanceof Error ? error.message : String(error)
239
+ });
240
+ }
241
+ };
242
+ /** Apply non-overlapping edits against their original snapshot. */
243
+ function applySourceEdits(source, edits) {
244
+ const ordered = [...edits].sort((a, b) => a.start - b.start || a.end - b.end);
245
+ let cursor = 0;
246
+ const parts = [];
247
+ for (const edit of ordered) {
248
+ if (edit.start < cursor || edit.end < edit.start || edit.end > source.length) throw new Error("Formatter returned overlapping or invalid source edits.");
249
+ parts.push(source.slice(cursor, edit.start), edit.newText);
250
+ cursor = edit.end;
251
+ }
252
+ parts.push(source.slice(cursor));
253
+ return parts.join("");
254
+ }
255
+
256
+ //#endregion
257
+ //#region packages/ts-format/src/fences.ts
258
+ function buildMdxFenceDelimiterEdits(document, excludedRanges, options = {}) {
259
+ const edits = [];
260
+ let activeFence;
261
+ let frontmatterHandled = false;
262
+ let inFrontmatter = false;
263
+ for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex++) {
264
+ const line = document.lineAt(lineIndex);
265
+ const lineText = line.text;
266
+ const trimmed = lineText.trim();
267
+ if (!frontmatterHandled && lineIndex === 0 && trimmed === "---") {
268
+ frontmatterHandled = true;
269
+ inFrontmatter = true;
270
+ continue;
271
+ }
272
+ if (inFrontmatter) {
273
+ if (trimmed === "---") inFrontmatter = false;
274
+ continue;
275
+ }
276
+ if (isLineInsideAnyRange(line.range, excludedRanges)) continue;
277
+ if (options.onlyWithinRange && !intersects(line.range, options.onlyWithinRange)) continue;
278
+ if (!activeFence) {
279
+ const splitLine = splitLineBeforeInlineMdxFenceOpener(lineText);
280
+ if (splitLine) {
281
+ const replacement = [...splitAdjacentMarkupTags(splitLine.before), splitLine.fenceLine].join("\n");
282
+ pushLineReplacement(edits, line.range, lineText, replacement);
283
+ const delimiter = parseMdxFenceDelimiter(splitLine.fenceLine.trim());
284
+ activeFence = delimiter ? {
285
+ length: delimiter.length,
286
+ marker: delimiter.marker
287
+ } : void 0;
288
+ continue;
289
+ }
290
+ const delimiter = parseMdxFenceDelimiter(trimmed);
291
+ if (!delimiter) continue;
292
+ pushLineReplacement(edits, line.range, lineText, trimLineStartIndent(lineText));
293
+ activeFence = {
294
+ length: delimiter.length,
295
+ marker: delimiter.marker
296
+ };
297
+ continue;
298
+ }
299
+ const splitClosingPrefix = splitLineAfterInlineMdxFenceCloser(lineText, activeFence);
300
+ if (splitClosingPrefix) {
301
+ const replacement = [trimLineStartIndent(splitClosingPrefix.fenceLine), splitClosingPrefix.after].join("\n");
302
+ pushLineReplacement(edits, line.range, lineText, replacement);
303
+ activeFence = void 0;
304
+ continue;
305
+ }
306
+ const splitClosingSuffix = splitLineBeforeInlineMdxFenceCloser(lineText, activeFence);
307
+ if (splitClosingSuffix) {
308
+ const replacement = [splitClosingSuffix.before, trimLineStartIndent(splitClosingSuffix.fenceLine)].join("\n");
309
+ pushLineReplacement(edits, line.range, lineText, replacement);
310
+ activeFence = void 0;
311
+ continue;
312
+ }
313
+ const delimiter = parseMdxFenceDelimiter(trimmed);
314
+ if (!delimiter || !isMdxFenceClosingDelimiter(delimiter, activeFence)) continue;
315
+ pushLineReplacement(edits, line.range, lineText, trimLineStartIndent(lineText));
316
+ activeFence = void 0;
317
+ }
318
+ return edits.map((edit) => ({
319
+ ...edit,
320
+ newText: document.preserveEol(edit.newText)
321
+ }));
322
+ }
323
+ function maskMdxFencedRegions(source) {
324
+ const fences = [];
325
+ const lines = splitInlineMdxFenceDelimiters(source).split("\n");
326
+ const placeholderLines = [];
327
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
328
+ const openingDelimiter = parseMdxFenceDelimiter(lines[lineIndex].trim());
329
+ if (!openingDelimiter) {
330
+ placeholderLines.push(lines[lineIndex]);
331
+ continue;
332
+ }
333
+ let closingLineIndex = -1;
334
+ for (let candidateLineIndex = lineIndex + 1; candidateLineIndex < lines.length; candidateLineIndex++) {
335
+ const closingDelimiter = parseMdxFenceDelimiter(lines[candidateLineIndex].trim());
336
+ if (closingDelimiter && isMdxFenceClosingDelimiter(closingDelimiter, openingDelimiter)) {
337
+ closingLineIndex = candidateLineIndex;
338
+ break;
339
+ }
340
+ }
341
+ if (closingLineIndex === -1) {
342
+ placeholderLines.push(lines[lineIndex]);
343
+ continue;
344
+ }
345
+ const placeholder = `<!--PURESTACK_MDX_FENCE_${fences.length}-->`;
346
+ fences.push({
347
+ placeholder,
348
+ source: normalizeMdxFenceDelimiterIndent(lines.slice(lineIndex, closingLineIndex + 1))
349
+ });
350
+ placeholderLines.push(placeholder);
351
+ lineIndex = closingLineIndex;
352
+ }
353
+ return {
354
+ fences,
355
+ placeholderContent: placeholderLines.join("\n")
356
+ };
357
+ }
358
+ function restoreMdxFencedRegions(formatted, fences) {
359
+ let restored = formatted;
360
+ for (const fence of fences) {
361
+ const standalonePlaceholderPattern = new RegExp(`^[\\t ]*${escapeRegExp$1(fence.placeholder)}$`, "gm");
362
+ if (standalonePlaceholderPattern.test(restored)) {
363
+ restored = restored.replace(standalonePlaceholderPattern, fence.source);
364
+ continue;
365
+ }
366
+ restored = restored.split(fence.placeholder).join(fence.source);
367
+ }
368
+ return restored;
369
+ }
370
+ function parseMdxFenceDelimiter(trimmed) {
371
+ const match = /^(`{3,}|~{3,})(.*)$/.exec(trimmed);
372
+ if (!match) return void 0;
373
+ return {
374
+ length: match[1].length,
375
+ marker: match[1].startsWith("`") ? "`" : "~",
376
+ suffix: match[2]
377
+ };
378
+ }
379
+ function parseInlineMdxFenceOpener(trimmed) {
380
+ const splitLine = splitLineBeforeInlineMdxFenceOpener(trimmed);
381
+ if (!splitLine) return void 0;
382
+ return parseMdxFenceDelimiter(splitLine.fenceLine.trim());
383
+ }
384
+ function parseInlineMdxFenceCloser(trimmed, activeFence) {
385
+ const splitLine = splitLineAfterInlineMdxFenceCloser(trimmed, activeFence);
386
+ if (splitLine) return parseMdxFenceDelimiter(splitLine.fenceLine.trim());
387
+ const trailingSplitLine = splitLineBeforeInlineMdxFenceCloser(trimmed, activeFence);
388
+ if (!trailingSplitLine) return void 0;
389
+ return parseMdxFenceDelimiter(trailingSplitLine.fenceLine.trim());
390
+ }
391
+ function isMdxFenceClosingDelimiter(delimiter, activeFence) {
392
+ return delimiter.marker === activeFence.marker && delimiter.length >= activeFence.length && delimiter.suffix.trim().length === 0;
393
+ }
394
+ function splitInlineMdxFenceDelimiters(source) {
395
+ const lines = source.split("\n");
396
+ const splitLines = [];
397
+ let activeFence;
398
+ for (const line of lines) {
399
+ if (!activeFence) {
400
+ const splitLine = splitLineBeforeInlineMdxFenceOpener(line);
401
+ if (splitLine) {
402
+ splitLines.push(...splitAdjacentMarkupTags(splitLine.before), splitLine.fenceLine);
403
+ const delimiter = parseMdxFenceDelimiter(splitLine.fenceLine.trim());
404
+ activeFence = delimiter ? {
405
+ length: delimiter.length,
406
+ marker: delimiter.marker
407
+ } : void 0;
408
+ continue;
409
+ }
410
+ splitLines.push(line);
411
+ const delimiter = parseMdxFenceDelimiter(line.trim());
412
+ if (!delimiter) continue;
413
+ activeFence = {
414
+ length: delimiter.length,
415
+ marker: delimiter.marker
416
+ };
417
+ continue;
418
+ }
419
+ const splitClosingLine = splitLineAfterInlineMdxFenceCloser(line, activeFence);
420
+ if (splitClosingLine) {
421
+ splitLines.push(splitClosingLine.fenceLine, splitClosingLine.after);
422
+ activeFence = void 0;
423
+ continue;
424
+ }
425
+ const splitTrailingClosingLine = splitLineBeforeInlineMdxFenceCloser(line, activeFence);
426
+ if (splitTrailingClosingLine) {
427
+ splitLines.push(splitTrailingClosingLine.before, splitTrailingClosingLine.fenceLine);
428
+ activeFence = void 0;
429
+ continue;
430
+ }
431
+ splitLines.push(line);
432
+ const delimiter = parseMdxFenceDelimiter(line.trim());
433
+ if (!delimiter) continue;
434
+ if (isMdxFenceClosingDelimiter(delimiter, activeFence)) activeFence = void 0;
435
+ }
436
+ return splitLines.join("\n");
437
+ }
438
+ function splitLineBeforeInlineMdxFenceOpener(line) {
439
+ if (parseMdxFenceDelimiter(line.trim())) return void 0;
440
+ const match = /`{3}|~{3}/.exec(line);
441
+ if (!match) return void 0;
442
+ const before = line.slice(0, match.index).trimEnd();
443
+ if (before.trim().length === 0) return void 0;
444
+ return {
445
+ before,
446
+ fenceLine: line.slice(match.index)
447
+ };
448
+ }
449
+ function splitAdjacentMarkupTags(line) {
450
+ const tokens = scanTagTokens(line);
451
+ if (tokens.length < 2) return [line];
452
+ let cursor = 0;
453
+ const lines = [];
454
+ const indent = /^\s*/.exec(line)?.[0] ?? "";
455
+ for (const token of tokens) {
456
+ if (line.slice(cursor, token.start).trim().length > 0) return [line];
457
+ const tag = line.slice(token.start, token.end);
458
+ lines.push(lines.length === 0 ? `${indent}${tag}` : tag);
459
+ cursor = token.end;
460
+ }
461
+ if (line.slice(cursor).trim().length > 0) return [line];
462
+ return lines;
463
+ }
464
+ function splitLineBeforeInlineMdxFenceCloser(line, activeFence) {
465
+ let end = line.length;
466
+ while (end > 0 && (line[end - 1] === " " || line[end - 1] === " ")) end--;
467
+ let start = end;
468
+ while (start > 0 && line[start - 1] === activeFence.marker) start--;
469
+ if (end - start < activeFence.length) return void 0;
470
+ const before = line.slice(0, start).trimEnd();
471
+ if (before.trim().length === 0) return void 0;
472
+ return {
473
+ before,
474
+ fenceLine: line.slice(start, end)
475
+ };
476
+ }
477
+ function splitLineAfterInlineMdxFenceCloser(line, activeFence) {
478
+ const match = new RegExp(`^([\\t ]*${escapeRegExp$1(activeFence.marker.repeat(activeFence.length))}${escapeRegExp$1(activeFence.marker)}*)(.*)$`).exec(line);
479
+ if (!match || match[2].trim().length === 0) return void 0;
480
+ const closingDelimiter = parseMdxFenceDelimiter(match[1].trim());
481
+ if (!closingDelimiter) return void 0;
482
+ if (!isMdxFenceClosingDelimiter(closingDelimiter, activeFence)) return;
483
+ return {
484
+ fenceLine: match[1],
485
+ after: match[2]
486
+ };
487
+ }
488
+ function normalizeMdxFenceDelimiterIndent(lines) {
489
+ if (lines.length < 2) return lines.join("\n");
490
+ const normalizedLines = [...lines];
491
+ normalizedLines[0] = trimLineStartIndent(normalizedLines[0]);
492
+ normalizedLines[normalizedLines.length - 1] = trimLineStartIndent(normalizedLines[normalizedLines.length - 1]);
493
+ return normalizedLines.join("\n");
494
+ }
495
+ function trimLineStartIndent(line) {
496
+ return line.replace(/^[\t ]+/, "");
497
+ }
498
+ function pushLineReplacement(edits, range, oldText, newText) {
499
+ if (newText === oldText) return;
500
+ edits.push({
501
+ ...range,
502
+ newText
503
+ });
504
+ }
505
+ function isLineInsideAnyRange(line, ranges) {
506
+ return ranges.some((range) => line.start >= range.start && line.end <= range.end);
507
+ }
508
+ function escapeRegExp$1(value) {
509
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
510
+ }
511
+
512
+ //#endregion
513
+ //#region packages/ts-format/src/html.ts
514
+ const RAW_TEXT_TAG = "script|style|textarea|title|xmp|iframe|noembed|noframes|plaintext";
515
+ const TAG_BODY = `(?:"[^"]*"|'[^']*'|[^"'>])*>`;
516
+ /**
517
+ * Skips comments, raw text and opening tags; captures closing tags broken before `>`.
518
+ * Case-sensitive so PascalCase components such as `<Title>` are not raw text.
519
+ */
520
+ const BROKEN_CLOSING_TAG = new RegExp([
521
+ String.raw`<!--[\s\S]*?-->`,
522
+ String.raw`<(${RAW_TEXT_TAG})\b${TAG_BODY}[\s\S]*?</\1\s*>`,
523
+ `<[A-Za-z]${TAG_BODY}`,
524
+ String.raw`(</[^\s>]+)\s*\n\s*>`
525
+ ].join("|"), "g");
526
+ async function formatHtmlFragment(source, options) {
527
+ return normalizeClosingTagSpacing(await prettier.format(source, {
528
+ htmlWhitespaceSensitivity: "css",
529
+ parser: "html",
530
+ plugins: [htmlPlugin, postcssPlugin],
531
+ printWidth: options.printWidth,
532
+ tabWidth: options.tabSize,
533
+ useTabs: options.useTabs
534
+ }));
535
+ }
536
+ /** Keep closing tags readable without inserting whitespace into inline content. */
537
+ function normalizeClosingTagSpacing(source) {
538
+ return source.replace(BROKEN_CLOSING_TAG, (match, _rawTextTag, closingTag) => closingTag ? `${closingTag}>` : match);
539
+ }
540
+ function normalizeSelfClosingTagSpacing(formatted) {
541
+ return formatted.replace(/\s+/g, (whitespace, offset) => formatted.startsWith("/>", offset + whitespace.length) ? "" : whitespace);
542
+ }
543
+
544
+ //#endregion
545
+ //#region packages/ts-format/src/mdx.ts
546
+ async function buildMdxFormattingEdits(document, request = {}) {
547
+ const blocks = getMdxMarkupBlocks(document, { includeIncomplete: true }).filter((block) => request.onlyWithinRange ? intersects(block.range, request.onlyWithinRange) : true);
548
+ const edits = [];
549
+ const blockRanges = blocks.map((block) => block.range);
550
+ for (const block of blocks) {
551
+ const formatted = await formatMdxMarkupBlock(document, block);
552
+ if (!formatted || formatted === block.content) continue;
553
+ edits.push({
554
+ ...block.range,
555
+ newText: document.preserveEol(formatted)
556
+ });
557
+ }
558
+ edits.push(...buildMdxFenceDelimiterEdits(document, blockRanges, { onlyWithinRange: request.onlyWithinRange }));
559
+ return edits;
560
+ }
561
+ function getMdxMarkupBlocks(document, options = {}) {
562
+ const blocks = [];
563
+ const state = {
564
+ activeEndLine: void 0,
565
+ activeLines: [],
566
+ frontmatterHandled: false,
567
+ inFence: false,
568
+ inFrontmatter: false
569
+ };
570
+ for (let lineIndex = 0; lineIndex < document.lineCount; lineIndex++) {
571
+ const lineText = document.lineAt(lineIndex).text;
572
+ const trimmed = lineText.trim();
573
+ if (startMdxFrontmatter(lineIndex, trimmed, state)) continue;
574
+ if (closeMdxFrontmatter(trimmed, state)) continue;
575
+ collectActiveMarkupBlock(blocks, document, lineIndex, lineText, trimmed, state);
576
+ }
577
+ if (options.includeIncomplete && state.activeStartLine !== void 0 && state.activeEndLine !== void 0) pushActiveBlock(blocks, document, state.activeStartLine, state.activeEndLine, state.activeLines);
578
+ return blocks;
579
+ }
580
+ function pushActiveBlock(blocks, document, startLine, endLine, lines) {
581
+ const start = document.lineAt(startLine).range.start;
582
+ const end = document.lineAt(endLine).range.end;
583
+ blocks.push({
584
+ content: lines.join("\n"),
585
+ range: {
586
+ start,
587
+ end
588
+ }
589
+ });
590
+ }
591
+ function looksLikeMarkupBlockStart(trimmedLine) {
592
+ return /^<[A-Za-z][A-Za-z0-9._:$-]*/.test(trimmedLine);
593
+ }
594
+ function isCompleteMarkupBlock(markup) {
595
+ const { placeholderContent } = maskMdxFencedRegions(markup);
596
+ if (!placeholderContent.includes("<")) return false;
597
+ if (hasUnterminatedTag(placeholderContent)) return false;
598
+ return getUnclosedTagNames(placeholderContent).length === 0;
599
+ }
600
+ async function formatMdxMarkupBlock(document, block) {
601
+ const formattingOptions = document.options;
602
+ const { fences, placeholderContent: fencePlaceholderContent } = maskMdxFencedRegions(block.content.trim());
603
+ const { inlineCodeSpans, placeholderContent: inlineCodePlaceholderContent } = maskMdxInlineCodeSpans(fencePlaceholderContent);
604
+ const { expressions: regorExpressions, placeholderContent: regorPlaceholderContent } = maskRegorExpressions(inlineCodePlaceholderContent);
605
+ const { expressions, placeholderContent } = maskMdxExpressions(regorPlaceholderContent);
606
+ if (!placeholderContent.includes("<")) return void 0;
607
+ let formatted;
608
+ try {
609
+ formatted = await formatHtmlFragment(placeholderContent, formattingOptions);
610
+ } catch (error) {
611
+ document.report(block.range, error);
612
+ return;
613
+ }
614
+ const restoredExpressions = restoreMdxExpressions(normalizeSelfClosingTagSpacing(formatted.trim()), expressions);
615
+ const restoredInlineCode = restoreMdxInlineCodeSpans(restoreRegorExpressions(restoredExpressions, regorExpressions), inlineCodeSpans);
616
+ return restoreMdxFencedRegions(restoredInlineCode, fences);
617
+ }
618
+ function maskMdxInlineCodeSpans(source) {
619
+ const inlineCodeSpans = [];
620
+ return {
621
+ inlineCodeSpans,
622
+ placeholderContent: source.split("\n").map((line) => maskMdxInlineCodeSpansInLine(line, inlineCodeSpans)).join("\n")
623
+ };
624
+ }
625
+ function maskMdxInlineCodeSpansInLine(line, inlineCodeSpans) {
626
+ let placeholderLine = "";
627
+ for (let index = 0; index < line.length;) {
628
+ const start = line.indexOf("`", index);
629
+ if (start === -1) {
630
+ placeholderLine += line.slice(index);
631
+ break;
632
+ }
633
+ const tickCount = countRepeatedChar(line, start, "`");
634
+ if (tickCount !== 1) {
635
+ placeholderLine += line.slice(index, start + tickCount);
636
+ index = start + tickCount;
637
+ continue;
638
+ }
639
+ const end = findMdxInlineCodeEnd(line, start + 1);
640
+ if (end === -1) {
641
+ placeholderLine += line.slice(index);
642
+ break;
643
+ }
644
+ const source = line.slice(start, end + 1);
645
+ const placeholder = createMdxInlineCodePlaceholder(inlineCodeSpans.length, line, start, end + 1);
646
+ inlineCodeSpans.push({
647
+ placeholder,
648
+ source
649
+ });
650
+ placeholderLine += line.slice(index, start);
651
+ placeholderLine += placeholder;
652
+ index = end + 1;
653
+ }
654
+ return placeholderLine;
655
+ }
656
+ function createMdxInlineCodePlaceholder(index, line, start, end) {
657
+ const lineBefore = line.slice(0, start);
658
+ const lineAfter = line.slice(end);
659
+ if (lineBefore.trim().length === 0 && lineAfter.trim().length === 0) return `<!--PURESTACK_MDX_INLINE_CODE_${index}-->`;
660
+ return `PURESTACK_MDX_INLINE_CODE_${index}`;
661
+ }
662
+ function restoreMdxInlineCodeSpans(formatted, inlineCodeSpans) {
663
+ let restored = formatted;
664
+ for (const inlineCodeSpan of sortLongestPlaceholderFirst(inlineCodeSpans)) restored = restored.split(inlineCodeSpan.placeholder).join(inlineCodeSpan.source);
665
+ return restored;
666
+ }
667
+ function maskMdxExpressions(source) {
668
+ const expressions = [];
669
+ let placeholderContent = "";
670
+ for (let index = 0; index < source.length;) {
671
+ const rawTextEnd = findRawTextElementEnd(source, index);
672
+ if (rawTextEnd !== void 0) {
673
+ placeholderContent += source.slice(index, rawTextEnd);
674
+ index = rawTextEnd;
675
+ continue;
676
+ }
677
+ if (!isMdxExpressionStart(source, index)) {
678
+ placeholderContent += source[index];
679
+ index++;
680
+ continue;
681
+ }
682
+ const expressionEnd = findMdxExpressionEnd(source, index);
683
+ if (expressionEnd === -1) {
684
+ placeholderContent += source[index];
685
+ index++;
686
+ continue;
687
+ }
688
+ const placeholder = `PURESTACK_MDX_EXPR_${expressions.length}`;
689
+ expressions.push({
690
+ placeholder,
691
+ source: source.slice(index, expressionEnd)
692
+ });
693
+ placeholderContent += placeholder;
694
+ index = expressionEnd;
695
+ }
696
+ return {
697
+ expressions,
698
+ placeholderContent
699
+ };
700
+ }
701
+ function restoreMdxExpressions(formatted, expressions) {
702
+ let restored = formatted;
703
+ for (const expression of sortLongestPlaceholderFirst(expressions)) restored = restored.split(expression.placeholder).join(expression.source);
704
+ return restored;
705
+ }
706
+ function hasUnterminatedTag(markup) {
707
+ for (let index = 0; index < markup.length; index++) {
708
+ if (markup[index] !== "<") continue;
709
+ if (markup.startsWith("<!--", index)) {
710
+ const commentEnd = markup.indexOf("-->", index + 4);
711
+ if (commentEnd === -1) return true;
712
+ index = commentEnd + 2;
713
+ continue;
714
+ }
715
+ const nextTagEnd = findTagEnd(markup, index + 1);
716
+ if (nextTagEnd === -1) return true;
717
+ index = nextTagEnd;
718
+ }
719
+ return false;
720
+ }
721
+ function startMdxFrontmatter(lineIndex, trimmed, state) {
722
+ if (state.frontmatterHandled || lineIndex !== 0 || trimmed !== "---") return false;
723
+ state.inFrontmatter = true;
724
+ state.frontmatterHandled = true;
725
+ return true;
726
+ }
727
+ function closeMdxFrontmatter(trimmed, state) {
728
+ if (!state.inFrontmatter) return false;
729
+ if (trimmed === "---") state.inFrontmatter = false;
730
+ return true;
731
+ }
732
+ function updateMdxFenceState(trimmed, state) {
733
+ const activeFence = state.fenceMarker && state.fenceLength ? {
734
+ length: state.fenceLength,
735
+ marker: state.fenceMarker
736
+ } : void 0;
737
+ const delimiter = state.inFence && activeFence ? parseInlineMdxFenceCloser(trimmed, activeFence) ?? parseMdxFenceDelimiter(trimmed) : parseMdxFenceDelimiter(trimmed) ?? parseInlineMdxFenceOpener(trimmed);
738
+ if (!delimiter) return state.inFence;
739
+ if (!state.inFence) {
740
+ state.inFence = true;
741
+ state.fenceLength = delimiter.length;
742
+ state.fenceMarker = delimiter.marker;
743
+ return true;
744
+ }
745
+ if (state.fenceMarker && state.fenceLength && isMdxFenceClosingDelimiter(delimiter, {
746
+ length: state.fenceLength,
747
+ marker: state.fenceMarker
748
+ })) {
749
+ state.inFence = false;
750
+ state.fenceLength = void 0;
751
+ state.fenceMarker = void 0;
752
+ }
753
+ return true;
754
+ }
755
+ function collectActiveMarkupBlock(blocks, document, lineIndex, lineText, trimmed, state) {
756
+ if (state.activeStartLine === void 0) {
757
+ if (state.inFence) {
758
+ updateMdxFenceState(trimmed, state);
759
+ return;
760
+ }
761
+ if (!looksLikeMarkupBlockStart(trimmed)) {
762
+ if (updateMdxFenceState(trimmed, state)) return;
763
+ return;
764
+ }
765
+ state.activeStartLine = lineIndex;
766
+ state.activeEndLine = lineIndex;
767
+ state.activeLines = [lineText];
768
+ } else {
769
+ state.activeEndLine = lineIndex;
770
+ state.activeLines.push(lineText);
771
+ }
772
+ updateMdxFenceState(trimmed, state);
773
+ if (state.inFence) return;
774
+ if (!isCompleteMarkupBlock(state.activeLines.join("\n"))) return;
775
+ pushActiveBlock(blocks, document, state.activeStartLine, state.activeEndLine ?? lineIndex, state.activeLines);
776
+ state.activeEndLine = void 0;
777
+ state.activeStartLine = void 0;
778
+ state.activeLines = [];
779
+ }
780
+ function getUnclosedTagNames(markup) {
781
+ const stack = [];
782
+ for (const token of scanTagTokens(markup)) {
783
+ if (token.kind === "opening") {
784
+ if (isSelfContainedTag(token.name, token.selfClosing)) continue;
785
+ stack.push(token.name);
786
+ continue;
787
+ }
788
+ closeMatchingTag(stack, token.name);
789
+ }
790
+ return stack;
791
+ }
792
+ function isSelfContainedTag(tagName, selfClosing) {
793
+ return selfClosing || VOID_HTML_TAG_NAMES.has(tagName.toLowerCase());
794
+ }
795
+ function closeMatchingTag(stack, tagName) {
796
+ for (let index = stack.length - 1; index >= 0; index--) {
797
+ if (stack[index] !== tagName) continue;
798
+ stack.splice(index);
799
+ return;
800
+ }
801
+ }
802
+ function escapeRegExp(value) {
803
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
804
+ }
805
+ function sortLongestPlaceholderFirst(placeholders) {
806
+ return [...placeholders].sort((left, right) => right.placeholder.length - left.placeholder.length);
807
+ }
808
+ function findMdxInlineCodeEnd(line, startIndex) {
809
+ for (let index = startIndex; index < line.length; index++) if (line[index] === "`") return index;
810
+ return -1;
811
+ }
812
+ function countRepeatedChar(line, startIndex, char) {
813
+ let count = 0;
814
+ while (line[startIndex + count] === char) count++;
815
+ return count;
816
+ }
817
+ function findRawTextElementEnd(source, startIndex) {
818
+ if (source[startIndex] !== "<" || source[startIndex + 1] === "/") return;
819
+ const tagEnd = findTagEnd(source, startIndex + 1);
820
+ if (tagEnd === -1) return void 0;
821
+ const openingTag = source.slice(startIndex, tagEnd + 1);
822
+ const tagName = /^<\s*(script|style)\b/i.exec(openingTag)?.[1];
823
+ if (!tagName) return void 0;
824
+ const closingTagMatch = new RegExp(`<\\/\\s*${escapeRegExp(tagName)}\\s*>`, "i").exec(source.slice(tagEnd + 1));
825
+ if (!closingTagMatch) return void 0;
826
+ return tagEnd + 1 + closingTagMatch.index + closingTagMatch[0].length;
827
+ }
828
+
829
+ //#endregion
830
+ //#region packages/ts-format/src/templates.ts
831
+ const SUPPORTED_TEMPLATE_TAG_NAMES = ["html", "svg"];
832
+ async function buildTemplateFormattingEdits(document, request = {}) {
833
+ const templates = getSupportedTaggedTemplates(document).filter((template) => request.onlyWithinRange ? intersects(template.contentRange, request.onlyWithinRange) : true);
834
+ if (templates.length === 0) return [];
835
+ const edits = [];
836
+ for (const template of templates) {
837
+ const formatted = await formatTaggedTemplate(document, template);
838
+ if (!formatted || formatted === template.content) continue;
839
+ edits.push({
840
+ ...template.contentRange,
841
+ newText: document.preserveEol(formatted)
842
+ });
843
+ }
844
+ return edits;
845
+ }
846
+ function getSupportedTaggedTemplates(document) {
847
+ const sourceText = document.text;
848
+ const sourceFile = ts.createSourceFile(document.filePath, sourceText, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
849
+ const templates = [];
850
+ visitNode(sourceFile);
851
+ return templates;
852
+ function visitNode(node) {
853
+ if (ts.isTaggedTemplateExpression(node)) {
854
+ const template = createSupportedTaggedTemplate(node, sourceFile);
855
+ if (template) {
856
+ templates.push(template);
857
+ return;
858
+ }
859
+ }
860
+ ts.forEachChild(node, visitNode);
861
+ }
862
+ }
863
+ function createSupportedTaggedTemplate(node, sourceFile) {
864
+ const tagName = getSupportedTemplateTagName(node.tag.getText(sourceFile));
865
+ if (!tagName) return void 0;
866
+ const template = node.template;
867
+ if (isLikelyPrematureRawBacktickClose(sourceFile.text, template)) return;
868
+ const innerStart = template.getStart(sourceFile) + 1;
869
+ const innerEnd = template.getEnd() - 1;
870
+ if (innerEnd < innerStart) return void 0;
871
+ const contentRange = {
872
+ start: innerStart,
873
+ end: innerEnd
874
+ };
875
+ if (ts.isNoSubstitutionTemplateLiteral(template)) {
876
+ const content = sourceFile.text.slice(innerStart, innerEnd);
877
+ return {
878
+ content,
879
+ contentRange,
880
+ expressions: [],
881
+ interpolationRanges: [],
882
+ placeholderContent: content,
883
+ tagName
884
+ };
885
+ }
886
+ const expressions = [];
887
+ const interpolationRanges = [];
888
+ let placeholderContent = sliceTemplateHeadContent(template.head, sourceFile);
889
+ let currentContentOffset = placeholderContent.length;
890
+ template.templateSpans.forEach((span, index) => {
891
+ const placeholder = `PURESTACK_EXPR_${index}_${tagName.toUpperCase()}`;
892
+ const interpolationEnd = span.literal.getStart(sourceFile) - innerStart + 1;
893
+ expressions.push({
894
+ placeholder,
895
+ source: sourceFile.text.slice(span.expression.getStart(sourceFile), span.expression.getEnd())
896
+ });
897
+ interpolationRanges.push({
898
+ start: currentContentOffset,
899
+ end: interpolationEnd
900
+ });
901
+ placeholderContent += placeholder;
902
+ const literalContent = sliceTemplateSpanLiteralContent(span.literal, sourceFile);
903
+ placeholderContent += literalContent;
904
+ currentContentOffset = interpolationEnd + literalContent.length;
905
+ });
906
+ return {
907
+ content: sourceFile.text.slice(innerStart, innerEnd),
908
+ contentRange,
909
+ expressions,
910
+ interpolationRanges,
911
+ placeholderContent,
912
+ tagName
913
+ };
914
+ }
915
+ function getSupportedTemplateTagName(tagText) {
916
+ const normalizedTag = tagText.trim();
917
+ for (const tagName of SUPPORTED_TEMPLATE_TAG_NAMES) if (normalizedTag === tagName || normalizedTag.endsWith(`.${tagName}`)) return tagName;
918
+ }
919
+ function isLikelyPrematureRawBacktickClose(sourceText, template) {
920
+ return sourceText.startsWith("``", template.getEnd());
921
+ }
922
+ function sliceTemplateHeadContent(head, sourceFile) {
923
+ return sourceFile.text.slice(head.getStart(sourceFile) + 1, head.getEnd() - 2);
924
+ }
925
+ function sliceTemplateSpanLiteralContent(literal, sourceFile) {
926
+ const endOffset = ts.isTemplateTail(literal) ? 1 : 2;
927
+ return sourceFile.text.slice(literal.getStart(sourceFile) + 1, literal.getEnd() - endOffset);
928
+ }
929
+ async function formatTaggedTemplate(document, template) {
930
+ const formattingOptions = document.options;
931
+ const source = template.placeholderContent.trim();
932
+ const { expressions: regorExpressions, placeholderContent } = maskRegorExpressions(source);
933
+ if (!placeholderContent.includes("<")) return void 0;
934
+ let formatted;
935
+ try {
936
+ formatted = await formatHtmlFragment(placeholderContent, formattingOptions);
937
+ } catch (error) {
938
+ document.report(template.contentRange, error);
939
+ return;
940
+ }
941
+ const restoredTemplateExpressions = restoreTemplateExpressions(normalizeSelfClosingTagSpacing(formatted.trim()), template.expressions);
942
+ return restoreRegorExpressions(restoredTemplateExpressions, regorExpressions);
943
+ }
944
+ function restoreTemplateExpressions(formatted, expressions) {
945
+ let restored = formatted;
946
+ for (const expression of expressions) restored = restored.split(expression.placeholder).join(`\${${expression.source}}`);
947
+ return restored;
948
+ }
949
+
950
+ //#endregion
951
+ //#region packages/ts-format/src/index.ts
952
+ /** Formats only component markup and html/svg tagged template contents. */
953
+ async function formatSource(text, options = {}) {
954
+ const source = new SourceText(text, options);
955
+ const edits = await ((options.language ?? (/\.[cm]?tsx?$/i.test(source.filePath) ? "typescript" : "mdx")) === "typescript" ? buildTemplateFormattingEdits(source, options) : buildMdxFormattingEdits(source, options));
956
+ const code = applySourceEdits(text, edits);
957
+ return {
958
+ code,
959
+ edits,
960
+ diagnostics: source.diagnostics,
961
+ changed: code !== text
962
+ };
963
+ }
964
+
965
+ //#endregion
966
+ //#region packages/ts-format/src/files.ts
967
+ const exec = promisify(execFile);
968
+ const caseInsensitivePaths = process.platform === "win32" || process.platform === "darwin";
969
+ const comparablePath = (value) => caseInsensitivePaths ? value.toLowerCase() : value;
970
+ const extensions = /\.(?:[cm]?ts|mdx|rmdx)$/i;
971
+ const ignoredDirectories = /* @__PURE__ */ new Set([
972
+ ".git",
973
+ ".yarn",
974
+ ".tmp",
975
+ ".ts-ssg",
976
+ ".cache",
977
+ "node_modules",
978
+ "dist",
979
+ "out",
980
+ "coverage",
981
+ "artifacts"
982
+ ]);
983
+ /** Discover source files, honor Git ignores in repositories, and collect all failures. */
984
+ async function formatFiles(inputs = ["."], options = {}) {
985
+ const cwd = path.resolve(options.cwd ?? process.cwd());
986
+ const excluded = (options.exclude ?? []).map((entry) => comparablePath(path.resolve(cwd, entry)));
987
+ const files = /* @__PURE__ */ new Set();
988
+ const errors = [];
989
+ const changed = [];
990
+ function isExcluded(file) {
991
+ const comparable = comparablePath(file);
992
+ return path.relative(cwd, file).split(path.sep).some((part) => ignoredDirectories.has(comparablePath(part))) || excluded.some((entry) => comparable === entry || comparable.startsWith(`${entry}${path.sep}`));
993
+ }
994
+ function addFile(file) {
995
+ if (extensions.test(file) && !isExcluded(file)) files.add(file);
996
+ }
997
+ async function walk(directory) {
998
+ if (isExcluded(directory)) return;
999
+ for (const entry of await readdir(directory, { withFileTypes: true })) {
1000
+ const file = path.join(directory, entry.name);
1001
+ if (entry.isSymbolicLink()) continue;
1002
+ if (entry.isDirectory()) await walk(file);
1003
+ else if (entry.isFile()) addFile(file);
1004
+ }
1005
+ }
1006
+ for (const input of inputs) {
1007
+ const target = path.resolve(cwd, input);
1008
+ try {
1009
+ const entry = await lstat(target);
1010
+ if (entry.isSymbolicLink()) continue;
1011
+ if (entry.isFile()) {
1012
+ addFile(target);
1013
+ continue;
1014
+ }
1015
+ if (!entry.isDirectory() || isExcluded(target)) continue;
1016
+ let gitFiles;
1017
+ try {
1018
+ const { stdout } = await exec("git", [
1019
+ "-C",
1020
+ target,
1021
+ "ls-files",
1022
+ "--cached",
1023
+ "--others",
1024
+ "--exclude-standard",
1025
+ "-z",
1026
+ "--",
1027
+ "."
1028
+ ], { maxBuffer: 33554432 });
1029
+ gitFiles = stdout.split("\0").filter(Boolean).map((file) => path.resolve(target, file));
1030
+ } catch {}
1031
+ if (gitFiles) for (const file of gitFiles) addFile(file);
1032
+ else await walk(target);
1033
+ } catch (error) {
1034
+ errors.push({
1035
+ filePath: target,
1036
+ message: error instanceof Error ? error.message : String(error)
1037
+ });
1038
+ }
1039
+ }
1040
+ for (const filePath of [...files].sort()) try {
1041
+ const entry = await lstat(filePath).catch(() => void 0);
1042
+ if (!entry?.isFile() || entry.isSymbolicLink()) continue;
1043
+ const text = await readFile(filePath, "utf8");
1044
+ const result = await formatSource(text, {
1045
+ ...options,
1046
+ filePath
1047
+ });
1048
+ if (result.diagnostics.length) {
1049
+ for (const diagnostic of result.diagnostics) errors.push({
1050
+ filePath,
1051
+ message: diagnostic.message
1052
+ });
1053
+ continue;
1054
+ }
1055
+ if (!result.changed) continue;
1056
+ if (options.write) {
1057
+ if (await readFile(filePath, "utf8") !== text) throw new Error("File changed during formatting; skipped to preserve your edits.");
1058
+ await writeFile(filePath, result.code, "utf8");
1059
+ }
1060
+ changed.push(filePath);
1061
+ } catch (error) {
1062
+ errors.push({
1063
+ filePath,
1064
+ message: error instanceof Error ? error.message : String(error)
1065
+ });
1066
+ }
1067
+ return {
1068
+ checked: files.size,
1069
+ changed,
1070
+ errors
1071
+ };
1072
+ }
1073
+
1074
+ //#endregion
1075
+ export { formatFiles };
1076
+ //# sourceMappingURL=files.mjs.map