mirror of
https://github.com/iib0011/omni-tools.git
synced 2025-11-05 16:34:57 +05:30
Merge 8da90b279e into f3c5946e0d
This commit is contained in:
commit
b10988d2bd
9 changed files with 285 additions and 9 deletions
18
package-lock.json
generated
18
package-lock.json
generated
|
|
@ -32,6 +32,7 @@
|
|||
"cron-validator": "^1.3.1",
|
||||
"cronstrue": "^3.0.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"diff": "^8.0.2",
|
||||
"fast-xml-parser": "^5.2.5",
|
||||
"formik": "^2.4.6",
|
||||
"heic2any": "^0.0.4",
|
||||
|
|
@ -5631,10 +5632,9 @@
|
|||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/diff": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
|
||||
"integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
|
||||
"dev": true,
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-8.0.2.tgz",
|
||||
"integrity": "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
|
|
@ -8993,6 +8993,16 @@
|
|||
"node": "^12.20.0 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/locize-cli/node_modules/diff": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
|
||||
"integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/locize-cli/node_modules/glob": {
|
||||
"version": "9.3.5",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz",
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@
|
|||
"cron-validator": "^1.3.1",
|
||||
"cronstrue": "^3.0.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"diff": "^8.0.2",
|
||||
"fast-xml-parser": "^5.2.5",
|
||||
"formik": "^2.4.6",
|
||||
"heic2any": "^0.0.4",
|
||||
|
|
|
|||
|
|
@ -299,13 +299,29 @@
|
|||
"nonSpecialCharPlaceholder": "Encode non-special characters",
|
||||
"title": "Encoding Options"
|
||||
},
|
||||
"inputTitle": "Input String",
|
||||
"resultTitle": "Url-escaped String",
|
||||
"inputTitle": "Input String(URL-escaped)",
|
||||
"resultTitle": "Output string",
|
||||
"toolInfo": {
|
||||
"description": "Load your string and it will automatically get URL-escaped.",
|
||||
"longDescription": "This tool URL-encodes a string. Special URL characters get converted to percent-sign encoding. This encoding is called percent-encoding because each character's numeric value gets converted to a percent sign followed by a two-digit hexadecimal value. The hex values are determined based on the character's codepoint value. For example, a space gets escaped to %20, a colon to %3a, a slash to %2f. Characters that are not special stay unchanged. In case you also need to convert non-special characters to percent-encoding, then we've also added an extra option that lets you do that. Select the encode-non-special-chars option to enable this behavior.",
|
||||
"shortDescription": "Quickly URL-escape a string.",
|
||||
"title": "String URL encoder"
|
||||
}
|
||||
},
|
||||
"textCompare": {
|
||||
"title": "Compare Texts",
|
||||
"description": "Identify differences between two text blocks, highlighting insertions, deletions, and modifications.",
|
||||
"shortDescription": "Compare two texts",
|
||||
"longDescription": "",
|
||||
"withLabel": "Options",
|
||||
"outputOptions": "Comparison Options",
|
||||
"addDiffHighlight": "Highlight differences",
|
||||
"addDiffHighlightDescription": "Enable syntax highlighting to better visualize changes between texts",
|
||||
"ignoreWhitespace": "Ignore Whitespace",
|
||||
"ignoreWhitespaceDescription": "Ignore changes that are only due to whitespace (e.g., spaces, tabs, line breaks)",
|
||||
"toolInfo": {
|
||||
"title": "Compare Texts",
|
||||
"description": "This tool compares two text inputs and highlights the differences (insertions, deletions, and substitutions). It's useful for reviewing code changes, document revisions, or analyzing updates between versions of a file or message."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
91
src/components/result/ToolDiffResult.tsx
Normal file
91
src/components/result/ToolDiffResult.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import { Box, Typography } from '@mui/material';
|
||||
import InputHeader from '../InputHeader';
|
||||
import ResultFooter from './ResultFooter';
|
||||
import { useContext } from 'react';
|
||||
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export default function ToolDiffResult({
|
||||
title = 'Differences',
|
||||
value,
|
||||
loading,
|
||||
isHtml = false
|
||||
}: {
|
||||
title?: string;
|
||||
value: string;
|
||||
loading?: boolean;
|
||||
isHtml?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const { showSnackBar } = useContext(CustomSnackBarContext);
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard
|
||||
.writeText(value.replace(/<[^>]*>/g, ''))
|
||||
.then(() => showSnackBar(t('toolTextResult.copied'), 'success'))
|
||||
.catch((err) =>
|
||||
showSnackBar(t('toolTextResult.copyFailed', { error: err }), 'error')
|
||||
);
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
const blob = new Blob([value], { type: 'text/html' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'diff-output.html';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
window.URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<InputHeader title={title} />
|
||||
{loading ? (
|
||||
<Typography variant="body2">{t('toolTextResult.loading')}</Typography>
|
||||
) : isHtml ? (
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
backgroundColor: 'background.paper',
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
borderRadius: 1,
|
||||
fontFamily: 'monospace',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
'& .diff-line': {
|
||||
whiteSpace: 'pre-wrap',
|
||||
fontFamily: 'monospace'
|
||||
},
|
||||
'& .diff-added': {
|
||||
backgroundColor: '#c8f7c5' // green
|
||||
},
|
||||
'& .diff-removed': {
|
||||
backgroundColor: '#ffe0e0' // red
|
||||
}
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: value }}
|
||||
/>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
backgroundColor: 'background.paper',
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
borderRadius: 1,
|
||||
fontFamily: 'monospace',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word'
|
||||
}}
|
||||
>
|
||||
{value}
|
||||
</Box>
|
||||
)}
|
||||
<ResultFooter handleCopy={handleCopy} handleDownload={handleDownload} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
|
@ -20,7 +20,8 @@ import { tool as stringStatistic } from './statistic/meta';
|
|||
import { tool as stringCensor } from './censor/meta';
|
||||
import { tool as stringPasswordGenerator } from './password-generator/meta';
|
||||
import { tool as stringEncodeUrl } from './url-encode/meta';
|
||||
import { tool as StringDecodeUrl } from './url-decode/meta';
|
||||
import { tool as stringDecodeUrl } from './url-decode/meta';
|
||||
import { tool as stringCompare } from './text-compare/meta';
|
||||
|
||||
export const stringTools = [
|
||||
stringSplit,
|
||||
|
|
@ -44,6 +45,7 @@ export const stringTools = [
|
|||
stringCensor,
|
||||
stringPasswordGenerator,
|
||||
stringEncodeUrl,
|
||||
StringDecodeUrl,
|
||||
stringHiddenCharacterDetector
|
||||
stringDecodeUrl,
|
||||
stringHiddenCharacterDetector,
|
||||
stringCompare
|
||||
];
|
||||
|
|
|
|||
49
src/pages/tools/string/text-compare/index.tsx
Normal file
49
src/pages/tools/string/text-compare/index.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { Box } from '@mui/material';
|
||||
import { useState, useEffect } from 'react';
|
||||
import ToolContent from '@components/ToolContent';
|
||||
import { ToolComponentProps } from '@tools/defineTool';
|
||||
import ToolTextInput from '@components/input/ToolTextInput';
|
||||
import ToolDiffResult from '@components/result/ToolDiffResult';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { compareTextsHtml } from './service';
|
||||
|
||||
export default function TextCompare({ title }: ToolComponentProps) {
|
||||
const { t } = useTranslation('string');
|
||||
const [inputA, setInputA] = useState<string>('');
|
||||
const [inputB, setInputB] = useState<string>('');
|
||||
const [result, setResult] = useState<string>('');
|
||||
|
||||
const compute = () => {
|
||||
setResult(compareTextsHtml(inputA, inputB));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
compute();
|
||||
}, [inputA, inputB]);
|
||||
|
||||
return (
|
||||
<ToolContent
|
||||
title={title}
|
||||
input={inputA}
|
||||
inputComponent={
|
||||
<Box display="flex" flexDirection="column" gap={2}>
|
||||
<ToolTextInput value={inputA} onChange={setInputA} />
|
||||
<ToolTextInput value={inputB} onChange={setInputB} />
|
||||
</Box>
|
||||
}
|
||||
resultComponent={<ToolDiffResult value={result} isHtml />}
|
||||
initialValues={{}}
|
||||
getGroups={null}
|
||||
setInput={() => {
|
||||
setInputA('');
|
||||
setInputB('');
|
||||
setResult('');
|
||||
}}
|
||||
compute={compute}
|
||||
toolInfo={{
|
||||
title: t('textCompare.toolInfo.title'),
|
||||
description: t('textCompare.toolInfo.description')
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
15
src/pages/tools/string/text-compare/meta.ts
Normal file
15
src/pages/tools/string/text-compare/meta.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { defineTool } from '@tools/defineTool';
|
||||
import { lazy } from 'react';
|
||||
|
||||
export const tool = defineTool('string', {
|
||||
i18n: {
|
||||
name: 'string:textCompare.title',
|
||||
description: 'string:textCompare.description',
|
||||
shortDescription: 'string:textCompare.shortDescription',
|
||||
longDescription: 'string:textCompare.longDescription'
|
||||
},
|
||||
path: 'text-compare',
|
||||
icon: 'material-symbols-light:search',
|
||||
keywords: ['text', 'compare'],
|
||||
component: lazy(() => import('./index'))
|
||||
});
|
||||
24
src/pages/tools/string/text-compare/service.ts
Normal file
24
src/pages/tools/string/text-compare/service.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { diffWordsWithSpace } from 'diff';
|
||||
|
||||
function escapeHtml(str: string): string {
|
||||
return str.replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
export function compareTextsHtml(textA: string, textB: string): string {
|
||||
const diffs = diffWordsWithSpace(textA, textB);
|
||||
|
||||
const html = diffs
|
||||
.map((part) => {
|
||||
const val = escapeHtml(part.value).replace(/ /g, ' ');
|
||||
if (part.added) {
|
||||
return `<span class="diff-added">${val}</span>`;
|
||||
}
|
||||
if (part.removed) {
|
||||
return `<span class="diff-removed">${val}</span>`;
|
||||
}
|
||||
return `<span>${val}</span>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
return `<div class="diff-line">${html}</div>`;
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { compareTextsHtml } from './service';
|
||||
|
||||
describe('compareTextsHtml', () => {
|
||||
it('should highlight added text', () => {
|
||||
const textA = 'Hello world';
|
||||
const textB = 'Hello world here';
|
||||
|
||||
const result = compareTextsHtml(textA, textB);
|
||||
expect(result).toContain('<span class="diff-added"> here</span>');
|
||||
});
|
||||
|
||||
it('should highlight removed text', () => {
|
||||
const textA = 'Hello world here';
|
||||
const textB = 'Hello world';
|
||||
|
||||
const result = compareTextsHtml(textA, textB);
|
||||
expect(result).toContain('<span class="diff-removed"> here</span>');
|
||||
});
|
||||
|
||||
it('should highlight changes in the middle of a sentence', () => {
|
||||
const textA = 'I am in Lyon';
|
||||
const textB = 'I am in Marseille';
|
||||
|
||||
const result = compareTextsHtml(textA, textB);
|
||||
expect(result).toContain('I am in ');
|
||||
expect(result).toContain('<span class="diff-removed">Lyon</span>');
|
||||
expect(result).toContain('<span class="diff-added">Marseille</span>');
|
||||
});
|
||||
|
||||
it('should return plain diff if texts are identical', () => {
|
||||
const input = 'Same text everywhere';
|
||||
const result = compareTextsHtml(input, input);
|
||||
expect(result).toContain('<span>Same text everywhere</span>');
|
||||
expect(result).not.toContain('diff-added');
|
||||
expect(result).not.toContain('diff-removed');
|
||||
});
|
||||
|
||||
it('should escape HTML characters', () => {
|
||||
const textA = '<b>Hello</b>';
|
||||
const textB = '<b>Hello world</b>';
|
||||
|
||||
const result = compareTextsHtml(textA, textB);
|
||||
expect(result).toContain('<b>Hello');
|
||||
expect(result).toContain('<span class="diff-added"> world</span>');
|
||||
});
|
||||
|
||||
it('should wrap result in a single diff-line div', () => {
|
||||
const result = compareTextsHtml('foo', 'bar');
|
||||
expect(result.startsWith('<div class="diff-line">')).toBe(true);
|
||||
expect(result.endsWith('</div>')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle empty input strings', () => {
|
||||
const result = compareTextsHtml('', '');
|
||||
expect(result).toBe('<div class="diff-line"></div>');
|
||||
});
|
||||
|
||||
it('should handle only added input', () => {
|
||||
const result = compareTextsHtml('', 'New text');
|
||||
expect(result).toContain('<span class="diff-added">New text</span>');
|
||||
});
|
||||
|
||||
it('should handle only removed input', () => {
|
||||
const result = compareTextsHtml('Old text', '');
|
||||
expect(result).toContain('<span class="diff-removed">Old text</span>');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue