@thejaredwilcurt/csslop 0.0.25 → 0.0.27

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 CHANGED
@@ -31,7 +31,7 @@
31
31
  * Gemini 3.1 Pro (High Thinking)
32
32
  * GPT-5.4 High (Thinking)
33
33
 
34
- These were the latest and greatest models at the time.
34
+ These were the latest and greatest models at the time. Since then I've mostly used the latest Claude version for maintenance.
35
35
 
36
36
  **AI's used during maintenance:**
37
37
 
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@thejaredwilcurt/csslop",
3
3
  "main": "index.js",
4
4
  "type": "module",
5
- "version": "0.0.25",
5
+ "version": "0.0.27",
6
6
  "description": "Experimental CSS minification",
7
7
  "scripts": {
8
8
  "prestart": "node ./scripts/prestart.js",
@@ -31,7 +31,7 @@
31
31
  "@eslint/js": "^10.0.1",
32
32
  "@stylistic/eslint-plugin": "^5.10.0",
33
33
  "codemirror": "^6.0.2",
34
- "eslint": "^10.9.0",
34
+ "eslint": "^10.9.1",
35
35
  "eslint-config-tjw-base": "^5.0.0",
36
36
  "eslint-config-tjw-import-x": "^1.0.1",
37
37
  "eslint-config-tjw-jsdoc": "^2.0.1",
package/src/context.js CHANGED
@@ -5,14 +5,15 @@
5
5
  import { isUnicodeCompatibleCharset } from './charset.js';
6
6
 
7
7
  /**
8
- * Creates a fresh minification context used to track `@property`-registered custom properties and their declared syntax types across the entire stylesheet.
8
+ * Creates a fresh minification context used to track `@property`-registered custom properties, their declared syntax types, and the properties that a newly assembled shorthand must not silently reset, across the entire stylesheet.
9
9
  *
10
- * @return {object} A context object with a registeredCustomProperties Set and a registeredCustomPropertySyntax Map.
10
+ * @return {object} A context object with a registeredCustomProperties Set, a registeredCustomPropertySyntax Map, and a stylesheetResetProperties Set.
11
11
  */
12
12
  function createMinifyContext () {
13
13
  return {
14
14
  registeredCustomProperties: new Set(),
15
- registeredCustomPropertySyntax: new Map()
15
+ registeredCustomPropertySyntax: new Map(),
16
+ stylesheetResetProperties: new Set()
16
17
  };
17
18
  }
18
19
 
@@ -6,6 +6,8 @@ import { minifyValue } from '../value/minify.js';
6
6
  import { splitTopLevelComponents } from '../value/syntax.js';
7
7
 
8
8
  import { CSS_WIDE_KEYWORDS } from './config.js';
9
+ import { collectDeclaredProperties } from './lookup.js';
10
+ import { resetsPropertyDeclaredElsewhere } from './reset-hazards.js';
9
11
 
10
12
  const BORDER_TRIO_PROPERTIES = ['border-width', 'border-style', 'border-color'];
11
13
 
@@ -32,10 +34,17 @@ function findLastDeclarationIndex (declarations, property) {
32
34
  * by a `border-color` override that restores the per-edge colors. The rewrite is
33
35
  * only applied when the resulting pair is shorter than the three longhands.
34
36
  *
35
- * @param {Array} declarations The declarations of a single rule.
36
- * @return {Array} The declarations, with the trio rewritten when it is shorter.
37
+ * @param {Array} declarations The declarations of a single rule.
38
+ * @param {object} context The minification context with the stylesheet's reset properties.
39
+ * @return {Array} The declarations, with the trio rewritten when it is shorter.
37
40
  */
38
- function collapseBorderTrioWithPerEdgeColor (declarations) {
41
+ function collapseBorderTrioWithPerEdgeColor (declarations, context) {
42
+ // The `border` shorthand also resets `border-image`, so the trio stays as it
43
+ // is when another rule of the stylesheet relies on that value.
44
+ if (resetsPropertyDeclaredElsewhere('border', collectDeclaredProperties(declarations), context)) {
45
+ return declarations;
46
+ }
47
+
39
48
  const trioIndexes = BORDER_TRIO_PROPERTIES.map((property) => {
40
49
  return findLastDeclarationIndex(declarations, property);
41
50
  });
@@ -2,6 +2,46 @@
2
2
  * @file Defines lookup tables mapping CSS shorthand properties to their constituent longhand properties and override relationships.
3
3
  */
4
4
 
5
+ /**
6
+ * The gap decoration rule components that CSS Gaps 1 defines once per
7
+ * direction, as a `column-rule-*` and a `row-rule-*` longhand.
8
+ *
9
+ * @type {Array}
10
+ */
11
+ const GAP_DECORATION_RULE_COMPONENTS = [
12
+ 'width',
13
+ 'style',
14
+ 'color',
15
+ 'break',
16
+ 'visibility-items',
17
+ 'inset-cap-start',
18
+ 'inset-cap-end',
19
+ 'inset-junction-start',
20
+ 'inset-junction-end'
21
+ ];
22
+
23
+ /**
24
+ * Builds the bidirectional `rule-*` gap decoration shorthands, each of which
25
+ * applies a single value to both the column and the row longhand of one gap
26
+ * decoration component.
27
+ *
28
+ * @return {object} A lookup of shorthand name to its column and row longhands.
29
+ */
30
+ function createBidirectionalGapRuleShorthands () {
31
+ const shorthands = {};
32
+ for (const component of GAP_DECORATION_RULE_COMPONENTS) {
33
+ shorthands['rule-' + component] = ['column-rule-' + component, 'row-rule-' + component];
34
+ }
35
+ return shorthands;
36
+ }
37
+
38
+ /**
39
+ * The bidirectional gap decoration shorthands, keyed by shorthand name.
40
+ *
41
+ * @type {object}
42
+ */
43
+ const BIDIRECTIONAL_GAP_RULE_SHORTHANDS = createBidirectionalGapRuleShorthands();
44
+
5
45
  const shorthandMap = {
6
46
  margin: ['margin-top', 'margin-right', 'margin-bottom', 'margin-left'],
7
47
  padding: ['padding-top', 'padding-right', 'padding-bottom', 'padding-left'],
@@ -42,12 +82,15 @@ const shorthandMap = {
42
82
  animation: ['animation-name', 'animation-duration', 'animation-timing-function', 'animation-delay', 'animation-iteration-count', 'animation-direction', 'animation-fill-mode', 'animation-play-state'],
43
83
  mask: ['mask-image', 'mask-repeat', 'mask-size'],
44
84
  'position-try': ['position-try-order', 'position-try-fallbacks'],
45
- font: ['font-style', 'font-weight', 'font-size', 'line-height', 'font-family']
85
+ font: ['font-style', 'font-weight', 'font-size', 'line-height', 'font-family'],
86
+ marker: ['marker-start', 'marker-mid', 'marker-end'],
87
+ ...BIDIRECTIONAL_GAP_RULE_SHORTHANDS
46
88
  };
47
89
 
48
90
  const shorthandOverrideMap = {
91
+ animation: ['animation-timeline', 'animation-range', 'animation-range-start', 'animation-range-end'],
49
92
  border: ['border-image', 'border-image-source', 'border-image-slice', 'border-image-width', 'border-image-outset', 'border-image-repeat'],
50
- font: ['font-variant', 'font-variant-alternates', 'font-variant-caps', 'font-variant-east-asian', 'font-variant-ligatures', 'font-variant-numeric', 'font-variant-position'],
93
+ font: ['font-variant', 'font-variant-alternates', 'font-variant-caps', 'font-variant-east-asian', 'font-variant-ligatures', 'font-variant-numeric', 'font-variant-position', 'font-feature-settings', 'font-kerning', 'font-language-override', 'font-optical-sizing', 'font-size-adjust', 'font-variation-settings'],
51
94
  mask: ['mask-border', 'mask-border-source', 'mask-border-slice', 'mask-border-width', 'mask-border-outset', 'mask-border-repeat', 'mask-border-mode']
52
95
  };
53
96
 
@@ -77,6 +120,19 @@ const EDGE_SHORTHANDS = new Set([
77
120
  'border-block-end'
78
121
  ]);
79
122
 
123
+ /**
124
+ * Shorthands that apply one value to every longhand they set, such as the SVG
125
+ * `marker` shorthand and the bidirectional gap decoration rules. They have no
126
+ * way to express longhands that differ, so a group of longhands only collapses
127
+ * into them when every longhand already holds the same value.
128
+ *
129
+ * @type {Set<string>}
130
+ */
131
+ const UNIFORM_VALUE_SHORTHANDS = new Set([
132
+ 'marker',
133
+ ...Object.keys(BIDIRECTIONAL_GAP_RULE_SHORTHANDS)
134
+ ]);
135
+
80
136
  /**
81
137
  * The four physical edge shorthands that together cover the `border` shorthand.
82
138
  *
@@ -142,12 +198,50 @@ function getOverridesOf (shorthandName) {
142
198
  return OVERRIDES_BY_SHORTHAND.get(shorthandName) || NO_PROPERTIES;
143
199
  }
144
200
 
201
+ /**
202
+ * The leaf longhands each property ultimately sets, computed on first use. The
203
+ * shorthand tables never change, so a property always expands the same way.
204
+ *
205
+ * @type {Map<string, Set<string>>}
206
+ */
207
+ const leafPropertiesByProperty = new Map();
208
+
209
+ /**
210
+ * Expands a property into the set of leaf longhands it ultimately sets, so that
211
+ * different groupings of the same box, such as `border-width` and
212
+ * `border-top-width`, can be compared for equivalent coverage.
213
+ *
214
+ * @param {string} property The property name to expand.
215
+ * @return {Set} The set of leaf longhand property names.
216
+ */
217
+ function expandToLeafProperties (property) {
218
+ const cachedLeaves = leafPropertiesByProperty.get(property);
219
+ if (cachedLeaves) {
220
+ return cachedLeaves;
221
+ }
222
+ const leafProperties = new Set();
223
+ const longhands = shorthandMap[property];
224
+ if (!longhands) {
225
+ leafProperties.add(property);
226
+ } else {
227
+ for (const longhand of longhands) {
228
+ for (const leafProperty of expandToLeafProperties(longhand)) {
229
+ leafProperties.add(leafProperty);
230
+ }
231
+ }
232
+ }
233
+ leafPropertiesByProperty.set(property, leafProperties);
234
+ return leafProperties;
235
+ }
236
+
145
237
  export {
146
238
  BORDER_EDGE_PROPERTIES,
147
239
  CSS_WIDE_KEYWORDS,
148
240
  EDGE_SHORTHANDS,
241
+ expandToLeafProperties,
149
242
  getLonghandsOf,
150
243
  getOverridesOf,
151
244
  shorthandMap,
152
- shorthandOverrideMap
245
+ shorthandOverrideMap,
246
+ UNIFORM_VALUE_SHORTHANDS
153
247
  };
@@ -2,51 +2,18 @@
2
2
  * @file Rewrites longhand declarations that share a CSS-wide keyword into a shorthand carrying that keyword, followed by the longhands that override it.
3
3
  */
4
4
 
5
- import { minifyValue } from '../value/minify.js';
6
-
7
5
  import {
8
6
  CSS_WIDE_KEYWORDS,
7
+ expandToLeafProperties,
9
8
  getLonghandsOf,
10
9
  getOverridesOf,
11
10
  shorthandMap
12
11
  } from './config.js';
13
- import { collectDeclaredProperties } from './lookup.js';
14
-
15
- /**
16
- * The leaf longhands each property ultimately sets, computed on first use. The
17
- * shorthand tables never change, so a property always expands the same way.
18
- *
19
- * @type {Map<string, Set<string>>}
20
- */
21
- const leafPropertiesByProperty = new Map();
22
-
23
- /**
24
- * Expands a property into the set of leaf longhands it ultimately sets, so that
25
- * different groupings of the same box, such as `border-width` and
26
- * `border-top-width`, can be compared for equivalent coverage.
27
- *
28
- * @param {string} property The property name to expand.
29
- * @return {Set} The set of leaf longhand property names.
30
- */
31
- function expandToLeafProperties (property) {
32
- const cachedLeaves = leafPropertiesByProperty.get(property);
33
- if (cachedLeaves) {
34
- return cachedLeaves;
35
- }
36
- const leafProperties = new Set();
37
- const longhands = shorthandMap[property];
38
- if (!longhands) {
39
- leafProperties.add(property);
40
- } else {
41
- for (const longhand of longhands) {
42
- for (const leafProperty of expandToLeafProperties(longhand)) {
43
- leafProperties.add(leafProperty);
44
- }
45
- }
46
- }
47
- leafPropertiesByProperty.set(property, leafProperties);
48
- return leafProperties;
49
- }
12
+ import {
13
+ collectDeclaredProperties,
14
+ describeDeclaration
15
+ } from './lookup.js';
16
+ import { resetsPropertyDeclaredElsewhere } from './reset-hazards.js';
50
17
 
51
18
  /**
52
19
  * Checks whether a group of longhands sets every leaf longhand that the
@@ -70,16 +37,6 @@ function coversEveryLonghandOfShorthand (shorthandName, properties) {
70
37
  });
71
38
  }
72
39
 
73
- /**
74
- * @typedef {object} LonghandEntry
75
- * @property {object} declaration The original declaration object.
76
- * @property {number} index The declaration's index within the rule.
77
- * @property {string} property The longhand property name.
78
- * @property {string} text The minified `property:value` text.
79
- * @property {string} value The minified value, without any `!important`.
80
- * @property {boolean} isImportant Whether the declaration carries `!important`.
81
- */
82
-
83
40
  /**
84
41
  * Collects the declarations of a rule that set one of a shorthand's longhands,
85
42
  * in source order.
@@ -95,16 +52,7 @@ function collectLonghandEntries (declarations, shorthandName) {
95
52
  if (!declaration.property || !longhands.has(declaration.property)) {
96
53
  return;
97
54
  }
98
- const minifiedValue = minifyValue(declaration);
99
- const isImportant = minifiedValue.includes('!important');
100
- entries.push({
101
- declaration,
102
- index,
103
- property: declaration.property,
104
- text: declaration.property + ':' + minifiedValue,
105
- value: minifiedValue.replace('!important', '').trim(),
106
- isImportant
107
- });
55
+ entries.push(describeDeclaration(declaration, index));
108
56
  });
109
57
  return entries;
110
58
  }
@@ -206,12 +154,16 @@ function resetsEarlierDeclaration (declarations, shorthandName, insertionIndex)
206
154
  * @param {Array} declarations The declarations of a single rule.
207
155
  * @param {string} shorthandName The target shorthand property name.
208
156
  * @param {Set} declaredProperties The property names the rule currently declares.
157
+ * @param {object} context The minification context with the stylesheet's reset properties.
209
158
  * @return {Array|null} The rewritten declarations, or null when the rewrite does not apply.
210
159
  */
211
- function rewriteGroupAsKeywordShorthand (declarations, shorthandName, declaredProperties) {
160
+ function rewriteGroupAsKeywordShorthand (declarations, shorthandName, declaredProperties, context) {
212
161
  if (declaredProperties.has(shorthandName)) {
213
162
  return null;
214
163
  }
164
+ if (resetsPropertyDeclaredElsewhere(shorthandName, declaredProperties, context)) {
165
+ return null;
166
+ }
215
167
 
216
168
  const entries = collectLonghandEntries(declarations, shorthandName);
217
169
  if (entries.length < 2 || hasRepeatedProperty(entries)) {
@@ -283,10 +235,11 @@ function rewriteGroupAsKeywordShorthand (declarations, shorthandName, declaredPr
283
235
  * followed by `border-width:2px`, which inherits every border property and then
284
236
  * overrides the one that differs.
285
237
  *
286
- * @param {Array} declarations The declarations of a single rule.
287
- * @return {Array} The declarations, with eligible groups rewritten.
238
+ * @param {Array} declarations The declarations of a single rule.
239
+ * @param {object} context The minification context with the stylesheet's reset properties.
240
+ * @return {Array} The declarations, with eligible groups rewritten.
288
241
  */
289
- function hoistCssWideKeywordsIntoShorthands (declarations) {
242
+ function hoistCssWideKeywordsIntoShorthands (declarations, context) {
290
243
  let result = declarations;
291
244
  // Every shorthand needs to know which properties the rule declares, so that
292
245
  // set is kept alongside the declarations and only rebuilt after a rewrite
@@ -295,7 +248,7 @@ function hoistCssWideKeywordsIntoShorthands (declarations) {
295
248
  // Shorthands are visited in declaration order, so the widest shorthand of a
296
249
  // family is rewritten before the narrower shorthands it contains.
297
250
  for (const shorthandName of Object.keys(shorthandMap)) {
298
- const rewritten = rewriteGroupAsKeywordShorthand(result, shorthandName, declaredProperties);
251
+ const rewritten = rewriteGroupAsKeywordShorthand(result, shorthandName, declaredProperties, context);
299
252
  if (rewritten) {
300
253
  result = rewritten;
301
254
  declaredProperties = collectDeclaredProperties(result);
@@ -0,0 +1,342 @@
1
+ /**
2
+ * @file Folds longhand declarations that follow their own shorthand back into that shorthand, whenever restating the whole shorthand is shorter than keeping the shorthand and its overrides apart.
3
+ */
4
+
5
+ import { splitTopLevelComponents } from '../value/syntax.js';
6
+
7
+ import {
8
+ CSS_WIDE_KEYWORDS,
9
+ expandToLeafProperties,
10
+ shorthandMap
11
+ } from './config.js';
12
+ import { describeDeclaration } from './lookup.js';
13
+ import { canMergeVarValue } from './merge.js';
14
+ import { buildShorthandValue } from './shorthand-values.js';
15
+
16
+ /**
17
+ * The shorthands whose value is a positional list of the values of their
18
+ * longhands: either a start/end pair or the four sides of a box. Only these can
19
+ * be expanded back into one value per longhand and rebuilt around an override.
20
+ *
21
+ * @type {Array}
22
+ */
23
+ const POSITIONAL_SHORTHAND_NAMES = [
24
+ 'margin',
25
+ 'padding',
26
+ 'inset',
27
+ 'gap',
28
+ 'overflow',
29
+ 'place-items',
30
+ 'place-content',
31
+ 'place-self',
32
+ 'border-width',
33
+ 'border-style',
34
+ 'border-color',
35
+ 'border-radius',
36
+ 'margin-inline',
37
+ 'margin-block',
38
+ 'padding-inline',
39
+ 'padding-block',
40
+ 'inset-inline',
41
+ 'inset-block',
42
+ 'border-inline-width',
43
+ 'border-block-width'
44
+ ];
45
+
46
+ /**
47
+ * The words of the property names that describe the same box as a shorthand,
48
+ * computed on first use. The shorthand tables never change, so a shorthand
49
+ * always has the same family.
50
+ *
51
+ * @type {Map<string, Set<string>>}
52
+ */
53
+ const familyWordsByShorthand = new Map();
54
+
55
+ /**
56
+ * Collects the hyphen separated words of a shorthand and of every longhand it
57
+ * sets. Logical properties such as `padding-inline-start` describe the same box
58
+ * as physical ones such as `padding-right`, but which physical side they map to
59
+ * depends on the writing mode, so the words of the names are what relates them.
60
+ *
61
+ * @param {string} shorthandName The CSS shorthand property name.
62
+ * @return {Set} The words that mark a property as part of the same family.
63
+ */
64
+ function collectFamilyWords (shorthandName) {
65
+ const cachedWords = familyWordsByShorthand.get(shorthandName);
66
+ if (cachedWords) {
67
+ return cachedWords;
68
+ }
69
+ const words = new Set(shorthandName.split('-'));
70
+ for (const leafProperty of expandToLeafProperties(shorthandName)) {
71
+ for (const word of leafProperty.split('-')) {
72
+ words.add(word);
73
+ }
74
+ }
75
+ familyWordsByShorthand.set(shorthandName, words);
76
+ return words;
77
+ }
78
+
79
+ /**
80
+ * Checks whether a property might set part of the same box as a shorthand,
81
+ * which it does when the two names have a word in common.
82
+ *
83
+ * @param {string} property The property name to classify.
84
+ * @param {Set} familyWords The words of the shorthand's family.
85
+ * @return {boolean} Whether the property belongs to the same family.
86
+ */
87
+ function belongsToFamily (property, familyWords) {
88
+ return property.split('-').some((word) => {
89
+ return familyWords.has(word);
90
+ });
91
+ }
92
+
93
+ /**
94
+ * @typedef {object} FoldCandidate
95
+ * @property {object} shorthandEntry The description of the shorthand declaration.
96
+ * @property {Array} overrideEntries The descriptions of the longhands that follow it.
97
+ */
98
+
99
+ /**
100
+ * Finds the shorthand of a family and the longhands declared after it, which
101
+ * are the declarations a fold would replace with a single shorthand. A family
102
+ * that states its shorthand or one of its longhands twice keeps an intentional
103
+ * fallback, so it is left alone.
104
+ *
105
+ * @param {Array} declarations The declarations of a single rule, in source order.
106
+ * @param {string} shorthandName The CSS shorthand property name.
107
+ * @return {FoldCandidate|null} The declarations to fold, or null when there are none.
108
+ */
109
+ function findFoldCandidate (declarations, shorthandName) {
110
+ const longhandProperties = new Set(shorthandMap[shorthandName]);
111
+ const shorthandEntries = [];
112
+ const overrideEntries = [];
113
+ const overriddenProperties = new Set();
114
+ let hasRepeatedOverride = false;
115
+
116
+ declarations.forEach((declaration, index) => {
117
+ if (declaration.property === shorthandName) {
118
+ shorthandEntries.push(describeDeclaration(declaration, index));
119
+ return;
120
+ }
121
+ if (!shorthandEntries.length || !longhandProperties.has(declaration.property)) {
122
+ return;
123
+ }
124
+ if (overriddenProperties.has(declaration.property)) {
125
+ hasRepeatedOverride = true;
126
+ }
127
+ overriddenProperties.add(declaration.property);
128
+ overrideEntries.push(describeDeclaration(declaration, index));
129
+ });
130
+
131
+ if (shorthandEntries.length !== 1 || !overrideEntries.length || hasRepeatedOverride) {
132
+ return null;
133
+ }
134
+ return {
135
+ shorthandEntry: shorthandEntries[0],
136
+ overrideEntries
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Checks whether a declaration lying between the shorthand and the last of its
142
+ * overrides would change meaning once the overrides move up to the shorthand.
143
+ * Only a declaration from another family is harmless, and a nested rule is
144
+ * never safe to step over because its own declarations may set the same box.
145
+ *
146
+ * @param {Array} declarations The declarations of a single rule, in source order.
147
+ * @param {string} shorthandName The CSS shorthand property name.
148
+ * @param {object} candidate The shorthand and the longhands that follow it.
149
+ * @return {boolean} Whether nothing stands in the way of the fold.
150
+ */
151
+ function isFoldPathClear (declarations, shorthandName, candidate) {
152
+ const { shorthandEntry, overrideEntries } = candidate;
153
+ const foldedIndexes = new Set(overrideEntries.map((entry) => {
154
+ return entry.index;
155
+ }));
156
+ const lastOverrideIndex = overrideEntries[overrideEntries.length - 1].index;
157
+ const familyWords = collectFamilyWords(shorthandName);
158
+
159
+ for (let index = shorthandEntry.index + 1; index < lastOverrideIndex; index++) {
160
+ if (foldedIndexes.has(index)) {
161
+ continue;
162
+ }
163
+ const { property } = declarations[index];
164
+ if (!property || belongsToFamily(property, familyWords)) {
165
+ return false;
166
+ }
167
+ }
168
+ return true;
169
+ }
170
+
171
+ /**
172
+ * Determines whether a value can stand as one component of a positional
173
+ * shorthand. A CSS-wide keyword is only valid as a declaration's entire value,
174
+ * a `/` separates the two radii of a corner rather than two components, and a
175
+ * `var()` may expand to any number of components at computed value time.
176
+ *
177
+ * @param {string} component The value component to check.
178
+ * @param {object} context The minification context with registered custom property data.
179
+ * @return {boolean} Whether the component can be positioned in a shorthand.
180
+ */
181
+ function isPositionalComponent (component, context) {
182
+ return (
183
+ !component.includes('/') &&
184
+ !CSS_WIDE_KEYWORDS.has(component.toLowerCase()) &&
185
+ canMergeVarValue(component, context)
186
+ );
187
+ }
188
+
189
+ /**
190
+ * Expands the components of a positional shorthand into one value per longhand,
191
+ * repeating the value of the opposite side for every component the author left
192
+ * out, as the CSS box model rules require.
193
+ *
194
+ * @param {Array} components The value components the shorthand was written with.
195
+ * @param {number} longhandCount The number of longhands the shorthand sets.
196
+ * @return {Array} One value per longhand, in longhand order.
197
+ */
198
+ function expandPositionalComponents (components, longhandCount) {
199
+ if (longhandCount === 2) {
200
+ const [start, end = start] = components;
201
+ return [start, end];
202
+ }
203
+ const [top, right = top, bottom = top, left = right] = components;
204
+ return [top, right, bottom, left];
205
+ }
206
+
207
+ /**
208
+ * Rebuilds a shorthand from its own value plus the longhands declared after it.
209
+ *
210
+ * @param {string} shorthandName The CSS shorthand property name.
211
+ * @param {object} candidate The shorthand and the longhands that follow it.
212
+ * @param {object} context The minification context with registered custom property data.
213
+ * @return {string|null} The rebuilt shorthand value, or null when it cannot be built.
214
+ */
215
+ function buildFoldedValue (shorthandName, candidate, context) {
216
+ const { shorthandEntry, overrideEntries } = candidate;
217
+ const longhands = shorthandMap[shorthandName];
218
+ const components = splitTopLevelComponents(shorthandEntry.value);
219
+ if (!components.length || components.length > longhands.length) {
220
+ return null;
221
+ }
222
+
223
+ const expandedValues = expandPositionalComponents(components, longhands.length);
224
+ const valueByProperty = new Map(longhands.map((property, index) => {
225
+ return [property, expandedValues[index]];
226
+ }));
227
+ for (const entry of overrideEntries) {
228
+ const overrideComponents = splitTopLevelComponents(entry.value);
229
+ if (overrideComponents.length !== 1) {
230
+ return null;
231
+ }
232
+ valueByProperty.set(entry.property, entry.value);
233
+ }
234
+
235
+ const cleanValues = longhands.map((property) => {
236
+ return valueByProperty.get(property);
237
+ });
238
+ const areAllPositional = cleanValues.every((value) => {
239
+ return isPositionalComponent(value, context);
240
+ });
241
+ if (!areAllPositional) {
242
+ return null;
243
+ }
244
+
245
+ return buildShorthandValue(shorthandName, {
246
+ properties: longhands,
247
+ valueMap: valueByProperty,
248
+ cleanValues,
249
+ importantSuffix: shorthandEntry.isImportant ? '!important' : ''
250
+ });
251
+ }
252
+
253
+ /**
254
+ * Replaces a shorthand and the longhands declared after it with the single
255
+ * shorthand that states the same box.
256
+ *
257
+ * @param {Array} declarations The declarations of a single rule, in source order.
258
+ * @param {string} shorthandName The CSS shorthand property name.
259
+ * @param {object} candidate The shorthand and the longhands that follow it.
260
+ * @param {string} foldedValue The rebuilt shorthand value.
261
+ * @return {Array} The declarations, with the family stated once.
262
+ */
263
+ function applyFold (declarations, shorthandName, candidate, foldedValue) {
264
+ const { shorthandEntry, overrideEntries } = candidate;
265
+ const foldedIndexes = new Set(overrideEntries.map((entry) => {
266
+ return entry.index;
267
+ }));
268
+ return declarations.flatMap((declaration, index) => {
269
+ if (index === shorthandEntry.index) {
270
+ return [{
271
+ property: shorthandName,
272
+ value: foldedValue,
273
+ isAssembledShorthand: true
274
+ }];
275
+ }
276
+ if (foldedIndexes.has(index)) {
277
+ return [];
278
+ }
279
+ return [declaration];
280
+ });
281
+ }
282
+
283
+ /**
284
+ * Folds the longhands that follow one shorthand back into it, when the single
285
+ * rebuilt shorthand is shorter than the declarations it replaces.
286
+ *
287
+ * @param {Array} declarations The declarations of a single rule, in source order.
288
+ * @param {string} shorthandName The CSS shorthand property name.
289
+ * @param {object} context The minification context with registered custom property data.
290
+ * @return {Array} The declarations, folded when that is shorter.
291
+ */
292
+ function foldOverridesIntoShorthand (declarations, shorthandName, context) {
293
+ const candidate = findFoldCandidate(declarations, shorthandName);
294
+ if (!candidate) {
295
+ return declarations;
296
+ }
297
+
298
+ // A shorthand and a longhand of differing importance do not describe one
299
+ // cascade step, so the pair cannot be restated as a single declaration.
300
+ const { shorthandEntry, overrideEntries } = candidate;
301
+ const shareImportance = overrideEntries.every((entry) => {
302
+ return entry.isImportant === shorthandEntry.isImportant;
303
+ });
304
+ if (!shareImportance || !isFoldPathClear(declarations, shorthandName, candidate)) {
305
+ return declarations;
306
+ }
307
+
308
+ const foldedValue = buildFoldedValue(shorthandName, candidate, context);
309
+ if (!foldedValue) {
310
+ return declarations;
311
+ }
312
+
313
+ const foldedLength = (shorthandName + ':' + foldedValue).length;
314
+ const originalLength = [shorthandEntry.text, ...overrideEntries.map((entry) => {
315
+ return entry.text;
316
+ })].join(';').length;
317
+ if (foldedLength >= originalLength) {
318
+ return declarations;
319
+ }
320
+
321
+ return applyFold(declarations, shorthandName, candidate, foldedValue);
322
+ }
323
+
324
+ /**
325
+ * Folds every longhand that follows its own shorthand back into that shorthand.
326
+ * A longhand after a shorthand only overrides the one side the shorthand had
327
+ * already set, so `padding:10px;padding-right:20px` states the same box as
328
+ * `padding:10px 20px 10px 10px`, and the shorter of the two is kept.
329
+ *
330
+ * @param {Array} declarations The declarations of a single rule, in source order.
331
+ * @param {object} context The minification context with registered custom property data.
332
+ * @return {Array} The declarations, with eligible families folded.
333
+ */
334
+ function foldLonghandOverridesIntoShorthands (declarations, context) {
335
+ let result = declarations;
336
+ for (const shorthandName of POSITIONAL_SHORTHAND_NAMES) {
337
+ result = foldOverridesIntoShorthand(result, shorthandName, context);
338
+ }
339
+ return result;
340
+ }
341
+
342
+ export { foldLonghandOverridesIntoShorthands };