@thejaredwilcurt/csslop 0.0.26 → 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/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.26",
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
  });
@@ -90,7 +90,7 @@ const shorthandMap = {
90
90
  const shorthandOverrideMap = {
91
91
  animation: ['animation-timeline', 'animation-range', 'animation-range-start', 'animation-range-end'],
92
92
  border: ['border-image', 'border-image-source', 'border-image-slice', 'border-image-width', 'border-image-outset', 'border-image-repeat'],
93
- 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'],
94
94
  mask: ['mask-border', 'mask-border-source', 'mask-border-slice', 'mask-border-width', 'mask-border-outset', 'mask-border-repeat', 'mask-border-mode']
95
95
  };
96
96
 
@@ -13,6 +13,7 @@ import {
13
13
  collectDeclaredProperties,
14
14
  describeDeclaration
15
15
  } from './lookup.js';
16
+ import { resetsPropertyDeclaredElsewhere } from './reset-hazards.js';
16
17
 
17
18
  /**
18
19
  * Checks whether a group of longhands sets every leaf longhand that the
@@ -153,12 +154,16 @@ function resetsEarlierDeclaration (declarations, shorthandName, insertionIndex)
153
154
  * @param {Array} declarations The declarations of a single rule.
154
155
  * @param {string} shorthandName The target shorthand property name.
155
156
  * @param {Set} declaredProperties The property names the rule currently declares.
157
+ * @param {object} context The minification context with the stylesheet's reset properties.
156
158
  * @return {Array|null} The rewritten declarations, or null when the rewrite does not apply.
157
159
  */
158
- function rewriteGroupAsKeywordShorthand (declarations, shorthandName, declaredProperties) {
160
+ function rewriteGroupAsKeywordShorthand (declarations, shorthandName, declaredProperties, context) {
159
161
  if (declaredProperties.has(shorthandName)) {
160
162
  return null;
161
163
  }
164
+ if (resetsPropertyDeclaredElsewhere(shorthandName, declaredProperties, context)) {
165
+ return null;
166
+ }
162
167
 
163
168
  const entries = collectLonghandEntries(declarations, shorthandName);
164
169
  if (entries.length < 2 || hasRepeatedProperty(entries)) {
@@ -230,10 +235,11 @@ function rewriteGroupAsKeywordShorthand (declarations, shorthandName, declaredPr
230
235
  * followed by `border-width:2px`, which inherits every border property and then
231
236
  * overrides the one that differs.
232
237
  *
233
- * @param {Array} declarations The declarations of a single rule.
234
- * @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.
235
241
  */
236
- function hoistCssWideKeywordsIntoShorthands (declarations) {
242
+ function hoistCssWideKeywordsIntoShorthands (declarations, context) {
237
243
  let result = declarations;
238
244
  // Every shorthand needs to know which properties the rule declares, so that
239
245
  // set is kept alongside the declarations and only rebuilt after a rewrite
@@ -242,7 +248,7 @@ function hoistCssWideKeywordsIntoShorthands (declarations) {
242
248
  // Shorthands are visited in declaration order, so the widest shorthand of a
243
249
  // family is rewritten before the narrower shorthands it contains.
244
250
  for (const shorthandName of Object.keys(shorthandMap)) {
245
- const rewritten = rewriteGroupAsKeywordShorthand(result, shorthandName, declaredProperties);
251
+ const rewritten = rewriteGroupAsKeywordShorthand(result, shorthandName, declaredProperties, context);
246
252
  if (rewritten) {
247
253
  result = rewritten;
248
254
  declaredProperties = collectDeclaredProperties(result);
@@ -9,28 +9,34 @@ import {
9
9
  import { collectDeclaredProperties } from './lookup.js';
10
10
 
11
11
  /**
12
- * Pairs of properties where the first must be emitted before the second, because
13
- * the first resets the second and a merged shorthand is appended after the
14
- * longhands it was built from.
12
+ * Builds the pairs of properties where the first has to be emitted before the
13
+ * second, because the first resets the second while a merged shorthand is
14
+ * appended after the longhands it was built from. Every property a shorthand
15
+ * resets but cannot express has to be restated after that shorthand, and a
16
+ * `margin` built from a mixed `!important` group leaves its important longhands
17
+ * behind for the same reason.
18
+ *
19
+ * @return {Array} The property pairs, each as a shorthand followed by the property it resets.
20
+ */
21
+ function buildRequiredPropertyOrder () {
22
+ const orderedPairs = [];
23
+ for (const [shorthandProperty, resetProperties] of Object.entries(shorthandOverrideMap)) {
24
+ for (const resetProperty of resetProperties) {
25
+ orderedPairs.push([shorthandProperty, resetProperty]);
26
+ }
27
+ }
28
+ for (const longhandProperty of shorthandMap.margin) {
29
+ orderedPairs.push(['margin', longhandProperty]);
30
+ }
31
+ return orderedPairs;
32
+ }
33
+
34
+ /**
35
+ * The property pairs whose relative order the output has to correct.
15
36
  *
16
37
  * @type {Array}
17
38
  */
18
- const REQUIRED_PROPERTY_ORDER = [
19
- ['animation', 'animation-timeline'],
20
- ['animation', 'animation-range'],
21
- ['animation', 'animation-range-start'],
22
- ['animation', 'animation-range-end'],
23
- ['border', 'border-image'],
24
- ['font', 'font-feature-settings'],
25
- ['font', 'font-variant-ligatures'],
26
- ['font', 'font-kerning'],
27
- ['font', 'font-variation-settings'],
28
- ['mask', 'mask-border'],
29
- ['margin', 'margin-top'],
30
- ['margin', 'margin-right'],
31
- ['margin', 'margin-bottom'],
32
- ['margin', 'margin-left']
33
- ];
39
+ const REQUIRED_PROPERTY_ORDER = buildRequiredPropertyOrder();
34
40
 
35
41
  /**
36
42
  * Reorders declarations so that shorthands appear before any related longhands they would override, preventing cascade issues in the minified output.
@@ -25,6 +25,7 @@ import {
25
25
  getOverriddenLonghands,
26
26
  orderDeclarations
27
27
  } from './order.js';
28
+ import { resetsPropertyDeclaredElsewhere } from './reset-hazards.js';
28
29
 
29
30
  /**
30
31
  * Shorthands that keep their non-important longhands in the output, so a mixed
@@ -348,6 +349,12 @@ function mergeLonghandsIntoShorthands (declarations, context) {
348
349
  if (declaredProperties.has(shorthand)) {
349
350
  continue;
350
351
  }
352
+ // Assembling a shorthand also resets the properties it cannot express, so
353
+ // a rule only collapses into one when nothing else in the stylesheet
354
+ // relies on a value that reset would discard.
355
+ if (resetsPropertyDeclaredElsewhere(shorthand, declaredProperties, context)) {
356
+ continue;
357
+ }
351
358
 
352
359
  const mergeableProperties = getMergeProps(shorthand, longhands, declaredProperties);
353
360
  if (!mergeableProperties) {
@@ -398,8 +405,8 @@ function processDeclarations (declarations, context) {
398
405
  result = absorbBackgroundLonghandsIntoShorthand(result);
399
406
  result = mergeLonghandsIntoShorthands(result, context);
400
407
  result = foldLonghandOverridesIntoShorthands(result, context);
401
- result = hoistCssWideKeywordsIntoShorthands(result);
402
- result = collapseBorderTrioWithPerEdgeColor(result);
408
+ result = hoistCssWideKeywordsIntoShorthands(result, context);
409
+ result = collapseBorderTrioWithPerEdgeColor(result, context);
403
410
 
404
411
  return orderDeclarations(result);
405
412
  }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * @file Tracks the properties a shorthand resets without being able to express them, so that assembling a shorthand out of longhands never cancels a value another rule of the stylesheet set.
3
+ */
4
+
5
+ import {
6
+ getOverridesOf,
7
+ shorthandOverrideMap
8
+ } from './config.js';
9
+
10
+ /**
11
+ * Collects every property that some shorthand resets without being able to
12
+ * express it, such as `border-image` for `border`. Only these properties can
13
+ * lose their value to a newly assembled shorthand, so only these are worth
14
+ * tracking across the stylesheet.
15
+ *
16
+ * @return {Set} The property names a shorthand resets but cannot express.
17
+ */
18
+ function collectResettableProperties () {
19
+ const resettableProperties = new Set();
20
+ for (const resetProperties of Object.values(shorthandOverrideMap)) {
21
+ for (const property of resetProperties) {
22
+ resettableProperties.add(property);
23
+ }
24
+ }
25
+ return resettableProperties;
26
+ }
27
+
28
+ /**
29
+ * The properties a shorthand resets but cannot express. The shorthand tables
30
+ * never change, so the set is built once.
31
+ *
32
+ * @type {Set<string>}
33
+ */
34
+ const RESETTABLE_PROPERTIES = collectResettableProperties();
35
+
36
+ /**
37
+ * Collects the rules nested inside a rule. The parser stores the children of an
38
+ * at-rule as a list of rules, and the children of a style rule that uses CSS
39
+ * nesting as declarations that carry rules of their own.
40
+ *
41
+ * @param {object} rule The AST rule node to look inside.
42
+ * @return {Array} The rule nodes nested within it.
43
+ */
44
+ function collectNestedRules (rule) {
45
+ const nestedRules = [...(rule.rules || [])];
46
+ for (const declaration of rule.declarations || []) {
47
+ if (declaration.rules || declaration.declarations) {
48
+ nestedRules.push(declaration);
49
+ }
50
+ }
51
+ return nestedRules;
52
+ }
53
+
54
+ /**
55
+ * Records which resettable properties the stylesheet declares, in any rule at
56
+ * any nesting depth, so that each rule can later ask whether assembling a
57
+ * shorthand would cancel a value another rule set.
58
+ *
59
+ * @param {Array} rules The AST rule nodes of the whole stylesheet.
60
+ * @param {object} context The minification context to populate.
61
+ */
62
+ function recordStylesheetResetProperties (rules, context) {
63
+ const pendingRules = [...rules];
64
+ while (pendingRules.length) {
65
+ const rule = pendingRules.pop();
66
+ for (const declaration of rule.declarations || []) {
67
+ if (RESETTABLE_PROPERTIES.has(declaration.property)) {
68
+ context.stylesheetResetProperties.add(declaration.property);
69
+ }
70
+ }
71
+ pendingRules.push(...collectNestedRules(rule));
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Checks whether assembling a shorthand out of longhands would reset a property
77
+ * that the stylesheet sets somewhere else. The `border` shorthand resets
78
+ * `border-image`, so turning the border longhands of one rule into `border`
79
+ * cancels the `border-image` that another rule sets on the same element. A rule
80
+ * that states the reset property itself is safe, because that declaration is
81
+ * emitted after the shorthand and restates the value the shorthand discarded.
82
+ *
83
+ * @param {string} shorthandName The shorthand that would be assembled.
84
+ * @param {Set} declaredProperties The property names the rule declares.
85
+ * @param {object} context The minification context with the stylesheet's reset properties.
86
+ * @return {boolean} Whether assembling the shorthand would discard another rule's value.
87
+ */
88
+ function resetsPropertyDeclaredElsewhere (shorthandName, declaredProperties, context) {
89
+ const stylesheetResetProperties = context?.stylesheetResetProperties;
90
+ if (!stylesheetResetProperties?.size) {
91
+ return false;
92
+ }
93
+ for (const resetProperty of getOverridesOf(shorthandName)) {
94
+ const isSetElsewhere = (
95
+ stylesheetResetProperties.has(resetProperty) &&
96
+ !declaredProperties.has(resetProperty)
97
+ );
98
+ if (isSetElsewhere) {
99
+ return true;
100
+ }
101
+ }
102
+ return false;
103
+ }
104
+
105
+ export {
106
+ recordStylesheetResetProperties,
107
+ resetsPropertyDeclaredElsewhere
108
+ };
package/src/index.js CHANGED
@@ -13,6 +13,7 @@ import {
13
13
  createMinifyContext,
14
14
  setActiveCharset
15
15
  } from './context.js';
16
+ import { recordStylesheetResetProperties } from './declarations/reset-hazards.js';
16
17
  import {
17
18
  analyzePositionTryRules,
18
19
  cleanPositionTryRules,
@@ -231,6 +232,10 @@ export const minifyCSS = function (input) {
231
232
  const context = createMinifyContext();
232
233
 
233
234
  if (ast?.stylesheet?.rules) {
235
+ // Which properties a shorthand may not silently reset is a question about
236
+ // the whole stylesheet, so it is answered before any rule is rewritten.
237
+ recordStylesheetResetProperties(ast.stylesheet.rules, context);
238
+
234
239
  const {
235
240
  positionTryRules,
236
241
  positionTryUsage