diff --git a/lib/LessParser.js b/lib/LessParser.js index 098cc4e..1388af7 100644 --- a/lib/LessParser.js +++ b/lib/LessParser.js @@ -23,11 +23,42 @@ module.exports = class LessParser extends Parser { return; } + this.mergeParamsInterpolation(); + super.atrule(token); importNode(this.lastNode); variableNode(this.lastNode); } + // #180: `@media @{mq}` - merge `@{…}` sequences in the prelude into single word tokens, + // otherwise the interpolation's `{` is mistaken for the start of the at-rule block + mergeParamsInterpolation() { + const tokens = []; + let token = this.tokenizer.nextToken(); + + while (token) { + if (token[0] === 'at-word' && token[1] === '@' && interpolation.bind(this)(token)) { + token = this.tokenizer.nextToken(); + + if (!token) { + break; + } + } + + tokens.push(token); + + if (token[0] === '{' || token[0] === '}' || token[0] === ';') { + break; + } + + token = this.tokenizer.nextToken(); + } + + for (const tokn of tokens.reverse()) { + this.tokenizer.back(tokn); + } + } + decl(...args) { super.decl(...args); diff --git a/test/parser/interpolation.test.js b/test/parser/interpolation.test.js index 5b9b0be..6aa2e44 100644 --- a/test/parser/interpolation.test.js +++ b/test/parser/interpolation.test.js @@ -41,6 +41,46 @@ test('parses non-interpolation', (t) => { t.is(root.first.selector, '\\@'); }); +test('parses interpolation in at-rule params (#180)', (t) => { + const root = parse('@media @{mq-xs} { .a { color: red } }'); + + t.is(root.first.type, 'atrule'); + t.is(root.first.name, 'media'); + t.is(root.first.params, '@{mq-xs}'); + t.is(root.first.first.selector, '.a'); +}); + +test('parses interpolation in at-rule params without space before bracket (#180)', (t) => { + const root = parse('@media @{mq-xs}{ .a { color: red } }'); + + t.is(root.first.params, '@{mq-xs}'); + t.is(root.first.first.selector, '.a'); +}); + +test('parses interpolation followed by a media feature (#180)', (t) => { + const root = parse('@media @{mq-xs} and (min-width: 5px) { .a { color: red } }'); + + t.is(root.first.params, '@{mq-xs} and (min-width: 5px)'); +}); + +test('parses multiple interpolations in at-rule params (#180)', (t) => { + const root = parse('@media @{a} and @{b} { .a { color: red } }'); + + t.is(root.first.params, '@{a} and @{b}'); +}); + +test('parses interpolation inside at-rule params word (#180)', (t) => { + const root = parse('@media screen and (max-width: @{w}) { .a { color: red } }'); + + t.is(root.first.params, 'screen and (max-width: @{w})'); +}); + +test('parses a variable in at-rule params (#180)', (t) => { + const root = parse('@media @mq-xs { .a { color: red } }'); + + t.is(root.first.params, '@mq-xs'); +}); + // TODO: interpolation doesn't quite work yet test('interpolation', (t) => { const root = parse('@{selector}:hover { @{prop}-size: @{color} }'); diff --git a/test/stringify.test.js b/test/stringify.test.js index 826d356..94164bd 100644 --- a/test/stringify.test.js +++ b/test/stringify.test.js @@ -61,3 +61,9 @@ test('mixin with !important', async (t) => { const result = run(less); t.is(result, less); }); + +test('at-rule with interpolated params (#180)', (t) => { + const less = '@media @{mq-xs} and (min-width: 5px) { .a { color: red } }'; + const result = run(less); + t.is(result, less); +});