Skip to content
🎼 一款结构化的 Markdown 引擎,支持 Go 和 JavaScript。
Go Other
  1. Go 99.3%
  2. Other 0.7%
Branch: master
Clone or download
Latest commit 0e20aa7 Sep 15, 2019
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.github :octocat: 提交 Issue、PR 模板 Aug 27, 2019
benchmark ♻️ 调整解析渲染参数设置方式 Sep 8, 2019
chroma-styles :octocat: 导入 Chroma 样式 Aug 27, 2019
demo 🎨 更新 Go 示例 Sep 2, 2019
html #15 引入 html 包 Sep 10, 2019
http 🎨 调整 HTTP Server 报错日志 Sep 1, 2019
js #26 处理最后一行 Sep 15, 2019
pprof ♻️ 调整解析渲染参数设置方式 Sep 8, 2019
test #26 块节点源码位置解析 Sep 15, 2019
.gitattributes #13 JavaScript 支持更换技术方案 Sep 4, 2019
.gitignore 🙈 忽略性能调试输出 Aug 4, 2019
.header.json #15 引入 html 包 Sep 10, 2019
.header.txt 📄 使用木兰宽松许可证 Aug 24, 2019
.travis.yml 💚 CI 启用 Go Modules Sep 2, 2019
LICENSE 📄 使用木兰宽松许可证 Aug 24, 2019
README.md 📝 更新代码块语法高亮说明 Sep 11, 2019
auto_link.go ♻️ 移动变量 Sep 7, 2019
blockquote.go 🎨 重命名函数 Sep 3, 2019
blocks.go #26 处理最后一行 Sep 15, 2019
code_block.go #26 解析源码位置 Sep 15, 2019
code_block_html_renderer.go ♻️ 渲染器提取接口 Sep 10, 2019
code_block_html_renderer_js.go ♻️ 渲染器提取接口 Sep 10, 2019
code_span.go 🎨 Cleanup code Sep 6, 2019
delimiter.go 🐛 修复 Unicode 强调分隔符序列判断 Sep 15, 2019
doc.go 📄 使用木兰宽松许可证 Aug 24, 2019
emoji.go 📄 添加文件头 Sep 10, 2019
emoji_map.go 🎨 支持自定义 Emoji 路径 Sep 8, 2019
escape_encode.go ⚡️ 性能优化 Sep 2, 2019
format_renderer.go 🎨 #11 光标解析 Sep 15, 2019
go.mod #11 Vditor 插入符号处理 Sep 14, 2019
go.sum #11 Vditor 插入符号处理 Sep 14, 2019
heading.go #26 块级元素源码位置解析 Sep 15, 2019
html_block.go #26 块级元素源码位置解析 Sep 15, 2019
html_renderer.go 🎨 #11 光标解析 Sep 15, 2019
image.go ⚡️ 性能优化 Sep 2, 2019
inline.go 🎨 #11 光标解析 Sep 15, 2019
inline_html.go ⚡️ 性能优化 Sep 2, 2019
inline_link.go 🐛 Fix #20 修复链接解析中文路径问题 Sep 6, 2019
inline_math.go Fix #25 内联数学公式支持 Sep 14, 2019
inlines.go 🐛 JS 端 Code Block 报错 Sep 4, 2019
lex.go ⚗️ Wasm TinyGo 编译 Sep 3, 2019
link_ref_def.go ⚡️ 性能优化 Sep 2, 2019
list.go 🎨 #11 块延续新行 Sep 13, 2019
list_item.go 🎨 重命名函数 Sep 3, 2019
lute.go #26 块节点源码位置解析 Sep 15, 2019
math_block.go #26 块级元素源码位置解析 Sep 15, 2019
node.go ⚡️ #26 减少内存分配 Sep 15, 2019
panic.go ♻️ 调整解析渲染参数设置方式 Sep 8, 2019
panic_js.go 📦 优化 JS 端包大小 Sep 4, 2019
paragraph.go ♻️ 重命名函数 Sep 3, 2019
parse.go #26 处理最后一行 Sep 15, 2019
renderer.go ♻️ 渲染器提取接口 Sep 10, 2019
space.go 🎨 去掉冗余的判空 Sep 7, 2019
string_items.go #13 JavaScript 支持更换技术方案 Sep 4, 2019
string_items_js.go #13 JavaScript 支持更换技术方案 Sep 4, 2019
table.go :octocat: GFM 表中 | 反转义 Sep 14, 2019
term_typographer.go 🔧 更新术语字典 Sep 10, 2019
text.go 🎨 去掉冗余的判空 Sep 7, 2019
thematic_break.go #26 块级元素源码位置解析 Sep 15, 2019
token.go 🎨 #11 光标解析 Sep 15, 2019
unesc_str.go #15 引入 html 包 Sep 10, 2019
vditor_parser.go 🎨 #11 换行处理 Sep 14, 2019
vditor_renderer.go #26 块节点源码位置解析 Sep 15, 2019
walk.go 🎨 简化逻辑 Sep 5, 2019

