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
3296 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: any;
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] as Choice
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 (Boolean(value) && typeof this.ifValue === 'string') {
391
return this.ifValue;
392
} else if (!Boolean(value) && typeof this.elseValue === 'string') {
393
return this.elseValue;
394
} else {
395
return value || '';
396
}
397
}
398
399
private _toPascalCase(value: string): string {
400
const match = value.match(/[a-z0-9]+/gi);
401
if (!match) {
402
return value;
403
}
404
return match.map(word => {
405
return word.charAt(0).toUpperCase() + word.substr(1);
406
})
407
.join('');
408
}
409
410
private _toCamelCase(value: string): string {
411
const match = value.match(/[a-z0-9]+/gi);
412
if (!match) {
413
return value;
414
}
415
return match.map((word, index) => {
416
if (index === 0) {
417
return word.charAt(0).toLowerCase() + word.substr(1);
418
}
419
return word.charAt(0).toUpperCase() + word.substr(1);
420
})
421
.join('');
422
}
423
424
toTextmateString(): string {
425
let value = '${';
426
value += this.index;
427
if (this.shorthandName) {
428
value += `:/${this.shorthandName}`;
429
430
} else if (this.ifValue && this.elseValue) {
431
value += `:?${this.ifValue}:${this.elseValue}`;
432
} else if (this.ifValue) {
433
value += `:+${this.ifValue}`;
434
} else if (this.elseValue) {
435
value += `:-${this.elseValue}`;
436
}
437
value += '}';
438
return value;
439
}
440
441
clone(): FormatString {
442
const ret = new FormatString(this.index, this.shorthandName, this.ifValue, this.elseValue);
443
return ret;
444
}
445
}
446
447
export class Variable extends TransformableMarker {
448
449
constructor(public name: string) {
450
super();
451
}
452
453
resolve(resolver: VariableResolver): boolean {
454
let value = resolver.resolve(this);
455
if (this.transform) {
456
value = this.transform.resolve(value || '');
457
}
458
if (value !== undefined) {
459
this._children = [new Text(value)];
460
return true;
461
}
462
return false;
463
}
464
465
toTextmateString(): string {
466
let transformString = '';
467
if (this.transform) {
468
transformString = this.transform.toTextmateString();
469
}
470
if (this.children.length === 0) {
471
return `\${${this.name}${transformString}}`;
472
} else {
473
return `\${${this.name}:${this.children.map(child => child.toTextmateString()).join('')}${transformString}}`;
474
}
475
}
476
477
clone(): Variable {
478
const ret = new Variable(this.name);
479
if (this.transform) {
480
ret.transform = this.transform.clone();
481
}
482
ret._children = this.children.map(child => child.clone());
483
return ret;
484
}
485
}
486
487
export interface VariableResolver {
488
resolve(variable: Variable): string | undefined;
489
}
490
491
function walk(marker: Marker[], visitor: (marker: Marker) => boolean): void {
492
const stack = [...marker];
493
while (stack.length > 0) {
494
const marker = stack.shift()!;
495
const recurse = visitor(marker);
496
if (!recurse) {
497
break;
498
}
499
stack.unshift(...marker.children);
500
}
501
}
502
503
export class TextmateSnippet extends Marker {
504
505
private _placeholders?: { all: Placeholder[]; last?: Placeholder };
506
507
get placeholderInfo() {
508
if (!this._placeholders) {
509
// fill in placeholders
510
const all: Placeholder[] = [];
511
let last: Placeholder | undefined;
512
this.walk(function (candidate) {
513
if (candidate instanceof Placeholder) {
514
all.push(candidate);
515
last = !last || last.index < candidate.index ? candidate : last;
516
}
517
return true;
518
});
519
this._placeholders = { all, last };
520
}
521
return this._placeholders;
522
}
523
524
get placeholders(): Placeholder[] {
525
const { all } = this.placeholderInfo;
526
return all;
527
}
528
529
offset(marker: Marker): number {
530
let pos = 0;
531
let found = false;
532
this.walk(candidate => {
533
if (candidate === marker) {
534
found = true;
535
return false;
536
}
537
pos += candidate.len();
538
return true;
539
});
540
541
if (!found) {
542
return -1;
543
}
544
return pos;
545
}
546
547
fullLen(marker: Marker): number {
548
let ret = 0;
549
walk([marker], marker => {
550
ret += marker.len();
551
return true;
552
});
553
return ret;
554
}
555
556
enclosingPlaceholders(placeholder: Placeholder): Placeholder[] {
557
const ret: Placeholder[] = [];
558
let { parent } = placeholder;
559
while (parent) {
560
if (parent instanceof Placeholder) {
561
ret.push(parent);
562
}
563
parent = parent.parent;
564
}
565
return ret;
566
}
567
568
resolveVariables(resolver: VariableResolver): this {
569
this.walk(candidate => {
570
if (candidate instanceof Variable) {
571
if (candidate.resolve(resolver)) {
572
this._placeholders = undefined;
573
}
574
}
575
return true;
576
});
577
return this;
578
}
579
580
override appendChild(child: Marker) {
581
this._placeholders = undefined;
582
return super.appendChild(child);
583
}
584
585
override replace(child: Marker, others: Marker[]): void {
586
this._placeholders = undefined;
587
return super.replace(child, others);
588
}
589
590
toTextmateString(): string {
591
return this.children.reduce((prev, cur) => prev + cur.toTextmateString(), '');
592
}
593
594
clone(): TextmateSnippet {
595
const ret = new TextmateSnippet();
596
this._children = this.children.map(child => child.clone());
597
return ret;
598
}
599
600
walk(visitor: (marker: Marker) => boolean): void {
601
walk(this.children, visitor);
602
}
603
}
604
605
export class SnippetParser {
606
607
static escape(value: string): string {
608
return value.replace(/\$|}|\\/g, '\\$&');
609
}
610
611
/**
612
* Takes a snippet and returns the insertable string, e.g return the snippet-string
613
* without any placeholder, tabstop, variables etc...
614
*/
615
static asInsertText(value: string): string {
616
return new SnippetParser().parse(value).toString();
617
}
618
619
static guessNeedsClipboard(template: string): boolean {
620
return /\${?CLIPBOARD/.test(template);
621
}
622
623
private _scanner: Scanner = new Scanner();
624
private _token: Token = { type: TokenType.EOF, pos: 0, len: 0 };
625
626
parse(value: string, insertFinalTabstop?: boolean, enforceFinalTabstop?: boolean): TextmateSnippet {
627
const snippet = new TextmateSnippet();
628
this.parseFragment(value, snippet);
629
this.ensureFinalTabstop(snippet, enforceFinalTabstop ?? false, insertFinalTabstop ?? false);
630
return snippet;
631
}
632
633
parseFragment(value: string, snippet: TextmateSnippet): readonly Marker[] {
634
635
const offset = snippet.children.length;
636
this._scanner.text(value);
637
this._token = this._scanner.next();
638
while (this._parse(snippet)) {
639
// nothing
640
}
641
642
// fill in values for placeholders. the first placeholder of an index
643
// that has a value defines the value for all placeholders with that index
644
const placeholderDefaultValues = new Map<number, Marker[] | undefined>();
645
const incompletePlaceholders: Placeholder[] = [];
646
snippet.walk(marker => {
647
if (marker instanceof Placeholder) {
648
if (marker.isFinalTabstop) {
649
placeholderDefaultValues.set(0, undefined);
650
} else if (!placeholderDefaultValues.has(marker.index) && marker.children.length > 0) {
651
placeholderDefaultValues.set(marker.index, marker.children);
652
} else {
653
incompletePlaceholders.push(marker);
654
}
655
}
656
return true;
657
});
658
659
const fillInIncompletePlaceholder = (placeholder: Placeholder, stack: Set<number>) => {
660
const defaultValues = placeholderDefaultValues.get(placeholder.index);
661
if (!defaultValues) {
662
return;
663
}
664
const clone = new Placeholder(placeholder.index);
665
clone.transform = placeholder.transform;
666
for (const child of defaultValues) {
667
const newChild = child.clone();
668
clone.appendChild(newChild);
669
670
// "recurse" on children that are again placeholders
671
if (newChild instanceof Placeholder && placeholderDefaultValues.has(newChild.index) && !stack.has(newChild.index)) {
672
stack.add(newChild.index);
673
fillInIncompletePlaceholder(newChild, stack);
674
stack.delete(newChild.index);
675
}
676
}
677
snippet.replace(placeholder, [clone]);
678
};
679
680
const stack = new Set<number>();
681
for (const placeholder of incompletePlaceholders) {
682
fillInIncompletePlaceholder(placeholder, stack);
683
}
684
685
return snippet.children.slice(offset);
686
}
687
688
ensureFinalTabstop(snippet: TextmateSnippet, enforceFinalTabstop: boolean, insertFinalTabstop: boolean) {
689
690
if (enforceFinalTabstop || insertFinalTabstop && snippet.placeholders.length > 0) {
691
const finalTabstop = snippet.placeholders.find(p => p.index === 0);
692
if (!finalTabstop) {
693
// the snippet uses placeholders but has no
694
// final tabstop defined -> insert at the end
695
snippet.appendChild(new Placeholder(0));
696
}
697
}
698
699
}
700
701
private _accept(type?: TokenType): boolean;
702
private _accept(type: TokenType | undefined, value: true): string;
703
private _accept(type: TokenType, value?: boolean): boolean | string {
704
if (type === undefined || this._token.type === type) {
705
const ret = !value ? true : this._scanner.tokenText(this._token);
706
this._token = this._scanner.next();
707
return ret;
708
}
709
return false;
710
}
711
712
private _backTo(token: Token): false {
713
this._scanner.pos = token.pos + token.len;
714
this._token = token;
715
return false;
716
}
717
718
private _until(type: TokenType): false | string {
719
const start = this._token;
720
while (this._token.type !== type) {
721
if (this._token.type === TokenType.EOF) {
722
return false;
723
} else if (this._token.type === TokenType.Backslash) {
724
const nextToken = this._scanner.next();
725
if (nextToken.type !== TokenType.Dollar
726
&& nextToken.type !== TokenType.CurlyClose
727
&& nextToken.type !== TokenType.Backslash) {
728
return false;
729
}
730
}
731
this._token = this._scanner.next();
732
}
733
const value = this._scanner.value.substring(start.pos, this._token.pos).replace(/\\(\$|}|\\)/g, '$1');
734
this._token = this._scanner.next();
735
return value;
736
}
737
738
private _parse(marker: Marker): boolean {
739
return this._parseEscaped(marker)
740
|| this._parseTabstopOrVariableName(marker)
741
|| this._parseComplexPlaceholder(marker)
742
|| this._parseComplexVariable(marker)
743
|| this._parseAnything(marker);
744
}
745
746
// \$, \\, \} -> just text
747
private _parseEscaped(marker: Marker): boolean {
748
let value: string;
749
if (value = this._accept(TokenType.Backslash, true)) {
750
// saw a backslash, append escaped token or that backslash
751
value = this._accept(TokenType.Dollar, true)
752
|| this._accept(TokenType.CurlyClose, true)
753
|| this._accept(TokenType.Backslash, true)
754
|| value;
755
756
marker.appendChild(new Text(value));
757
return true;
758
}
759
return false;
760
}
761
762
// $foo -> variable, $1 -> tabstop
763
private _parseTabstopOrVariableName(parent: Marker): boolean {
764
let value: string;
765
const token = this._token;
766
const match = this._accept(TokenType.Dollar)
767
&& (value = this._accept(TokenType.VariableName, true) || this._accept(TokenType.Int, true));
768
769
if (!match) {
770
return this._backTo(token);
771
}
772
773
parent.appendChild(/^\d+$/.test(value!)
774
? new Placeholder(Number(value!))
775
: new Variable(value!)
776
);
777
return true;
778
}
779
780
// ${1:<children>}, ${1} -> placeholder
781
private _parseComplexPlaceholder(parent: Marker): boolean {
782
let index: string;
783
const token = this._token;
784
const match = this._accept(TokenType.Dollar)
785
&& this._accept(TokenType.CurlyOpen)
786
&& (index = this._accept(TokenType.Int, true));
787
788
if (!match) {
789
return this._backTo(token);
790
}
791
792
const placeholder = new Placeholder(Number(index!));
793
794
if (this._accept(TokenType.Colon)) {
795
// ${1:<children>}
796
while (true) {
797
798
// ...} -> done
799
if (this._accept(TokenType.CurlyClose)) {
800
parent.appendChild(placeholder);
801
return true;
802
}
803
804
if (this._parse(placeholder)) {
805
continue;
806
}
807
808
// fallback
809
parent.appendChild(new Text('${' + index! + ':'));
810
placeholder.children.forEach(parent.appendChild, parent);
811
return true;
812
}
813
} else if (placeholder.index > 0 && this._accept(TokenType.Pipe)) {
814
// ${1|one,two,three|}
815
const choice = new Choice();
816
817
while (true) {
818
if (this._parseChoiceElement(choice)) {
819
820
if (this._accept(TokenType.Comma)) {
821
// opt, -> more
822
continue;
823
}
824
825
if (this._accept(TokenType.Pipe)) {
826
placeholder.appendChild(choice);
827
if (this._accept(TokenType.CurlyClose)) {
828
// ..|} -> done
829
parent.appendChild(placeholder);
830
return true;
831
}
832
}
833
}
834
835
this._backTo(token);
836
return false;
837
}
838
839
} else if (this._accept(TokenType.Forwardslash)) {
840
// ${1/<regex>/<format>/<options>}
841
if (this._parseTransform(placeholder)) {
842
parent.appendChild(placeholder);
843
return true;
844
}
845
846
this._backTo(token);
847
return false;
848
849
} else if (this._accept(TokenType.CurlyClose)) {
850
// ${1}
851
parent.appendChild(placeholder);
852
return true;
853
854
} else {
855
// ${1 <- missing curly or colon
856
return this._backTo(token);
857
}
858
}
859
860
private _parseChoiceElement(parent: Choice): boolean {
861
const token = this._token;
862
const values: string[] = [];
863
864
while (true) {
865
if (this._token.type === TokenType.Comma || this._token.type === TokenType.Pipe) {
866
break;
867
}
868
let value: string;
869
if (value = this._accept(TokenType.Backslash, true)) {
870
// \, \|, or \\
871
value = this._accept(TokenType.Comma, true)
872
|| this._accept(TokenType.Pipe, true)
873
|| this._accept(TokenType.Backslash, true)
874
|| value;
875
} else {
876
value = this._accept(undefined, true);
877
}
878
if (!value) {
879
// EOF
880
this._backTo(token);
881
return false;
882
}
883
values.push(value);
884
}
885
886
if (values.length === 0) {
887
this._backTo(token);
888
return false;
889
}
890
891
parent.appendChild(new Text(values.join('')));
892
return true;
893
}
894
895
// ${foo:<children>}, ${foo} -> variable
896
private _parseComplexVariable(parent: Marker): boolean {
897
let name: string;
898
const token = this._token;
899
const match = this._accept(TokenType.Dollar)
900
&& this._accept(TokenType.CurlyOpen)
901
&& (name = this._accept(TokenType.VariableName, true));
902
903
if (!match) {
904
return this._backTo(token);
905
}
906
907
const variable = new Variable(name!);
908
909
if (this._accept(TokenType.Colon)) {
910
// ${foo:<children>}
911
while (true) {
912
913
// ...} -> done
914
if (this._accept(TokenType.CurlyClose)) {
915
parent.appendChild(variable);
916
return true;
917
}
918
919
if (this._parse(variable)) {
920
continue;
921
}
922
923
// fallback
924
parent.appendChild(new Text('${' + name! + ':'));
925
variable.children.forEach(parent.appendChild, parent);
926
return true;
927
}
928
929
} else if (this._accept(TokenType.Forwardslash)) {
930
// ${foo/<regex>/<format>/<options>}
931
if (this._parseTransform(variable)) {
932
parent.appendChild(variable);
933
return true;
934
}
935
936
this._backTo(token);
937
return false;
938
939
} else if (this._accept(TokenType.CurlyClose)) {
940
// ${foo}
941
parent.appendChild(variable);
942
return true;
943
944
} else {
945
// ${foo <- missing curly or colon
946
return this._backTo(token);
947
}
948
}
949
950
private _parseTransform(parent: TransformableMarker): boolean {
951
// ...<regex>/<format>/<options>}
952
953
const transform = new Transform();
954
let regexValue = '';
955
let regexOptions = '';
956
957
// (1) /regex
958
while (true) {
959
if (this._accept(TokenType.Forwardslash)) {
960
break;
961
}
962
963
let escaped: string;
964
if (escaped = this._accept(TokenType.Backslash, true)) {
965
escaped = this._accept(TokenType.Forwardslash, true) || escaped;
966
regexValue += escaped;
967
continue;
968
}
969
970
if (this._token.type !== TokenType.EOF) {
971
regexValue += this._accept(undefined, true);
972
continue;
973
}
974
return false;
975
}
976
977
// (2) /format
978
while (true) {
979
if (this._accept(TokenType.Forwardslash)) {
980
break;
981
}
982
983
let escaped: string;
984
if (escaped = this._accept(TokenType.Backslash, true)) {
985
escaped = this._accept(TokenType.Backslash, true) || this._accept(TokenType.Forwardslash, true) || escaped;
986
transform.appendChild(new Text(escaped));
987
continue;
988
}
989
990
if (this._parseFormatString(transform) || this._parseAnything(transform)) {
991
continue;
992
}
993
return false;
994
}
995
996
// (3) /option
997
while (true) {
998
if (this._accept(TokenType.CurlyClose)) {
999
break;
1000
}
1001
if (this._token.type !== TokenType.EOF) {
1002
regexOptions += this._accept(undefined, true);
1003
continue;
1004
}
1005
return false;
1006
}
1007
1008
try {
1009
transform.regexp = new RegExp(regexValue, regexOptions);
1010
} catch (e) {
1011
// invalid regexp
1012
return false;
1013
}
1014
1015
parent.transform = transform;
1016
return true;
1017
}
1018
1019
private _parseFormatString(parent: Transform): boolean {
1020
1021
const token = this._token;
1022
if (!this._accept(TokenType.Dollar)) {
1023
return false;
1024
}
1025
1026
let complex = false;
1027
if (this._accept(TokenType.CurlyOpen)) {
1028
complex = true;
1029
}
1030
1031
const index = this._accept(TokenType.Int, true);
1032
1033
if (!index) {
1034
this._backTo(token);
1035
return false;
1036
1037
} else if (!complex) {
1038
// $1
1039
parent.appendChild(new FormatString(Number(index)));
1040
return true;
1041
1042
} else if (this._accept(TokenType.CurlyClose)) {
1043
// ${1}
1044
parent.appendChild(new FormatString(Number(index)));
1045
return true;
1046
1047
} else if (!this._accept(TokenType.Colon)) {
1048
this._backTo(token);
1049
return false;
1050
}
1051
1052
if (this._accept(TokenType.Forwardslash)) {
1053
// ${1:/upcase}
1054
const shorthand = this._accept(TokenType.VariableName, true);
1055
if (!shorthand || !this._accept(TokenType.CurlyClose)) {
1056
this._backTo(token);
1057
return false;
1058
} else {
1059
parent.appendChild(new FormatString(Number(index), shorthand));
1060
return true;
1061
}
1062
1063
} else if (this._accept(TokenType.Plus)) {
1064
// ${1:+<if>}
1065
const ifValue = this._until(TokenType.CurlyClose);
1066
if (ifValue) {
1067
parent.appendChild(new FormatString(Number(index), undefined, ifValue, undefined));
1068
return true;
1069
}
1070
1071
} else if (this._accept(TokenType.Dash)) {
1072
// ${2:-<else>}
1073
const elseValue = this._until(TokenType.CurlyClose);
1074
if (elseValue) {
1075
parent.appendChild(new FormatString(Number(index), undefined, undefined, elseValue));
1076
return true;
1077
}
1078
1079
} else if (this._accept(TokenType.QuestionMark)) {
1080
// ${2:?<if>:<else>}
1081
const ifValue = this._until(TokenType.Colon);
1082
if (ifValue) {
1083
const elseValue = this._until(TokenType.CurlyClose);
1084
if (elseValue) {
1085
parent.appendChild(new FormatString(Number(index), undefined, ifValue, elseValue));
1086
return true;
1087
}
1088
}
1089
1090
} else {
1091
// ${1:<else>}
1092
const elseValue = this._until(TokenType.CurlyClose);
1093
if (elseValue) {
1094
parent.appendChild(new FormatString(Number(index), undefined, undefined, elseValue));
1095
return true;
1096
}
1097
}
1098
1099
this._backTo(token);
1100
return false;
1101
}
1102
1103
private _parseAnything(marker: Marker): boolean {
1104
if (this._token.type !== TokenType.EOF) {
1105
marker.appendChild(new Text(this._scanner.tokenText(this._token)));
1106
this._accept(undefined);
1107
return true;
1108
}
1109
return false;
1110
}
1111
}
1112
1113