890 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			890 lines
		
	
	
		
			30 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| "use strict";
 | |
| Object.defineProperty(exports, "__esModule", { value: true });
 | |
| const string_utils_js_1 = require("./utils/string_utils.js");
 | |
| /** @internal */
 | |
| var CommentChar;
 | |
| (function (CommentChar) {
 | |
|     CommentChar[CommentChar["Line"] = 0] = "Line";
 | |
|     CommentChar[CommentChar["Star"] = 1] = "Star";
 | |
| })(CommentChar || (CommentChar = {}));
 | |
| // Using the char codes is a performance improvement (about 5.5% faster when writing because it eliminates additional string allocations).
 | |
| const CHARS = {
 | |
|     BACK_SLASH: "\\".charCodeAt(0),
 | |
|     FORWARD_SLASH: "/".charCodeAt(0),
 | |
|     NEW_LINE: "\n".charCodeAt(0),
 | |
|     CARRIAGE_RETURN: "\r".charCodeAt(0),
 | |
|     ASTERISK: "*".charCodeAt(0),
 | |
|     DOUBLE_QUOTE: "\"".charCodeAt(0),
 | |
|     SINGLE_QUOTE: "'".charCodeAt(0),
 | |
|     BACK_TICK: "`".charCodeAt(0),
 | |
|     OPEN_BRACE: "{".charCodeAt(0),
 | |
|     CLOSE_BRACE: "}".charCodeAt(0),
 | |
|     DOLLAR_SIGN: "$".charCodeAt(0),
 | |
|     SPACE: " ".charCodeAt(0),
 | |
|     TAB: "\t".charCodeAt(0),
 | |
| };
 | |
| const isCharToHandle = new Set([
 | |
|     CHARS.BACK_SLASH,
 | |
|     CHARS.FORWARD_SLASH,
 | |
|     CHARS.NEW_LINE,
 | |
|     CHARS.CARRIAGE_RETURN,
 | |
|     CHARS.ASTERISK,
 | |
|     CHARS.DOUBLE_QUOTE,
 | |
|     CHARS.SINGLE_QUOTE,
 | |
|     CHARS.BACK_TICK,
 | |
|     CHARS.OPEN_BRACE,
 | |
|     CHARS.CLOSE_BRACE,
 | |
| ]);
 | |
| /**
 | |
|  * Code writer that assists with formatting and visualizing blocks of JavaScript or TypeScript code.
 | |
|  */
 | |
