less 4.8.0 → 4.9.0
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/dist/less-node.cjs +5061 -4766
- package/dist/less.cjs +5061 -4767
- package/dist/less.js +5061 -4767
- package/dist/less.min.js +2 -2
- package/dist/less.min.js.map +1 -1
- package/lib/less/functions/math-helper.js +8 -1
- package/lib/less/functions/math.js +14 -13
- package/lib/less/functions/number.js +1 -1
- package/lib/less/functions/string.js +6 -3
- package/lib/less/parser/lookup-pattern.js +26 -0
- package/lib/less/parser/parser.js +127 -23
- package/lib/less/tree/anonymous.js +5 -1
- package/lib/less/tree/interpolated-variable.js +106 -0
- package/lib/less/tree/js-eval-node.js +8 -2
- package/lib/less/tree/mixin-call.js +4 -0
- package/lib/less/tree/nested-at-rule.js +41 -0
- package/lib/less/tree/quoted.js +24 -7
- package/lib/less/tree/ruleset.js +3 -1
- package/package.json +1 -2
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import Dimension from '../tree/dimension.js';
|
|
2
2
|
|
|
3
|
-
const MathHelper = (fn, unit, n) => {
|
|
3
|
+
const MathHelper = (fn, unit, cssEvaluable, n) => {
|
|
4
4
|
if (!(n instanceof Dimension)) {
|
|
5
|
+
// A runtime CSS value such as var()/env() stays an unevaluated call and
|
|
6
|
+
// cannot be resolved to a number at compile time. Only functions with a
|
|
7
|
+
// CSS equivalent may be left for the browser; the rest (percentage, ceil,
|
|
8
|
+
// floor, round) have no CSS form and keep erroring on such input.
|
|
9
|
+
if (cssEvaluable && n && n.type === 'Call') {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
5
12
|
throw { type: 'Argument', message: 'argument must be a number' };
|
|
6
13
|
}
|
|
7
14
|
if (unit === null) {
|
|
@@ -1,29 +1,30 @@
|
|
|
1
1
|
import mathHelper from './math-helper.js';
|
|
2
2
|
|
|
3
3
|
const mathFunctions = {
|
|
4
|
-
// name
|
|
5
|
-
ceil: null,
|
|
6
|
-
floor: null,
|
|
7
|
-
sqrt: null,
|
|
8
|
-
abs: null,
|
|
9
|
-
tan: '',
|
|
10
|
-
sin: '',
|
|
11
|
-
cos: '',
|
|
12
|
-
atan: 'rad',
|
|
13
|
-
asin: 'rad',
|
|
14
|
-
acos: 'rad'
|
|
4
|
+
// name: [unit, has a CSS equivalent that the browser can evaluate]
|
|
5
|
+
ceil: [null, false],
|
|
6
|
+
floor: [null, false],
|
|
7
|
+
sqrt: [null, true],
|
|
8
|
+
abs: [null, true],
|
|
9
|
+
tan: ['', true],
|
|
10
|
+
sin: ['', true],
|
|
11
|
+
cos: ['', true],
|
|
12
|
+
atan: ['rad', true],
|
|
13
|
+
asin: ['rad', true],
|
|
14
|
+
acos: ['rad', true]
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
for (const f in mathFunctions) {
|
|
18
18
|
// eslint-disable-next-line no-prototype-builtins
|
|
19
19
|
if (mathFunctions.hasOwnProperty(f)) {
|
|
20
|
-
|
|
20
|
+
const [unit, cssEvaluable] = mathFunctions[f];
|
|
21
|
+
mathFunctions[f] = mathHelper.bind(null, Math[f], unit, cssEvaluable);
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
mathFunctions.round = (n, f) => {
|
|
25
26
|
const fraction = typeof f === 'undefined' ? 0 : f.value;
|
|
26
|
-
return mathHelper(num => num.toFixed(fraction), null, n);
|
|
27
|
+
return mathHelper(num => num.toFixed(fraction), null, false, n);
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
export default mathFunctions;
|
|
@@ -7,9 +7,12 @@ export default {
|
|
|
7
7
|
return new Quoted('"', str instanceof JavaScript ? str.evaluated : str.value, true);
|
|
8
8
|
},
|
|
9
9
|
escape: function (str) {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
const escapedValue = encodeURI(str.value).replace(/=/g, '%3D').replace(/:/g, '%3A').replace(/#/g, '%23').replace(/;/g, '%3B')
|
|
11
|
+
.replace(/\(/g, '%28').replace(/\)/g, '%29');
|
|
12
|
+
const escaped = new Anonymous(escapedValue);
|
|
13
|
+
// Percent escapes are literal CSS, not Less source.
|
|
14
|
+
escaped._preventReparse = escapedValue.includes('%');
|
|
15
|
+
return escaped;
|
|
13
16
|
},
|
|
14
17
|
replace: function (string, pattern, replacement, flags) {
|
|
15
18
|
let result = string.value;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Shared source-of-truth for the `[...]` lookup grammar.
|
|
4
|
+
*
|
|
5
|
+
* The parser consumes lookups structurally (`parsers.mixin.ruleLookups()`), but
|
|
6
|
+
* `Quoted` resolves interpolation by string replacement at eval time and so needs
|
|
7
|
+
* an equivalent regular expression. Keeping the pattern in one place is what stops
|
|
8
|
+
* the two from drifting apart — the drift is precisely why `@{map[key]}` used to be
|
|
9
|
+
* emitted verbatim instead of being substituted.
|
|
10
|
+
*
|
|
11
|
+
* A key mirrors `parsers.entities.lookupValue`: an optional `@`/`@@`/`$`/`$$`
|
|
12
|
+
* sigil followed by identifier characters. It may be empty (`@map[]` resolves to
|
|
13
|
+
* the last declaration). Critically the key pattern contains no brackets, so a
|
|
14
|
+
* lookup key can never itself be a lookup — `@{a[@b[c]]}` is not grammatical.
|
|
15
|
+
* A non-nesting regex is therefore exactly equivalent to the parsed grammar here
|
|
16
|
+
* rather than an approximation of it.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** A single lookup key, e.g. `key`, `@key`, `@@key`, `$key`, or empty. */
|
|
20
|
+
export const LOOKUP_KEY = '(?:[@$]{0,2})[_a-zA-Z0-9-]*';
|
|
21
|
+
|
|
22
|
+
/** Zero or more chained lookups, e.g. `[a]`, `[a][b]`, `[@a][]`. */
|
|
23
|
+
export const LOOKUP_CHAIN = `(?:\\[${LOOKUP_KEY}\\])*`;
|
|
24
|
+
|
|
25
|
+
/** A variable name followed by an optional lookup chain, e.g. `map[@a][b]`. */
|
|
26
|
+
export const VARIABLE_WITH_LOOKUPS = `[\\w-]+${LOOKUP_CHAIN}`;
|
|
@@ -9,6 +9,28 @@ import logger from '../logger.js';
|
|
|
9
9
|
import { DeprecationHandler } from '../deprecation.js';
|
|
10
10
|
import Selector from '../tree/selector.js';
|
|
11
11
|
import Anonymous from '../tree/anonymous.js';
|
|
12
|
+
import { VARIABLE_WITH_LOOKUPS } from './lookup-pattern.js';
|
|
13
|
+
import {
|
|
14
|
+
splitLookups,
|
|
15
|
+
resolveInterpolatedVariable,
|
|
16
|
+
resolveInterpolatedProperty,
|
|
17
|
+
hasInterpolation,
|
|
18
|
+
VARIABLE_INTERPOLATION,
|
|
19
|
+
PROPERTY_INTERPOLATION
|
|
20
|
+
} from '../tree/interpolated-variable.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* One particle of a property name: a literal chunk, an `@{...}` interpolation
|
|
24
|
+
* which may carry a lookup chain, or a `${...}` interpolation which may not.
|
|
25
|
+
*
|
|
26
|
+
* The sigils are spelled out separately rather than sharing `[@$]`: properties
|
|
27
|
+
* have no lookup grammar, so accepting `${name[key]}` here would hand `name[key]`
|
|
28
|
+
* to `resolveInterpolatedProperty` and produce a misleading "undefined property"
|
|
29
|
+
* error for syntax the language does not define.
|
|
30
|
+
*/
|
|
31
|
+
const RULE_PROPERTY_PARTICLE = new RegExp(
|
|
32
|
+
`^((?:[\\w-]+)|(?:@\\{${VARIABLE_WITH_LOOKUPS}\\})|(?:\\$\\{[\\w-]+\\}))`
|
|
33
|
+
);
|
|
12
34
|
|
|
13
35
|
//
|
|
14
36
|
// less.js - parser
|
|
@@ -106,6 +128,22 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
|
|
|
106
128
|
warn('A bare @variable in an at-rule prelude is deprecated. Use @{variable} interpolation instead.', index, 'DEPRECATED', 'variable-in-at-rule-prelude');
|
|
107
129
|
}
|
|
108
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Whether a parsed entity is a bare `@variable` reference in a structural
|
|
133
|
+
* position, and so subject to the interpolation deprecation.
|
|
134
|
+
*
|
|
135
|
+
* A lookup such as `@map[key]` is parsed by `entities.variable()` into a
|
|
136
|
+
* `NamespaceValue` wrapping a `VariableCall`, not a `Variable`. Testing for
|
|
137
|
+
* `Variable` alone silently exempted every bare lookup from the deprecation.
|
|
138
|
+
*
|
|
139
|
+
* @param {{ type?: string } | undefined | null} e
|
|
140
|
+
* @returns {boolean}
|
|
141
|
+
*/
|
|
142
|
+
function isBareVariableReference(e) {
|
|
143
|
+
if (!e) { return false; }
|
|
144
|
+
return e.type === 'Variable' || e.type === 'VariableCall' || e.type === 'NamespaceValue';
|
|
145
|
+
}
|
|
146
|
+
|
|
109
147
|
/**
|
|
110
148
|
* Numeric-leading variable names are a Less extension rather than valid CSS
|
|
111
149
|
* identifier syntax. Keep accepting them through Less 4, but make the Less 5
|
|
@@ -717,6 +755,20 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
|
|
|
717
755
|
value = this.quoted() || this.variable() || this.property() ||
|
|
718
756
|
parserInput.$re(/^(?:(?:\\[()'"])|[^()'"])+/) || '';
|
|
719
757
|
|
|
758
|
+
// An unquoted url() body is otherwise raw text wrapped in an
|
|
759
|
+
// Anonymous node, which never substitutes anything — so
|
|
760
|
+
// `url(@{path}/a.png)` used to emit the braces verbatim while the
|
|
761
|
+
// quoted form resolved. Hand text containing interpolation to an
|
|
762
|
+
// escaped Quoted so both spellings resolve identically.
|
|
763
|
+
//
|
|
764
|
+
// The empty quote string matters: `URL.eval` escapes a rewritten
|
|
765
|
+
// rootpath only for unquoted values, so a real quote character here
|
|
766
|
+
// would suppress that and emit `url(a(b)/x.png)` unescaped. Escaped
|
|
767
|
+
// means no quote is written to the output either way.
|
|
768
|
+
if (typeof value === 'string' && hasInterpolation(value)) {
|
|
769
|
+
value = new(tree.Quoted)('', value, true, index + currentIndex, fileInfo);
|
|
770
|
+
}
|
|
771
|
+
|
|
720
772
|
parserInput.autoCommentAbsorb = true;
|
|
721
773
|
|
|
722
774
|
expectChar(')');
|
|
@@ -760,16 +812,44 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
|
|
|
760
812
|
parserInput.restore();
|
|
761
813
|
},
|
|
762
814
|
|
|
763
|
-
// A variable entity using the protective {} e.g. @{var}
|
|
815
|
+
// A variable entity using the protective {} e.g. @{var}, optionally
|
|
816
|
+
// followed by a lookup chain e.g. @{map[key]} or @{map[@a][b]}.
|
|
817
|
+
//
|
|
818
|
+
// The chain is consumed by `mixin.ruleLookups()` rather than matched
|
|
819
|
+
// here, so the interpolated form shares one grammar with the bare
|
|
820
|
+
// `@map[key]` form instead of re-implementing it.
|
|
764
821
|
variableCurly: function () {
|
|
765
822
|
let curly;
|
|
766
823
|
const index = parserInput.i;
|
|
767
824
|
|
|
768
|
-
if (parserInput.currentChar()
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
825
|
+
if (parserInput.currentChar() !== '@') {
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
parserInput.save();
|
|
830
|
+
if (!(curly = parserInput.$re(/^@\{([\w-]+)/))) {
|
|
831
|
+
parserInput.restore();
|
|
832
|
+
return;
|
|
772
833
|
}
|
|
834
|
+
|
|
835
|
+
const name = curly[1];
|
|
836
|
+
const lookups = parsers.mixin.ruleLookups();
|
|
837
|
+
|
|
838
|
+
if (!parserInput.$char('}')) {
|
|
839
|
+
parserInput.restore();
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
parserInput.forget();
|
|
844
|
+
warnNumericVariableName(name, index);
|
|
845
|
+
warnDashOnlyVariableName(name, index);
|
|
846
|
+
|
|
847
|
+
if (!lookups) {
|
|
848
|
+
return new(tree.Variable)(`@${name}`, index + currentIndex, fileInfo);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
const call = new(tree.VariableCall)(`@${name}`, index + currentIndex, fileInfo);
|
|
852
|
+
return new(tree.NamespaceValue)(call, lookups, index + currentIndex, fileInfo);
|
|
773
853
|
},
|
|
774
854
|
//
|
|
775
855
|
// A Property accessor, such as `$color`, in
|
|
@@ -1725,7 +1805,9 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
|
|
|
1725
1805
|
merge = !isVariable && name.length > 1 && name.pop().value;
|
|
1726
1806
|
|
|
1727
1807
|
// Custom property values get permissive parsing
|
|
1728
|
-
|
|
1808
|
+
// A lookup particle (`NamespaceValue`) carries a node in `value`
|
|
1809
|
+
// rather than a string, and can never spell a `--` prefix.
|
|
1810
|
+
if (typeof name[0].value === 'string' && name[0].value.slice(0, 2) === '--') {
|
|
1729
1811
|
if (parserInput.$char(';')) {
|
|
1730
1812
|
value = new Anonymous('');
|
|
1731
1813
|
} else {
|
|
@@ -1827,7 +1909,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
|
|
|
1827
1909
|
if (!e) {
|
|
1828
1910
|
const varIndex = parserInput.i;
|
|
1829
1911
|
e = this.entity();
|
|
1830
|
-
if (e
|
|
1912
|
+
if (isBareVariableReference(e)) {
|
|
1831
1913
|
warnBareAtRuleVariable(varIndex);
|
|
1832
1914
|
}
|
|
1833
1915
|
}
|
|
@@ -1894,17 +1976,33 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
|
|
|
1894
1976
|
const quote = new tree.Quoted('\'', item, true, index, fileInfo);
|
|
1895
1977
|
const variableRegex = /@([\w-]+)/g;
|
|
1896
1978
|
const propRegex = /\$([\w-]+)/g;
|
|
1979
|
+
// These notices are about *bare* references, so test the text
|
|
1980
|
+
// with interpolations removed. A lookup key is itself allowed
|
|
1981
|
+
// to be a variable (`@{map[@key]}`), and without this the
|
|
1982
|
+
// `@key` inside the braces would be misreported as a bare use
|
|
1983
|
+
// of the very syntax the notice tells you to adopt.
|
|
1984
|
+
const bareOnly = item
|
|
1985
|
+
.replace(VARIABLE_INTERPOLATION, '')
|
|
1986
|
+
.replace(PROPERTY_INTERPOLATION, '');
|
|
1897
1987
|
// At-rule preludes are handled once above via
|
|
1898
1988
|
// `value.bareVarIndex`; the `variable-in-unknown-value`
|
|
1899
1989
|
// notice is for unknown declaration values only.
|
|
1900
|
-
if (!deprecateVariables && variableRegex.test(
|
|
1990
|
+
if (!deprecateVariables && variableRegex.test(bareOnly)) {
|
|
1901
1991
|
warn('@variable in unknown values will not be evaluated as variables in the future. Use @{variable}', index, 'DEPRECATED', 'variable-in-unknown-value');
|
|
1902
1992
|
}
|
|
1903
|
-
if (propRegex.test(
|
|
1993
|
+
if (propRegex.test(bareOnly)) {
|
|
1904
1994
|
warn('$property in unknown values will not be evaluated as property references in the future. Use ${property}', index, 'DEPRECATED', 'property-in-unknown-value');
|
|
1905
1995
|
}
|
|
1906
|
-
|
|
1907
|
-
|
|
1996
|
+
// Both alternatives carry the lookup chain. An unknown at-rule
|
|
1997
|
+
// prelude (`@supports`) is scanned as text, so a bare
|
|
1998
|
+
// `@map[key]` reaches this regex rather than being parsed
|
|
1999
|
+
// structurally; matching only `@map` would resolve it to the
|
|
2000
|
+
// whole ruleset and leave `[key]` behind as literal text.
|
|
2001
|
+
quote.variableRegex = new RegExp(
|
|
2002
|
+
`@(${VARIABLE_WITH_LOOKUPS})|@\\{(${VARIABLE_WITH_LOOKUPS})\\}`, 'g'
|
|
2003
|
+
);
|
|
2004
|
+
// Properties stay narrow — they have no lookup grammar.
|
|
2005
|
+
quote.propRegex = /\$([\w-]+)|\$\{([\w-]+)\}/g;
|
|
1908
2006
|
result.push(quote);
|
|
1909
2007
|
}
|
|
1910
2008
|
}
|
|
@@ -2246,7 +2344,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
|
|
|
2246
2344
|
if (curly) { return curly; }
|
|
2247
2345
|
const index = parserInput.i;
|
|
2248
2346
|
const e = this.entity();
|
|
2249
|
-
if (e
|
|
2347
|
+
if (isBareVariableReference(e)) {
|
|
2250
2348
|
warnBareAtRuleVariable(index);
|
|
2251
2349
|
}
|
|
2252
2350
|
return e;
|
|
@@ -2844,7 +2942,7 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
|
|
|
2844
2942
|
|
|
2845
2943
|
match(/^(\*?)/);
|
|
2846
2944
|
while (true) {
|
|
2847
|
-
if (!match(
|
|
2945
|
+
if (!match(RULE_PROPERTY_PARTICLE)) {
|
|
2848
2946
|
break;
|
|
2849
2947
|
}
|
|
2850
2948
|
}
|
|
@@ -2860,16 +2958,22 @@ const Parser = function Parser(context, imports, fileInfo, currentIndex) {
|
|
|
2860
2958
|
}
|
|
2861
2959
|
for (k = 0; k < name.length; k++) {
|
|
2862
2960
|
s = name[k];
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
}
|
|
2868
|
-
name[
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2961
|
+
const sigil = s.charAt(0);
|
|
2962
|
+
if (sigil !== '@' && sigil !== '$') {
|
|
2963
|
+
name[k] = new(tree.Keyword)(s);
|
|
2964
|
+
continue;
|
|
2965
|
+
}
|
|
2966
|
+
// `@{name}` / `@{name[key]}` — strip the sigil and braces, then
|
|
2967
|
+
// let the shared resolver decide between a plain reference and
|
|
2968
|
+
// a lookup so this path cannot drift from the others.
|
|
2969
|
+
const raw = s.slice(2, -1);
|
|
2970
|
+
if (sigil === '@') {
|
|
2971
|
+
warnNumericVariableName(splitLookups(raw).name, index[k]);
|
|
2972
|
+
warnDashOnlyVariableName(splitLookups(raw).name, index[k]);
|
|
2973
|
+
name[k] = resolveInterpolatedVariable(raw, index[k] + currentIndex, fileInfo);
|
|
2974
|
+
} else {
|
|
2975
|
+
name[k] = resolveInterpolatedProperty(raw, index[k] + currentIndex, fileInfo);
|
|
2976
|
+
}
|
|
2873
2977
|
}
|
|
2874
2978
|
return name;
|
|
2875
2979
|
}
|
|
@@ -20,13 +20,17 @@ class Anonymous extends Node {
|
|
|
20
20
|
this._fileInfo = currentFileInfo;
|
|
21
21
|
this.mapLines = mapLines;
|
|
22
22
|
this.rulesetLike = (typeof rulesetLike === 'undefined') ? false : rulesetLike;
|
|
23
|
+
/** @type {boolean} */
|
|
24
|
+
this._preventReparse = false;
|
|
23
25
|
this.allowRoot = true;
|
|
24
26
|
this.copyVisibilityInfo(visibilityInfo);
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/** @returns {Anonymous} */
|
|
28
30
|
eval() {
|
|
29
|
-
|
|
31
|
+
const anonymous = new Anonymous(/** @type {string | null} */ (this.value), this._index, this._fileInfo, this.mapLines, this.rulesetLike, this.visibilityInfo());
|
|
32
|
+
anonymous._preventReparse = this._preventReparse;
|
|
33
|
+
return anonymous;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
/**
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @import { FileInfo } from './node.js' */
|
|
3
|
+
/** @import Node from './node.js' */
|
|
4
|
+
import Variable from './variable.js';
|
|
5
|
+
import Property from './property.js';
|
|
6
|
+
import VariableCall from './variable-call.js';
|
|
7
|
+
import NamespaceValue from './namespace-value.js';
|
|
8
|
+
import { LOOKUP_KEY, VARIABLE_WITH_LOOKUPS } from '../parser/lookup-pattern.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Interpolation resolved by string replacement at eval time.
|
|
12
|
+
*
|
|
13
|
+
* The parser consumes `@{name[key]}` structurally via `entities.variableCurly`,
|
|
14
|
+
* but `Quoted` and inline JavaScript hold their contents as raw text and can only
|
|
15
|
+
* substitute at eval time. Both share this module so the two evaluation paths build
|
|
16
|
+
* identical nodes from identical patterns.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Matches `@{name}` and `@{name[a][b]}`, capturing the reference. */
|
|
20
|
+
export const VARIABLE_INTERPOLATION = new RegExp(`@\\{(${VARIABLE_WITH_LOOKUPS})\\}`, 'g');
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Matches `${name}`, capturing the reference.
|
|
24
|
+
*
|
|
25
|
+
* Deliberately narrower than {@link VARIABLE_INTERPOLATION}: a lookup chain is not
|
|
26
|
+
* part of the property grammar. `entities.property` parses `$name` with no lookup
|
|
27
|
+
* handling, so `$map[key]` is a property reference followed by the literal text
|
|
28
|
+
* `[key]`, and a property cannot hold a ruleset to look into in the first place
|
|
29
|
+
* (`prop: { … }` is a parse error in every scope). Accepting `${map[key]}` here
|
|
30
|
+
* would invent syntax the language does not have.
|
|
31
|
+
*/
|
|
32
|
+
export const PROPERTY_INTERPOLATION = /\$\{([\w-]+)\}/g;
|
|
33
|
+
|
|
34
|
+
const LOOKUP_SEGMENT = new RegExp(`\\[(${LOOKUP_KEY})\\]`, 'g');
|
|
35
|
+
|
|
36
|
+
// Non-global twins for membership tests. `RegExp.test` on a /g regex advances
|
|
37
|
+
// `lastIndex` and so returns alternating results across calls on shared instances.
|
|
38
|
+
const HAS_VARIABLE_INTERPOLATION = new RegExp(VARIABLE_INTERPOLATION.source);
|
|
39
|
+
const HAS_PROPERTY_INTERPOLATION = new RegExp(PROPERTY_INTERPOLATION.source);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether text contains an `@{...}` or `${...}` interpolation.
|
|
43
|
+
*
|
|
44
|
+
* @param {string} text
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
export function hasInterpolation(text) {
|
|
48
|
+
return HAS_VARIABLE_INTERPOLATION.test(text) || HAS_PROPERTY_INTERPOLATION.test(text);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Split a `name[a][b]` reference into its name and lookup keys.
|
|
53
|
+
*
|
|
54
|
+
* `lookups` is null for a plain reference, so callers keep using the cheaper
|
|
55
|
+
* `Variable`/`Property` node when there is no lookup to resolve.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} raw
|
|
58
|
+
* @returns {{ name: string, lookups: string[] | null }}
|
|
59
|
+
*/
|
|
60
|
+
export function splitLookups(raw) {
|
|
61
|
+
const open = raw.indexOf('[');
|
|
62
|
+
if (open === -1) {
|
|
63
|
+
return { name: raw, lookups: null };
|
|
64
|
+
}
|
|
65
|
+
/** @type {string[]} */
|
|
66
|
+
const lookups = [];
|
|
67
|
+
const re = new RegExp(LOOKUP_SEGMENT.source, 'g');
|
|
68
|
+
let match;
|
|
69
|
+
while ((match = re.exec(raw)) !== null) {
|
|
70
|
+
lookups.push(match[1]);
|
|
71
|
+
}
|
|
72
|
+
return { name: raw.slice(0, open), lookups };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Build the node for an interpolated `@variable` reference, with or without lookups.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} raw - the reference text inside `@{...}`
|
|
79
|
+
* @param {number} index
|
|
80
|
+
* @param {FileInfo} fileInfo
|
|
81
|
+
* @returns {Node}
|
|
82
|
+
*/
|
|
83
|
+
export function resolveInterpolatedVariable(raw, index, fileInfo) {
|
|
84
|
+
const { name, lookups } = splitLookups(raw);
|
|
85
|
+
if (!lookups) {
|
|
86
|
+
return new Variable(`@${name}`, index, fileInfo);
|
|
87
|
+
}
|
|
88
|
+
return new NamespaceValue(
|
|
89
|
+
new VariableCall(`@${name}`, index, fileInfo), lookups, index, fileInfo
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Build the node for an interpolated `$property` reference.
|
|
95
|
+
*
|
|
96
|
+
* No lookup handling: see {@link PROPERTY_INTERPOLATION}. Properties have no lookup
|
|
97
|
+
* grammar, so `raw` is always a bare name here.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} raw - the reference text inside `${...}`
|
|
100
|
+
* @param {number} index
|
|
101
|
+
* @param {FileInfo} fileInfo
|
|
102
|
+
* @returns {Node}
|
|
103
|
+
*/
|
|
104
|
+
export function resolveInterpolatedProperty(raw, index, fileInfo) {
|
|
105
|
+
return new Property(`$${raw}`, index, fileInfo);
|
|
106
|
+
}
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
/** @import { EvalContext } from './node.js' */
|
|
3
3
|
import Node from './node.js';
|
|
4
4
|
import Variable from './variable.js';
|
|
5
|
+
import {
|
|
6
|
+
VARIABLE_INTERPOLATION,
|
|
7
|
+
resolveInterpolatedVariable
|
|
8
|
+
} from './interpolated-variable.js';
|
|
5
9
|
|
|
6
10
|
class JsEvalNode extends Node {
|
|
7
11
|
/**
|
|
@@ -21,8 +25,10 @@ class JsEvalNode extends Node {
|
|
|
21
25
|
index: this.getIndex() };
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
expression = expression.replace(
|
|
25
|
-
return that.jsify(
|
|
28
|
+
expression = expression.replace(VARIABLE_INTERPOLATION, function (_, raw) {
|
|
29
|
+
return that.jsify(resolveInterpolatedVariable(
|
|
30
|
+
raw, that.getIndex(), that.fileInfo()
|
|
31
|
+
).eval(context));
|
|
26
32
|
});
|
|
27
33
|
|
|
28
34
|
/** @type {Function} */
|
|
@@ -151,6 +151,10 @@ class MixinCall extends Node {
|
|
|
151
151
|
for (m = 0; m < expandedValues.length; m++) {
|
|
152
152
|
args.push({value: expandedValues[m]});
|
|
153
153
|
}
|
|
154
|
+
} else if (argValue.type === 'Expression' && Array.isArray(argValue.value) && argValue.value.length === 0) {
|
|
155
|
+
// an unset variadic holds no captured args; omit it so the callee
|
|
156
|
+
// falls through to its own parameter defaults instead of receiving empty
|
|
157
|
+
|
|
154
158
|
} else {
|
|
155
159
|
args.push({name: arg.name, value: argValue});
|
|
156
160
|
}
|
|
@@ -37,6 +37,40 @@ import Node from './node.js';
|
|
|
37
37
|
* }} NestableAtRuleThis
|
|
38
38
|
*/
|
|
39
39
|
|
|
40
|
+
// A media query may only carry a media type at its front, before any
|
|
41
|
+
// conditions (https://drafts.csswg.org/mediaqueries-5/#typedef-media-query-list).
|
|
42
|
+
// Operators that join media-query parts, so a fragment leading with one is not
|
|
43
|
+
// a media type. Any other bare identifier is treated as a type, since unknown
|
|
44
|
+
// media types are valid non-matches in CSS, not syntax errors.
|
|
45
|
+
const MEDIA_QUERY_OPERATORS = ['and', 'or'];
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Whether a flattened media-query fragment leads with a media type, e.g.
|
|
49
|
+
* `screen`, `only screen`, `print and (color)` or an unknown type like `foo`.
|
|
50
|
+
* @param {Node & { value?: * }} fragment
|
|
51
|
+
*/
|
|
52
|
+
function startsWithMediaType(fragment) {
|
|
53
|
+
let head;
|
|
54
|
+
if (fragment.type === 'Keyword' || fragment.type === 'Anonymous') {
|
|
55
|
+
head = fragment.value;
|
|
56
|
+
} else if (fragment.type === 'Expression' && Array.isArray(fragment.value)) {
|
|
57
|
+
const parts = fragment.value.filter(p => p && p.value !== undefined);
|
|
58
|
+
let idx = 0;
|
|
59
|
+
const first = parts[idx] && String(parts[idx].value).toLowerCase();
|
|
60
|
+
if (first === 'not' || first === 'only') { idx++; }
|
|
61
|
+
head = parts[idx] && parts[idx].value;
|
|
62
|
+
}
|
|
63
|
+
if (typeof head !== 'string' || head === '') {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
// Inspect only the first token. A media type is a bare identifier; a feature
|
|
67
|
+
// condition begins with '(' - e.g. an escaped ~"(max-width: 1px)" is an
|
|
68
|
+
// Anonymous whose whole value is "(max-width: 1px)", which is not a media type.
|
|
69
|
+
const firstToken = head.trim().split(/[\s(]/)[0];
|
|
70
|
+
return firstToken !== ''
|
|
71
|
+
&& MEDIA_QUERY_OPERATORS.indexOf(firstToken.toLowerCase()) < 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
40
74
|
const NestableAtRulePrototype = {
|
|
41
75
|
|
|
42
76
|
isRulesetLike() {
|
|
@@ -149,6 +183,13 @@ const NestableAtRulePrototype = {
|
|
|
149
183
|
/** @param {Node & { toCSS?: Function }} fragment */
|
|
150
184
|
fragment => fragment.toCSS ? fragment : new Anonymous(/** @type {string} */ (/** @type {unknown} */ (fragment))));
|
|
151
185
|
|
|
186
|
+
// A media type nested inside conditions must move ahead of them
|
|
187
|
+
// so the flattened query stays valid (issue #3694, #3764).
|
|
188
|
+
const types = /** @type {Node[]} */ (path).filter(startsWithMediaType);
|
|
189
|
+
if (types.length && types.length < /** @type {Node[]} */ (path).length) {
|
|
190
|
+
path = types.concat(/** @type {Node[]} */ (path).filter(f => !startsWithMediaType(f)));
|
|
191
|
+
}
|
|
192
|
+
|
|
152
193
|
for (i = /** @type {Node[]} */ (path).length - 1; i > 0; i--) {
|
|
153
194
|
/** @type {Node[]} */ (path).splice(i, 0, new Anonymous('and'));
|
|
154
195
|
}
|
package/lib/less/tree/quoted.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
/** @import { EvalContext, CSSOutput, FileInfo } from './node.js' */
|
|
3
3
|
import Node from './node.js';
|
|
4
|
-
import
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
VARIABLE_INTERPOLATION,
|
|
6
|
+
PROPERTY_INTERPOLATION,
|
|
7
|
+
resolveInterpolatedVariable,
|
|
8
|
+
resolveInterpolatedProperty
|
|
9
|
+
} from './interpolated-variable.js';
|
|
6
10
|
|
|
7
11
|
class Quoted extends Node {
|
|
8
12
|
get type() { return 'Quoted'; }
|
|
@@ -25,9 +29,9 @@ class Quoted extends Node {
|
|
|
25
29
|
this._index = index;
|
|
26
30
|
this._fileInfo = currentFileInfo;
|
|
27
31
|
/** @type {RegExp} */
|
|
28
|
-
this.variableRegex =
|
|
32
|
+
this.variableRegex = new RegExp(VARIABLE_INTERPOLATION.source, 'g');
|
|
29
33
|
/** @type {RegExp} */
|
|
30
|
-
this.propRegex =
|
|
34
|
+
this.propRegex = new RegExp(PROPERTY_INTERPOLATION.source, 'g');
|
|
31
35
|
/** @type {boolean | undefined} */
|
|
32
36
|
this.allowRoot = escaped;
|
|
33
37
|
}
|
|
@@ -62,7 +66,9 @@ class Quoted extends Node {
|
|
|
62
66
|
* @returns {string}
|
|
63
67
|
*/
|
|
64
68
|
const variableReplacement = function (_, name1, name2) {
|
|
65
|
-
const v =
|
|
69
|
+
const v = resolveInterpolatedVariable(
|
|
70
|
+
name1 ?? name2, that.getIndex(), that.fileInfo()
|
|
71
|
+
).eval(context);
|
|
66
72
|
return (v instanceof Quoted) ? /** @type {string} */ (v.value) : v.toCSS(context);
|
|
67
73
|
};
|
|
68
74
|
/**
|
|
@@ -72,7 +78,9 @@ class Quoted extends Node {
|
|
|
72
78
|
* @returns {string}
|
|
73
79
|
*/
|
|
74
80
|
const propertyReplacement = function (_, name1, name2) {
|
|
75
|
-
const v =
|
|
81
|
+
const v = resolveInterpolatedProperty(
|
|
82
|
+
name1 ?? name2, that.getIndex(), that.fileInfo()
|
|
83
|
+
).eval(context);
|
|
76
84
|
return (v instanceof Quoted) ? /** @type {string} */ (v.value) : v.toCSS(context);
|
|
77
85
|
};
|
|
78
86
|
/**
|
|
@@ -91,7 +99,16 @@ class Quoted extends Node {
|
|
|
91
99
|
}
|
|
92
100
|
value = iterativeReplace(value, this.variableRegex, variableReplacement);
|
|
93
101
|
value = iterativeReplace(value, this.propRegex, propertyReplacement);
|
|
94
|
-
|
|
102
|
+
const result = new Quoted(
|
|
103
|
+
this.quote + value + this.quote, value, this.escaped, this.getIndex(), this.fileInfo()
|
|
104
|
+
);
|
|
105
|
+
// Carry the quote across rather than re-deriving it from the rebuilt string.
|
|
106
|
+
// For a real quote character the two agree, but an unquoted body (empty quote)
|
|
107
|
+
// is not round-trippable that way — it would pick up the first character of the
|
|
108
|
+
// substituted value and read as quoted, which suppresses rootpath escaping in
|
|
109
|
+
// `URL.eval`.
|
|
110
|
+
result.quote = this.quote;
|
|
111
|
+
return result;
|
|
95
112
|
}
|
|
96
113
|
|
|
97
114
|
/**
|
package/lib/less/tree/ruleset.js
CHANGED
|
@@ -426,7 +426,9 @@ class Ruleset extends Node {
|
|
|
426
426
|
const self = this;
|
|
427
427
|
/** @param {Declaration} decl */
|
|
428
428
|
function transformDeclaration(decl) {
|
|
429
|
-
if (decl.value instanceof Anonymous &&
|
|
429
|
+
if (decl.value instanceof Anonymous &&
|
|
430
|
+
!decl.value._preventReparse &&
|
|
431
|
+
!/** @type {Declaration & { parsed?: boolean }} */ (decl).parsed) {
|
|
430
432
|
if (typeof decl.value.value === 'string') {
|
|
431
433
|
new (/** @type {new (...args: [EvalContext, object, FileInfo, number]) => { parseNode: Function }} */ (/** @type {unknown} */ (Parser)))(/** @type {{ context: EvalContext, importManager: object }} */ (/** @type {Ruleset} */ (this).parse).context, /** @type {{ context: EvalContext, importManager: object }} */ (/** @type {Ruleset} */ (this).parse).importManager, decl.fileInfo(), decl.value.getIndex()).parseNode(
|
|
432
434
|
decl.value.value,
|