# 语法文件开发

MT 语法文件 (.mtsx) 用于文本编辑器的语法高亮功能,其核心是通过正则表达式按一定规则进行匹配,并将匹配到的文本进行着色处理,因此你需要熟练掌握正则表达式才能进行编写。

# 变更记录

v2.13.2 版本

v2.14.3 版本

# ★ 正则表达式

首先介绍下 MT 语法文件的正则表达式写法,主要有两种:

"正则表达式\\s+"
/正则表达式\s+/

第一种是把正则表达式写在双引号之间,与 Java 代码中字符串的写法完全一样,缺点是需要对\"等字符进行转义,因为在正则表达式中经常会用到\,转义会影响可读性,正常我们推荐第二种写法。

第二种是把正则表达式写在/之间,如果正则表达式中包含/,你需要把它写成\/,其它字符均无需转义。

"abc/def\\s\""
/abc\/def\s"/

你可以将多个子表达式使用+连接,子表达式可以不写在同一行,以下三个正则表达式是一样的:

/123456789/
"123" + /456789/
/132/
+ /456/
+ /789/

# keywordsToRegex 函数

对于大多数语言,我们需要对该语言的关键字进行高亮,因此需要编写正则表达式对关键字进行匹配。例如一个语言包含关键字 ifelse,那么我们编写的正则表达式应该是/\b(if|else)\b/

但将关键字换成 movemove-wide,如果你编写的正则表达式是/\b(move|move-wide)\b/,那么你会发现这个正则表达式只能匹配到 move,永远无法匹配到 move-wide

为了正确匹配,你需要将它写成/\b(move-wide|move)\b/,但这样性能不是很好,更高效率的写法是/\b(move(-wide)?)\b/

再进一步优化下,写成/(?:\b(?:move(?:-wide)?)\b)/,其中最外层的括号是为了方便其作为一个子表达式连接其它子表达式。

这还只是仅有两个关键字的情况,如果有很多个关键字,那么情况会更加复杂,为了解决该问题,你可以使用 keywordsToRegex 函数,它自动帮你完成以上各个优化工作,并返回一个正则表达式。

keywordsToRegex(
     "while volatile void try true transient throws throw this"
     "synchronized switch super strictfp static short return"
     "public protected private package null new native long"
     "interface int instanceof import implements if goto for"
     "float finally final false extends enum else double do"
     "default continue const class char catch case byte break"
     "boolean assert abstract"
 )

该函数输入参数为一个或多个字符串,每个字符串内的关键字用空格隔开,字符串之间直接换行,无需使用+连接

因为 keywordsToRegex 函数返回的是一个正则表达式,所以你可以把它当作一个子表达式去连接其它表达式,十分方便。

/(?m)^[ \t]*/ + keywordsToRegex("if else")

# include 函数

新增于 v2.14.3 版本

你可以把多次用到的正则表达式片段写在 defines: [] 块中:

defines: [
    "name": /正则表达式abc/
]

然后通过 include 来引用这个片段

include("name") // 相当于上面的 /正则表达式abc/

/(?m)^[ \t]*/ + include("name") // 一样可以和其它正则表达式连接

关于 defines 和 include 在本文后面有更详细的介绍。

# ★ 匹配器

正则表达式指定了用什么样的规则去查找文本,而匹配器则是在找到文本之后,用什么样的规则去给文本上色,因此匹配器是整个语法文件的核心。

当前 MT 的语法引擎支持五种类型的匹配器:

  • match 匹配器
  • start-end 匹配器
  • number 匹配器
  • builtin 匹配器
  • include 匹配器

# 1. match 匹配器

{match: /正则表达式/, <捕获组序号>: "颜色名称"}

match 匹配器使用指定的正则表达式进行匹配,一旦匹配成功,会根据匹配组的颜色对文本进行着色,如果要对整个匹配到的文本进行着色,则对应的捕获组序号为 0,具体请参考以下例子:

{match: /[a-z]+\d+[a-z]+/, 0: "string"}
  • 高亮结果:abc123456def
{match: /[a-z]+\d+([a-z]+)/, 1: "string"}
  • 高亮结果:abc123456def
{match: /([a-z]+)(\d+)([a-z]+)/, 1: "string", 2: "error", 3: "meta"}
  • 高亮结果:abc123456def

