如果你对 Markdown 语法理解很少,这里有一份极简的教程:https://studygolang.com/markdown。

CommonMark 和 GFM

我们常常会在一些 Markdown 解析库的描述中看到类似这样的话:兼容 CommonMark,支持 GFM 扩展等。

CommonMark 是什么

这是 Markdown 标准或规范。
CommonMark 官网(https://commonmark.org )提到:

phpmarkdown转html专为 Gopher 预备的 Markdown 教程 AJAX

由于没有明确的规范,Markdown 解析渲染会有很大差异。
因此用户常常会创造在一个别系(例如 GitHub)上渲染正常的文档在另一个别系上渲染不正常。
更糟糕的是由于 Markdown 中不存在“语法缺点”,以是无法立即创造这类问题。

在 Markdown 处理上“模糊的精确”是不可取的。
以是 CommonMark 规范的目的便是肃清二义性,制订统一明确的 Markdown 解析渲染规则。

该规范的紧张参与者包括:

David Greenspan, 来自 MeteorVicent Marti, 来自 GitHubNeil Williams, 来自 RedditBenjamin Dumke-von der Ehe, 来自 Stack ExchangeJeff Atwood, 前 Stack Exchange 联合创始人,Discourse 创始人

从作者阵容我们可以看出,该规范算是众望所归了,由于这几大社区都须要一个标准化的 Markdown。

至于规范的详细内容,有兴趣的可以看官网上的规范定义文档,个中的一些点,在后文先容 Go 措辞的 Markdown 解析器时会先容。

GFM 又是什么

有了 CommonMark(标准 Markdown),为什么又会有 GFM?

GFM 是 GitHub Flavored Markdown 的缩写,即 GitHub 风格的 Markdown,它和标准 Markdown 存在一些差异,紧张是增加一些功能,以是一样平常叫做 GFM 扩展。
类似的、基于标准 Markdown 的衍生分支有很多,但由于 GitHub 的盛行,GFM 险些成为了最强大的一个,而且各种解析器、编辑器都会支持 GFM。

对付大部分人来说,关于 CommonMark 和 GFM,知道这么多就够了。
GFM 规范见:https://github.github.com/gfm/。

Go 措辞 Markdown 解析器

在 GitHub 上一搜,创造 Go 措辞 Markdown 解析器不止一个,如何选择呢?一样平常来说根据 Star 数来。
其余,如果是实现某个规范的库,看这个规范有无推举。
比如我们找的是 Markdown 库,看看 CommonMark 有无推举。

在 List of CommonMark Implementations 列出了各个措辞的 CommonMark 实现,个中 Go 措辞列出了 4 个。
虽然 russross/blackfriday 是最早也是 Star 数最多的 Go 措辞 Markdown 解析库,然而它不兼容 CommonMark。

由于 Hugo 从 0.60.0 开始,Markdown 解析器默认利用 yuin/goldmark,之前利用的是 blackfriday。
因此本文我们紧张通过学习 goldmark 这个解析器来深入学习 Markdown。

简介

goldmark 是一个用 Go 编写的 Markdown 解析器。
易于扩展,符合标准,构造良好。
它兼容最新的 CommonMark 0.29 规范。

该库希望知足如下需求:

易于扩展与其他轻量级标记措辞(例如 reStructuredText)比较,Markdown 在文档表达式方面的表现较差。
我们可以对 Markdown 语法进行了扩展,例如 PHP Markdown Extra,GitHub 风格 Markdown(GFM)。
符合标准CommonMark 繁芜且难以完备实现。
Markdown 有许多方言。
GitHub Flavored Markdown 被广泛利用并且基于 CommonMark,有效地提出了 CommonMark 是否是空想规范的问题。
构造良好基于 AST;保留节点的源位置。
纯 Go 措辞实现

基于此,该库具有如下一些特性:

符合标准。
goldmark 完备符合最新的 CommonMark 规范。
可扩展。
你是否要在 Markdown 中添加 @username 提到谁的语法?你可以轻松地在 goldmark 中实现。
你可以添加 AST 节点,用于块级元素的解析器,用于内联级元素的解析器,用于段落的转换器,用于全体 AST 构造的转换器以及渲染器等。
性能。
goldmark 的性能与用 C 措辞编写的 CommonMark 参考实现 cmark 的性能相称。
健壮性。
goldmark 已通过模糊测试工具 go-fuzz 进行了测试。
内置扩展。
goldmark 附带了常见的扩展,例如表格,删除线,任务列表和定义列表。
只依赖标准库。
利用

本文利用的 Go 版本是 1.14.x,依赖管理利用 Go Module。

首先安装 goldmark:

$gogetgithub.com/yuin/goldmark

为了方便演示,我们利用 https://studygolang.com/markdown 上的教程作为原始 markdown 内容(部分内容是 studygolang 特有的)。

Demo1

代码如下:

funcdemo1(){source,err:=ioutil.ReadFile("guide.md")iferr!=nil{panic(err)}f,err:=os.Create("guide1.html")iferr!=nil{panic(err)}err=goldmark.Convert(source,f)iferr!=nil{panic(err)}}guide.md 存放 https://studygolang.com/markdown 上的原始 markdown 内容,略有增减;通过 goldmark 的 Convert 函数将 markdown 转为 html,没有利用任何选项;

在项目目录天生 guide1.html,在浏览器中打开,创造存在以下问题:

不支持自动链接,例如 https://studygolang.com 不会被识别为链接;不支持删除线,即 ~~;不支持表格;不支持任务列表;不支持语法高亮;不支持 @;不支持表情;Demo2

demo1 中问题 1-4 是 GFM 支持的语法,因此我们可以通过 goldmark 内置的 GFM 扩展实现。

funcdemo2(){source,err:=ioutil.ReadFile("guide.md")iferr!=nil{panic(err)}f,err:=os.Create("guide2.html")iferr!=nil{panic(err)}//自定义解析器markdown:=goldmark.New(//支持GFMgoldmark.WithExtensions(extension.GFM),)err=markdown.Convert(source,f)iferr!=nil{panic(err)}}验证上面问题 1-4 创造都办理了;这里关于自动链接,有一个小问题:中文标点符号问题。
自动链接:https://studygolang.com 这里的冒号是中文的,因此识别不出来。
以是,如果利用自动链接,把稳链接前后加上英文空格;或单独链接总是利用 <> 包裹,这是 CommonMark 支持的语法;Dome3

语法高亮问题,虽然 goldmark 库没有内置该扩展,但该库作者在其余一个包实现了,这便是 goldmark-highlighting。

funcdemo3(){source,err:=ioutil.ReadFile("guide.md")iferr!=nil{panic(err)}f,err:=os.Create("guide3.html")iferr!=nil{panic(err)}//自定义解析器markdown:=goldmark.New(//支持GFMgoldmark.WithExtensions(extension.GFM),//语法高亮goldmark.WithExtensions(highlighting.NewHighlighting(highlighting.WithStyle("monokai"),highlighting.WithFormatOptions(html.WithLineNumbers(true),),),),)err=markdown.Convert(source,f)iferr!=nil{panic(err)}}

这是语法高亮后的效果:

语法高亮利用的是一个 Go 第三方库:alecthomas/chroma学习 goldmark 的设计

上节遗留的问题先不处理,由于涉及到扩展 goldmark,我们先学习下 goldmark 的设计。

该库的主包(github.com/yuin/goldmark)公开的内容不多,一共 3 个类型和 3 个函数。

Markdown 是一个接口

typeMarkdowninterface{//ConvertinterpretsaUTF-8bytessourceinMarkdownandwriterendered//contentstoawriterw.Convert(source[]byte,writerio.Writer,opts...parser.ParseOption)error//ParserreturnsaParserthatwillbeusedforconversion.Parser()parser.Parser//SetParsersetsaParsertothisobject.SetParser(parser.Parser)//ParserreturnsaRendererthatwillbeusedforconversion.Renderer()renderer.Renderer//SetRenderersetsaRenderertothisobject.SetRenderer(renderer.Renderer)}解析器:Parser渲染器:Renderer解析 markdown 文本并将渲染的结果写入 io.Writer 中

从这个接口的定义可以看到,我们可以定义自己的 Parser 或 Renderer 来改变 goldmark 的事情办法。
一样平常我们不须要这么做,只须要利用默认的实现即可,这也便是 DefaultParser() 和 DefaultRenderer() 两个函数的浸染。
其余,在转换时(Convert)支持指定解析选项。

然而在该包中,我们并没有看到 Markdown 接口的实现类型。
很显然,实现类型没有导出。
这表示了“依赖抽象而不依赖详细”的设计原则。

看看获取 Markdown 接口实例的 New 函数:

funcNew(options...Option)Markdown

该函数吸收一个不定参数:Option,用于掌握 Markdown 的行为。
这里引出了 Go 中常见的一个设计模式。

Go 不是完备的面向工具措辞。
当类型中有较多成员,且可以通过外部掌握时,根据封装的原则,一样平常不建议将这些字段导出(公开),但这样一来布局函数就须要能吸收很多参数;亦或是希望通过其他办法扩展。
在 Go 中有两种较常见的设计方法。

1)通过其余一个构造体来掌握

这里以 BigCache 这个包为例,该包中的 Config 构造体便是这种设计。
这么做有什么好处?

funcNewBigCache(configConfig)(BigCache,error)

一方面掌握了 BigCache 类型的行为,避免实例化后可以随意变动,起到了封装的浸染。
另一方面,让布局函数更简洁,只须要吸收一个 Config 即可(把稳最好利用 Config 值类型,而不是指针)。
而且可以通过供应一些 Config 的默认值来做到更易用,比如 bigcache.DefaultConfig() 函数便是这样的例子。

2)通过一个函数类型来掌握

前面提到,goldmark 只导出了 Markdown 接口,并没有导出该接口的详细实现类型。
那想要掌握详细实现类型的行为怎么办呢?这便是 Option 这个函数类型的浸染:吸收一个 markdown 类型指针(把稳这里不是 Markdown 接口,而是实现了该接口的详细类型)

typeOptionfunc(markdown)

然后布局函数中吸收一个 Option 类型的不定参数,来掌握 Markdown 的行为。

funcNew(options...Option)Markdown

为了方便利用 ,一样平常包会供应多少得到 Option 实例的方法。
goldmark 包供应了 5 个返回 Option 的函数:

//增加扩展funcWithExtensions(ext...Extender)Option//许可你覆盖默认的ParserfuncWithParser(pparser.Parser)Option//为Parser修正配置选项funcWithParserOptions(opts...parser.Option)Option//许可你覆盖默认的RenderfuncWithRenderer(rrenderer.Renderer)Option//为Renderer修正配置选项funcWithRendererOptions(opts...renderer.Option)Option

在 Demo3 中利用了 WithExtensions() 为解析器增加扩展,该函数吸收一个 Extender 类型的不定参数。

Extender 也是一个接口

该接口用于扩展 Markdown,因此方法吸收一个 Markdown 参数:

typeExtenderinterface{//ExtendextendstheMarkdown.Extend(Markdown)}

由此可见,所谓的 goldmark 扩展,便是一个实现了 Extender 接口的类型。

Convert 函数

这是一个为了方便利用的函数,在 Go 措辞标准库中有大量这样的设计。

针对包的紧张类型供应一个默认实例,包级便利函数直接调用该默认实例的相应方法

比如 log 包中的 std 是一个默认的 Logger 实例、net/http 包中的 DefaultClient 是一个默认的 Client 实例。

而 Convert 函数的实现如下:

funcConvert(source[]byte,wio.Writer,opts...parser.ParseOption)error{returndefaultMarkdown.Convert(source,w,opts...)}

个中 defaultMarkdown 便是一个 Markdown 的实例:

vardefaultMarkdown=New()

因此终极调用的还是 Markdown 接口的 Convert 方法。

New 函数

末了看看该包 New 函数实现:

//NewreturnsanewMarkdownwithgivenoptions.funcNew(options...Option)Markdown{md:=&markdown{parser:DefaultParser(),renderer:DefaultRenderer(),extensions:[]Extender{},}for_,opt:=rangeoptions{opt(md)}for_,e:=rangemd.extensions{e.Extend(md)}returnmd}markdown 实现了 Markdown 接口;parser 和 renderer 利用 goldmark 包默认的;遍历实行 Options;如果 Option 是 Extender,则 md.extensions 会赋上值:func WithExtensions(ext ...Extender) Option { return func(m markdown) { m.extensions = append(m.extensions, ext...) }}末了遍历实行 Extender 的 Extend 方法;小结

从 goldmark 的设计和源码看出,它大量利用接口,包括 Parser 和 Renderer 都是接口,这使得它具有极强的可扩展性。
接下来我们会考试测验自己实现一个 goldmark 扩展。

自己实现一个 goldmark 扩展

上面我们遗留了两个问题没有处理,即支持 @ 和 :+1: 这种形式的表情。
现在我们通过实现自己的扩展来办理这两个问题。

如何实现一个扩展

goldmark 的文档有一些扩展开拓的内容,供应了一个 goldmark 处理 markdown 的概要图:

<Markdownin[]byte,parser.Context>|V+--------parser.Parser---------------------------|1.ParseblockelementsintoAST|1.Ifaparsedblockisaparagraph,apply|ast.ParagraphTransformer|2.TraverseASTandparseblocks.|1.Processdelimiters(emphasis)attheendof|blockparsing|3.Applyparser.ASTTransformerstoAST|V<ast.Node>|V+-------renderer.Renderer------------------------|1.TraverseASTandapplyrenderer.NodeRenderer|corespondtothenodetype|V<Output>

你可能看着有点晕。
整体上扩展开拓有 4 个事情:

定义一个 AST(抽象语法树)节点(构造体),该节点须要嵌入一个 ast.BaseBlock 或 ast.BaseInline;定义一个解析器(Parser),实现 parser.BlockParser 或parser.InlineParser;定义一个渲染器(Renderer),实现 renderer.NodeRenderer;定义一个 goldmark 扩展,实现 goldmark.Extender;

个中 Block 和 Inline 是什么意思?学习过 Web 前真个该当理解。
Markdown 和 HTML 类似,将内容元素分为块级元素(Block)和行级元素(Inline):(块级元素优先级高于行级元素)

块级元素:块引用(>)、列表项和列表(列表只能包含列表项)、分隔线、标题、代码块、某些 HTML 块、段落等行级元素:内联代码(code)、强调、加粗、链接、图片、某些 HTML 标签、文本等

根据以上的内容,要自己实现一个扩展还是有难度的。
好在 goldmark 中内置了一些扩展,可以作为参考。

GFM 的 strikethrough 扩展源码学习

CommonMark 是不支持删除线(strikethrough)的,而 GFM 支持。
因此 goldmark 通过 strikethrough 这个扩展来实现对 GFM 删除线的支持。
根据扩展实现的步骤来学习下 strikethrough 扩展的源码。

1)AST 节点构造体:Strikethrough

//AStrikethroughstructrepresentsastrikethroughofGFMtext.typeStrikethroughstruct{gast.BaseInline}//DumpimplementsNode.Dump.func(nStrikethrough)Dump(source[]byte,levelint){gast.DumpHelper(n,source,level,nil,nil)}//KindStrikethroughisaNodeKindoftheStrikethroughnode.varKindStrikethrough=gast.NewNodeKind("Strikethrough")//KindimplementsNode.Kind.func(nStrikethrough)Kind()gast.NodeKind{returnKindStrikethrough}//NewStrikethroughreturnsanewStrikethroughnode.funcNewStrikethrough()Strikethrough{return&Strikethrough{}}内嵌了一个 gast.BaseInline(gast 是 ast 导入时重命名的),表明是一个行级元素;根据 goldmark 的设计,AST 节点构造体须要实现 ast.Node 接口,然而该接口拥有很多方法,为了方便实现,该库利用了内嵌的办法来处理;ast.NewNodeKind() 布局函数得到一个 NodeKind;

看一下类图:

由于 Go 不是完备面向工具措辞,因此这里的类图是不严谨的:

BaseNode 并没有实现 Node 接口,由于它没有实现 Kind 和 Dump 方法,这也是由于 Go 没有抽象类的观点;BaseBlock 和 BaseInline 的差异在于实现的几个方法,BaseLine 里面好几个方法直接 panic,不须要实现;由于 BaseNode、BaseBlock 和 BaseInline 实际没有实现 Node 接口,因此它们不能直接当做 Node 利用,一定程度上仿照了抽象类;Strikethrough 扩展通过内嵌 BaseInline “继续”了相应的实现方法,同时供应 Kind 和 Dump 的实现,达到完全实现了 Node 接口的目的,因此是一个 Node;

2)Strikethrough 解析器

vardefaultStrikethroughDelimiterProcessor=&strikethroughDelimiterProcessor{}typestrikethroughParserstruct{}vardefaultStrikethroughParser=&strikethroughParser{}//NewStrikethroughParserreturnanewInlineParserthatparses//strikethroughexpressions.funcNewStrikethroughParser()parser.InlineParser{returndefaultStrikethroughParser}func(sstrikethroughParser)Trigger()[]byte{return[]byte{'~'}}func(sstrikethroughParser)Parse(parentgast.Node,blocktext.Reader,pcparser.Context)gast.Node{before:=block.PrecendingCharacter()line,segment:=block.PeekLine()node:=parser.ScanDelimiter(line,before,2,defaultStrikethroughDelimiterProcessor)ifnode==nil{returnnil}node.Segment=segment.WithStop(segment.Start+node.OriginalLength)block.Advance(node.OriginalLength)pc.PushDelimiter(node)returnnode}InlineParser 接口只有两个方法:Trigger 和 Parse;Trigger 表示碰着什么字符触发该节点解析;Parse 是扩展的一个关键点,不同的扩展实现办法不同。
这里有两点提一下:block.PeekLine() 获取当前行,这个很有用,解析基本都能用到;block.Advance() 表示移动内部指针,可以理解为文件读取过程中的 Seek;

3)Strikethrough 渲染器

