diff --git a/src/__fixtures__/bol-com-20231008.json b/src/__fixtures__/bol-com-20231008.json index a5a2efa..fcc61b8 100644 --- a/src/__fixtures__/bol-com-20231008.json +++ b/src/__fixtures__/bol-com-20231008.json @@ -54964,6 +54964,39 @@ "uniquenessRatio": 0, "ratio": 0 }, + "pseudoClasses": { + "total": 2010, + "totalUnique": 26, + "unique": { + "not": 167, + "root": 3, + "-moz-focusring": 8, + "focus": 94, + "focus-visible": 66, + "first-child": 57, + "after": 447, + "before": 586, + "hover": 280, + "active": 131, + "has": 2, + "last-child": 45, + "checked": 35, + "focus-within": 13, + "empty": 1, + "-moz-placeholder": 3, + "-ms-input-placeholder": 3, + "last-of-type": 5, + "nth-of-type": 11, + "first-of-type": 2, + "nth-child": 4, + "disabled": 34, + "placeholder-shown": 6, + "only-child": 2, + "indeterminate": 3, + "first-letter": 2 + }, + "uniquenessRatio": 0.012935323383084577 + }, "accessibility": { "total": 2, "totalUnique": 2, diff --git a/src/__fixtures__/bootstrap-5.3.2.json b/src/__fixtures__/bootstrap-5.3.2.json index 05806f5..20a1c9d 100644 --- a/src/__fixtures__/bootstrap-5.3.2.json +++ b/src/__fixtures__/bootstrap-5.3.2.json @@ -25889,6 +25889,37 @@ "uniquenessRatio": 0, "ratio": 0 }, + "pseudoClasses": { + "total": 467, + "totalUnique": 24, + "unique": { + "root": 3, + "hover": 47, + "not": 145, + "focus": 66, + "focus-visible": 11, + "disabled": 27, + "last-child": 34, + "first-child": 36, + "nth-of-type": 1, + "nth-child": 2, + "-moz-focusring": 1, + "active": 11, + "checked": 13, + "indeterminate": 1, + "-moz-placeholder-shown": 4, + "placeholder-shown": 4, + "-webkit-autofill": 3, + "focus-within": 7, + "nth-last-child": 5, + "valid": 17, + "invalid": 17, + "empty": 6, + "first-of-type": 3, + "last-of-type": 3 + }, + "uniquenessRatio": 0.05139186295503212 + }, "accessibility": { "total": 1, "totalUnique": 1, diff --git a/src/__fixtures__/cnn-20231008.json b/src/__fixtures__/cnn-20231008.json index 21969fc..2220a63 100644 --- a/src/__fixtures__/cnn-20231008.json +++ b/src/__fixtures__/cnn-20231008.json @@ -31271,6 +31271,33 @@ "uniquenessRatio": 1, "ratio": 0.0004955401387512388 }, + "pseudoClasses": { + "total": 2476, + "totalUnique": 20, + "unique": { + "root": 6, + "hover": 301, + "after": 113, + "not": 1363, + "last-child": 67, + "has": 76, + "first-child": 178, + "nth-child": 107, + "before": 127, + "active": 34, + "focus": 49, + "empty": 10, + "only-child": 9, + "is": 1, + "first-of-type": 13, + "last-of-type": 6, + "-ms-input-placeholder": 2, + "checked": 10, + "disabled": 2, + "nth-of-type": 2 + }, + "uniquenessRatio": 0.008077544426494346 + }, "accessibility": { "total": 4, "totalUnique": 4, diff --git a/src/__fixtures__/css-tricks-20231008.json b/src/__fixtures__/css-tricks-20231008.json index 178d273..7e52be0 100644 --- a/src/__fixtures__/css-tricks-20231008.json +++ b/src/__fixtures__/css-tricks-20231008.json @@ -12512,6 +12512,31 @@ "uniquenessRatio": 0.8913043478260869, "ratio": 0.030223390275952694 }, + "pseudoClasses": { + "total": 386, + "totalUnique": 18, + "unique": { + "after": 19, + "before": 75, + "not": 130, + "nth-child": 2, + "first-child": 12, + "last-child": 17, + "focus": 47, + "hover": 46, + "is": 2, + "target": 6, + "first-of-type": 8, + "empty": 1, + "first-letter": 1, + "nth-of-type": 1, + "focus-within": 3, + "active": 2, + "root": 2, + "where": 12 + }, + "uniquenessRatio": 0.046632124352331605 + }, "accessibility": { "total": 2, "totalUnique": 2, diff --git a/src/__fixtures__/gazelle-20231008.json b/src/__fixtures__/gazelle-20231008.json index 08511bd..a511df5 100644 --- a/src/__fixtures__/gazelle-20231008.json +++ b/src/__fixtures__/gazelle-20231008.json @@ -87405,6 +87405,37 @@ "uniquenessRatio": 0.8229508196721311, "ratio": 0.05726086548390125 }, + "pseudoClasses": { + "total": 2883, + "totalUnique": 24, + "unique": { + "not": 269, + "active": 128, + "hover": 437, + "root": 2, + "after": 275, + "before": 821, + "visited": 6, + "focus": 278, + "last-child": 208, + "first-child": 220, + "nth-of-type": 7, + "-ms-input-placeholder": 1, + "empty": 4, + "link": 5, + "last-of-type": 39, + "first-of-type": 46, + "checked": 58, + "disabled": 34, + "nth-child": 35, + "first-letter": 1, + "has": 6, + "where": 1, + "-moz-focusring": 1, + "-moz-ui-invalid": 1 + }, + "uniquenessRatio": 0.008324661810613945 + }, "accessibility": { "total": 16, "totalUnique": 13, diff --git a/src/__fixtures__/github-20231008.json b/src/__fixtures__/github-20231008.json index a7241b2..460cb04 100644 --- a/src/__fixtures__/github-20231008.json +++ b/src/__fixtures__/github-20231008.json @@ -91789,6 +91789,46 @@ "uniquenessRatio": 1, "ratio": 0.0011177347242921013 }, + "pseudoClasses": { + "total": 1942, + "totalUnique": 33, + "unique": { + "root": 65, + "not": 344, + "hover": 484, + "focus": 252, + "focus-visible": 112, + "disabled": 57, + "only-child": 6, + "active": 107, + "first-child": 91, + "last-child": 87, + "checked": 22, + "focus-within": 11, + "first-of-type": 14, + "last-of-type": 14, + "nth-child": 10, + "nth-last-child": 1, + "target": 16, + "empty": 13, + "is": 4, + "popover-open": 1, + "before": 113, + "after": 71, + "indeterminate": 1, + "nth-of-type": 30, + "has": 2, + "placeholder-shown": 3, + "placeholder": 1, + "dir": 2, + "defined": 1, + "-webkit-autofill": 2, + "invalid": 2, + "nth-last-of-type": 1, + "visited": 2 + }, + "uniquenessRatio": 0.016992790937178166 + }, "accessibility": { "total": 331, "totalUnique": 317, diff --git a/src/__fixtures__/indiatimes-20231008.json b/src/__fixtures__/indiatimes-20231008.json index fba72df..7d2755b 100644 --- a/src/__fixtures__/indiatimes-20231008.json +++ b/src/__fixtures__/indiatimes-20231008.json @@ -56553,6 +56553,29 @@ "uniquenessRatio": 0.90625, "ratio": 0.003918207420105302 }, + "pseudoClasses": { + "total": 1081, + "totalUnique": 16, + "unique": { + "first-of-type": 13, + "after": 108, + "last-child": 255, + "checked": 3, + "first-child": 20, + "before": 294, + "last-of-type": 40, + "focus": 8, + "not": 110, + "hover": 103, + "nth-child": 119, + "nth-last-child": 3, + "active": 1, + "empty": 2, + "root": 1, + "Georgia": 1 + }, + "uniquenessRatio": 0.014801110083256245 + }, "accessibility": { "total": 0, "totalUnique": 0, diff --git a/src/__fixtures__/smashing-magazine-20231008.json b/src/__fixtures__/smashing-magazine-20231008.json index 97af514..013b1d9 100644 --- a/src/__fixtures__/smashing-magazine-20231008.json +++ b/src/__fixtures__/smashing-magazine-20231008.json @@ -48874,6 +48874,40 @@ "uniquenessRatio": 0.875, "ratio": 0.0027590963959303327 }, + "pseudoClasses": { + "total": 1259, + "totalUnique": 27, + "unique": { + "not": 306, + "before": 83, + "after": 82, + "root": 3, + "-moz-focusring": 11, + "nth-of-type": 28, + "last-child": 25, + "active": 127, + "focus": 178, + "hover": 191, + "first-of-type": 21, + "nth-child": 49, + "-moz-placeholder": 2, + "-ms-input-placeholder": 10, + "target": 11, + "empty": 7, + "vertical": 1, + "horizontal": 1, + "first-child": 48, + "last-of-type": 20, + "-webkit-autofill": 29, + "checked": 12, + "nth-last-child": 4, + "visited": 1, + "-moz-placeholder-shown": 4, + "placeholder-shown": 4, + "first-line": 1 + }, + "uniquenessRatio": 0.021445591739475776 + }, "accessibility": { "total": 29, "totalUnique": 29, diff --git a/src/__fixtures__/trello-20231008.json b/src/__fixtures__/trello-20231008.json index 18648d6..adf7fc0 100644 --- a/src/__fixtures__/trello-20231008.json +++ b/src/__fixtures__/trello-20231008.json @@ -10428,6 +10428,27 @@ "uniquenessRatio": 1, "ratio": 0.003469210754553339 }, + "pseudoClasses": { + "total": 493, + "totalUnique": 14, + "unique": { + "hover": 85, + "focus": 120, + "not": 44, + "focus-visible": 40, + "disabled": 4, + "root": 1, + "last-child": 170, + "active": 17, + "last-of-type": 2, + "nth-child": 3, + "-ms-input-placeholder": 2, + "after": 1, + "before": 1, + "first-child": 3 + }, + "uniquenessRatio": 0.028397565922920892 + }, "accessibility": { "total": 0, "totalUnique": 0, diff --git a/src/index.js b/src/index.js index 251f519..06aca91 100644 --- a/src/index.js +++ b/src/index.js @@ -2,7 +2,7 @@ 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, isPrefixed } from './selectors/utils.js' +import { getCombinators, getComplexity, isAccessibility, isPrefixed, hasPseudoClass } from './selectors/utils.js' import { colorFunctions, colorKeywords, namedColors, systemColors } from './values/colors.js' import { destructure, isSystemFont } from './values/destructure-font-shorthand.js' import { isValueKeyword, keywords } from './values/values.js' @@ -156,6 +156,7 @@ export function analyze(css, options = {}) { let specificities = [] let ids = new Collection(useLocations) let a11y = new Collection(useLocations) + let pseudoClasses = new Collection(useLocations) let combinators = new Collection(useLocations) // Declarations @@ -307,6 +308,13 @@ export function analyze(css, options = {}) { a11y.p(selector, node.loc) } + let pseudos = hasPseudoClass(node) + if (pseudos !== false) { + for (let pseudo of pseudos) { + pseudoClasses.p(pseudo, node.loc) + } + } + let complexity = getComplexity(node) if (isPrefixed(node)) { @@ -820,6 +828,7 @@ export function analyze(css, options = {}) { ids.c(), { ratio: ratio(ids.size(), totalSelectors), }), + pseudoClasses: pseudoClasses.c(), accessibility: assign( a11y.c(), { ratio: ratio(a11y.size(), totalSelectors), diff --git a/src/index.test.js b/src/index.test.js index 958977b..7542a85 100644 --- a/src/index.test.js +++ b/src/index.test.js @@ -256,6 +256,12 @@ Api("handles empty input gracefully", () => { uniquenessRatio: 0, ratio: 0, }, + pseudoClasses: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + }, accessibility: { total: 0, totalUnique: 0, diff --git a/src/selectors/pseudos.test.js b/src/selectors/pseudos.test.js new file mode 100644 index 0000000..41756a3 --- /dev/null +++ b/src/selectors/pseudos.test.js @@ -0,0 +1,59 @@ +import { suite } from 'uvu' +import * as assert from 'uvu/assert' +import { analyze } from '../index.js' + +let test = suite('Selector Pseudo Classes') + +test('calculates pseudo classes', () => { + const actual = analyze(` + a, + a:hover, + a:active, + a:lang(en), + a:dir(ltr), + a:dir(ltr), + a:has(.thing) {}` + ).selectors.pseudoClasses + let expected = { + total: 6, + totalUnique: 5, + uniquenessRatio: 5 / 6, + unique: { + 'hover': 1, + 'active': 1, + 'lang': 1, + 'dir': 2, + 'has': 1 + }, + } + assert.equal(actual, expected) +}) + +test('logs the whole parent selector when using locations', () => { + let actual = analyze(` + a:hover, + a:lang(en) {}`, + { useUnstableLocations: true } + ).selectors.pseudoClasses.__unstable__uniqueWithLocations + let expected = { + 'hover': [ + { + line: 2, + column: 3, + offset: 3, + length: 7, + } + ], + 'lang': [ + { + line: 3, + column: 3, + offset: 14, + length: 10, + } + ] + } + assert.equal(actual, expected) +}) + +test.run() diff --git a/src/selectors/selectors.test.js b/src/selectors/selectors.test.js index f6c300d..753b7ea 100644 --- a/src/selectors/selectors.test.js +++ b/src/selectors/selectors.test.js @@ -70,6 +70,12 @@ Selectors('handles CSS without selectors', () => { uniquenessRatio: 0, ratio: 0, }, + pseudoClasses: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + }, accessibility: { total: 0, totalUnique: 0, @@ -313,6 +319,12 @@ Selectors('handles emoji selectors', () => { uniquenessRatio: 0, ratio: 0, }, + pseudoClasses: { + total: 0, + totalUnique: 0, + unique: {}, + uniquenessRatio: 0, + }, accessibility: { total: 0, totalUnique: 0, diff --git a/src/selectors/utils.js b/src/selectors/utils.js index 8ac2011..965f826 100644 --- a/src/selectors/utils.js +++ b/src/selectors/utils.js @@ -96,6 +96,26 @@ export function isPrefixed(selector) { return isPrefixed; } +/** + * @param {import('css-tree').Selector} selector + * @returns {string[] | false} The pseudo-class name if it exists, otherwise false + */ +export function hasPseudoClass(selector) { + let pseudos = [] + + walk(selector, function (node) { + if (node.type === PseudoClassSelector) { + pseudos.push(node.name) + } + }) + + if (pseudos.length === 0) { + return false + } + + return pseudos; +} + /** * Get the Complexity for the AST of a Selector Node * @param {import('css-tree').Selector} selector - AST Node for a Selector