Files
dewedev/src/pages/BeautifierTool.js
dwindown e1bc8d193d Fix HTML Preview Tool critical bugs: PreviewFrame ref and click handler
- 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
2025-08-03 22:04:25 +07:00

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>&copy; 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;