+
+
+
+ }
+ resultComponent={}
+ initialValues={{}}
+ getGroups={null}
+ setInput={() => {
+ setInputA('');
+ setInputB('');
+ setResult('');
+ }}
+ compute={compute}
+ toolInfo={{
+ title: t('textCompare.toolInfo.title'),
+ description: t('textCompare.toolInfo.description')
+ }}
+ />
+ );
+}
diff --git a/src/pages/tools/string/text-compare/meta.ts b/src/pages/tools/string/text-compare/meta.ts
new file mode 100644
index 0000000..8329b2c
--- /dev/null
+++ b/src/pages/tools/string/text-compare/meta.ts
@@ -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'))
+});
diff --git a/src/pages/tools/string/text-compare/service.ts b/src/pages/tools/string/text-compare/service.ts
new file mode 100644
index 0000000..2b4141c
--- /dev/null
+++ b/src/pages/tools/string/text-compare/service.ts
@@ -0,0 +1,24 @@
+import { diffWordsWithSpace } from 'diff';
+
+function escapeHtml(str: string): string {
+ return str.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 `${val}`;
+ }
+ if (part.removed) {
+ return `${val}`;
+ }
+ return `${val}`;
+ })
+ .join('');
+
+ return `${html}
`;
+}
diff --git a/src/pages/tools/string/text-compare/text-compare.service.test.ts b/src/pages/tools/string/text-compare/text-compare.service.test.ts
new file mode 100644
index 0000000..d5a0816
--- /dev/null
+++ b/src/pages/tools/string/text-compare/text-compare.service.test.ts
@@ -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(' here');
+ });
+
+ it('should highlight removed text', () => {
+ const textA = 'Hello world here';
+ const textB = 'Hello world';
+
+ const result = compareTextsHtml(textA, textB);
+ expect(result).toContain(' here');
+ });
+
+ 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('Lyon');
+ expect(result).toContain('Marseille');
+ });
+
+ it('should return plain diff if texts are identical', () => {
+ const input = 'Same text everywhere';
+ const result = compareTextsHtml(input, input);
+ expect(result).toContain('Same text everywhere');
+ expect(result).not.toContain('diff-added');
+ expect(result).not.toContain('diff-removed');
+ });
+
+ it('should escape HTML characters', () => {
+ const textA = 'Hello';
+ const textB = 'Hello world';
+
+ const result = compareTextsHtml(textA, textB);
+ expect(result).toContain('<b>Hello');
+ expect(result).toContain(' world');
+ });
+
+ it('should wrap result in a single diff-line div', () => {
+ const result = compareTextsHtml('foo', 'bar');
+ expect(result.startsWith('')).toBe(true);
+ expect(result.endsWith('
')).toBe(true);
+ });
+
+ it('should handle empty input strings', () => {
+ const result = compareTextsHtml('', '');
+ expect(result).toBe('');
+ });
+
+ it('should handle only added input', () => {
+ const result = compareTextsHtml('', 'New text');
+ expect(result).toContain('New text');
+ });
+
+ it('should handle only removed input', () => {
+ const result = compareTextsHtml('Old text', '');
+ expect(result).toContain('Old text');
+ });
+});