Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 87 additions & 50 deletions src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { test, expect } from 'vitest'
import { test, expect, describe } from 'vitest'
import {
analyze,
compareSpecificity,
Expand All @@ -17,72 +17,109 @@ import {
systemColors,
colorFunctions,
colorKeywords,
type UniqueWithLocations,
type Location,
type Specificity,
} from './index.js'

test("exposes the 'analyze' method", () => {
expect(typeof analyze).toBe('function')
})
describe('Public API', () => {
test("exposes the 'analyze' method", () => {
expect(typeof analyze).toBe('function')
})

test('exposes the "compareSpecificity" method', () => {
expect(typeof compareSpecificity).toBe('function')
})
test('exposes the "compareSpecificity" method', () => {
expect(typeof compareSpecificity).toBe('function')
})

test('exposes the "selectorComplexity" method', () => {
expect(typeof selectorComplexity).toBe('function')
})
test('exposes the "selectorComplexity" method', () => {
expect(typeof selectorComplexity).toBe('function')
})

test('exposes the "isSelectorPrefixed" method', () => {
expect(typeof isSelectorPrefixed).toBe('function')
})
test('exposes the "isSelectorPrefixed" method', () => {
expect(typeof isSelectorPrefixed).toBe('function')
})

test('exposes the "isAccessibilitySelector" method', () => {
expect(typeof isAccessibilitySelector).toBe('function')
})
test('exposes the "isAccessibilitySelector" method', () => {
expect(typeof isAccessibilitySelector).toBe('function')
})

test('exposes the "isMediaBrowserhack" method', () => {
expect(typeof isMediaBrowserhack).toBe('function')
})
test('exposes the "isMediaBrowserhack" method', () => {
expect(typeof isMediaBrowserhack).toBe('function')
})

test('exposes the "isSupportsBrowserhack" method', () => {
expect(typeof isSupportsBrowserhack).toBe('function')
})
test('exposes the "isSupportsBrowserhack" method', () => {
expect(typeof isSupportsBrowserhack).toBe('function')
})

test('exposes the "isPropertyHack" method', () => {
expect(typeof isPropertyHack).toBe('function')
})
test('exposes the "isPropertyHack" method', () => {
expect(typeof isPropertyHack).toBe('function')
})

test('exposes the "isValuePrefixed" method', () => {
expect(typeof isValuePrefixed).toBe('function')
})
test('exposes the "isValuePrefixed" method', () => {
expect(typeof isValuePrefixed).toBe('function')
})

test('exposes the "hasVendorPrefix" method', () => {
expect(typeof hasVendorPrefix).toBe('function')
})
test('exposes the "hasVendorPrefix" method', () => {
expect(typeof hasVendorPrefix).toBe('function')
})

test('exposes the namedColors KeywordSet', () => {
expect(namedColors.has('Red')).toBeTruthy()
})
test('exposes the "compareSpecificity" method', () => {
expect(typeof compareSpecificity).toBe('function')
})

test('exposes the systemColors KeywordSet', () => {
expect(systemColors.has('LinkText')).toBeTruthy()
})
test('exposes the namedColors KeywordSet', () => {
expect(namedColors.has('Red')).toBeTruthy()
})

test('exposes the colorFunctions KeywordSet', () => {
expect(colorFunctions.has('okLAB')).toBeTruthy()
})
test('exposes the systemColors KeywordSet', () => {
expect(systemColors.has('LinkText')).toBeTruthy()
})

test('exposes the colorKeywords KeywordSet', () => {
expect(colorKeywords.has('TRANSPARENT')).toBeTruthy()
})
test('exposes the colorFunctions KeywordSet', () => {
expect(colorFunctions.has('okLAB')).toBeTruthy()
})

test('exposes CSS keywords KeywordSet', () => {
expect(cssKeywords.has('Auto')).toBeTruthy()
expect(cssKeywords.has('inherit')).toBeTruthy()
})
test('exposes the colorKeywords KeywordSet', () => {
expect(colorKeywords.has('TRANSPARENT')).toBeTruthy()
})

test('exposes CSS keywords KeywordSet', () => {
expect(cssKeywords.has('Auto')).toBeTruthy()
expect(cssKeywords.has('inherit')).toBeTruthy()
})

test('exposes the KeywordSet class', () => {
expect(typeof KeywordSet).toBe('function')
expect(new KeywordSet([]).constructor.name).toBe('KeywordSet')
})

test('exposes Location type', () => {
let location: Location = {
offset: 0,
line: 0,
length: 0,
column: 0,
}
expect(location).toHaveProperty('line')
})

test('exposes UniqueWithLocations type', () => {
let location: Location = {
offset: 0,
line: 0,
length: 0,
column: 0,
}
let uniqueWithLocations: UniqueWithLocations = {
'my-item': [location],
}
expect(uniqueWithLocations).toHaveProperty('my-item')
})

test('exposes the KeywordSet class', () => {
expect(typeof KeywordSet).toBe('function')
expect(new KeywordSet([]).constructor.name).toBe('KeywordSet')
test('exposes Specificity type', () => {
let specificity: Specificity = [1, 1, 1]
expect(specificity).toHaveLength(3)
})
})

test('does not break on CSS Syntax Errors', () => {
Expand Down
35 changes: 19 additions & 16 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { isValueKeyword, keywords, isValueReset } from './values/values.js'
import { analyzeAnimation } from './values/animations.js'
import { isValuePrefixed } from './values/vendor-prefix.js'
import { ContextCollection } from './context-collection.js'
import { Collection } from './collection.js'
import { Collection, type Location } from './collection.js'
import { AggregateCollection } from './aggregate-collection.js'
import { strEquals, startsWith, endsWith } from './string-utils.js'
import { hasVendorPrefix } from './vendor-prefix.js'
Expand All @@ -24,7 +24,7 @@ import { Atrule, Selector, Dimension, Url, Value, Hash, Rule, Identifier, Func,
import { KeywordSet } from './keyword-set.js'
import type { CssNode, Declaration, SelectorList } from 'css-tree'

type Specificity = [number, number, number]
export type Specificity = [number, number, number]

let border_radius_properties = new KeywordSet([
'border-radius',
Expand Down Expand Up @@ -63,14 +63,14 @@ function analyzeInternal<T extends boolean>(css: string, options: Options, useLo

/**
* Recreate the authored CSS from a CSSTree node
* @param {import('css-tree').CssNode} node - Node from CSSTree AST to stringify
* @returns {string} str - The stringified node
* @param node - Node from CSSTree AST to stringify
* @returns The stringified node
*/
function stringifyNode(node: CssNode) {
function stringifyNode(node: CssNode): string {
return stringifyNodePlain(node).trim()
}

function stringifyNodePlain(node: CssNode) {
function stringifyNodePlain(node: CssNode): string {
let loc = node.loc!
return css.substring(loc.start.offset, loc.end.offset)
}
Expand All @@ -86,7 +86,7 @@ function analyzeInternal<T extends boolean>(css: string, options: Options, useLo
{
size: number
count: number
uniqueWithLocations?: { offset: number; line: number; column: number; length: number }[]
uniqueWithLocations?: Location[]
}
>,
}
Expand Down Expand Up @@ -337,40 +337,41 @@ function analyzeInternal<T extends boolean>(css: string, options: Options, useLo
}
case Selector: {
let selector = stringifyNode(node)
let loc = node.loc!

if (this.atrule && endsWith('keyframes', this.atrule.name)) {
keyframeSelectors.p(selector, node.loc!)
keyframeSelectors.p(selector, loc)
return this.skip
}

if (isAccessibility(node)) {
a11y.p(selector, node.loc!)
a11y.p(selector, loc)
}

let pseudos = hasPseudoClass(node)
if (pseudos !== false) {
for (let pseudo of pseudos) {
pseudoClasses.p(pseudo, node.loc!)
pseudoClasses.p(pseudo, loc)
}
}

let complexity = getComplexity(node)

if (isPrefixed(node)) {
prefixedSelectors.p(selector, node.loc!)
prefixedSelectors.p(selector, loc)
}

uniqueSelectors.add(selector)
selectorComplexities.push(complexity)
uniqueSelectorComplexities.p(complexity, node.loc!)
uniqueSelectorComplexities.p(complexity, loc)
selectorNesting.push(nestingDepth - 1)
uniqueSelectorNesting.p(nestingDepth - 1, node.loc!)
uniqueSelectorNesting.p(nestingDepth - 1, loc)

// #region specificity
let specificity = calculateForAST(node).toArray()
let specificity: Specificity = calculateForAST(node).toArray()
let [sa, sb, sc] = specificity

uniqueSpecificities.p(specificity.toString(), node.loc!)
uniqueSpecificities.p(specificity.toString(), loc)

specificityA.push(sa)
specificityB.push(sb)
Expand All @@ -396,7 +397,7 @@ function analyzeInternal<T extends boolean>(css: string, options: Options, useLo
// #endregion

if (sa > 0) {
ids.p(selector, node.loc!)
ids.p(selector, loc)
}

getCombinators(node, function onCombinator(combinator) {
Expand Down Expand Up @@ -1032,3 +1033,5 @@ export { keywords as cssKeywords } from './values/values.js'
export { hasVendorPrefix } from './vendor-prefix.js'

export { KeywordSet } from './keyword-set.js'

export type { Location, UniqueWithLocations } from './collection.js'
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
// Strictness
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitAny": true,

// Type checking, not transpiling
"module": "ESNext",
Expand Down