@velarscript/compiler 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +70 -0
  3. package/dist/analyzer.d.ts +1175 -0
  4. package/dist/analyzer.d.ts.map +1 -0
  5. package/dist/analyzer.js +12130 -0
  6. package/dist/analyzer.js.map +1 -0
  7. package/dist/application-package-host.d.ts +28 -0
  8. package/dist/application-package-host.d.ts.map +1 -0
  9. package/dist/application-package-host.js +2 -0
  10. package/dist/application-package-host.js.map +1 -0
  11. package/dist/ast.d.ts +853 -0
  12. package/dist/ast.d.ts.map +1 -0
  13. package/dist/ast.js +245 -0
  14. package/dist/ast.js.map +1 -0
  15. package/dist/class-runtime.d.ts +6 -0
  16. package/dist/class-runtime.d.ts.map +1 -0
  17. package/dist/class-runtime.js +45 -0
  18. package/dist/class-runtime.js.map +1 -0
  19. package/dist/collection-lowering-runtime.d.ts +7 -0
  20. package/dist/collection-lowering-runtime.d.ts.map +1 -0
  21. package/dist/collection-lowering-runtime.js +952 -0
  22. package/dist/collection-lowering-runtime.js.map +1 -0
  23. package/dist/collection-runtime.d.ts +16 -0
  24. package/dist/collection-runtime.d.ts.map +1 -0
  25. package/dist/collection-runtime.js +231 -0
  26. package/dist/collection-runtime.js.map +1 -0
  27. package/dist/core-vocabulary.d.ts +141 -0
  28. package/dist/core-vocabulary.d.ts.map +1 -0
  29. package/dist/core-vocabulary.js +88 -0
  30. package/dist/core-vocabulary.js.map +1 -0
  31. package/dist/diagnostic.d.ts +42 -0
  32. package/dist/diagnostic.d.ts.map +1 -0
  33. package/dist/diagnostic.js +66 -0
  34. package/dist/diagnostic.js.map +1 -0
  35. package/dist/embedded-javascript.d.ts +74 -0
  36. package/dist/embedded-javascript.d.ts.map +1 -0
  37. package/dist/embedded-javascript.js +341 -0
  38. package/dist/embedded-javascript.js.map +1 -0
  39. package/dist/embedded-module.d.ts +11 -0
  40. package/dist/embedded-module.d.ts.map +1 -0
  41. package/dist/embedded-module.js +67 -0
  42. package/dist/embedded-module.js.map +1 -0
  43. package/dist/embedded-source.d.ts +19 -0
  44. package/dist/embedded-source.d.ts.map +1 -0
  45. package/dist/embedded-source.js +61 -0
  46. package/dist/embedded-source.js.map +1 -0
  47. package/dist/emitter.d.ts +188 -0
  48. package/dist/emitter.d.ts.map +1 -0
  49. package/dist/emitter.js +3254 -0
  50. package/dist/emitter.js.map +1 -0
  51. package/dist/error-runtime.d.ts +17 -0
  52. package/dist/error-runtime.d.ts.map +1 -0
  53. package/dist/error-runtime.js +109 -0
  54. package/dist/error-runtime.js.map +1 -0
  55. package/dist/extension.d.ts +373 -0
  56. package/dist/extension.d.ts.map +1 -0
  57. package/dist/extension.js +25 -0
  58. package/dist/extension.js.map +1 -0
  59. package/dist/formatter.d.ts +13 -0
  60. package/dist/formatter.d.ts.map +1 -0
  61. package/dist/formatter.js +1440 -0
  62. package/dist/formatter.js.map +1 -0
  63. package/dist/framework-host.d.ts +87 -0
  64. package/dist/framework-host.d.ts.map +1 -0
  65. package/dist/framework-host.js +2 -0
  66. package/dist/framework-host.js.map +1 -0
  67. package/dist/index.d.ts +75 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +932 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/interpolated-string.d.ts +47 -0
  72. package/dist/interpolated-string.d.ts.map +1 -0
  73. package/dist/interpolated-string.js +344 -0
  74. package/dist/interpolated-string.js.map +1 -0
  75. package/dist/json-runtime.d.ts +2 -0
  76. package/dist/json-runtime.d.ts.map +1 -0
  77. package/dist/json-runtime.js +193 -0
  78. package/dist/json-runtime.js.map +1 -0
  79. package/dist/language-guidance.d.ts +31 -0
  80. package/dist/language-guidance.d.ts.map +1 -0
  81. package/dist/language-guidance.js +187 -0
  82. package/dist/language-guidance.js.map +1 -0
  83. package/dist/lexer.d.ts +85 -0
  84. package/dist/lexer.d.ts.map +1 -0
  85. package/dist/lexer.js +1004 -0
  86. package/dist/lexer.js.map +1 -0
  87. package/dist/limits.d.ts +2 -0
  88. package/dist/limits.d.ts.map +1 -0
  89. package/dist/limits.js +2 -0
  90. package/dist/limits.js.map +1 -0
  91. package/dist/mechanical-fix.d.ts +26 -0
  92. package/dist/mechanical-fix.d.ts.map +1 -0
  93. package/dist/mechanical-fix.js +61 -0
  94. package/dist/mechanical-fix.js.map +1 -0
  95. package/dist/narrowing-runtime.d.ts +5 -0
  96. package/dist/narrowing-runtime.d.ts.map +1 -0
  97. package/dist/narrowing-runtime.js +27 -0
  98. package/dist/narrowing-runtime.js.map +1 -0
  99. package/dist/number-runtime.d.ts +3 -0
  100. package/dist/number-runtime.d.ts.map +1 -0
  101. package/dist/number-runtime.js +37 -0
  102. package/dist/number-runtime.js.map +1 -0
  103. package/dist/parser.d.ts +245 -0
  104. package/dist/parser.d.ts.map +1 -0
  105. package/dist/parser.js +3582 -0
  106. package/dist/parser.js.map +1 -0
  107. package/dist/primitive-runtime.d.ts +4 -0
  108. package/dist/primitive-runtime.d.ts.map +1 -0
  109. package/dist/primitive-runtime.js +39 -0
  110. package/dist/primitive-runtime.js.map +1 -0
  111. package/dist/promise-runtime.d.ts +6 -0
  112. package/dist/promise-runtime.d.ts.map +1 -0
  113. package/dist/promise-runtime.js +86 -0
  114. package/dist/promise-runtime.js.map +1 -0
  115. package/dist/reactive-bridge-runtime.d.ts +7 -0
  116. package/dist/reactive-bridge-runtime.d.ts.map +1 -0
  117. package/dist/reactive-bridge-runtime.js +112 -0
  118. package/dist/reactive-bridge-runtime.js.map +1 -0
  119. package/dist/runtime-abi.d.ts +24 -0
  120. package/dist/runtime-abi.d.ts.map +1 -0
  121. package/dist/runtime-abi.js +24 -0
  122. package/dist/runtime-abi.js.map +1 -0
  123. package/dist/semantic.d.ts +142 -0
  124. package/dist/semantic.d.ts.map +1 -0
  125. package/dist/semantic.js +845 -0
  126. package/dist/semantic.js.map +1 -0
  127. package/dist/source-names.d.ts +32 -0
  128. package/dist/source-names.d.ts.map +1 -0
  129. package/dist/source-names.js +101 -0
  130. package/dist/source-names.js.map +1 -0
  131. package/dist/source.d.ts +19 -0
  132. package/dist/source.d.ts.map +1 -0
  133. package/dist/source.js +64 -0
  134. package/dist/source.js.map +1 -0
  135. package/dist/text-runtime.d.ts +2 -0
  136. package/dist/text-runtime.d.ts.map +1 -0
  137. package/dist/text-runtime.js +285 -0
  138. package/dist/text-runtime.js.map +1 -0
  139. package/dist/token.d.ts +19 -0
  140. package/dist/token.d.ts.map +1 -0
  141. package/dist/token.js +51 -0
  142. package/dist/token.js.map +1 -0
  143. package/dist/type-registry-runtime.d.ts +7 -0
  144. package/dist/type-registry-runtime.d.ts.map +1 -0
  145. package/dist/type-registry-runtime.js +51 -0
  146. package/dist/type-registry-runtime.js.map +1 -0
  147. package/dist/type-validation-runtime.d.ts +17 -0
  148. package/dist/type-validation-runtime.d.ts.map +1 -0
  149. package/dist/type-validation-runtime.js +150 -0
  150. package/dist/type-validation-runtime.js.map +1 -0
  151. package/dist/types.d.ts +356 -0
  152. package/dist/types.d.ts.map +1 -0
  153. package/dist/types.js +1170 -0
  154. package/dist/types.js.map +1 -0
  155. package/dist/utf8-runtime.d.ts +7 -0
  156. package/dist/utf8-runtime.d.ts.map +1 -0
  157. package/dist/utf8-runtime.js +43 -0
  158. package/dist/utf8-runtime.js.map +1 -0
  159. package/package.json +48 -0
