From 94042621034ff5e14294be0885b7f376d2dc05a9 Mon Sep 17 00:00:00 2001 From: Evilebot Tnawi Date: Tue, 20 Mar 2018 20:01:48 +0300 Subject: [PATCH] feat: double slash comments (#51) --- API.md | 5 ++ lib/comment.js | 5 +- lib/parser.js | 14 +++++- lib/tokenize.js | 26 +++++++++-- test/comment.js | 119 ++++++++++++++++++++++++++++++++++++++++++++++- test/function.js | 112 +++++++++++++++++++++++++++++++++++++++++++- 6 files changed, 271 insertions(+), 10 deletions(-) diff --git a/API.md b/API.md index 1478821..b90ddaa 100644 --- a/API.md +++ b/API.md @@ -107,6 +107,11 @@ not documented here are subject to change at any point.* parser.comment({ value: 'Affirmative, Dave. I read you.' }); // → /* Affirmative, Dave. I read you. */ ``` + + ```js + parser.comment({ value: 'Affirmative, Dave. I read you.', inline: true }); + // → // Affirmative, Dave. I read you. + ``` Arguments: diff --git a/lib/comment.js b/lib/comment.js index f96f5c5..cffa16a 100644 --- a/lib/comment.js +++ b/lib/comment.js @@ -7,14 +7,15 @@ class Comment extends Node { constructor (opts) { super(opts); this.type = 'comment'; + this.inline = opts.inline || false; } toString () { return [ this.raws.before, - '/*', + this.inline ? '//' : '/*', String(this.value), - '*/', + this.inline ? '' : '*/', this.raws.after ].join(''); } diff --git a/lib/parser.js b/lib/parser.js index 2bdb4de..87d39c1 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -94,8 +94,18 @@ module.exports = class Parser { } comment () { - let node = new Comment({ - value: this.currToken[1].replace(/\/\*|\*\//g, ''), + let inline = false, + value = this.currToken[1].replace(/\/\*|\*\//g, ''), + node; + + if (this.options.loose && value.startsWith("//")) { + value = value.substring(2); + inline = true; + } + + node = new Comment({ + value: value, + inline: inline, source: { start: { line: this.currToken[2], diff --git a/lib/tokenize.js b/lib/tokenize.js index 375b6bf..ef21bf3 100644 --- a/lib/tokenize.js +++ b/lib/tokenize.js @@ -46,6 +46,8 @@ module.exports = function tokenize (input, options) { offset = -1, line = 1, pos = 0, + parentCount = 0, + isURLArg = null, code, next, quote, lines, last, content, escape, nextLine, nextOffset, escaped, escapePos, nextChar; @@ -136,6 +138,11 @@ module.exports = function tokenize (input, options) { break; case openParen: + parentCount++; + isURLArg = !isURLArg && parentCount === 1 && + tokens.length > 0 && + tokens[tokens.length - 1][0] === "word" && + tokens[tokens.length - 1][1] === "url"; tokens.push(['(', '(', line, pos - offset, line, next - offset, @@ -144,6 +151,8 @@ module.exports = function tokenize (input, options) { break; case closeParen: + parentCount--; + isURLArg = !isURLArg && parentCount === 1; tokens.push([')', ')', line, pos - offset, line, next - offset, @@ -249,10 +258,19 @@ module.exports = function tokenize (input, options) { break; default: - if (code === slash && css.charCodeAt(pos + 1) === asterisk) { - next = css.indexOf('*/', pos + 2) + 1; - if (next === 0) { - unclosed('comment', '*/'); + if (code === slash && (css.charCodeAt(pos + 1) === asterisk || (options.loose && !isURLArg && css.charCodeAt(pos + 1) === slash))) { + const isStandardComment = css.charCodeAt(pos + 1) === asterisk; + + if (isStandardComment) { + next = css.indexOf('*/', pos + 2) + 1; + if (next === 0) { + unclosed('comment', '*/'); + } + } + else { + const newlinePos = css.indexOf('\n', pos + 2); + + next = newlinePos !== -1 ? newlinePos - 1 : length; } content = css.slice(pos, next + 1); diff --git a/test/comment.js b/test/comment.js index 8307cb9..eb66e93 100644 --- a/test/comment.js +++ b/test/comment.js @@ -14,6 +14,7 @@ describe('Parser → Comment', () => { failures; fixtures = [ + // standard comments { it: 'should parse comments', test: '/*before*/ 1px /*between*/ 1px /*after*/', @@ -53,6 +54,97 @@ describe('Parser → Comment', () => { { type: 'paren', value: ')' } ] }, + { + it: 'should parse only one empty comment', + test: '/**/', + expected: [ + { type: 'comment', value: '', raws: { before: '' } }, + ] + }, + + // double slash comment + { + it: 'should parse double slash comments', + test: '//before\n 1px //between\n 1px //after\n', + loose: true, + expected: [ + { type: 'comment', value: 'before', inline: true }, + { type: 'number', value: '1', unit: 'px', raws: { before: '\n ' } }, + { type: 'comment', value: 'between', inline: true, raws: { before: ' ' } }, + { type: 'number', value: '1', unit: 'px', raws: { before: '\n ' } }, + { type: 'comment', value: 'after', inline: true, raws: { before: ' ' } } + ] + }, + { + it: 'should parse double slash comments inside functions', + test: 'rgba( 0, 55/55, 0//,.5\n )', + loose: true, + expected: [ + { type: 'func', value: 'rgba' }, + { type: 'paren', value: '(' }, + { type: 'number', value: '0', raws: { before: ' ' } }, + { type: 'comma', value: ',' }, + { type: 'number', value: '55', raws: { before: ' ' } }, + { type: 'operator', value: '/' }, + { type: 'number', value: '55' }, + { type: 'comma', value: ',' }, + { type: 'number', value: '0', raws: { before: ' ' } }, + { type: 'comment', value: ',.5' }, + + ] + }, + { + it: 'should parse double slash comments when url function have nested function', + test: 'url(var(//comment\n))', + loose: true, + expected: [ + { type: 'func', value: 'url' }, + { type: 'paren', value: '(' }, + { type: 'func', value: 'var' }, + { type: 'paren', value: '(' }, + { type: 'comment', value: 'comment' }, + { type: 'paren', value: ')' }, + { type: 'paren', value: ')' } + ] + }, + { + it: 'should parse only one double slash empty comment', + test: '//\n', + loose: true, + expected: [ + { type: 'comment', value: '', inline: true, raws: { before: '' } }, + ] + }, + { + it: 'should parse only one double slash empty comment without newline', + test: '//', + loose: true, + expected: [ + { type: 'comment', value: '', inline: true, raws: { before: '' } }, + ] + }, + + // mixed standard and double slash comments + { + it: 'should parse mixed comments #1', + test: '/*before*/\n//between\n/*after*/', + loose: true, + expected: [ + { type: 'comment', value: 'before', inline: false, raws: { before: '' } }, + { type: 'comment', value: 'between', inline: true, raws: { before: '\n' } }, + { type: 'comment', value: 'after', inline: false, raws: { before: '\n' } }, + ] + }, + { + it: 'should parse mixed comments #2', + test: '//before\n/*between*/\n//after', + loose: true, + expected: [ + { type: 'comment', value: 'before', inline: true, raws: { before: '' } }, + { type: 'comment', value: 'between', inline: false, raws: { before: '\n' } }, + { type: 'comment', value: 'after', inline: true, raws: { before: '\n' } }, + ] + }, // these tests are based on the spec rules surrounding legacy // quotation-mark–less url notation. @@ -61,6 +153,8 @@ describe('Parser → Comment', () => { // doesn't start with either kind of quotation mark. // postcss-value-parser ignores those rules and allows comments within a url() // function if the first param starts with a space. + + // standard comments { it: 'should not parse comments at the end of url functions with quoted first argument, lead by a space', test: 'url( "/demo/bg.png" /*comment*/ )', @@ -90,6 +184,29 @@ describe('Parser → Comment', () => { { type: 'word', value: ' /demo/bg.png /*comment*/ ' }, { type: 'paren', value: ')' } ] + }, + + // double slash comments + { + it: 'should not parse double slash as comment in url function', + test: 'url(//bar\n http://domain.com/foo/bar //foo\n)', + loose: true, + expected: [ + { type: 'func', value: 'url' }, + { type: 'paren', value: '(' }, + { type: 'operator', value: '/' }, + { type: 'operator', value: '/' }, + { type: 'word', value: 'bar' }, + { type: 'word', value: 'http' }, + { type: 'colon', value: ':' }, + { type: 'operator', value: '/' }, + { type: 'operator', value: '/' }, + { type: 'word', value: 'domain.com/foo/bar' }, + { type: 'operator', value: '/' }, + { type: 'operator', value: '/' }, + { type: 'word', value: 'foo' }, + { type: 'paren', value: ')' }, + ] } ]; @@ -100,7 +217,7 @@ describe('Parser → Comment', () => { fixtures.forEach((fixture) => { it(fixture.it, () => { - let ast = new Parser(fixture.test).parse(), + let ast = new Parser(fixture.test, { loose: fixture.loose }).parse(), index = 0; ast.first.walk((node) => { diff --git a/test/function.js b/test/function.js index ac6bd4c..e0ac1eb 100644 --- a/test/function.js +++ b/test/function.js @@ -60,7 +60,94 @@ describe('Parser → Function', () => { ] }, { - it: 'should parse url function with quoted first argument', + it: 'should parse url function with single quotes', + test: 'url( \'/gfx/img/bg.jpg\' )', + expected: [ + { type: 'func', value: 'url' }, + { type: 'paren', value: '(' }, + { type: 'word', value: ' \'/gfx/img/bg.jpg\' ' }, + { type: 'paren', value: ')' } + ] + }, + { + it: 'should parse url function with single quotes (loose)', + test: 'url( \'/gfx/img/bg.jpg\' )', + loose: true, + expected: [ + { type: 'func', value: 'url' }, + { type: 'paren', value: '(' }, + { type: 'string', value: '/gfx/img/bg.jpg', raws: { before: ' ', after: " ", quote: '\'' } }, + { type: 'paren', value: ')' } + ] + }, + { + it: 'should parse url function with double quotes', + test: 'url( "/gfx/img/bg.jpg" )', + expected: [ + { type: 'func', value: 'url' }, + { type: 'paren', value: '(' }, + { type: 'word', value: ' "/gfx/img/bg.jpg" ' }, + { type: 'paren', value: ')' } + ] + }, + { + it: 'should parse url function with double quotes (loose)', + test: 'url( "/gfx/img/bg.jpg" )', + loose: true, + expected: [ + { type: 'func', value: 'url' }, + { type: 'paren', value: '(' }, + { type: 'string', value: '/gfx/img/bg.jpg', raws: { before: ' ', after: " ", quote: '"' } }, + { type: 'paren', value: ')' } + ] + }, + { + it: 'should parse absolute url function', + test: 'url( http://domain.com/gfx/img/bg.jpg )', + expected: [ + { type: 'func', value: 'url' }, + { type: 'paren', value: '(' }, + { type: 'word', value: ' http://domain.com/gfx/img/bg.jpg ' }, + { type: 'paren', value: ')' } + ] + }, + { + it: 'should parse absolute url function (loose)', + test: 'url( http://domain.com/gfx/img/bg.jpg )', + loose: true, + expected: [ + { type: 'func', value: 'url' }, + { type: 'paren', value: '(' }, + { type: 'word', value: 'http' }, + { type: 'colon', value: ':' }, + { type: 'operator', value: '/' }, + { type: 'operator', value: '/' }, + { type: 'word', value: 'domain.com/gfx/img/bg.jpg' }, + { type: 'paren', value: ')' } + ] + }, + { + it: 'should parse absolute url function with single quotes', + test: 'url( \'http://domain.com/gfx/img/bg.jpg\' )', + expected: [ + { type: 'func', value: 'url' }, + { type: 'paren', value: '(' }, + { type: 'word', value: ' \'http://domain.com/gfx/img/bg.jpg\' ' }, + { type: 'paren', value: ')' } + ] + }, + { + it: 'should parse absolute url function with double quotes', + test: 'url( "http://domain.com/gfx/img/bg.jpg" )', + expected: [ + { type: 'func', value: 'url' }, + { type: 'paren', value: '(' }, + { type: 'word', value: ' "http://domain.com/gfx/img/bg.jpg" ' }, + { type: 'paren', value: ')' } + ] + }, + { + it: 'should parse url function with quoted first argument', test: 'url("/gfx/img/bg.jpg" hello )', expected: [ { type: 'func', value: 'url' }, @@ -70,6 +157,29 @@ describe('Parser → Function', () => { { type: 'paren', value: ')' } ] }, + { + it: 'should parse absolute url function with quoted first argument', + test: 'url("http://domain.com/gfx/img/bg.jpg" hello )', + expected: [ + { type: 'func', value: 'url' }, + { type: 'paren', value: '(' }, + { type: 'string', value: 'http://domain.com/gfx/img/bg.jpg', raws: { quote: '"' } }, + { type: 'word', value: 'hello' }, + { type: 'paren', value: ')' } + ] + }, + { + it: 'should parse absolute url function with quoted first argument (loose)', + test: 'url("http://domain.com/gfx/img/bg.jpg" hello )', + loose: true, + expected: [ + { type: 'func', value: 'url' }, + { type: 'paren', value: '(' }, + { type: 'string', value: 'http://domain.com/gfx/img/bg.jpg', raws: { quote: '"' } }, + { type: 'word', value: 'hello' }, + { type: 'paren', value: ')' } + ] + }, { it: 'should parse rgba function', test: 'rgba( 29, 439 , 29 )',