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

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