@thejaredwilcurt/csslop 0.0.1

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.
@@ -0,0 +1,159 @@
1
+ /**
2
+ * @file CSS named color keyword to RGB channel mapping.
3
+ *
4
+ * Each entry maps a lowercase CSS color keyword to an [r, g, b] array
5
+ * with channel values in the 0–255 range, per the CSS Color Level 4 specification.
6
+ */
7
+
8
+ const namedColors = {
9
+ black: [0, 0, 0],
10
+ silver: [192, 192, 192],
11
+ gray: [128, 128, 128],
12
+ grey: [128, 128, 128],
13
+ white: [255, 255, 255],
14
+ maroon: [128, 0, 0],
15
+ red: [255, 0, 0],
16
+ purple: [128, 0, 128],
17
+ fuchsia: [255, 0, 255],
18
+ green: [0, 128, 0],
19
+ lime: [0, 255, 0],
20
+ olive: [128, 128, 0],
21
+ yellow: [255, 255, 0],
22
+ navy: [0, 0, 128],
23
+ blue: [0, 0, 255],
24
+ teal: [0, 128, 128],
25
+ aqua: [0, 255, 255],
26
+ orange: [255, 165, 0],
27
+ aliceblue: [240, 248, 255],
28
+ antiquewhite: [250, 235, 215],
29
+ aquamarine: [127, 255, 212],
30
+ azure: [240, 255, 255],
31
+ beige: [245, 245, 220],
32
+ bisque: [255, 228, 196],
33
+ blanchedalmond: [255, 235, 205],
34
+ blueviolet: [138, 43, 226],
35
+ brown: [165, 42, 42],
36
+ burlywood: [222, 184, 135],
37
+ cadetblue: [95, 158, 160],
38
+ chartreuse: [127, 255, 0],
39
+ chocolate: [210, 105, 30],
40
+ coral: [255, 127, 80],
41
+ cornflowerblue: [100, 149, 237],
42
+ cornsilk: [255, 248, 220],
43
+ crimson: [220, 20, 60],
44
+ cyan: [0, 255, 255],
45
+ darkblue: [0, 0, 139],
46
+ darkcyan: [0, 139, 139],
47
+ darkgoldenrod: [184, 134, 11],
48
+ darkgray: [169, 169, 169],
49
+ darkgreen: [0, 100, 0],
50
+ darkgrey: [169, 169, 169],
51
+ darkkhaki: [189, 183, 107],
52
+ darkmagenta: [139, 0, 139],
53
+ darkolivegreen: [85, 107, 47],
54
+ darkorange: [255, 140, 0],
55
+ darkorchid: [153, 50, 204],
56
+ darkred: [139, 0, 0],
57
+ darksalmon: [233, 150, 122],
58
+ darkseagreen: [143, 188, 143],
59
+ darkslateblue: [72, 61, 139],
60
+ darkslategray: [47, 79, 79],
61
+ darkslategrey: [47, 79, 79],
62
+ darkturquoise: [0, 206, 209],
63
+ darkviolet: [148, 0, 211],
64
+ deeppink: [255, 20, 147],
65
+ deepskyblue: [0, 191, 255],
66
+ dimgray: [105, 105, 105],
67
+ dimgrey: [105, 105, 105],
68
+ dodgerblue: [30, 144, 255],
69
+ firebrick: [178, 34, 34],
70
+ floralwhite: [255, 250, 240],
71
+ forestgreen: [34, 139, 34],
72
+ gainsboro: [220, 220, 220],
73
+ ghostwhite: [248, 248, 255],
74
+ gold: [255, 215, 0],
75
+ goldenrod: [218, 165, 32],
76
+ greenyellow: [173, 255, 47],
77
+ honeydew: [240, 255, 240],
78
+ hotpink: [255, 105, 180],
79
+ indianred: [205, 92, 92],
80
+ indigo: [75, 0, 130],
81
+ ivory: [255, 255, 240],
82
+ khaki: [240, 230, 140],
83
+ lavender: [230, 230, 250],
84
+ lavenderblush: [255, 240, 245],
85
+ lawngreen: [124, 252, 0],
86
+ lemonchiffon: [255, 250, 205],
87
+ lightblue: [173, 216, 230],
88
+ lightcoral: [240, 128, 128],
89
+ lightcyan: [224, 255, 255],
90
+ lightgoldenrodyellow: [250, 250, 210],
91
+ lightgray: [211, 211, 211],
92
+ lightgreen: [144, 238, 144],
93
+ lightgrey: [211, 211, 211],
94
+ lightpink: [255, 182, 193],
95
+ lightsalmon: [255, 160, 122],
96
+ lightseagreen: [32, 178, 170],
97
+ lightskyblue: [135, 206, 250],
98
+ lightslategray: [119, 136, 153],
99
+ lightslategrey: [119, 136, 153],
100
+ lightsteelblue: [176, 196, 222],
101
+ lightyellow: [255, 255, 224],
102
+ limegreen: [50, 205, 50],
103
+ linen: [250, 240, 230],
104
+ magenta: [255, 0, 255],
105
+ mediumaquamarine: [102, 205, 170],
106
+ mediumblue: [0, 0, 205],
107
+ mediumorchid: [186, 85, 211],
108
+ mediumpurple: [147, 111, 219],
109
+ mediumseagreen: [60, 179, 113],
110
+ mediumslateblue: [123, 104, 238],
111
+ mediumspringgreen: [0, 250, 154],
112
+ mediumturquoise: [72, 209, 204],
113
+ mediumvioletred: [199, 21, 133],
114
+ midnightblue: [25, 25, 112],
115
+ mintcream: [245, 255, 250],
116
+ mistyrose: [255, 228, 225],
117
+ moccasin: [255, 228, 181],
118
+ navajowhite: [255, 222, 173],
119
+ oldlace: [253, 245, 230],
120
+ olivedrab: [107, 142, 35],
121
+ orangered: [255, 69, 0],
122
+ orchid: [218, 112, 214],
123
+ palegoldenrod: [238, 232, 170],
124
+ palegreen: [152, 251, 152],
125
+ paleturquoise: [175, 238, 238],
126
+ palevioletred: [219, 112, 147],
127
+ papayawhip: [255, 239, 213],
128
+ peachpuff: [255, 218, 185],
129
+ peru: [205, 133, 63],
130
+ pink: [255, 192, 203],
131
+ plum: [221, 160, 221],
132
+ powderblue: [176, 224, 230],
133
+ rebeccapurple: [102, 51, 153],
134
+ rosybrown: [188, 143, 143],
135
+ royalblue: [65, 105, 225],
136
+ saddlebrown: [139, 69, 19],
137
+ salmon: [250, 128, 114],
138
+ sandybrown: [244, 164, 96],
139
+ seagreen: [46, 139, 87],
140
+ seashell: [255, 245, 238],
141
+ sienna: [160, 82, 45],
142
+ skyblue: [135, 206, 235],
143
+ slateblue: [106, 90, 205],
144
+ slategray: [112, 128, 144],
145
+ slategrey: [112, 128, 144],
146
+ snow: [255, 250, 250],
147
+ springgreen: [0, 255, 127],
148
+ steelblue: [70, 130, 180],
149
+ tan: [210, 180, 140],
150
+ thistle: [216, 191, 216],
151
+ tomato: [255, 99, 71],
152
+ turquoise: [64, 224, 208],
153
+ violet: [238, 130, 238],
154
+ wheat: [245, 222, 179],
155
+ whitesmoke: [245, 245, 245],
156
+ yellowgreen: [154, 205, 50]
157
+ };
158
+
159
+ export { namedColors };
@@ -0,0 +1,146 @@
1
+ /**
2
+ * @file Shared numeric formatting and unit conversion utilities used across CSS value minification.
3
+ */
4
+
5
+ /**
6
+ * Formats a number as a compact string, stripping leading zeros before decimal points (e.g. 0.5 becomes .5).
7
+ *
8
+ * @param {number|string} value The numeric value to format.
9
+ * @return {string} The compact string representation.
10
+ */
11
+ function formatCompactNumber (value) {
12
+ let result = String(Number(value));
13
+ if (result.startsWith('0.')) {
14
+ result = result.slice(1);
15
+ }
16
+ if (result.startsWith('-0.')) {
17
+ result = '-' + result.slice(2);
18
+ }
19
+ return result;
20
+ }
21
+
22
+ /**
23
+ * Converts a percentage scale component to its decimal equivalent (e.g. "150%" becomes "1.5"), or returns the value unchanged if it is not a percentage.
24
+ *
25
+ * @param {string} value The scale component string, possibly ending in %.
26
+ * @return {string} The normalized decimal string.
27
+ */
28
+ function normalizeScaleComponent (value) {
29
+ const trimmed = value.trim();
30
+ // Match a percentage value (e.g. "150%", "-50%") and capture the numeric portion
31
+ const percentMatch = trimmed.match(/^(-?(?:\d+|\d*\.\d+))%$/);
32
+ if (!percentMatch) {
33
+ return trimmed;
34
+ }
35
+ return formatCompactNumber(parseFloat(percentMatch[1]) / 100);
36
+ }
37
+
38
+ /**
39
+ * Rounds a number to the given decimal precision and formats it compactly, removing trailing zeros and leading zeros before the decimal point.
40
+ *
41
+ * @param {number|string} value The numeric value to round and format.
42
+ * @param {number} precision The number of decimal places to keep.
43
+ * @return {string} The rounded and compact string representation.
44
+ */
45
+ function roundCompactNumber (value, precision = 3) {
46
+ const number = typeof value === 'number' ? value : parseFloat(value);
47
+ if (!Number.isFinite(number)) {
48
+ return String(value);
49
+ }
50
+ // Strip trailing zeros and trailing decimal point from the fixed-precision string
51
+ let result = number.toFixed(precision).replace(/0+$/, '').replace(/\.$/, '');
52
+ if (result.startsWith('0.')) {
53
+ result = result.slice(1);
54
+ }
55
+ if (result.startsWith('-0.')) {
56
+ result = '-' + result.slice(2);
57
+ }
58
+ return result;
59
+ }
60
+
61
+ /**
62
+ * Converts an absolute CSS length value (pt, pc, in, cm, mm, q) to its pixel equivalent using standard conversion factors.
63
+ *
64
+ * @param {number|string} value The numeric length value to convert.
65
+ * @param {string} unit The CSS length unit (e.g. "pt", "in", "cm").
66
+ * @return {number|null} The pixel equivalent, or null if the unit is unrecognized or the value is not finite.
67
+ */
68
+ function convertAbsoluteLengthToPx (value, unit) {
69
+ const numeric = typeof value === 'number' ? value : parseFloat(value);
70
+ const conversionMap = {
71
+ px: 1,
72
+ pt: 96 / 72,
73
+ pc: 16,
74
+ in: 96,
75
+ cm: 96 / 2.54,
76
+ mm: 96 / 25.4,
77
+ q: 96 / 101.6
78
+ };
79
+ const factor = conversionMap[unit.toLowerCase()];
80
+ if (!factor || !Number.isFinite(numeric)) {
81
+ return null;
82
+ }
83
+ return numeric * factor;
84
+ }
85
+
86
+ /**
87
+ * Formats a numeric value with its CSS unit as a compact string, rounding to the specified precision.
88
+ *
89
+ * @param {number} value The numeric dimension value.
90
+ * @param {string} unit The CSS unit suffix (e.g. "px", "%").
91
+ * @param {number} precision The number of decimal places to keep.
92
+ * @return {string} The formatted dimension string.
93
+ */
94
+ function formatDimension (value, unit, precision = 3) {
95
+ return roundCompactNumber(value, precision) + unit;
96
+ }
97
+
98
+ /**
99
+ * Parses a CSS alpha string (e.g. "0.5", "50%") into a numeric 0–1 value.
100
+ * If the string is undefined or null, returns the provided fallback.
101
+ *
102
+ * @param {string|undefined} alphaStr The alpha string, optionally ending in "%".
103
+ * @param {number} fallback The value to return when alphaStr is absent.
104
+ * @return {number} The parsed alpha value in the 0–1 range.
105
+ */
106
+ function parseAlphaString (alphaStr, fallback = 1) {
107
+ if (alphaStr === undefined || alphaStr === null) {
108
+ return fallback;
109
+ }
110
+ if (alphaStr.endsWith('%')) {
111
+ return parseFloat(alphaStr) / 100;
112
+ }
113
+ return parseFloat(alphaStr);
114
+ }
115
+
116
+ /**
117
+ * Collapses redundant CSS shorthand parts using the standard box-model
118
+ * reduction rules: 4-value → 3-value → 2-value → 1-value.
119
+ *
120
+ * For example, `["10px", "5px", "10px", "5px"]` becomes `["10px", "5px"]`.
121
+ *
122
+ * @param {Array} parts The array of shorthand value strings to collapse in place.
123
+ * @return {Array} The same array, mutated with redundant entries removed.
124
+ */
125
+ function collapseShorthandParts (parts) {
126
+ if (parts.length === 4 && parts[1] === parts[3]) {
127
+ parts.splice(3, 1);
128
+ }
129
+ if (parts.length === 3 && parts[0] === parts[2]) {
130
+ parts.splice(2, 1);
131
+ }
132
+ if (parts.length === 2 && parts[0] === parts[1]) {
133
+ parts.splice(1, 1);
134
+ }
135
+ return parts;
136
+ }
137
+
138
+ export {
139
+ collapseShorthandParts,
140
+ convertAbsoluteLengthToPx,
141
+ formatCompactNumber,
142
+ formatDimension,
143
+ normalizeScaleComponent,
144
+ parseAlphaString,
145
+ roundCompactNumber
146
+ };
@@ -0,0 +1,222 @@
1
+ /**
2
+ * @file Minifies CSS transform values by simplifying individual transform functions, collapsing axes, and removing identity components.
3
+ */
4
+
5
+ import {
6
+ normalizeScaleComponent,
7
+ roundCompactNumber
8
+ } from './shared.js';
9
+
10
+ /**
11
+ * Checks if a transform function argument represents a zero value, regardless of what CSS unit is attached.
12
+ *
13
+ * @param {string} value The transform argument string to test.
14
+ * @return {boolean} True if the value is zero with any unit.
15
+ */
16
+ function isZeroTransformValue (value) {
17
+ return /^[-+]?0(?:[a-z%]+)?$/i.test(value.trim());
18
+ }
19
+
20
+ /**
21
+ * Splits a CSS function's comma-separated argument string into an array, correctly handling nested parentheses.
22
+ *
23
+ * @param {string} value The raw arguments string inside the function parentheses.
24
+ * @return {Array} An array of trimmed argument strings.
25
+ */
26
+ function splitFunctionArguments (value) {
27
+ const parts = [];
28
+ let depth = 0;
29
+ let current = '';
30
+ for (const character of value) {
31
+ if (character === '(') {
32
+ depth++;
33
+ }
34
+ if (character === ')') {
35
+ depth--;
36
+ }
37
+ if (character === ',' && depth === 0) {
38
+ parts.push(current.trim());
39
+ current = '';
40
+ } else {
41
+ current += character;
42
+ }
43
+ }
44
+ if (current.trim() || parts.length) {
45
+ parts.push(current.trim());
46
+ }
47
+ return parts;
48
+ }
49
+
50
+ /**
51
+ * Parses a CSS transform value string into an array of objects, each containing a function name and its raw arguments string.
52
+ *
53
+ * @param {string} value The full CSS transform value string.
54
+ * @return {Array|null} An array of { name, args } objects, or null if the string cannot be parsed.
55
+ */
56
+ function splitTransformFunctions (value) {
57
+ const parts = [];
58
+ let position = 0;
59
+ while (position < value.length) {
60
+ // Skip whitespace between transform functions
61
+ while (position < value.length && /\s/.test(value[position])) {
62
+ position++;
63
+ }
64
+ if (position >= value.length) {
65
+ break;
66
+ }
67
+ const nameStart = position;
68
+ // Consume alphanumeric and hyphen characters for the function name
69
+ while (position < value.length && /[A-Za-z0-9-]/.test(value[position])) {
70
+ position++;
71
+ }
72
+ if (nameStart === position || value[position] !== '(') {
73
+ return null;
74
+ }
75
+ const name = value.slice(nameStart, position);
76
+ let depth = 1;
77
+ let end = position + 1;
78
+ while (end < value.length && depth > 0) {
79
+ if (value[end] === '(') {
80
+ depth++;
81
+ }
82
+ if (value[end] === ')') {
83
+ depth--;
84
+ }
85
+ end++;
86
+ }
87
+ if (depth !== 0) {
88
+ return null;
89
+ }
90
+ parts.push({ name, args: value.slice(position + 1, end - 1) });
91
+ position = end;
92
+ }
93
+ return parts;
94
+ }
95
+
96
+ /**
97
+ * Minifies a single CSS transform function by simplifying 3D functions to 2D equivalents, collapsing redundant axes, and normalizing scale percentages.
98
+ *
99
+ * @param {string} name The transform function name (e.g. "translate3d", "scale").
100
+ * @param {string} args The raw comma-separated arguments string.
101
+ * @return {string} The minified transform function call string.
102
+ */
103
+ function minifyTransformFunction (name, args) {
104
+ const lowerName = name.toLowerCase();
105
+ const parts = splitFunctionArguments(args);
106
+
107
+ if (lowerName === 'rotate' && parts.length === 1) {
108
+ // Convert turn units to degrees (e.g. 0.5turn → 180deg)
109
+ const angle = parts[0].replace(/^(-?(?:\d+|\d*\.\d+))turn$/i, (_, turns) => {
110
+ return roundCompactNumber(parseFloat(turns) * 360) + 'deg';
111
+ });
112
+ return 'rotate(' + angle + ')';
113
+ }
114
+
115
+ if (lowerName === 'translate3d' && parts.length === 3) {
116
+ const [x, y, z] = parts;
117
+ if (isZeroTransformValue(y) && isZeroTransformValue(z)) {
118
+ return 'translate(' + x + ')';
119
+ }
120
+ if (isZeroTransformValue(x) && isZeroTransformValue(z)) {
121
+ return 'translateY(' + y + ')';
122
+ }
123
+ if (isZeroTransformValue(x) && isZeroTransformValue(y)) {
124
+ return 'translateZ(' + z + ')';
125
+ }
126
+ return name + '(' + parts.join(',') + ')';
127
+ }
128
+
129
+ if (lowerName === 'translate' && parts.length === 2) {
130
+ const [x, y] = parts;
131
+ if (isZeroTransformValue(x)) {
132
+ return 'translateY(' + y + ')';
133
+ }
134
+ if (isZeroTransformValue(y)) {
135
+ return 'translate(' + x + ')';
136
+ }
137
+ return name + '(' + parts.join(',') + ')';
138
+ }
139
+
140
+ if (lowerName === 'scale') {
141
+ const normalized = parts.map(normalizeScaleComponent);
142
+ if (normalized.length === 1) {
143
+ return 'scale(' + normalized[0] + ')';
144
+ }
145
+ if (normalized.length === 2) {
146
+ const [x, y] = normalized;
147
+ if (x === y) {
148
+ return 'scale(' + x + ')';
149
+ }
150
+ if (y === '1') {
151
+ return 'scaleX(' + x + ')';
152
+ }
153
+ if (x === '1') {
154
+ return 'scaleY(' + y + ')';
155
+ }
156
+ return 'scale(' + normalized.join(',') + ')';
157
+ }
158
+ }
159
+
160
+ if (lowerName === 'scale3d' && parts.length === 3) {
161
+ const normalized = parts.map(normalizeScaleComponent);
162
+ const [x, y, z] = normalized;
163
+ if (z === '1') {
164
+ return minifyTransformFunction('scale', x + ',' + y);
165
+ }
166
+ if (y === '1' && z === '1') {
167
+ return 'scaleX(' + x + ')';
168
+ }
169
+ if (x === '1' && z === '1') {
170
+ return 'scaleY(' + y + ')';
171
+ }
172
+ if (x === '1' && y === '1') {
173
+ return 'scaleZ(' + z + ')';
174
+ }
175
+ return 'scale3d(' + normalized.join(',') + ')';
176
+ }
177
+
178
+ if (lowerName === 'rotatez' && parts.length === 1) {
179
+ return 'rotate(' + parts[0] + ')';
180
+ }
181
+
182
+ if (lowerName === 'rotate3d' && parts.length === 4) {
183
+ const [x, y, z, angle] = parts;
184
+ if (x === '1' && y === '0' && z === '0') {
185
+ return 'rotateX(' + angle + ')';
186
+ }
187
+ if (x === '0' && y === '1' && z === '0') {
188
+ return 'rotateY(' + angle + ')';
189
+ }
190
+ if (x === '0' && y === '0' && z === '1') {
191
+ return 'rotate(' + angle + ')';
192
+ }
193
+ return name + '(' + parts.join(',') + ')';
194
+ }
195
+
196
+ if (lowerName.startsWith('scale')) {
197
+ return name + '(' + parts.map(normalizeScaleComponent).join(',') + ')';
198
+ }
199
+
200
+ return name + '(' + parts.join(',') + ')';
201
+ }
202
+
203
+ /**
204
+ * Minifies an entire CSS transform value by parsing each function call and applying per-function optimizations.
205
+ *
206
+ * @param {string} value The full CSS transform value string.
207
+ * @return {string} The minified transform value, or the original value if parsing fails.
208
+ */
209
+ function minifyTransformValue (value) {
210
+ if (!value || value === 'none') {
211
+ return value;
212
+ }
213
+ const functions = splitTransformFunctions(value);
214
+ if (!functions) {
215
+ return value;
216
+ }
217
+ return functions.map(({ name, args }) => {
218
+ return minifyTransformFunction(name, args);
219
+ }).join(' ');
220
+ }
221
+
222
+ export { minifyTransformValue };