@@ -0,0 +1,1440 @@
1
+ import { CORE_STATEMENT_HEAD_KEYWORDS } from "./core-vocabulary.js";
2
+ import { scanEmbeddedJavaScriptLiteral } from "./embedded-javascript.js";
3
+ import { MAX_VELAR_SOURCE_CODE_UNITS } from "./limits.js";
4
+ import { findInterpolatedExpressionEnd, scanStringEscape, scanStringLiteral } from "./interpolated-string.js";
5
+ import { isSourceIdentifierPart, isSourceIdentifierStart } from "./source-names.js";
6
+ import { keywordKinds } from "./token.js";
7
+ const multiCharacterOperators = [">>>=", "<<=", ">>=", ">>>", "<<", ">>", "...", "?.", "??", "->", "=>", "==", "!=", "<=", ">=", "**", "+=", "-=", "*=", "/=", "%=", "|=", "&=", "^="];
8
+ const binaryWords = new Set(["and", "or", "in", "is"]);
9
+ const prefixWords = new Set(["not", "await"]);
10
+ // D30 item 16: `match` and `case` are contextual keywords, so `match(value)` is
11
+ // a call and must not gain a keyword's space. They keep it only where a
12
+ // keyword can stand — the head of a statement line. D62 rule 157: which words
13
+ // those are is Core's roster to answer, not this file's — the copy that stood
14
+ // here knew two of the ten and could not have learned about an eleventh.
15
+ const statementHeadKeywordWords = new Set(CORE_STATEMENT_HEAD_KEYWORDS);
16
+ /**
17
+ * The reserved words that stand in expression position: `super` and `import`
18
+ * name one directly — `super(id)`, `import("./page.vel")` — and the formatter
19
+ * has already read `true`, `false` and `null` as literals by the time this set
20
+ * is consulted. Every other reserved word is a keyword in the structural sense
21
+ * `endsExpression` uses: it cannot end an expression, so what follows it opens
22
+ * a new one.
23
+ */
24
+ const expressionKeywordWords = new Set(["true", "false", "null", "super", "import"]);
25
+ const nonExpressionKeywordWords = new Set(Object.keys(keywordKinds).filter((word) => !expressionKeywordWords.has(word)));
26
+ /**
27
+ * Formats VelarScript source without round-tripping through generated JavaScript.
28
+ * The formatter tokenizes each logical source line so strings, comments,
29
+ * extension-owned embeddings and literals, operators, named arguments, and
30
+ * type syntax retain their meaning while whitespace becomes canonical.
31
+ */
32
+ export function formatSource(text, options = {}) {
33
+ if (text.length > MAX_VELAR_SOURCE_CODE_UNITS)
34
+ throw new RangeError("A VelarScript source module cannot exceed 4 MiB");
35
+ const indentWidth = options.indentWidth ?? 4;
36
+ if (!Number.isSafeInteger(indentWidth) || indentWidth < 1 || indentWidth > 16) {
37
+ throw new RangeError("VelarScript formatter indentWidth must be an integer from 1 through 16");
38
+ }
39
+ const angleOwners = (options.extensions ?? []).flatMap((extension) => extension.formatting?.angleBracketEmbedding
40
+ ? [extension.formatting.angleBracketEmbedding]
41
+ : []);
42
+ if (angleOwners.length > 1)
43
+ throw new Error("Only one compiler extension may own angle-bracket formatting");
44
+ const angleEmbedding = angleOwners[0] ?? null;
45
+ const opaqueSourceScanners = (options.extensions ?? []).flatMap((extension) => extension.formatting?.scanOpaqueSource
46
+ ? [extension.formatting.scanOpaqueSource]
47
+ : []);
48
+ const protectedStrings = protectMultilineStrings(text, opaqueSourceScanners);
49
+ const lines = protectedStrings.text.replaceAll("\r\n", "\n").replaceAll("\r", "\n").split("\n");
50
+ const indentation = [0];
51
+ const formatted = [];
52
+ let embeddedDepth = 0;
53
+ let statementLevel = 0;
54
+ /** The last token of the previous line — the context a continuation reads. */
55
+ let preceding;
56
+ for (const original of lines) {
57
+ const line = original.replace(/[ \t]+$/u, "");
58
+ if (line.trim().length === 0) {
59
+ if (formatted.length > 0 && formatted.at(-1) !== "")
60
+ formatted.push("");
61
+ continue;
62
+ }
63
+ const leading = line.match(/^[ \t]*/u)?.[0] ?? "";
64
+ const width = [...leading].reduce((total, character) => total + (character === "\t" ? indentWidth : 1), 0);
65
+ const content = line.slice(leading.length);
66
+ // A leading-dot chain continuation keeps its own canonical indentation —
67
+ // one level past the statement it continues — without opening a block for
68
+ // the lines that follow it.
69
+ if (embeddedDepth === 0 && isChainContinuationLine(content) && formatted.length > 0) {
70
+ const column = (statementLevel + 1) * indentWidth;
71
+ const line = formatInlineLine(content, angleEmbedding, markupLayout(indentWidth, column, angleEmbedding), preceding);
72
+ formatted.push(`${" ".repeat(column)}${line.text}`);
73
+ preceding = line.trailing ?? preceding;
74
+ continue;
75
+ }
76
+ const current = indentation.at(-1) ?? 0;
77
+ if (width > current) {
78
+ indentation.push(width);
79
+ }
80
+ else if (width < current) {
81
+ while (indentation.length > 1 && width < (indentation.at(-1) ?? 0))
82
+ indentation.pop();
83
+ if (width !== (indentation.at(-1) ?? 0))
84
+ indentation.push(width);
85
+ }
86
+ statementLevel = indentation.length - 1;
87
+ const indent = " ".repeat(statementLevel * indentWidth);
88
+ const layout = markupLayout(indentWidth, statementLevel * indentWidth, angleEmbedding);
89
+ if (embeddedDepth > 0) {
90
+ formatted.push(`${indent}${formatEmbeddedContent(content, angleEmbedding, layout, layout.column)}`);
91
+ preceding = undefined;
92
+ }
93
+ else {
94
+ const line = formatInlineLine(content, angleEmbedding, layout, preceding);
95
+ formatted.push(`${indent}${line.text}`);
96
+ preceding = line.trailing ?? preceding;
97
+ }
98
+ embeddedDepth = nextEmbeddedDepth(content, embeddedDepth, angleEmbedding);
99
+ }
100
+ while (formatted.at(-1) === "")
101
+ formatted.pop();
102
+ return protectedStrings.restore(`${formatted.join("\n")}\n`);
103
+ }
104
+ function protectMultilineStrings(source, opaqueSourceScanners) {
105
+ const replacements = [];
106
+ let output = "";
107
+ let index = 0;
108
+ while (index < source.length) {
109
+ if (source.startsWith("//", index)) {
110
+ const end = source.indexOf("\n", index + 2);
111
+ const next = end === -1 ? source.length : end;
112
+ output += source.slice(index, next);
113
+ index = next;
114
+ continue;
115
+ }
116
+ if (source.startsWith("/*", index)) {
117
+ const end = blockCommentEnd(source, index);
118
+ const value = source.slice(index, end);
119
+ if (!value.includes("\n") && !value.includes("\r")) {
120
+ output += value;
121
+ index = end;
122
+ continue;
123
+ }
124
+ let marker = `__velar_formatter_multiline_comment_${replacements.length}__`;
125
+ while (source.includes(marker))
126
+ marker += "_";
127
+ const placeholder = JSON.stringify(marker);
128
+ const lineStart = Math.max(source.lastIndexOf("\n", index - 1), source.lastIndexOf("\r", index - 1)) + 1;
129
+ const originalIndent = /^[ \t]*/u.exec(source.slice(lineStart, index))?.[0] ?? "";
130
+ replacements.push({ placeholder, value, kind: "blockComment", originalIndent });
131
+ output += placeholder;
132
+ index = end;
133
+ continue;
134
+ }
135
+ const opaqueSource = scanFormattingOpaqueSource(source, index, opaqueSourceScanners);
136
+ if (opaqueSource) {
137
+ const start = index;
138
+ index = opaqueSource.end;
139
+ const value = source.slice(start, index);
140
+ let marker = opaqueSource.attachedToPrevious
141
+ ? `__velar_formatter_attached_opaque_source_${replacements.length}__`
142
+ : `__velar_formatter_opaque_source_${replacements.length}__`;
143
+ while (source.includes(marker))
144
+ marker += "_";
145
+ const placeholder = JSON.stringify(marker);
146
+ const lineStart = Math.max(source.lastIndexOf("\n", start - 1), source.lastIndexOf("\r", start - 1)) + 1;
147
+ const originalIndent = /^[ \t]*/u.exec(source.slice(lineStart, start))?.[0] ?? "";
148
+ replacements.push({ placeholder, value, kind: "opaqueString", originalIndent });
149
+ output += placeholder;
150
+ continue;
151
+ }
152
+ const previous = source[index - 1];
153
+ const scanned = (!previous || !isSourceIdentifierPart(previous)) ? scanStringLiteral(source, index) : null;
154
+ if (!scanned) {
155
+ output += source[index];
156
+ index += 1;
157
+ continue;
158
+ }
159
+ const start = index;
160
+ index = scanned.end;
161
+ const value = source.slice(start, index);
162
+ if (!value.includes("\n") && !value.includes("\r")) {
163
+ output += value;
164
+ continue;
165
+ }
166
+ let marker = `__velar_formatter_multiline_string_${replacements.length}__`;
167
+ while (source.includes(marker))
168
+ marker += "_";
169
+ const placeholder = JSON.stringify(marker);
170
+ const lineStart = Math.max(source.lastIndexOf("\n", start - 1), source.lastIndexOf("\r", start - 1)) + 1;
171
+ const originalIndent = /^[ \t]*/u.exec(source.slice(lineStart, start))?.[0] ?? "";
172
+ replacements.push({ placeholder, value, kind: scanned.layout ? "layout" : "opaqueString", originalIndent });
173
+ output += placeholder;
174
+ }
175
+ return {
176
+ text: output,
177
+ restore: (formatted) => replacements.reduce((current, replacement) => {
178
+ const marker = current.indexOf(replacement.placeholder);
179
+ if (marker < 0)
180
+ return current;
181
+ const lineStart = Math.max(current.lastIndexOf("\n", marker - 1), current.lastIndexOf("\r", marker - 1)) + 1;
182
+ const formattedIndent = /^[ \t]*/u.exec(current.slice(lineStart, marker))?.[0] ?? "";
183
+ const value = replacement.kind === "layout"
184
+ ? reindentLayoutLiteral(replacement.value, replacement.originalIndent, formattedIndent)
185
+ : replacement.kind === "blockComment"
186
+ ? reindentBlockComment(replacement.value, replacement.originalIndent, formattedIndent)
187
+ : replacement.value;
188
+ return `${current.slice(0, marker)}${value}${current.slice(marker + replacement.placeholder.length)}`;
189
+ }, formatted),
190
+ };
191
+ }
192
+ function scanFormattingOpaqueSource(source, start, extensionScanners) {
193
+ const core = scanEmbeddedJavaScriptLiteral(source, start);
194
+ let claimed = core
195
+ ? { end: core.end, attachedToPrevious: true }
196
+ : null;
197
+ for (const scan of extensionScanners) {
198
+ const candidate = scan(source, start);
199
+ if (!candidate)
200
+ continue;
201
+ if (!Number.isSafeInteger(candidate.end) || candidate.end <= start || candidate.end > source.length
202
+ || typeof candidate.attachedToPrevious !== "boolean") {
203
+ throw new RangeError("A compiler formatting opaque-source scanner returned an invalid result");
204
+ }
205
+ if (claimed && (claimed.end !== candidate.end || claimed.attachedToPrevious !== candidate.attachedToPrevious)) {
206
+ throw new Error("Multiple compiler formatting owners claimed the same opaque source with different boundaries");
207
+ }
208
+ claimed = candidate;
209
+ }
210
+ return claimed;
211
+ }
212
+ function reindentBlockComment(value, originalIndent, formattedIndent) {
213
+ const lines = value.split(/(\r\n|\r|\n)/u);
214
+ for (let index = 0; index < lines.length; index += 2) {
215
+ const line = lines[index];
216
+ if (index === 0)
217
+ continue;
218
+ if (line.startsWith(originalIndent))
219
+ lines[index] = `${formattedIndent}${line.slice(originalIndent.length)}`;
220
+ }
221
+ return lines.join("");
222
+ }
223
+ function blockCommentEnd(source, start) {
224
+ let index = start + 2;
225
+ let depth = 1;
226
+ while (index < source.length && depth > 0) {
227
+ if (source.startsWith("/*", index)) {
228
+ depth += 1;
229
+ index += 2;
230
+ }
231
+ else if (source.startsWith("*/", index)) {
232
+ depth -= 1;
233
+ index += 2;
234
+ }
235
+ else {
236
+ index += 1;
237
+ }
238
+ }
239
+ return index;
240
+ }
241
+ function reindentLayoutLiteral(value, originalIndent, formattedIndent) {
242
+ const lines = [];
243
+ let cursor = 0;
244
+ while (cursor < value.length) {
245
+ const boundary = /\r\n|\r|\n/gu;
246
+ boundary.lastIndex = cursor;
247
+ const match = boundary.exec(value);
248
+ const end = match?.index ?? value.length;
249
+ lines.push({ text: value.slice(cursor, end), newline: match?.[0] ?? "" });
250
+ if (!match)
251
+ break;
252
+ cursor = end + match[0].length;
253
+ }
254
+ const leading = (line) => /^[ \t]*/u.exec(line)?.[0] ?? "";
255
+ const width = (indent) => [...indent].reduce((total, character) => total + (character === "\t" ? 4 : 1), 0);
256
+ const contentMargin = lines.slice(1, -1)
257
+ .map((line) => line.text)
258
+ .find((line) => line.trim().length > 0);
259
+ const originalMargin = contentMargin === undefined ? null : leading(contentMargin);
260
+ const marginWidth = originalMargin === null ? 0 : width(originalMargin);
261
+ const shiftedMarginWidth = Math.max(width(formattedIndent) + 1, marginWidth + width(formattedIndent) - width(originalIndent));
262
+ const formattedMargin = " ".repeat(shiftedMarginWidth);
263
+ return lines.map((line, index) => {
264
+ let text = line.text;
265
+ if (index === lines.length - 1) {
266
+ if (text.startsWith(originalIndent))
267
+ text = `${formattedIndent}${text.slice(originalIndent.length)}`;
268
+ }
269
+ else if (index > 0 && originalMargin !== null) {
270
+ // Blank layout-string lines participate in the literal without needing
271
+ // an indentation payload. Reintroducing the content margin here writes
272
+ // trailing spaces into otherwise canonical source and makes a formatter
273
+ // result fail `git diff --check`.
274
+ if (text.trim().length === 0)
275
+ text = "";
276
+ else if (text.startsWith(originalMargin))
277
+ text = `${formattedMargin}${text.slice(originalMargin.length)}`;
278
+ }
279
+ return `${text}${line.newline}`;
280
+ }).join("");
281
+ }
282
+ function isChainContinuationLine(content) {
283
+ const member = content.startsWith("?.") ? content[2] : content[1];
284
+ return (content.startsWith(".") || content.startsWith("?.")) && Boolean(member && isSourceIdentifierStart(member));
285
+ }
286
+ function nextEmbeddedDepth(source, currentDepth, embedding) {
287
+ if (!embedding)
288
+ return 0;
289
+ let depth = currentDepth;
290
+ let index = 0;
291
+ if (depth === 0) {
292
+ const start = embeddedStart(source);
293
+ if (start === -1)
294
+ return 0;
295
+ index = start;
296
+ }
297
+ while (index < source.length) {
298
+ if (source.startsWith("<!--", index)) {
299
+ const end = source.indexOf("-->", index + 4);
300
+ if (end === -1)
301
+ return depth;
302
+ index = end + 3;
303
+ continue;
304
+ }
305
+ if (source.startsWith("/*", index)) {
306
+ index = blockCommentEnd(source, index);
307
+ continue;
308
+ }
309
+ if (source[index] !== "<" || !/[A-Za-z/>]/u.test(source[index + 1] ?? "")) {
310
+ index += 1;
311
+ continue;
312
+ }
313
+ const closing = source[index + 1] === "/";
314
+ let cursor = index + (closing ? 2 : 1);
315
+ const nameStart = cursor;
316
+ while (/[A-Za-z0-9_.:-]/u.test(source[cursor] ?? ""))
317
+ cursor += 1;
318
+ const name = source.slice(nameStart, cursor);
319
+ const fragment = name === "" && source[cursor] === ">";
320
+ if (!name && !fragment) {
321
+ index += 1;
322
+ continue;
323
+ }
324
+ let quote = "";
325
+ let braces = 0;
326
+ while (cursor < source.length) {
327
+ const character = source[cursor++];
328
+ if (quote) {
329
+ if (character === "\\")
330
+ cursor += 1;
331
+ else if (character === quote)
332
+ quote = "";
333
+ }
334
+ else if (character === '"' || character === "'")
335
+ quote = character;
336
+ else if (character === "{")
337
+ braces += 1;
338
+ else if (character === "}")
339
+ braces = Math.max(0, braces - 1);
340
+ else if (character === ">" && braces === 0)
341
+ break;
342
+ }
343
+ const tag = source.slice(index, cursor);
344
+ const selfClosing = /\/\s*>$/u.test(tag) || embedding.voidElements?.has(name) === true;
345
+ if (closing)
346
+ depth = Math.max(0, depth - 1);
347
+ else if (!selfClosing)
348
+ depth += 1;
349
+ index = cursor;
350
+ }
351
+ return depth;
352
+ }
353
+ function embeddedStart(source) {
354
+ for (let index = 0; index < source.length - 1; index += 1) {
355
+ if (source[index] !== "<" || !/[A-Za-z>]/u.test(source[index + 1] ?? ""))
356
+ continue;
357
+ const prefix = source.slice(0, index).trimEnd();
358
+ if (prefix === "" || /(?:\breturn|=>|=|\(|\[|\{|,|:|\?)$/u.test(prefix))
359
+ return index;
360
+ }
361
+ return -1;
362
+ }
363
+ function formatInline(source, embedding, layout = heldLayoutFor(embedding)) {
364
+ return formatInlineLine(source, embedding, layout).text;
365
+ }
366
+ /**
367
+ * D59 rule 143, fourth item: the formatter reads one physical line at a time,
368
+ * so a line that opens with `+` or `-` used to have no token in front of it at
369
+ * all and was read as a negation — `basePrice` on one line and `+ shipping` on
370
+ * the next came back as `+shipping`, against the charter's own example. Inside
371
+ * brackets a newline is not a statement boundary (charter §2), so the token in
372
+ * front is simply on the previous line: the caller carries it across, and the
373
+ * unary question is answered from the same position every other one is. It also
374
+ * answers the case with the opposite result, where a list literal's `-1` on its
375
+ * own line follows the `[` that opened it and stays a negation.
376
+ */
377
+ function formatInlineLine(source, embedding, layout = heldLayoutFor(embedding), preceding = undefined) {
378
+ const tokens = tokenizeInline(source, embedding, layout);
379
+ if (tokens.length === 0)
380
+ return { text: "", trailing: undefined };
381
+ let output = "";
382
+ for (let index = 0; index < tokens.length; index += 1) {
383
+ const token = tokens[index];
384
+ const previous = tokens[index - 1];
385
+ const next = tokens[index + 1];
386
+ if (previous && needsSpace(previous, token, next, tokens, index, preceding))
387
+ output += " ";
388
+ output += token.element
389
+ ? renderMarkupElement(token.element, layout, layout.column + lastLineWidth(output))
390
+ : token.text;
391
+ }
392
+ // A comment is not part of the expression it sits next to, so it never
393
+ // becomes the context the next line reads.
394
+ return { text: output, trailing: tokens.findLast((token) => token.kind !== "comment") };
395
+ }
396
+ function lastLineWidth(output) {
397
+ const start = output.lastIndexOf("\n");
398
+ return start === -1 ? output.length : output.length - start - 1;
399
+ }
400
+ function tokenizeInline(source, embedding, layout = heldLayoutFor(embedding)) {
401
+ const tokens = [];
402
+ const genericStack = [];
403
+ let index = 0;
404
+ while (index < source.length) {
405
+ const character = source[index];
406
+ if (character === " " || character === "\t") {
407
+ index += 1;
408
+ continue;
409
+ }
410
+ if (character === "/" && source[index + 1] === "/") {
411
+ tokens.push({ kind: "comment", text: source.slice(index).trimEnd() });
412
+ break;
413
+ }
414
+ if (character === "/" && source[index + 1] === "*") {
415
+ const end = blockCommentEnd(source, index);
416
+ tokens.push({ kind: "comment", text: source.slice(index, end) });
417
+ index = end;
418
+ continue;
419
+ }
420
+ const scannedString = scanStringLiteral(source, index);
421
+ if (scannedString) {
422
+ const start = index;
423
+ index = scannedString.end;
424
+ const formattedString = scannedString.interpolated
425
+ ? formatInterpolatedString(source, start, scannedString, embedding)
426
+ : source.slice(start, index);
427
+ tokens.push({
428
+ kind: "string",
429
+ text: canonicalizeInlineString(formattedString),
430
+ });
431
+ continue;
432
+ }
433
+ if (embedding && character === "<" && beginsEmbeddedAngleSyntax(tokens, source, index)) {
434
+ // D39 §54: an element that both opens and closes on this line is the
435
+ // formatter's to shape. Anything else — an element whose children live on
436
+ // later lines — stays exactly as the author laid it out.
437
+ const scanned = scanMarkupElement(source, index, embedding, layout);
438
+ if (scanned) {
439
+ tokens.push({ kind: "markup", text: source.slice(index, scanned.end), element: scanned.element });
440
+ index = scanned.end;
441
+ continue;
442
+ }
443
+ tokens.push({ kind: "embedded", text: source.slice(index).trimEnd() });
444
+ break;
445
+ }
446
+ if (isSourceIdentifierStart(character)) {
447
+ const start = index++;
448
+ while (index < source.length && isSourceIdentifierPart(source[index]))
449
+ index += 1;
450
+ const value = source.slice(start, index);
451
+ tokens.push({ kind: value === "true" || value === "false" || value === "null" ? "literal" : "word", text: value });
452
+ continue;
453
+ }
454
+ if (/[0-9]/u.test(character)) {
455
+ const start = index++;
456
+ if (character === "0" && /[xXbBoO]/u.test(source[index] ?? "")) {
457
+ index += 1;
458
+ while (index < source.length && /[A-Za-z0-9_]/u.test(source[index]))
459
+ index += 1;
460
+ tokens.push({ kind: "literal", text: source.slice(start, index) });
461
+ continue;
462
+ }
463
+ while (index < source.length && /[0-9_]/u.test(source[index]))
464
+ index += 1;
465
+ if (source[index] === "." && /[0-9_]/u.test(source[index + 1] ?? "")) {
466
+ index += 1;
467
+ while (index < source.length && /[0-9_]/u.test(source[index]))
468
+ index += 1;
469
+ }
470
+ if ((source[index] === "e" || source[index] === "E") && /[+\-0-9_]/u.test(source[index + 1] ?? "")) {
471
+ index += 1;
472
+ if (source[index] === "+" || source[index] === "-")
473
+ index += 1;
474
+ while (index < source.length && /[0-9_]/u.test(source[index]))
475
+ index += 1;
476
+ }
477
+ while (index < source.length && /[A-Za-z%]/u.test(source[index]))
478
+ index += 1;
479
+ tokens.push({ kind: "literal", text: source.slice(start, index) });
480
+ continue;
481
+ }
482
+ if (character === ">" && genericStack.at(-1) === true) {
483
+ genericStack.pop();
484
+ tokens.push({ kind: "close", text: character, generic: true });
485
+ index += 1;
486
+ continue;
487
+ }
488
+ const operator = multiCharacterOperators.find((candidate) => source.startsWith(candidate, index));
489
+ if (operator) {
490
+ tokens.push({ kind: operator === "?." ? "dot" : "operator", text: operator });
491
+ index += operator.length;
492
+ continue;
493
+ }
494
+ if (character === "@") {
495
+ tokens.push({ kind: "at", text: character });
496
+ index += 1;
497
+ continue;
498
+ }
499
+ if (character === ".") {
500
+ tokens.push({ kind: "dot", text: character });
501
+ index += 1;
502
+ continue;
503
+ }
504
+ if (character === ",") {
505
+ tokens.push({ kind: "comma", text: character });
506
+ index += 1;
507
+ continue;
508
+ }
509
+ if (character === ":") {
510
+ tokens.push({ kind: "colon", text: character });
511
+ index += 1;
512
+ continue;
513
+ }
514
+ if (character === "(" || character === "[" || character === "{") {
515
+ tokens.push({ kind: "open", text: character });
516
+ index += 1;
517
+ continue;
518
+ }
519
+ if (character === ")" || character === "]" || character === "}") {
520
+ tokens.push({ kind: "close", text: character });
521
+ index += 1;
522
+ continue;
523
+ }
524
+ if (character === "<") {
525
+ const previous = tokens.at(-1);
526
+ const generic = previous?.kind === "word"
527
+ && (genericStack.at(-1) === true
528
+ || beginsTypeBracket(tokens)
529
+ || (opensAnnotatedType(tokens) && closesAsTypeArguments(source, index)));
530
+ if (generic)
531
+ genericStack.push(true);
532
+ tokens.push({ kind: generic ? "open" : "operator", text: character, generic });
533
+ index += 1;
534
+ continue;
535
+ }
536
+ if (character === ">") {
537
+ tokens.push({ kind: "operator", text: character });
538
+ index += 1;
539
+ continue;
540
+ }
541
+ if ("=+-*/%?|".includes(character)) {
542
+ tokens.push({ kind: "operator", text: character });
543
+ index += 1;
544
+ continue;
545
+ }
546
+ tokens.push({ kind: "operator", text: character });
547
+ index += 1;
548
+ }
549
+ return tokens;
550
+ }
551
+ // Structural generic-bracket detection: '<' after the name of a def/type/class
552
+ // declaration or after a type-only operator opens a bracket; every other
553
+ // expression-position '<' stays a comparison.
554
+ const typeBracketDeclarationWords = new Set(["def", "type", "class"]);
555
+ const typeBracketOperators = new Set(["->", "|", "is", "case", "extends"]);
556
+ function beginsTypeBracket(tokens) {
557
+ const before = tokens.at(-2);
558
+ if (!before)
559
+ return false;
560
+ if (before.kind === "word" && typeBracketDeclarationWords.has(before.text))
561
+ return true;
562
+ if (before.text === "not" && tokens.at(-3)?.text === "is")
563
+ return true;
564
+ return typeBracketOperators.has(before.text);
565
+ }
566
+ /**
567
+ * D55 rule 127.2 / D57 rule 134: which '<' opens a type argument list is a
568
+ * question about position, never about the name in front of it. A whitelist of
569
+ * generic names — the shape this used to have — is blind to `Record<T>` today
570
+ * and to every generic a program declares for itself tomorrow, so the two
571
+ * remaining type positions are read structurally instead.
572
+ *
573
+ * The first is the annotation a ':' introduces (`x: Record<string>` as a
574
+ * parameter, a field, or a `const`), reached by walking back over the words and
575
+ * dots the type itself owns so modifiers such as `readonly` do not hide it.
576
+ * The second is the target of a type alias, where everything right of '=' on a
577
+ * `type` line is type syntax.
578
+ */
579
+ function opensAnnotatedType(tokens) {
580
+ let index = tokens.length - 2;
581
+ while (index >= 0) {
582
+ const token = tokens[index];
583
+ if (token.kind !== "word" && token.kind !== "dot")
584
+ break;
585
+ if (typeBracketOperators.has(token.text))
586
+ break;
587
+ index -= 1;
588
+ }
589
+ const introducer = tokens[index];
590
+ if (!introducer)
591
+ return false;
592
+ if (introducer.kind === "colon")
593
+ return true;
594
+ if (typeBracketOperators.has(introducer.text))
595
+ return true;
596
+ return introducer.text === "=" && isTypeAliasLine(tokens);
597
+ }
598
+ function isTypeAliasLine(tokens) {
599
+ const head = tokens[0]?.text;
600
+ return head === "type" || (head === "export" && tokens[1]?.text === "type");
601
+ }
602
+ /**
603
+ * The annotation position is the one type position a comparison also occupies,
604
+ * so the bracket has to prove itself: a type argument list closes on this line
605
+ * with nothing between the brackets but type syntax. `{visible: count < limit}`
606
+ * never closes, and `{ok: a < b and c > d}` carries a word no type argument
607
+ * list can hold.
608
+ *
609
+ * A function type carries its parameter names, so the `:` of `List<(x: number)
610
+ * -> string>` is type syntax too — but only inside the parameter list it names
611
+ * a parameter in. At the top of the argument list a `:` is the one in
612
+ * `{visible: count < limit, other: x > y}`, which is a record and two
613
+ * comparisons, so the paren depth is what separates them.
614
+ */
615
+ function closesAsTypeArguments(source, start) {
616
+ let depth = 0;
617
+ let parenthesized = 0;
618
+ let index = start;
619
+ while (index < source.length) {
620
+ const character = source[index];
621
+ if (character === "<") {
622
+ depth += 1;
623
+ index += 1;
624
+ continue;
625
+ }
626
+ if (character === ">") {
627
+ // The close is the close whatever follows it: an author may write
628
+ // `const values: List<number>=[1, 2, 3]` and expect canonical spacing.
629
+ depth -= 1;
630
+ index += 1;
631
+ if (depth === 0)
632
+ return true;
633
+ continue;
634
+ }
635
+ if (source.startsWith("->", index)) {
636
+ index += 2;
637
+ continue;
638
+ }
639
+ if (isSourceIdentifierStart(character)) {
640
+ const wordStart = index;
641
+ index += 1;
642
+ while (index < source.length && isSourceIdentifierPart(source[index]))
643
+ index += 1;
644
+ const word = source.slice(wordStart, index);
645
+ if (binaryWords.has(word) || prefixWords.has(word))
646
+ return false;
647
+ continue;
648
+ }
649
+ if (character === "(") {
650
+ parenthesized += 1;
651
+ index += 1;
652
+ continue;
653
+ }
654
+ if (character === ")") {
655
+ parenthesized -= 1;
656
+ if (parenthesized < 0)
657
+ return false;
658
+ index += 1;
659
+ continue;
660
+ }
661
+ if (character === ":" && parenthesized > 0) {
662
+ index += 1;
663
+ continue;
664
+ }
665
+ if (" \t,.?|".includes(character)) {
666
+ index += 1;
667
+ continue;
668
+ }
669
+ return false;
670
+ }
671
+ return false;
672
+ }
673
+ function formatInterpolatedString(source, start, scanned, embedding) {
674
+ if (!scanned.closed)
675
+ return source.slice(start, scanned.end);
676
+ let output = source.slice(start, scanned.contentStart);
677
+ let index = scanned.contentStart;
678
+ while (index < scanned.contentEnd) {
679
+ const character = source[index];
680
+ const next = source[index + 1];
681
+ if (!scanned.raw && character === "\\" && next !== undefined) {
682
+ const escaped = scanStringEscape(source, index, scanned.contentEnd);
683
+ output += source.slice(index, escaped.end);
684
+ index = escaped.end;
685
+ continue;
686
+ }
687
+ if ((character === "{" || character === "}") && next === character) {
688
+ output += `${character}${next}`;
689
+ index += 2;
690
+ continue;
691
+ }
692
+ if (character === "$" && next === "{") {
693
+ const close = source.indexOf("}", index + 2);
694
+ const end = close < 0 || close >= scanned.contentEnd ? Math.min(scanned.contentEnd, index + 2) : close + 1;
695
+ output += source.slice(index, end);
696
+ index = end;
697
+ continue;
698
+ }
699
+ if (character !== "{") {
700
+ output += character;
701
+ index += 1;
702
+ continue;
703
+ }
704
+ const close = findInterpolatedExpressionEnd(source, index + 1, scanned.contentEnd);
705
+ if (close < 0)
706
+ return source.slice(start, scanned.end);
707
+ output += `{${formatInline(source.slice(index + 1, close).trim(), embedding)}}`;
708
+ index = close + 1;
709
+ }
710
+ return `${output}${source.slice(scanned.contentEnd, scanned.end)}`;
711
+ }
712
+ function canonicalizeInlineString(source) {
713
+ let scanned = scanStringLiteral(source, 0);
714
+ if (!scanned || !scanned.closed || scanned.layout || scanned.quote === "'")
715
+ return source;
716
+ const protectedEscapes = scanned.raw ? null : protectCanonicalEscapes(source, scanned);
717
+ const working = protectedEscapes?.source ?? source;
718
+ if (protectedEscapes) {
719
+ const rescanned = scanStringLiteral(working, 0);
720
+ if (!rescanned)
721
+ return source;
722
+ scanned = rescanned;
723
+ }
724
+ const chunks = splitCanonicalStringChunks(working, scanned);
725
+ if (!chunks)
726
+ return source;
727
+ const literal = chunks.filter((chunk) => chunk.kind !== "expression").map((chunk) => chunk.value).join("");
728
+ const doubleQuotes = [...literal].filter((character) => character === '"').length;
729
+ const backticks = [...literal].filter((character) => character === "`").length;
730
+ const quote = doubleQuotes === 0
731
+ ? '"'
732
+ : backticks === 0
733
+ ? "`"
734
+ : backticks < doubleQuotes
735
+ ? "`"
736
+ : '"';
737
+ const body = chunks.map((chunk) => chunk.kind === "expression"
738
+ ? chunk.value
739
+ : encodeCanonicalStringText(chunk.value, quote, scanned.raw, scanned.interpolated, chunk.kind === "templateText"))
740
+ .join("");
741
+ const formatted = `${scanned.prefix}${quote}${body}${quote}`;
742
+ return protectedEscapes?.restore(formatted) ?? formatted;
743
+ }
744
+ function protectCanonicalEscapes(source, scanned) {
745
+ const replacements = [];
746
+ let output = source.slice(0, scanned.contentStart);
747
+ let index = scanned.contentStart;
748
+ while (index < scanned.contentEnd) {
749
+ if (source[index] !== "\\") {
750
+ output += source[index];
751
+ index += 1;
752
+ continue;
753
+ }
754
+ const escaped = scanStringEscape(source, index, scanned.contentEnd);
755
+ const spelling = source.slice(index, escaped.end);
756
+ const preserve = escaped.error === null
757
+ && (spelling === "\\n" || spelling === "\\r" || spelling === "\\t" || spelling === "\\\\" || spelling.startsWith("\\u{"));
758
+ if (!preserve) {
759
+ output += spelling;
760
+ index = escaped.end;
761
+ continue;
762
+ }
763
+ let codePoint = 0xe000 + replacements.length;
764
+ let marker = String.fromCodePoint(codePoint);
765
+ while (source.includes(marker) || replacements.some((item) => item.marker === marker)) {
766
+ codePoint += 1;
767
+ marker = String.fromCodePoint(codePoint);
768
+ }
769
+ replacements.push({ marker, value: spelling });
770
+ output += marker;
771
+ index = escaped.end;
772
+ }
773
+ output += source.slice(scanned.contentEnd);
774
+ return {
775
+ source: output,
776
+ restore: (value) => replacements.reduce((current, item) => current.replaceAll(item.marker, item.value), value),
777
+ };
778
+ }
779
+ function splitCanonicalStringChunks(source, scanned) {
780
+ const chunks = [];
781
+ let cursor = scanned.contentStart;
782
+ let textStart = cursor;
783
+ const flush = (end) => {
784
+ if (end <= textStart)
785
+ return true;
786
+ const decoded = decodeCanonicalStringText(source.slice(textStart, end), scanned, true);
787
+ if (decoded === null)
788
+ return false;
789
+ chunks.push({ kind: "text", value: decoded });
790
+ return true;
791
+ };
792
+ if (!scanned.interpolated) {
793
+ const decoded = decodeCanonicalStringText(scanned.content, scanned, false);
794
+ return decoded === null ? null : [{ kind: "text", value: decoded }];
795
+ }
796
+ while (cursor < scanned.contentEnd) {
797
+ const character = source[cursor];
798
+ const next = source[cursor + 1];
799
+ if (!scanned.raw && character === "\\") {
800
+ cursor = scanStringEscape(source, cursor, scanned.contentEnd).end;
801
+ continue;
802
+ }
803
+ if (scanned.raw && character === scanned.quote && next === scanned.quote) {
804
+ cursor += 2;
805
+ continue;
806
+ }
807
+ if ((character === "{" || character === "}") && next === character) {
808
+ cursor += 2;
809
+ continue;
810
+ }
811
+ if (character === "$" && next === "{") {
812
+ if (!flush(cursor))
813
+ return null;
814
+ const close = source.indexOf("}", cursor + 2);
815
+ const end = close < 0 || close >= scanned.contentEnd ? Math.min(scanned.contentEnd, cursor + 2) : close + 1;
816
+ const decoded = decodeCanonicalStringText(source.slice(cursor, end), scanned, false);
817
+ if (decoded === null)
818
+ return null;
819
+ chunks.push({ kind: "templateText", value: decoded });
820
+ cursor = end;
821
+ textStart = cursor;
822
+ continue;
823
+ }
824
+ if (character === "{") {
825
+ const close = findInterpolatedExpressionEnd(source, cursor + 1, scanned.contentEnd);
826
+ if (close < 0)
827
+ return null;
828
+ if (!flush(cursor))
829
+ return null;
830
+ chunks.push({ kind: "expression", value: source.slice(cursor, close + 1) });
831
+ cursor = close + 1;
832
+ textStart = cursor;
833
+ continue;
834
+ }
835
+ cursor += 1;
836
+ }
837
+ if (!flush(scanned.contentEnd))
838
+ return null;
839
+ return chunks;
840
+ }
841
+ function decodeCanonicalStringText(value, scanned, collapseBraces) {
842
+ let output = "";
843
+ for (let index = 0; index < value.length; index += 1) {
844
+ const character = value[index];
845
+ const next = value[index + 1];
846
+ if (scanned.raw && character === scanned.quote && next === scanned.quote) {
847
+ output += character;
848
+ index += 1;
849
+ }
850
+ else if (!scanned.raw && character === "\\") {
851
+ const escaped = scanStringEscape(value, index);
852
+ if (escaped.error !== null || escaped.value === null)
853
+ return null;
854
+ output += escaped.value;
855
+ index = escaped.end - 1;
856
+ }
857
+ else if (collapseBraces && scanned.interpolated
858
+ && (character === "{" || character === "}") && next === character) {
859
+ output += character;
860
+ index += 1;
861
+ }
862
+ else {
863
+ output += character;
864
+ }
865
+ }
866
+ return output;
867
+ }
868
+ function encodeCanonicalStringText(value, quote, raw, interpolated, templateText) {
869
+ let output = "";
870
+ for (const character of value) {
871
+ const codePoint = character.codePointAt(0);
872
+ if (raw) {
873
+ if (character === quote)
874
+ output += `${quote}${quote}`;
875
+ else if (interpolated && !templateText && (character === "{" || character === "}"))
876
+ output += `${character}${character}`;
877
+ else
878
+ output += character;
879
+ continue;
880
+ }
881
+ if (character === "\\")
882
+ output += "\\\\";
883
+ else if (character === "\n")
884
+ output += "\\n";
885
+ else if (character === "\r")
886
+ output += "\\r";
887
+ else if (character === "\t")
888
+ output += "\\t";
889
+ else if (character === quote)
890
+ output += `\\${quote}`;
891
+ else if (interpolated && !templateText && (character === "{" || character === "}"))
892
+ output += `${character}${character}`;
893
+ else if ((codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f)
894
+ || (codePoint >= 0x202a && codePoint <= 0x202e) || (codePoint >= 0x2066 && codePoint <= 0x2069))) {
895
+ output += `\\u{${codePoint.toString(16).toUpperCase()}}`;
896
+ }
897
+ else
898
+ output += character;
899
+ }
900
+ return output;
901
+ }
902
+ /**
903
+ * D57 rule 134, restated for this file: the spacing questions below are all one
904
+ * question — does the token in front end an expression? What follows something
905
+ * that ends one is applied to it (`values[0]` indexes, `f(x)` calls, `a - b`
906
+ * subtracts, `a < b` compares); what follows anything else begins a fresh
907
+ * expression (`const [head, ...tail]` destructures, `async (id) =>` takes
908
+ * parameters, `return -1` negates, `?? <em>x</em>` is markup).
909
+ *
910
+ * The tokens that can end an expression are a closed structural set: a name, a
911
+ * literal, a string, a closing bracket, an element. The words that cannot are
912
+ * the language's own reserved vocabulary, read from the lexer's table rather
913
+ * than kept here. The hand-kept list this replaced was blind to `const` and
914
+ * `let` (D59 143.1), to `async` (143.2), and to `return` in front of an
915
+ * operator (143.3), and a word the language gains tomorrow would have gone
916
+ * missing from it the same way.
917
+ */
918
+ function endsExpression(token, statementHead = false) {
919
+ if (!token)
920
+ return false;
921
+ switch (token.kind) {
922
+ case "word":
923
+ // `match` and `case` are keywords only at the head of a statement line
924
+ // (D30 item 16); anywhere else the same spelling is an ordinary name.
925
+ if (statementHead && statementHeadKeywordWords.has(token.text))
926
+ return false;
927
+ return !nonExpressionKeywordWords.has(token.text);
928
+ case "literal":
929
+ case "string":
930
+ case "close":
931
+ case "markup":
932
+ case "embedded":
933
+ return true;
934
+ default:
935
+ return false;
936
+ }
937
+ }
938
+ /**
939
+ * D60 rule 147: whether a `<` opens embedded markup is the same question. An
940
+ * element stands where an expression can begin — after `??`, after `and`, after
941
+ * a comma, at the head of a line — and a comparison stands after something that
942
+ * ends an expression. Reading a list of the positions instead is what wrote
943
+ * `{text ?? <em>x</em>}` out as `{text ?? < em > x < / em >}`, which no longer
944
+ * compiles.
945
+ */
946
+ function beginsEmbeddedAngleSyntax(tokens, source, index) {
947
+ if (!/[A-Za-z>]/u.test(source[index + 1] ?? ""))
948
+ return false;
949
+ return !endsExpression(tokens.at(-1), tokens.length === 1);
950
+ }
951
+ /**
952
+ * D59 rule 142 — `name=value` is one argument, and the charter and every
953
+ * documentation table spell it tight. Which `=` separates a named argument is a
954
+ * question about position: the name has to open an argument (it follows the
955
+ * call's `(`, or a `,` inside it) and the parentheses have to be a call's. The
956
+ * other `=` that stands inside parentheses is a default value, and a default
957
+ * value's parentheses belong to a declaration or a lambda, never to a call.
958
+ */
959
+ function isNamedArgumentEquals(tokens, index) {
960
+ const equals = tokens[index];
961
+ if (!equals || equals.kind !== "operator" || equals.text !== "=")
962
+ return false;
963
+ if (tokens[index - 1]?.kind !== "word")
964
+ return false;
965
+ const opener = tokens[index - 2];
966
+ if (!opener || (opener.kind !== "comma" && !(opener.kind === "open" && opener.text === "(")))
967
+ return false;
968
+ const open = enclosingParenIndex(tokens, index - 2);
969
+ if (open < 0)
970
+ return false;
971
+ return !isDeclarationParameterList(tokens, open) && endsExpression(tokens[open - 1], open === 1);
972
+ }
973
+ /** The index of the `(` whose argument list `index` sits directly inside. */
974
+ function enclosingParenIndex(tokens, index) {
975
+ let depth = 0;
976
+ for (let cursor = index; cursor >= 0; cursor -= 1) {
977
+ const token = tokens[cursor];
978
+ if (token.kind === "close")
979
+ depth += 1;
980
+ else if (token.kind === "open") {
981
+ if (depth === 0)
982
+ return token.text === "(" ? cursor : -1;
983
+ depth -= 1;
984
+ }
985
+ }
986
+ return -1;
987
+ }
988
+ /**
989
+ * A declaration's parentheses hold parameters, and a parameter's `= value` is a
990
+ * default. Three positions say the list is a declaration's, and none of them
991
+ * needs to know which words introduce a declaration:
992
+ *
993
+ * - `def name(...)`, with or without a type argument list of its own.
994
+ * - Two names in a row in front of it. Nothing applies one name to another in
995
+ * an expression, so `component Row(...)` and `action submit(...)` are
996
+ * declaration headers while `check(...)` and `if check(...)` are calls —
997
+ * including the declaration forms an extension owns, which this file cannot
998
+ * otherwise see.
999
+ * - A single name at the head of a line whose `)` opens a block, which is
1000
+ * `constructor(...):` and nothing a call can be.
1001
+ */
1002
+ function isDeclarationParameterList(tokens, open) {
1003
+ let name = open - 1;
1004
+ const typeArguments = tokens[name];
1005
+ if (typeArguments?.kind === "close" && typeArguments.generic === true) {
1006
+ name = matchingGenericOpenIndex(tokens, name) - 1;
1007
+ }
1008
+ if (name < 0 || tokens[name]?.kind !== "word")
1009
+ return false;
1010
+ const introducer = tokens[name - 1];
1011
+ if (introducer?.text === "def")
1012
+ return true;
1013
+ if (introducer?.kind === "word" && endsExpression(introducer, name === 1))
1014
+ return true;
1015
+ const close = matchingCloseIndex(tokens, open);
1016
+ return name === 0 && close >= 0 && tokens[close + 1]?.kind === "colon";
1017
+ }
1018
+ function matchingGenericOpenIndex(tokens, close) {
1019
+ let depth = 0;
1020
+ for (let cursor = close; cursor >= 0; cursor -= 1) {
1021
+ const token = tokens[cursor];
1022
+ if (token.kind === "close" && token.generic === true)
1023
+ depth += 1;
1024
+ else if (token.kind === "open" && token.generic === true) {
1025
+ depth -= 1;
1026
+ if (depth === 0)
1027
+ return cursor;
1028
+ }
1029
+ }
1030
+ return -1;
1031
+ }
1032
+ function matchingCloseIndex(tokens, open) {
1033
+ let depth = 0;
1034
+ for (let cursor = open; cursor < tokens.length; cursor += 1) {
1035
+ const token = tokens[cursor];
1036
+ if (token.kind === "open")
1037
+ depth += 1;
1038
+ else if (token.kind === "close") {
1039
+ depth -= 1;
1040
+ if (depth === 0)
1041
+ return cursor;
1042
+ }
1043
+ }
1044
+ return -1;
1045
+ }
1046
+ function needsSpace(previous, current, next, tokens, index, preceding) {
1047
+ if (isAttachedOpaqueSourcePlaceholder(current))
1048
+ return false;
1049
+ if (current.kind === "comment")
1050
+ return true;
1051
+ if (previous.kind === "comment")
1052
+ return !previous.text.startsWith("//");
1053
+ if (current.kind === "embedded" || current.kind === "markup") {
1054
+ // D60 rule 147: markup is an argument like any other after a `,` or a `:`,
1055
+ // so it keeps the separator's space; only an opening bracket sits tight
1056
+ // against it. A named argument's value is the one exception, because
1057
+ // `name=value` is written as one thing.
1058
+ if (previous.text === "=" && isNamedArgumentEquals(tokens, index - 1))
1059
+ return false;
1060
+ return previous.kind !== "open";
1061
+ }
1062
+ if (current.kind === "comma" || current.kind === "close" || current.kind === "dot" || current.kind === "colon") {
1063
+ if (current.kind === "colon" && isTernaryColon(tokens, index))
1064
+ return true;
1065
+ return false;
1066
+ }
1067
+ if (previous.kind === "dot" || previous.kind === "at")
1068
+ return false;
1069
+ if (current.kind === "at")
1070
+ return previous.kind !== "open" && previous.kind !== "operator";
1071
+ if (previous.kind === "comma" || previous.kind === "colon")
1072
+ return true;
1073
+ if (previous.kind === "open")
1074
+ return false;
1075
+ if (current.kind === "open") {
1076
+ if (current.text === "(" && previous.text === "js" && tokens[0]?.text === "extern")
1077
+ return false;
1078
+ // A named argument's value is written against its name whatever the value
1079
+ // is — `initial=0`, `combine=(total, value) => …`, `value={type: "bool"}`.
1080
+ if (previous.text === "=" && isNamedArgumentEquals(tokens, index - 1))
1081
+ return false;
1082
+ if (current.text === "{")
1083
+ return true;
1084
+ if (current.generic)
1085
+ return false;
1086
+ // A member name is a name even when it is spelled like a keyword —
1087
+ // `values.in(other)`, `values.case[0]` — so the dot decides, not the word.
1088
+ if (tokens[index - 2]?.kind === "dot")
1089
+ return false;
1090
+ // D51 item NEW-D9, now derived: `[` and `(` after something that ends an
1091
+ // expression apply to it — `values[0]`, `format(value)`. After a keyword
1092
+ // they open a fresh one — `const [head, ...tail]`, `for i in [1, 2]`,
1093
+ // `async (id: string) =>`. The whitelist this replaced had `in` but never
1094
+ // `const`, and `--check` then enforced the shape it wrote.
1095
+ return !endsExpression(previous, index === 1);
1096
+ }
1097
+ if (previous.kind === "close" && previous.generic)
1098
+ return current.text !== "?";
1099
+ if (previous.kind === "operator" || current.kind === "operator") {
1100
+ if (current.text === "=" && isNamedArgumentEquals(tokens, index))
1101
+ return false;
1102
+ if (previous.text === "=" && isNamedArgumentEquals(tokens, index - 1))
1103
+ return false;
1104
+ if (previous.text === "..." || current.text === "...")
1105
+ return false;
1106
+ if (current.text === "?" && isOptionalQuestion(current, next, tokens[index + 2]))
1107
+ return false;
1108
+ if (previous.text === "?" && isOptionalQuestion(previous, current, next))
1109
+ return true;
1110
+ if (isUnaryOperator(previous, index >= 2 ? tokens[index - 2] : preceding, index === 2)) {
1111
+ return previous.text === "not" || previous.text === "await";
1112
+ }
1113
+ if (isUnaryOperator(current, previous, index === 1))
1114
+ return true;
1115
+ return true;
1116
+ }
1117
+ if ((previous.kind === "word" && (binaryWords.has(previous.text) || prefixWords.has(previous.text)))
1118
+ || (current.kind === "word" && binaryWords.has(current.text)))
1119
+ return true;
1120
+ return true;
1121
+ }
1122
+ function isAttachedOpaqueSourcePlaceholder(token) {
1123
+ return token.kind === "string" && token.text.includes("__velar_formatter_attached_opaque_source_");
1124
+ }
1125
+ function isUnaryOperator(token, previous, statementHead = false) {
1126
+ if (prefixWords.has(token.text))
1127
+ return true;
1128
+ if (token.text === "~")
1129
+ return true;
1130
+ if (token.text !== "+" && token.text !== "-")
1131
+ return false;
1132
+ return !endsExpression(previous, statementHead);
1133
+ }
1134
+ function isOptionalQuestion(token, next, after) {
1135
+ if (token.text !== "?")
1136
+ return false;
1137
+ return !next || next.kind === "comma" || next.kind === "close" || next.kind === "colon"
1138
+ || next.text === "=" || next.text === "|" || after?.text === "=>";
1139
+ }
1140
+ function isTernaryColon(tokens, colonIndex) {
1141
+ let depth = 0;
1142
+ for (let index = colonIndex - 1; index >= 0; index -= 1) {
1143
+ const token = tokens[index];
1144
+ if (token.kind === "close")
1145
+ depth += 1;
1146
+ else if (token.kind === "open")
1147
+ depth -= 1;
1148
+ else if (depth === 0 && token.text === "?" && !isOptionalQuestion(token, tokens[index + 1], tokens[index + 2]))
1149
+ return true;
1150
+ else if (depth === 0 && (token.kind === "colon" || token.kind === "comma"))
1151
+ return false;
1152
+ }
1153
+ return false;
1154
+ }
1155
+ /**
1156
+ * D39 §54 — the canonical shape of embedded angle-bracket markup.
1157
+ *
1158
+ * The formatter reflows one thing and only one thing: an element that both
1159
+ * opens and closes on a single physical line. Such an element is written on one
1160
+ * line while it fits inside the print width, and takes the block shape — open
1161
+ * tag, one child per line indented one level, closing tag at the element's own
1162
+ * indentation — as soon as it does not. Attributes follow the same rule one
1163
+ * level down: they stay on the open tag until the open tag alone overflows,
1164
+ * and then take one line each.
1165
+ *
1166
+ * Two rules keep this a layout change and never a rendering change:
1167
+ *
1168
+ * - Whitespace between children is program text. Markup drops a line break
1169
+ * with its surrounding indentation but keeps a written space, so an element
1170
+ * whose children carry meaningful spaces is never broken, and text is never
1171
+ * re-wrapped or re-spaced.
1172
+ * - Markup the author already spread across lines keeps its line structure,
1173
+ * exactly like every other construct in the language: the formatter
1174
+ * canonicalizes spelling, not the author's line breaks.
1175
+ */
1176
+ const MARKUP_PRINT_WIDTH = 120;
1177
+ const MAX_MARKUP_DEPTH = 48;
1178
+ /**
1179
+ * The layout of markup that cannot take a line of its own — inside a string
1180
+ * interpolation, or inside a `{...}` hole. It still carries the embedding, so
1181
+ * markup nested further in is recognized as markup rather than re-spaced as
1182
+ * comparison operators.
1183
+ */
1184
+ function heldLayoutFor(embedding) {
1185
+ return { indentWidth: 4, column: 0, breakable: false, embedding };
1186
+ }
1187
+ function markupLayout(indentWidth, column, embedding) {
1188
+ return { indentWidth, column, breakable: true, embedding };
1189
+ }
1190
+ function heldMarkupLayout(layout) {
1191
+ return { ...layout, breakable: false };
1192
+ }
1193
+ /**
1194
+ * Reads one balanced element starting at `<`. It returns null the moment the
1195
+ * element is not complete and unambiguous within `source` — an unclosed
1196
+ * element, a mismatched closing tag, an HTML comment, an unterminated string or
1197
+ * expression — and the caller then leaves the text exactly as written.
1198
+ */
1199
+ function scanMarkupElement(source, start, embedding, layout, depth = 0) {
1200
+ if (depth > MAX_MARKUP_DEPTH || source[start] !== "<")
1201
+ return null;
1202
+ let index = start + 1;
1203
+ const nameStart = index;
1204
+ while (index < source.length && /[A-Za-z0-9_.:-]/u.test(source[index]))
1205
+ index += 1;
1206
+ const tag = source.slice(nameStart, index);
1207
+ if (!tag && source[index] !== ">")
1208
+ return null;
1209
+ const attributes = [];
1210
+ let selfClosing = false;
1211
+ let closed = false;
1212
+ while (index < source.length) {
1213
+ while (index < source.length && /\s/u.test(source[index]))
1214
+ index += 1;
1215
+ if (source.startsWith("/>", index)) {
1216
+ index += 2;
1217
+ selfClosing = true;
1218
+ closed = true;
1219
+ break;
1220
+ }
1221
+ if (source[index] === ">") {
1222
+ index += 1;
1223
+ closed = true;
1224
+ break;
1225
+ }
1226
+ if (source[index] === "{") {
1227
+ const end = findInterpolatedExpressionEnd(source, index + 1);
1228
+ if (end < 0)
1229
+ return null;
1230
+ attributes.push({ name: "", value: source.slice(index, end + 1) });
1231
+ index = end + 1;
1232
+ continue;
1233
+ }
1234
+ const attributeStart = index;
1235
+ while (index < source.length && /[A-Za-z0-9_.:-]/u.test(source[index]))
1236
+ index += 1;
1237
+ const name = source.slice(attributeStart, index);
1238
+ if (!name)
1239
+ return null;
1240
+ let cursor = index;
1241
+ while (cursor < source.length && /[ \t]/u.test(source[cursor]))
1242
+ cursor += 1;
1243
+ if (source[cursor] !== "=") {
1244
+ attributes.push({ name, value: null });
1245
+ continue;
1246
+ }
1247
+ cursor += 1;
1248
+ while (cursor < source.length && /[ \t]/u.test(source[cursor]))
1249
+ cursor += 1;
1250
+ if (source[cursor] === '"' || source[cursor] === "'") {
1251
+ const quote = source[cursor];
1252
+ const close = source.indexOf(quote, cursor + 1);
1253
+ if (close < 0)
1254
+ return null;
1255
+ attributes.push({ name, value: source.slice(cursor, close + 1) });
1256
+ index = close + 1;
1257
+ continue;
1258
+ }
1259
+ if (source[cursor] === "{") {
1260
+ const end = findInterpolatedExpressionEnd(source, cursor + 1);
1261
+ if (end < 0)
1262
+ return null;
1263
+ attributes.push({ name, value: source.slice(cursor, end + 1) });
1264
+ index = end + 1;
1265
+ continue;
1266
+ }
1267
+ return null;
1268
+ }
1269
+ if (!closed)
1270
+ return null;
1271
+ if (selfClosing || (tag !== "" && tag === tag.toLowerCase() && embedding?.voidElements?.has(tag) === true)) {
1272
+ return { element: { tag, attributes, children: [], selfClosing: true }, end: index };
1273
+ }
1274
+ const children = [];
1275
+ while (index < source.length && !source.startsWith("</", index)) {
1276
+ if (source.startsWith("<!--", index))
1277
+ return null;
1278
+ if (source[index] === "<") {
1279
+ const child = scanMarkupElement(source, index, embedding, layout, depth + 1);
1280
+ if (!child)
1281
+ return null;
1282
+ children.push({ kind: "element", element: child.element });
1283
+ index = child.end;
1284
+ continue;
1285
+ }
1286
+ if (source[index] === "{") {
1287
+ const end = findInterpolatedExpressionEnd(source, index + 1);
1288
+ if (end < 0)
1289
+ return null;
1290
+ children.push({ kind: "expression", text: source.slice(index, end + 1) });
1291
+ index = end + 1;
1292
+ continue;
1293
+ }
1294
+ const textStart = index;
1295
+ while (index < source.length && source[index] !== "<" && source[index] !== "{")
1296
+ index += 1;
1297
+ children.push({ kind: "text", text: source.slice(textStart, index) });
1298
+ }
1299
+ if (!source.startsWith("</", index))
1300
+ return null;
1301
+ index += 2;
1302
+ const closingStart = index;
1303
+ while (index < source.length && /[A-Za-z0-9_.:-]/u.test(source[index]))
1304
+ index += 1;
1305
+ if (source.slice(closingStart, index) !== tag)
1306
+ return null;
1307
+ while (index < source.length && /\s/u.test(source[index]))
1308
+ index += 1;
1309
+ if (source[index] !== ">")
1310
+ return null;
1311
+ return { element: { tag, attributes, children, selfClosing: false }, end: index + 1 };
1312
+ }
1313
+ function renderMarkupElement(element, layout, column) {
1314
+ const inline = renderInlineMarkup(element, layout);
1315
+ if (!layout.breakable || column + inline.length <= MARKUP_PRINT_WIDTH)
1316
+ return inline;
1317
+ if (element.selfClosing)
1318
+ return renderMarkupOpenTag(element, layout, column);
1319
+ if (!isBreakableMarkup(element))
1320
+ return inline;
1321
+ const indent = " ".repeat(layout.column);
1322
+ const childIndent = " ".repeat(layout.column + layout.indentWidth);
1323
+ const childLayout = markupLayout(layout.indentWidth, layout.column + layout.indentWidth, layout.embedding);
1324
+ const lines = [renderMarkupOpenTag(element, layout, column)];
1325
+ for (const child of element.children) {
1326
+ if (child.kind === "text") {
1327
+ const text = child.text.trim();
1328
+ if (text.length > 0)
1329
+ lines.push(`${childIndent}${text}`);
1330
+ continue;
1331
+ }
1332
+ lines.push(`${childIndent}${child.kind === "element"
1333
+ ? renderMarkupElement(child.element, childLayout, childLayout.column)
1334
+ : renderMarkupExpression(child.text, childLayout)}`);
1335
+ }
1336
+ lines.push(`${indent}</${element.tag}>`);
1337
+ return lines.join("\n");
1338
+ }
1339
+ function renderMarkupOpenTag(element, layout, column) {
1340
+ const inline = `<${element.tag}${element.attributes.map((attribute) => ` ${renderMarkupAttribute(attribute, layout)}`).join("")}${element.selfClosing ? " />" : ">"}`;
1341
+ if (!layout.breakable || column + inline.length <= MARKUP_PRINT_WIDTH || element.attributes.length === 0)
1342
+ return inline;
1343
+ const indent = " ".repeat(layout.column);
1344
+ const attributeIndent = " ".repeat(layout.column + layout.indentWidth);
1345
+ const attributeLayout = markupLayout(layout.indentWidth, layout.column + layout.indentWidth, layout.embedding);
1346
+ return [
1347
+ `<${element.tag}`,
1348
+ ...element.attributes.map((attribute) => `${attributeIndent}${renderMarkupAttribute(attribute, attributeLayout)}`),
1349
+ `${indent}${element.selfClosing ? "/>" : ">"}`,
1350
+ ].join("\n");
1351
+ }
1352
+ function renderInlineMarkup(element, layout) {
1353
+ const open = `<${element.tag}${element.attributes.map((attribute) => ` ${renderMarkupAttribute(attribute, layout)}`).join("")}`;
1354
+ if (element.selfClosing)
1355
+ return `${open} />`;
1356
+ const children = element.children.map((child) => child.kind === "text"
1357
+ ? child.text
1358
+ : child.kind === "element"
1359
+ ? renderInlineMarkup(child.element, layout)
1360
+ : renderMarkupExpression(child.text, layout)).join("");
1361
+ return `${open}>${children}</${element.tag}>`;
1362
+ }
1363
+ function renderMarkupAttribute(attribute, layout) {
1364
+ if (attribute.name === "")
1365
+ return renderMarkupExpression(attribute.value ?? "{}", layout);
1366
+ if (attribute.value === null)
1367
+ return attribute.name;
1368
+ if (!attribute.value.startsWith("{"))
1369
+ return `${attribute.name}=${attribute.value}`;
1370
+ return `${attribute.name}=${renderMarkupExpression(attribute.value, layout)}`;
1371
+ }
1372
+ /** Formats the code inside `{...}`; a hole never breaks across lines. */
1373
+ function renderMarkupExpression(text, layout) {
1374
+ return `{${formatInline(text.slice(1, -1).trim(), layout.embedding, heldMarkupLayout(layout))}}`;
1375
+ }
1376
+ /**
1377
+ * An element breaks between children only when it has no text child at all —
1378
+ * when it is a container of elements and holes rather than a piece of written
1379
+ * content.
1380
+ *
1381
+ * That is one line drawn for two reasons at once. It is the safe line: markup
1382
+ * renders a written space between children but not a line break with its
1383
+ * indentation, so any text child (even a bare "/" separator) could change what
1384
+ * the page shows if the boundaries around it moved. It is also the readable
1385
+ * line: a sentence belongs on its line, not spread one word and one hole at a
1386
+ * time.
1387
+ */
1388
+ function isBreakableMarkup(element) {
1389
+ if (element.selfClosing || element.children.length === 0)
1390
+ return false;
1391
+ return element.children.every((child) => child.kind !== "text" || child.text.trim() === child.text);
1392
+ }
1393
+ /**
1394
+ * Formats a line inside markup the author spread across lines. The line's own
1395
+ * layout is the author's; each balanced element on it still takes its canonical
1396
+ * shape, and everything else — code inside holes, text, unbalanced tag
1397
+ * fragments — is copied exactly.
1398
+ */
1399
+ function formatEmbeddedContent(source, embedding, layout, column, depth = 0) {
1400
+ if (!embedding || depth > MAX_MARKUP_DEPTH)
1401
+ return source;
1402
+ let output = "";
1403
+ let index = 0;
1404
+ while (index < source.length) {
1405
+ const character = source[index];
1406
+ if (character === '"' || character === "'" || character === "`") {
1407
+ const scanned = scanStringLiteral(source, index);
1408
+ const end = scanned && scanned.end > index ? scanned.end : index + 1;
1409
+ output += source.slice(index, end);
1410
+ index = end;
1411
+ continue;
1412
+ }
1413
+ if (character === "{") {
1414
+ const end = findInterpolatedExpressionEnd(source, index + 1);
1415
+ if (end < 0) {
1416
+ output += character;
1417
+ index += 1;
1418
+ continue;
1419
+ }
1420
+ // A hole is code, and the formatter never reflows code: markup inside it
1421
+ // keeps its line, exactly as it does on a statement line.
1422
+ const innerColumn = column + lastLineWidth(output) + 1;
1423
+ output += `{${formatEmbeddedContent(source.slice(index + 1, end), embedding, heldMarkupLayout(layout), innerColumn, depth + 1)}}`;
1424
+ index = end + 1;
1425
+ continue;
1426
+ }
1427
+ if (character === "<" && /[A-Za-z>]/u.test(source[index + 1] ?? "")) {
1428
+ const scanned = scanMarkupElement(source, index, embedding, layout);
1429
+ if (scanned) {
1430
+ output += renderMarkupElement(scanned.element, layout, column + lastLineWidth(output));
1431
+ index = scanned.end;
1432
+ continue;
1433
+ }
1434
+ }
1435
+ output += character;
1436
+ index += 1;
1437
+ }
1438
+ return output;
1439
+ }
1440
+ //# sourceMappingURL=formatter.js.map