diff --git a/package-lock.json b/package-lock.json index de9fc4e..e47a4a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "3.0.2", "license": "MIT", "dependencies": { + "html-entities": "^2.6.0", "marked": "^16.0.0" }, "devDependencies": { @@ -5504,6 +5505,22 @@ "dev": true, "license": "MIT" }, + "node_modules/html-entities": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", + "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", diff --git a/package.json b/package.json index f4ce8bd..799cfad 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "react": "^16.8.0 || >=17.0.0" }, "dependencies": { + "html-entities": "^2.6.0", "marked": "^16.0.0" }, "devDependencies": { diff --git a/src/helpers.ts b/src/helpers.ts index 2d2f587..bb6c60e 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,19 +1,11 @@ -const htmlUnescapes: Record = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - ''': "'", - ' ': ' ', - ' ': ' ', -}; - -/** Used to match HTML entities and HTML characters. */ -const reEscapedHtml = /&(?:amp|lt|gt|quot|nbsp|#(?:0+)?(?:39|160));/g; -const reHasEscapedHtml = RegExp(reEscapedHtml.source); +import { decode } from 'html-entities'; export const unescape = (str = '') => { - return reHasEscapedHtml.test(str) ? str.replace(reEscapedHtml, (entity) => htmlUnescapes[entity] || "'") : str; + if (!str || !str.includes('&')) { + return str; + } + + return decode(str); }; export const joinBase = (path: string, base?: string) => { diff --git a/tests/helpers.spec.ts b/tests/helpers.spec.ts index 518ac95..98b39aa 100644 --- a/tests/helpers.spec.ts +++ b/tests/helpers.spec.ts @@ -9,15 +9,52 @@ describe('Helpers', () => { expect(joinBase('/path', 'http:')).toBe('/path'); }); - it('should unescape strings', () => { + it('should unescape basic entities', () => { expect(unescape('&')).toBe('&'); expect(unescape('<')).toBe('<'); expect(unescape('>')).toBe('>'); expect(unescape('"')).toBe('"'); expect(unescape(''')).toBe("'"); - expect(unescape(' ')).toBe(' '); - expect(unescape(' ')).toBe(' '); + }); + + it('should unescape named entities', () => { + expect(unescape(' ')).toBe('\u00A0'); + expect(unescape('©')).toBe('©'); + expect(unescape('®')).toBe('®'); + expect(unescape('™')).toBe('™'); + expect(unescape('€')).toBe('€'); + }); + + it('should unescape numeric character references', () => { + expect(unescape('@')).toBe('@'); + expect(unescape('€')).toBe('€'); + expect(unescape('@')).toBe('@'); + expect(unescape('€')).toBe('€'); + expect(unescape(' ')).toBe('\u00A0'); + }); + + it('should unescape exotic entities', () => { + expect(unescape('♥')).toBe('♥'); + expect(unescape('♠')).toBe('♠'); + expect(unescape('♣')).toBe('♣'); + expect(unescape('♦')).toBe('♦'); + expect(unescape('α')).toBe('α'); + expect(unescape('β')).toBe('β'); + expect(unescape('γ')).toBe('γ'); + expect(unescape('π')).toBe('π'); + expect(unescape('∑')).toBe('∑'); + expect(unescape('∞')).toBe('∞'); + expect(unescape('≈')).toBe('≈'); + expect(unescape('≠')).toBe('≠'); + expect(unescape('≤')).toBe('≤'); + expect(unescape('≥')).toBe('≥'); + }); + + it('should handle edge cases', () => { + expect(unescape('Tom & Jerry <div>')).toBe('Tom & Jerry
'); expect(unescape('')).toBe(''); expect(unescape()).toBe(''); + expect(unescape('No entities here')).toBe('No entities here'); + expect(unescape('Just & ampersand')).toBe('Just & ampersand'); }); });