mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-09-19 22:19:36 +02:00
Merge pull request #197 from bhavesh158/json-compare
feat: add JSON comparison tool
This commit is contained in:
37
package-lock.json
generated
37
package-lock.json
generated
@@ -15,6 +15,7 @@
|
||||
"@ffmpeg/util": "^0.12.2",
|
||||
"@imgly/background-removal": "^1.6.0",
|
||||
"@jimp/types": "^1.6.0",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@mui/icons-material": "^5.15.20",
|
||||
"@mui/material": "^5.15.20",
|
||||
"@playwright/test": "^1.45.0",
|
||||
@@ -2104,6 +2105,29 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@monaco-editor/loader": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.5.0.tgz",
|
||||
"integrity": "sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"state-local": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@monaco-editor/react": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.7.0.tgz",
|
||||
"integrity": "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@monaco-editor/loader": "^1.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"monaco-editor": ">= 0.25.0 < 1",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/base": {
|
||||
"version": "5.0.0-beta.40",
|
||||
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.40.tgz",
|
||||
@@ -8902,6 +8926,13 @@
|
||||
"ufo": "^1.5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/monaco-editor": {
|
||||
"version": "0.52.2",
|
||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz",
|
||||
"integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/morsee": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/morsee/-/morsee-1.0.9.tgz",
|
||||
@@ -11271,6 +11302,12 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/state-local": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz",
|
||||
"integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/std-env": {
|
||||
"version": "3.7.0",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz",
|
||||
|
@@ -34,6 +34,7 @@
|
||||
"@ffmpeg/util": "^0.12.2",
|
||||
"@imgly/background-removal": "^1.6.0",
|
||||
"@jimp/types": "^1.6.0",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@mui/icons-material": "^5.15.20",
|
||||
"@mui/material": "^5.15.20",
|
||||
"@playwright/test": "^1.45.0",
|
||||
|
@@ -58,5 +58,10 @@
|
||||
"title": "What is JSON Validation?"
|
||||
},
|
||||
"validJson": "✅ Valid JSON"
|
||||
},
|
||||
"comparison": {
|
||||
"title": "Compare JSON",
|
||||
"description": "Compare two JSON objects to identify differences in structure and values.",
|
||||
"shortDescription": "Find differences between two JSON objects"
|
||||
}
|
||||
}
|
||||
|
@@ -106,5 +106,10 @@
|
||||
},
|
||||
"validJson": "✅ मान्य JSON",
|
||||
"validationOptions": "मान्यता विकल्प"
|
||||
},
|
||||
"comparison": {
|
||||
"title": "JSON तुलना करें",
|
||||
"description": "दो JSON वस्तुओं की संरचना और मूल्यों में अंतर की पहचान करें।",
|
||||
"shortDescription": "दो JSON वस्तुओं के बीच अंतर ढूंढें"
|
||||
}
|
||||
}
|
||||
|
73
src/components/input/ToolCodeInput.tsx
Normal file
73
src/components/input/ToolCodeInput.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Box } from '@mui/material';
|
||||
import React, { useContext, useRef } from 'react';
|
||||
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
|
||||
import InputHeader from '../InputHeader';
|
||||
import InputFooter from './InputFooter';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import Editor from '@monaco-editor/react';
|
||||
import { globalInputHeight } from '../../config/uiConfig';
|
||||
|
||||
export default function ToolCodeInput({
|
||||
value,
|
||||
onChange,
|
||||
title = 'Input text',
|
||||
language
|
||||
}: {
|
||||
title?: string;
|
||||
value: string;
|
||||
language: string;
|
||||
onChange: (value: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { showSnackBar } = useContext(CustomSnackBarContext);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard
|
||||
.writeText(value)
|
||||
.then(() => showSnackBar(t('toolTextInput.copied'), 'success'))
|
||||
.catch((err) => {
|
||||
showSnackBar(t('toolTextInput.copyFailed', { error: err }), 'error');
|
||||
});
|
||||
};
|
||||
|
||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const text = e.target?.result;
|
||||
if (typeof text === 'string') {
|
||||
onChange(text);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImportClick = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<InputHeader title={title || t('toolTextInput.input')} />
|
||||
<Box height={globalInputHeight}>
|
||||
<Editor
|
||||
height={'87%'}
|
||||
language={language}
|
||||
value={value}
|
||||
onChange={(value) => onChange(value ?? '')}
|
||||
/>
|
||||
<InputFooter handleCopy={handleCopy} handleImport={handleImportClick} />
|
||||
<input
|
||||
type="file"
|
||||
accept="*"
|
||||
ref={fileInputRef}
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
@@ -5,6 +5,7 @@ import { tool as validateJson } from './validateJson/meta';
|
||||
import { tool as jsonToXml } from './json-to-xml/meta';
|
||||
import { tool as escapeJson } from './escape-json/meta';
|
||||
import { tool as tsvToJson } from './tsv-to-json/meta';
|
||||
import { tool as jsonComparison } from './json-comparison/meta';
|
||||
|
||||
export const jsonTools = [
|
||||
validateJson,
|
||||
@@ -13,5 +14,6 @@ export const jsonTools = [
|
||||
jsonStringify,
|
||||
jsonToXml,
|
||||
escapeJson,
|
||||
tsvToJson
|
||||
tsvToJson,
|
||||
jsonComparison
|
||||
];
|
||||
|
89
src/pages/tools/json/json-comparison/index.tsx
Normal file
89
src/pages/tools/json/json-comparison/index.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import ToolCodeInput from '@components/input/ToolCodeInput';
|
||||
import ToolTextResult from '@components/result/ToolTextResult';
|
||||
import { compareJson } from './service';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import { Grid } from '@mui/material';
|
||||
|
||||
type InitialValuesType = {};
|
||||
|
||||
const initialValues: InitialValuesType = {};
|
||||
|
||||
export default function JsonComparison({ title }: ToolComponentProps) {
|
||||
const [input1, setInput1] = useState<string>('');
|
||||
const [input2, setInput2] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
const compareInputs = () => {
|
||||
try {
|
||||
// Only compare if at least one input has content
|
||||
if (input1.trim() || input2.trim()) {
|
||||
const differences = compareJson(
|
||||
input1 || '{}',
|
||||
input2 || '{}',
|
||||
'text'
|
||||
);
|
||||
setResult(differences);
|
||||
} else {
|
||||
setResult('');
|
||||
}
|
||||
} catch (error) {
|
||||
setResult(
|
||||
`Error: ${
|
||||
error instanceof Error ? error.message : 'Invalid JSON format'
|
||||
}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
compareInputs();
|
||||
}, [input1, input2]);
|
||||
|
||||
const handleInput1Change = (value: string | undefined) => {
|
||||
setInput1(value ?? '');
|
||||
};
|
||||
|
||||
const handleInput2Change = (value: string) => {
|
||||
setInput2(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<ToolContent
|
||||
title={title}
|
||||
input={input1}
|
||||
setInput={setInput1}
|
||||
initialValues={initialValues}
|
||||
getGroups={null}
|
||||
compute={() => {}}
|
||||
inputComponent={
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={6} lg={4}>
|
||||
<ToolCodeInput
|
||||
title="First JSON"
|
||||
value={input1}
|
||||
onChange={handleInput1Change}
|
||||
language={'json'}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6} lg={4}>
|
||||
<ToolCodeInput
|
||||
title="Second JSON"
|
||||
language={'json'}
|
||||
value={input2}
|
||||
onChange={handleInput2Change}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={12} lg={4}>
|
||||
<ToolTextResult
|
||||
title="Differences"
|
||||
value={result}
|
||||
extension={'txt'}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
15
src/pages/tools/json/json-comparison/meta.ts
Normal file
15
src/pages/tools/json/json-comparison/meta.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('json', {
|
||||
path: 'json-comparison',
|
||||
icon: 'fluent:branch-compare-24-regular',
|
||||
keywords: ['json', 'compare', 'diff', 'differences', 'match', 'validation'],
|
||||
component: lazy(() => import('./index')),
|
||||
|
||||
i18n: {
|
||||
name: 'json:comparison.title',
|
||||
description: 'json:comparison.description',
|
||||
shortDescription: 'json:comparison.shortDescription'
|
||||
}
|
||||
});
|
64
src/pages/tools/json/json-comparison/service.test.ts
Normal file
64
src/pages/tools/json/json-comparison/service.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { compareJson } from './service';
|
||||
|
||||
describe('compareJson', () => {
|
||||
it('should identify missing properties', () => {
|
||||
const json1 = '{"name": "John", "age": 30}';
|
||||
const json2 = '{"name": "John"}';
|
||||
|
||||
expect(compareJson(json1, json2, 'text')).toContain(
|
||||
'age: Missing in second JSON'
|
||||
);
|
||||
});
|
||||
|
||||
it('should identify value mismatches', () => {
|
||||
const json1 = '{"name": "John", "age": 30}';
|
||||
const json2 = '{"name": "John", "age": 25}';
|
||||
|
||||
expect(compareJson(json1, json2, 'text')).toContain(
|
||||
'age: Mismatch: 30 != 25'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle nested objects', () => {
|
||||
const json1 = '{"person": {"name": "John", "age": 30}}';
|
||||
const json2 = '{"person": {"name": "Jane", "age": 30}}';
|
||||
|
||||
expect(compareJson(json1, json2, 'text')).toContain(
|
||||
'person.name: Mismatch: John != Jane'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return JSON format when specified', () => {
|
||||
const json1 = '{"name": "John", "age": 30}';
|
||||
const json2 = '{"name": "Jane", "age": 25}';
|
||||
|
||||
const result = compareJson(json1, json2, 'json');
|
||||
const parsed = JSON.parse(result);
|
||||
|
||||
expect(parsed).toHaveProperty('name');
|
||||
expect(parsed).toHaveProperty('age');
|
||||
});
|
||||
|
||||
it('should handle arrays', () => {
|
||||
const json1 = '{"numbers": [1, 2, 3]}';
|
||||
const json2 = '{"numbers": [1, 2, 4]}';
|
||||
|
||||
expect(compareJson(json1, json2, 'text')).toContain(
|
||||
'numbers.2: Mismatch: 3 != 4'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return "No differences found" for identical JSONs', () => {
|
||||
const json1 = '{"name": "John", "age": 30}';
|
||||
const json2 = '{"name": "John", "age": 30}';
|
||||
|
||||
expect(compareJson(json1, json2, 'text')).toBe('No differences found');
|
||||
});
|
||||
|
||||
it('should throw error for invalid JSON', () => {
|
||||
const json1 = '{"name": "John"';
|
||||
const json2 = '{"name": "John"}';
|
||||
|
||||
expect(() => compareJson(json1, json2, 'text')).toThrow();
|
||||
});
|
||||
});
|
139
src/pages/tools/json/json-comparison/service.ts
Normal file
139
src/pages/tools/json/json-comparison/service.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
const fixTrailingCommas = (json: string): string => {
|
||||
// Replace trailing commas in objects and arrays with proper JSON syntax
|
||||
return json
|
||||
.replace(/,\s*([}\]])/g, '$1') // Remove trailing commas in objects and arrays
|
||||
.replace(/,\s*\n\s*([}\]])/g, '\n$1'); // Also handle when the closing bracket is on a new line
|
||||
};
|
||||
|
||||
const tryParseJSON = (
|
||||
json: string
|
||||
): { valid: boolean; data?: any; error?: string } => {
|
||||
if (!json.trim()) {
|
||||
return { valid: true, data: {} };
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to parse after fixing trailing commas
|
||||
const fixedJson = fixTrailingCommas(json);
|
||||
const data = JSON.parse(fixedJson);
|
||||
return { valid: true, data };
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof SyntaxError ? error.message : 'Invalid JSON format';
|
||||
// Extract line and column info from the error message if available
|
||||
const match = errorMessage.match(/at line (\d+) column (\d+)/);
|
||||
if (match) {
|
||||
const [, line, column] = match;
|
||||
return {
|
||||
valid: false,
|
||||
error: `${errorMessage}\nLocation: Line ${line}, Column ${column}`
|
||||
};
|
||||
}
|
||||
return {
|
||||
valid: false,
|
||||
error: errorMessage
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const compareJson = (
|
||||
json1: string,
|
||||
json2: string,
|
||||
format: 'text' | 'json'
|
||||
): string => {
|
||||
// Handle empty inputs
|
||||
if (!json1.trim() && !json2.trim()) return '';
|
||||
|
||||
// Parse both JSON inputs
|
||||
const parsed1 = tryParseJSON(json1);
|
||||
const parsed2 = tryParseJSON(json2);
|
||||
|
||||
// Handle parsing errors
|
||||
if (!parsed1.valid || !parsed2.valid) {
|
||||
const errors = [];
|
||||
if (!parsed1.valid) {
|
||||
errors.push(`First JSON: ${parsed1.error}`);
|
||||
}
|
||||
if (!parsed2.valid) {
|
||||
errors.push(`Second JSON: ${parsed2.error}`);
|
||||
}
|
||||
throw new Error(errors.join('\n\n'));
|
||||
}
|
||||
|
||||
// Compare the valid JSON objects
|
||||
if (format === 'json') {
|
||||
const diffs = findDifferencesJSON(parsed1.data, parsed2.data);
|
||||
return JSON.stringify(diffs);
|
||||
} else {
|
||||
const differences = findDifferencesText(parsed1.data, parsed2.data);
|
||||
if (differences.length === 0) {
|
||||
return 'No differences found';
|
||||
}
|
||||
return differences.join('\n');
|
||||
}
|
||||
};
|
||||
|
||||
const findDifferencesText = (
|
||||
obj1: any,
|
||||
obj2: any,
|
||||
path: string[] = []
|
||||
): string[] => {
|
||||
const differences: string[] = [];
|
||||
const processPath = (p: string[]): string =>
|
||||
p.length ? p.join('.') : 'root';
|
||||
|
||||
// Compare all keys in obj1
|
||||
for (const key in obj1) {
|
||||
const currentPath = [...path, key];
|
||||
|
||||
if (!(key in obj2)) {
|
||||
differences.push(`${processPath(currentPath)}: Missing in second JSON`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const value1 = obj1[key];
|
||||
const value2 = obj2[key];
|
||||
|
||||
if (
|
||||
typeof value1 === 'object' &&
|
||||
value1 !== null &&
|
||||
typeof value2 === 'object' &&
|
||||
value2 !== null
|
||||
) {
|
||||
differences.push(...findDifferencesText(value1, value2, currentPath));
|
||||
} else if (value1 !== value2) {
|
||||
differences.push(
|
||||
`${processPath(currentPath)}: Mismatch: ${value1} != ${value2}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for keys in obj2 that don't exist in obj1
|
||||
for (const key in obj2) {
|
||||
if (!(key in obj1)) {
|
||||
const currentPath = [...path, key];
|
||||
differences.push(`${processPath(currentPath)}: Missing in first JSON`);
|
||||
}
|
||||
}
|
||||
|
||||
return differences;
|
||||
};
|
||||
|
||||
const findDifferencesJSON = (obj1: any, obj2: any): Record<string, string> => {
|
||||
const result: Record<string, string> = {};
|
||||
|
||||
// Compare all properties
|
||||
const allKeys = new Set([...Object.keys(obj1), ...Object.keys(obj2)]);
|
||||
|
||||
for (const key of allKeys) {
|
||||
if (!(key in obj1)) {
|
||||
result[key] = 'Missing in first JSON';
|
||||
} else if (!(key in obj2)) {
|
||||
result[key] = 'Missing in second JSON';
|
||||
} else if (obj1[key] !== obj2[key]) {
|
||||
result[key] = `Mismatch: ${obj1[key]} != ${obj2[key]}`;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
Reference in New Issue
Block a user