@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.
- package/LICENSE +201 -0
- package/README.md +70 -0
- package/dist/analyzer.d.ts +1175 -0
- package/dist/analyzer.d.ts.map +1 -0
- package/dist/analyzer.js +12130 -0
- package/dist/analyzer.js.map +1 -0
- package/dist/application-package-host.d.ts +28 -0
- package/dist/application-package-host.d.ts.map +1 -0
- package/dist/application-package-host.js +2 -0
- package/dist/application-package-host.js.map +1 -0
- package/dist/ast.d.ts +853 -0
- package/dist/ast.d.ts.map +1 -0
- package/dist/ast.js +245 -0
- package/dist/ast.js.map +1 -0
- package/dist/class-runtime.d.ts +6 -0
- package/dist/class-runtime.d.ts.map +1 -0
- package/dist/class-runtime.js +45 -0
- package/dist/class-runtime.js.map +1 -0
- package/dist/collection-lowering-runtime.d.ts +7 -0
- package/dist/collection-lowering-runtime.d.ts.map +1 -0
- package/dist/collection-lowering-runtime.js +952 -0
- package/dist/collection-lowering-runtime.js.map +1 -0
- package/dist/collection-runtime.d.ts +16 -0
- package/dist/collection-runtime.d.ts.map +1 -0
- package/dist/collection-runtime.js +231 -0
- package/dist/collection-runtime.js.map +1 -0
- package/dist/core-vocabulary.d.ts +141 -0
- package/dist/core-vocabulary.d.ts.map +1 -0
- package/dist/core-vocabulary.js +88 -0
- package/dist/core-vocabulary.js.map +1 -0
- package/dist/diagnostic.d.ts +42 -0
- package/dist/diagnostic.d.ts.map +1 -0
- package/dist/diagnostic.js +66 -0
- package/dist/diagnostic.js.map +1 -0
- package/dist/embedded-javascript.d.ts +74 -0
- package/dist/embedded-javascript.d.ts.map +1 -0
- package/dist/embedded-javascript.js +341 -0
- package/dist/embedded-javascript.js.map +1 -0
- package/dist/embedded-module.d.ts +11 -0
- package/dist/embedded-module.d.ts.map +1 -0
- package/dist/embedded-module.js +67 -0
- package/dist/embedded-module.js.map +1 -0
- package/dist/embedded-source.d.ts +19 -0
- package/dist/embedded-source.d.ts.map +1 -0
- package/dist/embedded-source.js +61 -0
- package/dist/embedded-source.js.map +1 -0
- package/dist/emitter.d.ts +188 -0
- package/dist/emitter.d.ts.map +1 -0
- package/dist/emitter.js +3254 -0
- package/dist/emitter.js.map +1 -0
- package/dist/error-runtime.d.ts +17 -0
- package/dist/error-runtime.d.ts.map +1 -0
- package/dist/error-runtime.js +109 -0
- package/dist/error-runtime.js.map +1 -0
- package/dist/extension.d.ts +373 -0
- package/dist/extension.d.ts.map +1 -0
- package/dist/extension.js +25 -0
- package/dist/extension.js.map +1 -0
- package/dist/formatter.d.ts +13 -0
- package/dist/formatter.d.ts.map +1 -0
- package/dist/formatter.js +1440 -0
- package/dist/formatter.js.map +1 -0
- package/dist/framework-host.d.ts +87 -0
- package/dist/framework-host.d.ts.map +1 -0
- package/dist/framework-host.js +2 -0
- package/dist/framework-host.js.map +1 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +932 -0
- package/dist/index.js.map +1 -0
- package/dist/interpolated-string.d.ts +47 -0
- package/dist/interpolated-string.d.ts.map +1 -0
- package/dist/interpolated-string.js +344 -0
- package/dist/interpolated-string.js.map +1 -0
- package/dist/json-runtime.d.ts +2 -0
- package/dist/json-runtime.d.ts.map +1 -0
- package/dist/json-runtime.js +193 -0
- package/dist/json-runtime.js.map +1 -0
- package/dist/language-guidance.d.ts +31 -0
- package/dist/language-guidance.d.ts.map +1 -0
- package/dist/language-guidance.js +187 -0
- package/dist/language-guidance.js.map +1 -0
- package/dist/lexer.d.ts +85 -0
- package/dist/lexer.d.ts.map +1 -0
- package/dist/lexer.js +1004 -0
- package/dist/lexer.js.map +1 -0
- package/dist/limits.d.ts +2 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +2 -0
- package/dist/limits.js.map +1 -0
- package/dist/mechanical-fix.d.ts +26 -0
- package/dist/mechanical-fix.d.ts.map +1 -0
- package/dist/mechanical-fix.js +61 -0
- package/dist/mechanical-fix.js.map +1 -0
- package/dist/narrowing-runtime.d.ts +5 -0
- package/dist/narrowing-runtime.d.ts.map +1 -0
- package/dist/narrowing-runtime.js +27 -0
- package/dist/narrowing-runtime.js.map +1 -0
- package/dist/number-runtime.d.ts +3 -0
- package/dist/number-runtime.d.ts.map +1 -0
- package/dist/number-runtime.js +37 -0
- package/dist/number-runtime.js.map +1 -0
- package/dist/parser.d.ts +245 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +3582 -0
- package/dist/parser.js.map +1 -0
- package/dist/primitive-runtime.d.ts +4 -0
- package/dist/primitive-runtime.d.ts.map +1 -0
- package/dist/primitive-runtime.js +39 -0
- package/dist/primitive-runtime.js.map +1 -0
- package/dist/promise-runtime.d.ts +6 -0
- package/dist/promise-runtime.d.ts.map +1 -0
- package/dist/promise-runtime.js +86 -0
- package/dist/promise-runtime.js.map +1 -0
- package/dist/reactive-bridge-runtime.d.ts +7 -0
- package/dist/reactive-bridge-runtime.d.ts.map +1 -0
- package/dist/reactive-bridge-runtime.js +112 -0
- package/dist/reactive-bridge-runtime.js.map +1 -0
- package/dist/runtime-abi.d.ts +24 -0
- package/dist/runtime-abi.d.ts.map +1 -0
- package/dist/runtime-abi.js +24 -0
- package/dist/runtime-abi.js.map +1 -0
- package/dist/semantic.d.ts +142 -0
- package/dist/semantic.d.ts.map +1 -0
- package/dist/semantic.js +845 -0
- package/dist/semantic.js.map +1 -0
- package/dist/source-names.d.ts +32 -0
- package/dist/source-names.d.ts.map +1 -0
- package/dist/source-names.js +101 -0
- package/dist/source-names.js.map +1 -0
- package/dist/source.d.ts +19 -0
- package/dist/source.d.ts.map +1 -0
- package/dist/source.js +64 -0
- package/dist/source.js.map +1 -0
- package/dist/text-runtime.d.ts +2 -0
- package/dist/text-runtime.d.ts.map +1 -0
- package/dist/text-runtime.js +285 -0
- package/dist/text-runtime.js.map +1 -0
- package/dist/token.d.ts +19 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +51 -0
- package/dist/token.js.map +1 -0
- package/dist/type-registry-runtime.d.ts +7 -0
- package/dist/type-registry-runtime.d.ts.map +1 -0
- package/dist/type-registry-runtime.js +51 -0
- package/dist/type-registry-runtime.js.map +1 -0
- package/dist/type-validation-runtime.d.ts +17 -0
- package/dist/type-validation-runtime.d.ts.map +1 -0
- package/dist/type-validation-runtime.js +150 -0
- package/dist/type-validation-runtime.js.map +1 -0
- package/dist/types.d.ts +356 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1170 -0
- package/dist/types.js.map +1 -0
- package/dist/utf8-runtime.d.ts +7 -0
- package/dist/utf8-runtime.d.ts.map +1 -0
- package/dist/utf8-runtime.js +43 -0
- package/dist/utf8-runtime.js.map +1 -0
- 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
|