- 网络安全
【免费下载链接】sliver
Adversary Emulation Framework
导读
goldmark 是当前仓库(sliver 项目)vendor 目录中引入的一款用纯 Go 编写的 Markdown 解析器,其核心定位是"易于扩展、符合标准、结构清晰"。本项目通过 go.mod 以github.com/yuin/goldmark v1.7.13(间接依赖)引入,同时附带 goldmark-emoji 等生态扩展。本文将以 goldmark 官方 README 为主体,结合仓库内的实际源码实现,系统讲解其设计动机、API 用法、Parser/Renderer 选项体系、内置扩展、安全模型、性能表现以及面向扩展开发者的内部架构,帮助读者掌握在 Go 项目中集成、配置和二次开发 goldmark 的完整能力。
设计动机:为什么需要一个"新"的 Markdown 解析器
goldmark 的作者在开发之初提出了四项硬性需求,这也是它区别于当时主流 Go Markdown 库的核心出发点:
- 易于扩展(Easy to extend):Markdown 相比 reStructuredText 等轻量标记语言,在文档表达力上有所欠缺,社区因此衍生出 PHP Markdown Extra、GitHub Flavored Markdown 等方言扩展。goldmark 希望让自定义 AST 节点、块级解析器、行内解析器、段落转换器、AST 转换器、渲染器都能被外部注入。
- 符合标准(Standards-compliant):Markdown 方言众多,而 GitHub Flavored Markdown(GFM)基于 CommonMark 且被广泛使用,因此 goldmark 以 CommonMark 0.31.2 规范为基准实现,完全合规(fully compliant)。
- 结构良好(Well-structured):采用 AST 设计,并保留节点的源位置信息(preserves source position of nodes)。
- 纯 Go 实现(Written in pure Go):不依赖 C 扩展,便于交叉编译与集成。
README 中还对当时的同类库做了横向对比,可以作为选型参考:golang-commonmark被认为本质上是markdown-it的移植;blackfriday.v2虽然速度快、使用广泛,但不符合 CommonMark 规范,且其 AST 使用结构体而非接口,导致无法从包外部扩展。此外 blackfriday 在列表渲染上与 CommonMark 存在差异(如深层嵌套列表输出不正确、列表块无法有第二行等问题),从 GitHub 迁移 Markdown 文本到 blackfriday 系 wiki 时列表很容易被破坏。这些对比结论在仓库内可以通过 goldmark 源码目录 的结构与实现得到印证。
核心特性一览
- 符合标准:完全兼容最新 CommonMark 规范(当前 README 声明为 CommonMark 0.31.2)。
- 可扩展:可以轻松添加
@username提及等自定义语法,支持注入 AST 节点、块级元素解析器、行内元素解析器、段落转换器、AST 整体转换器以及自定义渲染器。 - 性能:与 CommonMark 的 C 语言参考实现 cmark 性能相当。
- 健壮:通过
go test --fuzz模糊测试验证。 - 内置扩展:自带表格(Table)、删除线(Strikethrough)、任务列表(TaskList)、定义列表(DefinitionList)等常用扩展。
- 零外部依赖:仅依赖 Go 标准库。
快速上手:安装与最小转换
安装命令:
go get github.com/yuin/goldmark导入并转换 Markdown 文档,使用 CommonMark 合规模式:
import ( "bytes" "github.com/yuin/goldmark" ) var buf bytes.Buffer if err := goldmark.Convert(source, &buf); err != nil { panic(err) }这里的goldmark.Convert实际上是包级便捷函数。从仓库源码 markdown.go 可以看到,它内部复用了一个包级默认实例defaultMarkdown = New(),其完整转换链路为:
func (m *markdown) Convert(source []byte, writer io.Writer, opts ...parser.ParseOption) error { reader := text.NewReader(source) doc := m.parser.Parse(reader, opts...) return m.renderer.Render(writer, source, doc) }即:构造text.Reader→ 调用parser.Parse生成 AST → 调用renderer.Render输出 HTML。默认 Parser 由 DefaultParser 提供,注册了默认的块级解析器、行内解析器和段落转换器;默认 Renderer 由 DefaultRenderer 提供,注册了优先级为 1000 的 HTML 渲染器。
如果需要在转换时传入解析期选项,可以这样写:
var buf bytes.Buffer if err := goldmark.Convert(source, &buf, parser.WithContext(ctx)); err != nil { panic(err) }| 函数式选项 | 类型 | 说明 |
|---|---|---|
parser.WithContext | parser.Context | 解析阶段使用的上下文 |
上下文选项
| 函数式选项 | 类型 | 说明 |
|---|---|---|
parser.WithIDs | parser.IDs | 允许改变与元素 id 相关的逻辑(例如标题 id 的自动生成) |
自定义 Parser 与 Renderer:goldmark.New 的完整配置
大多数生产场景需要使用goldmark.New构建定制实例。官方示例如下:
import ( "bytes" "github.com/yuin/goldmark" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/renderer/html" ) md := goldmark.New( goldmark.WithExtensions(extension.GFM), goldmark.WithParserOptions( parser.WithAutoHeadingID(), ), goldmark.WithRendererOptions( html.WithHardWraps(), html.WithXHTML(), ), ) var buf bytes.Buffer if err := md.Convert(source, &buf); err != nil { panic(err) }从 markdown.go 的实现可以看出New的装配顺序:先以DefaultParser()和DefaultRenderer()初始化结构体,然后依次应用所有Option,最后遍历extensions调用各自的Extend(md)方法完成扩展注册。这也解释了 README 中的选项使用约束:
| 函数式选项 | 类型 | 说明 |
|---|---|---|
goldmark.WithParser | parser.Parser | 覆盖默认解析器,必须在WithParserOptions和WithExtensions之前传入 |
goldmark.WithRenderer | renderer.Renderer | 覆盖默认渲染器,必须在WithRendererOptions和WithExtensions之前传入 |
goldmark.WithParserOptions | ...parser.Option | 追加解析器选项 |
goldmark.WithRendererOptions | ...renderer.Option | 追加渲染器选项 |
goldmark.WithExtensions | ...goldmark.Extender | 追加扩展 |
Parser 选项
| 函数式选项 | 类型 | 说明 |
|---|---|---|
parser.WithBlockParsers | 元素为parser.BlockParser的util.PrioritizedSlice | 解析块级元素的解析器 |
parser.WithInlineParsers | 元素为parser.InlineParser的util.PrioritizedSlice | 解析行内元素的解析器 |
parser.WithParagraphTransformers | 元素为parser.ParagraphTransformer的util.PrioritizedSlice | 用于转换段落节点的转换器 |
parser.WithASTTransformers | 元素为parser.ASTTransformer的util.PrioritizedSlice | 用于转换整个 AST 的转换器 |
parser.WithAutoHeadingID | - | 开启标题 id 自动生成 |
parser.WithAttribute | - | 开启自定义属性(当前仅标题支持属性) |
HTML Renderer 选项
| 函数式选项 | 类型 | 说明 |
|---|---|---|
html.WithWriter | html.Writer | 用于向io.Writer写入内容的 Writer |
html.WithHardWraps | - | 将换行渲染为<br> |
html.WithXHTML | - | 以 XHTML 格式渲染 |
html.WithUnsafe | - | 默认情况下 goldmark 不渲染原始 HTML 和潜在危险链接;开启后按原文渲染 |
内置扩展体系
goldmark 官方内置的扩展定义在 extension 目录 中,每个扩展都实现了goldmark.Extender接口,可通过goldmark.WithExtensions注册。
扩展总览
| 扩展 | 规范依据 | 说明 |
|---|---|---|
extension.Table | GFM: Tables | 表格语法 |
extension.Strikethrough | GFM: Strikethrough | 删除线语法(~~text~~) |
extension.Linkify | GFM: Autolinks | 自动识别 URL 并生成链接 |
extension.TaskList | GFM: Task list items | 任务列表(- [ ]/- [x]) |
extension.GFM | — | 同时启用 Table、Strikethrough、Linkify、TaskList 的组合扩展 |
extension.DefinitionList | PHP Markdown Extra: Definition lists | 定义列表 |
extension.Footnote | PHP Markdown Extra: Footnotes | 脚注 |
extension.Typographer | smartypants 风格 | 将 ASCII 标点替换为排版实体 |
extension.CJK | — | 面向中日韩(CJK)文本的扩展快捷键 |
关于extension.GFM,仓库源码 gfm.go 清晰展示了它的组合本质——Extend方法依次调用Linkify.Extend、Table.Extend、Strikethrough.Extend、TaskList.Extend:
func (e *gfm) Extend(m goldmark.Markdown) { Linkify.Extend(m) Table.Extend(m) Strikethrough.Extend(m) TaskList.Extend(m) }同时注意 README 的两个重要提醒:
- GFM 扩展不会过滤 [6.11 Disallowed Raw HTML] 规范定义的禁用标签,如需过滤 HTML 标签,请参考下方"安全"章节。
- 如需解析 GitHub emoji,可使用 goldmark-emoji 扩展(本仓库 go.mod 中记录的 goldmark-emoji v1.0.5 即为此用途)。
自定义属性(Attributes)
parser.WithAttribute选项允许在部分元素上定义属性。目前仅标题支持属性,且该语法仍在 CommonMark 论坛讨论中,未来可能变更。支持的写法有三种(ATX 标题后置、ATX 标题行内、Setext 标题后置):
## heading ## {#id .className attrName=attrValue class="class1 class2"} ## heading {#id .className attrName=attrValue class="class1 class2"}heading {#id .className attrName=attrValue} ============各扩展深度解析
Table 扩展
Table 扩展实现了 GFM 规范中的 Tables(扩展)部分。注意其规范测试用例按 XHTML 定义,因此使用了部分在 HTML5 中已废弃的属性。对齐渲染方式可通过选项覆盖:
| 函数式选项 | 类型 | 说明 |
|---|---|---|
extension.WithTableCellAlignMethod | extension.TableCellAlignMethod | 指定表格单元格的对齐方式 |
Typographer 扩展
Typographer 扩展将纯 ASCII 标点转换为排版标点的 HTML 实体,默认替换规则如下:
| 标点 | 默认实体 |
|---|---|
' | ‘、’ |
" | “、” |
-- | – |
--- | — |
... | … |
<< | « |
>> | » |
可通过extensions.WithTypographicSubstitutions覆盖默认替换,例如将左右单引号改为‚,并用nil禁用某个替换:
markdown := goldmark.New( goldmark.WithExtensions( extension.NewTypographer( extension.WithTypographicSubstitutions(extension.TypographicSubstitutions{ extension.LeftSingleQuote: []byte("‚"), extension.RightSingleQuote: nil, // nil 表示禁用该替换 }), ), ), )Linkify 扩展
Linkify 扩展实现了 GFM 的 Autolinks(扩展)部分。由于规范未定义 URL 细节,存在大量歧义场景,因此该扩展允许通过选项覆盖自动链接匹配模式:
| 函数式选项 | 类型 | 说明 |
|---|---|---|
extension.WithLinkifyAllowedProtocols | [][]byte或[]string | 允许的协议列表,如[]string{ "http:" } |
extension.WithLinkifyURLRegexp | *regexp.Regexp | 定义 URL(含协议)的正则 |
extension.WithLinkifyWWWRegexp | *regexp.Regexp | 定义以www.开头的 URL 的正则,对应 GFM 的扩展 www 自动链接 |
extension.WithLinkifyEmailRegexp | *regexp.Regexp | 定义邮箱地址的正则 |
配合mvdan.cc/xurls/v2使用的示例(同时开启了html.WithUnsafe以保留原始内容):
import "mvdan.cc/xurls/v2" markdown := goldmark.New( goldmark.WithRendererOptions( html.WithXHTML(), html.WithUnsafe(), ), goldmark.WithExtensions( extension.NewLinkify( extension.WithLinkifyAllowedProtocols([]string{ "http:", "https:", }), extension.WithLinkifyURLRegexp( xurls.Strict(), ), ), ), )Footnote 扩展
Footnote 扩展实现了 PHP Markdown Extra 的脚注功能,支持以下选项:
| 函数式选项 | 类型 | 说明 |
|---|---|---|
extension.WithFootnoteIDPrefix | []byte或string | id 属性前缀 |
extension.WithFootnoteIDPrefixFunction | func(gast.Node) []byte | 根据给定 Node 决定 id 属性的函数 |
extension.WithFootnoteLinkTitle | []byte或string | 脚注链接的可选 title 属性 |
extension.WithFootnoteBacklinkTitle | []byte或string | 脚注返回链接的可选 title 属性 |
extension.WithFootnoteLinkClass | []byte或string | 脚注链接的 class,默认footnote-ref |
extension.WithFootnoteBacklinkClass | []byte或string | 脚注返回链接的 class,默认footnote-backref |
extension.WithFootnoteBacklinkHTML | []byte或string | 脚注返回链接的 HTML,默认↩︎ |
特殊替换规则:部分选项字符串中的^^会在 HTML 输出中被替换为对应的脚注编号;%%会被替换为引用编号(一个脚注可能被多次引用)。
WithFootnoteIDPrefix与WithFootnoteIDPrefixFunction在"多个 Markdown 文档渲染进同一个 HTML 页面"的场景下尤为有用,可避免脚注 id 相互冲突:
for _, path := range files { source := readAll(path) prefix := getPrefix(path) markdown := goldmark.New( goldmark.WithExtensions( NewFootnote( WithFootnoteIDPrefix(path), ), ), ) var b bytes.Buffer err := markdown.Convert(source, &b) if err != nil { t.Error(err.Error()) } }而WithFootnoteIDPrefixFunction通过回调函数动态决定前缀,可以从节点的文档元信息(Meta())中取值:
markdown := goldmark.New( goldmark.WithExtensions( NewFootnote( WithFootnoteIDPrefixFunction(func(n gast.Node) []byte { v, ok := n.OwnerDocument().Meta()["footnote-prefix"] if ok { return util.StringToReadOnlyBytes(v.(string)) } return nil }), ), ), ) for _, path := range files { source := readAll(path) var b bytes.Buffer doc := markdown.Parser().Parse(text.NewReader(source)) doc.Meta()["footnote-prefix"] = getPrefix(path) err := markdown.Renderer().Render(&b, source, doc) }这段代码还演示了一个关键模式:通过markdown.Parser()和markdown.Renderer()可以分离 Parse 与 Render 两步,在中间自由操作文档元数据(这两个方法定义于 markdown.go)。
配合 goldmark-meta 扩展,还可以直接在 Markdown 文档的 YAML front matter 中声明前缀:
--- title: document title slug: article1 footnote-prefix: article1 --- # My articleCJK 扩展
CommonMark 将兼容性置于最高优先级,而原始 Markdown 由西方人设计,对 CJK(中日韩)语言缺乏考量。CJK 扩展为东亚文字用户提供额外选项:
| 函数式选项 | 类型 | 说明 |
|---|---|---|
extension.WithEastAsianLineBreaks | ...extension.EastAsianLineBreaksStyle | 默认EastAsianLineBreaksStyleSimple;软换行默认渲染为换行,部分亚洲用户会视其为多余空格,开启后东亚宽字符之间的软换行将被忽略 |
extension.WithEscapedSpace | - | 按 CommonMark 规范,以东亚标点开头的强调若无空格环绕不会被解析为强调;此选项可通过在强调两侧加入"不渲染"的空格来规避,如太郎は\ **「こんにちわ」**\ といった |
换行样式(Styles of Line Breaking)
| 样式 | 说明 |
|---|---|
EastAsianLineBreaksStyleSimple | 若换行两侧均为东亚宽字符则忽略软换行,行为与 Pandoc 的east_asian_line_breaks一致 |
EastAsianLineBreaksCSS3Draft | 实现 CSS Text Level 3 的 Segment Break Transformation Rules(并带有若干增强) |
两种样式对同一输入的对比——输入 Markdown:
私はプログラマーです。 東京の会社に勤めています。 GoでWebアプリケーションを開発しています。EastAsianLineBreaksStyleSimple的输出(Go前的换行被保留,因为。与G不构成宽字符邻接):
<p>私はプログラマーです。東京の会社に勤めています。\nGoでWebアプリケーションを開発しています。</p>EastAsianLineBreaksCSS3Draft的输出(全部软换行被忽略):
<p>私はプログラマーです。東京の会社に勤めています。GoでWebアプリケーションを開発しています。</p>安全模型:默认拒绝原始 HTML 与危险链接
goldmark 的默认行为是不渲染原始 HTML 与潜在危险链接(例如javascript:协议的链接)。README 的明确建议是:如果需要对不可信内容做更精细的控制,应叠加 HTML 净化器(sanitizer),例如 bluemonday。与渲染相关的html.WithUnsafe选项正是"默认安全"这一模型的开关——只有显式开启后,goldmark 才会按原文渲染原始 HTML 或危险链接。
性能基准
goldmark 的性能与 CommonMark 的 C 参考实现 cmark 相当。README 给出了两组基准数据(测试环境:MBP 2019 13 英寸,i5,16GB,Go 1.17)。
与其它 Go Markdown 库对比(_benchmark目录中可复现):
BenchmarkMarkdown/Blackfriday-v2-8 302 3743747 ns/op 3290445 B/op 20050 allocs/op BenchmarkMarkdown/GoldMark-8 280 4200974 ns/op 2559738 B/op 13435 allocs/op BenchmarkMarkdown/CommonMark-8 226 5283686 ns/op 2702490 B/op 20792 allocs/op BenchmarkMarkdown/Lute-8 12 92652857 ns/op 10602649 B/op 40555 allocs/op BenchmarkMarkdown/GoMarkdown-8 13 81380167 ns/op 2245002 B/op 22889 allocs/opREADME 同时客观指出:blackfriday v2 看似最快,但由于不符合 CommonMark 规范,其性能不能与 CommonMark 合规库直接比较;goldmark 在构建干净、可扩展 AST 结构、达到 CommonMark 完全合规的前提下,内存占用更低(2.56 MB/op vs blackfriday 3.29 MB/op),分配次数也更少(13435 vs 20050),速度合理。
与 cmark(C 参考实现)对比(_data.md,50 次迭代平均值):
----------- cmark ----------- file: _data.md iteration: 50 average: 0.0044073057 sec ------- goldmark ------- file: _data.md iteration: 50 average: 0.0041611990 sec内部架构:面向扩展开发者的处理管线
goldmark 的 Markdown 处理流程在 README 中以示意图呈现,核心是一条"解析 → 渲染"的流水线:
<Markdown in []byte, parser.Context> | V +-------- parser.Parser --------------------------- | 1. Parse block elements into AST | 1. If a parsed block is a paragraph, apply | ast.ParagraphTransformer | 2. Traverse AST and parse blocks. | 1. Process delimiters(emphasis) at the end of | block parsing | 3. Apply parser.ASTTransformers to AST | V <ast.Node> | V +------- renderer.Renderer ------------------------ | 1. Traverse AST and apply renderer.NodeRenderer | corespond to the node type | V <Output>结合 markdown.go 的Convert实现,这条管线可以这样理解:
- 读取:Markdown 文档通过
text.Reader接口读取(定义于 text/reader.go)。 - 解析(Parse):
parser.Parser先将块级元素解析进 AST;若解析出的块是段落,则应用ast.ParagraphTransformer;随后遍历 AST 解析行内元素(在块解析结束时统一处理强调等分隔符 delimiter);最后对整个 AST 应用parser.ASTTransformer。 - 渲染(Render):
renderer.Renderer遍历 AST,对每个节点类型应用对应的renderer.NodeRenderer,产出最终输出。
关于 AST 节点的关键设计
一个重要的设计细节是:AST 节点不持有具体文本。AST 节点只保存文档的片段信息,由text.Segment表示,text.Segment包含三个属性:Start、End、Padding。这种"引用而非拷贝"的设计保证了内存效率和源位置的可追溯性(对应 README 中"preserves source position of nodes"的特性承诺)。
编写自定义扩展的四步摘要
README 给出了扩展开发的最小路线图,完整的示例实现可参考仓库的 extension 目录(如 linkify.go、typographer.go 等均为现成的扩展范式):
- 定义 AST 节点:定义一个嵌入
ast.BaseBlock或ast.BaseInline的结构体。 - 编写解析器:实现
parser.BlockParser或parser.InlineParser接口。 - 编写渲染器:实现
renderer.NodeRenderer接口。 - 封装扩展:定义实现
goldmark.Extender接口(即Extend(Markdown)方法)的扩展类型。
生态扩展与运行时加载
goldmark 拥有丰富的社区扩展生态,README 中列出的第三方扩展覆盖了元数据、代码高亮、emoji、数学公式、图表、PDF/LaTeX/Telegram 渲染等场景,例如:YAML 元数据(goldmark-meta)、语法高亮(goldmark-highlighting)、emoji(goldmark-emoji)、MathJax/KaTeX 数学支持、Mermaid/Pikchr/D2 图表、Wikilink、TOC 目录生成、PDF 渲染器(可通过goldmark.WithRenderer()传入)、LaTeX 渲染器等。此外,goldmark-dynamic 允许用 Lua 编写扩展并在运行时加载,无需重新编译。这些扩展均以goldmark.WithExtensions/goldmark.WithRenderer的方式接入,与本仓库内的集成路径一致。
结语
从本仓库的实际集成(go.mod 记录github.com/yuin/goldmark v1.7.13)到 README 承诺的 CommonMark 0.31.2 完全合规,goldmark 为 Go 生态提供了一个"默认安全、结构清晰、扩展开放"的 Markdown 处理基座。无论是直接使用goldmark.Convert快速渲染,还是通过goldmark.New组合 GFM 等扩展、注入自定义 AST 与渲染器,亦或是借助parser/renderer两级选项体系精细控制输出,开发者都能在本文覆盖的 API 骨架之上构建自己的文档处理能力。对于需要深度定制语法的场景,extension 目录 中的各扩展源码就是最佳的学习模板。
- 网络安全
【免费下载链接】sliver
Adversary Emulation Framework
相关推荐
goldmark-emoji 完全指南:为 Go 的 goldmark Markdown 解析器扩展 `:joy:` 风格表情支持
goldmark emoji 完全指南:为 Go 的 goldmark Markdown 解析器扩展 :joy: 风格表情支持 goldmark emoji 是
网络安全漏洞扫描渗透测试应用安全深入解析 Markd:基于 Crystal 的 CommonMark 兼容高性能 Markdown 解析器
深入解析 Markd:基于 Crystal 的 CommonMark 兼容高性能 Markdown 解析器 Markd 是一个用 Crystal 编写的 Mar
编程语言编译器标准库语言运行时Nhost 集成指南:深入解析 html-to-markdown v2 的 Go 转换器架构、插件机制与 CLI 实战
Nhost 集成指南:深入解析 html to markdown v2 的 Go 转换器架构、插件机制与 CLI 实战 本文以 Nhost 仓库中 vendor
后端认证鉴权数据库无服务开发工具云原生
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考