diff --git a/index.js b/index.js index cc8fe11..7a2306e 100644 --- a/index.js +++ b/index.js @@ -973,6 +973,7 @@ Lexer.prototype = { var whitespaceRe = /[ \n\t]/; var quoteRe = /['"]/; + var spreadRe = /^\.\.\./; var escapedAttr = true var key = ''; @@ -981,16 +982,38 @@ Lexer.prototype = { var lineno = startingLine; var colno = this.colno; var loc = 'key'; + var incrementColumn = function (i) { + if (str[i] === '\n') { + // Save the line number locally to keep this.lineno at the start of + // the attribute. + lineno++; + self.colno = 1; + // If the key has not been started, update this.lineno + // immediately. + if (!key) self.lineno = lineno; + } else if (str[i] !== undefined) { + self.incrementColumn(1); + } + }; + var skipWhitespace = function (i) { + if (whitespaceRe.test(str[i])) { + for (; i < str.length; i++) { + if (!whitespaceRe.test(str[i])) break; + incrementColumn(i); + } + } + return i; + }; var isEndOfAttribute = function (i) { // if the key is not started, then the attribute cannot be ended - if (key.trim() === '') { - colno = this.colno; - return false; - } + if (key === '') return false; // if there's nothing more then the attribute must be ended if (i === str.length) return true; - if (loc === 'key') { + // if the spread attribute has just started the attribute cannot be + // ended + if (loc === 'spread') return false; + else if (loc === 'key') { if (whitespaceRe.test(str[i])) { // find the first non-whitespace character for (var x = i; x < str.length; x++) { @@ -1019,7 +1042,8 @@ Lexer.prototype = { if (!whitespaceRe.test(str[x])) { // if it is a JavaScript punctuator, then assume that it is // a part of the value - return !characterParser.isPunctuator(str[x]) || quoteRe.test(str[x]); + // also make exception for spread syntax + return spreadRe.test(str.slice(x)) || !characterParser.isPunctuator(str[x]) || quoteRe.test(str[x]); } } } @@ -1044,18 +1068,27 @@ Lexer.prototype = { this.error('COLON_ATTRIBUTE', '":" is not valid as the start or end of an un-quoted attribute.'); } key = key.trim(); - key = key.replace(/^['"]|['"]$/g, ''); - var tok = this.tok('attribute'); - tok.name = key; - tok.val = '' == val ? true : val; - tok.col = colno; + var tok; + if (key === '...') { + tok = this.tok('spread-attribute'); + tok.val = val; + if (!val) { + return this.error('EMPTY_SPREAD_ATTRIBUTE', 'A spread attribute must have a value.') + } + } else { + key = key.replace(/^['"]|['"]$/g, ''); + tok = this.tok('attribute'); + tok.name = key; + tok.val = '' == val ? true : val; + } tok.mustEscape = escapedAttr; + tok.col = colno; this.tokens.push(tok); key = val = ''; loc = 'key'; - escapedAttr = false; + escapedAttr = true; this.lineno = lineno; } else { switch (loc) { @@ -1064,14 +1097,18 @@ Lexer.prototype = { loc = 'key'; if (i + 1 < str.length && !/[ ,!=\n\t]/.test(str[i + 1])) this.error('INVALID_KEY_CHARACTER', 'Unexpected character "' + str[i + 1] + '" expected ` `, `\\n`, `\t`, `,`, `!` or `=`'); - } else { - key += str[i]; } + key += str[i]; break; case 'key': + if (key === '') { + i = skipWhitespace(i); + colno = this.colno; + } if (key === '' && quoteRe.test(str[i])) { loc = 'key-char'; quote = str[i]; + key += str[i]; } else if (str[i] === '!' || str[i] === '=') { escapedAttr = str[i] !== '!'; if (str[i] === '!') { @@ -1084,23 +1121,25 @@ Lexer.prototype = { } else { key += str[i] } + if (key === '...') loc = 'spread'; break; + case 'spread': + if (str[i] === '!') { + escapedAttr = false; + loc = 'value'; + break; + } else if (!whitespaceRe.test(str[i])) { + loc = 'value'; + } + // no cleaner ways to do so, so has to use a + // fallthrough case 'value': state = characterParser.parseChar(str[i], state); val += str[i]; break; } } - if (str[i] === '\n') { - // Save the line number locally to keep this.lineno at the start of - // the attribute. - lineno++; - this.colno = 1; - // If the key has not been started, update this.lineno immediately. - if (!key.trim()) this.lineno = lineno; - } else if (str[i] !== undefined) { - this.incrementColumn(1); - } + incrementColumn(i); } // Reset the line numbers based on the line started on diff --git a/test/cases/attrs-spread.expected.json b/test/cases/attrs-spread.expected.json new file mode 100644 index 0000000..23bfa65 --- /dev/null +++ b/test/cases/attrs-spread.expected.json @@ -0,0 +1,48 @@ +{"type":"tag","line":1,"col":1,"val":"div"} +{"type":"start-attributes","line":1,"col":4} +{"type":"spread-attribute","line":1,"col":5,"val":"attrs","mustEscape":true} +{"type":"end-attributes","line":1,"col":13} +{"type":"newline","line":2,"col":1} +{"type":"tag","line":2,"col":1,"val":"div"} +{"type":"start-attributes","line":2,"col":4} +{"type":"attribute","line":2,"col":5,"name":"a","val":"'bleh'","mustEscape":true} +{"type":"spread-attribute","line":2,"col":15,"val":"attrs","mustEscape":true} +{"type":"end-attributes","line":2,"col":23} +{"type":"newline","line":3,"col":1} +{"type":"tag","line":3,"col":1,"val":"div"} +{"type":"start-attributes","line":3,"col":4} +{"type":"attribute","line":3,"col":5,"name":"a","val":"'bleh'","mustEscape":true} +{"type":"spread-attribute","line":3,"col":14,"val":"attrs","mustEscape":true} +{"type":"end-attributes","line":3,"col":22} +{"type":"newline","line":4,"col":1} +{"type":"tag","line":4,"col":1,"val":"div"} +{"type":"start-attributes","line":4,"col":4} +{"type":"attribute","line":4,"col":5,"name":"a","val":true,"mustEscape":true} +{"type":"spread-attribute","line":4,"col":7,"val":"attrs","mustEscape":true} +{"type":"end-attributes","line":4,"col":15} +{"type":"newline","line":5,"col":1} +{"type":"tag","line":5,"col":1,"val":"div"} +{"type":"start-attributes","line":5,"col":4} +{"type":"attribute","line":5,"col":5,"name":"a","val":true,"mustEscape":true} +{"type":"spread-attribute","line":5,"col":7,"val":"attrs","mustEscape":true} +{"type":"end-attributes","line":5,"col":16} +{"type":"newline","line":6,"col":1} +{"type":"tag","line":6,"col":1,"val":"div"} +{"type":"start-attributes","line":6,"col":4} +{"type":"spread-attribute","line":6,"col":5,"val":"attrs","mustEscape":false} +{"type":"end-attributes","line":6,"col":14} +{"type":"newline","line":7,"col":1} +{"type":"tag","line":7,"col":1,"val":"div"} +{"type":"start-attributes","line":7,"col":4} +{"type":"spread-attribute","line":7,"col":5,"val":"attrs","mustEscape":false} +{"type":"end-attributes","line":9,"col":7} +{"type":"newline","line":10,"col":1} +{"type":"tag","line":10,"col":1,"val":"div"} +{"type":"start-attributes","line":10,"col":4} +{"type":"attribute","line":10,"col":5,"name":"...attrs","val":true,"mustEscape":true} +{"type":"spread-attribute","line":10,"col":16,"val":"attrs","mustEscape":true} +{"type":"attribute","line":10,"col":26,"name":"...attrs","val":"'val'","mustEscape":true} +{"type":"attribute","line":10,"col":44,"name":"...","val":true,"mustEscape":true} +{"type":"end-attributes","line":10,"col":49} +{"type":"newline","line":11,"col":1} +{"type":"eos","line":11,"col":1} \ No newline at end of file diff --git a/test/cases/attrs-spread.pug b/test/cases/attrs-spread.pug new file mode 100644 index 0000000..c20c1c2 --- /dev/null +++ b/test/cases/attrs-spread.pug @@ -0,0 +1,10 @@ +div(...attrs) +div(a='bleh', ...attrs) +div(a='bleh' ...attrs) +div(a ...attrs) +div(a ... attrs) +div(...!attrs) +div(... +! + attrs) +div("...attrs" ...attrs, '...attrs' ='val' "...") diff --git a/test/cases/attrs.expected.json b/test/cases/attrs.expected.json index 32a0ad3..4d5a791 100644 --- a/test/cases/attrs.expected.json +++ b/test/cases/attrs.expected.json @@ -14,8 +14,8 @@ {"type":"tag","line":3,"col":1,"val":"a"} {"type":"start-attributes","line":3,"col":2} {"type":"attribute","line":3,"col":3,"name":"foo","val":true,"mustEscape":true} -{"type":"attribute","line":3,"col":8,"name":"bar","val":true,"mustEscape":false} -{"type":"attribute","line":3,"col":13,"name":"baz","val":true,"mustEscape":false} +{"type":"attribute","line":3,"col":8,"name":"bar","val":true,"mustEscape":true} +{"type":"attribute","line":3,"col":13,"name":"baz","val":true,"mustEscape":true} {"type":"end-attributes","line":3,"col":16} {"type":"newline","line":4,"col":1} {"type":"tag","line":4,"col":1,"val":"a"} @@ -35,7 +35,7 @@ {"type":"tag","line":7,"col":3,"val":"option"} {"type":"start-attributes","line":7,"col":9} {"type":"attribute","line":7,"col":10,"name":"value","val":"'foo'","mustEscape":true} -{"type":"attribute","line":7,"col":23,"name":"selected","val":true,"mustEscape":false} +{"type":"attribute","line":7,"col":23,"name":"selected","val":true,"mustEscape":true} {"type":"end-attributes","line":7,"col":31} {"type":"text","line":7,"col":33,"val":"Foo"} {"type":"newline","line":8,"col":1} @@ -72,8 +72,8 @@ {"type":"tag","line":14,"col":1,"val":"a"} {"type":"start-attributes","line":14,"col":2} {"type":"attribute","line":14,"col":3,"name":"foo","val":true,"mustEscape":true} -{"type":"attribute","line":14,"col":7,"name":"bar","val":true,"mustEscape":false} -{"type":"attribute","line":14,"col":11,"name":"baz","val":true,"mustEscape":false} +{"type":"attribute","line":14,"col":7,"name":"bar","val":true,"mustEscape":true} +{"type":"attribute","line":14,"col":11,"name":"baz","val":true,"mustEscape":true} {"type":"end-attributes","line":14,"col":14} {"type":"newline","line":15,"col":1} {"type":"tag","line":15,"col":1,"val":"a"} @@ -93,7 +93,7 @@ {"type":"tag","line":18,"col":3,"val":"option"} {"type":"start-attributes","line":18,"col":9} {"type":"attribute","line":18,"col":10,"name":"value","val":"'foo'","mustEscape":true} -{"type":"attribute","line":18,"col":22,"name":"selected","val":true,"mustEscape":false} +{"type":"attribute","line":18,"col":22,"name":"selected","val":true,"mustEscape":true} {"type":"end-attributes","line":18,"col":30} {"type":"text","line":18,"col":32,"val":"Foo"} {"type":"newline","line":19,"col":1} @@ -127,37 +127,37 @@ {"type":"tag","line":25,"col":1,"val":"foo"} {"type":"start-attributes","line":25,"col":4} {"type":"attribute","line":25,"col":5,"name":"abc","val":true,"mustEscape":true} -{"type":"attribute","line":26,"col":5,"name":"def","val":true,"mustEscape":false} +{"type":"attribute","line":26,"col":5,"name":"def","val":true,"mustEscape":true} {"type":"end-attributes","line":26,"col":8} {"type":"newline","line":27,"col":1} {"type":"tag","line":27,"col":1,"val":"foo"} {"type":"start-attributes","line":27,"col":4} {"type":"attribute","line":27,"col":5,"name":"abc","val":true,"mustEscape":true} -{"type":"attribute","line":28,"col":5,"name":"def","val":true,"mustEscape":false} +{"type":"attribute","line":28,"col":5,"name":"def","val":true,"mustEscape":true} {"type":"end-attributes","line":28,"col":8} {"type":"newline","line":29,"col":1} {"type":"tag","line":29,"col":1,"val":"foo"} {"type":"start-attributes","line":29,"col":4} {"type":"attribute","line":29,"col":5,"name":"abc","val":true,"mustEscape":true} -{"type":"attribute","line":30,"col":3,"name":"def","val":true,"mustEscape":false} +{"type":"attribute","line":30,"col":3,"name":"def","val":true,"mustEscape":true} {"type":"end-attributes","line":30,"col":6} {"type":"newline","line":31,"col":1} {"type":"tag","line":31,"col":1,"val":"foo"} {"type":"start-attributes","line":31,"col":4} {"type":"attribute","line":31,"col":5,"name":"abc","val":true,"mustEscape":true} -{"type":"attribute","line":32,"col":4,"name":"def","val":true,"mustEscape":false} +{"type":"attribute","line":32,"col":4,"name":"def","val":true,"mustEscape":true} {"type":"end-attributes","line":32,"col":7} {"type":"newline","line":33,"col":1} {"type":"tag","line":33,"col":1,"val":"foo"} {"type":"start-attributes","line":33,"col":4} {"type":"attribute","line":33,"col":5,"name":"abc","val":true,"mustEscape":true} -{"type":"attribute","line":34,"col":3,"name":"def","val":true,"mustEscape":false} +{"type":"attribute","line":34,"col":3,"name":"def","val":true,"mustEscape":true} {"type":"end-attributes","line":34,"col":6} {"type":"newline","line":35,"col":1} {"type":"tag","line":35,"col":1,"val":"foo"} {"type":"start-attributes","line":35,"col":4} {"type":"attribute","line":35,"col":5,"name":"abc","val":true,"mustEscape":true} -{"type":"attribute","line":36,"col":5,"name":"def","val":true,"mustEscape":false} +{"type":"attribute","line":36,"col":5,"name":"def","val":true,"mustEscape":true} {"type":"end-attributes","line":36,"col":8} {"type":"newline","line":38,"col":1} {"type":"code","line":38,"col":1,"val":"var attrs = {foo: 'bar', bar: ''}","mustEscape":false,"buffer":false} @@ -168,13 +168,13 @@ {"type":"tag","line":42,"col":1,"val":"a"} {"type":"start-attributes","line":42,"col":2} {"type":"attribute","line":42,"col":3,"name":"foo","val":"'foo'","mustEscape":true} -{"type":"attribute","line":42,"col":14,"name":"bar","val":"\"bar\"","mustEscape":true} +{"type":"attribute","line":42,"col":13,"name":"bar","val":"\"bar\"","mustEscape":true} {"type":"end-attributes","line":42,"col":24} {"type":"newline","line":43,"col":1} {"type":"tag","line":43,"col":1,"val":"a"} {"type":"start-attributes","line":43,"col":2} {"type":"attribute","line":43,"col":3,"name":"foo","val":"'foo'","mustEscape":true} -{"type":"attribute","line":43,"col":14,"name":"bar","val":"'bar'","mustEscape":true} +{"type":"attribute","line":43,"col":13,"name":"bar","val":"'bar'","mustEscape":true} {"type":"end-attributes","line":43,"col":24} {"type":"newline","line":44,"col":1} {"type":"eos","line":44,"col":1} \ No newline at end of file diff --git a/test/cases/html5.expected.json b/test/cases/html5.expected.json index 2368d7a..8e28400 100644 --- a/test/cases/html5.expected.json +++ b/test/cases/html5.expected.json @@ -3,7 +3,7 @@ {"type":"tag","line":2,"col":1,"val":"input"} {"type":"start-attributes","line":2,"col":6} {"type":"attribute","line":2,"col":7,"name":"type","val":"'checkbox'","mustEscape":true} -{"type":"attribute","line":2,"col":24,"name":"checked","val":true,"mustEscape":false} +{"type":"attribute","line":2,"col":24,"name":"checked","val":true,"mustEscape":true} {"type":"end-attributes","line":2,"col":31} {"type":"newline","line":3,"col":1} {"type":"tag","line":3,"col":1,"val":"input"} diff --git a/test/cases/mixin.attrs.expected.json b/test/cases/mixin.attrs.expected.json index 7ba476c..d182b92 100644 --- a/test/cases/mixin.attrs.expected.json +++ b/test/cases/mixin.attrs.expected.json @@ -132,7 +132,7 @@ {"type":"outdent","line":41,"col":1} {"type":"call","line":41,"col":1,"val":"work_filmstrip_item","args":"'work'"} {"type":"start-attributes","line":41,"col":29} -{"type":"attribute","line":41,"col":31,"name":"data-profile","val":"'profile'","mustEscape":true} +{"type":"attribute","line":41,"col":30,"name":"data-profile","val":"'profile'","mustEscape":true} {"type":"attribute","line":41,"col":56,"name":"data-creator-name","val":"'name'","mustEscape":true} {"type":"end-attributes","line":41,"col":82} {"type":"newline","line":43,"col":1} diff --git a/test/cases/source.expected.json b/test/cases/source.expected.json index 05cc23b..7570d22 100644 --- a/test/cases/source.expected.json +++ b/test/cases/source.expected.json @@ -3,8 +3,8 @@ {"type":"tag","line":2,"col":3,"val":"audio"} {"type":"start-attributes","line":2,"col":8} {"type":"attribute","line":2,"col":9,"name":"preload","val":"'auto'","mustEscape":true} -{"type":"attribute","line":2,"col":25,"name":"autobuffer","val":true,"mustEscape":false} -{"type":"attribute","line":2,"col":37,"name":"controls","val":true,"mustEscape":false} +{"type":"attribute","line":2,"col":25,"name":"autobuffer","val":true,"mustEscape":true} +{"type":"attribute","line":2,"col":37,"name":"controls","val":true,"mustEscape":true} {"type":"end-attributes","line":2,"col":45} {"type":"indent","line":3,"col":1,"val":4} {"type":"tag","line":3,"col":5,"val":"source"} diff --git a/test/index.js b/test/index.js index 1ff2ba9..ae7b8fd 100644 --- a/test/index.js +++ b/test/index.js @@ -12,7 +12,7 @@ fs.readdirSync(dir).forEach(function (testCase) { if (/\.pug$/.test(testCase)) { console.dir(testCase); var expected = fs.readFileSync(dir + testCase.replace(/\.pug$/, '.expected.json'), 'utf8') - .split(/\n/).map(JSON.parse); + .trim().split(/\n/).map(JSON.parse); var result = lex(fs.readFileSync(dir + testCase, 'utf8'), dir + testCase); fs.writeFileSync(dir + testCase.replace(/\.pug$/, '.actual.json'), result.map(JSON.stringify).join('\n'));