vlambda博客
学习文章列表

使用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图,引入了umlumldemo两种语法,同时利用https://g.gravizo.com/网站在线将PlantUML语法渲染图片。

首先,需要扫描HTML中所有的<pre>标签,由于strapdown.jsumlumldemo语法的代码段渲染为<pre class="lang-lang-uml"><pre class="lang-lang-umldemo">标签, 因此需要根据<pre>标签的class属性对其进行过滤,然后对uml采取图片替换的操作,对umldemo采取图片后向插入的操作。

对于umlumldemo代码段中的内容进行如下处理:
(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

使用nginx和strapdownjs搭建markdown在线渲染站点(支持UML)

源码

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