/* eslint no-param-reassign: off */ const Comment = require('postcss/lib/comment'); const Parser = require('postcss/lib/parser'); const { isInlineComment } = require('./nodes/inline-comment'); const { interpolation } = require('./nodes/interpolation'); const { isMixinToken } = require('./nodes/mixin'); const importNode = require('./nodes/import'); const variableNode = require('./nodes/variable'); const importantPattern = /(!\s*important)$/i; module.exports = class LessParser extends Parser { constructor(...args) { super(...args); this.lastNode = null; } atrule(token) { if (interpolation.bind(this)(token)) { return; } super.atrule(token); importNode(this.lastNode); variableNode(this.lastNode); } decl(...args) { super.decl(...args); // #123: add `extend` decorator to nodes const extendPattern = /extend\(.+\)/i; if (extendPattern.test(this.lastNode.value)) { this.lastNode.extend = true; } } 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; } inlineComment(token) { const node = new Comment(); const text = token[1].slice(2); this.init(node, token[2], token[3]); node.source.end = { line: token[4], column: token[5] }; node.inline = true; node.raws.begin = '//'; if (/^\s*$/.test(text)) { node.text = ''; node.raws.left = text; node.raws.right = ''; } else { const match = text.match(/^(\s*)([^]*[^\s])(\s*)$/); [, node.raws.left, node.text, node.raws.right] = match; } } 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.reduce((last, t, i) => (t[0] === ')' ? i : last)); 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 importantTokens = []; for (const token of tokens) { if (token[1] === '!' || importantTokens.length) { importantTokens.push(token); } if (token[1] === 'important') { break; } } if (importantTokens.length) { const [bangToken] = importantTokens; const bangIndex = tokens.indexOf(bangToken); const last = importantTokens[importantTokens.length - 1]; const start = [bangToken[2], bangToken[3]]; const end = [last[4], last[5]]; const combined = importantTokens.map((t) => t[1]).join(''); const newToken = ['word', combined].concat(start, end); tokens.splice(bangIndex, importantTokens.length, newToken); } 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); } } rule(tokens) { const last = tokens[tokens.length - 1]; const prev = tokens[tokens.length - 2]; if (prev[0] === 'at-word' && last[0] === '{') { this.tokenizer.back(last); if (interpolation.bind(this)(prev)) { const newToken = this.tokenizer.nextToken(); tokens = tokens.slice(0, tokens.length - 2).concat([newToken]); for (const tokn of tokens.reverse()) { this.tokenizer.back(tokn); } return; } } super.rule(tokens); // #123: add `extend` decorator to nodes const extendPattern = /:extend\(.+\)/i; if (extendPattern.test(this.lastNode.selector)) { this.lastNode.extend = true; } } unknownWord(tokens) { // NOTE: keep commented for examining unknown structures // console.log('unknown', tokens); 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)) { this.mixin(tokens); return; } super.unknownWord(tokens); } };