閒談Monaco Editor-自定義語言之Monarch

什麼是Monarch

Monarch 是 Monaco Editor 自帶的一個語法高亮庫,經過它,咱們能夠用相似 Json 的語法來實現自定義語言語法高亮功能。本文將經過編寫一個簡單的自定義日誌語言(下文簡稱 log )來介紹 Monarch 的使用。javascript

開始

初始化

首先,咱們須要在 monaco 裏註冊一下咱們的 log 語言。css

monaco.languages.register({ id: 'log' });
複製代碼

很簡單,咱們只須要傳入語言的 id 便可,可是,如今這個語言除了有個名字,還空空如也,因此,接下來,咱們就要開始給 log 語言加上咱們的語法高亮功能。html

monaco.languages.setMonarchTokensProvider('log', monarchObj);
複製代碼

monaco 提供了setMonarchTokensProvider函數來讓我定義語言的高亮功能,而monarchObj就是咱們所須要填寫的 Monarch 所規定的 Json 內容。java

Monarch

Monarch 由一系列 Json 鍵值對組成,他有許多屬性,其中最重要的就是 tokenizer 屬性,咱們描述語法的代碼就寫在這裏面。先來看一個簡單的例子:git

monaco.languages.setMonarchTokensProvider('log', {
	tokenizer: {
		root:[
			[/\d+/,{token:"keyword"}],
			[/[a-z]+/,{token:"string"}]
		],
	}
});
複製代碼

咱們在 tokenizer 中定義了一個 root 屬性,roottokenizer 中的一個 state , 這就是咱們用來編寫解析規則(rule)的地方,在 rule 中,咱們能夠編寫匹配文本的正則表達式,而後再給匹配到的文本設置一個執行動做的 action ,在 action 中,咱們能夠給匹配到的文本設置 token classgithub

在咱們的例子中,咱們在 root 中設置了兩個 rule ,分別用來匹配數字字母,匹配成功後就接着執行對應的 action ,最後在 action 中,咱們設置了匹配文本的 token class :keywordstring。最終效果如圖: 正則表達式

這裏有些人就會有疑問,token class 是 css class 嗎?本質上,token class 其實就是設置 css 中的 class ,不過,keyword != .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]
複製代碼

不過上面的高亮代碼還存在一點問題json

咱們發現大寫沒有識別出來,這時,咱們能夠再給完善如下匹配字符串的 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"}]
	    ],
    }
});
複製代碼

最終效果以下: curl

瞭解了 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

不過,還有一個小問題,那就是除了errorinfowarning這三個日誌類型,其他的 [debug][test] 也會被匹配進去。這時候,咱們須要引入一個新的工具:cases

{ cases: { guard1: action1, ..., guardN: actionN } }
複製代碼

cases 和普通的 if ,else if 語法同樣,能夠寫多個判斷條件(guard),而後根據不一樣 guard 去執行對應的 action

guard 和正則表達式相似,功能是用來匹配文本,當他不以 @$ 開頭時,他就是一個普通的正則表達式,不過,當他以 @$ 開頭時,他纔是一個真正意義上的 guard 。

guard 有固定的結構 [pat][op]matchpat 表明匹配的文本op 表明一個比較符match 則是要比較的內容

pat$ 開頭,和咱們上文正則表達式使用的 $1 含義是同樣的,不過這邊 $# 表明所有匹配文本,而正則表達式是使用 $0 表明所有匹配文本。另外,咱們還能夠用 $Sn 來獲取當前 state的名字,例如在 root state 下 $S0 就表明 root

opmatch 稍微複雜點,能夠是這幾個內容

  • ~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 提供的功能,例如括號匹配,語言嵌入等,也還有許多細節點未列出,同窗們若是有興趣想深刻研究,能夠閱讀官方文檔與示例

相關文章
相關標籤/搜索