feat: Enhanced developer tools UX with visual improvements
- Fixed StructuredEditor auto-type detection to only trigger on empty fields - Made array keys readonly with proper index display and full-width value inputs - Enhanced PHP unserialize to handle empty strings, NULL values, and complex data - Added JSON to CSV support for single objects as Key-Value format - Upgraded DiffTool with react-diff-view for professional GitHub-style diffs - Added theme-synchronized diff colors with proper contrast in light/dark modes - Implemented red/green text on matching backgrounds for optimal readability
This commit is contained in:
45
package-lock.json
generated
45
package-lock.json
generated
@@ -25,6 +25,7 @@
|
||||
"papaparse": "^5.4.1",
|
||||
"react": "^18.2.0",
|
||||
"react-codemirror2": "^8.0.1",
|
||||
"react-diff-view": "^3.3.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-scripts": "5.0.1",
|
||||
@@ -6888,6 +6889,12 @@
|
||||
"integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/classnames": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
|
||||
"integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/clean-css": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
|
||||
@@ -10162,6 +10169,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/gitdiff-parser": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/gitdiff-parser/-/gitdiff-parser-0.3.1.tgz",
|
||||
"integrity": "sha512-YQJnY8aew65id8okGxKCksH3efDCJ9HzV7M9rsvd65habf39Pkh4cgYJ27AaoDMqo1X98pgNJhNMrm/kpV7UVQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "10.4.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
|
||||
@@ -17508,6 +17521,23 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-diff-view": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/react-diff-view/-/react-diff-view-3.3.2.tgz",
|
||||
"integrity": "sha512-wPVq4ktTcGOHbhnWKU/gHLtd3N2Xd+OZ/XQWcKA06dsxlSsESePAumQILwHtiak2nMCMiWcIfBpqZ5OiharUPA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"classnames": "^2.3.2",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"gitdiff-parser": "^0.3.1",
|
||||
"lodash": "^4.17.21",
|
||||
"shallow-equal": "^3.1.0",
|
||||
"warning": "^4.0.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
@@ -18624,6 +18654,12 @@
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/shallow-equal": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-3.1.0.tgz",
|
||||
"integrity": "sha512-pfVOw8QZIXpMbhBWvzBISicvToTiM5WBF1EeAUZDDSb5Dt29yl4AYbyywbJFSEsRUMr7gJaxqCdr4L3tQf9wVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
@@ -20429,6 +20465,15 @@
|
||||
"makeerror": "1.0.12"
|
||||
}
|
||||
},
|
||||
"node_modules/warning": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
|
||||
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/watchpack": {
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"papaparse": "^5.4.1",
|
||||
"react": "^18.2.0",
|
||||
"react-codemirror2": "^8.0.1",
|
||||
"react-diff-view": "^3.3.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-scripts": "5.0.1",
|
||||
|
||||
@@ -78,15 +78,21 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
||||
}
|
||||
|
||||
const key = pathParts[pathParts.length - 1];
|
||||
const currentValue = current[key];
|
||||
|
||||
// Auto-detect type
|
||||
if (value === 'true' || value === 'false') {
|
||||
current[key] = value === 'true';
|
||||
} else if (value === 'null') {
|
||||
current[key] = null;
|
||||
} else if (!isNaN(value) && value !== '') {
|
||||
current[key] = Number(value);
|
||||
// Auto-detect type only when current value is empty/null/undefined
|
||||
if (currentValue === '' || currentValue === null || currentValue === undefined) {
|
||||
if (value === 'true' || value === 'false') {
|
||||
current[key] = value === 'true';
|
||||
} else if (value === 'null') {
|
||||
current[key] = null;
|
||||
} else if (!isNaN(value) && value !== '') {
|
||||
current[key] = Number(value);
|
||||
} else {
|
||||
current[key] = value;
|
||||
}
|
||||
} else {
|
||||
// If current value exists, preserve as string unless explicitly changed via type dropdown
|
||||
current[key] = value;
|
||||
}
|
||||
|
||||
@@ -171,8 +177,18 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
||||
const isExpanded = expandedNodes.has(path);
|
||||
const canExpand = typeof value === 'object' && value !== null;
|
||||
|
||||
// Check if parent is an array by looking at the parent path
|
||||
const isArrayItem = parentPath !== 'root' && (() => {
|
||||
const parentPathParts = parentPath.split('.');
|
||||
let current = data;
|
||||
for (let i = 1; i < parentPathParts.length; i++) {
|
||||
current = current[parentPathParts[i]];
|
||||
}
|
||||
return Array.isArray(current);
|
||||
})();
|
||||
|
||||
return (
|
||||
<div key={path} className="ml-4 border-l border-gray-200 dark:border-gray-700 pl-4 mb-2 overflow-hidden">
|
||||
<div key={path} className="ml-4 border-l border-gray-200 dark:border-gray-700 pl-4 overflow-hidden">
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
{canExpand && (
|
||||
<button
|
||||
@@ -189,30 +205,40 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
||||
|
||||
{!canExpand && <div className="w-6" />}
|
||||
|
||||
<div className="flex flex-col sm:flex-row sm:items-center space-y-2 sm:space-y-0 sm:space-x-2 flex-1">
|
||||
<div className="flex items-center space-x-2 flex-1">
|
||||
{getTypeIcon(value)}
|
||||
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={key}
|
||||
onBlur={(e) => {
|
||||
const newKey = e.target.value.trim();
|
||||
if (newKey && newKey !== key) {
|
||||
renameKey(key, newKey, path);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.target.blur(); // Trigger blur to save changes
|
||||
}
|
||||
}}
|
||||
className="px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 min-w-0 flex-1"
|
||||
placeholder="Property name"
|
||||
/>
|
||||
|
||||
<span className="text-gray-500 hidden sm:inline">:</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 flex-1">
|
||||
{isArrayItem ? (
|
||||
// Array items: icon + index span (compact)
|
||||
<>
|
||||
{getTypeIcon(value)}
|
||||
<span className="px-2 py-1 text-sm text-gray-600 dark:text-gray-400 font-mono whitespace-nowrap">
|
||||
[{key}]
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
// Object properties: icon + editable key + colon (compact)
|
||||
<>
|
||||
{getTypeIcon(value)}
|
||||
<input
|
||||
type="text"
|
||||
defaultValue={key}
|
||||
onBlur={(e) => {
|
||||
const newKey = e.target.value.trim();
|
||||
if (newKey && newKey !== key) {
|
||||
renameKey(key, newKey, path);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.target.blur(); // Trigger blur to save changes
|
||||
}
|
||||
}}
|
||||
className="px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500 min-w-0"
|
||||
placeholder="Property name"
|
||||
style={{width: '120px'}} // Fixed width for consistency
|
||||
/>
|
||||
<span className="text-gray-500 hidden sm:inline">:</span>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!canExpand ? (
|
||||
<input
|
||||
@@ -223,11 +249,11 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
||||
value.toString()
|
||||
}
|
||||
onChange={(e) => updateValue(e.target.value, path)}
|
||||
className="flex-1 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
|
||||
className="flex-1 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 min-w-0"
|
||||
placeholder="Value"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
||||
<span className="flex-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
{Array.isArray(value) ? `Array (${value.length} items)` : `Object (${Object.keys(value).length} properties)`}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -45,38 +45,77 @@ const CsvJsonTool = () => {
|
||||
try {
|
||||
const data = JSON.parse(input);
|
||||
|
||||
if (!Array.isArray(data)) {
|
||||
setOutput('Error: JSON must be an array of objects');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
setOutput('Error: Empty array');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get headers from first object
|
||||
const headers = Object.keys(data[0]);
|
||||
|
||||
let csv = '';
|
||||
|
||||
// Add headers if enabled
|
||||
if (hasHeaders) {
|
||||
csv += headers.join(delimiter) + '\n';
|
||||
}
|
||||
if (Array.isArray(data)) {
|
||||
// Handle array of objects (original functionality)
|
||||
if (data.length === 0) {
|
||||
setOutput('Error: Empty array');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add data rows
|
||||
data.forEach(row => {
|
||||
const values = headers.map(header => {
|
||||
const value = row[header] || '';
|
||||
// Escape values containing delimiter or quotes
|
||||
if (typeof value === 'string' && (value.includes(delimiter) || value.includes('"') || value.includes('\n'))) {
|
||||
return `"${value.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return value;
|
||||
// Get headers from first object
|
||||
const headers = Object.keys(data[0]);
|
||||
|
||||
// Add headers if enabled
|
||||
if (hasHeaders) {
|
||||
csv += headers.join(delimiter) + '\n';
|
||||
}
|
||||
|
||||
// Add data rows
|
||||
data.forEach(row => {
|
||||
const values = headers.map(header => {
|
||||
const value = row[header] || '';
|
||||
// Escape values containing delimiter or quotes
|
||||
if (typeof value === 'string' && (value.includes(delimiter) || value.includes('"') || value.includes('\n'))) {
|
||||
return `"${value.replace(/"/g, '""')}"`;
|
||||
}
|
||||
return value;
|
||||
});
|
||||
csv += values.join(delimiter) + '\n';
|
||||
});
|
||||
csv += values.join(delimiter) + '\n';
|
||||
});
|
||||
|
||||
} else if (typeof data === 'object' && data !== null) {
|
||||
// Handle single object as key-value pairs
|
||||
|
||||
// Add headers if enabled
|
||||
if (hasHeaders) {
|
||||
csv += `Key${delimiter}Value\n`;
|
||||
}
|
||||
|
||||
// Add key-value rows
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
// Format the key
|
||||
let formattedKey = key;
|
||||
if (typeof key === 'string' && (key.includes(delimiter) || key.includes('"') || key.includes('\n'))) {
|
||||
formattedKey = `"${key.replace(/"/g, '""')}"`;
|
||||
}
|
||||
|
||||
// Format the value
|
||||
let formattedValue = '';
|
||||
if (value === null) {
|
||||
formattedValue = 'null';
|
||||
} else if (value === undefined) {
|
||||
formattedValue = 'undefined';
|
||||
} else if (typeof value === 'object') {
|
||||
// Convert objects/arrays to JSON string
|
||||
formattedValue = JSON.stringify(value);
|
||||
} else {
|
||||
formattedValue = String(value);
|
||||
}
|
||||
|
||||
// Escape value if needed
|
||||
if (typeof formattedValue === 'string' && (formattedValue.includes(delimiter) || formattedValue.includes('"') || formattedValue.includes('\n'))) {
|
||||
formattedValue = `"${formattedValue.replace(/"/g, '""')}"`;
|
||||
}
|
||||
|
||||
csv += `${formattedKey}${delimiter}${formattedValue}\n`;
|
||||
});
|
||||
|
||||
} else {
|
||||
setOutput('Error: JSON must be an object or an array of objects');
|
||||
return;
|
||||
}
|
||||
|
||||
setOutput(csv.trim());
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,70 +1,74 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { GitCompare, Upload } from 'lucide-react';
|
||||
import { parseDiff, Diff, Hunk } from 'react-diff-view';
|
||||
import 'react-diff-view/style/index.css';
|
||||
import '../styles/diff-theme.css';
|
||||
import ToolLayout from '../components/ToolLayout';
|
||||
import CopyButton from '../components/CopyButton';
|
||||
|
||||
const DiffTool = () => {
|
||||
// Enhanced diff tool with react-diff-view and theme support
|
||||
const [leftText, setLeftText] = useState('');
|
||||
const [rightText, setRightText] = useState('');
|
||||
const [diffResult, setDiffResult] = useState('');
|
||||
const [diffMode, setDiffMode] = useState('unified'); // 'unified' or 'side-by-side'
|
||||
const [diffMode, setDiffMode] = useState('unified'); // 'unified' or 'split'
|
||||
|
||||
// Generate unified diff format for react-diff-view
|
||||
const diffText = useMemo(() => {
|
||||
if (!leftText && !rightText) return null;
|
||||
|
||||
// Simple diff implementation
|
||||
const computeDiff = () => {
|
||||
const leftLines = leftText.split('\n');
|
||||
const rightLines = rightText.split('\n');
|
||||
|
||||
// Create a unified diff format
|
||||
let diff = `--- Text A\n+++ Text B\n`;
|
||||
|
||||
const maxLines = Math.max(leftLines.length, rightLines.length);
|
||||
let result = '';
|
||||
let diffCount = 0;
|
||||
let hunkStart = 1;
|
||||
let hunkLines = [];
|
||||
|
||||
if (diffMode === 'unified') {
|
||||
result += `--- Text A\n+++ Text B\n`;
|
||||
for (let i = 0; i < maxLines; i++) {
|
||||
const leftLine = leftLines[i];
|
||||
const rightLine = rightLines[i];
|
||||
|
||||
for (let i = 0; i < maxLines; i++) {
|
||||
const leftLine = leftLines[i] || '';
|
||||
const rightLine = rightLines[i] || '';
|
||||
|
||||
if (leftLine !== rightLine) {
|
||||
diffCount++;
|
||||
if (leftLine && !rightLine) {
|
||||
result += `- ${leftLine}\n`;
|
||||
} else if (!leftLine && rightLine) {
|
||||
result += `+ ${rightLine}\n`;
|
||||
} else {
|
||||
result += `- ${leftLine}\n+ ${rightLine}\n`;
|
||||
}
|
||||
} else {
|
||||
result += ` ${leftLine}\n`;
|
||||
if (leftLine === rightLine) {
|
||||
// Unchanged line
|
||||
if (leftLine !== undefined) {
|
||||
hunkLines.push(` ${leftLine}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Side by side format
|
||||
result += `${'Text A'.padEnd(50)} | Text B\n`;
|
||||
result += `${'-'.repeat(50)} | ${'-'.repeat(50)}\n`;
|
||||
|
||||
for (let i = 0; i < maxLines; i++) {
|
||||
const leftLine = leftLines[i] || '';
|
||||
const rightLine = rightLines[i] || '';
|
||||
|
||||
if (leftLine !== rightLine) {
|
||||
diffCount++;
|
||||
const leftDisplay = leftLine.padEnd(50);
|
||||
result += `${leftDisplay} | ${rightLine}\n`;
|
||||
} else {
|
||||
const leftDisplay = leftLine.padEnd(50);
|
||||
result += `${leftDisplay} | ${rightLine}\n`;
|
||||
} else {
|
||||
// Changed line
|
||||
if (leftLine !== undefined) {
|
||||
hunkLines.push(`-${leftLine}`);
|
||||
}
|
||||
if (rightLine !== undefined) {
|
||||
hunkLines.push(`+${rightLine}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (diffCount === 0) {
|
||||
result = '✅ No differences found - texts are identical!';
|
||||
} else {
|
||||
result = `Found ${diffCount} difference(s):\n\n${result}`;
|
||||
if (hunkLines.length > 0) {
|
||||
diff += `@@ -${hunkStart},${leftLines.length} +${hunkStart},${rightLines.length} @@\n`;
|
||||
diff += hunkLines.join('\n');
|
||||
}
|
||||
|
||||
setDiffResult(result);
|
||||
return diff;
|
||||
}, [leftText, rightText]);
|
||||
|
||||
// Parse the diff for react-diff-view
|
||||
const parsedDiff = useMemo(() => {
|
||||
if (!diffText) return null;
|
||||
try {
|
||||
const files = parseDiff(diffText);
|
||||
return files[0]; // We only have one file diff
|
||||
} catch (error) {
|
||||
console.error('Error parsing diff:', error);
|
||||
return null;
|
||||
}
|
||||
}, [diffText]);
|
||||
|
||||
const computeDiff = () => {
|
||||
// This function is now just a trigger since diff is computed in useMemo
|
||||
// The actual diff computation happens automatically when leftText or rightText changes
|
||||
};
|
||||
|
||||
const handleFileUpload = (side, event) => {
|
||||
@@ -85,7 +89,6 @@ const DiffTool = () => {
|
||||
const clearAll = () => {
|
||||
setLeftText('');
|
||||
setRightText('');
|
||||
setDiffResult('');
|
||||
};
|
||||
|
||||
const loadSample = () => {
|
||||
@@ -144,9 +147,9 @@ const user = {
|
||||
Unified Diff
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setDiffMode('side-by-side')}
|
||||
onClick={() => setDiffMode('split')}
|
||||
className={`px-4 py-2 rounded-md font-medium transition-colors ${
|
||||
diffMode === 'side-by-side'
|
||||
diffMode === 'split'
|
||||
? 'bg-white dark:bg-gray-700 text-primary-600 shadow-sm'
|
||||
: 'text-gray-600 dark:text-gray-400'
|
||||
}`}
|
||||
@@ -234,13 +237,37 @@ const user = {
|
||||
Comparison Result
|
||||
</label>
|
||||
<div className="relative">
|
||||
<textarea
|
||||
value={diffResult}
|
||||
readOnly
|
||||
placeholder="Comparison result will appear here..."
|
||||
className="tool-input h-96 bg-gray-50 dark:bg-gray-800"
|
||||
/>
|
||||
{diffResult && <CopyButton text={diffResult} />}
|
||||
{parsedDiff && parsedDiff.hunks && parsedDiff.hunks.length > 0 ? (
|
||||
<div className="border border-gray-300 dark:border-gray-600 rounded-lg overflow-hidden bg-white dark:bg-gray-800 relative">
|
||||
<div className="diff-container max-h-96 overflow-auto">
|
||||
<Diff
|
||||
viewType={diffMode}
|
||||
diffType="modify"
|
||||
hunks={parsedDiff.hunks}
|
||||
renderHunk={(hunk) => (
|
||||
<Hunk key={hunk.content} hunk={hunk} />
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{diffText && (
|
||||
<div className="absolute top-2 right-2">
|
||||
<CopyButton text={diffText} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : leftText || rightText ? (
|
||||
<div className="flex items-center justify-center h-32 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
|
||||
<div className="text-center">
|
||||
<div className="text-green-600 dark:text-green-400 text-lg mb-1">✅</div>
|
||||
<p className="text-green-700 dark:text-green-300 font-medium">No differences found</p>
|
||||
<p className="text-green-600 dark:text-green-400 text-sm">The texts are identical!</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-32 bg-gray-50 dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-lg">
|
||||
<p className="text-gray-500 dark:text-gray-400">Enter text in both fields to see the comparison</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -47,49 +47,107 @@ const SerializeTool = () => {
|
||||
let index = 0;
|
||||
|
||||
const parseValue = () => {
|
||||
if (index >= str.length) {
|
||||
throw new Error('Unexpected end of string');
|
||||
}
|
||||
|
||||
const type = str[index];
|
||||
|
||||
// Handle NULL case (no colon after N)
|
||||
if (type === 'N') {
|
||||
index += 2; // Skip 'N;'
|
||||
return null;
|
||||
}
|
||||
|
||||
// For all other types, expect colon after type
|
||||
if (str[index + 1] !== ':') {
|
||||
throw new Error(`Expected ':' after type '${type}' at position ${index + 1}`);
|
||||
}
|
||||
|
||||
index += 2; // Skip type and ':'
|
||||
|
||||
switch (type) {
|
||||
case 'N':
|
||||
index++; // Skip ';'
|
||||
return null;
|
||||
case 'b':
|
||||
const boolVal = str[index] === '1';
|
||||
index += 2; // Skip value and ';'
|
||||
return boolVal;
|
||||
|
||||
case 'i':
|
||||
let intStr = '';
|
||||
while (str[index] !== ';') {
|
||||
while (index < str.length && str[index] !== ';') {
|
||||
intStr += str[index++];
|
||||
}
|
||||
if (index >= str.length) {
|
||||
throw new Error('Unexpected end of string while parsing integer');
|
||||
}
|
||||
index++; // Skip ';'
|
||||
return parseInt(intStr);
|
||||
|
||||
case 'd':
|
||||
let floatStr = '';
|
||||
while (str[index] !== ';') {
|
||||
while (index < str.length && str[index] !== ';') {
|
||||
floatStr += str[index++];
|
||||
}
|
||||
if (index >= str.length) {
|
||||
throw new Error('Unexpected end of string while parsing float');
|
||||
}
|
||||
index++; // Skip ';'
|
||||
return parseFloat(floatStr);
|
||||
|
||||
case 's':
|
||||
let lenStr = '';
|
||||
while (str[index] !== ':') {
|
||||
while (index < str.length && str[index] !== ':') {
|
||||
lenStr += str[index++];
|
||||
}
|
||||
if (index >= str.length) {
|
||||
throw new Error('Unexpected end of string while parsing string length');
|
||||
}
|
||||
index++; // Skip ':'
|
||||
index++; // Skip '"'
|
||||
|
||||
// Expect opening quote
|
||||
if (str[index] !== '"') {
|
||||
throw new Error(`Expected '"' at position ${index}`);
|
||||
}
|
||||
index++; // Skip opening '"'
|
||||
|
||||
const length = parseInt(lenStr);
|
||||
const stringVal = str.substr(index, length);
|
||||
index += length + 2; // Skip string and '";'
|
||||
if (isNaN(length) || length < 0) {
|
||||
throw new Error(`Invalid string length: ${lenStr}`);
|
||||
}
|
||||
|
||||
// Extract string content
|
||||
const stringVal = str.substring(index, index + length);
|
||||
index += length;
|
||||
|
||||
// Expect closing quote and semicolon
|
||||
if (index + 1 >= str.length || str[index] !== '"' || str[index + 1] !== ';') {
|
||||
throw new Error(`Expected '";' after string at position ${index}`);
|
||||
}
|
||||
index += 2; // Skip closing '";'
|
||||
|
||||
return stringVal;
|
||||
|
||||
case 'a':
|
||||
let arrayLenStr = '';
|
||||
while (str[index] !== ':') {
|
||||
while (index < str.length && str[index] !== ':') {
|
||||
arrayLenStr += str[index++];
|
||||
}
|
||||
index += 2; // Skip ':{'
|
||||
if (index >= str.length) {
|
||||
throw new Error('Unexpected end of string while parsing array length');
|
||||
}
|
||||
index++; // Skip ':'
|
||||
|
||||
// Expect opening brace
|
||||
if (str[index] !== '{') {
|
||||
throw new Error(`Expected '{' at position ${index}`);
|
||||
}
|
||||
index++; // Skip '{'
|
||||
|
||||
const arrayLength = parseInt(arrayLenStr);
|
||||
if (isNaN(arrayLength) || arrayLength < 0) {
|
||||
throw new Error(`Invalid array length: ${arrayLenStr}`);
|
||||
}
|
||||
|
||||
const result = {};
|
||||
let isArray = true;
|
||||
|
||||
@@ -97,13 +155,20 @@ const SerializeTool = () => {
|
||||
const key = parseValue();
|
||||
const value = parseValue();
|
||||
result[key] = value;
|
||||
|
||||
// Check if this looks like a sequential array
|
||||
if (typeof key !== 'number' || key !== i) {
|
||||
isArray = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Expect closing brace
|
||||
if (index >= str.length || str[index] !== '}') {
|
||||
throw new Error(`Expected '}' at position ${index}`);
|
||||
}
|
||||
index++; // Skip '}'
|
||||
|
||||
// Convert to array if all keys are sequential integers
|
||||
// Convert to array if all keys are sequential integers starting from 0
|
||||
if (isArray && arrayLength > 0) {
|
||||
const arr = [];
|
||||
for (let i = 0; i < arrayLength; i++) {
|
||||
@@ -113,12 +178,24 @@ const SerializeTool = () => {
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown type: ${type}`);
|
||||
throw new Error(`Unknown type: '${type}' at position ${index - 2}`);
|
||||
}
|
||||
};
|
||||
|
||||
return parseValue();
|
||||
try {
|
||||
const result = parseValue();
|
||||
|
||||
// Check if there's unexpected trailing data
|
||||
if (index < str.length) {
|
||||
console.warn(`Warning: Trailing data after parsing: "${str.substring(index)}"`);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
throw new Error(`Parse error at position ${index}: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSerialize = () => {
|
||||
|
||||
114
src/styles/diff-theme.css
Normal file
114
src/styles/diff-theme.css
Normal file
@@ -0,0 +1,114 @@
|
||||
/* Theme-aware styles for react-diff-view */
|
||||
/* Using higher specificity selectors to override library defaults */
|
||||
|
||||
/* Light theme (default) */
|
||||
.diff-container {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Target actual react-diff-view classes with high specificity */
|
||||
.diff-container .diff-code-insert,
|
||||
.diff-container .diff-line-insert,
|
||||
.diff-container .diff-code-insert .diff-code-text,
|
||||
.diff-container .diff-line-insert .diff-code-text {
|
||||
background-color: #dcfce7 !important;
|
||||
color: #15803d !important;
|
||||
}
|
||||
|
||||
.diff-container .diff-code-delete,
|
||||
.diff-container .diff-line-delete,
|
||||
.diff-container .diff-code-delete .diff-code-text,
|
||||
.diff-container .diff-line-delete .diff-code-text {
|
||||
background-color: #fee2e2 !important;
|
||||
color: #dc2626 !important;
|
||||
}
|
||||
|
||||
.diff-container .diff-code-normal,
|
||||
.diff-container .diff-line-normal,
|
||||
.diff-container .diff-code-normal .diff-code-text,
|
||||
.diff-container .diff-line-normal .diff-code-text {
|
||||
background-color: #ffffff !important;
|
||||
color: #1f2937 !important;
|
||||
}
|
||||
|
||||
.diff-container .diff-gutter,
|
||||
.diff-container .diff-gutter-normal,
|
||||
.diff-container .diff-gutter-insert,
|
||||
.diff-container .diff-gutter-delete {
|
||||
background-color: #f9fafb !important;
|
||||
color: #6b7280 !important;
|
||||
border-color: #e5e7eb !important;
|
||||
}
|
||||
|
||||
/* Dark theme with higher specificity */
|
||||
.dark .diff-container .diff-code-insert,
|
||||
.dark .diff-container .diff-line-insert,
|
||||
.dark .diff-container .diff-code-insert .diff-code-text,
|
||||
.dark .diff-container .diff-line-insert .diff-code-text {
|
||||
background-color: #064e3b !important;
|
||||
color: #10b981 !important;
|
||||
}
|
||||
|
||||
.dark .diff-container .diff-code-delete,
|
||||
.dark .diff-container .diff-line-delete,
|
||||
.dark .diff-container .diff-code-delete .diff-code-text,
|
||||
.dark .diff-container .diff-line-delete .diff-code-text {
|
||||
background-color: #7f1d1d !important;
|
||||
color: #f87171 !important;
|
||||
}
|
||||
|
||||
.dark .diff-container .diff-code-normal,
|
||||
.dark .diff-container .diff-line-normal,
|
||||
.dark .diff-container .diff-code-normal .diff-code-text,
|
||||
.dark .diff-container .diff-line-normal .diff-code-text {
|
||||
background-color: #1f2937 !important;
|
||||
color: #f9fafb !important;
|
||||
}
|
||||
|
||||
.dark .diff-container .diff-gutter,
|
||||
.dark .diff-container .diff-gutter-normal,
|
||||
.dark .diff-container .diff-gutter-insert,
|
||||
.dark .diff-container .diff-gutter-delete {
|
||||
background-color: #374151 !important;
|
||||
color: #9ca3af !important;
|
||||
border-color: #4b5563 !important;
|
||||
}
|
||||
|
||||
/* Additional styling for better appearance */
|
||||
.diff-container .diff-hunk-header {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
font-weight: 600;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.dark .diff-container .diff-hunk-header {
|
||||
background: #374151;
|
||||
color: #d1d5db;
|
||||
border-bottom: 1px solid #4b5563;
|
||||
}
|
||||
|
||||
/* Ensure proper line spacing and alignment */
|
||||
.diff-container .diff-line {
|
||||
padding: 2px 8px;
|
||||
border-left: 4px solid transparent;
|
||||
}
|
||||
|
||||
.diff-container .diff-line-add {
|
||||
border-left-color: #22c55e;
|
||||
}
|
||||
|
||||
.diff-container .diff-line-delete {
|
||||
border-left-color: #ef4444;
|
||||
}
|
||||
|
||||
.dark .diff-container .diff-line-add {
|
||||
border-left-color: #16a34a;
|
||||
}
|
||||
|
||||
.dark .diff-container .diff-line-delete {
|
||||
border-left-color: #dc2626;
|
||||
}
|
||||
Reference in New Issue
Block a user