+
+
+
+ }
+ resultComponent={
+
+ }
+ initialValues={initialValues}
+ getGroups={getGroups}
+ setInput={() => {
+ setInputA('');
+ setInputB('');
+ setResult('');
+ }}
+ compute={compute}
+ toolInfo={{
+ title: t('textCompare.title'),
+ description: t('textCompare.longDescription')
+ }}
+ />
+ );
+}
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..9132e37
--- /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: 'ic:twotone-difference',
+ 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..7030905
--- /dev/null
+++ b/src/pages/tools/string/text-compare/service.ts
@@ -0,0 +1,31 @@
+import { diffWordsWithSpace, diffChars } from 'diff';
+import { level } from './types';
+import { escapeHtml } from 'utils/string';
+
+const DIFF_FN = {
+ word: diffWordsWithSpace,
+ char: diffChars
+} as const;
+
+export function compareTextsHtml(
+ textA: string,
+ textB: string,
+ level: level
+): string {
+ const diffs = DIFF_FN[level](textA, textB);
+
+ const html = diffs
+ .map((part) => {
+ const val = escapeHtml(part.value).replace(/\n/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..44b81be
--- /dev/null
+++ b/src/pages/tools/string/text-compare/text-compare.service.test.ts
@@ -0,0 +1,82 @@
+import { describe, it, expect } from 'vitest';
+import { compareTextsHtml } from './service';
+
+describe('compareTextsHtml', () => {
+ it('should highlight added text', () => {
+ const result = compareTextsHtml('Hello world', 'Hello world here', 'word');
+ expect(result).toContain(' here');
+ });
+
+ it('should highlight removed text', () => {
+ const result = compareTextsHtml('Hello world here', 'Hello world', 'word');
+ expect(result).toContain(' here');
+ });
+
+ it('should highlight changes in the middle of a sentence', () => {
+ const result = compareTextsHtml(
+ 'I am in Lyon',
+ 'I am in Marseille',
+ 'word'
+ );
+ expect(result).toContain('I am in ');
+ expect(result).toContain('Lyon');
+ expect(result).toContain('Marseille');
+ });
+
+ it('should return plain text for identical inputs', () => {
+ const result = compareTextsHtml(
+ 'Same text everywhere',
+ 'Same text everywhere',
+ 'word'
+ );
+ expect(result).toContain('Same text everywhere');
+ expect(result).not.toContain('diff-added');
+ expect(result).not.toContain('diff-removed');
+ });
+
+ it('should escape HTML characters', () => {
+ const result = compareTextsHtml(
+ 'Hello',
+ 'Hello world',
+ 'word'
+ );
+ expect(result).toContain('<b>Hello');
+ expect(result).toContain(' world');
+ });
+
+ it('should escape & character', () => {
+ const result = compareTextsHtml('AT&T', 'AT&T rocks', 'word');
+ expect(result).toContain('AT&T');
+ });
+
+ it('should wrap result in a diff-line div', () => {
+ const result = compareTextsHtml('foo', 'bar', 'word');
+ expect(result.startsWith('')).toBe(true);
+ expect(result.endsWith('
')).toBe(true);
+ });
+
+ it('should handle empty input strings', () => {
+ const result = compareTextsHtml('', '', 'word');
+ expect(result).toBe('');
+ });
+
+ it('should handle only added input', () => {
+ const result = compareTextsHtml('', 'New text', 'word');
+ expect(result).toContain('New text');
+ });
+
+ it('should handle only removed input', () => {
+ const result = compareTextsHtml('Old text', '', 'word');
+ expect(result).toContain('Old text');
+ });
+
+ it('should convert newlines to
', () => {
+ const result = compareTextsHtml('line1\nline2', 'line1\nline2', 'word');
+ expect(result).toContain('
');
+ });
+
+ it('should highlight individual characters with char level', () => {
+ const result = compareTextsHtml('color', 'colour', 'char');
+ expect(result).toContain('u');
+ });
+});
diff --git a/src/pages/tools/string/text-compare/types.ts b/src/pages/tools/string/text-compare/types.ts
new file mode 100644
index 0000000..441f9e4
--- /dev/null
+++ b/src/pages/tools/string/text-compare/types.ts
@@ -0,0 +1,5 @@
+export type level = 'word' | 'char';
+
+export type InitialValuesType = {
+ level: level;
+};
diff --git a/src/utils/string.ts b/src/utils/string.ts
index b5a4cc0..e874c28 100644
--- a/src/utils/string.ts
+++ b/src/utils/string.ts
@@ -138,3 +138,49 @@ export const getI18nNamespaceFromToolCategory = (
return 'translation';
};
+
+/**
+ * Strips HTML tags from a string and decodes HTML entities into plain text.
+ *
+ * This function parses the provided HTML string using the browser's DOMParser,
+ * extracts the text content, and returns a clean, human-readable string.
+ *
+ * @param html - A string containing HTML markup
+ * @returns A plain text string with all HTML tags removed and entities decoded
+ *
+ * @example
+ * stripAndDecodeHtml('Hello world
')
+ * // returns: 'Hello world'
+ *
+ * @example
+ * stripAndDecodeHtml('<div>Test</div>')
+ * // returns: 'Test
'
+ */
+export function stripAndDecodeHtml(html: string): string {
+ const doc = new DOMParser().parseFromString(html, 'text/html');
+ return doc.body.textContent ?? '';
+}
+
+/**
+ * Escapes special HTML characters in a string to prevent HTML injection.
+ *
+ * Replaces the following characters with their corresponding HTML entities:
+ * - & → &
+ * - < → <
+ * - > → >
+ * - " → "
+ *
+ * @param {string} str - The input string to escape.
+ * @returns {string} The escaped string safe for insertion into HTML.
+ *
+ * @example
+ * escapeHtml('Hello & welcome
');
+ * // Returns: '<div class="test">Hello & welcome</div>'
+ */
+export function escapeHtml(str: string): string {
+ return str
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"');
+}