purgecss 6.0.0 → 7.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1,1024 @@
1
- import*as e from"fs";import*as t from"glob";import*as s from"path";import*as r from"postcss";import o from"postcss-selector-parser";import{promisify as i}from"util";function a(e,t){t&&t.forEach(e.add,e)}class ExtractorResultSets{constructor(e){this.undetermined=new Set,this.attrNames=new Set,this.attrValues=new Set,this.classes=new Set,this.ids=new Set,this.tags=new Set,this.merge(e)}merge(e){return Array.isArray(e)?a(this.undetermined,e):e instanceof ExtractorResultSets?(a(this.undetermined,e.undetermined),a(this.attrNames,e.attrNames),a(this.attrValues,e.attrValues),a(this.classes,e.classes),a(this.ids,e.ids),a(this.tags,e.tags)):(a(this.undetermined,e.undetermined),e.attributes&&(a(this.attrNames,e.attributes.names),a(this.attrValues,e.attributes.values)),a(this.classes,e.classes),a(this.ids,e.ids),a(this.tags,e.tags)),this}hasAttrName(e){return this.attrNames.has(e)||this.undetermined.has(e)}someAttrValue(e){for(const t of this.attrValues)if(e(t))return!0;for(const t of this.undetermined)if(e(t))return!0;return!1}hasAttrPrefix(e){return this.someAttrValue((t=>t.startsWith(e)))}hasAttrSuffix(e){return this.someAttrValue((t=>t.endsWith(e)))}hasAttrSubstr(e){return e.trim().split(" ").every((e=>this.someAttrValue((t=>t.includes(e)))))}hasAttrValue(e){return this.attrValues.has(e)||this.undetermined.has(e)}hasClass(e){return this.classes.has(e)||this.undetermined.has(e)}hasId(e){return this.ids.has(e)||this.undetermined.has(e)}hasTag(e){return this.tags.has(e)||this.undetermined.has(e)}}const n=["*",":root",":after",":before"],c={css:[],content:[],defaultExtractor:e=>e.match(/[A-Za-z0-9_-]+/g)||[],extractors:[],fontFace:!1,keyframes:!1,rejected:!1,rejectedCss:!1,sourceMap:!1,stdin:!1,stdout:!1,variables:!1,safelist:{standard:[],deep:[],greedy:[],variables:[],keyframes:[]},blocklist:[],skippedContentGlobs:[],dynamicAttributes:[]};class VariableNode{constructor(e){this.nodes=[],this.isUsed=!1,this.value=e}}class VariablesStructure{constructor(){this.nodes=new Map,this.usedVariables=new Set,this.safelist=[]}addVariable(e){const{prop:t}=e;if(this.nodes.has(t)){const s=new VariableNode(e),r=this.nodes.get(t)||[];this.nodes.set(t,[...r,s])}else{const s=new VariableNode(e);this.nodes.set(t,[s])}}addVariableUsage(e,t){const{prop:s}=e,r=this.nodes.get(s);for(const e of t){const t=e[1];if(this.nodes.has(t)){const e=this.nodes.get(t);null==r||r.forEach((t=>{null==e||e.forEach((e=>t.nodes.push(e)))}))}}}addVariableUsageInProperties(e){for(const t of e){const e=t[1];this.usedVariables.add(e)}}setAsUsed(e){const t=this.nodes.get(e);if(t){const e=[...t];for(;0!==e.length;){const t=e.pop();t&&!t.isUsed&&(t.isUsed=!0,e.push(...t.nodes))}}}removeUnused(){for(const e of this.usedVariables){const t=this.nodes.get(e);if(t)for(const e of t){const t=e.value.value.matchAll(/var\((.+?)[,)]/g);for(const e of t)this.usedVariables.has(e[1])||this.usedVariables.add(e[1])}}for(const e of this.usedVariables)this.setAsUsed(e);for(const[e,t]of this.nodes)for(const s of t)s.isUsed||this.isVariablesSafelisted(e)||s.value.remove()}isVariablesSafelisted(e){return this.safelist.some((t=>"string"==typeof t?t===e:t.test(e)))}}const l={access:i(e.access),readFile:i(e.readFile)};function u(e=[]){return Array.isArray(e)?{...c.safelist,standard:e}:{...c.safelist,...e}}async function d(e="purgecss.config.js"){let t;try{const r=s.resolve(process.cwd(),e);t=await import(r)}catch(e){if(e instanceof Error)throw new Error(`Error loading the config file ${e.message}`);throw new Error}return{...c,...t,safelist:u(t.safelist)}}async function h(e,t){return new ExtractorResultSets(await t(e))}function f(e,t){switch(t){case"next":return e.text.includes("purgecss ignore");case"start":return e.text.includes("purgecss start ignore");case"end":return e.text.includes("purgecss end ignore")}}function p(...e){const t=new ExtractorResultSets([]);return e.forEach(t.merge,t),t}function m(e){return e.replace(/(^["'])|(["']$)/g,"")}function g(e,t){if(!t.hasAttrName(e.attribute))return!1;if(void 0===e.value)return!0;switch(e.operator){case"$=":return t.hasAttrSuffix(e.value);case"~=":case"*=":return t.hasAttrSubstr(e.value);case"=":return t.hasAttrValue(e.value);case"|=":case"^=":return t.hasAttrPrefix(e.value);default:return!0}}function v(e,t){return t.hasId(e.value)}function S(e,t){return t.hasTag(e.value)}function y(e){return"atrule"===(null==e?void 0:e.type)}function b(e){return"rule"===(null==e?void 0:e.type)}class PurgeCSS{constructor(){this.ignore=!1,this.atRules={fontFace:[],keyframes:[]},this.usedAnimations=new Set,this.usedFontFaces=new Set,this.selectorsRemoved=new Set,this.removedNodes=[],this.variablesStructure=new VariablesStructure,this.options=c}collectDeclarationsData(e){const{prop:t,value:s}=e;if(this.options.variables){const r=s.matchAll(/var\((.+?)[,)]/g);t.startsWith("--")?(this.variablesStructure.addVariable(e),this.variablesStructure.addVariableUsage(e,r)):this.variablesStructure.addVariableUsageInProperties(r)}if(!this.options.keyframes||"animation"!==t&&"animation-name"!==t)if(this.options.fontFace){if("font-family"===t)for(const e of s.split(",")){const t=m(e.trim());this.usedFontFaces.add(t)}}else;else for(const e of s.split(/[\s,]+/))this.usedAnimations.add(e)}getFileExtractor(e,t){const s=t.find((t=>t.extensions.find((t=>e.endsWith(t)))));return void 0===s?this.options.defaultExtractor:s.extractor}async extractSelectorsFromFiles(s,r){const o=new ExtractorResultSets([]),i=[];for(const r of s)try{await l.access(r,e.constants.F_OK),i.push(r)}catch(e){i.push(...t.sync(r,{nodir:!0,ignore:this.options.skippedContentGlobs.map((e=>e.replace(/^\.\//,"")))}))}s.length>0&&0===i.length&&console.warn("No files found from the passed PurgeCSS option 'content'.");for(const e of i){const t=await l.readFile(e,"utf-8"),s=this.getFileExtractor(e,r),i=await h(t,s);o.merge(i)}return o}async extractSelectorsFromString(e,t){const s=new ExtractorResultSets([]);for(const{raw:r,extension:o}of e){const e=this.getFileExtractor(`.${o}`,t),i=await h(r,e);s.merge(i)}return s}evaluateAtRule(e){if(this.options.keyframes&&e.name.endsWith("keyframes"))this.atRules.keyframes.push(e);else if(this.options.fontFace&&"font-face"===e.name&&e.nodes)for(const t of e.nodes)"decl"===t.type&&"font-family"===t.prop&&this.atRules.fontFace.push({name:m(t.value),node:e})}evaluateRule(e,t){if(this.ignore)return;const s=e.prev();if(function(e){return"comment"===(null==e?void 0:e.type)}(s)&&f(s,"next"))return void s.remove();if(e.parent&&y(e.parent)&&e.parent.name.endsWith("keyframes"))return;if(!b(e))return;if(function(e){let t=!1;return e.walkComments((e=>{e&&"comment"===e.type&&e.text.includes("purgecss ignore current")&&(t=!0,e.remove())})),t}(e))return;const r=[];if(e.selector=o((e=>{e.walk((e=>{if("selector"!==e.type)return;this.shouldKeepSelector(e,t)||(this.options.rejected&&this.selectorsRemoved.add(e.toString()),this.options.rejectedCss&&r.push(e.toString()),e.remove())})),e.walk((e=>{"selector"===e.type&&e.toString()&&/(:where)|(:is)/.test(e.toString())&&e.walk((t=>{"pseudo"===t.type&&(":where"!==t.value&&":is"!==t.value||0===t.nodes.length&&e.remove())}))}))})).processSync(e.selector),e.selector&&void 0!==e.nodes)for(const t of e.nodes)"decl"===t.type&&this.collectDeclarationsData(t);const i=e.parent;if(e.selector||e.remove(),function(e){return!!(b(e)&&!e.selector||(null==e?void 0:e.nodes)&&!e.nodes.length||y(e)&&(!e.nodes&&!e.params||!e.params&&e.nodes&&!e.nodes.length))}(i)&&(null==i||i.remove()),this.options.rejectedCss&&r.length>0){const t=e.clone(),s=null==i?void 0:i.clone().removeAll().append(t);t.selectors=r;const o=s||t;this.removedNodes.push(o)}}async getPurgedCSS(e,s){var o;const i=[],a=[];for(const s of e)"string"==typeof s?a.push(...t.sync(s,{nodir:!0,ignore:this.options.skippedContentGlobs})):a.push(s);for(const e of a){const t="string"==typeof e?this.options.stdin?e:await l.readFile(e,"utf-8"):e.raw,a="string"==typeof e&&!this.options.stdin,n=r.parse(t,{from:a?e:void 0});this.walkThroughCSS(n,s),this.options.fontFace&&this.removeUnusedFontFaces(),this.options.keyframes&&this.removeUnusedKeyframes(),this.options.variables&&this.removeUnusedCSSVariables();const c=n.toResult({map:this.options.sourceMap,to:"object"==typeof this.options.sourceMap?this.options.sourceMap.to:void 0}),u={css:c.toString(),file:"string"==typeof e?e:e.name};this.options.sourceMap&&(u.sourceMap=null===(o=c.map)||void 0===o?void 0:o.toString()),this.options.rejected&&(u.rejected=Array.from(this.selectorsRemoved),this.selectorsRemoved.clear()),this.options.rejectedCss&&(u.rejectedCss=r.root({nodes:this.removedNodes}).toString()),i.push(u)}return i}isKeyframesSafelisted(e){return this.options.safelist.keyframes.some((t=>"string"==typeof t?t===e:t.test(e)))}isSelectorBlocklisted(e){return this.options.blocklist.some((t=>"string"==typeof t?t===e:t.test(e)))}isSelectorSafelisted(e){const t=this.options.safelist.standard.some((t=>"string"==typeof t?t===e:t.test(e))),s=/^::.*/.test(e);return n.includes(e)||s||t}isSelectorSafelistedDeep(e){return this.options.safelist.deep.some((t=>t.test(e)))}isSelectorSafelistedGreedy(e){return this.options.safelist.greedy.some((t=>t.test(e)))}async purge(e){this.options="object"!=typeof e?await d(e):{...c,...e,safelist:u(e.safelist)};const{content:t,css:s,extractors:r,safelist:o}=this.options;this.options.variables&&(this.variablesStructure.safelist=o.variables||[]);const i=t.filter((e=>"string"==typeof e)),a=t.filter((e=>"object"==typeof e)),n=await this.extractSelectorsFromFiles(i,r),l=await this.extractSelectorsFromString(a,r);return this.getPurgedCSS(s,p(n,l))}removeUnusedCSSVariables(){this.variablesStructure.removeUnused()}removeUnusedFontFaces(){for(const{name:e,node:t}of this.atRules.fontFace)this.usedFontFaces.has(e)||t.remove()}removeUnusedKeyframes(){for(const e of this.atRules.keyframes)this.usedAnimations.has(e.params)||this.isKeyframesSafelisted(e.params)||e.remove()}getSelectorValue(e){return"attribute"===e.type&&e.attribute||e.value}shouldKeepSelector(e,t){if(function(e){return e.parent&&"pseudo"===e.parent.type&&e.parent.value.startsWith(":")||!1}(e)&&!function(e){return e.parent&&"pseudo"===e.parent.type&&(":where"===e.parent.value||":is"===e.parent.value)||!1}(e))return!0;if(this.options.safelist.greedy.length>0){if(e.nodes.map(this.getSelectorValue).some((e=>e&&this.isSelectorSafelistedGreedy(e))))return!0}let s=!1;for(const o of e.nodes){const e=this.getSelectorValue(o);if(e&&this.isSelectorSafelistedDeep(e))return!0;if(e&&(n.includes(e)||this.isSelectorSafelisted(e)))s=!0;else{if(e&&this.isSelectorBlocklisted(e))return!1;switch(o.type){case"attribute":s=!![...this.options.dynamicAttributes,"value","checked","selected","open"].includes(o.attribute)||g(o,t);break;case"class":r=o,s=t.hasClass(r.value);break;case"id":s=v(o,t);break;case"tag":s=S(o,t);break;default:continue}if(!s)return!1}}var r;return s}walkThroughCSS(e,t){e.walk((e=>"rule"===e.type?this.evaluateRule(e,t):"atrule"===e.type?this.evaluateAtRule(e):void("comment"===e.type&&(f(e,"start")?(this.ignore=!0,e.remove()):f(e,"end")&&(this.ignore=!1,e.remove())))))}}export{ExtractorResultSets,PurgeCSS,VariableNode,VariablesStructure,c as defaultOptions,p as mergeExtractorSelectors,d as setOptions,u as standardizeSafelist};
1
+ import * as fs from 'fs';
2
+ import * as glob from 'glob';
3
+ import * as path from 'path';
4
+ import * as postcss from 'postcss';
5
+ import selectorParser from 'postcss-selector-parser';
6
+ import { promisify } from 'util';
7
+
8
+ const IGNORE_ANNOTATION_CURRENT = "purgecss ignore current";
9
+ const IGNORE_ANNOTATION_NEXT = "purgecss ignore";
10
+ const IGNORE_ANNOTATION_START = "purgecss start ignore";
11
+ const IGNORE_ANNOTATION_END = "purgecss end ignore";
12
+ const CONFIG_FILENAME = "purgecss.config.js";
13
+ // Error Message
14
+ const ERROR_CONFIG_FILE_LOADING = "Error loading the config file";
15
+
16
+ function mergeSets(into, from) {
17
+ if (from) {
18
+ from.forEach(into.add, into);
19
+ }
20
+ }
21
+ /**
22
+ * @public
23
+ */
24
+ class ExtractorResultSets {
25
+ constructor(er) {
26
+ this.undetermined = new Set();
27
+ this.attrNames = new Set();
28
+ this.attrValues = new Set();
29
+ this.classes = new Set();
30
+ this.ids = new Set();
31
+ this.tags = new Set();
32
+ this.merge(er);
33
+ }
34
+ merge(that) {
35
+ if (Array.isArray(that)) {
36
+ mergeSets(this.undetermined, that);
37
+ }
38
+ else if (that instanceof ExtractorResultSets) {
39
+ mergeSets(this.undetermined, that.undetermined);
40
+ mergeSets(this.attrNames, that.attrNames);
41
+ mergeSets(this.attrValues, that.attrValues);
42
+ mergeSets(this.classes, that.classes);
43
+ mergeSets(this.ids, that.ids);
44
+ mergeSets(this.tags, that.tags);
45
+ }
46
+ else {
47
+ // ExtractorResultDetailed:
48
+ mergeSets(this.undetermined, that.undetermined);
49
+ if (that.attributes) {
50
+ mergeSets(this.attrNames, that.attributes.names);
51
+ mergeSets(this.attrValues, that.attributes.values);
52
+ }
53
+ mergeSets(this.classes, that.classes);
54
+ mergeSets(this.ids, that.ids);
55
+ mergeSets(this.tags, that.tags);
56
+ }
57
+ return this;
58
+ }
59
+ hasAttrName(name) {
60
+ return this.attrNames.has(name) || this.undetermined.has(name);
61
+ }
62
+ someAttrValue(predicate) {
63
+ for (const val of this.attrValues) {
64
+ if (predicate(val))
65
+ return true;
66
+ }
67
+ for (const val of this.undetermined) {
68
+ if (predicate(val))
69
+ return true;
70
+ }
71
+ return false;
72
+ }
73
+ hasAttrPrefix(prefix) {
74
+ return this.someAttrValue((value) => value.startsWith(prefix));
75
+ }
76
+ hasAttrSuffix(suffix) {
77
+ return this.someAttrValue((value) => value.endsWith(suffix));
78
+ }
79
+ hasAttrSubstr(substr) {
80
+ const wordSubstr = substr.trim().split(" ");
81
+ return wordSubstr.every((word) => this.someAttrValue((value) => value.includes(word)));
82
+ }
83
+ hasAttrValue(value) {
84
+ return this.attrValues.has(value) || this.undetermined.has(value);
85
+ }
86
+ hasClass(name) {
87
+ return this.classes.has(name) || this.undetermined.has(name);
88
+ }
89
+ hasId(id) {
90
+ return this.ids.has(id) || this.undetermined.has(id);
91
+ }
92
+ hasTag(tag) {
93
+ return this.tags.has(tag) || this.undetermined.has(tag);
94
+ }
95
+ }
96
+
97
+ const CSS_SAFELIST = ["*", ":root", ":after", ":before"];
98
+
99
+ /**
100
+ * @public
101
+ */
102
+ const defaultOptions = {
103
+ css: [],
104
+ content: [],
105
+ defaultExtractor: (content) => content.match(/[A-Za-z0-9_-]+/g) || [],
106
+ extractors: [],
107
+ fontFace: false,
108
+ keyframes: false,
109
+ rejected: false,
110
+ rejectedCss: false,
111
+ sourceMap: false,
112
+ stdin: false,
113
+ stdout: false,
114
+ variables: false,
115
+ safelist: {
116
+ standard: [],
117
+ deep: [],
118
+ greedy: [],
119
+ variables: [],
120
+ keyframes: [],
121
+ },
122
+ blocklist: [],
123
+ skippedContentGlobs: [],
124
+ dynamicAttributes: [],
125
+ };
126
+
127
+ /**
128
+ * @public
129
+ */
130
+ class VariableNode {
131
+ constructor(declaration) {
132
+ this.nodes = [];
133
+ this.isUsed = false;
134
+ this.value = declaration;
135
+ }
136
+ }
137
+ /**
138
+ * @public
139
+ */
140
+ class VariablesStructure {
141
+ constructor() {
142
+ this.nodes = new Map();
143
+ this.usedVariables = new Set();
144
+ this.safelist = [];
145
+ }
146
+ addVariable(declaration) {
147
+ const { prop } = declaration;
148
+ if (!this.nodes.has(prop)) {
149
+ const node = new VariableNode(declaration);
150
+ this.nodes.set(prop, [node]);
151
+ }
152
+ else {
153
+ const node = new VariableNode(declaration);
154
+ const variableNodes = this.nodes.get(prop) || [];
155
+ this.nodes.set(prop, [...variableNodes, node]);
156
+ }
157
+ }
158
+ addVariableUsage(declaration, matchedVariables) {
159
+ const { prop } = declaration;
160
+ const nodes = this.nodes.get(prop);
161
+ for (const variableMatch of matchedVariables) {
162
+ // capturing group containing the variable is in index 1
163
+ const variableName = variableMatch[1];
164
+ if (this.nodes.has(variableName)) {
165
+ const usedVariableNodes = this.nodes.get(variableName);
166
+ nodes === null || nodes === void 0 ? void 0 : nodes.forEach((node) => {
167
+ usedVariableNodes === null || usedVariableNodes === void 0 ? void 0 : usedVariableNodes.forEach((usedVariableNode) => node.nodes.push(usedVariableNode));
168
+ });
169
+ }
170
+ }
171
+ }
172
+ addVariableUsageInProperties(matchedVariables) {
173
+ for (const variableMatch of matchedVariables) {
174
+ // capturing group containing the variable is in index 1
175
+ const variableName = variableMatch[1];
176
+ this.usedVariables.add(variableName);
177
+ }
178
+ }
179
+ setAsUsed(variableName) {
180
+ const nodes = this.nodes.get(variableName);
181
+ if (nodes) {
182
+ const queue = [...nodes];
183
+ while (queue.length !== 0) {
184
+ const currentNode = queue.pop();
185
+ if (currentNode && !currentNode.isUsed) {
186
+ currentNode.isUsed = true;
187
+ queue.push(...currentNode.nodes);
188
+ }
189
+ }
190
+ }
191
+ }
192
+ removeUnused() {
193
+ // check unordered usage
194
+ for (const used of this.usedVariables) {
195
+ const usedNodes = this.nodes.get(used);
196
+ if (usedNodes) {
197
+ for (const usedNode of usedNodes) {
198
+ const usedVariablesMatchesInDeclaration = usedNode.value.value.matchAll(/var\((.+?)[,)]/g);
199
+ for (const usage of usedVariablesMatchesInDeclaration) {
200
+ if (!this.usedVariables.has(usage[1])) {
201
+ this.usedVariables.add(usage[1]);
202
+ }
203
+ }
204
+ }
205
+ }
206
+ }
207
+ for (const used of this.usedVariables) {
208
+ this.setAsUsed(used);
209
+ }
210
+ for (const [name, declarations] of this.nodes) {
211
+ for (const declaration of declarations) {
212
+ if (!declaration.isUsed && !this.isVariablesSafelisted(name)) {
213
+ declaration.value.remove();
214
+ }
215
+ }
216
+ }
217
+ }
218
+ isVariablesSafelisted(variable) {
219
+ return this.safelist.some((safelistItem) => {
220
+ return typeof safelistItem === "string"
221
+ ? safelistItem === variable
222
+ : safelistItem.test(variable);
223
+ });
224
+ }
225
+ }
226
+
227
+ /**
228
+ * Core package of PurgeCSS
229
+ *
230
+ * Contains the core methods to analyze the files, remove unused CSS.
231
+ *
232
+ * @packageDocumentation
233
+ */
234
+ const asyncFs = {
235
+ access: promisify(fs.access),
236
+ readFile: promisify(fs.readFile),
237
+ };
238
+ /**
239
+ * Format the user defined safelist into a standardized safelist object
240
+ *
241
+ * @param userDefinedSafelist - the user defined safelist
242
+ * @returns the formatted safelist object that can be used in the PurgeCSS options
243
+ *
244
+ * @public
245
+ */
246
+ function standardizeSafelist(userDefinedSafelist = []) {
247
+ if (Array.isArray(userDefinedSafelist)) {
248
+ return {
249
+ ...defaultOptions.safelist,
250
+ standard: userDefinedSafelist,
251
+ };
252
+ }
253
+ return {
254
+ ...defaultOptions.safelist,
255
+ ...userDefinedSafelist,
256
+ };
257
+ }
258
+ /**
259
+ * Load the configuration file from the path
260
+ *
261
+ * @param configFile - Path of the config file
262
+ * @returns The options from the configuration file
263
+ *
264
+ * @throws Error
265
+ * This exception is thrown if the configuration file was not imported
266
+ *
267
+ * @public
268
+ */
269
+ async function setOptions(configFile = CONFIG_FILENAME) {
270
+ let options;
271
+ try {
272
+ const t = path.resolve(process.cwd(), configFile);
273
+ options = await import(t);
274
+ }
275
+ catch (err) {
276
+ if (err instanceof Error) {
277
+ throw new Error(`${ERROR_CONFIG_FILE_LOADING} ${err.message}`);
278
+ }
279
+ throw new Error();
280
+ }
281
+ return {
282
+ ...defaultOptions,
283
+ ...options,
284
+ safelist: standardizeSafelist(options.safelist),
285
+ };
286
+ }
287
+ /**
288
+ * Use the extract function to get the list of selectors
289
+ *
290
+ * @param content - content (e.g. html file)
291
+ * @param extractor - PurgeCSS extractor used to extract the selectors
292
+ * @returns the sets containing the result of the extractor function
293
+ */
294
+ async function extractSelectors(content, extractor) {
295
+ return new ExtractorResultSets(await extractor(content));
296
+ }
297
+ /**
298
+ * Check if the node is a css comment indication to ignore the selector rule
299
+ *
300
+ * @param node - node of postcss AST
301
+ * @param type - type of css comment
302
+ * @returns true if the node is a PurgeCSS ignore comment
303
+ */
304
+ function isIgnoreAnnotation(node, type) {
305
+ switch (type) {
306
+ case "next":
307
+ return node.text.includes(IGNORE_ANNOTATION_NEXT);
308
+ case "start":
309
+ return node.text.includes(IGNORE_ANNOTATION_START);
310
+ case "end":
311
+ return node.text.includes(IGNORE_ANNOTATION_END);
312
+ }
313
+ }
314
+ /**
315
+ * Check if the node correspond to an empty css rule
316
+ *
317
+ * @param node - node of postcss AST
318
+ * @returns true if the rule is empty
319
+ */
320
+ function isRuleEmpty(node) {
321
+ if ((isPostCSSRule(node) && !node.selector) ||
322
+ ((node === null || node === void 0 ? void 0 : node.nodes) && !node.nodes.length) ||
323
+ (isPostCSSAtRule(node) &&
324
+ ((!node.nodes && !node.params) ||
325
+ (!node.params && node.nodes && !node.nodes.length)))) {
326
+ return true;
327
+ }
328
+ return false;
329
+ }
330
+ /**
331
+ * Check if the node has a css comment indicating to ignore the current selector rule
332
+ *
333
+ * @param rule - rule of postcss AST
334
+ */
335
+ function hasIgnoreAnnotation(rule) {
336
+ let found = false;
337
+ rule.walkComments((node) => {
338
+ if (node &&
339
+ node.type === "comment" &&
340
+ node.text.includes(IGNORE_ANNOTATION_CURRENT)) {
341
+ found = true;
342
+ node.remove();
343
+ }
344
+ });
345
+ return found;
346
+ }
347
+ /**
348
+ * Merge two extractor selectors
349
+ *
350
+ * @param extractorSelectorsA - extractor selectors A
351
+ * @param extractorSelectorsB - extractor selectors B
352
+ * @returns the merged extractor result sets
353
+ *
354
+ * @public
355
+ */
356
+ function mergeExtractorSelectors(...extractors) {
357
+ const result = new ExtractorResultSets([]);
358
+ extractors.forEach(result.merge, result);
359
+ return result;
360
+ }
361
+ /**
362
+ * Strips quotes of a string
363
+ *
364
+ * @param str - string to be stripped
365
+ */
366
+ function stripQuotes(str) {
367
+ return str.replace(/(^["'])|(["']$)/g, "");
368
+ }
369
+ /**
370
+ * Returns true if the attribute is found in the extractor selectors
371
+ *
372
+ * @param attributeNode - node of type `attribute`
373
+ * @param selectors - extractor selectors
374
+ */
375
+ function isAttributeFound(attributeNode, selectors) {
376
+ if (!selectors.hasAttrName(attributeNode.attribute)) {
377
+ return false;
378
+ }
379
+ if (typeof attributeNode.value === "undefined") {
380
+ return true;
381
+ }
382
+ switch (attributeNode.operator) {
383
+ case "$=":
384
+ return selectors.hasAttrSuffix(attributeNode.value);
385
+ case "~=":
386
+ case "*=":
387
+ return selectors.hasAttrSubstr(attributeNode.value);
388
+ case "=":
389
+ return selectors.hasAttrValue(attributeNode.value);
390
+ case "|=":
391
+ case "^=":
392
+ return selectors.hasAttrPrefix(attributeNode.value);
393
+ default:
394
+ return true;
395
+ }
396
+ }
397
+ /**
398
+ * Returns true if the class is found in the extractor selectors
399
+ *
400
+ * @param classNode - node of type `class`
401
+ * @param selectors - extractor selectors
402
+ */
403
+ function isClassFound(classNode, selectors) {
404
+ return selectors.hasClass(classNode.value);
405
+ }
406
+ /**
407
+ * Returns true if the identifier is found in the extractor selectors
408
+ *
409
+ * @param identifierNode - node of type `identifier`
410
+ * @param selectors - extractor selectors
411
+ */
412
+ function isIdentifierFound(identifierNode, selectors) {
413
+ return selectors.hasId(identifierNode.value);
414
+ }
415
+ /**
416
+ * Returns true if the tag is found in the extractor selectors
417
+ *
418
+ * @param tagNode - node of type `tag`
419
+ * @param selectors - extractor selectors
420
+ */
421
+ function isTagFound(tagNode, selectors) {
422
+ return selectors.hasTag(tagNode.value);
423
+ }
424
+ /**
425
+ * Returns true if the selector is inside a pseudo class
426
+ * (e.g. :nth-child, :nth-of-type, :only-child, :not)
427
+ *
428
+ * @param selector - selector
429
+ */
430
+ function isInPseudoClass(selector) {
431
+ return ((selector.parent &&
432
+ selector.parent.type === "pseudo" &&
433
+ selector.parent.value.startsWith(":")) ||
434
+ false);
435
+ }
436
+ /**
437
+ * Returns true if the selector is inside the pseudo classes :where() or :is()
438
+ * @param selector - selector
439
+ */
440
+ function isInPseudoClassWhereOrIs(selector) {
441
+ return ((selector.parent &&
442
+ selector.parent.type === "pseudo" &&
443
+ (selector.parent.value === ":where" ||
444
+ selector.parent.value === ":is")) ||
445
+ false);
446
+ }
447
+ /**
448
+ * Returns true if the selector is a pseudo class at the root level
449
+ * Pseudo classes checked: :where, :is, :has, :not
450
+ * @param selector - selector
451
+ */
452
+ function isPseudoClassAtRootLevel(selector) {
453
+ var _a;
454
+ let result = false;
455
+ if (selector.type === "selector" &&
456
+ ((_a = selector.parent) === null || _a === void 0 ? void 0 : _a.type) === "root" &&
457
+ selector.nodes.length === 1) {
458
+ selector.walk((node) => {
459
+ if (node.type === "pseudo" &&
460
+ (node.value === ":where" ||
461
+ node.value === ":is" ||
462
+ node.value === ":has" ||
463
+ node.value === ":not")) {
464
+ result = true;
465
+ }
466
+ });
467
+ }
468
+ return result;
469
+ }
470
+ function isPostCSSAtRule(node) {
471
+ return (node === null || node === void 0 ? void 0 : node.type) === "atrule";
472
+ }
473
+ function isPostCSSRule(node) {
474
+ return (node === null || node === void 0 ? void 0 : node.type) === "rule";
475
+ }
476
+ function isPostCSSComment(node) {
477
+ return (node === null || node === void 0 ? void 0 : node.type) === "comment";
478
+ }
479
+ /**
480
+ * Class used to instantiate PurgeCSS and can then be used
481
+ * to purge CSS files.
482
+ *
483
+ * @example
484
+ * ```ts
485
+ * await new PurgeCSS().purge({
486
+ * content: ['index.html'],
487
+ * css: ['css/app.css']
488
+ * })
489
+ * ```
490
+ *
491
+ * @public
492
+ */
493
+ class PurgeCSS {
494
+ constructor() {
495
+ this.ignore = false;
496
+ this.atRules = {
497
+ fontFace: [],
498
+ keyframes: [],
499
+ };
500
+ this.usedAnimations = new Set();
501
+ this.usedFontFaces = new Set();
502
+ this.selectorsRemoved = new Set();
503
+ this.removedNodes = [];
504
+ this.variablesStructure = new VariablesStructure();
505
+ this.options = defaultOptions;
506
+ }
507
+ collectDeclarationsData(declaration) {
508
+ const { prop, value } = declaration;
509
+ // collect css properties data
510
+ if (this.options.variables) {
511
+ const usedVariablesMatchesInDeclaration = value.matchAll(/var\((.+?)[,)]/g);
512
+ if (prop.startsWith("--")) {
513
+ this.variablesStructure.addVariable(declaration);
514
+ this.variablesStructure.addVariableUsage(declaration, usedVariablesMatchesInDeclaration);
515
+ }
516
+ else {
517
+ this.variablesStructure.addVariableUsageInProperties(usedVariablesMatchesInDeclaration);
518
+ }
519
+ }
520
+ // collect keyframes data
521
+ if (this.options.keyframes) {
522
+ if (prop === "animation" || prop === "animation-name") {
523
+ for (const word of value.split(/[\s,]+/)) {
524
+ this.usedAnimations.add(word);
525
+ }
526
+ return;
527
+ }
528
+ }
529
+ // collect font faces data
530
+ if (this.options.fontFace) {
531
+ if (prop === "font-family") {
532
+ for (const fontName of value.split(",")) {
533
+ const cleanedFontFace = stripQuotes(fontName.trim());
534
+ this.usedFontFaces.add(cleanedFontFace);
535
+ }
536
+ }
537
+ return;
538
+ }
539
+ }
540
+ /**
541
+ * Get the extractor corresponding to the extension file
542
+ * @param filename - Name of the file
543
+ * @param extractors - Array of extractors definition
544
+ */
545
+ getFileExtractor(filename, extractors) {
546
+ const extractorObj = extractors.find((extractor) => extractor.extensions.find((ext) => filename.endsWith(ext)));
547
+ return typeof extractorObj === "undefined"
548
+ ? this.options.defaultExtractor
549
+ : extractorObj.extractor;
550
+ }
551
+ /**
552
+ * Extract the selectors present in the files using a PurgeCSS extractor
553
+ *
554
+ * @param files - Array of files path or glob pattern
555
+ * @param extractors - Array of extractors
556
+ */
557
+ async extractSelectorsFromFiles(files, extractors) {
558
+ const selectors = new ExtractorResultSets([]);
559
+ const filesNames = [];
560
+ for (const globFile of files) {
561
+ try {
562
+ await asyncFs.access(globFile, fs.constants.F_OK);
563
+ filesNames.push(globFile);
564
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
565
+ }
566
+ catch (err) {
567
+ filesNames.push(...glob.sync(globFile, {
568
+ nodir: true,
569
+ ignore: this.options.skippedContentGlobs.map((glob) => glob.replace(/^\.\//, "")),
570
+ }));
571
+ }
572
+ }
573
+ if (files.length > 0 && filesNames.length === 0) {
574
+ console.warn("No files found from the passed PurgeCSS option 'content'.");
575
+ }
576
+ for (const file of filesNames) {
577
+ const content = await asyncFs.readFile(file, "utf-8");
578
+ const extractor = this.getFileExtractor(file, extractors);
579
+ const extractedSelectors = await extractSelectors(content, extractor);
580
+ selectors.merge(extractedSelectors);
581
+ }
582
+ return selectors;
583
+ }
584
+ /**
585
+ * Extract the selectors present in the passed string using a PurgeCSS extractor
586
+ *
587
+ * @param content - Array of content
588
+ * @param extractors - Array of extractors
589
+ */
590
+ async extractSelectorsFromString(content, extractors) {
591
+ const selectors = new ExtractorResultSets([]);
592
+ for (const { raw, extension } of content) {
593
+ const extractor = this.getFileExtractor(`.${extension}`, extractors);
594
+ const extractedSelectors = await extractSelectors(raw, extractor);
595
+ selectors.merge(extractedSelectors);
596
+ }
597
+ return selectors;
598
+ }
599
+ /**
600
+ * Evaluate at-rule and register it for future reference
601
+ * @param node - node of postcss AST
602
+ */
603
+ evaluateAtRule(node) {
604
+ // keyframes
605
+ if (this.options.keyframes && node.name.endsWith("keyframes")) {
606
+ this.atRules.keyframes.push(node);
607
+ return;
608
+ }
609
+ // font-face
610
+ if (this.options.fontFace && node.name === "font-face" && node.nodes) {
611
+ for (const childNode of node.nodes) {
612
+ if (childNode.type === "decl" && childNode.prop === "font-family") {
613
+ this.atRules.fontFace.push({
614
+ name: stripQuotes(childNode.value),
615
+ node,
616
+ });
617
+ }
618
+ }
619
+ }
620
+ }
621
+ /**
622
+ * Evaluate css selector and decide if it should be removed or not
623
+ *
624
+ * @param node - node of postcss AST
625
+ * @param selectors - selectors used in content files
626
+ */
627
+ evaluateRule(node, selectors) {
628
+ // exit if is in ignoring state activated by an ignore comment
629
+ if (this.ignore) {
630
+ return;
631
+ }
632
+ // exit if the previous annotation is a ignore next line comment
633
+ const annotation = node.prev();
634
+ if (isPostCSSComment(annotation) &&
635
+ isIgnoreAnnotation(annotation, "next")) {
636
+ annotation.remove();
637
+ return;
638
+ }
639
+ // exit if it is inside a keyframes
640
+ if (node.parent &&
641
+ isPostCSSAtRule(node.parent) &&
642
+ node.parent.name.endsWith("keyframes")) {
643
+ return;
644
+ }
645
+ // exit if it is not a rule
646
+ if (!isPostCSSRule(node)) {
647
+ return;
648
+ }
649
+ // exit if it has an ignore rule comment inside
650
+ if (hasIgnoreAnnotation(node)) {
651
+ return;
652
+ }
653
+ const selectorsRemovedFromRule = [];
654
+ // selector transformer, walk over the list of the parsed selectors twice.
655
+ // First pass will remove the unused selectors. It goes through
656
+ // pseudo-classes like :where() and :is() and remove the unused
657
+ // selectors inside of them, but will not remove the pseudo-classes
658
+ // themselves. Second pass will remove selectors containing empty
659
+ // :where and :is.
660
+ node.selector = selectorParser((selectorsParsed) => {
661
+ selectorsParsed.walk((selector) => {
662
+ if (selector.type !== "selector") {
663
+ return;
664
+ }
665
+ const keepSelector = this.shouldKeepSelector(selector, selectors);
666
+ if (!keepSelector) {
667
+ if (this.options.rejected) {
668
+ this.selectorsRemoved.add(selector.toString());
669
+ }
670
+ if (this.options.rejectedCss) {
671
+ selectorsRemovedFromRule.push(selector.toString());
672
+ }
673
+ selector.remove();
674
+ }
675
+ });
676
+ // removes selectors containing empty :where and :is
677
+ selectorsParsed.walk((selector) => {
678
+ if (selector.type !== "selector") {
679
+ return;
680
+ }
681
+ if (selector.toString() && /(:where)|(:is)/.test(selector.toString())) {
682
+ selector.walk((node) => {
683
+ if (node.type !== "pseudo")
684
+ return;
685
+ if (node.value !== ":where" && node.value !== ":is")
686
+ return;
687
+ if (node.nodes.length === 0) {
688
+ selector.remove();
689
+ }
690
+ });
691
+ }
692
+ });
693
+ }).processSync(node.selector);
694
+ // declarations
695
+ if (node.selector && typeof node.nodes !== "undefined") {
696
+ for (const childNode of node.nodes) {
697
+ if (childNode.type !== "decl")
698
+ continue;
699
+ this.collectDeclarationsData(childNode);
700
+ }
701
+ }
702
+ // remove empty rules
703
+ const parent = node.parent;
704
+ if (!node.selector) {
705
+ node.remove();
706
+ }
707
+ if (isRuleEmpty(parent))
708
+ parent === null || parent === void 0 ? void 0 : parent.remove();
709
+ // rebuild the rule with the removed selectors and optionally its parent
710
+ if (this.options.rejectedCss) {
711
+ if (selectorsRemovedFromRule.length > 0) {
712
+ const clone = node.clone();
713
+ const parentClone = parent === null || parent === void 0 ? void 0 : parent.clone().removeAll().append(clone);
714
+ clone.selectors = selectorsRemovedFromRule;
715
+ const nodeToPreserve = parentClone ? parentClone : clone;
716
+ this.removedNodes.push(nodeToPreserve);
717
+ }
718
+ }
719
+ }
720
+ /**
721
+ * Get the purged version of the css based on the files
722
+ *
723
+ * @param cssOptions - css options, files or raw strings
724
+ * @param selectors - set of extracted css selectors
725
+ */
726
+ async getPurgedCSS(cssOptions, selectors) {
727
+ var _a;
728
+ const sources = [];
729
+ // resolve any globs
730
+ const processedOptions = [];
731
+ for (const option of cssOptions) {
732
+ if (typeof option === "string") {
733
+ processedOptions.push(...glob.sync(option, {
734
+ nodir: true,
735
+ ignore: this.options.skippedContentGlobs,
736
+ }));
737
+ }
738
+ else {
739
+ processedOptions.push(option);
740
+ }
741
+ }
742
+ for (const option of processedOptions) {
743
+ const cssContent = typeof option === "string"
744
+ ? this.options.stdin
745
+ ? option
746
+ : await asyncFs.readFile(option, "utf-8")
747
+ : option.raw;
748
+ const isFromFile = typeof option === "string" && !this.options.stdin;
749
+ const root = postcss.parse(cssContent, {
750
+ from: isFromFile ? option : undefined,
751
+ });
752
+ // purge unused selectors
753
+ this.walkThroughCSS(root, selectors);
754
+ if (this.options.fontFace)
755
+ this.removeUnusedFontFaces();
756
+ if (this.options.keyframes)
757
+ this.removeUnusedKeyframes();
758
+ if (this.options.variables)
759
+ this.removeUnusedCSSVariables();
760
+ const postCSSResult = root.toResult({
761
+ map: this.options.sourceMap,
762
+ to: typeof this.options.sourceMap === "object"
763
+ ? this.options.sourceMap.to
764
+ : undefined,
765
+ });
766
+ const result = {
767
+ css: postCSSResult.toString(),
768
+ file: typeof option === "string" ? option : option.name,
769
+ };
770
+ if (this.options.sourceMap) {
771
+ result.sourceMap = (_a = postCSSResult.map) === null || _a === void 0 ? void 0 : _a.toString();
772
+ }
773
+ if (this.options.rejected) {
774
+ result.rejected = Array.from(this.selectorsRemoved);
775
+ this.selectorsRemoved.clear();
776
+ }
777
+ if (this.options.rejectedCss) {
778
+ result.rejectedCss = postcss
779
+ .root({ nodes: this.removedNodes })
780
+ .toString();
781
+ }
782
+ sources.push(result);
783
+ }
784
+ return sources;
785
+ }
786
+ /**
787
+ * Check if the keyframe is safelisted with the option safelist keyframes
788
+ *
789
+ * @param keyframesName - name of the keyframe animation
790
+ */
791
+ isKeyframesSafelisted(keyframesName) {
792
+ return this.options.safelist.keyframes.some((safelistItem) => {
793
+ return typeof safelistItem === "string"
794
+ ? safelistItem === keyframesName
795
+ : safelistItem.test(keyframesName);
796
+ });
797
+ }
798
+ /**
799
+ * Check if the selector is blocklisted with the option blocklist
800
+ *
801
+ * @param selector - css selector
802
+ */
803
+ isSelectorBlocklisted(selector) {
804
+ return this.options.blocklist.some((blocklistItem) => {
805
+ return typeof blocklistItem === "string"
806
+ ? blocklistItem === selector
807
+ : blocklistItem.test(selector);
808
+ });
809
+ }
810
+ /**
811
+ * Check if the selector is safelisted with the option safelist standard
812
+ *
813
+ * @param selector - css selector
814
+ */
815
+ isSelectorSafelisted(selector) {
816
+ const isSafelisted = this.options.safelist.standard.some((safelistItem) => {
817
+ return typeof safelistItem === "string"
818
+ ? safelistItem === selector
819
+ : safelistItem.test(selector);
820
+ });
821
+ const isPseudoElement = /^::.*/.test(selector);
822
+ return CSS_SAFELIST.includes(selector) || isPseudoElement || isSafelisted;
823
+ }
824
+ /**
825
+ * Check if the selector is safelisted with the option safelist deep
826
+ *
827
+ * @param selector - selector
828
+ */
829
+ isSelectorSafelistedDeep(selector) {
830
+ return this.options.safelist.deep.some((safelistItem) => safelistItem.test(selector));
831
+ }
832
+ /**
833
+ * Check if the selector is safelisted with the option safelist greedy
834
+ *
835
+ * @param selector - selector
836
+ */
837
+ isSelectorSafelistedGreedy(selector) {
838
+ return this.options.safelist.greedy.some((safelistItem) => safelistItem.test(selector));
839
+ }
840
+ /**
841
+ * Remove unused CSS
842
+ *
843
+ * @param userOptions - PurgeCSS options or path to the configuration file
844
+ * @returns an array of object containing the filename and the associated CSS
845
+ *
846
+ * @example Using a configuration file named purgecss.config.js
847
+ * ```ts
848
+ * const purgeCSSResults = await new PurgeCSS().purge()
849
+ * ```
850
+ *
851
+ * @example Using a custom path to the configuration file
852
+ * ```ts
853
+ * const purgeCSSResults = await new PurgeCSS().purge('./purgecss.config.js')
854
+ * ```
855
+ *
856
+ * @example Using the PurgeCSS options
857
+ * ```ts
858
+ * const purgeCSSResults = await new PurgeCSS().purge({
859
+ * content: ['index.html', '**\/*.js', '**\/*.html', '**\/*.vue'],
860
+ * css: ['css/app.css']
861
+ * })
862
+ * ```
863
+ */
864
+ async purge(userOptions) {
865
+ this.options =
866
+ typeof userOptions !== "object"
867
+ ? await setOptions(userOptions)
868
+ : {
869
+ ...defaultOptions,
870
+ ...userOptions,
871
+ safelist: standardizeSafelist(userOptions.safelist),
872
+ };
873
+ const { content, css, extractors, safelist } = this.options;
874
+ if (this.options.variables) {
875
+ this.variablesStructure.safelist = safelist.variables || [];
876
+ }
877
+ const fileFormatContents = content.filter((o) => typeof o === "string");
878
+ const rawFormatContents = content.filter((o) => typeof o === "object");
879
+ const cssFileSelectors = await this.extractSelectorsFromFiles(fileFormatContents, extractors);
880
+ const cssRawSelectors = await this.extractSelectorsFromString(rawFormatContents, extractors);
881
+ return this.getPurgedCSS(css, mergeExtractorSelectors(cssFileSelectors, cssRawSelectors));
882
+ }
883
+ /**
884
+ * Remove unused CSS variables
885
+ */
886
+ removeUnusedCSSVariables() {
887
+ this.variablesStructure.removeUnused();
888
+ }
889
+ /**
890
+ * Remove unused font-faces
891
+ */
892
+ removeUnusedFontFaces() {
893
+ for (const { name, node } of this.atRules.fontFace) {
894
+ if (!this.usedFontFaces.has(name)) {
895
+ node.remove();
896
+ }
897
+ }
898
+ }
899
+ /**
900
+ * Remove unused keyframes
901
+ */
902
+ removeUnusedKeyframes() {
903
+ for (const node of this.atRules.keyframes) {
904
+ if (!this.usedAnimations.has(node.params) &&
905
+ !this.isKeyframesSafelisted(node.params)) {
906
+ node.remove();
907
+ }
908
+ }
909
+ }
910
+ /**
911
+ * Transform a selector node into a string
912
+ */
913
+ getSelectorValue(selector) {
914
+ return ((selector.type === "attribute" && selector.attribute) || selector.value);
915
+ }
916
+ /**
917
+ * Determine if the selector should be kept, based on the selectors found in the files
918
+ *
919
+ * @param selector - set of css selectors found in the content files or string
920
+ * @param selectorsFromExtractor - selectors in the css rule
921
+ *
922
+ * @returns true if the selector should be kept in the processed CSS
923
+ */
924
+ shouldKeepSelector(selector, selectorsFromExtractor) {
925
+ // selectors in pseudo classes are ignored except :where() and :is(). For those pseudo-classes, we are treating the selectors inside the same way as they would be outside.
926
+ if (isInPseudoClass(selector) && !isInPseudoClassWhereOrIs(selector)) {
927
+ return true;
928
+ }
929
+ if (isPseudoClassAtRootLevel(selector)) {
930
+ return true;
931
+ }
932
+ // if there is any greedy safelist pattern, run all the selector parts through them
933
+ // if there is any match, return true
934
+ if (this.options.safelist.greedy.length > 0) {
935
+ const selectorParts = selector.nodes.map(this.getSelectorValue);
936
+ if (selectorParts.some((selectorPart) => selectorPart && this.isSelectorSafelistedGreedy(selectorPart))) {
937
+ return true;
938
+ }
939
+ }
940
+ let isPresent = false;
941
+ for (const selectorNode of selector.nodes) {
942
+ const selectorValue = this.getSelectorValue(selectorNode);
943
+ // if the selector is safelisted with children
944
+ // returns true to keep all children selectors
945
+ if (selectorValue && this.isSelectorSafelistedDeep(selectorValue)) {
946
+ return true;
947
+ }
948
+ // The selector is found in the internal and user-defined safelist
949
+ if (selectorValue &&
950
+ (CSS_SAFELIST.includes(selectorValue) ||
951
+ this.isSelectorSafelisted(selectorValue))) {
952
+ isPresent = true;
953
+ continue;
954
+ }
955
+ // The selector is present in the blocklist
956
+ if (selectorValue && this.isSelectorBlocklisted(selectorValue)) {
957
+ return false;
958
+ }
959
+ switch (selectorNode.type) {
960
+ case "attribute":
961
+ // `value` is a dynamic attribute, highly used in input element
962
+ // the choice is to always leave `value` as it can change based on the user
963
+ // idem for `checked`, `selected`, `open`
964
+ isPresent = [
965
+ ...this.options.dynamicAttributes,
966
+ "value",
967
+ "checked",
968
+ "selected",
969
+ "open",
970
+ ].includes(selectorNode.attribute)
971
+ ? true
972
+ : isAttributeFound(selectorNode, selectorsFromExtractor);
973
+ break;
974
+ case "class":
975
+ isPresent = isClassFound(selectorNode, selectorsFromExtractor);
976
+ break;
977
+ case "id":
978
+ isPresent = isIdentifierFound(selectorNode, selectorsFromExtractor);
979
+ break;
980
+ case "tag":
981
+ isPresent = isTagFound(selectorNode, selectorsFromExtractor);
982
+ break;
983
+ default:
984
+ continue;
985
+ }
986
+ // selector is not safelisted
987
+ // and it has not been found as an attribute/class/id/tag
988
+ if (!isPresent) {
989
+ return false;
990
+ }
991
+ }
992
+ return isPresent;
993
+ }
994
+ /**
995
+ * Walk through the CSS AST and remove unused CSS
996
+ *
997
+ * @param root - root node of the postcss AST
998
+ * @param selectors - selectors used in content files
999
+ */
1000
+ walkThroughCSS(root, selectors) {
1001
+ root.walk((node) => {
1002
+ if (node.type === "rule") {
1003
+ return this.evaluateRule(node, selectors);
1004
+ }
1005
+ if (node.type === "atrule") {
1006
+ return this.evaluateAtRule(node);
1007
+ }
1008
+ if (node.type === "comment") {
1009
+ if (isIgnoreAnnotation(node, "start")) {
1010
+ this.ignore = true;
1011
+ // remove ignore annotation
1012
+ node.remove();
1013
+ }
1014
+ else if (isIgnoreAnnotation(node, "end")) {
1015
+ this.ignore = false;
1016
+ // remove ignore annotation
1017
+ node.remove();
1018
+ }
1019
+ }
1020
+ });
1021
+ }
1022
+ }
1023
+
1024
+ export { ExtractorResultSets, PurgeCSS, VariableNode, VariablesStructure, defaultOptions, mergeExtractorSelectors, setOptions, standardizeSafelist };