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:
dwindown
2025-08-21 23:12:43 +07:00
parent 97459ea313
commit 22d333d932
6 changed files with 1926 additions and 3415 deletions

4981
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -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": {

View File

@@ -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)`}

View File

@@ -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">

View File

@@ -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'

View File

@@ -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"
/>
)}