postcss-normalize-timing-functions 9.0.2 → 9.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postcss-normalize-timing-functions",
3
- "version": "9.0.2",
3
+ "version": "9.0.4",
4
4
  "description": "Normalize CSS animation/transition timing functions.",
5
5
  "exports": {
6
6
  ".": {
@@ -18,7 +18,8 @@
18
18
  ],
19
19
  "license": "MIT",
20
20
  "dependencies": {
21
- "postcss-value-parser": "^4.2.0"
21
+ "@csstools/css-tokenizer": "^4.0.2",
22
+ "cssnano-utils": "^8.0.0"
22
23
  },
23
24
  "author": {
24
25
  "name": "Ben Briggs",
package/src/index.js CHANGED
@@ -1,9 +1,11 @@
1
- import valueParser from 'postcss-value-parser';
1
+ import cssnanoUtils from 'cssnano-utils';
2
2
 
3
- /** @type {(node: valueParser.Node) => number} */
4
- const getValue = (node) => Number.parseFloat(node.value);
3
+ const { TokenType, asciiLowerCase, decoded } = cssnanoUtils;
4
+ /** @import {CSSToken} from '@csstools/css-tokenizer' */
5
+ /** @type {(source: string) => {tokens: readonly CSSToken[], endForOpening(index: number): number | undefined, topLevelSegments(start: number, end: number): {startIndex: number, endIndex: number}[]} | undefined} */
6
+ const getBalancedTokens = cssnanoUtils.balancedTokens;
5
7
  const animationTransitionRegex =
6
- /^(-\w+-)?(animation|transition)(-timing-function)?$/i;
8
+ /^(?:-[A-Za-z0-9_]+-)?(?:[aA][nN][iI][mM][aA][tT][iI][oO][nN]|[tT][rR][aA][nN][sS][iI][tT][iI][oO][nN])(?:-[tT][iI][mM][iI][nN][gG]-[fF][uU][nN][cC][tT][iI][oO][nN])?$/v;
7
9
 
8
10
  /* Works because toString() normalizes the formatting,
9
11
  so comparing the string forms behaves the same as number equality*/
@@ -14,103 +16,62 @@ const conversions = new Map([
14
16
  [[0, 0, 0.58, 1].toString(), 'ease-out'],
15
17
  [[0.42, 0, 0.58, 1].toString(), 'ease-in-out'],
16
18
  ]);
17
- /**
18
- * @param {valueParser.Node} node
19
- * @return {void | false}
20
- */
21
- function reduce(node) {
22
- if (node.type !== 'function') {
23
- return false;
24
- }
25
-
26
- if (!node.value) {
27
- return;
28
- }
29
-
30
- const lowerCasedValue = node.value.toLowerCase();
31
-
32
- if (lowerCasedValue === 'steps') {
33
- return normalizeSteps(node);
34
- }
35
-
36
- if (lowerCasedValue === 'cubic-bezier') {
37
- return normalizeCubicBezier(node);
19
+ /** @param {readonly CSSToken[]} input @param {{startIndex: number, endIndex: number}} segment @return {CSSToken | undefined} */
20
+ function singleToken(input, segment) {
21
+ /** @type {CSSToken | undefined} */
22
+ let result;
23
+ for (let index = segment.startIndex; index < segment.endIndex; index++) {
24
+ const token = input[index];
25
+ if (token[0] === TokenType.Whitespace || token[0] === TokenType.Comment)
26
+ continue;
27
+ if (result) return;
28
+ result = token;
38
29
  }
30
+ return result;
39
31
  }
40
32
 
41
- /**
42
- * @param {valueParser.FunctionNode} node
43
- * @return {void | false}
44
- */
45
- function normalizeSteps(node) {
46
- const count = node.nodes[0];
47
- const position = node.nodes[2];
48
- const isSingleStep = count.type === 'word' && getValue(count) === 1;
49
-
50
- if (isSingleStep && isStepPosition(position, 'start', 'jump-start')) {
51
- /** @type string */ (node.type) = 'word';
52
- node.value = 'step-start';
53
-
54
- delete (/** @type Partial<valueParser.FunctionNode> */ (node).nodes);
55
-
56
- return;
57
- }
58
-
59
- if (isSingleStep && isStepPosition(position, 'end', 'jump-end')) {
60
- /** @type string */ (node.type) = 'word';
61
- node.value = 'step-end';
62
-
63
- delete (/** @type Partial<valueParser.FunctionNode> */ (node).nodes);
64
-
65
- return;
66
- }
67
-
68
- // The end case is actually the browser default, so it isn't required.
69
- if (isStepPosition(position, 'end', 'jump-end')) {
70
- node.nodes = [count];
71
-
72
- return;
73
- }
74
-
75
- return false;
33
+ /** @param {string} value @param {CSSToken} token @return {string} */
34
+ function tokenSource(value, token) {
35
+ return value.slice(token[2], token[3] + 1);
76
36
  }
77
37
 
78
- /**
79
- * @param {valueParser.Node | undefined} node
80
- * @param {string} first
81
- * @param {string} second
82
- * @return {boolean}
83
- */
84
- function isStepPosition(node, first, second) {
85
- return (
86
- node?.type === 'word' &&
87
- (node.value.toLowerCase() === first || node.value.toLowerCase() === second)
88
- );
89
- }
90
-
91
- /**
92
- * @param {valueParser.FunctionNode} node
93
- * @return {void}
94
- */
95
- function normalizeCubicBezier(node) {
96
- const values = node.nodes
97
- .filter((list, index) => {
98
- return index % 2 === 0;
99
- })
100
- .map(getValue);
101
-
102
- if (values.length !== 4) {
103
- return;
38
+ /** @param {string} value @param {readonly CSSToken[]} input @param {NonNullable<ReturnType<typeof getBalancedTokens>>} structure @param {number} index @param {number} end @return {string | null} */
39
+ function reduceFunction(value, input, structure, index, end) {
40
+ const name = asciiLowerCase(decoded(input[index]));
41
+ const segments = structure.topLevelSegments(index + 1, end);
42
+ if (name === 'cubic-bezier' && segments.length === 4) {
43
+ const values = segments.map((segment) => {
44
+ const token = singleToken(input, segment);
45
+ return token?.[0] === TokenType.Number
46
+ ? /** @type {{value: number}} */ (token[4]).value
47
+ : Number.NaN;
48
+ });
49
+ return values.every((number) => !Number.isNaN(number))
50
+ ? (conversions.get(values.toString()) ?? null)
51
+ : null;
104
52
  }
105
-
106
- const match = conversions.get(values.toString());
107
-
108
- if (match) {
109
- /** @type string */ (node.type) = 'word';
110
- node.value = match;
111
-
112
- delete (/** @type Partial<valueParser.FunctionNode> */ (node).nodes);
53
+ if (name !== 'steps' || segments.length !== 2) return null;
54
+ const count = singleToken(input, segments[0]);
55
+ if (
56
+ !count ||
57
+ count[0] !== TokenType.Number ||
58
+ !Number.isInteger(/** @type {{value: number}} */ (count[4]).value) ||
59
+ /** @type {{value: number}} */ (count[4]).value < 1
60
+ )
61
+ return null;
62
+ const position = singleToken(input, segments[1]);
63
+ if (!position || position[0] !== TokenType.Ident) return null;
64
+ const positionName = asciiLowerCase(decoded(position));
65
+ const countValue = /** @type {{value: number}} */ (count[4]).value;
66
+ if (countValue === 1) {
67
+ if (positionName === 'start' || positionName === 'jump-start')
68
+ return 'step-start';
69
+ if (positionName === 'end' || positionName === 'jump-end')
70
+ return 'step-end';
113
71
  }
72
+ return positionName === 'end' || positionName === 'jump-end'
73
+ ? `steps(${tokenSource(value, count)})`
74
+ : null;
114
75
  }
115
76
 
116
77
  /**
@@ -118,7 +79,27 @@ function normalizeCubicBezier(node) {
118
79
  * @return {string}
119
80
  */
120
81
  function transform(value) {
121
- return valueParser(value).walk(reduce).toString();
82
+ const structure = getBalancedTokens(value);
83
+ if (!structure) return value;
84
+ const { tokens: input } = structure;
85
+ /** @type {{start:number,end:number,text:string}[]} */
86
+ const edits = [];
87
+ for (let index = 0; index < input.length; index++) {
88
+ if (input[index][0] !== TokenType.Function) continue;
89
+ const end = structure.endForOpening(index);
90
+ if (end === undefined) continue;
91
+ const replacement = reduceFunction(value, input, structure, index, end);
92
+ if (replacement)
93
+ edits.push({
94
+ start: input[index][2],
95
+ end: input[end][3] + 1,
96
+ text: replacement,
97
+ });
98
+ }
99
+ let result = value;
100
+ for (const edit of edits.toSorted((a, b) => b.start - a.start))
101
+ result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
102
+ return result;
122
103
  }
123
104
 
124
105
  /**
@@ -134,22 +115,31 @@ function pluginCreator() {
134
115
  const cache = new Map();
135
116
 
136
117
  css.walkDecls(animationTransitionRegex, (decl) => {
137
- const value = decl.value;
118
+ const value =
119
+ decl.raws.value?.value === decl.value
120
+ ? (decl.raws.value.raw ?? decl.value)
121
+ : decl.value;
138
122
 
139
123
  if (cache.has(value)) {
140
- decl.value = cache.get(value);
124
+ assignValue(decl, cache.get(value));
141
125
 
142
126
  return;
143
127
  }
144
128
 
145
129
  const result = transform(value);
146
130
 
147
- decl.value = result;
131
+ assignValue(decl, result);
148
132
  cache.set(value, result);
149
133
  });
150
134
  },
151
135
  };
152
136
  }
137
+
138
+ /** @param {import('postcss').Declaration} decl @param {string} value */
139
+ function assignValue(decl, value) {
140
+ decl.value = value;
141
+ if (decl.raws.value?.raw) decl.raws.value = { raw: value, value };
142
+ }
153
143
  /** @type {true} */
154
144
  pluginCreator.postcss = true;
155
145
  const moduleExports = pluginCreator;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"AA2HA;;GAEG;AACH,iBAAS,aAAa,IAFV,OAAO,SAAS,EAAE,MAAM,CA2BnC;kBAzBQ,aAAa;iBA0BX,IAAI;;AAEf,QAAA,MAAM,aAAa,sBAAgB,CAAC;AAEpC,OAAO,EAAE,aAAa,IAAI,OAAO,EAAE,aAAa,IAAI,gBAAgB,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"AAwGA;;GAEG;AACH,iBAAS,aAAa,IAFV,OAAO,SAAS,EAAE,MAAM,CA8BnC;kBA5BQ,aAAa;iBAmCX,IAAI;;AAEf,QAAA,MAAM,aAAa,sBAAgB,CAAC;AAEpC,OAAO,EAAE,aAAa,IAAI,OAAO,EAAE,aAAa,IAAI,gBAAgB,EAAE,CAAC"}