README.md

Lute



一款结构化的 Markdown 引擎,为未来而构建

千呼万唤始出来 犹抱琵琶半遮面
转轴拨弦三两声 未成曲调先有情





        

💡 简介

Lute 是一款结构化的 Markdown 引擎,完整实现了最新的 GFM/CommonMark 规范,对中文语境支持更好。

📽️ 背景

之前我一直在使用其他 Markdown 引擎,它们或多或少都有些“瑕疵”:

  • 对标准规范的支持不一致
  • 对“怪异”文本处理非常耗时,甚至挂死
  • 对中文支持不够好

Lute 的目标是构建一个结构化的 Markdown 引擎,实现 GFM/CM 规范并对中文提供更好的支持。所谓的“结构化”指的是从输入的 MD 文本构建抽象语法树,通过操作树来进行 HTML 输出、原文格式化等。 实现规范是为了保证 Markdown 渲染不存在二义性,让同一份 Markdown 文本可以在实现规范的 Markdown 引擎处理后得到一样的结果,这一点非常重要。

实现规范的引擎并不多,我想试试看自己能不能写上一个,这也是 Lute 的动机之一。关于如何实现一个 Markdown 引擎,网上众说纷纭:

  • 有的人说 Markdown 适合用正则解析,因为文法规则太简单
  • 也有的人说 Markdown 可以用编译原理来处理,正则太难维护

我赞同后者,因为正则确实太难维护而且运行效率较低。最重要的原因是符合 GFM/CM 规范的 Markdown 引擎的核心解析算法不可能用正则写出来,因为规范定义的规则实在是太复杂了。

最后,还有一个很重要的动机就是 B3log 开源社区需要一款自己的 Markdown 引擎:

  • SoloPipeSym 需要效果统一的 Markdown 渲染,并且性能非常重要
  • Vditor 需要一款结构化的引擎作为支撑以实现下一代的 Markdown 编辑器

特性

  • 实现最新版 GFM/CM 规范
  • 零正则,非常快
  • 代码块语法高亮
  • 更好地支持中文语境
  • Emoji 解析
  • 支持 Markdown 格式化

🗃 案例

🇨🇳 中文语境优化

  • 自动链接识别加强
  • 在中西文间自动插入空格
  • 术语拼写修正

♍ 格式化

格式化功能可将“不整洁”的 Markdown 文本格式化为统一风格,在需要公共编辑的场景下,统一的排版风格能让大家更容易协作。

点此展开格式化示例。
Markdown 原文:
# ATX 标题也有可能需要格式化的 ##
一个简短的段落。

Setext 说实话我不喜欢 Setext 标题
----
0. 有序列表可以从 0 开始
0. 应该自增序号的
1.   对齐对齐对齐

我们再来看看另一个有序列表。
1. 没空行的情况下序号要从 1 开始才能打断段落开始一个新列表
3. 虽然乱序不影响渲染
2. 但是随意写序号容易引起误解

试下贴段代码:
```go
package main

import "fmt"

func main() {
  fmt.Println("Hello, 世界")
}
```
对了,缩进代码块建议换成围栏代码块:

    缩进代码块太隐晦了
    也没法指定编程语言,容易导致代码高亮失效
    多以建议大家用 ``` 围栏代码块
试下围栏代码块匹配场景:
````markdown
围栏代码块只要开头的 ` 和结束的 ` 数量匹配即可,这样可以实现在围栏代码块中显示围栏代码块:
```
这里只有 3 个 `,所以不会匹配markdown代码块结束
```
下面匹配到就真的结束了。
````
以上块级内容都挤在一坨了,插入合理的空行也很有必要。


但是过多的空行分段也不好啊,用来分段的话一个空行就够了。



接下来让我们试试稍微复杂点的场景,比如列表项包含多个段落的情况:
1. 列表项中的第一段

   这里是第二个段落,贴段代码:
   ```markdown
   要成为Markdown程序员并不容易,同理PPT架构师也是。
   注意代码块中的中西文间并没有插入空格。
   ```
   这里是最后一段了。
