什麼是Monarch
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 內容。
Monarch
Monarch 由一系列 Json 鍵值對組成,他有許多屬性,其中最重要的就是 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
,Monarch 中會有一層對應關系,keyword 對應著 css 中的 .mtk8
,而 string 對應著 css 中的 .mtk5
。Monarch 中內建了以下幾種 token class:
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
屬性。
monaco.languages.setMonarchTokensProvider('log', {
ignoreCase: true,
tokenizer: {
root:[
[/\d+/, {token: "keyword"}],
[/[a-z]+/, {token: "string"}]
],
}
});
複製程式碼
最終效果如下:
瞭解了 Monarch 的基本結構,下面,我們就開始正式編寫 log 語言。log 語言
我們要實現的 log 語言主要是用來區分顯示不同型別的日誌,大體效果如下:
我們以[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 都是類似的,所以,我們可以想辦法把他們合在一起。
tokenizer: {
root: [
[/^\[(\w+)\]/, { token: "custom-$1" }]
]
}
複製程式碼
這裡我們用到了一個美元符號 $
,它代表取正規表示式第幾個匹配項,$0
代表取所有的匹配項(例:[error]),$1
代表取第一個匹配項(例:error)。上述程式碼將日誌型別作為引數傳入了 token class ,與 custom-
做拼接,從而組成了最終的 token class,例如 custom-error
。
不過,還有一個小問題,那就是除了error
,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 以 $
開頭,和我們上文正規表示式使用的 $1
含義是一樣的,不過這邊 $#
代表全部匹配文字,而正規表示式是使用 $0
代表全部匹配文字。另外,我們還可以用 $Sn
來獲取當前 state的名字,例如在 root state 下 $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
來匹配未定義的日誌型別。最終效果如下:
標記日誌
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。
這裡還有一個 @rematch
,意思是,匹配到了當前文字,但是,不做任何操作,讓後續的 rule 再匹配一次。
總結起來,上述程式碼的邏輯就是匹配到日誌型別之後,我們攜帶著日誌型別($1) 進入到了 text state ,在 text state 中,我們將後續文字(.*) 都標記成和 日誌型別相同的 token class ,然後在遇到日誌型別標記之後,利用 @rematch
和 @pop
重新回到 root state 再次執行匹配。效果如下:
{
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
的 @
不同,他只支援正規表示式,而不支援陣列型別。
結尾
本文介紹了 Monarch 的基本概念和使用方法,不過篇幅有限,本文無法介紹其他 Monarch 提供的功能,例如括號匹配,語言嵌入等,也還有許多細節點未列出,同學們如果有興趣想深入研究,可以閱讀官方文件與示例。