语言定义指南


概述


一般而言,编程语言代码都是由不同的解析规则组成:比如像 forif 这样的关键字在字符串内部没有意义,字符串可能包含像 \" 这样的反斜杠转义符,而注释通常除了注释结束标记外没有其他需要特别注意的。

在 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'
  }
}
注意:旧的 lexemes 配置项已被弃用,建议使用 keywords.$pattern,它们的功能是一样的。

子模式


子模式是列在 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']
}
注意:一种语言的根层级的 contains 中不能使用 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 的后行断言

贡献者说明


详情请见贡献者清单