- Fixed missing ref={previewFrameRef} on first PreviewFrame component (line 336)
* This was causing 'PreviewFrame API not available' errors during save operations
* Both fullscreen and normal mode PreviewFrame instances now have proper ref connection
- Fixed click handler attachment bug in setupInspectModeStyles function
* Click handler was being skipped when styles were already injected
* Now always attaches click handler when inspect mode is activated
* Added proper cleanup to prevent duplicate event listeners
- Fixed variable scope issues in PreviewFrame.fresh.js
* styleElement and cursorStyleElement now properly scoped for cleanup function
* Added references to existing elements when styles already present
- Removed unused variables and fixed eslint warnings
* Removed unused indentSize variable in BeautifierTool.js
* Removed unused onSave and onDomUpdate props in PreviewFrame.fresh.js
* Fixed unnecessary escape character in script tag
These fixes restore the Enhanced Option A DOM manipulation architecture:
- Inspector sidebar should now appear when clicking elements in inspect mode
- Save functionality should work without 'PreviewFrame ref not available' errors
- Live editing of element properties should work through PreviewFrame API
- Iframe refresh prevention during inspector operations maintained
449 lines
15 KiB
JavaScript
449 lines
15 KiB
JavaScript
import React, { useState } from 'react';
|
|
import { FileText } from 'lucide-react';
|
|
import ToolLayout from '../components/ToolLayout';
|
|
import CopyButton from '../components/CopyButton';
|
|
|
|
const BeautifierTool = () => {
|
|
const [input, setInput] = useState('');
|
|
const [output, setOutput] = useState('');
|
|
const [language, setLanguage] = useState('json');
|
|
const [mode, setMode] = useState('beautify'); // 'beautify' or 'minify'
|
|
|
|
const beautifyJson = (text) => {
|
|
try {
|
|
const parsed = JSON.parse(text);
|
|
return JSON.stringify(parsed, null, 2);
|
|
} catch (err) {
|
|
throw new Error(`Invalid JSON: ${err.message}`);
|
|
}
|
|
};
|
|
|
|
const minifyJson = (text) => {
|
|
try {
|
|
const parsed = JSON.parse(text);
|
|
return JSON.stringify(parsed);
|
|
} catch (err) {
|
|
throw new Error(`Invalid JSON: ${err.message}`);
|
|
}
|
|
};
|
|
|
|
const beautifyXml = (text) => {
|
|
try {
|
|
// Clean input text first
|
|
let cleanText = text.trim();
|
|
if (!cleanText) {
|
|
throw new Error('Empty XML input');
|
|
}
|
|
|
|
// Parse and validate XML
|
|
const parser = new DOMParser();
|
|
const xmlDoc = parser.parseFromString(cleanText, 'text/xml');
|
|
|
|
if (xmlDoc.getElementsByTagName('parsererror').length > 0) {
|
|
throw new Error('Invalid XML syntax');
|
|
}
|
|
|
|
// Format XML properly
|
|
const formatXmlNode = (node, depth = 0) => {
|
|
const indent = ' '.repeat(depth);
|
|
let result = '';
|
|
|
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
const tagName = node.tagName;
|
|
const attributes = Array.from(node.attributes)
|
|
.map(attr => `${attr.name}="${attr.value}"`)
|
|
.join(' ');
|
|
|
|
const openTag = `<${tagName}${attributes ? ' ' + attributes : ''}`;
|
|
|
|
if (node.childNodes.length === 0) {
|
|
result += `${indent}${openTag} />\n`;
|
|
} else {
|
|
const hasTextContent = Array.from(node.childNodes)
|
|
.some(child => child.nodeType === Node.TEXT_NODE && child.textContent.trim());
|
|
|
|
if (hasTextContent && node.childNodes.length === 1) {
|
|
// Single text node - keep on same line
|
|
result += `${indent}${openTag}>${node.textContent.trim()}</${tagName}>\n`;
|
|
} else {
|
|
// Multiple children or mixed content
|
|
result += `${indent}${openTag}>\n`;
|
|
|
|
Array.from(node.childNodes).forEach(child => {
|
|
if (child.nodeType === Node.ELEMENT_NODE) {
|
|
result += formatXmlNode(child, depth + 1);
|
|
} else if (child.nodeType === Node.TEXT_NODE) {
|
|
const text = child.textContent.trim();
|
|
if (text) {
|
|
result += `${' '.repeat(depth + 1)}${text}\n`;
|
|
}
|
|
}
|
|
});
|
|
|
|
result += `${indent}</${tagName}>\n`;
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
let formatted = '';
|
|
if (xmlDoc.documentElement) {
|
|
formatted = formatXmlNode(xmlDoc.documentElement);
|
|
}
|
|
|
|
return formatted.trim();
|
|
} catch (err) {
|
|
throw new Error(`XML formatting error: ${err.message}`);
|
|
}
|
|
};
|
|
|
|
const minifyXml = (text) => {
|
|
return text.replace(/>\s+</g, '><').replace(/\s+/g, ' ').trim();
|
|
};
|
|
|
|
const beautifyCss = (text) => {
|
|
let formatted = text
|
|
.replace(/\{/g, ' {\n ')
|
|
.replace(/\}/g, '\n}\n')
|
|
.replace(/;/g, ';\n ')
|
|
.replace(/,/g, ',\n')
|
|
.replace(/\n\s*\n/g, '\n')
|
|
.trim();
|
|
|
|
// Clean up extra spaces and indentation
|
|
const lines = formatted.split('\n');
|
|
let result = '';
|
|
let indent = 0;
|
|
|
|
lines.forEach(line => {
|
|
const trimmed = line.trim();
|
|
if (trimmed) {
|
|
if (trimmed === '}') {
|
|
indent--;
|
|
}
|
|
result += ' '.repeat(Math.max(0, indent)) + trimmed + '\n';
|
|
if (trimmed.endsWith('{')) {
|
|
indent++;
|
|
}
|
|
}
|
|
});
|
|
|
|
return result.trim();
|
|
};
|
|
|
|
const minifyCss = (text) => {
|
|
return text
|
|
.replace(/\s+/g, ' ')
|
|
.replace(/;\s*}/g, '}')
|
|
.replace(/\s*{\s*/g, '{')
|
|
.replace(/;\s*/g, ';')
|
|
.replace(/,\s*/g, ',')
|
|
.trim();
|
|
};
|
|
|
|
const beautifyHtml = (text) => {
|
|
// Declare formatted variable outside try/catch block
|
|
let formatted = '';
|
|
|
|
try {
|
|
// Clean input text first
|
|
let cleanText = text.trim();
|
|
if (!cleanText) {
|
|
throw new Error('Empty HTML input');
|
|
}
|
|
|
|
// Self-closing tags that don't need closing tags
|
|
const selfClosingTags = ['area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
|
|
|
|
let indent = 0;
|
|
const tab = ' ';
|
|
let formatted = ''; // Better HTML parsing
|
|
const tokens = cleanText.match(/<\/?[^>]+>|[^<]+/g) || [];
|
|
|
|
for (let i = 0; i < tokens.length; i++) {
|
|
const token = tokens[i].trim();
|
|
if (!token) continue;
|
|
|
|
if (token.startsWith('<')) {
|
|
// It's a tag
|
|
if (token.startsWith('</')) {
|
|
// Closing tag
|
|
indent = Math.max(0, indent - 1);
|
|
formatted += tab.repeat(indent) + token + '\n';
|
|
} else if (token.endsWith('/>')) {
|
|
// Self-closing tag
|
|
formatted += tab.repeat(indent) + token + '\n';
|
|
} else if (token.startsWith('<!')) {
|
|
// Comment or doctype
|
|
formatted += tab.repeat(indent) + token + '\n';
|
|
} else {
|
|
// Opening tag
|
|
const tagMatch = token.match(/<([^\s>]+)/);
|
|
const tagName = tagMatch ? tagMatch[1].toLowerCase() : '';
|
|
|
|
formatted += tab.repeat(indent) + token + '\n';
|
|
|
|
if (!selfClosingTags.includes(tagName)) {
|
|
indent++;
|
|
}
|
|
}
|
|
} else {
|
|
// It's text content
|
|
const textContent = token.trim();
|
|
if (textContent) {
|
|
formatted += tab.repeat(indent) + textContent + '\n';
|
|
}
|
|
}
|
|
}
|
|
|
|
return formatted.trim();
|
|
} catch (err) {
|
|
// Fallback to simple formatting if advanced parsing fails
|
|
formatted = '';
|
|
let indent = 0;
|
|
const tab = ' ';
|
|
|
|
text = text.replace(/></g, '>\n<');
|
|
const lines = text.split('\n');
|
|
|
|
lines.forEach(line => {
|
|
const trimmed = line.trim();
|
|
if (trimmed) {
|
|
if (trimmed.startsWith('</')) {
|
|
indent = Math.max(0, indent - 1);
|
|
}
|
|
formatted += tab.repeat(indent) + trimmed + '\n';
|
|
if (trimmed.startsWith('<') && !trimmed.startsWith('</') && !trimmed.endsWith('/>') && !trimmed.includes('<!')) {
|
|
indent++;
|
|
}
|
|
}
|
|
});
|
|
|
|
return formatted.trim();
|
|
}
|
|
};
|
|
|
|
const minifyHtml = (text) => {
|
|
return text
|
|
.replace(/>\s+</g, '><')
|
|
.replace(/\s+/g, ' ')
|
|
.trim();
|
|
};
|
|
|
|
const beautifySql = (text) => {
|
|
const keywords = ['SELECT', 'FROM', 'WHERE', 'JOIN', 'INNER JOIN', 'LEFT JOIN', 'RIGHT JOIN', 'ORDER BY', 'GROUP BY', 'HAVING', 'INSERT', 'UPDATE', 'DELETE', 'CREATE', 'ALTER', 'DROP'];
|
|
|
|
let formatted = text.toUpperCase();
|
|
|
|
keywords.forEach(keyword => {
|
|
const regex = new RegExp(`\\b${keyword}\\b`, 'gi');
|
|
formatted = formatted.replace(regex, `\n${keyword}`);
|
|
});
|
|
|
|
return formatted
|
|
.split('\n')
|
|
.map(line => line.trim())
|
|
.filter(line => line)
|
|
.join('\n')
|
|
.trim();
|
|
};
|
|
|
|
const minifySql = (text) => {
|
|
return text.replace(/\s+/g, ' ').trim();
|
|
};
|
|
|
|
const handleProcess = () => {
|
|
try {
|
|
let result = '';
|
|
|
|
if (mode === 'beautify') {
|
|
switch (language) {
|
|
case 'json':
|
|
result = beautifyJson(input);
|
|
break;
|
|
case 'xml':
|
|
result = beautifyXml(input);
|
|
break;
|
|
case 'css':
|
|
result = beautifyCss(input);
|
|
break;
|
|
case 'html':
|
|
result = beautifyHtml(input);
|
|
break;
|
|
case 'sql':
|
|
result = beautifySql(input);
|
|
break;
|
|
default:
|
|
result = input;
|
|
}
|
|
} else {
|
|
switch (language) {
|
|
case 'json':
|
|
result = minifyJson(input);
|
|
break;
|
|
case 'xml':
|
|
result = minifyXml(input);
|
|
break;
|
|
case 'css':
|
|
result = minifyCss(input);
|
|
break;
|
|
case 'html':
|
|
result = minifyHtml(input);
|
|
break;
|
|
case 'sql':
|
|
result = minifySql(input);
|
|
break;
|
|
default:
|
|
result = input;
|
|
}
|
|
}
|
|
|
|
setOutput(result);
|
|
} catch (err) {
|
|
setOutput(`Error: ${err.message}`);
|
|
}
|
|
};
|
|
|
|
const clearAll = () => {
|
|
setInput('');
|
|
setOutput('');
|
|
// Force re-render to ensure clean state
|
|
setTimeout(() => {
|
|
setInput('');
|
|
setOutput('');
|
|
}, 10);
|
|
};
|
|
|
|
const loadSample = () => {
|
|
// Clear first to ensure clean state
|
|
setInput('');
|
|
setOutput('');
|
|
|
|
const samples = {
|
|
json: '{"name":"John Doe","age":30,"city":"New York","address":{"street":"123 Main St","zipCode":"10001"},"hobbies":["reading","coding","traveling"],"isActive":true}',
|
|
xml: '<?xml version="1.0" encoding="UTF-8"?><root><person id="1"><name>John Doe</name><age>30</age><email>john@example.com</email><address><street>123 Main St</street><city>New York</city><zipCode>10001</zipCode></address></person><person id="2"><name>Jane Smith</name><age>25</age><email>jane@example.com</email></person></root>',
|
|
css: 'body{margin:0;padding:0;font-family:Arial,sans-serif;}h1{color:#333;font-size:24px;margin-bottom:10px;}.container{max-width:1200px;margin:0 auto;padding:20px;}.button{background-color:#007bff;color:white;padding:10px 20px;border:none;border-radius:4px;cursor:pointer;}.button:hover{background-color:#0056b3;}',
|
|
html: '<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Sample Page</title></head><body><div class="container"><header><h1>Welcome to Our Website</h1><nav><ul><li><a href="#home">Home</a></li><li><a href="#about">About</a></li><li><a href="#contact">Contact</a></li></ul></nav></header><main><section><h2>About Us</h2><p>This is a sample paragraph with <strong>bold text</strong> and <em>italic text</em>.</p><img src="image.jpg" alt="Sample Image" width="300" height="200"></section></main><footer><p>© 2024 Sample Website. All rights reserved.</p></footer></div></body></html>',
|
|
sql: 'SELECT u.id, u.name, u.email, p.title, p.created_at FROM users u INNER JOIN posts p ON u.id = p.user_id WHERE u.age > 18 AND p.status = "published" ORDER BY p.created_at DESC, u.name ASC LIMIT 10;'
|
|
};
|
|
|
|
// Set sample with slight delay to ensure clean state
|
|
setTimeout(() => {
|
|
setInput(samples[language] || '');
|
|
}, 50);
|
|
};
|
|
|
|
return (
|
|
<ToolLayout
|
|
title="Code Beautifier/Minifier"
|
|
description="Format and minify JSON, XML, SQL, CSS, and HTML code"
|
|
icon={FileText}
|
|
>
|
|
{/* Language and Mode Selection */}
|
|
<div className="flex flex-wrap gap-4 mb-6">
|
|
<div className="flex items-center space-x-2">
|
|
<label className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
Language:
|
|
</label>
|
|
<select
|
|
value={language}
|
|
onChange={(e) => setLanguage(e.target.value)}
|
|
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
|
>
|
|
<option value="json">JSON</option>
|
|
<option value="xml">XML</option>
|
|
<option value="css">CSS</option>
|
|
<option value="html">HTML</option>
|
|
<option value="sql">SQL</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div className="flex bg-gray-100 dark:bg-gray-800 rounded-lg p-1">
|
|
<button
|
|
onClick={() => setMode('beautify')}
|
|
className={`px-4 py-2 rounded-md font-medium transition-colors ${
|
|
mode === 'beautify'
|
|
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
|
|
: 'text-gray-600 dark:text-gray-400'
|
|
}`}
|
|
>
|
|
Beautify
|
|
</button>
|
|
<button
|
|
onClick={() => setMode('minify')}
|
|
className={`px-4 py-2 rounded-md font-medium transition-colors ${
|
|
mode === 'minify'
|
|
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
|
|
: 'text-gray-600 dark:text-gray-400'
|
|
}`}
|
|
>
|
|
Minify
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Controls */}
|
|
<div className="flex flex-wrap gap-3 mb-6">
|
|
<button onClick={handleProcess} className="tool-button">
|
|
{mode === 'beautify' ? `Beautify ${language.toUpperCase()}` : `Minify ${language.toUpperCase()}`}
|
|
</button>
|
|
<button onClick={loadSample} className="tool-button-secondary">
|
|
Load Sample
|
|
</button>
|
|
<button onClick={clearAll} className="tool-button-secondary">
|
|
Clear All
|
|
</button>
|
|
</div>
|
|
|
|
{/* Input/Output Grid */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
{/* Input */}
|
|
<div className="space-y-2">
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
{language.toUpperCase()} Input
|
|
</label>
|
|
<div className="relative">
|
|
<textarea
|
|
value={input}
|
|
onChange={(e) => setInput(e.target.value)}
|
|
placeholder={`Paste your ${language.toUpperCase()} code here...`}
|
|
className="tool-input h-96"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Output */}
|
|
<div className="space-y-2">
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
{mode === 'beautify' ? 'Beautified' : 'Minified'} Output
|
|
</label>
|
|
<div className="relative">
|
|
<textarea
|
|
value={output}
|
|
readOnly
|
|
placeholder={`${mode === 'beautify' ? 'Beautified' : 'Minified'} code will appear here...`}
|
|
className="tool-input h-96 bg-gray-50 dark:bg-gray-800"
|
|
/>
|
|
{output && <CopyButton text={output} />}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Usage Tips */}
|
|
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md p-4 mt-6">
|
|
<h4 className="text-blue-800 dark:text-blue-200 font-medium mb-2">Usage Tips</h4>
|
|
<ul className="text-blue-700 dark:text-blue-300 text-sm space-y-1">
|
|
<li>• Beautify adds proper indentation and formatting for readability</li>
|
|
<li>• Minify removes unnecessary whitespace to reduce file size</li>
|
|
<li>• Select the appropriate language for optimal formatting</li>
|
|
<li>• Use beautified code for development and minified for production</li>
|
|
</ul>
|
|
</div>
|
|
</ToolLayout>
|
|
);
|
|
};
|
|
|
|
export default BeautifierTool;
|