Repository: remarkablemark/html-dom-parser
Pull request #902 (2024-12-01 10:45:47)

fix: enable client parser to retain carriage return characters
<!--
Filling out the information below can facilitate the review/merge of the pull request (PR).
-->

## What is the motivation for this pull request?

Bug fix

## What is the current behavior?

Addresses [html-dom-parser #420](https://github.com/remarkablemark/html-dom-parser/issues/420)
Addresses [html-react-parser #864](https://github.com/remarkablemark/html-react-parser/issues/864)

## What is the new behavior?

const text = "toto \r\n toto"

parse(text)

old output: "toto toto"
new output: "toto \r\n toto"

## Checklist:

<!--
Feel free to remove any item that is irrelevant to your changes.
To check an item, place an "x" in the box like so: `- [x] Tests`
-->

- [x] [Conventional Commits](https://www.conventionalcommits.org/)
- [ ] Tests



codecov[bot]: ## [Codecov](https://app.codecov.io/gh/remarkablemark/html-dom-parser/pull/902?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Mark) Report
Attention: Patch coverage is `66.66667%` with `2 lines` in your changes missing coverage. Please review.
> Project coverage is 91.42%. Comparing base [(`d21dbaa`)](https://app.codecov.io/gh/remarkablemark/html-dom-parser/commit/d21dbaa968df849754533644c2e23abfd9f69971?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Mark) to head [(`bf5ef50`)](https://app.codecov.io/gh/remarkablemark/html-dom-parser/commit/bf5ef506c8ae8562c3929bb2f5ad1d92145f753a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Mark).

| [Files with missing lines](https://app.codecov.io/gh/remarkablemark/html-dom-parser/pull/902?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Mark) | Patch % | Lines |
|---|---|---|
| [src/client/domparser.ts](https://app.codecov.io/gh/remarkablemark/html-dom-parser/pull/902?src=pr&el=tree&filepath=src%2Fclient%2Fdomparser.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Mark#diff-c3JjL2NsaWVudC9kb21wYXJzZXIudHM=) | 50.00% | [2 Missing :warning: ](https://app.codecov.io/gh/remarkablemark/html-dom-parser/pull/902?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Mark) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #902      +/-   ##
==========================================
- Coverage   92.59%   91.42%   -1.17%     
==========================================
  Files           7        7              
  Lines         135      140       +5     
  Branches       30       31       +1     
==========================================
+ Hits          125      128       +3     
- Misses          8       10       +2     
  Partials        2        2              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/remarkablemark/html-dom-parser/pull/902?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Mark).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Mark).

kingflamez: @remarkablemark added tests

Patch:
diff --git a/package-lock.json b/package-lock.json
index e6e3241..f369cc8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1923,8 +1923,7 @@
       "version": "4.3.16",
       "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.16.tgz",
       "integrity": "sha512-PatH4iOdyh3MyWtmHVFXLWCCIhUbopaltqddG9BzB+gMIzee2MJrvd+jouii9Z3wzQJruGWAm7WOMjgfG8hQlQ==",
-      "dev": true,
-      "license": "MIT"
+      "dev": true
     },
     "node_modules/@types/conventional-commits-parser": {
       "version": "5.0.0",
@@ -3600,10 +3599,12 @@
       }
     },
     "node_modules/define-properties": {
-      "version": "1.2.0",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+      "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
       "dev": true,
-      "license": "MIT",
       "dependencies": {
+        "define-data-property": "^1.0.1",
         "has-property-descriptors": "^1.0.0",
         "object-keys": "^1.1.1"
       },
@@ -13276,9 +13277,12 @@
       }
     },
     "define-properties": {
-      "version": "1.2.0",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+      "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
       "dev": true,
       "requires": {
+        "define-data-property": "^1.0.1",
         "has-property-descriptors": "^1.0.0",
         "object-keys": "^1.1.1"
       }
diff --git a/src/client/domparser.ts b/src/client/domparser.ts
index ae25290..9851335 100644
--- a/src/client/domparser.ts
+++ b/src/client/domparser.ts
@@ -1,3 +1,5 @@
+import { escapeSpecialCharacters } from './utilities';
+
 // constants
 const HTML = 'html';
 const HEAD = 'head';
@@ -116,6 +118,9 @@ if (template && template.content) {
  * @returns - DOM nodes.
  */
 export default function domparser(html: string): NodeList {
+  // Escape special characters before parsing
+  html = escapeSpecialCharacters(html);
+
   const match = html.match(FIRST_TAG_REGEX);
   const firstTagName = match && match[1] ? match[1].toLowerCase() : '';
 
diff --git a/src/client/utilities.ts b/src/client/utilities.ts
index 7440ac3..05ed18c 100644
--- a/src/client/utilities.ts
+++ b/src/client/utilities.ts
@@ -51,6 +51,26 @@ function formatTagName(tagName: string): string {
   return tagName;
 }
 
+/**
+ * Escapes special characters before parsing.
+ *
+ * @param html - The HTML string.
+ * @returns - HTML string with escaped special characters.
+ */
+export function escapeSpecialCharacters(html: string): string {
+  return html.replace(/\r/g, '\\r');
+}
+
+/**
+ * Reverts escaped special characters back to actual characters.
+ *
+ * @param text - The text with escaped characters.
+ * @returns - Text with escaped characters reverted.
+ */
+export function revertEscapedCharacters(text: string): string {
+  return text.replace(/\\r/g, '\r');
+}
+
 /**
  * Transforms DOM nodes to `domhandler` nodes.
  *
@@ -95,7 +115,7 @@ export function formatDOM(
       }
 
       case 3:
-        current = new Text(node.nodeValue!);
+        current = new Text(revertEscapedCharacters(node.nodeValue!));
         break;
 
       case 8:


Test patch:
diff --git a/test/cases/html.js b/test/cases/html.js
index d747d5a..e946331 100644
--- a/test/cases/html.js
+++ b/test/cases/html.js
@@ -305,6 +305,28 @@ module.exports = [
     data: ' ',
   },
 
+  // text with special characters
+  {
+    name: 'text with carriage return',
+    data: 'Hello\rWorld',
+  },
+  {
+    name: 'text with multiple carriage returns',
+    data: 'Hello\rDear\rWorld',
+  },
+  {
+    name: 'text with mixed newlines and carriage returns',
+    data: 'Hello\rWorld\nNew\rLine',
+  },
+  {
+    name: 'text with carriage return in tag',
+    data: '<div>Hello\rWorld</div>',
+  },
+  {
+    name: 'nested tags with carriage returns',
+    data: '<div>Hello\r<span>Beautiful\r</span>World</div>',
+  },
+
   // custom tag
   {
     name: 'custom tag',
diff --git a/test/server/client.test.ts b/test/server/client.test.ts
index 24635a0..8bc5a1f 100644
--- a/test/server/client.test.ts
+++ b/test/server/client.test.ts
@@ -1,6 +1,8 @@
 import { expect } from 'chai';
 
 import { formatDOM } from '../../src/client/utilities';
+import { revertEscapedCharacters } from '../../src/client/utilities';
+import { escapeSpecialCharacters } from '../../src/client/utilities';
 
 describe('client utilities', () => {
   describe('formatDOM', () => {
@@ -10,4 +12,62 @@ describe('client utilities', () => {
       ).to.deep.equal([]);
     });
   });
+
+  describe('escapeSpecialCharacters', () => {
+    it('escapes carriage return characters', () => {
+      const input = 'Hello\rWorld';
+      const expected = 'Hello\\rWorld';
+      expect(escapeSpecialCharacters(input)).to.equal(expected);
+    });
+
+    it('does not modify strings without special characters', () => {
+      const input = 'Hello World';
+      expect(escapeSpecialCharacters(input)).to.equal(input);
+    });
+
+    it('handles empty strings', () => {
+      expect(escapeSpecialCharacters('')).to.equal('');
+    });
+
+    it('handles multiple carriage returns', () => {
+      const input = 'Hello\rDear\rWorld';
+      const expected = 'Hello\\rDear\\rWorld';
+      expect(escapeSpecialCharacters(input)).to.equal(expected);
+    });
+
+    it('only escapes carriage returns', () => {
+      const input = 'Hello\rWorld\n'; // \n should not be affected
+      const expected = 'Hello\\rWorld\n';
+      expect(escapeSpecialCharacters(input)).to.equal(expected);
+    });
+  });
+
+  describe('revertEscapedCharacters', () => {
+    it('reverts escaped carriage return characters', () => {
+      const input = 'Hello\\rWorld';
+      const expected = 'Hello\rWorld';
+      expect(revertEscapedCharacters(input)).to.equal(expected);
+    });
+
+    it('does not modify strings without escaped characters', () => {
+      const input = 'Hello World';
+      expect(revertEscapedCharacters(input)).to.equal(input);
+    });
+
+    it('handles empty strings', () => {
+      expect(revertEscapedCharacters('')).to.equal('');
+    });
+
+    it('handles multiple escaped carriage returns', () => {
+      const input = 'Hello\\rDear\\rWorld';
+      const expected = 'Hello\rDear\rWorld';
+      expect(revertEscapedCharacters(input)).to.equal(expected);
+    });
+
+    it('only reverts escaped carriage returns', () => {
+      const input = 'Hello\\rWorld\\n'; // \n should not be affected
+      const expected = 'Hello\rWorld\\n';
+      expect(revertEscapedCharacters(input)).to.equal(expected);
+    });
+  });
 });