@thejaredwilcurt/csslop 0.0.48 → 0.0.49
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 +3 -3
- package/package.json +3 -3
- package/src/declarations/css-wide-keywords.js +37 -1
- package/src/declarations/fold.js +13 -3
- package/src/declarations/process.js +60 -7
- package/src/index.js +8 -3
- package/src/parser/source-search.js +6 -0
- package/src/rules/optimize.js +422 -139
- package/src/rules/selectors.js +9 -0
- package/src/rules/stringify.js +138 -76
- package/src/utilities.js +0 -13
- package/src/value/minify.js +43 -17
package/README.md
CHANGED
|
@@ -157,7 +157,7 @@ These tools were prompted to pass the tests in the `/copiedTests` folder that ca
|
|
|
157
157
|
* While going through hundreds of old repos from the 2010's and checking the license in each repo I found almost all of them use MIT. Occasionally I'd find something different, one specific repo left a comment for why they chose their less common license:
|
|
158
158
|
* > This work is licensed under a [Creative Commons Attribution 3.0 Unported License](http://creativecommons.org/licenses/by/3.0/). (I love this license! The license summary doesn't have any paragraphs in all-caps that seems like the license is angry at you)
|
|
159
159
|
* I then spot checked a few of the 150 minified files to find bugs with the minifier and cases where it could have minified something better. I created issues upstream for all of these. If CSSLOP is passing 100% of the tests, but still outputting these bugs/missing optimizations, then the solution should be to add more tests around those edge cases. This is also beneficial to ALL CSS minifiers. The sloppy "do the minimum to pass the test" approach the AI took, is actually really great for finding missing tests. AI was not used in this evaluation or anything related to interacting with the upstream tests repo (issues or PRs).
|
|
160
|
-
* When testing the realworld CSS files, I found there were about 6 files where the output was the same as the input, and 5 where the output was empty. I created a small script to replicate this and throw
|
|
160
|
+
* When testing the realworld CSS files, I found there were about 6 files where the output was the same as the input, and 5 where the output was empty. I created a small script to replicate this and throw an error if the minification resulted in either of these outcomes, then gave a prompt to Gemini.
|
|
161
161
|
* **PROMPT:** When running this minification library on the `example/failing.css` file, the output should be minified, however it is not (`example/failing.min.css`). Investigate why. Then correct the issue by changing files in the `src` folder. Run `npm run fail` to see if the issue is resolved. When solving this problem, do not use naive solutions, hacks, or hard coded values. Make sure the implementation not only works for this specific case, but generally for any similar case. Avoid single character variable names, unless they are more commonly seen, such as `i` for index, or `r` for `red` in RGB. Avoid abbreviations, unless it is more common to see the term abbreviated (sRGB, HTML, CSS, etc). Group related logic into well named functions. Ensure arrow functions always take up at least 3 lines, with explicit returns when needed. Always comment regex if used. When finished, ensure that no tests fail after making your changes by running `npm t` and `npm run lint`. If anything is failing, make the needed corrections.
|
|
162
162
|
* It did okay, and solved the smaller/simpler issues, but then got stuck on a giant file that was too large for it to understand. The problem with the simpler files had to do with invalid CSS syntax in the real-world files. The AI's solution was to look for these specific syntax errors and patch the input CSS to fix them before they are sent to the parser. Interesting.
|
|
163
163
|
* Gave the prompt over to Claude Opus and was able to make more complex changes to get all the files to minify.
|
|
@@ -172,7 +172,7 @@ These tools were prompted to pass the tests in the `/copiedTests` folder that ca
|
|
|
172
172
|
* **PROMPT**: Do a refactoring pass over the files in the `src` folder. Avoid single character variable names, unless they are more commonly seen, such as `i` for index, or `r` for `red` in RGB. Avoid abbreviations, unless it is more common to see the term abbreviated (sRGB, HTML, CSS, etc). Group related logic into well named functions. Ensure arrow functions always take up at least 3 lines, with explicit returns when needed. Always comment regex if used. Run `npm run lint` and ensure the linter passes when done.
|
|
173
173
|
* Despite specifically telling it to run `npm run lint` to make the linter pass (which would require it to fill out all the JSDocs), it didn't do that. Instead, claude briefly skimmed the ESLint config file, and moved on..... Okay, let's FORCE IT to fix the linting errors:
|
|
174
174
|
* **PROMPT:** Fill out the description, types, and arguments/return details for all JSDoc comment blocks in the `src` folder. Add in a 1-2 sentance summary in the `@file` block on any files that are missing it.
|
|
175
|
-
* Okay, so GPT's changes look promising, it's passing all tests, passing the linter, and it added some new files related to worker threads. Though it did out
|
|
175
|
+
* Okay, so GPT's changes look promising, it's passing all tests, passing the linter, and it added some new files related to worker threads. Though it did opt-out of using promises, because it didn't want to change the entrypoint function of the library to be async. This would be a breaking change for library consumers, but I was aware of that when I told it to do it and it ignored me, so whatever.
|
|
176
176
|
* On to actually testing it! Before the optimizations, the 150 files took 3 hours and 4 minutes to run, and now with the new and improved optimizations, it only takes 3 hours and 20 minutes. Also the total minified filesize increased by 0.05%. So that's cool.... Dumping those changes.
|
|
177
177
|
* **KILL ALL HUMANS:**
|
|
178
178
|
* More libraries were added to the "Real World CSS Libraries". Now when I run `npm run real` to minify all of them, it ends up taking about 7 hours to run. This is because the optimizations CSSLOP performs do not scale linearly with more CSS rules, they scale exponentially. There are a handful of large CSS files that each take about an hour to complete, where as the smaller files all go by pretty fast.
|
|
@@ -187,7 +187,7 @@ These tools were prompted to pass the tests in the `/copiedTests` folder that ca
|
|
|
187
187
|
* Follow up, It randomly decided to ignore my instructions about running real-world tests, and did *most*, but not *all* of them, to double-check it's work. See [v0.0.33](https://github.com/TheJaredWilcurt/csslop/releases/tag/v0.0.33) for another instance of clerverly circumventing my instructions in order to kill humans.
|
|
188
188
|
* **Custom CSS Parser:** Since the performance fixes got the speed down from 7 to 2 hours, then said the rest of the slowness came from the parser, I removed the parser and stubbed in a dummy helper function for the AI to flesh out.
|
|
189
189
|
* **PROMPT:** The code in the `/src` folder relies on an imaginary CSS parser function. It has expecations around an API. Only modify/create files in the `src/parser` folder. Run `npm t` to pass in different CSS inputs, that if the parser works correctly would match the expected outputs. Try to get as many tests to pass as you can. Do not use naive solutions, hacks, or hard coded values. Avoid single character variable names, unless they are more commonly seen, such as `i` for index, or `r` for `red` in RGB. Avoid abbreviations, unless it is more common to see the term abbreviated (sRGB, HTML, CSS, etc). Group related logic into well named functions. Ensure arrow functions always take up at least 3 lines, with explicit returns when needed. Always comment regex if used. Run `npm run lint` and correct any linter warnings/errors that occur in the `/src` folder. Do not interact with anything related to "real world" tests, that is out of scope. Run `beep` when finished.
|
|
190
|
-
*
|
|
190
|
+
* Claude Opus 5 High reimplemented the bare minimum for a parser to recreate an API that matches the expectations of the existing code and nothing more. Now the 2 hour long real world tests take 20 seconds. I did a follow up prompt to do some cleanup.
|
|
191
191
|
* **PROMPT:** Review the files in the `/src` folder. If there is anything outside of `/src/parser` that would make more sense to exist in the parser code, move it there. Simplify as best you can. Do not use naive solutions, hacks, or hard coded values. Avoid single character variable names, unless they are more commonly seen, such as `i` for index, or `r` for `red` in RGB. Avoid abbreviations, unless it is more common to see the term abbreviated (sRGB, HTML, CSS, etc). Group related logic into well named functions. Ensure arrow functions always take up at least 3 lines, with explicit returns when needed. Always comment regex if used. Run `npm t` and `npm run lint` and fix any issues. Run `beep` when finished to alert me.
|
|
192
192
|
|
|
193
193
|
## The name
|
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.
|
|
5
|
+
"version": "0.0.49",
|
|
6
6
|
"description": "Experimental CSS minification",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"prestart": "node ./scripts/pre/start.js",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"@node-projects/css-parser": "^5.2.0",
|
|
31
31
|
"@stylistic/eslint-plugin": "^5.10.0",
|
|
32
32
|
"codemirror": "^6.0.2",
|
|
33
|
-
"eslint": "^10.
|
|
33
|
+
"eslint": "^10.14.0",
|
|
34
34
|
"eslint-config-tjw-base": "^5.0.0",
|
|
35
35
|
"eslint-config-tjw-import-x": "^1.0.1",
|
|
36
36
|
"eslint-config-tjw-jsdoc": "^2.0.1",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"fflate": "^0.8.3",
|
|
40
40
|
"globals": "^17.13.0",
|
|
41
41
|
"pretty-ms": "^10.0.0",
|
|
42
|
-
"real-world-css-libraries": "^1.0.
|
|
42
|
+
"real-world-css-libraries": "^1.0.9",
|
|
43
43
|
"vite": "^8.3.5"
|
|
44
44
|
},
|
|
45
45
|
"author": "The Jared Wilcurt",
|
|
@@ -2,6 +2,8 @@
|
|
|
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
|
+
|
|
5
7
|
import {
|
|
6
8
|
CSS_WIDE_KEYWORDS,
|
|
7
9
|
expandToLeafProperties,
|
|
@@ -15,6 +17,14 @@ import {
|
|
|
15
17
|
} from './lookup.js';
|
|
16
18
|
import { resetsPropertyDeclaredElsewhere } from './reset-hazards.js';
|
|
17
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Every shorthand name, in declaration order. The shorthand tables never
|
|
22
|
+
* change, so the names are listed once rather than once per rule.
|
|
23
|
+
*
|
|
24
|
+
* @type {Array<string>}
|
|
25
|
+
*/
|
|
26
|
+
const SHORTHAND_NAMES = Object.keys(shorthandMap);
|
|
27
|
+
|
|
18
28
|
/**
|
|
19
29
|
* Checks whether a group of longhands sets every leaf longhand that the
|
|
20
30
|
* shorthand sets. A CSS-wide keyword may only move into the shorthand when the
|
|
@@ -227,6 +237,29 @@ function rewriteGroupAsKeywordShorthand (declarations, shorthandName, declaredPr
|
|
|
227
237
|
return result;
|
|
228
238
|
}
|
|
229
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Checks whether any declaration of a rule could be set to a CSS-wide keyword.
|
|
242
|
+
* Every rewrite needs a longhand holding one, and a rewrite only adds the
|
|
243
|
+
* keyword that was already there, so a rule without any has nothing to hoist
|
|
244
|
+
* and every shorthand family can be skipped at once. A value that is not a
|
|
245
|
+
* string cannot be judged here, so it is assumed to hold a keyword.
|
|
246
|
+
*
|
|
247
|
+
* @param {Array} declarations The declarations of a single rule.
|
|
248
|
+
* @return {boolean} False when no declaration holds a CSS-wide keyword.
|
|
249
|
+
*/
|
|
250
|
+
function mayDeclareCssWideKeyword (declarations) {
|
|
251
|
+
return declarations.some((declaration) => {
|
|
252
|
+
if (!declaration.property) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
if (typeof declaration.value !== 'string') {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
const value = minifyValue(declaration).replace('!important', '').trim();
|
|
259
|
+
return CSS_WIDE_KEYWORDS.has(value.toLowerCase());
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
230
263
|
/**
|
|
231
264
|
* Hoists a CSS-wide keyword such as `inherit` out of a group of longhands into
|
|
232
265
|
* their shorthand. A CSS-wide keyword is only valid as a declaration's whole
|
|
@@ -240,6 +273,9 @@ function rewriteGroupAsKeywordShorthand (declarations, shorthandName, declaredPr
|
|
|
240
273
|
* @return {Array} The declarations, with eligible groups rewritten.
|
|
241
274
|
*/
|
|
242
275
|
function hoistCssWideKeywordsIntoShorthands (declarations, context) {
|
|
276
|
+
if (!mayDeclareCssWideKeyword(declarations)) {
|
|
277
|
+
return declarations;
|
|
278
|
+
}
|
|
243
279
|
let result = declarations;
|
|
244
280
|
// Every shorthand needs to know which properties the rule declares, so that
|
|
245
281
|
// set is kept alongside the declarations and only rebuilt after a rewrite
|
|
@@ -247,7 +283,7 @@ function hoistCssWideKeywordsIntoShorthands (declarations, context) {
|
|
|
247
283
|
let declaredProperties = collectDeclaredProperties(result);
|
|
248
284
|
// Shorthands are visited in declaration order, so the widest shorthand of a
|
|
249
285
|
// family is rewritten before the narrower shorthands it contains.
|
|
250
|
-
for (const shorthandName of
|
|
286
|
+
for (const shorthandName of SHORTHAND_NAMES) {
|
|
251
287
|
const rewritten = rewriteGroupAsKeywordShorthand(result, shorthandName, declaredProperties, context);
|
|
252
288
|
if (rewritten) {
|
|
253
289
|
result = rewritten;
|
package/src/declarations/fold.js
CHANGED
|
@@ -7,9 +7,13 @@ import { splitTopLevelComponents } from '../parser/source-search.js';
|
|
|
7
7
|
import {
|
|
8
8
|
CSS_WIDE_KEYWORDS,
|
|
9
9
|
expandToLeafProperties,
|
|
10
|
+
getLonghandsOf,
|
|
10
11
|
shorthandMap
|
|
11
12
|
} from './config.js';
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
collectDeclaredProperties,
|
|
15
|
+
describeDeclaration
|
|
16
|
+
} from './lookup.js';
|
|
13
17
|
import { canMergeVarValue } from './merge.js';
|
|
14
18
|
import { buildShorthandValue } from './shorthand-values.js';
|
|
15
19
|
|
|
@@ -107,7 +111,7 @@ function belongsToFamily (property, familyWords) {
|
|
|
107
111
|
* @return {FoldCandidate|null} The declarations to fold, or null when there are none.
|
|
108
112
|
*/
|
|
109
113
|
function findFoldCandidate (declarations, shorthandName) {
|
|
110
|
-
const longhandProperties =
|
|
114
|
+
const longhandProperties = getLonghandsOf(shorthandName);
|
|
111
115
|
const shorthandEntries = [];
|
|
112
116
|
const overrideEntries = [];
|
|
113
117
|
const overriddenProperties = new Set();
|
|
@@ -332,9 +336,15 @@ function foldOverridesIntoShorthand (declarations, shorthandName, context) {
|
|
|
332
336
|
* @return {Array} The declarations, with eligible families folded.
|
|
333
337
|
*/
|
|
334
338
|
function foldLonghandOverridesIntoShorthands (declarations, context) {
|
|
339
|
+
// A fold only ever rewrites its own shorthand and drops that shorthand's
|
|
340
|
+
// longhands, so the shorthands a rule declares are gathered once up front,
|
|
341
|
+
// and every family without its shorthand present is skipped.
|
|
342
|
+
const declaredProperties = collectDeclaredProperties(declarations);
|
|
335
343
|
let result = declarations;
|
|
336
344
|
for (const shorthandName of POSITIONAL_SHORTHAND_NAMES) {
|
|
337
|
-
|
|
345
|
+
if (declaredProperties.has(shorthandName)) {
|
|
346
|
+
result = foldOverridesIntoShorthand(result, shorthandName, context);
|
|
347
|
+
}
|
|
338
348
|
}
|
|
339
349
|
return result;
|
|
340
350
|
}
|
|
@@ -38,6 +38,57 @@ import { resetsPropertyDeclaredElsewhere } from './reset-hazards.js';
|
|
|
38
38
|
*/
|
|
39
39
|
const MIXED_IMPORTANT_SHORTHANDS = new Set(['margin', 'padding', 'inset']);
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Every shorthand paired with its longhands, in declaration order. The
|
|
43
|
+
* shorthand tables never change, so the pairs are listed once rather than on
|
|
44
|
+
* every pass over every rule.
|
|
45
|
+
*
|
|
46
|
+
* @type {Array}
|
|
47
|
+
*/
|
|
48
|
+
const SHORTHAND_ENTRIES = Object.entries(shorthandMap);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Lists, for each longhand, every shorthand it belongs to.
|
|
52
|
+
*
|
|
53
|
+
* @return {Map<string, Array<string>>} Map of longhand name to the shorthand names that include it.
|
|
54
|
+
*/
|
|
55
|
+
function indexShorthandsByLonghand () {
|
|
56
|
+
const shorthandsByLonghand = new Map();
|
|
57
|
+
for (const [shorthand, longhands] of SHORTHAND_ENTRIES) {
|
|
58
|
+
for (const longhand of longhands) {
|
|
59
|
+
const shorthands = shorthandsByLonghand.get(longhand) || [];
|
|
60
|
+
shorthands.push(shorthand);
|
|
61
|
+
shorthandsByLonghand.set(longhand, shorthands);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return shorthandsByLonghand;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The shorthands each longhand belongs to. The shorthand tables never change,
|
|
69
|
+
* so the index is built once.
|
|
70
|
+
*
|
|
71
|
+
* @type {Map<string, Array<string>>}
|
|
72
|
+
*/
|
|
73
|
+
const SHORTHANDS_BY_LONGHAND = indexShorthandsByLonghand();
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Collects the shorthands a rule declares at least one longhand of, which are
|
|
77
|
+
* the only shorthands that could be assembled from its declarations.
|
|
78
|
+
*
|
|
79
|
+
* @param {Set} declaredProperties The property names the rule currently declares.
|
|
80
|
+
* @return {Set} The names of the shorthands worth trying.
|
|
81
|
+
*/
|
|
82
|
+
function collectCandidateShorthands (declaredProperties) {
|
|
83
|
+
const candidateShorthands = new Set();
|
|
84
|
+
for (const property of declaredProperties) {
|
|
85
|
+
for (const shorthand of SHORTHANDS_BY_LONGHAND.get(property) || []) {
|
|
86
|
+
candidateShorthands.add(shorthand);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return candidateShorthands;
|
|
90
|
+
}
|
|
91
|
+
|
|
41
92
|
/**
|
|
42
93
|
* Functions and syntaxes that an older browser fails to parse, which makes it
|
|
43
94
|
* discard the whole declaration and use the preceding one for that property
|
|
@@ -386,9 +437,16 @@ function mergeLonghandsIntoShorthands (declarations, context) {
|
|
|
386
437
|
// set of declared properties is gathered once per pass rather than rescanned
|
|
387
438
|
// for each of the dozens of shorthand families.
|
|
388
439
|
const declaredProperties = collectDeclaredProperties(result);
|
|
440
|
+
// Most rules declare none of a given shorthand's longhands, so those
|
|
441
|
+
// shorthands are skipped before any of their merge checks run.
|
|
442
|
+
const candidateShorthands = collectCandidateShorthands(declaredProperties);
|
|
389
443
|
|
|
390
|
-
for (const [shorthand, longhands] of
|
|
391
|
-
if (declaredProperties.has(shorthand)) {
|
|
444
|
+
for (const [shorthand, longhands] of SHORTHAND_ENTRIES) {
|
|
445
|
+
if (!candidateShorthands.has(shorthand) || declaredProperties.has(shorthand)) {
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
const mergeableProperties = getMergeProps(shorthand, longhands, declaredProperties);
|
|
449
|
+
if (!mergeableProperties) {
|
|
392
450
|
continue;
|
|
393
451
|
}
|
|
394
452
|
// Assembling a shorthand also resets the properties it cannot express, so
|
|
@@ -397,11 +455,6 @@ function mergeLonghandsIntoShorthands (declarations, context) {
|
|
|
397
455
|
if (resetsPropertyDeclaredElsewhere(shorthand, declaredProperties, context)) {
|
|
398
456
|
continue;
|
|
399
457
|
}
|
|
400
|
-
|
|
401
|
-
const mergeableProperties = getMergeProps(shorthand, longhands, declaredProperties);
|
|
402
|
-
if (!mergeableProperties) {
|
|
403
|
-
continue;
|
|
404
|
-
}
|
|
405
458
|
const mergeablePropertySet = new Set(mergeableProperties);
|
|
406
459
|
const relevantDeclarations = result.filter((declaration) => {
|
|
407
460
|
return mergeablePropertySet.has(declaration.property);
|
package/src/index.js
CHANGED
|
@@ -23,7 +23,10 @@ import {
|
|
|
23
23
|
import { preprocessDeclarationBlocks } from './preprocess.js';
|
|
24
24
|
import { combineLanguageSelectors } from './rules/lang.js';
|
|
25
25
|
import { normalizeRuleSelectors } from './rules/normalize.js';
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
clearSelectorCaches,
|
|
28
|
+
optimizeRuleList
|
|
29
|
+
} from './rules/optimize.js';
|
|
27
30
|
import { combineLegacyAnySelectors } from './rules/selectors.js';
|
|
28
31
|
import {
|
|
29
32
|
removeRedundantLayerStatementSemicolon,
|
|
@@ -137,14 +140,15 @@ function mergeAdjacentRulesWithIdenticalBodies (ruleStrings) {
|
|
|
137
140
|
|
|
138
141
|
/**
|
|
139
142
|
* Prepares the module-level state a single minification pass relies on: the
|
|
140
|
-
* active charset, and
|
|
141
|
-
* different charset may no longer minify the same way.
|
|
143
|
+
* active charset, and empty value and selector caches, since a value
|
|
144
|
+
* memoized under a different charset may no longer minify the same way.
|
|
142
145
|
*
|
|
143
146
|
* @param {string} charset The `@charset` value detected in the source.
|
|
144
147
|
*/
|
|
145
148
|
function beginMinificationPass (charset) {
|
|
146
149
|
setActiveCharset(charset);
|
|
147
150
|
clearMinifiedValueCache();
|
|
151
|
+
clearSelectorCaches();
|
|
148
152
|
}
|
|
149
153
|
|
|
150
154
|
/**
|
|
@@ -154,6 +158,7 @@ function beginMinificationPass (charset) {
|
|
|
154
158
|
function endMinificationPass () {
|
|
155
159
|
clearActiveCharset();
|
|
156
160
|
clearMinifiedValueCache();
|
|
161
|
+
clearSelectorCaches();
|
|
157
162
|
}
|
|
158
163
|
|
|
159
164
|
/**
|
|
@@ -157,6 +157,12 @@ function skipBracketedGroup (source, openIndex) {
|
|
|
157
157
|
* @return {boolean} Whether a `url()` token starts here.
|
|
158
158
|
*/
|
|
159
159
|
function startsUrlToken (source, index) {
|
|
160
|
+
// Only a `u` can begin the token, so every other character is ruled out
|
|
161
|
+
// before anything is sliced or lowercased
|
|
162
|
+
const character = source[index];
|
|
163
|
+
if (character !== 'u' && character !== 'U') {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
160
166
|
return source.slice(index, index + 4).toLowerCase() === 'url(';
|
|
161
167
|
}
|
|
162
168
|
|