From 122190e495bd09ffcda07283481366b1dc6c76e0 Mon Sep 17 00:00:00 2001 From: Chris Staite Date: Mon, 19 Aug 2024 12:23:23 +0100 Subject: [PATCH] Multiple ignored CSS blocks If there are multiple ignored CSS blocks within a single clean-css warning message, only the first is replaced with the ignore code. This means that some ingored blocks are simply deleted from the output. The problem is simply that the RegExp is only applied once, despite having the global flag set on it. This is fixed by calling `exec` on the RegExp until it yields no further matches. Added a test for this error case. Fixes #180 --- src/htmlminifier.js | 35 ++++++++++++++++++++++++++++------- tests/minifier.spec.js | 11 +++++++++++ 2 files changed, 39 insertions(+), 7 deletions(-) diff --git a/src/htmlminifier.js b/src/htmlminifier.js index 6c2d6dd..1a930cb 100644 --- a/src/htmlminifier.js +++ b/src/htmlminifier.js @@ -904,14 +904,35 @@ async function minifyHTML(value, options, partialMarkup) { }); const ids = []; - new CleanCSS().minify(wrapCSS(text, type)).warnings.forEach(function (warning) { - const match = uidPattern.exec(warning); - if (match) { - const id = uidAttr + match[2] + uidAttr; - text = text.replace(id, ignoreCSS(id)); - ids.push(id); + // Loop removing a single ID at a time from the warnings, a + // warning might contain multiple IDs in the context, but we only + // handle the first match on each attempt. + while (true) { + const minifyTest = new CleanCSS().minify(wrapCSS(text, type)); + if (minifyTest.warnings.length === 0) { + // There are no warnings. + break; } - }); + if (!minifyTest.warnings.every(function (warning) { + // It is very important to reset the RegExp before searching + // as it's re-used each time. + uidPattern.lastIndex = 0; + const match = uidPattern.exec(warning); + if (match) { + const id = uidAttr + match[2] + uidAttr; + // Only substitute each ID once, if this has come up + // multiple times, then we need to abort. + if (!ids.includes(id)) { + text = text.replace(id, ignoreCSS(id)); + ids.push(id); + return true; + } + } + return false; + })) { + break; + } + } return fn(text, type).then(chunk => { ids.forEach(function (id) { diff --git a/tests/minifier.spec.js b/tests/minifier.spec.js index 67b61a6..2fa6463 100644 --- a/tests/minifier.spec.js +++ b/tests/minifier.spec.js @@ -3594,3 +3594,14 @@ test('minify Content-Security-Policy', async () => { input = ''; expect(await minify(input)).toBe(input); }); + +test('minify CSS multiple ignore in single warning', async () => { + const input = ''; + const output = ''; + expect(await minify(input, { + ignoreCustomFragments: [/\{%[\s\S]*?%\}/], + minifyCSS: { + level: 0 + } + })).toBe(output); +});