Commit d302d07 for mammothjs

commit d302d0755e075811a37268f4cef00e7c54cb409a
Author: Michael Williamson <mike@zwobble.org>
Date:   Sat Sep 26 22:28:05 2026 +0100

    Escape HTML IDs in markdown writer

diff --git a/NEWS b/NEWS
index 3d9324a..a820136 100644
--- a/NEWS
+++ b/NEWS
@@ -17,6 +17,8 @@

 * Handle paragraphs and runs that have been moved and tracked as a revision.

+* Escape HTML IDs in Markdown writer.
+
 # 1.12.3

 * Avoid excessive backtracking when parsing an unterminated string with many
diff --git a/lib/writers/markdown-writer.js b/lib/writers/markdown-writer.js
index a89ad9f..a5b572e 100644
--- a/lib/writers/markdown-writer.js
+++ b/lib/writers/markdown-writer.js
@@ -1,5 +1,6 @@
 var _ = require("underscore");

+var htmlWriter = require("./html-writer");

 function symmetricMarkdownElement(end) {
     return markdownElement(end, end);
@@ -52,10 +53,10 @@ function markdownListItem(attributes, list, listItem) {
     list = list || {indent: 0, isOrdered: false, count: 0};
     list.count++;
     listItem.hasClosed = false;
-
+
     var bullet = list.isOrdered ? list.count + "." : "-";
     var start = repeatString("\t", list.indent) + bullet + " ";
-
+
     return {
         start: start,
         end: function() {
@@ -94,20 +95,20 @@ function markdownWriter() {
     var elementStack = [];
     var list = null;
     var listItem = {};
-
+
     function open(tagName, attributes) {
         attributes = attributes || {};
-
+
         var createElement = htmlToMarkdown[tagName] || function() {
             return {};
         };
         var element = createElement(attributes, list, listItem);
         elementStack.push({end: element.end, list: list});
-
+
         if (element.list) {
             list = element.list;
         }
-
+
         var anchorBeforeStart = element.anchorPosition === "before";
         if (anchorBeforeStart) {
             writeAnchor(attributes);
@@ -118,29 +119,32 @@ function markdownWriter() {
             writeAnchor(attributes);
         }
     }
-
+
     function writeAnchor(attributes) {
         if (attributes.id) {
-            fragments.push('<a id="' + attributes.id + '"></a>');
+            var writer = htmlWriter.writer();
+            writer.open("a", {id: attributes.id});
+            writer.close("a");
+            fragments.push(writer.asString());
         }
     }
-
+
     function close(tagName) {
         var element = elementStack.pop();
         list = element.list;
         var end = _.isFunction(element.end) ? element.end() : element.end;
         fragments.push(end || "");
     }
-
+
     function selfClosing(tagName, attributes) {
         open(tagName, attributes);
         close(tagName);
     }
-
+
     function text(value) {
         fragments.push(escapeMarkdown(value));
     }
-
+
     function asString() {
         return fragments.join("");
     }
diff --git a/test/writers/markdown-writer.tests.js b/test/writers/markdown-writer.tests.js
index 6d078c5..e7ab9e1 100644
--- a/test/writers/markdown-writer.tests.js
+++ b/test/writers/markdown-writer.tests.js
@@ -90,6 +90,14 @@ test('elements with IDs have anchor tags with IDs appended to start of markdown
     return assert.equal(writer.asString(), '# <a id="start"></a>Hello\n\n');
 });

+test('anchor tag IDs are escaped', function() {
+    var writer = mdWriter.writer();
+    writer.open("h1", {id: "\"start\""});
+    writer.text("Hello");
+    writer.close("h1");
+    return assert.equal(writer.asString(), '# <a id="&quot;start&quot;"></a>Hello\n\n');
+});
+
 test('links have anchors before opening square bracket', function() {
     var writer = mdWriter.writer();
     writer.open("a", {href: "http://example.com", id: "start"});