@thejaredwilcurt/csslop 0.0.49 → 0.0.50

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.
Files changed (55) hide show
  1. package/README.md +0 -1
  2. package/package.json +1 -1
  3. package/src/declarations/border.js +1 -1
  4. package/src/index.js +7 -7
  5. package/src/rules/optimize/at-rules.js +113 -0
  6. package/src/rules/optimize/declaration-writes.js +70 -0
  7. package/src/rules/optimize/empty-rules.js +51 -0
  8. package/src/rules/optimize/expand-nesting.js +236 -0
  9. package/src/rules/optimize/factor-parents.js +343 -0
  10. package/src/rules/optimize/index.js +71 -0
  11. package/src/rules/optimize/merge-declarations.js +199 -0
  12. package/src/rules/optimize/merge-nested-rules.js +117 -0
  13. package/src/rules/optimize/merge-selectors.js +252 -0
  14. package/src/rules/optimize/nest-rules.js +202 -0
  15. package/src/rules/optimize/override-index.js +264 -0
  16. package/src/rules/optimize/prune-overridden.js +267 -0
  17. package/src/rules/optimize/selector-normalization.js +60 -0
  18. package/src/rules/optimize/specificity.js +280 -0
  19. package/src/{position-try.js → rules/position-try.js} +1 -1
  20. package/src/utilities.js +14 -0
  21. package/src/value/border.js +85 -0
  22. package/src/value/{colors.js → color/conversion.js} +3 -2
  23. package/src/value/color/functions.js +463 -0
  24. package/src/value/{light-dark.js → color/light-dark.js} +1 -1
  25. package/src/value/{color-mix.js → color/mix.js} +3 -3
  26. package/src/value/{relative-colors.js → color/relative.js} +3 -3
  27. package/src/value/color/shorten.js +93 -0
  28. package/src/value/gradients.js +1 -1
  29. package/src/value/hyphenate-limit-chars.js +72 -0
  30. package/src/value/initial-keywords.js +128 -0
  31. package/src/value/lengths.js +55 -0
  32. package/src/value/math/evaluate.js +36 -0
  33. package/src/value/math/fold.js +315 -0
  34. package/src/value/math/format.js +230 -0
  35. package/src/value/math/functions.js +302 -0
  36. package/src/value/math/index.js +219 -0
  37. package/src/value/math/parser.js +301 -0
  38. package/src/value/math/property-ranges.js +174 -0
  39. package/src/value/math/rewrite.js +222 -0
  40. package/src/value/math/tokenizer.js +160 -0
  41. package/src/value/math/unit-totals.js +194 -0
  42. package/src/value/minify.js +127 -1669
  43. package/src/value/numbers.js +28 -0
  44. package/src/value/positions.js +152 -0
  45. package/src/value/property-optimizations.js +485 -0
  46. package/src/value/segments.js +74 -0
  47. package/src/value/{svg-data-uri.js → svg/data-uri.js} +1 -1
  48. package/src/value/{svg-markup.js → svg/markup.js} +1 -1
  49. package/src/value/time.js +75 -0
  50. package/src/value/urls.js +242 -0
  51. package/src/value/whitespace.js +88 -0
  52. package/src/rules/optimize.js +0 -2314
  53. package/src/value/math-expression.js +0 -1156
  54. package/src/value/math.js +0 -886
  55. /package/src/value/{named-colors.js → color/named.js} +0 -0
@@ -146,8 +146,36 @@ function scaleByPowerOfTen (parts, power, unit) {
146
146
  };
147
147
  }
148
148
 
