For AI agents: the complete documentation index is available at /tc39-atlas/llms.txt, the full documentation bundle is available at /tc39-atlas/llms-full.txt, and this page is available as Markdown at /tc39-atlas/proposals/year/pending/proposal-improve-template-literals.md.
  • 简体中文
  • Improved Escapes for Template Literals S1

    中文标题:模板字面量的改进转义

    提案概览
    提案速览

    该提案旨在通过允许灵活的分隔符(例如 @``、@``` 等)和可控的插值语法,提供一种在 JavaScript 中创建能够包含任意文本而无需转义的原始字符串字面量的方式。它解决了在模板字面量中转义反引号和 ${} 的不便。

    Note

    以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。

    改进模板字面量

    简要总结。

    let query = `
        select * 
        from \`users\` 
        where \`name\` = ?
    `

    转义很烦人,而且 String.rawString.dedent 在这种情况下也无济于事。我们需要语法来解决这个问题:

    // 语法待定,仅使用 @sken130 的草稿作为演示
    let query = @``
        select * 
        from `users` 
        where `name` = ?
        ``

    状态

    • 阶段:1
    • 发起人:HE Shi-Jun (hax)
    • 作者:@hax, @sken130

    动机

    JavaScript 缺少一种通用的方式来创建可以包含实际上任意文本的简单字符串字面量。使用带有 String.raw 内置标签函数的模板字面量可以避免大多数转义,但不幸的是不能包含 `${,因为它们是模板字面量的分隔符。这阻止了轻松地在其中包含其他编程语言(特别是 JavaScript 本身)和文本格式(例如 Markdown)的字面量。

    目前所有在 JavaScript 中形成这些字面量的方法都强制用户手动转义内容,或者使用一些其他技巧。此时编辑可能非常烦人,因为无法避免转义,并且只要内容中出现就必须处理。对于同时包含反斜杠和反引号的文本,或包含反引号的多行文本,这尤其痛苦。

    问题的关键在于我们所有的字符串都有固定的开始/结束分隔符。只要如此,我们就必须使用转义或替换机制,因为字符串内容可能需要在其内容中指定该结束分隔符。当分隔符 ` 在许多语言中很常见时,这尤其成问题。

    为了解决这个问题,此提案允许灵活的开始和结束分隔符,以便它们总能以不与字符串内容冲突的方式设置。

    核心目标

    1. 提供一种机制,允许用户提供所有字符串值,而无需任何转义序列或替换。因为所有字符串都必须无需转义序列或替换即可表示,所以必须始终允许用户指定保证不与任何文本内容冲突的分隔符。
    2. 以相同方式支持插值。如上所述,因为所有字符串都必须无需转义或替换即可表示,所以必须始终允许用户指定保证不与任何文本内容冲突的插值分隔符。
    3. 像当前带标签的模板字面量那样支持标签函数。

    额外目标(如果可能)

    1. 多行字符串字面量在代码中应看起来美观,并且不应使编译单元内的缩进看起来奇怪。重要的是,本身没有缩进的字面量值不应被迫占据文件的第一列,因为那会破坏代码的流动,并且看起来与周围代码不对齐。此行为应易于覆盖,同时保持字面量清晰易读。
      • 注意,此目标也可以通过 String.dedent 提案实现,但语法解决方案可能具有更好的人体工程学和其他优势。
    2. 目前使用的模板字面量应能轻松迁移到新语法。重要的是,如果模板字面量不需要呈现 ${ 字符,则不应被迫更改插值语法。
    3. 拥有类似 Markdown 信息字符串 的机制会很好,工具可以利用它(例如,用于语法高亮)。
    4. 拥有注释机制会很好。(尽管人们可能会滥用插值来实现此目的)
    5. 拥有在指定位置启用转义的机制会很好。(Swift 支持此功能。)
    6. 要嵌套 JS 代码,如果语法允许外部短分隔符、内部长分隔符会更好。因此,当您编写嵌套的 JS 代码时,无需返回开头更改分隔符。这也有助于 LLM AI 生成 JS 代码,因为当前的 LLM 无法回溯并修改它们已经输出的代码。

    可能的解决方案

    有许多可能的语法选项,这里文档风格语法、Swift/Rust 风格 (#`raw string`#) 语法、Markdown 风格语法等。发起人计划在提案批准为阶段 1 后研究不同的语法选项。

    目前,让我们暂时使用 @sken130 的草稿,它深受 C# 11 原始字符串字面量语法启发。

    1. 字符串序列应以 @ 和至少(可以更多)2 个反引号字符开头,并以相同数量的反引号字符(不带 @)结尾。

    const str = @``
    I am a string
    ``

    2. 这些模式不需要转义

    显然,诸如 @、双引号、单引号和反斜杠等字符不需要转义。

    const mysqlQuery = @``
        SELECT * FROM `Strange table name` where `strange column name` = 'abcde';
        ``
    const str = @``
    I would like to request "2 leave days" that start from '11/26/2022' and end on '11/27/2022'.
    \_/
    If any enquiry, please send email to ken@example.com
    ``

    @、"、' 和 \ 字符将按原样存储在 str 中,如源代码中可见,无需 "、'、\.

    1 个反引号字符也不需要转义。

    const str = @``
    I would like to request `2 leave days` that ......
    ``

    并且

    @`

    序列也不需要转义,因为它不等于结束分隔符。

    3. 问题来了:如果我们想在内容中嵌入 2 个或更多反引号字符而不转义怎么办?在这种情况下,原始字符串字面量需要用更多反引号字符开始和结束:

    嵌入 2 个反引号字符需要用 @ 和 3 个反引号开始,并用 3 个反引号结束:

    const javaScriptTutorial = @```
       const emptyString = ``  // Yay, backtick quotes can be an empty string too
       ```

    (2 个反引号 ``` 字符将按原样表示,无需转义)

    嵌入 3 个反引号字符(一个常见示例是在 JavaScript 中嵌入 Markdown)需要用 @ 和 4 个反引号开始,并用 4 个反引号结束:

    const markdownExample = @````
        ```json
        {
          "firstName": "John",
          "lastName": "Smith",
          "age": 25
        }
        ```
        ````

    (3 个反引号 ``` 字符将按原样表示,无需转义)

    如果我们想在内容中表示 4 个反引号而不转义,我们需要用 5 个反引号字符来界定原始字符串,依此类推。 在此设计中,只要我们用更多反引号字符开始和结束来界定原始字符串,我们就可以在内容中放置任意数量的反引号。 这种语法不应该笨拙,因为此类字符串很少见(尽管我们想涵盖所有边缘情况)。

    4. 缩进 - 结束分隔符(```、或 ````、或 `````、...)左侧的任何空白将从字符串字面量中移除

      const markdownExample = @````
         |```json
         |{
         |  "firstName": "John",
         |  "lastName": "Smith",
         |  "age": 25
         |}
         |```
         |
          ````

    注意:| 字符实际上不在字符串中,它们用于说明字符串如何缩进以及 | 处及其左侧的空白不被捕获在字符串中。

    它相当于

        const markdownExample = @````
    ```json
    {
      "firstName": "John",
      "lastName": "Smith",
      "age": 25
    }
    ```
    
    ````

    结束分隔符左侧的任何字符将触发编译错误:

        const markdownExample = @````
            ```json
            {
              "firstName": "John",
              "lastName": "Smith",
              "age": 25
            }
            ```
           a   // 字符 a 在这里是非法的,应该给出编译错误而不是忽略它。
        b   ````  // 字符 b 在这里也是非法的,结束分隔符必须独占一行。应该给出编译错误而不是忽略它。

    5. 缩进 - 缩进空白必须一致

    如果结束分隔符左侧有 8 个空格,则内容中的所有行必须以 8 个空格开头,而不是制表符。它们可以在初始 8 个空格之后有制表符或空格字符。

    如果结束分隔符左侧有 2 个制表符,则内容中的所有行必须以 2 个制表符开头,而不是空格字符。它们可以在初始 2 个制表符之后有空格或制表符字符。

    6. 插值 - 你知道的

    let s = @``
      My name is ${myName}
      ``

    7. 如果我想将 ${myName} 作为内容的一部分而不是插值怎么办?

    答案借鉴自 C# 11 原始字符串字面量,即在开始分隔符处包含更多 @ 字符。

    开始分隔符处的 @ 字符数量将控制开始插值需要多少个 $ 字符:

    let s1 = @@``
      My name is ${myName}
      ``    // 没有插值,${myName} 现在按原样存储在字符串中
    let s1 = @@``
      My name is $${myName}
      ``    // 插值将发生
    const myName = "Ann"
    console.log(@@``
      JavaScript tutorial: If you write console.log(`my name is ${myName}`), it will print "my name is $${myName}" in the results (not including "")
    ``)

    将打印

    JavaScript tutorial: If you write console.log(`my name is ${myName}`), it will print "my name is Ann" in the results (not including "")

    更多示例

    @@@```
      My name is $${myName}
      ```    // 没有插值,${myName} 现在按原样存储在字符串中
    @@@```
      My name is $$${myName}
      ```    // 插值将发生

    8. 带标签的字符串

    let result = tag@``
      should also support tag function
      ``

    9. 多行原始字符串字面量的更多规则

    • 开始和结束引号字符必须在不同的行上。
    • 开始引号后面同一行上的空白被忽略。
    • 开始引号后面同一行上的任何非空白字符(注释除外)都是非法的,并将被视为未终止的单行原始字符串字面量。
    • 开始引号下方仅包含空白的行被包含在字符串字面量中。

    10. 为什么选择这种语法

    为什么不直接用 ``` 开始

    因为 ``` 不是完全向后兼容的。请参阅 https://github.com/tc39/proposal-string-dedent/issues/40、https://github.com/tc39/proposal-string-dedent/issues/8、https://gist.github.com/michaelficarra/70ce798feb25fdc91508f387190053a1,以及我在本提案之前的回复 https://es.discourse.group/t/raw-string-literals-that-can-contain-any-arbitrary-text-without-the-need-for-special-escape-sequences/1757/2

    为什么使用 @ 字符作为开始分隔符

    因为 @``、@```、@````、... 在 JavaScript 中以前从未是合法语法,因此没有向后兼容性问题。

    我们不能使用 $``、$```、...,因为 $ 是有效的变量标识符(jQuery)。

    为什么不直接使用其他分隔符字符/序列,而非反引号、双引号或单引号?
    • 如果分隔符字符/序列是固定的,无论您选择什么分隔字符/序列,都无法在不转义的情况下在内容中嵌入结束分隔符字符/序列。此功能的目标之一是在不转义的情况下嵌入任意文本,为此,开始和结束分隔符序列必须是灵活的。
    • 我们可能使用其他灵活的序列而不是 @``,但不确定这是否会浪费未来其他增强功能的可能语法空间。

    11. 原始字符串字面量的用例

    (a) MySQL 查询

    使用 MySQL 时,以下情况很常见

    const searchUser = () => {
        const query = `select *
    from \`users\`
    where \`name\` = ?`
    }

    允许无需转义的原始字符串表示,并带有适当的缩进,将使其维护起来不那么麻烦且更不容易出错。

    const searchUser = () => {
        const query = @``
            select *
            from `users`
            where `name` = ?
            ``
        ......
    }
    (b) Markdown 嵌入或生成

    没有原始字符串字面量功能,我们必须这样做(必须转义每个反引号):

    const generateMarkdown = () => {
        const markdownExample = `\`\`\`json
    {
      "firstName": "John",
      "lastName": "Smith",
      "age": 25
    }
    \`\`\`
    `
        doSomething(markdownExample)  // 在某个地方输出/处理 markdown
    }

    或者这样做(必须转义每个双引号):

    const generateMarkdown = () => {
        const markdownExample = [
            "```json",
            "{",
            "  \"firstName\": \"John\",",
            "  \"lastName\": \"Smith\",",
            "  \"age\": 25",
            "}",
            "```"
        ].join("\n")
    
        doSomething(markdownExample)  // 在某个地方输出/处理 markdown
    }

    有了原始字符串字面量功能,我们不需要转义任何内容:

    const generateMarkdown = () => {
        const markdownExample = @````
            ```json
            {
              "firstName": "John",
              "lastName": "Smith",
              "age": 25
            }
            ```
            ````
        doSomething(markdownExample)  // 在某个地方输出/处理 markdown
    }
    (c) 当字符串插值也必需时的正则表达式

    没有此提案,表示 "raw" 正则表达式的最接近方式是这样:

    new RegExp(String.raw`someRegex\b${processedSearchKeyword}\s*(?:\(?HKD\)?):?\s*`, "i")

    即便如此,正则表达式本身已经包含大量反斜杠。

    如果正则表达式模式本身包含几个反引号和要搜索的 "${xxx}",那么用眼睛辨别哪些反斜杠真正在正则表达式中、哪些仅用于在 JavaScript 侧转义字符将有点容易出错。

    (d) XML/HTML/源代码嵌入或生成

    如果我们处理一些 HTML、XML,甚至源代码生成器(例如 JavaScript、Linux 命令),我们经常需要同时包含特殊字符("、'、$、`)。能够以未转义的方式编写它们并带有清晰的缩进将有助于提高可读性和可维护性。

    如果我们处理一些 HTML、XML,甚至源代码生成器(例如 JavaScript、Linux 命令、PowerShell 命令),我们经常需要同时包含特殊字符("、'、$、`)。能够以未转义的方式编写它们并带有清晰的缩进将有助于提高可读性和可维护性。

    有人可能会争辩说反引号的使用并不频繁。但事实并非如此。如今,许多编程语言为了避开双引号转义而采用反引号字符,因此反引号字符变得越来越普遍。

    原始字符串字面量可能无法解决所有问题,但至少可以结束选择与开始和结束分隔符以及插值分隔符相关的转义字符的漫长追逐。

    (e) 涉及 MySQL 查询、Markdown、XML 的单元测试

    单元测试也是我们希望在源代码中嵌入这些内容而不是单独文件中的常见地方。

    如 (a) 和 (b) 所述,原始字符串字面量将提高清晰度。

    (f) 一个涉及嵌套 Markdown 和 JavaScript 本身的复杂示例
    let promptForLLM = `
    You are a AI assistant to give advice to programmers,
    for example, given the code:
    \`\`\`js
    let s1 = "This is a\\n"
      + "string across\\n"
      + "multiple lines.\\n"
    let a = 1, b = 2
    let s2 = "a + b = " + (a + b)
    \`\`\`
    you would output the advice:
    \`\`\`\`markdown
    ## Advice
    It's more readable to use template literal to replace
    the string concatenation.
    
    ## Original code
    \`\`\`js
    let s1 = "This is a\\n"
      + "string across\\n"
      + "multiple lines.\\n"
    let a = 1, b = 2
    let s2 = "a + b = " + (a + b)
    \`\`\`
    
    ## Improved code
    \`\`\`js
    let s1 = String.dedent\`
      This is a
      string across
      multiple lines
      \`
    let a = 1, b = 2
    let s2 = \`a + b = \${a + b}\`
    \`\`\`
    \`\`\`\`
    `

    使用提议的语法:

    let promptForLLM = @`````
        You are a AI assistant to give advice to programmers,
        for example, given the code:
        ```js
        let s1 = "This is a\n"
          + "string across\n"
          + "multiple lines.\n"
        let a = 1, b = 2
        let s2 = "a + b = " + (a + b)
        ```
        you would output the advice:
        ````markdown
        ## Advice
        It's more readable to use template literal to replace
        the string concatenation.
    
        ## Original code
        ```js
        let s1 = "This is a\n"
          + "string across\n"
          + "multiple lines.\n"
        let a = 1, b = 2
        let s2 = "a + b = " + (a + b)
        ```
    
        ## Improved code
        ```js
        let s1 = @``
          This is a
          string across
          multiple lines
          ``
        let a = 1, b = 2
        let s2 = `a + b = ${a + b}`
        ```
        ````
        `````

    与其他提案的关系

    String.dedent 提案

    待办事项

    String.cooked 提案

    原始字符串字面量应始终提供原始字符串,当前提议的 String.cooked 不会访问 raw 属性并烹饪字符串,因此结果可能不如开发人员预期。

    先行艺术

    先前的讨论/想法