Skip to content
Open
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
27 changes: 27 additions & 0 deletions src/v2/analyzers/atrule-all.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Counts every at-rule by its normalized name (vendor prefix stripped).
// Produces the combined count that v9 tracks as `atrules.c()`.

import { NODE_TYPES, is_atrule, type AnyNode } from '@projectwallace/css-parser'
import { basename } from '../../properties/property-utils.js'
import { CountCollection, type CountResult, type CountResultWithLocations } from '../internals/count-collection.js'
import type { AnalyzerInstance } from '../core.js'

export type AtruleAllOptions = { locations?: boolean }

export function atruleAll(
options: AtruleAllOptions = {},
): AnalyzerInstance<CountResult | CountResultWithLocations> {
const collection = new CountCollection(options.locations === true)

return {
subscribes: [NODE_TYPES.AT_RULE],
visit(node: AnyNode): void {
if (!is_atrule(node)) return
const name = basename(node.name ?? '')
collection.add(name, node.line, node.column, node.start, node.length)
},
collect() {
return collection.collect()
},
}
}
26 changes: 26 additions & 0 deletions src/v2/analyzers/atrule-charsets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { NODE_TYPES, is_atrule, type AnyNode, type Atrule } from '@projectwallace/css-parser'
import { CountCollection, type CountResult, type CountResultWithLocations } from '../internals/count-collection.js'
import type { AnalyzerInstance } from '../core.js'

export type AtruleCharsetsOptions = { locations?: boolean }

export function atruleCharsets(
options: AtruleCharsetsOptions = {},
): AnalyzerInstance<CountResult | CountResultWithLocations> {
const collection = new CountCollection(options.locations === true)

return {
subscribes: [NODE_TYPES.AT_RULE],
visit(node: AnyNode): void {
if (!is_atrule(node)) return
if ((node.name?.toLowerCase() ?? '') !== 'charset') return
const n = node as Atrule
if (n.has_prelude) {
collection.add(n.prelude.text.toLowerCase(), n.line, n.column, n.start, n.length)
}
},
collect() {
return collection.collect()
},
}
}
47 changes: 47 additions & 0 deletions src/v2/analyzers/atrule-containers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {
NODE_TYPES,
is_atrule,
is_atrule_prelude,
is_container_query,
is_identifier,
type AnyNode,
type Atrule,
} from '@projectwallace/css-parser'
import { CountCollection, type CountResult, type CountResultWithLocations } from '../internals/count-collection.js'
import type { AnalyzerInstance } from '../core.js'

export type AtruleContainersOptions = { locations?: boolean }

export type AtruleContainersResult = {
queries: CountResult | CountResultWithLocations
names: CountResult | CountResultWithLocations
}

export function atruleContainers(
options: AtruleContainersOptions = {},
): AnalyzerInstance<AtruleContainersResult> {
const withLocations = options.locations === true
const queries = new CountCollection(withLocations)
const names = new CountCollection(withLocations)

return {
subscribes: [NODE_TYPES.AT_RULE],
visit(node: AnyNode): void {
if (!is_atrule(node)) return
if ((node.name?.toLowerCase() ?? '') !== 'container') return
const n = node as Atrule
if (!n.has_prelude) return
queries.add(n.prelude.text, n.line, n.column, n.start, n.length)

if (is_atrule_prelude(n.prelude) && is_container_query(n.prelude.first_child)) {
const cq = n.prelude.first_child
if (cq.first_child && is_identifier(cq.first_child)) {
names.add(cq.first_child.text, n.line, n.column, n.start, n.length)
}
}
},
collect(): AtruleContainersResult {
return { queries: queries.collect(), names: names.collect() }
},
}
}
55 changes: 55 additions & 0 deletions src/v2/analyzers/atrule-fontfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import {
NODE_TYPES,
is_atrule,
is_declaration,
type AnyNode,
type Atrule,
} from '@projectwallace/css-parser'
import type { AnalyzerInstance } from '../core.js'
import type { Location } from '../internals/location-store.js'

export type FontFaceDescriptors = Record<string, string>

export type AtruleFontFacesResult = {
total: number
unique: FontFaceDescriptors[]
uniqueWithLocations?: Array<{ descriptors: FontFaceDescriptors; location: Location }>
}

export type AtruleFontFacesOptions = { locations?: boolean }

