blob: cfe0dda622188fdae02420718f90ed083db78d02 [file] [log] [blame]
Jack Franklina5fd0a42022-12-09 11:30:24 +00001'use strict';
2const valueParser = require('postcss-value-parser');
3
4/** @type {(node: valueParser.Node) => number} */
5const getValue = (node) => parseFloat(node.value);
6
7/* Works because toString() normalizes the formatting,
8 so comparing the string forms behaves the same as number equality*/
9const conversions = new Map([
10 [[0.25, 0.1, 0.25, 1].toString(), 'ease'],
11 [[0, 0, 1, 1].toString(), 'linear'],
12 [[0.42, 0, 1, 1].toString(), 'ease-in'],
13 [[0, 0, 0.58, 1].toString(), 'ease-out'],
14 [[0.42, 0, 0.58, 1].toString(), 'ease-in-out'],
15]);
16/**
17 * @param {valueParser.Node} node
18 * @return {void | false}
19 */
20function reduce(node) {
21 if (node.type !== 'function') {
22 return false;
23 }
24
25 if (!node.value) {
26 return;
27 }
28
29 const lowerCasedValue = node.value.toLowerCase();
30
31 if (lowerCasedValue === 'steps') {
32 // Don't bother checking the step-end case as it has the same length
33 // as steps(1)
34 if (
35 node.nodes[0].type === 'word' &&
36 getValue(node.nodes[0]) === 1 &&
37 node.nodes[2] &&
38 node.nodes[2].type === 'word' &&
39 (node.nodes[2].value.toLowerCase() === 'start' ||
40 node.nodes[2].value.toLowerCase() === 'jump-start')
41 ) {
42 /** @type string */ (node.type) = 'word';
43 node.value = 'step-start';
44
45 delete (/** @type Partial<valueParser.FunctionNode> */ (node).nodes);
46
47 return;
48 }
49
50 if (
51 node.nodes[0].type === 'word' &&
52 getValue(node.nodes[0]) === 1 &&
53 node.nodes[2] &&
54 node.nodes[2].type === 'word' &&
55 (node.nodes[2].value.toLowerCase() === 'end' ||
56 node.nodes[2].value.toLowerCase() === 'jump-end')
57 ) {
58 /** @type string */ (node.type) = 'word';
59 node.value = 'step-end';
60
61 delete (/** @type Partial<valueParser.FunctionNode> */ (node).nodes);
62
63 return;
64 }
65
66 // The end case is actually the browser default, so it isn't required.
67 if (
68 node.nodes[2] &&
69 node.nodes[2].type === 'word' &&
70 (node.nodes[2].value.toLowerCase() === 'end' ||
71 node.nodes[2].value.toLowerCase() === 'jump-end')
72 ) {
73 node.nodes = [node.nodes[0]];
74
75 return;
76 }
77
78 return false;
79 }
80
81 if (lowerCasedValue === 'cubic-bezier') {
82 const values = node.nodes
83 .filter((list, index) => {
84 return index % 2 === 0;
85 })
86 .map(getValue);
87
88 if (values.length !== 4) {
89 return;
90 }
91
92 const match = conversions.get(values.toString());
93
94 if (match) {
95 /** @type string */ (node.type) = 'word';
96 node.value = match;
97
98 delete (/** @type Partial<valueParser.FunctionNode> */ (node).nodes);
99
100 return;
101 }
102 }
103}
104
105/**
106 * @param {string} value
107 * @return {string}
108 */
109function transform(value) {
110 return valueParser(value).walk(reduce).toString();
111}
112
113/**
114 * @type {import('postcss').PluginCreator<void>}
115 * @return {import('postcss').Plugin}
116 */
117function pluginCreator() {
118 return {
119 postcssPlugin: 'postcss-normalize-timing-functions',
120
121 OnceExit(css) {
122 const cache = new Map();
123
124 css.walkDecls(
125 /^(-\w+-)?(animation|transition)(-timing-function)?$/i,
126 (decl) => {
127 const value = decl.value;
128
129 if (cache.has(value)) {
130 decl.value = cache.get(value);
131
132 return;
133 }
134
135 const result = transform(value);
136
137 decl.value = result;
138 cache.set(value, result);
139 }
140 );
141 },
142 };
143}
144
145pluginCreator.postcss = true;
146module.exports = pluginCreator;