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) 配對和貪婪配對。
這是 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 配對並不懂這點。
一個顯而易見但錯誤的修正方式是交換 comment
和 string
的順序。這可以修正 '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
範例的標記化也會是正確的。
在決定一組記號是否應為貪婪時,請使用下列指南
大多數記號不是貪婪的。
大多數語言中大多數的記號都不貪婪,因為它們不需要。通常只會需要註解、字串和正規表示法文字記號是貪婪的。所有其他記號可以使用 FCFS 匹配。
通常,一個記號僅在它能包含其他記號的開頭時才是貪婪的。
所有在貪婪記號之前的記號也應為貪婪。
如果在貪婪記號之前有非貪婪記號的話,貪婪匹配運作的方式會有些微不同。這通常會導致微妙且難以捉摸的錯誤,有時需要花上好幾年才能發現。
為了確保貪婪匹配能預期運作,貪婪記號應為語言的第一組記號。
貪婪記號以群組出現。
如果一個語言定義只包含單一貪婪記號,那麼該記號不應為貪婪。正如上述所解釋的,貪婪匹配在概念上會將所有貪婪記號的正規表示法結合為一。如果只有一個貪婪記號,貪婪匹配會表現得像 FCFS 匹配。
Prism 也提供一些有用的函式,以建立和修改語言定義。 Prism.languages.insertBefore
可用於修改現有的語言定義。Prism.languages.extend
在你的語言與現有的其他語言非常相似時會很有用。
語言定義中的 rest
屬性是特殊的。Prism 會期待這個屬性是另一個語言定義而非一個記號。rest
屬性中的文法記號會被追加至語言定義的結尾,並具有 rest
屬性。它可以被視為內建的物件擴散運算子。
這對於參照其他地方定義的記號會很有用。但是,rest
屬性應謹慎使用。在參照其他語言時,通常會比較好將該語言的文字封裝至一個記號中,並改用 inside
屬性。
本節將說明建立新的語言定義的一般工作流程。
舉例來說,我們將建立虛構的Foo's Binary, Artistic Robots™ 語言的語言定義,簡稱為 Foo Bar。
建立一個新的檔案 components/prism-foo-bar.js
.
在這個範例中,我們選擇 foo-bar
作為新語言的 ID。語言 ID 必須是唯一的,且應與 Prism 用來參照語言定義的 CSS 類別名稱 language-xxxx
搭配良好。你的語言 ID 理想上應符合正規表示法 /^[a-z][a-z\d]*(?:-[a-z][a-z\d]*)*$/
.
編輯 components.json
加入新語言到 languages
物件中來註冊新的語言。(請注意,所有語言項目會依標題字母順序排列。)
針對這個範例,我們的新項目會像這樣
"foo-bar": {
"title": "Foo Bar",
"owner": "Your GitHub name"
}
如果你的語言定義依賴於任何其他語言,你必須在此加入一個 "require"
屬性來指定此依賴關係。例如: "require": "clike"
,或 "require": ["markup", "css"]
。有關相依性的詳細資訊,請參閱 宣告相依關係 部分。
注意事項:對 components.json
進行的任何變更都需要重新建置(請參閱步驟 3)。
執行 npm run build
來重新建置 Prism。
這會讓你的語言對 測試頁面(更確切地說是它的本機版本)可用。你可以使用任何瀏覽器開啟本機的 test.html
頁面,選取你的語言,然後觀看你的語言定義如何突顯你輸入的任何程式碼。
注意事項:你必須重新載入測試頁面才能套用對 prism-foo-bar.js
所做的變更,但你不必重新建置 Prism 本身。不過,如果你變更了 components.json
(例如因為你加入了一個相依關係),在重新建置 Prism 之前,這些變更不會出現在測試頁面上。
撰寫語言定義。
上面的部分 已經說明了語言定義的組成。
加入別名。
如果你的語言除了主要名稱之外還有別名,或是有很常見的縮寫(例如 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"
}
加入測試。
建立資料夾 tests/languages/foo-bar/
。你的測試檔案會存在這個資料夾中。測試格式以及如何執行測試的說明 在這裡。
你應該為你的語言的每一個主要功能加入一個測試。測試檔案應該測試一般情況和某些邊界情況(如果有的話)。一些很好的範例是 JavaScript 語言的測試。
你可以使用這個範本建立新的 .test
檔案
The code to test.
----------------------------------------------------
----------------------------------------------------
Brief description.
對於每個測試檔案
加入要測試的程式碼和簡要說明。
驗證你的語言定義是否正確突顯了測試程式碼。你可以使用測試頁面的本機版本進行這項工作。
注意事項:使用 顯示代碼 選項,你可以看到你的語言定義建立的代碼串流。
在 仔細確認 測試案例已正確處理(也就是透過使用測試頁面)後,執行下列指令
npm run test:languages -- --language=foo-bar --accept
此命令會取得你的語言定義目前產生的代碼流,並將其插入到測試檔案。分隔代碼與測試範例說明兩行的空白會以 代碼流簡化版 取代。
仔細確認插入的代碼流 JSON 是否符合預期。
npm run test:languages -- --language=foo-bar
以驗證測試是否通過。新增範例頁面。
建立新檔案 examples/prism-foo-bar.html
。這將是包含範例標記的範本。只要查看其他範例就能了解這些檔案的結構。
我們沒有任何規則定義什麼算範例,因此只要一個 完整範例 區段即可,其中顯示該語言主要功能的重點。
執行 npm test
檢查 所有 測試是否通過,而不仅仅是你的语言測試。
通常情況下,這會順利通過。如果你無法讓所有測試都通過,請跳過此步驟。
再次執行 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
modify
。此種類的相依最常使用在例如延伸另一種語言或依賴元件的情況,例如 PHP 嵌入在 HTML 中。
optional
require
相依關係(這保證也會載入被依賴元件)不同,optional
相依關係僅保證載入組件的順序。modify
。如果你有嵌入式語言,但希望使用它時讓用戶能進行選擇,則可以使用此類似的相依性。透過使用 optional
相依關係,使用者可以只包含他們需要的語言來更好地控制 Prism 的套件大小。
例如,HTTP 可以重點顯示 JSON 和 XML 負載,但它不會強制使用者包含這些語言。
modify
optional
相依關係,它也宣告依賴元件可能會修改被依賴元件。如果你讓語言修改另一種語言(例如透過新增代碼),則可以使用此類型的相依性。
例如,CSS Extras 會為 CSS 語言新增新的代碼。
以下是不同相依類型的屬性摘要
非選用 | 選用 | |
---|---|---|
唯讀 | require |
optional |
可修改 | modify |
注意:你可以將一個組件宣告為 require
和 modify
。
我們將元件的依賴關係視為實作細節,因此它們可能會因版本而異。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 的原始程式碼。想像一下你會在哪裡新增你的程式碼,然後找出適當的掛鉤。如果你找不到可用的掛鉤,你可以要求新增一個,詳細說明你為什麼需要它出現在那裡。
所有。