Skip to content

[CS2] CSX spread attributes: <div {props…} /> #4607

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Aug 3, 2017
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions lib/coffeescript/lexer.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

59 changes: 45 additions & 14 deletions lib/coffeescript/nodes.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 17 additions & 5 deletions src/lexer.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -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.
@csxObjAttribute = no # Used to detect if CSX attributes is wrapped in {} (<div {props...} />).

@chunkLine =
opts.line or 0 # The start line for the current @chunk.
Expand Down Expand Up @@ -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 (
Expand All @@ -500,25 +503,30 @@ exports.Lexer = class Lexer
[input, id, colon] = match
origin = @token 'CSX_TAG', id, 1, id.length
@token 'CALL_START', '('
@token '{', '{'
@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 '{'
token = @token '(', '('
if prevChar is ':'
token = @token '(', '('
@csxObjAttribute = no
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what the right fix here is, but because this is limited to a single state, alternately nesting assignment and splats causes errors:

[stdin]:1:24: error: unexpected }
<A child={<B {...params} />} />
                       ^

I suppose that's what @pair is for, but that will currently throw if it doesn't match... It would be nice to avoid a new stack though...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@connec the latest commit fixes this bug.

else
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, '>', '</', CSX_INTERPOLATION
Expand All @@ -540,7 +548,11 @@ exports.Lexer = class Lexer
else if @atCSXTag 1
if firstChar is '}'
@pair firstChar
@token ')', ')'
if @csxObjAttribute
@token '}', '}'
@csxObjAttribute = no
else
@token ')', ')'
@token ',', ','
return 1
else
Expand Down
42 changes: 32 additions & 10 deletions src/nodes.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -880,7 +880,19 @@ exports.Call = class Call extends Base
content?.base.csx = yes
fragments = [@makeCode('<')]
fragments.push (tag = @variable.compileToFragments(o, LEVEL_ACCESS))...
fragments.push attributes.compileToFragments(o, LEVEL_PAREN)...
if attributes.base instanceof Arr
for obj in attributes.base.objects
attr = obj.base
attrProps = attr?.properties or []
# Catch invalid CSX attributes: <div {a:"b", props} {props} "value" />
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: <div id="val" src={getTheSource()} {props...} checked>hello world</div>.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don’t normally put examples in error messages. Please update the docs with this if the docs are insufficient.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll remove it.

"""
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)...
Expand Down Expand Up @@ -1171,11 +1183,14 @@ 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 not @csx

idt = o.indent += TAB
lastNoncom = @lastNonComment @properties

# CSX attributes <div id="val" attr={aaa} {props...} />
return @compileCSXAttributes o if @csx

# If this object is the left-hand side of an assignment, all its children
# are too.
if @lhs
Expand All @@ -1189,19 +1204,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
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'
Expand All @@ -1226,12 +1239,10 @@ exports.Obj = class Obj extends Base
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) ->
Expand Down Expand Up @@ -1266,7 +1277,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.
Expand Down
Loading