Repository: thlorenz/doctoc (2026-03-19T14:31:57Z)

Issue:
bug: doctoc does not respect "id" attribute of headers when constructing the TOC
When I try to generate TOC for a file that starts like such:

```md
<!-- START doctoc -->
<!-- END doctoc -->

# Test 1

<h1 id="test-a">Test á</a>
```

I'd expect doctoc to generate a second TOC entry that respects the explicit "id":

```md
<!-- START doctoc -->
- [Test 1](#test-1)
- [Test á](#test-a)
<!-- END doctoc -->

# Test 1

<h1 id="teste-a">Teste á</h1>
```

Instead, I get an autogenerated slug regardless:

```md
<!-- START doctoc -->
- [Test 1](#test-1)
- [Test á](#test-%C3%A1)
<!-- END doctoc -->

# Test 1

<h1 id="teste-a">Teste á</h1>
```

Is that correct? If not, how difficult would it be for a fix to be made? I can help, if someone points me to the right places.



Discussion:
Seems like a reasonable assumption to make, and sounds like it could be an improvement!

The codebase is pretty small, I think [here](https://github.com/thlorenz/doctoc/blob/master/lib/get-html-headers.js) would be a decent place to begin your investigation.



Patch:
diff --git a/lib/get-html-headers.js b/lib/get-html-headers.js
index e3b5786b..b8134ce1 100644
--- a/lib/get-html-headers.js
+++ b/lib/get-html-headers.js
@@ -60,7 +60,7 @@ var go = (module.exports = function (lines, maxHeaderLevel, minHeaderLevel) {
         if (grabbing[grabbing.length - 1]?.tagName === "pre") return;
 
         if (name === "pre" || /h\d/.test(name)) {
-          grabbing.push({ tagName: name, line: currentLine });
+          grabbing.push({ tagName: name, line: currentLine, tagId: attr.id });
         }
       },
       ontext: function (text_) {
@@ -85,7 +85,7 @@ var go = (module.exports = function (lines, maxHeaderLevel, minHeaderLevel) {
         if (grabbing.length === 0) return;
         if (grabbing[grabbing.length - 1].tagName === name) {
           var tag = grabbing.pop();
-          headers.push({ text: text, tagName: tag.tagName });
+          headers.push({ text: text, tagName: tag.tagName, href: tag.tagId });
           text = [];
         }
       },
diff --git a/lib/transform.js b/lib/transform.js
index dd467695..bd1328b3 100644
--- a/lib/transform.js
+++ b/lib/transform.js
@@ -37,7 +37,7 @@ function matchesEnd(syntax) {
 function notNull(x) { return  x !== null; }
 
 function addAnchor(mode, header) {
-  header.anchor = anchor(header.name, mode, header.instance);
+  header.anchor = anchor(header.name, mode, header.instance, header.href);
   return header;
 }
 
@@ -100,9 +100,9 @@ function countHeaders(headers, mode) {
 
   for (var i = 0; i < headers.length; i++) {
     var header = headers[i];
-    var name = anchor(header.name, mode).split('#')[1].slice(0, -1);
+    var name = header.href || anchor(header.name, mode).split('#')[1].slice(0, -1);
 
-    if (Object.prototype.hasOwnProperty.call(instances, name)) {
+    if (header.href === undefined && Object.prototype.hasOwnProperty.call(instances, name)) {
       // `instances.hasOwnProperty(name)` fails when there’s an instance named "hasOwnProperty".
       instances[name]++;
     } else {
@@ -110,7 +110,6 @@ function countHeaders(headers, mode) {
     }
 
     header.instance = instances[name];
-    header.tag = name;
   }
 
   return headers;
diff --git a/package-lock.json b/package-lock.json
index 5f5eac49..e9231f27 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,7 +10,7 @@
       "license": "MIT",
       "dependencies": {
         "@textlint/markdown-to-ast": "^15.5.2",
-        "anchor-markdown-header": "^0.8.2",
+        "anchor-markdown-header": "^0.8.3",
         "htmlparser2": "^7.2.0",
         "loglevel": "^1.9.2",
         "minimist": "^1.2.6",
@@ -1196,9 +1196,9 @@
       }
     },
     "node_modules/anchor-markdown-header": {
-      "version": "0.8.2",
-      "resolved": "https://registry.npmjs.org/anchor-markdown-header/-/anchor-markdown-header-0.8.2.tgz",
-      "integrity": "sha512-ix0Hx6ARkHOsQRmt1++ZmjURq4Pr5MGXQJjh0lQ/l5jTpTURn4aqhbZ+AJMpZ/Sd3JiyNwi7KaeiF64OsMGCPg==",
+      "version": "0.8.3",
+      "resolved": "https://registry.npmjs.org/anchor-markdown-header/-/anchor-markdown-header-0.8.3.tgz",
+      "integrity": "sha512-i2JealiBR1stamxKBE+LDebJ5p/0GPffwEs05KaYqG+3oS25Semk4AE+YG5lUsCGZNYp70I9K57tfqsm8Pdwxg==",
       "license": "MIT",
       "dependencies": {
         "emoji-regex": "~10.6.0",
diff --git a/package.json b/package.json
index b2c2a8f3..d6d2ad26 100644
--- a/package.json
+++ b/package.json
@@ -18,7 +18,7 @@
   "bin": "doctoc.js",
   "dependencies": {
     "@textlint/markdown-to-ast": "^15.5.2",
-    "anchor-markdown-header": "^0.8.2",
+    "anchor-markdown-header": "^0.8.3",
     "htmlparser2": "^7.2.0",
     "loglevel": "^1.9.2",
     "minimist": "^1.2.6",


Test patch:
diff --git a/test/transform-html.js b/test/transform-html.js
index 8692763f..d5574974 100644
--- a/test/transform-html.js
+++ b/test/transform-html.js
@@ -14,12 +14,12 @@ test('\ngiven a file that includes html with header tags and maxHeaderLevel 8',
         '',
         '- [Installation](#installation)',
         '- [API](#api)',
-        '    - [dockops::Containers(docker) → {Object}](#dockopscontainersdocker-%E2%86%92-object)',
+        '    - [dockops::Containers(docker) → {Object}](#dockops::Containers)',
         '      - [Parameters:](#parameters)',
         '      - [Returns:](#returns)',
-        '    - [dockops::Containers::activePorts(cb)](#dockopscontainersactiveportscb)',
+        '    - [dockops::Containers::activePorts(cb)](#dockops::Containers::activePorts)',
         '      - [Parameters:](#parameters-1)',
-        '    - [dockops::Containers::clean(id, cb)](#dockopscontainerscleanid-cb)',
+        '    - [dockops::Containers::clean(id, cb)](#dockops::Containers::clean)',
         '      - [Parameters:](#parameters-2)',
         '- [License](#license)',
         '' ]
@@ -39,9 +39,9 @@ test('\ngiven a file that includes html with header tags using default maxHeader
         '',
         '- [Installation](#installation)',
         '- [API](#api)',
-        '    - [dockops::Containers(docker) → {Object}](#dockopscontainersdocker-%E2%86%92-object)',
-        '    - [dockops::Containers::activePorts(cb)](#dockopscontainersactiveportscb)',
-        '    - [dockops::Containers::clean(id, cb)](#dockopscontainerscleanid-cb)',
+        '    - [dockops::Containers(docker) → {Object}](#dockops::Containers)',
+        '    - [dockops::Containers::activePorts(cb)](#dockops::Containers::activePorts)',
+        '    - [dockops::Containers::clean(id, cb)](#dockops::Containers::clean)',
         '- [License](#license)',
         '' ]
     , 'generates correct toc for non html and html headers omitting headers larger than maxHeaderLevel'
diff --git a/test/transform.js b/test/transform.js
index 0feebfbd..6f3620ad 100644
--- a/test/transform.js
+++ b/test/transform.js
@@ -389,6 +389,16 @@ check(
   , 2
 )
 
+check(
+    [ '# Test 1'
+    , ''
+    , '<h1 id="test-a">Teste á</h1>'
+    ].join('\n')
+  , [ '**Table of Contents**  *generated with [DocToc](https://github.com/thlorenz/doctoc)*\n\n'
+    , '- [Test 1](#test-1)\n',
+    , '- [Teste á](#test-a)\n\n\n'
+    ].join('')
+)
 
 test('transforming when old toc exists', function (t) {
   var md = [