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