昨天設定了 VitePress
集成的 markdown-it
套件功能,今天不出意外的就是要安裝其他的套件來水一天,但是不出意外的出意外了,除了 todo-list 的 checkbox 以外,好像也沒什麼其他想裝的了。
那就來選型吧!!
這些都是利用基本的 checkbox 來做的,大家都知道原生 HTML 的外觀,不能說難看,只是就... 膩了? 看來看去完全沒有一個套件是 Opshell 滿意的,乾脆來自己擴充 markdowm-it
好了。
需求確認
開始動工之前,確認好需求是最重要的,免得無限的做白工 (隕石開發除外) :
markdown-it 原理
既然要擴展 markdown-it
的功能,我們就得了解他的運作原理(markdown-it design principles),主要分成兩個部分:
核心組成
Parsing
parser
將markdown
所有的內容解析成一個個 Token Object 然後包成一個 Array。
Rendering
遍歷 Token Array 把每個 Token 轉化為對應的 HTML String,最後組裝起來成爲最後的輸出。
Token
markdown-it
的一切都是基於 Token 展開的,而這邊的 Token 指的其實不是一般"票劵"或"憑證"的意思,而是 markdown
解析後格式的稱呼。
Markdown
文件中,主要會解析出兩個類型的 Token:
block Token (塊狀元素)
以行為最小單位,段落、列表、程式碼空間等多行文字組成的內容。
inline Token (行內元素)
以字原為最小單位,標題、粗體、code 標示等。
一般的 Token 解析完會像這樣這樣
Token {
type: 'task_list_item_open', // token 名稱
tag: 'label', // HTML 標簽名
attrs: [ [ 'class', 'task-list--item' ] ], // [key, value][]
map: null, // 源映射,如:[ line_begin, line_end ]
nesting: 1, // (1, 0, -1) 1 => 標籤起始, 0 => 自閉標籤, -1 => 標籤結束
level: 0, // 套牽層數
children: null, // 子節點陣列,內聯和圖片Token 也會放在這裡
content: '', // 內容,如果是自閉標籤,內容也會放在這裡。
markup: '', // 標記字元,如:範圍塊字元
info: '', // 範圍塊裡面的字元,如:程式碼區塊內的程式碼
meta: null, // 存任意資料的地方,主要是給套件的擴充用
block: true, // true: 塊狀元素, false: 行內元素
hidden: false // 渲染時是否忽視這個 Token
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Parsing
在 markdown-it
中,有各種 parsing
規則,每個規則都會把相符條件的 Markdown
轉換成 Token,轉換會先從 block(塊狀元素)
轉換完再轉換一次 inline(行內元素)
。
Block Token
前面提到,block 是以行為最小單位,也就是說起始行符合條件後會一路包含到符合條件的結束行來生成 Block Token
,規則由 md.block.ruler
控制。
💡 錦囊 [Tips]:
生成的 Token 不一定是單個 Token,例如一個段落的 block 會生成三個 Token:
如:
這是一個*段落*!!
會解析成三個 Token
paragraph_open
inline
paragraph_close
2
3
Inline Token
等整個 Markdown
解析完之後,會把剛剛解出來的 Array 中 token type
為 inline 的 Token 在用 inline 規則解析一遍,規則由 md.inline.ruler
控制。
所以剛剛解出來的 Token 再解一次就會變成:
paragraph_open
inline
text
em_open
text
em_close
text
paragraph_close
2
3
4
5
6
7
8
Rendering
markdown-it
有預先定義好的 render
規則(在 md.renderer.rules),每個規則都是一個函數,輸入是 Token,輸出是 HTML string。
在遍歷各個 Token 的過程中,對每個 Token,都會查看一下是否有和 Token.type
同名的 render
規則。
如果有,則把 Token 丟進去轉換,得到該 Token 的 HTML string。
接續上面的例子:
paragraph_open => <p>
inline
text => 這是一個
em_open => <em>
text => 段落
em_close => </em>
text => !!
paragraph_close => </p>
2
3
4
5
6
7
8
最後的輸出會是:
<p>這是一個<em>段落</em>!!</p>
功能擴充
了解原理之後,就來正式的寫擴充吧, VitePress
集成以外的 Markdown
套件或擴充都會在 markdown.config
裡面做處理:
Parsing
export default defineConfig({
markdown: {
config: (md) => {
// 在 blockquote 規則前面添加 taskList 規則,透過 function 來解析
// alt 是這個規則可以打斷的 其他規則,一般不做特別的處理,這裡是直接把 blockquote 的 alt 拿來用。
md.block.ruler.before('blockquote', 'taskList', function (state, startLine, endLine, silent) {
const start: number = state.bMarks[startLine] + state.tShift[startLine]; // 行的開始位置
const max: number = state.eMarks[startLine]; // 行的結束位置
// 不符合基本規則的直接跳出
if (state.src.charCodeAt(start) !== 0x5B) /* [ */
return false;
if (state.src.charCodeAt(start + 2) !== 0x5D) /* ] */
return false;
const content = state.src.substring(start, max); // 取出整段文字
const reg = /\[(\s|x)\]/;
const match = content.match(reg);
if (match && match.length) {
let token;
const checked = match[1] === 'x';
if (silent) { // 解析異常時不做反應
return true;
}
token = state.push('task_list_item_open', 'label', 1);
token.attrs = [['class', 'task-list-item']];
token.map = [ startLine, state.line ];
token = state.push('inline', '', 0);
token.content = `
<input class="task-list-input" type="checkbox" ${checked ? 'checked' : ''} />
<span class="task-list-text">${content.replace(reg, '')}</span>
`;
token.map = [ startLine, state.line ];
token.children = [];
token = state.push('task_list_item_close', 'label', -1);
}
state.line = startLine + 1;
return true;
}, { alt: ['paragraph', 'reference', 'blockquote', 'list'] });
}
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
這樣就為把 [ ]
或者是 [x]
解析成 class 是 task-list-item 的 label
包起來的 input:checkbox
組。 但是這樣並不會把相鄰的 checkbox 用 div 包起來阿?!
別急,解析規則做完之後,我們可以用渲染規則把剩下的需求補完。
export default defineConfig({
markdown: {
config: (md) => {
md.block.ruler.before(......);
md.renderer.rules.task_list_item_open = (tokens, idx, options, env, slf) => {
return tokens[idx - 1].type !== 'task_list_item_close'
? `<div class="task-list"><label class="task-list-item">`
: `<label class="task-list-item">`;
};
md.renderer.rules.task_list_item_close = (tokens, idx, options, env, slf) => {
return tokens[idx + 1].type !== 'task_list_item_open' ? '</label></div>' : '</label>';
};
}
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
把 Token.type
的規則做一個補充,把第一個 task_list_item_open
前面加 <div>
最後一個 task_list_item_close
後面加 </div>
,這樣就完美的達成我們的需求囉:
小結
咦奇怪,你說為什麼你寫出來樣式長的跟我不一樣? 當然是因為 Opshell 沒有把 style 丟出來囉,
我的 style
嗎?想要的話就給你吧,去找吧!我把世界上的一切都放在那裡!
拉夫德爾
.vp-doc {
.task-list {
@include setFlex(flex-start, flex-start, 10px, column);
margin: 10px 0;
&--item {
position: relative;
@include setFlex(flex-start, center, 20px);
cursor: pointer;
&:hover {
.task-list {
&--input::before { border-color: var(--vp-c-brand-3); }
&--text { color: var(--vp-c-brand-3); }
}
}
}
&--input {
position: relative;
box-sizing: border-box;
margin-left: 10px;
@include setSize(0, 0);
cursor: pointer;
&::before,
&::after {
content: '';
position: absolute;
background: transparent;
box-sizing: border-box;
@include setSize(0, 0);
}
&::before {
@include setSize(20px, 20px);
border: 2px solid var(--vp-c-text-2);
border-radius: 3px;
transform: translate3d(-50%, -50%, 0);
transition: border-color 0.15s $cubic-FiSo, background-color 0.15s $cubic-FiSo 0.05s;
}
&::after {
border-top: none;
border-right: none;
border-width: 0;
border-radius: 1px;
transform: rotateZ(-45deg) translate3d(0, -120%, 0);
}
&:checked {
&::before {
background-color: var(--vp-c-brand-1);
border-color: var(--vp-c-brand-2);
}
&::after {
@include setSize(20px, 10px);
border: 3px solid #eee;
border-top: none;
border-right: none;
transition: border-color 0.15s $cubic-FiSo,
height 0.05s $cubic-FiSo,
width 0.1s $cubic-FiSo 0.04s;
filter: drop-shadow(0 0 3px rgba(0, 0, 0, 100%));
}
}
}
&--text {
transform: translate(0, 2px);
transition: .2s $cubic-FiSo;
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72