Monarch 是 Monaco Editor 自帶的一個語法高亮庫,通過它,我們可以用類似 Json 的語法來實現自定義語言的語法高亮功能。本文將通過編寫一個簡單的自定義日誌語言(下文簡稱 log )來介紹 Monarch 的使用。
首先,我們需要在 monaco 里註冊一下我們的 log 語言。
monaco.languages.register({ id: log });
很簡單,我們只需要傳入語言的 id 即可,但是,現在這個語言除了有個名字,還空空如也,所以,接下來,我們就要開始給 log 語言加上我們的語法高亮功能。
monaco.languages.setMonarchTokensProvider(log, monarchObj);
monaco 提供了setMonarchTokensProvider函數來讓我定義語言的高亮功能,而monarchObj就是我們所需要填寫的 Monarch 所規定的 Json 內容。
setMonarchTokensProvider
monarchObj
Monarch 由一系列 Json 鍵值對組成,他有許多屬性,其中最重要的就是 tokenizer 屬性,我們描述語法的代碼就寫在這裡面。先來看一個簡單的例子:
tokenizer
monaco.languages.setMonarchTokensProvider(log, { tokenizer: { root:[ [/d+/,{token:"keyword"}], [/[a-z]+/,{token:"string"}] ], } });
我們在 tokenizer 中定義了一個 root 屬性,root 是 tokenizer 中的一個 state , 這就是我們用來編寫解析規則(rule)的地方,在 rule 中,我們可以編寫匹配文本的正則表達式,然後再給匹配到的文本設置一個執行動作的 action ,在 action 中,我們可以給匹配到的文本設置 token class 。
在我們的例子中,我們在 root 中設置了兩個 rule ,分別用來匹配數字和字母,匹配成功後就接著執行對應的 action ,最後在 action 中,我們設置了匹配文本的 token class :keyword和string。最終效果如圖:
keyword
string
這裡有些人就會有疑問,token class 是 css class 嗎?本質上,token class 其實就是設置 css 中的 class ,不過,keyword != .keyword ,Monarch 中會有一層對應關系,keyword 對應著 css 中的 .mtk8,而 string 對應著 css 中的 .mtk5。Monarch 中內置了以下幾種 token class:
.keyword
.mtk8
.mtk5
identifier entity constructor operators tag namespace keyword info-token type string warn-token predefined string.escape error-token invalid comment debug-token comment.doc regexp constant attribute
delimiter .[curly,square,parenthesis,angle,array,bracket] number .[hex,octal,binary,float] variable .[name,value] meta .[content]
不過上面的高亮代碼還存在一點問題
我們發現大寫沒有識別出來,這時,我們可以再給完善以下匹配字元串的 rule 正則表達式。
tokenizer: { root:[ [/d+/,{token:"keyword"}], [/[a-zA-Z]+/,{token:"string"}] ], }
假如我們的語言是忽略大小寫的,那麼,我們可以直接添加一條 ignoreCase 屬性。
ignoreCase
monaco.languages.setMonarchTokensProvider(log, { ignoreCase: true, tokenizer: { root:[ [/d+/, {token: "keyword"}], [/[a-z]+/, {token: "string"}] ], } });
最終效果如下:
了解了 Monarch 的基本結構,下面,我們就開始正式編寫 log 語言。
我們要實現的 log 語言主要是用來區分顯示不同類型的日誌,大體效果如下:
我們以 [error], [info], [warning] 作為一行的開頭,從而代表日誌的級別。如圖所示, error 後的日誌將全部為紅色,直到遇到下一個日誌級別。
[error]
[info]
[warning]
error
首先,我們來標記一下[error],[info]這些日誌級別的顯示。
tokenizer: { root: [ [/^[error]/, { token: "custom-error" }], [/^[info]/, { token: "custom-info" }], [/^[warning]/, { token: "custom-warning" }] ] } //設置含有custom-error等token class的主題 monaco.editor.defineTheme(logTheme, { base: vs, inherit: true, rules: [ { token: custom-info, foreground: 808080 }, { token: custom-error, foreground: ff0000, fontStyle: bold }, { token: custom-warning, foreground: FFA500 } ] }); monaco.editor.create(document.getElementById("container"), { theme: logTheme, value: getCode(), language: log });
我們寫了三條 rule ,分別將 [error] 標記為 custom-error ,[info] 標記為 custom-info ,[warning] 標記為 custom-warning 。我們發現,這些 rule 都是類似的,所以,我們可以想辦法把他們合在一起。
custom-error
custom-info
custom-warning
tokenizer: { root: [ [/^[(w+)]/, { token: "custom-$1" }] ] }
這裡我們用到了一個美元符號 $,它代表取正則表達式第幾個匹配項,$0代表取所有的匹配項(例:[error]),$1 代表取第一個匹配項(例:error)。上述代碼將日誌類型作為參數傳入了 token class ,與 custom- 做拼接,從而組成了最終的 token class,例如 custom-error 。
$
$0
$1
custom-
不過,還有一個小問題,那就是除了error,info,warning這三個日誌類型,其餘的 [debug],[test] 也會被匹配進去。這時候,我們需要引入一個新的工具:cases。
info
warning
[debug]
[test]
cases
{ cases: { guard1: action1, ..., guardN: actionN } }
cases 和普通的 if ,else if 語法一樣,可以寫多個判斷條件(guard),然後根據不同 guard 去執行對應的 action 。
guard 和正則表達式類似,功能是用來匹配文本,當他不以 @ 或 $ 開頭時,他就是一個普通的正則表達式,不過,當他以 @ 或 $ 開頭時,他才是一個真正意義上的 guard 。
@
guard 有固定的結構 [pat][op]match,pat 代表匹配的文本,op 代表一個比較符,match 則是要比較的內容。
[pat][op]match
pat 以 $ 開頭,和我們上文正則表達式使用的 $1 含義是一樣的,不過這邊 $# 代表全部匹配文本,而正則表達式是使用 $0 代表全部匹配文本。另外,我們還可以用 $Sn 來獲取當前 state的名字,例如在 root state 下 $S0 就代表 root。
$#
$Sn
$S0
root
op 和 match 稍微複雜點,可以是這幾個內容 ~regex or !~regex :匹配/不匹配一個正則 @attribute or !@attribute :匹配/不匹配一個屬性,屬性定義在 Monarch 的根層級下,可以是數組、字元串、正則。 ==str or !=str :匹配/不匹配一個字元串 @default :匹配默認情況 * @eos : 一行結束,則匹配成功
有了這些工具,我們可以接著寫我們的高亮代碼
{ keywords: [error, info, warning], tokenizer: { root: [ [/^[(w+)]/, { cases: { "$1@keywords": { token: custom-$1 }, "@default": { token: "string" } } }] ] } }
這裡,我們用到了 $1@keywords 來判斷日誌類型($1) 是否存在於 keywords 數組中,還用到了 @default 來匹配未定義的日誌類型。最終效果如下:
$1@keywords
keywords
@default
到了這裡,我們終於完成了日誌類型的高亮,接下來,就可以開始處理日誌了
tokenizer: { root: [ [/^[(w+)]/, { cases: { "$1@keywords": {token:custom-$1, next:"@text.$1"}, "@default":string } }], ], text:[ [/^[(w+)]/,{token:"@rematch",next:"@pop"}], [/.*/,{token:"custom-$S2"}] ] }
這裡第一次出現了 next: "@text.$1",意思是由當前 root state 跳入 text state,並且把 root state 放入 tokenizer 棧中,在 text state 中,我們又可以通過 next:@pop 回到棧的第一個 state 中,也就是我們的 root state。
next: "@text.$1"
next:@pop
這裡還有一個 @rematch,意思是,匹配到了當前文本,但是,不做任何操作,讓後續的 rule 再匹配一次。
@rematch
總結起來,上述代碼的邏輯就是匹配到日誌類型之後,我們攜帶著日誌類型($1) 進入到了 text state ,在 text state 中,我們將後續文本(.*) 都標記成和 日誌類型相同的 token class ,然後在遇到日誌類型標記之後,利用 @rematch 和 @pop 重新回到 root state 再次執行匹配。效果如下:
@pop
再進一步,我們可以簡化一下代碼
{ keywords: [error, warning, info], header: /[(w+)]/, tokenizer: { root: [ [/^@header/, { cases: { "$1@keywords": { token: custom-$1, next: "@text.$1" }, "@default": string } }], ], text: [ [/^@header/, { token: "@rematch", next: "@pop" }], [/.*/, { token: "custom-$S2" }] ] } }
我們將匹配日誌類型的正則表達式提取為一個單獨的 header ,然後通過 @ 來嵌入。但是這裡的 @ 和 guard的 @ 不同,他只支持正則表達式,而不支持數組類型。
guard
本文介紹了 Monarch 的基本概念和使用方法,不過篇幅有限,本文無法介紹其他 Monarch 提供的功能,例如括弧匹配,語言嵌入等,也還有許多細節點未列出,同學們如果有興趣想深入研究,可以閱讀官方文檔與示例。
推薦閱讀: