import type Position from "./position"; import repeating from "repeating"; import trimRight from "trim-right"; /** * Buffer for collecting generated output. */ export default class Buffer { constructor(position: Position, format: Object) { this.printedCommentStarts = {}; this.parenPushNewlineState = null; this.position = position; this._indent = format.indent.base; this.format = format; this.buf = ""; } parenPushNewlineState: ?Object; buf: string; position: Position; _indent: number; format: Object; /** * Description */ catchUp(node) { // catch up to this nodes newline if we're behind if (node.loc && this.format.retainLines && this.buf) { while (this.position.line < node.loc.start.line) { this._push("\n"); } } } /** * Get the current trimmed buffer. */ get() { return trimRight(this.buf); } /** * Get the current indent. */ getIndent() { if (this.format.compact || this.format.concise) { return ""; } else { return repeating(this.format.indent.style, this._indent); } } /** * Get the current indent size. */ indentSize() { return this.getIndent().length; } /** * Increment indent size. */ indent() { this._indent++; } /** * Decrement indent size. */ dedent() { this._indent--; } /** * Add a semicolon to the buffer. */ semicolon() { this.push(";"); } /** * Ensure last character is a semicolon. */ ensureSemicolon() { if (!this.isLast(";")) this.semicolon(); } /** * Add a right brace to the buffer. */ rightBrace() { this.newline(true); if (this.format.compact && !this._lastPrintedIsEmptyStatement) { this._removeLast(";"); } this.push("}"); } /** * Add a keyword to the buffer. */ keyword(name: string) { this.push(name); this.space(); } /** * Add a space to the buffer unless it is compact (override with force). */ space(force?: boolean) { if (!force && this.format.compact) return; if (force || this.buf && !this.isLast(" ") && !this.isLast("\n")) { this.push(" "); } } /** * Remove the last character. */ removeLast(cha: string) { if (this.format.compact) return; return this._removeLast(cha); } _removeLast(cha: string) { if (!this._isLast(cha)) return; this.buf = this.buf.substr(0, this.buf.length - 1); this.position.unshift(cha); } /** * Set some state that will be modified if a newline has been inserted before any * non-space characters. * * This is to prevent breaking semantics for terminatorless separator nodes. eg: * * return foo; * * returns `foo`. But if we do: * * return * foo; * * `undefined` will be returned and not `foo` due to the terminator. */ startTerminatorless(): Object { return this.parenPushNewlineState = { printed: false }; } /** * Print an ending parentheses if a starting one has been printed. */ endTerminatorless(state: Object) { if (state.printed) { this.dedent(); this.newline(); this.push(")"); } } /** * Add a newline (or many newlines), maintaining formatting. * Strips multiple newlines if removeLast is true. */ newline(i?: boolean | number, removeLast?: boolean) { if (this.format.retainLines || this.format.compact) return; if (this.format.concise) { this.space(); return; } removeLast = removeLast || false; if (typeof i === "number") { i = Math.min(2, i); if (this.endsWith("{\n") || this.endsWith(":\n")) i--; if (i <= 0) return; while (i > 0) { this._newline(removeLast); i--; } return; } if (typeof i === "boolean") { removeLast = i; } this._newline(removeLast); } /** * Adds a newline unless there is already two previous newlines. */ _newline(removeLast?: boolean) { // never allow more than two lines if (this.endsWith("\n\n")) return; // remove the last newline if (removeLast && this.isLast("\n")) this.removeLast("\n"); this.removeLast(" "); this._removeSpacesAfterLastNewline(); this._push("\n"); } /** * If buffer ends with a newline and some spaces after it, trim those spaces. */ _removeSpacesAfterLastNewline() { let lastNewlineIndex = this.buf.lastIndexOf("\n"); if (lastNewlineIndex === -1) { return; } let index = this.buf.length - 1; while (index > lastNewlineIndex) { if (this.buf[index] !== " ") { break; } index--; } if (index === lastNewlineIndex) { this.buf = this.buf.substring(0, index + 1); } } /** * Push a string to the buffer, maintaining indentation and newlines. */ push(str: string, noIndent?: boolean) { if (!this.format.compact && this._indent && !noIndent && str !== "\n") { // we have an indent level and we aren't pushing a newline let indent = this.getIndent(); // replace all newlines with newlines with the indentation str = str.replace(/\n/g, `\n${indent}`); // we've got a newline before us so prepend on the indentation if (this.isLast("\n")) this._push(indent); } this._push(str); } /** * Push a string to the buffer. */ _push(str) { // see startTerminatorless() instance method let parenPushNewlineState = this.parenPushNewlineState; if (parenPushNewlineState) { for (let i = 0; i < str.length; i++) { let cha = str[i]; // we can ignore spaces since they wont interupt a terminatorless separator if (cha === " ") continue; this.parenPushNewlineState = null; if (cha === "\n" || cha === "/") { // we're going to break this terminator expression so we need to add a parentheses this._push("("); this.indent(); parenPushNewlineState.printed = true; } break; } } // this.position.push(str); this.buf += str; } /** * Test if the buffer ends with a string. */ endsWith(str: string, buf: string = this.buf): boolean { if (str.length === 1) { return buf[buf.length - 1] === str; } else { return buf.slice(-str.length) === str; } } /** * Test if a character is last in the buffer. */ isLast(cha: string) { if (this.format.compact) return false; return this._isLast(cha); } _isLast(cha: string) { let buf = this.buf; let last = buf[buf.length - 1]; if (Array.isArray(cha)) { return cha.indexOf(last) >= 0; } else { return cha === last; } } }