Path: blob/main/src/vs/editor/contrib/snippet/browser/snippetParser.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { CharCode } from '../../../../base/common/charCode.js';67export const enum TokenType {8Dollar,9Colon,10Comma,11CurlyOpen,12CurlyClose,13Backslash,14Forwardslash,15Pipe,16Int,17VariableName,18Format,19Plus,20Dash,21QuestionMark,22EOF23}2425export interface Token {26type: TokenType;27pos: number;28len: number;29}303132export class Scanner {3334private static _table: { [ch: number]: TokenType } = {35[CharCode.DollarSign]: TokenType.Dollar,36[CharCode.Colon]: TokenType.Colon,37[CharCode.Comma]: TokenType.Comma,38[CharCode.OpenCurlyBrace]: TokenType.CurlyOpen,39[CharCode.CloseCurlyBrace]: TokenType.CurlyClose,40[CharCode.Backslash]: TokenType.Backslash,41[CharCode.Slash]: TokenType.Forwardslash,42[CharCode.Pipe]: TokenType.Pipe,43[CharCode.Plus]: TokenType.Plus,44[CharCode.Dash]: TokenType.Dash,45[CharCode.QuestionMark]: TokenType.QuestionMark,46};4748static isDigitCharacter(ch: number): boolean {49return ch >= CharCode.Digit0 && ch <= CharCode.Digit9;50}5152static isVariableCharacter(ch: number): boolean {53return ch === CharCode.Underline54|| (ch >= CharCode.a && ch <= CharCode.z)55|| (ch >= CharCode.A && ch <= CharCode.Z);56}5758value: string = '';59pos: number = 0;6061text(value: string) {62this.value = value;63this.pos = 0;64}6566tokenText(token: Token): string {67return this.value.substr(token.pos, token.len);68}6970next(): Token {7172if (this.pos >= this.value.length) {73return { type: TokenType.EOF, pos: this.pos, len: 0 };74}7576const pos = this.pos;77let len = 0;78let ch = this.value.charCodeAt(pos);79let type: TokenType;8081// static types82type = Scanner._table[ch];83if (typeof type === 'number') {84this.pos += 1;85return { type, pos, len: 1 };86}8788// number89if (Scanner.isDigitCharacter(ch)) {90type = TokenType.Int;91do {92len += 1;93ch = this.value.charCodeAt(pos + len);94} while (Scanner.isDigitCharacter(ch));9596this.pos += len;97return { type, pos, len };98}99100// variable name101if (Scanner.isVariableCharacter(ch)) {102type = TokenType.VariableName;103do {104ch = this.value.charCodeAt(pos + (++len));105} while (Scanner.isVariableCharacter(ch) || Scanner.isDigitCharacter(ch));106107this.pos += len;108return { type, pos, len };109}110111112// format113type = TokenType.Format;114do {115len += 1;116ch = this.value.charCodeAt(pos + len);117} while (118!isNaN(ch)119&& typeof Scanner._table[ch] === 'undefined' // not static token120&& !Scanner.isDigitCharacter(ch) // not number121&& !Scanner.isVariableCharacter(ch) // not variable122);123124this.pos += len;125return { type, pos, len };126}127}128129export abstract class Marker {130131readonly _markerBrand: any;132133public parent!: Marker;134protected _children: Marker[] = [];135136appendChild(child: Marker): this {137if (child instanceof Text && this._children[this._children.length - 1] instanceof Text) {138// this and previous child are text -> merge them139(<Text>this._children[this._children.length - 1]).value += child.value;140} else {141// normal adoption of child142child.parent = this;143this._children.push(child);144}145return this;146}147148replace(child: Marker, others: Marker[]): void {149const { parent } = child;150const idx = parent.children.indexOf(child);151const newChildren = parent.children.slice(0);152newChildren.splice(idx, 1, ...others);153parent._children = newChildren;154155(function _fixParent(children: Marker[], parent: Marker) {156for (const child of children) {157child.parent = parent;158_fixParent(child.children, child);159}160})(others, parent);161}162163get children(): Marker[] {164return this._children;165}166167get rightMostDescendant(): Marker {168if (this._children.length > 0) {169return this._children[this._children.length - 1].rightMostDescendant;170}171return this;172}173174get snippet(): TextmateSnippet | undefined {175let candidate: Marker = this;176while (true) {177if (!candidate) {178return undefined;179}180if (candidate instanceof TextmateSnippet) {181return candidate;182}183candidate = candidate.parent;184}185}186187toString(): string {188return this.children.reduce((prev, cur) => prev + cur.toString(), '');189}190191abstract toTextmateString(): string;192193len(): number {194return 0;195}196197abstract clone(): Marker;198}199200export class Text extends Marker {201202static escape(value: string): string {203return value.replace(/\$|}|\\/g, '\\$&');204}205206constructor(public value: string) {207super();208}209override toString() {210return this.value;211}212toTextmateString(): string {213return Text.escape(this.value);214}215override len(): number {216return this.value.length;217}218clone(): Text {219return new Text(this.value);220}221}222223export abstract class TransformableMarker extends Marker {224public transform?: Transform;225}226227export class Placeholder extends TransformableMarker {228static compareByIndex(a: Placeholder, b: Placeholder): number {229if (a.index === b.index) {230return 0;231} else if (a.isFinalTabstop) {232return 1;233} else if (b.isFinalTabstop) {234return -1;235} else if (a.index < b.index) {236return -1;237} else if (a.index > b.index) {238return 1;239} else {240return 0;241}242}243244constructor(public index: number) {245super();246}247248get isFinalTabstop() {249return this.index === 0;250}251252get choice(): Choice | undefined {253return this._children.length === 1 && this._children[0] instanceof Choice254? this._children[0] as Choice255: undefined;256}257258toTextmateString(): string {259let transformString = '';260if (this.transform) {261transformString = this.transform.toTextmateString();262}263if (this.children.length === 0 && !this.transform) {264return `\$${this.index}`;265} else if (this.children.length === 0) {266return `\${${this.index}${transformString}}`;267} else if (this.choice) {268return `\${${this.index}|${this.choice.toTextmateString()}|${transformString}}`;269} else {270return `\${${this.index}:${this.children.map(child => child.toTextmateString()).join('')}${transformString}}`;271}272}273274clone(): Placeholder {275const ret = new Placeholder(this.index);276if (this.transform) {277ret.transform = this.transform.clone();278}279ret._children = this.children.map(child => child.clone());280return ret;281}282}283284export class Choice extends Marker {285286readonly options: Text[] = [];287288override appendChild(marker: Marker): this {289if (marker instanceof Text) {290marker.parent = this;291this.options.push(marker);292}293return this;294}295296override toString() {297return this.options[0].value;298}299300toTextmateString(): string {301return this.options302.map(option => option.value.replace(/\||,|\\/g, '\\$&'))303.join(',');304}305306override len(): number {307return this.options[0].len();308}309310clone(): Choice {311const ret = new Choice();312this.options.forEach(ret.appendChild, ret);313return ret;314}315}316317export class Transform extends Marker {318319regexp: RegExp = new RegExp('');320321resolve(value: string): string {322const _this = this;323let didMatch = false;324let ret = value.replace(this.regexp, function () {325didMatch = true;326return _this._replace(Array.prototype.slice.call(arguments, 0, -2));327});328// when the regex didn't match and when the transform has329// else branches, then run those330if (!didMatch && this._children.some(child => child instanceof FormatString && Boolean(child.elseValue))) {331ret = this._replace([]);332}333return ret;334}335336private _replace(groups: string[]): string {337let ret = '';338for (const marker of this._children) {339if (marker instanceof FormatString) {340let value = groups[marker.index] || '';341value = marker.resolve(value);342ret += value;343} else {344ret += marker.toString();345}346}347return ret;348}349350override toString(): string {351return '';352}353354toTextmateString(): string {355return `/${this.regexp.source}/${this.children.map(c => c.toTextmateString())}/${(this.regexp.ignoreCase ? 'i' : '') + (this.regexp.global ? 'g' : '')}`;356}357358clone(): Transform {359const ret = new Transform();360ret.regexp = new RegExp(this.regexp.source, '' + (this.regexp.ignoreCase ? 'i' : '') + (this.regexp.global ? 'g' : ''));361ret._children = this.children.map(child => child.clone());362return ret;363}364365}366367export class FormatString extends Marker {368369constructor(370readonly index: number,371readonly shorthandName?: string,372readonly ifValue?: string,373readonly elseValue?: string,374) {375super();376}377378resolve(value?: string): string {379if (this.shorthandName === 'upcase') {380return !value ? '' : value.toLocaleUpperCase();381} else if (this.shorthandName === 'downcase') {382return !value ? '' : value.toLocaleLowerCase();383} else if (this.shorthandName === 'capitalize') {384return !value ? '' : (value[0].toLocaleUpperCase() + value.substr(1));385} else if (this.shorthandName === 'pascalcase') {386return !value ? '' : this._toPascalCase(value);387} else if (this.shorthandName === 'camelcase') {388return !value ? '' : this._toCamelCase(value);389} else if (Boolean(value) && typeof this.ifValue === 'string') {390return this.ifValue;391} else if (!Boolean(value) && typeof this.elseValue === 'string') {392return this.elseValue;393} else {394return value || '';395}396}397398private _toPascalCase(value: string): string {399const match = value.match(/[a-z0-9]+/gi);400if (!match) {401return value;402}403return match.map(word => {404return word.charAt(0).toUpperCase() + word.substr(1);405})406.join('');407}408409private _toCamelCase(value: string): string {410const match = value.match(/[a-z0-9]+/gi);411if (!match) {412return value;413}414return match.map((word, index) => {415if (index === 0) {416return word.charAt(0).toLowerCase() + word.substr(1);417}418return word.charAt(0).toUpperCase() + word.substr(1);419})420.join('');421}422423toTextmateString(): string {424let value = '${';425value += this.index;426if (this.shorthandName) {427value += `:/${this.shorthandName}`;428429} else if (this.ifValue && this.elseValue) {430value += `:?${this.ifValue}:${this.elseValue}`;431} else if (this.ifValue) {432value += `:+${this.ifValue}`;433} else if (this.elseValue) {434value += `:-${this.elseValue}`;435}436value += '}';437return value;438}439440clone(): FormatString {441const ret = new FormatString(this.index, this.shorthandName, this.ifValue, this.elseValue);442return ret;443}444}445446export class Variable extends TransformableMarker {447448constructor(public name: string) {449super();450}451452resolve(resolver: VariableResolver): boolean {453let value = resolver.resolve(this);454if (this.transform) {455value = this.transform.resolve(value || '');456}457if (value !== undefined) {458this._children = [new Text(value)];459return true;460}461return false;462}463464toTextmateString(): string {465let transformString = '';466if (this.transform) {467transformString = this.transform.toTextmateString();468}469if (this.children.length === 0) {470return `\${${this.name}${transformString}}`;471} else {472return `\${${this.name}:${this.children.map(child => child.toTextmateString()).join('')}${transformString}}`;473}474}475476clone(): Variable {477const ret = new Variable(this.name);478if (this.transform) {479ret.transform = this.transform.clone();480}481ret._children = this.children.map(child => child.clone());482return ret;483}484}485486export interface VariableResolver {487resolve(variable: Variable): string | undefined;488}489490function walk(marker: Marker[], visitor: (marker: Marker) => boolean): void {491const stack = [...marker];492while (stack.length > 0) {493const marker = stack.shift()!;494const recurse = visitor(marker);495if (!recurse) {496break;497}498stack.unshift(...marker.children);499}500}501502export class TextmateSnippet extends Marker {503504private _placeholders?: { all: Placeholder[]; last?: Placeholder };505506get placeholderInfo() {507if (!this._placeholders) {508// fill in placeholders509const all: Placeholder[] = [];510let last: Placeholder | undefined;511this.walk(function (candidate) {512if (candidate instanceof Placeholder) {513all.push(candidate);514last = !last || last.index < candidate.index ? candidate : last;515}516return true;517});518this._placeholders = { all, last };519}520return this._placeholders;521}522523get placeholders(): Placeholder[] {524const { all } = this.placeholderInfo;525return all;526}527528offset(marker: Marker): number {529let pos = 0;530let found = false;531this.walk(candidate => {532if (candidate === marker) {533found = true;534return false;535}536pos += candidate.len();537return true;538});539540if (!found) {541return -1;542}543return pos;544}545546fullLen(marker: Marker): number {547let ret = 0;548walk([marker], marker => {549ret += marker.len();550return true;551});552return ret;553}554555enclosingPlaceholders(placeholder: Placeholder): Placeholder[] {556const ret: Placeholder[] = [];557let { parent } = placeholder;558while (parent) {559if (parent instanceof Placeholder) {560ret.push(parent);561}562parent = parent.parent;563}564return ret;565}566567resolveVariables(resolver: VariableResolver): this {568this.walk(candidate => {569if (candidate instanceof Variable) {570if (candidate.resolve(resolver)) {571this._placeholders = undefined;572}573}574return true;575});576return this;577}578579override appendChild(child: Marker) {580this._placeholders = undefined;581return super.appendChild(child);582}583584override replace(child: Marker, others: Marker[]): void {585this._placeholders = undefined;586return super.replace(child, others);587}588589toTextmateString(): string {590return this.children.reduce((prev, cur) => prev + cur.toTextmateString(), '');591}592593clone(): TextmateSnippet {594const ret = new TextmateSnippet();595this._children = this.children.map(child => child.clone());596return ret;597}598599walk(visitor: (marker: Marker) => boolean): void {600walk(this.children, visitor);601}602}603604export class SnippetParser {605606static escape(value: string): string {607return value.replace(/\$|}|\\/g, '\\$&');608}609610/**611* Takes a snippet and returns the insertable string, e.g return the snippet-string612* without any placeholder, tabstop, variables etc...613*/614static asInsertText(value: string): string {615return new SnippetParser().parse(value).toString();616}617618static guessNeedsClipboard(template: string): boolean {619return /\${?CLIPBOARD/.test(template);620}621622private _scanner: Scanner = new Scanner();623private _token: Token = { type: TokenType.EOF, pos: 0, len: 0 };624625parse(value: string, insertFinalTabstop?: boolean, enforceFinalTabstop?: boolean): TextmateSnippet {626const snippet = new TextmateSnippet();627this.parseFragment(value, snippet);628this.ensureFinalTabstop(snippet, enforceFinalTabstop ?? false, insertFinalTabstop ?? false);629return snippet;630}631632parseFragment(value: string, snippet: TextmateSnippet): readonly Marker[] {633634const offset = snippet.children.length;635this._scanner.text(value);636this._token = this._scanner.next();637while (this._parse(snippet)) {638// nothing639}640641// fill in values for placeholders. the first placeholder of an index642// that has a value defines the value for all placeholders with that index643const placeholderDefaultValues = new Map<number, Marker[] | undefined>();644const incompletePlaceholders: Placeholder[] = [];645snippet.walk(marker => {646if (marker instanceof Placeholder) {647if (marker.isFinalTabstop) {648placeholderDefaultValues.set(0, undefined);649} else if (!placeholderDefaultValues.has(marker.index) && marker.children.length > 0) {650placeholderDefaultValues.set(marker.index, marker.children);651} else {652incompletePlaceholders.push(marker);653}654}655return true;656});657658const fillInIncompletePlaceholder = (placeholder: Placeholder, stack: Set<number>) => {659const defaultValues = placeholderDefaultValues.get(placeholder.index);660if (!defaultValues) {661return;662}663const clone = new Placeholder(placeholder.index);664clone.transform = placeholder.transform;665for (const child of defaultValues) {666const newChild = child.clone();667clone.appendChild(newChild);668669// "recurse" on children that are again placeholders670if (newChild instanceof Placeholder && placeholderDefaultValues.has(newChild.index) && !stack.has(newChild.index)) {671stack.add(newChild.index);672fillInIncompletePlaceholder(newChild, stack);673stack.delete(newChild.index);674}675}676snippet.replace(placeholder, [clone]);677};678679const stack = new Set<number>();680for (const placeholder of incompletePlaceholders) {681fillInIncompletePlaceholder(placeholder, stack);682}683684return snippet.children.slice(offset);685}686687ensureFinalTabstop(snippet: TextmateSnippet, enforceFinalTabstop: boolean, insertFinalTabstop: boolean) {688689if (enforceFinalTabstop || insertFinalTabstop && snippet.placeholders.length > 0) {690const finalTabstop = snippet.placeholders.find(p => p.index === 0);691if (!finalTabstop) {692// the snippet uses placeholders but has no693// final tabstop defined -> insert at the end694snippet.appendChild(new Placeholder(0));695}696}697698}699700private _accept(type?: TokenType): boolean;701private _accept(type: TokenType | undefined, value: true): string;702private _accept(type: TokenType, value?: boolean): boolean | string {703if (type === undefined || this._token.type === type) {704const ret = !value ? true : this._scanner.tokenText(this._token);705this._token = this._scanner.next();706return ret;707}708return false;709}710711private _backTo(token: Token): false {712this._scanner.pos = token.pos + token.len;713this._token = token;714return false;715}716717private _until(type: TokenType): false | string {718const start = this._token;719while (this._token.type !== type) {720if (this._token.type === TokenType.EOF) {721return false;722} else if (this._token.type === TokenType.Backslash) {723const nextToken = this._scanner.next();724if (nextToken.type !== TokenType.Dollar725&& nextToken.type !== TokenType.CurlyClose726&& nextToken.type !== TokenType.Backslash) {727return false;728}729}730this._token = this._scanner.next();731}732const value = this._scanner.value.substring(start.pos, this._token.pos).replace(/\\(\$|}|\\)/g, '$1');733this._token = this._scanner.next();734return value;735}736737private _parse(marker: Marker): boolean {738return this._parseEscaped(marker)739|| this._parseTabstopOrVariableName(marker)740|| this._parseComplexPlaceholder(marker)741|| this._parseComplexVariable(marker)742|| this._parseAnything(marker);743}744745// \$, \\, \} -> just text746private _parseEscaped(marker: Marker): boolean {747let value: string;748if (value = this._accept(TokenType.Backslash, true)) {749// saw a backslash, append escaped token or that backslash750value = this._accept(TokenType.Dollar, true)751|| this._accept(TokenType.CurlyClose, true)752|| this._accept(TokenType.Backslash, true)753|| value;754755marker.appendChild(new Text(value));756return true;757}758return false;759}760761// $foo -> variable, $1 -> tabstop762private _parseTabstopOrVariableName(parent: Marker): boolean {763let value: string;764const token = this._token;765const match = this._accept(TokenType.Dollar)766&& (value = this._accept(TokenType.VariableName, true) || this._accept(TokenType.Int, true));767768if (!match) {769return this._backTo(token);770}771772parent.appendChild(/^\d+$/.test(value!)773? new Placeholder(Number(value!))774: new Variable(value!)775);776return true;777}778779// ${1:<children>}, ${1} -> placeholder780private _parseComplexPlaceholder(parent: Marker): boolean {781let index: string;782const token = this._token;783const match = this._accept(TokenType.Dollar)784&& this._accept(TokenType.CurlyOpen)785&& (index = this._accept(TokenType.Int, true));786787if (!match) {788return this._backTo(token);789}790791const placeholder = new Placeholder(Number(index!));792793if (this._accept(TokenType.Colon)) {794// ${1:<children>}795while (true) {796797// ...} -> done798if (this._accept(TokenType.CurlyClose)) {799parent.appendChild(placeholder);800return true;801}802803if (this._parse(placeholder)) {804continue;805}806807// fallback808parent.appendChild(new Text('${' + index! + ':'));809placeholder.children.forEach(parent.appendChild, parent);810return true;811}812} else if (placeholder.index > 0 && this._accept(TokenType.Pipe)) {813// ${1|one,two,three|}814const choice = new Choice();815816while (true) {817if (this._parseChoiceElement(choice)) {818819if (this._accept(TokenType.Comma)) {820// opt, -> more821continue;822}823824if (this._accept(TokenType.Pipe)) {825placeholder.appendChild(choice);826if (this._accept(TokenType.CurlyClose)) {827// ..|} -> done828parent.appendChild(placeholder);829return true;830}831}832}833834this._backTo(token);835return false;836}837838} else if (this._accept(TokenType.Forwardslash)) {839// ${1/<regex>/<format>/<options>}840if (this._parseTransform(placeholder)) {841parent.appendChild(placeholder);842return true;843}844845this._backTo(token);846return false;847848} else if (this._accept(TokenType.CurlyClose)) {849// ${1}850parent.appendChild(placeholder);851return true;852853} else {854// ${1 <- missing curly or colon855return this._backTo(token);856}857}858859private _parseChoiceElement(parent: Choice): boolean {860const token = this._token;861const values: string[] = [];862863while (true) {864if (this._token.type === TokenType.Comma || this._token.type === TokenType.Pipe) {865break;866}867let value: string;868if (value = this._accept(TokenType.Backslash, true)) {869// \, \|, or \\870value = this._accept(TokenType.Comma, true)871|| this._accept(TokenType.Pipe, true)872|| this._accept(TokenType.Backslash, true)873|| value;874} else {875value = this._accept(undefined, true);876}877if (!value) {878// EOF879this._backTo(token);880return false;881}882values.push(value);883}884885if (values.length === 0) {886this._backTo(token);887return false;888}889890parent.appendChild(new Text(values.join('')));891return true;892}893894// ${foo:<children>}, ${foo} -> variable895private _parseComplexVariable(parent: Marker): boolean {896let name: string;897const token = this._token;898const match = this._accept(TokenType.Dollar)899&& this._accept(TokenType.CurlyOpen)900&& (name = this._accept(TokenType.VariableName, true));901902if (!match) {903return this._backTo(token);904}905906const variable = new Variable(name!);907908if (this._accept(TokenType.Colon)) {909// ${foo:<children>}910while (true) {911912// ...} -> done913if (this._accept(TokenType.CurlyClose)) {914parent.appendChild(variable);915return true;916}917918if (this._parse(variable)) {919continue;920}921922// fallback923parent.appendChild(new Text('${' + name! + ':'));924variable.children.forEach(parent.appendChild, parent);925return true;926}927928} else if (this._accept(TokenType.Forwardslash)) {929// ${foo/<regex>/<format>/<options>}930if (this._parseTransform(variable)) {931parent.appendChild(variable);932return true;933}934935this._backTo(token);936return false;937938} else if (this._accept(TokenType.CurlyClose)) {939// ${foo}940parent.appendChild(variable);941return true;942943} else {944// ${foo <- missing curly or colon945return this._backTo(token);946}947}948949private _parseTransform(parent: TransformableMarker): boolean {950// ...<regex>/<format>/<options>}951952const transform = new Transform();953let regexValue = '';954let regexOptions = '';955956// (1) /regex957while (true) {958if (this._accept(TokenType.Forwardslash)) {959break;960}961962let escaped: string;963if (escaped = this._accept(TokenType.Backslash, true)) {964escaped = this._accept(TokenType.Forwardslash, true) || escaped;965regexValue += escaped;966continue;967}968969if (this._token.type !== TokenType.EOF) {970regexValue += this._accept(undefined, true);971continue;972}973return false;974}975976// (2) /format977while (true) {978if (this._accept(TokenType.Forwardslash)) {979break;980}981982let escaped: string;983if (escaped = this._accept(TokenType.Backslash, true)) {984escaped = this._accept(TokenType.Backslash, true) || this._accept(TokenType.Forwardslash, true) || escaped;985transform.appendChild(new Text(escaped));986continue;987}988989if (this._parseFormatString(transform) || this._parseAnything(transform)) {990continue;991}992return false;993}994995// (3) /option996while (true) {997if (this._accept(TokenType.CurlyClose)) {998break;999}1000if (this._token.type !== TokenType.EOF) {1001regexOptions += this._accept(undefined, true);1002continue;1003}1004return false;1005}10061007try {1008transform.regexp = new RegExp(regexValue, regexOptions);1009} catch (e) {1010// invalid regexp1011return false;1012}10131014parent.transform = transform;1015return true;1016}10171018private _parseFormatString(parent: Transform): boolean {10191020const token = this._token;1021if (!this._accept(TokenType.Dollar)) {1022return false;1023}10241025let complex = false;1026if (this._accept(TokenType.CurlyOpen)) {1027complex = true;1028}10291030const index = this._accept(TokenType.Int, true);10311032if (!index) {1033this._backTo(token);1034return false;10351036} else if (!complex) {1037// $11038parent.appendChild(new FormatString(Number(index)));1039return true;10401041} else if (this._accept(TokenType.CurlyClose)) {1042// ${1}1043parent.appendChild(new FormatString(Number(index)));1044return true;10451046} else if (!this._accept(TokenType.Colon)) {1047this._backTo(token);1048return false;1049}10501051if (this._accept(TokenType.Forwardslash)) {1052// ${1:/upcase}1053const shorthand = this._accept(TokenType.VariableName, true);1054if (!shorthand || !this._accept(TokenType.CurlyClose)) {1055this._backTo(token);1056return false;1057} else {1058parent.appendChild(new FormatString(Number(index), shorthand));1059return true;1060}10611062} else if (this._accept(TokenType.Plus)) {1063// ${1:+<if>}1064const ifValue = this._until(TokenType.CurlyClose);1065if (ifValue) {1066parent.appendChild(new FormatString(Number(index), undefined, ifValue, undefined));1067return true;1068}10691070} else if (this._accept(TokenType.Dash)) {1071// ${2:-<else>}1072const elseValue = this._until(TokenType.CurlyClose);1073if (elseValue) {1074parent.appendChild(new FormatString(Number(index), undefined, undefined, elseValue));1075return true;1076}10771078} else if (this._accept(TokenType.QuestionMark)) {1079// ${2:?<if>:<else>}1080const ifValue = this._until(TokenType.Colon);1081if (ifValue) {1082const elseValue = this._until(TokenType.CurlyClose);1083if (elseValue) {1084parent.appendChild(new FormatString(Number(index), undefined, ifValue, elseValue));1085return true;1086}1087}10881089} else {1090// ${1:<else>}1091const elseValue = this._until(TokenType.CurlyClose);1092if (elseValue) {1093parent.appendChild(new FormatString(Number(index), undefined, undefined, elseValue));1094return true;1095}1096}10971098this._backTo(token);1099return false;1100}11011102private _parseAnything(marker: Marker): boolean {1103if (this._token.type !== TokenType.EOF) {1104marker.appendChild(new Text(this._scanner.tokenText(this._token)));1105this._accept(undefined);1106return true;1107}1108return false;1109}1110}111111121113