From da086b9f321e493ebafbc267c671839d3afaebef Mon Sep 17 00:00:00 2001 From: Zdenko Vujasinovic Date: Sat, 8 Jul 2017 14:26:14 +0200 Subject: [PATCH 01/16] CSX spread attributes:
--- lib/coffeescript/lexer.js | 26 +++++++++++++++++++------- lib/coffeescript/nodes.js | 23 ++++++++++++++--------- src/lexer.coffee | 14 +++++++++++--- src/nodes.coffee | 19 ++++++++++++------- 4 files changed, 56 insertions(+), 26 deletions(-) diff --git a/lib/coffeescript/lexer.js b/lib/coffeescript/lexer.js index b14688ac4d..416c9107bc 100644 --- a/lib/coffeescript/lexer.js +++ b/lib/coffeescript/lexer.js @@ -25,6 +25,7 @@ this.importSpecifierList = false; this.exportSpecifierList = false; this.csxDepth = 0; + this.csxSpreadProps = false; this.chunkLine = opts.line || 0; this.chunkColumn = opts.column || 0; code = this.clean(code); @@ -577,8 +578,9 @@ } csxToken() { - var afterTag, colon, csxTag, end, firstChar, id, input, match, origin, prev, ref, token, tokens; + var afterTag, colon, csxTag, end, firstChar, id, input, match, origin, prev, prevChar, ref, token, tokens; firstChar = this.chunk[0]; + prevChar = this.tokens.length > 0 ? this.tokens[this.tokens.length - 1][0] : ''; if (firstChar === '<') { match = CSX_IDENTIFIER.exec(this.chunk.slice(1)); if (!(match && (this.csxDepth > 0 || !(prev = this.prev()) || prev.spaced || (ref = prev[0], indexOf.call(COMPARABLE_LEFT_SIDE, ref) < 0)))) { @@ -603,11 +605,19 @@ this.csxDepth--; return 2; } else if (firstChar === '{') { - token = this.token('(', '('); - this.ends.push({ - tag: '}', - origin: token - }); + if (prevChar === ':') { + token = this.token('(', '('); + this.ends.push({ + tag: '}', + origin: token + }); + this.csxSpreadProps = false; + } else { + this.ends.push({ + tag: '}' + }); + this.csxSpreadProps = true; + } return 1; } else if (firstChar === '>') { this.pair('/>'); @@ -644,7 +654,9 @@ } else if (this.atCSXTag(1)) { if (firstChar === '}') { this.pair(firstChar); - this.token(')', ')'); + if (!this.csxSpreadProps) { + this.token(')', ')'); + } this.token(',', ','); return 1; } else { diff --git a/lib/coffeescript/nodes.js b/lib/coffeescript/nodes.js index 93a5220608..b44fced773 100644 --- a/lib/coffeescript/nodes.js +++ b/lib/coffeescript/nodes.js @@ -1558,7 +1558,7 @@ } compileNode(o) { - var answer, i, idt, indent, isCompact, j, join, k, key, l, lastNoncom, len1, len2, len3, len4, node, prop, props, q, ref1, unwrappedVal, value; + var answer, i, idt, indent, isCompact, j, join, k, key, l, lastNoncom, len1, len2, len3, len4, node, prop, propHasSplat, props, q, ref1, unwrappedVal, value; props = this.properties; if (this.generated) { for (j = 0, len1 = props.length; j < len1; j++) { @@ -1568,7 +1568,7 @@ } } } - if (this.hasSplat()) { + if (this.hasSplat() && !this.csx) { return this.compileSpread(o); } idt = o.indent += TAB; @@ -1600,6 +1600,7 @@ answer.push(this.makeCode(isCompact ? '' : '\n')); for (i = q = 0, len4 = props.length; q < len4; i = ++q) { prop = props[i]; + propHasSplat = prop instanceof Splat; join = i === props.length - 1 ? '' : isCompact && this.csx ? ' ' : isCompact ? ', ' : prop === lastNoncom || prop instanceof Comment || this.csx ? '\n' : ',\n'; indent = isCompact || prop instanceof Comment ? '' : idt; key = prop instanceof Assign && prop.context === 'object' ? prop.variable : prop instanceof Assign ? (!this.lhs ? prop.operatorToken.error(`unexpected ${prop.operatorToken.value}`) : void 0, prop.variable) : !(prop instanceof Comment) ? prop : void 0; @@ -1611,14 +1612,18 @@ prop = new Assign(key, prop, 'object'); } if (key === prop) { - if (prop.shouldCache()) { - [key, value] = prop.base.cache(o); - if (key instanceof IdentifierLiteral) { - key = new PropertyName(key.value); + if (!propHasSplat) { + if (prop.shouldCache()) { + [key, value] = prop.base.cache(o); + if (key instanceof IdentifierLiteral) { + key = new PropertyName(key.value); + } + prop = new Assign(key, value, 'object'); + } else if (!(typeof prop.bareLiteral === "function" ? prop.bareLiteral(IdentifierLiteral) : void 0)) { + prop = new Assign(prop, prop, 'object'); } - prop = new Assign(key, value, 'object'); - } else if (!(typeof prop.bareLiteral === "function" ? prop.bareLiteral(IdentifierLiteral) : void 0)) { - prop = new Assign(prop, prop, 'object'); + } else { + prop = this.csx ? new Literal(`{${prop.compile(o)}}`) : prop; } } if (indent) { diff --git a/src/lexer.coffee b/src/lexer.coffee index d4a2775973..c6cda0f43e 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -49,6 +49,7 @@ exports.Lexer = class Lexer @importSpecifierList = no # Used to identify when in an IMPORT {...} FROM? ... @exportSpecifierList = no # Used to identify when in an EXPORT {...} FROM? ... @csxDepth = 0 # Used to optimize CSX checks, how deep in CSX we are. + @csxSpreadProps = no # Used to detect if CSX attributes include spreads (
) @chunkLine = opts.line or 0 # The start line for the current @chunk. @@ -488,6 +489,8 @@ exports.Lexer = class Lexer # CSX is like JSX but for CoffeeScript. csxToken: -> firstChar = @chunk[0] + # Check the previous token to detect if attribute is spread. + prevChar = if @tokens.length > 0 then @tokens[@tokens.length - 1][0] else '' if firstChar is '<' match = CSX_IDENTIFIER.exec @chunk[1...] return 0 unless match and ( @@ -512,8 +515,13 @@ exports.Lexer = class Lexer @csxDepth-- return 2 else if firstChar is '{' - token = @token '(', '(' - @ends.push {tag: '}', origin: token} + if prevChar == ':' + token = @token '(', '(' + @ends.push {tag: '}', origin: token} + @csxSpreadProps = no + else + @ends.push {tag: '}'} + @csxSpreadProps = yes return 1 else if firstChar is '>' # Ignore terminators inside a tag. @@ -540,7 +548,7 @@ exports.Lexer = class Lexer else if @atCSXTag 1 if firstChar is '}' @pair firstChar - @token ')', ')' + @token ')', ')' unless @csxSpreadProps @token ',', ',' return 1 else diff --git a/src/nodes.coffee b/src/nodes.coffee index c57e9297bb..1de4a616b3 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -1171,7 +1171,7 @@ exports.Obj = class Obj extends Base node.error 'cannot have an implicit value in an implicit object' # Object spread properties. https://github.com/tc39/proposal-object-rest-spread/blob/master/Spread.md - return @compileSpread o if @hasSplat() + return @compileSpread o if @hasSplat() and !@csx idt = o.indent += TAB lastNoncom = @lastNonComment @properties @@ -1195,6 +1195,7 @@ exports.Obj = class Obj extends Base answer = [] answer.push @makeCode if isCompact then '' else '\n' for prop, i in props + propHasSplat = prop instanceof Splat join = if i is props.length - 1 '' else if isCompact and @csx @@ -1219,12 +1220,16 @@ exports.Obj = class Obj extends Base key = key.properties[0].name prop = new Assign key, prop, 'object' if key is prop - if prop.shouldCache() - [key, value] = prop.base.cache o - key = new PropertyName key.value if key instanceof IdentifierLiteral - prop = new Assign key, value, 'object' - else if not prop.bareLiteral?(IdentifierLiteral) - prop = new Assign prop, prop, 'object' + unless propHasSplat + if prop.shouldCache() + [key, value] = prop.base.cache o + key = new PropertyName key.value if key instanceof IdentifierLiteral + prop = new Assign key, value, 'object' + else if not prop.bareLiteral?(IdentifierLiteral) + prop = new Assign prop, prop, 'object' + else + # Spread in CSX + prop = if @csx then new Literal "{#{prop.compile(o)}}" else prop if indent then answer.push @makeCode indent prop.csx = yes if @csx answer.push @makeCode ' ' if @csx and i is 0 From 8841e477ce87f6119d9cc20fb227fd79ef8c7d14 Mon Sep 17 00:00:00 2001 From: Zdenko Vujasinovic Date: Sat, 8 Jul 2017 14:31:58 +0200 Subject: [PATCH 02/16] whitespace cleanup --- src/lexer.coffee | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lexer.coffee b/src/lexer.coffee index c6cda0f43e..b7a5c5a299 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -520,8 +520,8 @@ exports.Lexer = class Lexer @ends.push {tag: '}', origin: token} @csxSpreadProps = no else - @ends.push {tag: '}'} - @csxSpreadProps = yes + @ends.push {tag: '}'} + @csxSpreadProps = yes return 1 else if firstChar is '>' # Ignore terminators inside a tag. From a6172c0fe008e350051c234a65f1edbe0ddf1f8d Mon Sep 17 00:00:00 2001 From: Geoffrey Booth Date: Sat, 8 Jul 2017 22:35:46 -0700 Subject: [PATCH 03/16] Style --- src/lexer.coffee | 2 +- src/nodes.coffee | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lexer.coffee b/src/lexer.coffee index b7a5c5a299..01195eb185 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -49,7 +49,7 @@ exports.Lexer = class Lexer @importSpecifierList = no # Used to identify when in an IMPORT {...} FROM? ... @exportSpecifierList = no # Used to identify when in an EXPORT {...} FROM? ... @csxDepth = 0 # Used to optimize CSX checks, how deep in CSX we are. - @csxSpreadProps = no # Used to detect if CSX attributes include spreads (
) + @csxSpreadProps = no # Used to detect if CSX attributes include spreads (
). @chunkLine = opts.line or 0 # The start line for the current @chunk. diff --git a/src/nodes.coffee b/src/nodes.coffee index 1de4a616b3..aead787b37 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -1171,7 +1171,7 @@ exports.Obj = class Obj extends Base node.error 'cannot have an implicit value in an implicit object' # Object spread properties. https://github.com/tc39/proposal-object-rest-spread/blob/master/Spread.md - return @compileSpread o if @hasSplat() and !@csx + return @compileSpread o if @hasSplat() and not @csx idt = o.indent += TAB lastNoncom = @lastNonComment @properties @@ -1225,10 +1225,10 @@ exports.Obj = class Obj extends Base [key, value] = prop.base.cache o key = new PropertyName key.value if key instanceof IdentifierLiteral prop = new Assign key, value, 'object' - else if not prop.bareLiteral?(IdentifierLiteral) + else unless prop.bareLiteral?(IdentifierLiteral) prop = new Assign prop, prop, 'object' else - # Spread in CSX + # Spread in CSX. prop = if @csx then new Literal "{#{prop.compile(o)}}" else prop if indent then answer.push @makeCode indent prop.csx = yes if @csx From c62be6bb2148935e0c32d6e35df0c29e2cadac58 Mon Sep 17 00:00:00 2001 From: Zdenko Vujasinovic Date: Wed, 12 Jul 2017 02:04:39 +0200 Subject: [PATCH 04/16] valid CSX attributes --- lib/coffeescript/nodes.js | 11 ++++++++++- src/nodes.coffee | 6 ++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/lib/coffeescript/nodes.js b/lib/coffeescript/nodes.js index b44fced773..ecc0a12de3 100644 --- a/lib/coffeescript/nodes.js +++ b/lib/coffeescript/nodes.js @@ -1558,7 +1558,7 @@ } compileNode(o) { - var answer, i, idt, indent, isCompact, j, join, k, key, l, lastNoncom, len1, len2, len3, len4, node, prop, propHasSplat, props, q, ref1, unwrappedVal, value; + var answer, i, idt, indent, isCompact, j, join, k, key, l, lastNoncom, len1, len2, len3, len4, node, prop, propHasSplat, propVal, props, q, ref1, unwrappedVal, value; props = this.properties; if (this.generated) { for (j = 0, len1 = props.length; j < len1; j++) { @@ -1626,6 +1626,15 @@ prop = this.csx ? new Literal(`{${prop.compile(o)}}`) : prop; } } + if (this.csx && prop instanceof Assign) { + if (!(prop.variable.unwrap() instanceof PropertyName)) { + prop.variable.error("Unexpected token"); + } + propVal = prop.value.unwrap(); + if (!(((propVal instanceof Parens || propVal instanceof StringWithInterpolations) && propVal.body instanceof Block) || propVal instanceof StringLiteral || propVal instanceof Obj)) { + prop.value.error("expected wrapped or quoted CSX attribute"); + } + } if (indent) { answer.push(this.makeCode(indent)); } diff --git a/src/nodes.coffee b/src/nodes.coffee index aead787b37..b7944c3da9 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -1230,6 +1230,12 @@ exports.Obj = class Obj extends Base else # Spread in CSX. prop = if @csx then new Literal "{#{prop.compile(o)}}" else prop + # Check if CSX attribute is valid. + if @csx and prop instanceof Assign + prop.variable.error "Unexpected token" unless prop.variable.unwrap() instanceof PropertyName + propVal = prop.value.unwrap() + unless ((propVal instanceof Parens or propVal instanceof StringWithInterpolations) and propVal.body instanceof Block) or propVal instanceof StringLiteral or propVal instanceof Obj + prop.value.error "expected wrapped or quoted CSX attribute" if indent then answer.push @makeCode indent prop.csx = yes if @csx answer.push @makeCode ' ' if @csx and i is 0 From 91a294b56c9ef53cc82ba09f6e54e52f0fee05c6 Mon Sep 17 00:00:00 2001 From: Zdenko Vujasinovic Date: Mon, 24 Jul 2017 08:02:26 +0200 Subject: [PATCH 05/16] added comments; cleanup --- lib/coffeescript/nodes.js | 2 +- src/nodes.coffee | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/coffeescript/nodes.js b/lib/coffeescript/nodes.js index ecc0a12de3..93242c7254 100644 --- a/lib/coffeescript/nodes.js +++ b/lib/coffeescript/nodes.js @@ -1631,7 +1631,7 @@ prop.variable.error("Unexpected token"); } propVal = prop.value.unwrap(); - if (!(((propVal instanceof Parens || propVal instanceof StringWithInterpolations) && propVal.body instanceof Block) || propVal instanceof StringLiteral || propVal instanceof Obj)) { + if (!(propVal instanceof StringLiteral || ((propVal instanceof Parens || propVal instanceof StringWithInterpolations) && propVal.body instanceof Block))) { prop.value.error("expected wrapped or quoted CSX attribute"); } } diff --git a/src/nodes.coffee b/src/nodes.coffee index b7944c3da9..49ada3a9aa 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -1231,10 +1231,18 @@ exports.Obj = class Obj extends Base # Spread in CSX. prop = if @csx then new Literal "{#{prop.compile(o)}}" else prop # Check if CSX attribute is valid. + # CSX atributes are processed in the `lexer` and converted from + # `
` into + # `{ id:(abc), name:"tags", ...props, title:("" + (foo())), src:{a:1, b:2} }` if @csx and prop instanceof Assign + # The `prop.variable` must be the instance of `PropertyName` (i.e. `PROPERTY`). prop.variable.error "Unexpected token" unless prop.variable.unwrap() instanceof PropertyName propVal = prop.value.unwrap() - unless ((propVal instanceof Parens or propVal instanceof StringWithInterpolations) and propVal.body instanceof Block) or propVal instanceof StringLiteral or propVal instanceof Obj + # The `prop.value` instance can be: + # - `StringLiteral`, e.g. id:"abc" + # - `Parens` or `StringWithInterpolations`, e.g. id:{abc} or id:"#{abc}" or id:(abc) + unless propVal instanceof StringLiteral or + ((propVal instanceof Parens or propVal instanceof StringWithInterpolations) and propVal.body instanceof Block) prop.value.error "expected wrapped or quoted CSX attribute" if indent then answer.push @makeCode indent prop.csx = yes if @csx From 4a318fcd5a67c2352bfcabb045970faa4e5ed624 Mon Sep 17 00:00:00 2001 From: Zdenko Vujasinovic Date: Tue, 25 Jul 2017 00:31:50 +0200 Subject: [PATCH 06/16] Fixed allowed CSX properties. --- lib/coffeescript/lexer.js | 39 +++++++++++++++++++++++++++++---------- lib/coffeescript/nodes.js | 5 +++-- src/lexer.coffee | 19 +++++++++++++++++-- src/nodes.coffee | 4 ++-- 4 files changed, 51 insertions(+), 16 deletions(-) diff --git a/lib/coffeescript/lexer.js b/lib/coffeescript/lexer.js index 416c9107bc..ad529ac936 100644 --- a/lib/coffeescript/lexer.js +++ b/lib/coffeescript/lexer.js @@ -1,10 +1,12 @@ // Generated by CoffeeScript 2.0.0-beta3 (function() { - var BOM, BOOL, CALLABLE, CODE, COFFEE_ALIASES, COFFEE_ALIAS_MAP, COFFEE_KEYWORDS, COMMENT, COMPARABLE_LEFT_SIDE, COMPARE, COMPOUND_ASSIGN, CSX_ATTRIBUTE, CSX_IDENTIFIER, CSX_INTERPOLATION, HERECOMMENT_ILLEGAL, HEREDOC_DOUBLE, HEREDOC_INDENT, HEREDOC_SINGLE, HEREGEX, HEREGEX_OMIT, HERE_JSTOKEN, IDENTIFIER, INDENTABLE_CLOSERS, INDEXABLE, INSIDE_CSX, INVERSES, JSTOKEN, JS_KEYWORDS, LEADING_BLANK_LINE, LINE_BREAK, LINE_CONTINUER, Lexer, MATH, MULTI_DENT, NOT_REGEX, NUMBER, OPERATOR, POSSIBLY_DIVISION, REGEX, REGEX_FLAGS, REGEX_ILLEGAL, REGEX_INVALID_ESCAPE, RELATION, RESERVED, Rewriter, SHIFT, SIMPLE_STRING_OMIT, STRICT_PROSCRIBED, STRING_DOUBLE, STRING_INVALID_ESCAPE, STRING_OMIT, STRING_SINGLE, STRING_START, TRAILING_BLANK_LINE, TRAILING_SPACES, UNARY, UNARY_MATH, UNICODE_CODE_POINT_ESCAPE, VALID_FLAGS, WHITESPACE, compact, count, invertLiterate, isForFrom, isUnassignable, key, locationDataToString, merge, repeat, starts, throwSyntaxError, + var BOM, BOOL, CALLABLE, CODE, COFFEE_ALIASES, COFFEE_ALIAS_MAP, COFFEE_KEYWORDS, COMMENT, COMPARABLE_LEFT_SIDE, COMPARE, COMPOUND_ASSIGN, CSX_ATTRIBUTE, CSX_IDENTIFIER, CSX_INTERPOLATION, HERECOMMENT_ILLEGAL, HEREDOC_DOUBLE, HEREDOC_INDENT, HEREDOC_SINGLE, HEREGEX, HEREGEX_OMIT, HERE_JSTOKEN, IDENTIFIER, INDENTABLE_CLOSERS, INDEXABLE, INSIDE_CSX, INVERSES, JSTOKEN, JS_KEYWORDS, LEADING_BLANK_LINE, LINE_BREAK, LINE_CONTINUER, Lexer, MATH, MULTI_DENT, NOT_REGEX, NUMBER, OPERATOR, POSSIBLY_DIVISION, REGEX, REGEX_FLAGS, REGEX_ILLEGAL, REGEX_INVALID_ESCAPE, RELATION, RESERVED, Rewriter, SHIFT, SIMPLE_STRING_OMIT, STRICT_PROSCRIBED, STRING_DOUBLE, STRING_INVALID_ESCAPE, STRING_OMIT, STRING_SINGLE, STRING_START, TRAILING_BLANK_LINE, TRAILING_SPACES, UNARY, UNARY_MATH, UNICODE_CODE_POINT_ESCAPE, VALID_FLAGS, WHITESPACE, compact, count, invertLiterate, isForFrom, isUnassignable, key, locationDataToString, log, merge, repeat, starts, throwSyntaxError, indexOf = [].indexOf; ({Rewriter, INVERSES} = require('./rewriter')); + log = console.log; + ({count, starts, compact, repeat, invertLiterate, merge, locationDataToString, throwSyntaxError} = require('./helpers')); exports.Lexer = Lexer = class Lexer { @@ -67,8 +69,25 @@ } identifierToken() { - var alias, colon, colonOffset, colonToken, id, idLength, inCSXTag, input, match, poppedToken, prev, prevprev, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, regex, tag, tagToken; + var alias, badSpread, colon, colonOffset, colonToken, csxSpreadMatch, id, idLength, inCSXTag, input, lastBrace, match, offset, poppedToken, prev, prevprev, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, ref8, ref9, regex, tag, tagToken; inCSXTag = this.atCSXTag(); + if (inCSXTag && this.prev()[1] !== ':' && ((ref = this.chunk[0]) === '{' || ref === '"' || ref === "'")) { + if ((ref1 = this.chunk[0]) === '"' || ref1 === "'") { + this.error("Unexpected token"); + } + csxSpreadMatch = /^({\.\.\.\S+})|({\S+\.\.\.})/.exec(this.chunk); + if (!csxSpreadMatch) { + if (lastBrace = /^({[^}:\.]+)(:|\})/.exec(this.chunk)) { + this.error("Unexpected token, expected ...", { + offset: lastBrace[0].length - 1 + }); + } + if (badSpread = /^({\S+\.\.\.)|({\.\.\.\S+)[^\}]/.exec(this.chunk)) { + offset = badSpread[0][1] === '.' ? badSpread[0].length - 1 : badSpread[0].length; + this.error("Unexpected token, expected }", {offset}); + } + } + } regex = inCSXTag ? CSX_ATTRIBUTE : IDENTIFIER; if (!(match = regex.exec(this.chunk))) { return 0; @@ -87,27 +106,27 @@ if (id === 'as' && this.seenImport) { if (this.value() === '*') { this.tokens[this.tokens.length - 1][0] = 'IMPORT_ALL'; - } else if (ref = this.value(), indexOf.call(COFFEE_KEYWORDS, ref) >= 0) { + } else if (ref2 = this.value(), indexOf.call(COFFEE_KEYWORDS, ref2) >= 0) { this.tokens[this.tokens.length - 1][0] = 'IDENTIFIER'; } - if ((ref1 = this.tag()) === 'DEFAULT' || ref1 === 'IMPORT_ALL' || ref1 === 'IDENTIFIER') { + if ((ref3 = this.tag()) === 'DEFAULT' || ref3 === 'IMPORT_ALL' || ref3 === 'IDENTIFIER') { this.token('AS', id); return id.length; } } - if (id === 'as' && this.seenExport && ((ref2 = this.tag()) === 'IDENTIFIER' || ref2 === 'DEFAULT')) { + if (id === 'as' && this.seenExport && ((ref4 = this.tag()) === 'IDENTIFIER' || ref4 === 'DEFAULT')) { this.token('AS', id); return id.length; } - if (id === 'default' && this.seenExport && ((ref3 = this.tag()) === 'EXPORT' || ref3 === 'AS')) { + if (id === 'default' && this.seenExport && ((ref5 = this.tag()) === 'EXPORT' || ref5 === 'AS')) { this.token('DEFAULT', id); return id.length; } prev = this.prev(); - tag = colon || (prev != null) && (((ref4 = prev[0]) === '.' || ref4 === '?.' || ref4 === '::' || ref4 === '?::') || !prev.spaced && prev[0] === '@') ? 'PROPERTY' : 'IDENTIFIER'; + tag = colon || (prev != null) && (((ref6 = prev[0]) === '.' || ref6 === '?.' || ref6 === '::' || ref6 === '?::') || !prev.spaced && prev[0] === '@') ? 'PROPERTY' : 'IDENTIFIER'; if (tag === 'IDENTIFIER' && (indexOf.call(JS_KEYWORDS, id) >= 0 || indexOf.call(COFFEE_KEYWORDS, id) >= 0) && !(this.exportSpecifierList && indexOf.call(COFFEE_KEYWORDS, id) >= 0)) { tag = id.toUpperCase(); - if (tag === 'WHEN' && (ref5 = this.tag(), indexOf.call(LINE_BREAK, ref5) >= 0)) { + if (tag === 'WHEN' && (ref7 = this.tag(), indexOf.call(LINE_BREAK, ref7) >= 0)) { tag = 'LEADING_WHEN'; } else if (tag === 'FOR') { this.seenFor = true; @@ -135,11 +154,11 @@ tag = 'FORFROM'; this.seenFor = false; } else if (tag === 'PROPERTY' && prev) { - if (prev.spaced && (ref6 = prev[0], indexOf.call(CALLABLE, ref6) >= 0) && /^[gs]et$/.test(prev[1])) { + if (prev.spaced && (ref8 = prev[0], indexOf.call(CALLABLE, ref8) >= 0) && /^[gs]et$/.test(prev[1])) { this.error(`'${prev[1]}' cannot be used as a keyword, or as a function call without parentheses`, prev[2]); } else { prevprev = this.tokens[this.tokens.length - 2]; - if (((ref7 = prev[0]) === '@' || ref7 === 'THIS') && prevprev && prevprev.spaced && /^[gs]et$/.test(prevprev[1]) && this.tokens[this.tokens.length - 3][0] !== '.') { + if (((ref9 = prev[0]) === '@' || ref9 === 'THIS') && prevprev && prevprev.spaced && /^[gs]et$/.test(prevprev[1]) && this.tokens[this.tokens.length - 3][0] !== '.') { this.error(`'${prevprev[1]}' cannot be used as a keyword, or as a function call without parentheses`, prevprev[2]); } } diff --git a/lib/coffeescript/nodes.js b/lib/coffeescript/nodes.js index 93242c7254..54025a87cc 100644 --- a/lib/coffeescript/nodes.js +++ b/lib/coffeescript/nodes.js @@ -1558,7 +1558,7 @@ } compileNode(o) { - var answer, i, idt, indent, isCompact, j, join, k, key, l, lastNoncom, len1, len2, len3, len4, node, prop, propHasSplat, propVal, props, q, ref1, unwrappedVal, value; + var answer, i, idt, indent, isCompact, j, join, k, key, l, lastNoncom, len1, len2, len3, len4, log, node, prop, propHasSplat, propVal, props, q, ref1, unwrappedVal, value; props = this.properties; if (this.generated) { for (j = 0, len1 = props.length; j < len1; j++) { @@ -1603,6 +1603,7 @@ propHasSplat = prop instanceof Splat; join = i === props.length - 1 ? '' : isCompact && this.csx ? ' ' : isCompact ? ', ' : prop === lastNoncom || prop instanceof Comment || this.csx ? '\n' : ',\n'; indent = isCompact || prop instanceof Comment ? '' : idt; + log = console.log; key = prop instanceof Assign && prop.context === 'object' ? prop.variable : prop instanceof Assign ? (!this.lhs ? prop.operatorToken.error(`unexpected ${prop.operatorToken.value}`) : void 0, prop.variable) : !(prop instanceof Comment) ? prop : void 0; if (key instanceof Value && key.hasProperties()) { if (prop.context === 'object' || !key.this) { @@ -1623,7 +1624,7 @@ prop = new Assign(prop, prop, 'object'); } } else { - prop = this.csx ? new Literal(`{${prop.compile(o)}}`) : prop; + prop = propHasSplat ? new Literal(`{${prop.compile(o)}}`) : prop; } } if (this.csx && prop instanceof Assign) { diff --git a/src/lexer.coffee b/src/lexer.coffee index 01195eb185..f6bc37064f 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -10,7 +10,7 @@ # are read by jison in the `parser.lexer` function defined in coffeescript.coffee. {Rewriter, INVERSES} = require './rewriter' - +log = console.log # Import the helpers we need. {count, starts, compact, repeat, invertLiterate, merge, locationDataToString, throwSyntaxError} = require './helpers' @@ -109,6 +109,20 @@ exports.Lexer = class Lexer # though `is` means `===` otherwise. identifierToken: -> inCSXTag = @atCSXTag() + # Check CSX properties synatx. + # Allowed properties: `
+ if inCSXTag and @prev()[1] isnt ':' and @chunk[0] in ['{', '"', "'"] + if @chunk[0] in ['"', "'"] + @error "Unexpected token" + csxSpreadMatch = /^({\.\.\.\S+})|({\S+\.\.\.})/.exec @chunk + unless csxSpreadMatch + if lastBrace = /^({[^}:\.]+)(:|\})/.exec(@chunk) + @error "Unexpected token, expected ...", offset: lastBrace[0].length - 1 + if badSpread = /^({\S+\.\.\.)|({\.\.\.\S+)[^\}]/.exec @chunk + # Offset value for left or right spread dots position. + offset = if badSpread[0][1] is '.' then badSpread[0].length - 1 else badSpread[0].length + @error "Unexpected token, expected }", {offset} + regex = if inCSXTag then CSX_ATTRIBUTE else IDENTIFIER return 0 unless match = regex.exec @chunk [input, id, colon] = match @@ -515,7 +529,7 @@ exports.Lexer = class Lexer @csxDepth-- return 2 else if firstChar is '{' - if prevChar == ':' + if prevChar is ':' token = @token '(', '(' @ends.push {tag: '}', origin: token} @csxSpreadProps = no @@ -561,6 +575,7 @@ exports.Lexer = class Lexer i = @ends.length - 1 i-- while @ends[i]?.tag is 'OUTDENT' or depth-- > 0 # Ignore indents. last = @ends[i] + # log @csxDepth, last?.tag last?.tag is '/>' and last # We treat all other single characters as a token. E.g.: `( ) , . !` diff --git a/src/nodes.coffee b/src/nodes.coffee index 49ada3a9aa..011341a695 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -1207,7 +1207,7 @@ exports.Obj = class Obj extends Base else ',\n' indent = if isCompact or prop instanceof Comment then '' else idt - + log = console.log key = if prop instanceof Assign and prop.context is 'object' prop.variable else if prop instanceof Assign @@ -1229,7 +1229,7 @@ exports.Obj = class Obj extends Base prop = new Assign prop, prop, 'object' else # Spread in CSX. - prop = if @csx then new Literal "{#{prop.compile(o)}}" else prop + prop = if propHasSplat then new Literal "{#{prop.compile(o)}}" else prop # Check if CSX attribute is valid. # CSX atributes are processed in the `lexer` and converted from # `
` into From 2d06fb1e39531fd504cee6b94553449ce9595a79 Mon Sep 17 00:00:00 2001 From: Zdenko Vujasinovic Date: Tue, 25 Jul 2017 00:35:01 +0200 Subject: [PATCH 07/16] Cleanup --- lib/coffeescript/lexer.js | 4 +--- lib/coffeescript/nodes.js | 5 ++--- src/lexer.coffee | 3 +-- src/nodes.coffee | 4 ++-- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/lib/coffeescript/lexer.js b/lib/coffeescript/lexer.js index ad529ac936..56dca795be 100644 --- a/lib/coffeescript/lexer.js +++ b/lib/coffeescript/lexer.js @@ -1,12 +1,10 @@ // Generated by CoffeeScript 2.0.0-beta3 (function() { - var BOM, BOOL, CALLABLE, CODE, COFFEE_ALIASES, COFFEE_ALIAS_MAP, COFFEE_KEYWORDS, COMMENT, COMPARABLE_LEFT_SIDE, COMPARE, COMPOUND_ASSIGN, CSX_ATTRIBUTE, CSX_IDENTIFIER, CSX_INTERPOLATION, HERECOMMENT_ILLEGAL, HEREDOC_DOUBLE, HEREDOC_INDENT, HEREDOC_SINGLE, HEREGEX, HEREGEX_OMIT, HERE_JSTOKEN, IDENTIFIER, INDENTABLE_CLOSERS, INDEXABLE, INSIDE_CSX, INVERSES, JSTOKEN, JS_KEYWORDS, LEADING_BLANK_LINE, LINE_BREAK, LINE_CONTINUER, Lexer, MATH, MULTI_DENT, NOT_REGEX, NUMBER, OPERATOR, POSSIBLY_DIVISION, REGEX, REGEX_FLAGS, REGEX_ILLEGAL, REGEX_INVALID_ESCAPE, RELATION, RESERVED, Rewriter, SHIFT, SIMPLE_STRING_OMIT, STRICT_PROSCRIBED, STRING_DOUBLE, STRING_INVALID_ESCAPE, STRING_OMIT, STRING_SINGLE, STRING_START, TRAILING_BLANK_LINE, TRAILING_SPACES, UNARY, UNARY_MATH, UNICODE_CODE_POINT_ESCAPE, VALID_FLAGS, WHITESPACE, compact, count, invertLiterate, isForFrom, isUnassignable, key, locationDataToString, log, merge, repeat, starts, throwSyntaxError, + var BOM, BOOL, CALLABLE, CODE, COFFEE_ALIASES, COFFEE_ALIAS_MAP, COFFEE_KEYWORDS, COMMENT, COMPARABLE_LEFT_SIDE, COMPARE, COMPOUND_ASSIGN, CSX_ATTRIBUTE, CSX_IDENTIFIER, CSX_INTERPOLATION, HERECOMMENT_ILLEGAL, HEREDOC_DOUBLE, HEREDOC_INDENT, HEREDOC_SINGLE, HEREGEX, HEREGEX_OMIT, HERE_JSTOKEN, IDENTIFIER, INDENTABLE_CLOSERS, INDEXABLE, INSIDE_CSX, INVERSES, JSTOKEN, JS_KEYWORDS, LEADING_BLANK_LINE, LINE_BREAK, LINE_CONTINUER, Lexer, MATH, MULTI_DENT, NOT_REGEX, NUMBER, OPERATOR, POSSIBLY_DIVISION, REGEX, REGEX_FLAGS, REGEX_ILLEGAL, REGEX_INVALID_ESCAPE, RELATION, RESERVED, Rewriter, SHIFT, SIMPLE_STRING_OMIT, STRICT_PROSCRIBED, STRING_DOUBLE, STRING_INVALID_ESCAPE, STRING_OMIT, STRING_SINGLE, STRING_START, TRAILING_BLANK_LINE, TRAILING_SPACES, UNARY, UNARY_MATH, UNICODE_CODE_POINT_ESCAPE, VALID_FLAGS, WHITESPACE, compact, count, invertLiterate, isForFrom, isUnassignable, key, locationDataToString, merge, repeat, starts, throwSyntaxError, indexOf = [].indexOf; ({Rewriter, INVERSES} = require('./rewriter')); - log = console.log; - ({count, starts, compact, repeat, invertLiterate, merge, locationDataToString, throwSyntaxError} = require('./helpers')); exports.Lexer = Lexer = class Lexer { diff --git a/lib/coffeescript/nodes.js b/lib/coffeescript/nodes.js index 54025a87cc..93242c7254 100644 --- a/lib/coffeescript/nodes.js +++ b/lib/coffeescript/nodes.js @@ -1558,7 +1558,7 @@ } compileNode(o) { - var answer, i, idt, indent, isCompact, j, join, k, key, l, lastNoncom, len1, len2, len3, len4, log, node, prop, propHasSplat, propVal, props, q, ref1, unwrappedVal, value; + var answer, i, idt, indent, isCompact, j, join, k, key, l, lastNoncom, len1, len2, len3, len4, node, prop, propHasSplat, propVal, props, q, ref1, unwrappedVal, value; props = this.properties; if (this.generated) { for (j = 0, len1 = props.length; j < len1; j++) { @@ -1603,7 +1603,6 @@ propHasSplat = prop instanceof Splat; join = i === props.length - 1 ? '' : isCompact && this.csx ? ' ' : isCompact ? ', ' : prop === lastNoncom || prop instanceof Comment || this.csx ? '\n' : ',\n'; indent = isCompact || prop instanceof Comment ? '' : idt; - log = console.log; key = prop instanceof Assign && prop.context === 'object' ? prop.variable : prop instanceof Assign ? (!this.lhs ? prop.operatorToken.error(`unexpected ${prop.operatorToken.value}`) : void 0, prop.variable) : !(prop instanceof Comment) ? prop : void 0; if (key instanceof Value && key.hasProperties()) { if (prop.context === 'object' || !key.this) { @@ -1624,7 +1623,7 @@ prop = new Assign(prop, prop, 'object'); } } else { - prop = propHasSplat ? new Literal(`{${prop.compile(o)}}`) : prop; + prop = this.csx ? new Literal(`{${prop.compile(o)}}`) : prop; } } if (this.csx && prop instanceof Assign) { diff --git a/src/lexer.coffee b/src/lexer.coffee index f6bc37064f..8a6f74ac88 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -10,7 +10,7 @@ # are read by jison in the `parser.lexer` function defined in coffeescript.coffee. {Rewriter, INVERSES} = require './rewriter' -log = console.log + # Import the helpers we need. {count, starts, compact, repeat, invertLiterate, merge, locationDataToString, throwSyntaxError} = require './helpers' @@ -575,7 +575,6 @@ exports.Lexer = class Lexer i = @ends.length - 1 i-- while @ends[i]?.tag is 'OUTDENT' or depth-- > 0 # Ignore indents. last = @ends[i] - # log @csxDepth, last?.tag last?.tag is '/>' and last # We treat all other single characters as a token. E.g.: `( ) , . !` diff --git a/src/nodes.coffee b/src/nodes.coffee index 011341a695..49ada3a9aa 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -1207,7 +1207,7 @@ exports.Obj = class Obj extends Base else ',\n' indent = if isCompact or prop instanceof Comment then '' else idt - log = console.log + key = if prop instanceof Assign and prop.context is 'object' prop.variable else if prop instanceof Assign @@ -1229,7 +1229,7 @@ exports.Obj = class Obj extends Base prop = new Assign prop, prop, 'object' else # Spread in CSX. - prop = if propHasSplat then new Literal "{#{prop.compile(o)}}" else prop + prop = if @csx then new Literal "{#{prop.compile(o)}}" else prop # Check if CSX attribute is valid. # CSX atributes are processed in the `lexer` and converted from # `
` into From e3d1546f501bd398054c1a31be419f85d49b65c0 Mon Sep 17 00:00:00 2001 From: Zdenko Vujasinovic Date: Tue, 25 Jul 2017 01:20:37 +0200 Subject: [PATCH 08/16] Typo --- src/lexer.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lexer.coffee b/src/lexer.coffee index 8a6f74ac88..197751cd20 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -109,7 +109,7 @@ exports.Lexer = class Lexer # though `is` means `===` otherwise. identifierToken: -> inCSXTag = @atCSXTag() - # Check CSX properties synatx. + # Check CSX properties syntax. # Allowed properties: `
if inCSXTag and @prev()[1] isnt ':' and @chunk[0] in ['{', '"', "'"] if @chunk[0] in ['"', "'"] From 00861f61a41513515a691b9d3f770509a7f3e8bc Mon Sep 17 00:00:00 2001 From: Zdenko Vujasinovic Date: Tue, 25 Jul 2017 11:05:12 +0200 Subject: [PATCH 09/16] Improved RegEx --- lib/coffeescript/lexer.js | 24 +++++++++++++----------- src/lexer.coffee | 35 +++++++++++++++++++++++++++-------- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/lib/coffeescript/lexer.js b/lib/coffeescript/lexer.js index 56dca795be..1c046da8e6 100644 --- a/lib/coffeescript/lexer.js +++ b/lib/coffeescript/lexer.js @@ -1,6 +1,6 @@ // Generated by CoffeeScript 2.0.0-beta3 (function() { - var BOM, BOOL, CALLABLE, CODE, COFFEE_ALIASES, COFFEE_ALIAS_MAP, COFFEE_KEYWORDS, COMMENT, COMPARABLE_LEFT_SIDE, COMPARE, COMPOUND_ASSIGN, CSX_ATTRIBUTE, CSX_IDENTIFIER, CSX_INTERPOLATION, HERECOMMENT_ILLEGAL, HEREDOC_DOUBLE, HEREDOC_INDENT, HEREDOC_SINGLE, HEREGEX, HEREGEX_OMIT, HERE_JSTOKEN, IDENTIFIER, INDENTABLE_CLOSERS, INDEXABLE, INSIDE_CSX, INVERSES, JSTOKEN, JS_KEYWORDS, LEADING_BLANK_LINE, LINE_BREAK, LINE_CONTINUER, Lexer, MATH, MULTI_DENT, NOT_REGEX, NUMBER, OPERATOR, POSSIBLY_DIVISION, REGEX, REGEX_FLAGS, REGEX_ILLEGAL, REGEX_INVALID_ESCAPE, RELATION, RESERVED, Rewriter, SHIFT, SIMPLE_STRING_OMIT, STRICT_PROSCRIBED, STRING_DOUBLE, STRING_INVALID_ESCAPE, STRING_OMIT, STRING_SINGLE, STRING_START, TRAILING_BLANK_LINE, TRAILING_SPACES, UNARY, UNARY_MATH, UNICODE_CODE_POINT_ESCAPE, VALID_FLAGS, WHITESPACE, compact, count, invertLiterate, isForFrom, isUnassignable, key, locationDataToString, merge, repeat, starts, throwSyntaxError, + var BOM, BOOL, CALLABLE, CODE, COFFEE_ALIASES, COFFEE_ALIAS_MAP, COFFEE_KEYWORDS, COMMENT, COMPARABLE_LEFT_SIDE, COMPARE, COMPOUND_ASSIGN, CSX_ATTRIBUTE, CSX_FIND_ATTRIBUTE_ERROR, CSX_IDENTIFIER, CSX_INTERPOLATION, CSX_SPREAD_LEFT, CSX_SPREAD_RIGHT, HERECOMMENT_ILLEGAL, HEREDOC_DOUBLE, HEREDOC_INDENT, HEREDOC_SINGLE, HEREGEX, HEREGEX_OMIT, HERE_JSTOKEN, IDENTIFIER, INDENTABLE_CLOSERS, INDEXABLE, INSIDE_CSX, INVERSES, JSTOKEN, JS_KEYWORDS, LEADING_BLANK_LINE, LINE_BREAK, LINE_CONTINUER, Lexer, MATH, MULTI_DENT, NOT_REGEX, NUMBER, OPERATOR, POSSIBLY_DIVISION, REGEX, REGEX_FLAGS, REGEX_ILLEGAL, REGEX_INVALID_ESCAPE, RELATION, RESERVED, Rewriter, SHIFT, SIMPLE_STRING_OMIT, STRICT_PROSCRIBED, STRING_DOUBLE, STRING_INVALID_ESCAPE, STRING_OMIT, STRING_SINGLE, STRING_START, TRAILING_BLANK_LINE, TRAILING_SPACES, UNARY, UNARY_MATH, UNICODE_CODE_POINT_ESCAPE, VALID_FLAGS, WHITESPACE, compact, count, invertLiterate, isForFrom, isUnassignable, key, locationDataToString, merge, repeat, starts, throwSyntaxError, indexOf = [].indexOf; ({Rewriter, INVERSES} = require('./rewriter')); @@ -67,23 +67,19 @@ } identifierToken() { - var alias, badSpread, colon, colonOffset, colonToken, csxSpreadMatch, id, idLength, inCSXTag, input, lastBrace, match, offset, poppedToken, prev, prevprev, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, ref8, ref9, regex, tag, tagToken; + var alias, badCSXSpread, colon, colonOffset, colonToken, expected, id, idLength, inCSXTag, input, match, poppedToken, prev, prevprev, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, ref8, ref9, regex, tag, tagToken; inCSXTag = this.atCSXTag(); if (inCSXTag && this.prev()[1] !== ':' && ((ref = this.chunk[0]) === '{' || ref === '"' || ref === "'")) { if ((ref1 = this.chunk[0]) === '"' || ref1 === "'") { this.error("Unexpected token"); } - csxSpreadMatch = /^({\.\.\.\S+})|({\S+\.\.\.})/.exec(this.chunk); - if (!csxSpreadMatch) { - if (lastBrace = /^({[^}:\.]+)(:|\})/.exec(this.chunk)) { - this.error("Unexpected token, expected ...", { - offset: lastBrace[0].length - 1 + if (!(CSX_SPREAD_LEFT.exec(this.chunk) || CSX_SPREAD_RIGHT.exec(this.chunk))) { + if (badCSXSpread = CSX_FIND_ATTRIBUTE_ERROR.exec(this.chunk)) { + expected = badCSXSpread[1] === '...' || badCSXSpread[3] === '...' ? "}" : "..."; + this.error(`Unexpected token, expected ${expected}`, { + offset: badCSXSpread[0].length - 1 }); } - if (badSpread = /^({\S+\.\.\.)|({\.\.\.\S+)[^\}]/.exec(this.chunk)) { - offset = badSpread[0][1] === '.' ? badSpread[0].length - 1 : badSpread[0].length; - this.error("Unexpected token, expected }", {offset}); - } } } regex = inCSXTag ? CSX_ATTRIBUTE : IDENTIFIER; @@ -1234,6 +1230,12 @@ CSX_ATTRIBUTE = /^(?!\d)((?:(?!\s)[\-$\w\x7f-\uffff])+)([^\S]*=(?!=))?/; + CSX_SPREAD_RIGHT = /^{\s*((?:(?!\s)[$\w\x7f-\uffff])+)\.{3}\s*}/; + + CSX_SPREAD_LEFT = /^{\s*\.{3}((?:(?!\s)[$\w\x7f-\uffff])+)\s*}/; + + CSX_FIND_ATTRIBUTE_ERROR = /^{\s*(?!\d)(\.{3})?((?:(?!\s)[$\w\x7f-\uffff])+)(\.{3})?([:=;,\s}])/; + NUMBER = /^0b[01]+|^0o[0-7]+|^0x[\da-f]+|^\d*\.?\d+(?:e[+-]?\d+)?/i; OPERATOR = /^(?:[-=]>|[-+*\/%<>&|^!?=]=|>>>=?|([-+:])\1|([&|<>*\/%])\2=?|\?(\.|::)|\.{2,3})/; diff --git a/src/lexer.coffee b/src/lexer.coffee index 197751cd20..922c2d9c08 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -114,14 +114,10 @@ exports.Lexer = class Lexer if inCSXTag and @prev()[1] isnt ':' and @chunk[0] in ['{', '"', "'"] if @chunk[0] in ['"', "'"] @error "Unexpected token" - csxSpreadMatch = /^({\.\.\.\S+})|({\S+\.\.\.})/.exec @chunk - unless csxSpreadMatch - if lastBrace = /^({[^}:\.]+)(:|\})/.exec(@chunk) - @error "Unexpected token, expected ...", offset: lastBrace[0].length - 1 - if badSpread = /^({\S+\.\.\.)|({\.\.\.\S+)[^\}]/.exec @chunk - # Offset value for left or right spread dots position. - offset = if badSpread[0][1] is '.' then badSpread[0].length - 1 else badSpread[0].length - @error "Unexpected token, expected }", {offset} + unless CSX_SPREAD_LEFT.exec(@chunk) or CSX_SPREAD_RIGHT.exec(@chunk) + if badCSXSpread = CSX_FIND_ATTRIBUTE_ERROR.exec @chunk + expected = if badCSXSpread[1] is '...' or badCSXSpread[3] is '...' then "}" else "..." + @error "Unexpected token, expected #{expected}", offset: badCSXSpread[0].length - 1 regex = if inCSXTag then CSX_ATTRIBUTE else IDENTIFIER return 0 unless match = regex.exec @chunk @@ -1106,6 +1102,29 @@ CSX_ATTRIBUTE = /// ^ ( [^\S]* = (?!=) )? # Is this an attribute with a value? /// +CSX_SPREAD_RIGHT = /// ^ + {\s* + ( (?: (?!\s)[$\w\x7f-\uffff] )+ ) # `IDENTIFIER` + \.{3} + \s*} +/// + +CSX_SPREAD_LEFT = /// ^ + {\s* + \.{3} + ( (?: (?!\s)[$\w\x7f-\uffff] )+ ) # `IDENTIFIER` + \s*} +/// + +CSX_FIND_ATTRIBUTE_ERROR = /// ^ + {\s* + (?!\d) + (\.{3})? # Possible spreads on the left side. + ( (?: (?!\s)[$\w\x7f-\uffff] )+ ) # `IDENTIFIER` + (\.{3})? # Possible spreads on the right side. + ([:=;,\s}]) # Wrong character +/// + NUMBER = /// ^ 0b[01]+ | # binary ^ 0o[0-7]+ | # octal From 3bed4a41e5aea8261231a1b7664cbf62e2885e57 Mon Sep 17 00:00:00 2001 From: Zdenko Vujasinovic Date: Fri, 28 Jul 2017 10:58:12 +0200 Subject: [PATCH 10/16] Reworked CSX attributes --- lib/coffeescript/lexer.js | 69 +++++++++++-------------------- lib/coffeescript/nodes.js | 87 +++++++++++++++++++++++---------------- src/lexer.coffee | 56 ++++++------------------- src/nodes.coffee | 68 +++++++++++++++--------------- 4 files changed, 124 insertions(+), 156 deletions(-) diff --git a/lib/coffeescript/lexer.js b/lib/coffeescript/lexer.js index 1c046da8e6..146ae535eb 100644 --- a/lib/coffeescript/lexer.js +++ b/lib/coffeescript/lexer.js @@ -1,6 +1,6 @@ // Generated by CoffeeScript 2.0.0-beta3 (function() { - var BOM, BOOL, CALLABLE, CODE, COFFEE_ALIASES, COFFEE_ALIAS_MAP, COFFEE_KEYWORDS, COMMENT, COMPARABLE_LEFT_SIDE, COMPARE, COMPOUND_ASSIGN, CSX_ATTRIBUTE, CSX_FIND_ATTRIBUTE_ERROR, CSX_IDENTIFIER, CSX_INTERPOLATION, CSX_SPREAD_LEFT, CSX_SPREAD_RIGHT, HERECOMMENT_ILLEGAL, HEREDOC_DOUBLE, HEREDOC_INDENT, HEREDOC_SINGLE, HEREGEX, HEREGEX_OMIT, HERE_JSTOKEN, IDENTIFIER, INDENTABLE_CLOSERS, INDEXABLE, INSIDE_CSX, INVERSES, JSTOKEN, JS_KEYWORDS, LEADING_BLANK_LINE, LINE_BREAK, LINE_CONTINUER, Lexer, MATH, MULTI_DENT, NOT_REGEX, NUMBER, OPERATOR, POSSIBLY_DIVISION, REGEX, REGEX_FLAGS, REGEX_ILLEGAL, REGEX_INVALID_ESCAPE, RELATION, RESERVED, Rewriter, SHIFT, SIMPLE_STRING_OMIT, STRICT_PROSCRIBED, STRING_DOUBLE, STRING_INVALID_ESCAPE, STRING_OMIT, STRING_SINGLE, STRING_START, TRAILING_BLANK_LINE, TRAILING_SPACES, UNARY, UNARY_MATH, UNICODE_CODE_POINT_ESCAPE, VALID_FLAGS, WHITESPACE, compact, count, invertLiterate, isForFrom, isUnassignable, key, locationDataToString, merge, repeat, starts, throwSyntaxError, + var BOM, BOOL, CALLABLE, CODE, COFFEE_ALIASES, COFFEE_ALIAS_MAP, COFFEE_KEYWORDS, COMMENT, COMPARABLE_LEFT_SIDE, COMPARE, COMPOUND_ASSIGN, CSX_ATTRIBUTE, CSX_IDENTIFIER, CSX_INTERPOLATION, HERECOMMENT_ILLEGAL, HEREDOC_DOUBLE, HEREDOC_INDENT, HEREDOC_SINGLE, HEREGEX, HEREGEX_OMIT, HERE_JSTOKEN, IDENTIFIER, INDENTABLE_CLOSERS, INDEXABLE, INSIDE_CSX, INVERSES, JSTOKEN, JS_KEYWORDS, LEADING_BLANK_LINE, LINE_BREAK, LINE_CONTINUER, Lexer, MATH, MULTI_DENT, NOT_REGEX, NUMBER, OPERATOR, POSSIBLY_DIVISION, REGEX, REGEX_FLAGS, REGEX_ILLEGAL, REGEX_INVALID_ESCAPE, RELATION, RESERVED, Rewriter, SHIFT, SIMPLE_STRING_OMIT, STRICT_PROSCRIBED, STRING_DOUBLE, STRING_INVALID_ESCAPE, STRING_OMIT, STRING_SINGLE, STRING_START, TRAILING_BLANK_LINE, TRAILING_SPACES, UNARY, UNARY_MATH, UNICODE_CODE_POINT_ESCAPE, VALID_FLAGS, WHITESPACE, compact, count, invertLiterate, isForFrom, isUnassignable, key, locationDataToString, merge, repeat, starts, throwSyntaxError, indexOf = [].indexOf; ({Rewriter, INVERSES} = require('./rewriter')); @@ -25,7 +25,7 @@ this.importSpecifierList = false; this.exportSpecifierList = false; this.csxDepth = 0; - this.csxSpreadProps = false; + this.csxObjAttribute = false; this.chunkLine = opts.line || 0; this.chunkColumn = opts.column || 0; code = this.clean(code); @@ -67,21 +67,8 @@ } identifierToken() { - var alias, badCSXSpread, colon, colonOffset, colonToken, expected, id, idLength, inCSXTag, input, match, poppedToken, prev, prevprev, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, ref8, ref9, regex, tag, tagToken; + var alias, colon, colonOffset, colonToken, id, idLength, inCSXTag, input, match, poppedToken, prev, prevprev, ref, ref1, ref2, ref3, ref4, ref5, ref6, ref7, regex, tag, tagToken; inCSXTag = this.atCSXTag(); - if (inCSXTag && this.prev()[1] !== ':' && ((ref = this.chunk[0]) === '{' || ref === '"' || ref === "'")) { - if ((ref1 = this.chunk[0]) === '"' || ref1 === "'") { - this.error("Unexpected token"); - } - if (!(CSX_SPREAD_LEFT.exec(this.chunk) || CSX_SPREAD_RIGHT.exec(this.chunk))) { - if (badCSXSpread = CSX_FIND_ATTRIBUTE_ERROR.exec(this.chunk)) { - expected = badCSXSpread[1] === '...' || badCSXSpread[3] === '...' ? "}" : "..."; - this.error(`Unexpected token, expected ${expected}`, { - offset: badCSXSpread[0].length - 1 - }); - } - } - } regex = inCSXTag ? CSX_ATTRIBUTE : IDENTIFIER; if (!(match = regex.exec(this.chunk))) { return 0; @@ -100,27 +87,27 @@ if (id === 'as' && this.seenImport) { if (this.value() === '*') { this.tokens[this.tokens.length - 1][0] = 'IMPORT_ALL'; - } else if (ref2 = this.value(), indexOf.call(COFFEE_KEYWORDS, ref2) >= 0) { + } else if (ref = this.value(), indexOf.call(COFFEE_KEYWORDS, ref) >= 0) { this.tokens[this.tokens.length - 1][0] = 'IDENTIFIER'; } - if ((ref3 = this.tag()) === 'DEFAULT' || ref3 === 'IMPORT_ALL' || ref3 === 'IDENTIFIER') { + if ((ref1 = this.tag()) === 'DEFAULT' || ref1 === 'IMPORT_ALL' || ref1 === 'IDENTIFIER') { this.token('AS', id); return id.length; } } - if (id === 'as' && this.seenExport && ((ref4 = this.tag()) === 'IDENTIFIER' || ref4 === 'DEFAULT')) { + if (id === 'as' && this.seenExport && ((ref2 = this.tag()) === 'IDENTIFIER' || ref2 === 'DEFAULT')) { this.token('AS', id); return id.length; } - if (id === 'default' && this.seenExport && ((ref5 = this.tag()) === 'EXPORT' || ref5 === 'AS')) { + if (id === 'default' && this.seenExport && ((ref3 = this.tag()) === 'EXPORT' || ref3 === 'AS')) { this.token('DEFAULT', id); return id.length; } prev = this.prev(); - tag = colon || (prev != null) && (((ref6 = prev[0]) === '.' || ref6 === '?.' || ref6 === '::' || ref6 === '?::') || !prev.spaced && prev[0] === '@') ? 'PROPERTY' : 'IDENTIFIER'; + tag = colon || (prev != null) && (((ref4 = prev[0]) === '.' || ref4 === '?.' || ref4 === '::' || ref4 === '?::') || !prev.spaced && prev[0] === '@') ? 'PROPERTY' : 'IDENTIFIER'; if (tag === 'IDENTIFIER' && (indexOf.call(JS_KEYWORDS, id) >= 0 || indexOf.call(COFFEE_KEYWORDS, id) >= 0) && !(this.exportSpecifierList && indexOf.call(COFFEE_KEYWORDS, id) >= 0)) { tag = id.toUpperCase(); - if (tag === 'WHEN' && (ref7 = this.tag(), indexOf.call(LINE_BREAK, ref7) >= 0)) { + if (tag === 'WHEN' && (ref5 = this.tag(), indexOf.call(LINE_BREAK, ref5) >= 0)) { tag = 'LEADING_WHEN'; } else if (tag === 'FOR') { this.seenFor = true; @@ -148,11 +135,11 @@ tag = 'FORFROM'; this.seenFor = false; } else if (tag === 'PROPERTY' && prev) { - if (prev.spaced && (ref8 = prev[0], indexOf.call(CALLABLE, ref8) >= 0) && /^[gs]et$/.test(prev[1])) { + if (prev.spaced && (ref6 = prev[0], indexOf.call(CALLABLE, ref6) >= 0) && /^[gs]et$/.test(prev[1])) { this.error(`'${prev[1]}' cannot be used as a keyword, or as a function call without parentheses`, prev[2]); } else { prevprev = this.tokens[this.tokens.length - 2]; - if (((ref9 = prev[0]) === '@' || ref9 === 'THIS') && prevprev && prevprev.spaced && /^[gs]et$/.test(prevprev[1]) && this.tokens[this.tokens.length - 3][0] !== '.') { + if (((ref7 = prev[0]) === '@' || ref7 === 'THIS') && prevprev && prevprev.spaced && /^[gs]et$/.test(prevprev[1]) && this.tokens[this.tokens.length - 3][0] !== '.') { this.error(`'${prevprev[1]}' cannot be used as a keyword, or as a function call without parentheses`, prevprev[2]); } } @@ -591,7 +578,7 @@ } csxToken() { - var afterTag, colon, csxTag, end, firstChar, id, input, match, origin, prev, prevChar, ref, token, tokens; + var afterTag, colon, csxStart, csxTag, end, firstChar, id, input, match, origin, prev, prevChar, ref, token, tokens; firstChar = this.chunk[0]; prevChar = this.tokens.length > 0 ? this.tokens[this.tokens.length - 1][0] : ''; if (firstChar === '<') { @@ -602,7 +589,7 @@ [input, id, colon] = match; origin = this.token('CSX_TAG', id, 1, id.length); this.token('CALL_START', '('); - this.token('{', '{'); + csxStart = this.token('[', '['); this.ends.push({ tag: '/>', origin: origin, @@ -613,28 +600,26 @@ } else if (csxTag = this.atCSXTag()) { if (this.chunk.slice(0, 2) === '/>') { this.pair('/>'); - this.token('}', '}', 0, 2); + this.token(']', ']', 0, 2); this.token('CALL_END', ')', 0, 2); this.csxDepth--; return 2; } else if (firstChar === '{') { if (prevChar === ':') { token = this.token('(', '('); - this.ends.push({ - tag: '}', - origin: token - }); - this.csxSpreadProps = false; + this.csxObjAttribute = false; } else { - this.ends.push({ - tag: '}' - }); - this.csxSpreadProps = true; + token = this.token('{', '{'); + this.csxObjAttribute = true; } + this.ends.push({ + tag: '}', + origin: token + }); return 1; } else if (firstChar === '>') { this.pair('/>'); - origin = this.token('}', '}'); + origin = this.token(']', ']'); this.token(',', ','); ({ tokens, @@ -667,7 +652,9 @@ } else if (this.atCSXTag(1)) { if (firstChar === '}') { this.pair(firstChar); - if (!this.csxSpreadProps) { + if (this.csxObjAttribute) { + this.token('}', '}'); + } else { this.token(')', ')'); } this.token(',', ','); @@ -1230,12 +1217,6 @@ CSX_ATTRIBUTE = /^(?!\d)((?:(?!\s)[\-$\w\x7f-\uffff])+)([^\S]*=(?!=))?/; - CSX_SPREAD_RIGHT = /^{\s*((?:(?!\s)[$\w\x7f-\uffff])+)\.{3}\s*}/; - - CSX_SPREAD_LEFT = /^{\s*\.{3}((?:(?!\s)[$\w\x7f-\uffff])+)\s*}/; - - CSX_FIND_ATTRIBUTE_ERROR = /^{\s*(?!\d)(\.{3})?((?:(?!\s)[$\w\x7f-\uffff])+)(\.{3})?([:=;,\s}])/; - NUMBER = /^0b[01]+|^0o[0-7]+|^0x[\da-f]+|^\d*\.?\d+(?:e[+-]?\d+)?/i; OPERATOR = /^(?:[-=]>|[-+*\/%<>&|^!?=]=|>>>=?|([-+:])\1|([&|<>*\/%])\2=?|\?(\.|::)|\.{2,3})/; diff --git a/lib/coffeescript/nodes.js b/lib/coffeescript/nodes.js index 93242c7254..7f057e0a47 100644 --- a/lib/coffeescript/nodes.js +++ b/lib/coffeescript/nodes.js @@ -1198,7 +1198,7 @@ } compileCSX(o) { - var attributes, content, fragments, tag; + var attr, attrProps, attributes, content, fragments, j, len1, obj, ref1, tag; [attributes, content] = this.args; attributes.base.csx = true; if (content != null) { @@ -1206,7 +1206,22 @@ } fragments = [this.makeCode('<')]; fragments.push(...(tag = this.variable.compileToFragments(o, LEVEL_ACCESS))); - fragments.push(...attributes.compileToFragments(o, LEVEL_PAREN)); + if (attributes.base instanceof Arr) { + ref1 = attributes.base.objects; + for (j = 0, len1 = ref1.length; j < len1; j++) { + obj = ref1[j]; + attr = obj.base; + attrProps = (attr != null ? attr.properties : void 0) || []; + if (!(attr instanceof Obj || attr instanceof IdentifierLiteral) || (attr instanceof Obj && !attr.generated && (attrProps > 1 || !(attrProps[0] instanceof Splat)))) { + obj.error('Unexpected token. Allowed CSX attributes are: id="val", src={source} or {props...}'); + } + if (obj.base instanceof Obj) { + obj.base.csx = true; + } + fragments.push(this.makeCode(' ')); + fragments.push(...obj.compileToFragments(o, LEVEL_PAREN)); + } + } if (content) { fragments.push(this.makeCode('>')); fragments.push(...content.compileNode(o, LEVEL_LIST)); @@ -1558,7 +1573,7 @@ } compileNode(o) { - var answer, i, idt, indent, isCompact, j, join, k, key, l, lastNoncom, len1, len2, len3, len4, node, prop, propHasSplat, propVal, props, q, ref1, unwrappedVal, value; + var answer, i, idt, indent, isCompact, j, join, k, key, l, lastNoncom, len1, len2, len3, len4, node, prop, props, q, ref1, unwrappedVal, value; props = this.properties; if (this.generated) { for (j = 0, len1 = props.length; j < len1; j++) { @@ -1573,6 +1588,9 @@ } idt = o.indent += TAB; lastNoncom = this.lastNonComment(this.properties); + if (this.csx) { + return this.compileCSXAttributes(o); + } if (this.lhs) { for (k = 0, len2 = props.length; k < len2; k++) { prop = props[k]; @@ -1592,7 +1610,7 @@ ref1 = this.properties; for (l = 0, len3 = ref1.length; l < len3; l++) { prop = ref1[l]; - if (prop instanceof Comment || (prop instanceof Assign && prop.context === 'object' && !this.csx)) { + if (prop instanceof Comment || (prop instanceof Assign && prop.context === 'object')) { isCompact = false; } } @@ -1600,8 +1618,7 @@ answer.push(this.makeCode(isCompact ? '' : '\n')); for (i = q = 0, len4 = props.length; q < len4; i = ++q) { prop = props[i]; - propHasSplat = prop instanceof Splat; - join = i === props.length - 1 ? '' : isCompact && this.csx ? ' ' : isCompact ? ', ' : prop === lastNoncom || prop instanceof Comment || this.csx ? '\n' : ',\n'; + join = i === props.length - 1 ? '' : isCompact ? ', ' : prop === lastNoncom || prop instanceof Comment ? '\n' : ',\n'; indent = isCompact || prop instanceof Comment ? '' : idt; key = prop instanceof Assign && prop.context === 'object' ? prop.variable : prop instanceof Assign ? (!this.lhs ? prop.operatorToken.error(`unexpected ${prop.operatorToken.value}`) : void 0, prop.variable) : !(prop instanceof Comment) ? prop : void 0; if (key instanceof Value && key.hasProperties()) { @@ -1612,47 +1629,26 @@ prop = new Assign(key, prop, 'object'); } if (key === prop) { - if (!propHasSplat) { - if (prop.shouldCache()) { - [key, value] = prop.base.cache(o); - if (key instanceof IdentifierLiteral) { - key = new PropertyName(key.value); - } - prop = new Assign(key, value, 'object'); - } else if (!(typeof prop.bareLiteral === "function" ? prop.bareLiteral(IdentifierLiteral) : void 0)) { - prop = new Assign(prop, prop, 'object'); + if (prop.shouldCache()) { + [key, value] = prop.base.cache(o); + if (key instanceof IdentifierLiteral) { + key = new PropertyName(key.value); } - } else { - prop = this.csx ? new Literal(`{${prop.compile(o)}}`) : prop; - } - } - if (this.csx && prop instanceof Assign) { - if (!(prop.variable.unwrap() instanceof PropertyName)) { - prop.variable.error("Unexpected token"); - } - propVal = prop.value.unwrap(); - if (!(propVal instanceof StringLiteral || ((propVal instanceof Parens || propVal instanceof StringWithInterpolations) && propVal.body instanceof Block))) { - prop.value.error("expected wrapped or quoted CSX attribute"); + prop = new Assign(key, value, 'object'); + } else if (!(typeof prop.bareLiteral === "function" ? prop.bareLiteral(IdentifierLiteral) : void 0)) { + prop = new Assign(prop, prop, 'object'); } } if (indent) { answer.push(this.makeCode(indent)); } - if (this.csx) { - prop.csx = true; - } - if (this.csx && i === 0) { - answer.push(this.makeCode(' ')); - } answer.push(...prop.compileToFragments(o, LEVEL_TOP)); if (join) { answer.push(this.makeCode(join)); } } answer.push(this.makeCode(isCompact ? '' : `\n${this.tab}`)); - if (!this.csx) { - answer = this.wrapInBraces(answer); - } + answer = this.wrapInBraces(answer); if (this.front) { return this.wrapInParentheses(answer); } else { @@ -1723,6 +1719,27 @@ return (new Call(new Literal('Object.assign'), slices)).compileToFragments(o); } + compileCSXAttributes(o) { + var answer, i, j, join, len1, prop, props; + props = this.properties; + answer = []; + for (i = j = 0, len1 = props.length; j < len1; i = ++j) { + prop = props[i]; + prop.csx = true; + join = i === props.length - 1 ? '' : ' '; + if (prop instanceof Splat) { + prop = new Literal(`{${prop.compile(o)}}`); + } + answer.push(...prop.compileToFragments(o, LEVEL_TOP)); + answer.push(this.makeCode(join)); + } + if (this.front) { + return this.wrapInParentheses(answer); + } else { + return answer; + } + } + }; Obj.prototype.children = ['properties']; diff --git a/src/lexer.coffee b/src/lexer.coffee index 922c2d9c08..a50d11be47 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -49,7 +49,7 @@ exports.Lexer = class Lexer @importSpecifierList = no # Used to identify when in an IMPORT {...} FROM? ... @exportSpecifierList = no # Used to identify when in an EXPORT {...} FROM? ... @csxDepth = 0 # Used to optimize CSX checks, how deep in CSX we are. - @csxSpreadProps = no # Used to detect if CSX attributes include spreads (
). + @csxObjAttribute = no # Used to detect if CSX attributes is wrapped in {} (
). @chunkLine = opts.line or 0 # The start line for the current @chunk. @@ -80,7 +80,7 @@ exports.Lexer = class Lexer i += consumed return {@tokens, index: i} if opts.untilBalanced and @ends.length is 0 - + @closeIndentation() @error "missing #{end.tag}", (end.origin ? end)[2] if end = @ends.pop() return @tokens if opts.rewrite is off @@ -109,16 +109,6 @@ exports.Lexer = class Lexer # though `is` means `===` otherwise. identifierToken: -> inCSXTag = @atCSXTag() - # Check CSX properties syntax. - # Allowed properties: `
- if inCSXTag and @prev()[1] isnt ':' and @chunk[0] in ['{', '"', "'"] - if @chunk[0] in ['"', "'"] - @error "Unexpected token" - unless CSX_SPREAD_LEFT.exec(@chunk) or CSX_SPREAD_RIGHT.exec(@chunk) - if badCSXSpread = CSX_FIND_ATTRIBUTE_ERROR.exec @chunk - expected = if badCSXSpread[1] is '...' or badCSXSpread[3] is '...' then "}" else "..." - @error "Unexpected token, expected #{expected}", offset: badCSXSpread[0].length - 1 - regex = if inCSXTag then CSX_ATTRIBUTE else IDENTIFIER return 0 unless match = regex.exec @chunk [input, id, colon] = match @@ -513,30 +503,30 @@ exports.Lexer = class Lexer [input, id, colon] = match origin = @token 'CSX_TAG', id, 1, id.length @token 'CALL_START', '(' - @token '{', '{' + csxStart = @token '[', '[' @ends.push tag: '/>', origin: origin, name: id @csxDepth++ return id.length + 1 else if csxTag = @atCSXTag() if @chunk[...2] is '/>' @pair '/>' - @token '}', '}', 0, 2 + @token ']', ']', 0, 2 @token 'CALL_END', ')', 0, 2 @csxDepth-- return 2 else if firstChar is '{' if prevChar is ':' token = @token '(', '(' - @ends.push {tag: '}', origin: token} - @csxSpreadProps = no + @csxObjAttribute = no else - @ends.push {tag: '}'} - @csxSpreadProps = yes + token = @token '{', '{' + @csxObjAttribute = yes + @ends.push {tag: '}', origin: token} return 1 else if firstChar is '>' # Ignore terminators inside a tag. @pair '/>' # As if the current tag was self-closing. - origin = @token '}', '}' + origin = @token ']', ']' @token ',', ',' {tokens, index: end} = @matchWithInterpolations INSIDE_CSX, '>', ' + if not (attr instanceof Obj or attr instanceof IdentifierLiteral) or (attr instanceof Obj and not attr.generated and (attrProps > 1 or not (attrProps[0] instanceof Splat))) + obj.error 'Unexpected token. Allowed CSX attributes are: id="val", src={source} or {props...}' + obj.base.csx = yes if obj.base instanceof Obj + fragments.push @makeCode ' ' + fragments.push obj.compileToFragments(o, LEVEL_PAREN)... if content fragments.push @makeCode('>') fragments.push content.compileNode(o, LEVEL_LIST)... @@ -1176,6 +1185,9 @@ exports.Obj = class Obj extends Base idt = o.indent += TAB lastNoncom = @lastNonComment @properties + # CSX attributes
+ return @compileCSXAttributes o if @csx + # If this object is the left-hand side of an assignment, all its children # are too. if @lhs @@ -1189,20 +1201,17 @@ exports.Obj = class Obj extends Base isCompact = yes for prop in @properties - if prop instanceof Comment or (prop instanceof Assign and prop.context is 'object' and not @csx) + if prop instanceof Comment or (prop instanceof Assign and prop.context is 'object') isCompact = no answer = [] answer.push @makeCode if isCompact then '' else '\n' for prop, i in props - propHasSplat = prop instanceof Splat join = if i is props.length - 1 '' - else if isCompact and @csx - ' ' else if isCompact ', ' - else if prop is lastNoncom or prop instanceof Comment or @csx + else if prop is lastNoncom or prop instanceof Comment '\n' else ',\n' @@ -1220,37 +1229,17 @@ exports.Obj = class Obj extends Base key = key.properties[0].name prop = new Assign key, prop, 'object' if key is prop - unless propHasSplat - if prop.shouldCache() - [key, value] = prop.base.cache o - key = new PropertyName key.value if key instanceof IdentifierLiteral - prop = new Assign key, value, 'object' - else unless prop.bareLiteral?(IdentifierLiteral) - prop = new Assign prop, prop, 'object' - else - # Spread in CSX. - prop = if @csx then new Literal "{#{prop.compile(o)}}" else prop - # Check if CSX attribute is valid. - # CSX atributes are processed in the `lexer` and converted from - # `
` into - # `{ id:(abc), name:"tags", ...props, title:("" + (foo())), src:{a:1, b:2} }` - if @csx and prop instanceof Assign - # The `prop.variable` must be the instance of `PropertyName` (i.e. `PROPERTY`). - prop.variable.error "Unexpected token" unless prop.variable.unwrap() instanceof PropertyName - propVal = prop.value.unwrap() - # The `prop.value` instance can be: - # - `StringLiteral`, e.g. id:"abc" - # - `Parens` or `StringWithInterpolations`, e.g. id:{abc} or id:"#{abc}" or id:(abc) - unless propVal instanceof StringLiteral or - ((propVal instanceof Parens or propVal instanceof StringWithInterpolations) and propVal.body instanceof Block) - prop.value.error "expected wrapped or quoted CSX attribute" + if prop.shouldCache() + [key, value] = prop.base.cache o + key = new PropertyName key.value if key instanceof IdentifierLiteral + prop = new Assign key, value, 'object' + else if not prop.bareLiteral?(IdentifierLiteral) + prop = new Assign prop, prop, 'object' if indent then answer.push @makeCode indent - prop.csx = yes if @csx - answer.push @makeCode ' ' if @csx and i is 0 answer.push prop.compileToFragments(o, LEVEL_TOP)... if join then answer.push @makeCode join answer.push @makeCode if isCompact then '' else "\n#{@tab}" - answer = @wrapInBraces answer if not @csx + answer = @wrapInBraces answer if @front then @wrapInParentheses answer else answer assigns: (name) -> @@ -1285,7 +1274,18 @@ exports.Obj = class Obj extends Base addSlice() slices.unshift new Obj unless slices[0] instanceof Obj (new Call new Literal('Object.assign'), slices).compileToFragments o - + + compileCSXAttributes: (o) -> + props = @properties + answer = [] + for prop, i in props + prop.csx = yes + join = if i is props.length - 1 then '' else ' ' + prop = new Literal "{#{prop.compile(o)}}" if prop instanceof Splat + answer.push prop.compileToFragments(o, LEVEL_TOP)... + answer.push @makeCode join + if @front then @wrapInParentheses answer else answer + #### Arr # An array literal. From 590844802727a60db6b26bf251042b448b58b547 Mon Sep 17 00:00:00 2001 From: Zdenko Vujasinovic Date: Fri, 28 Jul 2017 19:11:30 +0200 Subject: [PATCH 11/16] small fix for CSX attribute validation --- lib/coffeescript/nodes.js | 5 +++-- src/nodes.coffee | 8 ++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/coffeescript/nodes.js b/lib/coffeescript/nodes.js index 7f057e0a47..52dd782764 100644 --- a/lib/coffeescript/nodes.js +++ b/lib/coffeescript/nodes.js @@ -1212,8 +1212,9 @@ obj = ref1[j]; attr = obj.base; attrProps = (attr != null ? attr.properties : void 0) || []; - if (!(attr instanceof Obj || attr instanceof IdentifierLiteral) || (attr instanceof Obj && !attr.generated && (attrProps > 1 || !(attrProps[0] instanceof Splat)))) { - obj.error('Unexpected token. Allowed CSX attributes are: id="val", src={source} or {props...}'); + console.log(attr, attrProps); + if (!(attr instanceof Obj || attr instanceof IdentifierLiteral) || (attr instanceof Obj && !attr.generated && (attrProps.length > 1 || !(attrProps[0] instanceof Splat)))) { + obj.error("Unexpected token. Allowed CSX attributes are: id=\"val\", src={source}, {props...} or attribute.\nExample:
hellow world
."); } if (obj.base instanceof Obj) { obj.base.csx = true; diff --git a/src/nodes.coffee b/src/nodes.coffee index 488b9cb553..4fef1733f1 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -884,9 +884,13 @@ exports.Call = class Call extends Base for obj in attributes.base.objects attr = obj.base attrProps = attr?.properties or [] + console.log attr, attrProps # Catch invalid CSX attributes:
- if not (attr instanceof Obj or attr instanceof IdentifierLiteral) or (attr instanceof Obj and not attr.generated and (attrProps > 1 or not (attrProps[0] instanceof Splat))) - obj.error 'Unexpected token. Allowed CSX attributes are: id="val", src={source} or {props...}' + if not (attr instanceof Obj or attr instanceof IdentifierLiteral) or (attr instanceof Obj and not attr.generated and (attrProps.length > 1 or not (attrProps[0] instanceof Splat))) + obj.error """ + Unexpected token. Allowed CSX attributes are: id="val", src={source}, {props...} or attribute. + Example:
hellow world
. + """ obj.base.csx = yes if obj.base instanceof Obj fragments.push @makeCode ' ' fragments.push obj.compileToFragments(o, LEVEL_PAREN)... From 437ff7033c8cc8f1cfd8e75a9018b38b48b523ce Mon Sep 17 00:00:00 2001 From: Zdenko Vujasinovic Date: Fri, 28 Jul 2017 19:12:55 +0200 Subject: [PATCH 12/16] cleanup --- lib/coffeescript/nodes.js | 3 +-- src/lexer.coffee | 2 +- src/nodes.coffee | 5 ++--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/coffeescript/nodes.js b/lib/coffeescript/nodes.js index 52dd782764..104827c30c 100644 --- a/lib/coffeescript/nodes.js +++ b/lib/coffeescript/nodes.js @@ -1212,9 +1212,8 @@ obj = ref1[j]; attr = obj.base; attrProps = (attr != null ? attr.properties : void 0) || []; - console.log(attr, attrProps); if (!(attr instanceof Obj || attr instanceof IdentifierLiteral) || (attr instanceof Obj && !attr.generated && (attrProps.length > 1 || !(attrProps[0] instanceof Splat)))) { - obj.error("Unexpected token. Allowed CSX attributes are: id=\"val\", src={source}, {props...} or attribute.\nExample:
hellow world
."); + obj.error("Unexpected token. Allowed CSX attributes are: id=\"val\", src={source}, {props...} or attribute.\nExample:
hello world
."); } if (obj.base instanceof Obj) { obj.base.csx = true; diff --git a/src/lexer.coffee b/src/lexer.coffee index f216fb6fed..8ddd17eb39 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -80,7 +80,7 @@ exports.Lexer = class Lexer i += consumed return {@tokens, index: i} if opts.untilBalanced and @ends.length is 0 - + @closeIndentation() @error "missing #{end.tag}", (end.origin ? end)[2] if end = @ends.pop() return @tokens if opts.rewrite is off diff --git a/src/nodes.coffee b/src/nodes.coffee index 4fef1733f1..399629ed91 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -884,13 +884,12 @@ exports.Call = class Call extends Base for obj in attributes.base.objects attr = obj.base attrProps = attr?.properties or [] - console.log attr, attrProps # Catch invalid CSX attributes:
if not (attr instanceof Obj or attr instanceof IdentifierLiteral) or (attr instanceof Obj and not attr.generated and (attrProps.length > 1 or not (attrProps[0] instanceof Splat))) obj.error """ Unexpected token. Allowed CSX attributes are: id="val", src={source}, {props...} or attribute. - Example:
hellow world
. - """ + Example:
hello world
. + """ obj.base.csx = yes if obj.base instanceof Obj fragments.push @makeCode ' ' fragments.push obj.compileToFragments(o, LEVEL_PAREN)... From b097188f71f26758bcb15d68d3d0d1053997a2da Mon Sep 17 00:00:00 2001 From: Zdenko Vujasinovic Date: Sat, 29 Jul 2017 07:55:43 +0200 Subject: [PATCH 13/16] tests --- test/csx.coffee | 238 ++++++++++++++++++++++-------------------------- 1 file changed, 107 insertions(+), 131 deletions(-) diff --git a/test/csx.coffee b/test/csx.coffee index e326afe64c..62d015eb22 100644 --- a/test/csx.coffee +++ b/test/csx.coffee @@ -464,147 +464,115 @@ test 'self closing tag with namespace', -> ; ''' -# TODO: Uncomment the following test once destructured object spreads are supported. -# test 'self closing tag with spread attribute', -> -# eqJS ''' -# -# ''', ''' -# React.createElement(Component, Object.assign({"a": (b)}, x , {"b": "c"})) -# ''' - -# TODO: Uncomment the following test once destructured object spreads are supported. -# test 'complex spread attribute', -> -# eqJS ''' -# -# ''', ''' -# React.createElement(Component, Object.assign({}, x, {"a": (b)}, x , {"b": "c"}, $my_xtraCoolVar123 )) -# ''' - -# TODO: Uncomment the following test once destructured object spreads are supported. -# test 'multiline spread attribute', -> -# eqJS ''' -# -# -# ''', ''' -# React.createElement(Component, Object.assign({}, -# x , {"a": (b)}, x , {"b": "c"}, z ) -# ) -# ''' +test 'self closing tag with spread attribute', -> + eqJS ''' + + ''', ''' + ; + ''' -# TODO: Uncomment the following test once destructured object spreads are supported. -# test 'multiline tag with spread attribute', -> -# eqJS ''' -# -# -# ''', ''' -# React.createElement(Component, Object.assign({ \ -# "z": "1" -# }, x, { \ -# "a": (b), \ -# "b": "c" -# }) -# ) -# ''' +test 'complex spread attribute', -> + eqJS ''' + + ''', ''' + ; + ''' -# TODO: Uncomment the following test once destructured object spreads are supported. -# test 'multiline tag with spread attribute first', -> -# eqJS ''' -# -# -# ''', ''' -# React.createElement(Component, Object.assign({}, \ - -# x, { \ -# "z": "1", \ -# "a": (b), \ -# "b": "c" -# }) -# ) -# ''' +test 'multiline spread attribute', -> + eqJS ''' + + + ''', ''' + + ; + ''' -# TODO: Uncomment the following test once destructured object spreads are supported. -# test 'complex multiline spread attribute', -> -# eqJS ''' -# -#
-# -# ''', ''' -# React.createElement(Component, Object.assign({}, \ +test 'multiline tag with spread attribute', -> + eqJS ''' + + + ''', ''' + + ; + ''' -# y, {"a": (b)}, x , {"b": "c"}, z ), -# React.createElement("div", {"code": (someFunc({a:{b:{}, C:'}'}}))}) -# ) -# ''' +test 'multiline tag with spread attribute first', -> + eqJS ''' + + + ''', ''' + + ; + ''' -# TODO: Uncomment the following test once destructured object spreads are supported. -# test 'self closing spread attribute on single line', -> -# eqJS ''' -# -# ''', ''' -# React.createElement(Component, Object.assign({"a": "b", "c": "d"}, @props )) -# ''' +test 'complex multiline spread attribute', -> + eqJS ''' + +
+ + ''', ''' + +
+ ; + ''' -# TODO: Uncomment the following test once destructured object spreads are supported. -# test 'self closing spread attribute on new line', -> -# eqJS ''' -# -# ''', ''' -# React.createElement(Component, Object.assign({ \ -# "a": "b", \ -# "c": "d" -# }, @props -# )) -# ''' +test 'self closing spread attribute on single line', -> + eqJS ''' + + ''', ''' + ; + ''' -# TODO: Uncomment the following test once destructured object spreads are supported. -# test 'self closing spread attribute on same line', -> -# eqJS ''' -# -# ''', ''' -# React.createElement(Component, Object.assign({ \ -# "a": "b", \ -# "c": "d" -# }, @props )) -# ''' +test 'self closing spread attribute on new line', -> + eqJS ''' + + ''', ''' + ; + ''' -# TODO: Uncomment the following test once destructured object spreads are supported. -# test 'self closing spread attribute on next line', -> -# eqJS ''' -# + eqJS ''' + + ''', ''' + ; + ''' -# /> -# ''', ''' -# React.createElement(Component, Object.assign({ \ -# "a": "b", \ -# "c": "d" -# }, @props +test 'self closing spread attribute on next line', -> + eqJS ''' + + ''', ''' + ; + ''' test 'empty strings are not converted to true', -> eqJS ''' @@ -724,3 +692,11 @@ test 'unspaced less than after CSX works but is not encouraged', -> res = 2 < div; ''' + +test 'CSX invalid attribute errors', -> + throws (-> CoffeeScript.compile '
') + throws (-> CoffeeScript.compile '
') + throws (-> CoffeeScript.compile '
') + throws (-> CoffeeScript.compile '
') + throws (-> CoffeeScript.compile '
') + throws (-> CoffeeScript.compile '
') From e86285b5d5ee90a871174bbcaa17d7c1e9ad9454 Mon Sep 17 00:00:00 2001 From: Zdenko Vujasinovic Date: Mon, 31 Jul 2017 17:18:04 +0200 Subject: [PATCH 14/16] fix nested assignement; remove unused variable --- lib/coffeescript/lexer.js | 5 +++-- src/lexer.coffee | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/coffeescript/lexer.js b/lib/coffeescript/lexer.js index a2d2e98570..9c5e9e3d9c 100644 --- a/lib/coffeescript/lexer.js +++ b/lib/coffeescript/lexer.js @@ -578,7 +578,7 @@ } csxToken() { - var afterTag, colon, csxStart, csxTag, end, firstChar, id, input, match, origin, prev, prevChar, ref, token, tokens; + var afterTag, colon, csxTag, end, firstChar, id, input, match, origin, prev, prevChar, ref, token, tokens; firstChar = this.chunk[0]; prevChar = this.tokens.length > 0 ? this.tokens[this.tokens.length - 1][0] : ''; if (firstChar === '<') { @@ -589,7 +589,7 @@ [input, id, colon] = match; origin = this.token('CSX_TAG', id, 1, id.length); this.token('CALL_START', '('); - csxStart = this.token('[', '['); + this.token('[', '['); this.ends.push({ tag: '/>', origin: origin, @@ -654,6 +654,7 @@ this.pair(firstChar); if (this.csxObjAttribute) { this.token('}', '}'); + this.csxObjAttribute = false; } else { this.token(')', ')'); } diff --git a/src/lexer.coffee b/src/lexer.coffee index 8ddd17eb39..806264a738 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -503,7 +503,7 @@ exports.Lexer = class Lexer [input, id, colon] = match origin = @token 'CSX_TAG', id, 1, id.length @token 'CALL_START', '(' - csxStart = @token '[', '[' + @token '[', '[' @ends.push tag: '/>', origin: origin, name: id @csxDepth++ return id.length + 1 @@ -550,6 +550,7 @@ exports.Lexer = class Lexer @pair firstChar if @csxObjAttribute @token '}', '}' + @csxObjAttribute = no else @token ')', ')' @token ',', ',' From 7de2eb9818def18072cc46b05f7828b37f870a70 Mon Sep 17 00:00:00 2001 From: Zdenko Vujasinovic Date: Wed, 2 Aug 2017 11:32:21 +0200 Subject: [PATCH 15/16] cleanup; improve tests --- lib/coffeescript/nodes.js | 2 +- src/nodes.coffee | 1 - test/csx.coffee | 8 ------- test/error_messages.coffee | 44 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 45 insertions(+), 10 deletions(-) diff --git a/lib/coffeescript/nodes.js b/lib/coffeescript/nodes.js index 104827c30c..57aafa0427 100644 --- a/lib/coffeescript/nodes.js +++ b/lib/coffeescript/nodes.js @@ -1213,7 +1213,7 @@ attr = obj.base; attrProps = (attr != null ? attr.properties : void 0) || []; if (!(attr instanceof Obj || attr instanceof IdentifierLiteral) || (attr instanceof Obj && !attr.generated && (attrProps.length > 1 || !(attrProps[0] instanceof Splat)))) { - obj.error("Unexpected token. Allowed CSX attributes are: id=\"val\", src={source}, {props...} or attribute.\nExample:
hello world
."); + obj.error("Unexpected token. Allowed CSX attributes are: id=\"val\", src={source}, {props...} or attribute."); } if (obj.base instanceof Obj) { obj.base.csx = true; diff --git a/src/nodes.coffee b/src/nodes.coffee index 399629ed91..817262337d 100644 --- a/src/nodes.coffee +++ b/src/nodes.coffee @@ -888,7 +888,6 @@ exports.Call = class Call extends Base if not (attr instanceof Obj or attr instanceof IdentifierLiteral) or (attr instanceof Obj and not attr.generated and (attrProps.length > 1 or not (attrProps[0] instanceof Splat))) obj.error """ Unexpected token. Allowed CSX attributes are: id="val", src={source}, {props...} or attribute. - Example:
hello world
. """ obj.base.csx = yes if obj.base instanceof Obj fragments.push @makeCode ' ' diff --git a/test/csx.coffee b/test/csx.coffee index 62d015eb22..d5a99300c6 100644 --- a/test/csx.coffee +++ b/test/csx.coffee @@ -692,11 +692,3 @@ test 'unspaced less than after CSX works but is not encouraged', -> res = 2 < div; ''' - -test 'CSX invalid attribute errors', -> - throws (-> CoffeeScript.compile '
') - throws (-> CoffeeScript.compile '
') - throws (-> CoffeeScript.compile '
') - throws (-> CoffeeScript.compile '
') - throws (-> CoffeeScript.compile '
') - throws (-> CoffeeScript.compile '
') diff --git a/test/error_messages.coffee b/test/error_messages.coffee index c4a8830264..8a700e753a 100644 --- a/test/error_messages.coffee +++ b/test/error_messages.coffee @@ -1587,6 +1587,50 @@ test "CSX error: ambiguous tag-like expression", -> ^ ''' +test 'CSX error: invalid attributes', -> + assertErrorFormat ''' +
+ ''', ''' + [stdin]:1:12: error: Unexpected token. Allowed CSX attributes are: id="val", src={source}, {props...} or attribute. +
+ ^^^^^^^ + ''' + assertErrorFormat ''' +
+ ''', ''' + [stdin]:1:12: error: Unexpected token. Allowed CSX attributes are: id="val", src={source}, {props...} or attribute. +
+ ^^^^^^^ + ''' + assertErrorFormat ''' +
+ ''', ''' + [stdin]:1:6: error: Unexpected token. Allowed CSX attributes are: id="val", src={source}, {props...} or attribute. +
+ ^^^^^^^^ + ''' + assertErrorFormat ''' +
+ ''', ''' + [stdin]:1:11: error: Unexpected token. Allowed CSX attributes are: id="val", src={source}, {props...} or attribute. +
+ ^^^ + ''' + assertErrorFormat ''' +
+ ''', ''' + [stdin]:1:6: error: Unexpected token. Allowed CSX attributes are: id="val", src={source}, {props...} or attribute. +
+ ^^^^^^^^^^^^^^^^^^^^^^^^ + ''' + assertErrorFormat ''' +
+ ''', ''' + [stdin]:1:6: error: Unexpected token. Allowed CSX attributes are: id="val", src={source}, {props...} or attribute. +
+ ^^^^^^^^^^^^^^^^ + ''' + test 'Bound method called as callback before binding throws runtime error', -> class Base constructor: -> From ff8d4f2ead2534e0d0e0eb4fa925c9d6e338c4a8 Mon Sep 17 00:00:00 2001 From: Zdenko Vujasinovic Date: Wed, 2 Aug 2017 21:43:19 +0200 Subject: [PATCH 16/16] fix esoteric case; improve tracking nested splats in CSX tag --- lib/coffeescript/lexer.js | 10 +++++----- src/lexer.coffee | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/coffeescript/lexer.js b/lib/coffeescript/lexer.js index 9c5e9e3d9c..c3c8e4e6a4 100644 --- a/lib/coffeescript/lexer.js +++ b/lib/coffeescript/lexer.js @@ -25,7 +25,7 @@ this.importSpecifierList = false; this.exportSpecifierList = false; this.csxDepth = 0; - this.csxObjAttribute = false; + this.csxObjAttribute = {}; this.chunkLine = opts.line || 0; this.chunkColumn = opts.column || 0; code = this.clean(code); @@ -607,10 +607,10 @@ } else if (firstChar === '{') { if (prevChar === ':') { token = this.token('(', '('); - this.csxObjAttribute = false; + this.csxObjAttribute[this.csxDepth] = false; } else { token = this.token('{', '{'); - this.csxObjAttribute = true; + this.csxObjAttribute[this.csxDepth] = true; } this.ends.push({ tag: '}', @@ -652,9 +652,9 @@ } else if (this.atCSXTag(1)) { if (firstChar === '}') { this.pair(firstChar); - if (this.csxObjAttribute) { + if (this.csxObjAttribute[this.csxDepth]) { this.token('}', '}'); - this.csxObjAttribute = false; + this.csxObjAttribute[this.csxDepth] = false; } else { this.token(')', ')'); } diff --git a/src/lexer.coffee b/src/lexer.coffee index 806264a738..e3b5591976 100644 --- a/src/lexer.coffee +++ b/src/lexer.coffee @@ -49,7 +49,7 @@ exports.Lexer = class Lexer @importSpecifierList = no # Used to identify when in an IMPORT {...} FROM? ... @exportSpecifierList = no # Used to identify when in an EXPORT {...} FROM? ... @csxDepth = 0 # Used to optimize CSX checks, how deep in CSX we are. - @csxObjAttribute = no # Used to detect if CSX attributes is wrapped in {} (
). + @csxObjAttribute = {} # Used to detect if CSX attributes is wrapped in {} (
). @chunkLine = opts.line or 0 # The start line for the current @chunk. @@ -517,10 +517,10 @@ exports.Lexer = class Lexer else if firstChar is '{' if prevChar is ':' token = @token '(', '(' - @csxObjAttribute = no + @csxObjAttribute[@csxDepth] = no else token = @token '{', '{' - @csxObjAttribute = yes + @csxObjAttribute[@csxDepth] = yes @ends.push {tag: '}', origin: token} return 1 else if firstChar is '>' @@ -548,9 +548,9 @@ exports.Lexer = class Lexer else if @atCSXTag 1 if firstChar is '}' @pair firstChar - if @csxObjAttribute + if @csxObjAttribute[@csxDepth] @token '}', '}' - @csxObjAttribute = no + @csxObjAttribute[@csxDepth] = no else @token ')', ')' @token ',', ','