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
1 change: 0 additions & 1 deletion knip.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { KnipConfig } from 'knip'

const config: KnipConfig = {
project: ['src/**/*.ts'],
ignoreDependencies: ['@projectwallace/preset-oxlint'],
}

export default config
49 changes: 49 additions & 0 deletions src/defined-used.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,55 @@ describe('Animation names / keyframes', () => {
expect(result.used).toEqual([])
})

test('tracks quoted animation-name references as used', () => {
let css = `.foo { animation-name: "slide in"; }`
let result = analyze(css).atrules.keyframes
expect(result.used).toEqual(['slide in'])
})

test('tracks quoted animation shorthand names as used', () => {
let css = `.foo { animation: "slide in" 1s ease; }`
let result = analyze(css).atrules.keyframes
expect(result.used).toEqual(['slide in'])
})

test('matches a quoted @keyframes definition with a quoted usage', () => {
let css = `
@keyframes "slide in" { from { opacity: 0; } to { opacity: 1; } }
.foo { animation-name: "slide in"; }
`
let result = analyze(css).atrules.keyframes
expect(result.defined).toEqual(['slide in'])
expect(result.used).toEqual(['slide in'])
expect(result.unused).toEqual([])
expect(result.unknown).toEqual([])
})

test('matches a quoted @keyframes definition with an unquoted usage and vice versa', () => {
let css = `
@keyframes spin { from { opacity: 0; } to { opacity: 1; } }
@keyframes "fade" { from { opacity: 0; } to { opacity: 1; } }
.foo {
animation-name: "spin";
animation: fade 1s;
}
`
let result = analyze(css).atrules.keyframes
expect(result.defined).toEqual(['spin', 'fade'])
expect(result.used).toEqual(['spin', 'fade'])
expect(result.unused).toEqual([])
expect(result.unknown).toEqual([])
})

test('quoted reserved keywords are tracked as used animation names, unlike their bare forms', () => {
// `none` on its own means "no animation", but a quoted `"none"` is a
// valid (if unusual) custom animation name per the CSS Animations spec.
let css = `.foo { animation-name: "none", "infinite", none; }`
let result = analyze(css).atrules.keyframes
expect(result.used).toEqual(['none', 'infinite'])
expect(result.unknown).toEqual(['none', 'infinite'])
})

test('handles vendor-prefixed @keyframes as defined', () => {
let css = `@-webkit-keyframes spin {}`
let result = analyze(css).atrules.keyframes
Expand Down
12 changes: 9 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
is_hash,
is_function,
is_operator,
is_string,
} from '@projectwallace/css-parser'
import { isSupportsBrowserhack, isMediaBrowserhack } from './atrules/atrules.js'
import { getCombinators, getComplexity, isPrefixed, isAccessibility } from './selectors/utils.js'
Expand Down Expand Up @@ -287,7 +288,7 @@ function analyzeInternal<T extends boolean>(css: string, options: Options, useLo
complexity++
})
} else if (normalized_name.endsWith('keyframes')) {
let prelude = node.prelude.text
let prelude = unquote(node.prelude.text)
keyframes.p(prelude, toLoc(node))
animationNamesTracking.define(prelude)

Expand Down Expand Up @@ -671,8 +672,9 @@ function analyzeInternal<T extends boolean>(css: string, options: Options, useLo
} else if (item.type === 'keyword') {
valueKeywords.p(item.value.text.toLowerCase(), valueLoc)
} else if (item.type === 'name' && isAnimation) {
animationNames.p(item.value.text, valueLoc)
animationNamesTracking.use(item.value.text)
let name = unquote(item.value.text)
animationNames.p(name, valueLoc)
animationNamesTracking.use(name)
}
})
return SKIP
Expand Down Expand Up @@ -704,6 +706,10 @@ function analyzeInternal<T extends boolean>(css: string, options: Options, useLo
if (is_identifier(child) && !keywords.has(child.name)) {
animationNames.p(child.text, valueLoc)
animationNamesTracking.use(child.text)
} else if (is_string(child)) {
let name = unquote(child.text)
animationNames.p(name, valueLoc)
animationNamesTracking.use(name)
}
}
} else if (normalizedProperty === 'container-name') {
Expand Down
27 changes: 27 additions & 0 deletions src/values/animations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,3 +333,30 @@ test('emits name for dashed-ident animation names', () => {
})
expect(names).toEqual(['--my-name'])
})

test('emits name for quoted (string) animation names', () => {
const names: string[] = []
analyzeAnimation(parse_value('"slide in" 300ms ease-out'), (item) => {
if (item.type === 'name') names.push(item.value.text)
})
expect(names).toEqual(['"slide in"'])
})

test('emits name for each quoted animation in a value list', () => {
const names: string[] = []
analyzeAnimation(parse_value('"slide in" 1s, "fade out" 2s'), (item) => {
if (item.type === 'name') names.push(item.value.text)
})
expect(names).toEqual(['"slide in"', '"fade out"'])
})

test('emits name for a quoted keyword used as animation name', () => {
// Unlike the bare `none` keyword, a quoted "none" is a valid animation name
const names: string[] = []
for (const kw of ['none', 'infinite', 'paused', 'alternate', 'linear']) {
analyzeAnimation(parse_value(`"${kw}" 1s ease-in`), (item) => {
if (item.type === 'name') names.push(item.value.text)
})
}
expect(names).toEqual(['"none"', '"infinite"', '"paused"', '"alternate"', '"linear"'])
})
7 changes: 7 additions & 0 deletions src/values/animations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
is_operator,
is_dimension,
is_function,
is_string,
} from '@projectwallace/css-parser'
import { KeywordSet } from '../keyword-set.js'
import { keywords } from './values.js'
Expand Down Expand Up @@ -78,6 +79,12 @@ export function analyzeAnimation(
type: 'fn',
value: node,
})
} else if (is_string(node)) {
// A quoted string is always a valid animation name, even when it's a reserved keyword (e.g. "none" or "infinite")
cb({
type: 'name',
value: node,
})
}
}
}
Loading