diff --git a/lib/LessParser.js b/lib/LessParser.js index 599901f..7398fe7 100644 --- a/lib/LessParser.js +++ b/lib/LessParser.js @@ -28,6 +28,25 @@ module.exports = class LessParser extends Parser { variableNode(this.lastNode); } + each(tokens) { + // prepend a space so the `name` will be parsed correctly + tokens[0][1] = ` ${tokens[0][1]}`; + + const firstParenIndex = tokens.findIndex((t) => t[0] === '('); + const lastParen = tokens.reverse().find((t) => t[0] === ')'); + const lastParenIndex = tokens.reverse().indexOf(lastParen); + const paramTokens = tokens.splice(firstParenIndex, lastParenIndex); + const params = paramTokens.map((t) => t[1]).join(''); + + for (const token of tokens.reverse()) { + this.tokenizer.back(token); + } + + this.atrule(this.tokenizer.nextToken()); + this.lastNode.function = true; + this.lastNode.params = params; + } + init(node, line, column) { super.init(node, line, column); this.lastNode = node; @@ -53,6 +72,53 @@ module.exports = class LessParser extends Parser { } } + mixin(tokens) { + const [first] = tokens; + const identifier = first[1].slice(0, 1); + const bracketsIndex = tokens.findIndex((t) => t[0] === 'brackets'); + const firstParenIndex = tokens.findIndex((t) => t[0] === '('); + let important = ''; + + // fix for #86. if rulesets are mixin params, they need to be converted to a brackets token + if ((bracketsIndex < 0 || bracketsIndex > 3) && firstParenIndex > 0) { + const lastParenIndex = tokens.findIndex((t) => t[0] === ')'); + + const contents = tokens.slice(firstParenIndex, lastParenIndex + firstParenIndex); + const brackets = contents.map((t) => t[1]).join(''); + const [paren] = tokens.slice(firstParenIndex); + const start = [paren[2], paren[3]]; + const [last] = tokens.slice(lastParenIndex, lastParenIndex + 1); + const end = [last[2], last[3]]; + const newToken = ['brackets', brackets].concat(start, end); + + const tokensBefore = tokens.slice(0, firstParenIndex); + const tokensAfter = tokens.slice(lastParenIndex + 1); + tokens = tokensBefore; + tokens.push(newToken); + tokens = tokens.concat(tokensAfter); + } + + const importantIndex = tokens.findIndex((t) => importantPattern.test(t[1])); + + if (importantIndex > 0) { + [, important] = tokens[importantIndex]; + tokens.splice(importantIndex, 1); + } + + for (const token of tokens.reverse()) { + this.tokenizer.back(token); + } + + this.atrule(this.tokenizer.nextToken()); + this.lastNode.mixin = true; + this.lastNode.raws.identifier = identifier; + + if (important) { + this.lastNode.important = true; + this.lastNode.raws.important = important; + } + } + other(token) { if (!isInlineComment.bind(this)(token)) { super.other(token); @@ -84,56 +150,18 @@ module.exports = class LessParser extends Parser { unknownWord(tokens) { // NOTE: keep commented for examining unknown structures // console.log('unknown', tokens); - // console.log(this.root.first); const [first] = tokens; + // #121 support `each` - http://lesscss.org/functions/#list-functions-each + if (tokens[0][1] === 'each' && tokens[1][0] === '(') { + this.each(tokens); + return; + } + // TODO: move this into a util function/file if (isMixinToken(first)) { - const identifier = first[1].slice(0, 1); - const bracketsIndex = tokens.findIndex((t) => t[0] === 'brackets'); - const firstParenIndex = tokens.findIndex((t) => t[0] === '('); - let important = ''; - - // fix for #86. if rulesets are mixin params, they need to be converted to a brackets token - if ((bracketsIndex < 0 || bracketsIndex > 3) && firstParenIndex > 0) { - const lastParenIndex = tokens.findIndex((t) => t[0] === ')'); - - const contents = tokens.slice(firstParenIndex, lastParenIndex + firstParenIndex); - const brackets = contents.map((t) => t[1]).join(''); - const [paren] = tokens.slice(firstParenIndex); - const start = [paren[2], paren[3]]; - const [last] = tokens.slice(lastParenIndex, lastParenIndex + 1); - const end = [last[2], last[3]]; - const newToken = ['brackets', brackets].concat(start, end); - - const tokensBefore = tokens.slice(0, firstParenIndex); - const tokensAfter = tokens.slice(lastParenIndex + 1); - tokens = tokensBefore; - tokens.push(newToken); - tokens = tokens.concat(tokensAfter); - } - - const importantIndex = tokens.findIndex((t) => importantPattern.test(t[1])); - - if (importantIndex > 0) { - [, important] = tokens[importantIndex]; - tokens.splice(importantIndex, 1); - } - - for (const token of tokens.reverse()) { - this.tokenizer.back(token); - } - - this.atrule(this.tokenizer.nextToken()); - this.lastNode.mixin = true; - this.lastNode.raws.identifier = identifier; - - if (important) { - this.lastNode.important = true; - this.lastNode.raws.important = important; - } - + this.mixin(tokens); return; } diff --git a/lib/LessStringifier.js b/lib/LessStringifier.js index 8cb36c2..a101559 100644 --- a/lib/LessStringifier.js +++ b/lib/LessStringifier.js @@ -2,12 +2,13 @@ const Stringifier = require('postcss/lib/stringifier'); module.exports = class LessStringifier extends Stringifier { atrule(node, semicolon) { - if (!node.mixin && !node.variable) { + if (!node.mixin && !node.variable && !node.function) { super.atrule(node, semicolon); return; } - let name = `${node.raws.identifier || '@'}${node.name}`; + const identifier = node.function ? '' : node.raws.identifier || '@'; + let name = `${identifier}${node.name}`; let params = node.params ? this.rawValue(node, 'params') : ''; const important = node.raws.important || ''; diff --git a/test/parser/function.test.js b/test/parser/function.test.js new file mode 100644 index 0000000..7f81be1 --- /dev/null +++ b/test/parser/function.test.js @@ -0,0 +1,20 @@ +const test = require('ava'); + +const { parse, nodeToString } = require('../../lib'); + +test('each (#121)', (t) => { + const params = `(@colors, { + .@{value}-color { + color: @value; + } +})`; + const less = `each${params};`; + const root = parse(less); + const { first } = root; + + t.is(first.name, 'each'); + t.is(first.params, params); + t.truthy(first.function); + + t.is(nodeToString(root), less); +});