export function atruleFontFaces(
options: AtruleFontFacesOptions = {},
): AnalyzerInstance<AtruleFontFacesResult> {
const withLocations = options.locations === true
const items: Array<{ descriptors: FontFaceDescriptors; location: Location }> = []

return {
subscribes: [NODE_TYPES.AT_RULE],
visit(node: AnyNode): void {
if (!is_atrule(node)) return
if ((node.name?.toLowerCase() ?? '') !== 'font-face') return
const n = node as Atrule
const descriptors: FontFaceDescriptors = Object.create(null)
for (const child of n.block?.children ?? []) {
if (is_declaration(child) && child.value) {
descriptors[child.property] = child.value.text
}
}
items.push({
descriptors,
location: { line: n.line, column: n.column, offset: n.start, length: n.length },
})
},
collect(): AtruleFontFacesResult {
const base: AtruleFontFacesResult = {
total: items.length,
unique: items.map((i) => i.descriptors),
}
if (withLocations) {
base.uniqueWithLocations = items
}
return base
},
}
}
26 changes: 26 additions & 0 deletions src/v2/analyzers/atrule-imports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { NODE_TYPES, is_atrule, type AnyNode, type Atrule } from '@projectwallace/css-parser'
import { CountCollection, type CountResult, type CountResultWithLocations } from '../internals/count-collection.js'
import type { AnalyzerInstance } from '../core.js'

export type AtruleImportsOptions = { locations?: boolean }

export function atruleImports(
options: AtruleImportsOptions = {},
): AnalyzerInstance<CountResult | CountResultWithLocations> {
const collection = new CountCollection(options.locations === true)

return {
subscribes: [NODE_TYPES.AT_RULE],
visit(node: AnyNode): void {
if (!is_atrule(node)) return
if ((node.name?.toLowerCase() ?? '') !== 'import') return
const n = node as Atrule
if (n.has_prelude) {
collection.add(n.prelude.text, n.line, n.column, n.start, n.length)
}
},
collect() {
return collection.collect()
},
}
}
48 changes: 48 additions & 0 deletions src/v2/analyzers/atrule-keyframes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { NODE_TYPES, is_atrule, type AnyNode, type Atrule } from '@projectwallace/css-parser'
import { CountCollection, type CountResult, type CountResultWithLocations } from '../internals/count-collection.js'
import type { AnalyzerInstance } from '../core.js'

export type AtruleKeyframesResult = {
total: number
totalUnique: number
uniquenessRatio: number
unique: Record<string, number>
prefixed: CountResult | CountResultWithLocations
prefixedRatio: number
uniqueWithLocations?: Record<string, import('../internals/location-store.js').Location[]>
}

export type AtruleKeyframesOptions = { locations?: boolean }

export function atruleKeyframes(
options: AtruleKeyframesOptions = {},
): AnalyzerInstance<AtruleKeyframesResult> {
const withLocations = options.locations === true
const all = new CountCollection(withLocations)
const prefixed = new CountCollection(withLocations)

return {
subscribes: [NODE_TYPES.AT_RULE],
visit(node: AnyNode): void {
if (!is_atrule(node)) return
const name = node.name?.toLowerCase() ?? ''
if (!name.endsWith('keyframes')) return
const n = node as Atrule
if (!n.has_prelude) return
const prelude = n.prelude.text
all.add(prelude, n.line, n.column, n.start, n.length)
if (n.is_vendor_prefixed) {
prefixed.add(`@${name} ${prelude}`, n.line, n.column, n.start, n.length)
}
},
collect(): AtruleKeyframesResult {
const allResult = all.collect()
const prefixedResult = prefixed.collect()
return {
...allResult,
prefixed: prefixedResult,
prefixedRatio: allResult.total === 0 ? 0 : prefixedResult.total / allResult.total,
}
},
}
}
30 changes: 30 additions & 0 deletions src/v2/analyzers/atrule-layers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { NODE_TYPES, is_atrule, type AnyNode, type Atrule } from '@projectwallace/css-parser'
import { CountCollection, type CountResult, type CountResultWithLocations } from '../internals/count-collection.js'
import type { AnalyzerInstance } from '../core.js'

export type AtruleLayersOptions = { locations?: boolean }

