[技術] 讓Graphviz也支援亮暗主題色變化吧

緣起

自己一直以來都是hackMD的重度使用者,無論是上課的筆記,還是自學知識的知識,基本上都是記錄在hackmd上面。

我會這麼喜歡hackMD的原因,就是因為他結合了很多十分方便的功能:例如像是用$$包住就可以打出$\LaTeX$的數學式,用```graphviz```就可以打出流程圖(Graphviz)。這種只需要文字就可以打出需要的圖/文字的設計,我真的覺得非常好用。

在打算開始建立個人部落格後,因為他一樣是使用markdown的型式來書寫,所以我也不想脫離上述兩個好用功能。而我使用的blog主題是基於Fluid的修改版,他也提供了內建的亮、暗色模式,所以我就發現了一個讓我看得不是很順眼的問題。

問題

我們先來看一個由$\LaTeX$語法寫出的數學式:

$$
\sum\limits_{i=0}^{n}a_i
$$

雖然$\LaTeX$能夠支援根據背影的顏色來調整現在字體的顏色(可以試著手動切換亮暗色模式試試看!),但是Graphviz就沒辦法支援這一點。因為原生的hexo並不支援用像是寫hackMD的方式來渲染Graphviz,所以我使用的套件是hexo-filter-viz。我們來看一個用原本的hexo-filter-viz渲染出來的SVG:

relax A A Updated Want to go to the cloest b b Cloest to the current subgraph (Update first) A->b 1 c c Second close to the current subgraph A->c 5 b->c 2

第一眼看到的問題就是它不是置中的!而且亮色模式下看起來沒什麼問題,但切到暗色模試就很痛苦了。那突兀的背景,還有不隨著主題色變化的線條,真的感覺需要好好修理一番😢。經過一連串的調整,最終我得到了以下的結果:


看起來非常棒!除了原本就有顏色的部分,其餘的部分都會隨著亮暗色主題變化,並且也沒有背景了!我在markdown檔裡面只需要像平常使用graphviz的方式,就可以生成支援暗色模式的流程圖了!接下來就讓我來介紹我到底做了哪些事吧!

開始工作

如何渲染

首先一開始要先感謝Chris 技術筆記這篇文章。其實最一開始fluid主題是沒有支援使用像hackmd的方式產生Graphviz的,所以我一開始也是用他改寫好的套件來使用。如果大家有興趣的話記得點進去看看!

總之,點進他寫好的套件後,可以發現最重要的檔案就是lib/render.js

