From de4b96d1bd2f7118415e49ea16507294c58b7a0f Mon Sep 17 00:00:00 2001 From: Bronley Plumb Date: Mon, 9 Jul 2018 16:10:32 -0400 Subject: [PATCH] Added option to remove trailing whitespace --- README.md | 2 +- dist/BrightScriptFormatter.d.ts | 13 ++++- dist/BrightScriptFormatter.js | 92 ++++++++++++++++++++----------- package-lock.json | 5 ++ package.json | 3 +- src/BrightScriptFormatter.spec.ts | 29 ++++++++++ src/BrightScriptFormatter.ts | 54 +++++++++++++++++- 7 files changed, 160 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 203c3fc..e8b9aa0 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ var formattedFileContents = formatter.format(unformattedFileContents, formatting ## Formatting Options -Click [here](https://github.com/TwitchBronBron/brightscript-formatter/blob/master/src/BrightScriptFormatter.ts#L265) to view all of the formatting options +Click [here](https://github.com/TwitchBronBron/brightscript-formatter/blob/master/src/BrightScriptFormatter.ts#L368) to view all of the formatting options ## Known issues diff --git a/dist/BrightScriptFormatter.d.ts b/dist/BrightScriptFormatter.d.ts index 80cd35b..39e21ef 100644 --- a/dist/BrightScriptFormatter.d.ts +++ b/dist/BrightScriptFormatter.d.ts @@ -19,9 +19,13 @@ export declare class BrightScriptFormatter { private getCompositeKeywordParts(token); private formatKeywordCasing(tokens, options); private formatIndentation(tokens, options); + /** + * Remove all trailing whitespace + */ + private formatTrailingWhiteSpace(tokens, options); private tokenIndexOf(tokenType, tokens); /** - * Get the tokens for the whole line starting at the given index + * 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 */ @@ -36,7 +40,7 @@ export interface FormattingOptions { /** * The type of indentation to use when indenting the beginning of lines. */ - indentStyle?: 'tabs' | 'spaces' | null; + indentStyle?: 'tabs' | 'spaces' | 'existing'; /** * The number of spaces to use when indentStyle is 'spaces'. Default is 4 */ @@ -53,4 +57,9 @@ export interface FormattingOptions { * 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; } diff --git a/dist/BrightScriptFormatter.js b/dist/BrightScriptFormatter.js index db28126..1b1caab 100644 --- a/dist/BrightScriptFormatter.js +++ b/dist/BrightScriptFormatter.js @@ -1,9 +1,11 @@ "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 @@ -24,6 +26,9 @@ var BrightScriptFormatter = /** @class */ (function () { 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 var outputText = ''; for (var _i = 0, tokens_1 = tokens; _i < tokens_1.length; _i++) { @@ -63,8 +68,7 @@ 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; @@ -78,8 +82,7 @@ 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 { return [token.value.substring(0, 4), token.value.substring(4).trim()]; } }; @@ -99,27 +102,25 @@ 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(); } } } @@ -169,8 +170,7 @@ 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, @@ -178,15 +178,13 @@ var BrightScriptFormatter = /** @class */ (function () { 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) { + } else if (interumTokens.indexOf(token.tokenType) > -1) { //these need outdented, but don't change the tabCount thisTabCount--; } @@ -204,8 +202,7 @@ 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 @@ -231,6 +228,37 @@ var BrightScriptFormatter = /** @class */ (function () { } return outputTokens; }; + /** + * Remove all trailing whitespace + */ + BrightScriptFormatter.prototype.formatTrailingWhiteSpace = function (tokens, options) { + var nextLineStartTokenIndex = 0; + //the list of output tokens + var outputTokens = []; + //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 (var outerLoopCounter = 0; outerLoopCounter <= tokens.length * 2; outerLoopCounter++) { + var lineObj = this.getLineTokens(nextLineStartTokenIndex, tokens); + nextLineStartTokenIndex = lineObj.stopIndex + 1; + var lineTokens = lineObj.tokens; + //the last token is newline or EOF, so the next-to-last token is where the trailing whitespace would reside + var potentialWhitespaceTokenIndex = lineTokens.length - 2; + var whitespaceTokenCandidate = lineTokens[potentialWhitespaceTokenIndex]; + //if the final token is whitespace, throw it away + 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) { + 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 === brightscript_parser_1.TokenType.END_OF_FILE) { + break; + } + } + return outputTokens; + }; BrightScriptFormatter.prototype.tokenIndexOf = function (tokenType, tokens) { for (var i = 0; i < tokens.length; i++) { var token = tokens[i]; @@ -241,7 +269,7 @@ var BrightScriptFormatter = /** @class */ (function () { return -1; }; /** - * Get the tokens for the whole line starting at the given index + * 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 */ @@ -266,7 +294,8 @@ var BrightScriptFormatter = /** @class */ (function () { indentStyle: 'spaces', indentSpaceCount: BrightScriptFormatter.DEFAULT_INDENT_SPACE_COUNT, keywordCase: 'lower', - compositeKeywords: 'split' + compositeKeywords: 'split', + removeTrailingWhiteSpace: true }; if (options) { for (var attrname in options) { @@ -294,8 +323,7 @@ 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; } @@ -308,4 +336,4 @@ var BrightScriptFormatter = /** @class */ (function () { BrightScriptFormatter.DEFAULT_INDENT_SPACE_COUNT = 4; return BrightScriptFormatter; }()); -exports.BrightScriptFormatter = BrightScriptFormatter; +exports.BrightScriptFormatter = BrightScriptFormatter; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b57d729..a4b00e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4011,6 +4011,11 @@ "punycode": "1.4.1" } }, + "trim-right": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", + "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" + }, "ts-node": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-5.0.1.tgz", diff --git a/package.json b/package.json index 37c1edf..16c196a 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,8 @@ }, "homepage": "https://github.com/TwitchBronBron/brightscript-formatter#readme", "dependencies": { - "brightscript-parser": "^1.0.3-1" + "brightscript-parser": "^1.0.3-1", + "trim-right": "^1.0.1" }, "devDependencies": { "@types/chai": "^4.1.2", diff --git a/src/BrightScriptFormatter.spec.ts b/src/BrightScriptFormatter.spec.ts index 216bb91..142bffd 100644 --- a/src/BrightScriptFormatter.spec.ts +++ b/src/BrightScriptFormatter.spec.ts @@ -227,6 +227,35 @@ describe('BrightScriptFormatter', () => { }); }); + describe('removeTrailingWhitespace', () => { + it('removes trailing spaces by default', () => { + expect(formatter.format(`name = "bob" `)).to.equal(`name = "bob"`); + }); + it('removes trailing tabs by default', () => { + expect(formatter.format(`name = "bob"\t`)).to.equal(`name = "bob"`); + }); + it('removes both tabs and spaces in same line', () => { + expect(formatter.format(`name = "bob"\t `)).to.equal(`name = "bob"`); + expect(formatter.format(`name = "bob" \t`)).to.equal(`name = "bob"`); + }); + it('removes whitespace from end of comment', () => { + expect(formatter.format(`'comment `)).to.equal(`'comment`); + expect(formatter.format(`'comment\t`)).to.equal(`'comment`); + expect(formatter.format(`'comment \t`)).to.equal(`'comment`); + expect(formatter.format(`'comment\t `)).to.equal(`'comment`); + }); + it('handles multi-line prorgams', () => { + expect(formatter.format(`name = "bob"\t \nage=22 `)).to.equal(`name = "bob"\nage=22`); + }); + it('leaves normal programs alone', () => { + expect(formatter.format(`name = "bob"\nage=22 `)).to.equal(`name = "bob"\nage=22`); + }); + it('skips formatting when the option is set to false', () => { + expect(formatter.format(`name = "bob" `, { removeTrailingWhiteSpace: false })).to.equal(`name = "bob" `); + + }); + }); + describe('break composite keywords', () => { function format(text, tokenType) { let token = { diff --git a/src/BrightScriptFormatter.ts b/src/BrightScriptFormatter.ts index ead4e68..899cbbb 100644 --- a/src/BrightScriptFormatter.ts +++ b/src/BrightScriptFormatter.ts @@ -1,4 +1,6 @@ import { BrightScriptLexer, CompositeKeywordTokenTypes, KeywordTokenTypes, Token, TokenType } from 'brightscript-parser'; +import * as trimRight from 'trim-right'; + export class BrightScriptFormatter { constructor() { @@ -31,6 +33,9 @@ export class BrightScriptFormatter { 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 = ''; @@ -243,6 +248,44 @@ export class BrightScriptFormatter { 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; + } + } + return outputTokens; + } + private tokenIndexOf(tokenType: TokenType, tokens: Token[]) { for (let i = 0; i < tokens.length; i++) { let token = tokens[i]; @@ -254,7 +297,7 @@ export class BrightScriptFormatter { } /** - * Get the tokens for the whole line starting at the given index + * 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 */ @@ -281,7 +324,8 @@ export class BrightScriptFormatter { indentStyle: 'spaces', indentSpaceCount: BrightScriptFormatter.DEFAULT_INDENT_SPACE_COUNT, keywordCase: 'lower', - compositeKeywords: 'split' + compositeKeywords: 'split', + removeTrailingWhiteSpace: true }; if (options) { for (let attrname in options) { @@ -342,4 +386,10 @@ export interface FormattingOptions { * 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