擴展 Prism

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

此選項啟用了代碼的貪婪比對。若要取得更多資訊,請參閱 比對演算法 的部分。

alias: string | string[]

可以使用這個選項為代碼定義一個或多個別名。結果會將代碼名稱和別名的樣式結合。這對於結合 標準代碼(大部分的佈景主題支援)和更精確代碼名稱的樣式很有用。若要取得關於此主題的更多資訊,請參閱 細緻突顯

例如,latex-equation 代碼名稱在大多數佈景主題中不受支援,但是會以下列範例以字串的方式突顯

Prism.languages.latex = {
	// ...
	'latex-equation': {
		pattern: /\$.*?\$/,
		alias: 'string'
	}
};
inside: Grammar

此選項接受另一個物件文字,其中包含允許巢狀在此代碼中的代碼。inside 語法的代碼全部會被此代碼封裝。這樣會比較容易定義特定的語言。

有關巢狀代碼範例,請在 CSS 語言定義中查看網址代碼

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

內部選項也可以用於建立遞迴語言。這適用於代碼中一個代碼可包含任意運算式的語言,例如語法具有字串內插功能的語言。

例如,以下是 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 屬性是特殊的。Prism 會期待這個屬性是另一個語言定義而非一個記號。rest 屬性中的文法記號會被追加至語言定義的結尾,並具有 rest 屬性。它可以被視為內建的物件擴散運算子。

這對於參照其他地方定義的記號會很有用。但是,rest 屬性應謹慎使用。在參照其他語言時,通常會比較好將該語言的文字封裝至一個記號中,並改用 inside 屬性。

建立新的語言定義

本節將說明建立新的語言定義的一般工作流程。

舉例來說,我們將建立虛構的Foo's Binary, Artistic Robots™ 語言的語言定義,簡稱為 Foo Bar。

  1. 建立一個新的檔案 components/prism-foo-bar.js.

    在這個範例中,我們選擇 foo-bar 作為新語言的 ID。語言 ID 必須是唯一的,且應與 Prism 用來參照語言定義的 CSS 類別名稱 language-xxxx 搭配良好。你的語言 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),那麼別名就會很有用。請注意,別名在於它們也必須是唯一的(也就是說,不能有與其他語言識別碼或別名相同的別名)以及可用作 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": {
    	"title": "Markup",
    	"alias": ["html", "xml", "svg", "mathml"],
    	"aliasTitles": {
    		"html": "HTML",
    		"xml": "XML",
    		"svg": "SVG",
    		"mathml": "MathML"
    	},
    	"option": "default"
    }
  6. 加入測試。

    建立資料夾 tests/languages/foo-bar/。你的測試檔案會存在這個資料夾中。測試格式以及如何執行測試的說明 在這裡

    你應該為你的語言的每一個主要功能加入一個測試。測試檔案應該測試一般情況和某些邊界情況(如果有的話)。一些很好的範例是 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 通常會自動幫你解決依賴關係。因此,如果你下載套件或在 Node.js 中使用 loadLanguages 函式、自動載入器或我們的 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(hookID, 回呼)hookID 是包含掛鉤識別碼的字串,可唯一識別你的程式碼應執行的掛鉤。回呼 是接受一個參數的函式:一個包含各種變數的物件,這些變數可以進行修改,因為 JavaScript 中的物件是透過參照傳遞的。例如,以下是標記語言定義中的外掛程式,它會在實體代碼中加入提示工具,顯示實際編碼的字元:

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

當然,你要了解該使用哪些掛鉤,必須先閱讀 Prism 的原始程式碼。想像一下你會在哪裡新增你的程式碼,然後找出適當的掛鉤。如果你找不到可用的掛鉤,你可以要求新增一個,詳細說明你為什麼需要它出現在那裡。

API 文件

所有