From 8dbab692d77b1c15d25696e816e77b111fb0e2ab Mon Sep 17 00:00:00 2001 From: Zach Leatherman Date: Wed, 4 Sep 2024 10:04:10 -0500 Subject: [PATCH] =?UTF-8?q?Don=E2=80=99t=20rename=20any=20existing=20`id`?= =?UTF-8?q?=20attributes,=20do=20rename=20heading=20node=20ids=20out=20of?= =?UTF-8?q?=20order=20when=20conflicts=20arise=20#3430?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Plugins/IdAttributePlugin.js | 31 ++++++++++++++++--- src/Plugins/InputPathToUrl.js | 2 +- test/IdAttributePluginTest.js | 53 ++++++++++++++++++++++++++++---- 3 files changed, 74 insertions(+), 12 deletions(-) diff --git a/src/Plugins/IdAttributePlugin.js b/src/Plugins/IdAttributePlugin.js index 1e63f6f9c..1285b4a24 100644 --- a/src/Plugins/IdAttributePlugin.js +++ b/src/Plugins/IdAttributePlugin.js @@ -30,6 +30,7 @@ function IdAttributePlugin(eleventyConfig, options = {}) { options.selector = "[id],h1,h2,h3,h4,h5,h6"; } options.decodeEntities = options.decodeEntities ?? true; + options.checkDuplicates = options.checkDuplicates ?? "error"; eleventyConfig.htmlTransformer.addPosthtmlPlugin( "html", @@ -43,21 +44,40 @@ function IdAttributePlugin(eleventyConfig, options = {}) { return function (tree) { // One per page let conflictCheck = {}; + // Cache heading nodes for conflict resolution + let headingNodes = {}; tree.match(matchHelper(options.selector), function (node) { - let id; if (node.attrs?.id) { - id = node.attrs?.id; + let id = node.attrs?.id; + if (conflictCheck[id]) { + conflictCheck[id]++; + if (headingNodes[id]) { + // Rename conflicting assigned heading id + let newId = `${id}-${conflictCheck[id]}`; + headingNodes[newId] = headingNodes[id]; + headingNodes[newId].attrs.id = newId; + delete headingNodes[id]; + } else if (options.checkDuplicates === "error") { + // Existing `id` conflicts with assigned heading id, throw error + throw new Error( + "Duplicate `id` attribute (" + + id + + ") in markup on " + + pluginOptions.page.inputPath, + ); + } + } else { + conflictCheck[id] = 1; + } } else if (!node.attrs?.id && node.content) { node.attrs = node.attrs || {}; let textContent = getTextNodeContent(node); if (options.decodeEntities) { textContent = decodeHTML(textContent); } - id = options.slugify(textContent); - } + let id = options.slugify(textContent); - if (id) { if (conflictCheck[id]) { conflictCheck[id]++; id = `${id}-${conflictCheck[id]}`; @@ -65,6 +85,7 @@ function IdAttributePlugin(eleventyConfig, options = {}) { conflictCheck[id] = 1; } + headingNodes[id] = node; node.attrs.id = id; } diff --git a/src/Plugins/InputPathToUrl.js b/src/Plugins/InputPathToUrl.js index f2683c9fa..ddc60ed70 100644 --- a/src/Plugins/InputPathToUrl.js +++ b/src/Plugins/InputPathToUrl.js @@ -134,10 +134,10 @@ function TransformPlugin(eleventyConfig, defaultOptions = {}) { let suffix = ""; [suffix, targetFilepathOrUrl] = parseFilePath(targetFilepathOrUrl); - // @ts-ignore targetFilepathOrUrl = normalizeInputPath( targetFilepathOrUrl, inputDir, + // @ts-ignore this.page.inputPath, contentMap, ); diff --git a/test/IdAttributePluginTest.js b/test/IdAttributePluginTest.js index ebd7e859f..0b4c67a0e 100644 --- a/test/IdAttributePluginTest.js +++ b/test/IdAttributePluginTest.js @@ -52,7 +52,7 @@ test("Issue #3424, id attribute conflicts (id attribute supplied last)", async ( }); let results = await elev.toJSON(); - t.is(results[0].content.trim(), `

Testing

`); + t.is(results[0].content.trim(), `

Testing

`); }); test("Issue #3424, id attribute conflicts (hard coded id conflicts)", async (t) => { @@ -65,7 +65,46 @@ test("Issue #3424, id attribute conflicts (hard coded id conflicts)", async (t) }); let results = await elev.toJSON(); - t.is(results[0].content.trim(), `

Testing

Testing

`); + t.is(results[0].content.trim(), `

Testing

Testing

`); +}); + +test("Issue #3424, id attribute conflicts (three deep, hard coded id conflicts)", async (t) => { + let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/_site", { + config: function (eleventyConfig) { + eleventyConfig.addPlugin(IdAttributePlugin); + + eleventyConfig.addTemplate("test.html", `

Testing

Testing

Testing

`, {}); + }, + }); + + let results = await elev.toJSON(); + t.is(results[0].content.trim(), `

Testing

Testing

Testing

`); +}); + +test("Issue #3424, id attribute conflicts (four deep, hard coded id conflicts)", async (t) => { + let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/_site", { + config: function (eleventyConfig) { + eleventyConfig.addPlugin(IdAttributePlugin); + + eleventyConfig.addTemplate("test.html", `

Testing

Testing

Testing

Testing

`, {}); + }, + }); + + let results = await elev.toJSON(); + t.is(results[0].content.trim(), `

Testing

Testing

Testing

Testing

`); +}); + +test("Issue #3424, id attribute conflicts (five deep, hard coded id conflicts)", async (t) => { + let elev = new Eleventy("./test/stubs-virtual/", "./test/stubs-virtual/_site", { + config: function (eleventyConfig) { + eleventyConfig.addPlugin(IdAttributePlugin); + + eleventyConfig.addTemplate("test.html", `

Testing

Testing

Testing

Testing

Testing

`, {}); + }, + }); + + let results = await elev.toJSON(); + t.is(results[0].content.trim(), `

Testing

Testing

Testing

Testing

Testing

`); }); test("Issue #3424, id attribute conflicts (two hard coded id conflicts)", async (t) => { @@ -76,9 +115,10 @@ test("Issue #3424, id attribute conflicts (two hard coded id conflicts)", async eleventyConfig.addTemplate("test.html", `

Testing

Testing

`, {}); }, }); + elev.disableLogger(); - let results = await elev.toJSON(); - t.is(results[0].content.trim(), `

Testing

Testing

`); + let e = await t.throwsAsync(() => elev.toJSON()); + t.is(e.originalError.originalError.toString(), "Error: Duplicate `id` attribute (testing) in markup on ./test/stubs-virtual/test.html"); }); test("Issue #3424, filter callback skips", async (t) => { @@ -93,12 +133,13 @@ test("Issue #3424, filter callback skips", async (t) => { } }); - eleventyConfig.addTemplate("test.html", `

Testing

Testing

`, {}); + eleventyConfig.addTemplate("test.html", `

Testing

Testing

`, {}); eleventyConfig.addTemplate("test-skipped.html", `

Testing

Testing

`, {}); }, }); + elev.disableLogger(); let results = await elev.toJSON(); - t.is(results[0].content.trim(), `

Testing

Testing

`); + t.is(results[0].content.trim(), `

Testing

Testing

`); t.is(results[1].content.trim(), `

Testing

Testing

`); });