1. 整个有序列表是“松散”的:列表项内容要用 `<p>` 标签

最后,我们试下对 GFM 的格式化支持:

|表格列a|表格列b|       表格列c   |
:---           |:---------------:|--:
第1列开头不要竖线      |   第2列   |第3列结尾不要竖线
                                 ||这个表格看得我眼都花了|

**以上就是为什么我们需要Markdown Format,而且是带中西文自动空格的格式化。**

格式化后:

# ATX 标题也有可能需要格式化的

一个简短的段落。

## Setext 说实话我不喜欢 Setext 标题

0. 有序列表可以从 0 开始
1. 应该自增序号的
2. 对齐对齐对齐

我们再来看看另一个有序列表。

1. 没空行的情况下序号要从 1 开始才能打断段落开始一个新列表
2. 虽然乱序不影响渲染
3. 但是随意写序号容易引起误解

试下贴段代码:

```go
package main

import "fmt"

func main() {
  fmt.Println("Hello, 世界")
}
```

对了,缩进代码块建议换成围栏代码块:

```
缩进代码块太隐晦了
也没法指定编程语言,容易导致代码高亮失效
多以建议大家用 ``` 围栏代码块
```

试下围栏代码块匹配场景:

````markdown
围栏代码块只要开头的 ` 和结束的 ` 数量匹配即可,这样可以实现在围栏代码块中显示围栏代码块:
```
这里只有 3 个 `,所以不会匹配markdown代码块结束
```
下面匹配到就真的结束了。
````

以上块级内容都挤在一坨了,插入合理的空行也很有必要。

但是过多的空行分段也不好啊,用来分段的话一个空行就够了。

接下来让我们试试稍微复杂点的场景,比如列表项包含多个段落的情况:

1. 列表项中的第一段

   这里是第二个段落,贴段代码:

   ```markdown
   要成为Markdown程序员并不容易,同理PPT架构师也是。
   注意代码块中的中西文间并没有插入空格。
   ```

   这里是最后一段了。
2. 整个有序列表是“松散”的:列表项内容要用 `<p>` 标签

最后,我们试下对 GFM 的格式化支持:

|表格列 a|表格列 b|表格列 c|
|:---|:---:|---:|
|第 1 列开头不要竖线|第 2 列|第 3 列结尾不要竖线|
||这个表格看得我眼都花了||

**以上就是为什么我们需要 Markdown Format,而且是带中西文自动空格的格式化。**

✍️ 术语修正

Markdown 原文:

在github上做开源项目是一件很开心的事情,请不要把Github拼写成`github`哦!

特别是简历中千万不要出现这样的情况:

> 熟练使用JAVA、Javascript、GIT,对android、ios开发有一定了解,熟练使用Mysql、postgresql数据库。

修正后:

在 GitHub 上做开源项目是一件很开心的事情,请不要把 GitHub 拼写成`github`哦!

特别是简历中千万不要出现这样的情况:

> 熟练使用 Java、JavaScript、Git,对 Android、iOS 开发有一定了解,熟练使用 MySQL、PostgreSQL 数据库。

⚡ 性能

  1. 主要对比了 4 款 Go 实现的 Markdown 引擎:Lutegolang-commonmarkgoldmarkBlackfriday
  2. 均已关闭 Typographer 相关配置,因为在排版优化上各库的功能差异较大,对比性能意义不大
  3. 均已开启 GFM 支持,因为 GFM 在实际使用场景下是必备的,仅测试 CommonMark 支持意义不大
  4. Lute 在多核平台上性能优势比较明显,因为 Lute 使用了并行解析算法
  5. Blackfriday 没有实现 GFM 所以性能看上去更好一些
  6. 基准测试数据 CommonMark Spec ~197K,参数 -test.cpu 2,4,8 -test.benchmem