如果同时使用捕获组序列号 0 与其它序列号,则序列号 0 对应的颜色相当于整个匹配文本段的默认颜色。

{match: /([a-z]+)(\d+)([a-z]+)/, 0: "string", 2: "error"}
  • 高亮结果:abc123456def(abc与def使用默认颜色进行着色)

# recordAllGroups 属性

新增于 v2.14.3 版本,可不填,默认值为 false,若填写则必须在 match 属性后面!

正则表达式的每个子匹配组只能记录最后一个位置,如果你希望记录子匹配组的所有匹配位置并进行着色,将 recordAllGroups 属性设为 true 即可。

例如你想用正则表达式 a(1|2)+b 去匹配文本,并让其中的 1 显示为黄色,2 显示为红色,你可能会这么写 match 匹配器:

{match: /a(?:(1)|(2))+b/, 1: "meta", 2: "error"}
  • 高亮结果:a121212b

只有最后一个 1 和 2 被高亮了,这是因为子匹配组分别匹配了三次 1 和 2,但只记录了最后一次。

添加 recordAllGroups 属性即可解决:

{match: /a(?:(1)|(2))+b/, recordAllGroups: true, 1: "meta", 2: "error"}
  • 高亮结果:a121212b

# 2. start-end 匹配器

{
    start: 起点匹配器
    end: 终点匹配器
    color: "默认颜色名称,可空"
    matchEndFirst: "优先匹配终点,默认false"
    contains: [
        子匹配器1
        子匹配器2
        ......
    ]
}

start-end 匹配器是比较高级的用法,在默认情况下,其匹配算法是首先使用起点匹配器进行匹配,一旦匹配成功,将不断使用终点匹配器与子匹配器进行匹配,其中子匹配器可能匹配多次,而一旦终点匹配器匹配成功或者到达文本末尾,将结束匹配。

通俗点说就是先匹配起点,然后终点匹配器和子匹配器一轮又一轮相互竞争,谁匹配位置更靠前就用谁,直到终点匹配器成功匹配或者到达文本末尾。

以代码字符串为例,字符串开头是一个",结尾也是一个",那么就可以这么写:

