Skip to content

Commit 9404262

Browse files
evilebottnawishellscape
authored andcommitted
feat: double slash comments (shellscape#51)
1 parent 81e32fb commit 9404262

File tree

6 files changed

+271
-10
lines changed

6 files changed

+271
-10
lines changed

API.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,11 @@ not documented here are subject to change at any point.*
107107
parser.comment({ value: 'Affirmative, Dave. I read you.' });
108108
// → /* Affirmative, Dave. I read you. */
109109
```
110+
111+
```js
112+
parser.comment({ value: 'Affirmative, Dave. I read you.', inline: true });
113+
// → // Affirmative, Dave. I read you.
114+
```
110115

111116
Arguments:
112117

lib/comment.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ class Comment extends Node {
77
constructor (opts) {
88
super(opts);
99
this.type = 'comment';
10+
this.inline = opts.inline || false;
1011
}
1112

1213
toString () {
1314
return [
1415
this.raws.before,
15-
'/*',
16+
this.inline ? '//' : '/*',
1617
String(this.value),
17-
'*/',
18+
this.inline ? '' : '*/',
1819
this.raws.after
1920
].join('');
2021
}

lib/parser.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,18 @@ module.exports = class Parser {
9494
}
9595

9696
comment () {
97-
let node = new Comment({
98-
value: this.currToken[1].replace(/\/\*|\*\//g, ''),
97+
let inline = false,
98+
value = this.currToken[1].replace(/\/\*|\*\//g, ''),
99+
node;
100+
101+
if (this.options.loose && value.startsWith("//")) {
102+
value = value.substring(2);
103+
inline = true;
104+
}
105+
106+
node = new Comment({
107+
value: value,
108+
inline: inline,
99109
source: {
100110
start: {
101111
line: this.currToken[2],

lib/tokenize.js

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ module.exports = function tokenize (input, options) {
4646
offset = -1,
4747
line = 1,
4848
pos = 0,
49+
parentCount = 0,
50+
isURLArg = null,
4951

5052
code, next, quote, lines, last, content, escape, nextLine, nextOffset,
5153
escaped, escapePos, nextChar;
@@ -136,6 +138,11 @@ module.exports = function tokenize (input, options) {
136138
break;
137139

138140
case openParen:
141+
parentCount++;
142+
isURLArg = !isURLArg && parentCount === 1 &&
143+
tokens.length > 0 &&
144+
tokens[tokens.length - 1][0] === "word" &&
145+
tokens[tokens.length - 1][1] === "url";
139146
tokens.push(['(', '(',
140147
line, pos - offset,
141148
line, next - offset,
@@ -144,6 +151,8 @@ module.exports = function tokenize (input, options) {
144151
break;
145152

146153
case closeParen:
154+
parentCount--;
155+
isURLArg = !isURLArg && parentCount === 1;
147156
tokens.push([')', ')',
148157
line, pos - offset,
149158
line, next - offset,
@@ -249,10 +258,19 @@ module.exports = function tokenize (input, options) {
249258
break;
250259

251260
default:
252-
if (code === slash && css.charCodeAt(pos + 1) === asterisk) {
253-
next = css.indexOf('*/', pos + 2) + 1;
254-
if (next === 0) {
255-
unclosed('comment', '*/');
261+
if (code === slash && (css.charCodeAt(pos + 1) === asterisk || (options.loose && !isURLArg && css.charCodeAt(pos + 1) === slash))) {
262+
const isStandardComment = css.charCodeAt(pos + 1) === asterisk;
263+
264+
if (isStandardComment) {
265+
next = css.indexOf('*/', pos + 2) + 1;
266+
if (next === 0) {
267+
unclosed('comment', '*/');
268+
}
269+
}
270+
else {
271+
const newlinePos = css.indexOf('\n', pos + 2);
272+
273+
next = newlinePos !== -1 ? newlinePos - 1 : length;
256274
}
257275

258276
content = css.slice(pos, next + 1);

test/comment.js

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ describe('Parser → Comment', () => {
1414
failures;
1515

1616
fixtures = [
17+
// standard comments
1718
{
1819
it: 'should parse comments',
1920
test: '/*before*/ 1px /*between*/ 1px /*after*/',
@@ -53,6 +54,97 @@ describe('Parser → Comment', () => {
5354
{ type: 'paren', value: ')' }
5455
]
5556
},
57+
{
58+
it: 'should parse only one empty comment',
59+
test: '/**/',
60+
expected: [
61+
{ type: 'comment', value: '', raws: { before: '' } },
62+
]
63+
},
64+
65+
// double slash comment
66+
{
67+
it: 'should parse double slash comments',
68+
test: '//before\n 1px //between\n 1px //after\n',
69+
loose: true,
70+
expected: [
71+
{ type: 'comment', value: 'before', inline: true },
72+
{ type: 'number', value: '1', unit: 'px', raws: { before: '\n ' } },
73+
{ type: 'comment', value: 'between', inline: true, raws: { before: ' ' } },
74+
{ type: 'number', value: '1', unit: 'px', raws: { before: '\n ' } },
75+
{ type: 'comment', value: 'after', inline: true, raws: { before: ' ' } }
76+
]
77+
},
78+
{
79+
it: 'should parse double slash comments inside functions',
80+
test: 'rgba( 0, 55/55, 0//,.5\n )',
81+
loose: true,
82+
expected: [
83+
{ type: 'func', value: 'rgba' },
84+
{ type: 'paren', value: '(' },
85+
{ type: 'number', value: '0', raws: { before: ' ' } },
86+
{ type: 'comma', value: ',' },
87+
{ type: 'number', value: '55', raws: { before: ' ' } },
88+
{ type: 'operator', value: '/' },
89+
{ type: 'number', value: '55' },
90+
{ type: 'comma', value: ',' },
91+
{ type: 'number', value: '0', raws: { before: ' ' } },
92+
{ type: 'comment', value: ',.5' },
93+
94+
]
95+
},
96+
{
97+
it: 'should parse double slash comments when url function have nested function',
98+
test: 'url(var(//comment\n))',
99+
loose: true,
100+
expected: [
101+
{ type: 'func', value: 'url' },
102+
{ type: 'paren', value: '(' },
103+
{ type: 'func', value: 'var' },
104+
{ type: 'paren', value: '(' },
105+
{ type: 'comment', value: 'comment' },
106+
{ type: 'paren', value: ')' },
107+
{ type: 'paren', value: ')' }
108+
]
109+
},
110+
{
111+
it: 'should parse only one double slash empty comment',
112+
test: '//\n',
113+
loose: true,
114+
expected: [
115+
{ type: 'comment', value: '', inline: true, raws: { before: '' } },
116+
]
117+
},
118+
{
119+
it: 'should parse only one double slash empty comment without newline',
120+
test: '//',
121+
loose: true,
122+
expected: [
123+
{ type: 'comment', value: '', inline: true, raws: { before: '' } },
124+
]
125+
},
126+
127+
// mixed standard and double slash comments
128+
{
129+
it: 'should parse mixed comments #1',
130+
test: '/*before*/\n//between\n/*after*/',
131+
loose: true,
132+
expected: [
133+
{ type: 'comment', value: 'before', inline: false, raws: { before: '' } },
134+
{ type: 'comment', value: 'between', inline: true, raws: { before: '\n' } },
135+
{ type: 'comment', value: 'after', inline: false, raws: { before: '\n' } },
136+
]
137+
},
138+
{
139+
it: 'should parse mixed comments #2',
140+
test: '//before\n/*between*/\n//after',
141+
loose: true,
142+
expected: [
143+
{ type: 'comment', value: 'before', inline: true, raws: { before: '' } },
144+
{ type: 'comment', value: 'between', inline: false, raws: { before: '\n' } },
145+
{ type: 'comment', value: 'after', inline: true, raws: { before: '\n' } },
146+
]
147+
},
56148

57149
// these tests are based on the spec rules surrounding legacy
58150
// quotation-mark–less url notation.
@@ -61,6 +153,8 @@ describe('Parser → Comment', () => {
61153
// doesn't start with either kind of quotation mark.
62154
// postcss-value-parser ignores those rules and allows comments within a url()
63155
// function if the first param starts with a space.
156+
157+
// standard comments
64158
{
65159
it: 'should not parse comments at the end of url functions with quoted first argument, lead by a space',
66160
test: 'url( "/demo/bg.png" /*comment*/ )',
@@ -90,6 +184,29 @@ describe('Parser → Comment', () => {
90184
{ type: 'word', value: ' /demo/bg.png /*comment*/ ' },
91185
{ type: 'paren', value: ')' }
92186
]
187+
},
188+
189+
// double slash comments
190+
{
191+
it: 'should not parse double slash as comment in url function',
192+
test: 'url(//bar\n http://domain.com/foo/bar //foo\n)',
193+
loose: true,
194+
expected: [
195+
{ type: 'func', value: 'url' },
196+
{ type: 'paren', value: '(' },
197+
{ type: 'operator', value: '/' },
198+
{ type: 'operator', value: '/' },
199+
{ type: 'word', value: 'bar' },
200+
{ type: 'word', value: 'http' },
201+
{ type: 'colon', value: ':' },
202+
{ type: 'operator', value: '/' },
203+
{ type: 'operator', value: '/' },
204+
{ type: 'word', value: 'domain.com/foo/bar' },
205+
{ type: 'operator', value: '/' },
206+
{ type: 'operator', value: '/' },
207+
{ type: 'word', value: 'foo' },
208+
{ type: 'paren', value: ')' },
209+
]
93210
}
94211
];
95212

@@ -100,7 +217,7 @@ describe('Parser → Comment', () => {
100217

101218
fixtures.forEach((fixture) => {
102219
it(fixture.it, () => {
103-
let ast = new Parser(fixture.test).parse(),
220+
let ast = new Parser(fixture.test, { loose: fixture.loose }).parse(),
104221
index = 0;
105222

106223
ast.first.walk((node) => {

test/function.js

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,94 @@ describe('Parser → Function', () => {
6060
]
6161
},
6262
{
63-
it: 'should parse url function with quoted first argument',
63+
it: 'should parse url function with single quotes',
64+
test: 'url( \'/gfx/img/bg.jpg\' )',
65+
expected: [
66+
{ type: 'func', value: 'url' },
67+
{ type: 'paren', value: '(' },
68+
{ type: 'word', value: ' \'/gfx/img/bg.jpg\' ' },
69+
{ type: 'paren', value: ')' }
70+
]
71+
},
72+
{
73+
it: 'should parse url function with single quotes (loose)',
74+
test: 'url( \'/gfx/img/bg.jpg\' )',
75+
loose: true,
76+
expected: [
77+
{ type: 'func', value: 'url' },
78+
{ type: 'paren', value: '(' },
79+
{ type: 'string', value: '/gfx/img/bg.jpg', raws: { before: ' ', after: " ", quote: '\'' } },
80+
{ type: 'paren', value: ')' }
81+
]
82+
},
83+
{
84+
it: 'should parse url function with double quotes',
85+
test: 'url( "/gfx/img/bg.jpg" )',
86+
expected: [
87+
{ type: 'func', value: 'url' },
88+
{ type: 'paren', value: '(' },
89+
{ type: 'word', value: ' "/gfx/img/bg.jpg" ' },
90+
{ type: 'paren', value: ')' }
91+
]
92+
},
93+
{
94+
it: 'should parse url function with double quotes (loose)',
95+
test: 'url( "/gfx/img/bg.jpg" )',
96+
loose: true,
97+
expected: [
98+
{ type: 'func', value: 'url' },
99+
{ type: 'paren', value: '(' },
100+
{ type: 'string', value: '/gfx/img/bg.jpg', raws: { before: ' ', after: " ", quote: '"' } },
101+
{ type: 'paren', value: ')' }
102+
]
103+
},
104+
{
105+
it: 'should parse absolute url function',
106+
test: 'url( http://domain.com/gfx/img/bg.jpg )',
107+
expected: [
108+
{ type: 'func', value: 'url' },
109+
{ type: 'paren', value: '(' },
110+
{ type: 'word', value: ' http://domain.com/gfx/img/bg.jpg ' },
111+
{ type: 'paren', value: ')' }
112+
]
113+
},
114+
{
115+
it: 'should parse absolute url function (loose)',
116+
test: 'url( http://domain.com/gfx/img/bg.jpg )',
117+
loose: true,
118+
expected: [
119+
{ type: 'func', value: 'url' },
120+
{ type: 'paren', value: '(' },
121+
{ type: 'word', value: 'http' },
122+
{ type: 'colon', value: ':' },
123+
{ type: 'operator', value: '/' },
124+
{ type: 'operator', value: '/' },
125+
{ type: 'word', value: 'domain.com/gfx/img/bg.jpg' },
126+
{ type: 'paren', value: ')' }
127+
]
128+
},
129+
{
130+
it: 'should parse absolute url function with single quotes',
131+
test: 'url( \'http://domain.com/gfx/img/bg.jpg\' )',
132+
expected: [
133+
{ type: 'func', value: 'url' },
134+
{ type: 'paren', value: '(' },
135+
{ type: 'word', value: ' \'http://domain.com/gfx/img/bg.jpg\' ' },
136+
{ type: 'paren', value: ')' }
137+
]
138+
},
139+
{
140+
it: 'should parse absolute url function with double quotes',
141+
test: 'url( "http://domain.com/gfx/img/bg.jpg" )',
142+
expected: [
143+
{ type: 'func', value: 'url' },
144+
{ type: 'paren', value: '(' },
145+
{ type: 'word', value: ' "http://domain.com/gfx/img/bg.jpg" ' },
146+
{ type: 'paren', value: ')' }
147+
]
148+
},
149+
{
150+
it: 'should parse url function with quoted first argument',
64151
test: 'url("/gfx/img/bg.jpg" hello )',
65152
expected: [
66153
{ type: 'func', value: 'url' },
@@ -70,6 +157,29 @@ describe('Parser → Function', () => {
70157
{ type: 'paren', value: ')' }
71158
]
72159
},
160+
{
161+
it: 'should parse absolute url function with quoted first argument',
162+
test: 'url("http://domain.com/gfx/img/bg.jpg" hello )',
163+
expected: [
164+
{ type: 'func', value: 'url' },
165+
{ type: 'paren', value: '(' },
166+
{ type: 'string', value: 'http://domain.com/gfx/img/bg.jpg', raws: { quote: '"' } },
167+
{ type: 'word', value: 'hello' },
168+
{ type: 'paren', value: ')' }
169+
]
170+
},
171+
{
172+
it: 'should parse absolute url function with quoted first argument (loose)',
173+
test: 'url("http://domain.com/gfx/img/bg.jpg" hello )',
174+
loose: true,
175+
expected: [
176+
{ type: 'func', value: 'url' },
177+
{ type: 'paren', value: '(' },
178+
{ type: 'string', value: 'http://domain.com/gfx/img/bg.jpg', raws: { quote: '"' } },
179+
{ type: 'word', value: 'hello' },
180+
{ type: 'paren', value: ')' }
181+
]
182+
},
73183
{
74184
it: 'should parse rgba function',
75185
test: 'rgba( 29, 439 , 29 )',

0 commit comments

Comments
 (0)