BenchmarkLute-2                      200           6058789 ns/op         5356988 B/op      24767 allocs/op
BenchmarkLute-4                      300           4517919 ns/op         5349500 B/op      24755 allocs/op
BenchmarkLute-8                      300           4258608 ns/op         5348686 B/op      24744 allocs/op
BenchmarkGolangCommonMark-2          300           4916845 ns/op         2914352 B/op      18423 allocs/op
BenchmarkGolangCommonMark-4          300           4807139 ns/op         2923699 B/op      18424 allocs/op
BenchmarkGolangCommonMark-8          300           4827190 ns/op         2930485 B/op      18425 allocs/op
BenchmarkGoldMark-2                  300           5894211 ns/op         2308201 B/op      15367 allocs/op
BenchmarkGoldMark-4                  300           5724676 ns/op         2315634 B/op      15368 allocs/op
BenchmarkGoldMark-8                  300           5771226 ns/op         2314451 B/op      15368 allocs/op
BenchmarkBlackFriday-2               300           3982679 ns/op         3296658 B/op      20046 allocs/op
BenchmarkBlackFriday-4               500           3734009 ns/op         3304971 B/op      20048 allocs/op
BenchmarkBlackFriday-8               500           3736069 ns/op         3313467 B/op      20049 allocs/op

另外,JavaScript 写的 markdown-it 循环渲染 300 次,平均每次调用耗时 ~10ms,耗时大致是 golang 实现的两倍,测试代码见此

💪 健壮性

Lute 承载了黑客派上的所有 Markdown 处理,每天处理数十万请求,运行表现稳定。

🔒 安全

Lute 没有实现实现 GFM 中的 Disallowed Raw HTML (extension),因为该扩展还是存在一定漏洞(比如没有处理 <input>)。 建议通过其他库(比如 bluemonday)来进行 HTML 安全过滤,这样也能更好地适配应用场景。

🛠️ 使用

有三种方式使用 Lute:

  1. 后端:用 Go 语言的话引入 github.com/b3log/lute 包即可
  2. 后端:将 Lute 启动为一个 HTTP 服务进程供其他进程调用,具体请参考 http/main.go
  3. 前端:引入 js 目录下的 lute.min.js 即可,支持 Node.js

Go

引入 Lute 库:

go get -u github.com/b3log/lute

最小化可工作示例:

package main

import (
	"fmt"

	"github.com/b3log/lute"
)

func main() {
	luteEngine := lute.New() // 默认已经启用 GFM 支持以及中文语境优化
	html, err := luteEngine.MarkdownStr("demo", "**Lute** - A structured markdown engine.")
	if nil != err {
		panic(err)
	}
	fmt.Println(html)
	// <p><strong>Lute</strong> - A structured Markdown engine.</p>
}

关于代码块语法高亮:

  • 默认使用外部样式表,主题为 github.css,可从 chroma-styles 目录下拷贝该样式文件到项目中引入
  • 可通过 lutenEngine.SetCodeSyntaxHighlightXXX() 来指定高亮相关参数,比如是否启用内联样式、行号以及主题

JavaScript

通过 GopherJS 将 Lute 编译为 lute.js 使用。简单示例可参考 js 目录下的 demo,结合前端编辑器的完整用法请参考 Vditor 中的示例

Vditor

一些细节:

  1. lute.js 没有内置语法高亮特性
  2. lute.js 编译压缩后大小为 ~800KB,通过 brotli -o lute.min.js.br lute.min.js 压缩后大小 ~110KB,常规 GZip 压缩后大小 ~150KB

📜 文档

🏘️ 社区

📄 授权

Lute 使用 木兰宽松许可证, 第1版 开源协议。

🙏 鸣谢

Lute 的诞生离不开以下开源项目,在此对这些项目的贡献者们致敬!

  • commonmark.js:该项目是 CommonMark 官方参考实现的 JavaScript 版,Lute 参考了其解析器实现部分
  • mdast:该项目介绍了一种 Markdown 抽象语法树结构的表现形式,Lute 的 AST 在初始设计阶段参考了该项目
  • goldmark:另一款用 golang 写的 Markdown 引擎,Lute 参考了其树遍历实现部分
  • golang-commonmark:另一款用 golang 写的 Markdown 引擎,Lute 参考了其 URL 编码以及 HTML 转义算法
  • Chroma:用 golang 写的语法高亮引擎
  • fasthttp:用 golang 写的高性能 HTTP 实现
  • 中文文案排版指北:统一中文文案、排版的相关用法,降低团队成员之间的沟通成本,增强网站气质
  • autocorrect:自动给中英文之间加入空格、术语拼写修正
  • GopherJS:将 Go 代码编译成 JavaScript 代码

👍 开源项目推荐

  • 如果你需要集成一个浏览器端的 Markdown 编辑器,可以考虑使用 Vditor
  • 如果你需要搭建一个个人博客系统,可以考虑使用 Solo
  • 如果你需要搭建一个社区平台,可以考虑使用 Sym
  • 欢迎加入我们的小众开源社区,详情请看这里
You can’t perform that action at this time.