Skip to content

Commit 1dafb34

Browse files
author
dkniazevych
committed
parse mixins as Rule nodes; remove LESS specific hacks;
1 parent 7e743aa commit 1dafb34

13 files changed

+172
-480
lines changed

README.md

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -42,28 +42,6 @@ postcss(plugins).process(lessText, { syntax: syntax }).then(function (result) {
4242
});
4343
```
4444

45-
#### Parser options
46-
* mixinsAsAtRules - parse all mixins as [AtRule nodes](https://github.com/postcss/postcss/blob/master/docs/api.md#atrule-node)
47-
48-
* innerMixinsAsRules - parse all inner mixins as [Rule nodes](https://github.com/postcss/postcss/blob/master/docs/api.md#rule-node)
49-
50-
It makes sense to use this option with `mixinsAsAtRules` if you want to parse mixins on the [root level](https://github.com/postcss/postcss/blob/master/docs/api.md#root-node) as `AtRule` and all inner mixins as `Rule` nodes.
51-
52-
Options help you to tell `postcss-less` to _fake_ LESS-specific syntax. For example, [Stylelint] requires the next options:
53-
54-
```js
55-
const syntax = require('postcss-less');
56-
const postcssParserOptions = {
57-
syntax: syntax,
58-
mixinsAsAtRules: true,
59-
innerMixinsAsRules: true
60-
};
61-
62-
postcss().process(lessText, postcssParserOptions).then(function (result) {
63-
result.content // LESS with transformations
64-
});
65-
```
66-
6745
### Inline Comments for PostCSS
6846

6947
This module also enables parsing of single-line comments in CSS source code.
@@ -78,22 +56,6 @@ This module also enables parsing of single-line comments in CSS source code.
7856
Note that you don't need a special stringifier to handle the output; the default
7957
one will automatically convert single line comments into block comments.
8058

81-
## Restrictions
82-
83-
### Skipping mixins without body
84-
`postcss-less` parses all mixins without body as custom nodes with specifix types:
85-
86-
````less
87-
.foo(); //'mixin-function' node
88-
.foo; // 'mixin-inline' node
89-
````
90-
91-
`PostCSS` skips all these mixins, so you won't have them in the result.
92-
93-
### Skipping inner extend
94-
`postcss-less` parses nested `&:extend` constructions as custom nodes with type `nested-extend`.
95-
`PostCSS` doesn't parse this node type and you won't have these constructions in the result.
96-
9759
## Contribution
9860
Please, check our guidelines: [CONTRIBUTING.md](./CONTRIBUTING.md)
9961

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {dot, hash, hashColorPattern} from './globals';
1+
import {dot, hash, hashColorPattern} from './tokenizer/globals';
22

33
const unpaddedFractionalNumbersPattern = /\.[0-9]/;
44

lib/less-parser.js

Lines changed: 103 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,9 @@
1-
import AtRule from 'postcss/lib/at-rule';
21
import Comment from 'postcss/lib/comment';
32
import Parser from 'postcss/lib/parser';
4-
5-
import isMixinName from './tokenizer/is-mixin-name';
3+
import isMixinName from './is-mixin-name';
64
import lessTokenizer from './less-tokenize';
75

8-
const mixinRuleToken = 'mixinRule';
9-
const mixinParamToken = 'mixin-param';
10-
const atRuleToken = 'atrule';
11-
const defaultOptions = {
12-
mixinsAsAtRules: false,
13-
innerMixinsAsRules: false
14-
};
15-
166
export default class LessParser extends Parser {
17-
constructor (input, opts) {
18-
super(input);
19-
20-
Object.assign(this, defaultOptions, opts);
21-
}
22-
237
tokenize () {
248
this.tokens = lessTokenizer(this.input);
259
}
@@ -50,98 +34,128 @@ export default class LessParser extends Parser {
5034
}
5135
}
5236

53-
rule (tokens) {
54-
super.rule(tokens);
55-
56-
const {selector} = this.current;
37+
/**
38+
* @description Create a Rule node
39+
* @param options {{start: number, params: Array}}
40+
*/
41+
createRule (options) {
42+
this.rule(this.tokens.slice(options.start, this.pos + 1));
43+
this.raw(this.current, 'params', options.params);
44+
}
45+
46+
/**
47+
* @description Create a Declaration
48+
* @param options {{start: number}}
49+
*/
50+
createDeclaration (options) {
51+
this.decl(this.tokens.slice(options.start, this.pos + 1));
52+
}
53+
54+
/**
55+
* @description Create a Rule block and close it, because this mixin doesn't have a body
56+
* @param options
57+
*/
58+
mixinWithoutBody (options) {
59+
this.createRule(options);
5760

5861
/**
59-
* We should separate rules and mixins. According to LESS spec (http://lesscss.org/features/#mixins-feature)
60-
* mixins can have the same syntax as inner blocks, so we will handle only mixins as functions,
61-
* that contain symbol '('.
62+
* @description Mark this node
63+
* @type {boolean}
6264
*/
63-
if (isMixinName(selector) && selector.indexOf('(') > 0) {
64-
// this.current is the 'rule' node created in super.rule()
65-
this.current.name = tokens[0][1].slice(1);
65+
this.current.mixinWithoutBody = true;
66+
this.current.important = this.current.selector.indexOf('!important') >= 0;
67+
this.end(this.tokens[this.pos]);
68+
}
6669

67-
// parse inner mixin rule nodes as base rule node
68-
if (this.innerMixinsAsRules && this.current.parent.type !== 'root') {
69-
return;
70-
}
70+
/* eslint-disable max-statements, complexity */
71+
word () {
72+
let token = this.tokens[this.pos];
73+
let end = false;
74+
let colon = false;
75+
let bracket = null;
76+
let brackets = 0;
77+
const start = this.pos;
78+
const isMixin = isMixinName(token[1]);
79+
const params = [];
80+
81+
this.pos += 1;
7182

72-
if (this.mixinsAsAtRules === true) {
73-
this.current.type = atRuleToken;
74-
this.current.lessType = mixinRuleToken;
75-
} else {
76-
this.current.type = mixinRuleToken;
77-
}
83+
while (this.pos < this.tokens.length) {
84+
token = this.tokens[this.pos];
85+
const type = token[0];
7886

79-
this.current.definition = true;
80-
this.current.params = [];
87+
if (type === '(') {
88+
if (!bracket) {
89+
bracket = token;
90+
}
8191

82-
let blockStart = tokens.findIndex((x) => x[0] === '(');
83-
const blockEnd = tokens.findIndex((x) => x[0] === ')');
92+
brackets += 1;
93+
} else if (brackets === 0) {
94+
if (type === ';') {
95+
if (colon) {
96+
this.createDeclaration({start});
97+
return;
98+
}
99+
100+
if (isMixin) {
101+
this.mixinWithoutBody({start, params});
102+
return;
103+
}
84104

85-
tokens.forEach((token) => {
86-
if (token[0] !== mixinParamToken) {
105+
break;
106+
} else if (type === '{') {
107+
this.createRule({start, params});
87108
return;
109+
} else if (type === '}') {
110+
this.pos -= 1;
111+
end = true;
112+
break;
113+
} else if (type === ':') {
114+
colon = true;
88115
}
89-
90-
const index = tokens.indexOf(token);
91-
const param = new AtRule();
92-
let pos = blockStart;
93-
const prev = this.current.params[this.current.params.length - 1];
94-
let termToken = null;
95-
96-
param.name = token[1].slice(1);
97-
param.lessType = mixinParamToken;
98-
param.source = {
99-
start: {line: token[2], column: token[3]},
100-
input: this.input
101-
};
102-
param.raws = {before: '', after: '', between: ''};
103-
104-
if (token[6] && token[6] === 'rest-variables') {
105-
param.restVariables = true;
116+
} else if (type === ')') {
117+
brackets -= 1;
118+
if (brackets === 0) {
119+
bracket = null;
106120
}
121+
}
107122

108-
while (pos < blockEnd) {
109-
termToken = tokens[pos];
110-
111-
if (termToken[0] === ';') {
112-
param.source.end = {line: termToken[2], column: termToken[3]};
113-
param.raws.semicolon = true;
114-
blockStart = pos + 1;
115-
break;
116-
} else if (termToken[0] === 'space') {
117-
param.raws.before += termToken[1];
118-
119-
if (prev) {
120-
prev.raws.after += termToken[1];
121-
}
122-
}
123+
if (brackets || type === 'brackets' || params[0]) {
124+
params.push(token);
125+
}
123126

124-
pos += 1;
125-
}
127+
this.pos += 1;
128+
}
126129

127-
// this is the last param in the block
128-
if (pos === blockEnd) {
129-
pos = index;
130-
while (pos < blockEnd) {
131-
termToken = tokens[pos];
130+
if (this.pos === this.tokens.length) {
131+
this.pos -= 1;
132+
end = true;
133+
}
132134

133-
if (termToken[0] === 'space') {
134-
param.raws.after += termToken[1];
135-
}
135+
if (brackets > 0) {
136+
this.unclosedBracket(bracket);
137+
}
136138

137-
pos += 1;
139+
if (end) {
140+
if (colon) {
141+
while (this.pos > start) {
142+
token = this.tokens[this.pos][0];
143+
if (token !== 'space' && token !== 'comment') {
144+
break;
138145
}
139146

140-
param.source.end = {line: token[4], column: token[5]};
147+
this.pos -= 1;
141148
}
142149

143-
this.current.params.push(param);
144-
});
150+
this.createDeclaration({start});
151+
return;
152+
} else if (isMixin) {
153+
this.mixinWithoutBody({start, params});
154+
return;
155+
}
145156
}
157+
158+
this.unknownWord(start);
146159
}
160+
/* eslint-enable max-statements, complexity */
147161
}

lib/less-stringifier.js

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,4 @@ export default class LessStringifier extends Stringifier {
1111
this.builder(`/*${ left }${ node.text }${ right }*/`, node);
1212
}
1313
}
14-
15-
atrule (node, semicolon) {
16-
const {lessType} = node;
17-
18-
if (lessType) {
19-
if (this[lessType]) {
20-
this[lessType](node, semicolon);
21-
}
22-
} else {
23-
super.atrule(node, semicolon);
24-
}
25-
}
26-
27-
mixinRule (node) {
28-
this.builder(node.source.input.css);
29-
}
3014
}

lib/tokenizer/find-unclosed-parenthesis-index.js

Lines changed: 0 additions & 20 deletions
This file was deleted.

lib/tokenizer/globals.js

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ export const openedCurlyBracket = '{'.charCodeAt(0);
1313
export const closedCurlyBracket = '}'.charCodeAt(0);
1414
export const semicolon = ';'.charCodeAt(0);
1515
export const asterisk = '*'.charCodeAt(0);
16-
export const ampersand = '&'.charCodeAt(0);
1716
export const colon = ':'.charCodeAt(0);
1817
export const comma = ','.charCodeAt(0);
1918
export const dot = '.'.charCodeAt(0);
@@ -24,6 +23,4 @@ export const atEndPattern = /[ \n\t\r\f\{\(\)'"`\\;/]/g;
2423
export const wordEndPattern = /[ \n\t\r\f\(\)\{\}:,;@!'"`\\#]|\/(?=\*)/g;
2524
export const badBracketPattern = /.[\\\/\("'\n]/;
2625
export const variablePattern = /^@[^:\(\{]+:/;
27-
export const hashColorPattern = /^#[0-9a-fA-F]{6}$|^#[0-9a-fA-F]{3}$/;
28-
export const nestedExtendWord = '&:extend';
29-
export const nestedExtendWordLength = nestedExtendWord.length;
26+
export const hashColorPattern = /^#[0-9a-fA-F]{6}$|^#[0-9a-fA-F]{3}$/;

lib/tokenizer/tokenize-at-rule.js

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {atEndPattern, openedCurlyBracket, variablePattern, wordEndPattern} from './globals';
2-
import findUnclosedParenthesisIndex from './find-unclosed-parenthesis-index';
32
import unclosed from './unclosed';
43

54
export default function tokenizeAtRule (state) {
@@ -56,25 +55,13 @@ export default function tokenizeAtRule (state) {
5655

5756
state.cssPart = state.css.slice(state.pos, state.nextPos + 1);
5857
state.token = 'word';
59-
} else if (findUnclosedParenthesisIndex(state) >= 0) {
60-
// it's a variable/param inside a mixin declaration
61-
state.token = 'mixin-param';
6258
}
6359

6460
state.tokens.push([
6561
state.token, state.cssPart,
6662
state.line, state.pos - state.offset,
6763
state.line, state.nextPos - state.offset
6864
]);
69-
70-
if (state.token === 'mixin-param') {
71-
const restVariablesSymbol = '...';
72-
const restVariablesPos = state.cssPart.indexOf(restVariablesSymbol);
73-
74-
if (restVariablesPos >= 0 && restVariablesPos + restVariablesSymbol.length === state.cssPart.length) {
75-
state.tokens[state.tokens.length - 1].push('rest-variables');
76-
}
77-
}
7865
}
7966

8067
state.pos = state.nextPos;

0 commit comments

Comments
 (0)