-
Notifications
You must be signed in to change notification settings - Fork 26.9k
Support untagged template literals in expressions #59230
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
Conversation
42404fc to
44704f0
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't bother implementing the downleveling code here, because all the browsers we support also support template literals natively.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense. There wasn't downleveling here before anyway right? (even with tagged template literals)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think for the tagged ones we only downleveled "known" ones like $localize and not in a standards-based way.
|
As a follow up, we should make sure to update the docs: https://angular.dev/guide/templates/expression-syntax#unsupported-literals |
|
@crisbeto whether it will support the class with template literals? <div class=`h-12 mt-4 ${cond() ? 'flex flex-col' : 'flex flex-row'} ${cond1() ? 'p-4' : 'p-2'}`></div> |
|
@sheikalthaf This PR adds the support for template literals within angular expressions. Your example isn't an angular expression. |
|
@JeanMeche then this will work right? <div class="{{`h-12 mt-4 ${cond() ? 'flex flex-col' : 'flex flex-row'} ${cond1() ? 'p-4' : 'p-2'}`}}"></div>I'm thinking of using this pattern alternative to |
44704f0 to
e7fbd77
Compare
devversion
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM overall. Nice work!
I don't see any reasons why we wouldn't want to support this, as it's aligning us even further with TS/JS.
A few minor nits on the lexer portion, but the rest looks great
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is there an expected error?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's because TS doesn't know that advance mutated this.peek so it narrows the value to chars.$$ and doesn't allow the comparison to chars.$LBRACE.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be worth a TODO
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Kinda would have wished that we could advance through the template literal within the scanTemplateLiteralPart, but it's probably not possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess that's a related reason why we also have this.tokens now exposed, and .push-ed here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah exactly, the scanner isn't really set up to produce more than one token at a time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible/reasonable to keep it one token at a time (to not introduce some extra complexity around following the order etc.) and just basing the scans on state? (like you did with brace depth etc.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could do it with a flag, but I feel like that'll be less readable than just producing two tokens at once since we'd basically split up this logic across two places instead of keeping it together. I'm not too concerned about the tokens being exposed, because the lexer isn't exposed anywhere.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I felt like it was a little hard to reason about the order if are returning a token during scan, and suddenly we could push additional tokens in between. But it's not a super big of a deal. I do see your concerns as well.
Will defer to others here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OOC: Do you know why this exists if we have the serializer already?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm good question. My guess is because we use the unparser in tests and it has a little bit of extra test-specific functionality around recording the spans. We may be able to consolidate them a bit though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense. There wasn't downleveling here before anyway right? (even with tagged template literals)
Reworks the lexer's scanner to produce more than one token at a time. This can be useful for the cases where one token means the end of another one. Also cleans up the scanner by making all non-essential methods private and using strict equality everywhere.
Reworks the lexer to produce tokens for template literal expressions.
Updates the compiler to parse the template literal tokens into the new `TemplateLiteral` and `TemplateLiteralElement` AST nodes.
Makes the following cleanups in the output AST: * The `TemplateLiteral` and `TemplateLiteralElement` nodes have been renamed to `TemplateLiteralExpr` and `TemplateLiteralElementExpr` respectively for consistency and to avoid overlaps with the expression AST nodes. * The `TemplateLiteralExpr` and `TemplateLiteralElementExpr` have been refactored to be `Expression`s for correctness. This involves updating some existing code. * The `TaggedTemplateExpr` has been renamed to `TaggedTemplateLiteralExpr` for consistency.
Updates the translators that convert expression ASTs to account for template literals.
Updates the compiler to support untagged template literals inside of the expression syntax (e.g. ``hello ${world}``).
e7fbd77 to
6af7ff8
Compare
crisbeto
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feedback has been addressed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think for the tagged ones we only downleveled "known" ones like $localize and not in a standards-based way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah exactly, the scanner isn't really set up to produce more than one token at a time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's because TS doesn't know that advance mutated this.peek so it narrows the value to chars.$$ and doesn't allow the comparison to chars.$LBRACE.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm good question. My guess is because we use the unparser in tests and it has a little bit of extra test-specific functionality around recording the spans. We may be able to consolidate them a bit though.
|
This PR was merged into the repository by commit fe8a683. The changes were merged into the following branches: main |
Reworks the lexer to produce tokens for template literal expressions. PR Close #59230
Updates the compiler to parse the template literal tokens into the new `TemplateLiteral` and `TemplateLiteralElement` AST nodes. PR Close #59230
Makes the following cleanups in the output AST: * The `TemplateLiteral` and `TemplateLiteralElement` nodes have been renamed to `TemplateLiteralExpr` and `TemplateLiteralElementExpr` respectively for consistency and to avoid overlaps with the expression AST nodes. * The `TemplateLiteralExpr` and `TemplateLiteralElementExpr` have been refactored to be `Expression`s for correctness. This involves updating some existing code. * The `TaggedTemplateExpr` has been renamed to `TaggedTemplateLiteralExpr` for consistency. PR Close #59230
Updates the translators that convert expression ASTs to account for template literals. PR Close #59230
JoostK
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Paul had asked for me to take a look at this, so leaving a few after-merge comments. The lexer does feel a bit awkward with the implicit multi-token production, but it doesn't hurt readability much so no big deal, I think
| const expression = i < ast.expressions.length ? ast.expressions[i] : null; | ||
| if (expression !== null) { | ||
| this.visit(expression, context); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit:
| const expression = i < ast.expressions.length ? ast.expressions[i] : null; | |
| if (expression !== null) { | |
| this.visit(expression, context); | |
| } | |
| if (i < ast.expressions.length) { | |
| this.visit(ast.expressions[i], context); | |
| } |
| return this.parseNoInterpolationTemplateLiteral(start); | ||
| } else if (this.next.isTemplateLiteralPart()) { | ||
| return this.parseTemplateLiteral(); | ||
| } else if (this.next.isString() && this.next.kind === StringTokenKind.Plain) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the Plain condition necessary here? It seems unexpected to me, seemingly allowing some strings to be reported as error?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's been some time since I wrote it, but I think the idea was to avoid confusion with the template literals in case we introduce some other string token. AFAIK in practice we can't actually hit that code path without it being StringTokenKind.Plain.
| this.advance(); | ||
| const expression = this.parsePipe(); | ||
| if (expression instanceof EmptyExpr) { | ||
| this.error('Template literal interpolation cannot be empty'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This error path doesn't collect the empty expression into expressions, yet a TemplateLiteral is being returned. I suspect this may cause the invariant elements.length === expressions.length + 1 to be violated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm potentially, but I think that whatever is consuming the result will report the error.
| const expression = i < ast.expressions.length ? ast.expressions[i] : null; | ||
| if (expression !== null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: same here for inlining the condition into the if
| const expression = i < ast.expressions.length ? ast.expressions[i] : null; | ||
| if (expression !== null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The nit here too
…9230) Reworks the lexer's scanner to produce more than one token at a time. This can be useful for the cases where one token means the end of another one. Also cleans up the scanner by making all non-essential methods private and using strict equality everywhere. PR Close angular#59230
Reworks the lexer to produce tokens for template literal expressions. PR Close angular#59230
Updates the compiler to parse the template literal tokens into the new `TemplateLiteral` and `TemplateLiteralElement` AST nodes. PR Close angular#59230
…59230) Makes the following cleanups in the output AST: * The `TemplateLiteral` and `TemplateLiteralElement` nodes have been renamed to `TemplateLiteralExpr` and `TemplateLiteralElementExpr` respectively for consistency and to avoid overlaps with the expression AST nodes. * The `TemplateLiteralExpr` and `TemplateLiteralElementExpr` have been refactored to be `Expression`s for correctness. This involves updating some existing code. * The `TaggedTemplateExpr` has been renamed to `TaggedTemplateLiteralExpr` for consistency. PR Close angular#59230
) Updates the translators that convert expression ASTs to account for template literals. PR Close angular#59230
…gular#59230) Updates the compiler to support untagged template literals inside of the expression syntax (e.g. ``hello ${world}``). PR Close angular#59230
|
This issue has been automatically locked due to inactivity. Read more about our automatic conversation locking policy. This action has been performed automatically by a bot. |
Updates the compiler to allow users to use untagged template literals inside of our expression syntax (e.g.
hello ${world}) which makes string concatenation a bit simpler and more convenient. Includes the following changes:refactor(compiler): allow lexer to produce multiple tokens
Reworks the lexer's scanner to produce more than one token at a time. This can be useful for the cases where one token means the end of another one.
Also cleans up the scanner by making all non-essential methods private and using strict equality everywhere.
refactor(compiler): tokenize template literals
Reworks the lexer to produce tokens for template literal expressions.
refactor(compiler): produce AST for template literals
Updates the compiler to parse the template literal tokens into the new
TemplateLiteralandTemplateLiteralElementAST nodes.refactor(compiler): clean up tagged templates in output AST
Makes the following cleanups in the output AST:
TemplateLiteralandTemplateLiteralElementnodes have been renamed toTemplateLiteralExprandTemplateLiteralElementExprrespectively for consistency and to avoid overlaps with the expression AST nodes.TemplateLiteralExprandTemplateLiteralElementExprhave been refactored to beExpressions for correctness. This involves updating some existing code.TaggedTemplateExprhas been renamed toTaggedTemplateLiteralExprfor consistency.refactor(compiler-cli): handle template literals in ngtsc
Updates the translators that convert expression ASTs to account for template literals.
feat(compiler): support untagged template literals in expressions
Updates the compiler to support untagged template literals inside of the expression syntax (e.g.
hello ${world}).