@thejaredwilcurt/csslop 0.0.54 → 0.0.56
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.
|
|
5
|
+
"version": "0.0.56",
|
|
6
6
|
"description": "Experimental CSS minification",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"prestart": "node ./scripts/pre/start.js",
|
|
@@ -34,13 +34,13 @@
|
|
|
34
34
|
"eslint-config-tjw-base": "^5.0.0",
|
|
35
35
|
"eslint-config-tjw-import-x": "^1.0.2",
|
|
36
36
|
"eslint-config-tjw-jsdoc": "^2.0.1",
|
|
37
|
-
"eslint-plugin-import-x": "^4.18.
|
|
37
|
+
"eslint-plugin-import-x": "^4.18.1",
|
|
38
38
|
"eslint-plugin-jsdoc": "^65.4.5",
|
|
39
39
|
"fflate": "^0.8.3",
|
|
40
40
|
"globals": "^17.14.0",
|
|
41
41
|
"pretty-ms": "^10.0.0",
|
|
42
42
|
"real-world-css-libraries": "^1.0.9",
|
|
43
|
-
"vite": "^8.3.
|
|
43
|
+
"vite": "^8.3.7"
|
|
44
44
|
},
|
|
45
45
|
"author": "The Jared Wilcurt",
|
|
46
46
|
"homepage": "https://github.com/TheJaredWilcurt/csslop#readme",
|
|
@@ -23,6 +23,127 @@ function removeIEMediaRules (rules) {
|
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Matches the media type keyword that opens a normalized `@media` query, such as `screen` in `screen and (width>=768px)`.
|
|
28
|
+
*
|
|
29
|
+
* @type {RegExp}
|
|
30
|
+
*/
|
|
31
|
+
const LEADING_MEDIA_TYPE_PATTERN = /^[a-z-]+(?=\s|$)/i;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Matches one parenthesized media condition, allowing one level of nested parentheses such as `(not (color))`.
|
|
35
|
+
*
|
|
36
|
+
* @type {RegExp}
|
|
37
|
+
*/
|
|
38
|
+
const PARENTHESIZED_CONDITION_PATTERN = /\((?:[^()]|\([^()]*\))*\)/g;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Splits a `@media` query into the media type it names and the conditions it adds, so that two queries can be joined with `and`. Queries that use a list, `not`, or `only` cannot be joined this way.
|
|
42
|
+
*
|
|
43
|
+
* @param {string} media The raw `@media` query string.
|
|
44
|
+
* @return {object|null} The `type` (null when none applies) and `conditions` text, or null when the query cannot be joined.
|
|
45
|
+
*/
|
|
46
|
+
function parseJoinableMediaQuery (media) {
|
|
47
|
+
const normalized = normalizeMedia(media);
|
|
48
|
+
const typeText = normalized.match(LEADING_MEDIA_TYPE_PATTERN)?.[0] ?? '';
|
|
49
|
+
const typeName = typeText.toLowerCase();
|
|
50
|
+
if (normalized.includes(',') || typeName === 'not' || typeName === 'only') {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
// Drop the `and` that separates the media type from the conditions
|
|
54
|
+
const conditions = normalized.slice(typeText.length).replace(/^\s*and\s*/i, '');
|
|
55
|
+
// Anything left once the parenthesized conditions and `and` joiners are removed is an unsupported operator
|
|
56
|
+
const unparsedText = conditions.replace(PARENTHESIZED_CONDITION_PATTERN, '').replace(/\band\b/gi, '').trim();
|
|
57
|
+
if (unparsedText) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
// `all` matches every document, so it adds no restriction of its own
|
|
62
|
+
type: typeName && typeName !== 'all' ? typeName : null,
|
|
63
|
+
conditions
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Reports whether two queries name different media types. A document has only one media type, so a block requiring both can never match.
|
|
69
|
+
*
|
|
70
|
+
* @param {object|null} outer The parsed enclosing query, from `parseJoinableMediaQuery`.
|
|
71
|
+
* @param {object|null} inner The parsed nested query, from `parseJoinableMediaQuery`.
|
|
72
|
+
* @return {boolean} True when the two queries name different media types.
|
|
73
|
+
*/
|
|
74
|
+
function areExclusiveMediaTypes (outer, inner) {
|
|
75
|
+
return Boolean(outer?.type && inner?.type && outer.type !== inner.type);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Joins a `@media` block nested directly inside another into the single query that matches the same documents, such as `screen and (width>=768px)`. A media type both queries share is written once.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} outerMedia The raw query of the enclosing `@media` block.
|
|
82
|
+
* @param {string} innerMedia The raw query of the nested `@media` block.
|
|
83
|
+
* @return {string|null} The joined query, or null when the pair must stay nested.
|
|
84
|
+
*/
|
|
85
|
+
function joinNestedMediaQueries (outerMedia, innerMedia) {
|
|
86
|
+
const outer = parseJoinableMediaQuery(outerMedia);
|
|
87
|
+
const inner = parseJoinableMediaQuery(innerMedia);
|
|
88
|
+
if (!outer || !inner || areExclusiveMediaTypes(outer, inner)) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const parts = [outer.type || inner.type, outer.conditions, inner.conditions].filter(Boolean);
|
|
92
|
+
return parts.join(' and ') || 'all';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Reports whether an entry of a rule list is real content rather than the whitespace between entries.
|
|
97
|
+
*
|
|
98
|
+
* @param {object} entry An entry of a rule list.
|
|
99
|
+
* @return {boolean} True when the entry is not whitespace.
|
|
100
|
+
*/
|
|
101
|
+
function isContentEntry (entry) {
|
|
102
|
+
return entry.type !== 'whitespace';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Simplifies one `@media` block. A block whose only content is a single nested `@media` block is joined into one query, and the simplification repeats on the result. Nested `@media` blocks whose media type the block already excludes are removed.
|
|
107
|
+
*
|
|
108
|
+
* @param {object} rule The `@media` rule node to simplify.
|
|
109
|
+
* @return {object} A new `@media` rule node.
|
|
110
|
+
*/
|
|
111
|
+
function simplifyMediaBlock (rule) {
|
|
112
|
+
const children = rule.rules || [];
|
|
113
|
+
const content = children.filter(isContentEntry);
|
|
114
|
+
if (content.length === 1 && content[0].type === 'media') {
|
|
115
|
+
const joinedMedia = joinNestedMediaQueries(rule.media, content[0].media);
|
|
116
|
+
if (joinedMedia !== null) {
|
|
117
|
+
return simplifyMediaBlock({ ...content[0], media: joinedMedia });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const outerQuery = parseJoinableMediaQuery(rule.media);
|
|
121
|
+
return {
|
|
122
|
+
...rule,
|
|
123
|
+
rules: children.filter((child) => {
|
|
124
|
+
return !(child.type === 'media' && areExclusiveMediaTypes(outerQuery, parseJoinableMediaQuery(child.media)));
|
|
125
|
+
})
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Simplifies the `@media` blocks in a rule list, dropping any block whose content was removed entirely as dead.
|
|
131
|
+
*
|
|
132
|
+
* @param {Array} rules The AST rule nodes to process.
|
|
133
|
+
* @return {Array} A new array of rules with nested `@media` blocks simplified.
|
|
134
|
+
*/
|
|
135
|
+
function simplifyNestedMediaRules (rules) {
|
|
136
|
+
return rules.flatMap((rule) => {
|
|
137
|
+
if (rule.type !== 'media') {
|
|
138
|
+
return [rule];
|
|
139
|
+
}
|
|
140
|
+
const simplified = simplifyMediaBlock(rule);
|
|
141
|
+
const hadContent = (rule.rules || []).some(isContentEntry);
|
|
142
|
+
const isEmptiedByDeadContent = hadContent && !simplified.rules.some(isContentEntry);
|
|
143
|
+
return isEmptiedByDeadContent ? [] : [simplified];
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
26
147
|
/**
|
|
27
148
|
* Merges adjacent `@media` rules that share an identical normalized query string and deduplicates their child selector rules.
|
|
28
149
|
*
|
|
@@ -126,5 +247,6 @@ export {
|
|
|
126
247
|
deduplicateKeyframes,
|
|
127
248
|
mergeLayerRules,
|
|
128
249
|
mergeMediaRules,
|
|
129
|
-
removeIEMediaRules
|
|
250
|
+
removeIEMediaRules,
|
|
251
|
+
simplifyNestedMediaRules
|
|
130
252
|
};
|
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
deduplicateKeyframes,
|
|
7
7
|
mergeLayerRules,
|
|
8
8
|
mergeMediaRules,
|
|
9
|
-
removeIEMediaRules
|
|
9
|
+
removeIEMediaRules,
|
|
10
|
+
simplifyNestedMediaRules
|
|
10
11
|
} from './at-rules.js';
|
|
11
12
|
import { removeEmptyRules } from './empty-rules.js';
|
|
12
13
|
import { expandPureNestedRules } from './expand-nesting.js';
|
|
@@ -45,6 +46,7 @@ function clearSelectorCaches () {
|
|
|
45
46
|
function optimizeRuleList (rules) {
|
|
46
47
|
let result = expandPureNestedRules(rules);
|
|
47
48
|
result = removeIEMediaRules(result);
|
|
49
|
+
result = simplifyNestedMediaRules(result);
|
|
48
50
|
result = mergeLayerRules(result, mergeSelectorRules);
|
|
49
51
|
result = mergeMediaRules(result, mergeSelectorRules);
|
|
50
52
|
result = deduplicateKeyframes(result);
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* @file Nests flat rules under a preceding rule whose selector their own selector starts with, so CSS nesting writes the shared part once.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { findTopLevelDelimiter } from '../../parser/source-search.js';
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Matches the part of a child selector that follows its parent when that part
|
|
7
9
|
* starts with an explicit combinator. Optional whitespace, then the combinator
|
|
@@ -78,6 +80,46 @@ function isSelectorWhitespace (character) {
|
|
|
78
80
|
return (/\s/).test(character);
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
/**
|
|
84
|
+
* The characters that can end a parent selector: the colon of a pseudo-class,
|
|
85
|
+
* the whitespace of a descendant combinator, and the explicit combinators.
|
|
86
|
+
*
|
|
87
|
+
* @type {string}
|
|
88
|
+
*/
|
|
89
|
+
const PARENT_BOUNDARY_CHARACTERS = ':>+~ \t\n\r\f';
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Records the parent selector that a boundary character would end, when it
|
|
93
|
+
* ends one. Boundaries are only found at the top level of the selector, so
|
|
94
|
+
* whitespace inside an attribute selector or a pseudo-class argument, and
|
|
95
|
+
* characters escaped with a backslash, never end a parent.
|
|
96
|
+
*
|
|
97
|
+
* @param {string} childSelector The trimmed child selector.
|
|
98
|
+
* @param {number} boundaryIndex The index of the boundary character within the child selector.
|
|
99
|
+
* @param {Set} candidates The set the candidate parent selector is added to.
|
|
100
|
+
*/
|
|
101
|
+
function addCandidateParentSelector (childSelector, boundaryIndex, candidates) {
|
|
102
|
+
const character = childSelector[boundaryIndex];
|
|
103
|
+
if (character === ':' || isSelectorWhitespace(character)) {
|
|
104
|
+
// The second colon of a pseudo-element is part of its `::` introducer,
|
|
105
|
+
// not a boundary between a potential parent and nested child.
|
|
106
|
+
if (character === ':' && childSelector[boundaryIndex - 1] === ':') {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
candidates.add(childSelector.slice(0, boundaryIndex));
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// A combinator may be separated from its parent by whitespace, which
|
|
113
|
+
// belongs to neither side, so the parent ends where that run begins.
|
|
114
|
+
let boundary = boundaryIndex;
|
|
115
|
+
while (boundary > 0 && isSelectorWhitespace(childSelector[boundary - 1])) {
|
|
116
|
+
boundary--;
|
|
117
|
+
}
|
|
118
|
+
if (boundary > 0) {
|
|
119
|
+
candidates.add(childSelector.slice(0, boundary));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
81
123
|
/**
|
|
82
124
|
* Collects every prefix of a child selector that could act as its nesting
|
|
83
125
|
* parent. A parent always ends immediately before a pseudo-class colon, a
|
|
@@ -91,29 +133,10 @@ function isSelectorWhitespace (character) {
|
|
|
91
133
|
*/
|
|
92
134
|
function collectCandidateParentSelectors (childSelector) {
|
|
93
135
|
const candidates = new Set();
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// not a boundary between a potential parent and nested child.
|
|
99
|
-
if (character === ':' && childSelector[index - 1] === ':') {
|
|
100
|
-
continue;
|
|
101
|
-
}
|
|
102
|
-
candidates.add(childSelector.slice(0, index));
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
if (character !== '>' && character !== '+' && character !== '~') {
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
// A combinator may be separated from its parent by whitespace, which
|
|
109
|
-
// belongs to neither side, so the parent ends where that run begins.
|
|
110
|
-
let boundary = index;
|
|
111
|
-
while (boundary > 0 && isSelectorWhitespace(childSelector[boundary - 1])) {
|
|
112
|
-
boundary--;
|
|
113
|
-
}
|
|
114
|
-
if (boundary > 0) {
|
|
115
|
-
candidates.add(childSelector.slice(0, boundary));
|
|
116
|
-
}
|
|
136
|
+
let boundaryIndex = findTopLevelDelimiter(childSelector, PARENT_BOUNDARY_CHARACTERS, 1);
|
|
137
|
+
while (boundaryIndex !== -1) {
|
|
138
|
+
addCandidateParentSelector(childSelector, boundaryIndex, candidates);
|
|
139
|
+
boundaryIndex = findTopLevelDelimiter(childSelector, PARENT_BOUNDARY_CHARACTERS, boundaryIndex + 1);
|
|
117
140
|
}
|
|
118
141
|
return candidates;
|
|
119
142
|
}
|
package/src/rules/stringify.js
CHANGED
|
@@ -306,6 +306,83 @@ function simplifyNthPseudoClasses (selector) {
|
|
|
306
306
|
return minified;
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
+
/**
|
|
310
|
+
* Matches an attribute selector's brackets: the attribute name, the matcher
|
|
311
|
+
* (such as `=` or `^=`), the value, and the optional case-sensitivity flag `i`
|
|
312
|
+
* or `s`. The value is a double-quoted string, a single-quoted string, or a run
|
|
313
|
+
* of unquoted characters in which escapes may stand in for spaces.
|
|
314
|
+
*
|
|
315
|
+
* @type {RegExp}
|
|
316
|
+
*/
|
|
317
|
+
const ATTRIBUTE_SELECTOR_PATTERN = /\[\s*([^\s=\]]+?)\s*([~|^$*]?=)\s*("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|(?:\\.|[^\s"'\\\]])+)(?:\s*([is]))?\s*\]/gi;
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Matches a value that can be written without quotes: an identifier made of
|
|
321
|
+
* letters, digits, hyphens, underscores, and escapes. It cannot start with a
|
|
322
|
+
* digit, or with a hyphen followed by a digit.
|
|
323
|
+
*
|
|
324
|
+
* @type {RegExp}
|
|
325
|
+
*/
|
|
326
|
+
const UNQUOTED_ATTRIBUTE_VALUE_PATTERN = /^(?:-?[a-zA-Z_]|\\.)(?:[\w-]|\\.)*$/;
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Writes the text of a quoted attribute value without its quotes, escaping the
|
|
330
|
+
* characters that would otherwise end an unquoted identifier.
|
|
331
|
+
*
|
|
332
|
+
* @param {string} content The value's text, without its quotes.
|
|
333
|
+
* @return {string|null} The unquoted value, or null when it cannot be written without quotes.
|
|
334
|
+
*/
|
|
335
|
+
function escapeAttributeValue (content) {
|
|
336
|
+
if (content.includes('\\')) {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
// Escape special characters that require quoting (spaces, #, ., :, /, ;)
|
|
340
|
+
const escaped = content.replace(/([ #.:/;])/g, '\\$1');
|
|
341
|
+
if (!UNQUOTED_ATTRIBUTE_VALUE_PATTERN.test(escaped)) {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
return escaped;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Writes an attribute value with double quotes, removing the escapes that only
|
|
349
|
+
* an unquoted value needs. A value that already has quotes keeps them, unless
|
|
350
|
+
* a single-quoted value holds a double quote that would have to be escaped.
|
|
351
|
+
*
|
|
352
|
+
* @param {string} rawValue The value as written, with or without quotes.
|
|
353
|
+
* @return {string} The value wrapped in double quotes.
|
|
354
|
+
*/
|
|
355
|
+
function quoteAttributeValue (rawValue) {
|
|
356
|
+
if (rawValue[0] === '"' || rawValue[0] === '\'') {
|
|
357
|
+
const content = rawValue.slice(1, -1);
|
|
358
|
+
return content.includes('"') ? rawValue : '"' + content + '"';
|
|
359
|
+
}
|
|
360
|
+
// Unescape the characters that only an unquoted value needs escaped
|
|
361
|
+
return '"' + rawValue.replace(/\\([ #.:/;])/g, '$1') + '"';
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Writes an attribute value and its case-sensitivity flag in whichever form is
|
|
366
|
+
* shorter. A value written with quotes stays quoted on a tie, and a value written
|
|
367
|
+
* without quotes stays unquoted. A flag after an unquoted value needs a space,
|
|
368
|
+
* but a flag after a quoted value does not.
|
|
369
|
+
*
|
|
370
|
+
* @param {string} rawValue The value as written, including its quotes when it has them.
|
|
371
|
+
* @param {string|null} flag The flag as written, or null when there is none.
|
|
372
|
+
* @return {string} The value followed by the flag, in its shortest form.
|
|
373
|
+
*/
|
|
374
|
+
function formatAttributeValue (rawValue, flag) {
|
|
375
|
+
const isQuoted = rawValue[0] === '"' || rawValue[0] === '\'';
|
|
376
|
+
const quotedForm = quoteAttributeValue(rawValue) + (flag || '');
|
|
377
|
+
const unquotedValue = isQuoted ? escapeAttributeValue(rawValue.slice(1, -1)) : rawValue;
|
|
378
|
+
if (unquotedValue === null) {
|
|
379
|
+
return quotedForm;
|
|
380
|
+
}
|
|
381
|
+
const unquotedForm = unquotedValue + (flag ? ' ' + flag : '');
|
|
382
|
+
const useUnquoted = isQuoted ? unquotedForm.length < quotedForm.length : unquotedForm.length <= quotedForm.length;
|
|
383
|
+
return useUnquoted ? unquotedForm : quotedForm;
|
|
384
|
+
}
|
|
385
|
+
|
|
309
386
|
/**
|
|
310
387
|
* Writes each attribute selector's value quoted or escaped, whichever is
|
|
311
388
|
* shorter, and removes the whitespace inside its brackets.
|
|
@@ -314,35 +391,9 @@ function simplifyNthPseudoClasses (selector) {
|
|
|
314
391
|
* @return {string} The selector with minified attribute selectors.
|
|
315
392
|
*/
|
|
316
393
|
function minifyAttributeSelectors (selector) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
minified = minified.replace(/\[\s*([^=]+)\s*=\s*"(.*?)"\s*\]/g, (match, attribute, value) => {
|
|
320
|
-
// Escape special characters that require quoting (spaces, #, ., :, /, ;), and compare lengths
|
|
321
|
-
let escaped = value.replace(/([ #.:/;])/g, '\\$1');
|
|
322
|
-
if (escaped.length < value.length + 2) {
|
|
323
|
-
return '[' + attribute + '=' + escaped + ']';
|
|
324
|
-
}
|
|
325
|
-
return '[' + attribute + '="' + value + '"]';
|
|
326
|
-
});
|
|
327
|
-
// Minify single-quoted attribute selectors: remove inner whitespace and escape when shorter
|
|
328
|
-
minified = minified.replace(/\[\s*([^=]+)\s*=\s*'(.*?)'\s*\]/g, (match, attribute, value) => {
|
|
329
|
-
// Escape special characters that require quoting (spaces, #, ., :, /, ;), and compare lengths
|
|
330
|
-
let escaped = value.replace(/([ #.:/;])/g, '\\$1');
|
|
331
|
-
if (escaped.length < value.length + 2) {
|
|
332
|
-
return '[' + attribute + '=' + escaped + ']';
|
|
333
|
-
}
|
|
334
|
-
return '[' + attribute + '="' + value + '"]';
|
|
335
|
-
});
|
|
336
|
-
// Minify unquoted attribute selectors: quote when unescaping produces a shorter result
|
|
337
|
-
minified = minified.replace(/\[\s*([^=]+)\s*=\s*([^"'].*?)\s*\]/g, (match, attribute, value) => {
|
|
338
|
-
// Unescape special characters (spaces, #, ., :, /, ;) and compare with quoted form
|
|
339
|
-
let unescaped = value.replace(/\\([ #.:/;])/g, '$1');
|
|
340
|
-
if (unescaped.length + 2 < value.length) {
|
|
341
|
-
return '[' + attribute + '="' + unescaped + '"]';
|
|
342
|
-
}
|
|
343
|
-
return '[' + attribute + '=' + value + ']';
|
|
394
|
+
return selector.replace(ATTRIBUTE_SELECTOR_PATTERN, (match, attribute, matcher, rawValue, flag) => {
|
|
395
|
+
return '[' + attribute + matcher + formatAttributeValue(rawValue, flag) + ']';
|
|
344
396
|
});
|
|
345
|
-
return minified;
|
|
346
397
|
}
|
|
347
398
|
|
|
348
399
|
/**
|