<!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;
}
.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>
</select>
<label for="indent">Indent:</label>
<select id="indent">
<option value="2">2 spaces</option>
<option value="4" selected>4 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>
// Robust SQL Formatter with proper formatting
const SQLFormatter = (function() {
'use strict';
const reservedWords = new Set([
'SELECT', 'FROM', 'WHERE', 'GROUP', 'BY', 'HAVING', 'ORDER', 'LIMIT', 'UNION', 'ALL',
'INSERT', 'INTO', 'UPDATE', 'DELETE', 'CREATE', 'TABLE', 'ALTER', 'DROP', 'VALUES',
'SET', 'AND', 'OR', 'NOT', 'IN', 'BETWEEN', 'LIKE', 'IS', 'NULL', 'JOIN', 'LEFT',
'RIGHT', 'INNER', 'OUTER', 'ON', 'AS', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END',
'EXISTS', 'DISTINCT', 'COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'ASC', 'DESC'
]);
const topLevelKeywords = new Set(['SELECT', 'FROM', 'WHERE', 'GROUP BY', 'HAVING', 'ORDER BY', 'LIMIT', 'UNION', 'INSERT', 'UPDATE', 'DELETE']);
const newlineKeywords = new Set(['AND', 'OR', 'JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'INNER JOIN', 'OUTER JOIN', 'ON']);
function tokenize(sql) {
const tokens = [];
let i = 0;
const n = sql.length;
while (i < n) {
const char = sql[i];
// Whitespace
if (/\s/.test(char)) {
let whitespace = '';
while (i < n && /\s/.test(sql[i])) {
whitespace += sql[i];
i++;
}
if (whitespace.includes('\n')) {
tokens.push({ type: 'NEWLINE', value: '\n' });
} else if (whitespace) {
tokens.push({ type: 'WHITESPACE', value: whitespace });
}
continue;
}
// Comments
if (char === '-' && sql[i + 1] === '-') {
let comment = '';
while (i < n && sql[i] !== '\n') {
comment += sql[i];
i++;
}
tokens.push({ type: 'COMMENT', value: comment });
continue;
}
if (char === '/' && sql[i + 1] === '*') {
let comment = '';
while (i < n && !(sql[i] === '*' && sql[i + 1] === '/')) {
comment += sql[i];
i++;
}
if (i < n - 1) {
comment += sql[i] + sql[i + 1];
i += 2;
}
tokens.push({ type: 'COMMENT', value: comment });
continue;
}
// Strings
if (char === "'" || char === '"' || char === '`') {
let str = char;
i++;
while (i < n && sql[i] !== char) {
if (sql[i] === '\\') {
str += sql[i];
i++;
if (i < n) {
str += sql[i];
i++;
}
} else {
str += sql[i];
i++;
}
}
if (i < n) {
str += sql[i];
i++;
}
tokens.push({ type: 'STRING', value: str });
continue;
}
// Numbers
if (/\d/.test(char)) {
let num = '';
while (i < n && /[\d.]/.test(sql[i])) {
num += sql[i];
i++;
}
tokens.push({ type: 'NUMBER', value: num });
continue;
}
// Special characters
if ('(),;.=<>!+-*/%'.includes(char)) {
tokens.push({ type: 'SYMBOL', value: char });
i++;
continue;
}
// Words and identifiers
if (/[a-zA-Z_]/.test(char)) {
let word = '';
while (i < n && /[\w]/.test(sql[i])) {
word += sql[i];
i++;
}
const upperWord = word.toUpperCase();
if (reservedWords.has(upperWord)) {
tokens.push({ type: 'KEYWORD', value: word, upper: upperWord });
} else {
tokens.push({ type: 'IDENTIFIER', value: word });
}
continue;
}
// Unknown character
tokens.push({ type: 'SYMBOL', value: char });
i++;
}
return tokens;
}
function formatSQL(sql, options = {}) {
const {
indent: indentStr = ' ',
uppercase = true
} = options;
const tokens = tokenize(sql);
let output = '';
let indentLevel = 0;
let inSelectList = false;
let needsIndent = true;
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;
// Handle indentation
if (needsIndent && token.type !== 'NEWLINE' && token.type !== 'WHITESPACE') {
output += indentStr.repeat(indentLevel);
needsIndent = false;
}
switch (token.type) {
case 'NEWLINE':
output += '\n';
needsIndent = true;
break;
case 'WHITESPACE':
// Only add space if not at start of line and not before certain symbols
if (!needsIndent && nextToken &&
![')', ',', ';', '.'].includes(nextToken.value)) {
output += ' ';
}
break;
case 'COMMENT':
output += token.value;
if (token.value.includes('\n')) {
needsIndent = true;
}
break;
case 'STRING':
case 'NUMBER':
output += token.value;
break;
case 'IDENTIFIER':
output += token.value;
break;
case 'KEYWORD':
const keyword = uppercase ? token.upper : token.value;
output += keyword;
// Handle top-level keywords
if (topLevelKeywords.has(keyword)) {
if (keyword === 'SELECT') {
inSelectList = true;
output += ' ';
} else if (keyword === 'FROM') {
inSelectList = false;
output += '\n';
needsIndent = true;
} else {
output += '\n';
needsIndent = true;
}
} else if (newlineKeywords.has(keyword)) {
output += '\n';
needsIndent = true;
} else {
output += ' ';
}
break;
case 'SYMBOL':
switch (token.value) {
case '(':
output += token.value;
if (!inSelectList) {
indentLevel++;
output += '\n';
needsIndent = true;
}
break;
case ')':
if (!inSelectList) {
indentLevel = Math.max(0, indentLevel - 1);
output += '\n' + indentStr.repeat(indentLevel) + token.value;
} else {
output += token.value;
}
break;
case ',':
output += token.value;
if (inSelectList) {
output += ' ';
} else {
output += '\n';
needsIndent = true;
}
break;
case '.':
output += token.value;
break;
case ';':
output += token.value + '\n\n';
indentLevel = 0;
needsIndent = true;
break;
default:
output += ' ' + token.value + ' ';
}
break;
}
}
return output.trim();
}
return {
format: formatSQL
};
})();
// 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,
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) {
document.execCommand('copy');
status.textContent = 'Formatted SQL copied to clipboard';
}
}
formatBtn.addEventListener('click', formatSQL);
clearBtn.addEventListener('click', clearAll);
copyBtn.addEventListener('click', copyFormatted);
inputSQL.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.key === 'Enter') {
formatSQL();
}
});
// Test SQL
inputSQL.value = `SELECT c.COLUMN, t.ANOTHER_COLUMN, COUNT(*) as total_count FROM table1 c JOIN table2 t ON c.id = t.ref_id WHERE c.status = 'ACTIVE' AND t.value > 100 GROUP BY c.COLUMN, t.ANOTHER_COLUMN ORDER BY total_count DESC`;
});
</script>
</body>
</html>
For further actions, you may consider blocking this person and/or reporting abuse
Top comments (0)