由於我在 pixnet 部落格裡撰寫程式及網頁技術的文章, 給程式上色這個需求一直都是使用套件 highlight.js. 不過有幾個地方總是不那麼滿意: 像是用於 linux 指令或批次檔的 <code class="language-bash"> 或者用於 Windows 命令提示字元 (CMD 指令或批次檔) 的 <code class="language-dos">, 它們總是會少幾個指令沒有上到色. 下面幾個方法是我這幾年來自己琢磨出來的修正 bash 和 dos 這二類程式區塊上色問題的辦法, 在此分享給有需要的朋友.
方法一: 直接修改上色的部份
這個方法適合用於補足少量 highlight.js 沒有上色的部份. 這是最直觀的方法, 而且不限制被上色的語言, 我們只要將想要上色的部份用 <span> tag 括住, 並加上需要的 style 即可. 或者也可以用預設 display 為 inline 的元素 (<b>, <i>...等) 修改字型變化, 請看下例的 HTML 上色片斷:
一般文字 <i style="color:red">text_to_use_red_color</i> 一般文字
一般文字 <span class="hljs-built_in">text_to_use_as_built_in</span> 一般文字
上色後的效果如下:
一般文字 text_to_use_red_color 一般文字
一般文字 text_to_use_as_built_in 一般文字
除了用 style 更動 color (顏色), font-weight (字重, 粗體), font-style (斜體). 我們也可以像第二行那樣套用 highlight.js 的標準化 CSS class: class="hljs-xxxx". 其中的 xxxx 是 highlight.js 解析出來的範圍 scope, 如下表:
Scope name 範圍名稱 |
Description | 說明 |
---|---|---|
General purpose 一般用途類 | ||
keyword | keyword in a regular Algol-style language | 一般 "類 algol" 語言的關鍵字 (或稱保留字) |
built_in | built-in or library object (constant, class, function) | 語言的內建 (功能), 或者 (標準) 函式庫裡的物件 (如: const, class, function) |
type | data type (in a language with syntactically significant types) (string , int , array , etc.) |
資料型態 (語言中具語法意義的資料型態), 如: string (字串), int (整數), array (陣列). |
literal | special identifier for a built-in value (true , false , null , etc.) |
字面值 (定數). 用來表示內建值的特殊識別字. 如: true (真), false (偽), null (空值) |
number | number, including units and modifiers, if any. | 數值, 包含其單位及修飾詞, 如果存在的話. |
operator | operators: + , - , >> , | , == |
運算子. 例如: +, -, >>, |, ==...等等 |
punctuation | aux. punctuation that should be subtly highlighted (parentheses, brackets, etc.) | 標點符號. 應該被精巧上色的輔助標點. 如: 圓括號 (parentheses), 方括號 (brackets). |
property | object property obj.prop1.prop2.value |
物件的 (固有) 特性. |
regexp | literal regular expression | 字面正規表示式 (不是 "字串" 轉換的那種) |
string | literal string, character | 字面字串, 字元. |
char.escape | an escape character such as \n |
脫逸字元. 例如: \n |
subst | parsed section inside a literal string | 字面字串內的解析區段 |
symbol | symbolic constant, interned string, goto label | 符號: 符號定數 (或稱定義常數), 留存字串, goto 標籤. |
class | deprecated You probably want title.class |
類別. 已棄用. 可能你要的是 title.class. |
function | deprecated You probably want title.function |
函式. 已棄用. 可能你要的是 title.function. |
variable | variables | 變數 |
variable.language | variable with special meaning in a language, e.g.:
this , window , super , self , etc. |
在特定語言中有特別意義的變數. 例如: this, windows, super, self. |
variable.constant | variable that is a constant value, i.e. MAX_FILES |
其值為定值的變數. 也就是 MAX_FILES. |
title | name of a class or a function | 類別或函數的名稱 |
title.class | name of a class (interface, trait, module, etc) | 類別, 介面, 特徵, 模組... 的名稱 |
title.class.inherited | name of class being inherited from, extended, etc. | 繼承或擴充來源的類別名稱 |
title.function | name of a function | 函式名 |
title.function.invoke | name of a function (when being invoked) | 函式名 (被引用時) |
params | block of function arguments (parameters) at the place of declaration | 函數的參數區塊 (函數宣告) |
comment | comments | 註解 |
doctag | documentation markup within comments, e.g. @params |
位於註解裡的文件標記, 例如: @params |
Meta 自我描述類 | ||
meta | flags, modifiers, annotations, processing instructions, preprocessor directives, etc | 旗標, 修飾詞, 註解, 操作指引, 預處理指令... 等 |
meta.prompt | REPL or shell prompts or similar | REPL(讀取-求值-輸出) 環境或 unix/linux shell 環境或類似環境的提示字串 |
meta keyword | a keyword inside a meta block (note this is nested, not subscoped) | meta 區塊內的關鍵字. 此 class 為巢狀嵌套, 而非子範圍. |
meta string | a string inside a meta block (note this is nested, not subscoped) | meta 區塊內的字串. 此 class 為巢狀嵌套, 而非子範圍. |
Tags, attributes, configs 標籤, 屬性, 設定類 | ||
section | heading of a section in a config file, heading in text markup | 設定檔裡的區段標題, 文字標記的標題 |
tag | XML/HTML tag | XML/HTML 標籤 |
name | name of an XML tag, the first word in an s-expression | XML 標籤的名稱, "S-表示式" 裡的第一個單字. |
attr | name of an attribute with no language defined semantics (keys in JSON, setting names in .ini), also sub-attribute within another highlighted object, like XML tag | 屬性名稱, 不具特定語義的 (即: 非語言預先定義). (譬如: JSON 的鍵值, 或 .ini 檔裡的設定名稱); 或者某高亮物件的子屬性, 如: XML 標籤的屬性名稱. |
attribute | name of an attribute followed by a structured value part, like CSS properties | 屬性名稱, 後面接著結構化的值 (屬性值), 譬如: CSS 設定項. |
Text Markup 文字標記類 | ||
bullet | list item bullet | 列表項目的符號 |
code | code block | 程式區塊 |
emphasis | emphasis | 強調 |
strong | strong emphasis | 加重強調 |
formula | mathematical formula | 數學公式 |
link | hyperlink | 連結 |
quote | quotation or blockquote | 引述/引用 |
CSS 樣式類 | ||
selector-tag | tag selector | 標籤類 |
selector-id | #id selector | 識別 (身份) 類 |
selector-class | .class selector | 類別類 |
selector-attr | [attr] selector | 標籤屬性類 |
selector-pseudo | :pseudo selector | 偽類 (偽類別/偽元素) |
Templates 模板類 | ||
template-tag | tag of a template language | 樣板語言裡的標籤 |
template-variable | variable in a template language | 樣板語言裡的變數 |
diff 差異類 | ||
addition | added or changed line | 新增或有更動的行 |
deletion | deleted line | 刪除的行 |
注意事項:
- 上表中有含有新式的 scope 名稱 (如: 一般用途類的 title.function.invoke), 這種 scope 名稱轉換為 CSS class 時會是:
- hljs-title
- function_
- invoke__
<span class="hljs-title class_ other__">Render</span>
- 使用這個方法, highlight.js 的 regexp 設定有時會將你括住的字串拆成數段. 此時你的 CSS 設定可能需要增加一些 Selector 和 CSS 設定又或者 CSS 設定項需要加上 !important 才能達成你想要的上色效果.
- 這個方法在 v9 及 v10 這二個舊版本上都可以直接使用. 但是在最新的 v11 版卻是需要一個外掛 mergeHTMLPlugin: 部份原因是資安因素(註一), 部份原因則是因為 issue #2889.
註一: 參看 highlight.js 在 github 上的 "Security Policy" 頁面.
如何使用 mergeHTMLPlugin
如果你看過上述 issue 就會知道: 這個 plugin 本身就是由 highlight.js 分離出來的. 但是, 由於目前並沒有人主持這個 plugin 的維護工作, 所以想要在 pixnet 痞客邦的部落格內使用這個 plugin 就只有:
直接由某 CDN 業者處下載.無人維護, 無法使用此方法.- 直接將它貼在貼文中.
- 尋找一處可以提供 JS 檔案內嵌功能的網路磁碟空間來放置此檔案.
- 由於 XSS 問題, 目前我所知的網碟只剩 MS OneDrive 可用.
- DropBox, Google Drive 都已經因改用短時效金鑰而無法用於提供 JS 檔案.
- 不知哪天 MS OneDrive 也會失效.
好了! 廢話不多說, 來看如使用 mergeHTMLPlugin. 原本我在 pixnet 使用 highlight.js 是在文末貼上這一段:
<link href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/night-owl.min.css" rel="stylesheet" />
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/highlight.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/highlightjs-line-numbers.js/2.8.0/highlightjs-line-numbers.min.js"></script>
<p id='myCSS'></p>
<script>
document.getElementById('myCSS').outerHTML=[
'<style>'
,'.hljs-ln tr { background-color:inherit }'
,'.hljs-ln td { border:inherit }'
,'.hljs-ln td.hljs-ln-numbers { text-align:right; padding-right:4px}'
,'.hljs-ln td.hljs-ln-code { border-left:2px solid #999; padding-left:5px }'
,'</style>'].join('');
if (typeof(hljs) !== 'undefined') {
hljs.highlightAll()
hljs.initLineNumbersOnLoad({singleLine: true})
}
</script>
現在, 必需在前面多貼以下這段, 並將原本的第 13~16 行刪掉, 將它搬到下段的 11, 14~16 行. 而下段第 12 行, 為啟動 mergeHTMLPlugin 所必需的. 下段第 13 行為選項, 此設定讓 hljs 在 borwser 的 console 裡抑制警告訊息: 關於 <code> tag 內含有未轉換為 escape 字元的 HTML tag 的部份 (即: 我們想要自己上色的那一部份).
<script>
function myScript(o, url) {
let s = document.createElement(o)
s.src = url
s.async = true
document.head.appendChild(s);
return new Promise((fa, fj) => { s.onload = ()=>fa(); s.onerror = ()=>fj(); });
}
myScript('script', "由 MS OneDrive 下載 mergeHTMLPlugin 的 URL")
.then( data => {
if (typeof(hljs) !== 'undefined') {
hljs.addPlugin(mergeHTMLPlugin)
hljs.configure({ignoreUnescapedHTML: true})
hljs.highlightAll()
hljs.initLineNumbersOnLoad({singleLine: true})
}
})
</script>
方法二: 暫時修正 hightlight.js
提醒
本段所敍述的方法二雖然有以下二個優點:
- 免用 mergeHTMLPlugin.
- 可以自動完成海量的上色修補工作.
但是, 目前個人只在 bash, dos, python 等三種上色語言試驗過. 非上述三種可能要請你進入 Browser 的除錯器, 到 console 裡自行檢驗物件 hljs.getLanguage('xxxx').keywords 是否包含了你想要的內容. 或者你也可以直接到 hljs 在 github 上的版本倉查看一下你想修正的語言的原始 JS 程式.
例如: js, html, css 等三個 highlight.js 核心語言的結構明顯的和上述三個不同.
如果需要修補的上色部份多樣性高, 數量也很多, 則可以利用 JS 暫時增加缺少的 keyword 或 built_in 等等的字串, 或者刪除不必要的 (會引發上錯色的) 字串. 例如在 bash Script 裡: suso, python, pip 指令沒有上到色; 而參數 --export 裡的 export 被上色了, 那麼我們就可以把 scope built_in 裡的 export 刪掉, 再補上 python, pip. 然後我們還想讓 sudo 指令也使用 keyword 的顏色上色.
不過這個部份, highlight.js v9, v10, v11 三個版本的作法都不同.
v9 修正方法
v9 的上色字串本身就是字串. 所以直接將新增的字串附在後面, 要刪除的字串直接置換成空字串即可 (注意字串值的異動不會直接換掉原本的變數內容, 必需用 = 或 +=). 例如:
if (typeof(hljs) !== 'undefined') {
hljs.getLanguage('bash').k.keyword+=' sudo';
hljs.getLanguage('bash').k.built_in=hljs.getLanguage('bash').k.built_in.replace(' export');
hljs.getLanguage('bash').k.built_in+=' pip python';
...
}
v10 修正方法
v10 的上色字串本身也是字串. 不過, 上述指令中的 k, 改版時換成了 keywords. 所以上例換成 v10 程式就成了:
if (typeof(hljs) !== 'undefined') {
hljs.getLanguage('bash').keywords.keyword+=' sudo';
hljs.getLanguage('bash').keywords.built_in=hljs.getLanguage('bash').keywords.built_in.replace(' export');
hljs.getLanguage('bash').keywords.built_in+=' pip python';
...
}
v11 修正方法
v11 保留了 keywords, 但是上色的字串被分解成字串陣列. 所以上例換成 v11 程式就成了:
if (typeof(hljs) !== 'undefined') {
hljs.getLanguage('bash').keywords.keyword.push('sudo');
hljs.getLanguage('bash').keywords.built_in.push(...['pip', 'python']);
// remove built_in: sudo, export
let arr = hljs.getLanguage('bash').keywords.built_in;
let del_bulit_in = ['sudo', 'export'];
for (let kw of del_bulit_in) {
let idx = arr.indexOf(kw)
if (idx != -1)
arr.splice(idx, 1);
}
...
}
注意: 部份 scope 可能不存在 (例如: bash Script 的 keywords.title, keywords.type), 所以要 push 之前可能需要檢查一下該 scope 是否存在且為陣列.
幾個注意事項
舊版本的 bash script 解析 regrex 有些限制, 即使設定裡有也解析不出來:
- v9, v10 都不支援後面有數字的指令. 例如: python3.
- v9, v10 都不支援中間有 - 的指令. 例如: apt-get, apt-key.
- v9 會不洽當的斷開中間有 - 的指令為其中一部份上色.
- v9 不支援 .addPlugin().
由於上述原因 (還有資安因素) 建議大家盡量改用 v11.
還有一點: 方法一及方法二是可以混用 (例如: 需要克服上述的 - 及數字問題). 如果真的因為某些特定原因不能使用 v11, 例如: 要混用方法一及方法二, 但是沒有可用的網碟. 則建議使用 v10 (最後的版本是 v10.7.3).
常用的語言上色 class
- Python 程式:
- <code class="language-python">
- <code class="python">
- <code class="py">
- Python 互動指令:
- <code class="language-python-repl">
- <code class="python-repl">
- DOS 批次檔:
- <code class="language-dos">
- <code class="dos">
- <code class="cmd">
- 這個需另外載入語言 JS, 但是它不支援和有 bug 的部份還滿多的.
- bash 指令檔:
- <code class="language-bash">
- <code class="bash">
- bash 互動指令:
- <code class="language-shell">
- <code class="shell">
- <code class="console">
- 一般文字:
- <code class="language-plaintext">
- <code class="plaintext">
- <code class="text">
- C/CPP 程式:
- <code class="language-cpp">
- <code class="cpp">
- JS 程式:
- <code class="language-javascript">
- <code class="javascript">
- <code class="js">
- CSS 設定:
- <code class="language-css">
- <code class="css">
- HTML 網頁 (含 javascript 及 CSS):
- <code class="language-xml">
- <code class="html">