@thejaredwilcurt/csslop 0.0.6 → 0.0.7

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.6",
5
+ "version": "0.0.7",
6
6
  "description": "Experimental CSS minification",
7
7
  "scripts": {
8
8
  "copy": "node ./scripts/copyTests.js",
@@ -27,6 +27,7 @@ import {
27
27
  roundCompactNumber
28
28
  } from './shared.js';
29
29
  import { minifyTransformValue } from './transforms.js';
30
+ import { optimizeUnicodeRange } from './unicode-range.js';
30
31
 
31
32
  /**
32
33
  * Map of position-area two-keyword values to their single-keyword equivalents.
@@ -692,27 +693,9 @@ function minifyValue (declaration) {
692
693
  val = minifyGradients(val);
693
694
  }
694
695
 
695
- // Unicode range compaction: U+0000-00FF -> U+??
696
+ // Unicode range optimization: dedup, merge overlapping/adjacent, wildcard compression
696
697
  if (declaration.property === 'unicode-range') {
697
- val = val.replace(/U\+([0-9a-fA-F]+)-([0-9a-fA-F]+)/gi, (match, startHex, endHex) => {
698
- const len = Math.max(startHex.length, endHex.length);
699
- const s = startHex.padStart(len, '0').toUpperCase();
700
- const e = endHex.padStart(len, '0').toUpperCase();
701
- let prefixLen = 0;
702
- while (prefixLen < len && s[prefixLen] === e[prefixLen]) {
703
- prefixLen++;
704
- }
705
- const suffixS = s.slice(prefixLen);
706
- const suffixE = e.slice(prefixLen);
707
- // Check if the suffix range spans all values (all-zeros start, all-F end) for wildcard replacement
708
- if (/^0*$/.test(suffixS) && /^F*$/i.test(suffixE)) {
709
- const wildcardCount = len - prefixLen;
710
- // Strip leading zeros from the common prefix
711
- const prefix = s.slice(0, prefixLen).replace(/^0+/, '');
712
- return 'U+' + prefix + '?'.repeat(wildcardCount);
713
- }
714
- return match;
715
- });
698
+ val = optimizeUnicodeRange(val);
716
699
  }
717
700
 
718
701
  return val;
@@ -0,0 +1,176 @@
1
+ /**
2
+ * @file Optimizes CSS unicode-range values by deduplicating, merging overlapping
3
+ * and adjacent ranges, collapsing consecutive single values into ranges, and
4
+ * applying wildcard compression.
5
+ */
6
+
7
+ /**
8
+ * Parses a single unicode-range token into a numeric start/end pair.
9
+ * Handles single values (U+1F170), explicit ranges (U+2000-2002),
10
+ * and wildcard notation (U+4??).
11
+ *
12
+ * @param {string} token A trimmed unicode-range token.
13
+ * @return {object|null} Object with numeric start and end, or null if unparseable.
14
+ */
15
+ function parseUnicodeRangeToken (token) {
16
+ const trimmed = token.trim();
17
+
18
+ // Match wildcard notation: U+ followed by optional hex prefix then one or more ?
19
+ const wildcardMatch = trimmed.match(/^U\+([0-9a-fA-F]*)(\?+)$/i);
20
+ if (wildcardMatch) {
21
+ const prefix = wildcardMatch[1] || '';
22
+ const wildcardCount = wildcardMatch[2].length;
23
+ const start = parseInt(prefix + '0'.repeat(wildcardCount), 16);
24
+ const end = parseInt(prefix + 'F'.repeat(wildcardCount), 16);
25
+ return { start, end };
26
+ }
27
+
28
+ // Match explicit range: U+XXXX-YYYY
29
+ const rangeMatch = trimmed.match(/^U\+([0-9a-fA-F]+)-([0-9a-fA-F]+)$/i);
30
+ if (rangeMatch) {
31
+ return {
32
+ start: parseInt(rangeMatch[1], 16),
33
+ end: parseInt(rangeMatch[2], 16)
34
+ };
35
+ }
36
+
37
+ // Match single code point: U+XXXX
38
+ const singleMatch = trimmed.match(/^U\+([0-9a-fA-F]+)$/i);
39
+ if (singleMatch) {
40
+ const codePoint = parseInt(singleMatch[1], 16);
41
+ return { start: codePoint, end: codePoint };
42
+ }
43
+
44
+ return null;
45
+ }
46
+
47
+ /**
48
+ * Sorts ranges by start value and merges any that overlap or are adjacent
49
+ * (where the gap between them is zero or negative after +1 adjustment).
50
+ *
51
+ * @param {Array} ranges Array of {start, end} objects.
52
+ * @return {Array} New array of merged {start, end} objects.
53
+ */
54
+ function mergeOverlappingRanges (ranges) {
55
+ if (ranges.length <= 1) {
56
+ return ranges.map((range) => {
57
+ return { start: range.start, end: range.end };
58
+ });
59
+ }
60
+
61
+ const sorted = [...ranges].sort((rangeA, rangeB) => {
62
+ return rangeA.start - rangeB.start || rangeB.end - rangeA.end;
63
+ });
64
+
65
+ const merged = [{ start: sorted[0].start, end: sorted[0].end }];
66
+ for (let index = 1; index < sorted.length; index++) {
67
+ const current = sorted[index];
68
+ const previous = merged[merged.length - 1];
69
+
70
+ if (current.start <= previous.end + 1) {
71
+ previous.end = Math.max(previous.end, current.end);
72
+ } else {
73
+ merged.push({ start: current.start, end: current.end });
74
+ }
75
+ }
76
+
77
+ return merged;
78
+ }
79
+
80
+ /**
81
+ * Attempts to express a numeric range as wildcard notation (e.g. U+5??).
82
+ * Wildcards are only possible when the suffix of the start is all zeros
83
+ * and the suffix of the end is all F's, meaning the range covers an
84
+ * entire block aligned to a power-of-16 boundary.
85
+ *
86
+ * @param {number} start The range start code point.
87
+ * @param {number} end The range end code point.
88
+ * @return {string|null} Wildcard string like "U+5??", or null if not representable.
89
+ */
90
+ function tryWildcardNotation (start, end) {
91
+ if (start > end) {
92
+ return null;
93
+ }
94
+
95
+ const startHex = start.toString(16).toUpperCase();
96
+ const endHex = end.toString(16).toUpperCase();
97
+ const digitCount = Math.max(startHex.length, endHex.length);
98
+ const paddedStart = startHex.padStart(digitCount, '0');
99
+ const paddedEnd = endHex.padStart(digitCount, '0');
100
+
101
+ let commonPrefixLength = 0;
102
+ while (commonPrefixLength < digitCount && paddedStart[commonPrefixLength] === paddedEnd[commonPrefixLength]) {
103
+ commonPrefixLength++;
104
+ }
105
+
106
+ const startSuffix = paddedStart.slice(commonPrefixLength);
107
+ const endSuffix = paddedEnd.slice(commonPrefixLength);
108
+
109
+ // Wildcard requires suffix to span the full 0-F range (all zeros to all F's)
110
+ const suffixIsAllZeros = /^0+$/.test(startSuffix);
111
+ const suffixIsAllFs = /^F+$/i.test(endSuffix);
112
+ if (startSuffix.length > 0 && suffixIsAllZeros && suffixIsAllFs) {
113
+ const wildcardCount = digitCount - commonPrefixLength;
114
+ // Strip leading zeros from the fixed-width prefix
115
+ const prefix = paddedStart.slice(0, commonPrefixLength).replace(/^0+/, '');
116
+ return 'U+' + prefix + '?'.repeat(wildcardCount);
117
+ }
118
+
119
+ return null;
120
+ }
121
+
122
+ /**
123
+ * Formats a single merged range back into the shortest valid unicode-range token.
124
+ * Tries wildcard notation first, then falls back to explicit range or single value.
125
+ *
126
+ * @param {number} start The range start code point.
127
+ * @param {number} end The range end code point.
128
+ * @return {string} The formatted unicode-range token.
129
+ */
130
+ function formatUnicodeRange (start, end) {
131
+ const wildcardForm = tryWildcardNotation(start, end);
132
+ if (wildcardForm) {
133
+ return wildcardForm;
134
+ }
135
+
136
+ const startHex = start.toString(16).toUpperCase();
137
+ if (start === end) {
138
+ return 'U+' + startHex;
139
+ }
140
+
141
+ const endHex = end.toString(16).toUpperCase();
142
+ return 'U+' + startHex + '-' + endHex;
143
+ }
144
+
145
+ /**
146
+ * Optimizes a CSS unicode-range value by parsing all tokens, merging
147
+ * overlapping/adjacent/duplicate ranges, and re-encoding with the
148
+ * shortest possible notation (wildcards where applicable).
149
+ *
150
+ * @param {string} value The raw unicode-range CSS value (comma-separated tokens).
151
+ * @return {string} The optimized unicode-range value.
152
+ */
153
+ function optimizeUnicodeRange (value) {
154
+ const tokens = value.split(',');
155
+
156
+ const ranges = [];
157
+ for (const token of tokens) {
158
+ const parsed = parseUnicodeRangeToken(token);
159
+ if (parsed) {
160
+ ranges.push(parsed);
161
+ }
162
+ }
163
+
164
+ if (ranges.length === 0) {
165
+ return value;
166
+ }
167
+
168
+ const merged = mergeOverlappingRanges(ranges);
169
+
170
+ const formatted = merged.map((range) => {
171
+ return formatUnicodeRange(range.start, range.end);
172
+ });
173
+ return formatted.join(',');
174
+ }
175
+
176
+ export { optimizeUnicodeRange };