Path: blob/main/src/vs/editor/contrib/snippet/browser/snippetParser.ts
5272 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: undefined;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]255: 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 (this.shorthandName === 'kebabcase') {390return !value ? '' : this._toKebabCase(value);391} else if (this.shorthandName === 'snakecase') {392return !value ? '' : this._toSnakeCase(value);393} else if (Boolean(value) && typeof this.ifValue === 'string') {394return this.ifValue;395} else if (!Boolean(value) && typeof this.elseValue === 'string') {396return this.elseValue;397} else {398return value || '';399}400}401402// Note: word-based case transforms rely on uppercase/lowercase distinctions.403// For scripts without case, transforms are effectively no-ops.404private _toKebabCase(value: string): string {405const match = value.match(/[\p{L}0-9]+/gu);406if (!match) {407return value;408}409410if (!value.match(/[\p{L}0-9]/u)) {411return value412.trim()413.toLowerCase()414.replace(/^_+|_+$/g, '')415.replace(/[\s_]+/g, '-');416}417418const cleaned = value.trim().replace(/^_+|_+$/g, '');419420const 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);421422if (!match2) {423return cleaned424.split(/[\s_-]+/)425.filter(word => word.length > 0)426.map(word => word.toLowerCase())427.join('-');428}429430return match2431.map(x => x.toLowerCase())432.join('-');433}434435private _toPascalCase(value: string): string {436const match = value.match(/[\p{L}0-9]+/gu);437if (!match) {438return value;439}440return match.map(word => {441return word.charAt(0).toUpperCase() + word.substr(1);442})443.join('');444}445446private _toCamelCase(value: string): string {447const match = value.match(/[\p{L}0-9]+/gu);448if (!match) {449return value;450}451return match.map((word, index) => {452if (index === 0) {453return word.charAt(0).toLowerCase() + word.substr(1);454}455return word.charAt(0).toUpperCase() + word.substr(1);456})457.join('');458}459460private _toSnakeCase(value: string): string {461return value.replace(/(\p{Ll})(\p{Lu})/gu, '$1_$2')462.replace(/[\s\-]+/g, '_')463.toLowerCase();464}465466toTextmateString(): string {467let value = '${';468value += this.index;469if (this.shorthandName) {470value += `:/${this.shorthandName}`;471472} else if (this.ifValue && this.elseValue) {473value += `:?${this.ifValue}:${this.elseValue}`;474} else if (this.ifValue) {475value += `:+${this.ifValue}`;476} else if (this.elseValue) {477value += `:-${this.elseValue}`;478}479value += '}';480return value;481}482483clone(): FormatString {484const ret = new FormatString(this.index, this.shorthandName, this.ifValue, this.elseValue);485return ret;486}487}488489export class Variable extends TransformableMarker {490491constructor(public name: string) {492super();493}494495resolve(resolver: VariableResolver): boolean {496let value = resolver.resolve(this);497if (this.transform) {498value = this.transform.resolve(value || '');499}500if (value !== undefined) {501this._children = [new Text(value)];502return true;503}504return false;505}506507toTextmateString(): string {508let transformString = '';509if (this.transform) {510transformString = this.transform.toTextmateString();511}512if (this.children.length === 0) {513return `\${${this.name}${transformString}}`;514} else {515return `\${${this.name}:${this.children.map(child => child.toTextmateString()).join('')}${transformString}}`;516}517}518519clone(): Variable {520const ret = new Variable(this.name);521if (this.transform) {522ret.transform = this.transform.clone();523}524ret._children = this.children.map(child => child.clone());525return ret;526}527}528529export interface VariableResolver {530resolve(variable: Variable): string | undefined;531}532533function walk(marker: Marker[], visitor: (marker: Marker) => boolean): void {534const stack = [...marker];535while (stack.length > 0) {536const marker = stack.shift()!;537const recurse = visitor(marker);538if (!recurse) {539break;540}541stack.unshift(...marker.children);542}543}544545export class TextmateSnippet extends Marker {546547private _placeholders?: { all: Placeholder[]; last?: Placeholder };548549get placeholderInfo() {550if (!this._placeholders) {551// fill in placeholders552const all: Placeholder[] = [];553let last: Placeholder | undefined;554this.walk(function (candidate) {555if (candidate instanceof Placeholder) {556all.push(candidate);557last = !last || last.index < candidate.index ? candidate : last;558}559return true;560});561this._placeholders = { all, last };562}563return this._placeholders;564}565566get placeholders(): Placeholder[] {567const { all } = this.placeholderInfo;568return all;569}570571offset(marker: Marker): number {572let pos = 0;573let found = false;574this.walk(candidate => {575if (candidate === marker) {576found = true;577return false;578}579pos += candidate.len();580return true;581});582583if (!found) {584return -1;585}586return pos;587}588589fullLen(marker: Marker): number {590let ret = 0;591walk([marker], marker => {592ret += marker.len();593return true;594});595return ret;596}597598enclosingPlaceholders(placeholder: Placeholder): Placeholder[] {599const ret: Placeholder[] = [];600let { parent } = placeholder;601while (parent) {602if (parent instanceof Placeholder) {603ret.push(parent);604}605parent = parent.parent;606}607return ret;608}609610resolveVariables(resolver: VariableResolver): this {611this.walk(candidate => {612if (candidate instanceof Variable) {613if (candidate.resolve(resolver)) {614this._placeholders = undefined;615}616}617return true;618});619return this;620}621622override appendChild(child: Marker) {623this._placeholders = undefined;624return super.appendChild(child);625}626627override replace(child: Marker, others: Marker[]): void {628this._placeholders = undefined;629return super.replace(child, others);630}631632toTextmateString(): string {633return this.children.reduce((prev, cur) => prev + cur.toTextmateString(), '');634}635636clone(): TextmateSnippet {637const ret = new TextmateSnippet();638this._children = this.children.map(child => child.clone());639return ret;640}641642walk(visitor: (marker: Marker) => boolean): void {643walk(this.children, visitor);644}645}646647export class SnippetParser {648649static escape(value: string): string {650return value.replace(/\$|}|\\/g, '\\$&');651}652653/**654* Takes a snippet and returns the insertable string, e.g return the snippet-string655* without any placeholder, tabstop, variables etc...656*/657static asInsertText(value: string): string {658return new SnippetParser().parse(value).toString();659}660661static guessNeedsClipboard(template: string): boolean {662return /\${?CLIPBOARD/.test(template);663}664665private _scanner: Scanner = new Scanner();666private _token: Token = { type: TokenType.EOF, pos: 0, len: 0 };667668parse(value: string, insertFinalTabstop?: boolean, enforceFinalTabstop?: boolean): TextmateSnippet {669const snippet = new TextmateSnippet();670this.parseFragment(value, snippet);671this.ensureFinalTabstop(snippet, enforceFinalTabstop ?? false, insertFinalTabstop ?? false);672return snippet;673}674675parseFragment(value: string, snippet: TextmateSnippet): readonly Marker[] {676677const offset = snippet.children.length;678this._scanner.text(value);679this._token = this._scanner.next();680while (this._parse(snippet)) {681// nothing682}683684// fill in values for placeholders. the first placeholder of an index685// that has a value defines the value for all placeholders with that index686const placeholderDefaultValues = new Map<number, Marker[] | undefined>();687const incompletePlaceholders: Placeholder[] = [];688snippet.walk(marker => {689if (marker instanceof Placeholder) {690if (marker.isFinalTabstop) {691placeholderDefaultValues.set(0, undefined);692} else if (!placeholderDefaultValues.has(marker.index) && marker.children.length > 0) {693placeholderDefaultValues.set(marker.index, marker.children);694} else {695incompletePlaceholders.push(marker);696}697}698return true;699});700701const fillInIncompletePlaceholder = (placeholder: Placeholder, stack: Set<number>) => {702const defaultValues = placeholderDefaultValues.get(placeholder.index);703if (!defaultValues) {704return;705}706const clone = new Placeholder(placeholder.index);707clone.transform = placeholder.transform;708for (const child of defaultValues) {709const newChild = child.clone();710clone.appendChild(newChild);711712// "recurse" on children that are again placeholders713if (newChild instanceof Placeholder && placeholderDefaultValues.has(newChild.index) && !stack.has(newChild.index)) {714stack.add(newChild.index);715fillInIncompletePlaceholder(newChild, stack);716stack.delete(newChild.index);717}718}719snippet.replace(placeholder, [clone]);720};721722const stack = new Set<number>();723for (const placeholder of incompletePlaceholders) {724fillInIncompletePlaceholder(placeholder, stack);725}726727return snippet.children.slice(offset);728}729730ensureFinalTabstop(snippet: TextmateSnippet, enforceFinalTabstop: boolean, insertFinalTabstop: boolean) {731732if (enforceFinalTabstop || insertFinalTabstop && snippet.placeholders.length > 0) {733const finalTabstop = snippet.placeholders.find(p => p.index === 0);734if (!finalTabstop) {735// the snippet uses placeholders but has no736// final tabstop defined -> insert at the end737snippet.appendChild(new Placeholder(0));738}739}740741}742743private _accept(type?: TokenType): boolean;744private _accept(type: TokenType | undefined, value: true): string;745private _accept(type: TokenType, value?: boolean): boolean | string {746if (type === undefined || this._token.type === type) {747const ret = !value ? true : this._scanner.tokenText(this._token);748this._token = this._scanner.next();749return ret;750}751return false;752}753754private _backTo(token: Token): false {755this._scanner.pos = token.pos + token.len;756this._token = token;757return false;758}759760private _until(type: TokenType): false | string {761const start = this._token;762while (this._token.type !== type) {763if (this._token.type === TokenType.EOF) {764return false;765} else if (this._token.type === TokenType.Backslash) {766const nextToken = this._scanner.next();767if (nextToken.type !== TokenType.Dollar768&& nextToken.type !== TokenType.CurlyClose769&& nextToken.type !== TokenType.Backslash) {770return false;771}772}773this._token = this._scanner.next();774}775const value = this._scanner.value.substring(start.pos, this._token.pos).replace(/\\(\$|}|\\)/g, '$1');776this._token = this._scanner.next();777return value;778}779780private _parse(marker: Marker): boolean {781return this._parseEscaped(marker)782|| this._parseTabstopOrVariableName(marker)783|| this._parseComplexPlaceholder(marker)784|| this._parseComplexVariable(marker)785|| this._parseAnything(marker);786}787788// \$, \\, \} -> just text789private _parseEscaped(marker: Marker): boolean {790let value: string;791if (value = this._accept(TokenType.Backslash, true)) {792// saw a backslash, append escaped token or that backslash793value = this._accept(TokenType.Dollar, true)794|| this._accept(TokenType.CurlyClose, true)795|| this._accept(TokenType.Backslash, true)796|| value;797798marker.appendChild(new Text(value));799return true;800}801return false;802}803804// $foo -> variable, $1 -> tabstop805private _parseTabstopOrVariableName(parent: Marker): boolean {806let value: string;807const token = this._token;808const match = this._accept(TokenType.Dollar)809&& (value = this._accept(TokenType.VariableName, true) || this._accept(TokenType.Int, true));810811if (!match) {812return this._backTo(token);813}814815parent.appendChild(/^\d+$/.test(value!)816? new Placeholder(Number(value!))817: new Variable(value!)818);819return true;820}821822// ${1:<children>}, ${1} -> placeholder823private _parseComplexPlaceholder(parent: Marker): boolean {824let index: string;825const token = this._token;826const match = this._accept(TokenType.Dollar)827&& this._accept(TokenType.CurlyOpen)828&& (index = this._accept(TokenType.Int, true));829830if (!match) {831return this._backTo(token);832}833834const placeholder = new Placeholder(Number(index!));835836if (this._accept(TokenType.Colon)) {837// ${1:<children>}838while (true) {839840// ...} -> done841if (this._accept(TokenType.CurlyClose)) {842parent.appendChild(placeholder);843return true;844}845846if (this._parse(placeholder)) {847continue;848}849850// fallback851parent.appendChild(new Text('${' + index! + ':'));852placeholder.children.forEach(parent.appendChild, parent);853return true;854}855} else if (placeholder.index > 0 && this._accept(TokenType.Pipe)) {856// ${1|one,two,three|}857const choice = new Choice();858859while (true) {860if (this._parseChoiceElement(choice)) {861862if (this._accept(TokenType.Comma)) {863// opt, -> more864continue;865}866867if (this._accept(TokenType.Pipe)) {868placeholder.appendChild(choice);869if (this._accept(TokenType.CurlyClose)) {870// ..|} -> done871parent.appendChild(placeholder);872return true;873}874}875}876877this._backTo(token);878return false;879}880881} else if (this._accept(TokenType.Forwardslash)) {882// ${1/<regex>/<format>/<options>}883if (this._parseTransform(placeholder)) {884parent.appendChild(placeholder);885return true;886}887888this._backTo(token);889return false;890891} else if (this._accept(TokenType.CurlyClose)) {892// ${1}893parent.appendChild(placeholder);894return true;895896} else {897// ${1 <- missing curly or colon898return this._backTo(token);899}900}901902private _parseChoiceElement(parent: Choice): boolean {903const token = this._token;904const values: string[] = [];905906while (true) {907if (this._token.type === TokenType.Comma || this._token.type === TokenType.Pipe) {908break;909}910let value: string;911if (value = this._accept(TokenType.Backslash, true)) {912// \, \|, or \\913value = this._accept(TokenType.Comma, true)914|| this._accept(TokenType.Pipe, true)915|| this._accept(TokenType.Backslash, true)916|| value;917} else {918value = this._accept(undefined, true);919}920if (!value) {921// EOF922this._backTo(token);923return false;924}925values.push(value);926}927928if (values.length === 0) {929this._backTo(token);930return false;931}932933parent.appendChild(new Text(values.join('')));934return true;935}936937// ${foo:<children>}, ${foo} -> variable938private _parseComplexVariable(parent: Marker): boolean {939let name: string;940const token = this._token;941const match = this._accept(TokenType.Dollar)942&& this._accept(TokenType.CurlyOpen)943&& (name = this._accept(TokenType.VariableName, true));944945if (!match) {946return this._backTo(token);947}948949const variable = new Variable(name!);950951if (this._accept(TokenType.Colon)) {952// ${foo:<children>}953while (true) {954955// ...} -> done956if (this._accept(TokenType.CurlyClose)) {957parent.appendChild(variable);958return true;959}960961if (this._parse(variable)) {962continue;963}964965// fallback966parent.appendChild(new Text('${' + name! + ':'));967variable.children.forEach(parent.appendChild, parent);968return true;969}970971} else if (this._accept(TokenType.Forwardslash)) {972// ${foo/<regex>/<format>/<options>}973if (this._parseTransform(variable)) {974parent.appendChild(variable);975return true;976}977978this._backTo(token);979return false;980981} else if (this._accept(TokenType.CurlyClose)) {982// ${foo}983parent.appendChild(variable);984return true;985986} else {987// ${foo <- missing curly or colon988return this._backTo(token);989}990}991992private _parseTransform(parent: TransformableMarker): boolean {993// ...<regex>/<format>/<options>}994995const transform = new Transform();996let regexValue = '';997let regexOptions = '';998999// (1) /regex1000while (true) {1001if (this._accept(TokenType.Forwardslash)) {1002break;1003}10041005let escaped: string;1006if (escaped = this._accept(TokenType.Backslash, true)) {1007escaped = this._accept(TokenType.Forwardslash, true) || escaped;1008regexValue += escaped;1009continue;1010}10111012if (this._token.type !== TokenType.EOF) {1013regexValue += this._accept(undefined, true);1014continue;1015}1016return false;1017}10181019// (2) /format1020while (true) {1021if (this._accept(TokenType.Forwardslash)) {1022break;1023}10241025let escaped: string;1026if (escaped = this._accept(TokenType.Backslash, true)) {1027escaped = this._accept(TokenType.Backslash, true) || this._accept(TokenType.Forwardslash, true) || escaped;1028transform.appendChild(new Text(escaped));1029continue;1030}10311032if (this._parseFormatString(transform) || this._parseAnything(transform)) {1033continue;1034}1035return false;1036}10371038// (3) /option1039while (true) {1040if (this._accept(TokenType.CurlyClose)) {1041break;1042}1043if (this._token.type !== TokenType.EOF) {1044regexOptions += this._accept(undefined, true);1045continue;1046}1047return false;1048}10491050try {1051transform.regexp = new RegExp(regexValue, regexOptions);1052} catch (e) {1053// invalid regexp1054return false;1055}10561057parent.transform = transform;1058return true;1059}10601061private _parseFormatString(parent: Transform): boolean {10621063const token = this._token;1064if (!this._accept(TokenType.Dollar)) {1065return false;1066}10671068let complex = false;1069if (this._accept(TokenType.CurlyOpen)) {1070complex = true;1071}10721073const index = this._accept(TokenType.Int, true);10741075if (!index) {1076this._backTo(token);1077return false;10781079} else if (!complex) {1080// $11081parent.appendChild(new FormatString(Number(index)));1082return true;10831084} else if (this._accept(TokenType.CurlyClose)) {1085// ${1}1086parent.appendChild(new FormatString(Number(index)));1087return true;10881089} else if (!this._accept(TokenType.Colon)) {1090this._backTo(token);1091return false;1092}10931094if (this._accept(TokenType.Forwardslash)) {1095// ${1:/upcase}1096const shorthand = this._accept(TokenType.VariableName, true);1097if (!shorthand || !this._accept(TokenType.CurlyClose)) {1098this._backTo(token);1099return false;1100} else {1101parent.appendChild(new FormatString(Number(index), shorthand));1102return true;1103}11041105} else if (this._accept(TokenType.Plus)) {1106// ${1:+<if>}1107const ifValue = this._until(TokenType.CurlyClose);1108if (ifValue) {1109parent.appendChild(new FormatString(Number(index), undefined, ifValue, undefined));1110return true;1111}11121113} else if (this._accept(TokenType.Dash)) {1114// ${2:-<else>}1115const elseValue = this._until(TokenType.CurlyClose);1116if (elseValue) {1117parent.appendChild(new FormatString(Number(index), undefined, undefined, elseValue));1118return true;1119}11201121} else if (this._accept(TokenType.QuestionMark)) {1122// ${2:?<if>:<else>}1123const ifValue = this._until(TokenType.Colon);1124if (ifValue) {1125const elseValue = this._until(TokenType.CurlyClose);1126if (elseValue) {1127parent.appendChild(new FormatString(Number(index), undefined, ifValue, elseValue));1128return true;1129}1130}11311132} else {1133// ${1:<else>}1134const elseValue = this._until(TokenType.CurlyClose);1135if (elseValue) {1136parent.appendChild(new FormatString(Number(index), undefined, undefined, elseValue));1137return true;1138}1139}11401141this._backTo(token);1142return false;1143}11441145private _parseAnything(marker: Marker): boolean {1146if (this._token.type !== TokenType.EOF) {1147marker.appendChild(new Text(this._scanner.tokenText(this._token)));1148this._accept(undefined);1149return true;1150}1151return false;1152}1153}115411551156