diff --git a/packages/purgecss/__tests__/safelist.test.ts b/packages/purgecss/__tests__/safelist.test.ts index 90651e34..71a4a8d4 100644 --- a/packages/purgecss/__tests__/safelist.test.ts +++ b/packages/purgecss/__tests__/safelist.test.ts @@ -126,3 +126,53 @@ describe("safelist option: greedy", () => { ); }); }); + +describe("safelist option: keyframes", () => { + let purgedCSS: string; + beforeAll(async () => { + const resultsPurge = await new PurgeCSS().purge({ + content: [`${ROOT_TEST_EXAMPLES}safelist/safelist_keyframes.html`], + css: [`${ROOT_TEST_EXAMPLES}safelist/safelist_keyframes.css`], + safelist: { + keyframes: [/^scale/, "spin"], + }, + keyframes: true, + }); + purgedCSS = resultsPurge[0].css; + }); + + it("finds safelisted keyframes", () => { + findInCSS( + expect, + ["@keyframes scale", "@keyframes scale-down", "@keyframes spin"], + purgedCSS + ); + }); + + it("excludes non-safelisted keyframes", () => { + notFindInCSS(expect, ["flash"], purgedCSS); + }); +}); + +describe("safelist option: variables", () => { + let purgedCSS: string; + beforeAll(async () => { + const resultsPurge = await new PurgeCSS().purge({ + content: [`${ROOT_TEST_EXAMPLES}safelist/safelist_css_variables.html`], + css: [`${ROOT_TEST_EXAMPLES}safelist/safelist_css_variables.css`], + safelist: { + variables: [/^--b/, "--unused-color"], + }, + variables: true, + }); + purgedCSS = resultsPurge[0].css; + }); + + it("finds safelisted css variables", () => { + findInCSS(expect, ["--unused-color", "--button-color"], purgedCSS); + }); + + it("excludes non-safelisted css variables", () => { + notFindInCSS(expect, ["--tertiary-color:"], purgedCSS); + }); +}); diff --git a/packages/purgecss/__tests__/test_examples/safelist/safelist_css_variables.css b/packages/purgecss/__tests__/test_examples/safelist/safelist_css_variables.css new file mode 100644 index 00000000..1956037d --- /dev/null +++ b/packages/purgecss/__tests__/test_examples/safelist/safelist_css_variables.css @@ -0,0 +1,22 @@ +:root { + --primary-color: blue; + --secondary-color: indigo; + --tertiary-color: aqua; + --unused-color: violet; + --used-color: rebeccapurple; + --accent-color: orange; + } + + .button { + --button-color: var(--tertiary-color); + --border-color: linear-gradient(to top, var(--secondary-color), var(--used-color, white)); + + background-color: var(--primary-color); + color: var(--accent-color); + border-color: var(--border-color); + } + + .button:focus { + background-color: var(--accent-color); + color: var(--primary-color); + } \ No newline at end of file diff --git a/packages/purgecss/__tests__/test_examples/safelist/safelist_css_variables.html b/packages/purgecss/__tests__/test_examples/safelist/safelist_css_variables.html new file mode 100644 index 00000000..65214131 --- /dev/null +++ b/packages/purgecss/__tests__/test_examples/safelist/safelist_css_variables.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/purgecss/__tests__/test_examples/safelist/safelist_keyframes.css b/packages/purgecss/__tests__/test_examples/safelist/safelist_keyframes.css new file mode 100644 index 00000000..3be12e29 --- /dev/null +++ b/packages/purgecss/__tests__/test_examples/safelist/safelist_keyframes.css @@ -0,0 +1,61 @@ +@keyframes bounce { + from, 20%, 53%, 80%, to { + animation-timing-function: cubic-bezier(0.3, 0.1, 0.9, 1.000); + transform: translate3d(1, 1, 0); + } +} + +.bounce { + -webkit-animation-name: bounce; + animation-name: bounce; + -webkit-transform-origin: center bottom; + transform-origin: center bottom; +} + +@keyframes flash { + from, 50%, to { + opacity: 1; + } + + 25%, 75% { + opacity: 0.5; + } +} + +.flash { + animation: flash +} + +@keyframes scale { + from { + transform: scale(1); + } + + to { + transform: scale(2); + } +} + +@keyframes scale-down { + from { + transform: scale(2); + } + + to { + transform: scale(1); + } +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} + +.scale-spin { + animation: spin 300ms linear infinite forwards,scale 300ms linear infinite alternate; +} diff --git a/packages/purgecss/__tests__/test_examples/safelist/safelist_keyframes.html b/packages/purgecss/__tests__/test_examples/safelist/safelist_keyframes.html new file mode 100644 index 00000000..7b1f750a --- /dev/null +++ b/packages/purgecss/__tests__/test_examples/safelist/safelist_keyframes.html @@ -0,0 +1,2 @@ +
+
diff --git a/packages/purgecss/src/VariablesStructure.ts b/packages/purgecss/src/VariablesStructure.ts index 06f9b666..af5a9221 100644 --- a/packages/purgecss/src/VariablesStructure.ts +++ b/packages/purgecss/src/VariablesStructure.ts @@ -1,4 +1,5 @@ import postcss from "postcss"; +import { StringRegExpArray } from "./types"; class VariableNode { public nodes: VariableNode[] = []; @@ -13,6 +14,7 @@ class VariableNode { class VariablesStructure { public nodes: Map = new Map(); public usedVariables: Set = new Set(); + public safelist: StringRegExpArray = []; addVariable(declaration: postcss.Declaration): void { const { prop } = declaration; @@ -63,12 +65,20 @@ class VariablesStructure { for (const used of this.usedVariables) { this.setAsUsed(used); } - for (const [, declaration] of this.nodes) { - if (!declaration.isUsed) { + for (const [name, declaration] of this.nodes) { + if (!declaration.isUsed && !this.isVariablesSafelisted(name)) { declaration.value.remove(); } } } + + isVariablesSafelisted(variable: string): boolean { + return this.safelist.some((safelistItem) => { + return typeof safelistItem === "string" + ? safelistItem === variable + : safelistItem.test(variable); + }); + } } export default VariablesStructure; diff --git a/packages/purgecss/src/index.ts b/packages/purgecss/src/index.ts index c395a9f4..7b76fe02 100644 --- a/packages/purgecss/src/index.ts +++ b/packages/purgecss/src/index.ts @@ -542,6 +542,18 @@ class PurgeCSS { return sources; } + /** + * Check if the keyframe is safelisted with the option safelist keyframes + * @param keyframesName name of the keyframe animation + */ + private isKeyframesSafelisted(keyframesName: string): boolean { + return this.options.safelist.keyframes.some((safelistItem) => { + return typeof safelistItem === "string" + ? safelistItem === keyframesName + : safelistItem.test(keyframesName); + }); + } + /** * Check if the selector is safelisted with the option safelist standard * @param selector css selector @@ -590,7 +602,11 @@ class PurgeCSS { ...userOptions, safelist: standardizeSafelist(userOptions.safelist), }; - const { content, css, extractors } = this.options; + const { content, css, extractors, safelist } = this.options; + + if (this.options.variables) { + this.variablesStructure.safelist = safelist.variables || []; + } const fileFormatContents = content.filter( (o) => typeof o === "string" @@ -637,7 +653,10 @@ class PurgeCSS { */ public removeUnusedKeyframes(): void { for (const node of this.atRules.keyframes) { - if (!this.usedAnimations.has(node.params)) { + if ( + !this.usedAnimations.has(node.params) && + !this.isKeyframesSafelisted(node.params) + ) { node.remove(); } }