UNPKG

5.95 kBJavaScriptView Raw
1/* eslint no-param-reassign: off */
2
3const Comment = require('postcss/lib/comment');
4const Parser = require('postcss/lib/parser');
5
6const { isInlineComment } = require('./nodes/inline-comment');
7const { interpolation } = require('./nodes/interpolation');
8const { isMixinToken } = require('./nodes/mixin');
9const importNode = require('./nodes/import');
10const variableNode = require('./nodes/variable');
11
12const importantPattern = /(!\s*important)$/i;
13
14module.exports = class LessParser extends Parser {
15 constructor(...args) {
16 super(...args);
17
18 this.lastNode = null;
19 }
20
21 atrule(token) {
22 if (interpolation.bind(this)(token)) {
23 return;
24 }
25
26 super.atrule(token);
27 importNode(this.lastNode);
28 variableNode(this.lastNode);
29 }
30
31 decl(...args) {
32 super.decl(...args);
33
34 // #123: add `extend` decorator to nodes
35 const extendPattern = /extend\(.+\)/i;
36
37 if (extendPattern.test(this.lastNode.value)) {
38 this.lastNode.extend = true;
39 }
40 }
41
42 each(tokens) {
43 // prepend a space so the `name` will be parsed correctly
44 tokens[0][1] = ` ${tokens[0][1]}`;
45
46 const firstParenIndex = tokens.findIndex((t) => t[0] === '(');
47 const lastParen = tokens.reverse().find((t) => t[0] === ')');
48 const lastParenIndex = tokens.reverse().indexOf(lastParen);
49 const paramTokens = tokens.splice(firstParenIndex, lastParenIndex);
50 const params = paramTokens.map((t) => t[1]).join('');
51
52 for (const token of tokens.reverse()) {
53 this.tokenizer.back(token);
54 }
55
56 this.atrule(this.tokenizer.nextToken());
57 this.lastNode.function = true;
58 this.lastNode.params = params;
59 }
60
61 init(node, line, column) {
62 super.init(node, line, column);
63 this.lastNode = node;
64 }
65
66 inlineComment(token) {
67 const node = new Comment();
68 const text = token[1].slice(2);
69
70 this.init(node, token[2], token[3]);
71
72 node.source.end = { line: token[4], column: token[5] };
73 node.inline = true;
74 node.raws.begin = '//';
75
76 if (/^\s*$/.test(text)) {
77 node.text = '';
78 node.raws.left = text;
79 node.raws.right = '';
80 } else {
81 const match = text.match(/^(\s*)([^]*[^\s])(\s*)$/);
82 [, node.raws.left, node.text, node.raws.right] = match;
83 }
84 }
85
86 mixin(tokens) {
87 const [first] = tokens;
88 const identifier = first[1].slice(0, 1);
89 const bracketsIndex = tokens.findIndex((t) => t[0] === 'brackets');
90 const firstParenIndex = tokens.findIndex((t) => t[0] === '(');
91 let important = '';
92
93 // fix for #86. if rulesets are mixin params, they need to be converted to a brackets token
94 if ((bracketsIndex < 0 || bracketsIndex > 3) && firstParenIndex > 0) {
95 const lastParenIndex = tokens.reduce((last, t, i) => (t[0] === ')' ? i : last));
96
97 const contents = tokens.slice(firstParenIndex, lastParenIndex + firstParenIndex);
98 const brackets = contents.map((t) => t[1]).join('');
99 const [paren] = tokens.slice(firstParenIndex);
100 const start = [paren[2], paren[3]];
101 const [last] = tokens.slice(lastParenIndex, lastParenIndex + 1);
102 const end = [last[2], last[3]];
103 const newToken = ['brackets', brackets].concat(start, end);
104
105 const tokensBefore = tokens.slice(0, firstParenIndex);
106 const tokensAfter = tokens.slice(lastParenIndex + 1);
107 tokens = tokensBefore;
108 tokens.push(newToken);
109 tokens = tokens.concat(tokensAfter);
110 }
111
112 const importantTokens = [];
113
114 for (const token of tokens) {
115 if (token[1] === '!' || importantTokens.length) {
116 importantTokens.push(token);
117 }
118
119 if (token[1] === 'important') {
120 break;
121 }
122 }
123
124 if (importantTokens.length) {
125 const [bangToken] = importantTokens;
126 const bangIndex = tokens.indexOf(bangToken);
127 const last = importantTokens[importantTokens.length - 1];
128 const start = [bangToken[2], bangToken[3]];
129 const end = [last[4], last[5]];
130 const combined = importantTokens.map((t) => t[1]).join('');
131 const newToken = ['word', combined].concat(start, end);
132 tokens.splice(bangIndex, importantTokens.length, newToken);
133 }
134
135 const importantIndex = tokens.findIndex((t) => importantPattern.test(t[1]));
136
137 if (importantIndex > 0) {
138 [, important] = tokens[importantIndex];
139 tokens.splice(importantIndex, 1);
140 }
141
142 for (const token of tokens.reverse()) {
143 this.tokenizer.back(token);
144 }
145
146 this.atrule(this.tokenizer.nextToken());
147 this.lastNode.mixin = true;
148 this.lastNode.raws.identifier = identifier;
149
150 if (important) {
151 this.lastNode.important = true;
152 this.lastNode.raws.important = important;
153 }
154 }
155
156 other(token) {
157 if (!isInlineComment.bind(this)(token)) {
158 super.other(token);
159 }
160 }
161
162 rule(tokens) {
163 const last = tokens[tokens.length - 1];
164 const prev = tokens[tokens.length - 2];
165
166 if (prev[0] === 'at-word' && last[0] === '{') {
167 this.tokenizer.back(last);
168 if (interpolation.bind(this)(prev)) {
169 const newToken = this.tokenizer.nextToken();
170
171 tokens = tokens.slice(0, tokens.length - 2).concat([newToken]);
172
173 for (const tokn of tokens.reverse()) {
174 this.tokenizer.back(tokn);
175 }
176
177 return;
178 }
179 }
180
181 super.rule(tokens);
182
183 // #123: add `extend` decorator to nodes
184 const extendPattern = /:extend\(.+\)/i;
185
186 if (extendPattern.test(this.lastNode.selector)) {
187 this.lastNode.extend = true;
188 }
189 }
190
191 unknownWord(tokens) {
192 // NOTE: keep commented for examining unknown structures
193 // console.log('unknown', tokens);
194
195 const [first] = tokens;
196
197 // #121 support `each` - http://lesscss.org/functions/#list-functions-each
198 if (tokens[0][1] === 'each' && tokens[1][0] === '(') {
199 this.each(tokens);
200 return;
201 }
202
203 // TODO: move this into a util function/file
204 if (isMixinToken(first)) {
205 this.mixin(tokens);
206 return;
207 }
208
209 super.unknownWord(tokens);
210 }
211};