diff --git a/civet.dev/cheatsheet.md b/civet.dev/cheatsheet.md index 5c3c70fb..515b9971 100644 --- a/civet.dev/cheatsheet.md +++ b/civet.dev/cheatsheet.md @@ -1253,7 +1253,9 @@ Implicit elements must start with `id` or `class` shorthand (`#` or `.`).
Civet
Civet
Civet -
Civet +
Civet +
Civet +
Civet
Civet
Civet diff --git a/source/lib.js b/source/lib.js index c6b70829..06f8e893 100644 --- a/source/lib.js +++ b/source/lib.js @@ -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)) { @@ -665,6 +685,7 @@ module.exports = { hoistRefDecs, insertTrimmingSpace, isFunction, + lastAccessInCallExpression, literalValue, modifyString, processCoffeeInterpolation, diff --git a/source/parser.hera b/source/parser.hera index b4cca594..fad9da92 100644 --- a/source/parser.hera +++ b/source/parser.hera @@ -23,6 +23,7 @@ const { hoistRefDecs, insertTrimmingSpace, isFunction, + lastAccessInCallExpression, literalValue, modifyString, processCoffeeInterpolation, @@ -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], @@ -2330,26 +2331,10 @@ 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 = { @@ -2357,7 +2342,8 @@ PropertyDefinition children: last.children, } } else { - return $skip + ({name} = last) + if (!name) return $skip } return { type: "Property", @@ -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 ] @@ -5380,7 +5392,7 @@ InlineJSXCallExpression type: "CallExpression", children: [ $1, - { type: "Call", children: [args] }, + { type: "Call", children: args }, ...rest.flat() ], }) @@ -5389,7 +5401,7 @@ InlineJSXCallExpression type: "CallExpression", children: [ $1, - { type: "Call", children: [args] }, + { type: "Call", children: args }, ...rest.flat() ], }) @@ -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 diff --git a/test/jsx/attr.civet b/test/jsx/attr.civet index 633ea588..51e2752b 100644 --- a/test/jsx/attr.civet +++ b/test/jsx/attr.civet @@ -197,6 +197,55 @@ describe "JSX computed attribute names", -> """ +describe "JSX @ attribute names", -> + testCase """ + @name + --- + + --- + + """ + + testCase """ + @method + --- + + --- + + """ + + testCase """ + @.name + --- + + --- + + """ + + testCase """ + @[computed] + --- + + --- + + """ + + testCase """ + complex @name + --- + + --- + + """ + + testCase """ + @@ bind + --- + + --- + + """ + describe "JSX id shorthand", -> testCase """ without space