Skip to content

Commit 4c63e71

Browse files
committed
Merge pull request #36 from webschik/webschik.35.escaped-string
webschik.35.escaped-string
2 parents 48d925f + fcb9afa commit 4c63e71

10 files changed

+121
-37
lines changed

lib/less-tokenize.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ export default function lessTokenize (input) {
1616
while (state.pos < state.length) {
1717
state.symbolCode = state.css.charCodeAt(state.pos);
1818
state.symbol = state.css[state.pos];
19-
state.nextSymbolCode = null;
2019
state.nextPos = null;
2120
state.escaped = null;
2221
state.lines = null;

lib/tokenizer/find-end-of-escaping.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import {backTick, backslash, doubleQuote, singleQuote} from './globals';
2+
3+
/**
4+
* @param state
5+
* @returns {number}
6+
*/
7+
export default function findEndOfEscaping (state) {
8+
let openQuotesCount = 0;
9+
let quoteCode = -1;
10+
11+
for (let i = state.pos + 1; i < state.length; i++) {
12+
const symbolCode = state.css.charCodeAt(i);
13+
const prevSymbolCode = state.css.charCodeAt(i - 1);
14+
15+
if (
16+
prevSymbolCode !== backslash &&
17+
(symbolCode === singleQuote || symbolCode === doubleQuote || symbolCode === backTick)
18+
) {
19+
if (quoteCode === -1) {
20+
quoteCode = symbolCode;
21+
openQuotesCount++;
22+
} else if (symbolCode === quoteCode) {
23+
openQuotesCount--;
24+
25+
if (!openQuotesCount) {
26+
return i;
27+
}
28+
}
29+
}
30+
}
31+
32+
return -1;
33+
}

lib/tokenizer/globals.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export const singleQuote = '\''.charCodeAt(0);
22
export const doubleQuote = '"'.charCodeAt(0);
33
export const backslash = '\\'.charCodeAt(0);
4+
export const backTick = '`'.charCodeAt(0);
45
export const slash = '/'.charCodeAt(0);
56
export const newline = '\n'.charCodeAt(0);
67
export const space = ' '.charCodeAt(0);

lib/tokenizer/is-escaping.js

Lines changed: 5 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,9 @@
1-
import findEndOfExpression from './find-end-of-expression';
2-
import {tilde} from './globals';
1+
import {backTick, doubleQuote, singleQuote, tilde} from './globals';
32

4-
export default function isEscaping (state) {
5-
let result = false;
6-
7-
if (state.symbolCode === tilde) {
8-
const quotePattern = /\s*['"`]/g;
9-
10-
quotePattern.lastIndex = state.pos + 1;
3+
const nextSymbolVariants = [backTick, doubleQuote, singleQuote];
114

12-
const match = quotePattern.exec(state.css);
13-
14-
if (match && match.index === state.pos + 1) {
15-
const end = findEndOfExpression(state.css, state.length, quotePattern.lastIndex + 1);
16-
17-
if (end !== -1) {
18-
result = true;
19-
state.nextPos = end;
20-
}
21-
}
22-
}
5+
export default function isEscaping (state) {
6+
const nextSymbolCode = state.css.charCodeAt(state.pos + 1);
237

24-
return result;
8+
return state.symbolCode === tilde && nextSymbolVariants.indexOf(nextSymbolCode) >= 0;
259
}

lib/tokenizer/tokenize-default.js

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,32 @@ import {
33
slash,
44
wordEndPattern
55
} from './globals';
6+
import findEndOfEscaping from './find-end-of-escaping';
67
import isEscaping from './is-escaping';
78
import tokenizeInlineComment from './tokenize-inline-comment';
89
import tokenizeMultilineComment from './tokenize-multiline-comment';
10+
import unclosed from './unclosed';
911

1012
export default function tokenizeDefault (state) {
11-
state.nextSymbolCode = state.css.charCodeAt(state.pos + 1);
13+
const nextSymbolCode = state.css.charCodeAt(state.pos + 1);
1214

13-
if (state.symbolCode === slash && state.nextSymbolCode === asterisk) {
15+
if (state.symbolCode === slash && nextSymbolCode === asterisk) {
1416
tokenizeMultilineComment(state);
15-
} else if (state.symbolCode === slash && state.nextSymbolCode === slash) {
17+
} else if (state.symbolCode === slash && nextSymbolCode === slash) {
1618
tokenizeInlineComment(state);
1719
} else {
18-
if (isEscaping(state) === false) {
20+
if (isEscaping(state)) {
21+
const pos = findEndOfEscaping(state);
22+
23+
if (pos < 0) {
24+
unclosed(state, 'escaping');
25+
} else {
26+
state.nextPos = pos;
27+
}
28+
} else {
1929
wordEndPattern.lastIndex = state.pos + 1;
2030
wordEndPattern.test(state.css);
31+
2132
if (wordEndPattern.lastIndex === 0) {
2233
state.nextPos = state.css.length - 1;
2334
} else {

lib/tokenizer/tokenize-opened-parenthesis.js

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,19 @@ function findClosedParenthesisPosition (css, length, start) {
3434
// it is not very reasonable to reduce complexity beyond this level
3535
// eslint-disable-next-line complexity
3636
export default function tokenizeOpenedParenthesis (state) {
37-
state.nextSymbolCode = state.css.charCodeAt(state.pos + 1);
37+
const nextSymbolCode = state.css.charCodeAt(state.pos + 1);
3838
const tokensCount = state.tokens.length;
3939
const prevTokenCssPart = tokensCount ? state.tokens[tokensCount - 1][1] : '';
4040

4141
if (
4242
prevTokenCssPart === 'url' &&
43-
state.nextSymbolCode !== singleQuote &&
44-
state.nextSymbolCode !== doubleQuote &&
45-
state.nextSymbolCode !== space &&
46-
state.nextSymbolCode !== newline &&
47-
state.nextSymbolCode !== tab &&
48-
state.nextSymbolCode !== feed &&
49-
state.nextSymbolCode !== carriageReturn
43+
nextSymbolCode !== singleQuote &&
44+
nextSymbolCode !== doubleQuote &&
45+
nextSymbolCode !== space &&
46+
nextSymbolCode !== newline &&
47+
nextSymbolCode !== tab &&
48+
nextSymbolCode !== feed &&
49+
nextSymbolCode !== carriageReturn
5050
) {
5151
state.nextPos = state.pos;
5252

@@ -91,7 +91,7 @@ export default function tokenizeOpenedParenthesis (state) {
9191
]);
9292
} else {
9393
const badBracket = badBracketPattern.test(state.cssPart);
94-
94+
9595
if (state.nextPos === -1 || badBracket) {
9696
state.tokens.push([
9797
state.symbol, state.symbol,

lib/tokenizer/tokenize-quotes.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,6 @@ export default function tokenizeQuotes (state) {
2525
state.line, state.pos - state.offset,
2626
state.line, state.nextPos - state.offset
2727
]);
28+
2829
state.pos = state.nextPos;
2930
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "postcss-less",
3-
"version": "0.9.0",
3+
"version": "0.10.0",
44
"description": "LESS parser for PostCSS",
55
"keywords": [
66
"css",

test/parser/escaping.spec.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
// chai uses expressions for validation
2+
/* eslint no-unused-expressions: 0 */
3+
4+
import {expect} from 'chai';
5+
import parse from '../../lib/less-parse';
6+
7+
describe('Parser', () => {
8+
describe('Escaping', () => {
9+
it('parses escaped string', () => {
10+
const code = `
11+
@testVar: 10px;
12+
13+
.test-wrap {
14+
.selector {
15+
height: calc(~"100vh - @{testVar}");
16+
}
17+
}
18+
`;
19+
const root = parse(code);
20+
21+
expect(root.first.prop).to.eql('@testVar');
22+
expect(root.first.value).to.eql('10px');
23+
expect(root.last.first.first.prop).to.eql('height');
24+
expect(root.last.first.first.value).to.eql('calc(~"100vh - @{testVar}")');
25+
});
26+
27+
it('parses escaping inside nested rules', () => {
28+
const code = `
29+
.test1 {
30+
.another-test {
31+
prop1: function(~"@{variable}");
32+
}
33+
}
34+
35+
.test2 {
36+
prop2: function(~\`@{test}\`);
37+
}
38+
39+
.test3 {
40+
filter: ~"alpha(opacity='@{opacity}')";
41+
}
42+
`;
43+
const root = parse(code);
44+
45+
expect(root.nodes[0].first.first.prop).to.eql('prop1');
46+
expect(root.nodes[0].first.first.value).to.eql('function(~"@{variable}")');
47+
expect(root.nodes[1].first.prop).to.eql('prop2');
48+
expect(root.nodes[1].first.value).to.eql('function(~`@{test}`)');
49+
expect(root.nodes[2].first.prop).to.eql('filter');
50+
expect(root.nodes[2].first.value).to.eql('~"alpha(opacity=\'@{opacity}\')"');
51+
});
52+
});
53+
});

test/tokenize.spec.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,9 @@ describe('#tokenize()', () => {
9898
['word', 'content', 1, 11, 1, 17],
9999
[':', ':', 1, 18],
100100
['space', ' '],
101-
['word', '~ "a"', 1, 20, 1, 25],
101+
['word', '~', 1, 20, 1, 20],
102+
['space', ' '],
103+
['string', '"a"', 1, 23, 1, 25],
102104
[';', ';', 1, 26],
103105
['space', ' '],
104106
['}', '}', 1, 28]

0 commit comments

Comments
 (0)