//StrikethroughHTMLRendererisarenderer.NodeRendererimplementationthat//rendersStrikethroughnodes.typeStrikethroughHTMLRendererstruct{html.Config}//NewStrikethroughHTMLRendererreturnsanewStrikethroughHTMLRenderer.funcNewStrikethroughHTMLRenderer(opts...html.Option)renderer.NodeRenderer{r:=&StrikethroughHTMLRenderer{Config:html.NewConfig(),}for_,opt:=rangeopts{opt.SetHTMLOption(&r.Config)}returnr}//RegisterFuncsimplementsrenderer.NodeRenderer.RegisterFuncs.func(rStrikethroughHTMLRenderer)RegisterFuncs(regrenderer.NodeRendererFuncRegisterer){reg.Register(ast.KindStrikethrough,r.renderStrikethrough)}//StrikethroughAttributeFilterdefinesattributenameswhichddelementscanhave.varStrikethroughAttributeFilter=html.GlobalAttributeFilterfunc(rStrikethroughHTMLRenderer)renderStrikethrough(wutil.BufWriter,source[]byte,ngast.Node,enteringbool)(gast.WalkStatus,error){ifentering{ifn.Attributes()!=nil{_,_=w.WriteString("<del")html.RenderAttributes(w,n,StrikethroughAttributeFilter)_=w.WriteByte('>')}else{_,_=w.WriteString("<del>")}}else{_,_=w.WriteString("</del>")}returngast.WalkContinue,nil}renderer.NodeRenderer 接口只有一个方法:RegisterFuncs,用于注册节点类型对应的渲染函数;渲染函数是一个回调函数,署名为:func(writer util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error),用于渲染某一个节点(Node);渲染为 HTML,删除线通过加 <del></del> 标签来实现;

