<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Offline SQL Formatter</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
text-align: center;
}
.editor-container {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
@media (max-width: 768px) {
.editor-container {
flex-direction: column;
}
}
.editor-panel {
flex: 1;
}
label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #555;
}
textarea {
width: 100%;
height: 300px;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 14px;
resize: vertical;
}
.controls {
margin: 20px 0;
text-align: center;
}
button {
background-color: #007cba;
color: white;
border: none;
padding: 10px 20px;
margin: 0 5px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
button:hover {
background-color: #005a87;
}
button:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.options {
margin: 10px 0;
text-align: center;
}
select {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
margin: 0 5px;
}
.status {
text-align: center;
margin: 10px 0;
color: #666;
font-style: italic;
}
</style>
</head>
<body>
<div class="container">
<h1>Offline SQL Formatter</h1>
<div class="options">
<label for="dialect">SQL Dialect:</label>
<select id="dialect">
<option value="sql">Standard SQL</option>
<option value="mysql">MySQL</option>
<option value="postgresql">PostgreSQL</option>
<option value="mariadb">MariaDB</option>
<option value="sqlite">SQLite</option>
<option value="mssql">SQL Server</option>
<option value="db2">DB2</option>
<option value="plsql">PL/SQL</option>
<option value="n1ql">N1QL</option>
<option value="redshift">Redshift</option>
<option value="spark">Spark</option>
</select>
<label for="indent">Indent:</label>
<select id="indent">
<option value="2">2 spaces</option>
<option value="4" selected>4 spaces</option>
<option value="8">8 spaces</option>
<option value="tab">Tabs</option>
</select>
</div>
<div class="editor-container">
<div class="editor-panel">
<label for="inputSQL">Input SQL:</label>
<textarea id="inputSQL" placeholder="Enter your SQL query here..."></textarea>
</div>
<div class="editor-panel">
<label for="outputSQL">Formatted SQL:</label>
<textarea id="outputSQL" readonly placeholder="Formatted SQL will appear here..."></textarea>
</div>
</div>
<div class="controls">
<button id="formatBtn">Format SQL</button>
<button id="clearBtn">Clear</button>
<button id="copyBtn">Copy Formatted</button>
</div>
<div class="status" id="status">Ready</div>
</div>
<script>
// Embedded sql-formatter library (minified version)
const SQLFormatter = (function() {
'use strict';
// Token types
const TokenType = {
WHITESPACE: 'WHITESPACE',
WORD: 'WORD',
STRING: 'STRING',
RESERVED: 'RESERVED',
RESERVED_TOPLEVEL: 'RESERVED_TOPLEVEL',
RESERVED_NEWLINE: 'RESERVED_NEWLINE',
OPERATOR: 'OPERATOR',
OPEN_PAREN: 'OPEN_PAREN',
CLOSE_PAREN: 'CLOSE_PAREN',
LINE_COMMENT: 'LINE_COMMENT',
BLOCK_COMMENT: 'BLOCK_COMMENT',
NUMBER: 'NUMBER',
PLACEHOLDER: 'PLACEHOLDER'
};
// SQL reserved words
const reservedWords = [
'ACCESS', 'ADD', 'ALL', 'ALTER', 'AND', 'ANY', 'AS', 'ASC', 'AUDIT', 'BETWEEN', 'BY',
'CASE', 'CHAR', 'CHECK', 'CLUSTER', 'COLUMN', 'COLUMNS', 'COMMENT', 'COMPRESS', 'CONNECT',
'CREATE', 'CURRENT', 'DATE', 'DECIMAL', 'DEFAULT', 'DELETE', 'DESC', 'DISTINCT', 'DROP',
'ELSE', 'EXCLUSIVE', 'EXISTS', 'FILE', 'FLOAT', 'FOR', 'FROM', 'GRANT', 'GROUP', 'HAVING',
'IDENTIFIED', 'IMMEDIATE', 'IN', 'INCREMENT', 'INDEX', 'INITIAL', 'INSERT', 'INTEGER',
'INTERSECT', 'INTO', 'IS', 'JOIN', 'LEFT', 'LEVEL', 'LIKE', 'LOCK', 'LONG', 'MAXEXTENTS',
'MINUS', 'MLSLABEL', 'MODE', 'MODIFY', 'NOAUDIT', 'NOCOMPRESS', 'NOT', 'NOTFOUND', 'NOWAIT',
'NULL', 'NUMBER', 'OF', 'OFFLINE', 'ON', 'ONLINE', 'OPTION', 'OR', 'ORDER', 'PCTFREE',
'PRIOR', 'PRIVILEGES', 'PUBLIC', 'RAW', 'RENAME', 'RESOURCE', 'REVOKE', 'RIGHT', 'ROW',
'ROWID', 'ROWNUM', 'ROWS', 'SELECT', 'SESSION', 'SET', 'SHARE', 'SIZE', 'SMALLINT', 'SQL',
'START', 'SUCCESSFUL', 'SYNONYM', 'SYSDATE', 'TABLE', 'THEN', 'TO', 'TRIGGER', 'UID',
'UNION', 'UNIQUE', 'UPDATE', 'USER', 'VALIDATE', 'VALUES', 'VARCHAR', 'VARCHAR2', 'VIEW',
'WHENEVER', 'WHERE', 'WITH'
];
const reservedToplevelWords = [
'ADD', 'ALTER', 'CASE', 'CREATE', 'DELETE', 'DROP', 'FROM', 'GROUP BY', 'HAVING',
'INSERT', 'LIMIT', 'ORDER BY', 'SELECT', 'SET', 'UPDATE', 'VALUES', 'WHERE', 'WITH'
];
const reservedNewlineWords = [
'AND', 'OR', 'WHEN', 'ELSE', 'END', 'LEFT JOIN', 'RIGHT JOIN', 'INNER JOIN',
'OUTER JOIN', 'JOIN', 'XOR'
];
// Tokenizer
class Tokenizer {
tokenize(input) {
const tokens = [];
let pos = 0;
while (pos < input.length) {
let token = this.getNextToken(input, pos);
if (token) {
tokens.push(token);
pos = token.end;
} else {
pos++;
}
}
return tokens;
}
getNextToken(input, pos) {
const char = input[pos];
// Whitespace
if (/\s/.test(char)) {
return this.tokenizeWhitespace(input, pos);
}
// Comments
if (char === '-' && input[pos + 1] === '-') {
return this.tokenizeLineComment(input, pos);
}
if (char === '/' && input[pos + 1] === '*') {
return this.tokenizeBlockComment(input, pos);
}
// Strings
if (char === "'" || char === '"') {
return this.tokenizeString(input, pos, char);
}
// Numbers
if (/\d/.test(char)) {
return this.tokenizeNumber(input, pos);
}
// Parentheses
if (char === '(') {
return { type: TokenType.OPEN_PAREN, value: '(', start: pos, end: pos + 1 };
}
if (char === ')') {
return { type: TokenType.CLOSE_PAREN, value: ')', start: pos, end: pos + 1 };
}
// Operators
if (/[=<>!+\-*/%&|~^]/.test(char)) {
return this.tokenizeOperator(input, pos);
}
// Words and identifiers
if (/[a-zA-Z_$]/.test(char)) {
return this.tokenizeWord(input, pos);
}
// Placeholders (like ? or :name)
if (char === '?' || char === ':') {
return this.tokenizePlaceholder(input, pos);
}
return null;
}
tokenizeWhitespace(input, pos) {
let value = '';
while (pos < input.length && /\s/.test(input[pos])) {
value += input[pos];
pos++;
}
return { type: TokenType.WHITESPACE, value, start: pos - value.length, end: pos };
}
tokenizeLineComment(input, pos) {
let value = '';
const start = pos;
while (pos < input.length && input[pos] !== '\n') {
value += input[pos];
pos++;
}
return { type: TokenType.LINE_COMMENT, value, start, end: pos };
}
tokenizeBlockComment(input, pos) {
let value = '';
const start = pos;
value += input[pos++]; // /
value += input[pos++]; // *
while (pos < input.length && !(input[pos] === '*' && input[pos + 1] === '/')) {
value += input[pos++];
}
if (pos < input.length) {
value += input[pos++]; // *
value += input[pos++]; // /
}
return { type: TokenType.BLOCK_COMMENT, value, start, end: pos };
}
tokenizeString(input, pos, quoteChar) {
let value = quoteChar;
const start = pos;
pos++;
while (pos < input.length) {
if (input[pos] === quoteChar) {
value += quoteChar;
pos++;
break;
}
if (input[pos] === '\\') {
value += input[pos++];
if (pos < input.length) {
value += input[pos++];
}
} else {
value += input[pos++];
}
}
return { type: TokenType.STRING, value, start, end: pos };
}
tokenizeNumber(input, pos) {
let value = '';
const start = pos;
while (pos < input.length && /[\d.]/.test(input[pos])) {
value += input[pos++];
}
return { type: TokenType.NUMBER, value, start, end: pos };
}
tokenizeOperator(input, pos) {
let value = '';
const start = pos;
const operatorChars = '=<>!+-*/%&|~^';
while (pos < input.length && operatorChars.includes(input[pos])) {
value += input[pos++];
}
return { type: TokenType.OPERATOR, value, start, end: pos };
}
tokenizeWord(input, pos) {
let value = '';
const start = pos;
while (pos < input.length && /[\w$]/.test(input[pos])) {
value += input[pos++];
}
const upperValue = value.toUpperCase();
let type = TokenType.WORD;
if (reservedToplevelWords.includes(upperValue)) {
type = TokenType.RESERVED_TOPLEVEL;
} else if (reservedNewlineWords.includes(upperValue)) {
type = TokenType.RESERVED_NEWLINE;
} else if (reservedWords.includes(upperValue)) {
type = TokenType.RESERVED;
}
return { type, value, start, end: pos };
}
tokenizePlaceholder(input, pos) {
let value = input[pos++];
const start = pos - 1;
if (value === ':' && pos < input.length && /[a-zA-Z]/.test(input[pos])) {
while (pos < input.length && /[\w]/.test(input[pos])) {
value += input[pos++];
}
}
return { type: TokenType.PLACEHOLDER, value, start, end: pos };
}
}
// Formatter
class Formatter {
constructor(cfg = {}) {
this.cfg = Object.assign({
indent: ' ',
language: 'sql',
uppercase: true,
linesBetweenQueries: 2
}, cfg);
this.tokenizer = new Tokenizer();
}
format(query) {
const tokens = this.tokenizer.tokenize(query);
let formatted = '';
let indentLevel = 0;
let newline = false;
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
const prevToken = i > 0 ? tokens[i - 1] : null;
const nextToken = i < tokens.length - 1 ? tokens[i + 1] : null;
if (token.type === TokenType.WHITESPACE) {
continue;
}
if (token.type === TokenType.LINE_COMMENT || token.type === TokenType.BLOCK_COMMENT) {
if (newline) {
formatted += '\n' + this.cfg.indent.repeat(indentLevel);
}
formatted += token.value;
newline = true;
continue;
}
if (token.type === TokenType.RESERVED_TOPLEVEL) {
if (newline) {
formatted += '\n';
}
formatted += '\n' + this.cfg.indent.repeat(indentLevel);
formatted += this.cfg.uppercase ? token.value.toUpperCase() : token.value;
newline = true;
continue;
}
if (token.type === TokenType.RESERVED_NEWLINE) {
formatted += '\n' + this.cfg.indent.repeat(indentLevel);
formatted += this.cfg.uppercase ? token.value.toUpperCase() : token.value;
newline = true;
continue;
}
if (token.type === TokenType.RESERVED) {
formatted += this.cfg.uppercase ? token.value.toUpperCase() : token.value;
formatted += ' ';
newline = false;
continue;
}
if (token.type === TokenType.OPEN_PAREN) {
indentLevel++;
formatted += token.value;
newline = true;
continue;
}
if (token.type === TokenType.CLOSE_PAREN) {
indentLevel = Math.max(0, indentLevel - 1);
if (newline) {
formatted += '\n' + this.cfg.indent.repeat(indentLevel);
}
formatted += token.value;
newline = false;
continue;
}
if (token.type === TokenType.OPERATOR) {
formatted += ' ' + token.value + ' ';
newline = false;
continue;
}
if (newline) {
formatted += '\n' + this.cfg.indent.repeat(indentLevel);
newline = false;
}
formatted += token.value;
if (nextToken && nextToken.type !== TokenType.OPERATOR &&
nextToken.type !== TokenType.WHITESPACE &&
token.type !== TokenType.OPERATOR) {
formatted += ' ';
}
}
return formatted.trim();
}
}
return {
format: (sql, cfg) => new Formatter(cfg).format(sql),
TokenType,
Formatter,
Tokenizer
};
})();
// DOM handling
document.addEventListener('DOMContentLoaded', function() {
const inputSQL = document.getElementById('inputSQL');
const outputSQL = document.getElementById('outputSQL');
const formatBtn = document.getElementById('formatBtn');
const clearBtn = document.getElementById('clearBtn');
const copyBtn = document.getElementById('copyBtn');
const dialectSelect = document.getElementById('dialect');
const indentSelect = document.getElementById('indent');
const status = document.getElementById('status');
function formatSQL() {
const sql = inputSQL.value.trim();
if (!sql) {
outputSQL.value = '';
status.textContent = 'Please enter SQL to format';
return;
}
try {
const indent = indentSelect.value === 'tab' ? '\t' : ' '.repeat(parseInt(indentSelect.value));
const formatted = SQLFormatter.format(sql, {
indent: indent,
language: dialectSelect.value,
uppercase: true
});
outputSQL.value = formatted;
status.textContent = 'SQL formatted successfully';
} catch (error) {
outputSQL.value = 'Error formatting SQL: ' + error.message;
status.textContent = 'Formatting failed';
}
}
function clearAll() {
inputSQL.value = '';
outputSQL.value = '';
status.textContent = 'Cleared';
}
function copyFormatted() {
if (!outputSQL.value.trim()) {
status.textContent = 'No formatted SQL to copy';
return;
}
outputSQL.select();
outputSQL.setSelectionRange(0, 99999);
try {
navigator.clipboard.writeText(outputSQL.value);
status.textContent = 'Formatted SQL copied to clipboard';
} catch (err) {
// Fallback for older browsers
document.execCommand('copy');
status.textContent = 'Formatted SQL copied to clipboard';
}
}
// Event listeners
formatBtn.addEventListener('click', formatSQL);
clearBtn.addEventListener('click', clearAll);
copyBtn.addEventListener('click', copyFormatted);
// Auto-format on Ctrl+Enter in input
inputSQL.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.key === 'Enter') {
formatSQL();
}
});
// Sample SQL for demonstration
inputSQL.value = `SELECT customers.name,orders.total FROM customers JOIN orders ON customers.id=orders.customer_id WHERE orders.total>1000 GROUP BY customers.name HAVING COUNT(orders.id)>5 ORDER BY orders.total DESC`;
});
</script>
</body>
</html>
For further actions, you may consider blocking this person and/or reporting abuse
Top comments (0)