@projectwallace/css-analyzer 9.3.0 → 9.5.0

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/dist/index.js ADDED
@@ -0,0 +1,1010 @@
1
+ import { n as isSupportsBrowserhack, t as isMediaBrowserhack } from "./atrules-CskmpIdJ.js";
2
+ import { n as unquote, r as KeywordSet, t as endsWith } from "./string-utils-olNNcOlY.js";
3
+ import { a as getComplexity, i as getCombinators, n as calculateForAST, o as isAccessibility, s as isPrefixed, t as calculate } from "./specificity-svLpcKkT.js";
4
+ import { a as colorKeywords, i as colorFunctions, n as isValueReset, o as namedColors, r as keywords, s as systemColors, t as isIe9Hack } from "./browserhacks-eP_e1D5u.js";
5
+ import { ATTRIBUTE_SELECTOR, AT_RULE, BLOCK, CONTAINER_QUERY, DECLARATION, DIMENSION, FUNCTION, HASH, IDENTIFIER, LAYER_NAME, MEDIA_FEATURE, NUMBER, OPERATOR, PSEUDO_CLASS_SELECTOR, PSEUDO_ELEMENT_SELECTOR, SELECTOR, SELECTOR_LIST, SKIP, STYLE_RULE, SUPPORTS_QUERY, TYPE_SELECTOR, URL, is_custom, is_vendor_prefixed, parse, str_starts_with, walk } from "@projectwallace/css-parser";
6
+ //#region src/values/destructure-font-shorthand.ts
7
+ const SYSTEM_FONTS = new KeywordSet([
8
+ "caption",
9
+ "icon",
10
+ "menu",
11
+ "message-box",
12
+ "small-caption",
13
+ "status-bar"
14
+ ]);
15
+ const SIZE_KEYWORDS = new KeywordSet([
16
+ "xx-small",
17
+ "x-small",
18
+ "small",
19
+ "medium",
20
+ "large",
21
+ "x-large",
22
+ "xx-large",
23
+ "xxx-large",
24
+ "smaller",
25
+ "larger"
26
+ ]);
27
+ const COMMA = 44;
28
+ const SLASH = 47;
29
+ function destructure(value, cb) {
30
+ let font_family = [void 0, void 0];
31
+ let font_size;
32
+ let line_height;
33
+ if (value.first_child.type === FUNCTION && value.first_child.name?.toLowerCase() === "var") return null;
34
+ let prev;
35
+ for (let node of value.children) {
36
+ let next = node.next_sibling;
37
+ if (node.type === IDENTIFIER && keywords.has(node.name)) cb({
38
+ type: "keyword",
39
+ value: node.name
40
+ });
41
+ if (next && next.type === OPERATOR && next.text.charCodeAt(0) === SLASH) {
42
+ font_size = node.text;
43
+ prev = node;
44
+ continue;
45
+ }
46
+ if (prev?.type === OPERATOR && prev.text.charCodeAt(0) === SLASH) {
47
+ line_height = node.text;
48
+ prev = node;
49
+ continue;
50
+ }
51
+ if (next?.type === OPERATOR && next.text.charCodeAt(0) === COMMA && !font_family[0]) {
52
+ font_family[0] = node;
53
+ if (!font_size && prev) font_size = prev.text;
54
+ prev = node;
55
+ continue;
56
+ }
57
+ if (node.next_sibling === null) {
58
+ font_family[1] = node;
59
+ if (!font_size && !font_family[0] && prev) font_size = prev.text;
60
+ prev = node;
61
+ continue;
62
+ }
63
+ if (node.type === NUMBER) {
64
+ prev = node;
65
+ continue;
66
+ }
67
+ if (node.type === IDENTIFIER) {
68
+ let name = node.name;
69
+ if (name && SIZE_KEYWORDS.has(name)) {
70
+ font_size = name;
71
+ prev = node;
72
+ continue;
73
+ }
74
+ }
75
+ prev = node;
76
+ }
77
+ let family = font_family[0] || font_family[1] ? value.text.substring((font_family?.[0] || font_family?.[1] || { start: value.start }).start - value.start, font_family[1] ? font_family[1].end - value.start : value.text.length) : null;
78
+ return {
79
+ font_size,
80
+ line_height,
81
+ font_family: family
82
+ };
83
+ }
84
+ //#endregion
85
+ //#region src/values/animations.ts
86
+ const TIMING_KEYWORDS = new KeywordSet([
87
+ "linear",
88
+ "ease",
89
+ "ease-in",
90
+ "ease-out",
91
+ "ease-in-out",
92
+ "step-start",
93
+ "step-end"
94
+ ]);
95
+ const TIMING_FUNCTION_VALUES = new KeywordSet(["cubic-bezier", "steps"]);
96
+ function analyzeAnimation(children, cb) {
97
+ let durationFound = false;
98
+ for (let child of children) {
99
+ let type = child.type;
100
+ let name = child.name;
101
+ if (type === OPERATOR) durationFound = false;
102
+ else if (type === DIMENSION && durationFound === false) {
103
+ durationFound = true;
104
+ cb({
105
+ type: "duration",
106
+ value: child
107
+ });
108
+ } else if (type === IDENTIFIER && name) {
109
+ if (TIMING_KEYWORDS.has(name)) cb({
110
+ type: "fn",
111
+ value: child
112
+ });
113
+ else if (keywords.has(name)) cb({
114
+ type: "keyword",
115
+ value: child
116
+ });
117
+ } else if (type === FUNCTION && name && TIMING_FUNCTION_VALUES.has(name)) cb({
118
+ type: "fn",
119
+ value: child
120
+ });
121
+ }
122
+ }
123
+ //#endregion
124
+ //#region src/values/vendor-prefix.ts
125
+ function isValuePrefixed(node, on_value) {
126
+ walk(node, function(child) {
127
+ if (child.is_vendor_prefixed) on_value(child.name || child.text);
128
+ });
129
+ }
130
+ //#endregion
131
+ //#region src/collection.ts
132
+ var Collection = class {
133
+ #items;
134
+ #total;
135
+ #nodes = [];
136
+ #useLocations;
137
+ constructor(useLocations = false) {
138
+ this.#items = /* @__PURE__ */ new Map();
139
+ this.#total = 0;
140
+ if (useLocations) this.#nodes = [];
141
+ this.#useLocations = useLocations;
142
+ }
143
+ p(item, node_location) {
144
+ let index = this.#total;
145
+ if (this.#useLocations) {
146
+ let position = index * 4;
147
+ this.#nodes[position] = node_location.line;
148
+ this.#nodes[position + 1] = node_location.column;
149
+ this.#nodes[position + 2] = node_location.offset;
150
+ this.#nodes[position + 3] = node_location.length;
151
+ }
152
+ if (this.#items.has(item)) {
153
+ this.#items.get(item).push(index);
154
+ this.#total++;
155
+ return;
156
+ }
157
+ this.#items.set(item, [index]);
158
+ this.#total++;
159
+ }
160
+ size() {
161
+ return this.#total;
162
+ }
163
+ c() {
164
+ let uniqueWithLocations = /* @__PURE__ */ new Map();
165
+ let unique = {};
166
+ let useLocations = this.#useLocations;
167
+ let items = this.#items;
168
+ let _nodes = this.#nodes;
169
+ let size = items.size;
170
+ items.forEach((list, key) => {
171
+ if (useLocations) {
172
+ let nodes = list.map(function(index) {
173
+ let position = index * 4;
174
+ return {
175
+ line: _nodes[position],
176
+ column: _nodes[position + 1],
177
+ offset: _nodes[position + 2],
178
+ length: _nodes[position + 3]
179
+ };
180
+ });
181
+ uniqueWithLocations.set(key, nodes);
182
+ } else unique[key] = list.length;
183
+ });
184
+ let total = this.#total;
185
+ if (useLocations) return {
186
+ total,
187
+ totalUnique: size,
188
+ unique,
189
+ uniquenessRatio: total === 0 ? 0 : size / total,
190
+ uniqueWithLocations: Object.fromEntries(uniqueWithLocations)
191
+ };
192
+ return {
193
+ total,
194
+ totalUnique: size,
195
+ unique,
196
+ uniquenessRatio: total === 0 ? 0 : size / total,
197
+ uniqueWithLocations: void 0
198
+ };
199
+ }
200
+ };
201
+ //#endregion
202
+ //#region src/context-collection.ts
203
+ var ContextCollection = class {
204
+ #list;
205
+ #contexts;
206
+ #useLocations;
207
+ constructor(useLocations) {
208
+ this.#list = new Collection(useLocations);
209
+ this.#contexts = /* @__PURE__ */ new Map();
210
+ this.#useLocations = useLocations;
211
+ }
212
+ /**
213
+ * Add an item to this #list's context
214
+ * @param item Item to push
215
+ * @param context Context to push Item to
216
+ * @param node_location
217
+ */
218
+ push(item, context, node_location) {
219
+ this.#list.p(item, node_location);
220
+ if (!this.#contexts.has(context)) this.#contexts.set(context, new Collection(this.#useLocations));
221
+ this.#contexts.get(context).p(item, node_location);
222
+ }
223
+ count() {
224
+ let itemsPerContext = /* @__PURE__ */ new Map();
225
+ for (let [context, value] of this.#contexts.entries()) itemsPerContext.set(context, value.c());
226
+ return Object.assign(this.#list.c(), { itemsPerContext: Object.fromEntries(itemsPerContext) });
227
+ }
228
+ };
229
+ //#endregion
230
+ //#region src/aggregate-collection.ts
231
+ /**
232
+ * Find the mode (most occurring value) in an array of Numbers
233
+ * Takes the mean/average of multiple values if multiple values occur the same amount of times.
234
+ *
235
+ * @see https://github.com/angus-c/just/blob/684af9ca0c7808bc78543ec89379b1fdfce502b1/packages/array-mode/index.js
236
+ * @param arr - Array to find the mode value for
237
+ * @returns mode - The `mode` value of `arr`
238
+ */
239
+ function Mode(arr) {
240
+ let frequencies = /* @__PURE__ */ new Map();
241
+ let maxOccurrences = -1;
242
+ let maxOccurenceCount = 0;
243
+ let sum = 0;
244
+ let len = arr.length;
245
+ for (let i = 0; i < len; i++) {
246
+ let element = arr[i];
247
+ let updatedCount = (frequencies.get(element) || 0) + 1;
248
+ frequencies.set(element, updatedCount);
249
+ if (updatedCount > maxOccurrences) {
250
+ maxOccurrences = updatedCount;
251
+ maxOccurenceCount = 0;
252
+ sum = 0;
253
+ }
254
+ if (updatedCount >= maxOccurrences) {
255
+ maxOccurenceCount++;
256
+ sum += element;
257
+ }
258
+ }
259
+ return sum / maxOccurenceCount;
260
+ }
261
+ var AggregateCollection = class {
262
+ #items;
263
+ #sum;
264
+ constructor() {
265
+ this.#items = [];
266
+ this.#sum = 0;
267
+ }
268
+ /**
269
+ * Add a new Integer at the end of this AggregateCollection
270
+ * @param item - The item to add
271
+ */
272
+ push(item) {
273
+ this.#items.push(item);
274
+ this.#sum += item;
275
+ }
276
+ size() {
277
+ return this.#items.length;
278
+ }
279
+ aggregate() {
280
+ let len = this.#items.length;
281
+ if (len === 0) return {
282
+ min: 0,
283
+ max: 0,
284
+ mean: 0,
285
+ mode: 0,
286
+ range: 0,
287
+ sum: 0
288
+ };
289
+ let sorted = this.#items.slice().sort((a, b) => a - b);
290
+ let min = sorted[0];
291
+ let max = sorted[len - 1];
292
+ let mode = Mode(sorted);
293
+ let sum = this.#sum;
294
+ return {
295
+ min,
296
+ max,
297
+ mean: sum / len,
298
+ mode,
299
+ range: max - min,
300
+ sum
301
+ };
302
+ }
303
+ toArray() {
304
+ return this.#items;
305
+ }
306
+ };
307
+ //#endregion
308
+ //#region src/stylesheet/stylesheet.ts
309
+ function getEmbedType(embed) {
310
+ let start = 5;
311
+ let semicolon = embed.indexOf(";");
312
+ let comma = embed.indexOf(",");
313
+ if (semicolon === -1) return embed.substring(start, comma);
314
+ if (comma !== -1 && comma < semicolon) return embed.substring(start, comma);
315
+ return embed.substring(start, semicolon);
316
+ }
317
+ //#endregion
318
+ //#region src/properties/property-utils.ts
319
+ const SPACING_RESET_PROPERTIES = new Set([
320
+ "margin",
321
+ "margin-block",
322
+ "margin-inline",
323
+ "margin-top",
324
+ "margin-block-start",
325
+ "margin-block-end",
326
+ "margin-inline-end",
327
+ "margin-inline-end",
328
+ "margin-right",
329
+ "margin-bottom",
330
+ "margin-left",
331
+ "padding",
332
+ "padding-block",
333
+ "padding-inline",
334
+ "padding-top",
335
+ "padding-right",
336
+ "padding-bottom",
337
+ "padding-left",
338
+ "padding-block-start",
339
+ "padding-block-end",
340
+ "padding-inline-start",
341
+ "padding-inline-end"
342
+ ]);
343
+ const border_radius_properties = new KeywordSet([
344
+ "border-radius",
345
+ "border-top-left-radius",
346
+ "border-top-right-radius",
347
+ "border-bottom-right-radius",
348
+ "border-bottom-left-radius",
349
+ "border-start-start-radius",
350
+ "border-start-end-radius",
351
+ "border-end-end-radius",
352
+ "border-end-start-radius"
353
+ ]);
354
+ /**
355
+ * @see https://github.com/csstree/csstree/blob/master/lib/utils/names.js#L69
356
+ */
357
+ function isHack(property) {
358
+ if (is_custom(property) || is_vendor_prefixed(property)) return false;
359
+ let code = property.charCodeAt(0);
360
+ return code === 47 || code === 42 || code === 95 || code === 43 || code === 38 || code === 36 || code === 35;
361
+ }
362
+ /**
363
+ * Get the normalized basename for a property with a vendor prefix
364
+ * @returns The property name without vendor prefix
365
+ */
366
+ function basename(property) {
367
+ if (is_custom(property)) return property;
368
+ if (is_vendor_prefixed(property)) return property.slice(property.indexOf("-", 2) + 1).toLowerCase();
369
+ if (isHack(property)) return property.slice(1).toLowerCase();
370
+ return property.toLowerCase();
371
+ }
372
+ //#endregion
373
+ //#region src/vendor-prefix.ts
374
+ /** Kept for backwards compatibility */
375
+ function hasVendorPrefix(keyword) {
376
+ return is_vendor_prefixed(keyword);
377
+ }
378
+ //#endregion
379
+ //#region src/index.ts
380
+ function ratio(part, total) {
381
+ if (total === 0) return 0;
382
+ return part / total;
383
+ }
384
+ function analyze(css, options = {}) {
385
+ if (options.useLocations === true) return analyzeInternal(css, options, true);
386
+ return analyzeInternal(css, options, false);
387
+ }
388
+ function analyzeInternal(css, options, useLocations) {
389
+ let start = Date.now();
390
+ let linesOfCode = (css.match(/\n/g) || []).length + 1;
391
+ let totalComments = 0;
392
+ let commentsSize = 0;
393
+ let embedSize = 0;
394
+ let embedTypes = {
395
+ total: 0,
396
+ unique: /* @__PURE__ */ new Map()
397
+ };
398
+ let startParse = Date.now();
399
+ let ast = parse(css, { on_comment({ length }) {
400
+ totalComments++;
401
+ commentsSize += length;
402
+ } });
403
+ let startAnalysis = Date.now();
404
+ let atrules = new Collection(useLocations);
405
+ let atRuleComplexities = new AggregateCollection();
406
+ /** @type {Record<string, string>[]} */
407
+ let fontfaces = [];
408
+ let fontfaces_with_loc = new Collection(useLocations);
409
+ let layers = new Collection(useLocations);
410
+ let imports = new Collection(useLocations);
411
+ let medias = new Collection(useLocations);
412
+ let mediaBrowserhacks = new Collection(useLocations);
413
+ let mediaFeatures = new Collection(useLocations);
414
+ let charsets = new Collection(useLocations);
415
+ let supports = new Collection(useLocations);
416
+ let supportsBrowserhacks = new Collection(useLocations);
417
+ let keyframes = new Collection(useLocations);
418
+ let prefixedKeyframes = new Collection(useLocations);
419
+ let containers = new Collection(useLocations);
420
+ let containerNames = new Collection(useLocations);
421
+ let registeredProperties = new Collection(useLocations);
422
+ let functions = new Collection(useLocations);
423
+ let scopes = new Collection(useLocations);
424
+ let atruleNesting = new AggregateCollection();
425
+ let uniqueAtruleNesting = new Collection(useLocations);
426
+ let totalRules = 0;
427
+ let emptyRules = 0;
428
+ let ruleSizes = new AggregateCollection();
429
+ let selectorsPerRule = new AggregateCollection();
430
+ let declarationsPerRule = new AggregateCollection();
431
+ let uniqueRuleSize = new Collection(useLocations);
432
+ let uniqueSelectorsPerRule = new Collection(useLocations);
433
+ let uniqueDeclarationsPerRule = new Collection(useLocations);
434
+ let ruleNesting = new AggregateCollection();
435
+ let uniqueRuleNesting = new Collection(useLocations);
436
+ let keyframeSelectors = new Collection(useLocations);
437
+ let uniqueSelectors = /* @__PURE__ */ new Set();
438
+ let prefixedSelectors = new Collection(useLocations);
439
+ let maxSpecificity;
440
+ let minSpecificity;
441
+ let specificityA = new AggregateCollection();
442
+ let specificityB = new AggregateCollection();
443
+ let specificityC = new AggregateCollection();
444
+ let uniqueSpecificities = new Collection(useLocations);
445
+ let selectorComplexities = new AggregateCollection();
446
+ let uniqueSelectorComplexities = new Collection(useLocations);
447
+ let specificities = [];
448
+ let ids = new Collection(useLocations);
449
+ let a11y = new Collection(useLocations);
450
+ let pseudoClasses = new Collection(useLocations);
451
+ let pseudoElements = new Collection(useLocations);
452
+ let attributeSelectors = new Collection(useLocations);
453
+ let customElementSelectors = new Collection(useLocations);
454
+ let combinators = new Collection(useLocations);
455
+ let selectorNesting = new AggregateCollection();
456
+ let uniqueSelectorNesting = new Collection(useLocations);
457
+ let uniqueDeclarations = /* @__PURE__ */ new Set();
458
+ let totalDeclarations = 0;
459
+ let declarationComplexities = new AggregateCollection();
460
+ let importantDeclarations = 0;
461
+ let importantsInKeyframes = 0;
462
+ let importantCustomProperties = new Collection(useLocations);
463
+ let declarationNesting = new AggregateCollection();
464
+ let uniqueDeclarationNesting = new Collection(useLocations);
465
+ let properties = new Collection(useLocations);
466
+ let propertyHacks = new Collection(useLocations);
467
+ let propertyVendorPrefixes = new Collection(useLocations);
468
+ let customProperties = new Collection(useLocations);
469
+ let propertyComplexities = new AggregateCollection();
470
+ let valueComplexities = new AggregateCollection();
471
+ let vendorPrefixedValues = new Collection(useLocations);
472
+ let valueBrowserhacks = new Collection(useLocations);
473
+ let displays = new Collection(useLocations);
474
+ let zindex = new Collection(useLocations);
475
+ let textShadows = new Collection(useLocations);
476
+ let boxShadows = new Collection(useLocations);
477
+ let fontFamilies = new Collection(useLocations);
478
+ let fontSizes = new Collection(useLocations);
479
+ let lineHeights = new Collection(useLocations);
480
+ let timingFunctions = new Collection(useLocations);
481
+ let durations = new Collection(useLocations);
482
+ let colors = new ContextCollection(useLocations);
483
+ let colorFormats = new Collection(useLocations);
484
+ let units = new ContextCollection(useLocations);
485
+ let gradients = new Collection(useLocations);
486
+ let valueKeywords = new Collection(useLocations);
487
+ let borderRadiuses = new ContextCollection(useLocations);
488
+ let resets = new Collection(useLocations);
489
+ function toLoc(node) {
490
+ return {
491
+ line: node.line,
492
+ column: node.column,
493
+ offset: node.start,
494
+ length: node.length
495
+ };
496
+ }
497
+ let keyframesDepth = -1;
498
+ walk(ast, (node, depth) => {
499
+ if (keyframesDepth >= 0 && depth <= keyframesDepth) keyframesDepth = -1;
500
+ let inKeyframes = keyframesDepth >= 0 && depth > keyframesDepth;
501
+ if (node.type === AT_RULE) {
502
+ let atruleLoc = toLoc(node);
503
+ atruleNesting.push(depth);
504
+ uniqueAtruleNesting.p(depth, atruleLoc);
505
+ let normalized_name = basename(node.name ?? "");
506
+ atrules.p(normalized_name, atruleLoc);
507
+ if (normalized_name === "font-face") {
508
+ let descriptors = Object.create(null);
509
+ if (useLocations) fontfaces_with_loc.p(node.start, toLoc(node));
510
+ let block = node.children.find((child) => child.type === BLOCK);
511
+ for (let descriptor of block?.children || []) if (descriptor.type === DECLARATION && descriptor.value) descriptors[descriptor.property] = descriptor.value.text;
512
+ atRuleComplexities.push(1);
513
+ fontfaces.push(descriptors);
514
+ }
515
+ if (node.prelude === null || node.prelude === void 0) {
516
+ if (normalized_name === "layer") {
517
+ layers.p("<anonymous>", toLoc(node));
518
+ atRuleComplexities.push(2);
519
+ }
520
+ } else {
521
+ let complexity = 1;
522
+ if (normalized_name === "media") {
523
+ medias.p(node.prelude.text, toLoc(node));
524
+ isMediaBrowserhack(node.prelude, (hack) => {
525
+ mediaBrowserhacks.p(hack, toLoc(node));
526
+ complexity++;
527
+ });
528
+ } else if (normalized_name === "supports") {
529
+ supports.p(node.prelude.text, toLoc(node));
530
+ isSupportsBrowserhack(node.prelude, (hack) => {
531
+ supportsBrowserhacks.p(hack, toLoc(node));
532
+ complexity++;
533
+ });
534
+ } else if (normalized_name.endsWith("keyframes")) {
535
+ let prelude = node.prelude.text;
536
+ keyframes.p(prelude, toLoc(node));
537
+ if (node.is_vendor_prefixed) {
538
+ prefixedKeyframes.p(`@${node.name?.toLowerCase()} ${node.prelude.text}`, toLoc(node));
539
+ complexity++;
540
+ }
541
+ keyframesDepth = depth;
542
+ } else if (normalized_name === "layer") for (let layer of node.prelude.text.split(",").map((s) => s.trim())) layers.p(layer, toLoc(node));
543
+ else if (normalized_name === "import") {
544
+ imports.p(node.prelude.text, toLoc(node));
545
+ if (node.prelude.has_children) {
546
+ for (let child of node.prelude) if (child.type === SUPPORTS_QUERY && typeof child.value === "string") supports.p(child.value, toLoc(child));
547
+ else if (child.type === LAYER_NAME && typeof child.value === "string") layers.p(child.value, toLoc(child));
548
+ }
549
+ } else if (normalized_name === "container") {
550
+ containers.p(node.prelude.text, toLoc(node));
551
+ if (node.prelude.first_child?.type === CONTAINER_QUERY) {
552
+ if (node.prelude.first_child.first_child?.type === IDENTIFIER) containerNames.p(node.prelude.first_child.first_child.text, toLoc(node));
553
+ }
554
+ } else if (normalized_name === "property") registeredProperties.p(node.prelude.text, toLoc(node));
555
+ else if (normalized_name === "function") {
556
+ let prelude = node.prelude.text;
557
+ let name = prelude.includes("(") ? prelude.slice(0, prelude.indexOf("(")).trim() : prelude.trim();
558
+ functions.p(name, toLoc(node));
559
+ } else if (normalized_name === "charset") charsets.p(node.prelude.text.toLowerCase(), toLoc(node));
560
+ else if (normalized_name === "scope") scopes.p(node.prelude.text, toLoc(node));
561
+ atRuleComplexities.push(complexity);
562
+ }
563
+ } else if (node.type === STYLE_RULE) if (inKeyframes && node.prelude) {
564
+ if (node.prelude.type === SELECTOR_LIST && node.prelude.children.length > 0) for (let keyframe_selector of node.prelude.children) keyframeSelectors.p(keyframe_selector.text, toLoc(keyframe_selector));
565
+ } else {
566
+ totalRules++;
567
+ if (node.block?.is_empty) emptyRules++;
568
+ let numSelectors = 0;
569
+ let numDeclarations = 0;
570
+ let loc = toLoc(node);
571
+ if (node.prelude) {
572
+ for (const selector of node.prelude.children) if (selector.type === SELECTOR) numSelectors++;
573
+ }
574
+ if (node.block) {
575
+ for (const declaration of node.block.children) if (declaration.type === DECLARATION) numDeclarations++;
576
+ }
577
+ ruleSizes.push(numSelectors + numDeclarations);
578
+ uniqueRuleSize.p(numSelectors + numDeclarations, loc);
579
+ selectorsPerRule.push(numSelectors);
580
+ uniqueSelectorsPerRule.p(numSelectors, loc);
581
+ declarationsPerRule.push(numDeclarations);
582
+ uniqueDeclarationsPerRule.p(numDeclarations, loc);
583
+ ruleNesting.push(depth);
584
+ uniqueRuleNesting.p(depth, loc);
585
+ }
586
+ else if (node.type === SELECTOR) {
587
+ if (inKeyframes) return SKIP;
588
+ let loc = toLoc(node);
589
+ selectorNesting.push(depth > 0 ? depth - 1 : 0);
590
+ uniqueSelectorNesting.p(depth > 0 ? depth - 1 : 0, loc);
591
+ uniqueSelectors.add(node.text);
592
+ let complexity = getComplexity(node);
593
+ selectorComplexities.push(complexity);
594
+ uniqueSelectorComplexities.p(complexity, loc);
595
+ isPrefixed(node, (prefix) => {
596
+ prefixedSelectors.p(prefix.toLowerCase(), loc);
597
+ });
598
+ isAccessibility(node, (a11y_selector) => {
599
+ a11y.p(a11y_selector, loc);
600
+ });
601
+ walk(node, (child) => {
602
+ if (child.type === ATTRIBUTE_SELECTOR) attributeSelectors.p(child.name?.toLowerCase() ?? "", loc);
603
+ else if (child.type === TYPE_SELECTOR && child.name?.includes("-")) customElementSelectors.p(child.name.toLowerCase(), loc);
604
+ else if (child.type === PSEUDO_CLASS_SELECTOR) pseudoClasses.p(child.name?.toLowerCase() ?? "", loc);
605
+ else if (child.type === PSEUDO_ELEMENT_SELECTOR) pseudoElements.p(child.name?.toLowerCase() ?? "", loc);
606
+ });
607
+ getCombinators(node, (combinator) => {
608
+ let name = combinator.name.trim() === "" ? " " : combinator.name;
609
+ combinators.p(name, combinator.loc);
610
+ });
611
+ let specificity = calculateForAST(node);
612
+ let [sa, sb, sc] = specificity;
613
+ uniqueSpecificities.p(specificity.toString(), loc);
614
+ specificityA.push(sa);
615
+ specificityB.push(sb);
616
+ specificityC.push(sc);
617
+ if (maxSpecificity === void 0) maxSpecificity = specificity;
618
+ if (minSpecificity === void 0) minSpecificity = specificity;
619
+ if (minSpecificity !== void 0 && compareSpecificity(minSpecificity, specificity) < 0) minSpecificity = specificity;
620
+ if (maxSpecificity !== void 0 && compareSpecificity(maxSpecificity, specificity) > 0) maxSpecificity = specificity;
621
+ specificities.push(specificity);
622
+ if (sa > 0) ids.p(node.text, loc);
623
+ return SKIP;
624
+ } else if (node.type === DECLARATION) {
625
+ totalDeclarations++;
626
+ uniqueDeclarations.add(node.text);
627
+ let loc = toLoc(node);
628
+ let declarationDepth = depth > 0 ? depth - 1 : 0;
629
+ declarationNesting.push(declarationDepth);
630
+ uniqueDeclarationNesting.p(declarationDepth, loc);
631
+ let complexity = 1;
632
+ if (node.is_important) {
633
+ complexity++;
634
+ if (!node.text.toLowerCase().includes("!important")) valueBrowserhacks.p("!ie", toLoc(node.value));
635
+ if (inKeyframes) {
636
+ importantsInKeyframes++;
637
+ complexity++;
638
+ }
639
+ }
640
+ declarationComplexities.push(complexity);
641
+ let { is_important, property, is_browserhack, is_vendor_prefixed } = node;
642
+ if (!property) return;
643
+ let propertyLoc = toLoc(node);
644
+ propertyLoc.length = property.length;
645
+ let normalizedProperty = basename(property);
646
+ properties.p(normalizedProperty, propertyLoc);
647
+ if (is_important) importantDeclarations++;
648
+ if (is_vendor_prefixed) {
649
+ propertyComplexities.push(2);
650
+ propertyVendorPrefixes.p(property, propertyLoc);
651
+ } else if (is_custom(property)) {
652
+ customProperties.p(property, propertyLoc);
653
+ propertyComplexities.push(is_important ? 3 : 2);
654
+ if (is_important) importantCustomProperties.p(property, propertyLoc);
655
+ } else if (is_browserhack) {
656
+ propertyHacks.p(property.charAt(0), propertyLoc);
657
+ propertyComplexities.push(2);
658
+ } else propertyComplexities.push(1);
659
+ {
660
+ let value = node.value;
661
+ let { text } = value;
662
+ let valueLoc = toLoc(value);
663
+ let complexity = 1;
664
+ if (keywords.has(text)) {
665
+ valueKeywords.p(text.toLowerCase(), valueLoc);
666
+ valueComplexities.push(complexity);
667
+ if (normalizedProperty === "display") displays.p(text.toLowerCase(), valueLoc);
668
+ return;
669
+ }
670
+ isValuePrefixed(value, (prefixed) => {
671
+ vendorPrefixedValues.p(prefixed.toLowerCase(), valueLoc);
672
+ complexity++;
673
+ });
674
+ if (isIe9Hack(value)) {
675
+ valueBrowserhacks.p("\\9", valueLoc);
676
+ text = text.slice(0, -2);
677
+ complexity++;
678
+ }
679
+ valueComplexities.push(complexity);
680
+ if (SPACING_RESET_PROPERTIES.has(normalizedProperty)) {
681
+ if (isValueReset(value)) resets.p(normalizedProperty, valueLoc);
682
+ } else if (normalizedProperty === "display") if (/var\(/i.test(text)) displays.p(text, valueLoc);
683
+ else displays.p(text.toLowerCase(), valueLoc);
684
+ else if (normalizedProperty === "z-index") {
685
+ zindex.p(text, valueLoc);
686
+ return SKIP;
687
+ } else if (normalizedProperty === "font") {
688
+ if (!SYSTEM_FONTS.has(text)) {
689
+ let result = destructure(value, function(item) {
690
+ if (item.type === "keyword") valueKeywords.p(item.value.toLowerCase(), valueLoc);
691
+ });
692
+ if (!result) return SKIP;
693
+ let { font_size, line_height, font_family } = result;
694
+ if (font_family) fontFamilies.p(font_family, valueLoc);
695
+ if (font_size) fontSizes.p(font_size.toLowerCase(), valueLoc);
696
+ if (line_height) lineHeights.p(line_height.toLowerCase(), valueLoc);
697
+ }
698
+ } else if (normalizedProperty === "font-size") {
699
+ if (!SYSTEM_FONTS.has(text)) {
700
+ let normalized = text.toLowerCase();
701
+ if (normalized.includes("var(")) fontSizes.p(text, valueLoc);
702
+ else fontSizes.p(normalized, valueLoc);
703
+ }
704
+ } else if (normalizedProperty === "font-family") {
705
+ if (!SYSTEM_FONTS.has(text)) fontFamilies.p(text, valueLoc);
706
+ return SKIP;
707
+ } else if (normalizedProperty === "line-height") {
708
+ let normalized = text.toLowerCase();
709
+ if (normalized.includes("var(")) lineHeights.p(text, valueLoc);
710
+ else lineHeights.p(normalized, valueLoc);
711
+ } else if (normalizedProperty === "transition" || normalizedProperty === "animation") {
712
+ analyzeAnimation(value.children, function(item) {
713
+ if (item.type === "fn") timingFunctions.p(item.value.text.toLowerCase(), valueLoc);
714
+ else if (item.type === "duration") durations.p(item.value.text.toLowerCase(), valueLoc);
715
+ else if (item.type === "keyword") valueKeywords.p(item.value.text.toLowerCase(), valueLoc);
716
+ });
717
+ return SKIP;
718
+ } else if (normalizedProperty === "animation-duration" || normalizedProperty === "transition-duration") {
719
+ for (let child of value.children) if (child.type !== OPERATOR) {
720
+ let text = child.text;
721
+ if (/var\(/i.test(text)) durations.p(text, valueLoc);
722
+ else durations.p(text.toLowerCase(), valueLoc);
723
+ }
724
+ } else if (normalizedProperty === "transition-timing-function" || normalizedProperty === "animation-timing-function") {
725
+ for (let child of value.children) if (child.type !== OPERATOR) timingFunctions.p(child.text, valueLoc);
726
+ } else if (normalizedProperty === "container-name") containerNames.p(text, valueLoc);
727
+ else if (normalizedProperty === "container") {
728
+ if (value.first_child?.type === IDENTIFIER) containerNames.p(value.first_child.text, valueLoc);
729
+ } else if (border_radius_properties.has(normalizedProperty)) borderRadiuses.push(text, property, valueLoc);
730
+ else if (normalizedProperty === "text-shadow") textShadows.p(text, valueLoc);
731
+ else if (normalizedProperty === "box-shadow") boxShadows.p(text, valueLoc);
732
+ let valueHasIe9Hack = isIe9Hack(value);
733
+ walk(value, (valueNode) => {
734
+ switch (valueNode.type) {
735
+ case DIMENSION: {
736
+ let unit = valueNode.unit?.toLowerCase() ?? "";
737
+ let loc = toLoc(valueNode);
738
+ units.push(unit, property, loc);
739
+ return SKIP;
740
+ }
741
+ case HASH: {
742
+ let hashText = valueNode.text;
743
+ if (!hashText || !hashText.startsWith("#")) return SKIP;
744
+ let hashValue = hashText.toLowerCase();
745
+ if (valueHasIe9Hack && !hashValue.endsWith("\\9")) hashValue = hashValue + "\\9";
746
+ let hexLength = hashValue.length - 1;
747
+ if (endsWith("\\9", hashValue)) hexLength = hexLength - 2;
748
+ let hashLoc = toLoc(valueNode);
749
+ colors.push(hashValue, property, hashLoc);
750
+ colorFormats.p(`hex` + hexLength, hashLoc);
751
+ return SKIP;
752
+ }
753
+ case IDENTIFIER: {
754
+ let identifierText = valueNode.text;
755
+ let identifierLoc = toLoc(valueNode);
756
+ if (normalizedProperty === "font" || normalizedProperty === "font-family") return SKIP;
757
+ if (keywords.has(identifierText)) valueKeywords.p(identifierText.toLowerCase(), identifierLoc);
758
+ let nodeLen = identifierText.length;
759
+ if (nodeLen > 20 || nodeLen < 3) return SKIP;
760
+ if (colorKeywords.has(identifierText)) {
761
+ let colorKeyword = identifierText.toLowerCase();
762
+ colors.push(colorKeyword, property, identifierLoc);
763
+ colorFormats.p(colorKeyword, identifierLoc);
764
+ return;
765
+ }
766
+ if (namedColors.has(identifierText)) {
767
+ colors.push(identifierText.toLowerCase(), property, identifierLoc);
768
+ colorFormats.p("named", identifierLoc);
769
+ return;
770
+ }
771
+ if (systemColors.has(identifierText)) {
772
+ colors.push(identifierText.toLowerCase(), property, identifierLoc);
773
+ colorFormats.p("system", identifierLoc);
774
+ return;
775
+ }
776
+ return SKIP;
777
+ }
778
+ case FUNCTION: {
779
+ let funcName = valueNode.name;
780
+ let funcLoc = toLoc(valueNode);
781
+ if (colorFunctions.has(funcName)) {
782
+ colors.push(valueNode.text, property, funcLoc);
783
+ colorFormats.p(funcName.toLowerCase(), funcLoc);
784
+ return;
785
+ }
786
+ if (endsWith("gradient", funcName)) {
787
+ gradients.p(valueNode.text, funcLoc);
788
+ return;
789
+ }
790
+ }
791
+ }
792
+ });
793
+ }
794
+ } else if (node.type === URL) {
795
+ let { value } = node;
796
+ let embed = unquote(value || "");
797
+ if (str_starts_with(embed, "data:")) {
798
+ let size = embed.length;
799
+ let type = getEmbedType(embed);
800
+ embedTypes.total++;
801
+ embedSize += size;
802
+ let loc = {
803
+ line: node.line,
804
+ column: node.column,
805
+ offset: node.start,
806
+ length: node.length
807
+ };
808
+ if (embedTypes.unique.has(type)) {
809
+ let item = embedTypes.unique.get(type);
810
+ item.count++;
811
+ item.size += size;
812
+ embedTypes.unique.set(type, item);
813
+ if (useLocations && item.uniqueWithLocations) item.uniqueWithLocations.push(loc);
814
+ } else {
815
+ let item = {
816
+ count: 1,
817
+ size,
818
+ uniqueWithLocations: useLocations ? [loc] : void 0
819
+ };
820
+ embedTypes.unique.set(type, item);
821
+ }
822
+ }
823
+ } else if (node.type === MEDIA_FEATURE) {
824
+ if (node.property) mediaFeatures.p(node.property.toLowerCase(), toLoc(node));
825
+ return SKIP;
826
+ }
827
+ });
828
+ let totalUniqueDeclarations = uniqueDeclarations.size;
829
+ let totalSelectors = selectorComplexities.size();
830
+ let specificitiesA = specificityA.aggregate();
831
+ let specificitiesB = specificityB.aggregate();
832
+ let specificitiesC = specificityC.aggregate();
833
+ let totalUniqueSelectors = uniqueSelectors.size;
834
+ let assign = Object.assign;
835
+ let cssLen = css.length;
836
+ let fontFacesCount = fontfaces.length;
837
+ let atRuleComplexity = atRuleComplexities.aggregate();
838
+ let selectorComplexity = selectorComplexities.aggregate();
839
+ let declarationComplexity = declarationComplexities.aggregate();
840
+ let propertyComplexity = propertyComplexities.aggregate();
841
+ let valueComplexity = valueComplexities.aggregate();
842
+ let atruleCount = atrules.c();
843
+ return {
844
+ stylesheet: {
845
+ sourceLinesOfCode: atruleCount.total + totalSelectors + totalDeclarations + keyframeSelectors.size(),
846
+ linesOfCode,
847
+ size: cssLen,
848
+ complexity: atRuleComplexity.sum + selectorComplexity.sum + declarationComplexity.sum + propertyComplexity.sum + valueComplexity.sum,
849
+ comments: {
850
+ total: totalComments,
851
+ size: commentsSize
852
+ },
853
+ embeddedContent: {
854
+ size: {
855
+ total: embedSize,
856
+ ratio: ratio(embedSize, cssLen)
857
+ },
858
+ types: {
859
+ total: embedTypes.total,
860
+ totalUnique: embedTypes.unique.size,
861
+ uniquenessRatio: ratio(embedTypes.unique.size, embedTypes.total),
862
+ unique: Object.fromEntries(embedTypes.unique)
863
+ }
864
+ }
865
+ },
866
+ atrules: assign(atruleCount, {
867
+ fontface: assign({
868
+ total: fontFacesCount,
869
+ totalUnique: fontFacesCount,
870
+ unique: fontfaces,
871
+ uniquenessRatio: fontFacesCount === 0 ? 0 : 1
872
+ }, useLocations ? { uniqueWithLocations: fontfaces_with_loc.c().uniqueWithLocations } : {}),
873
+ import: imports.c(),
874
+ media: assign(medias.c(), {
875
+ browserhacks: mediaBrowserhacks.c(),
876
+ features: mediaFeatures.c()
877
+ }),
878
+ charset: charsets.c(),
879
+ supports: assign(supports.c(), { browserhacks: supportsBrowserhacks.c() }),
880
+ keyframes: assign(keyframes.c(), { prefixed: assign(prefixedKeyframes.c(), { ratio: ratio(prefixedKeyframes.size(), keyframes.size()) }) }),
881
+ container: assign(containers.c(), { names: containerNames.c() }),
882
+ layer: layers.c(),
883
+ property: registeredProperties.c(),
884
+ function: functions.c(),
885
+ scope: scopes.c(),
886
+ complexity: atRuleComplexity,
887
+ nesting: assign(atruleNesting.aggregate(), { items: atruleNesting.toArray() }, uniqueAtruleNesting.c())
888
+ }),
889
+ rules: {
890
+ total: totalRules,
891
+ empty: {
892
+ total: emptyRules,
893
+ ratio: ratio(emptyRules, totalRules)
894
+ },
895
+ sizes: assign(ruleSizes.aggregate(), { items: ruleSizes.toArray() }, uniqueRuleSize.c()),
896
+ nesting: assign(ruleNesting.aggregate(), { items: ruleNesting.toArray() }, uniqueRuleNesting.c()),
897
+ selectors: assign(selectorsPerRule.aggregate(), { items: selectorsPerRule.toArray() }, uniqueSelectorsPerRule.c()),
898
+ declarations: assign(declarationsPerRule.aggregate(), { items: declarationsPerRule.toArray() }, uniqueDeclarationsPerRule.c())
899
+ },
900
+ selectors: {
901
+ total: totalSelectors,
902
+ totalUnique: totalUniqueSelectors,
903
+ uniquenessRatio: ratio(totalUniqueSelectors, totalSelectors),
904
+ specificity: assign({
905
+ min: minSpecificity === void 0 ? [
906
+ 0,
907
+ 0,
908
+ 0
909
+ ] : minSpecificity,
910
+ max: maxSpecificity === void 0 ? [
911
+ 0,
912
+ 0,
913
+ 0
914
+ ] : maxSpecificity,
915
+ sum: [
916
+ specificitiesA.sum,
917
+ specificitiesB.sum,
918
+ specificitiesC.sum
919
+ ],
920
+ mean: [
921
+ specificitiesA.mean,
922
+ specificitiesB.mean,
923
+ specificitiesC.mean
924
+ ],
925
+ mode: [
926
+ specificitiesA.mode,
927
+ specificitiesB.mode,
928
+ specificitiesC.mode
929
+ ],
930
+ items: specificities
931
+ }, uniqueSpecificities.c()),
932
+ complexity: assign(selectorComplexity, uniqueSelectorComplexities.c(), { items: selectorComplexities.toArray() }),
933
+ nesting: assign(selectorNesting.aggregate(), { items: selectorNesting.toArray() }, uniqueSelectorNesting.c()),
934
+ id: assign(ids.c(), { ratio: ratio(ids.size(), totalSelectors) }),
935
+ pseudoClasses: pseudoClasses.c(),
936
+ pseudoElements: pseudoElements.c(),
937
+ accessibility: assign(a11y.c(), { ratio: ratio(a11y.size(), totalSelectors) }),
938
+ attributes: attributeSelectors.c(),
939
+ customElements: customElementSelectors.c(),
940
+ keyframes: keyframeSelectors.c(),
941
+ prefixed: assign(prefixedSelectors.c(), { ratio: ratio(prefixedSelectors.size(), totalSelectors) }),
942
+ combinators: combinators.c()
943
+ },
944
+ declarations: {
945
+ total: totalDeclarations,
946
+ totalUnique: totalUniqueDeclarations,
947
+ uniquenessRatio: ratio(totalUniqueDeclarations, totalDeclarations),
948
+ importants: {
949
+ total: importantDeclarations,
950
+ ratio: ratio(importantDeclarations, totalDeclarations),
951
+ inKeyframes: {
952
+ total: importantsInKeyframes,
953
+ ratio: ratio(importantsInKeyframes, importantDeclarations)
954
+ }
955
+ },
956
+ complexity: declarationComplexity,
957
+ nesting: assign(declarationNesting.aggregate(), { items: declarationNesting.toArray() }, uniqueDeclarationNesting.c())
958
+ },
959
+ properties: assign(properties.c(), {
960
+ prefixed: assign(propertyVendorPrefixes.c(), { ratio: ratio(propertyVendorPrefixes.size(), properties.size()) }),
961
+ custom: assign(customProperties.c(), {
962
+ ratio: ratio(customProperties.size(), properties.size()),
963
+ importants: assign(importantCustomProperties.c(), { ratio: ratio(importantCustomProperties.size(), customProperties.size()) })
964
+ }),
965
+ browserhacks: assign(propertyHacks.c(), { ratio: ratio(propertyHacks.size(), properties.size()) }),
966
+ complexity: propertyComplexity
967
+ }),
968
+ values: {
969
+ colors: assign(colors.count(), { formats: colorFormats.c() }),
970
+ gradients: gradients.c(),
971
+ fontFamilies: fontFamilies.c(),
972
+ fontSizes: fontSizes.c(),
973
+ lineHeights: lineHeights.c(),
974
+ zindexes: zindex.c(),
975
+ textShadows: textShadows.c(),
976
+ boxShadows: boxShadows.c(),
977
+ borderRadiuses: borderRadiuses.count(),
978
+ animations: {
979
+ durations: durations.c(),
980
+ timingFunctions: timingFunctions.c()
981
+ },
982
+ prefixes: vendorPrefixedValues.c(),
983
+ browserhacks: valueBrowserhacks.c(),
984
+ units: units.count(),
985
+ complexity: valueComplexity,
986
+ keywords: valueKeywords.c(),
987
+ resets: resets.c(),
988
+ displays: displays.c()
989
+ },
990
+ __meta__: {
991
+ parseTime: startAnalysis - startParse,
992
+ analyzeTime: Date.now() - startAnalysis,
993
+ total: Date.now() - start
994
+ }
995
+ };
996
+ }
997
+ /**
998
+ * Compare specificity A to Specificity B
999
+ * @deprecated this one is the inverse of the one exported in /selectors/index.ts; wille be removed in next major version
1000
+ * @returns 0 when a==b, 1 when a<b, -1 when a>b
1001
+ */
1002
+ function compareSpecificity(a, b) {
1003
+ if (a[0] === b[0]) {
1004
+ if (a[1] === b[1]) return b[2] - a[2];
1005
+ return b[1] - a[1];
1006
+ }
1007
+ return b[0] - a[0];
1008
+ }
1009
+ //#endregion
1010
+ export { KeywordSet, analyze, calculate as calculateSpecificity, colorFunctions, colorKeywords, compareSpecificity, keywords as cssKeywords, hasVendorPrefix, isAccessibility as isAccessibilitySelector, isMediaBrowserhack, isHack as isPropertyHack, isPrefixed as isSelectorPrefixed, isSupportsBrowserhack, isValuePrefixed, namedColors, getComplexity as selectorComplexity, systemColors };