4)定义一个扩展类型

扩展须要实现 goldmark.Extender 接口。

typestrikethroughstruct{}//Strikethroughisanextensionthatallowyoutousestrikethroughexpressionlike'~~text~~'.varStrikethrough=&strikethrough{}func(estrikethrough)Extend(mgoldmark.Markdown){m.Parser().AddOptions(parser.WithInlineParsers(util.Prioritized(NewStrikethroughParser(),500),))m.Renderer().AddOptions(renderer.WithNodeRenderers(util.Prioritized(NewStrikethroughHTMLRenderer(),500),))}这是一个固定的形式:构造体不导出,导出一个实例。
外部利用时直接用导出的实例;Extend 方法的实现,设置解析选项和渲染选项,固定的写法;

至此内置扩展 Strikethrough 的源码剖析完成了,建议先别连续往下看,自己动手实现一个 @ 的扩展!

Mention 扩展的实现

现在我们实现一个 @ 的扩展,命名为:Mention。

1)AST 节点构造体:MentionNode

//KindMentionisaNodeKindoftheMentionnode.varKindMention=gast.NewNodeKind("Mention")typeMentionNodestruct{gast.BaseInlineWhostring}//NewStrikethroughreturnsanewMentionnode.funcNewMentionNode(usernamestring)MentionNode{return&MentionNode{BaseInline:gast.BaseInline{},Who:username,}}//DumpimplementsNode.Dump.func(nMentionNode)Dump(source[]byte,levelint){gast.DumpHelper(n,source,level,nil,nil)}//KindimplementsNode.Kind.func(nMentionNode)Kind()gast.NodeKind{returnKindMention}这里的关键是构造体字段 Who,用于保存 @ 谁;其他和 Strikethrough 没啥差异;

