@thejaredwilcurt/csslop 0.0.20 → 0.0.22
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 +12 -5
- 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/css-wide-keywords.js +285 -0
- package/src/declarations/merge.js +323 -0
- package/src/declarations/order.js +77 -0
- package/src/declarations/process.js +188 -869
- package/src/declarations/shorthand-values.js +374 -0
- package/src/index.js +5 -2
- package/src/rules/normalize.js +29 -5
- package/src/rules/optimize.js +5 -2
- package/src/rules/stringify.js +49 -18
- package/src/value/minify.js +102 -36
- 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/index.js
CHANGED
|
@@ -34,7 +34,10 @@ import {
|
|
|
34
34
|
removeEmptyRules,
|
|
35
35
|
removeOverriddenMultiSelectorProperties
|
|
36
36
|
} from './rules/optimize.js';
|
|
37
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
removeRedundantLayerStatementSemicolon,
|
|
39
|
+
stringifyRule
|
|
40
|
+
} from './rules/stringify.js';
|
|
38
41
|
import { minifyValue } from './value/minify.js';
|
|
39
42
|
|
|
40
43
|
/**
|
|
@@ -256,7 +259,7 @@ export const minifyCSS = function (input) {
|
|
|
256
259
|
output.push(stringifyRule(rule, context));
|
|
257
260
|
}
|
|
258
261
|
|
|
259
|
-
const mergedOutput = mergeAdjacentRulesWithIdenticalBodies(output);
|
|
262
|
+
const mergedOutput = removeRedundantLayerStatementSemicolon(mergeAdjacentRulesWithIdenticalBodies(output));
|
|
260
263
|
|
|
261
264
|
clearActiveCharset();
|
|
262
265
|
return restoreEscapeSequences(mergedOutput.join(''));
|
package/src/rules/normalize.js
CHANGED
|
@@ -48,6 +48,18 @@ function unescapeSelector (selector) {
|
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Normalizes a `@layer` cascade layer name list by trimming it and removing the
|
|
53
|
+
* optional whitespace that may surround the commas separating the layer names.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} layerNames The raw layer name list (e.g. "reset, base,\n components").
|
|
56
|
+
* @return {string} The normalized comma-separated layer name list.
|
|
57
|
+
*/
|
|
58
|
+
function normalizeLayerNames (layerNames) {
|
|
59
|
+
// Collapse the optional whitespace surrounding the commas between layer names
|
|
60
|
+
return String(layerNames ?? '').trim().replace(/\s*,\s*/g, ',');
|
|
61
|
+
}
|
|
62
|
+
|
|
51
63
|
/**
|
|
52
64
|
* Normalizes a `@media` query string by collapsing whitespace, stripping the default "all and" prefix, and converting min/max-width to range syntax.
|
|
53
65
|
*
|
|
@@ -70,7 +82,21 @@ function normalizeMedia (media) {
|
|
|
70
82
|
return fullMatch;
|
|
71
83
|
}
|
|
72
84
|
);
|
|
73
|
-
return media;
|
|
85
|
+
return compactLogicalOperators(media.trim());
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Removes the whitespace between a closing parenthesis and a following `and`/`or`
|
|
90
|
+
* logical operator, which a closing parenthesis already separates unambiguously.
|
|
91
|
+
* The space after the operator is required, since it separates the operator from
|
|
92
|
+
* the next condition's opening parenthesis.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} condition A normalized `@media` or `@supports` condition string.
|
|
95
|
+
* @return {string} The condition with tightened logical operator spacing.
|
|
96
|
+
*/
|
|
97
|
+
function compactLogicalOperators (condition) {
|
|
98
|
+
// Compact logical operator spacing: ") and (" → ")and (", ") or (" → ")or ("
|
|
99
|
+
return condition.replace(/\)\s*(and|or)\s*\(/gi, ')$1 (');
|
|
74
100
|
}
|
|
75
101
|
|
|
76
102
|
/**
|
|
@@ -83,10 +109,7 @@ function normalizeSupports (supports) {
|
|
|
83
109
|
// Collapse whitespace and strip spaces around punctuation
|
|
84
110
|
supports = supports.replace(/\s+/g, ' ').replace(/\s*([:,])\s*/g, '$1').replace(/\s*([=<>])\s*/g, '$1').replace(/\(\s+/g, '(').replace(/\s+\)/g, ')').trim();
|
|
85
111
|
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;
|
|
112
|
+
return compactLogicalOperators(supports);
|
|
90
113
|
}
|
|
91
114
|
|
|
92
115
|
/**
|
|
@@ -101,6 +124,7 @@ function canUnwrapSupports (supports) {
|
|
|
101
124
|
|
|
102
125
|
export {
|
|
103
126
|
canUnwrapSupports,
|
|
127
|
+
normalizeLayerNames,
|
|
104
128
|
normalizeMedia,
|
|
105
129
|
normalizeSupports,
|
|
106
130
|
unescapeIdent,
|
package/src/rules/optimize.js
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
import { escapeRegexString } from '../utilities.js';
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
normalizeLayerNames,
|
|
9
|
+
normalizeMedia
|
|
10
|
+
} from './normalize.js';
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* Expands rules that contain only nested sub-rules into flat rules with combined selectors, enabling further merging when the combined selectors already exist elsewhere.
|
|
@@ -786,7 +789,7 @@ function mergeLayerRules (rules, mergeSelectorRules) {
|
|
|
786
789
|
const result = [];
|
|
787
790
|
for (const rule of rules) {
|
|
788
791
|
if (rule.type === 'layer') {
|
|
789
|
-
const layerName = rule.layer
|
|
792
|
+
const layerName = normalizeLayerNames(rule.layer);
|
|
790
793
|
if (rule.rules && rule.rules.length > 0) {
|
|
791
794
|
if (layerName && layerBlockMap.has(layerName)) {
|
|
792
795
|
layerBlockMap.get(layerName).rules.push(...rule.rules);
|
package/src/rules/stringify.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from './custom-properties.js';
|
|
12
12
|
import {
|
|
13
13
|
canUnwrapSupports,
|
|
14
|
+
normalizeLayerNames,
|
|
14
15
|
normalizeMedia,
|
|
15
16
|
normalizeSupports,
|
|
16
17
|
unescapeIdent,
|
|
@@ -40,6 +41,34 @@ function stringifyDeclarations (declarations) {
|
|
|
40
41
|
.join(';');
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Matches a complete `@layer` statement, which declares layer names without a
|
|
46
|
+
* block and ends with the semicolon that separates it from the CSS that follows
|
|
47
|
+
* it. Layer names are identifiers, so any block, string, or function character
|
|
48
|
+
* means the string is something other than a lone layer statement.
|
|
49
|
+
*
|
|
50
|
+
* @type {RegExp}
|
|
51
|
+
*/
|
|
52
|
+
const LAYER_STATEMENT_PATTERN = /^@layer [^{}();'"]*;$/;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Removes the semicolon of a trailing `@layer` statement, since that semicolon
|
|
56
|
+
* only exists to separate the statement from whatever comes after it. When the
|
|
57
|
+
* statement ends a stylesheet or a block, there is nothing left to separate.
|
|
58
|
+
*
|
|
59
|
+
* @param {Array} ruleStrings The stringified rules, in output order.
|
|
60
|
+
* @return {Array} The stringified rules, without the redundant semicolon.
|
|
61
|
+
*/
|
|
62
|
+
function removeRedundantLayerStatementSemicolon (ruleStrings) {
|
|
63
|
+
const lastIndex = ruleStrings.length - 1;
|
|
64
|
+
if (lastIndex < 0 || !LAYER_STATEMENT_PATTERN.test(ruleStrings[lastIndex])) {
|
|
65
|
+
return ruleStrings;
|
|
66
|
+
}
|
|
67
|
+
const result = [...ruleStrings];
|
|
68
|
+
result[lastIndex] = result[lastIndex].slice(0, -1);
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
43
72
|
/**
|
|
44
73
|
* Recursively stringifies child rules into a concatenated minified CSS string.
|
|
45
74
|
*
|
|
@@ -48,9 +77,10 @@ function stringifyDeclarations (declarations) {
|
|
|
48
77
|
* @return {string} The concatenated minified CSS for all child rules.
|
|
49
78
|
*/
|
|
50
79
|
function stringifyChildRules (rules, context) {
|
|
51
|
-
|
|
80
|
+
const ruleStrings = (rules || []).map((childRule) => {
|
|
52
81
|
return stringifyRule(childRule, context);
|
|
53
|
-
}).
|
|
82
|
+
}).filter(Boolean);
|
|
83
|
+
return removeRedundantLayerStatementSemicolon(ruleStrings).join('');
|
|
54
84
|
}
|
|
55
85
|
/**
|
|
56
86
|
* Minifies a `@function` prelude (signature) by collapsing whitespace around
|
|
@@ -121,12 +151,11 @@ function stringifyAtRule (rule, context) {
|
|
|
121
151
|
/**
|
|
122
152
|
* 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
153
|
*
|
|
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.
|
|
154
|
+
* @param {object} rule The AST rule node to stringify.
|
|
155
|
+
* @param {object} context The minification context with registered custom property data.
|
|
156
|
+
* @return {string} The minified CSS string for this rule, or an empty string if the rule is empty.
|
|
128
157
|
*/
|
|
129
|
-
function stringifyRule (rule, context
|
|
158
|
+
function stringifyRule (rule, context) {
|
|
130
159
|
if (rule.type === 'rule') {
|
|
131
160
|
let declarations = rule.declarations
|
|
132
161
|
?.filter((declaration) => {
|
|
@@ -308,7 +337,7 @@ function stringifyRule (rule, context, nested = false) {
|
|
|
308
337
|
.join(';');
|
|
309
338
|
|
|
310
339
|
let renderedNested = nestedRules.map((nestedRule) => {
|
|
311
|
-
return stringifyRule(nestedRule, context
|
|
340
|
+
return stringifyRule(nestedRule, context);
|
|
312
341
|
}).join('');
|
|
313
342
|
|
|
314
343
|
output.push(renderedDeclarations);
|
|
@@ -323,11 +352,11 @@ function stringifyRule (rule, context, nested = false) {
|
|
|
323
352
|
|
|
324
353
|
if (rule.type === 'media') {
|
|
325
354
|
const normalizedMedia = normalizeMedia(rule.media);
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
|
|
355
|
+
// An opening parenthesis unambiguously starts the first media condition
|
|
356
|
+
// (including custom-media references like `(--modern)`), so the space after
|
|
357
|
+
// `@media` is only required when the query begins with an identifier.
|
|
329
358
|
let separator;
|
|
330
|
-
if (
|
|
359
|
+
if (normalizedMedia.startsWith('(')) {
|
|
331
360
|
separator = '';
|
|
332
361
|
} else {
|
|
333
362
|
separator = ' ';
|
|
@@ -342,9 +371,7 @@ function stringifyRule (rule, context, nested = false) {
|
|
|
342
371
|
const renderedDeclarations = mediaDeclarations.map((declaration) => {
|
|
343
372
|
return [unescapeIdent(declaration.property), ':', minifyValue(declaration)].join('');
|
|
344
373
|
}).join(';');
|
|
345
|
-
const renderedRules = subRules
|
|
346
|
-
return stringifyRule(childRule, context, false);
|
|
347
|
-
}).join('');
|
|
374
|
+
const renderedRules = stringifyChildRules(subRules, context);
|
|
348
375
|
const children = [renderedDeclarations, renderedRules].filter(Boolean).join('');
|
|
349
376
|
if (!children) {
|
|
350
377
|
return '';
|
|
@@ -475,10 +502,11 @@ function stringifyRule (rule, context, nested = false) {
|
|
|
475
502
|
}
|
|
476
503
|
|
|
477
504
|
if (rule.type === 'layer') {
|
|
505
|
+
const layerNames = normalizeLayerNames(rule.layer);
|
|
478
506
|
if (rule.rules && rule.rules.length) {
|
|
479
|
-
return '@layer ' +
|
|
507
|
+
return '@layer ' + layerNames + '{' + stringifyChildRules(rule.rules, context) + '}';
|
|
480
508
|
} else {
|
|
481
|
-
return '@layer ' +
|
|
509
|
+
return '@layer ' + layerNames + ';';
|
|
482
510
|
}
|
|
483
511
|
}
|
|
484
512
|
|
|
@@ -636,4 +664,7 @@ function stringifyRule (rule, context, nested = false) {
|
|
|
636
664
|
return ''; // Ignore unknown for now
|
|
637
665
|
}
|
|
638
666
|
|
|
639
|
-
export {
|
|
667
|
+
export {
|
|
668
|
+
removeRedundantLayerStatementSemicolon,
|
|
669
|
+
stringifyRule
|
|
670
|
+
};
|