diff --git a/src/index.test.ts b/src/index.test.ts index 335fc49..e58e0dd 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -20,6 +20,7 @@ import { type UniqueWithLocations, type Location, type Specificity, + type AnalysisPath, } from './index.js' describe('Public API', () => { @@ -681,3 +682,166 @@ test('has metadata', () => { expect(typeof actual.total).toBe('number') expect(actual.total).toBeGreaterThan(0) }) + +describe('include/exclude options', () => { + const CSS = ` + @media screen and (min-width: 600px) { + .foo, #bar { + color: red; + font-size: 1rem; + background: linear-gradient(to bottom, #fff, #000); + } + } + @keyframes spin { + from { transform: rotate(0deg) } + to { transform: rotate(360deg) } + } + ` + + test('no options returns full analysis', () => { + const result = analyze(CSS) + expect(result.atrules.total).toBeGreaterThan(0) + expect(result.selectors.total).toBeGreaterThan(0) + expect(result.declarations.total).toBeGreaterThan(0) + expect(result.properties.total).toBeGreaterThan(0) + expect(result.values.colors.total).toBeGreaterThan(0) + }) + + test('include: atrules returns atrules data and zeroes elsewhere', () => { + const result = analyze(CSS, { include: ['atrules'] }) + expect(result.atrules.total).toBeGreaterThan(0) + expect(result.atrules.media.total).toBeGreaterThan(0) + expect(result.atrules.keyframes.total).toBeGreaterThan(0) + // Excluded sections should be empty + expect(result.selectors.total).toBe(0) + expect(result.declarations.total).toBe(0) + expect(result.rules.total).toBe(0) + }) + + test('include: selectors.complexity only collects selector complexity', () => { + const result = analyze(CSS, { include: ['selectors.complexity'] }) + expect(result.selectors.complexity.total).toBeGreaterThan(0) + // Other selector sub-features should be empty + expect(result.selectors.specificity.total).toBe(0) + expect(result.selectors.id.total).toBe(0) + expect(result.selectors.pseudoClasses.total).toBe(0) + }) + + test('include: multiple paths collects all specified sections', () => { + const result = analyze(CSS, { include: ['atrules', 'selectors.complexity'] }) + expect(result.atrules.total).toBeGreaterThan(0) + expect(result.selectors.complexity.total).toBeGreaterThan(0) + expect(result.declarations.total).toBe(0) + }) + + test('exclude: values skips value analysis', () => { + const full = analyze(CSS) + const result = analyze(CSS, { exclude: ['values'] }) + expect(result.values.colors.total).toBe(0) + expect(result.values.fontSizes.total).toBe(0) + // Other sections should still have data + expect(result.atrules.total).toBe(full.atrules.total) + expect(result.selectors.total).toBe(full.selectors.total) + expect(result.declarations.total).toBe(full.declarations.total) + expect(result.properties.total).toBe(full.properties.total) + }) + + test('exclude: atrules.media.browserhacks skips only media browserhacks', () => { + const full = analyze(CSS) + const result = analyze(CSS, { exclude: ['atrules.media.browserhacks'] }) + expect(result.atrules.media.browserhacks.total).toBe(0) + // Other atrule data should still be present + expect(result.atrules.total).toBe(full.atrules.total) + expect(result.atrules.media.total).toBe(full.atrules.media.total) + }) + + test('include + exclude: include atrules then exclude atrules.media', () => { + const result = analyze(CSS, { include: ['atrules'], exclude: ['atrules.media'] }) + expect(result.atrules.total).toBeGreaterThan(0) + expect(result.atrules.keyframes.total).toBeGreaterThan(0) + expect(result.atrules.media.total).toBe(0) + // Non-included sections should be empty + expect(result.selectors.total).toBe(0) + }) + + test('include: values.colors only collects color data', () => { + const result = analyze(CSS, { include: ['values.colors'] }) + expect(result.values.colors.total).toBeGreaterThan(0) + expect(result.values.fontSizes.total).toBe(0) + expect(result.values.gradients.total).toBe(0) + // declarations.total is 0 because 'declarations' is not included + expect(result.declarations.total).toBe(0) + }) + + test('keyframes depth is always tracked even when atrules excluded', () => { + // When excluding atrules but including declarations, keyframe context must be + // tracked to correctly categorize importants in keyframes + const result = analyze(CSS, { include: ['declarations'] }) + // Declarations inside keyframes should not be incorrectly flagged + expect(result.declarations.total).toBeGreaterThan(0) + }) + + test('selectors.total is 0 when selectors section is excluded', () => { + const full = analyze(CSS) + const result = analyze(CSS, { exclude: ['selectors'] }) + // selectors.total is 0 since selectors section is excluded + expect(result.selectors.total).toBe(0) + // But sourceLinesOfCode still counts selector nodes via internal counter + expect(result.stylesheet.sourceLinesOfCode).toBe(full.stylesheet.sourceLinesOfCode) + }) + + test('exclude: properties still runs declaration counting', () => { + const full = analyze(CSS) + const result = analyze(CSS, { exclude: ['properties'] }) + expect(result.properties.total).toBe(0) + expect(result.declarations.total).toBe(full.declarations.total) + }) + + test('exclude: selectors.specificity.items omits items array but keeps aggregate stats', () => { + const full = analyze(CSS) + const result = analyze(CSS, { exclude: ['selectors.specificity.items'] }) + // Items array should be empty + expect(result.selectors.specificity.items).toEqual([]) + // Aggregate stats should still be present + expect(result.selectors.specificity.total).toBe(full.selectors.specificity.total) + expect(result.selectors.specificity.max).toEqual(full.selectors.specificity.max) + expect(result.selectors.specificity.sum).toEqual(full.selectors.specificity.sum) + }) + + test('exclude: selectors.complexity.items omits items array but keeps aggregate stats', () => { + const full = analyze(CSS) + const result = analyze(CSS, { exclude: ['selectors.complexity.items'] }) + expect(result.selectors.complexity.items).toEqual([]) + expect(result.selectors.complexity.total).toBe(full.selectors.complexity.total) + expect(result.selectors.complexity.max).toBe(full.selectors.complexity.max) + }) + + test('exclude: rules.sizes.items omits items but keeps stats', () => { + const full = analyze(CSS) + const result = analyze(CSS, { exclude: ['rules.sizes.items'] }) + expect(result.rules.sizes.items).toEqual([]) + expect(result.rules.sizes.total).toBe(full.rules.sizes.total) + }) + + test('exclude: atrules.nesting.items omits items but keeps stats', () => { + const full = analyze(CSS) + const result = analyze(CSS, { exclude: ['atrules.nesting.items'] }) + expect(result.atrules.nesting.items).toEqual([]) + expect(result.atrules.nesting.total).toBe(full.atrules.nesting.total) + }) + + test('exclude: declarations.nesting.items omits items but keeps stats', () => { + const full = analyze(CSS) + const result = analyze(CSS, { exclude: ['declarations.nesting.items'] }) + expect(result.declarations.nesting.items).toEqual([]) + expect(result.declarations.nesting.total).toBe(full.declarations.nesting.total) + }) + + test('exported AnalysisPath type is used correctly', () => { + // Type-level test: verify include/exclude accept AnalysisPath values + const result1 = analyze(CSS, { include: ['atrules', 'selectors'] }) + const result2 = analyze(CSS, { exclude: ['values.colors', 'selectors.specificity'] }) + expect(result1).toBeDefined() + expect(result2).toBeDefined() + }) +}) diff --git a/src/index.ts b/src/index.ts index 1acaf1c..81fe993 100644 --- a/src/index.ts +++ b/src/index.ts @@ -50,9 +50,235 @@ function ratio(part: number, total: number): number { return part / total } +/** All valid dot-notation paths matching the output structure of {@link analyze} */ +export type AnalysisPath = + | 'stylesheet' + | 'stylesheet.embeddedContent' + | 'atrules' + | 'atrules.fontface' + | 'atrules.import' + | 'atrules.media' + | 'atrules.media.browserhacks' + | 'atrules.media.features' + | 'atrules.charset' + | 'atrules.supports' + | 'atrules.supports.browserhacks' + | 'atrules.keyframes' + | 'atrules.keyframes.prefixed' + | 'atrules.container' + | 'atrules.container.names' + | 'atrules.layer' + | 'atrules.property' + | 'atrules.function' + | 'atrules.scope' + | 'atrules.complexity' + | 'atrules.nesting' + | 'atrules.nesting.items' + | 'rules' + | 'rules.empty' + | 'rules.sizes' + | 'rules.sizes.items' + | 'rules.nesting' + | 'rules.nesting.items' + | 'rules.selectors' + | 'rules.selectors.items' + | 'rules.declarations' + | 'rules.declarations.items' + | 'selectors' + | 'selectors.specificity' + | 'selectors.specificity.items' + | 'selectors.complexity' + | 'selectors.complexity.items' + | 'selectors.nesting' + | 'selectors.nesting.items' + | 'selectors.id' + | 'selectors.pseudoClasses' + | 'selectors.pseudoElements' + | 'selectors.accessibility' + | 'selectors.attributes' + | 'selectors.customElements' + | 'selectors.keyframes' + | 'selectors.prefixed' + | 'selectors.combinators' + | 'declarations' + | 'declarations.importants' + | 'declarations.complexity' + | 'declarations.nesting' + | 'declarations.nesting.items' + | 'properties' + | 'properties.prefixed' + | 'properties.custom' + | 'properties.shorthands' + | 'properties.browserhacks' + | 'properties.complexity' + | 'values' + | 'values.colors' + | 'values.gradients' + | 'values.fontFamilies' + | 'values.fontSizes' + | 'values.lineHeights' + | 'values.zindexes' + | 'values.textShadows' + | 'values.boxShadows' + | 'values.borderRadiuses' + | 'values.animations' + | 'values.prefixes' + | 'values.browserhacks' + | 'values.units' + | 'values.complexity' + | 'values.keywords' + | 'values.resets' + | 'values.displays' + +const ALL_PATHS: Set = new Set([ + 'stylesheet', + 'stylesheet.embeddedContent', + 'atrules', + 'atrules.fontface', + 'atrules.import', + 'atrules.media', + 'atrules.media.browserhacks', + 'atrules.media.features', + 'atrules.charset', + 'atrules.supports', + 'atrules.supports.browserhacks', + 'atrules.keyframes', + 'atrules.keyframes.prefixed', + 'atrules.container', + 'atrules.container.names', + 'atrules.layer', + 'atrules.property', + 'atrules.function', + 'atrules.scope', + 'atrules.complexity', + 'atrules.nesting', + 'atrules.nesting.items', + 'rules', + 'rules.empty', + 'rules.sizes', + 'rules.sizes.items', + 'rules.nesting', + 'rules.nesting.items', + 'rules.selectors', + 'rules.selectors.items', + 'rules.declarations', + 'rules.declarations.items', + 'selectors', + 'selectors.specificity', + 'selectors.specificity.items', + 'selectors.complexity', + 'selectors.complexity.items', + 'selectors.nesting', + 'selectors.nesting.items', + 'selectors.id', + 'selectors.pseudoClasses', + 'selectors.pseudoElements', + 'selectors.accessibility', + 'selectors.attributes', + 'selectors.customElements', + 'selectors.keyframes', + 'selectors.prefixed', + 'selectors.combinators', + 'declarations', + 'declarations.importants', + 'declarations.complexity', + 'declarations.nesting', + 'declarations.nesting.items', + 'properties', + 'properties.prefixed', + 'properties.custom', + 'properties.shorthands', + 'properties.browserhacks', + 'properties.complexity', + 'values', + 'values.colors', + 'values.gradients', + 'values.fontFamilies', + 'values.fontSizes', + 'values.lineHeights', + 'values.zindexes', + 'values.textShadows', + 'values.boxShadows', + 'values.borderRadiuses', + 'values.animations', + 'values.prefixes', + 'values.browserhacks', + 'values.units', + 'values.complexity', + 'values.keywords', + 'values.resets', + 'values.displays', +]) + +/** + * Resolve the active set of analysis paths from include/exclude options. + * Returns null when all features are active (no filtering needed). + */ +function resolve_features(include: AnalysisPath[] | undefined, exclude: AnalysisPath[] | undefined): Set | null { + if ((!include || include.length === 0) && (!exclude || exclude.length === 0)) { + return null + } + + let active: Set + + if (include && include.length > 0) { + active = new Set() + for (let path of include) { + // Add the path itself and all its children + for (let p of ALL_PATHS) { + if (p === path || p.startsWith(path + '.')) { + active.add(p) + } + } + } + } else { + active = new Set(ALL_PATHS) + } + + if (exclude && exclude.length > 0) { + for (let path of exclude) { + for (let p of [...active]) { + if (p === path || p.startsWith(path + '.')) { + active.delete(p) + } + } + } + } + + return active +} + +/** + * Check whether a given path should be analyzed. + * Since resolve_features already fully expands all child paths, we only need: + * - Exact match (the path itself is in the active set) + * - Child-requires-parent: a child path in the set requires its parent to run + * (e.g. if 'atrules.media' is in the set, 'atrules' must run to reach it) + */ +function feature_active(features: Set | null, path: AnalysisPath): boolean { + if (features === null) return true + if (features.has(path)) return true + // Child-requires-parent: check if any entry in the set is a child of this path + let prefix = path + '.' + for (let f of features) { + if (f.startsWith(prefix)) return true + } + return false +} + export type Options = { /** @description Use Locations (`{ 'item': [{ line, column, offset, length }] }`) instead of a regular count per occurrence (`{ 'item': 3 }`) */ useLocations?: boolean + /** + * @description Only analyze the specified paths. Paths match the keys of the output object (e.g. `['atrules', 'selectors.complexity']`). + * When both `include` and `exclude` are provided, `include` is resolved first and `exclude` is subtracted from the result. + */ + include?: AnalysisPath[] + /** + * @description Exclude the specified paths from analysis. Paths match the keys of the output object (e.g. `['values', 'atrules.media.browserhacks']`). + * Excluding a path also excludes all its children. + */ + exclude?: AnalysisPath[] } export function analyze(css: string, options?: Options & { useLocations?: false | undefined }): ReturnType> @@ -68,6 +294,87 @@ export function analyze(css: string, options: Options = {}): any { function analyzeInternal(css: string, options: Options, useLocations: T) { let start = Date.now() + // Resolve which features to analyze based on include/exclude options + let features = resolve_features(options.include, options.exclude) + + // Precomputed feature flags (avoid per-node overhead during walk) + let f_stylesheet_embedded = feature_active(features, 'stylesheet.embeddedContent') + let f_atrules = feature_active(features, 'atrules') + let f_atrules_fontface = feature_active(features, 'atrules.fontface') + let f_atrules_import = feature_active(features, 'atrules.import') + let f_atrules_media = feature_active(features, 'atrules.media') + let f_atrules_media_browserhacks = feature_active(features, 'atrules.media.browserhacks') + let f_atrules_media_features = feature_active(features, 'atrules.media.features') + let f_atrules_charset = feature_active(features, 'atrules.charset') + let f_atrules_supports = feature_active(features, 'atrules.supports') + let f_atrules_supports_browserhacks = feature_active(features, 'atrules.supports.browserhacks') + let f_atrules_keyframes = feature_active(features, 'atrules.keyframes') + let f_atrules_keyframes_prefixed = feature_active(features, 'atrules.keyframes.prefixed') + let f_atrules_container = feature_active(features, 'atrules.container') + let f_atrules_container_names = feature_active(features, 'atrules.container.names') + let f_atrules_layer = feature_active(features, 'atrules.layer') + let f_atrules_property = feature_active(features, 'atrules.property') + let f_atrules_function = feature_active(features, 'atrules.function') + let f_atrules_scope = feature_active(features, 'atrules.scope') + let f_atrules_complexity = feature_active(features, 'atrules.complexity') + let f_atrules_nesting = feature_active(features, 'atrules.nesting') + let f_atrules_nesting_items = feature_active(features, 'atrules.nesting.items') + let f_rules = feature_active(features, 'rules') + let f_rules_empty = feature_active(features, 'rules.empty') + let f_rules_sizes = feature_active(features, 'rules.sizes') + let f_rules_sizes_items = feature_active(features, 'rules.sizes.items') + let f_rules_nesting = feature_active(features, 'rules.nesting') + let f_rules_nesting_items = feature_active(features, 'rules.nesting.items') + let f_rules_selectors = feature_active(features, 'rules.selectors') + let f_rules_selectors_items = feature_active(features, 'rules.selectors.items') + let f_rules_declarations = feature_active(features, 'rules.declarations') + let f_rules_declarations_items = feature_active(features, 'rules.declarations.items') + let f_selectors = feature_active(features, 'selectors') + let f_selectors_specificity = feature_active(features, 'selectors.specificity') + let f_selectors_specificity_items = feature_active(features, 'selectors.specificity.items') + let f_selectors_complexity = feature_active(features, 'selectors.complexity') + let f_selectors_complexity_items = feature_active(features, 'selectors.complexity.items') + let f_selectors_nesting = feature_active(features, 'selectors.nesting') + let f_selectors_nesting_items = feature_active(features, 'selectors.nesting.items') + let f_selectors_id = feature_active(features, 'selectors.id') + let f_selectors_pseudoclasses = feature_active(features, 'selectors.pseudoClasses') + let f_selectors_pseudoelements = feature_active(features, 'selectors.pseudoElements') + let f_selectors_accessibility = feature_active(features, 'selectors.accessibility') + let f_selectors_attributes = feature_active(features, 'selectors.attributes') + let f_selectors_customelements = feature_active(features, 'selectors.customElements') + let f_selectors_keyframes = feature_active(features, 'selectors.keyframes') + let f_selectors_prefixed = feature_active(features, 'selectors.prefixed') + let f_selectors_combinators = feature_active(features, 'selectors.combinators') + let f_declarations = feature_active(features, 'declarations') + let f_declarations_importants = feature_active(features, 'declarations.importants') + let f_declarations_complexity = feature_active(features, 'declarations.complexity') + let f_declarations_nesting = feature_active(features, 'declarations.nesting') + let f_declarations_nesting_items = feature_active(features, 'declarations.nesting.items') + let f_properties = feature_active(features, 'properties') + let f_properties_prefixed = feature_active(features, 'properties.prefixed') + let f_properties_custom = feature_active(features, 'properties.custom') + let f_properties_shorthands = feature_active(features, 'properties.shorthands') + let f_properties_browserhacks = feature_active(features, 'properties.browserhacks') + let f_properties_complexity = feature_active(features, 'properties.complexity') + let f_values = feature_active(features, 'values') + let f_values_colors = feature_active(features, 'values.colors') + let f_values_gradients = feature_active(features, 'values.gradients') + let f_values_fontFamilies = feature_active(features, 'values.fontFamilies') + let f_values_fontSizes = feature_active(features, 'values.fontSizes') + let f_values_lineHeights = feature_active(features, 'values.lineHeights') + let f_values_zindexes = feature_active(features, 'values.zindexes') + let f_values_textShadows = feature_active(features, 'values.textShadows') + let f_values_boxShadows = feature_active(features, 'values.boxShadows') + let f_values_borderRadiuses = feature_active(features, 'values.borderRadiuses') + let f_values_animations = feature_active(features, 'values.animations') + let f_values_prefixes = feature_active(features, 'values.prefixes') + let f_values_browserhacks = feature_active(features, 'values.browserhacks') + let f_values_units = feature_active(features, 'values.units') + let f_values_complexity = feature_active(features, 'values.complexity') + let f_values_keywords = feature_active(features, 'values.keywords') + let f_values_resets = feature_active(features, 'values.resets') + let f_values_displays = feature_active(features, 'values.displays') + // Stylesheet let linesOfCode = (css.match(/\n/g) || []).length + 1 let totalComments = 0 @@ -133,6 +440,8 @@ function analyzeInternal(css: string, options: Options, useLo let uniqueRuleNesting = new Collection(useLocations) // Selectors + let totalSelectors = 0 + let totalKeyframeSelectors = 0 // always counted for sourceLinesOfCode let keyframeSelectors = new Collection(useLocations) let uniqueSelectors = new Set() let prefixedSelectors = new Collection(useLocations) @@ -217,155 +526,211 @@ function analyzeInternal(css: string, options: Options, useLo // Count nodes and track nesting if (node.type === AT_RULE) { - let atruleLoc = toLoc(node) - atruleNesting.push(depth) - uniqueAtruleNesting.p(depth, atruleLoc) let normalized_name = basename(node.name ?? '') + + // Always detect keyframes for correct rule/declaration categorization, + // regardless of whether atrules analysis is active + let is_keyframes_rule = normalized_name.endsWith('keyframes') + if (is_keyframes_rule && node.prelude !== null && node.prelude !== undefined) { + keyframesDepth = depth + } + + if (!f_atrules) return + + let atruleLoc = toLoc(node) + + if (f_atrules_nesting) { + atruleNesting.push(depth) + uniqueAtruleNesting.p(depth, atruleLoc) + } + atrules.p(normalized_name, atruleLoc) //#region @FONT-FACE if (normalized_name === 'font-face') { - let descriptors = Object.create(null) - if (useLocations) { - fontfaces_with_loc.p(node.start, toLoc(node)) - } - let block = node.children.find((child: CSSNode) => child.type === BLOCK) - for (let descriptor of block?.children || []) { - if (descriptor.type === DECLARATION && descriptor.value) { - descriptors[descriptor.property!] = (descriptor.value as CSSNode).text + if (f_atrules_fontface) { + let descriptors = Object.create(null) + if (useLocations) { + fontfaces_with_loc.p(node.start, toLoc(node)) + } + let block = node.children.find((child: CSSNode) => child.type === BLOCK) + for (let descriptor of block?.children || []) { + if (descriptor.type === DECLARATION && descriptor.value) { + descriptors[descriptor.property!] = (descriptor.value as CSSNode).text + } } + fontfaces.push(descriptors) + } + if (f_atrules_complexity) { + atRuleComplexities.push(1) } - atRuleComplexities.push(1) - fontfaces.push(descriptors) } //#endregion if (node.prelude === null || node.prelude === undefined) { if (normalized_name === 'layer') { // @layer without a prelude is anonymous - layers.p('', toLoc(node)) - atRuleComplexities.push(2) + if (f_atrules_layer) { + layers.p('', toLoc(node)) + } + if (f_atrules_complexity) { + atRuleComplexities.push(2) + } } } else { let complexity = 1 // All the AtRules in here MUST have a prelude, so we can count their names if (normalized_name === 'media') { - medias.p(node.prelude.text, toLoc(node)) - isMediaBrowserhack(node.prelude, (hack) => { - mediaBrowserhacks.p(hack, toLoc(node)) - complexity++ - }) + if (f_atrules_media) { + medias.p(node.prelude.text, toLoc(node)) + } + if (f_atrules_media_browserhacks) { + isMediaBrowserhack(node.prelude, (hack) => { + mediaBrowserhacks.p(hack, toLoc(node)) + complexity++ + }) + } } else if (normalized_name === 'supports') { - supports.p(node.prelude.text, toLoc(node)) - - isSupportsBrowserhack(node.prelude, (hack) => { - supportsBrowserhacks.p(hack, toLoc(node)) - complexity++ - }) - } else if (normalized_name.endsWith('keyframes')) { - let prelude = node.prelude.text - keyframes.p(prelude, toLoc(node)) - - if (node.is_vendor_prefixed) { + if (f_atrules_supports) { + supports.p(node.prelude.text, toLoc(node)) + } + if (f_atrules_supports_browserhacks) { + isSupportsBrowserhack(node.prelude, (hack) => { + supportsBrowserhacks.p(hack, toLoc(node)) + complexity++ + }) + } + } else if (is_keyframes_rule) { + if (f_atrules_keyframes) { + keyframes.p(node.prelude.text, toLoc(node)) + } + if (f_atrules_keyframes_prefixed && node.is_vendor_prefixed) { prefixedKeyframes.p(`@${node.name?.toLowerCase()} ${node.prelude.text}`, toLoc(node)) complexity++ } - - // Mark the depth at which we enter a keyframes atrule - keyframesDepth = depth } else if (normalized_name === 'layer') { - for (let layer of node.prelude.text.split(',').map((s: string) => s.trim())) { - layers.p(layer, toLoc(node)) + if (f_atrules_layer) { + for (let layer of node.prelude.text.split(',').map((s: string) => s.trim())) { + layers.p(layer, toLoc(node)) + } } } else if (normalized_name === 'import') { - imports.p(node.prelude.text, toLoc(node)) - - if (node.prelude.has_children) { - for (let child of node.prelude) { - if (child.type === SUPPORTS_QUERY && typeof child.value === 'string') { - supports.p(child.value, toLoc(child)) - } else if (child.type === LAYER_NAME && typeof child.value === 'string') { - layers.p(child.value, toLoc(child)) + if (f_atrules_import) { + imports.p(node.prelude.text, toLoc(node)) + if (node.prelude.has_children) { + for (let child of node.prelude) { + if (child.type === SUPPORTS_QUERY && typeof child.value === 'string') { + if (f_atrules_supports) supports.p(child.value, toLoc(child)) + } else if (child.type === LAYER_NAME && typeof child.value === 'string') { + if (f_atrules_layer) layers.p(child.value, toLoc(child)) + } } } } } else if (normalized_name === 'container') { - containers.p(node.prelude.text, toLoc(node)) - if (node.prelude.first_child?.type === CONTAINER_QUERY) { - if (node.prelude.first_child.first_child?.type === IDENTIFIER) { - containerNames.p(node.prelude.first_child.first_child.text, toLoc(node)) + if (f_atrules_container) { + containers.p(node.prelude.text, toLoc(node)) + if (f_atrules_container_names && node.prelude.first_child?.type === CONTAINER_QUERY) { + if (node.prelude.first_child.first_child?.type === IDENTIFIER) { + containerNames.p(node.prelude.first_child.first_child.text, toLoc(node)) + } } } } else if (normalized_name === 'property') { - registeredProperties.p(node.prelude.text, toLoc(node)) + if (f_atrules_property) { + registeredProperties.p(node.prelude.text, toLoc(node)) + } } else if (normalized_name === 'function') { - let prelude = node.prelude.text - let name = prelude.includes('(') ? prelude.slice(0, prelude.indexOf('(')).trim() : prelude.trim() - functions.p(name, toLoc(node)) + if (f_atrules_function) { + let prelude = node.prelude.text + let name = prelude.includes('(') ? prelude.slice(0, prelude.indexOf('(')).trim() : prelude.trim() + functions.p(name, toLoc(node)) + } } else if (normalized_name === 'charset') { - charsets.p(node.prelude.text.toLowerCase(), toLoc(node)) + if (f_atrules_charset) { + charsets.p(node.prelude.text.toLowerCase(), toLoc(node)) + } } else if (normalized_name === 'scope') { - scopes.p(node.prelude.text, toLoc(node)) + if (f_atrules_scope) { + scopes.p(node.prelude.text, toLoc(node)) + } } - atRuleComplexities.push(complexity) + if (f_atrules_complexity) { + atRuleComplexities.push(complexity) + } } } else if (node.type === STYLE_RULE) { // Handle keyframe rules specially if (inKeyframes && node.prelude) { 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)) + // Always count for sourceLinesOfCode + totalKeyframeSelectors += node.prelude.children.length + if (f_selectors_keyframes) { + for (let keyframe_selector of node.prelude.children) { + keyframeSelectors.p(keyframe_selector.text, toLoc(keyframe_selector)) + } } } // Don't count keyframe rules as regular rules, but continue walking // children to count declarations inside keyframes // (Declarations are counted in the Declaration handler below) - } else { + } else if (f_rules) { // Only count non-keyframe rules totalRules++ // Check if rule is empty (no declarations in block) - if (node.block?.is_empty) { + if (f_rules_empty && node.block?.is_empty) { emptyRules++ } - // Count selectors and declarations in this rule - let numSelectors = 0 - let numDeclarations = 0 - let loc = toLoc(node) - - // Find the SelectorList child and count Selector nodes inside it - if (node.prelude) { - for (const selector of node.prelude.children) { - if (selector.type === SELECTOR) { - numSelectors++ + if (f_rules_sizes || f_rules_selectors || f_rules_declarations) { + // Count selectors and declarations in this rule + let numSelectors = 0 + let numDeclarations = 0 + let loc = toLoc(node) + + // Find the SelectorList child and count Selector nodes inside it + if (f_rules_selectors && node.prelude) { + for (const selector of node.prelude.children) { + if (selector.type === SELECTOR) { + numSelectors++ + } } } - } - // Count declarations in the block - if (node.block) { - for (const declaration of node.block.children) { - if (declaration.type === DECLARATION) { - numDeclarations++ + // Count declarations in the block + if (f_rules_declarations && node.block) { + for (const declaration of node.block.children) { + if (declaration.type === DECLARATION) { + numDeclarations++ + } } } - } - // Track rule metrics - ruleSizes.push(numSelectors + numDeclarations) - uniqueRuleSize.p(numSelectors + numDeclarations, loc) + // Track rule metrics + if (f_rules_sizes) { + ruleSizes.push(numSelectors + numDeclarations) + uniqueRuleSize.p(numSelectors + numDeclarations, loc) + } - selectorsPerRule.push(numSelectors) - uniqueSelectorsPerRule.p(numSelectors, loc) + if (f_rules_selectors) { + selectorsPerRule.push(numSelectors) + uniqueSelectorsPerRule.p(numSelectors, loc) + } - declarationsPerRule.push(numDeclarations) - uniqueDeclarationsPerRule.p(numDeclarations, loc) + if (f_rules_declarations) { + declarationsPerRule.push(numDeclarations) + uniqueDeclarationsPerRule.p(numDeclarations, loc) + } + } - ruleNesting.push(depth) - uniqueRuleNesting.p(depth, loc) + if (f_rules_nesting) { + let loc = toLoc(node) + ruleNesting.push(depth) + uniqueRuleNesting.p(depth, loc) + } } } else if (node.type === SELECTOR) { // Keyframe selectors are now handled at the Rule level, so skip them here @@ -373,71 +738,94 @@ function analyzeInternal(css: string, options: Options, useLo return SKIP } - let loc = toLoc(node) - - selectorNesting.push(depth > 0 ? depth - 1 : 0) - uniqueSelectorNesting.p(depth > 0 ? depth - 1 : 0, loc) - uniqueSelectors.add(node.text) + totalSelectors++ - let complexity = getComplexity(node) - selectorComplexities.push(complexity) - uniqueSelectorComplexities.p(complexity, loc) - - isPrefixed(node, (prefix) => { - prefixedSelectors.p(prefix.toLowerCase(), loc) - }) - - // Check for accessibility selectors - isAccessibility(node, (a11y_selector) => { - a11y.p(a11y_selector, loc) - }) - - walk(node, (child) => { - if (child.type === ATTRIBUTE_SELECTOR) { - attributeSelectors.p(child.name?.toLowerCase() ?? '', loc) - } else if (child.type === TYPE_SELECTOR && !child.name?.startsWith('--') && child.name?.includes('-')) { - customElementSelectors.p(child.name.toLowerCase(), loc) - } else if (child.type === PSEUDO_CLASS_SELECTOR) { - pseudoClasses.p(child.name?.toLowerCase() ?? '', loc) - } else if (child.type === PSEUDO_ELEMENT_SELECTOR) { - pseudoElements.p(child.name?.toLowerCase() ?? '', loc) - } - }) + if (!f_selectors) { + return SKIP + } - getCombinators(node, (combinator) => { - let name = combinator.name.trim() === '' ? ' ' : combinator.name - combinators.p(name, combinator.loc) - }) + let loc = toLoc(node) - let specificity = calculateSpecificity(node) - let [sa, sb, sc] = specificity + uniqueSelectors.add(node.text) - uniqueSpecificities.p(specificity.toString(), loc) + if (f_selectors_nesting) { + selectorNesting.push(depth > 0 ? depth - 1 : 0) + uniqueSelectorNesting.p(depth > 0 ? depth - 1 : 0, loc) + } - specificityA.push(sa) - specificityB.push(sb) - specificityC.push(sc) + if (f_selectors_complexity) { + let complexity = getComplexity(node) + selectorComplexities.push(complexity) + uniqueSelectorComplexities.p(complexity, loc) + } - if (maxSpecificity === undefined) { - maxSpecificity = specificity + if (f_selectors_prefixed) { + isPrefixed(node, (prefix) => { + prefixedSelectors.p(prefix.toLowerCase(), loc) + }) } - if (minSpecificity === undefined) { - minSpecificity = specificity + if (f_selectors_accessibility) { + // Check for accessibility selectors + isAccessibility(node, (a11y_selector) => { + a11y.p(a11y_selector, loc) + }) } - if (minSpecificity !== undefined && compareSpecificity(minSpecificity, specificity) < 0) { - minSpecificity = specificity + if (f_selectors_attributes || f_selectors_customelements || f_selectors_pseudoclasses || f_selectors_pseudoelements) { + walk(node, (child) => { + if (f_selectors_attributes && child.type === ATTRIBUTE_SELECTOR) { + attributeSelectors.p(child.name?.toLowerCase() ?? '', loc) + } else if (f_selectors_customelements && child.type === TYPE_SELECTOR && !child.name?.startsWith('--') && child.name?.includes('-')) { + customElementSelectors.p(child.name.toLowerCase(), loc) + } else if (f_selectors_pseudoclasses && child.type === PSEUDO_CLASS_SELECTOR) { + pseudoClasses.p(child.name?.toLowerCase() ?? '', loc) + } else if (f_selectors_pseudoelements && child.type === PSEUDO_ELEMENT_SELECTOR) { + pseudoElements.p(child.name?.toLowerCase() ?? '', loc) + } + }) } - if (maxSpecificity !== undefined && compareSpecificity(maxSpecificity, specificity) > 0) { - maxSpecificity = specificity + if (f_selectors_combinators) { + getCombinators(node, (combinator) => { + let name = combinator.name.trim() === '' ? ' ' : combinator.name + combinators.p(name, combinator.loc) + }) } - specificities.push(specificity) + if (f_selectors_specificity) { + let specificity = calculateSpecificity(node) + let [sa, sb, sc] = specificity + + uniqueSpecificities.p(specificity.toString(), loc) - if (sa > 0) { - ids.p(node.text, loc) + specificityA.push(sa) + specificityB.push(sb) + specificityC.push(sc) + + if (maxSpecificity === undefined) { + maxSpecificity = specificity + } + + if (minSpecificity === undefined) { + minSpecificity = specificity + } + + if (minSpecificity !== undefined && compareSpecificity(minSpecificity, specificity) < 0) { + minSpecificity = specificity + } + + if (maxSpecificity !== undefined && compareSpecificity(maxSpecificity, specificity) > 0) { + maxSpecificity = specificity + } + + if (f_selectors_specificity_items) { + specificities.push(specificity) + } + + if (f_selectors_id && sa > 0) { + ids.p(node.text, loc) + } } // Avoid deeper walking of selectors to not mess with @@ -446,29 +834,48 @@ function analyzeInternal(css: string, options: Options, useLo // as children return SKIP } else if (node.type === DECLARATION) { + // Always count for sourceLinesOfCode; gate heavy work below totalDeclarations++ - uniqueDeclarations.add(node.text) - let loc = toLoc(node) - let declarationDepth = depth > 0 ? depth - 1 : 0 - declarationNesting.push(declarationDepth) - uniqueDeclarationNesting.p(declarationDepth, loc) + if (!f_declarations && !f_properties && !f_values) return - let complexity = 1 - if (node.is_important) { - complexity++ + if (f_declarations) { + uniqueDeclarations.add(node.text) + } - let declaration = node.text - if (!declaration.toLowerCase().includes('!important')) { - valueBrowserhacks.p('!ie', toLoc(node.value as CSSNode)) - } + let loc = toLoc(node) + + if (f_declarations_nesting) { + let declarationDepth = depth > 0 ? depth - 1 : 0 + declarationNesting.push(declarationDepth) + uniqueDeclarationNesting.p(declarationDepth, loc) + } - if (inKeyframes) { - importantsInKeyframes++ + if (f_declarations_complexity || f_declarations_importants || f_values_browserhacks) { + let complexity = 1 + if (node.is_important) { complexity++ + + if (f_declarations_importants || f_values_browserhacks) { + let declaration = node.text + if (f_values_browserhacks && !declaration.toLowerCase().includes('!important')) { + valueBrowserhacks.p('!ie', toLoc(node.value as CSSNode)) + } + + if (f_declarations_importants && inKeyframes) { + importantsInKeyframes++ + complexity++ + } + } } + if (f_declarations_complexity) { + declarationComplexities.push(complexity) + } + } + + if (f_declarations_importants && node.is_important) { + importantDeclarations++ } - declarationComplexities.push(complexity) //#region PROPERTIES let { is_important, property, is_browserhack, is_vendor_prefixed } = node @@ -479,35 +886,34 @@ function analyzeInternal(css: string, options: Options, useLo propertyLoc.length = property.length let normalizedProperty = basename(property) - properties.p(normalizedProperty, propertyLoc) - - if (is_important) { - importantDeclarations++ - } - - // Count important declarations - if (is_vendor_prefixed) { - propertyComplexities.push(2) - propertyVendorPrefixes.p(property, propertyLoc) - } else if (is_custom(property)) { - customProperties.p(property, propertyLoc) - propertyComplexities.push(is_important ? 3 : 2) - - if (is_important) { - importantCustomProperties.p(property, propertyLoc) + if (f_properties) { + properties.p(normalizedProperty, propertyLoc) + + // Count important declarations + if (f_properties_prefixed && is_vendor_prefixed) { + if (f_properties_complexity) propertyComplexities.push(2) + propertyVendorPrefixes.p(property, propertyLoc) + } else if (f_properties_custom && is_custom(property)) { + customProperties.p(property, propertyLoc) + if (f_properties_complexity) propertyComplexities.push(is_important ? 3 : 2) + if (is_important) { + importantCustomProperties.p(property, propertyLoc) + } + } else if (f_properties_browserhacks && is_browserhack) { + propertyHacks.p(property.charAt(0), propertyLoc) + if (f_properties_complexity) propertyComplexities.push(2) + } else if (f_properties_complexity) { + propertyComplexities.push(1) } - } else if (is_browserhack) { - propertyHacks.p(property.charAt(0), propertyLoc) - propertyComplexities.push(2) - } else { - propertyComplexities.push(1) - } - if (shorthand_properties.has(normalizedProperty)) { - shorthands.p(property, propertyLoc) + if (f_properties_shorthands && shorthand_properties.has(normalizedProperty)) { + shorthands.p(property, propertyLoc) + } } //#endregion PROPERTIES + if (!f_values) return + //#region VALUES // Values are analyzed inside declaration because we need context, like which property is used { @@ -519,10 +925,10 @@ function analyzeInternal(css: string, options: Options, useLo // auto, inherit, initial, none, etc. if (keywords.has(text)) { - valueKeywords.p(text.toLowerCase(), valueLoc) - valueComplexities.push(complexity) + if (f_values_keywords) valueKeywords.p(text.toLowerCase(), valueLoc) + if (f_values_complexity) valueComplexities.push(complexity) - if (normalizedProperty === 'display') { + if (f_values_displays && normalizedProperty === 'display') { displays.p(text.toLowerCase(), valueLoc) } @@ -531,41 +937,46 @@ function analyzeInternal(css: string, options: Options, useLo //#region VALUE COMPLEXITY // i.e. `background-image: -webkit-linear-gradient()` - isValuePrefixed(value, (prefixed) => { - vendorPrefixedValues.p(prefixed.toLowerCase(), valueLoc) - complexity++ - }) + if (f_values_prefixes || f_values_complexity) { + isValuePrefixed(value, (prefixed) => { + if (f_values_prefixes) vendorPrefixedValues.p(prefixed.toLowerCase(), valueLoc) + complexity++ + }) + } // i.e. `property: value\9` - if (isIe9Hack(value)) { - valueBrowserhacks.p('\\9', valueLoc) - text = text.slice(0, -2) - complexity++ + if (f_values_browserhacks || f_values_complexity) { + if (isIe9Hack(value)) { + if (f_values_browserhacks) valueBrowserhacks.p('\\9', valueLoc) + text = text.slice(0, -2) + complexity++ + } } //#endregion VALUE COMPLEXITY - // TODO: should shorthands be counted towards complexity? - valueComplexities.push(complexity) + if (f_values_complexity) { + valueComplexities.push(complexity) + } // Process properties first that don't have colors, // so we can avoid further walking them; - if (SPACING_RESET_PROPERTIES.has(normalizedProperty)) { + if (f_values_resets && SPACING_RESET_PROPERTIES.has(normalizedProperty)) { if (isValueReset(value)) { resets.p(normalizedProperty, valueLoc) } - } else if (normalizedProperty === 'display') { + } else if (f_values_displays && normalizedProperty === 'display') { if (/var\(/i.test(text)) { displays.p(text, valueLoc) } else { displays.p(text.toLowerCase(), valueLoc) } - } else if (normalizedProperty === 'z-index') { + } else if (f_values_zindexes && normalizedProperty === 'z-index') { zindex.p(text, valueLoc) return SKIP } else if (normalizedProperty === 'font') { if (!SYSTEM_FONTS.has(text)) { let result = destructure(value, function (keyword) { - valueKeywords.p(keyword.toLowerCase(), valueLoc) + if (f_values_keywords) valueKeywords.p(keyword.toLowerCase(), valueLoc) }) if (!result) { @@ -573,21 +984,21 @@ function analyzeInternal(css: string, options: Options, useLo } let { font_size, line_height, font_family } = result - if (font_family) { + if (f_values_fontFamilies && font_family) { fontFamilies.p(font_family, valueLoc) } - if (font_size) { + if (f_values_fontSizes && font_size) { fontSizes.p(font_size.toLowerCase(), valueLoc) } - if (line_height) { + if (f_values_lineHeights && line_height) { lineHeights.p(line_height.toLowerCase(), valueLoc) } } // Don't return SKIP here - let walker continue to find // units, colors, and font families in var() fallbacks - } else if (normalizedProperty === 'font-size') { + } else if (f_values_fontSizes && normalizedProperty === 'font-size') { if (!SYSTEM_FONTS.has(text)) { let normalized = text.toLowerCase() if (normalized.includes('var(')) { @@ -597,29 +1008,29 @@ function analyzeInternal(css: string, options: Options, useLo } } } else if (normalizedProperty === 'font-family') { - if (!SYSTEM_FONTS.has(text)) { + if (f_values_fontFamilies && !SYSTEM_FONTS.has(text)) { fontFamilies.p(text, valueLoc) } return SKIP // to prevent finding color false positives (Black as font family name is not a color) - } else if (normalizedProperty === 'line-height') { + } else if (f_values_lineHeights && normalizedProperty === 'line-height') { let normalized = text.toLowerCase() if (normalized.includes('var(')) { lineHeights.p(text, valueLoc) } else { lineHeights.p(normalized, valueLoc) } - } else if (normalizedProperty === 'transition' || normalizedProperty === 'animation') { + } else if (f_values_animations && (normalizedProperty === 'transition' || normalizedProperty === 'animation')) { analyzeAnimation(value.children, function (item) { if (item.type === 'fn') { timingFunctions.p(item.value.text.toLowerCase(), valueLoc) } else if (item.type === 'duration') { durations.p(item.value.text.toLowerCase(), valueLoc) } else if (item.type === 'keyword') { - valueKeywords.p(item.value.text.toLowerCase(), valueLoc) + if (f_values_keywords) valueKeywords.p(item.value.text.toLowerCase(), valueLoc) } }) return SKIP - } else if (normalizedProperty === 'animation-duration' || normalizedProperty === 'transition-duration') { + } else if (f_values_animations && (normalizedProperty === 'animation-duration' || normalizedProperty === 'transition-duration')) { for (let child of value.children) { if (child.type !== OPERATOR) { let text = child.text @@ -630,40 +1041,44 @@ function analyzeInternal(css: string, options: Options, useLo } } } - } else if (normalizedProperty === 'transition-timing-function' || normalizedProperty === 'animation-timing-function') { + } else if (f_values_animations && (normalizedProperty === 'transition-timing-function' || normalizedProperty === 'animation-timing-function')) { for (let child of value.children) { if (child.type !== OPERATOR) { timingFunctions.p(child.text, valueLoc) } } - } else if (normalizedProperty === 'container-name') { + } else if (f_atrules_container_names && normalizedProperty === 'container-name') { containerNames.p(text, valueLoc) - } else if (normalizedProperty === 'container') { + } else if (f_atrules_container_names && normalizedProperty === 'container') { // The first identifier in the `container` shorthand is the container name // Example: container: my-layout / inline-size; if (value.first_child?.type === IDENTIFIER) { containerNames.p(value.first_child.text, valueLoc) } - } else if (border_radius_properties.has(normalizedProperty)) { + } else if (f_values_borderRadiuses && border_radius_properties.has(normalizedProperty)) { borderRadiuses.push(text, property, valueLoc) - } else if (normalizedProperty === 'text-shadow') { + } else if (f_values_textShadows && normalizedProperty === 'text-shadow') { textShadows.p(text, valueLoc) - } else if (normalizedProperty === 'box-shadow') { + } else if (f_values_boxShadows && normalizedProperty === 'box-shadow') { boxShadows.p(text, valueLoc) } + if (!f_values_colors && !f_values_units && !f_values_keywords && !f_values_gradients) return + // Check if the value has an IE9 browserhack before walking - let valueHasIe9Hack = isIe9Hack(value) + let valueHasIe9Hack = f_values_colors && isIe9Hack(value) walk(value, (valueNode) => { switch (valueNode.type) { case DIMENSION: { + if (!f_values_units) return SKIP let unit = valueNode.unit?.toLowerCase() ?? '' let loc = toLoc(valueNode) units.push(unit, property, loc) return SKIP } case HASH: { + if (!f_values_colors) return SKIP // Use text property for the hash value let hashText = valueNode.text if (!hashText || !hashText.startsWith('#')) { @@ -699,10 +1114,12 @@ function analyzeInternal(css: string, options: Options, useLo return SKIP } - if (keywords.has(identifierText)) { + if (f_values_keywords && keywords.has(identifierText)) { valueKeywords.p(identifierText.toLowerCase(), identifierLoc) } + if (!f_values_colors) return SKIP + // Bail out if it can't be a color name // 20 === 'lightgoldenrodyellow'.length // 3 === 'red'.length @@ -735,17 +1152,18 @@ function analyzeInternal(css: string, options: Options, useLo return SKIP } case FUNCTION: { + if (!f_values_colors && !f_values_gradients) return let funcName = valueNode.name as string let funcLoc = toLoc(valueNode) // rgb(a), hsl(a), color(), hwb(), lch(), lab(), oklab(), oklch() - if (colorFunctions.has(funcName)) { + if (f_values_colors && colorFunctions.has(funcName)) { colors.push(valueNode.text, property, funcLoc) colorFormats.p(funcName.toLowerCase(), funcLoc) return } - if (endsWith('gradient', funcName)) { + if (f_values_gradients && endsWith('gradient', funcName)) { gradients.p(valueNode.text, funcLoc) } // No SKIP here intentionally, @@ -756,6 +1174,7 @@ function analyzeInternal(css: string, options: Options, useLo } //#endregion VALUES } else if (node.type === URL) { + if (!f_stylesheet_embedded) return let { value } = node let embed = unquote((value as string) || '') if (str_starts_with(embed, 'data:')) { @@ -790,7 +1209,7 @@ function analyzeInternal(css: string, options: Options, useLo } } } else if (node.type === MEDIA_FEATURE) { - if (node.property) { + if (f_atrules_media_features && node.property) { mediaFeatures.p(node.property.toLowerCase(), toLoc(node)) } return SKIP @@ -799,7 +1218,6 @@ function analyzeInternal(css: string, options: Options, useLo let totalUniqueDeclarations = uniqueDeclarations.size - let totalSelectors = selectorComplexities.size() let specificitiesA = specificityA.aggregate() let specificitiesB = specificityB.aggregate() let specificitiesC = specificityC.aggregate() @@ -816,7 +1234,7 @@ function analyzeInternal(css: string, options: Options, useLo return { stylesheet: { - sourceLinesOfCode: atruleCount.total + totalSelectors + totalDeclarations + keyframeSelectors.size(), + sourceLinesOfCode: atruleCount.total + totalSelectors + totalDeclarations + totalKeyframeSelectors, linesOfCode, size: cssLen, complexity: atRuleComplexity.sum + selectorComplexity.sum + declarationComplexity.sum + propertyComplexity.sum + valueComplexity.sum, @@ -876,7 +1294,7 @@ function analyzeInternal(css: string, options: Options, useLo nesting: assign( atruleNesting.aggregate(), { - items: atruleNesting.toArray(), + items: f_atrules_nesting_items ? atruleNesting.toArray() : [], }, uniqueAtruleNesting.c(), ), @@ -890,36 +1308,36 @@ function analyzeInternal(css: string, options: Options, useLo sizes: assign( ruleSizes.aggregate(), { - items: ruleSizes.toArray(), + items: f_rules_sizes_items ? ruleSizes.toArray() : [], }, uniqueRuleSize.c(), ), nesting: assign( ruleNesting.aggregate(), { - items: ruleNesting.toArray(), + items: f_rules_nesting_items ? ruleNesting.toArray() : [], }, uniqueRuleNesting.c(), ), selectors: assign( selectorsPerRule.aggregate(), { - items: selectorsPerRule.toArray(), + items: f_rules_selectors_items ? selectorsPerRule.toArray() : [], }, uniqueSelectorsPerRule.c(), ), declarations: assign( declarationsPerRule.aggregate(), { - items: declarationsPerRule.toArray(), + items: f_rules_declarations_items ? declarationsPerRule.toArray() : [], }, uniqueDeclarationsPerRule.c(), ), }, selectors: { - total: totalSelectors, + total: f_selectors ? totalSelectors : 0, totalUnique: totalUniqueSelectors, - uniquenessRatio: ratio(totalUniqueSelectors, totalSelectors), + uniquenessRatio: ratio(totalUniqueSelectors, f_selectors ? totalSelectors : 0), specificity: assign( { /** @type Specificity */ @@ -938,38 +1356,38 @@ function analyzeInternal(css: string, options: Options, useLo uniqueSpecificities.c(), ), complexity: assign(selectorComplexity, uniqueSelectorComplexities.c(), { - items: selectorComplexities.toArray(), + items: f_selectors_complexity_items ? selectorComplexities.toArray() : [], }), nesting: assign( selectorNesting.aggregate(), { - items: selectorNesting.toArray(), + items: f_selectors_nesting_items ? selectorNesting.toArray() : [], }, uniqueSelectorNesting.c(), ), id: assign(ids.c(), { - ratio: ratio(ids.size(), totalSelectors), + ratio: ratio(ids.size(), f_selectors ? totalSelectors : 0), }), pseudoClasses: pseudoClasses.c(), pseudoElements: pseudoElements.c(), accessibility: assign(a11y.c(), { - ratio: ratio(a11y.size(), totalSelectors), + ratio: ratio(a11y.size(), f_selectors ? totalSelectors : 0), }), attributes: attributeSelectors.c(), customElements: customElementSelectors.c(), keyframes: keyframeSelectors.c(), prefixed: assign(prefixedSelectors.c(), { - ratio: ratio(prefixedSelectors.size(), totalSelectors), + ratio: ratio(prefixedSelectors.size(), f_selectors ? totalSelectors : 0), }), combinators: combinators.c(), }, declarations: { - total: totalDeclarations, + total: f_declarations ? totalDeclarations : 0, totalUnique: totalUniqueDeclarations, - uniquenessRatio: ratio(totalUniqueDeclarations, totalDeclarations), + uniquenessRatio: ratio(totalUniqueDeclarations, f_declarations ? totalDeclarations : 0), importants: { total: importantDeclarations, - ratio: ratio(importantDeclarations, totalDeclarations), + ratio: ratio(importantDeclarations, f_declarations ? totalDeclarations : 0), inKeyframes: { total: importantsInKeyframes, ratio: ratio(importantsInKeyframes, importantDeclarations), @@ -979,7 +1397,7 @@ function analyzeInternal(css: string, options: Options, useLo nesting: assign( declarationNesting.aggregate(), { - items: declarationNesting.toArray(), + items: f_declarations_nesting_items ? declarationNesting.toArray() : [], }, uniqueDeclarationNesting.c(), ),