From cb3cc61ce88630238b4e7d279581716d563c6c13 Mon Sep 17 00:00:00 2001 From: Mike Samuel Date: Fri, 27 Feb 2026 17:00:24 -0700 Subject: [PATCH] Re-enable backquoted string syntax. Make tagged strings with backquotes work. Since double quotes are normal for HTML strings, it's nice to be able to do: html`...` And it's also what LLMs reach for first. This was enabled before the most recent rework of `"""` syntax. Signed-off-by: Mike Samuel --- .../kotlin/lang/temper/astbuild/Grammar.kt | 2 +- .../lang/temper/astbuild/GrammarDiagrams.kt | 16 + .../lang/temper/astbuild/BuildTreeTest.kt | 17 + docs/for-users/.snippet-hashes.json | 12 +- .../temper-docs/docs/reference/syntax.md | 6 +- .../temper-docs/docs/reference/types.md | 2 +- .../snippet/syntax/StringExpr/snippet.svg | 842 ++++++++++++----- .../snippet/syntax/StringGroup/snippet.svg | 842 ++++++++++++----- .../syntax/StringGroupTagged/snippet.svg | 844 +++++++++++++----- 9 files changed, 1938 insertions(+), 645 deletions(-) diff --git a/astbuild/src/commonMain/kotlin/lang/temper/astbuild/Grammar.kt b/astbuild/src/commonMain/kotlin/lang/temper/astbuild/Grammar.kt index 2bcd1ac7..81117efe 100644 --- a/astbuild/src/commonMain/kotlin/lang/temper/astbuild/Grammar.kt +++ b/astbuild/src/commonMain/kotlin/lang/temper/astbuild/Grammar.kt @@ -1811,7 +1811,7 @@ internal abstract class DomainSpecificLanguage { val wordMatcher = Match(GrammarDoc.NonTerminal("Word"), emit = true) { it.tokenType == TokenType.Word } - val delimiterMatcher = patternMatcher("^\"+|'$", emit = false) + val delimiterMatcher = patternMatcher("^\"+|['`]$", emit = false) val litMatcher = Match( GrammarDoc.NonTerminal("Number"), emit = true, diff --git a/astbuild/src/commonMain/kotlin/lang/temper/astbuild/GrammarDiagrams.kt b/astbuild/src/commonMain/kotlin/lang/temper/astbuild/GrammarDiagrams.kt index 3edddc94..7170cab0 100644 --- a/astbuild/src/commonMain/kotlin/lang/temper/astbuild/GrammarDiagrams.kt +++ b/astbuild/src/commonMain/kotlin/lang/temper/astbuild/GrammarDiagrams.kt @@ -13,6 +13,22 @@ private val quotedStringGrammarDoc = GrammarDoc.Choice( GrammarDoc.Terminal("\""), ), ), + // `...` + GrammarDoc.Sequence( + listOf( + GrammarDoc.Terminal("`"), + stringContentGrammar(sourceCharacterText = "SourceCharacter - ('\\n', '\\r', '\\', '`')"), + GrammarDoc.Terminal("`"), + ), + ), + // '...' + GrammarDoc.Sequence( + listOf( + GrammarDoc.Terminal("'"), + stringContentGrammar(sourceCharacterText = "SourceCharacter - ('\\n', '\\r', '\\', '\\'')"), + GrammarDoc.Terminal("'"), + ), + ), // """... GrammarDoc.Sequence( listOf( diff --git a/astbuild/src/commonTest/kotlin/lang/temper/astbuild/BuildTreeTest.kt b/astbuild/src/commonTest/kotlin/lang/temper/astbuild/BuildTreeTest.kt index 6a30db88..94b85b79 100644 --- a/astbuild/src/commonTest/kotlin/lang/temper/astbuild/BuildTreeTest.kt +++ b/astbuild/src/commonTest/kotlin/lang/temper/astbuild/BuildTreeTest.kt @@ -1890,6 +1890,23 @@ class BuildTreeTest { """, ) + @Test + fun taggedTemplateStringWithBackquotes() = assertAst( + input = $$"f`a${ b }`", + startProduction = "Expr", + wantJson = """ + [ "Call", [ + [ "Value", [ "stringExpr", "Function" ] ], + [ "RightName", "f" ], + [ "Value", [ true, "Boolean" ] ], + [ "Value", [ "a", "String" ] ], + [ "Value", [ "interpolate", "Symbol" ] ], + [ "RightName", "b" ], + ] + ] + """, + ) + @Test fun taggedTemplateStringWithStatementFragments() = assertAst( input = $$""" diff --git a/docs/for-users/.snippet-hashes.json b/docs/for-users/.snippet-hashes.json index dc7e5524..96253f95 100644 --- a/docs/for-users/.snippet-hashes.json +++ b/docs/for-users/.snippet-hashes.json @@ -466,14 +466,14 @@ "build-user-docs/build/snippet/syntax/StmtBlock/snippet.js": "209DD7FC91CC53AB58A935DEB40D1B3A8439363E86DDF0EBC5C376619051A69C", "build-user-docs/build/snippet/syntax/StmtBlock/snippet.md": "F758B71E7E310E302F97CE0C2765A0D71F0EC8DC930F6E2BE00A578D6F238C1B", "build-user-docs/build/snippet/syntax/StmtBlock/snippet.svg": "FE48B62351417FE4F552AEDE3A742D504FC411FD54F0C6BD38C7D6529B482070", - "build-user-docs/build/snippet/syntax/StringExpr/snippet.js": "F785AC5D9F918AA83A758731646509DD44659B0054E79021B84B38941D6E6189", - "build-user-docs/build/snippet/syntax/StringExpr/snippet.md": "7D15C0D4170FE7BB2E1A5908F618C7B429833AD6469575B754276BB6323EDEE8", + "build-user-docs/build/snippet/syntax/StringExpr/snippet.js": "4770CDAAA87C6C36A07454C66FBBF8E3829E591AEB7AA0DFCFA2FAA17AAC98BF", + "build-user-docs/build/snippet/syntax/StringExpr/snippet.md": "78DF29E9563776F84A5F85FC2A1E38103842C31EAAF8826BFFFD5861D356AA39", "build-user-docs/build/snippet/syntax/StringExpr/snippet.svg": "3670EC808C1342C3A7508470AA4AB8EAF89EB625446E3FED37F14FB76D074C24", - "build-user-docs/build/snippet/syntax/StringGroup/snippet.js": "987C07471633DE9E46C2382FAD11E7735F98D0F05CA076D0A36AB105ECB94B76", - "build-user-docs/build/snippet/syntax/StringGroup/snippet.md": "EE3A5FCD0275248E2BC93C90E435BD9B22589C10E7CF44D29291BD10CDB29A29", + "build-user-docs/build/snippet/syntax/StringGroup/snippet.js": "F304A1A5A3E01A44A77C3AC0AA61D9BC42E428990D3460AB5C29C9282FD1CBF3", + "build-user-docs/build/snippet/syntax/StringGroup/snippet.md": "F001EEFC8FE52D0406AE204B030F1A57B4DF6DBA64200BBE0C122634B022C70A", "build-user-docs/build/snippet/syntax/StringGroup/snippet.svg": "23A0A3B8A6D2D84410D7AB46AD95376BC40C997F7DCBC697ABB53EB4B8DD7BCE", - "build-user-docs/build/snippet/syntax/StringGroupTagged/snippet.js": "CD4027EB4CAA1303F07B4006EFC32A5E69188D8F514F95C2596FE459AFFDFB9C", - "build-user-docs/build/snippet/syntax/StringGroupTagged/snippet.md": "4D017E9EA6DF71BA1190FA13433D6D956A540790A3ABF24625E1D69C09C92271", + "build-user-docs/build/snippet/syntax/StringGroupTagged/snippet.js": "751CC60F4215E3ECD44C792B38739A1FCED69529ED53CB353FDEEED19C380376", + "build-user-docs/build/snippet/syntax/StringGroupTagged/snippet.md": "0568E628A52CB9D0E58393FCC06A10625D73E55EA32662C0B8AAAE6FF6C423D8", "build-user-docs/build/snippet/syntax/StringGroupTagged/snippet.svg": "161D42AAFCF4E830B79DFCABA7F7C00792D4494A313EBB8CCA96E6C10CF98275", "build-user-docs/build/snippet/syntax/StringHole/snippet.js": "673292D00CD16C6B68A788046922FC86D5F49E6D2D37BA14855FE63DD58A1C85", "build-user-docs/build/snippet/syntax/StringHole/snippet.md": "78EA49B02A3D91C64342B5E4D99B4217E202EAA7F0FDD033E937A8D11A03F54F", diff --git a/docs/for-users/temper-docs/docs/reference/syntax.md b/docs/for-users/temper-docs/docs/reference/syntax.md index 96b6bf7d..5fd6b596 100644 --- a/docs/for-users/temper-docs/docs/reference/syntax.md +++ b/docs/for-users/temper-docs/docs/reference/syntax.md @@ -452,7 +452,7 @@ Ruby-style block functions (`f { ... }`) and flow-control like ### Syntax for *StringExpr* -![StringExpr := "\u0022" (SourceCharacter - (\\n\, \\r\, \\\, \"\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* "\u0022" | "\u0022\u0022\u0022" (Content line starting with `"`: "LineBreak" indentation (Ignored margin quote: "\u0022") ((SourceCharacter - (\\\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* | "{:" StatementFragment ":}"))* "LineBreak"](../snippet/syntax/StringExpr/snippet.svg) +![StringExpr := "\u0022" (SourceCharacter - (\\n\, \\r\, \\\, \"\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* "\u0022" | "`" (SourceCharacter - (\\n\, \\r\, \\\, \`\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* "`" | "\" (SourceCharacter - (\\n\, \\r\, \\\, \\\\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* "\" | "\u0022\u0022\u0022" (Content line starting with `"`: "LineBreak" indentation (Ignored margin quote: "\u0022") ((SourceCharacter - (\\\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* | "{:" StatementFragment ":}"))* "LineBreak"](../snippet/syntax/StringExpr/snippet.svg) @@ -1457,7 +1457,7 @@ There are two kinds of match cases: run-time type checks that use keyword `is`, #### Syntax for *StringGroup* -![StringGroup := "\u0022" (SourceCharacter - (\\n\, \\r\, \\\, \"\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* "\u0022" | "\u0022\u0022\u0022" (Content line starting with `"`: "LineBreak" indentation (Ignored margin quote: "\u0022") ((SourceCharacter - (\\\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* | "{:" StatementFragment ":}"))* "LineBreak"](../snippet/syntax/StringGroup/snippet.svg) +![StringGroup := "\u0022" (SourceCharacter - (\\n\, \\r\, \\\, \"\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* "\u0022" | "`" (SourceCharacter - (\\n\, \\r\, \\\, \`\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* "`" | "\" (SourceCharacter - (\\n\, \\r\, \\\, \\\\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* "\" | "\u0022\u0022\u0022" (Content line starting with `"`: "LineBreak" indentation (Ignored margin quote: "\u0022") ((SourceCharacter - (\\\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* | "{:" StatementFragment ":}"))* "LineBreak"](../snippet/syntax/StringGroup/snippet.svg) @@ -1467,7 +1467,7 @@ There are two kinds of match cases: run-time type checks that use keyword `is`, #### Syntax for *StringGroupTagged* -![StringGroupTagged := (Whether escape sequences are expanded is up to the tag: "\u0022" (SourceCharacter - (\\n\, \\r\, \\\, \"\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* "\u0022" | "\u0022\u0022\u0022" (Content line starting with `"`: "LineBreak" indentation (Ignored margin quote: "\u0022") ((SourceCharacter - (\\\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* | "{:" StatementFragment ":}"))* "LineBreak")](../snippet/syntax/StringGroupTagged/snippet.svg) +![StringGroupTagged := (Whether escape sequences are expanded is up to the tag: "\u0022" (SourceCharacter - (\\n\, \\r\, \\\, \"\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* "\u0022" | "`" (SourceCharacter - (\\n\, \\r\, \\\, \`\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* "`" | "\" (SourceCharacter - (\\n\, \\r\, \\\, \\\\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* "\" | "\u0022\u0022\u0022" (Content line starting with `"`: "LineBreak" indentation (Ignored margin quote: "\u0022") ((SourceCharacter - (\\\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* | "{:" StatementFragment ":}"))* "LineBreak")](../snippet/syntax/StringGroupTagged/snippet.svg) diff --git a/docs/for-users/temper-docs/docs/reference/types.md b/docs/for-users/temper-docs/docs/reference/types.md index d1b5201f..6463fe42 100644 --- a/docs/for-users/temper-docs/docs/reference/types.md +++ b/docs/for-users/temper-docs/docs/reference/types.md @@ -1590,7 +1590,7 @@ A *String* is a chunk of textual content. -![StringExpr := "\u0022" (SourceCharacter - (\\n\, \\r\, \\\, \"\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* "\u0022" | "\u0022\u0022\u0022" (Content line starting with `"`: "LineBreak" indentation (Ignored margin quote: "\u0022") ((SourceCharacter - (\\\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* | "{:" StatementFragment ":}"))* "LineBreak"](../snippet/syntax/StringExpr/snippet.svg) +![StringExpr := "\u0022" (SourceCharacter - (\\n\, \\r\, \\\, \"\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* "\u0022" | "`" (SourceCharacter - (\\n\, \\r\, \\\, \`\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* "`" | "\" (SourceCharacter - (\\n\, \\r\, \\\, \\\\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* "\" | "\u0022\u0022\u0022" (Content line starting with `"`: "LineBreak" indentation (Ignored margin quote: "\u0022") ((SourceCharacter - (\\\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* | "{:" StatementFragment ":}"))* "LineBreak"](../snippet/syntax/StringExpr/snippet.svg) diff --git a/docs/for-users/temper-docs/docs/snippet/syntax/StringExpr/snippet.svg b/docs/for-users/temper-docs/docs/snippet/syntax/StringExpr/snippet.svg index 2f9ea7f2..9d68a1a3 100644 --- a/docs/for-users/temper-docs/docs/snippet/syntax/StringExpr/snippet.svg +++ b/docs/for-users/temper-docs/docs/snippet/syntax/StringExpr/snippet.svg @@ -1,4 +1,4 @@ -StringExpr := "\u0022" (SourceCharacter - (\\n\, \\r\, \\\, \"\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* "\u0022" | "\u0022\u0022\u0022" (Content line starting with `"`: "LineBreak" indentation (Ignored margin quote: "\u0022") ((SourceCharacter - (\\\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* | "{:" StatementFragment ":}"))* "LineBreak" +StringExpr := "\u0022" (SourceCharacter - (\\n\, \\r\, \\\, \"\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* "\u0022" | "`" (SourceCharacter - (\\n\, \\r\, \\\, \`\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* "`" | "\" (SourceCharacter - (\\n\, \\r\, \\\, \\\\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* "\" | "\u0022\u0022\u0022" (Content line starting with `"`: "LineBreak" indentation (Ignored margin quote: "\u0022") ((SourceCharacter - (\\\) | EscapeSequence | (UTF-16 Code Unit: "\\u" Hex Hex Hex Hex) | (Unicode Scalar Values: "\\u{" HexDigits+(",") "}") | (Interpolation: "\u0024{" Expr "}"))* | "{:" StatementFragment ":}"))* "LineBreak"