rawsql-ts
Version:
High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.
219 lines • 7.69 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PrintLine = exports.LinePrinter = void 0;
/**
* SqlPrintHelper provides utility methods for SQL pretty printing.
*/
class LinePrinter {
/**
* @param indentChar Character used for indentation (default: ' ') // Accepts logical names like 'space'/'tab'
* @param indentSize Number of indentChar per level (default: 0)
* @param newline Newline string (default: '\r\n') // Accepts logical names like 'lf'/'crlf'/'cr'
* @param commaBreak Comma break style (default: 'none')
*/
constructor(indentChar = ' ', indentSize = 0, newline = '\r\n', commaBreak = 'none') {
this.indentChar = indentChar;
this.indentSize = indentSize;
this.newline = newline;
this.commaBreak = commaBreak;
this.lines = [];
this.appendNewline(0);
}
print() {
let result = '';
for (const line of this.lines) {
if (line.text !== '') {
// append indent and text
result += this.indent(line.level) + line.text;
}
}
return result.trimEnd();
}
/**
* Returns the indent string for a given level.
* @param level Indentation level
*/
indent(level) {
return this.indentChar.repeat(this.indentSize * level);
}
/**
* Appends a newline token to the given tokens array if newline is set, or adds an empty line if tokens is empty.
* @param tokens Array of token objects with 'level' and 'text' property
* @param level Indentation level
*/
appendNewline(level) {
if (this.lines.length > 0) {
const current = this.lines[this.lines.length - 1];
if (current.text !== '') {
const lineText = this.endsWithAsciiWhitespace(current.text) ? current.text.trimEnd() : current.text;
current.text = lineText + this.newline;
}
}
this.lines.push(new PrintLine(level, ''));
}
/**
* Appends text to the last element of tokens array.
* @param tokens Array of token objects with 'text' property
* @param text Text to append
*/
appendText(text) {
// Handle special comma cleanup only when a comma token is appended.
if (text === ',' && this.cleanupLine()) {
// If cleanup was performed, add comma to previous line
const previousLine = this.lines[this.lines.length - 1];
const lineText = this.endsWithAsciiWhitespace(previousLine.text) ? previousLine.text.trimEnd() : previousLine.text;
previousLine.text = lineText + text;
return;
}
const workLine = this.getCurrentLine();
// Leading space is not needed
if (!(text === ' ' && workLine.text === '')) {
workLine.text += text;
}
}
endsWithAsciiWhitespace(text) {
if (text.length === 0) {
return false;
}
const tail = text.charCodeAt(text.length - 1);
return tail === 32 || tail === 9 || tail === 10 || tail === 13;
}
trimTrailingWhitespaceFromPreviousLine() {
if (this.lines.length < 2) {
return;
}
const previousLine = this.lines[this.lines.length - 2];
const text = previousLine.text;
const lineEnd = this.findLineEndIndex(text);
let contentEnd = lineEnd;
// Trim only trailing spaces/tabs before a preserved newline suffix.
while (contentEnd > 0) {
const code = text.charCodeAt(contentEnd - 1);
if (code !== 32 && code !== 9) {
break;
}
contentEnd--;
}
if (contentEnd === lineEnd) {
return;
}
previousLine.text = text.slice(0, contentEnd) + text.slice(lineEnd);
}
findLineEndIndex(text) {
if (text.endsWith('\r\n')) {
return text.length - 2;
}
if (text.endsWith('\n')) {
return text.length - 1;
}
return text.length;
}
/**
* Cleans up the current line for comma formatting.
* For 'after' and 'none' comma styles, removes empty line when a comma is being added.
* @returns true if cleanup was performed, false otherwise
*/
cleanupLine() {
const workLine = this.getCurrentLine();
if (this.isAsciiWhitespaceOnly(workLine.text) && this.lines.length > 1 && (this.commaBreak === 'after' || this.commaBreak === 'none')) {
let previousIndex = this.lines.length - 2;
while (previousIndex >= 0 && this.isAsciiWhitespaceOnly(this.lines[previousIndex].text)) {
this.lines.splice(previousIndex, 1);
previousIndex--;
}
if (previousIndex < 0) {
return false;
}
const previousLine = this.lines[previousIndex];
// Avoid pulling commas onto a line comment to keep the comma executable
if (this.lineHasTrailingComment(previousLine.text)) {
return false;
}
this.lines.pop(); // Safe: we checked lines.length > 1
return true; // Cleanup performed
}
return false; // No cleanup needed
}
isAsciiWhitespaceOnly(text) {
for (let i = 0; i < text.length; i++) {
const code = text.charCodeAt(i);
if (code !== 32 && code !== 9 && code !== 10 && code !== 13) {
return false;
}
}
return true;
}
lineHasTrailingComment(text) {
if (!text.includes('--')) {
return false;
}
if (!text.includes("'") && !text.includes('"')) {
return true;
}
let quote = null;
for (let i = 0; i < text.length - 1; i++) {
const current = text.charCodeAt(i);
const next = text.charCodeAt(i + 1);
if (quote === 39) {
if (current === 39) {
if (next === 39) {
i++;
continue;
}
quote = null;
}
continue;
}
if (quote === 34) {
if (current === 34) {
if (next === 34) {
i++;
continue;
}
quote = null;
}
continue;
}
if (current === 39) {
quote = 39;
continue;
}
if (current === 34) {
quote = 34;
continue;
}
if (current === 45 && next === 45) {
return true;
}
}
return false;
}
getCurrentLine() {
if (this.lines.length > 0) {
return this.lines[this.lines.length - 1];
}
else {
throw new Error('No tokens to get current line from.');
}
}
/**
* Checks if the current line is empty (has no text content)
* @returns true if current line is empty, false otherwise
*/
isCurrentLineEmpty() {
if (this.lines.length > 0) {
const currentLine = this.lines[this.lines.length - 1];
return this.isAsciiWhitespaceOnly(currentLine.text);
}
return true;
}
}
exports.LinePrinter = LinePrinter;
class PrintLine {
constructor(level, text) {
this.level = level;
this.text = text;
}
}
exports.PrintLine = PrintLine;
//# sourceMappingURL=LinePrinter.js.map