| class CodeBlockWriter {
 | |
|     /**
 | |
|      * Constructor.
 | |
|      * @param opts - Options for the writer.
 | |
|      */
 | |
|     constructor(opts = {}) {
 | |
|         /** @internal */
 | |
|         Object.defineProperty(this, "_indentationText", {
 | |
|             enumerable: true,
 | |
|             configurable: true,
 | |
|             writable: true,
 | |
|             value: void 0
 | |
|         });
 | |
|         /** @internal */
 | |
|         Object.defineProperty(this, "_newLine", {
 | |
|             enumerable: true,
 | |
|             configurable: true,
 | |
|             writable: true,
 | |
|             value: void 0
 | |
|         });
 | |
|         /** @internal */
 | |
|         Object.defineProperty(this, "_useTabs", {
 | |
|             enumerable: true,
 | |
|             configurable: true,
 | |
|             writable: true,
 | |
|             value: void 0
 | |
|         });
 | |
|         /** @internal */
 | |
|         Object.defineProperty(this, "_quoteChar", {
 | |
|             enumerable: true,
 | |
|             configurable: true,
 | |
|             writable: true,
 | |
|             value: void 0
 | |
|         });
 | |
|         /** @internal */
 | |
|         Object.defineProperty(this, "_indentNumberOfSpaces", {
 | |
|             enumerable: true,
 | |
|             configurable: true,
 | |
|             writable: true,
 | |
|             value: void 0
 | |
|         });
 | |
|         /** @internal */
 | |
|         Object.defineProperty(this, "_currentIndentation", {
 | |
|             enumerable: true,
 | |
|             configurable: true,
 | |
|             writable: true,
 | |
|             value: 0
 | |
|         });
 | |
|         /** @internal */
 | |
|         Object.defineProperty(this, "_queuedIndentation", {
 | |
|             enumerable: true,
 | |
|             configurable: true,
 | |
|             writable: true,
 | |
|             value: void 0
 | |
|         });
 | |
|         /** @internal */
 | |
|         Object.defineProperty(this, "_queuedOnlyIfNotBlock", {
 | |
|             enumerable: true,
 | |
|             configurable: true,
 | |
|             writable: true,
 | |
|             value: void 0
 | |
|         });
 | |
|         /** @internal */
 | |
|         Object.defineProperty(this, "_length", {
 | |
|             enumerable: true,
 | |
|             configurable: true,
 | |
|             writable: true,
 | |
|             value: 0
 | |
|         });
 | |
|         /** @internal */
 | |
|         Object.defineProperty(this, "_newLineOnNextWrite", {
 | |
|             enumerable: true,
 | |
|             configurable: true,
 | |
|             writable: true,
 | |
|             value: false
 | |
|         });
 | |
|         /** @internal */
 | |
|         Object.defineProperty(this, "_currentCommentChar", {
 | |
|             enumerable: true,
 | |
|             configurable: true,
 | |
|             writable: true,
 | |
|             value: undefined
 | |
|         });
 | |
|         /** @internal */
 | |
|         Object.defineProperty(this, "_stringCharStack", {
 | |
|             enumerable: true,
 | |
|             configurable: true,
 | |
|             writable: true,
 | |
|             value: []
 | |
|         });
 | |
|         /** @internal */
 | |
|         Object.defineProperty(this, "_isInRegEx", {
 | |
|             enumerable: true,
 | |
|             configurable: true,
 | |
|             writable: true,
 | |
|             value: false
 | |
|         });
 | |
|         /** @internal */
 | |
|         Object.defineProperty(this, "_isOnFirstLineOfBlock", {
 | |
|             enumerable: true,
 | |
|             configurable: true,
 | |
|             writable: true,
 | |
|             value: true
 | |
|         });
 | |
|         // An array of strings is used rather than a single string because it was
 | |
|         // found to be ~11x faster when printing a 10K line file (~11s to ~1s).
 | |
|         /** @internal */
 | |
|         Object.defineProperty(this, "_texts", {
 | |
|             enumerable: true,
 | |
|             configurable: true,
 | |
|             writable: true,
 | |
|             value: []
 | |
|         });
 | |
|         this._newLine = opts.newLine || "\n";
 | |
|         this._useTabs = opts.useTabs || false;
 | |
|         this._indentNumberOfSpaces = opts.indentNumberOfSpaces || 4;
 | |
|         this._indentationText = getIndentationText(this._useTabs, this._indentNumberOfSpaces);
 | |
|         this._quoteChar = opts.useSingleQuote ? "'" : `"`;
 | |
|     }
 | |
|     /**
 | |
|      * Gets the options.
 | |
|      */
 | |
|     getOptions() {
 | |
|         return {
 | |
|             indentNumberOfSpaces: this._indentNumberOfSpaces,
 | |
|             newLine: this._newLine,
 | |
|             useTabs: this._useTabs,
 | |
|             useSingleQuote: this._quoteChar === "'",
 | |
|         };
 | |
|     }
 | |
|     queueIndentationLevel(countOrText) {
 | |
|         this._queuedIndentation = this._getIndentationLevelFromArg(countOrText);
 | |
|         this._queuedOnlyIfNotBlock = undefined;
 | |
|         return this;
 | |
|     }
 | |
|     /**
 | |
|      * Writes the text within the provided action with hanging indentation.
 | |
|      * @param action - Action to perform with hanging indentation.
 | |
|      */
 | |
|     hangingIndent(action) {
 | |
|         return this._withResetIndentation(() => this.queueIndentationLevel(this.getIndentationLevel() + 1), action);
 | |
|     }
 | |
|     /**
 | |
|      * Writes the text within the provided action with hanging indentation unless writing a block.
 | |
|      * @param action - Action to perform with hanging indentation unless a block is written.
 | |
|      */
 | |
|     hangingIndentUnlessBlock(action) {
 | |
|         return this._withResetIndentation(() => {
 | |
|             this.queueIndentationLevel(this.getIndentationLevel() + 1);
 | |
|             this._queuedOnlyIfNotBlock = true;
 | |
|         }, action);
 | |
|     }
 | |
|     setIndentationLevel(countOrText) {
 | |
|         this._currentIndentation = this._getIndentationLevelFromArg(countOrText);
 | |
|         return this;
 | |
|     }
 | |
|     withIndentationLevel(countOrText, action) {
 | |
|         return this._withResetIndentation(() => this.setIndentationLevel(countOrText), action);
 | |
|     }
 | |
|     /** @internal */
 | |
|     _withResetIndentation(setStateAction, writeAction) {
 | |
|         const previousState = this._getIndentationState();
 | |
|         setStateAction();
 | |
|         try {
 | |
|             writeAction();
 | |
|         }
 | |
|         finally {
 | |
|             this._setIndentationState(previousState);
 | |
|         }
 | |
|         return this;
 | |
|     }
 | |
|     /**
 | |
|      * Gets the current indentation level.
 | |
|      */
 | |
|     getIndentationLevel() {
 | |
|         return this._currentIndentation;
 | |
|     }
 | |
|     /**
 | |
|      * Writes a block using braces.
 | |
|      * @param block - Write using the writer within this block.
 | |
|      */
 | |
|     block(block) {
 | |
|         this._newLineIfNewLineOnNextWrite();
 | |
|         if (this.getLength() > 0 && !this.isLastNewLine()) {
 | |
|             this.spaceIfLastNot();
 | |
|         }
 | |
|         this.inlineBlock(block);
 | |
|         this._newLineOnNextWrite = true;
 | |
|         return this;
 | |
|     }
 | |
|     /**
 | |
|      * Writes an inline block with braces.
 | |
|      * @param block - Write using the writer within this block.
 | |
|      */
 | |
|     inlineBlock(block) {
 | |
|         this._newLineIfNewLineOnNextWrite();
 | |
|         this.write("{");
 | |
|         this._indentBlockInternal(block);
 | |
|         this.newLineIfLastNot().write("}");
 | |
|         return this;
 | |
|     }
 | |
|     indent(timesOrBlock = 1) {
 | |
|         if (typeof timesOrBlock === "number") {
 | |
|             this._newLineIfNewLineOnNextWrite();
 | |
|             return this.write(this._indentationText.repeat(timesOrBlock));
 | |
|         }
 | |
|         else {
 | |
|             this._indentBlockInternal(timesOrBlock);
 | |
|             if (!this.isLastNewLine()) {
 | |
|                 this._newLineOnNextWrite = true;
 | |
|             }
 | |
|             return this;
 | |
|         }
 | |
|     }
 | |
|     /** @internal */
 | |
|     _indentBlockInternal(block) {
 | |
|         if (this.getLastChar() != null) {
 | |
|             this.newLineIfLastNot();
 | |
|         }
 | |
|         this._currentIndentation++;
 | |
|         this._isOnFirstLineOfBlock = true;
 | |
|         if (block != null) {
 | |
|             block();
 | |
|         }
 | |
|         this._isOnFirstLineOfBlock = false;
 | |
|         this._currentIndentation = Math.max(0, this._currentIndentation - 1);
 | |
|     }
 | |
|     conditionalWriteLine(condition, strOrFunc) {
 | |
|         if (condition) {
 | |
|             this.writeLine((0, string_utils_js_1.getStringFromStrOrFunc)(strOrFunc));
 | |
|         }
 | |
|         return this;
 | |
|     }
 | |
|     /**
 | |
|      * Writes a line of text.
 | |
|      * @param text - String to write.
 | |
|      */
 | |
|     writeLine(text) {
 | |
|         this._newLineIfNewLineOnNextWrite();
 | |
|         if (this.getLastChar() != null) {
 | |
|             this.newLineIfLastNot();
 | |
|         }
 | |
|         this._writeIndentingNewLines(text);
 | |
|         this.newLine();
 | |
|         return this;
 | |
|     }
 | |
|     /**
 | |
|      * Writes a newline if the last line was not a newline.
 | |
|      */
 | |
|     newLineIfLastNot() {
 | |
|         this._newLineIfNewLineOnNextWrite();
 | |
|         if (!this.isLastNewLine()) {
 | |
|             this.newLine();
 | |
|         }
 | |
|         return this;
 | |
|     }
 | |
|     /**
 | |
|      * Writes a blank line if the last written text was not a blank line.
 | |
|      */
 | |
|     blankLineIfLastNot() {
 | |
|         if (!this.isLastBlankLine()) {
 | |
|             this.blankLine();
 | |
|         }
 | |
|         return this;
 | |
|     }
 | |
|     /**
 | |
|      * Writes a blank line if the condition is true.
 | |
|      * @param condition - Condition to evaluate.
 | |
|      */
 | |
|     conditionalBlankLine(condition) {
 | |
|         if (condition) {
 | |
|             this.blankLine();
 | |
|         }
 | |
|         return this;
 | |
|     }
 | |
|     /**
 | |
|      * Writes a blank line.
 | |
|      */
 | |
|     blankLine() {
 | |
|         return this.newLineIfLastNot().newLine();
 | |
|     }
 | |
|     /**
 | |
|      * Writes a newline if the condition is true.
 | |
|      * @param condition - Condition to evaluate.
 | |
|      */
 | |
|     conditionalNewLine(condition) {
 | |
|         if (condition) {
 | |
|             this.newLine();
 | |
|         }
 | |
|         return this;
 | |
|     }
 | |
|     /**
 | |
|      * Writes a newline.
 | |
|      */
 | |
|     newLine() {
 | |
|         this._newLineOnNextWrite = false;
 | |
|         this._baseWriteNewline();
 | |
|         return this;
 | |
|     }
 | |
|     quote(text) {
 | |
|         this._newLineIfNewLineOnNextWrite();
 | |
|         this._writeIndentingNewLines(text == null ? this._quoteChar : this._quoteChar + (0, string_utils_js_1.escapeForWithinString)(text, this._quoteChar) + this._quoteChar);
 | |
|         return this;
 | |
|     }
 | |
|     /**
 | |
|      * Writes a space if the last character was not a space.
 | |
|      */
 | |
|     spaceIfLastNot() {
 | |
|         this._newLineIfNewLineOnNextWrite();
 | |
|         if (!this.isLastSpace()) {
 | |
|             this._writeIndentingNewLines(" ");
 | |
|         }
 | |
|         return this;
 | |
|     }
 | |
|     /**
 | |
|      * Writes a space.
 | |
|      * @param times - Number of times to write a space.
 | |
|      */
 | |
|     space(times = 1) {
 | |
|         this._newLineIfNewLineOnNextWrite();
 | |
|         this._writeIndentingNewLines(" ".repeat(times));
 | |
|         return this;
 | |
|     }
 | |
|     /**
 | |
|      * Writes a tab if the last character was not a tab.
 | |
|      */
 | |
|     tabIfLastNot() {
 | |
|         this._newLineIfNewLineOnNextWrite();
 | |
|         if (!this.isLastTab()) {
 | |
|             this._writeIndentingNewLines("\t");
 | |
|         }
 | |
|         return this;
 | |
|     }
 | |
|     /**
 | |
|      * Writes a tab.
 | |
|      * @param times - Number of times to write a tab.
 | |
|      */
 | |
|     tab(times = 1) {
 | |
|         this._newLineIfNewLineOnNextWrite();
 | |
|         this._writeIndentingNewLines("\t".repeat(times));
 | |
|         return this;
 | |
|     }
 | |
|     conditionalWrite(condition, textOrFunc) {
 | |
|         if (condition) {
 | |
|             this.write((0, string_utils_js_1.getStringFromStrOrFunc)(textOrFunc));
 | |
|         }
 | |
|         return this;
 | |
|     }
 | |
|     /**
 | |
|      * Writes the provided text.
 | |
|      * @param text - Text to write.
 | |
|      */
 | |
|     write(text) {
 | |
|         this._newLineIfNewLineOnNextWrite();
 | |
|         this._writeIndentingNewLines(text);
 | |
|         return this;
 | |
|     }
 | |
|     /**
 | |
|      * Writes text to exit a comment if in a comment.
 | |
|      */
 | |
|     closeComment() {
 | |
|         const commentChar = this._currentCommentChar;
 | |
|         switch (commentChar) {
 | |
|             case CommentChar.Line:
 | |
|                 this.newLine();
 | |
|                 break;
 | |
|             case CommentChar.Star:
 | |
|                 if (!this.isLastNewLine()) {
 | |
|                     this.spaceIfLastNot();
 | |
|                 }
 | |
|                 this.write("*/");
 | |
|                 break;
 | |
|             default: {
 | |
|                 const _assertUndefined = commentChar;
 | |
|                 break;
 | |
|             }
 | |
|         }
 | |
|         return this;
 | |
|     }
 | |
|     /**
 | |
|      * Inserts text at the provided position.
 | |
|      *
 | |
|      * This method is "unsafe" because it won't update the state of the writer unless
 | |
|      * inserting at the end position. It is biased towards being fast at inserting closer
 | |
|      * to the start or end, but slower to insert in the middle. Only use this if
 | |
|      * absolutely necessary.
 | |
|      * @param pos - Position to insert at.
 | |
|      * @param text - Text to insert.
 | |
|      */
 | |
|     unsafeInsert(pos, text) {
 | |
|         const textLength = this._length;
 | |
|         const texts = this._texts;
 | |
|         verifyInput();
 | |
|         if (pos === textLength) {
 | |
|             return this.write(text);
 | |
|         }
 | |
|         updateInternalArray();
 | |
|         this._length += text.length;
 | |
|         return this;
 | |
|         function verifyInput() {
 | |
|             if (pos < 0) {
 | |
|                 throw new Error(`Provided position of '${pos}' was less than zero.`);
 | |
|             }
 | |
|             if (pos > textLength) {
 | |
|                 throw new Error(`Provided position of '${pos}' was greater than the text length of '${textLength}'.`);
 | |
|             }
 | |
|         }
 | |
|         function updateInternalArray() {
 | |
|             const { index, localIndex } = getArrayIndexAndLocalIndex();
 | |
|             if (localIndex === 0) {
 | |
|                 texts.splice(index, 0, text);
 | |
|             }
 | |
|             else if (localIndex === texts[index].length) {
 | |
|                 texts.splice(index + 1, 0, text);
 | |
|             }
 | |
|             else {
 | |
|                 const textItem = texts[index];
 | |
|                 const startText = textItem.substring(0, localIndex);
 | |
|                 const endText = textItem.substring(localIndex);
 | |
|                 texts.splice(index, 1, startText, text, endText);
 | |
|             }
 | |
|         }
 | |
|         function getArrayIndexAndLocalIndex() {
 | |
|             if (pos < textLength / 2) {
 | |
|                 // start searching from the front
 | |
|                 let endPos = 0;
 | |
|                 for (let i = 0; i < texts.length; i++) {
 | |
|                     const textItem = texts[i];
 | |
|                     const startPos = endPos;
 | |
|                     endPos += textItem.length;
 | |
|                     if (endPos >= pos) {
 | |
|                         return { index: i, localIndex: pos - startPos };
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|             else {
 | |
|                 // start searching from the back
 | |
|                 let startPos = textLength;
 | |
|                 for (let i = texts.length - 1; i >= 0; i--) {
 | |
|                     const textItem = texts[i];
 | |
|                     startPos -= textItem.length;
 | |
|                     if (startPos <= pos) {
 | |
|                         return { index: i, localIndex: pos - startPos };
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|             throw new Error("Unhandled situation inserting. This should never happen.");
 | |
|         }
 | |
|     }
 | |
|     /**
 | |
|      * Gets the length of the string in the writer.
 | |
|      */
 | |
|     getLength() {
 | |
|         return this._length;
 | |
|     }
 | |
|     /**
 | |
|      * Gets if the writer is currently in a comment.
 | |
|      */
 | |
|     isInComment() {
 | |
|         return this._currentCommentChar !== undefined;
 | |
|     }
 | |
|     /**
 | |
|      * Gets if the writer is currently at the start of the first line of the text, block, or indentation block.
 | |
|      */
 | |
|     isAtStartOfFirstLineOfBlock() {
 | |
|         return this.isOnFirstLineOfBlock() && (this.isLastNewLine() || this.getLastChar() == null);
 | |
|     }
 | |
|     /**
 | |
|      * Gets if the writer is currently on the first line of the text, block, or indentation block.
 | |
|      */
 | |
|     isOnFirstLineOfBlock() {
 | |
|         return this._isOnFirstLineOfBlock;
 | |
|     }
 | |
|     /**
 | |
|      * Gets if the writer is currently in a string.
 | |
|      */
 | |
|     isInString() {
 | |
|         return this._stringCharStack.length > 0 && this._stringCharStack[this._stringCharStack.length - 1] !== CHARS.OPEN_BRACE;
 | |
|     }
 | |
|     /**
 | |
|      * Gets if the last chars written were for a newline.
 | |
|      */
 | |
|     isLastNewLine() {
 | |
|         const lastChar = this.getLastChar();
 | |
|         return lastChar === "\n" || lastChar === "\r";
 | |
|     }
 | |
|     /**
 | |
|      * Gets if the last chars written were for a blank line.
 | |
|      */
 | |
|     isLastBlankLine() {
 | |
|         let foundCount = 0;
 | |
|         // todo: consider extracting out iterating over past characters, but don't use
 | |
|         // an iterator because it will be slow.
 | |
|         for (let i = this._texts.length - 1; i >= 0; i--) {
 | |
|             const currentText = this._texts[i];
 | |
|             for (let j = currentText.length - 1; j >= 0; j--) {
 | |
|                 const currentChar = currentText.charCodeAt(j);
 | |
|                 if (currentChar === CHARS.NEW_LINE) {
 | |
|                     foundCount++;
 | |
|                     if (foundCount === 2) {
 | |
|                         return true;
 | |
|                     }
 | |
|                 }
 | |
|                 else if (currentChar !== CHARS.CARRIAGE_RETURN) {
 | |
|                     return false;
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         return false;
 | |
|     }
 | |
|     /**
 | |
|      * Gets if the last char written was a space.
 | |
|      */
 | |
|     isLastSpace() {
 | |
|         return this.getLastChar() === " ";
 | |
|     }
 | |
|     /**
 | |
|      * Gets if the last char written was a tab.
 | |
|      */
 | |
|     isLastTab() {
 | |
|         return this.getLastChar() === "\t";
 | |
|     }
 | |
|     /**
 | |
|      * Gets the last char written.
 | |
|      */
 | |
|     getLastChar() {
 | |
|         const charCode = this._getLastCharCodeWithOffset(0);
 | |
|         return charCode == null ? undefined : String.fromCharCode(charCode);
 | |
|     }
 | |
|     /**
 | |
|      * Gets if the writer ends with the provided text.
 | |
|      * @param text - Text to check if the writer ends with the provided text.
 | |
|      */
 | |
|     endsWith(text) {
 | |
|         const length = this._length;
 | |
|         return this.iterateLastCharCodes((charCode, index) => {
 | |
|             const offset = length - index;
 | |
|             const textIndex = text.length - offset;
 | |
|             if (text.charCodeAt(textIndex) !== charCode) {
 | |
|                 return false;
 | |
|             }
 | |
|             return textIndex === 0 ? true : undefined;
 | |
|         }) || false;
 | |
|     }
 | |
|     /**
 | |
|      * Iterates over the writer characters in reverse order. The iteration stops when a non-null or
 | |
|      * undefined value is returned from the action. The returned value is then returned by the method.
 | |
|      *
 | |
|      * @remarks It is much more efficient to use this method rather than `#toString()` since `#toString()`
 | |
|      * will combine the internal array into a string.
 | |
|      */
 | |
|     iterateLastChars(action) {
 | |
|         return this.iterateLastCharCodes((charCode, index) => action(String.fromCharCode(charCode), index));
 | |
|     }
 | |
|     /**
 | |
|      * Iterates over the writer character char codes in reverse order. The iteration stops when a non-null or
 | |
|      * undefined value is returned from the action. The returned value is then returned by the method.
 | |
|      *
 | |
|      * @remarks It is much more efficient to use this method rather than `#toString()` since `#toString()`
 | |
|      * will combine the internal array into a string. Additionally, this is slightly more efficient that
 | |
|      * `iterateLastChars` as this won't allocate a string per character.
 | |
|      */
 | |
|     iterateLastCharCodes(action) {
 | |
|         let index = this._length;
 | |
|         for (let i = this._texts.length - 1; i >= 0; i--) {
 | |
|             const currentText = this._texts[i];
 | |
|             for (let j = currentText.length - 1; j >= 0; j--) {
 | |
|                 index--;
 | |
|                 const result = action(currentText.charCodeAt(j), index);
 | |
|                 if (result != null) {
 | |
|                     return result;
 | |
|                 }
 | |
|             }
 | |
|         }
 | |
|         return undefined;
 | |
|     }
 | |
|     /**
 | |
|      * Gets the writer's text.
 | |
|      */
 | |
|     toString() {
 | |
|         if (this._texts.length > 1) {
 | |
|             const text = this._texts.join("");
 | |
|             this._texts.length = 0;
 | |
|             this._texts.push(text);
 | |
|         }
 | |
|         return this._texts[0] || "";
 | |
|     }
 | |
|     /** @internal */
 | |
|     _writeIndentingNewLines(text) {
 | |
|         text = text || "";
 | |
|         if (text.length === 0) {
 | |
|             writeIndividual(this, "");
 | |
|             return;
 | |
|         }
 | |
|         const items = text.split(CodeBlockWriter._newLineRegEx);
 | |
|         items.forEach((s, i) => {
 | |
|             if (i > 0) {
 | |
|                 this._baseWriteNewline();
 | |
|             }
 | |
|             if (s.length === 0) {
 | |
|                 return;
 | |
|             }
 | |
|             writeIndividual(this, s);
 | |
|         });
 | |
|         function writeIndividual(writer, s) {
 | |
|             if (!writer.isInString()) {
 | |
|                 const isAtStartOfLine = writer.isLastNewLine() || writer.getLastChar() == null;
 | |
|                 if (isAtStartOfLine) {
 | |
|                     writer._writeIndentation();
 | |
|                 }
 | |
|             }
 | |
|             writer._updateInternalState(s);
 | |
|             writer._internalWrite(s);
 | |
|         }
 | |
|     }
 | |
|     /** @internal */
 | |
|     _baseWriteNewline() {
 | |
|         if (this._currentCommentChar === CommentChar.Line) {
 | |
|             this._currentCommentChar = undefined;
 | |
|         }
 | |
|         const lastStringCharOnStack = this._stringCharStack[this._stringCharStack.length - 1];
 | |
|         if ((lastStringCharOnStack === CHARS.DOUBLE_QUOTE || lastStringCharOnStack === CHARS.SINGLE_QUOTE) && this._getLastCharCodeWithOffset(0) !== CHARS.BACK_SLASH) {
 | |
|             this._stringCharStack.pop();
 | |
|         }
 | |
|         this._internalWrite(this._newLine);
 | |
|         this._isOnFirstLineOfBlock = false;
 | |
|         this._dequeueQueuedIndentation();
 | |
|     }
 | |
|     /** @internal */
 | |
|     _dequeueQueuedIndentation() {
 | |
|         if (this._queuedIndentation == null) {
 | |
|             return;
 | |
|         }
 | |
|         if (this._queuedOnlyIfNotBlock && wasLastBlock(this)) {
 | |
|             this._queuedIndentation = undefined;
 | |
|             this._queuedOnlyIfNotBlock = undefined;
 | |
|         }
 | |
|         else {
 | |
|             this._currentIndentation = this._queuedIndentation;
 | |
|             this._queuedIndentation = undefined;
 | |
|         }
 | |
|         function wasLastBlock(writer) {
 | |
|             let foundNewLine = false;
 | |
|             return writer.iterateLastCharCodes(charCode => {
 | |
|                 switch (charCode) {
 | |
|                     case CHARS.NEW_LINE:
 | |
|                         if (foundNewLine) {
 | |
|                             return false;
 | |
|                         }
 | |
|                         else {
 | |
|                             foundNewLine = true;
 | |
|                         }
 | |
|                         break;
 | |
|                     case CHARS.CARRIAGE_RETURN:
 | |
|                         return undefined;
 | |
|                     case CHARS.OPEN_BRACE:
 | |
|                         return true;
 | |
|                     default:
 | |
|                         return false;
 | |
|                 }
 | |
|             });
 | |
|         }
 | |
|     }
 | |
|     /** @internal */
 | |
|     _updateInternalState(str) {
 | |
|         for (let i = 0; i < str.length; i++) {
 | |
|             const currentChar = str.charCodeAt(i);
 | |
|             // This is a performance optimization to short circuit all the checks below. If the current char
 | |
|             // is not in this set then it won't change any internal state so no need to continue and do
 | |
|             // so many other checks (this made it 3x faster in one scenario I tested).
 | |
|             if (!isCharToHandle.has(currentChar)) {
 | |
|                 continue;
 | |
|             }
 | |
|             const pastChar = i === 0 ? this._getLastCharCodeWithOffset(0) : str.charCodeAt(i - 1);
 | |
|             const pastPastChar = i === 0 ? this._getLastCharCodeWithOffset(1) : i === 1 ? this._getLastCharCodeWithOffset(0) : str.charCodeAt(i - 2);
 | |
|             // handle regex
 | |
|             if (this._isInRegEx) {
 | |
|                 if (pastChar === CHARS.FORWARD_SLASH && pastPastChar !== CHARS.BACK_SLASH || pastChar === CHARS.NEW_LINE) {
 | |
|                     this._isInRegEx = false;
 | |
|                 }
 | |
|                 else {
 | |
|                     continue;
 | |
|                 }
 | |
|             }
 | |
|             else if (!this.isInString() && !this.isInComment() && isRegExStart(currentChar, pastChar, pastPastChar)) {
 | |
|                 this._isInRegEx = true;
 | |
|                 continue;
 | |
|             }
 | |
|             // handle comments
 | |
|             if (!this.isInString()) {
 | |
|                 if (this._currentCommentChar == null && pastChar === CHARS.FORWARD_SLASH && currentChar === CHARS.FORWARD_SLASH) {
 | |
|                     this._currentCommentChar = CommentChar.Line;
 | |
|                 }
 | |
|                 else if (this._currentCommentChar == null && pastChar === CHARS.FORWARD_SLASH && currentChar === CHARS.ASTERISK) {
 | |
|                     this._currentCommentChar = CommentChar.Star;
 | |
|                 }
 | |
|                 else if (this._currentCommentChar === CommentChar.Star && pastChar === CHARS.ASTERISK && currentChar === CHARS.FORWARD_SLASH) {
 | |
|                     this._currentCommentChar = undefined;
 | |
|                 }
 | |
|             }
 | |
|             if (this.isInComment()) {
 | |
|                 continue;
 | |
|             }
 | |
|             // handle strings
 | |
|             const lastStringCharOnStack = this._stringCharStack.length === 0 ? undefined : this._stringCharStack[this._stringCharStack.length - 1];
 | |
|             if (pastChar !== CHARS.BACK_SLASH && (currentChar === CHARS.DOUBLE_QUOTE || currentChar === CHARS.SINGLE_QUOTE || currentChar === CHARS.BACK_TICK)) {
 | |
|                 if (lastStringCharOnStack === currentChar) {
 | |
|                     this._stringCharStack.pop();
 | |
|                 }
 | |
|                 else if (lastStringCharOnStack === CHARS.OPEN_BRACE || lastStringCharOnStack === undefined) {
 | |
|                     this._stringCharStack.push(currentChar);
 | |
|                 }
 | |
|             }
 | |
|             else if (pastPastChar !== CHARS.BACK_SLASH && pastChar === CHARS.DOLLAR_SIGN && currentChar === CHARS.OPEN_BRACE && lastStringCharOnStack === CHARS.BACK_TICK) {
 | |
|                 this._stringCharStack.push(currentChar);
 | |
|             }
 | |
|             else if (currentChar === CHARS.CLOSE_BRACE && lastStringCharOnStack === CHARS.OPEN_BRACE) {
 | |
|                 this._stringCharStack.pop();
 | |
|             }
 | |
|         }
 | |
|     }
 | |
|     /** @internal - This is private, but exposed for testing. */
 | |
|     _getLastCharCodeWithOffset(offset) {
 | |
|         if (offset >= this._length || offset < 0) {
 | |
|             return undefined;
 | |
|         }
 | |
|         for (let i = this._texts.length - 1; i >= 0; i--) {
 | |
|             const currentText = this._texts[i];
 | |
|             if (offset >= currentText.length) {
 | |
|                 offset -= currentText.length;
 | |
|             }
 | |
|             else {
 | |
|                 return currentText.charCodeAt(currentText.length - 1 - offset);
 | |
|             }
 | |
|         }
 | |
|         return undefined;
 | |
|     }
 | |
|     /** @internal */
 | |
|     _writeIndentation() {
 | |
|         const flooredIndentation = Math.floor(this._currentIndentation);
 | |
|         this._internalWrite(this._indentationText.repeat(flooredIndentation));
 | |
|         const overflow = this._currentIndentation - flooredIndentation;
 | |
|         if (this._useTabs) {
 | |
|             if (overflow > 0.5) {
 | |
|                 this._internalWrite(this._indentationText);
 | |
|             }
 | |
|         }
 | |
|         else {
 | |
|             const portion = Math.round(this._indentationText.length * overflow);
 | |
|             // build up the string first, then append it for performance reasons
 | |
|             let text = "";
 | |
|             for (let i = 0; i < portion; i++) {
 | |
|                 text += this._indentationText[i];
 | |
|             }
 | |
|             this._internalWrite(text);
 | |
|         }
 | |
|     }
 | |
|     /** @internal */
 | |
|     _newLineIfNewLineOnNextWrite() {
 | |
|         if (!this._newLineOnNextWrite) {
 | |
|             return;
 | |
|         }
 | |
|         this._newLineOnNextWrite = false;
 | |
|         this.newLine();
 | |
|     }
 | |
|     /** @internal */
 | |
|     _internalWrite(text) {
 | |
|         if (text.length === 0) {
 | |
|             return;
 | |
|         }
 | |
|         this._texts.push(text);
 | |
|         this._length += text.length;
 | |
|     }
 | |
|     /** @internal */
 | |
|     _getIndentationLevelFromArg(countOrText) {
 | |
|         if (typeof countOrText === "number") {
 | |
|             if (countOrText < 0) {
 | |
|                 throw new Error("Passed in indentation level should be greater than or equal to 0.");
 | |
|             }
 | |
|             return countOrText;
 | |
|         }
 | |
|         else if (typeof countOrText === "string") {
 | |
|             if (!CodeBlockWriter._spacesOrTabsRegEx.test(countOrText)) {
 | |
|                 throw new Error("Provided string must be empty or only contain spaces or tabs.");
 | |
|             }
 | |
|             const { spacesCount, tabsCount } = getSpacesAndTabsCount(countOrText);
 | |
|             return tabsCount + spacesCount / this._indentNumberOfSpaces;
 | |
|         }
 | |
|         else {
 | |
|             throw new Error("Argument provided must be a string or number.");
 | |
|         }
 | |
|     }
 | |
|     /** @internal */
 | |
|     _setIndentationState(state) {
 | |
|         this._currentIndentation = state.current;
 | |
|         this._queuedIndentation = state.queued;
 | |
|         this._queuedOnlyIfNotBlock = state.queuedOnlyIfNotBlock;
 | |
|     }
 | |
|     /** @internal */
 | |
|     _getIndentationState() {
 | |
|         return {
 | |
|             current: this._currentIndentation,
 | |
|             queued: this._queuedIndentation,
 | |
|             queuedOnlyIfNotBlock: this._queuedOnlyIfNotBlock,
 | |
|         };
 | |
|     }
 | |
| }
 | |
| /** @internal */
 | |
| Object.defineProperty(CodeBlockWriter, "_newLineRegEx", {
 | |
|     enumerable: true,
 | |
|     configurable: true,
 | |
|     writable: true,
 | |
|     value: /\r?\n/
 | |
| });
 | |
| /** @internal */
 | |
| Object.defineProperty(CodeBlockWriter, "_spacesOrTabsRegEx", {
 | |
|     enumerable: true,
 | |
|     configurable: true,
 | |
|     writable: true,
 | |
|     value: /^[ \t]*$/
 | |
| });
 | |
| exports.default = CodeBlockWriter;
 | |
| function isRegExStart(currentChar, pastChar, pastPastChar) {
 | |
|     return pastChar === CHARS.FORWARD_SLASH
 | |
|         && currentChar !== CHARS.FORWARD_SLASH
 | |
|         && currentChar !== CHARS.ASTERISK
 | |
|         && pastPastChar !== CHARS.ASTERISK
 | |
|         && pastPastChar !== CHARS.FORWARD_SLASH;
 | |
| }
 | |
| function getIndentationText(useTabs, numberSpaces) {
 | |
|     if (useTabs) {
 | |
|         return "\t";
 | |
|     }
 | |
|     return Array(numberSpaces + 1).join(" ");
 | |
| }
 | |
| function getSpacesAndTabsCount(str) {
 | |
|     let spacesCount = 0;
 | |
|     let tabsCount = 0;
 | |
|     for (let i = 0; i < str.length; i++) {
 | |
|         const charCode = str.charCodeAt(i);
 | |
|         if (charCode === CHARS.SPACE) {
 | |
|             spacesCount++;
 | |
|         }
 | |
|         else if (charCode === CHARS.TAB) {
 | |
|             tabsCount++;
 | |
|         }
 | |
|     }
 | |
|     return { spacesCount, tabsCount };
 | |
| }
 |