Skip to content

Commit 36cb587

Browse files
authored
feat: add analysis for spacing resets (#467)
Prior art: csssstats.com This analysis checks for: - scientific notation (`0e0`) - signed numbers (`+0`, `-0`) - floats (`0.0`) - with or without units (`0`, `0em`) - vendor prefixed property names (`-webkit-padding-inline`) - all of the values in a shorthand being zero (`0 0 0 0`, `0px 0 0em`) - combinations of the above (`-moz-margin: 0 0px +0.0e0em`) Implementation sparked by @ohhelloana
1 parent 704d7ac commit 36cb587

File tree

4 files changed

+255
-16
lines changed

4 files changed

+255
-16
lines changed

src/index.js

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { isSupportsBrowserhack, isMediaBrowserhack } from './atrules/atrules.js'
55
import { getCombinators, getComplexity, isAccessibility, isPrefixed, hasPseudoClass } from './selectors/utils.js'
66
import { colorFunctions, colorKeywords, namedColors, systemColors } from './values/colors.js'
77
import { destructure, isSystemFont } from './values/destructure-font-shorthand.js'
8-
import { isValueKeyword, keywords } from './values/values.js'
8+
import { isValueKeyword, keywords, isValueReset } from './values/values.js'
99
import { analyzeAnimation } from './values/animations.js'
1010
import { isValuePrefixed } from './values/vendor-prefix.js'
1111
import { ContextCollection } from './context-collection.js'
@@ -193,6 +193,7 @@ export function analyze(css, options = {}) {
193193
let gradients = new Collection(useLocations)
194194
let valueKeywords = new Collection(useLocations)
195195
let borderRadiuses = new ContextCollection(useLocations)
196+
let resets = new Collection(useLocations)
196197

197198
walk(ast, function (node) {
198199
switch (node.type) {
@@ -452,6 +453,7 @@ export function analyze(css, options = {}) {
452453
break
453454
}
454455

456+
/** @type {import('css-tree').Declaration} */
455457
let declaration = this.declaration
456458
let { property, important } = declaration
457459
let complexity = 1
@@ -482,10 +484,31 @@ export function analyze(css, options = {}) {
482484

483485
// Process properties first that don't have colors,
484486
// so we can avoid further walking them;
485-
if (isProperty('z-index', property)) {
487+
if (
488+
isProperty('margin', property) ||
489+
isProperty('margin-block', property) ||
490+
isProperty('margin-inline', property) ||
491+
isProperty('margin-top', property) ||
492+
isProperty('margin-right', property) ||
493+
isProperty('margin-bottom', property) ||
494+
isProperty('margin-left', property) ||
495+
isProperty('padding', property) ||
496+
isProperty('padding-block', property) ||
497+
isProperty('padding-inline', property) ||
498+
isProperty('padding-top', property) ||
499+
isProperty('padding-right', property) ||
500+
isProperty('padding-bottom', property) ||
501+
isProperty('padding-left', property)
502+
) {
503+
if (isValueReset(node)) {
504+
resets.p(property, declaration.loc)
505+
}
506+
}
507+
else if (isProperty('z-index', property)) {
486508
zindex.p(stringifyNode(node), loc)
487509
return this.skip
488-
} else if (isProperty('font', property)) {
510+
}
511+
else if (isProperty('font', property)) {
489512
if (isSystemFont(node)) return
490513

491514
let { font_size, line_height, font_family } = destructure(node, stringifyNode, function (item) {
@@ -507,48 +530,58 @@ export function analyze(css, options = {}) {
507530
}
508531

509532
break
510-
} else if (isProperty('font-size', property)) {
533+
}
534+
else if (isProperty('font-size', property)) {
511535
if (!isSystemFont(node)) {
512536
fontSizes.p(stringifyNode(node), loc)
513537
}
514538
break
515-
} else if (isProperty('font-family', property)) {
539+
}
540+
else if (isProperty('font-family', property)) {
516541
if (!isSystemFont(node)) {
517542
fontFamilies.p(stringifyNode(node), loc)
518543
}
519544
break
520-
} else if (isProperty('line-height', property)) {
545+
}
546+
else if (isProperty('line-height', property)) {
521547
lineHeights.p(stringifyNode(node), loc)
522-
} else if (isProperty('transition', property) || isProperty('animation', property)) {
548+
}
549+
else if (isProperty('transition', property) || isProperty('animation', property)) {
523550
analyzeAnimation(children, function (item) {
524551
if (item.type === 'fn') {
525552
timingFunctions.p(stringifyNode(item.value), loc)
526-
} else if (item.type === 'duration') {
553+
}
554+
else if (item.type === 'duration') {
527555
durations.p(stringifyNode(item.value), loc)
528-
} else if (item.type === 'keyword') {
556+
}
557+
else if (item.type === 'keyword') {
529558
valueKeywords.p(stringifyNode(item.value), loc)
530559
}
531560
})
532561
break
533-
} else if (isProperty('animation-duration', property) || isProperty('transition-duration', property)) {
562+
}
563+
else if (isProperty('animation-duration', property) || isProperty('transition-duration', property)) {
534564
if (children && children.size > 1) {
535565
children.forEach(child => {
536566
if (child.type !== Operator) {
537567
durations.p(stringifyNode(child), loc)
538568
}
539569
})
540-
} else {
570+
}
571+
else {
541572
durations.p(stringifyNode(node), loc)
542573
}
543574
break
544-
} else if (isProperty('transition-timing-function', property) || isProperty('animation-timing-function', property)) {
575+
}
576+
else if (isProperty('transition-timing-function', property) || isProperty('animation-timing-function', property)) {
545577
if (children && children.size > 1) {
546578
children.forEach(child => {
547579
if (child.type !== Operator) {
548580
timingFunctions.p(stringifyNode(child), loc)
549581
}
550582
})
551-
} else {
583+
}
584+
else {
552585
timingFunctions.p(stringifyNode(node), loc)
553586
}
554587
break
@@ -568,12 +601,14 @@ export function analyze(css, options = {}) {
568601
borderRadiuses.push(stringifyNode(node), property, loc)
569602
}
570603
break
571-
} else if (isProperty('text-shadow', property)) {
604+
}
605+
else if (isProperty('text-shadow', property)) {
572606
if (!isValueKeyword(node)) {
573607
textShadows.p(stringifyNode(node), loc)
574608
}
575609
// no break here: potentially contains colors
576-
} else if (isProperty('box-shadow', property)) {
610+
}
611+
else if (isProperty('box-shadow', property)) {
577612
if (!isValueKeyword(node)) {
578613
boxShadows.p(stringifyNode(node), loc)
579614
}
@@ -936,6 +971,7 @@ export function analyze(css, options = {}) {
936971
units: units.count(),
937972
complexity: valueComplexity,
938973
keywords: valueKeywords.c(),
974+
resets: resets.c(),
939975
},
940976
__meta__: {
941977
parseTime: startAnalysis - startParse,

src/index.test.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,12 @@ Api("handles empty input gracefully", () => {
470470
unique: {},
471471
uniquenessRatio: 0,
472472
},
473+
resets: {
474+
total: 0,
475+
totalUnique: 0,
476+
unique: {},
477+
uniquenessRatio: 0,
478+
},
473479
},
474480
}
475481

src/values/resets.test.js

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { analyze } from "../index.js"
2+
import { suite } from 'uvu'
3+
import * as assert from 'uvu/assert'
4+
5+
const test = suite("resets")
6+
7+
test('does not report false positives', () => {
8+
let actual = analyze(`t {
9+
margin: 0px 10px;
10+
margin: 10px 10px;
11+
margin: auto 10px;
12+
margin: 10px auto;
13+
}`)
14+
let resets = actual.values.resets
15+
assert.is(resets.total, 0)
16+
assert.equal(resets.unique, {})
17+
})
18+
19+
test('accepts zeroes with units', () => {
20+
let actual = analyze(`t {
21+
margin: 0px 0em 0pc 0vw;
22+
padding: 0px 0dvh 0rem 0in;
23+
}`)
24+
let resets = actual.values.resets
25+
assert.equal(resets.unique, { 'margin': 1, 'padding': 1 })
26+
})
27+
28+
test('crazy notations', () => {
29+
let actual = analyze(`t {
30+
margin: 0;
31+
margin: -0;
32+
margin: +0;
33+
margin: 0px;
34+
margin: -0px;
35+
margin: +0px;
36+
margin: 0.0;
37+
margin: -0.0;
38+
margin: +0.0;
39+
margin: 0.0e0;
40+
margin: -0.0e0;
41+
margin: +0.0e0;
42+
}`)
43+
let resets = actual.values.resets
44+
assert.equal(resets.unique, { 'margin': 12 })
45+
})
46+
47+
test('accepts weird casing', () => {
48+
let actual = analyze(`t {
49+
MARGIN: 0;
50+
}`)
51+
let resets = actual.values.resets
52+
assert.equal(resets.unique, { 'MARGIN': 1 })
53+
})
54+
55+
test('accepts vendor prefixes', () => {
56+
let actual = analyze(`t {
57+
-webkit-margin: 0;
58+
}`)
59+
let resets = actual.values.resets
60+
assert.equal(resets.unique, { '-webkit-margin': 1 })
61+
})
62+
63+
// Test all properties
64+
65+
test('margin: 0', () => {
66+
let actual = analyze(`t { margin: 0; }`)
67+
let resets = actual.values.resets
68+
assert.equal(resets.unique, { 'margin': 1 })
69+
})
70+
71+
test('margin-top: 0', () => {
72+
let actual = analyze(`t { margin-top: 0; }`)
73+
let resets = actual.values.resets
74+
assert.equal(resets.unique, { 'margin-top': 1 })
75+
})
76+
77+
test('margin-right: 0', () => {
78+
let actual = analyze(`t { margin-right: 0; }`)
79+
let resets = actual.values.resets
80+
assert.equal(resets.unique, { 'margin-right': 1 })
81+
})
82+
83+
test('margin-bottom: 0', () => {
84+
let actual = analyze(`t { margin-bottom: 0; }`)
85+
let resets = actual.values.resets
86+
assert.equal(resets.unique, { 'margin-bottom': 1 })
87+
})
88+
89+
test('margin-left: 0', () => {
90+
let actual = analyze(`t { margin-left: 0; }`)
91+
let resets = actual.values.resets
92+
assert.equal(resets.unique, { 'margin-left': 1 })
93+
})
94+
95+
test('margin-inline: 0', () => {
96+
let actual = analyze(`t { margin-inline: 0; }`)
97+
let resets = actual.values.resets
98+
assert.equal(resets.unique, { 'margin-inline': 1 })
99+
})
100+
101+
test('margin-block: 0', () => {
102+
let actual = analyze(`t { margin-block: 0; }`)
103+
let resets = actual.values.resets
104+
assert.equal(resets.unique, { 'margin-block': 1 })
105+
})
106+
107+
test('padding: 0', () => {
108+
let actual = analyze(`t { padding: 0; }`)
109+
let resets = actual.values.resets
110+
assert.equal(resets.unique, { 'padding': 1 })
111+
})
112+
113+
test('padding-top: 0', () => {
114+
let actual = analyze(`t { padding-top: 0; }`)
115+
let resets = actual.values.resets
116+
assert.equal(resets.unique, { 'padding-top': 1 })
117+
})
118+
119+
test('padding-right: 0', () => {
120+
let actual = analyze(`t { padding-right: 0; }`)
121+
let resets = actual.values.resets
122+
assert.equal(resets.unique, { 'padding-right': 1 })
123+
})
124+
125+
test('padding-bottom: 0', () => {
126+
let actual = analyze(`t { padding-bottom: 0; }`)
127+
let resets = actual.values.resets
128+
assert.equal(resets.unique, { 'padding-bottom': 1 })
129+
})
130+
131+
test('padding-left: 0', () => {
132+
let actual = analyze(`t { padding-left: 0; }`)
133+
let resets = actual.values.resets
134+
assert.equal(resets.unique, { 'padding-left': 1 })
135+
})
136+
137+
test('padding-inline: 0', () => {
138+
let actual = analyze(`t { padding-inline: 0; }`)
139+
let resets = actual.values.resets
140+
assert.equal(resets.unique, { 'padding-inline': 1 })
141+
})
142+
143+
test('padding-block: 0', () => {
144+
let actual = analyze(`t { padding-block: 0; }`)
145+
let resets = actual.values.resets
146+
assert.equal(resets.unique, { 'padding-block': 1 })
147+
})
148+
149+
// Shorthands
150+
151+
test('margin-inline: 0 0', () => {
152+
let actual = analyze(`t { margin-inline: 0 0; }`)
153+
let resets = actual.values.resets
154+
assert.equal(resets.unique, { 'margin-inline': 1 })
155+
})
156+
157+
test('padding-inline: 0 0', () => {
158+
let actual = analyze(`t { padding-inline: 0 0; }`)
159+
let resets = actual.values.resets
160+
assert.equal(resets.unique, { 'padding-inline': 1 })
161+
})
162+
163+
test('margin-block: 0 0', () => {
164+
let actual = analyze(`t { margin-block: 0 0; }`)
165+
let resets = actual.values.resets
166+
assert.equal(resets.unique, { 'margin-block': 1 })
167+
})
168+
169+
test('padding-block: 0 0', () => {
170+
let actual = analyze(`t { padding-block: 0 0; }`)
171+
let resets = actual.values.resets
172+
assert.equal(resets.unique, { 'padding-block': 1 })
173+
})
174+
175+
test.run()

src/values/values.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { KeywordSet } from "../keyword-set.js"
2-
import { Identifier } from "../css-tree-node-types.js"
2+
import { Identifier, Value, Nr, Dimension } from "../css-tree-node-types.js"
33

44
export const keywords = new KeywordSet([
55
'auto',
@@ -24,3 +24,25 @@ export function isValueKeyword(node) {
2424
let firstChild = children.first
2525
return firstChild.type === Identifier && keywords.has(firstChild.name)
2626
}
27+
28+
/**
29+
* @param {string} string
30+
* @returns {boolean}
31+
*/
32+
function isZero(string) {
33+
return parseFloat(string) === 0
34+
}
35+
36+
/**
37+
* Test whether a value is a reset (0, 0px, -0.0e0 etc.)
38+
* @param {import('css-tree').Value} node
39+
*/
40+
export function isValueReset(node) {
41+
for (let child of node.children.toArray()) {
42+
if (child.type === Nr && isZero(child.value)) continue
43+
if (child.type === Dimension && isZero(child.value)) continue
44+
return false
45+
}
46+
47+
return true
48+
}

0 commit comments

Comments
 (0)