325 lines
7.2 KiB
JavaScript
325 lines
7.2 KiB
JavaScript
import type Position from "./position";
|
|
import repeat from "lodash/repeat";
|
|
import trimEnd from "lodash/trimEnd";
|
|
|
|
/**
|
|
* 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 = "";
|
|
|
|
// Maintaining a reference to the last char in the buffer is an optimization
|
|
// to make sure that v8 doesn't "flatten" the string more often than needed
|
|
// see https://github.com/babel/babel/pull/3283 for details.
|
|
this.last = "";
|
|
|
|
this.map = null;
|
|
this._sourcePosition = {
|
|
line: null,
|
|
column: null,
|
|
filename: null,
|
|
};
|
|
}
|
|
|
|
printedCommentStarts: Object;
|
|
parenPushNewlineState: ?Object;
|
|
position: Position;
|
|
_indent: number;
|
|
format: Object;
|
|
buf: string;
|
|
last: string;
|
|
|
|
/**
|
|
* Description
|
|
*/
|
|
|
|
catchUp(node: Object) {
|
|
// 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(): string {
|
|
return trimEnd(this.buf);
|
|
}
|
|
|
|
/**
|
|
* Get the current indent.
|
|
*/
|
|
|
|
getIndent(): string {
|
|
if (this.format.compact || this.format.concise) {
|
|
return "";
|
|
} else {
|
|
return repeat(this.format.indent.style, this._indent);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the current indent size.
|
|
*/
|
|
|
|
indentSize(): number {
|
|
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(";");
|
|
}
|
|
|
|
/**
|
|
* Add a right brace to the buffer.
|
|
*/
|
|
|
|
rightBrace() {
|
|
this.newline(true);
|
|
if (this.format.minified && !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.
|
|
*/
|
|
|
|
space() {
|
|
if (this.format.compact) return;
|
|
|
|
if (this.buf && !this.endsWith(" ") && !this.endsWith("\n")) {
|
|
this.push(" ");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Remove the last character.
|
|
*/
|
|
|
|
removeLast(cha: string) {
|
|
if (this.format.compact) return;
|
|
return this._removeLast(cha);
|
|
}
|
|
|
|
_removeLast(cha: string) {
|
|
if (!this.endsWith(cha)) return;
|
|
this.buf = this.buf.slice(0, -1);
|
|
this.last = this.buf[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;
|
|
}
|
|
|
|
// never allow more than two lines
|
|
if (this.endsWith("\n\n")) return;
|
|
|
|
if (typeof i === "boolean") removeLast = i;
|
|
if (typeof i !== "number") i = 1;
|
|
|
|
i = Math.min(2, i);
|
|
if (this.endsWith("{\n") || this.endsWith(":\n")) i--;
|
|
if (i <= 0) return;
|
|
|
|
// remove the last newline
|
|
if (removeLast) {
|
|
this.removeLast("\n");
|
|
}
|
|
|
|
this.removeLast(" ");
|
|
this.push(repeat("\n", i));
|
|
}
|
|
|
|
/**
|
|
* If buffer ends with a newline and some spaces after it, trim those spaces.
|
|
*/
|
|
|
|
_removeSpacesAfterLastNewline() {
|
|
let lastNewlineIndex = this.buf.lastIndexOf("\n");
|
|
if (lastNewlineIndex >= 0 && this.get().length <= lastNewlineIndex) {
|
|
let toRemove = this.buf.slice(lastNewlineIndex + 1);
|
|
this.buf = this.buf.substring(0, lastNewlineIndex + 1);
|
|
this.last = "\n";
|
|
this.position.unshift(toRemove);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets a given position as the current source location so generated code after this call
|
|
* will be given this position in the sourcemap.
|
|
*/
|
|
|
|
source(prop: string, loc: Location) {
|
|
if (prop && !loc) return;
|
|
|
|
let pos = loc ? loc[prop] : null;
|
|
|
|
this._sourcePosition.line = pos ? pos.line : null;
|
|
this._sourcePosition.column = pos ? pos.column : null;
|
|
this._sourcePosition.filename = loc && loc.filename || null;
|
|
}
|
|
|
|
/**
|
|
* Call a callback with a specific source location and restore on completion.
|
|
*/
|
|
|
|
withSource(prop: string, loc: Location, cb: () => void) {
|
|
if (!this.opts.sourceMaps) return cb();
|
|
|
|
// Use the call stack to manage a stack of "source location" data.
|
|
let originalLine = this._sourcePosition.line;
|
|
let originalColumn = this._sourcePosition.column;
|
|
let originalFilename = this._sourcePosition.filename;
|
|
|
|
this.source(prop, loc);
|
|
|
|
cb();
|
|
|
|
this._sourcePosition.line = originalLine;
|
|
this._sourcePosition.column = originalColumn;
|
|
this._sourcePosition.filename = originalFilename;
|
|
}
|
|
|
|
/**
|
|
* Push a string to the buffer, maintaining indentation and newlines.
|
|
*/
|
|
|
|
push(str: string) {
|
|
if (!this.format.compact && this._indent && str[0] !== "\n") {
|
|
// we've got a newline before us so prepend on the indentation
|
|
if (this.endsWith("\n")) str = this.getIndent() + 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
|
|
str = "(" + str;
|
|
this.indent();
|
|
parenPushNewlineState.printed = true;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If there the line is ending, adding a new mapping marker is redundant
|
|
if (this.opts.sourceMaps && str[0] !== "\n") this.map.mark(this._sourcePosition);
|
|
|
|
//
|
|
this.position.push(str);
|
|
this.buf += str;
|
|
this.last = str[str.length - 1];
|
|
}
|
|
|
|
/**
|
|
* Test if the buffer ends with a string.
|
|
*/
|
|
|
|
endsWith(str: string): boolean {
|
|
if (Array.isArray(str)) return str.some((s) => this.endsWith(s));
|
|
|
|
if (str.length === 1) {
|
|
return this.last === str;
|
|
} else {
|
|
return this.buf.slice(-str.length) === str;
|
|
}
|
|
}
|
|
|
|
}
|