扩展 Prism

Prism 本身为一个非常棒的工具,而它经过定制以满足您的需求时会更加棒。本节将帮助您编写新的语言定义、插件以及 Prism 全方位黑客技术。

语言定义

每种语言都定义为一组标记,这些标记表示为 正则表达式。例如,这是 JSON 的语言定义



	

从本质上讲,语言定义只是一个 JavaScript 对象,而令牌只是语言定义的一个条目。最简单的语言定义是一个空对象

Prism.languages['some-language'] = { };

遗憾的是,一个空语言定义并不是很有用,所以让我们添加一个令牌。表示令牌的最简单方法是使用正则表达式文字

Prism.languages['some-language'] = {
	'token-name': /regex/,
};

另外,也可以使用对象文字。通过这种表示法,描述令牌的正则表达式是对象的 pattern 属性

Prism.languages['some-language'] = {
	'token-name': {
		pattern: /regex/
	},
};

到目前为止,正则表达式和对象表示法之间的功能完全相同。但是,对象表示法允许使用 其他选项。稍后将对此进行更多介绍。

从理论上讲,令牌的名称可以是任何字符串,该字符串也是有效的 CSS 类,但有一些 需要遵循的准则。稍后将对此进行更多介绍。

语言定义可以包含任意数目的令牌,但是每个令牌的名称必须唯一

Prism.languages['some-language'] = {
	'token-1': /I love regexes!/,
	'token-2': /regex/,
};

Prism 会逐个按顺序将令牌与输入文本进行匹配,并且令牌不能与前面令牌的匹配结果重叠。因此,在上例中,token-2 不会匹配 token-1 的匹配结果内部的子字符串“regex”。稍后会提供有关 Prism 的匹配算法 的更多信息。

最后,在许多语言中,有多种不同的方法来声明相同的构造(例如,注释、字符串...),有时很难或不切实际用一个正则表达式匹配所有这些构造。要为一个令牌名称添加多个正则表达式,可以使用数组

Prism.languages['some-language'] = {
	'token-name': [
		/regex 1/,
		/regex 2/,
		{ pattern: /regex 3/ }
	],
};

注意:不能在 pattern 属性中使用数组。

对象表示法

除了使用纯正则表达式外,Prism 还支持令牌的对象表示法。此表示法启用了以下选项

pattern: RegExp

这是唯一必需的选项。它包含令牌的正则表达式。

lookbehind: boolean

此选项减轻了 JavaScript 对后顾的浏览器支持较差的问题。当设置为 true 时,在匹配此令牌时会丢弃 pattern 正则表达式中的第一个捕获组,因此它实际上可以用作后顾。

关于此项的一个示例,请了解 C 类语言定义如何查找 class-name 令牌

Prism.languages.clike = {
	// ...
	'class-name': {
		pattern: /(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+)\w+/i,
		lookbehind: true
	}
};
greedy: boolean

此选项允许令牌的贪婪匹配。有关详细信息,请参阅关于匹配算法的部分。

别名:字符串 | 字符串数组

此选项可用于为令牌定义一个或多个别名。结果将是令牌名称和别名的样式组合在一起。这对于将标准令牌的样式(大多数主题已支持此样式)与更精确的令牌名称结合起来很有用。有关此主题的更详细信息,请参阅细粒度突出显示

例如,latex-equation令牌名称不受大多数主题支持,但在此示例中,它将与字符串突出显示相同

Prism.languages.latex = {
	// ...
	'latex-equation': {
		pattern: /\$.*?\$/,
		alias: 'string'
	}
};
内部:语法

此选项接受另一个对象字面量,其中包含允许嵌套在该令牌中的令牌。inside语法中的所有令牌都将被此令牌封装。这让定义某些语言变得更容易。

有关嵌套令牌的示例,请查看 CSS 语言定义中的url令牌

Prism.languages.css = {
	// ...
	'url': {
		// e.g. url(https://example.com)
		pattern: /\burl\(.*?\)/i,
		inside: {
			'function': /^url/i,
			'punctuation': /^\(|\)$/
		}
	}
};