149
+ /**
150
+ * Reports whether rewriting a number keeps its numeric type. A number written
151
+ * with a decimal point or an exponent has the `<number>` type, while one
152
+ * written with digits alone is an `<integer>`. A custom property can be
153
+ * substituted into, or registered with, a grammar that tells the two apart, so
154
+ * `100.0` cannot become `100` there.
155
+ *
156
+ * @param {string} originalNumber The number as it was written.
157
+ * @param {string} rewrittenNumber The number as it would be rewritten.
158
+ * @return {boolean} True when both spellings have the same type.
159
+ */
160
+ function keepsNumberType (originalNumber, rewrittenNumber) {
161
+ return isIntegerSpelling(originalNumber) === isIntegerSpelling(rewrittenNumber);
162
+ }
163
+
164
+ /**
165
+ * Reports whether a number is spelled as an `<integer>`: digits with an
166
+ * optional sign, and no decimal point or exponent.
167
+ *
168
+ * @param {string} number The number as written.
169
+ * @return {boolean} True when the spelling is an integer.
170
+ */
171
+ function isIntegerSpelling (number) {
172
+ // An optional sign followed by nothing but digits
173
+ return /^[+-]?\d+$/.test(number);
174
+ }
175
+
149
176
  export {
150
177
  formatShortestNumber,
178
+ keepsNumberType,
151
179
  parseNumericParts,
152
180
  scaleByPowerOfTen,
153
181
  shortenNumericText
@@ -0,0 +1,152 @@
1
+ /**
2
+ * @file Shortens position values: the keyword pairs of `position-area`, the keywords of `background-position`, and the separator between a layer's image and its position.
3
+ */
4
+
5
+ /**
6
+ * Map of position-area two-keyword values to their single-keyword equivalents.
7
+ * Per CSS spec, `center center` is redundant with `center`, etc.
8
+ *
9
+ * @type {{[key: string]: string}}
10
+ */
11
+ const POSITION_AREA_SHORTHANDS = {
12
+ 'center center': 'center',
13
+ 'top center': 'top',
14
+ 'bottom center': 'bottom',
15
+ 'center top': 'top',
16
+ 'center bottom': 'bottom',
17
+ 'left center': 'left',
18
+ 'right center': 'right'
19
+ };
20
+
21
+ /**
22
+ * Map from background-position keyword names to their equivalent percent values.
23
+ *
24
+ * @type {{[key: string]: string}}
25
+ */
26
+ const POSITION_KEYWORD_TO_PERCENT = {
27
+ left: '0',
28
+ center: '50%',
29
+ right: '100%',
30
+ top: '0',
31
+ bottom: '100%'
32
+ };
33
+
34
+ /**
35
+ * Converts background-position keyword values to their shorter percent
36
+ * equivalents when possible. Single Y-axis keywords (top, bottom) and
37
+ * multi-value offset syntax (3 or 4 values) are left unchanged.
38
+ *
39
+ * @param {string} value The background-position value string.
40
+ * @return {string} The value with keywords converted to percents where shorter.
41
+ */
42
+ function convertBackgroundPositionKeywords (value) {
43
+ // Split on whitespace to determine the number of position parts
44
+ const parts = value.split(/\s+/);
45
+
46
+ // 3 or 4 value syntax uses keyword offsets, retain keywords
47
+ if (parts.length >= 3) {
48
+ return value;
49
+ }
50
+
51
+ if (parts.length === 1) {
52
+ const keyword = parts[0].toLowerCase();
53
+ // Y-axis-only keywords (top, bottom) can't be expressed as a single X-axis percent
54
+ const isYAxisOnly = keyword === 'top' || keyword === 'bottom';
55
+ if (isYAxisOnly) {
56
+ return value;
57
+ }
58
+ if (POSITION_KEYWORD_TO_PERCENT[keyword] !== undefined) {
59
+ return POSITION_KEYWORD_TO_PERCENT[keyword];
60
+ }
61
+ return value;
62
+ }
63
+
64
+ if (parts.length === 2) {
65
+ const firstKeyword = parts[0].toLowerCase();
66
+ const secondKeyword = parts[1].toLowerCase();
67
+ const firstIsPositionKeyword = POSITION_KEYWORD_TO_PERCENT[firstKeyword] !== undefined;
68
+ const secondIsPositionKeyword = POSITION_KEYWORD_TO_PERCENT[secondKeyword] !== undefined;
69
+
70
+ if (firstIsPositionKeyword && secondIsPositionKeyword) {
71
+ const firstPercent = POSITION_KEYWORD_TO_PERCENT[firstKeyword];
72
+ const secondPercent = POSITION_KEYWORD_TO_PERCENT[secondKeyword];
73
+ // Collapse to single value when Y is center (50%), since a single value defaults Y to 50%
74
+ if (secondPercent === '50%') {
75
+ return firstPercent;
76
+ }
77
+ return firstPercent + ' ' + secondPercent;
78
+ }
79
+ }
80
+
81
+ return value;
82
+ }
83
+
84
+ /**
85
+ * Matches one offset of a position: an edge keyword or a numeric distance.
86
+ *
87
+ * @type {string}
88
+ */
89
+ const POSITION_OFFSET_SOURCE = '(?:left|center|right|top|bottom|[+-]?(?:\\d+|\\d*\\.\\d+)(?:%|[a-z]+)?)';
90
+
91
+ /**
92
+ * Matches a whole position component, which is one or two of those offsets.
93
+ *
94
+ * @type {string}
95
+ */
96
+ const POSITION_COMPONENT_SOURCE = '(' + POSITION_OFFSET_SOURCE + '(?:\\s+' + POSITION_OFFSET_SOURCE + ')?)';
97
+
98
+ /**
99
+ * Matches everything a `url()` holds up to but not including its own closing
100
+ * parenthesis. A url is written without nested parentheses, so the first one
101
+ * that closes it is the end of the token.
102
+ *
103
+ * @type {string}
104
+ */
105
+ const URL_TOKEN_BODY_SOURCE = 'url\\([^()]*';
106
+
107
+ /**
108
+ * Matches a close-paren that ends an image function, followed by a separated
109
+ * position that no slash follows. The parenthesis that ends a `url()` is
110
+ * skipped, since that one closes a token rather than a function.
111
+ *
112
+ * @type {RegExp}
113
+ */
114
+ const SEPARATED_FUNCTION_POSITION_PATTERN = new RegExp('(?<!' + URL_TOKEN_BODY_SOURCE + ')\\)\\s+' + POSITION_COMPONENT_SOURCE + '(?!\\/)', 'gi');
115
+
116
+ /**
117
+ * Matches a `url()` separated from the position that follows it, which is the
118
+ * separator a url does not need.
119
+ *
120
+ * @type {RegExp}
121
+ */
122
+ const SEPARATED_URL_POSITION_PATTERN = new RegExp('(' + URL_TOKEN_BODY_SOURCE + '\\))\\s+' + POSITION_COMPONENT_SOURCE, 'gi');
123
+
124
+ /**
125
+ * Matches a close-paren separated from a position that a slash follows, where
126
+ * the slash already delimits the position from the size behind it.
127
+ *
128
+ * @type {RegExp}
129
+ */
130
+ const SEPARATED_IMAGE_SIZE_POSITION_PATTERN = new RegExp('\\)\\s+' + POSITION_COMPONENT_SOURCE + '(?=\\/)', 'gi');
131
+
132
+ /**
133
+ * Normalizes the separator between the image of a layer and the position that
134
+ * follows it. Both `background` and `mask` take a `<position> [ / <size> ]`
135
+ * component after their image. The closing parenthesis already delimits a
136
+ * function token from the position, and neither a position that a `/` follows
137
+ * nor one that follows a `url()` needs a separator either.
138
+ *
139
+ * @param {string} value The layered image value, with the parenthesis padding removed.
140
+ * @return {string} The value with the position separator normalized.
141
+ */
142
+ function normalizeImagePositionSeparator (value) {
143
+ const joinedToFunction = value.replace(SEPARATED_FUNCTION_POSITION_PATTERN, ')$1');
144
+ const joinedToUrl = joinedToFunction.replace(SEPARATED_URL_POSITION_PATTERN, '$1$2');
145
+ return joinedToUrl.replace(SEPARATED_IMAGE_SIZE_POSITION_PATTERN, ')$1');
146
+ }
147
+
148
+ export {
149
+ convertBackgroundPositionKeywords,
150
+ normalizeImagePositionSeparator,
151
+ POSITION_AREA_SHORTHANDS
152
+ };
@@ -0,0 +1,485 @@
1
+ /**
2
+ * @file Applies the optimizations specific to individual properties, such as shortening timing functions, collapsing shorthand components, and reordering `border` and `flex-flow` values.
3
+ */
4
+
5
+ import { isUnicodeCharset } from '../context.js';
6
+ import { splitTopLevelComponents } from '../parser/source-search.js';
7
+ import { isCustomProperty } from '../utilities.js';
8
+
9
+ import {
10
+ removeDefaultBorderWidth,
11
+ reorderBorderWidthBeforeStyle,
12
+ separateOutlineStyleFromColor
13
+ } from './border.js';
14
+ import { rgbaToHex } from './color/conversion.js';
15
+ import { shortenColorValues } from './color/shorten.js';
16
+ import { minifyFontShorthand } from './font.js';
17
+ import { collapseHyphenateLimitChars } from './hyphenate-limit-chars.js';
18
+ import { resolveInitialKeyword } from './initial-keywords.js';
19
+ import { convertAbsoluteLengthsToPx } from './lengths.js';
20
+ import {
21
+ convertBackgroundPositionKeywords,
22
+ normalizeImagePositionSeparator
23
+ } from './positions.js';
24
+ import { replaceOutsideStringsAndUrls } from './segments.js';
25
+ import {
26
+ collapseShorthandParts,
27
+ normalizeScaleComponent,
28
+ parseAlphaString
29
+ } from './shared.js';
30
+ import { convertTimesToShortestUnit } from './time.js';
31
+ import { minifyTransformValue } from './transforms.js';
32
+ import { removeDefaultBackgroundPositionsAfterUrlTokens } from './urls.js';
33
+ import { restoreSpaceBeforeMathOperators } from './whitespace.js';
34
+
35
+ /**
36
+ * Properties whose value is a time, where a millisecond amount may be worth
37
+ * rewriting in seconds.
38
+ *
39
+ * @type {Set<string>}
40
+ */
41
+ const TIME_PROPERTIES = new Set([
42
+ 'transition',
43
+ 'transition-duration',
44
+ 'transition-delay',
45
+ 'animation',
46
+ 'animation-duration',
47
+ 'animation-delay'
48
+ ]);
49
+
50
+ /**
51
+ * Properties whose grammar delimits its own components, either with punctuation
52
+ * (the `/` and `,` of the `background`, `mask`, and `src` layers) or by taking
53
+ * nothing but functions (`transform`). Their components stay readable without
54
+ * the whitespace that follows a closing parenthesis, so that whitespace is left
55
+ * elided and each of them restores only the separators its grammar still needs.
56
+ *
57
+ * @type {Set<string>}
58
+ */
59
+ const PUNCTUATED_COMPONENT_PROPERTIES = new Set([
60
+ 'background',
61
+ 'mask',
62
+ 'src',
63
+ 'transform'
64
+ ]);
65
+
66
+ /**
67
+ * Properties whose one to four box-model components collapse to the fewest
68
+ * that state the same edges (4 → 3 → 2 → 1 values).
69
+ *
70
+ * @type {Set<string>}
71
+ */
72
+ const COLLAPSIBLE_BOX_SHORTHAND_PROPERTIES = new Set([
73
+ 'margin',
74
+ 'padding',
75
+ 'inset',
76
+ 'border-width',
77
+ 'border-style',
78
+ 'border-color',
79
+ 'gap',
80
+ 'overflow'
81
+ ]);
82
+
83
+ /**
84
+ * Whole values that have a shorter keyword stating the same thing, keyed by
85
+ * the property they are declared on.
86
+ *
87
+ * @type {Map<string, Map<string, string>>}
88
+ */
89
+ const SHORTER_EQUIVALENT_VALUES = new Map([
90
+ ['background', new Map([
91
+ ['none', '0 0']
92
+ ])],
93
+ ['display', new Map([
94
+ ['block flow', 'block'],
95
+ ['inline flow-root', 'inline-block']
96
+ ])],
97
+ ['background-repeat', new Map([
98
+ ['no-repeat no-repeat', 'no-repeat'],
99
+ ['repeat no-repeat', 'repeat-x'],
100
+ ['no-repeat repeat', 'repeat-y']
101
+ ])]
102
+ ]);
103
+
104
+ /**
105
+ * Replaces a whole value with the shorter keyword that states the same thing
106
+ * for its property, when there is one.
107
+ *
108
+ * @param {string} value The CSS value string.
109
+ * @param {string} property The CSS property name.
110
+ * @return {string} The shorter equivalent value, or the value unchanged.
111
+ */
112
+ function replaceWithShorterEquivalentValue (value, property) {
113
+ return SHORTER_EQUIVALENT_VALUES.get(property)?.get(value) ?? value;
114
+ }
115
+
116
+ /**
117
+ * Replaces the `bold` and `normal` keywords of `font-weight` with their
118
+ * numeric equivalents.
119
+ *
120
+ * @param {string} value The `font-weight` value.
121
+ * @return {string} The value with its weight keywords written as numbers.
122
+ */
123
+ function convertFontWeightKeywords (value) {
124
+ // Replace font-weight keyword "bold" with its numeric equivalent
125
+ const withBold = value.replace(/\bbold\b/gi, '700');
126
+ // Replace font-weight keyword "normal" with its numeric equivalent
127
+ return withBold.replace(/\bnormal\b/gi, '400');
128
+ }
129
+
130
+ /**
131
+ * Minifies a `transition` value by removing zero durations and replacing
132
+ * `cubic-bezier()` curves with the timing keywords they are equivalent to
133
+ * (transition: all 0s -> transition: all).
134
+ *
135
+ * @param {string} value The `transition` value.
136
+ * @return {string} The minified value.
137
+ */
138
+ function minifyTransitionValue (value) {
139
+ // Remove zero-second duration from transition shorthand
140
+ value = value.replace(/\s+0s/g, ' ');
141
+ // Remove leading zero-pixel value from transition shorthand
142
+ value = value.replace(/^0px\s*/, '');
143
+ // Replace cubic-bezier functions with their equivalent named timing-function keywords
144
+ value = value.replace(/cubic-bezier\(0,0,1,1\)/g, 'linear');
145
+ value = value.replace(/cubic-bezier\(\.25,\.1,\.25,1\)/g, 'ease');
146
+ value = value.replace(/cubic-bezier\(\.42,0,1,1\)/g, 'ease-in');
147
+ value = value.replace(/cubic-bezier\(0,0,\.58,1\)/g, 'ease-out');
148
+ return value.trim();
149
+ }
150
+
151
+ /**
152
+ * Minifies an `animation` value by replacing `steps()` functions with the
153
+ * timing keywords they are equivalent to.
154
+ *
155
+ * @param {string} value The `animation` value.
156
+ * @return {string} The minified value.
157
+ */
158
+ function minifyAnimationValue (value) {
159
+ // Replace steps() functions with their equivalent named timing-function keywords
160
+ value = value.replace(/steps\(1,start\)/g, 'step-start');
161
+ value = value.replace(/steps\(1,end\)/g, 'step-end');
162
+ // Restore space between step-start/step-end keyword and following token
163
+ // (the parenthesis whitespace stripping removes the space before replacement)
164
+ return value.replace(/(step-start|step-end)(?=[a-zA-Z0-9#-])/g, '$1 ');
165
+ }
166
+
167
+ /**
168
+ * Minifies a `flex` value by removing the zero basis that the shorthand
169
+ * already reads an unwritten basis as (flex: 0 0 0px -> flex: 0 0).
170
+ *
171
+ * @param {string} value The `flex` value.
172
+ * @return {string} The minified value.
173
+ */
174
+ function minifyFlexValue (value) {
175
+ // Remove trailing zero-pixel basis value
176
+ value = value.replace(/\s+0px/g, ' ');
177
+ // Remove leading zero-pixel value
178
+ value = value.replace(/^0px\s*/, '');
179
+ // Remove an explicit zero basis: a trailing zero is only a basis when grow
180
+ // and shrink precede it, and the shorthand already reads an unwritten basis
181
+ // as 0. In `0 0` the trailing zero is the shrink, which the shorthand
182
+ // defaults to 1 instead, so dropping it would size the element differently.
183
+ value = value.replace(/^(\S+\s+\S+)\s+0$/, '$1');
184
+ // Remove standalone zero-pixel value
185
+ value = value.replace(/^0px$/, '');
186
+ return value.trim();
187
+ }
188
+
189
+ /**
190
+ * Rewrites a `flex-flow` value from wrap-direction to the canonical
191
+ * direction-wrap order.
192
+ *
193
+ * @param {string} value The `flex-flow` value.
194
+ * @return {string} The value in canonical order.
195
+ */
196
+ function reorderFlexFlowValue (value) {
197
+ // Reorder flex-flow from wrap-direction to direction-wrap (canonical order)
198
+ return value.replace(/^(nowrap|wrap|wrap-reverse)\s+(row|row-reverse|column|column-reverse)$/i, '$2 $1');
199
+ }
200
+
201
+ /**
202
+ * Minifies a `font-family` list by unquoting the multi-word family names that
203
+ * do not need quotes and dropping every repeat of a family already listed.
204
+ *
205
+ * @param {string} value The `font-family` value.
206
+ * @return {string} The minified family list.
207
+ */
208
+ function minifyFontFamilyValue (value) {
209
+ // Strip quotes from simple multi-word font family names that don't require quoting
210
+ const unquoted = value.replace(/"([A-Za-z0-9-]+(?: [A-Za-z0-9-]+)+)"/g, '$1');
211
+ const seenFamilies = new Set();
212
+ return unquoted.split(',').map((part) => {
213
+ return part.trim();
214
+ }).filter(Boolean).filter((part) => {
215
+ const lowercaseName = part.toLowerCase();
216
+ if (seenFamilies.has(lowercaseName)) {
217
+ return false;
218
+ }
219
+ seenFamilies.add(lowercaseName);
220
+ return true;
221
+ }).join(',');
222
+ }
223
+
224
+ /**
225
+ * Normalizes each quoted row string of a `grid-template-areas` value.
226
+ *
227
+ * @param {string} value The `grid-template-areas` value.
228
+ * @return {string} The value with every row's whitespace and null cells collapsed.
229
+ */
230
+ function normalizeGridTemplateAreas (value) {
231
+ // Normalize each quoted grid-template-areas row string
232
+ return value.replace(/"([^"]*)"/g, (match, inner) => {
233
+ // Collapse whitespace to single space within grid row
234
+ let normalized = inner.replace(/\s+/g, ' ').trim();
235
+ // Collapse consecutive dots (null cell tokens) to a single dot
236
+ normalized = normalized.replace(/(^| )\.{2,}(?= |$)/g, '$1.');
237
+ return '"' + normalized + '"';
238
+ });
239
+ }
240
+
241
+ /**
242
+ * Converts `display-p3` neutral grays to sRGB hex, since a gray with equal
243
+ * channels is the same color in either gamut.
244
+ *
245
+ * @param {string} value The CSS value string.
246
+ * @return {string} The value with display-p3 grays written as hex.
247
+ */
248
+ function convertDisplayP3GraysToHex (value) {
249
+ // Match color(display-p3 X X X) with three identical channels and an optional slash-delimited alpha
250
+ return value.replace(/\bcolor\(display-p3\s+([\d.]+)\s+\1\s+\1(?:\s*\/\s*(-?(?:\d+|\d*\.\d+)%?))?\s*\)/gi, (match, channelText, alphaText) => {
251
+ const channelValue = parseFloat(channelText);
252
+ const channel = Math.round(channelValue * 255);
253
+ return rgbaToHex(channel, channel, channel, parseAlphaString(alphaText));
254
+ });
255
+ }
256
+
257
+ /**
258
+ * Minifies a `background` value by removing the components that only restate
259
+ * their defaults, then normalizing the separator between each layer's image
260
+ * and its position.
261
+ *
262
+ * @param {string} value The `background` value.
263
+ * @param {Array} urlTokens URL tokens currently represented by markers.
264
+ * @return {string} The minified value.
265
+ */
266
+ function minifyBackgroundValue (value, urlTokens) {
267
+ let normalized = value
268
+ // Remove default "0 0" background-position values
269
+ .replace(/(?:^|\s)0(?:%|px)? 0(?:%|px)?(?=\s|$)/g, ' ')
270
+ // Remove "0 0" background-position after a close-paren (e.g. after url())
271
+ .replace(/\)0(?:%|px)? 0(?:%|px)?(?=\s|$)/g, ') ')
272
+ // Remove default "repeat" background-repeat keyword (excluding compound values like no-repeat)
273
+ .replace(/(?<!-)\brepeat\b(?!-)/g, ' ')
274
+ // Remove default "scroll" background-attachment keyword
275
+ .replace(/\bscroll\b/g, ' ')
276
+ // Remove default "none" background-image keyword
277
+ .replace(/\bnone\b/g, ' ')
278
+ // Collapse whitespace to single space
279
+ .replace(/\s+/g, ' ')
280
+ .trim();
281
+ normalized = removeDefaultBackgroundPositionsAfterUrlTokens(normalized, urlTokens);
282
+ return normalizeImagePositionSeparator(normalized || value);
283
+ }
284
+
285
+ /**
286
+ * Minifies a `transform` value, then removes the whitespace between its
287
+ * consecutive transform functions.
288
+ *
289
+ * @param {string} value The `transform` value.
290
+ * @return {string} The minified value.
291
+ */
292
+ function minifyTransformProperty (value) {
293
+ // Remove whitespace between consecutive transform functions
294
+ return minifyTransformValue(value).replace(/\)\s+(?=[a-z-]+\()/gi, ')');
295
+ }
296
+
297
+ /**
298
+ * Collapses a `scale` value to the fewest axis components that state the
299
+ * same scaling, since an unstated y axis repeats x and an unstated z axis is 1.
300
+ *
301
+ * @param {string} value The `scale` value.
302
+ * @return {string} The collapsed value.
303
+ */
304
+ function collapseScaleValue (value) {
305
+ // Split scale value on whitespace into individual axis components
306
+ const parts = value.split(/\s+/).filter(Boolean).map(normalizeScaleComponent);
307
+ if (parts.length === 2 && parts[0] === parts[1]) {
308
+ return parts[0];
309
+ }
310
+ if (parts.length === 3 && parts[2] === '1') {
311
+ if (parts[0] === parts[1]) {
312
+ return parts[0];
313
+ }
314
+ return parts[0] + ' ' + parts[1];
315
+ }
316
+ return parts.join(' ');
317
+ }
318
+
319
+ /**
320
+ * Collapses a box-model shorthand value to the fewest components that state
321
+ * the same edges.
322
+ *
323
+ * @param {string} value The shorthand value.
324
+ * @return {string} The collapsed value.
325
+ */
326
+ function collapseBoxShorthandValue (value) {
327
+ // Split components instead of splitting on spaces, so values that legally
328
+ // touch, like the adjacent hex colors of `#ddd#ddd`, count separately
329
+ return collapseShorthandParts(splitTopLevelComponents(value))
330
+ .join(' ')
331
+ // A hash token may touch the component before it, so the separator space is not needed
332
+ .replace(/ #/g, '#');
333
+ }
334
+
335
+ /**
336
+ * Collapses each slash-separated half of a `border-radius` value to the fewest
337
+ * components that state the same corners.
338
+ *
339
+ * @param {string} value The `border-radius` value.
340
+ * @return {string} The collapsed value.
341
+ */
342
+ function collapseBorderRadiusValue (value) {
343
+ const segments = value.split('/').map((segment) => {
344
+ return segment.trim();
345
+ }).filter(Boolean).map((segment) => {
346
+ // Split each segment on whitespace and collapse redundant parts
347
+ return collapseShorthandParts(segment.split(/\s+/)).join(' ');
348
+ });
349
+ return segments.join('/');
350
+ }
351
+
352
+ /**
353
+ * Applies property-specific optimizations to a CSS value (transition, flex, font,
354
+ * background, display, scale, border-radius, shorthand collapsing, etc.).
355
+ *
356
+ * @param {string} value The CSS value string after generic minification.
357
+ * @param {string} property The CSS property name.
358
+ * @param {Array} urlTokens URL tokens currently represented by markers.
359
+ * @return {string} The value with property-specific optimizations applied.
360
+ */
361
+ function applyPropertyOptimizations (value, property, urlTokens) {
362
+ if (property === 'font-weight' && isUnicodeCharset()) {
363
+ value = convertFontWeightKeywords(value);
364
+ }
365
+
366
+ // Convert ms to s for time-related properties when the seconds form is shorter
367
+ if (TIME_PROPERTIES.has(property)) {
368
+ value = convertTimesToShortestUnit(value);
369
+ }
370
+
371
+ if (property === 'transition') {
372
+ value = minifyTransitionValue(value);
373
+ }
374
+
375
+ if (property === 'animation') {
376
+ value = minifyAnimationValue(value);
377
+ }
378
+
379
+ if (property === 'flex') {
380
+ value = minifyFlexValue(value);
381
+ }
382
+
383
+ // Initial values
384
+ value = resolveInitialKeyword(value, property);
385
+ value = replaceWithShorterEquivalentValue(value, property);
386
+
387
+ if (property === 'background-position') {
388
+ value = convertBackgroundPositionKeywords(value);
389
+ }
390
+
391
+ if (property === 'border') {
392
+ value = reorderBorderWidthBeforeStyle(value);
393
+ }
394
+
395
+ if (property === 'flex-flow') {
396
+ value = reorderFlexFlowValue(value);
397
+ }
398
+
399
+ if (property === 'font-family') {
400
+ value = minifyFontFamilyValue(value);
401
+ }
402
+
403
+ if (property === 'grid-template-areas') {
404
+ value = normalizeGridTemplateAreas(value);
405
+ }
406
+
407
+ // Custom properties hold an arbitrary token stream rather than a typed value,
408
+ // so a unit-like token in one is not necessarily a length.
409
+ if (!isCustomProperty(property)) {
410
+ value = replaceOutsideStringsAndUrls(value, convertAbsoluteLengthsToPx);
411
+ }
412
+
413
+ if (property === 'syntax') {
414
+ // Remove whitespace around pipe separators in @property syntax descriptors
415
+ value = value.replace(/\s*\|\s*/g, '|');
416
+ }
417
+
418
+ // Simplify clamp() where all three arguments are identical (e.g. clamp(1rem,1rem,1rem) → 1rem)
419
+ value = value.replace(/\bclamp\(([^,]+),\1,\1\)/gi, '$1');
420
+
421
+ value = convertDisplayP3GraysToHex(value);
422
+
423
+ // Font families may contain color names, but never color values.
424
+ if (property !== 'font' && property !== 'font-family') {
425
+ value = replaceOutsideStringsAndUrls(value, (segment) => {
426
+ return shortenColorValues(segment);
427
+ });
428
+ }
429
+
430
+ if (!PUNCTUATED_COMPONENT_PROPERTIES.has(property)) {
431
+ // Restore space after close-paren when followed by an alphanumeric, hash, or hyphen
432
+ value = value.replace(/\)(?=[0-9a-zA-Z#-])/g, ') ');
433
+ }
434
+ value = restoreSpaceBeforeMathOperators(value);
435
+
436
+ if (property === 'font') {
437
+ value = minifyFontShorthand(value);
438
+ }
439
+
440
+ if (property === 'background' && value !== 'none') {
441
+ value = minifyBackgroundValue(value, urlTokens);
442
+ }
443
+
444
+ if (property === 'mask') {
445
+ value = normalizeImagePositionSeparator(value);
446
+ }
447
+
448
+ if (property === 'border') {
449
+ value = removeDefaultBorderWidth(value);
450
+ }
451
+
452
+ if (property === 'outline') {
453
+ value = separateOutlineStyleFromColor(value);
454
+ }
455
+
456
+ if (property === 'transform') {
457
+ value = minifyTransformProperty(value);
458
+ }
459
+
460
+ if (property === 'scale') {
461
+ value = collapseScaleValue(value);
462
+ }
463
+
464
+ // Replace multiple spaces
465
+ value = value.replace(/\s+/g, ' ');
466
+
467
+ if (COLLAPSIBLE_BOX_SHORTHAND_PROPERTIES.has(property)) {
468
+ value = collapseBoxShorthandValue(value);
469
+ }
470
+
471
+ if (property === 'hyphenate-limit-chars') {
472
+ value = collapseHyphenateLimitChars(value);
473
+ }
474
+
475
+ if (property === 'border-radius') {
476
+ value = collapseBorderRadiusValue(value);
477
+ }
478
+
479
+ return value;
480
+ }
481
+
482
+ export {
483
+ applyPropertyOptimizations,
484
+ PUNCTUATED_COMPONENT_PROPERTIES
485
+ };