Skip to content

Commit

Permalink
Merge pull request #507 from DanielXMoore/bind-jsx
Browse files Browse the repository at this point in the history
JSX unbraced @ and @@ shorthand
  • Loading branch information
edemaine authored Apr 15, 2023
2 parents 058df45 + cc4f6e2 commit 05e3c3c
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 28 deletions.
4 changes: 3 additions & 1 deletion civet.dev/cheatsheet.md
Original file line number Diff line number Diff line change
Expand Up @@ -1253,7 +1253,9 @@ Implicit elements must start with `id` or `class` shorthand (`#` or `.`).
<div {foo}>Civet
<div {props.name}>Civet
<div {data()}>Civet
<div {@@onClick}>Civet
<div @name>Civet
<div @data()>Civet
<div @@onClick>Civet
<div ...foo>Civet
<div [expr]={value}>Civet
</Playground>
Expand Down
21 changes: 21 additions & 0 deletions source/lib.js
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,26 @@ function quoteString(str) {
}
}

// Look for last property access like `.foo` or `[computed]` or root Identifier,
// before any calls like `(args)`, non-null assertions `!`, and optionals `?`.
// The return value should have a `name` property (for "Identifier" and
// "Index"), or have `type` of "Index" (for `[computed]`), or be undefined.
function lastAccessInCallExpression(exp) {
let children, i
do {
({children} = exp)
i = children.length - 1
while (i >= 0 && (
children[i].type === "Call" ||
children[i].type === "NonNullAssertion" ||
children[i].type === "Optional"
)) i--
if (i < 0) return
// Recurse into nested MemberExpression, e.g. from `x.y()`
} while (children[i].type === "MemberExpression" && (exp = children[i]))
return children[i]
}

function processCoffeeInterpolation(s, parts, e, $loc) {
// Check for no interpolations
if (parts.length === 0 || (parts.length === 1 && parts[0].token != null)) {
Expand Down Expand Up @@ -665,6 +685,7 @@ module.exports = {
hoistRefDecs,
insertTrimmingSpace,
isFunction,
lastAccessInCallExpression,
literalValue,
modifyString,
processCoffeeInterpolation,
Expand Down
67 changes: 40 additions & 27 deletions source/parser.hera
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const {
hoistRefDecs,
insertTrimmingSpace,
isFunction,
lastAccessInCallExpression,
literalValue,
modifyString,
processCoffeeInterpolation,
Expand Down Expand Up @@ -2281,8 +2282,8 @@ PropertyDefinitionList
# https://262.ecma-international.org/#prod-PropertyDefinition
PropertyDefinition
# NOTE: Added CoffeeScript {@id} -> {id: this.id} shorthand
__:ws At:at IdentifierReference:id ->
const value = [{...at, token: "this."}, id]
__:ws AtThis:at IdentifierReference:id ->
const value = [at, ".", id]
return {
type: "Property",
children: [ws, id, ": ", ...value],
Expand Down Expand Up @@ -2330,34 +2331,19 @@ PropertyDefinition
if (value.type === "Identifier") {
return {...value, children: [ws, ...value.children]}
}
// More complicated expressions gains `name:` prefix
// Look for last PropertyAccess like `.foo` or Identifier,
// before any calls like `(args)`.
let exp = value, children, i
do {
({children} = exp)
i = children.length-1
while (i >= 0 && (
children[i].type === "Call" ||
children[i].type === "NonNullAssertion" ||
children[i].type === "Optional"
)) i--
if (i < 0) return $skip
// Recurse into nested MemberExpression, e.g. from `x.y()`
} while (children[i].type === "MemberExpression" && (exp = children[i]))
const last = children[i]
const last = lastAccessInCallExpression(value)
if (!last) return $skip
let name
if (last.name) {
({name} = last)
} else if (last.type === "Index") {
if (last.type === "Index") {
// TODO: If `last` is a suitable string literal, could use it for `name`.
// TODO: Should use a ref instead of duplicating the expression.
name = {
type: "ComputedPropertyName",
children: last.children,
}
} else {
return $skip
({name} = last)
if (!name) return $skip
}
return {
type: "Property",
Expand Down Expand Up @@ -5277,6 +5263,32 @@ JSXAttribute
# NOTE: Adding ...foo shorthand for {...foo}
InsertInlineOpenBrace DotDotDot InlineJSXAttributeValue InsertCloseBrace &JSXAttributeSpace

# NOTE: @foo and @@foo shorthands
# for foo={this.foo} and foo={this.foo.bind(this)}
AtThis:at Identifier?:id InlineJSXCallExpressionRest*:rest &JSXAttributeSpace ->
const access = id && {
type: "PropertyAccess",
children: [".", id],
name: id,
}
const expr = module.processCallMemberExpression({
type: "CallExpression",
children: [ at, access, ...rest ],
})
const last = lastAccessInCallExpression(expr)
if (!last) return $skip
let name
if (last.type === "Index") {
return [
"{...{",
{...last, type: "ComputedPropertyName"},
": ", expr, "}}"
]
} else if (last.name) {
return [last.name, "={", expr, "}"]
}
return $skip

# NOTE: #id shorthand
"#" JSXShorthandString ->
return [ " ", "id=", $2 ]
Expand Down Expand Up @@ -5380,7 +5392,7 @@ InlineJSXCallExpression
type: "CallExpression",
children: [
$1,
{ type: "Call", children: [args] },
{ type: "Call", children: args },
...rest.flat()
],
})
Expand All @@ -5389,7 +5401,7 @@ InlineJSXCallExpression
type: "CallExpression",
children: [
$1,
{ type: "Call", children: [args] },
{ type: "Call", children: args },
...rest.flat()
],
})
Expand All @@ -5411,9 +5423,10 @@ InlineJSXCallExpressionRest
return "`" + $1.token.slice(1, -1).replace(/(`|\$\{)/g, "\\$1") + "`"
}
return $1
( OptionalShorthand / NonNullAssertion )? ExplicitArguments ->
if (!$1) return $2
return [ $1, ...$2 ]
( OptionalShorthand / NonNullAssertion )? ExplicitArguments:args ->
args = { type: "Call", children: args }
if (!$1) return args
return [ $1, args ]

# MemberExpression, with PrimaryExpression -> InlineJSXPrimaryExpression
InlineJSXMemberExpression
Expand Down
49 changes: 49 additions & 0 deletions test/jsx/attr.civet
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,55 @@ describe "JSX computed attribute names", ->
<Component {...{[`x${y}z`]: true}} />
"""

describe "JSX @ attribute names", ->
testCase """
@name
---
<Component @name />
---
<Component name={this.name} />
"""

testCase """
@method
---
<Component @method() />
---
<Component method={this.method()} />
"""

testCase """
@.name
---
<Component @.name />
---
<Component name={this.name} />
"""

testCase """
@[computed]
---
<Component @[computed] />
---
<Component {...{[computed]: this[computed]}} />
"""

testCase """
complex @name
---
<Component @method()?.value />
---
<Component value={this.method()?.value} />
"""

testCase """
@@ bind
---
<Component @@onClick />
---
<Component onClick={this.onClick.bind(this)} />
"""

describe "JSX id shorthand", ->
testCase """
without space
Expand Down

0 comments on commit 05e3c3c

Please sign in to comment.