1
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
var reg = /(\s*)(```) *(graphviz) *\n?([\s\S]+?)\s*(\2)(\n+|$)/g;

function ignore(data) {
var source = data.source;
var ext = source.substring(source.lastIndexOf('.')).toLowerCase();
return ['.js', '.css', '.html', '.htm'].indexOf(ext) > -1;
}

function getId(index) {
return 'graphviz-' + index;
}

exports.render = function(data) {
if (!ignore(data)) {

var graphviz = [];

data.content = data.content
.replace(reg, function(raw, start, startQuote, lang, content, endQuote, end) {
var graphvizId = getId(graphviz.length);
graphviz.push(content);
return start + '<div id="' + graphvizId + '></div>' + end;
});

if (graphviz.length) {
var config = this.config.graphviz;
// resources
data.content += '<script src="' + config.vizjs + '"></script>';
data.content += '<script src="' + config.render + '"></script>';
data.content += graphviz.map(function(code, index) {
var graphvizId = getId(index);
var codeId = graphvizId + '-code';
return '' +
'{% raw %}' +
'<textarea id="' + codeId + '" style="display: none">' + code + '</textarea>' +
'<script>' +
' var viz = new Viz();' +
' var code = document.getElementById("' + codeId + '").value;' +
' viz.renderSVGElement(code)' +
' .then(function(element) {' +
' document.getElementById("' + graphvizId + '").append(element);' +
' });' +
'</script>' +
'{% endraw %}';
}).join('');
}
}
};

原來如此,所以其實就是把原本的程式碼塊進行拆解,然後用{% raw %}來注入程式碼,並用Viz來渲染整份程式碼。最後會把render好的element回傳到寫著<div id="graphvizID"></div>的裡面。原來如此,大致了解了。

看起來這裡就是我該動手修改的地方。還在等什麼?直接fork一份開始改起來囉!

置中

但其實我一開始看到不順眼的地方是預設的結果並不是置中的,這讓我看的很痛苦。所以就讓我手動在<div id="graphvizID"></div>這個tag裡面加上style="text-align:center;"這個attribute吧!

移除背景和修改線條顏色

再來一個就是背景色,雖然可以在markdown書寫時手動寫bgcolor=transparent;,但我覺得這樣做太不優雅了,所以還是打算直接修改這裡注入{% raw %}裡的程式碼。首先我們要先來看他渲染出來的SVG架構長什麼樣子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<svg width="337pt" height="184pt" viewBox="0.00 0.00 337.30 183.74" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 179.7361)">
<title>relax</title>
<polygon fill="#ffffff" stroke="transparent" points="-4,4 -4,-179.7361 333.2956,-179.7361 333.2956,4 -4,4"></polygon>
<!-- A -->
<g id="node1" class="node">
<title>A</title>
<ellipse fill="none" stroke="#b2dfee" cx="151.3691" cy="-18.0079" rx="18.0157" ry="18.0157"></ellipse>
<text text-anchor="middle" x="151.3691" y="-13.8079" font-family="Times,serif" font-size="14.00" fill="#000000">A</text>
<text text-anchor="middle" x="66.6806" y="-57.0157" font-family="Times,serif" font-size="14.00" fill="#000000">Updated</text>
<text text-anchor="middle" x="66.6806" y="-40.2157" font-family="Times,serif" font-size="14.00" fill="#000000">Want to go to the cloest</text>
</g>
<!-- A&#45;&gt;c -->
<g id="edge3" class="edge">
<title>A-&gt;c</title>
<path fill="none" stroke="#a52a2a" d="M167.0241,-27.0463C177.7195,-33.2213 192.1078,-41.5284 204.3,-48.5675"></path>
<polygon fill="#a52a2a" stroke="#a52a2a" points="202.6875,-51.678 213.0978,-53.647 206.1876,-45.6158 202.6875,-51.678"></polygon>
<text text-anchor="middle" x="182.162" y="-42.0069" font-family="Times,serif" font-size="14.00" fill="#000000">5</text>
</g>
</g>
</svg>

雖然這裡我只列出幾個重要的部件,但由上述渲染出的結果我們就可以發現:最外層的svgtag調整的是整個SVG的圖片大小,而進到第一層g則是代表整個圖,其裡面第一層polygon就是背景,而接下來幾個g則是圖中的部件,像是表示形狀的node,還有箭頭的edge等等,裡面會再包幾個小的element來湊出需要的形狀。

而我這裡因為想處理的方式是透過調整fillstroke來調整顏色:如果是希望看不到的話,就把顏色設成transparent,而若是想變成因應主題的字體顏色的話,就改成var(--text-color)。所以這裡我把背景還有其他polygon一起處理:

1
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
var viz = new Viz();
var code = document.getElementById(" + codeId + ").value;
viz.renderSVGElement(code)
.then(function(element) {
element.setAttribute("width", "100%"); // Adjust size
polygons = element.getElementsByTagName("polygon");
for (var i = 0; i < polygons.length; i += 1) {
if (polygons[i].parentElement.className.baseVal == "graph") {
polygons[i].setAttribute("stroke", "transparent"); // Background
} else if (polygons[i].getAttribute("stroke") == "#000000") {
polygons[i].setAttribute("stroke", "var(--text-color)");
}
if (polygons[i].parentElement.className.baseVal == "graph") {
polygons[i].setAttribute("fill", "transparent"); // Background
} else if (polygons[i].getAttribute("fill") == "#000000" && polygons[i].parentElement.className.baseVal == "edge") {
polygons[i].setAttribute("fill", "var(--text-color)");
}
}
paths = element.getElementsByTagName("path");
for (var i = 0; i < paths.length; i += 1) {
if (paths[i].getAttribute("fill") == "none" && paths[i].parentElement.className.baseVal == "edge") {
paths[i].setAttribute("fill", "var(--text-color)");
}
if (paths[i].getAttribute("stroke") == "#000000" && paths[i].parentElement.className.baseVal == "edge") {
paths[i].setAttribute("stroke", "var(--text-color)");
}
}
ellipses = element.getElementsByTagName("ellipse");
for (var i = 0; i < ellipses.length; i += 1) {
if (ellipses[i].getAttribute("stroke") == "#000000") {
ellipses[i].setAttribute("stroke", "var(--text-color)");
}
}
texts = element.getElementsByTagName("text");
for (var i = 0; i < texts.length; i += 1) {
if (texts[i].getAttribute("fill") == "#000000") {
texts[i].setAttribute("fill", "var(--text-color)");
}
texts[i].setAttribute("stroke", "none");
}
document.getElementById(" + graphvizId + ").append(element);
});

這裡在調整個element只有上述幾個我覺得比較常用的,至於其他我覺得應該用不太到的我就沒有放上去了。

這裡只是重點把回傳要注入{% raw %}的JavaScript Code寫出來。可以看到就只是簡單地把fill和stroke為預設的值改為對應主題色的var(--text-color),而在Polygon內還會再特判背景的狀況這樣。

調整大小

最後的最後,在調整完才發現如果給定的圖太大張,其實會超過整個筆記的版面(往右邊超出界),因此這裡在第5行手動調整width為100%,這樣就不會出界了!

成品

Okay!在經過上述的調整後,看過一輪應該是沒什麼大問題了!於是就把修改好的結果存在hexo-filter-viz-customized,這樣我用在github上的CI/CD也能夠下載到這個套件,正常地幫我渲染Graphviz啦!讚讚!

結論

其實這個修改大概是2023暑假就想做的了,只是因為太忙一直拖到了寒假,因為心血來潮就把這個小問題解掉了。原本在大二還沒修完軟體設計實驗對網頁內的JavaScript都還是敬而遠之的狀態,不過我想在經過這一次的實作,也發現自己開始有能力透過閱讀document,來完成自己需要的功能。我想這也算是一種成長吧XD。雖然做的東西很簡單,但是對整體的美觀性來說還是有很顯著的提升的,期待下次還可以繼續強化並讓這個部落格更完整😎。


[技術] 讓Graphviz也支援亮暗主題色變化吧
https://torrid-fish.github.io/tech-graphviz_light_dark_support/
Author
Torrid-Fish
Posted on
2024年2月1日
Licensed under