2)Mention 解析器

varusernameRegexp=regexp.MustCompile(`@([^\s@]{4,20})`)typementionParserstruct{}funcNewMentionParser()parser.InlineParser{returnmentionParser{}}func(mmentionParser)Trigger()[]byte{return[]byte{'@'}}func(mmentionParser)Parse(parentgast.Node,blocktext.Reader,pcparser.Context)gast.Node{before:=block.PrecendingCharacter()if!unicode.IsSpace(before){returnnil}line,_:=block.PeekLine()matched:=usernameRegexp.FindSubmatch(line)iflen(matched)<2{returnnil}block.Advance(len(matched[0]))node:=NewMentionNode(string(matched[1]))returnnode}这里假定 @ 用户名长度在 4-20 字符;Trigger 在碰着 @ 时触发;哀求 @ 之前必须是空格;通过正则找到当前行(line)中的目标字符串(用户名),matched 中第一个元素包含 @,第二个元素不包含 @;由于解析出了用户名,用户名这个字符串就不须要再解析了,因此通过 block.Advance 移动指针;构建一个 MentionNode 并返回;

3)Mention 渲染器

typementionHTMLRendererstruct{}funcNewMentionHTMLRenderer(opts...html.Option)renderer.NodeRenderer{returnmentionHTMLRenderer{}}func(mmentionHTMLRenderer)RegisterFuncs(regrenderer.NodeRendererFuncRegisterer){reg.Register(KindMention,m.renderMention)}func(mmentionHTMLRenderer)renderMention(wutil.BufWriter,source[]byte,ngast.Node,enteringbool)(gast.WalkStatus,error){ifentering{mn:=n.(MentionNode)w.WriteString(`<ahref="https://studygolang.com/user/`+mn.Who+`">@`)w.WriteString(mn.Who)}else{w.WriteString("</a>")}returngast.WalkContinue,nil}entering 为 true,表示进入该节点,因此布局链接写入 w 中;由于在 Parse 时移动了指针,因此这里将 Who 写入 w;else 中闭合 a 标签;

