语言定义指南
概述
一般而言,编程语言代码都是由不同的解析规则组成:比如像 for 或 if 这样的关键字在字符串内部没有意义,字符串可能包含像 \" 这样的反斜杠转义符,而注释通常除了注释结束标记外没有其他需要特别注意的。
在 Highlight.js 中,上述这些组成部分我们称之为“语言模式”。
每个模式包括以下内容:
- 起始条件
- 结束条件
- 包含的子模式列表
- 词法规则和关键字
- …...特殊部分,比如语言内的另一种语言
解析器的功能是查找模式及其关键字。一旦找到它们,则将它们封装到标记 <span class="...">...</span> 中,并将模式名称(例如 "string", "comment", "number" )或关键字组名(例如 "keyword", “literal”, “built-in”)作为 span 的类名。
通用语法
首先,每个语法定义项得是一个 JavaScript 对象,用来描述该语言的解析模式。其默认模式中可以包含子模式,子模式又包含其他子模式,从而有效地构成一颗树结构的模式。
下面提供一个例子:
{
case_insensitive: true, // language is case-insensitive
keywords: 'for if while',
contains: [
{
scope: 'string',
begin: '"',
end: '"'
},
hljs.COMMENT(
'/\\*', // begin
'\\*/', // end
{
contains: [
{
scope: 'doc',
begin: '@\\w+'
}
]
}
)
]
}
通常,默认模式占据了代码的大部分内容,并描述了语言的所有关键字。不过有一个值得注意的例外,就是 XML,其默认模式只是一个不包含任何关键字的用户文本,而大部分的要解析的内容实际上是包含在标签内部的。
关键字
在简单的情况下,语言关键字可以使用字符串(以空格分隔)或数组来定义:
{
keywords: 'else for if while',
// or with an array
keywords: ['else', 'for', 'if', 'while']
}
而有些语言则会有不同种类的“关键字”,它们在语言规范中可能不称为“关键字”,但从语法高亮处理的角度看来,它们其实差不多。这些关键字组包括各种“字面量”译者注:字面量通常是程序中的常量,比如字符常量、数字、布尔值等、“内置函数”、“符号”等。为了定义这些关键字组,keywords 属性要变成一个对象,其中的每项属性都定义了自己的关键字组:
{
keywords: {
keyword: 'else for if while',
literal: ['false','true','null'],
_relevance_only: 'one two three four'
}
}
组名会成为生成标记的类名,使得不同类型的关键字可以使用不同的样式。任何以 _ 开头的属性,该组关键字只用来提高相关性,不会对它们进行高亮处理。
为了检测关键字,highlight.js 会将所处理的代码块拆成独立的单词,这个过程被称为词法分析。默认情况下,所谓拆分“单词”的方式和正则表达式 \w+ 匹配规则是一致的,事实上这种方式在许多语言上都是有效的。当然,你也可以修改词法分析的规则,通过 $pattern 属性定义即可:
{
keywords: {
$pattern: /-[a-z]+/, // allow keywords to begin with dash
keyword: '-import -export'
}
}
子模式
子模式是列在 contains 属性中的:
{
keywords: '...',
contains: [
hljs.QUOTE_STRING_MODE,
hljs.C_LINE_COMMENT,
{ ... custom mode definition ... }
]
}
一个模式可以通过 contains 数组中使用一个特殊关键字 'self’ 来引用自身。这通常用于定义嵌套模式:
{
scope: 'object',
begin: /\{/,
end: /\}/,
contains: [hljs.QUOTE_STRING_MODE, 'self']
}
注释
为了定义自定义注释,建议使用内置的工具函数 hljs.COMMENT,而不是直接进行模式定义,因为该函数还定义了一些默认的子模式,通过它们可以提高语言检测性能并执行其他有用的操作。
该函数的参数包括:
hljs.COMMENT(
begin, // begin regex
end, // end regex
extra // optional object with extra attributes to override defaults
// (for example {relevance: 0})
)
标记生成
通常在各模式的作用下会生成具体的高亮标记,即具有特定类名的 <span> 元素,而这些类名由 scope 属性所定义:
{
contains: [
{
scope: 'string',
// ... other attributes
},
{
scope: 'number',
// ...
}
]
}
上述 scope 的值并不要求是唯一的,反而在多个定义下使用了同一个 scope 也是比较常见的。例如,在许多语言中都存在字符串、注释等各种语法。
有时定义模型只是为了支持特定的解析规则,在最终标记中它是不需要的。一个典型的例子是字符串中的转义序列,允许它们包含结束引号。
{
scope: 'string',
begin: '"',
end: '"',
contains: [{begin: '\\\\.'}],
}
对于这样的模式,应省略 scope 属性,以便它们不会生成过多的标记。
有关 scope 名称支持详细清单,请参阅作用域引用。
模式属性
关于其他属性参数,详情请见模式参考。
相关性
相关性(relevance)是指 highlight.js 针对代码片段进行自动检测语言的一个指标。检测方法基本上很简单:它会把所有的语言定义套在指定的代码片段上进行高亮处理,而最多模型和关键字匹配的那一项则作为最终检测结果。换言之,我们进行语言定义,就是为了使各模型能更准确地对代码进行相关性(或不相关)匹配,从而提高这套检测方法的精准度。
这最好通过一个例子来说明。Python 有一种特殊字符串,在引号前加一个前缀字符,比如:r"...",u"..."。如果某段代码中包含这样的字符串,那么它很可能就是一段 python 代码。因此,我们可以对这种字符串定一个比较高的 relevance,模式定义如下:
{
scope: 'string',
begin: 'r"', end: '"',
relevance: 10
}
换个方向考虑,由单引号或双引号定义的字符串,在所有语言中都是这么定义的,那我们就要把它的 relevance 降到 0。这样在进行检测统计时可以少做许多无用功:
{
scope: 'string',
begin: '"', end: '"',
relevance: 0
}
一般 relevance 的默认值为 1,而当你需要对其进行设置时,通常我们可以设为 10 或 0。设为 0 表示该匹配项不应考虑用于语言检测,其主要针对那些任何语言都可能匹配的类型(数字、字符串等),或者如果可能产生过多误报情况的也应设为 0。而设为 10 表示“这几乎能确定就是 XYZ 代码”,不过这种设置我们要谨慎对待。
关键字也会对相关性造成影响。每一个关键字通常 relevance 值为 1,但有些特殊的关键字在除了这门语言外的其他地方很难再找到了,即使作为变量出现都不太可能,这种情况我们提高它的 relevance 也是可以的。例如,若代码中出现了 reinterpret_cast,那我们基本可以确定这段代码就是 C++ 了。针对关键字修改 relevance,可以使用竖杆(|)将关键字和 relevance 值连起来:
{
keywords: 'for if reinterpret_cast|10'
}
非法字符
另一种提高语言检测准确度的方法是定义模式的非法字符。例如,在 Python 中,类定义的第一行(class MyClass(object):)不能包含 { 符号或换行符。若存在了这些符号,那可以清楚地表明该语言不是 Python,这时解析器也就不用继续在这门语言上费工夫了。
非法字符属性 illegal 要用正则表达式来定义:
{
scope: 'class',
illegal: '[${]'
}
预定义模式和正则表达式
有时候一些通用的模式或正则表达式可以给多种语言所共享,这些表达式我们已经定义在 lib/modes.js 中,只要需要都能拿去使用。
正则表达式特性
Highlight.js 关于正则表达式的宗旨是凡是 JavaScript 所支持的正则表达式的功能它都支持。所以只要符合正则表达式的规范,你都能放心地去用。不过虽然如此,由于解析器的设计有其独特性,还是有一些注意事项。以下是这些问题的详细说明。
之前不支持,但是现在已完全支持的功能:
- 正则匹配中用于 begin 的先行断言(#2135)
- 正则匹配中用于 end 的先行断言(#2237)
- 正则匹配中用于 illegal 的先行断言(#2135)
- 正则表达式匹配中的反向引用(#1897)
下面的则是理论上可以,但是我们并不支持的功能(因为 Safari 不支持后行断言):
- 正则匹配中用于 begin 的后行断言(#2135)
由于解析引擎自身问题而不支持的功能:
- 正则匹配中用于 end 的后行断言
贡献者说明
详情请见贡献者清单。