diff --git a/package.json b/package.json index 5af9f85..f26268b 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,12 @@ "analytics", "performance", "styleguide", - "metrics" + "metrics", + "designsystem", + "fonts", + "colors", + "quality", + "code" ], "dependencies": { "@bramus/specificity": "^2.3.0", diff --git a/src/aggregate-collection.js b/src/aggregate-collection.js index 8e3ccdb..3fbbcbf 100644 --- a/src/aggregate-collection.js +++ b/src/aggregate-collection.js @@ -11,8 +11,9 @@ function Mode(arr) { let maxOccurrences = -1 let maxOccurenceCount = 0 let sum = 0 + let len = arr.length - for (let i = 0; i < arr.length; i++) { + for (let i = 0; i < len; i++) { let element = arr[i] let updatedCount = (frequencies.get(element) || 0) + 1 frequencies.set(element, updatedCount) @@ -70,7 +71,9 @@ class AggregateCollection { } aggregate() { - if (this._items.length === 0) { + let len = this._items.length + + if (len === 0) { return { min: 0, max: 0, @@ -86,19 +89,20 @@ class AggregateCollection { /** @type Number[] */ let sorted = this._items.slice().sort((a, b) => a - b) let min = sorted[0] - let max = sorted[sorted.length - 1] + let max = sorted[len - 1] let mode = Mode(sorted) let median = Median(sorted) + let sum = this._sum return { min, max, - mean: this._sum / sorted.length, + mean: sum / len, mode, median, range: max - min, - sum: this._sum, + sum: sum, } } diff --git a/src/atrules/atrules.js b/src/atrules/atrules.js index 61828a8..e95b3e2 100644 --- a/src/atrules/atrules.js +++ b/src/atrules/atrules.js @@ -1,5 +1,11 @@ import { strEquals, startsWith, endsWith } from '../string-utils.js' import walk from 'css-tree/walker' +import { + Identifier, + MediaQuery, + MediaFeature, + Declaration, +} from '../css-tree-node-types.js' /** * Check whether node.property === property and node.value === value, @@ -10,9 +16,10 @@ import walk from 'css-tree/walker' * @returns true if declaratioNode is the given property: value, false otherwise */ function isPropertyValue(node, property, value) { + let firstChild = node.value.children.first return strEquals(property, node.property) - && node.value.children.first.type === 'Identifier' - && strEquals(value, node.value.children.first.name) + && firstChild.type === Identifier + && strEquals(value, firstChild.name) } /** @@ -24,7 +31,7 @@ export function isSupportsBrowserhack(prelude) { let returnValue = false walk(prelude, function (node) { - if (node.type === 'Declaration') { + if (node.type === Declaration) { if ( isPropertyValue(node, '-webkit-appearance', 'none') || isPropertyValue(node, '-moz-appearance', 'meterbar') @@ -47,38 +54,43 @@ export function isMediaBrowserhack(prelude) { let returnValue = false walk(prelude, function (node) { - if (node.type === 'MediaQuery' - && node.children.size === 1 - && node.children.first.type === 'Identifier' + let children = node.children + let name = node.name + let value = node.value + + if (node.type === MediaQuery + && children.size === 1 + && children.first.type === Identifier ) { - node = node.children.first + let n = children.first.name // Note: CSSTree adds a trailing space to \\9 - if (startsWith('\\0', node.name) || endsWith('\\9 ', node.name)) { + if (startsWith('\\0', n) || endsWith('\\9 ', n)) { returnValue = true return this.break } } - if (node.type === 'MediaFeature') { - if (node.value !== null && node.value.unit === '\\0') { + if (node.type === MediaFeature) { + if (value !== null && value.unit === '\\0') { returnValue = true return this.break } - if (strEquals('-moz-images-in-menus', node.name) - || strEquals('min--moz-device-pixel-ratio', node.name) - || strEquals('-ms-high-contrast', node.name) + if (strEquals('-moz-images-in-menus', name) + || strEquals('min--moz-device-pixel-ratio', name) + || strEquals('-ms-high-contrast', name) ) { returnValue = true return this.break } - if (strEquals('min-resolution', node.name) - && strEquals('.001', node.value.value) - && strEquals('dpcm', node.value.unit) + if (strEquals('min-resolution', name) + && strEquals('.001', value.value) + && strEquals('dpcm', value.unit) ) { returnValue = true return this.break } - if (strEquals('-webkit-min-device-pixel-ratio', node.name)) { - if ((strEquals('0', node.value.value) || strEquals('10000', node.value.value))) { + if (strEquals('-webkit-min-device-pixel-ratio', name)) { + let val = value.value + if ((strEquals('0', val) || strEquals('10000', val))) { returnValue = true return this.break } diff --git a/src/collection.js b/src/collection.js index 947795e..74303a4 100644 --- a/src/collection.js +++ b/src/collection.js @@ -1,16 +1,20 @@ export class Collection { - constructor({ useLocations = false }) { + /** @param {boolean} useLocations */ + constructor(useLocations = false) { /** @type {Map} */ this._items = new Map() this._total = 0 - /** @type {number[]} */ - this.node_lines = [] - /** @type {number[]} */ - this.node_columns = [] - /** @type {number[]} */ - this.node_lengths = [] - /** @type {number[]} */ - this.node_offsets = [] + + if (useLocations) { + /** @type {number[]} */ + this._node_lines = [] + /** @type {number[]} */ + this._node_columns = [] + /** @type {number[]} */ + this._node_lengths = [] + /** @type {number[]} */ + this._node_offsets = [] + } /** @type {boolean} */ this._useLocations = useLocations @@ -20,13 +24,18 @@ export class Collection { * @param {string} item * @param {import('css-tree').CssLocation} node_location */ - push(item, node_location) { + p(item, node_location) { let index = this._total - this.node_lines[index] = node_location.start.line - this.node_columns[index] = node_location.start.column - this.node_offsets[index] = node_location.start.offset - this.node_lengths[index] = node_location.end.offset - node_location.start.offset + if (this._useLocations) { + let start = node_location.start + let start_offset = start.offset + + this._node_lines[index] = start.line + this._node_columns[index] = start.column + this._node_offsets[index] = start_offset + this._node_lengths[index] = node_location.end.offset - start_offset + } if (this._items.has(item)) { /** @type number[] */ @@ -53,35 +62,41 @@ export class Collection { * * @returns {{ total: number; totalUnique: number; uniquenessRatio: number; unique: Record; __unstable__uniqueWithLocations: Record}} */ - count() { + c() { + let useLocations = this._useLocations let uniqueWithLocations = new Map() let unique = {} - this._items.forEach((list, key) => { - let nodes = list.map(index => ({ - line: this.node_lines[index], - column: this.node_columns[index], - offset: this.node_offsets[index], - length: this.node_lengths[index], - })) - uniqueWithLocations.set(key, nodes) + let items = this._items + let size = items.size + + items.forEach((list, key) => { + if (useLocations) { + let nodes = list.map(index => ({ + line: this._node_lines[index], + column: this._node_columns[index], + offset: this._node_offsets[index], + length: this._node_lengths[index], + })) + uniqueWithLocations.set(key, nodes) + } unique[key] = list.length }) if (this._useLocations) { return { total: this._total, - totalUnique: this._items.size, + totalUnique: size, unique, - uniquenessRatio: this._total === 0 ? 0 : this._items.size / this._total, + uniquenessRatio: this._total === 0 ? 0 : size / this._total, __unstable__uniqueWithLocations: Object.fromEntries(uniqueWithLocations), } } return { total: this._total, - totalUnique: this._items.size, + totalUnique: size, unique, - uniquenessRatio: this._total === 0 ? 0 : this._items.size / this._total, + uniquenessRatio: this._total === 0 ? 0 : size / this._total, } } } diff --git a/src/context-collection.js b/src/context-collection.js index 667da68..3549540 100644 --- a/src/context-collection.js +++ b/src/context-collection.js @@ -1,12 +1,13 @@ import { Collection } from './collection.js' class ContextCollection { - constructor({ useLocations = false }) { - this._list = new Collection({ useLocations }) + /** @param {boolean} useLocations */ + constructor(useLocations) { + this._list = new Collection(useLocations) /** @type {Map} */ this._contexts = new Map() /** @type {boolean} */ - this.useLocations = useLocations + this._useLocations = useLocations } /** @@ -16,13 +17,13 @@ class ContextCollection { * @param {import('css-tree').CssLocation} node_location */ push(item, context, node_location) { - this._list.push(item, node_location) + this._list.p(item, node_location) if (!this._contexts.has(context)) { - this._contexts.set(context, new Collection({ useLocations: this.useLocations })) + this._contexts.set(context, new Collection(this._useLocations)) } - this._contexts.get(context).push(item, node_location) + this._contexts.get(context).p(item, node_location) } count() { @@ -37,10 +38,10 @@ class ContextCollection { let itemsPerContext = new Map() for (let [context, value] of this._contexts.entries()) { - itemsPerContext.set(context, value.count()) + itemsPerContext.set(context, value.c()) } - return Object.assign(this._list.count(), { + return Object.assign(this._list.c(), { itemsPerContext: Object.fromEntries(itemsPerContext) }) } diff --git a/src/countable-collection.js b/src/countable-collection.js index 404716a..67864c5 100644 --- a/src/countable-collection.js +++ b/src/countable-collection.js @@ -30,11 +30,15 @@ class CountableCollection { } count() { + let items = this._items + let size = items.size + let total = this._total + return { - total: this._total, - totalUnique: this._items.size, - unique: Object.fromEntries(this._items), - uniquenessRatio: this._total === 0 ? 0 : this._items.size / this._total, + total: total, + totalUnique: size, + unique: Object.fromEntries(items), + uniquenessRatio: total === 0 ? 0 : size / total, } } } diff --git a/src/css-tree-node-types.js b/src/css-tree-node-types.js new file mode 100644 index 0000000..3de1a80 --- /dev/null +++ b/src/css-tree-node-types.js @@ -0,0 +1,30 @@ +// Atrule +export const Atrule = 'Atrule' +export const MediaQuery = 'MediaQuery' +export const MediaFeature = 'MediaFeature' +// Rule +export const Rule = 'Rule' + +// Selector +export const Selector = 'Selector' +export const TypeSelector = 'TypeSelector' +export const PseudoClassSelector = 'PseudoClassSelector' +export const AttributeSelector = 'AttributeSelector' +export const IdSelector = 'IdSelector' +export const ClassSelector = 'ClassSelector' +export const PseudoElementSelector = 'PseudoElementSelector' + +// Declaration +export const Declaration = 'Declaration' + +// Values +export const Value = 'Value' +export const Identifier = 'Identifier' +export const Nth = 'Nth' +export const Combinator = 'Combinator' +export const Nr = 'Number' +export const Dimension = 'Dimension' +export const Operator = 'Operator' +export const Hash = 'Hash' +export const Url = 'Url' +export const Func = 'Function' diff --git a/src/index.js b/src/index.js index 2ac2d5c..22c1257 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,19 @@ import { hasVendorPrefix } from './vendor-prefix.js' import { isCustom, isHack, isProperty } from './properties/property-utils.js' import { getEmbedType } from './stylesheet/stylesheet.js' import { isIe9Hack } from './values/browserhacks.js' +import { + Atrule, + Selector, + Dimension, + Url, + Value, + Declaration, + Hash, + Rule, + Identifier, + Func, + Operator +} from './css-tree-node-types.js' /** @typedef {[number, number, number]} Specificity */ @@ -53,13 +66,14 @@ export function analyze(css, options = {}) { } function stringifyNodePlain(node) { - return css.substring(node.loc.start.offset, node.loc.end.offset) + let loc = node.loc + return css.substring(loc.start.offset, loc.end.offset) } // Stylesheet let totalComments = 0 let commentsSize = 0 - let embeds = new Collection({ useLocations }) + let embeds = new Collection(useLocations) let embedSize = 0 let embedTypes = { total: 0, @@ -86,17 +100,17 @@ export function analyze(css, options = {}) { let totalAtRules = 0 /** @type {Record}[]} */ let fontfaces = [] - let layers = new Collection({ useLocations }) - let imports = new Collection({ useLocations }) - let medias = new Collection({ useLocations }) - let mediaBrowserhacks = new Collection({ useLocations }) - let charsets = new Collection({ useLocations }) - let supports = new Collection({ useLocations }) - let supportsBrowserhacks = new Collection({ useLocations }) - let keyframes = new Collection({ useLocations }) - let prefixedKeyframes = new Collection({ useLocations }) - let containers = new Collection({ useLocations }) - let registeredProperties = new Collection({ useLocations }) + let layers = new Collection(useLocations) + let imports = new Collection(useLocations) + let medias = new Collection(useLocations) + let mediaBrowserhacks = new Collection(useLocations) + let charsets = new Collection(useLocations) + let supports = new Collection(useLocations) + let supportsBrowserhacks = new Collection(useLocations) + let keyframes = new Collection(useLocations) + let prefixedKeyframes = new Collection(useLocations) + let containers = new Collection(useLocations) + let registeredProperties = new Collection(useLocations) // Rules let totalRules = 0 @@ -104,14 +118,14 @@ export function analyze(css, options = {}) { let ruleSizes = new AggregateCollection() let selectorsPerRule = new AggregateCollection() let declarationsPerRule = new AggregateCollection() - let uniqueRuleSize = new Collection({ useLocations }) - let uniqueSelectorsPerRule = new Collection({ useLocations }) - let uniqueDeclarationsPerRule = new Collection({ useLocations }) + let uniqueRuleSize = new Collection(useLocations) + let uniqueSelectorsPerRule = new Collection(useLocations) + let uniqueDeclarationsPerRule = new Collection(useLocations) // Selectors - let keyframeSelectors = new Collection({ useLocations }) + let keyframeSelectors = new Collection(useLocations) let uniqueSelectors = new Set() - let prefixedSelectors = new Collection({ useLocations }) + let prefixedSelectors = new Collection(useLocations) /** @type {Specificity} */ let maxSpecificity /** @type {Specificity} */ @@ -119,48 +133,48 @@ export function analyze(css, options = {}) { let specificityA = new AggregateCollection() let specificityB = new AggregateCollection() let specificityC = new AggregateCollection() - let uniqueSpecificities = new Collection({ useLocations }) + let uniqueSpecificities = new Collection(useLocations) let selectorComplexities = new AggregateCollection() - let uniqueSelectorComplexities = new Collection({ useLocations }) + let uniqueSelectorComplexities = new Collection(useLocations) /** @type {Specificity[]} */ let specificities = [] - let ids = new Collection({ useLocations }) - let a11y = new Collection({ useLocations }) - let combinators = new Collection({ useLocations }) + let ids = new Collection(useLocations) + let a11y = new Collection(useLocations) + let combinators = new Collection(useLocations) // Declarations let uniqueDeclarations = new Set() let totalDeclarations = 0 let importantDeclarations = 0 let importantsInKeyframes = 0 - let importantCustomProperties = new Collection({ useLocations }) + let importantCustomProperties = new Collection(useLocations) // Properties - let properties = new Collection({ useLocations }) - let propertyHacks = new Collection({ useLocations }) - let propertyVendorPrefixes = new Collection({ useLocations }) - let customProperties = new Collection({ useLocations }) + let properties = new Collection(useLocations) + let propertyHacks = new Collection(useLocations) + let propertyVendorPrefixes = new Collection(useLocations) + let customProperties = new Collection(useLocations) let propertyComplexities = new AggregateCollection() // Values - let vendorPrefixedValues = new Collection({ useLocations }) - let valueBrowserhacks = new Collection({ useLocations }) - let zindex = new Collection({ useLocations }) - let textShadows = new Collection({ useLocations }) - let boxShadows = new Collection({ useLocations }) - let fontFamilies = new Collection({ useLocations }) - let fontSizes = new Collection({ useLocations }) - let lineHeights = new Collection({ useLocations }) - let timingFunctions = new Collection({ useLocations }) - let durations = new Collection({ useLocations }) - let colors = new ContextCollection({ useLocations }) - let colorFormats = new Collection({ useLocations }) - let units = new ContextCollection({ useLocations }) - let gradients = new Collection({ useLocations }) + let vendorPrefixedValues = new Collection(useLocations) + let valueBrowserhacks = new Collection(useLocations) + let zindex = new Collection(useLocations) + let textShadows = new Collection(useLocations) + let boxShadows = new Collection(useLocations) + let fontFamilies = new Collection(useLocations) + let fontSizes = new Collection(useLocations) + let lineHeights = new Collection(useLocations) + let timingFunctions = new Collection(useLocations) + let durations = new Collection(useLocations) + let colors = new ContextCollection(useLocations) + let colorFormats = new Collection(useLocations) + let units = new ContextCollection(useLocations) + let gradients = new Collection(useLocations) walk(ast, function (node) { switch (node.type) { - case 'Atrule': { + case Atrule: { totalAtRules++ let atRuleName = node.name @@ -169,7 +183,7 @@ export function analyze(css, options = {}) { node.block.children.forEach(descriptor => { // Ignore 'Raw' nodes in case of CSS syntax errors - if (descriptor.type === 'Declaration') { + if (descriptor.type === Declaration) { descriptors[descriptor.property] = stringifyNode(descriptor.value) } }) @@ -180,67 +194,71 @@ export function analyze(css, options = {}) { // All the AtRules in here MUST have a prelude, we we can count their names if (node.prelude !== null) { + let prelude = node.prelude + let preludeStr = prelude && stringifyNode(node.prelude) + let loc = prelude.loc + if (atRuleName === 'media') { - let prelude = stringifyNode(node.prelude) - medias.push(prelude, node.prelude.loc) - if (isMediaBrowserhack(node.prelude)) { - mediaBrowserhacks.push(prelude, node.prelude.loc) + medias.p(preludeStr, loc) + if (isMediaBrowserhack(prelude)) { + mediaBrowserhacks.p(preludeStr, loc) } break } if (atRuleName === 'supports') { - let prelude = stringifyNode(node.prelude) - supports.push(prelude, node.prelude.loc) - if (isSupportsBrowserhack(node.prelude)) { - supportsBrowserhacks.push(prelude, node.prelude.loc) + supports.p(preludeStr, loc) + if (isSupportsBrowserhack(prelude)) { + supportsBrowserhacks.p(preludeStr, loc) } break } if (endsWith('keyframes', atRuleName)) { - let name = '@' + atRuleName + ' ' + stringifyNode(node.prelude) + let name = '@' + atRuleName + ' ' + preludeStr if (hasVendorPrefix(atRuleName)) { - prefixedKeyframes.push(name, node.prelude.loc) + prefixedKeyframes.p(name, loc) } - keyframes.push(name, node.prelude.loc) + keyframes.p(name, loc) break } if (atRuleName === 'import') { - imports.push(stringifyNode(node.prelude), node.prelude.loc) + imports.p(preludeStr, loc) break } if (atRuleName === 'charset') { - charsets.push(stringifyNode(node.prelude), node.prelude.loc) + charsets.p(preludeStr, loc) break } if (atRuleName === 'container') { - containers.push(stringifyNode(node.prelude), node.prelude.loc) + containers.p(preludeStr, loc) break } if (atRuleName === 'layer') { - let prelude = stringifyNode(node.prelude) - prelude + preludeStr .split(',') - .forEach(name => layers.push(name.trim(), node.prelude.loc)) + .forEach(name => layers.p(name.trim(), loc)) break } if (atRuleName === 'property') { - let prelude = stringifyNode(node.prelude) - registeredProperties.push(prelude, node.prelude.loc) + registeredProperties.p(preludeStr, loc) break } } break } - case 'Rule': { - let numSelectors = node.prelude.children ? node.prelude.children.size : 0 - let numDeclarations = node.block.children ? node.block.children.size : 0 + case Rule: { + let prelude = node.prelude + let block = node.block + let preludeChildren = prelude.children + let blockChildren = block.children + let numSelectors = preludeChildren ? preludeChildren.size : 0 + let numDeclarations = blockChildren ? blockChildren.size : 0 ruleSizes.push(numSelectors + numDeclarations) - uniqueRuleSize.push(numSelectors + numDeclarations, node.loc) + uniqueRuleSize.p(numSelectors + numDeclarations, node.loc) selectorsPerRule.push(numSelectors) - uniqueSelectorsPerRule.push(numSelectors, node.prelude.loc) + uniqueSelectorsPerRule.p(numSelectors, prelude.loc) declarationsPerRule.push(numDeclarations) - uniqueDeclarationsPerRule.push(numDeclarations, node.block.loc) + uniqueDeclarationsPerRule.p(numDeclarations, block.loc) totalRules++ @@ -249,27 +267,27 @@ export function analyze(css, options = {}) { } break } - case 'Selector': { + case Selector: { let selector = stringifyNode(node) if (this.atrule && endsWith('keyframes', this.atrule.name)) { - keyframeSelectors.push(selector, node.loc) + keyframeSelectors.p(selector, node.loc) return this.skip } if (isAccessibility(node)) { - a11y.push(selector, node.loc) + a11y.p(selector, node.loc) } let [complexity, isPrefixed] = getComplexity(node) if (isPrefixed) { - prefixedSelectors.push(selector, node.loc) + prefixedSelectors.p(selector, node.loc) } uniqueSelectors.add(selector) selectorComplexities.push(complexity) - uniqueSelectorComplexities.push(complexity, node.loc) + uniqueSelectorComplexities.p(complexity, node.loc) // #region specificity let [{ value: specificityObj }] = calculate(node) @@ -280,7 +298,7 @@ export function analyze(css, options = {}) { /** @type {Specificity} */ let specificity = [sa, sb, sc] - uniqueSpecificities.push(sa + ',' + sb + ',' + sc, node.loc) + uniqueSpecificities.p(sa + ',' + sb + ',' + sc, node.loc) specificityA.push(sa) specificityB.push(sb) @@ -306,11 +324,11 @@ export function analyze(css, options = {}) { // #endregion if (sa > 0) { - ids.push(selector, node.loc) + ids.p(selector, node.loc) } getCombinators(node, function onCombinator(combinator) { - combinators.push(combinator.name, combinator.loc) + combinators.p(combinator.name, combinator.loc) }) // Avoid deeper walking of selectors to not mess with @@ -319,7 +337,7 @@ export function analyze(css, options = {}) { // as children return this.skip } - case 'Dimension': { + case Dimension: { if (!this.declaration) { break } @@ -335,7 +353,7 @@ export function analyze(css, options = {}) { return this.skip } - case 'Url': { + case Url: { if (startsWith('data:', node.value)) { let embed = node.value let size = embed.length @@ -357,11 +375,11 @@ export function analyze(css, options = {}) { } // @deprecated - embeds.push(embed, node.loc) + embeds.p(embed, node.loc) } break } - case 'Value': { + case Value: { if (isValueKeyword(node)) { break } @@ -370,23 +388,26 @@ export function analyze(css, options = {}) { let { property, important } = declaration if (isAstVendorPrefixed(node)) { - vendorPrefixedValues.push(stringifyNode(node), node.loc) + vendorPrefixedValues.p(stringifyNode(node), node.loc) } // i.e. `property: value !ie` if (typeof important === 'string') { - valueBrowserhacks.push(stringifyNodePlain(node) + '!' + important, node.loc) + valueBrowserhacks.p(stringifyNodePlain(node) + '!' + important, node.loc) } // i.e. `property: value\9` if (isIe9Hack(node)) { - valueBrowserhacks.push(stringifyNode(node), node.loc) + valueBrowserhacks.p(stringifyNode(node), node.loc) } + let children = node.children + let loc = node.loc + // Process properties first that don't have colors, // so we can avoid further walking them; if (isProperty('z-index', property)) { - zindex.push(stringifyNode(node), node.loc) + zindex.p(stringifyNode(node), loc) return this.skip } else if (isProperty('font', property)) { if (isSystemFont(node)) return @@ -394,67 +415,67 @@ export function analyze(css, options = {}) { let { font_size, line_height, font_family } = destructure(node, stringifyNode) if (font_family) { - fontFamilies.push(font_family, node.loc) + fontFamilies.p(font_family, loc) } if (font_size) { - fontSizes.push(font_size, node.loc) + fontSizes.p(font_size, loc) } if (line_height) { - lineHeights.push(line_height, node.loc) + lineHeights.p(line_height, loc) } break } else if (isProperty('font-size', property)) { if (!isSystemFont(node)) { - fontSizes.push(stringifyNode(node), node.loc) + fontSizes.p(stringifyNode(node), loc) } break } else if (isProperty('font-family', property)) { if (!isSystemFont(node)) { - fontFamilies.push(stringifyNode(node), node.loc) + fontFamilies.p(stringifyNode(node), loc) } break } else if (isProperty('line-height', property)) { - lineHeights.push(stringifyNode(node), node.loc) + lineHeights.p(stringifyNode(node), loc) } else if (isProperty('transition', property) || isProperty('animation', property)) { - let [times, fns] = analyzeAnimation(node.children, stringifyNode) + let [times, fns] = analyzeAnimation(children, stringifyNode) for (let i = 0; i < times.length; i++) { - durations.push(times[i], node.loc) + durations.p(times[i], loc) } for (let i = 0; i < fns.length; i++) { - timingFunctions.push(fns[i], node.loc) + timingFunctions.p(fns[i], loc) } break } else if (isProperty('animation-duration', property) || isProperty('transition-duration', property)) { - if (node.children && node.children.size > 1) { - node.children.forEach(child => { - if (child.type !== 'Operator') { - durations.push(stringifyNode(child), node.loc) + if (children && children.size > 1) { + children.forEach(child => { + if (child.type !== Operator) { + durations.p(stringifyNode(child), loc) } }) } else { - durations.push(stringifyNode(node), node.loc) + durations.p(stringifyNode(node), loc) } break } else if (isProperty('transition-timing-function', property) || isProperty('animation-timing-function', property)) { - if (node.children && node.children.size > 1) { - node.children.forEach(child => { - if (child.type !== 'Operator') { - timingFunctions.push(stringifyNode(child), node.loc) + if (children && children.size > 1) { + children.forEach(child => { + if (child.type !== Operator) { + timingFunctions.p(stringifyNode(child), loc) } }) } else { - timingFunctions.push(stringifyNode(node), node.loc) + timingFunctions.p(stringifyNode(node), loc) } break } else if (isProperty('text-shadow', property)) { if (!isValueKeyword(node)) { - textShadows.push(stringifyNode(node), node.loc) + textShadows.p(stringifyNode(node), loc) } // no break here: potentially contains colors } else if (isProperty('box-shadow', property)) { if (!isValueKeyword(node)) { - boxShadows.push(stringifyNode(node), node.loc) + boxShadows.p(stringifyNode(node), loc) } // no break here: potentially contains colors } @@ -463,47 +484,48 @@ export function analyze(css, options = {}) { let nodeName = valueNode.name switch (valueNode.type) { - case 'Hash': { + case Hash: { let hexLength = valueNode.value.length if (endsWith('\\9', valueNode.value)) { hexLength = hexLength - 2 } - colors.push('#' + valueNode.value, property, valueNode.loc) - colorFormats.push(`hex` + hexLength, valueNode.loc) + colors.push('#' + valueNode.value, property, loc) + colorFormats.p(`hex` + hexLength, loc) return this.skip } - case 'Identifier': { + case Identifier: { // Bail out if it can't be a color name // 20 === 'lightgoldenrodyellow'.length // 3 === 'red'.length - if (nodeName.length > 20 || nodeName.length < 3) { + let nodeLen = nodeName.length + if (nodeLen > 20 || nodeLen < 3) { return this.skip } if (namedColors.has(nodeName)) { let stringified = stringifyNode(valueNode) - colors.push(stringified, property, valueNode.loc) - colorFormats.push('named', valueNode.loc) + colors.push(stringified, property, loc) + colorFormats.p('named', loc) return } if (colorKeywords.has(nodeName)) { let stringified = stringifyNode(valueNode) - colors.push(stringified, property, valueNode.loc) - colorFormats.push(nodeName.toLowerCase(), valueNode.loc) + colors.push(stringified, property, loc) + colorFormats.p(nodeName.toLowerCase(), loc) return } if (systemColors.has(nodeName)) { let stringified = stringifyNode(valueNode) - colors.push(stringified, property, valueNode.loc) - colorFormats.push('system', valueNode.loc) + colors.push(stringified, property, loc) + colorFormats.p('system', loc) return } return this.skip } - case 'Function': { + case Func: { // Don't walk var() multiple times if (strEquals('var', nodeName)) { return this.skip @@ -511,12 +533,12 @@ export function analyze(css, options = {}) { if (colorFunctions.has(nodeName)) { colors.push(stringifyNode(valueNode), property, valueNode.loc) - colorFormats.push(nodeName.toLowerCase(), valueNode.loc) + colorFormats.p(nodeName.toLowerCase(), valueNode.loc) return } if (endsWith('gradient', nodeName)) { - gradients.push(stringifyNode(valueNode), valueNode.loc) + gradients.p(stringifyNode(valueNode), valueNode.loc) return } // No this.skip here intentionally, @@ -526,7 +548,7 @@ export function analyze(css, options = {}) { }) break } - case 'Declaration': { + case Declaration: { // Do not process Declarations in atRule preludes // because we will handle them manually if (this.atrulePrelude !== null) { @@ -545,31 +567,31 @@ export function analyze(css, options = {}) { } } - let { property } = node + let { property, loc: { start } } = node let propertyLoc = { start: { - line: node.loc.start.line, - column: node.loc.start.column, - offset: node.loc.start.offset + line: start.line, + column: start.column, + offset: start.offset }, end: { - offset: node.loc.start.offset + property.length + offset: start.offset + property.length } } - properties.push(property, propertyLoc) + properties.p(property, propertyLoc) if (hasVendorPrefix(property)) { - propertyVendorPrefixes.push(property, propertyLoc) + propertyVendorPrefixes.p(property, propertyLoc) propertyComplexities.push(2) } else if (isHack(property)) { - propertyHacks.push(property, propertyLoc) + propertyHacks.p(property, propertyLoc) propertyComplexities.push(2) } else if (isCustom(property)) { - customProperties.push(property, propertyLoc) + customProperties.p(property, propertyLoc) propertyComplexities.push(2) if (node.important === true) { - importantCustomProperties.push(property, propertyLoc) + importantCustomProperties.p(property, propertyLoc) } } else { propertyComplexities.push(1) @@ -579,7 +601,7 @@ export function analyze(css, options = {}) { } }) - let embeddedContent = embeds.count() + let embeddedContent = embeds.c() let totalUniqueDeclarations = uniqueDeclarations.size @@ -589,12 +611,14 @@ export function analyze(css, options = {}) { let specificitiesC = specificityC.aggregate() let totalUniqueSelectors = uniqueSelectors.size let assign = Object.assign + let cssLen = css.length + let fontFacesCount = fontfaces.length return { stylesheet: { sourceLinesOfCode: totalAtRules + totalSelectors + totalDeclarations + keyframeSelectors.size(), linesOfCode, - size: css.length, + size: cssLen, comments: { total: totalComments, size: commentsSize, @@ -602,7 +626,7 @@ export function analyze(css, options = {}) { embeddedContent: assign(embeddedContent, { size: { total: embedSize, - ratio: ratio(embedSize, css.length), + ratio: ratio(embedSize, cssLen), }, types: { total: embedTypes.total, @@ -614,35 +638,35 @@ export function analyze(css, options = {}) { }, atrules: { fontface: { - total: fontfaces.length, - totalUnique: fontfaces.length, + total: fontFacesCount, + totalUnique: fontFacesCount, unique: fontfaces, - uniquenessRatio: fontfaces.length === 0 ? 0 : 1 + uniquenessRatio: fontFacesCount === 0 ? 0 : 1 }, - import: imports.count(), + import: imports.c(), media: assign( - medias.count(), + medias.c(), { - browserhacks: mediaBrowserhacks.count(), + browserhacks: mediaBrowserhacks.c(), } ), - charset: charsets.count(), + charset: charsets.c(), supports: assign( - supports.count(), + supports.c(), { - browserhacks: supportsBrowserhacks.count(), + browserhacks: supportsBrowserhacks.c(), }, ), keyframes: assign( - keyframes.count(), { + keyframes.c(), { prefixed: assign( - prefixedKeyframes.count(), { + prefixedKeyframes.c(), { ratio: ratio(prefixedKeyframes.size(), keyframes.size()) }), }), - container: containers.count(), - layer: layers.count(), - property: registeredProperties.count(), + container: containers.c(), + layer: layers.c(), + property: registeredProperties.c(), }, rules: { total: totalRules, @@ -655,21 +679,21 @@ export function analyze(css, options = {}) { { items: ruleSizes.toArray(), }, - uniqueRuleSize.count(), + uniqueRuleSize.c(), ), selectors: assign( selectorsPerRule.aggregate(), { items: selectorsPerRule.toArray(), }, - uniqueSelectorsPerRule.count(), + uniqueSelectorsPerRule.c(), ), declarations: assign( declarationsPerRule.aggregate(), { items: declarationsPerRule.toArray(), }, - uniqueDeclarationsPerRule.count(), + uniqueDeclarationsPerRule.c(), ), }, selectors: { @@ -692,31 +716,31 @@ export function analyze(css, options = {}) { median: [specificitiesA.median, specificitiesB.median, specificitiesC.median], items: specificities, }, - uniqueSpecificities.count(), + uniqueSpecificities.c(), ), complexity: assign( selectorComplexities.aggregate(), - uniqueSelectorComplexities.count(), + uniqueSelectorComplexities.c(), { items: selectorComplexities.toArray(), } ), id: assign( - ids.count(), { + ids.c(), { ratio: ratio(ids.size(), totalSelectors), }), accessibility: assign( - a11y.count(), { + a11y.c(), { ratio: ratio(a11y.size(), totalSelectors), }), - keyframes: keyframeSelectors.count(), + keyframes: keyframeSelectors.c(), prefixed: assign( - prefixedSelectors.count(), + prefixedSelectors.c(), { ratio: ratio(prefixedSelectors.size(), totalSelectors), }, ), - combinators: combinators.count(), + combinators: combinators.c(), }, declarations: { total: totalDeclarations, @@ -737,20 +761,20 @@ export function analyze(css, options = {}) { }, }, properties: assign( - properties.count(), + properties.c(), { prefixed: assign( - propertyVendorPrefixes.count(), + propertyVendorPrefixes.c(), { ratio: ratio(propertyVendorPrefixes.size(), properties.size()), }, ), custom: assign( - customProperties.count(), + customProperties.c(), { ratio: ratio(customProperties.size(), properties.size()), importants: assign( - importantCustomProperties.count(), + importantCustomProperties.c(), { ratio: ratio(importantCustomProperties.size(), customProperties.size()), } @@ -758,7 +782,7 @@ export function analyze(css, options = {}) { }, ), browserhacks: assign( - propertyHacks.count(), { + propertyHacks.c(), { ratio: ratio(propertyHacks.size(), properties.size()), }), complexity: propertyComplexities.aggregate(), @@ -767,22 +791,22 @@ export function analyze(css, options = {}) { colors: assign( colors.count(), { - formats: colorFormats.count(), + formats: colorFormats.c(), }, ), - gradients: gradients.count(), - fontFamilies: fontFamilies.count(), - fontSizes: fontSizes.count(), - lineHeights: lineHeights.count(), - zindexes: zindex.count(), - textShadows: textShadows.count(), - boxShadows: boxShadows.count(), + gradients: gradients.c(), + fontFamilies: fontFamilies.c(), + fontSizes: fontSizes.c(), + lineHeights: lineHeights.c(), + zindexes: zindex.c(), + textShadows: textShadows.c(), + boxShadows: boxShadows.c(), animations: { - durations: durations.count(), - timingFunctions: timingFunctions.count(), + durations: durations.c(), + timingFunctions: timingFunctions.c(), }, - prefixes: vendorPrefixedValues.count(), - browserhacks: valueBrowserhacks.count(), + prefixes: vendorPrefixedValues.c(), + browserhacks: valueBrowserhacks.c(), units: units.count(), }, __meta__: { diff --git a/src/selectors/utils.js b/src/selectors/utils.js index e8503f2..643779a 100644 --- a/src/selectors/utils.js +++ b/src/selectors/utils.js @@ -1,6 +1,17 @@ import walk from 'css-tree/walker' import { startsWith, strEquals } from '../string-utils.js' import { hasVendorPrefix } from '../vendor-prefix.js' +import { + PseudoClassSelector, + IdSelector, + ClassSelector, + PseudoElementSelector, + TypeSelector, + Combinator, + Selector, + AttributeSelector, + Nth, +} from '../css-tree-node-types.js' /** * @@ -10,7 +21,7 @@ import { hasVendorPrefix } from '../vendor-prefix.js' function analyzeList(selectorListAst, cb) { let childSelectors = [] walk(selectorListAst, { - visit: 'Selector', + visit: Selector, enter: function (node) { childSelectors.push(cb(node)) } @@ -38,14 +49,15 @@ export function isAccessibility(selector) { let isA11y = false walk(selector, function (node) { - if (node.type === 'AttributeSelector') { - if (strEquals('role', node.name.name) || startsWith('aria-', node.name.name)) { + if (node.type === AttributeSelector) { + let name = node.name.name + if (strEquals('role', name) || startsWith('aria-', name)) { isA11y = true return this.break } } // Test for [aria-] or [role] inside :is()/:where() and friends - else if (node.type === 'PseudoClassSelector') { + else if (node.type === PseudoClassSelector) { if (isPseudoFunction(node.name)) { let list = analyzeList(node, isAccessibility) @@ -72,15 +84,15 @@ export function getComplexity(selector) { let isPrefixed = false walk(selector, function (node) { - if (node.type === 'Selector' || node.type === 'Nth') return + if (node.type === Selector || node.type === Nth) return complexity++ - if (node.type === 'IdSelector' - || node.type === 'ClassSelector' - || node.type === 'PseudoElementSelector' - || node.type === 'TypeSelector' - || node.type === 'PseudoClassSelector' + if (node.type === IdSelector + || node.type === ClassSelector + || node.type === PseudoElementSelector + || node.type === TypeSelector + || node.type === PseudoClassSelector ) { if (hasVendorPrefix(node.name)) { isPrefixed = true @@ -88,7 +100,7 @@ export function getComplexity(selector) { } } - if (node.type === 'AttributeSelector') { + if (node.type === AttributeSelector) { if (Boolean(node.value)) { complexity++ } @@ -99,7 +111,7 @@ export function getComplexity(selector) { return this.skip } - if (node.type === 'PseudoClassSelector') { + if (node.type === PseudoClassSelector) { if (isPseudoFunction(node.name)) { let list = analyzeList(node, getComplexity) @@ -132,9 +144,12 @@ export function getCombinators(node, onMatch) { /** @type {import('css-tree').CssNode} */ selectorNode, /** @type {import('css-tree').ListItem} */ item ) { - if (selectorNode.type === 'Combinator') { + if (selectorNode.type === Combinator) { + let loc = selectorNode.loc + let name = selectorNode.name + // .loc is null when selectorNode.name === ' ' - if (selectorNode.loc === null) { + if (loc === null) { let previousLoc = item.prev.data.loc.end let start = { offset: previousLoc.offset, @@ -143,7 +158,7 @@ export function getCombinators(node, onMatch) { } onMatch({ - name: selectorNode.name, + name, loc: { start, end: { @@ -155,8 +170,8 @@ export function getCombinators(node, onMatch) { }) } else { onMatch({ - name: selectorNode.name, - loc: selectorNode.loc + name, + loc }) } } diff --git a/src/string-utils.js b/src/string-utils.js index 137ae74..463c73b 100644 --- a/src/string-utils.js +++ b/src/string-utils.js @@ -1,3 +1,11 @@ +/** + * @param {string} str + * @param {number} at + */ +function charCodeAt(str, at) { + return str.charCodeAt(at) +} + /** * Case-insensitive compare two character codes * @param {string} referenceCode @@ -30,7 +38,7 @@ export function strEquals(base, maybe) { if (len !== maybe.length) return false for (let i = 0; i < len; i++) { - if (compareChar(base.charCodeAt(i), maybe.charCodeAt(i)) === false) { + if (compareChar(charCodeAt(base, i), charCodeAt(maybe, i)) === false) { return false } } @@ -58,7 +66,7 @@ export function endsWith(base, maybe) { } for (let i = len - 1; i >= offset; i--) { - if (compareChar(base.charCodeAt(i - offset), maybe.charCodeAt(i)) === false) { + if (compareChar(charCodeAt(base, i - offset), charCodeAt(maybe, i)) === false) { return false } } @@ -77,7 +85,7 @@ export function startsWith(base, maybe) { if (maybe.length < len) return false for (let i = 0; i < len; i++) { - if (compareChar(base.charCodeAt(i), maybe.charCodeAt(i)) === false) { + if (compareChar(charCodeAt(base, i), charCodeAt(maybe, i)) === false) { return false } } diff --git a/src/stylesheet/stylesheet.js b/src/stylesheet/stylesheet.js index a01a70d..ffa64c9 100644 --- a/src/stylesheet/stylesheet.js +++ b/src/stylesheet/stylesheet.js @@ -1,3 +1,12 @@ +/** + * @param {string} str + * @param {number} start + * @param {number} end + */ +function substring(str, start, end) { + return str.substring(start, end) +} + /** @param {string} embed */ export function getEmbedType(embed) { // data:image/gif;base64,R0lG @@ -6,12 +15,12 @@ export function getEmbedType(embed) { let comma = embed.indexOf(',') if (semicolon === -1) { - return embed.substring(start, comma) + return substring(embed, start, comma) } if (comma !== -1 && comma < semicolon) { - return embed.substring(start, comma); + return substring(embed, start, comma); } - return embed.substring(start, semicolon) + return substring(embed, start, semicolon) } \ No newline at end of file diff --git a/src/values/animations.js b/src/values/animations.js index 888ebd6..d1e3b8e 100644 --- a/src/values/animations.js +++ b/src/values/animations.js @@ -1,4 +1,10 @@ import { KeywordSet } from "../keyword-set.js" +import { + Operator, + Dimension, + Identifier, + Func, +} from '../css-tree-node-types.js' const TIMING_KEYWORDS = new KeywordSet([ 'linear', @@ -22,19 +28,20 @@ export function analyzeAnimation(children, stringifyNode) { children.forEach(child => { let type = child.type + let name = child.name // Right after a ',' we start over again - if (type === 'Operator') { + if (type === Operator) { return durationFound = false } - if (type === 'Dimension' && durationFound === false) { + if (type === Dimension && durationFound === false) { durationFound = true return durations.push(stringifyNode(child)) } - if (type === 'Identifier' && TIMING_KEYWORDS.has(child.name)) { + if (type === Identifier && TIMING_KEYWORDS.has(name)) { return timingFunctions.push(stringifyNode(child)) } - if (type === 'Function' && TIMING_FUNCTION_VALUES.has(child.name)) { + if (type === Func && TIMING_FUNCTION_VALUES.has(name)) { return timingFunctions.push(stringifyNode(child)) } }) diff --git a/src/values/browserhacks.js b/src/values/browserhacks.js index fa98a26..557ea16 100644 --- a/src/values/browserhacks.js +++ b/src/values/browserhacks.js @@ -1,9 +1,16 @@ import { endsWith } from "../string-utils.js" +import { Identifier } from "../css-tree-node-types.js" +/** + * @param {import('css-tree').Value} node + */ export function isIe9Hack(node) { let children = node.children - return children - && children.last - && children.last.type === 'Identifier' - && endsWith('\\9', children.last.name) + if (children) { + let last = children.last + return last + && last.type === Identifier + && endsWith('\\9', last.name) + } + return false } \ No newline at end of file diff --git a/src/values/destructure-font-shorthand.js b/src/values/destructure-font-shorthand.js index 02b366e..8e0bbde 100644 --- a/src/values/destructure-font-shorthand.js +++ b/src/values/destructure-font-shorthand.js @@ -1,4 +1,5 @@ import { KeywordSet } from "../keyword-set.js" +import { Identifier, Nr, Dimension, Operator } from '../css-tree-node-types.js' const SYSTEM_FONTS = new KeywordSet([ 'caption', @@ -26,13 +27,19 @@ const SIZE_KEYWORDS = new KeywordSet([ const COMMA = 44 // ','.charCodeAt(0) === 44 const SLASH = 47 // '/'.charCodeAt(0) === 47 -const TYPE_OPERATOR = 'Operator' -const TYPE_IDENTIFIER = 'Identifier' + +/** + * @param {string} str + * @param {number} at + */ +function charCodeAt(str, at) { + return str.charCodeAt(at) +} export function isSystemFont(node) { let firstChild = node.children.first if (firstChild === null) return false - return firstChild.type === TYPE_IDENTIFIER && SYSTEM_FONTS.has(firstChild.name) + return firstChild.type === Identifier && SYSTEM_FONTS.has(firstChild.name) } /** @@ -40,7 +47,6 @@ export function isSystemFont(node) { * @param {*} stringifyNode */ export function destructure(value, stringifyNode) { - let font_family = new Array(2) let font_size let line_height @@ -49,8 +55,8 @@ export function destructure(value, stringifyNode) { // any node that comes before the '/' is the font-size if ( item.next && - item.next.data.type === TYPE_OPERATOR && - item.next.data.value.charCodeAt(0) === SLASH + item.next.data.type === Operator && + charCodeAt(item.next.data.value, 0) === SLASH ) { font_size = stringifyNode(node) return @@ -59,8 +65,8 @@ export function destructure(value, stringifyNode) { // any node that comes after '/' is the line-height if ( item.prev && - item.prev.data.type === TYPE_OPERATOR && - item.prev.data.value.charCodeAt(0) === SLASH + item.prev.data.type === Operator && + charCodeAt(item.prev.data.value, 0) === SLASH ) { line_height = stringifyNode(node) return @@ -69,8 +75,8 @@ export function destructure(value, stringifyNode) { // any node that's followed by ',' is a font-family if ( item.next && - item.next.data.type === TYPE_OPERATOR && - item.next.data.value.charCodeAt(0) === COMMA && + item.next.data.type === Operator && + charCodeAt(item.next.data.value, 0) === COMMA && !font_family[0] ) { font_family[0] = node @@ -82,23 +88,9 @@ export function destructure(value, stringifyNode) { return } - // If, after taking care of font-size and line-height, we still have a remaining dimension, it must be the oblique angle - if ( - node.type === 'Dimension' && - item.prev && - item.prev.data.type === TYPE_IDENTIFIER && - item.prev.data.name === 'oblique' - ) { - // put in the correct amount of whitespace between `oblique` and `` - font_style += - ''.padStart(node.loc.start.offset - item.prev.data.loc.end.offset) + - stringifyNode(node) - return - } - // any node that's a number and not previously caught by line-height or font-size is the font-weight // (oblique will not be caught here, because that's a Dimension, not a Number) - if (node.type === 'Number') { + if (node.type === Nr) { return } @@ -116,9 +108,10 @@ export function destructure(value, stringifyNode) { } // Any remaining identifiers can be font-size, font-style, font-stretch, font-variant or font-weight - if (node.type === TYPE_IDENTIFIER) { - if (SIZE_KEYWORDS.has(node.name)) { - font_size = node.name + if (node.type === Identifier) { + let name = node.name + if (SIZE_KEYWORDS.has(name)) { + font_size = name return } } diff --git a/src/values/values.js b/src/values/values.js index cfd33fb..ba9e1f5 100644 --- a/src/values/values.js +++ b/src/values/values.js @@ -1,4 +1,5 @@ import { KeywordSet } from "../keyword-set.js" +import { Identifier } from "../css-tree-node-types.js" const keywords = new KeywordSet([ 'auto', @@ -10,10 +11,16 @@ const keywords = new KeywordSet([ 'none', // for `text-shadow`, `box-shadow` and `background` ]) +/** + * @param {import('css-tree').Value} node + */ export function isValueKeyword(node) { - if (!node.children) return false - if (node.children.size > 1 || node.children.size === 0) return false + let children = node.children + let size = children.size - let firstChild = node.children.first - return firstChild.type === 'Identifier' && keywords.has(firstChild.name) + if (!children) return false + if (size > 1 || size === 0) return false + + let firstChild = children.first + return firstChild.type === Identifier && keywords.has(firstChild.name) } diff --git a/src/values/vendor-prefix.js b/src/values/vendor-prefix.js index 8ddf824..26afd5b 100644 --- a/src/values/vendor-prefix.js +++ b/src/values/vendor-prefix.js @@ -1,21 +1,27 @@ import { hasVendorPrefix } from '../vendor-prefix.js' +import { Func, Identifier } from '../css-tree-node-types.js' +/** + * @param {import('css-tree').Value} node + */ export function isAstVendorPrefixed(node) { - if (!node.children) { + let children = node.children + + if (!children) { return false } - let children = node.children.toArray() + let list = children.toArray() - for (let index = 0; index < children.length; index++) { - let node = children[index] + for (let index = 0; index < list.length; index++) { + let node = list[index] let { type, name } = node; - if (type === 'Identifier' && hasVendorPrefix(name)) { + if (type === Identifier && hasVendorPrefix(name)) { return true } - if (type === 'Function') { + if (type === Func) { if (hasVendorPrefix(name)) { return true }