1.3 语法

优质
小牛编辑
130浏览
2023-12-01

Sublime Text 可以使用.sublime-syntax.tmLanguage文件进行语法高亮,本文介绍.sublime-syntax文件。

概述

Sublime 语法文件是 YAML,此文件有一个小的头部,紧跟一个上下文列表。每个上下文都有一个匹配模式的列表,这个列表描述了在这个上下文中如何高亮文本、如何改变当前文本。

这里有一个高亮 C 语言语法的示例:

%YAML 1.2
---
name: C
file_extensions: [c, h]
scope: source.c

contexts:
  main:
    - match: \b(if|else|for|while)\b
      scope: keyword.control.c

核心是一个语法定义,把作用域安排给文本区域,这个作用域由配色方案使用来对文本进行高亮。

这个语法文件包含一个 context,main,匹配到[if, else, for, while]等单词时,就会把它们放进keyword.control.c的作用域。每个语法都必须定义一个 main 上下文,这会最先被使用。

是正则表达式,使用了Ruby 语法。上面的例子中,\b用来确保单词的边界被匹配上,其他位置的这些词不会被作为关键词。

注意:由于 YAML 语法,.sublime-syntax文件中不允许有 tab 字符。

Header

header 区域允许的 key 有:

  • name. 菜单栏中语法的名称。可选,如果没有时将会从文件名中提取。
  • file_extensions.一个字符串列表,定义使用这种语法高亮的文件的扩展名。
  • first_line_match. 当打开一个未识别的文件时,文件的第一行内容将会通过这个正则表达式去识别,从而判断是否应该应用这个语法高亮。
  • scope. 文件中全部文本的默认作用域。
  • hidden. 菜单栏中不会显示隐藏的语法定义,但仍然可以被插件使用,或是包含在其它语法定义中。

Contexts

对于大多数语言来说,你可能需要不止一种 context。如,C 语言中,我们不需要一个字符串中间被匹配到的单词有关键词的语法高亮。这里有个例子:

%YAML 1.2
---
name: C
file_extensions: [c, h]
scope: source.c

contexts:
  main:
    - match: \b(if|else|for|while)\b
      scope: keyword.control.c
    - match: '"'
      push: string

  string:
    - meta_scope: string.quoted.double.c
    - match: \\.
      scope: constant.character.escape.c
    - match: '"'
      pop: true