4)定义一个扩展类型

typementionstruct{}varMention=mention{}func(mmention)Extend(markdowngoldmark.Markdown){markdown.Parser().AddOptions(parser.WithInlineParsers(util.Prioritized(NewMentionParser(),500),))markdown.Renderer().AddOptions(renderer.WithNodeRenderers(util.Prioritized(NewMentionHTMLRenderer(),500),))}

跟上面 Strikethrough 没有太多差异。

利用 Mention

利用和其他扩展没有差异:

markdown:=goldmark.New(//支持GFMgoldmark.WithExtensions(extension.GFM),//语法高亮goldmark.WithExtensions(highlighting.NewHighlighting(highlighting.WithStyle("monokai"),highlighting.WithFormatOptions(html.WithLineNumbers(true),),),),//支持@goldmark.WithExtensions(mention.Mention),)

这样 @ 就能正常解析了。

关键字办法的表情解析扩展

这个就不讲解了,有兴趣的可以动手实现下,有问题可以互换。

其余两个不错的“扩展”

其余先容两个 goldmark “扩展”。
这里扩展用引号,是由于它们并非按照上面哀求的办法实现的,因此不算是真正意义上 goldmark 说的扩展,只能说是增加了 goldmark 的功能。
(这也证明了 goldmark 的可扩展性很强)

天生目录:TOC

这是一个很实用的功能,特殊对付长文来说。
有一个扩展实现了该功能,即 https://github.com/mdigger/goldmark-toc。

它的实现办法是扩展 goldmark.Markdown 接口,也可以说是类似 goldmark.Extender 的办法。

//Markdownextendsinitialiedgoldmark.Markdownandreturnconverterfunction.funcMarkdown(mgoldmark.Markdown)ConverterFunc{m.Parser().AddOptions(parser.WithAttribute(),parser.WithAutoHeadingID(),)returnfunc(source[]byte,writerio.Writer)(toc[]Header,errerror){doc:=m.Parser().Parse(text.NewReader(source),WithIDs())toc=Headers(doc,source)ifwriter!=nil{err=m.Renderer().Render(writer,source,doc)}returntoc,err}}

接着 Demo4,为其增加 TOC 输出,干系代码改为:

convertFunc:=toc.Markdown(markdown)headers,err:=convertFunc(source,f)for_,header:=rangeheaders{fmt.Printf("%+v\n",header)}

运行输出如下信息:

{Level:2Text:语法辅导ID:yu-fa-zhi-dao}{Level:3Text:普通内容ID:pu-tong-nei-rong}{Level:3Text:提及用户ID:ti-ji-yong-hu}{Level:3Text:表情符号EmojiID:biao-qing-fu-hao-emoji}{Level:4Text:一些表情例子ID:xie-biao-qing-li-zi}{Level:3Text:大标题-Heading3ID:da-biao-ti-heading-3}{Level:4Text:Heading4ID:heading-4}{Level:5Text:Heading5ID:heading-5}{Level:6Text:Heading6ID:heading-6}{Level:3Text:图片ID:tu-pian}{Level:3Text:代码块ID:dai-ma-kuai}{Level:4Text:普通ID:pu-tong}{Level:4Text:语法高亮支持ID:yu-fa-gao-liang-zhi-chi}{Level:5Text:演示Go代码高亮ID:yan-shi-go-dai-ma-gao-liang}{Level:5Text:演示JSON代码高亮ID:yan-shi-json-dai-ma-gao-liang}{Level:3Text:有序、无序列表ID:you-xu-wu-xu-lie-biao}{Level:4Text:无序列表ID:wu-xu-lie-biao}{Level:4Text:有序列表ID:you-xu-lie-biao}{Level:3Text:表格ID:biao-ge}{Level:3Text:段落ID:duan-luo}{Level:3Text:任务列表ID:ren-wu-lie-biao}

通过这些数据可以很方便的实现 TOC。

文本统计

这个扩展来自同一个作者:https://github.com/mdigger/goldmark-stats,用于进行文本统计,这个扩展统计的是渲染后的,因此比直接统计原始 markdown 文本要更准确。
为我们 Dem4 增加统计功能,在末了加上如下代码:

doc:=goldmark.DefaultParser().Parse(text.NewReader(source))info:=stats.New(doc,source)fmt.Printf("words:%d,unique:%d,chars:%d,readingtime:%v\n",info.Words,info.Unique(),info.Chars,info.Duration(400))

输出:

words:194,unique:141,chars:1263,readingtime:3m9s作者是俄国人,这个 words 是针对西方国家的,用空格分隔的词,并非中文的字,因此中文忽略该字段;中文紧张看 chars 字段,表示多少个字;阅读韶光,我们按照一分钟 400 个字算,须要 3m9s;总结

Markdown 已经成为程序员必须节制的技能,如果你还不会,抓紧学习下。
而这篇文章带领你从 Go 的角度对 Markdown 有了更深的理解。

希望通过 goldmark 这个库,除了学习到 Markdown 的知识,更能学习到一些 Go 措辞精良库的设计思想。

本文完全代码存放在 GitHub:https://github.com/polaris1119/go-demo/tree/master/goldmark。