Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/snippet/browser/snippetParser.ts
5272 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { CharCode } from '../../../../base/common/charCode.js';
7
8
export const enum TokenType {
9
Dollar,
10
Colon,
11
Comma,
12
CurlyOpen,
13
CurlyClose,
14
Backslash,
15
Forwardslash,
16
Pipe,
17
Int,
18
VariableName,
19
Format,
20
Plus,
21
Dash,
22
QuestionMark,
23
EOF
24
}
25
26
export interface Token {
27
type: TokenType;
28
pos: number;
29
len: number;
30
}
31
32
33
export class Scanner {
34
35
private static _table: { [ch: number]: TokenType } = {
36
[CharCode.DollarSign]: TokenType.Dollar,
37
[CharCode.Colon]: TokenType.Colon,
38
[CharCode.Comma]: TokenType.Comma,
39
[CharCode.OpenCurlyBrace]: TokenType.CurlyOpen,
40
[CharCode.CloseCurlyBrace]: TokenType.CurlyClose,
41
[CharCode.Backslash]: TokenType.Backslash,
42
[CharCode.Slash]: TokenType.Forwardslash,
43
[CharCode.Pipe]: TokenType.Pipe,
44
[CharCode.Plus]: TokenType.Plus,
45
[CharCode.Dash]: TokenType.Dash,
46
[CharCode.QuestionMark]: TokenType.QuestionMark,
47
};
48
49
static isDigitCharacter(ch: number): boolean {
50
return ch >= CharCode.Digit0 && ch <= CharCode.Digit9;
51
}
52
53
static isVariableCharacter(ch: number): boolean {
54
return ch === CharCode.Underline
55
|| (ch >= CharCode.a && ch <= CharCode.z)
56
|| (ch >= CharCode.A && ch <= CharCode.Z);
57
}
58
59
value: string = '';
60
pos: number = 0;
61
62
text(value: string) {
63
this.value = value;
64
this.pos = 0;
65
}
66
67
tokenText(token: Token): string {
68
return this.value.substr(token.pos, token.len);
69
}
70
71
next(): Token {
72
73
if (this.pos >= this.value.length) {
74
return { type: TokenType.EOF, pos: this.pos, len: 0 };
75
}
76
77
const pos = this.pos;
78
let len = 0;
79
let ch = this.value.charCodeAt(pos);
80
let type: TokenType;
81
82
// static types
83
type = Scanner._table[ch];
84
if (typeof type === 'number') {
85
this.pos += 1;
86
return { type, pos, len: 1 };
87
}
88
89
// number
90
if (Scanner.isDigitCharacter(ch)) {
91
type = TokenType.Int;
92
do {
93
len += 1;
94
ch = this.value.charCodeAt(pos + len);
95
} while (Scanner.isDigitCharacter(ch));
96
97
this.pos += len;
98
return { type, pos, len };
99
}
100
101
// variable name
102
if (Scanner.isVariableCharacter(ch)) {
103
type = TokenType.VariableName;
104
do {
105
ch = this.value.charCodeAt(pos + (++len));
106
} while (Scanner.isVariableCharacter(ch) || Scanner.isDigitCharacter(ch));
107
108
this.pos += len;
109
return { type, pos, len };
110
}
111
112
113
// format
114
type = TokenType.Format;
115
do {
116
len += 1;
117
ch = this.value.charCodeAt(pos + len);
118
} while (
119
!isNaN(ch)
120
&& typeof Scanner._table[ch] === 'undefined' // not static token
121
&& !Scanner.isDigitCharacter(ch) // not number
122
&& !Scanner.isVariableCharacter(ch) // not variable
123
);
124
125
this.pos += len;
126
return { type, pos, len };
127
}
128
}
129
130
export abstract class Marker {
131
132
readonly _markerBrand: undefined;
133
134
public parent!: Marker;
135
protected _children: Marker[] = [];
136
137
appendChild(child: Marker): this {
138
if (child instanceof Text && this._children[this._children.length - 1] instanceof Text) {
139
// this and previous child are text -> merge them
140
(<Text>this._children[this._children.length - 1]).value += child.value;
141
} else {
142
// normal adoption of child
143
child.parent = this;
144
this._children.push(child);
145
}
146
return this;
147
}
148
149
replace(child: Marker, others: Marker[]): void {
150
const { parent } = child;
151
const idx = parent.children.indexOf(child);
152
const newChildren = parent.children.slice(0);
153
newChildren.splice(idx, 1, ...others);
154
parent._children = newChildren;
155
156
(function _fixParent(children: Marker[], parent: Marker) {
157
for (const child of children) {
158
child.parent = parent;
159
_fixParent(child.children, child);
160
}
161
})(others, parent);
162
}
163
164
get children(): Marker[] {
165
return this._children;
166
}
167
168
get rightMostDescendant(): Marker {
169
if (this._children.length > 0) {
170
return this._children[this._children.length - 1].rightMostDescendant;
171
}
172
return this;
173
}
174
175
get snippet(): TextmateSnippet | undefined {
176
let candidate: Marker = this;
177
while (true) {
178
if (!candidate) {
179
return undefined;
180
}
181
if (candidate instanceof TextmateSnippet) {
182
return candidate;
183
}
184
candidate = candidate.parent;
185
}
186
}
187
188
toString(): string {
189
return this.children.reduce((prev, cur) => prev + cur.toString(), '');
190
}
191
192
abstract toTextmateString(): string;
193
194
len(): number {
195
return 0;
196
}
197
198
abstract clone(): Marker;
199
}
200
201
export class Text extends Marker {
202
203
static escape(value: string): string {
204
return value.replace(/\$|}|\\/g, '\\$&');
205
}
206
207
constructor(public value: string) {
208
super();
209
}
210
override toString() {
211
return this.value;
212
}
213
toTextmateString(): string {
214
return Text.escape(this.value);
215
}
216
override len(): number {
217
return this.value.length;
218
}
219
clone(): Text {
220
return new Text(this.value);
221
}
222
}
223
224
export abstract class TransformableMarker extends Marker {
225
public transform?: Transform;
226
}
227
228
export class Placeholder extends TransformableMarker {
229
static compareByIndex(a: Placeholder, b: Placeholder): number {
230
if (a.index === b.index) {
231
return 0;
232
} else if (a.isFinalTabstop) {
233
return 1;
234
} else if (b.isFinalTabstop) {
235
return -1;
236
} else if (a.index < b.index) {
237
return -1;
238
} else if (a.index > b.index) {
239
return 1;
240
} else {
241
return 0;
242
}
243
}
244
245
constructor(public index: number) {
246
super();
247
}
248
249
get isFinalTabstop() {
250
return this.index === 0;
251
}
252
253
get choice(): Choice | undefined {
254
return this._children.length === 1 && this._children[0] instanceof Choice
255
? this._children[0]
256
: undefined;
257
}
258
259
toTextmateString(): string {
260
let transformString = '';
261
if (this.transform) {
262
transformString = this.transform.toTextmateString();
263
}
264
if (this.children.length === 0 && !this.transform) {
265
return `\$${this.index}`;
266
} else if (this.children.length === 0) {
267
return `\${${this.index}${transformString}}`;
268
} else if (this.choice) {
269
return `\${${this.index}|${this.choice.toTextmateString()}|${transformString}}`;
270
} else {
271
return `\${${this.index}:${this.children.map(child => child.toTextmateString()).join('')}${transformString}}`;
272
}
273
}
274
275
clone(): Placeholder {
276
const ret = new Placeholder(this.index);
277
if (this.transform) {
278
ret.transform = this.transform.clone();
279
}
280
ret._children = this.children.map(child => child.clone());
281
return ret;
282
}
283
}
284
285
export class Choice extends Marker {
286
287
readonly options: Text[] = [];
288
289
override appendChild(marker: Marker): this {
290
if (marker instanceof Text) {
291
marker.parent = this;
292
this.options.push(marker);
293
}
294
return this;
295
}
296
297
override toString() {
298
return this.options[0].value;
299
}
300
301
toTextmateString(): string {
302
return this.options
303
.map(option => option.value.replace(/\||,|\\/g, '\\$&'))
304
.join(',');
305
}
306
307
override len(): number {
308
return this.options[0].len();
309
}
310
311
clone(): Choice {
312
const ret = new Choice();
313
this.options.forEach(ret.appendChild, ret);
314
return ret;
315
}
316
}
317
318
export class Transform extends Marker {
319
320
regexp: RegExp = new RegExp('');
321
322
resolve(value: string): string {
323
const _this = this;
324
let didMatch = false;
325
let ret = value.replace(this.regexp, function () {
326
didMatch = true;
327
return _this._replace(Array.prototype.slice.call(arguments, 0, -2));
328
});
329
// when the regex didn't match and when the transform has
330
// else branches, then run those
331
if (!didMatch && this._children.some(child => child instanceof FormatString && Boolean(child.elseValue))) {
332
ret = this._replace([]);
333
}
334
return ret;
335
}
336
337
private _replace(groups: string[]): string {
338
let ret = '';
339
for (const marker of this._children) {
340
if (marker instanceof FormatString) {
341
let value = groups[marker.index] || '';
342
value = marker.resolve(value);
343
ret += value;
344
} else {
345
ret += marker.toString();
346
}
347
}
348
return ret;
349
}
350
351
override toString(): string {
352
return '';
353
}
354
355
toTextmateString(): string {
356
return `/${this.regexp.source}/${this.children.map(c => c.toTextmateString())}/${(this.regexp.ignoreCase ? 'i' : '') + (this.regexp.global ? 'g' : '')}`;
357
}
358
359
clone(): Transform {
360
const ret = new Transform();
361
ret.regexp = new RegExp(this.regexp.source, '' + (this.regexp.ignoreCase ? 'i' : '') + (this.regexp.global ? 'g' : ''));
362
ret._children = this.children.map(child => child.clone());
363
return ret;
364
}
365
366
}
367
368
export class FormatString extends Marker {
369
370
constructor(
371
readonly index: number,
372
readonly shorthandName?: string,
373
readonly ifValue?: string,
374
readonly elseValue?: string,
375
) {
376
super();
377
}
378
379
resolve(value?: string): string {
380
if (this.shorthandName === 'upcase') {
381
return !value ? '' : value.toLocaleUpperCase();
382
} else if (this.shorthandName === 'downcase') {
383
return !value ? '' : value.toLocaleLowerCase();
384
} else if (this.shorthandName === 'capitalize') {
385
return !value ? '' : (value[0].toLocaleUpperCase() + value.substr(1));
386
} else if (this.shorthandName === 'pascalcase') {
387
return !value ? '' : this._toPascalCase(value);
388
} else if (this.shorthandName === 'camelcase') {
389
return !value ? '' : this._toCamelCase(value);
390
} else if (this.shorthandName === 'kebabcase') {
391
return !value ? '' : this._toKebabCase(value);
392
} else if (this.shorthandName === 'snakecase') {
393
return !value ? '' : this._toSnakeCase(value);
394
} else if (Boolean(value) && typeof this.ifValue === 'string') {
395
return this.ifValue;
396
} else if (!Boolean(value) && typeof this.elseValue === 'string') {
397
return this.elseValue;
398
} else {
399
return value || '';
400
}
401
}
402
403
// Note: word-based case transforms rely on uppercase/lowercase distinctions.
404
// For scripts without case, transforms are effectively no-ops.
405
private _toKebabCase(value: string): string {
406
const match = value.match(/[\p{L}0-9]+/gu);
407
if (!match) {
408
return value;
409
}
410
411
if (!value.match(/[\p{L}0-9]/u)) {
412
return value
413
.trim()
414
.toLowerCase()
415
.replace(/^_+|_+$/g, '')
416
.replace(/[\s_]+/g, '-');
417
}
418
419
const cleaned = value.trim().replace(/^_+|_+$/g, '');
420
421
const match2 = cleaned.match(/\p{Lu}{2,}(?=\p{Lu}\p{Ll}+[0-9]*|[\s_-]|$)|\p{Lu}?\p{Ll}+[0-9]*|\p{Lu}(?=\p{Lu}\p{Ll})|\p{Lu}(?=[\s_-]|$)|[0-9]+/gu);
422
423
if (!match2) {
424
return cleaned
425
.split(/[\s_-]+/)
426
.filter(word => word.length > 0)
427
.map(word => word.toLowerCase())
428
.join('-');
429
}
430
431
return match2
432
.map(x => x.toLowerCase())
433
.join('-');
434
}
435
436
private _toPascalCase(value: string): string {
437
const match = value.match(/[\p{L}0-9]+/gu);
438
if (!match) {
439
return value;
440
}
441
return match.map(word => {
442
return word.charAt(0).toUpperCase() + word.substr(1);
443
})
444
.join('');
445
}
446
447
private _toCamelCase(value: string): string {
448
const match = value.match(/[\p{L}0-9]+/gu);
449
if (!match) {
450
return value;
451
}
452
return match.map((word, index) => {
453
if (index === 0) {
454
return word.charAt(0).toLowerCase() + word.substr(1);
455
}
456
return word.charAt(0).toUpperCase() + word.substr(1);
457
})
458
.join('');
459
}
460
461
private _toSnakeCase(value: string): string {
462
return value.replace(/(\p{Ll})(\p{Lu})/gu, '$1_$2')
463
.replace(/[\s\-]+/g, '_')
464
.toLowerCase();
465
}
466
467
toTextmateString(): string {
468
let value = '${';
469
value += this.index;
470
if (this.shorthandName) {
471
value += `:/${this.shorthandName}`;
472
473
} else if (this.ifValue && this.elseValue) {
474
value += `:?${this.ifValue}:${this.elseValue}`;
475
} else if (this.ifValue) {
476
value += `:+${this.ifValue}`;
477
} else if (this.elseValue) {
478
value += `:-${this.elseValue}`;
479
}
480
value += '}';
481
return value;
482
}
483
484
clone(): FormatString {
485
const ret = new FormatString(this.index, this.shorthandName, this.ifValue, this.elseValue);
486
return ret;
487
}
488
}
489
490
export class Variable extends TransformableMarker {
491
492
constructor(public name: string) {
493
super();
494
}
495
496
resolve(resolver: VariableResolver): boolean {
497
let value = resolver.resolve(this);
498
if (this.transform) {
499
value = this.transform.resolve(value || '');
500
}
501
if (value !== undefined) {
502
this._children = [new Text(value)];
503
return true;
504
}
505
return false;
506
}
507
508
toTextmateString(): string {
509
let transformString = '';
510
if (this.transform) {
511
transformString = this.transform.toTextmateString();
512
}
513
if (this.children.length === 0) {
514
return `\${${this.name}${transformString}}`;
515
} else {
516
return `\${${this.name}:${this.children.map(child => child.toTextmateString()).join('')}${transformString}}`;
517
}
518
}
519
520
clone(): Variable {
521
const ret = new Variable(this.name);
522
if (this.transform) {
523
ret.transform = this.transform.clone();
524
}
525
ret._children = this.children.map(child => child.clone());
526
return ret;
527
}
528
}
529
530
export interface VariableResolver {
531
resolve(variable: Variable): string | undefined;
532
}
533
534
function walk(marker: Marker[], visitor: (marker: Marker) => boolean): void {
535
const stack = [...marker];
536
while (stack.length > 0) {
537
const marker = stack.shift()!;
538
const recurse = visitor(marker);
539
if (!recurse) {
540
break;
541
}
542
stack.unshift(...marker.children);
543
}
544
}
545
546
export class TextmateSnippet extends Marker {
547
548
private _placeholders?: { all: Placeholder[]; last?: Placeholder };
549
550
get placeholderInfo() {
551
if (!this._placeholders) {
552
// fill in placeholders
553
const all: Placeholder[] = [];
554
let last: Placeholder | undefined;
555
this.walk(function (candidate) {
556
if (candidate instanceof Placeholder) {
557
all.push(candidate);
558
last = !last || last.index < candidate.index ? candidate : last;
559
}
560
return true;
561
});
562
this._placeholders = { all, last };
563
}
564
return this._placeholders;
565
}
566
567
get placeholders(): Placeholder[] {
568
const { all } = this.placeholderInfo;
569
return all;
570
}
571
572
offset(marker: Marker): number {
573
let pos = 0;
574
let found = false;
575
this.walk(candidate => {
576
if (candidate === marker) {
577
found = true;
578
return false;
579
}
580
pos += candidate.len();
581
return true;
582
});
583
584
if (!found) {
585
return -1;
586
}
587
return pos;
588
}
589
590
fullLen(marker: Marker): number {
591
let ret = 0;
592
walk([marker], marker => {
593
ret += marker.len();
594
return true;
595
});
596
return ret;
597
}
598
599
enclosingPlaceholders(placeholder: Placeholder): Placeholder[] {
600
const ret: Placeholder[] = [];
601
let { parent } = placeholder;
602
while (parent) {
603
if (parent instanceof Placeholder) {
604
ret.push(parent);
605
}
606
parent = parent.parent;
607
}
608
return ret;
609
}
610
611
resolveVariables(resolver: VariableResolver): this {
612
this.walk(candidate => {
613
if (candidate instanceof Variable) {
614
if (candidate.resolve(resolver)) {
615
this._placeholders = undefined;
616
}
617
}
618
return true;
619
});
620
return this;
621
}
622
623
override appendChild(child: Marker) {
624
this._placeholders = undefined;
625
return super.appendChild(child);
626
}
627
628
override replace(child: Marker, others: Marker[]): void {
629
this._placeholders = undefined;
630
return super.replace(child, others);
631
}
632
633
toTextmateString(): string {
634
return this.children.reduce((prev, cur) => prev + cur.toTextmateString(), '');
635
}
636
637
clone(): TextmateSnippet {
638
const ret = new TextmateSnippet();
639
this._children = this.children.map(child => child.clone());
640
return ret;
641
}
642
643
walk(visitor: (marker: Marker) => boolean): void {
644
walk(this.children, visitor);
645
}
646
}
647
648
export class SnippetParser {
649
650
static escape(value: string): string {
651
return value.replace(/\$|}|\\/g, '\\$&');
652
}
653
654
/**
655
* Takes a snippet and returns the insertable string, e.g return the snippet-string
656
* without any placeholder, tabstop, variables etc...
657
*/
658
static asInsertText(value: string): string {
659
return new SnippetParser().parse(value).toString();
660
}
661
662
static guessNeedsClipboard(template: string): boolean {
663
return /\${?CLIPBOARD/.test(template);
664
}
665
666
private _scanner: Scanner = new Scanner();
667
private _token: Token = { type: TokenType.EOF, pos: 0, len: 0 };
668
669
parse(value: string, insertFinalTabstop?: boolean, enforceFinalTabstop?: boolean): TextmateSnippet {
670
const snippet = new TextmateSnippet();
671
this.parseFragment(value, snippet);
672
this.ensureFinalTabstop(snippet, enforceFinalTabstop ?? false, insertFinalTabstop ?? false);
673
return snippet;
674
}
675
676
parseFragment(value: string, snippet: TextmateSnippet): readonly Marker[] {
677
678
const offset = snippet.children.length;
679
this._scanner.text(value);
680
this._token = this._scanner.next();
681
while (this._parse(snippet)) {
682
// nothing
683
}
684
685
// fill in values for placeholders. the first placeholder of an index
686
// that has a value defines the value for all placeholders with that index
687
const placeholderDefaultValues = new Map<number, Marker[] | undefined>();
688
const incompletePlaceholders: Placeholder[] = [];
689
snippet.walk(marker => {
690
if (marker instanceof Placeholder) {
691
if (marker.isFinalTabstop) {
692
placeholderDefaultValues.set(0, undefined);
693
} else if (!placeholderDefaultValues.has(marker.index) && marker.children.length > 0) {
694
placeholderDefaultValues.set(marker.index, marker.children);
695
} else {
696
incompletePlaceholders.push(marker);
697
}
698
}
699
return true;
700
});
701
702
const fillInIncompletePlaceholder = (placeholder: Placeholder, stack: Set<number>) => {
703
const defaultValues = placeholderDefaultValues.get(placeholder.index);
704
if (!defaultValues) {
705
return;
706
}
707
const clone = new Placeholder(placeholder.index);
708
clone.transform = placeholder.transform;
709
for (const child of defaultValues) {
710
const newChild = child.clone();
711
clone.appendChild(newChild);
712
713
// "recurse" on children that are again placeholders
714
if (newChild instanceof Placeholder && placeholderDefaultValues.has(newChild.index) && !stack.has(newChild.index)) {
715
stack.add(newChild.index);
716
fillInIncompletePlaceholder(newChild, stack);
717
stack.delete(newChild.index);
718
}
719
}
720
snippet.replace(placeholder, [clone]);
721
};
722
723
const stack = new Set<number>();
724
for (const placeholder of incompletePlaceholders) {
725
fillInIncompletePlaceholder(placeholder, stack);
726
}
727
728
return snippet.children.slice(offset);
729
}
730
731
ensureFinalTabstop(snippet: TextmateSnippet, enforceFinalTabstop: boolean, insertFinalTabstop: boolean) {
732
733
if (enforceFinalTabstop || insertFinalTabstop && snippet.placeholders.length > 0) {
734
const finalTabstop = snippet.placeholders.find(p => p.index === 0);
735
if (!finalTabstop) {
736
// the snippet uses placeholders but has no
737
// final tabstop defined -> insert at the end
738
snippet.appendChild(new Placeholder(0));
739
}
740
}
741
742
}
743
744
private _accept(type?: TokenType): boolean;
745
private _accept(type: TokenType | undefined, value: true): string;
746
private _accept(type: TokenType, value?: boolean): boolean | string {
747
if (type === undefined || this._token.type === type) {
748
const ret = !value ? true : this._scanner.tokenText(this._token);
749
this._token = this._scanner.next();
750
return ret;
751
}
752
return false;
753
}
754
755
private _backTo(token: Token): false {
756
this._scanner.pos = token.pos + token.len;
757
this._token = token;
758
return false;
759
}
760
761
private _until(type: TokenType): false | string {
762
const start = this._token;
763
while (this._token.type !== type) {
764
if (this._token.type === TokenType.EOF) {
765
return false;
766
} else if (this._token.type === TokenType.Backslash) {
767
const nextToken = this._scanner.next();
768
if (nextToken.type !== TokenType.Dollar
769
&& nextToken.type !== TokenType.CurlyClose
770
&& nextToken.type !== TokenType.Backslash) {
771
return false;
772
}
773
}
774
this._token = this._scanner.next();
775
}
776
const value = this._scanner.value.substring(start.pos, this._token.pos).replace(/\\(\$|}|\\)/g, '$1');
777
this._token = this._scanner.next();
778
return value;
779
}
780
781
private _parse(marker: Marker): boolean {
782
return this._parseEscaped(marker)
783
|| this._parseTabstopOrVariableName(marker)
784
|| this._parseComplexPlaceholder(marker)
785
|| this._parseComplexVariable(marker)
786
|| this._parseAnything(marker);
787
}
788
789
// \$, \\, \} -> just text
790
private _parseEscaped(marker: Marker): boolean {
791
let value: string;
792
if (value = this._accept(TokenType.Backslash, true)) {
793
// saw a backslash, append escaped token or that backslash
794
value = this._accept(TokenType.Dollar, true)
795
|| this._accept(TokenType.CurlyClose, true)
796
|| this._accept(TokenType.Backslash, true)
797
|| value;
798
799
marker.appendChild(new Text(value));
800
return true;
801
}
802
return false;
803
}
804
805
// $foo -> variable, $1 -> tabstop
806
private _parseTabstopOrVariableName(parent: Marker): boolean {
807
let value: string;
808
const token = this._token;
809
const match = this._accept(TokenType.Dollar)
810
&& (value = this._accept(TokenType.VariableName, true) || this._accept(TokenType.Int, true));
811
812
if (!match) {
813
return this._backTo(token);
814
}
815
816
parent.appendChild(/^\d+$/.test(value!)
817
? new Placeholder(Number(value!))
818
: new Variable(value!)
819
);
820
return true;
821
}
822
823
// ${1:<children>}, ${1} -> placeholder
824
private _parseComplexPlaceholder(parent: Marker): boolean {
825
let index: string;
826
const token = this._token;
827
const match = this._accept(TokenType.Dollar)
828
&& this._accept(TokenType.CurlyOpen)
829
&& (index = this._accept(TokenType.Int, true));
830
831
if (!match) {
832
return this._backTo(token);
833
}
834
835
const placeholder = new Placeholder(Number(index!));
836
837
if (this._accept(TokenType.Colon)) {
838
// ${1:<children>}
839
while (true) {
840
841
// ...} -> done
842
if (this._accept(TokenType.CurlyClose)) {
843
parent.appendChild(placeholder);
844
return true;
845
}
846
847
if (this._parse(placeholder)) {
848
continue;
849
}
850
851
// fallback
852
parent.appendChild(new Text('${' + index! + ':'));
853
placeholder.children.forEach(parent.appendChild, parent);
854
return true;
855
}
856
} else if (placeholder.index > 0 && this._accept(TokenType.Pipe)) {
857
// ${1|one,two,three|}
858
const choice = new Choice();
859
860
while (true) {
861
if (this._parseChoiceElement(choice)) {
862
863
if (this._accept(TokenType.Comma)) {
864
// opt, -> more
865
continue;
866
}
867
868
if (this._accept(TokenType.Pipe)) {
869
placeholder.appendChild(choice);
870
if (this._accept(TokenType.CurlyClose)) {
871
// ..|} -> done
872
parent.appendChild(placeholder);
873
return true;
874
}
875
}
876
}
877
878
this._backTo(token);
879
return false;
880
}
881
882
} else if (this._accept(TokenType.Forwardslash)) {
883
// ${1/<regex>/<format>/<options>}
884
if (this._parseTransform(placeholder)) {
885
parent.appendChild(placeholder);
886
return true;
887
}
888
889
this._backTo(token);
890
return false;
891
892
} else if (this._accept(TokenType.CurlyClose)) {
893
// ${1}
894
parent.appendChild(placeholder);
895
return true;
896
897
} else {
898
// ${1 <- missing curly or colon
899
return this._backTo(token);
900
}
901
}
902
903
private _parseChoiceElement(parent: Choice): boolean {
904
const token = this._token;
905
const values: string[] = [];
906
907
while (true) {
908
if (this._token.type === TokenType.Comma || this._token.type === TokenType.Pipe) {
909
break;
910
}
911
let value: string;
912
if (value = this._accept(TokenType.Backslash, true)) {
913
// \, \|, or \\
914
value = this._accept(TokenType.Comma, true)
915
|| this._accept(TokenType.Pipe, true)
916
|| this._accept(TokenType.Backslash, true)
917
|| value;
918
} else {
919
value = this._accept(undefined, true);
920
}
921
if (!value) {
922
// EOF
923
this._backTo(token);
924
return false;
925
}
926
values.push(value);
927
}
928
929
if (values.length === 0) {
930
this._backTo(token);
931
return false;
932
}
933
934
parent.appendChild(new Text(values.join('')));
935
return true;
936
}
937
938
// ${foo:<children>}, ${foo} -> variable
939
private _parseComplexVariable(parent: Marker): boolean {
940
let name: string;
941
const token = this._token;
942
const match = this._accept(TokenType.Dollar)
943
&& this._accept(TokenType.CurlyOpen)
944
&& (name = this._accept(TokenType.VariableName, true));
945
946
if (!match) {
947
return this._backTo(token);
948
}
949
950
const variable = new Variable(name!);
951
952
if (this._accept(TokenType.Colon)) {
953
// ${foo:<children>}
954
while (true) {
955
956
// ...} -> done
957
if (this._accept(TokenType.CurlyClose)) {
958
parent.appendChild(variable);
959
return true;
960
}
961
962
if (this._parse(variable)) {
963
continue;
964
}
965
966
// fallback
967
parent.appendChild(new Text('${' + name! + ':'));
968
variable.children.forEach(parent.appendChild, parent);
969
return true;
970
}
971
972
} else if (this._accept(TokenType.Forwardslash)) {
973
// ${foo/<regex>/<format>/<options>}
974
if (this._parseTransform(variable)) {
975
parent.appendChild(variable);
976
return true;
977
}
978
979
this._backTo(token);
980
return false;
981
982
} else if (this._accept(TokenType.CurlyClose)) {
983
// ${foo}
984
parent.appendChild(variable);
985
return true;
986
987
} else {
988
// ${foo <- missing curly or colon
989
return this._backTo(token);
990
}
991
}
992
993
private _parseTransform(parent: TransformableMarker): boolean {
994
// ...<regex>/<format>/<options>}
995
996
const transform = new Transform();
997
let regexValue = '';
998
let regexOptions = '';
999
1000
// (1) /regex
1001
while (true) {
1002
if (this._accept(TokenType.Forwardslash)) {
1003
break;
1004
}
1005
1006
let escaped: string;
1007
if (escaped = this._accept(TokenType.Backslash, true)) {
1008
escaped = this._accept(TokenType.Forwardslash, true) || escaped;
1009
regexValue += escaped;
1010
continue;
1011
}
1012
1013
if (this._token.type !== TokenType.EOF) {
1014
regexValue += this._accept(undefined, true);
1015
continue;
1016
}
1017
return false;
1018
}
1019
1020
// (2) /format
1021
while (true) {
1022
if (this._accept(TokenType.Forwardslash)) {
1023
break;
1024
}
1025
1026
let escaped: string;
1027
if (escaped = this._accept(TokenType.Backslash, true)) {
1028
escaped = this._accept(TokenType.Backslash, true) || this._accept(TokenType.Forwardslash, true) || escaped;
1029
transform.appendChild(new Text(escaped));
1030
continue;
1031
}
1032
1033
if (this._parseFormatString(transform) || this._parseAnything(transform)) {
1034
continue;
1035
}
1036
return false;
1037
}
1038
1039
// (3) /option
1040
while (true) {
1041
if (this._accept(TokenType.CurlyClose)) {
1042
break;
1043
}
1044
if (this._token.type !== TokenType.EOF) {
1045
regexOptions += this._accept(undefined, true);
1046
continue;
1047
}
1048
return false;
1049
}
1050
1051
try {
1052
transform.regexp = new RegExp(regexValue, regexOptions);
1053
} catch (e) {
1054
// invalid regexp
1055
return false;
1056
}
1057
1058
parent.transform = transform;
1059
return true;
1060
}
1061
1062
private _parseFormatString(parent: Transform): boolean {
1063
1064
const token = this._token;
1065
if (!this._accept(TokenType.Dollar)) {
1066
return false;
1067
}
1068
1069
let complex = false;
1070
if (this._accept(TokenType.CurlyOpen)) {
1071
complex = true;
1072
}
1073
1074
const index = this._accept(TokenType.Int, true);
1075
1076
if (!index) {
1077
this._backTo(token);
1078
return false;
1079
1080
} else if (!complex) {
1081
// $1
1082
parent.appendChild(new FormatString(Number(index)));
1083
return true;
1084
1085
} else if (this._accept(TokenType.CurlyClose)) {
1086
// ${1}
1087
parent.appendChild(new FormatString(Number(index)));
1088
return true;
1089
1090
} else if (!this._accept(TokenType.Colon)) {
1091
this._backTo(token);
1092
return false;
1093
}
1094
1095
if (this._accept(TokenType.Forwardslash)) {
1096
// ${1:/upcase}
1097
const shorthand = this._accept(TokenType.VariableName, true);
1098
if (!shorthand || !this._accept(TokenType.CurlyClose)) {
1099
this._backTo(token);
1100
return false;
1101
} else {
1102
parent.appendChild(new FormatString(Number(index), shorthand));
1103
return true;
1104
}
1105
1106
} else if (this._accept(TokenType.Plus)) {
1107
// ${1:+<if>}
1108
const ifValue = this._until(TokenType.CurlyClose);
1109
if (ifValue) {
1110
parent.appendChild(new FormatString(Number(index), undefined, ifValue, undefined));
1111
return true;
1112
}
1113
1114
} else if (this._accept(TokenType.Dash)) {
1115
// ${2:-<else>}
1116
const elseValue = this._until(TokenType.CurlyClose);
1117
if (elseValue) {
1118
parent.appendChild(new FormatString(Number(index), undefined, undefined, elseValue));
1119
return true;
1120
}
1121
1122
} else if (this._accept(TokenType.QuestionMark)) {
1123
// ${2:?<if>:<else>}
1124
const ifValue = this._until(TokenType.Colon);
1125
if (ifValue) {
1126
const elseValue = this._until(TokenType.CurlyClose);
1127
if (elseValue) {
1128
parent.appendChild(new FormatString(Number(index), undefined, ifValue, elseValue));
1129
return true;
1130
}
1131
}
1132
1133
} else {
1134
// ${1:<else>}
1135
const elseValue = this._until(TokenType.CurlyClose);
1136
if (elseValue) {
1137
parent.appendChild(new FormatString(Number(index), undefined, undefined, elseValue));
1138
return true;
1139
}
1140
}
1141
1142
this._backTo(token);
1143
return false;
1144
}
1145
1146
private _parseAnything(marker: Marker): boolean {
1147
if (this._token.type !== TokenType.EOF) {
1148
marker.appendChild(new Text(this._scanner.tokenText(this._token)));
1149
this._accept(undefined);
1150
return true;
1151
}
1152
return false;
1153
}
1154
}
1155
1156