main上下文中第二个匹配项是一个双引号(注意"必须是'"'这样的,单独一个双引号会报 YAL 语法错误),添加一个字符串上下文到 context 的堆中。这就意味着这个文件的余下部分将使用contexts中的string来处理, 而不是main,直到string从堆中被移除。

string上下文介绍了一个新的模式:meta_scope,档string上下文在堆中时,这会把string.quoted.double.c作用域分配给文本。

在 Sublime Text 中编辑时,你可以通过按下ctrl+shift+p(OS X)或ctrl+alt+shift+p(Windows 和 Linux)来查看光标选中的文本应用的作用域。

string有两个匹配项:第一个是反斜杠字符跟一个任意字符,第二个是一个双引号字符。注意最后一个匹配项指定了一个行为:当遇到转义引号时,string将被从 context 堆栈中移除,然后重新去使用main中的作用域。

当一个 context 有多个匹配项,最左边的一个将被发现。当同一位置匹配到多个模式时,将会应用第一个定义的模式。

Meta 模式

  • meta_scope. 这把给定的作用域分配给这个上下文中的所有文本,包括把 context 添加到堆栈或从中移除的 patterns。
  • meta_content_scope. 和上面一样,但是不应用于触发此 context 的文本(如,上面的string例子中,当前作用域不会应用于引号字符)。
  • meta_include_prototype. 用来阻止当前 context 自动包含原型 context。

Meta 模式必须放在 contexts 中的第一位。

Match 模式

match 模式允许有如下 key:

  • match. 正则表达式。YAML 允许很多字符串不带双引号,这可以使正则表达式更清晰,但你仍然需要知道何时把正则表达式用引号包起来。如果你的正则表达式包含了#:-{[>等字符,这时你就需要 用到引号了。正则表达式同一时间只能对单行文本生效。
  • scope. 安排给匹配到的文本的作用域。
  • captures. 一个数字到作用域的映射,给正则表达式匹配到的部分分配作用域。
  • push. 追加到 contexts 堆栈中的 context,这可以是一个 context 名称、context 名称列表或是一个行内、匿名的 context。
  • pop. 把当前上下文从 contexts 堆栈中移除,这个 key 唯一可被接收的值是true
  • set. 和push接收同样的参数,但是首先会把这个 context 移除,然后才把给定的 context 添加到堆栈中。
  • syntax. 看下面的包含其它文件

注意:push、pop、set 和 syntax 都是独立的,一个单独的匹配模式中只能使用其中的一个。

这个例子中,正则表达式包含了两个捕获,捕获的 key 用来给每一个分配不同的作用域。

- match: "^\\s*(#)\\s*\\b(include)\\b"
  captures:
    1: meta.preprocessor.c++
    2: keyword.control.include.c++

Include 模式

便于在一个 context 中包含另外一个。例如,你可以定义多种不同的 context 来解析 C 语言,它们几乎都可以包含comments。你可以包含他们,而不是在每个 context 中复制一份:

expr:
  - include: comments
  - match: \b[0-9]+\b
    scope: constant.numeric.c
  ...

这里,所有的 match 匹配项和 include 模式都将被拉取,它们将在 include 模式的位置被插入,因此你仍然可以控制模式的顺序。所有定义在 comments 上下文中的 meta 模式都将被忽视。

当有一个元素,如comments时,这个被包含简直太常见了,对每一个 context 自动包含这个是很简单的,只需要列出异常。你可以创建一个 context 明明的原型,这将会自动被包含到每一个其它 context 的顶部,除非 context 使用了meta_include_prototypemeta 模式。例如:

prototype:
  - include: comments

string:
  - meta_include_prototype: false
  ...

C 语言中,字符串中的/*不会作为注释的开始,因此stringcontext 表明原型不应该被包含。

包含其它文件

Sublime 语法文件支持在一个语法定义中嵌入另一个,如,HTML 可以嵌入 JavaScript。这里有一个 HTML 的基本语法定义的例子:

scope: text.html

contexts:
  main:
    - match: <script>
      push: Packages/JavaScript/JavaScript.sublime-syntax
      with_prototype:
        - match: (?=</script>)
          pop: true
    - match: "<"
      scope: punctuation.definition.tag.begin
    - match: ">"
      scope: punctuation.definition.tag.end

注意上面的第一条规则,它表明了当遇到<script>标签时,JavaScript.sublime-syntax将被 push 到 context 的堆栈中。它还定义了另外一个 keywith_prototype。包含了一个将被插入到每一 个JavaScript.sublime-syntax中定义的 context 中的匹配项列表。注意with_prototype在概念上和prototype上下文类似,然而它将始终被插入到每一个被引用的上下文,无需考虑meta_include_prototype设置。

这个例子中,当下一个标签是</script>时,插入的 patter 会被从当前 context 中移除。注意这并不是真正匹配</script>标签,只是使用了一个在这里扮演了两个角色的预判:既允许 HTML 规则匹配结束标签,按照正常 方式高亮,保证 JavaScript 上下文可以被移出。上下文堆栈也许在 JavaScript 字符串中间,例如,一旦遇到</script>标签,JavaScript 字符和maincontext 都将被移出。

注意:不能把.tmLanguage文件包含到.sublime-syntax文件中。

一种常见的情景是包含 HTML 的模板语言,这里有一个例子:

scope: text.jinja
contexts:
  main:
    - match: ""
      push: "Packages/HTML/HTML.sublime-syntax"
      with_prototype:
        - match: "
          push: expr

  expr:
    - match: "
      pop: true
    - match: \b(if|else)\b
      scope: keyword.control

这个和 HTML 嵌套 JavaScript 的例子大不一样,因为模板语言往往是从内到外进行操作:默认情况,它需要像 HTML 一样,只有转码成特定表达式的底层模板语言。

上面的例子,我们可以看到它默认以 HTML 模式运行:main上下文包含一个单独的永远被匹配到的模式,只包含了 HTML 语法。

变量

几个正则表达式有共同部分并不罕见,你可以使用变量来避免重复输入:

variables:
  ident: '[A-Za-z_][A-Za-z_0-9]*'
contexts:
  main:
    - match: '\b\b'
      scope: keyword.control

变量必须定义在 .sublime-syntax 文件的顶部,在正则表达式中通过 \{\{varname\}\} 的形式来引用,变量本身可以引用其它变量。注意任何不匹配 \{\{[A-Za-z0-9_]+\}\} 的文本将不会被认为是一个变量,因此正则 表达式仍然可以包含常量 \{\{ 字符。

示例

括号平衡

这个例子高亮没有对应的开括号的闭合括号:

name: C
scope: source.c

contexts:
  main:
    - match: \(
      push: brackets
    - match: \)
      scope: invalid.illegal.stray-bracket-end

  brackets:
    - match: \)
      pop: true
    - include: main

连续的 context

这个例子将高亮包含很多分号的 C 语言风格的for语句:

for_stmt:
  - match: \(
    set: for_stmt_expr1
for_stmt_expr1:
  - match: ";"
    set: for_stmt_expr2
  - match: \)
    pop: true
  - include: expr
for_stmt_expr2:
  - match: ";"
    set: for_stmt_expr3
  - match: \)
    pop: true
  - include: expr
for_stmt_expr3:
  - match: \)
    pop: true
  - match: ";"
    scope: invalid.illegal.stray-semi-colon
  - include: expr

高级堆栈的使用

C 语言中,符号通常是和一个typedef关键词一起定义的。符号有一个附加到它的entity.name.typescope

这样做事带有一点取巧,因为虽然类型定义很简单,但你也可以把它玩的很复杂。

typedef int coordinate_t;

typedef struct
{
    int x;
    int y;
} point_t;

认识到这些,当匹配到typoedef关键词后,两个 context 会被添加到堆栈中:第一个将识别typename,然后移出;第二个将识别这个类型介绍的名称:

main:
  - match: \btypedef\b
    scope: keyword.control.c
    set: [typedef_after_typename, typename]

typename:
  - match: \bstruct\b
    set:
      - match: "{"
        set:
          - match: "}"
            pop: true
  - match: \b[A-Za-z_][A-Za-z_0-9]*\b
    pop: true

typedef_after_typename:
  - match: \b[A-Za-z_][A-Za-z_0-9]*\b
    scope: entity.name.type
    pop: true

上面的例子中,typename是一个可复用的 context,它将在typename中读取然后在完成时从堆栈中移出。它可以在任意上下文中使用,如:typedef 中,或作为一个函数的参数。

PHP Heredocs

这个例子展示了如何匹配 PHP 中的Heredocsmaincontext 中的match捕获 heredoc 的识别符,heredoccontext 中相应的pop指的 是捕获到的文本中有\1的。

name: PHP
scope: source.php

contexts:
  main:
    - match: <<<([A-Za-z][A-Za-z0-9_]*)
      push: heredoc

  heredoc:
    - meta_scope: string.unquoted.heredoc
    - match: ^\1;
        pop: true

Testing

当构建了一个语法定义,你可以定义一个语法测试文件来帮你检测作用域,而不是手动通过show_scope_name指令进行检测。

// SYNTAX TEST "Packages/C/C.sublime-syntax"
#pragma once
// <- source.c meta.preprocessor.c++
 // <- keyword.control.import

// foo
// ^ source.c comment.line
// <- punctuation.definition.comment

/* foo */
// ^ source.c comment.block
// <- punctuation.definition.comment.begin
//     ^ punctuation.definition.comment.end

#include "stdio.h"
// <- meta.preprocessor.include.c++
//       ^ meta string punctuation.definition.string.begin
//               ^ meta string punctuation.definition.string.end
int square(int x)
// <- storage.type
//  ^ meta.function entity.name.function
//         ^ storage.type
{
    return x * x;
//  ^^^^^^ keyword.control
}

"Hello, World! // not a comment";
// ^ string.quoted.double
//                  ^ string.quoted.double - comment

遵循以下规则:

  1. 确保文件名以"syntax_test"开头。
  2. 确保文件存储在Packages目录下的某个地方:推荐和相应的.sublime-syntax文件放在一起。
  3. 确保文件第一行以<comment_token> SYNTAX TEST "<syntax_file>"开头。注意语法文件可以是.sublime-syntax.tmLanguage文件。

一但上述要求都满足了,运行 build 指令就可以在输出面板看到结果。下一个结果(F4)可以用来导航到第一个失败的测试。

语法测试文件中的每一个测试必须首先以注释标记开头,然后才是^<-标记。

这两种类型的测试分别是:

  • 尖号:^将测试针对最近一次的非测试行的作用域选择器,它将测试和^所在列。连续的^将测试选择器对应的每一列。
  • 箭头:<- 将测试针对最近一次的非测试行的作用域选择器,它将测试注释字符所在的列。