diff --git a/.vscode/settings.json b/.vscode/settings.json index 35009f1..2699a19 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -15,4 +15,4 @@ "typescript.autoClosingTags": true, "typescript.format.placeOpenBraceOnNewLineForControlBlocks": false, "window.title": "BRS Formatter - ${activeEditorShort}${separator}${appName}" -} \ No newline at end of file +} diff --git a/dist/BrightScriptFormatter.js b/dist/BrightScriptFormatter.js index 1b1caab..bac4400 100644 --- a/dist/BrightScriptFormatter.js +++ b/dist/BrightScriptFormatter.js @@ -1,11 +1,10 @@ "use strict"; -Object.defineProperty(exports, "__esModule", { - value: true -}); +Object.defineProperty(exports, "__esModule", { value: true }); var brightscript_parser_1 = require("brightscript-parser"); var trimRight = require("trim-right"); var BrightScriptFormatter = /** @class */ (function () { - function BrightScriptFormatter() {} + function BrightScriptFormatter() { + } /** * Format the given input. * @param inputText the text to format @@ -68,7 +67,8 @@ var BrightScriptFormatter = /** @class */ (function () { var tokenValue = token.value; if (options.compositeKeywords === 'combine') { token.value = parts[0] + parts[1]; - } else { + } + else { token.value = parts[0] + ' ' + parts[1]; } var offsetDifference = token.value.length - tokenValue.length; @@ -82,7 +82,11 @@ var BrightScriptFormatter = /** @class */ (function () { //split the parts of the token, but retain their case if (lowerValue.indexOf('end') === 0) { return [token.value.substring(0, 3), token.value.substring(3).trim()]; - } else { + } + else if (lowerValue.indexOf('#else') === 0) { + return [token.value.substring(0, 5), token.value.substring(5).trim()]; + } + else { return [token.value.substring(0, 4), token.value.substring(4).trim()]; } }; @@ -102,25 +106,27 @@ var BrightScriptFormatter = /** @class */ (function () { var lowerValue = token.value.toLowerCase(); if (brightscript_parser_1.CompositeKeywordTokenTypes.indexOf(token.tokenType) === -1) { token.value = token.value.substring(0, 1).toUpperCase() + token.value.substring(1).toLowerCase(); - } else { + } + else { var spaceCharCount = (lowerValue.match(/\s+/) || []).length; var firstWordLength = 0; if (lowerValue.indexOf('end') === 0) { firstWordLength = 3; - } else { + } + else { firstWordLength = 4; } token.value = //first character token.value.substring(0, 1).toUpperCase() + - //rest of first word - token.value.substring(1, firstWordLength).toLowerCase() + - //add back the whitespace - token.value.substring(firstWordLength, firstWordLength + spaceCharCount) + - //first character of second word - token.value.substring(firstWordLength + spaceCharCount, firstWordLength + spaceCharCount + 1).toUpperCase() + - //rest of second word - token.value.substring(firstWordLength + spaceCharCount + 1).toLowerCase(); + //rest of first word + token.value.substring(1, firstWordLength).toLowerCase() + + //add back the whitespace + token.value.substring(firstWordLength, firstWordLength + spaceCharCount) + + //first character of second word + token.value.substring(firstWordLength + spaceCharCount, firstWordLength + spaceCharCount + 1).toUpperCase() + + //rest of second word + token.value.substring(firstWordLength + spaceCharCount + 1).toLowerCase(); } } } @@ -135,7 +141,8 @@ var BrightScriptFormatter = /** @class */ (function () { brightscript_parser_1.TokenType.if, brightscript_parser_1.TokenType.openCurlyBraceSymbol, brightscript_parser_1.TokenType.openSquareBraceSymbol, - brightscript_parser_1.TokenType.while + brightscript_parser_1.TokenType.while, + brightscript_parser_1.TokenType.condIf ]; var outdentTokens = [ brightscript_parser_1.TokenType.closeCurlyBraceSymbol, @@ -146,10 +153,13 @@ var BrightScriptFormatter = /** @class */ (function () { brightscript_parser_1.TokenType.endWhile, brightscript_parser_1.TokenType.endFor, brightscript_parser_1.TokenType.next, + brightscript_parser_1.TokenType.condEndIf ]; var interumTokens = [ brightscript_parser_1.TokenType.else, - brightscript_parser_1.TokenType.elseIf + brightscript_parser_1.TokenType.elseIf, + brightscript_parser_1.TokenType.condElse, + brightscript_parser_1.TokenType.condElseIf ]; var tabCount = 0; var nextLineStartTokenIndex = 0; @@ -170,22 +180,25 @@ var BrightScriptFormatter = /** @class */ (function () { // } else { // //do nothing with single-line if statement indentation // } - } else { + } + else { for (var _i = 0, lineTokens_1 = lineTokens; _i < lineTokens_1.length; _i++) { var token = lineTokens_1[_i]; - //if this is an indentor token, + //if this is an indentor token, if (indentTokens.indexOf(token.tokenType) > -1) { tabCount++; foundIndentorThisLine = true; //this is an outdentor token - } else if (outdentTokens.indexOf(token.tokenType) > -1) { + } + else if (outdentTokens.indexOf(token.tokenType) > -1) { tabCount--; if (foundIndentorThisLine === false) { thisTabCount--; } //this is an interum token - } else if (interumTokens.indexOf(token.tokenType) > -1) { - //these need outdented, but don't change the tabCount + } + else if (interumTokens.indexOf(token.tokenType) > -1) { + //these need outdented, but don't change the tabCount thisTabCount--; } // else if (token.tokenType === TokenType.return && foundIndentorThisLine) { @@ -202,7 +215,8 @@ var BrightScriptFormatter = /** @class */ (function () { var indentSpaceCount = options.indentSpaceCount ? options.indentSpaceCount : BrightScriptFormatter.DEFAULT_INDENT_SPACE_COUNT; var spaceCount = thisTabCount * indentSpaceCount; leadingWhitespace = Array(spaceCount + 1).join(' '); - } else { + } + else { leadingWhitespace = Array(thisTabCount + 1).join('\t'); } //create a whitespace token if there isn't one @@ -215,7 +229,7 @@ var BrightScriptFormatter = /** @class */ (function () { } //replace the whitespace with the formatted whitespace lineTokens[0].value = leadingWhitespace; - //add this list of tokens + //add this list of tokens outputTokens.push.apply(outputTokens, lineTokens); //if we have found the end of file if (lineTokens[lineTokens.length - 1].tokenType === brightscript_parser_1.TokenType.END_OF_FILE) { @@ -247,7 +261,8 @@ var BrightScriptFormatter = /** @class */ (function () { if (whitespaceTokenCandidate.tokenType === brightscript_parser_1.TokenType.whitespace) { lineTokens.splice(potentialWhitespaceTokenIndex, 1); //if the final token is a comment, trim the whitespace from the righthand side - } else if (whitespaceTokenCandidate.tokenType === brightscript_parser_1.TokenType.quoteComment || whitespaceTokenCandidate.tokenType === brightscript_parser_1.TokenType.remComment) { + } + else if (whitespaceTokenCandidate.tokenType === brightscript_parser_1.TokenType.quoteComment || whitespaceTokenCandidate.tokenType === brightscript_parser_1.TokenType.remComment) { whitespaceTokenCandidate.value = trimRight(whitespaceTokenCandidate.value); } //add this line to the output @@ -302,9 +317,6 @@ var BrightScriptFormatter = /** @class */ (function () { fullOptions[attrname] = options[attrname]; } } - if (!fullOptions.indentStyle) { - fullOptions.indentStyle = 'spaces'; - } return fullOptions; }; BrightScriptFormatter.prototype.isSingleLineIfStatement = function (lineTokens, allTokens) { @@ -323,7 +335,8 @@ var BrightScriptFormatter = /** @class */ (function () { var token = lineTokens[i]; if (token.tokenType === brightscript_parser_1.TokenType.whitespace || token.tokenType === brightscript_parser_1.TokenType.newline) { //do nothing with whitespace and newlines - } else { + } + else { //we encountered a non whitespace and non newline token, so this line must be a single-line if statement return true; } @@ -336,4 +349,4 @@ var BrightScriptFormatter = /** @class */ (function () { BrightScriptFormatter.DEFAULT_INDENT_SPACE_COUNT = 4; return BrightScriptFormatter; }()); -exports.BrightScriptFormatter = BrightScriptFormatter; \ No newline at end of file +exports.BrightScriptFormatter = BrightScriptFormatter; diff --git a/package-lock.json b/package-lock.json index 78a9e3d..b22ccaa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,12 +14,12 @@ } }, "@babel/generator": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.1.2.tgz", - "integrity": "sha512-70A9HWLS/1RHk3Ck8tNHKxOoKQuSKocYgwDN85Pyl/RBduss6AKxUR7RIZ/lzduQMSYfWEM4DDBu6A+XGbkFig==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.1.3.tgz", + "integrity": "sha512-ZoCZGcfIJFJuZBqxcY9OjC1KW2lWK64qrX1o4UYL3yshVhwKFYgzpWZ0vvtGMNJdTlvkw0W+HR1VnYN8q3QPFQ==", "dev": true, "requires": { - "@babel/types": "^7.1.2", + "@babel/types": "^7.1.3", "jsesc": "^2.5.1", "lodash": "^4.17.10", "source-map": "^0.5.0", @@ -83,9 +83,9 @@ } }, "@babel/parser": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.1.2.tgz", - "integrity": "sha512-x5HFsW+E/nQalGMw7hu+fvPqnBeBaIr0lWJ2SG0PPL2j+Pm9lYvCrsZJGIgauPIENx0v10INIyFjmSNUD/gSqQ==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.1.3.tgz", + "integrity": "sha512-gqmspPZOMW3MIRb9HlrnbZHXI1/KHTOroBwN1NcLL6pWxzqzEKGvRTq0W/PxS45OtQGbaFikSQpkS5zbnsQm2w==", "dev": true }, "@babel/template": { @@ -100,26 +100,26 @@ } }, "@babel/traverse": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.1.0.tgz", - "integrity": "sha512-bwgln0FsMoxm3pLOgrrnGaXk18sSM9JNf1/nHC/FksmNGFbYnPWY4GYCfLxyP1KRmfsxqkRpfoa6xr6VuuSxdw==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.1.4.tgz", + "integrity": "sha512-my9mdrAIGdDiSVBuMjpn/oXYpva0/EZwWL3sm3Wcy/AVWO2eXnsoZruOT9jOGNRXU8KbCIu5zsKnXcAJ6PcV6Q==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", - "@babel/generator": "^7.0.0", + "@babel/generator": "^7.1.3", "@babel/helper-function-name": "^7.1.0", "@babel/helper-split-export-declaration": "^7.0.0", - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0", + "@babel/parser": "^7.1.3", + "@babel/types": "^7.1.3", "debug": "^3.1.0", "globals": "^11.1.0", "lodash": "^4.17.10" } }, "@babel/types": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.1.2.tgz", - "integrity": "sha512-pb1I05sZEKiSlMUV9UReaqsCPUpgbHHHu2n1piRm7JkuBkm6QxcaIzKu6FMnMtCbih/cEYTR+RGYYC96Yk9HAg==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.1.3.tgz", + "integrity": "sha512-RpPOVfK+yatXyn8n4PB1NW6k9qjinrXrRR8ugBN8fD6hCy5RXI6PSbVqpOJBO9oSaY7Nom4ohj35feb0UR9hSA==", "dev": true, "requires": { "esutils": "^2.0.2", @@ -291,9 +291,9 @@ } }, "brightscript-parser": { - "version": "1.0.3-1", - "resolved": "https://registry.npmjs.org/brightscript-parser/-/brightscript-parser-1.0.3-1.tgz", - "integrity": "sha512-nifHLXiwEEEUwxipovHFycozRTVVWV0NaHkdDf3FkbZBVH4aMwgjy4C270uMSVJS2kk5xF+m2XiGIUXxn2tCuQ==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brightscript-parser/-/brightscript-parser-1.1.0.tgz", + "integrity": "sha512-6f0KvZvHAboMKJOQZ6tZ5DYpTEPM8FkAD8xJ0ATFqDkCX1OTYsH5iLU0Z9cPRbNLVsLK/XEO41HZEJv3iFmFJg==" }, "browser-stdout": { "version": "1.3.1", diff --git a/package.json b/package.json index c795336..37f2236 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ }, "homepage": "https://github.com/TwitchBronBron/brightscript-formatter#readme", "dependencies": { - "brightscript-parser": "^1.0.3-1", + "brightscript-parser": "1.1.0", "trim-right": "^1.0.1" }, "devDependencies": { diff --git a/src/BrightScriptFormatter.spec.ts b/src/BrightScriptFormatter.spec.ts index 142bffd..f2146ca 100644 --- a/src/BrightScriptFormatter.spec.ts +++ b/src/BrightScriptFormatter.spec.ts @@ -27,6 +27,10 @@ describe('BrightScriptFormatter', () => { parts = (formatter as any).getCompositeKeywordParts({ value: 'else if' }); expect(parts[0]).to.equal('else'); expect(parts[1]).to.equal('if'); + + parts = (formatter as any).getCompositeKeywordParts({ value: '#else if' }); + expect(parts[0]).to.equal('#else'); + expect(parts[1]).to.equal('if'); }); }); @@ -315,4 +319,32 @@ describe('BrightScriptFormatter', () => { expect(formatter.format(program)).to.equal(`function DoSomething()\n data=[\n 1,\n 2\n ]\nend function`); }); }); + + describe('indentStyle for conditional block', () => { + it('correctly fixes the indentation', () => { + let expected = `#if isDebug\n doSomething()\n#end if`; + let current = `#if isDebug\n doSomething()\n#end if`; + expect(formatter.format(current)).to.equal(expected); + }); + + it('skips indentation when indentStyle:undefined for conditional block', () => { + let program = `#if isDebug\n doSomething()\n#else\n doSomethingElse\n #end if`; + expect(formatter.format(program, { indentStyle: undefined })).to.equal(program); + }); + + it('correctly fixes the indentation2', () => { + let program = `#if isDebug\n doSomething()\n#else if isPartialDebug\n doSomethingElse()\n#else\n doFinalThing()\n#end if`; + expect(formatter.format(program)).to.equal(program); + }); + + it('correctly fixes the indentation nested if in conditional block', () => { + let program = `#if isDebug\n if true then\n doSomething()\n end if\n#end if`; + expect(formatter.format(program)).to.equal(program); + }); + + it('correctly fixes the indentation nested #if in if block', () => { + let program = `if true then\n #if isDebug\n doSomething()\n #end if\nend if`; + expect(formatter.format(program)).to.equal(program); + }); + }); }); \ No newline at end of file diff --git a/src/BrightScriptFormatter.ts b/src/BrightScriptFormatter.ts index 899cbbb..4b306ae 100644 --- a/src/BrightScriptFormatter.ts +++ b/src/BrightScriptFormatter.ts @@ -1,395 +1,444 @@ -import { BrightScriptLexer, CompositeKeywordTokenTypes, KeywordTokenTypes, Token, TokenType } from 'brightscript-parser'; +import { + BrightScriptLexer, + CompositeKeywordTokenTypes, + KeywordTokenTypes, + Token, + TokenType +} from 'brightscript-parser'; import * as trimRight from 'trim-right'; export class BrightScriptFormatter { - constructor() { - + constructor() {} + /** + * The default number of spaces when indenting with spaces + */ + private static DEFAULT_INDENT_SPACE_COUNT = 4; + /** + * Format the given input. + * @param inputText the text to format + * @param formattingOptions options specifying formatting preferences + */ + public format(inputText: string, formattingOptions?: FormattingOptions) { + let options = this.normalizeOptions(formattingOptions); + let lexer = new BrightScriptLexer(); + let tokens = lexer.tokenize(inputText); + + //force all composite keywords to have 0 or 1 spaces in between, but no more than 1 + tokens = this.normalizeCompositeKeywords(tokens); + + if (options.compositeKeywords) { + tokens = this.formatCompositeKeywords(tokens, options); } - /** - * The default number of spaces when indenting with spaces - */ - private static DEFAULT_INDENT_SPACE_COUNT = 4; - /** - * Format the given input. - * @param inputText the text to format - * @param formattingOptions options specifying formatting preferences - */ - public format(inputText: string, formattingOptions?: FormattingOptions) { - let options = this.normalizeOptions(formattingOptions); - let lexer = new BrightScriptLexer(); - let tokens = lexer.tokenize(inputText); - - //force all composite keywords to have 0 or 1 spaces in between, but no more than 1 - tokens = this.normalizeCompositeKeywords(tokens); - - if (options.compositeKeywords) { - tokens = this.formatCompositeKeywords(tokens, options); - } - - if (options.indentStyle) { - tokens = this.formatIndentation(tokens, options); - } - - if (options.keywordCase) { - tokens = this.formatKeywordCasing(tokens, options); - } - if (options.removeTrailingWhiteSpace) { - tokens = this.formatTrailingWhiteSpace(tokens, options); - } - //join all tokens back together into a single string - let outputText = ''; - for (let token of tokens) { - outputText += token.value; - } - return outputText; + if (options.indentStyle) { + tokens = this.formatIndentation(tokens, options); } - /** - * Remove all whitespace in the composite keyword tokens with a single space - * @param tokens - */ - private normalizeCompositeKeywords(tokens: Token[]) { - let indexOffset = 0; - for (let token of tokens) { - token.startIndex += indexOffset; - //is this a composite token - if (CompositeKeywordTokenTypes.indexOf(token.tokenType) > -1) { - let value = token.value; - //remove all whitespace with a single space - token.value.replace(/s+/g, ' '); - let indexDifference = value.length - token.value.length; - indexOffset -= indexDifference; - } - } - return tokens; + if (options.keywordCase) { + tokens = this.formatKeywordCasing(tokens, options); } - - private formatCompositeKeywords(tokens: Token[], options: FormattingOptions) { - let indexOffset = 0; - for (let token of tokens) { - token.startIndex += indexOffset; - //is this a composite token - if (CompositeKeywordTokenTypes.indexOf(token.tokenType) > -1) { - let parts = this.getCompositeKeywordParts(token); - let tokenValue = token.value; - if (options.compositeKeywords === 'combine') { - token.value = parts[0] + parts[1]; - } else {// if(options.compositeKeywords === 'split'){ - token.value = parts[0] + ' ' + parts[1]; - } - let offsetDifference = token.value.length - tokenValue.length; - indexOffset += offsetDifference; - } - } - return tokens; + if (options.removeTrailingWhiteSpace) { + tokens = this.formatTrailingWhiteSpace(tokens, options); } - private getCompositeKeywordParts(token: Token) { - let lowerValue = token.value.toLowerCase(); - //split the parts of the token, but retain their case - if (lowerValue.indexOf('end') === 0) { - return [token.value.substring(0, 3), token.value.substring(3).trim()]; - } else { // if (lowerValue.indexOf('exit') === 0 || lowerValue.indexOf('else') === 0) { - return [token.value.substring(0, 4), token.value.substring(4).trim()]; - } + //join all tokens back together into a single string + let outputText = ''; + for (let token of tokens) { + outputText += token.value; } - - private formatKeywordCasing(tokens: Token[], options: FormattingOptions) { - for (let token of tokens) { - //if this token is a keyword - if (KeywordTokenTypes.indexOf(token.tokenType) > -1) { - switch (options.keywordCase) { - case 'lower': - token.value = token.value.toLowerCase(); - break; - case 'upper': - token.value = token.value.toUpperCase(); - break; - case 'title': - let lowerValue = token.value.toLowerCase(); - if (CompositeKeywordTokenTypes.indexOf(token.tokenType) === -1) { - token.value = token.value.substring(0, 1).toUpperCase() + token.value.substring(1).toLowerCase(); - } else { - let spaceCharCount = (lowerValue.match(/\s+/) || []).length; - let firstWordLength: number = 0; - if (lowerValue.indexOf('end') === 0) { - firstWordLength = 3; - } else { //if (lowerValue.indexOf('exit') > -1 || lowerValue.indexOf('else') > -1) - firstWordLength = 4; - } - token.value = - //first character - token.value.substring(0, 1).toUpperCase() + - //rest of first word - token.value.substring(1, firstWordLength).toLowerCase() + - //add back the whitespace - token.value.substring(firstWordLength, firstWordLength + spaceCharCount) + - //first character of second word - token.value.substring(firstWordLength + spaceCharCount, firstWordLength + spaceCharCount + 1).toUpperCase() + - //rest of second word - token.value.substring(firstWordLength + spaceCharCount + 1).toLowerCase(); - } - } - } + return outputText; + } + + /** + * Remove all whitespace in the composite keyword tokens with a single space + * @param tokens + */ + private normalizeCompositeKeywords(tokens: Token[]) { + let indexOffset = 0; + for (let token of tokens) { + token.startIndex += indexOffset; + //is this a composite token + if (CompositeKeywordTokenTypes.indexOf(token.tokenType) > -1) { + let value = token.value; + //remove all whitespace with a single space + token.value.replace(/s+/g, ' '); + let indexDifference = value.length - token.value.length; + indexOffset -= indexDifference; + } + } + return tokens; + } + + private formatCompositeKeywords(tokens: Token[], options: FormattingOptions) { + let indexOffset = 0; + for (let token of tokens) { + token.startIndex += indexOffset; + //is this a composite token + if (CompositeKeywordTokenTypes.indexOf(token.tokenType) > -1) { + let parts = this.getCompositeKeywordParts(token); + let tokenValue = token.value; + if (options.compositeKeywords === 'combine') { + token.value = parts[0] + parts[1]; + } else { + // if(options.compositeKeywords === 'split'){ + token.value = parts[0] + ' ' + parts[1]; } - return tokens; + let offsetDifference = token.value.length - tokenValue.length; + indexOffset += offsetDifference; + } } - - private formatIndentation(tokens: Token[], options: FormattingOptions) { - let indentTokens = [ - TokenType.sub, - TokenType.for, - TokenType.function, - TokenType.if, - TokenType.openCurlyBraceSymbol, - TokenType.openSquareBraceSymbol, - TokenType.while - ]; - let outdentTokens = [ - TokenType.closeCurlyBraceSymbol, - TokenType.closeSquareBraceSymbol, - TokenType.endFunction, - TokenType.endIf, - TokenType.endSub, - TokenType.endWhile, - TokenType.endFor, - TokenType.next, - ]; - let interumTokens = [ - TokenType.else, - TokenType.elseIf - ]; - let tabCount = 0; - - let nextLineStartTokenIndex = 0; - //the list of output tokens - let outputTokens: Token[] = []; - //set the loop to run for a max of double the number of tokens we found so we don't end up with an infinite loop - outer: for (let outerLoopCounter = 0; outerLoopCounter <= tokens.length * 2; outerLoopCounter++) { - let lineObj = this.getLineTokens(nextLineStartTokenIndex, tokens); - - nextLineStartTokenIndex = lineObj.stopIndex + 1; - let lineTokens = lineObj.tokens; - let thisTabCount = tabCount; - let foundIndentorThisLine = false; - - //if this is a single-line if statement, do nothing with indentation - if (this.isSingleLineIfStatement(lineTokens, tokens)) { - // //if this line has a return statement, outdent - // if (this.tokenIndexOf(TokenType.return, lineTokens) > -1) { - // tabCount--; - // } else { - // //do nothing with single-line if statement indentation - // } - - } else { - for (let token of lineTokens) { - //if this is an indentor token, - if (indentTokens.indexOf(token.tokenType) > -1) { - tabCount++; - foundIndentorThisLine = true; - - //this is an outdentor token - } else if (outdentTokens.indexOf(token.tokenType) > -1) { - tabCount--; - if (foundIndentorThisLine === false) { - thisTabCount--; - } - - //this is an interum token - } else if (interumTokens.indexOf(token.tokenType) > -1) { - //these need outdented, but don't change the tabCount - thisTabCount--; - } - // else if (token.tokenType === TokenType.return && foundIndentorThisLine) { - // //a return statement on the same line as an indentor means we don't want to indent - // tabCount--; - // } - - } - } - //if the tab counts are less than zero, something is wrong. However, at least try to do formatting as best we can by resetting to 0 - thisTabCount = thisTabCount < 0 ? 0 : thisTabCount; - tabCount = tabCount < 0 ? 0 : tabCount; - - let leadingWhitespace: string; - - if (options.indentStyle === 'spaces') { - let indentSpaceCount = options.indentSpaceCount ? options.indentSpaceCount : BrightScriptFormatter.DEFAULT_INDENT_SPACE_COUNT; - let spaceCount = thisTabCount * indentSpaceCount; - leadingWhitespace = Array(spaceCount + 1).join(' '); + return tokens; + } + + private getCompositeKeywordParts(token: Token) { + let lowerValue = token.value.toLowerCase(); + //split the parts of the token, but retain their case + if (lowerValue.indexOf('end') === 0) { + return [token.value.substring(0, 3), token.value.substring(3).trim()]; + } else if (lowerValue.indexOf('#else') === 0) { + return [token.value.substring(0, 5), token.value.substring(5).trim()]; + } else { + // if (lowerValue.indexOf('exit') === 0 || lowerValue.indexOf('else') === 0) { + return [token.value.substring(0, 4), token.value.substring(4).trim()]; + } + } + + private formatKeywordCasing(tokens: Token[], options: FormattingOptions) { + for (let token of tokens) { + //if this token is a keyword + if (KeywordTokenTypes.indexOf(token.tokenType) > -1) { + switch (options.keywordCase) { + case 'lower': + token.value = token.value.toLowerCase(); + break; + case 'upper': + token.value = token.value.toUpperCase(); + break; + case 'title': + let lowerValue = token.value.toLowerCase(); + if (CompositeKeywordTokenTypes.indexOf(token.tokenType) === -1) { + token.value = + token.value.substring(0, 1).toUpperCase() + + token.value.substring(1).toLowerCase(); } else { - leadingWhitespace = Array(thisTabCount + 1).join('\t'); - } - //create a whitespace token if there isn't one - if (lineTokens[0] && lineTokens[0].tokenType !== TokenType.whitespace) { - lineTokens.unshift({ - startIndex: -1, - tokenType: TokenType.whitespace, - value: '' - }); - } - - //replace the whitespace with the formatted whitespace - lineTokens[0].value = leadingWhitespace; - - //add this list of tokens - outputTokens.push.apply(outputTokens, lineTokens); - //if we have found the end of file - if (lineTokens[lineTokens.length - 1].tokenType === TokenType.END_OF_FILE) { - break outer; - } - /* istanbul ignore next */ - if (outerLoopCounter === tokens.length * 2) { - throw new Error('Something went terribly wrong'); + let spaceCharCount = (lowerValue.match(/\s+/) || []).length; + let firstWordLength: number = 0; + if (lowerValue.indexOf('end') === 0) { + firstWordLength = 3; + } else { + //if (lowerValue.indexOf('exit') > -1 || lowerValue.indexOf('else') > -1) + firstWordLength = 4; + } + token.value = + //first character + token.value.substring(0, 1).toUpperCase() + + //rest of first word + token.value.substring(1, firstWordLength).toLowerCase() + + //add back the whitespace + token.value.substring( + firstWordLength, + firstWordLength + spaceCharCount + ) + + //first character of second word + token.value + .substring( + firstWordLength + spaceCharCount, + firstWordLength + spaceCharCount + 1 + ) + .toUpperCase() + + //rest of second word + token.value + .substring(firstWordLength + spaceCharCount + 1) + .toLowerCase(); } } - return outputTokens; + } } - - /** - * Remove all trailing whitespace - */ - private formatTrailingWhiteSpace(tokens: Token[], options: FormattingOptions) { - let nextLineStartTokenIndex = 0; - //the list of output tokens - let outputTokens: Token[] = []; - - //set the loop to run for a max of double the number of tokens we found so we don't end up with an infinite loop - for (let outerLoopCounter = 0; outerLoopCounter <= tokens.length * 2; outerLoopCounter++) { - let lineObj = this.getLineTokens(nextLineStartTokenIndex, tokens); - - nextLineStartTokenIndex = lineObj.stopIndex + 1; - let lineTokens = lineObj.tokens; - //the last token is newline or EOF, so the next-to-last token is where the trailing whitespace would reside - let potentialWhitespaceTokenIndex = lineTokens.length - 2; - - let whitespaceTokenCandidate = lineTokens[potentialWhitespaceTokenIndex]; - //if the final token is whitespace, throw it away - if (whitespaceTokenCandidate.tokenType === TokenType.whitespace) { - lineTokens.splice(potentialWhitespaceTokenIndex, 1); - - //if the final token is a comment, trim the whitespace from the righthand side - } else if (whitespaceTokenCandidate.tokenType === TokenType.quoteComment || whitespaceTokenCandidate.tokenType === TokenType.remComment) { - whitespaceTokenCandidate.value = trimRight(whitespaceTokenCandidate.value); + return tokens; + } + + private formatIndentation(tokens: Token[], options: FormattingOptions) { + let indentTokens = [ + TokenType.sub, + TokenType.for, + TokenType.function, + TokenType.if, + TokenType.openCurlyBraceSymbol, + TokenType.openSquareBraceSymbol, + TokenType.while, + TokenType.condIf + ]; + let outdentTokens = [ + TokenType.closeCurlyBraceSymbol, + TokenType.closeSquareBraceSymbol, + TokenType.endFunction, + TokenType.endIf, + TokenType.endSub, + TokenType.endWhile, + TokenType.endFor, + TokenType.next, + TokenType.condEndIf + ]; + let interumTokens = [ + TokenType.else, + TokenType.elseIf, + TokenType.condElse, + TokenType.condElseIf + ]; + let tabCount = 0; + + let nextLineStartTokenIndex = 0; + //the list of output tokens + let outputTokens: Token[] = []; + //set the loop to run for a max of double the number of tokens we found so we don't end up with an infinite loop + outer: for ( + let outerLoopCounter = 0; + outerLoopCounter <= tokens.length * 2; + outerLoopCounter++ + ) { + let lineObj = this.getLineTokens(nextLineStartTokenIndex, tokens); + + nextLineStartTokenIndex = lineObj.stopIndex + 1; + let lineTokens = lineObj.tokens; + let thisTabCount = tabCount; + let foundIndentorThisLine = false; + + //if this is a single-line if statement, do nothing with indentation + if (this.isSingleLineIfStatement(lineTokens, tokens)) { + // //if this line has a return statement, outdent + // if (this.tokenIndexOf(TokenType.return, lineTokens) > -1) { + // tabCount--; + // } else { + // //do nothing with single-line if statement indentation + // } + } else { + for (let token of lineTokens) { + //if this is an indentor token, + if (indentTokens.indexOf(token.tokenType) > -1) { + tabCount++; + foundIndentorThisLine = true; + + //this is an outdentor token + } else if (outdentTokens.indexOf(token.tokenType) > -1) { + tabCount--; + if (foundIndentorThisLine === false) { + thisTabCount--; } - //add this line to the output - outputTokens.push.apply(outputTokens, lineTokens); - - //if we have found the end of file, quit the loop - if (lineTokens[lineTokens.length - 1].tokenType === TokenType.END_OF_FILE) { - break; - } + //this is an interum token + } else if (interumTokens.indexOf(token.tokenType) > -1) { + //these need outdented, but don't change the tabCount + thisTabCount--; + } + // else if (token.tokenType === TokenType.return && foundIndentorThisLine) { + // //a return statement on the same line as an indentor means we don't want to indent + // tabCount--; + // } } - return outputTokens; + } + //if the tab counts are less than zero, something is wrong. However, at least try to do formatting as best we can by resetting to 0 + thisTabCount = thisTabCount < 0 ? 0 : thisTabCount; + tabCount = tabCount < 0 ? 0 : tabCount; + + let leadingWhitespace: string; + + if (options.indentStyle === 'spaces') { + let indentSpaceCount = options.indentSpaceCount + ? options.indentSpaceCount + : BrightScriptFormatter.DEFAULT_INDENT_SPACE_COUNT; + let spaceCount = thisTabCount * indentSpaceCount; + leadingWhitespace = Array(spaceCount + 1).join(' '); + } else { + leadingWhitespace = Array(thisTabCount + 1).join('\t'); + } + //create a whitespace token if there isn't one + if (lineTokens[0] && lineTokens[0].tokenType !== TokenType.whitespace) { + lineTokens.unshift({ + startIndex: -1, + tokenType: TokenType.whitespace, + value: '' + }); + } + + //replace the whitespace with the formatted whitespace + lineTokens[0].value = leadingWhitespace; + + //add this list of tokens + outputTokens.push.apply(outputTokens, lineTokens); + //if we have found the end of file + if ( + lineTokens[lineTokens.length - 1].tokenType === TokenType.END_OF_FILE + ) { + break outer; + } + /* istanbul ignore next */ + if (outerLoopCounter === tokens.length * 2) { + throw new Error('Something went terribly wrong'); + } } - - private tokenIndexOf(tokenType: TokenType, tokens: Token[]) { - for (let i = 0; i < tokens.length; i++) { - let token = tokens[i]; - if (token.tokenType === tokenType) { - return i; - } - } - return -1; + return outputTokens; + } + + /** + * Remove all trailing whitespace + */ + private formatTrailingWhiteSpace( + tokens: Token[], + options: FormattingOptions + ) { + let nextLineStartTokenIndex = 0; + //the list of output tokens + let outputTokens: Token[] = []; + + //set the loop to run for a max of double the number of tokens we found so we don't end up with an infinite loop + for ( + let outerLoopCounter = 0; + outerLoopCounter <= tokens.length * 2; + outerLoopCounter++ + ) { + let lineObj = this.getLineTokens(nextLineStartTokenIndex, tokens); + + nextLineStartTokenIndex = lineObj.stopIndex + 1; + let lineTokens = lineObj.tokens; + //the last token is newline or EOF, so the next-to-last token is where the trailing whitespace would reside + let potentialWhitespaceTokenIndex = lineTokens.length - 2; + + let whitespaceTokenCandidate = lineTokens[potentialWhitespaceTokenIndex]; + //if the final token is whitespace, throw it away + if (whitespaceTokenCandidate.tokenType === TokenType.whitespace) { + lineTokens.splice(potentialWhitespaceTokenIndex, 1); + + //if the final token is a comment, trim the whitespace from the righthand side + } else if ( + whitespaceTokenCandidate.tokenType === TokenType.quoteComment || + whitespaceTokenCandidate.tokenType === TokenType.remComment + ) { + whitespaceTokenCandidate.value = trimRight( + whitespaceTokenCandidate.value + ); + } + + //add this line to the output + outputTokens.push.apply(outputTokens, lineTokens); + + //if we have found the end of file, quit the loop + if ( + lineTokens[lineTokens.length - 1].tokenType === TokenType.END_OF_FILE + ) { + break; + } } - - /** - * Get the tokens for the whole line starting at the given index (including the newline or EOF token at the end) - * @param startIndex - * @param tokens - */ - private getLineTokens(startIndex: number, tokens: Token[]) { - let outputTokens: Token[] = []; - let index = startIndex; - for (index = startIndex; index < tokens.length; index++) { - let token = tokens[index]; - outputTokens[outputTokens.length] = token; - - if (token.tokenType === TokenType.newline || token.tokenType === TokenType.END_OF_FILE) { - break; - } - } - return { - startIndex, - stopIndex: index, - tokens: outputTokens - }; + return outputTokens; + } + + private tokenIndexOf(tokenType: TokenType, tokens: Token[]) { + for (let i = 0; i < tokens.length; i++) { + let token = tokens[i]; + if (token.tokenType === tokenType) { + return i; + } } - - private normalizeOptions(options: FormattingOptions | undefined) { - let fullOptions: FormattingOptions = { - indentStyle: 'spaces', - indentSpaceCount: BrightScriptFormatter.DEFAULT_INDENT_SPACE_COUNT, - keywordCase: 'lower', - compositeKeywords: 'split', - removeTrailingWhiteSpace: true - }; - if (options) { - for (let attrname in options) { - fullOptions[attrname] = options[attrname]; - } - } - return fullOptions; + return -1; + } + + /** + * Get the tokens for the whole line starting at the given index (including the newline or EOF token at the end) + * @param startIndex + * @param tokens + */ + private getLineTokens(startIndex: number, tokens: Token[]) { + let outputTokens: Token[] = []; + let index = startIndex; + for (index = startIndex; index < tokens.length; index++) { + let token = tokens[index]; + outputTokens[outputTokens.length] = token; + + if ( + token.tokenType === TokenType.newline || + token.tokenType === TokenType.END_OF_FILE + ) { + break; + } } + return { + startIndex, + stopIndex: index, + tokens: outputTokens + }; + } + + private normalizeOptions(options: FormattingOptions | undefined) { + let fullOptions: FormattingOptions = { + indentStyle: 'spaces', + indentSpaceCount: BrightScriptFormatter.DEFAULT_INDENT_SPACE_COUNT, + keywordCase: 'lower', + compositeKeywords: 'split', + removeTrailingWhiteSpace: true + }; + if (options) { + for (let attrname in options) { + fullOptions[attrname] = options[attrname]; + } + } + return fullOptions; + } - private isSingleLineIfStatement(lineTokens: Token[], allTokens: Token[]) { - let ifIndex = this.tokenIndexOf(TokenType.if, lineTokens); - if (ifIndex === -1) { - return false; - } - let thenIndex = this.tokenIndexOf(TokenType.then, lineTokens); - let elseIndex = this.tokenIndexOf(TokenType.else, lineTokens); - //if there's an else on this line, assume this is a one-line if statement - if (elseIndex > -1) { - return true; - } - - //see if there is anything after the "then". If so, assume it's a one-line if statement - for (let i = thenIndex + 1; i < lineTokens.length; i++) { - let token = lineTokens[i]; - if (token.tokenType === TokenType.whitespace || token.tokenType === TokenType.newline) { - //do nothing with whitespace and newlines + private isSingleLineIfStatement(lineTokens: Token[], allTokens: Token[]) { + let ifIndex = this.tokenIndexOf(TokenType.if, lineTokens); + if (ifIndex === -1) { + return false; + } + let thenIndex = this.tokenIndexOf(TokenType.then, lineTokens); + let elseIndex = this.tokenIndexOf(TokenType.else, lineTokens); + //if there's an else on this line, assume this is a one-line if statement + if (elseIndex > -1) { + return true; + } - } else { - //we encountered a non whitespace and non newline token, so this line must be a single-line if statement - return true; - } - } - return false; + //see if there is anything after the "then". If so, assume it's a one-line if statement + for (let i = thenIndex + 1; i < lineTokens.length; i++) { + let token = lineTokens[i]; + if ( + token.tokenType === TokenType.whitespace || + token.tokenType === TokenType.newline + ) { + //do nothing with whitespace and newlines + } else { + //we encountered a non whitespace and non newline token, so this line must be a single-line if statement + return true; + } } + return false; + } } /** * A set of formatting options used to determine how the file should be formatted. */ export interface FormattingOptions { - /** - * The type of indentation to use when indenting the beginning of lines. - */ - indentStyle?: 'tabs' | 'spaces' | 'existing'; - /** - * The number of spaces to use when indentStyle is 'spaces'. Default is 4 - */ - indentSpaceCount?: number; - /** - * Replaces all keywords with the upper or lower case settings specified. - * If set to null, they are not modified at all. - */ - keywordCase?: 'lower' | 'upper' | 'title' | null; - /** - * Forces all composite keywords (i.e. "elseif", "endwhile", etc...) to be consistent. - * If 'split', they are split into their alternatives ("else if", "end while"). - * If 'combine', they are combined ("elseif", "endwhile"). - * If null, they are not modified. - */ - compositeKeywords?: 'split' | 'combine' | null; - /** - * If true (the default), trailing white space is removed - * If false, trailing white space is left intact - */ - removeTrailingWhiteSpace?: boolean; - -} \ No newline at end of file + /** + * The type of indentation to use when indenting the beginning of lines. + */ + indentStyle?: 'tabs' | 'spaces' | 'existing'; + /** + * The number of spaces to use when indentStyle is 'spaces'. Default is 4 + */ + indentSpaceCount?: number; + /** + * Replaces all keywords with the upper or lower case settings specified. + * If set to null, they are not modified at all. + */ + keywordCase?: 'lower' | 'upper' | 'title' | null; + /** + * Forces all composite keywords (i.e. "elseif", "endwhile", etc...) to be consistent. + * If 'split', they are split into their alternatives ("else if", "end while"). + * If 'combine', they are combined ("elseif", "endwhile"). + * If null, they are not modified. + */ + compositeKeywords?: 'split' | 'combine' | null; + /** + * If true (the default), trailing white space is removed + * If false, trailing white space is left intact + */ + removeTrailingWhiteSpace?: boolean; +}