Repository: lbenie/reading-time-estimator
Pull request #1835: feat: strips HTML content from input
Author: login: lbenie
Created: 2025-03-23T19:42:35Z | Merged: 2025-03-23T20:03:21Z

Closes: https://github.com/lbenie/reading-time-estimator/issues/1761


Diff:
diff --git a/.vscode/settings.json b/.vscode/settings.json
index d7ecf975..0b790dca 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -28,7 +28,7 @@
     "javascript",
     "typescript"
   ],
-  "eslint.experimental.useFlatConfig": true,
+  "eslint.useFlatConfig": true,
   "editor.codeActionsOnSave": {
     "source.fixAll.eslint": "explicit"
   }
diff --git a/__tests__/reading-time-estimation.spec.ts b/__tests__/reading-time-estimation.spec.ts
index 846fdeff..855b37bb 100755
--- a/__tests__/reading-time-estimation.spec.ts
+++ b/__tests__/reading-time-estimation.spec.ts
@@ -29,7 +29,6 @@ const slovakText = `Reading Time Estimator bol vytvorený s cieľom poskytnúť
 
 const czechText = `Reading Time Estimator byl vytvořen s cílem poskytnout odhad, jak dlouhou dobu je zapotřebí k přečtení článku nebo blogu tak, jak je to implementováno na stránce medium.`
 
-
 interface TestSetup {
   readonly language: SupportedLanguages | undefined
   readonly words: string | undefined
@@ -193,4 +192,16 @@ describe('readingTime', () => {
       expect(result).toStrictEqual(expectedResult)
     },
   )
+
+  it('strips out HTML tags', () => {
+    const text = `<p>Reading Time Estimator</p>`
+
+    const result = readingTime(text, 10, 'en')
+
+    expect(result).toStrictEqual({
+      minutes: 0,
+      words: 3,
+      text: 'less than a minute read',
+    })
+  })
 })
diff --git a/auto-imports.d.ts b/auto-imports.d.ts
index 13dd447e..a122e6ae 100644
--- a/auto-imports.d.ts
+++ b/auto-imports.d.ts
@@ -3,6 +3,7 @@
 // @ts-nocheck
 // noinspection JSUnusedGlobalSymbols
 // Generated by unplugin-auto-import
+// biome-ignore lint: disable
 export {}
 declare global {
   const afterAll: typeof import('vitest')['afterAll']
diff --git a/lib/reading-time-estimator.ts b/lib/reading-time-estimator.ts
index 672b25b7..7cab928f 100644
--- a/lib/reading-time-estimator.ts
+++ b/lib/reading-time-estimator.ts
@@ -1,5 +1,6 @@
 import { translations } from './i18n'
 import type { SupportedLanguages } from './i18n'
+import sanitizeHtml from 'sanitize-html'
 
 /**
  * Reading time estimator result shape
@@ -19,14 +20,23 @@ type ReadingTime = {
   readonly text: string
 }
 
+/**
+ * Strips HTML tags from the input text
+ * @param {string} data - The text to be stripped of HTML tags
+ * @returns {string} The text without HTML tags
+ */
+const stripHtmlTags = (data: string) =>
+  sanitizeHtml(data, { allowedTags: [], allowedAttributes: {} })
+
 /**
  * Parses the text and returns an array of words
  * @param {string} data - The text to be analyzed
  * @returns {ReadonlyArray<string>} Parsed chinese, japanese and accented text
  */
 const parseWords = (data: string) => {
+  const strippedHTML = stripHtmlTags(data)
   const words =
-    data.match(
+    strippedHTML.match(
       /[\w|\d|\s|,|.|\u00C0-\u024F|\u4E00-\u9FA5|\u3041-\u309F]+/giu,
     ) ?? []
   return words.flatMap((word) => word.match(/[\u4E00-\u9FA5]/gu) ?? word)
diff --git a/package.json b/package.json
index 772e6977..dd68011b 100644
--- a/package.json
+++ b/package.json
@@ -60,6 +60,7 @@
     "@semantic-release/github": "11.0.1",
     "@semantic-release/release-notes-generator": "14.0.3",
     "@types/node": "22.13.11",
+    "@types/sanitize-html": "^2.13.0",
     "@vitest/coverage-v8": "^2.0.5",
     "all-contributors-cli": "6.26.1",
     "concurrently": "9.1.2",
@@ -73,6 +74,8 @@
     "vite": "6.2.2",
     "vitest": "^2.0.5"
   },
-  "dependencies": {},
+  "dependencies": {
+    "sanitize-html": "^2.15.0"
+  },
   "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
 }
diff --git a/tsconfig.json b/tsconfig.json
index 972633c8..6fe5c862 100755
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -13,7 +13,8 @@
     "emitDecoratorMetadata": true,
     "declaration": true,
     "strictNullChecks": true,
-    "declarationDir": "dist/types"
+    "declarationDir": "dist/types",
+    "allowSyntheticDefaultImports": true,
   },
   "include": ["lib/**/*.ts"],
   "exclude": ["node_modules", "*.spec.ts"]
diff --git a/yarn.lock b/yarn.lock
index 1db9cc11..7fd78d19 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1369,6 +1369,13 @@
   dependencies:
     "@types/node" "*"
 
+"@types/sanitize-html@^2.13.0":
+  version "2.13.0"
+  resolved "https://registry.yarnpkg.com/@types/sanitize-html/-/sanitize-html-2.13.0.tgz#ac3620e867b7c68deab79c72bd117e2049cdd98e"
+  integrity sha512-X31WxbvW9TjIhZZNyNBZ/p5ax4ti7qsNDBDEnH4zAgmEh35YnFD1UiS6z9Cd34kKm0LslFW0KPmTQzu/oGtsqQ==
+  dependencies:
+    htmlparser2 "^8.0.0"
+
 "@types/semver@^7.5.5":
   version "7.5.8"
   resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e"
@@ -2526,6 +2533,11 @@ deepmerge-ts@^7.1.3:
   resolved "https://registry.yarnpkg.com/deepmerge-ts/-/deepmerge-ts-7.1.3.tgz#9a07e5e7dff7afa8ddf48b90b7161ca9439ca4ca"
   integrity sha512-qCSH6I0INPxd9Y1VtAiLpnYvz5O//6rCfJXKk0z66Up9/VOSr+1yS8XSKA5IWRxjocFGlzPyaZYe+jxq7OOLtQ==
 
+deepmerge@^4.2.2:
+  version "4.3.1"
+  resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
+  integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
+
 defaults@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d"
@@ -5391,6 +5403,11 @@ parse-ms@^4.0.0:
   resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-4.0.0.tgz#c0c058edd47c2a590151a718990533fd62803df4"
   integrity sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==
 
+parse-srcset@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1"
+  integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==
+
 parse5-htmlparser2-tree-adapter@^6.0.0, parse5-htmlparser2-tree-adapter@^6.0.1:
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
@@ -5651,7 +5668,7 @@ postcss-value-parser@^4.2.0:
   resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
   integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
 
-postcss@8.5.3, postcss@^8.5.3:
+postcss@8.5.3, postcss@^8.3.11, postcss@^8.5.3:
   version "8.5.3"
   resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.3.tgz#1463b6f1c7fb16fe258736cba29a2de35237eafb"
   integrity sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==
@@ -6050,6 +6067,18 @@ safe-buffer@~5.2.0:
   resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
   integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
 
+sanitize-html@^2.15.0:
+  version "2.15.0"
+  resolved "https://registry.yarnpkg.com/sanitize-html/-/sanitize-html-2.15.0.tgz#8e7f97ee1fecdec1bb1fb2e37f6d2c6acfdbabe3"
+  integrity sha512-wIjst57vJGpLyBP8ioUbg6ThwJie5SuSIjHxJg53v5Fg+kUK+AXlb7bK3RNXpp315MvwM+0OBGCV6h5pPHsVhA==
+  dependencies:
+    deepmerge "^4.2.2"
+    escape-string-regexp "^4.0.0"
+    htmlparser2 "^8.0.0"
+    is-plain-object "^5.0.0"
+    parse-srcset "^1.0.2"
+    postcss "^8.3.11"
+
 sax@~1.2.4:
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"


Reviews:
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-23T19:44:03Z
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR adds functionality to strip HTML tags from the input text in the reading time estimator, addressing issue https://github.com/lbenie/reading-time-estimator/issues/1761. The changes include:
- Adding a new helper function (stripHtmlTags) to remove HTML tags.
- Updating the parseWords function to operate on the HTML-stripped text.
- Introducing tests to validate that HTML content is correctly removed.

### Reviewed Changes

Copilot reviewed 2 out of 3 changed files in this pull request and generated no comments.

| File                                                 | Description                                                           |
| ---------------------------------------------------- | --------------------------------------------------------------------- |
| __tests__/reading-time-estimation.spec.ts           | Added a test to verify that HTML tags are removed as expected.         |
| lib/reading-time-estimator.ts                         | Added stripHtmlTags helper and updated parseWords to use it.            |

<details>
<summary>Files not reviewed (1)</summary>

* **.vscode/settings.json**: Language not supported
</details>



state: COMMENTED
author_association: NONE
submitted_at: 2025-03-23T19:44:30Z
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-23T19:50:05Z

Review comments:
diff_hunk: @@ -19,14 +19,22 @@
   readonly text: string
 }
 
+/**
+ * Strips HTML tags from the input text
+ * @param {string} data - The text to be stripped of HTML tags
+ * @returns {string} The text without HTML tags
+ */
+const stripHtmlTags = (data: string) => data.replace(/<\/?[^>]+(>|$)/g, '')
path: lib/reading-time-estimator.ts
user: login: github-advanced-security[bot]
body: ## Polynomial regular expression used on uncontrolled data

This [regular expression](1) that depends on [library input](2) may run slow on strings starting with '<' and with many repetitions of '</'.

[Show more details](https://github.com/lbenie/reading-time-estimator/security/code-scanning/4)
created_at: 2025-03-23T19:44:03Z
author_association: NONE
diff_hunk: @@ -19,14 +19,22 @@
   readonly text: string
 }
 
+/**
+ * Strips HTML tags from the input text
+ * @param {string} data - The text to be stripped of HTML tags
+ * @returns {string} The text without HTML tags
+ */
+const stripHtmlTags = (data: string) => data.replace(/<[^>]*>/g, '')
path: lib/reading-time-estimator.ts
user: login: github-advanced-security[bot]
body: ## Polynomial regular expression used on uncontrolled data

This [regular expression](1) that depends on [library input](2) may run slow on strings starting with '<' and with many repetitions of '<'.

[Show more details](https://github.com/lbenie/reading-time-estimator/security/code-scanning/5)
created_at: 2025-03-23T19:50:05Z
author_association: NONE
diff_hunk: @@ -19,14 +19,22 @@
   readonly text: string
 }
 
+/**
+ * Strips HTML tags from the input text
+ * @param {string} data - The text to be stripped of HTML tags
+ * @returns {string} The text without HTML tags
+ */
+const stripHtmlTags = (data: string) => data.replace(/<[^>]*>/g, '')
path: lib/reading-time-estimator.ts
user: login: github-advanced-security[bot]
body: ## Incomplete multi-character sanitization

This string may still contain [<script](1), which may cause an HTML element injection vulnerability.

[Show more details](https://github.com/lbenie/reading-time-estimator/security/code-scanning/6)
created_at: 2025-03-23T19:50:05Z
author_association: NONE

PR comments:
user: login: MickL
created_at: 2025-03-23T21:02:32Z
body: @lbenie Looks amazing, thanks!
Is reading-time-estimator now always removing html or does it need to be activated by a setting? I didnt see an update to the readme that explains this :)
author_association: NONE
user: login: lbenie
created_at: 2025-03-24T12:59:31Z
body: > @lbenie Looks amazing, thanks! Is reading-time-estimator now always removing html or does it need to be activated by a setting? I didnt see an update to the readme that explains this :)

It's always stripping the HTML, good call I'll add something to the README
author_association: OWNER