@deckbuilderkit/editor 0.0.0-stage → 0.1.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/package.json +53 -4
- package/src/ArchiveView.tsx +111 -0
- package/src/CodeEditor.tsx +265 -0
- package/src/CodeView.tsx +67 -0
- package/src/DiffView.tsx +139 -0
- package/src/EnvView.tsx +42 -0
- package/src/FilePreview.tsx +108 -0
- package/src/FontView.tsx +48 -0
- package/src/HarView.tsx +70 -0
- package/src/HexView.tsx +28 -0
- package/src/HtmlPreview.tsx +23 -0
- package/src/MarkdownEditor.tsx +90 -0
- package/src/MarkdownToc.tsx +135 -0
- package/src/MarkdownView.tsx +84 -0
- package/src/MediaView.tsx +17 -0
- package/src/NotebookView.tsx +74 -0
- package/src/PdfView.tsx +289 -0
- package/src/RequestEditor.tsx +419 -0
- package/src/ResponseView.tsx +89 -0
- package/src/SqlGraph.tsx +322 -0
- package/src/SqlSteps.tsx +487 -0
- package/src/SqliteView.tsx +68 -0
- package/src/SvgView.tsx +231 -0
- package/src/TableView.tsx +85 -0
- package/src/VarSuggest.tsx +125 -0
- package/src/curl-file.ts +70 -0
- package/src/editorOptions.ts +83 -0
- package/src/editorOptionsContext.ts +4 -0
- package/src/fileKinds.ts +69 -0
- package/src/http-client.ts +9 -0
- package/src/http-import.ts +594 -0
- package/src/http.ts +251 -0
- package/src/import-warning.ts +8 -0
- package/src/index.ts +56 -0
- package/src/languages.ts +107 -0
- package/src/markdown.ts +123 -0
- package/src/markdownLive.ts +289 -0
- package/src/options.ts +3 -0
- package/src/pdf-url.d.ts +4 -0
- package/src/sql/format.ts +637 -0
- package/src/sql/formatOptions.ts +71 -0
- package/src/sql/lineage.ts +365 -0
- package/src/sql/lint.ts +951 -0
- package/src/sql/markerHighlight.ts +50 -0
- package/src/sql/markers.ts +138 -0
- package/src/sql/outline.ts +662 -0
- package/src/sql/statements.ts +83 -0
- package/src/sql/tokens.ts +156 -0
- package/src/sql/xmlQuery.ts +249 -0
- package/src/sqlProblems.ts +40 -0
- package/src/table.ts +89 -0
- package/src/text-save.ts +28 -0
- package/src/theme.ts +86 -0
- package/src/toc.ts +11 -0
- package/src/variables.ts +123 -0
- package/README.md +0 -3
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
import type { SqlFormatOptions } from './formatOptions.ts';
|
|
2
|
+
export { DEFAULT_FORMAT_OPTIONS, FORMAT_OPTION_META, type FormatOptionMeta, type SqlFormatOptions } from './formatOptions.ts';
|
|
3
|
+
import { isPlsqlStart, splitStatements } from './statements.ts';
|
|
4
|
+
import { tokenize, type Token } from './tokens.ts';
|
|
5
|
+
|
|
6
|
+
interface Item {
|
|
7
|
+
tok: Token;
|
|
8
|
+
comment: boolean;
|
|
9
|
+
own: boolean;
|
|
10
|
+
idx: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type Mode = 'select' | 'from' | 'cond' | 'set' | 'with' | 'insert' | 'inline' | 'list';
|
|
14
|
+
|
|
15
|
+
interface Clause {
|
|
16
|
+
words: number;
|
|
17
|
+
mode: Mode;
|
|
18
|
+
deeper: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const CLAUSES: [string, Mode][] = [
|
|
22
|
+
['select', 'select'],
|
|
23
|
+
['from', 'from'],
|
|
24
|
+
['where', 'cond'],
|
|
25
|
+
['group by', 'list'],
|
|
26
|
+
['having', 'cond'],
|
|
27
|
+
['order siblings by', 'list'],
|
|
28
|
+
['order by', 'list'],
|
|
29
|
+
['union all', 'inline'],
|
|
30
|
+
['union', 'inline'],
|
|
31
|
+
['intersect', 'inline'],
|
|
32
|
+
['minus', 'inline'],
|
|
33
|
+
['except', 'inline'],
|
|
34
|
+
['connect by', 'cond'],
|
|
35
|
+
['start with', 'cond'],
|
|
36
|
+
['fetch', 'inline'],
|
|
37
|
+
['offset', 'inline'],
|
|
38
|
+
['limit', 'inline'],
|
|
39
|
+
['for update', 'inline'],
|
|
40
|
+
['insert into', 'insert'],
|
|
41
|
+
['values', 'inline'],
|
|
42
|
+
['update', 'inline'],
|
|
43
|
+
['set', 'set'],
|
|
44
|
+
['delete from', 'inline'],
|
|
45
|
+
['delete', 'inline'],
|
|
46
|
+
['returning', 'inline'],
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const CLAUSE_WORDS = CLAUSES.map(([text, mode]) => ({ words: text.split(' '), mode }));
|
|
50
|
+
const JOIN_MODIFIERS = new Set(['inner', 'left', 'right', 'full', 'cross', 'natural', 'outer']);
|
|
51
|
+
const DML_START = new Set(['select', 'with', 'insert', 'update', 'delete']);
|
|
52
|
+
const SPACE_BEFORE_PAREN = new Set([
|
|
53
|
+
'in', 'and', 'or', 'not', 'on', 'using', 'values', 'from', 'join', 'where', 'as', 'exists', 'any', 'all', 'some',
|
|
54
|
+
'between', 'then', 'else', 'when', 'select', 'set', 'by', 'like', 'over', 'partition', 'into', 'union', 'intersect',
|
|
55
|
+
'minus', 'except', 'having', 'case', 'is', 'with', 'distinct', 'update', 'delete', 'insert', 'returning', 'of',
|
|
56
|
+
'apply', 'lateral',
|
|
57
|
+
]);
|
|
58
|
+
const EXTRA_KEYWORDS = new Set(['over', 'null', 'offset', 'siblings', 'merge', 'matched']);
|
|
59
|
+
const VALUE_KEYWORDS = new Set(['end', 'null', 'true', 'false', 'level', 'user', 'rownum', 'rowid', 'sysdate']);
|
|
60
|
+
const KNOWN_OPERATORS = new Set(['||', '<=', '>=', '<>', '!=', '^=', '~=', ':=', '=>', '..', '**', '<<', '>>', '+', '-', '*', '/', '=', '<', '>', '!', '^', '~', '%', '|', ':']);
|
|
61
|
+
|
|
62
|
+
class Unformattable extends Error {}
|
|
63
|
+
|
|
64
|
+
class Printer {
|
|
65
|
+
private lines: string[] = [];
|
|
66
|
+
private text = '';
|
|
67
|
+
private started = false;
|
|
68
|
+
level = 0;
|
|
69
|
+
head = false;
|
|
70
|
+
breakAt: number | null = null;
|
|
71
|
+
|
|
72
|
+
constructor(private readonly unit: string) {}
|
|
73
|
+
|
|
74
|
+
has(): boolean {
|
|
75
|
+
return this.started;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
newline(level: number, head = false): void {
|
|
79
|
+
this.flush();
|
|
80
|
+
this.level = level;
|
|
81
|
+
this.head = head;
|
|
82
|
+
this.breakAt = null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
breakNext(level: number): void {
|
|
86
|
+
this.breakAt = level;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
word(text: string, space: boolean): void {
|
|
90
|
+
if (this.breakAt !== null) this.newline(this.breakAt);
|
|
91
|
+
if (this.started) this.text += (space ? ' ' : '') + text;
|
|
92
|
+
else {
|
|
93
|
+
this.started = true;
|
|
94
|
+
this.text = text;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
trail(text: string): void {
|
|
99
|
+
if (this.started) this.text += ' ' + text;
|
|
100
|
+
else this.word(text, false);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
finish(eol: string): string {
|
|
104
|
+
this.flush();
|
|
105
|
+
return this.lines.join(eol);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private flush(): void {
|
|
109
|
+
if (!this.started) return;
|
|
110
|
+
this.lines.push(this.unit.repeat(this.level) + this.text);
|
|
111
|
+
this.started = false;
|
|
112
|
+
this.text = '';
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const isBlank = (t: Token): boolean => t.kind === 'whitespace' || t.kind === 'newline';
|
|
117
|
+
const isPunct = (t: Token, text: string): boolean => t.kind === 'punctuation' && t.text === text;
|
|
118
|
+
|
|
119
|
+
function applyCase(text: string, mode: 'upper' | 'lower' | 'preserve'): string {
|
|
120
|
+
return mode === 'upper' ? text.toUpperCase() : mode === 'lower' ? text.toLowerCase() : text;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const caseOf = (text: string, o: SqlFormatOptions): string => applyCase(text, o.keywordCase);
|
|
124
|
+
|
|
125
|
+
const MERGING = new Set(['--', '/*', '*/']);
|
|
126
|
+
|
|
127
|
+
const isKeyword = (t: Token): boolean => t.kind === 'keyword' || (t.kind === 'identifier' && EXTRA_KEYWORDS.has(t.text.toLowerCase()));
|
|
128
|
+
|
|
129
|
+
function caseOnly(tokens: Token[], o: SqlFormatOptions): string {
|
|
130
|
+
let out = '';
|
|
131
|
+
let prev = '';
|
|
132
|
+
for (const t of tokens) {
|
|
133
|
+
out += isKeyword(t) && prev !== '.' ? caseOf(t.text, o) : t.text;
|
|
134
|
+
if (!isBlank(t)) prev = t.text;
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
class Layout {
|
|
140
|
+
private i = 0;
|
|
141
|
+
private prev: Item | null = null;
|
|
142
|
+
private prevSig: Item | null = null;
|
|
143
|
+
private unary = false;
|
|
144
|
+
private binary = false;
|
|
145
|
+
private mode: Mode = 'inline';
|
|
146
|
+
private readonly p: Printer;
|
|
147
|
+
private readonly columns: number;
|
|
148
|
+
|
|
149
|
+
constructor(
|
|
150
|
+
private readonly items: Item[],
|
|
151
|
+
private readonly o: SqlFormatOptions,
|
|
152
|
+
mode: Mode = 'inline',
|
|
153
|
+
) {
|
|
154
|
+
this.mode = mode;
|
|
155
|
+
this.p = new Printer(o.indent === 'tab' ? '\t' : ' '.repeat(o.indent));
|
|
156
|
+
this.columns = Math.max(1, Math.floor(o.columnsPerLine) || 1);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
run(eol: string): string {
|
|
160
|
+
this.query(0);
|
|
161
|
+
if (this.i < this.items.length) throw new Unformattable();
|
|
162
|
+
return this.p.finish(eol);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private isWord(it: Item | undefined): it is Item {
|
|
166
|
+
return it !== undefined && !it.comment && (it.tok.kind === 'keyword' || it.tok.kind === 'identifier');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private lower(it: Item): string {
|
|
170
|
+
return it.tok.text.toLowerCase();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private closes(it: Item | undefined): boolean {
|
|
174
|
+
return it !== undefined && !it.comment && isPunct(it.tok, ')');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private nextNonComment(j: number): number {
|
|
178
|
+
while (this.items[j]?.comment) j++;
|
|
179
|
+
return j;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private clauseAt(j: number, first: boolean, afterDot = this.prevSig !== null && isPunct(this.prevSig.tok, '.')): Clause | null {
|
|
183
|
+
if (!this.isWord(this.items[j])) return null;
|
|
184
|
+
if (afterDot) return null;
|
|
185
|
+
const w: string[] = [];
|
|
186
|
+
for (let k = j; k < j + 4 && this.isWord(this.items[k]); k++) w.push(this.lower(this.items[k]!));
|
|
187
|
+
if (first && w[0] === 'with') return { words: 1, mode: 'with', deeper: false };
|
|
188
|
+
let m = 0;
|
|
189
|
+
while (m < w.length && JOIN_MODIFIERS.has(w[m]!)) m++;
|
|
190
|
+
if (m < w.length && (w[m] === 'join' || w[m] === 'apply')) return { words: m + 1, mode: 'inline', deeper: false };
|
|
191
|
+
if (w[0] === 'on') return { words: 1, mode: 'cond', deeper: true };
|
|
192
|
+
if (w[0] === 'using') return { words: 1, mode: 'inline', deeper: true };
|
|
193
|
+
for (const c of CLAUSE_WORDS) {
|
|
194
|
+
if (c.words.every((word, k) => w[k] === word)) return { words: c.words.length, mode: c.mode, deeper: false };
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private leading(prev: Item | null): boolean {
|
|
200
|
+
if (!prev) return true;
|
|
201
|
+
const t = prev.tok;
|
|
202
|
+
if (t.kind === 'punctuation') return t.text === '(' || t.text === ',' || t.text === '.';
|
|
203
|
+
if (t.kind === 'operator') return true;
|
|
204
|
+
if (t.kind === 'keyword') return !VALUE_KEYWORDS.has(t.text.toLowerCase());
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private parenSpace(prev: Item): boolean {
|
|
209
|
+
if (prev.comment) return true;
|
|
210
|
+
const t = prev.tok;
|
|
211
|
+
if (t.kind === 'keyword' || t.kind === 'identifier') {
|
|
212
|
+
if (SPACE_BEFORE_PAREN.has(t.text.toLowerCase())) return true;
|
|
213
|
+
return t.kind === 'identifier' && this.mode === 'insert';
|
|
214
|
+
}
|
|
215
|
+
if (t.kind === 'quotedIdentifier') return this.mode === 'insert';
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private spaceBefore(it: Item): boolean {
|
|
220
|
+
const prev = this.prev;
|
|
221
|
+
if (!prev) return false;
|
|
222
|
+
const t = it.tok;
|
|
223
|
+
if (!it.comment && t.kind === 'punctuation') {
|
|
224
|
+
if (t.text === ',' || t.text === ')' || t.text === '.' || t.text === '@' || t.text === ';') return false;
|
|
225
|
+
}
|
|
226
|
+
if (!prev.comment && prev.tok.kind === 'punctuation') {
|
|
227
|
+
const x = prev.tok.text;
|
|
228
|
+
if (x === '(' || x === '.' || x === '@') return false;
|
|
229
|
+
}
|
|
230
|
+
if (this.unary) return false;
|
|
231
|
+
if (!this.o.spaceAroundOperators && !it.comment && !prev.comment) {
|
|
232
|
+
const operator = t.kind === 'operator' && !this.leading(this.prevSig);
|
|
233
|
+
if ((operator || this.binary) && !this.merges(prev.tok, t)) return false;
|
|
234
|
+
}
|
|
235
|
+
if (!it.comment && isPunct(t, '(')) return this.parenSpace(prev);
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private merges(a: Token, b: Token): boolean {
|
|
240
|
+
if (a.kind !== 'operator' || b.kind !== 'operator') return false;
|
|
241
|
+
const pair = a.text + b.text;
|
|
242
|
+
return MERGING.has(pair) || KNOWN_OPERATORS.has(pair);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private isFunction(it: Item): boolean {
|
|
246
|
+
const t = it.tok;
|
|
247
|
+
if (t.kind !== 'identifier' || isKeyword(t)) return false;
|
|
248
|
+
if (this.mode === 'insert' || this.mode === 'with') return false;
|
|
249
|
+
const open = this.items[this.nextNonComment(it.idx + 1)];
|
|
250
|
+
if (!open || !isPunct(open.tok, '(')) return false;
|
|
251
|
+
const after = this.items[this.nextNonComment(open.idx + 1)];
|
|
252
|
+
if (after && after.tok.kind === 'operator' && after.tok.text === '+') return false;
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private emit(it: Item, force = false): void {
|
|
257
|
+
const t = it.tok;
|
|
258
|
+
const afterDot = this.prevSig !== null && isPunct(this.prevSig.tok, '.');
|
|
259
|
+
let text = t.text;
|
|
260
|
+
if (!it.comment) {
|
|
261
|
+
if ((force || isKeyword(t)) && !afterDot) text = caseOf(t.text, this.o);
|
|
262
|
+
else if (!force && this.isFunction(it)) text = applyCase(t.text, this.o.functionCase);
|
|
263
|
+
}
|
|
264
|
+
const binary = !it.comment && t.kind === 'operator' && !this.leading(this.prevSig);
|
|
265
|
+
let unary = false;
|
|
266
|
+
if (!it.comment && t.kind === 'operator' && (t.text === '+' || t.text === '-')) unary = this.leading(this.prevSig);
|
|
267
|
+
this.p.word(text, this.spaceBefore(it));
|
|
268
|
+
this.prev = it;
|
|
269
|
+
if (!it.comment) this.prevSig = it;
|
|
270
|
+
this.unary = unary;
|
|
271
|
+
this.binary = binary;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private comment(it: Item, look: ((j: number) => number | null) | null): void {
|
|
275
|
+
const t = it.tok;
|
|
276
|
+
const p = this.p;
|
|
277
|
+
if (!it.own) {
|
|
278
|
+
if (t.kind === 'lineComment') {
|
|
279
|
+
p.trail(t.text);
|
|
280
|
+
if (p.breakAt === null) p.breakNext(p.level + 1);
|
|
281
|
+
} else if (p.breakAt !== null) p.trail(t.text);
|
|
282
|
+
else {
|
|
283
|
+
this.emit(it);
|
|
284
|
+
this.i++;
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
} else {
|
|
288
|
+
const hint = look ? look(this.nextNonComment(this.i)) : null;
|
|
289
|
+
const level = hint ?? p.breakAt ?? p.level + (p.head ? 1 : 0);
|
|
290
|
+
p.newline(level);
|
|
291
|
+
p.word(t.text, false);
|
|
292
|
+
p.breakNext(level);
|
|
293
|
+
}
|
|
294
|
+
this.prev = it;
|
|
295
|
+
this.unary = false;
|
|
296
|
+
this.binary = false;
|
|
297
|
+
this.i++;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private query(indent: number, stay = false): void {
|
|
301
|
+
const saved = this.mode;
|
|
302
|
+
const items = this.items;
|
|
303
|
+
const p = this.p;
|
|
304
|
+
const look = (j: number): number | null => {
|
|
305
|
+
const nx = items[j];
|
|
306
|
+
if (!nx || this.closes(nx)) return indent;
|
|
307
|
+
const m = this.clauseAt(j, false);
|
|
308
|
+
if (m) return m.deeper ? indent + 1 : indent;
|
|
309
|
+
if (this.mode === 'cond' && this.o.conditionPerLine && this.o.andOrPosition === 'before' && this.isWord(nx) && ['and', 'or'].includes(this.lower(nx))) return indent + 1;
|
|
310
|
+
return null;
|
|
311
|
+
};
|
|
312
|
+
let col = 0;
|
|
313
|
+
let between = false;
|
|
314
|
+
let first = true;
|
|
315
|
+
let stayFirst = stay;
|
|
316
|
+
while (this.i < items.length) {
|
|
317
|
+
const it = items[this.i]!;
|
|
318
|
+
if (it.comment) {
|
|
319
|
+
this.comment(it, look);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const t = it.tok;
|
|
323
|
+
if (this.closes(it)) break;
|
|
324
|
+
if (isPunct(t, '(')) {
|
|
325
|
+
first = false;
|
|
326
|
+
this.paren();
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
const clause = this.clauseAt(this.i, first);
|
|
330
|
+
first = false;
|
|
331
|
+
if (clause) {
|
|
332
|
+
if (stayFirst) p.head = true;
|
|
333
|
+
else if (!clause.deeper || this.o.joinConditionOnNewLine) p.newline(clause.deeper ? indent + 1 : indent, true);
|
|
334
|
+
stayFirst = false;
|
|
335
|
+
for (let k = 0; k < clause.words; k++) this.emit(items[this.i++]!, true);
|
|
336
|
+
this.mode = clause.mode;
|
|
337
|
+
col = 1;
|
|
338
|
+
between = false;
|
|
339
|
+
if (clause.mode === 'select') this.selectHead(indent);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (this.isWord(it)) {
|
|
343
|
+
const w = this.lower(it);
|
|
344
|
+
if (w === 'case') {
|
|
345
|
+
const listLevel = this.mode === 'select' ? indent + 1 : null;
|
|
346
|
+
if (this.caseExpr(listLevel) && listLevel !== null) col = this.columns;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (this.mode === 'cond') {
|
|
350
|
+
if (w === 'between') between = true;
|
|
351
|
+
else if (w === 'and' && between) between = false;
|
|
352
|
+
else if (w === 'and' || w === 'or') {
|
|
353
|
+
const split = this.o.conditionPerLine;
|
|
354
|
+
if (split && this.o.andOrPosition === 'before') p.newline(indent + 1);
|
|
355
|
+
this.emit(it);
|
|
356
|
+
this.i++;
|
|
357
|
+
if (split && this.o.andOrPosition === 'after') p.breakNext(indent + 1);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
this.emit(it);
|
|
363
|
+
this.i++;
|
|
364
|
+
if (isPunct(t, ',')) {
|
|
365
|
+
if (this.mode === 'select') {
|
|
366
|
+
if (col >= this.columns) {
|
|
367
|
+
p.breakNext(indent + 1);
|
|
368
|
+
col = 1;
|
|
369
|
+
} else col++;
|
|
370
|
+
} else if (this.mode === 'from') {
|
|
371
|
+
if (this.o.fromTablesPerLine) p.breakNext(indent + 1);
|
|
372
|
+
} else if (this.mode === 'set') {
|
|
373
|
+
if (this.o.setItemsPerLine) p.breakNext(indent + 1);
|
|
374
|
+
} else if (this.mode === 'list') {
|
|
375
|
+
if (this.o.groupOrderListPerLine) p.breakNext(indent + 1);
|
|
376
|
+
} else if (this.mode === 'with') p.breakNext(indent);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
this.mode = saved;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
private selectHead(indent: number): void {
|
|
383
|
+
const items = this.items;
|
|
384
|
+
while (this.i < items.length) {
|
|
385
|
+
const it = items[this.i]!;
|
|
386
|
+
if (it.comment && !it.own && it.tok.kind === 'blockComment') this.emit(it);
|
|
387
|
+
else if (this.isWord(it) && ['distinct', 'all', 'unique'].includes(this.lower(it))) this.emit(it, true);
|
|
388
|
+
else break;
|
|
389
|
+
this.i++;
|
|
390
|
+
}
|
|
391
|
+
if (this.o.newlineAfterSelect && !(this.o.singleColumnInline && this.singleItem())) this.p.breakNext(indent + 1);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private singleItem(): boolean {
|
|
395
|
+
const items = this.items;
|
|
396
|
+
let depth = 0;
|
|
397
|
+
let last: Item | null = this.prevSig;
|
|
398
|
+
for (let j = this.i; j < items.length; j++) {
|
|
399
|
+
const it = items[j]!;
|
|
400
|
+
if (it.comment) continue;
|
|
401
|
+
if (isPunct(it.tok, '(')) depth++;
|
|
402
|
+
else if (isPunct(it.tok, ')')) {
|
|
403
|
+
if (depth === 0) break;
|
|
404
|
+
depth--;
|
|
405
|
+
} else if (depth === 0) {
|
|
406
|
+
if (isPunct(it.tok, ',')) return false;
|
|
407
|
+
if (this.clauseAt(j, false, last !== null && isPunct(last.tok, '.'))) break;
|
|
408
|
+
}
|
|
409
|
+
last = it;
|
|
410
|
+
}
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private paren(): void {
|
|
415
|
+
const items = this.items;
|
|
416
|
+
const next = items[this.nextNonComment(this.i + 1)];
|
|
417
|
+
const subquery = this.isWord(next) && (this.lower(next) === 'select' || this.lower(next) === 'with');
|
|
418
|
+
this.emit(items[this.i++]!);
|
|
419
|
+
if (subquery) {
|
|
420
|
+
const level = this.p.level;
|
|
421
|
+
const split = this.o.subqueryNewlineAfterParen;
|
|
422
|
+
if (split) this.p.breakNext(level + 1);
|
|
423
|
+
this.query(level + 1, !split);
|
|
424
|
+
if (!this.closes(items[this.i])) throw new Unformattable();
|
|
425
|
+
if (this.o.subqueryCloseParenOnOwnLine || this.p.breakAt !== null) this.p.newline(level);
|
|
426
|
+
} else {
|
|
427
|
+
this.inline();
|
|
428
|
+
}
|
|
429
|
+
this.emit(items[this.i++]!);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
private inline(): void {
|
|
433
|
+
const items = this.items;
|
|
434
|
+
while (this.i < items.length) {
|
|
435
|
+
const it = items[this.i]!;
|
|
436
|
+
if (it.comment) {
|
|
437
|
+
this.comment(it, null);
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
if (this.closes(it)) return;
|
|
441
|
+
if (isPunct(it.tok, '(')) {
|
|
442
|
+
this.paren();
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (this.isWord(it) && this.lower(it) === 'case') {
|
|
446
|
+
this.caseExpr();
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
this.emit(it);
|
|
450
|
+
this.i++;
|
|
451
|
+
}
|
|
452
|
+
throw new Unformattable();
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private caseEnd(): number {
|
|
456
|
+
const items = this.items;
|
|
457
|
+
let depth = 0;
|
|
458
|
+
for (let j = this.i; j < items.length; j++) {
|
|
459
|
+
const it = items[j]!;
|
|
460
|
+
if (!this.isWord(it)) continue;
|
|
461
|
+
const w = this.lower(it);
|
|
462
|
+
if (w === 'case') depth++;
|
|
463
|
+
else if (w === 'end' && --depth === 0) return j;
|
|
464
|
+
}
|
|
465
|
+
return -1;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private caseBreaks(): boolean {
|
|
469
|
+
const layout = this.o.caseLayout;
|
|
470
|
+
if (layout !== 'auto') return layout === 'always';
|
|
471
|
+
const end = this.caseEnd();
|
|
472
|
+
if (end < 0) throw new Unformattable();
|
|
473
|
+
const part = this.items.slice(this.i, end + 1);
|
|
474
|
+
for (const it of part) {
|
|
475
|
+
if (it.comment) return true;
|
|
476
|
+
if (isPunct(it.tok, '(')) {
|
|
477
|
+
const next = this.items[this.nextNonComment(it.idx + 1)];
|
|
478
|
+
if (this.isWord(next) && (this.lower(next) === 'select' || this.lower(next) === 'with')) return true;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const sub = new Layout(
|
|
482
|
+
part.map((it, k) => ({ ...it, idx: k })),
|
|
483
|
+
{ ...this.o, caseLayout: 'never' },
|
|
484
|
+
this.mode,
|
|
485
|
+
);
|
|
486
|
+
sub.caseExpr(null);
|
|
487
|
+
const width = Number.isFinite(this.o.caseMaxWidth) ? Math.max(1, Math.floor(this.o.caseMaxWidth)) : 60;
|
|
488
|
+
return sub.p.finish('\n').length > width;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private atItemStart(): boolean {
|
|
492
|
+
const prev = this.prevSig;
|
|
493
|
+
if (!prev) return true;
|
|
494
|
+
if (isPunct(prev.tok, ',')) return true;
|
|
495
|
+
return this.isWord(prev) && ['select', 'distinct', 'all', 'unique'].includes(this.lower(prev));
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
private caseExpr(listLevel: number | null = null): boolean {
|
|
499
|
+
const items = this.items;
|
|
500
|
+
const split = this.caseBreaks();
|
|
501
|
+
if (split && listLevel !== null && this.atItemStart() && this.p.breakAt === null && this.p.has()) this.p.newline(listLevel);
|
|
502
|
+
this.emit(items[this.i++]!, true);
|
|
503
|
+
const level = this.p.level;
|
|
504
|
+
const look = (j: number): number | null => {
|
|
505
|
+
const nx = items[j];
|
|
506
|
+
if (!split || !this.isWord(nx)) return null;
|
|
507
|
+
const w = this.lower(nx);
|
|
508
|
+
return w === 'when' || w === 'else' ? level + 1 : w === 'end' ? level : null;
|
|
509
|
+
};
|
|
510
|
+
while (this.i < items.length) {
|
|
511
|
+
const it = items[this.i]!;
|
|
512
|
+
if (it.comment) {
|
|
513
|
+
this.comment(it, look);
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
if (this.isWord(it)) {
|
|
517
|
+
const w = this.lower(it);
|
|
518
|
+
if (w === 'when' || w === 'else') {
|
|
519
|
+
if (split) this.p.newline(level + 1);
|
|
520
|
+
this.emit(it, true);
|
|
521
|
+
this.i++;
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
if (w === 'end') {
|
|
525
|
+
if (split) this.p.newline(level);
|
|
526
|
+
this.emit(it, true);
|
|
527
|
+
this.i++;
|
|
528
|
+
return split;
|
|
529
|
+
}
|
|
530
|
+
if (w === 'case') {
|
|
531
|
+
this.caseExpr();
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (isPunct(it.tok, '(')) {
|
|
536
|
+
this.paren();
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
if (this.closes(it)) throw new Unformattable();
|
|
540
|
+
this.emit(it);
|
|
541
|
+
this.i++;
|
|
542
|
+
}
|
|
543
|
+
throw new Unformattable();
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function buildItems(tokens: Token[]): Item[] {
|
|
548
|
+
const items: Item[] = [];
|
|
549
|
+
let own = true;
|
|
550
|
+
for (const tok of tokens) {
|
|
551
|
+
if (tok.kind === 'whitespace') continue;
|
|
552
|
+
if (tok.kind === 'newline') {
|
|
553
|
+
own = true;
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
const comment = tok.kind === 'lineComment' || tok.kind === 'blockComment';
|
|
557
|
+
items.push({ tok, comment, own, idx: items.length });
|
|
558
|
+
own = tok.kind === 'lineComment';
|
|
559
|
+
}
|
|
560
|
+
return items;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function layout(tokens: Token[], o: SqlFormatOptions, eol: string): string {
|
|
564
|
+
const items = buildItems(tokens);
|
|
565
|
+
const firstWord = items.find((it) => !it.comment);
|
|
566
|
+
const start = firstWord?.tok.text.toLowerCase();
|
|
567
|
+
if (!start || !DML_START.has(start)) throw new Unformattable();
|
|
568
|
+
if (start === 'insert') {
|
|
569
|
+
const second = items.filter((it) => !it.comment)[1]?.tok.text.toLowerCase();
|
|
570
|
+
if (second === 'all' || second === 'first') throw new Unformattable();
|
|
571
|
+
}
|
|
572
|
+
let depth = 0;
|
|
573
|
+
for (const { tok, comment } of items) {
|
|
574
|
+
if (comment) continue;
|
|
575
|
+
if (tok.kind === 'slash') throw new Unformattable();
|
|
576
|
+
if (tok.kind === 'punctuation') {
|
|
577
|
+
if (';[]{}'.includes(tok.text)) throw new Unformattable();
|
|
578
|
+
if (tok.text === '(') depth++;
|
|
579
|
+
if (tok.text === ')' && --depth < 0) throw new Unformattable();
|
|
580
|
+
}
|
|
581
|
+
if (tok.kind === 'operator' && !KNOWN_OPERATORS.has(tok.text)) throw new Unformattable();
|
|
582
|
+
}
|
|
583
|
+
if (depth !== 0) throw new Unformattable();
|
|
584
|
+
return new Layout(items, o).run(eol);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function sameTokens(a: Token[], b: Token[]): boolean {
|
|
588
|
+
const sig = (tokens: Token[]): Token[] => tokens.filter((t) => !isBlank(t));
|
|
589
|
+
const x = sig(a);
|
|
590
|
+
const y = sig(b);
|
|
591
|
+
if (x.length !== y.length) return false;
|
|
592
|
+
return x.every((t, k) => {
|
|
593
|
+
const u = y[k]!;
|
|
594
|
+
const word = (t.kind === 'keyword' || t.kind === 'identifier') && (u.kind === 'keyword' || u.kind === 'identifier');
|
|
595
|
+
return word ? t.text.toLowerCase() === u.text.toLowerCase() : t.kind === u.kind && t.text === u.text;
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function formatStatement(text: string, o: SqlFormatOptions, eol: string): string {
|
|
600
|
+
const tokens = tokenize(text);
|
|
601
|
+
if (isPlsqlStart(tokens, 0)) return caseOnly(tokens, o);
|
|
602
|
+
try {
|
|
603
|
+
const out = layout(tokens, o, eol);
|
|
604
|
+
if (sameTokens(tokens, tokenize(out))) return out;
|
|
605
|
+
} catch {
|
|
606
|
+
// fall through to keyword case only
|
|
607
|
+
}
|
|
608
|
+
return caseOnly(tokens, o);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function endsInLineComment(text: string): boolean {
|
|
612
|
+
const tokens = tokenize(text).filter((t) => !isBlank(t));
|
|
613
|
+
return tokens[tokens.length - 1]?.kind === 'lineComment';
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
function splitGap(gap: string): { term: string; rest: string } {
|
|
617
|
+
const first = tokenize(gap).find((t) => !isBlank(t));
|
|
618
|
+
if (first && (isPunct(first, ';') || first.kind === 'slash')) return { term: first.text, rest: gap.slice(first.to).trim() };
|
|
619
|
+
return { term: '', rest: gap.trim() };
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
export function formatSql(text: string, options: SqlFormatOptions): string {
|
|
623
|
+
const statements = splitStatements(text);
|
|
624
|
+
if (statements.length === 0) return text;
|
|
625
|
+
const eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
626
|
+
const separator = options.blankLineBetweenStatements ? eol + eol : eol;
|
|
627
|
+
const parts = statements.map((s, k) => {
|
|
628
|
+
const gapEnd = k + 1 < statements.length ? statements[k + 1]!.from : text.length;
|
|
629
|
+
const { term, rest } = splitGap(text.slice(s.to, gapEnd));
|
|
630
|
+
let piece = formatStatement(s.text, options, eol);
|
|
631
|
+
if (term === ';') piece += endsInLineComment(s.text) ? eol + ';' : ';';
|
|
632
|
+
else if (term === '/') piece += eol + '/';
|
|
633
|
+
if (rest) piece += eol + rest;
|
|
634
|
+
return piece;
|
|
635
|
+
});
|
|
636
|
+
return text.slice(0, statements[0]!.from) + parts.join(separator) + text.slice(text.trimEnd().length);
|
|
637
|
+
}
|