@thejaredwilcurt/csslop 0.0.20 → 0.0.21
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 +5 -7
- package/src/declarations/background.js +322 -0
- package/src/declarations/border.js +120 -0
- package/src/declarations/config.js +36 -0
- package/src/declarations/merge.js +323 -0
- package/src/declarations/order.js +77 -0
- package/src/declarations/process.js +186 -869
- package/src/declarations/shorthand-values.js +374 -0
- package/src/rules/normalize.js +16 -5
- package/src/rules/stringify.js +10 -11
- package/src/value/minify.js +53 -31
- package/src/value/syntax.js +82 -1
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Serializes the collected longhand values of a shorthand into the shortest valid shorthand value, one builder per shorthand family.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { collapseShorthandParts } from '../value/shared.js';
|
|
6
|
+
import { splitTopLevelComponents } from '../value/syntax.js';
|
|
7
|
+
|
|
8
|
+
import { buildBackgroundShorthandValue } from './background.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {object} ShorthandComponents
|
|
12
|
+
* @property {Array} properties The longhand property names being merged, in shorthand order.
|
|
13
|
+
* @property {Map} valueMap A map of each longhand property name to its cleaned value.
|
|
14
|
+
* @property {Array} cleanValues The cleaned longhand values, in the same order as `properties`.
|
|
15
|
+
* @property {string} importantSuffix A trailing `!important` suffix, or an empty string.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Builds a `position-try` value, which only collapses while the order component
|
|
20
|
+
* is at its `normal` default.
|
|
21
|
+
*
|
|
22
|
+
* @param {ShorthandComponents} components The collected longhand values.
|
|
23
|
+
* @return {string|null} The shorthand value, or null when it cannot be built.
|
|
24
|
+
*/
|
|
25
|
+
function buildPositionTryValue ({ valueMap, importantSuffix }) {
|
|
26
|
+
const order = valueMap.get('position-try-order');
|
|
27
|
+
const fallbacks = valueMap.get('position-try-fallbacks');
|
|
28
|
+
if (order === 'normal' && fallbacks) {
|
|
29
|
+
return fallbacks + importantSuffix;
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Builds a `transition` value, omitting the trailing components that already
|
|
36
|
+
* hold their initial value.
|
|
37
|
+
*
|
|
38
|
+
* @param {ShorthandComponents} components The collected longhand values.
|
|
39
|
+
* @return {string|null} The shorthand value, or null when it cannot be built.
|
|
40
|
+
*/
|
|
41
|
+
function buildTransitionValue ({ valueMap, importantSuffix }) {
|
|
42
|
+
const transitionProperty = valueMap.get('transition-property');
|
|
43
|
+
const duration = valueMap.get('transition-duration');
|
|
44
|
+
const timing = valueMap.get('transition-timing-function');
|
|
45
|
+
const delay = valueMap.get('transition-delay');
|
|
46
|
+
if (!transitionProperty || !duration) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const result = [transitionProperty, duration];
|
|
50
|
+
if (timing && timing !== 'ease') {
|
|
51
|
+
result.push(timing);
|
|
52
|
+
}
|
|
53
|
+
if (delay && delay !== '0' && delay !== '0s') {
|
|
54
|
+
result.push(delay);
|
|
55
|
+
}
|
|
56
|
+
return result.join(' ') + importantSuffix;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Builds an `animation` value, omitting the components that already hold their
|
|
61
|
+
* initial value.
|
|
62
|
+
*
|
|
63
|
+
* @param {ShorthandComponents} components The collected longhand values.
|
|
64
|
+
* @return {string|null} The shorthand value, or null when it cannot be built.
|
|
65
|
+
*/
|
|
66
|
+
function buildAnimationValue ({ valueMap, importantSuffix }) {
|
|
67
|
+
const animationName = valueMap.get('animation-name');
|
|
68
|
+
const duration = valueMap.get('animation-duration');
|
|
69
|
+
if (!animationName || !duration) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const result = [animationName, duration];
|
|
73
|
+
const timing = valueMap.get('animation-timing-function');
|
|
74
|
+
const delay = valueMap.get('animation-delay');
|
|
75
|
+
const iteration = valueMap.get('animation-iteration-count');
|
|
76
|
+
const direction = valueMap.get('animation-direction');
|
|
77
|
+
const fillMode = valueMap.get('animation-fill-mode');
|
|
78
|
+
const playState = valueMap.get('animation-play-state');
|
|
79
|
+
if (timing && timing !== 'ease') {
|
|
80
|
+
result.push(timing);
|
|
81
|
+
}
|
|
82
|
+
if (delay && delay !== '0' && delay !== '0s') {
|
|
83
|
+
result.push(delay);
|
|
84
|
+
}
|
|
85
|
+
if (iteration && iteration !== '1') {
|
|
86
|
+
result.push(iteration);
|
|
87
|
+
}
|
|
88
|
+
if (direction && direction !== 'normal') {
|
|
89
|
+
result.push(direction);
|
|
90
|
+
}
|
|
91
|
+
if (fillMode && fillMode !== 'none') {
|
|
92
|
+
result.push(fillMode);
|
|
93
|
+
}
|
|
94
|
+
if (playState && playState !== 'running') {
|
|
95
|
+
result.push(playState);
|
|
96
|
+
}
|
|
97
|
+
return result.join(' ') + importantSuffix;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Builds a `background-position` value from its two axis longhands.
|
|
102
|
+
*
|
|
103
|
+
* @param {ShorthandComponents} components The collected longhand values.
|
|
104
|
+
* @return {string|null} The shorthand value, or null when it cannot be built.
|
|
105
|
+
*/
|
|
106
|
+
function buildBackgroundPositionValue ({ valueMap, importantSuffix }) {
|
|
107
|
+
const positionX = valueMap.get('background-position-x');
|
|
108
|
+
const positionY = valueMap.get('background-position-y');
|
|
109
|
+
if (!positionX || !positionY) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return positionX + ' ' + positionY + importantSuffix;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Builds a `background` value from its component longhands.
|
|
117
|
+
*
|
|
118
|
+
* @param {ShorthandComponents} components The collected longhand values.
|
|
119
|
+
* @return {string|null} The shorthand value, or null when it cannot be built.
|
|
120
|
+
*/
|
|
121
|
+
function buildBackgroundValue ({ valueMap, importantSuffix }) {
|
|
122
|
+
return buildBackgroundShorthandValue(valueMap, importantSuffix);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Builds a `mask` value, attaching the size after the position separator.
|
|
127
|
+
*
|
|
128
|
+
* @param {ShorthandComponents} components The collected longhand values.
|
|
129
|
+
* @return {string|null} The shorthand value, or null when it cannot be built.
|
|
130
|
+
*/
|
|
131
|
+
function buildMaskValue ({ valueMap, importantSuffix }) {
|
|
132
|
+
const image = valueMap.get('mask-image');
|
|
133
|
+
const repeat = valueMap.get('mask-repeat');
|
|
134
|
+
const size = valueMap.get('mask-size');
|
|
135
|
+
if (!image) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
let result = image;
|
|
139
|
+
if (repeat) {
|
|
140
|
+
result += ' ' + repeat;
|
|
141
|
+
}
|
|
142
|
+
if (size) {
|
|
143
|
+
result += '/' + size;
|
|
144
|
+
}
|
|
145
|
+
return result + importantSuffix;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Builds a `border-image` value, which requires a source to be meaningful.
|
|
150
|
+
*
|
|
151
|
+
* @param {ShorthandComponents} components The collected longhand values.
|
|
152
|
+
* @return {string|null} The shorthand value, or null when it cannot be built.
|
|
153
|
+
*/
|
|
154
|
+
function buildBorderImageValue ({ valueMap, importantSuffix }) {
|
|
155
|
+
const source = valueMap.get('border-image-source');
|
|
156
|
+
const slice = valueMap.get('border-image-slice');
|
|
157
|
+
const repeat = valueMap.get('border-image-repeat');
|
|
158
|
+
if (!source) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
const result = [source];
|
|
162
|
+
if (slice) {
|
|
163
|
+
result.push(slice);
|
|
164
|
+
}
|
|
165
|
+
if (repeat) {
|
|
166
|
+
result.push(repeat);
|
|
167
|
+
}
|
|
168
|
+
return result.join(' ') + importantSuffix;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Builds a `text-decoration` value, omitting the default style and color.
|
|
173
|
+
*
|
|
174
|
+
* @param {ShorthandComponents} components The collected longhand values.
|
|
175
|
+
* @return {string|null} The shorthand value, or null when it cannot be built.
|
|
176
|
+
*/
|
|
177
|
+
function buildTextDecorationValue ({ valueMap, importantSuffix }) {
|
|
178
|
+
const line = valueMap.get('text-decoration-line');
|
|
179
|
+
const style = valueMap.get('text-decoration-style');
|
|
180
|
+
const color = valueMap.get('text-decoration-color');
|
|
181
|
+
if (!line) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
const result = [line];
|
|
185
|
+
if (style && style !== 'solid') {
|
|
186
|
+
result.push(style);
|
|
187
|
+
}
|
|
188
|
+
if (color && color !== 'currentcolor') {
|
|
189
|
+
result.push(color);
|
|
190
|
+
}
|
|
191
|
+
return result.join(' ') + importantSuffix;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Builds a `columns` value, which keeps both components even when they match,
|
|
196
|
+
* because the width and count are not interchangeable.
|
|
197
|
+
*
|
|
198
|
+
* @param {ShorthandComponents} components The collected longhand values.
|
|
199
|
+
* @return {string|null} The shorthand value, or null when it cannot be built.
|
|
200
|
+
*/
|
|
201
|
+
function buildColumnsValue ({ cleanValues, importantSuffix }) {
|
|
202
|
+
return cleanValues.join(' ') + importantSuffix;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Builds a `list-style` value, omitting default components and falling back to
|
|
207
|
+
* `inside` when every component holds its initial value.
|
|
208
|
+
*
|
|
209
|
+
* @param {ShorthandComponents} components The collected longhand values.
|
|
210
|
+
* @return {string|null} The shorthand value, or null when it cannot be built.
|
|
211
|
+
*/
|
|
212
|
+
function buildListStyleValue ({ valueMap, importantSuffix }) {
|
|
213
|
+
const position = valueMap.get('list-style-position');
|
|
214
|
+
const image = valueMap.get('list-style-image');
|
|
215
|
+
const type = valueMap.get('list-style-type');
|
|
216
|
+
const result = [];
|
|
217
|
+
if (position && position !== 'outside') {
|
|
218
|
+
result.push(position);
|
|
219
|
+
}
|
|
220
|
+
if (image && image !== 'none') {
|
|
221
|
+
result.push(image);
|
|
222
|
+
}
|
|
223
|
+
if (type && type !== 'disc') {
|
|
224
|
+
result.push(type);
|
|
225
|
+
}
|
|
226
|
+
const joined = result.join(' ') || 'inside';
|
|
227
|
+
return joined + importantSuffix;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Builds a `font` value, which requires both a size and a family, and attaches
|
|
232
|
+
* any line height after the size separator.
|
|
233
|
+
*
|
|
234
|
+
* @param {ShorthandComponents} components The collected longhand values.
|
|
235
|
+
* @return {string|null} The shorthand value, or null when it cannot be built.
|
|
236
|
+
*/
|
|
237
|
+
function buildFontValue ({ valueMap, importantSuffix }) {
|
|
238
|
+
const fontSize = valueMap.get('font-size');
|
|
239
|
+
const fontFamily = valueMap.get('font-family');
|
|
240
|
+
if (!fontSize || !fontFamily) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
const result = [];
|
|
244
|
+
const fontStyle = valueMap.get('font-style');
|
|
245
|
+
const fontWeight = valueMap.get('font-weight');
|
|
246
|
+
const lineHeight = valueMap.get('line-height');
|
|
247
|
+
if (fontStyle && fontStyle !== 'normal') {
|
|
248
|
+
result.push(fontStyle);
|
|
249
|
+
}
|
|
250
|
+
if (fontWeight && fontWeight !== '400' && fontWeight !== 'normal') {
|
|
251
|
+
result.push(fontWeight);
|
|
252
|
+
}
|
|
253
|
+
if (lineHeight) {
|
|
254
|
+
result.push(fontSize + '/' + lineHeight);
|
|
255
|
+
} else {
|
|
256
|
+
result.push(fontSize);
|
|
257
|
+
}
|
|
258
|
+
result.push(fontFamily);
|
|
259
|
+
return result.join(' ') + importantSuffix;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Builds a `flex` value from its three required components.
|
|
264
|
+
*
|
|
265
|
+
* @param {ShorthandComponents} components The collected longhand values.
|
|
266
|
+
* @return {string|null} The shorthand value, or null when it cannot be built.
|
|
267
|
+
*/
|
|
268
|
+
function buildFlexValue ({ valueMap, importantSuffix }) {
|
|
269
|
+
const grow = valueMap.get('flex-grow');
|
|
270
|
+
const shrink = valueMap.get('flex-shrink');
|
|
271
|
+
const basis = valueMap.get('flex-basis');
|
|
272
|
+
if (!grow || !shrink || !basis) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
return [grow, shrink, basis].join(' ') + importantSuffix;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Determines whether a shorthand takes a single width, style, and color, as
|
|
280
|
+
* `border` and `outline` do.
|
|
281
|
+
*
|
|
282
|
+
* @param {Array} properties The longhand property names being merged.
|
|
283
|
+
* @return {boolean} Whether the longhands form a width/style/color trio.
|
|
284
|
+
*/
|
|
285
|
+
function isWidthStyleColorTrio (properties) {
|
|
286
|
+
return (
|
|
287
|
+
properties.length === 3 &&
|
|
288
|
+
(properties.includes('border-width') || properties.includes('outline-width')) &&
|
|
289
|
+
properties.some((property) => {
|
|
290
|
+
// Check if one longhand ends with "-style" (e.g. border-style, outline-style)
|
|
291
|
+
return /-style$/.test(property);
|
|
292
|
+
}) &&
|
|
293
|
+
properties.some((property) => {
|
|
294
|
+
// Check if one longhand ends with "-color" (e.g. border-color, outline-color)
|
|
295
|
+
return /-color$/.test(property);
|
|
296
|
+
})
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Builds the value of a shorthand whose components are positional: two-value
|
|
302
|
+
* logical pairs, four-value box sides, and width/style/color trios.
|
|
303
|
+
*
|
|
304
|
+
* @param {ShorthandComponents} components The collected longhand values.
|
|
305
|
+
* @return {string|null} The shorthand value, or null when it cannot be built.
|
|
306
|
+
*/
|
|
307
|
+
function buildPositionalShorthandValue ({ properties, cleanValues, importantSuffix }) {
|
|
308
|
+
if (properties.length === 2) {
|
|
309
|
+
// A logical pair collapses to one value when both sides match
|
|
310
|
+
if (cleanValues[0] === cleanValues[1]) {
|
|
311
|
+
return cleanValues[0] + importantSuffix;
|
|
312
|
+
}
|
|
313
|
+
return cleanValues.join(' ') + importantSuffix;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (properties.length === 4) {
|
|
317
|
+
// Box sides collapse from top/right/bottom/left down to as few values as possible
|
|
318
|
+
return collapseShorthandParts([...cleanValues]).join(' ') + importantSuffix;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (isWidthStyleColorTrio(properties)) {
|
|
322
|
+
// Every component of a border/outline shorthand accepts a single value, so a
|
|
323
|
+
// per-edge value such as `border-color:#0000 red` cannot be merged directly.
|
|
324
|
+
const hasOnlySingleComponentValues = cleanValues.every((value) => {
|
|
325
|
+
return splitTopLevelComponents(value).length === 1;
|
|
326
|
+
});
|
|
327
|
+
if (!hasOnlySingleComponentValues) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
return cleanValues.join(' ') + importantSuffix;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Builders for shorthands whose components are identified by name rather than by
|
|
338
|
+
* position, keyed by shorthand property name.
|
|
339
|
+
*
|
|
340
|
+
* @type {{[key: string]: function(ShorthandComponents): (string|null)}}
|
|
341
|
+
*/
|
|
342
|
+
const NAMED_SHORTHAND_BUILDERS = {
|
|
343
|
+
animation: buildAnimationValue,
|
|
344
|
+
background: buildBackgroundValue,
|
|
345
|
+
'background-position': buildBackgroundPositionValue,
|
|
346
|
+
'border-image': buildBorderImageValue,
|
|
347
|
+
columns: buildColumnsValue,
|
|
348
|
+
flex: buildFlexValue,
|
|
349
|
+
font: buildFontValue,
|
|
350
|
+
'list-style': buildListStyleValue,
|
|
351
|
+
mask: buildMaskValue,
|
|
352
|
+
'position-try': buildPositionTryValue,
|
|
353
|
+
'text-decoration': buildTextDecorationValue,
|
|
354
|
+
transition: buildTransitionValue
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Serializes the collected longhand values of a shorthand into its minified
|
|
359
|
+
* shorthand value, using the builder registered for that shorthand and falling
|
|
360
|
+
* back to positional assembly for box-model style shorthands.
|
|
361
|
+
*
|
|
362
|
+
* @param {string} shorthandName The target shorthand property name.
|
|
363
|
+
* @param {ShorthandComponents} components The collected longhand values.
|
|
364
|
+
* @return {string|null} The shorthand value, or null when it cannot be built.
|
|
365
|
+
*/
|
|
366
|
+
function buildShorthandValue (shorthandName, components) {
|
|
367
|
+
const namedBuilder = NAMED_SHORTHAND_BUILDERS[shorthandName];
|
|
368
|
+
if (namedBuilder) {
|
|
369
|
+
return namedBuilder(components);
|
|
370
|
+
}
|
|
371
|
+
return buildPositionalShorthandValue(components);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export { buildShorthandValue };
|
package/src/rules/normalize.js
CHANGED
|
@@ -70,7 +70,21 @@ function normalizeMedia (media) {
|
|
|
70
70
|
return fullMatch;
|
|
71
71
|
}
|
|
72
72
|
);
|
|
73
|
-
return media;
|
|
73
|
+
return compactLogicalOperators(media.trim());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Removes the whitespace between a closing parenthesis and a following `and`/`or`
|
|
78
|
+
* logical operator, which a closing parenthesis already separates unambiguously.
|
|
79
|
+
* The space after the operator is required, since it separates the operator from
|
|
80
|
+
* the next condition's opening parenthesis.
|
|
81
|
+
*
|
|
82
|
+
* @param {string} condition A normalized `@media` or `@supports` condition string.
|
|
83
|
+
* @return {string} The condition with tightened logical operator spacing.
|
|
84
|
+
*/
|
|
85
|
+
function compactLogicalOperators (condition) {
|
|
86
|
+
// Compact logical operator spacing: ") and (" → ")and (", ") or (" → ")or ("
|
|
87
|
+
return condition.replace(/\)\s*(and|or)\s*\(/gi, ')$1 (');
|
|
74
88
|
}
|
|
75
89
|
|
|
76
90
|
/**
|
|
@@ -83,10 +97,7 @@ function normalizeSupports (supports) {
|
|
|
83
97
|
// Collapse whitespace and strip spaces around punctuation
|
|
84
98
|
supports = supports.replace(/\s+/g, ' ').replace(/\s*([:,])\s*/g, '$1').replace(/\s*([=<>])\s*/g, '$1').replace(/\(\s+/g, '(').replace(/\s+\)/g, ')').trim();
|
|
85
99
|
supports = supports.replace(/\s+and\s+/g, ' and ').replace(/\s+or\s+/g, ' or ').replace(/\s+not\s+/g, ' not ');
|
|
86
|
-
|
|
87
|
-
supports = supports.replace(/\)\s*and\s*\(/g, ')and (');
|
|
88
|
-
supports = supports.replace(/\)\s*or\s*\(/g, ')or (');
|
|
89
|
-
return supports;
|
|
100
|
+
return compactLogicalOperators(supports);
|
|
90
101
|
}
|
|
91
102
|
|
|
92
103
|
/**
|
package/src/rules/stringify.js
CHANGED
|
@@ -121,12 +121,11 @@ function stringifyAtRule (rule, context) {
|
|
|
121
121
|
/**
|
|
122
122
|
* Converts a parsed CSS AST rule node into a minified CSS string, dispatching to specialized handlers for each rule type including selectors, `@media`, `@keyframes`, `@layer`, and other at-rules.
|
|
123
123
|
*
|
|
124
|
-
* @param {object}
|
|
125
|
-
* @param {object}
|
|
126
|
-
* @
|
|
127
|
-
* @return {string} The minified CSS string for this rule, or an empty string if the rule is empty.
|
|
124
|
+
* @param {object} rule The AST rule node to stringify.
|
|
125
|
+
* @param {object} context The minification context with registered custom property data.
|
|
126
|
+
* @return {string} The minified CSS string for this rule, or an empty string if the rule is empty.
|
|
128
127
|
*/
|
|
129
|
-
function stringifyRule (rule, context
|
|
128
|
+
function stringifyRule (rule, context) {
|
|
130
129
|
if (rule.type === 'rule') {
|
|
131
130
|
let declarations = rule.declarations
|
|
132
131
|
?.filter((declaration) => {
|
|
@@ -308,7 +307,7 @@ function stringifyRule (rule, context, nested = false) {
|
|
|
308
307
|
.join(';');
|
|
309
308
|
|
|
310
309
|
let renderedNested = nestedRules.map((nestedRule) => {
|
|
311
|
-
return stringifyRule(nestedRule, context
|
|
310
|
+
return stringifyRule(nestedRule, context);
|
|
312
311
|
}).join('');
|
|
313
312
|
|
|
314
313
|
output.push(renderedDeclarations);
|
|
@@ -323,11 +322,11 @@ function stringifyRule (rule, context, nested = false) {
|
|
|
323
322
|
|
|
324
323
|
if (rule.type === 'media') {
|
|
325
324
|
const normalizedMedia = normalizeMedia(rule.media);
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
|
|
325
|
+
// An opening parenthesis unambiguously starts the first media condition
|
|
326
|
+
// (including custom-media references like `(--modern)`), so the space after
|
|
327
|
+
// `@media` is only required when the query begins with an identifier.
|
|
329
328
|
let separator;
|
|
330
|
-
if (
|
|
329
|
+
if (normalizedMedia.startsWith('(')) {
|
|
331
330
|
separator = '';
|
|
332
331
|
} else {
|
|
333
332
|
separator = ' ';
|
|
@@ -343,7 +342,7 @@ function stringifyRule (rule, context, nested = false) {
|
|
|
343
342
|
return [unescapeIdent(declaration.property), ':', minifyValue(declaration)].join('');
|
|
344
343
|
}).join(';');
|
|
345
344
|
const renderedRules = subRules.map((childRule) => {
|
|
346
|
-
return stringifyRule(childRule, context
|
|
345
|
+
return stringifyRule(childRule, context);
|
|
347
346
|
}).join('');
|
|
348
347
|
const children = [renderedDeclarations, renderedRules].filter(Boolean).join('');
|
|
349
348
|
if (!children) {
|
package/src/value/minify.js
CHANGED
|
@@ -59,7 +59,9 @@ const POSITION_AREA_SHORTHANDS = {
|
|
|
59
59
|
* Regex matching hex color tokens (#rgb, #rgba, #rrggbb, #rrggbbaa) and CSS named
|
|
60
60
|
* color keywords. Hex patterns are ordered longest-first to avoid partial matches.
|
|
61
61
|
* Named colors are sorted longest-first so longer names like `darkslategray` are
|
|
62
|
-
* matched before shorter substrings.
|
|
62
|
+
* matched before shorter substrings. A named color only counts as a color keyword
|
|
63
|
+
* when it is a complete identifier, so hyphens on either side disqualify it (this
|
|
64
|
+
* keeps identifiers such as the custom property name in `var(--grey)` intact).
|
|
63
65
|
*
|
|
64
66
|
* @type {RegExp}
|
|
65
67
|
*/
|
|
@@ -68,11 +70,11 @@ const COLOR_TOKEN_PATTERN = new RegExp(
|
|
|
68
70
|
'#[0-9a-fA-F]{6}(?![0-9a-fA-F])|' +
|
|
69
71
|
'#[0-9a-fA-F]{4}(?![0-9a-fA-F])|' +
|
|
70
72
|
'#[0-9a-fA-F]{3}(?![0-9a-fA-F])|' +
|
|
71
|
-
'\\
|
|
73
|
+
'(?<![\\w-])(?:' +
|
|
72
74
|
Object.keys(namedColors).sort((a, b) => {
|
|
73
75
|
return b.length - a.length;
|
|
74
76
|
}).join('|') +
|
|
75
|
-
')\\
|
|
77
|
+
')(?![\\w-])',
|
|
76
78
|
'gi'
|
|
77
79
|
);
|
|
78
80
|
|
|
@@ -180,6 +182,37 @@ function replaceOutsideStringsAndUrls (value, replacer) {
|
|
|
180
182
|
return result;
|
|
181
183
|
}
|
|
182
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Lowercases every hex color token in a CSS value, since uppercase hex digits
|
|
187
|
+
* compress worse and are equivalent to their lowercase form.
|
|
188
|
+
*
|
|
189
|
+
* @param {string} value The CSS value string.
|
|
190
|
+
* @return {string} The value with all hex color tokens lowercased.
|
|
191
|
+
*/
|
|
192
|
+
function lowercaseHexColors (value) {
|
|
193
|
+
return replaceOutsideStringsAndUrls(value, (segment) => {
|
|
194
|
+
// Match hex color tokens of 3 to 8 hex digits
|
|
195
|
+
return segment.replace(/#([0-9a-fA-F]{3,8})\b/gi, (hexColor) => {
|
|
196
|
+
return hexColor.toLowerCase();
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Removes whitespace that precedes a hex color token. A `#` unambiguously starts
|
|
203
|
+
* a hash token in CSS, so no separator is required between it and a preceding
|
|
204
|
+
* ident, keyword, or number (e.g. `1px solid #f00` becomes `1px solid#f00`).
|
|
205
|
+
*
|
|
206
|
+
* @param {string} value The CSS value string.
|
|
207
|
+
* @return {string} The value with spaces before hex colors removed.
|
|
208
|
+
*/
|
|
209
|
+
function elideSpaceBeforeHexColors (value) {
|
|
210
|
+
return replaceOutsideStringsAndUrls(value, (segment) => {
|
|
211
|
+
// Match whitespace followed by a hex color token of 3 to 8 hex digits
|
|
212
|
+
return segment.replace(/\s+#([0-9a-fA-F]{3,8})\b/gi, '#$1');
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
183
216
|
/**
|
|
184
217
|
* Chooses the shortest valid representation for the path inside a `url(...)`
|
|
185
218
|
* token, weighing an unquoted form, an escaped single space, and a quoted form.
|
|
@@ -682,11 +715,12 @@ function convertMillisecondsToSeconds (value) {
|
|
|
682
715
|
* Applies property-specific optimizations to a CSS value (transition, flex, font,
|
|
683
716
|
* background, display, scale, border-radius, shorthand collapsing, etc.).
|
|
684
717
|
*
|
|
685
|
-
* @param {string}
|
|
686
|
-
* @param {string}
|
|
687
|
-
* @
|
|
718
|
+
* @param {string} val The CSS value string after generic minification.
|
|
719
|
+
* @param {string} property The CSS property name.
|
|
720
|
+
* @param {boolean} allowsHexSpaceElision Whether the space preceding a hex color may be removed.
|
|
721
|
+
* @return {string} The value with property-specific optimizations applied.
|
|
688
722
|
*/
|
|
689
|
-
function applyPropertyOptimizations (val, property) {
|
|
723
|
+
function applyPropertyOptimizations (val, property, allowsHexSpaceElision) {
|
|
690
724
|
if (property === 'font-weight' && isUnicodeCharset()) {
|
|
691
725
|
// Replace font-weight keyword "bold" with its numeric equivalent
|
|
692
726
|
val = val.replace(/\bbold\b/gi, '700');
|
|
@@ -859,12 +893,9 @@ function applyPropertyOptimizations (val, property) {
|
|
|
859
893
|
val = replaceOutsideStringsAndUrls(val, shortenColorValues);
|
|
860
894
|
|
|
861
895
|
// Remove space before hex colors (second pass after color evaluations)
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
// Then remove other spaces before hex colors
|
|
866
|
-
return segment.replace(/\s+#([0-9a-fA-F]{3,8})\b/gi, '#$1');
|
|
867
|
-
});
|
|
896
|
+
if (allowsHexSpaceElision) {
|
|
897
|
+
val = elideSpaceBeforeHexColors(val);
|
|
898
|
+
}
|
|
868
899
|
if (property !== 'transform' && property !== 'background' && property !== 'src') {
|
|
869
900
|
// Restore space after close-paren when followed by an alphanumeric, hash, or hyphen
|
|
870
901
|
val = val.replace(/\)(?=[0-9a-zA-Z#-])/g, ') ');
|
|
@@ -911,9 +942,6 @@ function applyPropertyOptimizations (val, property) {
|
|
|
911
942
|
if (property === 'border') {
|
|
912
943
|
// Remove default "medium" border-width keyword
|
|
913
944
|
val = val.replace(/\bmedium\s+/g, '');
|
|
914
|
-
// Restore missing space between border-style and a 4-digit hex color (with alpha) when they are adjacent
|
|
915
|
-
// This is needed because solid#0000 could be parsed as solid followed by #000 followed by position 0
|
|
916
|
-
val = val.replace(/\b(solid|dashed|dotted|double|groove|ridge|inset|outset|hidden|none)#([0-9a-fA-F]{4})\b/gi, '$1 #$2');
|
|
917
945
|
}
|
|
918
946
|
|
|
919
947
|
if (property === 'outline') {
|
|
@@ -982,6 +1010,10 @@ function minifyValue (declaration) {
|
|
|
982
1010
|
return 'none';
|
|
983
1011
|
}
|
|
984
1012
|
let val = declaration.value;
|
|
1013
|
+
// Values assembled from already-minified longhands keep the separator spaces
|
|
1014
|
+
// between their components, because those spaces delimit the shorthand's
|
|
1015
|
+
// parts rather than the authored whitespace of a single written value.
|
|
1016
|
+
const allowsHexSpaceElision = !declaration.isAssembledShorthand;
|
|
985
1017
|
|
|
986
1018
|
if (typeof val === 'string') {
|
|
987
1019
|
val = val.trim();
|
|
@@ -1024,20 +1056,10 @@ function minifyValue (declaration) {
|
|
|
1024
1056
|
val = roundCompactNumber(rawNumber, 4) + rawUnit;
|
|
1025
1057
|
}
|
|
1026
1058
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
// Remove other spaces before hex colors
|
|
1032
|
-
segment = segment.replace(/\s+#([0-9a-fA-F]{3,8})\b/gi, '#$1');
|
|
1033
|
-
// Restore the preserved space
|
|
1034
|
-
segment = segment.replace(/__BORDER_SPACE__#/g, ' #');
|
|
1035
|
-
// Lowercase hex color tokens for consistency and shorter output
|
|
1036
|
-
segment = segment.replace(/#([0-9a-fA-F]{3,8})\b/gi, (m) => {
|
|
1037
|
-
return m.toLowerCase();
|
|
1038
|
-
});
|
|
1039
|
-
return segment;
|
|
1040
|
-
});
|
|
1059
|
+
val = lowercaseHexColors(val);
|
|
1060
|
+
if (allowsHexSpaceElision) {
|
|
1061
|
+
val = elideSpaceBeforeHexColors(val);
|
|
1062
|
+
}
|
|
1041
1063
|
|
|
1042
1064
|
// Convert color functions to hex equivalents
|
|
1043
1065
|
val = convertColorsToHex(val);
|
|
@@ -1049,7 +1071,7 @@ function minifyValue (declaration) {
|
|
|
1049
1071
|
val = simplifyEquivalentLightDarkFunctions(val);
|
|
1050
1072
|
|
|
1051
1073
|
// Property-specific optimizations
|
|
1052
|
-
val = applyPropertyOptimizations(val, declaration.property);
|
|
1074
|
+
val = applyPropertyOptimizations(val, declaration.property, allowsHexSpaceElision);
|
|
1053
1075
|
|
|
1054
1076
|
// Minify relative color syntax (identity resolution and whitespace collapsing)
|
|
1055
1077
|
val = minifyRelativeColorSyntax(val);
|