{
    start: {match: /"/}
    end: {match: /"/}
    color: "string"
}

高亮结果如下:

  • "abcdefg"
  • "abcd\"efg"

可以看到第二个字符串没有识别出转义符,导致高亮有误,此时我们就需要子匹配器去和终点匹配器竞争。

{
    start: {match: /"/}
    end: {match: /"/}
    color: "string"
    contains: [
        {match: /\\./}
    ]
}

这样就可以正确匹配了,我们来分析一下:

" a b c d \ " e f g "
0 1 2 3 4 5 6 7 8 9 10  >> 字符位置参照
  1. 起点匹配器搜索到位置为 0"
  2. 子匹配器终点匹配器从位置 1 开始搜索;
  3. 子匹配器搜索到位置为 5\"终点匹配器搜索到位置为 6"
  4. 子匹配器搜索到的位置更靠前,优先采用,并且下轮搜索从位置 7 开始;
  5. 子匹配器搜索失败,终点匹配器搜索到位置为 10"
  6. 终点匹配器的结果被采用,结束匹配。

start-end 匹配器的强大之处在于起点匹配器、终点匹配器、子匹配器都是可以单独控制着色的,嵌套使用后可以处理非常复杂的情况。

{
    start: {match: /<(\w+)/, 1: "error"}
    end: {match: /(\w+)>/, 1: "error"}
    contains: [
        {match: /\d+/, 0: "number"}
        {match: /\w+/, 0: "meta"}
    ]
}

高亮结果如下:

  • 123 abc <aaa abc 123 abc 123 bbb> abc 123

可以看到只有范围内的 abc123 会被高亮。

# matchEndFirst 属性

新增于 v2.14.3 版本

上面提到 start-end 匹配器在默认情况下,其算法是先匹配起点,然后终点匹配器与子匹配器进行竞争匹配,直到匹配到终点,或者到达文字末尾,才结束匹配。

如果将 matchEndFirst 属性设为 true,则算法是先匹配起点和终点,如果终点匹配失败则默认为匹配到文本结尾,然后起点和终点之间剩余的文本再使用子匹配器去匹配。

# 3. number 匹配器

新增于 v2.13.2 版本

{
    number: "2|8|10|16|F|E|P|_"
    iSuffixes: "l" // 整数后缀,可选,默认为空
    fSuffixes: "f|d" // 浮点数后缀,可选,默认为空
    color: "颜色名称" // 可选,默认为number
}

number 匹配器可以快速构建一个编程语言数字匹配器,只需简单填写一些参数即可完成,无需构建复杂的正则表达式。

其中 number 用于指定匹配的数字格式,每个配置项之间使用 | 连接:

配置项 说明
2 匹配二进制数,即 0b 开头的数字,如:0b101010
8 匹配八进制数,即 0o 开头的数字,如:0o777
10 匹配十进制数,包括 0 开头的数字,如 1320777
16 匹配十六进制数,即 0x 开头的数字,如:0xFFF
F 匹配浮点数,如:0.123
E 匹配科学计数法数字,如:1.321E10
P 匹配十六进制浮点数,如:0xFF.AAP123
_ 数字之间允许使用下划线连接,如 1_2_3

iSuffixesfSuffixes 分别用于指定整数后缀与浮点数后缀,不区分大小写,每个后缀用 | 隔开。

  • 二进制数、八进制数、十进制数、十六进制数会尝试匹配整数后缀
  • 十进制数、浮点数、科学计数法数字、十六进制浮点数会尝试匹配浮点数后缀

后缀只会匹配一次,且不互相叠加,如果需要叠加则要手动列出所有情况。

例如 C 语言中,整数后缀 L 表示长整数,U 表示无符号整数,可叠加使用,为此你需要这么写:

{
    iSuffixes: "l|u|lu|ul"
}

# 关于八进制

许多编程语言使用 0 开头来表示八进制,而这里却使用 0o 开头来表示,其实 0 开头已经包含在十进制中,也就是说开启了十进制后,0777 就会被高亮。

当然这种情况下 0888 也会被高亮,这明显是一个错误的八进制数字,但在很多 IDE 中,0888 一样会被当作数字来高亮,只是它会提示你这是一个错误的数字。

基于这个理解,高亮 0888 并没有错,只是 MT 不是 IDE,不会提示你它存在语法错误。

如果你就是希望不高亮错误的八进制数字,那么可以自己编写正则表达式,这并不难。

// 高亮十进制与0开头的八进制,跳过错误的八进制
{match: /\b(0|0[0-7]+|[1-9][0-9]*)\b/, 0: "number"}

# 其它说明

很多编程语言的数字语法之间存在着些许差异,本匹配器主要是基于 Java 的语法标准,并没有针对其它语言提供细节选项,这会导致一些错误。

例如 C 语言中十进制整数不允许使用浮点数后缀,即 123f 是非法的;Python 中不允许使用连续的下划线,即 1__2 是非法的,而它们在 Java 中是合法的。

其实这和上面提到的八进制一样,我们的重点是高亮,这类非法的数字也完全可以被高亮,缺的只是一个语法错误提示,而提示语法错误更多是 IDE 的职责,所以如果你遇到这个问题,不必太过于纠结。

# 4. builtin 匹配器

{builtin: #内置的匹配器#}

builtin 匹配器可以直接调用 MT 内置的匹配器,包含一些常用的规则,如匹配字符串、匹配数字,拿来即用十分方便。

具体内置的匹配器请看附录

# 5. include 匹配器

新增于 v2.14.3 版本

{include: "自定义匹配器"}

引用在 defines 中定义的匹配器,具体请看后面的介绍

# ★ 文件结构一览

// 注释内容
{
    name: ["语法名称", ".后缀名1", ".后缀名2"]
    hide: false
    ignoreCase: false
    colors: [
        "颜色名称", #RRGGBB, #RRGGBB
        "颜色名称" > "已有的颜色名称"
    ]
    comment: {startsWith: "/*", endsWith: "*/"}
    lineBackground: {match: /正则表达式/, color: "颜色名称"}
    defines: [
        // 这里可以定义正则表达式或者匹配器,然后在别处引用
        "regex": /正则表达式/
        "number": {match: /\d+/, 0: "颜色名称"}
        "string": [
            {match: /".*?"/, 0: "颜色名称"}
            {match: /'.*?'/, 0: "颜色名称"}
        ]
    ]
    contains: [
        //
        // 此处内容是整个语法文件的核心,语法引擎会使用这里的匹配器去查找并高亮文本
        //
        
        // match 匹配器
        {match: /正则表达式/, 0: "颜色名称"}

        // start-end 匹配器
        {
            start: {match: /正则表达式/}
            end: {match: /正则表达式/}
            color: "颜色名称"
            contains: [
                ......
            ]
        }
        
        // number 匹配器
        {
            number: "2|8|10|16|F|E|P|_"
            iSuffixes: "整数后缀"
            fSuffixes: "浮点数后缀"
            color: "颜色名称"
        }

        // builtin 匹配器
        {builtin: #内置的匹配器#}

        // 引用 defines 中的正则表达式
        {match: include("regex"), 0: "颜色名称"}
    
        // 引用 defines 中的匹配器
        {include: "number"}
    ]
    codeFormatter: #内置的代码格式化器#
    codeShrinker: #内置的代码压缩器#
}

# 属性:name

name: ["语法名称", ".后缀名1", ".后缀名2"]

指定语法名称与文件后缀,第一个字符串为语法名称,第二个开始均为后缀名,后缀名可以有多个。

# 属性:hide

hide: true | false

设置是否隐藏,隐藏后在文本编辑器中手动选择语法时将无法看到该语法,默认为 false。

# 属性:ignoreCase

ignoreCase: true | false

设置是否不区分大小写,设置为 true 后,所有正则表达式在匹配时均不会区分大小写,默认为 false。

如果只是想要部分正则表达式不区分大小写,可使用 i 标志,如/(?i)[a-z]+/

# 属性:colors

colors: [
    "颜色名称", #RRGGBB, #RRGGBB
    "颜色名称" > "已有的颜色名称"
]

自定义颜色,有两种格式。

一种是直接定义颜色值,包含日间模式和夜间模式的颜色。

另一种是使用已有的颜色,相当于给已有颜色取个别名。

MT 已经内置了一些常用颜色,具体请看附录

# 属性:comment

// 行注释
comment: {startsWith: "//"}
// 块注释
comment: {startsWith: "/*", endsWith: "*/"}

定义注释,支持行注释与块注释。

定义注释之后,语法引擎会自动添加正则表达式对注释内容进行高亮,您无需额外配置,并且可同时开启该语法的注释切换功能,当存在多个注释配置时,注释切换功能将优先使用第一个行注释。

默认在切换为注释时,注释符号与代码内容之间会增加一个空格,如果不希望增加空格,可配置insertSpace选项。

comment: {startsWith: "//", insertSpace: false}

原内容
abc
切换注释(insertSpace: true)
// abc
切换注释(insertSpace: false)
//abc

另外如果你仅仅只想开启注释切换功能,不希望语法引擎自动添加正则表达式对注释内容进行高亮,可添加addToContains选项。

comment: {startsWith: "//", insertSpace: false, addToContains: false}

注意,startsWithendsWithinsertSpaceaddToContains这四个选项的先后顺序不能变,也就是说addToContains不能放在insertSpace的前面。

# 属性:lineBackground

lineBackground: {match: /正则表达式/, color: "颜色名称"}

如果正则表达式可以完全匹配某一行的文本内容,则为该行添加背景颜色。

例如Smali语法中会对.method所在行进行高亮:

// 注意:这里的 methodHeader 不是内置颜色,需单独定义
lineBackground: {match: /[\t ]*\.method .*/, color: "methodHeader"}

alt

# 属性:contains

contains: [
    // match 匹配器
    {match: /正则表达式/, 0: "颜色名称"}

    // start-end 匹配器
    {
        start: {match: /正则表达式/}
        end: {match: /正则表达式/}
        color: "颜色名称"
        contains: [
            ......
        ]
    }

    // number 匹配器
    {
        number: "2|8|10|16|F|E|P|_"
        iSuffixes: "整数后缀"
        fSuffixes: "浮点数后缀"
        color: "颜色名称"
    }

    // builtin 匹配器
    {builtin: #内置的匹配器#}

    // 引用 defines 中的正则表达式
    {match: include("regex"), 0: "颜色名称"}

    // 引用 defines 中的匹配器
    {include: "number"}
]

你需要在 contains 内编写你的匹配器,它是整个语法文件的核心,语法引擎会使用这里的匹配器去查找并高亮文本。

关于匹配器已在上文有详细的介绍,这边不再重复。

# 属性:defines

新增于 v2.14.3 版本

你可以在 defines: [] 块中定义正则表达式或者匹配器,然后在其它地方引用。

# 用于正则表达式

举个例子,你需要把使用 () 包裹起来的邮箱地址高亮为红色,使用 [] 包裹起来的邮箱地址高亮为绿色,那么可以这么写:

contains: [
    {match: /\(^[\w\.-]+@[\w\.-]+\.\w+$\)/, 0: "error"}
    {match: /\[^[\w\.-]+@[\w\.-]+\.\w+$\]/, 0: "string"}
]

这样写没有问题,但不利于维护,如果你后面发现匹配邮箱地址的正则表达式无法匹配一些特殊邮箱,需要进行完善,就得修改两处代码。

而实际情况可能比这更复杂,可能要修改不止两处代码,可能要在一段复杂的正则表达式中找出需要修改的片段将它替换掉,这样非常容易出错。

因此你可以把这个正则表达式片段提取出来放到 defines 中:

defines: [
    "emails": /[\w\.-]+@[\w\.-]+\.\w+/
]
contains: [
    {match: /\(/ + include("email") + /\)/, 0: "error"}
    {match: /\[/ + include("email") + /\]/, 0: "string"}
]

这样后期维护就比较简单不容易出错了。

defines 中定义的正则表达式也可以引用在它前面定义的正则表达式:

defines: [
    "exp1": /正则表达式1/
    "exp2": include("exp1") + /正则表达式2/
]

被引用的表达式必须在前面,像下面这样则不行:

defines: [
    "exp2": include("exp1") + /正则表达式2/
    "exp1": /正则表达式1/
]

# 用于匹配器

在 defines 中,你可以这样定义一个匹配器:

defines: [
    "number": {match: /\d+/, 0: "颜色名称"}
]

或者这样定义一个匹配器组:

defines: [
    "string": [
        {match: /".*?"/, 0: "颜色名称"}
        {match: /'.*?'/, 0: "颜色名称"}
    ]
]

然后在 contains 中引用:

contains: [
    {include: "number"}
    {
        // 所有使用匹配器的地方都可以使用 include 代替
        start: {include: "string"}
        end: {include: "string"}
    }
]

支持自递归引用:

defines: [
    "group": {
        start: {match: /</, 0: "颜色名称"}
        end: {match: />/, 0: "颜色名称"}
        contains: [
            {include: "group"}
        ]
    }
]

也可以交叉递归引用:

defines: [
    "group1": {
        start: {match: /</, 0: "红色"}
        end: {match: />/, 0: "红色"}
        contains: [
            {include: "group2"}
        ]
    }
    "group2": {
        start: {match: /</, 0: "绿色"}
        end: {match: />/, 0: "绿色"}
        contains: [
            {include: "group3"}
        ]
    }
    "group3": {
        start: {match: /</, 0: "蓝色"}
        end: {match: />/, 0: "蓝色"}
        contains: [
            {include: "group1"}
        ]
    }
]

你可以下载安装这个例子 (opens new window)来体会交叉递归引用的妙用。


与定义正则表达式不同,定义匹配器时可以随意交叉引用,不需要考虑先后顺序。

但是在交叉引用时,注意不要出现循环硬性依赖,通俗的讲就是匹配器1查找成功的前提是匹配器2查找成功,而匹配器2查找成功的前提又是匹配器1查找成功,这样就出现死循环。

例如下面这个例子:

defines: [
    "xxx": {
        start: {include: "yyy"}
        end: {match: /./}
    }
    "yyy": {
        start: {include: "xxx"}
        end: {match: /./}
    }
]
contains: [
    {include: "xxx"}
]

在安装时将会收到一个错误:

There is a circular hard dependency: inlude "xxx" > inlude "yyy" > inlude "xxx"

# 属性:codeFormatter

codeFormatter: #内置的代码格式化器#

指定该语法的代码格式化器,设置后可以在文本编辑器中使用代码格式化功能。

目前仅支持指定内置的代码格式化器,不支持自定义,具体包含 CSS、HTML、JavaScript、JSON、XML、Smali 代码格式化器,分别对应:

#BUILT_IN_CSS_FORMATTER#
#BUILT_IN_HTML_FORMATTER#
#BUILT_IN_JS_FORMATTER#
#BUILT_IN_JSON_FORMATTER#
#BUILT_IN_XML_FORMATTER#
#BUILT_IN_SMALI_FORMATTER#

# 属性:codeShrinker

codeShrinker: #内置的代码压缩器#

指定该语法的代码压缩器,设置后可以在文本编辑器中使用代码压缩功能。

目前仅支持指定内置的代码压缩器,不支持自定义,具体包含 CSS、HTML、JSON 代码格式化器,分别对应:

#BUILT_IN_CSS_SHRINKER#
#BUILT_IN_HTML_SHRINKER#
#BUILT_IN_JSON_SHRINKER#

# 安装语法高亮文件

将写好的语法规则保存为 .mtsx 文件,在 MT 管理器内打开它即可安装。

# 附录

# 内置的颜色

内置的颜色可在MT.apk/assets/syntax/init/colors.mtsx文件中找到,请以安装包内文件为准。

"default"       #000000     #A9B7C6 
"string"        #067D17     #6A8759 
"strEscape"     #0037A6     #CC7832 
"comment"       #8C8C8C     #808080 
"meta"          #9E880D     #BBB529 
"number"        #1750EB     #6897BB 
"keyword"       #0033B3     #CC7832 
"keyword2"      #800000     #AE8ABE 
"constant"      #871094     #9876AA 
"type"          #808000     #808000 
"label"         #7050E0     #6080B0 
"variable"      #1750EB     #58908A 
"operator"      #205060     #508090 
"propKey"       #083080     #CC7832 
"propVal"       #067D17     #6A8759 
"tagName"       #0030B3     #E8BF6A 
"attrName"      #174AD4     #BABABA 
"namespace"     #871094     #9876AA 
"error"         #F50000     #BC3F3C 

# 内置的匹配器

内置的匹配器及其具体规则可在MT.apk/assets/syntax/init/builtins.mtsx文件中找到,请以安装包内文件为准。

//
// 普通字符串
//

// 匹配转义符号,仅包含\x
#ESCAPED_CHAR#

// 单引号字符串
#SINGLE_QUOTED_STRING#

// 双引号字符串
#DOUBLE_QUOTED_STRING#

// 单双引号字符串
#QUOTED_STRING#

//
// Java字符串
//

// 匹配Java转义符号,包含 \b \t \n \f \r \" \' \\ \000 \u0000,匹配失败时会进行红色标记
#JAVA_ESCAPED_CHAR#

// Java单引号字符串
#JAVA_SINGLE_QUOTED_STRING#

// Java双引号字符串
#JAVA_DOUBLE_QUOTED_STRING#

// Java单双引号字符串
#JAVA_QUOTED_STRING#

//
// C字符串
//

// 匹配C转义符号,包含 \b \t \n \f \r \x \" \' \\ \000 \u0000,匹配失败时会进行红色标记
#C_ESCAPED_CHAR#

// C单引号字符串
#C_SINGLE_QUOTED_STRING#

// C双引号字符串
#C_DOUBLE_QUOTED_STRING#

// C单双引号字符串
#C_QUOTED_STRING#

//
// 数字相关
//

// 匹配数字,支持整数与小数
#NORMAL_NUMBER#

// 编程语言数字,大多数语言适用
#PROGRAM_NUMBER#

// 编程语言数字,与上面比取消了二进制支持
#PROGRAM_NUMBER2#

// Java数字
#JAVA_NUMBER#

// C语言数字
#C_NUMBER#