@thejaredwilcurt/csslop 0.0.48 → 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.
- package/README.md +3 -4
- package/package.json +3 -3
- package/src/declarations/border.js +1 -1
- package/src/declarations/css-wide-keywords.js +37 -1
- package/src/declarations/fold.js +13 -3
- package/src/declarations/process.js +60 -7
- package/src/index.js +12 -7
- package/src/parser/source-search.js +6 -0
- package/src/rules/optimize/at-rules.js +113 -0
- package/src/rules/optimize/declaration-writes.js +70 -0
- package/src/rules/optimize/empty-rules.js +51 -0
- package/src/rules/optimize/expand-nesting.js +236 -0
- package/src/rules/optimize/factor-parents.js +343 -0
- package/src/rules/optimize/index.js +71 -0
- package/src/rules/optimize/merge-declarations.js +199 -0
- package/src/rules/optimize/merge-nested-rules.js +117 -0
- package/src/rules/optimize/merge-selectors.js +252 -0
- package/src/rules/optimize/nest-rules.js +202 -0
- package/src/rules/optimize/override-index.js +264 -0
- package/src/rules/optimize/prune-overridden.js +267 -0
- package/src/rules/optimize/selector-normalization.js +60 -0
- package/src/rules/optimize/specificity.js +280 -0
- package/src/{position-try.js → rules/position-try.js} +1 -1
- package/src/rules/selectors.js +9 -0
- package/src/rules/stringify.js +138 -76
- package/src/utilities.js +9 -8
- package/src/value/border.js +85 -0
- package/src/value/{colors.js → color/conversion.js} +3 -2
- package/src/value/color/functions.js +463 -0
- package/src/value/{light-dark.js → color/light-dark.js} +1 -1
- package/src/value/{color-mix.js → color/mix.js} +3 -3
- package/src/value/{relative-colors.js → color/relative.js} +3 -3
- package/src/value/color/shorten.js +93 -0
- package/src/value/gradients.js +1 -1
- package/src/value/hyphenate-limit-chars.js +72 -0
- package/src/value/initial-keywords.js +128 -0
- package/src/value/lengths.js +55 -0
- package/src/value/math/evaluate.js +36 -0
- package/src/value/math/fold.js +315 -0
- package/src/value/math/format.js +230 -0
- package/src/value/math/functions.js +302 -0
- package/src/value/math/index.js +219 -0
- package/src/value/math/parser.js +301 -0
- package/src/value/math/property-ranges.js +174 -0
- package/src/value/math/rewrite.js +222 -0
- package/src/value/math/tokenizer.js +160 -0
- package/src/value/math/unit-totals.js +194 -0
- package/src/value/minify.js +162 -1678
- package/src/value/numbers.js +28 -0
- package/src/value/positions.js +152 -0
- package/src/value/property-optimizations.js +485 -0
- package/src/value/segments.js +74 -0
- package/src/value/{svg-data-uri.js → svg/data-uri.js} +1 -1
- package/src/value/{svg-markup.js → svg/markup.js} +1 -1
- package/src/value/time.js +75 -0
- package/src/value/urls.js +242 -0
- package/src/value/whitespace.js +88 -0
- package/src/rules/optimize.js +0 -2031
- package/src/value/math-expression.js +0 -1156
- package/src/value/math.js +0 -886
- /package/src/value/{named-colors.js → color/named.js} +0 -0
package/src/value/numbers.js
CHANGED
|
@@ -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
|
+
};
|