使用nginx和strapdownjs搭建markdown在线渲染站点(支持UML)
Nginx相关配置
Nginx是一个高性能的HTTP和反向代理web服务器,同时也提供了IMAP/POP3/SMTP服务。
本文中使用Nginx实现如下功能:
展示Markdown仓库的目录结构
提供原始Markdown格式文件的在线查看
将HTML查看请求修改到Markdown渲染页面
online.html
,以对原始的文件内容进行渲染代理strapdownjs脚本及样式表(部分使用场景涉及断网环境)
具体配置如下:
# 拦截/blogs/下的html请求,修改为online.html
rewrite ^/blogs/(.*?)\.html$ /blogs/online.html last;
# Markdown仓库
location /blogs/ {
# Markdown仓库路径
alias /path/to/your/markdown/repository/;
# 可以查看目录
autoindex on;
# 为md、html等文件的指定正确的MIME类型
types {
"text/x-markdown;charset=UTF-8" md;
"text/html;charset=UTF-8" html;
}
}
# 站点镜像
location /mirrors/ {
alias /path/to/your/mirrors/;
autoindex on;
}
online.html
Strapdownjs是一个Markdown渲染库,用于将Markdown转换为对应HTML内容。
其主要特性是在页面中直接引用strapdown.js
脚本即可将<xmp></xmp>
标签内的Markdown文本渲染为HTML并插入当前页面中。
本文中由于需要以参数形式动态加载Markdown文件,因此对strapdown.js
脚本也需要动态地进行加载。
在strapdown.js
对Markdown进行渲染生成HTML后,还需要进行一些后处理工作:
(1)处理Markdown中的UML图进行渲染
1. 处理Markdown中的UML图进行渲染
UML(统一建模语言)是业界普遍接受的建模方法,为了在站点中渲染UML图,引入了uml
和umldemo
两种语法,同时利用https://g.gravizo.com/
网站在线将PlantUML语法渲染图片。
首先,需要扫描HTML中所有的<pre>
标签,由于strapdown.js
将uml
和umldemo
语法的代码段渲染为<pre class="lang-lang-uml">
和<pre class="lang-lang-umldemo">
标签, 因此需要根据<pre>
标签的class
属性对其进行过滤,然后对uml
采取图片替换的操作,对umldemo
采取图片后向插入的操作。
对于uml
和umldemo
代码段中的内容进行如下处理:
(1)按换行符对代码内容进行分割,变成行数组。
(2)如果行不是以;
或者{
,则向其末尾添加;
,方便后续拼接为单行。
(3)如果行首为单引号'
(PlantUML中单引号后的内容视为行内注释),则视为自定义指令,支持:
'STYLE ...
将参数添加到<img>
标签的style
属性。'CENTER
,使图片居中(将"display: table-cell; margin: 0 auto;
添加到<img>
标签的style
属性)。
(4)将代码段中的#
(类图中表示protected
,或者指定颜色)编码为%23
,使其不会被误认为是URL中fragment段的开头。
(5)添加skinparam handwritten true
指令(使用手写模式渲染UML图)。
(6)将缩进\t
编码为%09
,并将处理后的行数组拼接为单行字符串传递给https://g.gravizo.com/svg
作为参数(如果同时具有\t\r\n
和<
会因为可能导致攻击被Chrome拦截)。
使用uml
语法绘制UML图:
使用umldemo
语法绘制UML图并保留代码段(用于编写UML示例):
class Hello
Hello : -world : String
Hello : +run()
'CENTER
源码
strapdown.js
的具体代码如下:
<!DOCTYPE html>
<html>
<img src="/share/%E8%B5%84%E6%BA%90/logo/%E6%89%AB%E7%A0%81_%E6%90%9C%E7%B4%A2%E8%81%94%E5%90%88%E4%BC%A0%E6%92%AD%E6%A0%B7%E5%BC%8F-%E7%99%BD%E8%89%B2%E7%89%88.png" style="display: table-cell; width: 60vw; margin: 0 auto"/>
<!-- strapdownjs节点 -->
<xmp id="content" theme="simplex" style="display:none;"></xmp>
<script type="text/javascript">
// 获取文件路径
var path = window.location.pathname
var url = path == "/blogs/online.html" ? window.location.search.substring(1) : path.substring(0, path.length - ".html".length) ;
document.title = decodeURIComponent(url);
// 读取文件内容,加载到strapdownjs节点中
var request = new XMLHttpRequest();
request.open("GET", url, true);function handleText(event) {
if (event.target.readyState == 4 && event.target && event.target.responseText) {
document.getElementById("content").innerHTML = event.target.responseText;
// 加载strapdownjs脚本;必须在内容之后加载,否则有可能渲染失败
var script = document.createElement("script");
script.type = "text/javascript";
script.src = "/mirrors/strapdownjs.com/v/0.2/strapdown.js";
document.body.appendChild(script);
script.onload = function(event) {
// 列举所有的pre标签
var pres = document.getElementsByTagName("pre");
// 渲染UML
function renderUML(text) {
var image = document.createElement("img");
var style = "";
var lines = text.split("\n");
text = "";
for (var lineIndex = 0; lineIndex < lines.length; lineIndex++) {
var line = lines[lineIndex];
if (line) {
if (line.startsWith("'")) {
// 使用 'STYLE 注释定义图片样式
if (line.startsWith("'STYLE"))
style += line.substring(6) + ";";
// 使用 'CENTER 注释定义图片居中
else if (line.startsWith("'CENTER"))
style += "display:table-cell;margin:0 auto;";
} else if (line.endsWith(";") || line.endsWith("{"))
text += line;
else
text += line + ";";
text += " ";
}
}
image.src = "https://g.gravizo.com/svg?skinparam handwritten true;" + text.replace(/#/g, "%23").replace(/\t/, "%09");
image.style = style;
return image
}
// pre处理器
var preHandlers = {
// 指定语言为 uml,使用图片替换 pre 标签
uml : function(pre, text, index) {
pre.parentNode.replaceChild(renderUML(text), pre);
return index - 1;
},
// 指定语言为 umldemo,在 pre 标签插入图片
umldemo : function(pre, text) {
if (pre.nextSibling)
pre.parentNode.insertBefore(renderUML(text), pre.nextSibling);
else
pre.parentNode.appendChild(renderUML(text))
}
}
// 遍历所有的pre标签
for (var i = 0; i < pres.length; i++) {
var pre = pres[i];
if (pre.children && pre.children.length == 1) {
var code = pre.children[0];
var classList = code.classList;
for (var j = 0; j < classList.length; j++) {
var clazz = classList[j];
if (clazz.startsWith("lang-lang-")) {
clazz = clazz.substring("lang-lang-".length)
var handler = preHandlers[clazz];
if (handler) {
var result = handler(pre, pre.innerText, i);
if (typeof(result) == "number")
i = result
}
}
}
}
}
}
}}
request.onreadystatechange = handleText;
request.send();</script></html>
示例
Markdown
HTML