@thejaredwilcurt/csslop 0.0.45 → 0.0.46

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.45",
5
+ "version": "0.0.46",
6
6
  "description": "Experimental CSS minification",
7
7
  "scripts": {
8
8
  "prestart": "node ./scripts/pre/start.js",
@@ -23,9 +23,9 @@
23
23
  "proto": "proto pin node latest --resolve --tool-native && proto pin npm latest --resolve --tool-native && git status"
24
24
  },
25
25
  "devDependencies": {
26
- "@codemirror/autocomplete": "^6.20.3",
26
+ "@codemirror/autocomplete": "^6.20.4",
27
27
  "@codemirror/lang-css": "^6.3.1",
28
- "@codemirror/view": "^6.43.14",
28
+ "@codemirror/view": "^6.43.15",
29
29
  "@eslint/js": "^10.0.1",
30
30
  "@node-projects/css-parser": "^5.2.0",
31
31
  "@stylistic/eslint-plugin": "^5.10.0",
@@ -35,7 +35,7 @@
35
35
  "eslint-config-tjw-import-x": "^1.0.1",
36
36
  "eslint-config-tjw-jsdoc": "^2.0.1",
37
37
  "eslint-plugin-import-x": "^4.17.0",
38
- "eslint-plugin-jsdoc": "^65.3.11",
38
+ "eslint-plugin-jsdoc": "^65.4.0",
39
39
  "fflate": "^0.8.3",
40
40
  "globals": "^17.13.0",
41
41
  "pretty-ms": "^10.0.0",
@@ -162,8 +162,8 @@ function startsUrlToken (source, index) {
162
162
 
163
163
  /**
164
164
  * Finds the first of several delimiter characters that is written at the top
165
- * level of the text, meaning it is not escaped, quoted, or nested inside a
166
- * group.
165
+ * level of the text, meaning it is not escaped, quoted, inside a comment, or
166
+ * nested inside a group.
167
167
  *
168
168
  * @param {string} source The raw CSS text being scanned.
169
169
  * @param {string} delimiters The delimiter characters to look for.
@@ -174,6 +174,11 @@ function findTopLevelDelimiter (source, delimiters, startIndex) {
174
174
  let index = startIndex;
175
175
  while (index < source.length) {
176
176
  const character = source[index];
177
+ if (character === '/' && source[index + 1] === '*') {
178
+ const closingIndex = source.indexOf('*/', index + 2);
179
+ index = closingIndex === -1 ? source.length : closingIndex + 2;
180
+ continue;
181
+ }
177
182
  if (character === '\\') {
178
183
  index += 2;
179
184
  continue;
@@ -262,6 +262,9 @@ function createNestedSelectorMatcher (parentSelector) {
262
262
 
263
263
  return (childSelector) => {
264
264
  const child = childSelector.trim();
265
+ if (child === parent) {
266
+ return '&';
267
+ }
265
268
  if (child.startsWith(parent + ':') || child.startsWith(parent + '::')) {
266
269
  return '&' + child.slice(parent.length);
267
270
  }
@@ -497,7 +500,7 @@ function factorCommonParents (rules) {
497
500
  while (index < rules.length) {
498
501
  const rule = rules[index];
499
502
  if (rule.type === 'rule' && rule.selectors?.length > 1) {
500
- const candidateParent = extractLeadingCompound(rule.selectors[0]);
503
+ const candidateParent = extractLeadingCompound(rule.selectors[0]) || rule.selectors[0];
501
504
  if (candidateParent) {
502
505
  const matchNestedSelector = createNestedSelectorMatcher(candidateParent);
503
506
  const childSelectors = rule.selectors.map(matchNestedSelector);
@@ -571,8 +574,8 @@ function factorCommonParents (rules) {
571
574
  }
572
575
 
573
576
  /**
574
- * Shares a descendant prefix through `:is()` when the parent itself is also
575
- * in the selector list, preventing a nesting wrapper from saving space.
577
+ * Shares a descendant prefix through `:is()` when a nesting wrapper did not
578
+ * save enough space to factor the parent out of the selector list.
576
579
  *
577
580
  * @param {object} rule The style rule whose selector list may be shortened.
578
581
  */
@@ -3,8 +3,15 @@
3
3
  */
4
4
 
5
5
  import { processDeclarations } from '../declarations/process.js';
6
- import { splitTopLevelCommaList } from '../parser/source-search.js';
6
+ import {
7
+ skipParenthesizedGroup,
8
+ skipQuotedString,
9
+ splitTopLevelCommaList,
10
+ startsUrlToken
11
+ } from '../parser/source-search.js';
12
+ import { tokenizeCssValue } from '../parser/tokenizer.js';
7
13
  import { minifyValueForOutput } from '../value/minify.js';
14
+ import { elideRedundantSeparators } from '../value/tokens.js';
8
15
 
9
16
  import {
10
17
  collapseCustomPropertyWhitespace,
@@ -111,6 +118,67 @@ function minifyFunctionPrelude (prelude) {
111
118
  return functionName + '(' + minifiedParameters.join(',') + ')';
112
119
  }
113
120
 
121
+ /**
122
+ * Removes comments between namespace tokens while leaving strings, URLs, and
123
+ * escaped characters intact.
124
+ *
125
+ * @param {string} prelude The namespace prelude as written.
126
+ * @return {string} The prelude with comments replaced by separators.
127
+ */
128
+ function removeNamespaceComments (prelude) {
129
+ let result = '';
130
+ let index = 0;
131
+ while (index < prelude.length) {
132
+ if (prelude.startsWith('/*', index)) {
133
+ const closingIndex = prelude.indexOf('*/', index + 2);
134
+ index = closingIndex === -1 ? prelude.length : closingIndex + 2;
135
+ result += ' ';
136
+ continue;
137
+ }
138
+ let end = index + 1;
139
+ if (prelude[index] === '"' || prelude[index] === '\'') {
140
+ end = skipQuotedString(prelude, index);
141
+ } else if (prelude[index] === '\\') {
142
+ end = Math.min(index + 2, prelude.length);
143
+ } else if (startsUrlToken(prelude, index)) {
144
+ end = skipParenthesizedGroup(prelude, index + 3);
145
+ }
146
+ result += prelude.slice(index, end);
147
+ index = end;
148
+ }
149
+ return result;
150
+ }
151
+
152
+ /**
153
+ * Unwraps quoted namespace URLs and removes token separators without changing
154
+ * whitespace or escapes inside the namespace name's string.
155
+ *
156
+ * @param {string} prelude The optional namespace prefix and its URL.
157
+ * @return {string} The minified namespace statement.
158
+ */
159
+ function stringifyNamespace (prelude) {
160
+ const tokens = tokenizeCssValue(removeNamespaceComments(prelude)).filter((token) => {
161
+ return token.type !== 'whitespace';
162
+ });
163
+ const parts = ['@namespace'];
164
+ for (let index = 0; index < tokens.length; index++) {
165
+ const token = tokens[index];
166
+ const isQuotedUrl = (
167
+ token.type === 'function' &&
168
+ token.text.toLowerCase() === 'url(' &&
169
+ tokens[index + 1]?.type === 'string' &&
170
+ tokens[index + 2]?.text === ')'
171
+ );
172
+ if (isQuotedUrl) {
173
+ parts.push(tokens[index + 1].text);
174
+ index += 2;
175
+ } else {
176
+ parts.push(token.text);
177
+ }
178
+ }
179
+ return elideRedundantSeparators(parts.join(' ')) + ';';
180
+ }
181
+
114
182
  /**
115
183
  * Stringifies a generic `at-rule` AST node into minified CSS, with specialized
116
184
  * prelude minification for `@function` rules and generic whitespace handling
@@ -466,6 +534,10 @@ function stringifyRule (rule, context) {
466
534
  return '@charset ' + rule.charset + ';';
467
535
  }
468
536
 
537
+ if (rule.type === 'namespace') {
538
+ return stringifyNamespace(rule.namespace);
539
+ }
540
+
469
541
  if (rule.type === 'import') {
470
542
  let importStatement = rule.import;
471
543
  // Unwrap url() with a quoted string to just the quoted string