起因
博客换了新的主题 Bleaching,支持黑夜模式。但在黑夜模式下,原本一些流程图就不好看了,比如原本在《中控考勤机破解杂记》一文中,对触发器更新打卡时间的示意图。那张图原本是用 processon 画的,背景也置为了透明,但 1. 位图对缩放不友好;2. 颜色无法根据主题自适应;3. 制作和修改流程图繁琐。
原本是考虑用 mermaid 来做替换,但 mermaid 有个欠缺的地方,就是流程图方向难以控制绘图边界,后来经过研究,决定让博客支持 PlantUML。
PlantUML 支持在线调用:仅需要将代码 encode 成对应的字符串,拼接成对应的 URL 即可访问并获取图形,即允许我们在最终渲染完成的 html 文件中采用 img
标签来显示对应的图形。当然,也可以直接生成对应图形,通过指定 base64 的方式渲染在 HTML 中。
初探
当我们在 markdown 里写 LaTeX 和 mermaid 时,我们是将其包裹在 fenced code block 中的:
```mermaid
graph LR
A --> B
```
经最终渲染后,一般都会将其内容包裹在 pre
标签和 code
标签中,并在标签上打上对应语言 class 属性:
<pre>
<code class="language-mermaid">graph LR
A --> B</code>
</pre>
但显然,mermaid.js 和 katex.js 是针对 class 为对应语法的div
层起作用的,如果将其也包裹在 pre 和 code 中,那么前端渲染将失效。
但 flexmark-java 开启了 GitLabExtension 后,是不会将语言为mermaid
和math
的 fenced code block 渲染在 pre 和 code 标签内的,而仅仅是将其内容作为文本包裹在 div 层中,然后打上对应的 class,使其能够被前端的 js 正确捕获并渲染:
<div class="mermaid">graph LR
A --> B
</div>
也就是说,flexmark-java 有自己的一套流程(实际上 flexmark 的任何衍生版本都是),允许扩展对特定语言类型的 fenced code block 进行自定义渲染。而为了支持 PlantUML,我们也恰巧需要对语言标记为plantuml
的内容进行自定义渲染,这便对我们支持 PlantUML 提供了可能。
原理
根据 flexmark-java 的 Wiki 指南,以及对 GitLabExtension 的源码阅读,我们知道对特定 fenced code block 的渲染发生在第 6 步:HTML Rendering 里,其本质是当识别到特定语言的 Node 时,对其进行自定义拼接:
// Code pieces from GitLabNodeRenderer.java
if (this.options.renderBlockMermaid
// If `language` is in String[]{"mermaid"}
&& language.isIn(options.mermaidLanguages)) {
html.line();
// Open a `div` tag, and set the `class`
// tag to `options.blockMermaidClass`,
// which is "mermaid" by default.
html.srcPosWithTrailingEOL(node.getChars()).attr(Attribute.CLASS_ATTR, this.options.blockMermaidClass).withAttr().tag("div").line().openPre();
if (codeContentBlock) {
context.renderChildren(node);
} else {
// node.getContentChars() provides the
// inner text of the fenced block code.
html.text(node.getContentChars().normalizeEOL());
}
// close this div
html.closePre().tag("/div");
html.lineIf(htmlOptions.htmlBlockCloseTagEol);
} else {
context.delegateRender();
}
我们要做的就是仿照 GitLabExtension
来写一个 PlantUMLExtension
,然后按照 flexmark-java 提供的拼装方式,拼装成对应的 HTML 元素即可。
private void render(FencedCodeBlock node, NodeRendererContext context, HtmlWriter html) {
HtmlRendererOptions htmlOptions = context.getHtmlOptions();
BasedSequence language = node.getInfoDelimitedByAny(htmlOptions.languageDelimiterSet);
if (this.options.renderBlockPlantUML
// I set `plantUMLLanguages` as String[]{
// "plantuml",
// "plantuml:png",
// "plantuml:svg"}
// by default in order to appoint format
&& language.isIn(options.plantUMLLanguages)) {
String format = "png";
if (language.length() > 8) {
@NotNull BasedSequence formatSeq = language.subSequence(9);
format = formatSeq.toString();
}
String encode = deflaterCompressString(node.getContentChars().normalizeEOL());
// Do your own rendering
render(node, html, htmlOptions, format, encode, options.darkMode);
if (options.bothModes) {
render(node, html, htmlOptions, format, encode, !options.darkMode);
}
} else {
context.delegateRender();
}
}
通过调用 url 的方式生成图形
PlantUML 允许通过在线服务的形式,远程调用并生成图形:
需要做的就是参照文档提供的 特殊的 Base64 编码,将源码转换为压缩后的编码,然后在自定义的 NodeRenderer
中拼装为 img 标签。
Deflate 压缩
Source Code 采用 Deflate 压缩成字节数组,压缩率任选。可以 Raw Deflate,也可以是带 Header 的 Deflate。
特殊的仿 URL-Safe Base64 编码
按上面提供的编码文档进行编码。
拼接 URL
如果一开始选用的压缩是 Raw Delfate,那么直接拼接即可。如果是带 Header 的 Deflate,那么要在 64 编码后的字符串前拼上“~1”,再拼接到 URL 中。
适应黑夜模式
对于 PlantUML 的在线服务,其提供了正常和黑夜两种模式的渲染,我们可以通过拼接不同 URL 来获取不同模式下的图形:
正常模式:
png: https://www.plantuml.com/plantuml/png/[encoding]
svg: https://www.plantuml.com/plantuml/svg/[encoding]
黑夜模式:
png: https://www.plantuml.com/plantuml/dpng/[encoding]
svg: https://www.plantuml.com/plantuml/dsvg/[encoding]
为了适应博客的日夜模式,只需要在渲染的时候,同时渲染两张图形,并对两种不同模式的图形的 img 标签打上不同的 class:
<!-- for bright mode -->
<img class="plantuml" src="[bright mode image]"/>
<!-- for dark mode -->
<img class="plantuml dark" src="[dark mode image]"/>
然后在主题上配置对应的样式:
body:not(.dark) .plantuml.dark, body.dark .plantuml:not(.dark) {
display: none;
}
即可。具体根据不同主题来。
通过调用 api 的方式生成图形
可以参照 这篇文档,渲染出对应图形后,再编码为 data:[<mediatype>][;base64],<data>
的 Data URLs 的形式,再进行拼装。原理类似。
适应黑夜模式
方式 1
你可以自定义 NodeRenderer
对 fenced code block 的解析规则,比如在语言后加上样式注入:
```plantuml[class="dark"]
[Your PlantUML source code here]
```
让 NodeRenderer
在识别到对应的 [class=*]
序列时,将其解析并添加到 Attribute 中。然后,在编辑 Markdown 时,同时编辑两份 PlantUML 代码:一份是正常的不带 class 的代码,另一份是配置了 PlantUML 黑夜主题,并且带 class 的代码,最后两张图形同时加载,通过前端主题样式控制对应白天/黑夜模式下的图形显隐即可。
方式2
渲染时,采用 SVG 渲染,最终格式不要采用 img 标签,而是直接将 SVG 结构插入 HTML 中,而后再在前端配置对应 SVG 元素在黑夜模式下的样式即可。
后记
flexmark 很好用。PlantUML 很强大,但有一定的学习门槛。用来做博客文章的流程插图,有点杀鸡用牛刀的感觉了。
兄弟,中控那篇文章提示“服务器出错”,打不开了!
已修复