diff --git a/src/index.js b/src/index.js index 4ae057c..7506731 100644 --- a/src/index.js +++ b/src/index.js @@ -2,12 +2,12 @@ import parse from 'css-tree/parser' import walk from 'css-tree/walker' import { calculate } from '@bramus/specificity/core' import { isSupportsBrowserhack, isMediaBrowserhack } from './atrules/atrules.js' -import { getCombinators, getComplexity, isAccessibility } from './selectors/utils.js' +import { getCombinators, getComplexity, isAccessibility, isPrefixed } from './selectors/utils.js' import { colorFunctions, colorKeywords, namedColors, systemColors } from './values/colors.js' import { destructure, isSystemFont } from './values/destructure-font-shorthand.js' import { isValueKeyword } from './values/values.js' import { analyzeAnimation } from './values/animations.js' -import { isAstVendorPrefixed } from './values/vendor-prefix.js' +import { isValuePrefixed } from './values/vendor-prefix.js' import { ContextCollection } from './context-collection.js' import { Collection } from './collection.js' import { AggregateCollection } from './aggregate-collection.js' @@ -284,9 +284,9 @@ export function analyze(css, options = {}) { a11y.p(selector, node.loc) } - let [complexity, isPrefixed] = getComplexity(node) + let complexity = getComplexity(node) - if (isPrefixed) { + if (isPrefixed(node)) { prefixedSelectors.p(selector, node.loc) } @@ -392,7 +392,7 @@ export function analyze(css, options = {}) { let declaration = this.declaration let { property, important } = declaration - if (isAstVendorPrefixed(node)) { + if (isValuePrefixed(node)) { vendorPrefixedValues.p(stringifyNode(node), node.loc) } @@ -838,4 +838,29 @@ export function compareSpecificity(a, b) { } return b[0] - a[0] -} \ No newline at end of file +} + +export { + getComplexity as selectorComplexity, + isPrefixed as isSelectorPrefixed, + isAccessibility as isAccessibilitySelector, +} from './selectors/utils.js' + +export { + isSupportsBrowserhack, + isMediaBrowserhack +} from './atrules/atrules.js' + +export { + isBrowserhack as isValueBrowserhack +} from './values/browserhacks.js' + +export { + isHack as isPropertyHack, +} from './properties/property-utils.js' + +export { + isValuePrefixed +} from './values/vendor-prefix.js' + +export { hasVendorPrefix } from './vendor-prefix.js' diff --git a/src/index.test.js b/src/index.test.js index 837f3b3..853b4d9 100644 --- a/src/index.test.js +++ b/src/index.test.js @@ -1,174 +1,222 @@ -import { suite } from 'uvu'; -import * as assert from 'uvu/assert'; -import { analyze, compareSpecificity } from './index.js' +import { suite } from "uvu"; +import * as assert from "uvu/assert"; +import { + analyze, + compareSpecificity, + selectorComplexity, + isAccessibilitySelector, + isSelectorPrefixed, + isMediaBrowserhack, + isSupportsBrowserhack, + isValueBrowserhack, + isPropertyHack, + isValuePrefixed, + hasVendorPrefix, +} from "./index.js"; -const Api = suite('Public API') +const Api = suite("Public API") -Api('exposes the analyze method', () => { - assert.is(typeof analyze, 'function') +Api("exposes the 'analyze' method", () => { + assert.is(typeof analyze, "function") }) -Api('exposes the compareSpecificity method', () => { - assert.is(typeof compareSpecificity, 'function') +Api('exposes the "compareSpecificity" method', () => { + assert.is(typeof compareSpecificity, "function") }) -Api('does not break on CSS Syntax Errors', () => { - assert.not.throws(() => analyze('test, {}')) - assert.not.throws(() => analyze('test { color red }')) +Api('exposes the "selectorComplexity" method', () => { + assert.is(typeof selectorComplexity, "function") }) -Api('handles empty input gracefully', () => { - const actual = analyze('') - delete actual.__meta__ +Api('exposes the "isSelectorPrefixed" method', () => { + assert.is(typeof isSelectorPrefixed, "function") +}) + +Api('exposes the "isAccessibilitySelector" method', () => { + assert.is(typeof isAccessibilitySelector, "function") +}) + +Api('exposes the "isMediaBrowserhack" method', () => { + assert.is(typeof isMediaBrowserhack, "function") +}) + +Api('exposes the "isSupportsBrowserhack" method', () => { + assert.is(typeof isSupportsBrowserhack, "function") +}) + +Api('exposes the "isValueBrowserhack" method', () => { + assert.is(typeof isValueBrowserhack, "function") +}) + +Api('exposes the "isPropertyHack" method', () => { + assert.is(typeof isPropertyHack, "function") +}) + +Api('exposes the "isValuePrefixed" method', () => { + assert.is(typeof isValuePrefixed, "function") +}) + +Api('exposes the "hasVendorPrefix" method', () => { + assert.is(typeof hasVendorPrefix, "function") +}) + +Api("does not break on CSS Syntax Errors", () => { + assert.not.throws(() => analyze("test, {}")) + assert.not.throws(() => analyze("test { color red }")) +}) + +Api("handles empty input gracefully", () => { + const actual = analyze("") + delete actual.__meta__; const expected = { - "stylesheet": { - "sourceLinesOfCode": 0, - "linesOfCode": 1, - "size": 0, - "comments": { - "total": 0, - "size": 0 - }, - "embeddedContent": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0, - "size": { - "total": 0, - "ratio": 0 + stylesheet: { + sourceLinesOfCode: 0, + linesOfCode: 1, + size: 0, + comments: { + total: 0, + size: 0, + }, + embeddedContent: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + size: { + total: 0, + ratio: 0, + }, + types: { + total: 0, + totalUnique: 0, + uniquenessRatio: 0, + unique: {}, }, - "types": { - "total": 0, - "totalUnique": 0, - "uniquenessRatio": 0, - "unique": {} - } - } - }, - "atrules": { - "fontface": { - "total": 0, - "totalUnique": 0, - "unique": [], - "uniquenessRatio": 0 - }, - "import": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0 }, - "media": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0, - "browserhacks": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0, + }, + atrules: { + fontface: { + total: 0, + totalUnique: 0, + unique: [], + uniquenessRatio: 0, + }, + import: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + }, + media: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + browserhacks: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, }, }, - "charset": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0, + charset: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + }, + supports: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + browserhacks: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + }, }, - "supports": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0, - "browserhacks": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0, + keyframes: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + prefixed: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + ratio: 0, }, }, - "keyframes": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0, - "prefixed": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0, - "ratio": 0 - } + container: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, }, - "container": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0 + layer: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, }, - "layer": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0 + property: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, }, - "property": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0 - } }, - "rules": { - "total": 0, - "empty": { - "total": 0, - "ratio": 0, + rules: { + total: 0, + empty: { + total: 0, + ratio: 0, + }, + sizes: { + min: 0, + max: 0, + mean: 0, + mode: 0, + median: 0, + range: 0, + sum: 0, + items: [], + unique: {}, + total: 0, + totalUnique: 0, + uniquenessRatio: 0, + }, + selectors: { + min: 0, + max: 0, + mean: 0, + mode: 0, + median: 0, + range: 0, + sum: 0, + items: [], + unique: {}, + total: 0, + totalUnique: 0, + uniquenessRatio: 0, + }, + declarations: { + min: 0, + max: 0, + mean: 0, + mode: 0, + median: 0, + range: 0, + sum: 0, + items: [], + unique: {}, + total: 0, + totalUnique: 0, + uniquenessRatio: 0, }, - "sizes": { - "min": 0, - "max": 0, - "mean": 0, - "mode": 0, - "median": 0, - "range": 0, - "sum": 0, - "items": [], - "unique": {}, - "total": 0, - "totalUnique": 0, - "uniquenessRatio": 0, - }, - "selectors": { - "min": 0, - "max": 0, - "mean": 0, - "mode": 0, - "median": 0, - "range": 0, - "sum": 0, - "items": [], - "unique": {}, - "total": 0, - "totalUnique": 0, - "uniquenessRatio": 0, - }, - "declarations": { - "min": 0, - "max": 0, - "mean": 0, - "mode": 0, - "median": 0, - "range": 0, - "sum": 0, - "items": [], - "unique": {}, - "total": 0, - "totalUnique": 0, - "uniquenessRatio": 0, - } }, "selectors": { "total": 0, @@ -251,42 +299,42 @@ Api('handles empty input gracefully', () => { "uniquenessRatio": 0, }, }, - "declarations": { - "total": 0, - "totalUnique": 0, - "uniquenessRatio": 0, - "unique": { - "total": 0, - "ratio": 0 + declarations: { + total: 0, + totalUnique: 0, + uniquenessRatio: 0, + unique: { + total: 0, + ratio: 0, + }, + importants: { + total: 0, + ratio: 0, + inKeyframes: { + total: 0, + ratio: 0, + }, }, - "importants": { - "total": 0, - "ratio": 0, - "inKeyframes": { - "total": 0, - "ratio": 0 - } - } }, - "properties": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0, - "prefixed": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0, - "ratio": 0 - }, - "custom": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0, - "ratio": 0, - "importants": { + properties: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + prefixed: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + ratio: 0, + }, + custom: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + ratio: 0, + importants: { total: 0, totalUnique: 0, unique: {}, @@ -294,123 +342,125 @@ Api('handles empty input gracefully', () => { ratio: 0, }, }, - "browserhacks": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0, - "ratio": 0 + browserhacks: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + ratio: 0, + }, + complexity: { + min: 0, + max: 0, + mean: 0, + mode: 0, + median: 0, + range: 0, + sum: 0, }, - "complexity": { - "min": 0, - "max": 0, - "mean": 0, - "mode": 0, - "median": 0, - "range": 0, - "sum": 0, - } }, - "values": { - "colors": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0, - "itemsPerContext": {}, - "formats": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0, - } - }, - "gradients": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0 - }, - "fontFamilies": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0 - }, - "fontSizes": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0 - }, - "lineHeights": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0 - }, - "zindexes": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0 - }, - "textShadows": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0 - }, - "boxShadows": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0 - }, - "animations": { - "durations": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0 + values: { + colors: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + itemsPerContext: {}, + formats: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, }, - "timingFunctions": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0 - } }, - "prefixes": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0 + gradients: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + }, + fontFamilies: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + }, + fontSizes: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + }, + lineHeights: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + }, + zindexes: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + }, + textShadows: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + }, + boxShadows: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + }, + animations: { + durations: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + }, + timingFunctions: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + }, }, - "browserhacks": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0 + prefixes: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + }, + browserhacks: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + }, + units: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + itemsPerContext: {}, }, - "units": { - "total": 0, - "totalUnique": 0, - "unique": {}, - "uniquenessRatio": 0, - "itemsPerContext": {} - } }, - } + }; assert.equal(actual, expected) }) -Api('has metadata', () => { - const fixture = Array.from({ length: 100 }).map(_ => ` +Api("has metadata", () => { + const fixture = Array.from({ length: 100 }) + .map( + (_) => ` html { font: 1em/1 sans-serif; - color: rgb(0 0 0 / 0.5); + color: rgb(0 0 0 / 0.5) } @media screen { @@ -421,19 +471,30 @@ Api('has metadata', () => { } } } - `).join('') + ` + ) + .join("") const result = analyze(fixture) - const actual = result.__meta__ + const actual = result.__meta__; - assert.type(actual.parseTime, 'number') - assert.ok(actual.parseTime > 0, `expected parseTime to be bigger than 0, got ${actual.parseTime}`) + assert.type(actual.parseTime, "number") + assert.ok( + actual.parseTime > 0, + `expected parseTime to be bigger than 0, got ${actual.parseTime}` + ) - assert.type(actual.analyzeTime, 'number') - assert.ok(actual.analyzeTime > 0, `expected analyzeTime to be bigger than 0, got ${actual.parseTime}`) + assert.type(actual.analyzeTime, "number") + assert.ok( + actual.analyzeTime > 0, + `expected analyzeTime to be bigger than 0, got ${actual.parseTime}` + ) - assert.type(actual.total, 'number') - assert.ok(actual.total > 0, `expected total time to be bigger than 0, got ${actual.parseTime}`) + assert.type(actual.total, "number") + assert.ok( + actual.total > 0, + `expected total time to be bigger than 0, got ${actual.parseTime}` + ) }) -Api.run() \ No newline at end of file +Api.run() diff --git a/src/selectors/utils.js b/src/selectors/utils.js index 738ab0e..21090ab 100644 --- a/src/selectors/utils.js +++ b/src/selectors/utils.js @@ -74,6 +74,35 @@ export function isAccessibility(selector) { return isA11y; } +/** + * @param {import('css-tree').Selector} selector + * @returns {boolean} Whether the selector contains a vendor prefix + */ +export function isPrefixed(selector) { + let isPrefixed = false + + walk(selector, function (node) { + if (node.type === IdSelector + || node.type === ClassSelector + || node.type === PseudoElementSelector + || node.type === TypeSelector + || node.type === PseudoClassSelector + ) { + if (hasVendorPrefix(node.name)) { + isPrefixed = true + return this.break + } + } else if (node.type === AttributeSelector) { + if (hasVendorPrefix(node.name.name)) { + isPrefixed = true + return this.break + } + } + }) + + return isPrefixed; +} + /** * Get the Complexity for the AST of a Selector Node * @param {import('css-tree').Selector} selector - AST Node for a Selector @@ -81,7 +110,6 @@ export function isAccessibility(selector) { */ export function getComplexity(selector) { let complexity = 0 - let isPrefixed = false walk(selector, function (node) { if (node.type === Selector || node.type === Nth) return @@ -95,7 +123,6 @@ export function getComplexity(selector) { || node.type === PseudoClassSelector ) { if (hasVendorPrefix(node.name)) { - isPrefixed = true complexity++ } } @@ -105,7 +132,6 @@ export function getComplexity(selector) { complexity++ } if (hasVendorPrefix(node.name.name)) { - isPrefixed = true complexity++ } return this.skip @@ -118,16 +144,15 @@ export function getComplexity(selector) { // Bail out for empty/non-existent :nth-child() params if (list.length === 0) return - list.forEach(([c, p]) => { + list.forEach((c) => { complexity += c - if (p === true) isPrefixed = true }) return this.skip } } }) - return [complexity, isPrefixed] + return complexity } /** diff --git a/src/values/browserhacks.js b/src/values/browserhacks.js index 557ea16..eaadc1f 100644 --- a/src/values/browserhacks.js +++ b/src/values/browserhacks.js @@ -13,4 +13,12 @@ export function isIe9Hack(node) { && endsWith('\\9', last.name) } return false +} + +/** + * @param {import('css-tree').Value} node + * @param {boolean|string} important - // i.e. `property: value !ie` + */ +export function isBrowserhack(node, important) { + return isIe9Hack(node) || typeof important === 'string' } \ No newline at end of file diff --git a/src/values/vendor-prefix.js b/src/values/vendor-prefix.js index 26afd5b..6f9bcd3 100644 --- a/src/values/vendor-prefix.js +++ b/src/values/vendor-prefix.js @@ -4,7 +4,7 @@ import { Func, Identifier } from '../css-tree-node-types.js' /** * @param {import('css-tree').Value} node */ -export function isAstVendorPrefixed(node) { +export function isValuePrefixed(node) { let children = node.children if (!children) { @@ -26,7 +26,7 @@ export function isAstVendorPrefixed(node) { return true } - if (isAstVendorPrefixed(node)) { + if (isValuePrefixed(node)) { return true } }