@hokaccha/sql-formatter
Version:
Format whitespace in a SQL query to make it more readable
234 lines • 9.07 kB
JavaScript
import { trimSpacesEnd } from "../utils";
import Indentation from "./Indentation";
import InlineBlock from "./InlineBlock";
import Params from "./Params";
import { isAnd, isBetween, isLimit } from "./token";
import { tokenTypes } from "./tokenTypes";
export default class Formatter {
constructor(config) {
this.config = config;
this.indentation = new Indentation(this.config.indent);
this.inlineBlock = new InlineBlock();
this.params = new Params(this.config.params);
this.previousReservedToken = null;
this.tokens = [];
this.index = 0;
}
/**
* SQL Tokenizer for this formatter, provided by subclasses.
*/
tokenizer() {
throw new Error("tokenizer() not implemented by subclass");
}
/**
* Reprocess and modify a token based on parsed context.
*
* @param {Object} token The token to modify
* @param {String} token.type
* @param {String} token.value
* @return {Object} new token or the original
* @return {String} token.type
* @return {String} token.value
*/
tokenOverride(token) {
// subclasses can override this to modify tokens during formatting
return token;
}
/**
* Formats whitespace in a SQL string to make it easier to read.
*
* @param {String} query The SQL query string
* @return {String} formatted query
*/
format(query) {
this.tokens = this.tokenizer().tokenize(query);
const formattedQuery = this.getFormattedQueryFromTokens();
return formattedQuery.trim();
}
getFormattedQueryFromTokens() {
let formattedQuery = "";
this.tokens.forEach((token, index) => {
this.index = index;
token = this.tokenOverride(token);
if (token.type === tokenTypes.LINE_COMMENT) {
formattedQuery = this.formatLineComment(token, formattedQuery);
}
else if (token.type === tokenTypes.BLOCK_COMMENT) {
formattedQuery = this.formatBlockComment(token, formattedQuery);
}
else if (token.type === tokenTypes.RESERVED_TOP_LEVEL) {
formattedQuery = this.formatTopLevelReservedWord(token, formattedQuery);
this.previousReservedToken = token;
}
else if (token.type === tokenTypes.RESERVED_TOP_LEVEL_NO_INDENT) {
formattedQuery = this.formatTopLevelReservedWordNoIndent(token, formattedQuery);
this.previousReservedToken = token;
}
else if (token.type === tokenTypes.RESERVED_NEWLINE) {
formattedQuery = this.formatNewlineReservedWord(token, formattedQuery);
this.previousReservedToken = token;
}
else if (token.type === tokenTypes.RESERVED) {
formattedQuery = this.formatWithSpaces(token, formattedQuery);
this.previousReservedToken = token;
}
else if (token.type === tokenTypes.OPEN_PAREN) {
formattedQuery = this.formatOpeningParentheses(token, formattedQuery);
}
else if (token.type === tokenTypes.CLOSE_PAREN) {
formattedQuery = this.formatClosingParentheses(token, formattedQuery);
}
else if (token.type === tokenTypes.PLACEHOLDER) {
formattedQuery = this.formatPlaceholder(token, formattedQuery);
}
else if (token.value === ",") {
formattedQuery = this.formatComma(token, formattedQuery);
}
else if (token.value === ":") {
formattedQuery = this.formatWithSpaceAfter(token, formattedQuery);
}
else if (token.value === ".") {
formattedQuery = this.formatWithoutSpaces(token, formattedQuery);
}
else if (token.value === ";") {
formattedQuery = this.formatQuerySeparator(token, formattedQuery);
}
else {
formattedQuery = this.formatWithSpaces(token, formattedQuery);
}
});
return formattedQuery;
}
formatLineComment(token, query) {
return this.addNewline(query + this.show(token));
}
formatBlockComment(token, query) {
return this.addNewline(this.addNewline(query) + this.indentComment(token.value));
}
indentComment(comment) {
return comment.replace(/\n[ \t]*/gu, "\n" + this.indentation.getIndent() + " ");
}
formatTopLevelReservedWordNoIndent(token, query) {
this.indentation.decreaseTopLevel();
query = this.addNewline(query) + this.equalizeWhitespace(this.show(token));
return this.addNewline(query);
}
formatTopLevelReservedWord(token, query) {
this.indentation.decreaseTopLevel();
query = this.addNewline(query);
this.indentation.increaseTopLevel();
query += this.equalizeWhitespace(this.show(token));
return this.addNewline(query);
}
formatNewlineReservedWord(token, query) {
if (isAnd(token) && isBetween(this.tokenLookBehind(2))) {
return this.formatWithSpaces(token, query);
}
return (this.addNewline(query) + this.equalizeWhitespace(this.show(token)) + " ");
}
// Replace any sequence of whitespace characters with single space
equalizeWhitespace(str) {
return str.replace(/\s+/gu, " ");
}
// Opening parentheses increase the block indent level and start a new line
formatOpeningParentheses(token, query) {
// Take out the preceding space unless there was whitespace there in the original query
// or another opening parens or line comment
const preserveWhitespaceFor = {
[tokenTypes.OPEN_PAREN]: true,
[tokenTypes.LINE_COMMENT]: true,
[tokenTypes.OPERATOR]: true,
};
if (token.whitespaceBefore.length === 0 &&
!preserveWhitespaceFor[this.tokenLookBehind()?.type]) {
query = trimSpacesEnd(query);
}
query += this.show(token);
this.inlineBlock.beginIfPossible(this.tokens, this.index);
if (!this.inlineBlock.isActive()) {
this.indentation.increaseBlockLevel();
query = this.addNewline(query);
}
return query;
}
// Closing parentheses decrease the block indent level
formatClosingParentheses(token, query) {
if (this.inlineBlock.isActive()) {
this.inlineBlock.end();
return this.formatWithSpaceAfter(token, query);
}
else {
this.indentation.decreaseBlockLevel();
return this.formatWithSpaces(token, this.addNewline(query));
}
}
formatPlaceholder(token, query) {
return query + this.params.get(token) + " ";
}
// Commas start a new line (unless within inline parentheses or SQL "LIMIT" clause)
formatComma(token, query) {
query = trimSpacesEnd(query) + this.show(token) + " ";
if (this.inlineBlock.isActive()) {
return query;
}
else if (this.previousReservedToken &&
isLimit(this.previousReservedToken)) {
return query;
}
else {
return this.addNewline(query);
}
}
formatWithSpaceAfter(token, query) {
return trimSpacesEnd(query) + this.show(token) + " ";
}
formatWithoutSpaces(token, query) {
return trimSpacesEnd(query) + this.show(token);
}
formatWithSpaces(token, query) {
return query + this.show(token) + " ";
}
formatQuerySeparator(token, query) {
this.indentation.resetIndentation();
return (trimSpacesEnd(query) +
this.show(token) +
"\n".repeat(this.config.linesBetweenQueries));
}
// Converts token to string
show(token) {
const { type, value } = token;
const isKeyword = type === tokenTypes.RESERVED ||
type === tokenTypes.RESERVED_TOP_LEVEL ||
type === tokenTypes.RESERVED_TOP_LEVEL_NO_INDENT ||
type === tokenTypes.RESERVED_NEWLINE ||
type === tokenTypes.OPEN_PAREN ||
type === tokenTypes.CLOSE_PAREN;
if (isKeyword === false) {
return value;
}
switch (this.config.keywordCase) {
case "lower":
return value.toLowerCase();
case "upper":
return value.toUpperCase();
case "preserve":
return value;
default:
return value;
}
}
addNewline(query) {
query = trimSpacesEnd(query);
if (!query.endsWith("\n")) {
query += "\n";
}
return query + this.indentation.getIndent();
}
tokenLookBehind(n = 1) {
return this.tokens[this.index - n];
}
tokenLookAhead(n = 1) {
return this.tokens[this.index + n];
}
}
//# sourceMappingURL=Formatter.js.map