fix serialize and json tool to handle boolean type and number type field, and fix the nested rows in array and object type
This commit is contained in:
4981
package-lock.json
generated
4981
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
24
package.json
24
package.json
@@ -8,22 +8,23 @@
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-html": "^6.4.9",
|
||||
"@codemirror/lang-javascript": "^6.2.4",
|
||||
"@codemirror/lang-json": "^6.0.2",
|
||||
"@codemirror/search": "^6.5.11",
|
||||
"@codemirror/theme-one-dark": "^6.1.3",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^13.3.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@uiw/react-codemirror": "^4.24.2",
|
||||
"codemirror": "^5.65.19",
|
||||
"@codemirror/view": "^6.38.1",
|
||||
"@testing-library/jest-dom": "^6.8.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@uiw/react-codemirror": "^4.25.1",
|
||||
"codemirror": "^6.0.2",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"js-beautify": "^1.15.4",
|
||||
"lucide-react": "^0.263.1",
|
||||
"papaparse": "^5.4.1",
|
||||
"react": "^18.2.0",
|
||||
"react-codemirror2": "^8.0.1",
|
||||
"lucide-react": "^0.540.0",
|
||||
"papaparse": "^5.5.3",
|
||||
"react": "18.3.1",
|
||||
"react-diff-view": "^3.3.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-dom": "18.3.1",
|
||||
"react-router-dom": "6.26.2",
|
||||
"react-scripts": "5.0.1",
|
||||
"serialize-javascript": "^6.0.0",
|
||||
"web-vitals": "^2.1.4"
|
||||
@@ -31,6 +32,7 @@
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.14",
|
||||
"postcss": "^8.4.24",
|
||||
"react-scripts": "5.0.1",
|
||||
"tailwindcss": "^3.3.0"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -6,6 +6,7 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
||||
const [expandedNodes, setExpandedNodes] = useState(new Set(['root']));
|
||||
|
||||
const updateData = (newData) => {
|
||||
console.log('📊 DATA UPDATE:', { keys: Object.keys(newData), totalProps: JSON.stringify(newData).length });
|
||||
setData(newData);
|
||||
onDataChange(newData);
|
||||
};
|
||||
@@ -21,11 +22,25 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
||||
};
|
||||
|
||||
const addProperty = (obj, path) => {
|
||||
const newObj = { ...obj };
|
||||
const keys = Object.keys(newObj);
|
||||
console.log('🔧 ADD PROPERTY - Before:', { path, dataKeys: Object.keys(data), objKeys: Object.keys(obj) });
|
||||
|
||||
const pathParts = path.split('.');
|
||||
const newData = { ...data };
|
||||
let current = newData;
|
||||
|
||||
// Navigate to the target object in the full data structure
|
||||
for (let i = 1; i < pathParts.length; i++) {
|
||||
current = current[pathParts[i]];
|
||||
}
|
||||
|
||||
// Add new property to the target object
|
||||
const keys = Object.keys(current);
|
||||
const newKey = `property${keys.length + 1}`;
|
||||
newObj[newKey] = '';
|
||||
updateData(newObj);
|
||||
current[newKey] = '';
|
||||
|
||||
console.log('🔧 ADD PROPERTY - After:', { path, newKey, dataKeys: Object.keys(newData), targetKeys: Object.keys(current) });
|
||||
|
||||
updateData(newData);
|
||||
setExpandedNodes(new Set([...expandedNodes, path]));
|
||||
};
|
||||
|
||||
@@ -69,6 +84,52 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
||||
};
|
||||
|
||||
const updateValue = (value, path) => {
|
||||
console.log('✏️ UPDATE VALUE:', { path, value, currentType: typeof getValue(path) });
|
||||
|
||||
const pathParts = path.split('.');
|
||||
const newData = { ...data };
|
||||
let current = newData;
|
||||
|
||||
for (let i = 1; i < pathParts.length - 1; i++) {
|
||||
current = current[pathParts[i]];
|
||||
}
|
||||
|
||||
const key = pathParts[pathParts.length - 1];
|
||||
const currentValue = current[key];
|
||||
const currentType = typeof currentValue;
|
||||
|
||||
// Preserve the current type when updating value
|
||||
if (currentType === 'boolean') {
|
||||
current[key] = value === 'true';
|
||||
} else if (currentType === 'number') {
|
||||
const numValue = Number(value);
|
||||
current[key] = isNaN(numValue) ? 0 : numValue;
|
||||
} else if (currentValue === null) {
|
||||
current[key] = value === 'null' ? null : value;
|
||||
} else {
|
||||
// For strings and initial empty values, use auto-detection
|
||||
if (currentValue === '' || currentValue === undefined) {
|
||||
if (value === 'true' || value === 'false') {
|
||||
current[key] = value === 'true';
|
||||
} else if (value === 'null') {
|
||||
current[key] = null;
|
||||
} else if (!isNaN(value) && value !== '' && value.trim() !== '') {
|
||||
current[key] = Number(value);
|
||||
} else {
|
||||
current[key] = value;
|
||||
}
|
||||
} else {
|
||||
current[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✏️ UPDATE VALUE - Result:', { path, newValue: current[key], newType: typeof current[key] });
|
||||
updateData(newData);
|
||||
};
|
||||
|
||||
const changeType = (newType, path) => {
|
||||
console.log('🔄 CHANGE TYPE:', { path, newType, currentValue: getValue(path) });
|
||||
|
||||
const pathParts = path.split('.');
|
||||
const newData = { ...data };
|
||||
let current = newData;
|
||||
@@ -80,45 +141,28 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
||||
const key = pathParts[pathParts.length - 1];
|
||||
const currentValue = current[key];
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
updateData(newData);
|
||||
};
|
||||
|
||||
const changeType = (newType, path) => {
|
||||
const pathParts = path.split('.');
|
||||
const newData = { ...data };
|
||||
let current = newData;
|
||||
|
||||
for (let i = 1; i < pathParts.length - 1; i++) {
|
||||
current = current[pathParts[i]];
|
||||
}
|
||||
|
||||
const key = pathParts[pathParts.length - 1];
|
||||
|
||||
// Try to preserve value when changing types if possible
|
||||
switch (newType) {
|
||||
case 'string':
|
||||
current[key] = '';
|
||||
current[key] = currentValue === null ? '' : currentValue.toString();
|
||||
break;
|
||||
case 'number':
|
||||
current[key] = 0;
|
||||
if (typeof currentValue === 'string' && !isNaN(currentValue) && currentValue.trim() !== '') {
|
||||
current[key] = Number(currentValue);
|
||||
} else if (typeof currentValue === 'boolean') {
|
||||
current[key] = currentValue ? 1 : 0;
|
||||
} else {
|
||||
current[key] = 0;
|
||||
}
|
||||
break;
|
||||
case 'boolean':
|
||||
current[key] = false;
|
||||
if (typeof currentValue === 'string') {
|
||||
current[key] = currentValue.toLowerCase() === 'true';
|
||||
} else if (typeof currentValue === 'number') {
|
||||
current[key] = currentValue !== 0;
|
||||
} else {
|
||||
current[key] = false;
|
||||
}
|
||||
break;
|
||||
case 'array':
|
||||
current[key] = [];
|
||||
@@ -133,10 +177,20 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
||||
current[key] = '';
|
||||
}
|
||||
|
||||
console.log('🔄 CHANGE TYPE - Result:', { path, newValue: current[key], actualType: typeof current[key] });
|
||||
updateData(newData);
|
||||
setExpandedNodes(new Set([...expandedNodes, path]));
|
||||
};
|
||||
|
||||
const getValue = (path) => {
|
||||
const pathParts = path.split('.');
|
||||
let current = data;
|
||||
for (let i = 1; i < pathParts.length; i++) {
|
||||
current = current[pathParts[i]];
|
||||
}
|
||||
return current;
|
||||
};
|
||||
|
||||
const renameKey = (oldKey, newKey, path) => {
|
||||
if (oldKey === newKey || !newKey.trim()) return;
|
||||
|
||||
@@ -241,17 +295,37 @@ const StructuredEditor = ({ onDataChange, initialData = {} }) => {
|
||||
)}
|
||||
|
||||
{!canExpand ? (
|
||||
<input
|
||||
type="text"
|
||||
value={
|
||||
value === null ? 'null' :
|
||||
value === undefined ? '' :
|
||||
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 min-w-0"
|
||||
placeholder="Value"
|
||||
/>
|
||||
typeof value === 'boolean' ? (
|
||||
<div className="flex-1 flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => updateValue((!value).toString(), path)}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 ${
|
||||
value ? 'bg-blue-600' : 'bg-gray-200 dark:bg-gray-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
value ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400 font-mono">
|
||||
{value.toString()}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
value={
|
||||
value === null ? 'null' :
|
||||
value === undefined ? '' :
|
||||
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 min-w-0"
|
||||
placeholder="Value"
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<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)`}
|
||||
|
||||
@@ -12,6 +12,7 @@ const DiffTool = () => {
|
||||
const [rightText, setRightText] = useState('');
|
||||
const [diffMode, setDiffMode] = useState('unified'); // 'unified' or 'split'
|
||||
|
||||
|
||||
// Generate unified diff format for react-diff-view
|
||||
const diffText = useMemo(() => {
|
||||
if (!leftText && !rightText) return null;
|
||||
@@ -231,6 +232,7 @@ const user = {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Diff Result */}
|
||||
<div className="space-y-2">
|
||||
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
|
||||
@@ -12,6 +12,7 @@ const SerializeTool = () => {
|
||||
const [editorMode, setEditorMode] = useState('text'); // 'text' or 'visual'
|
||||
const [structuredData, setStructuredData] = useState({});
|
||||
|
||||
|
||||
// Simple PHP serialize implementation for common data types
|
||||
const phpSerialize = (data) => {
|
||||
if (data === null) return 'N;';
|
||||
@@ -45,7 +46,8 @@ const SerializeTool = () => {
|
||||
// Simple PHP unserialize implementation
|
||||
const phpUnserialize = (str) => {
|
||||
let index = 0;
|
||||
|
||||
|
||||
|
||||
const parseValue = () => {
|
||||
if (index >= str.length) {
|
||||
throw new Error('Unexpected end of string');
|
||||
@@ -110,20 +112,60 @@ const SerializeTool = () => {
|
||||
}
|
||||
index++; // Skip opening '"'
|
||||
|
||||
const length = parseInt(lenStr);
|
||||
if (isNaN(length) || length < 0) {
|
||||
const byteLength = parseInt(lenStr);
|
||||
if (isNaN(byteLength) || byteLength < 0) {
|
||||
throw new Error(`Invalid string length: ${lenStr}`);
|
||||
}
|
||||
|
||||
// Extract string content
|
||||
const stringVal = str.substring(index, index + length);
|
||||
index += length;
|
||||
// Handle empty strings
|
||||
if (byteLength === 0) {
|
||||
// Expect closing quote and semicolon immediately
|
||||
if (index + 1 >= str.length || str[index] !== '"' || str[index + 1] !== ';') {
|
||||
throw new Error(`Expected '";' after empty string at position ${index}`);
|
||||
}
|
||||
index += 2; // Skip closing '";'
|
||||
return '';
|
||||
}
|
||||
|
||||
// Expect closing quote and semicolon
|
||||
if (index + 1 >= str.length || str[index] !== '"' || str[index + 1] !== ';') {
|
||||
// Extract string by slicing exact UTF-8 byte length
|
||||
const startIndex = index;
|
||||
const remaining = str.slice(index);
|
||||
const encoder = new TextEncoder();
|
||||
const bytes = encoder.encode(remaining);
|
||||
|
||||
if (bytes.length < byteLength) {
|
||||
throw new Error(`String byte length mismatch: expected ${byteLength}, got ${bytes.length} (remaining) at position ${startIndex}`);
|
||||
}
|
||||
|
||||
// Take exactly `byteLength` bytes and decode back to a JS string.
|
||||
// If the slice ends mid-codepoint, TextDecoder with {fatal:true} will throw.
|
||||
let stringVal = '';
|
||||
try {
|
||||
const slice = bytes.slice(0, byteLength);
|
||||
const decoder = new TextDecoder('utf-8', { fatal: true });
|
||||
stringVal = decoder.decode(slice);
|
||||
} catch (e) {
|
||||
throw new Error(`Declared byte length splits a UTF-8 code point at position ${startIndex}`);
|
||||
}
|
||||
|
||||
// Advance `index` by the number of UTF-16 code units consumed by `stringVal`.
|
||||
index += stringVal.length;
|
||||
|
||||
// Verify the re-encoded byte length matches exactly
|
||||
if (new TextEncoder().encode(stringVal).length !== byteLength) {
|
||||
throw new Error(`String byte length mismatch: expected ${byteLength}, got ${new TextEncoder().encode(stringVal).length} at position ${startIndex}`);
|
||||
}
|
||||
|
||||
// Expect closing quote and semicolon normally. Some producers incorrectly include the closing quote in the declared byte length.
|
||||
if (index + 1 < str.length && str[index] === '"' && str[index + 1] === ';') {
|
||||
index += 2; // standard '";' terminator
|
||||
} else if (index < str.length && str[index] === ';' && str[index - 1] === '"') {
|
||||
// Len included the closing '"' in the byteCount; accept ';' directly.
|
||||
// This is a compatibility path for non-standard serialized inputs observed in the wild.
|
||||
index += 1; // consume ';'
|
||||
} else {
|
||||
throw new Error(`Expected '";' after string at position ${index}`);
|
||||
}
|
||||
index += 2; // Skip closing '";'
|
||||
|
||||
return stringVal;
|
||||
|
||||
@@ -186,12 +228,10 @@ const SerializeTool = () => {
|
||||
|
||||
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}`);
|
||||
@@ -355,7 +395,7 @@ const SerializeTool = () => {
|
||||
Clear All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Input/Output Grid */}
|
||||
<div className={`grid gap-6 ${
|
||||
mode === 'serialize' && editorMode === 'visual'
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { Search, Copy, Download } from 'lucide-react';
|
||||
import { Controlled as CodeMirror } from 'react-codemirror2';
|
||||
import 'codemirror/lib/codemirror.css';
|
||||
import 'codemirror/theme/material.css';
|
||||
import 'codemirror/mode/xml/xml';
|
||||
import 'codemirror/mode/css/css';
|
||||
import 'codemirror/mode/javascript/javascript';
|
||||
import 'codemirror/addon/search/search';
|
||||
import 'codemirror/addon/search/searchcursor';
|
||||
import 'codemirror/addon/dialog/dialog';
|
||||
import 'codemirror/addon/dialog/dialog.css';
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import { html } from '@codemirror/lang-html';
|
||||
import { css as cssLang } from '@codemirror/lang-css';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { EditorView, keymap } from '@codemirror/view';
|
||||
import { searchKeymap, openSearchPanel } from '@codemirror/search';
|
||||
|
||||
const CodeInputs = ({
|
||||
htmlInput,
|
||||
@@ -21,14 +17,15 @@ const CodeInputs = ({
|
||||
isFullscreen
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState('html');
|
||||
const htmlEditorRef = useRef(null);
|
||||
const cssEditorRef = useRef(null);
|
||||
const jsEditorRef = useRef(null);
|
||||
const htmlViewRef = useRef(null);
|
||||
const cssViewRef = useRef(null);
|
||||
const jsViewRef = useRef(null);
|
||||
|
||||
// Handle search functionality
|
||||
const handleSearch = (editorRef) => {
|
||||
if (editorRef.current && editorRef.current.editor) {
|
||||
editorRef.current.editor.execCommand('find');
|
||||
const handleSearch = (viewRef) => {
|
||||
const view = viewRef.current;
|
||||
if (view) {
|
||||
openSearchPanel(view);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -57,10 +54,10 @@ const CodeInputs = ({
|
||||
// Get current editor ref based on active tab
|
||||
const getCurrentEditorRef = () => {
|
||||
switch (activeTab) {
|
||||
case 'html': return htmlEditorRef;
|
||||
case 'css': return cssEditorRef;
|
||||
case 'js': return jsEditorRef;
|
||||
default: return htmlEditorRef;
|
||||
case 'html': return htmlViewRef;
|
||||
case 'css': return cssViewRef;
|
||||
case 'js': return jsViewRef;
|
||||
default: return htmlViewRef;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -139,69 +136,36 @@ const CodeInputs = ({
|
||||
<div className="flex-1">
|
||||
{activeTab === 'html' && (
|
||||
<CodeMirror
|
||||
ref={htmlEditorRef}
|
||||
value={htmlInput}
|
||||
onBeforeChange={(editor, data, value) => setHtmlInput(value)}
|
||||
options={{
|
||||
mode: 'xml',
|
||||
theme: 'material',
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
autoCloseTags: true,
|
||||
matchBrackets: true,
|
||||
indentUnit: 2,
|
||||
tabSize: 2,
|
||||
extraKeys: {
|
||||
'Ctrl-F': 'findPersistent',
|
||||
'Cmd-F': 'findPersistent'
|
||||
}
|
||||
}}
|
||||
height={isFullscreen ? 'calc(100vh - 210px)' : '380px'}
|
||||
extensions={[html(), keymap.of(searchKeymap), EditorView.lineWrapping]}
|
||||
onChange={(value) => setHtmlInput(value)}
|
||||
onUpdate={(vu) => { if (!htmlViewRef.current) htmlViewRef.current = vu.view; }}
|
||||
basicSetup={{ lineNumbers: true }}
|
||||
className="h-full"
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'css' && (
|
||||
<CodeMirror
|
||||
ref={cssEditorRef}
|
||||
value={cssInput}
|
||||
onBeforeChange={(editor, data, value) => setCssInput(value)}
|
||||
options={{
|
||||
mode: 'css',
|
||||
theme: 'material',
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
autoCloseBrackets: true,
|
||||
matchBrackets: true,
|
||||
indentUnit: 2,
|
||||
tabSize: 2,
|
||||
extraKeys: {
|
||||
'Ctrl-F': 'findPersistent',
|
||||
'Cmd-F': 'findPersistent'
|
||||
}
|
||||
}}
|
||||
height={isFullscreen ? 'calc(100vh - 210px)' : '380px'}
|
||||
extensions={[cssLang(), keymap.of(searchKeymap), EditorView.lineWrapping]}
|
||||
onChange={(value) => setCssInput(value)}
|
||||
onUpdate={(vu) => { if (!cssViewRef.current) cssViewRef.current = vu.view; }}
|
||||
basicSetup={{ lineNumbers: true }}
|
||||
className="h-full"
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === 'js' && (
|
||||
<CodeMirror
|
||||
ref={jsEditorRef}
|
||||
value={jsInput}
|
||||
onBeforeChange={(editor, data, value) => setJsInput(value)}
|
||||
options={{
|
||||
mode: 'javascript',
|
||||
theme: 'material',
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
autoCloseBrackets: true,
|
||||
matchBrackets: true,
|
||||
indentUnit: 2,
|
||||
tabSize: 2,
|
||||
extraKeys: {
|
||||
'Ctrl-F': 'findPersistent',
|
||||
'Cmd-F': 'findPersistent'
|
||||
}
|
||||
}}
|
||||
height={isFullscreen ? 'calc(100vh - 210px)' : '380px'}
|
||||
extensions={[javascript({ jsx: true }), keymap.of(searchKeymap), EditorView.lineWrapping]}
|
||||
onChange={(value) => setJsInput(value)}
|
||||
onUpdate={(vu) => { if (!jsViewRef.current) jsViewRef.current = vu.view; }}
|
||||
basicSetup={{ lineNumbers: true }}
|
||||
className="h-full"
|
||||
/>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user