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