export function atruleLayers(
options: AtruleLayersOptions = {},
): AnalyzerInstance<CountResult | CountResultWithLocations> {
const collection = new CountCollection(options.locations === true)

return {
subscribes: [NODE_TYPES.AT_RULE],
visit(node: AnyNode): void {
if (!is_atrule(node)) return
if ((node.name?.toLowerCase() ?? '') !== 'layer') return
const n = node as Atrule
if (!n.has_prelude) {

Check failure on line 18 in src/v2/analyzers/atrule-layers.ts

View workflow job for this annotation

GitHub Actions / Lint JS

unicorn(no-negated-condition)

Unexpected negated condition.

Check failure on line 18 in src/v2/analyzers/atrule-layers.ts

View workflow job for this annotation

GitHub Actions / Lint JS

eslint(no-negated-condition)

Unexpected negated condition.
collection.add('<anonymous>', n.line, n.column, n.start, n.length)
} else {
for (const layer of n.prelude.text.split(',').map((s: string) => s.trim())) {
collection.add(layer, n.line, n.column, n.start, n.length)
}
}
},
collect() {
return collection.collect()
},
}
}
39 changes: 39 additions & 0 deletions src/v2/analyzers/atrule-media.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Full @media analyzer: query strings + browser hacks.
// For individual feature counts use the separate uniqueMediaFeatures analyzer.

import { NODE_TYPES, is_atrule, type AnyNode, type Atrule } from '@projectwallace/css-parser'
import { isMediaBrowserhack } from '../../atrules/atrules.js'
import { CountCollection, type CountResult, type CountResultWithLocations } from '../internals/count-collection.js'
import type { AnalyzerInstance } from '../core.js'

export type AtruleMediaOptions = { locations?: boolean }

export type AtruleMediaResult = {
queries: CountResult | CountResultWithLocations
browserhacks: CountResult | CountResultWithLocations
}

export function atruleMedia(
options: AtruleMediaOptions = {},
): AnalyzerInstance<AtruleMediaResult> {
const withLocations = options.locations === true
const queries = new CountCollection(withLocations)
const hacks = new CountCollection(withLocations)

return {
subscribes: [NODE_TYPES.AT_RULE],
visit(node: AnyNode): void {
if (!is_atrule(node)) return
if ((node.name?.toLowerCase() ?? '') !== 'media') return
const n = node as Atrule
if (!n.has_prelude) return
queries.add(n.prelude.text, n.line, n.column, n.start, n.length)
isMediaBrowserhack(n.prelude, (hack) => {
hacks.add(hack, n.line, n.column, n.start, n.length)
})
},
collect(): AtruleMediaResult {
return { queries: queries.collect(), browserhacks: hacks.collect() }
},
}
}
62 changes: 62 additions & 0 deletions src/v2/analyzers/atrule-misc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Miscellaneous at-rules: @property (registered custom properties), @function, @scope.
// Also tracks overall atrule complexity and nesting depth.

import { NODE_TYPES, is_atrule, type AnyNode, type Atrule } from '@projectwallace/css-parser'
import { CountCollection, type CountResult, type CountResultWithLocations } from '../internals/count-collection.js'
import { AggregateCollection, type AggregateResult } from '../internals/aggregate-collection.js'
import type { AnalyzerInstance, WalkContext } from '../core.js'

export type AtruleMiscOptions = { locations?: boolean }

export type AtruleMiscResult = {
registeredProperties: CountResult | CountResultWithLocations
functions: CountResult | CountResultWithLocations
scopes: CountResult | CountResultWithLocations
complexity: AggregateResult
nesting: AggregateResult
}

export function atruleMisc(options: AtruleMiscOptions = {}): AnalyzerInstance<AtruleMiscResult> {
const withLocations = options.locations === true
const registeredProperties = new CountCollection(withLocations)
const functions = new CountCollection(withLocations)
const scopes = new CountCollection(withLocations)
const complexity = new AggregateCollection()
const nesting = new AggregateCollection()

return {
subscribes: [NODE_TYPES.AT_RULE],
visit(node: AnyNode, ctx: WalkContext): void {
if (!is_atrule(node)) return
const n = node as Atrule
const name = n.name?.toLowerCase() ?? ''
nesting.push(ctx.depth)

if (!n.has_prelude) {
complexity.push(name === 'layer' ? 2 : 1)
return
}

let c = 1
if (name === 'property') {
registeredProperties.add(n.prelude.text, n.line, n.column, n.start, n.length)
} else if (name === 'function') {
const prelude = n.prelude.text
const fname = prelude.includes('(') ? prelude.slice(0, prelude.indexOf('(')).trim() : prelude.trim()
functions.add(fname, n.line, n.column, n.start, n.length)
} else if (name === 'scope') {
scopes.add(n.prelude.text, n.line, n.column, n.start, n.length)
}
complexity.push(c)
},
collect(): AtruleMiscResult {
return {
registeredProperties: registeredProperties.collect(),
functions: functions.collect(),
scopes: scopes.collect(),
complexity: complexity.collect(),
nesting: nesting.collect(),
}
},
}
}
Loading
Loading