Repository: thlorenz/doctoc (2026-03-13T04:30:10Z)

Issue:
Invalid Regular Expression with `h#`
The following README file produces an error:

```
This readme causes an error `<p>` text  `<h1>` more text (`</p>`)
```

However, when the "h" is removed, the code runs just fine. Here is the error log:

```
/usr/local/lib/node_modules/doctoc/lib/get-html-headers.js:11
      if (new RegExp(x.text[0]).test(line)) {
          ^
SyntaxError: Invalid regular expression: /` element tells the rendering program that the content inside is a form.

The end tag is just like the start tag except that it has a forward slash added just after the less-than sign (`/: Unterminated group
    at new RegExp (native)
    at /usr/local/lib/node_modules/doctoc/lib/get-html-headers.js:11:11
    at Array.map (native)
    at addLinenos (/usr/local/lib/node_modules/doctoc/lib/get-html-headers.js:8:18)
    at module.exports (/usr/local/lib/node_modules/doctoc/lib/get-html-headers.js:66:13)
    at transform (/usr/local/lib/node_modules/doctoc/lib/transform.js:123:13)
    at /usr/local/lib/node_modules/doctoc/doctoc.js:25:20
    at Array.map (native)
    at transformAndSave (/usr/local/lib/node_modules/doctoc/doctoc.js:23:6)
    at Object.<anonymous> (/usr/local/lib/node_modules/doctoc/doctoc.js:100:3)
```



Discussion:

Patch:
diff --git a/lib/get-html-headers.js b/lib/get-html-headers.js
index 0c1077be..e3b5786b 100644
--- a/lib/get-html-headers.js
+++ b/lib/get-html-headers.js
@@ -28,7 +28,7 @@ function addLinenos(lines, headers) {
 function rankify(headers, max, min) {
   return headers
     .map(function (x) {
-      x.rank = parseInt(x.tag.slice(1), 10);
+      x.rank = parseInt(x.tagName.slice(1), 10);
       return x;
     })
     .filter(function (x) {
@@ -48,7 +48,8 @@ var go = (module.exports = function (lines, maxHeaderLevel, minHeaderLevel) {
     .join("\n");
 
   //var headers = [], grabbing = null, text = [];
-  var headers = [],
+  var currentLine = 1,
+    headers = [],
     grabbing = [],
     text = [];
 
@@ -56,24 +57,35 @@ var go = (module.exports = function (lines, maxHeaderLevel, minHeaderLevel) {
     {
       onopentag: function (name, attr) {
         // Short circuit if we're already inside a pre
-        if (grabbing[grabbing.length - 1] === "pre") return;
+        if (grabbing[grabbing.length - 1]?.tagName === "pre") return;
 
         if (name === "pre" || /h\d/.test(name)) {
-          grabbing.push(name);
+          grabbing.push({ tagName: name, line: currentLine });
         }
       },
       ontext: function (text_) {
+        // Count newlines
+        let newlines = text_.match(/\n/g);
+        if (newlines) currentLine += newlines.length;
+
         // Explicitly skip pre tags, and implicitly skip all others
-        if (grabbing.length === 0 || grabbing[grabbing.length - 1] === "pre")
+        if (grabbing.length === 0 || grabbing[grabbing.length - 1].tagName === "pre")
           return;
 
-        text.push(text_);
+        // Only accept text on the SAME LINE as the opening <hX> otherwise discard it
+        let top = grabbing[grabbing.length - 1];
+        if (currentLine === top.line) {
+          text.push(text_);
+        }
+        else{
+          grabbing.pop();
+        }
       },
       onclosetag: function (name) {
         if (grabbing.length === 0) return;
-        if (grabbing[grabbing.length - 1] === name) {
+        if (grabbing[grabbing.length - 1].tagName === name) {
           var tag = grabbing.pop();
-          headers.push({ text: text, tag: tag });
+          headers.push({ text: text, tagName: tag.tagName });
           text = [];
         }
       },


Test patch:
diff --git a/test/fixtures/readme-with-hX.md b/test/fixtures/readme-with-hX.md
new file mode 100644
index 00000000..26670316
--- /dev/null
+++ b/test/fixtures/readme-with-hX.md
@@ -0,0 +1,25 @@
+<!-- START doctoc -->
+<!-- END doctoc -->
+
+# Hello, world!
+
+This readme contains `<h2>` and <h3> html tags. When the heading tags are not open and closed on the same line, they are not considered titles for the TOC. 
+For ones that are open and closed on the same line, they are included in the TOC.
+
+`<h2>`This is invalid `</h2>`
+
+when it is embeded <h2>This remains invalid </h2> due to being embeded and detected as a raw text node
+
+<h2>Random2
+
+Close </h2>
+
+<h3>Random3
+
+<h3>Random4
+
+## Installation
+
+<h2><b>API</b></h2>
+
+## License
diff --git a/test/transform.js b/test/transform.js
index e1d1b8b3..0feebfbd 100644
--- a/test/transform.js
+++ b/test/transform.js
@@ -779,3 +779,21 @@ test('\nduplicate titles but with different symbols', function (t) {
 
   t.end()
 })
+
+test('\nignores the hX which is in the content on or a different line', function (t) {
+  var content = require('fs').readFileSync(__dirname + '/fixtures/readme-with-hX.md', 'utf8');
+  var headers = transform(content, 'github.com', 8);
+
+  t.same(
+      headers.toc.split('\n')
+    , [ '',
+        '- [Hello, world!](#hello-world)',
+        '  - [Installation](#installation)',
+        '  - [API](#api)',
+        '  - [License](#license)',
+        '' ]
+    , 'generates correct toc for non html and html headers'
+  )
+
+  t.end()
+})