inside选项也可以用于创建递归语言。这对于其中的一个令牌可以包含任意表达式的语言很有用,例如具有字符串插值语法的语言。

例如,以下是 JavaScript 如何实现模板字符串插值

Prism.languages.javascript = {
	// ...
	'template-string': {
		pattern: /`(?:\\.|\$\{[^{}]*\}|(?!\$\{)[^\\`])*`/,
		inside: {
			'interpolation': {
				pattern: /\$\{[^{}]*\}/,
				inside: {
					'punctuation': /^\$\{|\}$/,
					'expression': {
						pattern: /[\s\S]+/,
						inside: null // see below
					}
				}
			}
		}
	}
};
Prism.languages.javascript['template-string'].inside['interpolation'].inside['expression'].inside = Prism.languages.javascript;

在创建递归语法时要小心,因为它们可能导致无限递归,从而导致堆栈溢出。

令牌名称

令牌的名称决定了令牌匹配文本的语义含义。令牌可以获取任何内容,从简单的语言结构(如注释)到更复杂的结构(如模板字符串插值表达式)。令牌名称区分这些语言结构。

理论上,令牌名称可以是任何有效的 CSS 类名称的字符串。但是,实际上,令牌名称遵循一些规则是有意义的。在 Prism 的代码中,我们强制所有令牌名称使用短横线分隔法(foo-bar)并且只包含小写 ASCII 字母、数字和连字符。例如,class-name是允许的,但Class_name是不允许的。

Prism 还定义了一些标准令牌名称,应该用于大多数令牌。

主题

Prism 的主题根据其名称(和别名)向令牌分配颜色(和其他样式)。这意味着语言定义不控制令牌的颜色,主题控制。

但是,主题仅支持已知令牌名称的有限数量。如果某个主题不知道某个令牌名称,则不会应用任何样式。尽管不同的主题可能支持不同的令牌名称,但所有主题都保证支持 Prism 的标准令牌。标准令牌是具有特定语义含义的特殊令牌名称。它们是所有语言定义和主题都同意且必须遵循的共同基础。在选择令牌名称时应该优先考虑标准令牌。

细粒度高亮显示

尽管应该优先选择标准令牌,但它们也相当普遍。这是因为设计为它们必须适用于大量和各种不同的语言,但有时需要更细粒度的符号化(和后续的高亮显示)。

细粒度高亮显示是一种选择令牌名称的方法,用于启用对主题的精细控制,同时确保与所有主题的兼容性。

让我们看一个示例。假设我们有一种同时支持数字的十进制和二进制字面量的语言,并且我们希望为二进制数提供特殊的高亮显示。我们可能会像这样实现它

Prism.languages['my-language'] = {
	// ...
	'number': /\b\d+(?:\.\d+)?\b/,
	'binary-number': /\b0b[01]+\b/,
};

但这存在问题。 binary-number 不是一个标准令牌,因此几乎没有主题会给二进制数任何颜色。

解决此问题的办法是使用别名

Prism.languages['my-language'] = {
	// ...
	'number': /\b\d+(?:\.\d+)?\b/,
	'binary-number': {
		pattern: /\b0b[01]+\b/,
		alias: 'number'
	},
};

别名允许主题对一个令牌应用多个名称的样式。这意味着确实支持 binary-number 令牌名称的主题可以分配一种特殊颜色,并且不支持该令牌名称的主题将回退到它们通常用于数字的颜色。

这是细粒度高亮显示:使用非标准令牌名称和标准令牌作为别名。

匹配算法

Prism 匹配算法的工作是根据语言定义和一些文本生成令牌流。令牌流是 Prism 对(部分或完全)符号化文本的表示,并且实现为字符串(表示字面文本)和令牌(表示符号化文本)的列表。

注意:此处“令牌”一词含糊不清。我们使用“令牌”来同时指语言定义的条目(如上述各节所述)和令牌流内的令牌对象。可以从上下文中推断出是指哪种类型的“令牌”。

本节将使用简化的令牌流符号。简单来说,该符号使用 JSON 表示令牌流。例如,["foo ", ["keyword", "bar"], " baz"] 是令牌流的简化令牌流符号,该令牌流以字符串 foo 开始,后跟类型为 keyword 和文本为 bar 的令牌,并以字符串 baz 结束。

回到匹配算法:Prism 的匹配算法是具有两种模式的混合算法:先到先得 (FCFS) 匹配和贪婪匹配。

FCFS 匹配

这是 Prism 的默认匹配模式。所有记号按顺序逐一匹配,记号不能重叠,且记号不能匹配已由其他记号匹配的文本。

算法本身非常简单。假设我们想要对其 JS 代码 max(3, 5, exp2(7)); 进行标记,并且函数记号已处理。当前记号流为

[
	["function", "max"],
	"(3, 5, ",
	["function", "exp2"],
	"(7));"
]

接下来,我们将使用记号 'number': /[0-9]+/ 对数字进行标记。

FCFS 匹配将遍历当前记号流中的所有字符串,以查找数字正则表达式的匹配项。第一个字符串是 "(3, 5, ",因此找到了匹配项 3. 为 3 创建了一个新记号,并将该记号插入记号流中以替换匹配文本。记号流现在为

[
	["function", "max"],
	"(",
	["number", "3"],
	", 5, ",
	["function", "exp2"],
	"(7));"
]

现在,该算法会转到下一个字符串 ", 5, ",并找到另一个匹配项。为 5 创建了一个新记号,记号流现在为

[
	["function", "max"],
	"(",
	["number", "3"],
	", ",
	["number", "5"],
	", ",
	["function", "exp2"],
	"(7));"
]

下一个字符串是 ", ",没有找到任何匹配项。之后的字符串是 "(7));",并为 7 创建了一个新记号

[
	["function", "max"],
	"(",
	["number", "3"],
	", ",
	["number", "5"],
	", ",
	["function", "exp2"],
	"(",
	["number", "7"],
	"));"
]

要检查的最后一个字符串是 "));",没有找到任何匹配项。现在已处理数字记号,该算法将继续处理语言定义中的下一个记号。

请注意 FCFS 匹配如何在 exp2 中未找到 2。由于 FCFS 匹配会完全忽略记号流中的现有记号,因此数字正则表达式无法看到已标记的文本。这是一个非常有用的属性。在上述示例中,2 是函数名 exp2 的一部分,因此将其突出显示为数字是不正确的。

贪婪匹配

贪婪匹配与 FCFS 匹配非常类似。所有记号按顺序匹配,且记号不能重叠。关键区别在于贪婪记号可以匹配前一个记号的文本。

让我们看一个示例,了解为什么贪婪匹配很有用以及它在概念上如何工作。JavaScript 注释和字符串语法的一个非常简化的版本可以像这样实现

Prism.languages.javascript = {
	'comment': /\/\/.*/,
	'string': /'(?:\\.|[^\\\r\n])*'/
};

为了理解贪婪匹配的用途,让我们看看 FCFS 匹配如何对文本 'http://example.com' 进行标记

FCFS 匹配从记号流 ["'http://example.com'"] 开始,并尝试查找 'comment': /\/\/.*/ 的匹配项。匹配项 //example.com' 已找到并插入记号流中

[
	"'http:",
	["comment", "//example.com'"]
]

然后,FCFS 匹配将搜索 'string': /'(?:\\.|[^'\\\r\n])*'/ 的匹配项。记号流的第一个字符串 "'http:" 不匹配字符串正则表达式,因此记号流保持不变。现已处理字符串记号,上述记号流是最终结果。

显然,这是不好的。代码 'http://example.com' 明显是只包含一个 URL 的字符串,但 FCFS 匹配不理解这一点。

一个显而易见的但错误的修复方法可能是将 commentstring 的顺序互换。这将修复 'http://example.com'。但是,问题只是转移了。类似于 // it's my co-worker's code(注意两个单引号)的注释现在会被不正确地标记化。

这是贪婪匹配解决的问题。让我们让令牌变得贪婪,然后看看这对结果有什么影响

Prism.languages.javascript = {
	'comment': {
		pattern: /\/\/.*/,
		greedy: true
	},
	'string': {
		pattern: /'(?:\\.|[^'\\\r\n])*'/,
		greedy: true
	}
};

虽然实际的贪婪匹配算法非常复杂,并包含微妙的极端情况,但其效果非常简单:贪婪令牌列表将表现得好像是由一个正则表达式匹配的。这就是贪婪匹配在概念上的工作方式,以及你应如何思考贪婪令牌的方式。

这意味着贪婪的注释和字符串令牌将表现得像以下语言定义一样,但组合的令牌将产生原始贪婪令牌的正确令牌名称

Prism.languages.javascript = {
	'comment-or-string': /\/\/.*|'(?:\\.|[^'\\\r\n])*'/
};

在上述示例中,'http://example.com' 将完全由 /\/\/.*|'(?:\\.|[^'\\\r\n])*'/ 匹配。由于正则表达式中的 '(?:\\.|[^'\\\r\n])*' 部分导致匹配,因此将创建一个类型为 string 的令牌,并将产生以下令牌流

[
	["string", "'http://example.com'"]
]

类似地,// it's my co-worker's code 示例中的标记化也将是正确的。

在决定令牌是否应贪婪时,请使用以下指导原则

  1. 大多数令牌都不是贪婪的。

    大多数语言中的大多数令牌都不是贪婪的,因为它们不需要贪婪。通常只有注释、字符串和正则表达式文本令牌需要贪婪。所有其他令牌都可以使用 FCFS 匹配。

    通常,令牌只有在能包含另一个令牌的开头时才应该是贪婪的。

  2. 贪婪令牌之前的所有令牌也应为贪婪。

    如果贪婪令牌之前有非贪婪令牌,那么贪婪匹配的工作方式会略有不同。这通常会导致难以发现的细微错误,有时需要数年才能发现。

    为了确保贪婪匹配按预期工作,贪婪令牌应为语言的第一个令牌。

  3. 贪婪令牌成组出现。

    如果一个语言定义只包含一个贪婪令牌,那么贪婪令牌不应贪婪。如上所述,贪婪匹配在概念上将所有贪婪令牌的正则表达式组合成一个正则表达式。如果只有一个贪婪令牌,那么贪婪匹配将表现得像 FCFS 匹配。

辅助函数

Prism还提供了一些用于创建和修改语言定义的有用函数。Prism.languages.insertBefore用于修改现有的语言定义。Prism.languages.extend则适用于你的语言与已有的其他语言非常相似的情况。

rest 属性

语言定义中的rest属性非常特别。Prism希望此属性是另一个语言定义,而不是一个标记。rest属性中语法所定义的标记将会附加到具有rest属性的语言定义的末尾。它可视为一个内置的对象扩展运算符。

这对于引用其他地方已定义的标记非常有用。不过,rest属性应谨慎使用。在引用其他语言时,通常最好将该语言的文本封装到某个标记内,并改用inside属性

创建新的语言定义

本部分将解释创建新的语言定义的常规流程。

我们以虚构的Foo's Binary, Artistic Robots™语言,简称Foo Bar为例,来创建其语言定义。

  1. 创建新文件components/prism-foo-bar.js

    本例中,我们选择foo-bar作为新语言的id。语言id必须唯一,并且要匹配Prism用来引用语言定义的language-xxxxCSS类名称。理想情况下,你的语言id应匹配正则表达式/^[a-z][a-z\d]*(?:-[a-z][a-z\d]*)*$/

  2. 编辑components.json,通过将其添加到languages对象中来注册新语言。(请注意,按照标题对所有语言条目按字母顺序排序。)
    本例中的新条目如下所示

    "foo-bar": {
    	"title": "Foo Bar",
    	"owner": "Your GitHub name"
    }

    如果你的语言定义依赖于任何其他语言,还必须在此指定此依赖性,方法是添加一个"require"属性。例如,"require": "clike""require": ["markup", "css"]。有关依赖性的更多信息,请阅读声明依赖性部分。

    注意:components.json所做的任何更改都需要重新构建(参见步骤3)。

  3. 通过运行npm run build重建Prism。

    这会使你的语言可用于测试页面,或者更准确地说:可用于你本地版本的测试页面。你可以在任何浏览器中打开本地的test.html页面,选择你的语言,然后了解你的语言定义将如何高亮显示任何你输入的代码。

    注意:您必须重新加载测试页面,才能应用对 prism-foo-bar.js 中的更改,但不必重新构建 Prism 自身。然而,如果您更改 components.json(例如,因为您添加了一个依赖项),那么除非您重新构建 Prism,否则这些更改不会显示在测试页面上。

  4. 编写语言定义。

    上述部分已经阐述了语言定义的组成。

  5. 添加别名。

    如果您的语言以不止一个名称广为人知,或者有您的语言的非常常见的缩写(例如 JavaScript 的 JS),那么使用别名就很有用了。请记住,别名与语言 ID 非常相似,因为它们也必须是唯一的(即不能有与语言 ID 的另一个别名相同的别名),并且可用作 CSS 类名称。

    在本例中,我们将为 foo-bar 注册别名 foo,因为 Foo Bar 代码存储在 .foo 文件中。

    要添加别名,我们在 prism-foo-bar.js 末尾添加此行

    Prism.languages.foo = Prism.languages['foo-bar'];

    别名还必须在 components.json 中注册,方法是将 alias 属性添加到语言条目。在本例中,更新后的条目会像这样

    "foo-bar": {
    	"title": "Foo Bar",
    	"alias": "foo",
    	"owner": "Your GitHub name"
    }

    注意:如果您需要注册多个别名,alias 也可以是字符串数组。

    使用 aliasTitles,也可以给别名指定特定标题。在本例中,没有这个必要,但 markup 语言是一个很好的示例,说明此处它非常有用

    "markup": {
    	"title": "Markup",
    	"alias": ["html", "xml", "svg", "mathml"],
    	"aliasTitles": {
    		"html": "HTML",
    		"xml": "XML",
    		"svg": "SVG",
    		"mathml": "MathML"
    	},
    	"option": "default"
    }
  6. 添加测试。

    创建一个文件夹 tests/languages/foo-bar/。您的测试文件将存放在此文件夹中。测试格式和如何运行测试已在此处描述here

    您应该为您的语言的每个主要功能添加一个测试。测试文件应该测试典型情况和特定极端情况(如果有的)。一些很好的示例是JavaScript 语言测试

    您可以为新的 .test 文件使用此模板

    The code to test.
    
    ----------------------------------------------------
    
    ----------------------------------------------------
    
    Brief description.

    对于每个测试文件

    1. 添加要测试的代码和简短描述。

    2. 验证您的语言定义是否正确地高亮显示了测试代码。可以使用测试页面的本地版本进行此操作。
      注意:使用显示标记选项,您会看到您的语言定义创建的标记流。

    3. 一旦您仔细检查测试用例已经得到正确处理(即通过使用测试页面),运行以下命令

      npm run test:languages -- --language=foo-bar --accept

      此命令将获取您的语言定义当前产生的标记流并将其插入测试文件中。分隔代码和测试用例描述的两行之间的空行都将替换为标记流的简化版本

    4. 仔细检查插入的标记流 JSON 是否为您的预期内容。

    5. 重新运行 npm run test:languages -- --language=foo-bar 以验证该测试已通过。
  7. 添加示例页面。

    新建一个文件 examples/prism-foo-bar.html。这将是模板,包含示例标记。看看其他示例即可了解这些文件如何构建。
    没有关于什么算作示例的规则,因此一个展示语言的主要功能高亮的单个完整示例部分就足够了。

  8. 运行 npm test 检查是否所有测试通过,不仅是语言测试。
    这通常会在没有问题的情况下通过。如果无法使所有测试通过,则跳过此步骤。

  9. 再次运行 npm run build

    语言定义现在准备就绪!

依赖关系

语言和插件可以互相依赖,因此 Prism 拥有自己的依赖关系系统以声明并解决依赖关系。

声明依赖关系

可以在 components.json 文件的语言或插件入口中添加一个属性,从而声明依赖关系。属性的名称将是依赖关系的类型,其值将是依赖项的组件 id。如果依赖多个语言或插件,还可以声明组件 id 的数组。

在以下示例中,我们将使用 require 依赖关系类型声明虚构语言 Foo 依赖 JavaScript 语言,而另一个虚构语言 Bar 依赖 JavaScript 和 CSS。

{
	"languages": {
		"javascript": { "title": "JavaScript" },
		"css": { "title": "CSS" },
		...,
		"foo": {
			"title": "Foo",
			"require": "javascript"
		},
		"bar": {
			"title": "Bar",
			"require": ["css", "javascript"]
		}
	}
}

依赖关系类型

有 3 种类型的依赖关系

require
Prism 将确保在依赖项之前加载所有依赖项。
除非依赖项还声明为 modify,否则不允许修改依赖项。

如果你扩展另一种语言或依赖项作为嵌入式语言(例如像 PHP 嵌入在 HTML 中一样),这种类型的依赖关系非常有用。

optional
如果已加载依赖项,Prism 将确保在依赖项之前加载可选依赖项。与 require 依赖关系(还保证加载依赖项)不同,optional 依赖关系仅保证已加载组件的顺序。
除非你需要修改可选依赖项,否则不允许修改依赖项。如果需要修改可选依赖项,请将其声明为 modify

这种类型的依赖关系在你具有嵌入式语言但希望让用户选择是否包含嵌入式语言时非常有用。通过使用 optional 依赖关系,用户可以通过仅包含所需的语言来更好地控制 Prism 的绑定大小。
例如 HTTP 可以高亮 JSON 和 XML 负载,但它不会强制用户包含这些语言。

modify
这是一个 optional 依赖关系,它还声明依赖项可能会修改依赖项。

如果你语言修改另一种语言(例如通过添加令牌),这种类型的依赖关系非常有用。
例如 CSS Extras 向 CSS 语言添加了新的令牌。

总结不同依赖类型的属性

非可选 可选
只读 require optional
可修改 modify

注意:可以将部件声明为requiremodify

解析依赖关系

我们认为组件的依赖关系是实现细节,因此它们可能会随着每次版本发布而发生更改。Prism 通常会自动帮您解析依赖关系。因此如果您下载一个捆绑包或在 NodeJS 中使用loadLanguages函数、AutoLoader或我们的 Babel 插件,则不必担心依赖关系加载。

如果您需要自己解析依赖关系,请使用dependencies.js导出的getLoader函数。举例

const getLoader = require('prismjs/dependencies');
const components = require('prismjs/components');

const componentsToLoad = ['markup', 'css', 'php'];
const loadedComponents = ['clike', 'javascript'];

const loader = getLoader(components, componentsToLoad, loadedComponents);
loader.load(id => {
	require(`prismjs/components/prism-${id}.min.js`);
});

有关getLoader API 的更多详细信息,请参阅行内文档

编写插件

Prism 的插件架构非常简单。要添加回调,请使用Prism.hooks.add(hookname, callback)hookname是一个带有钩子 ID 的字符串,可唯一标识代码应运行的钩子。callback是一个接受一个参数的函数:一个带有可修改的各种变量的对象,因为 JavaScript 中的对象通过引用传递。例如,以下是来自 Markup 语言定义的插件,它将工具提示添加到实体标记中,其中显示实际编码的字符

Prism.hooks.add('wrap', function(env) {
	if (env.token === 'entity') {
		env.attributes['title'] = env.content.replace(/&/, '&');
	}
});

当然,要了解应使用哪些钩子,您必须阅读 Prism 的源代码。想象一下您将在哪里添加代码,然后找到相应的钩子。如果没有可以使用的钩子,您可以请求添加一个,详细说明为什么需要将它添加在那里。

API 文档

Prism API 的所有公共和稳定部分在此处记录