静态博客中的shortcode实现
前言
shortcode,直译过来就是 短代码。 使用过 hugo 的人对此也一定有所了解,shortcode 允许用户使用部分关键参数就可以在文章内部插入设置好的HTML模版内容,而不用写大段的HTML代码,比如插入指定视频或者音乐,使用 shortcode 之前需要使用 iframe
1<iframe frameborder="no" border="0" 2 marginwidth="0" marginheight="0" 3 width=330 height=86 4 src="//music.163.com/outchain/player?type=2&id=xxx&auto=0&height=66"></iframe>
而使用 shortcode 后就可以只用指定关键的 id 参数
1{{<163music xxx>}}
当网易云音乐的分享代码更新后,可以直接更新 shortcode 代码, 而不是搜索替换每一篇包括该iframe代码的文章, 同时 shortcode 也可以扩展 Markdown 的功能,比如文本的居中,居左,居右
1{{<align left>}} 2 文本内容 3 {{</align>}}
实现方式
而想要在静态博客中实现 shortcode 功能, 目前有两种方式
-
方式一: 修改 Markdown 解析器, 在静态生成器解析文章内容时就可以对 shortcode 进行解析,并将 shortcode 替换成写好的模版,至少,我看到 hugo 就是这么实现的
-
方式二: 如果不想修改 Markdown 解析器,还可以直接修改生成好的 HTML 内容,这种方式实现原理很简单,Markdown允许插入HTML代码, 那么我们就可以直接在文章中写一个
1<shortcode _name="163music" id="xxx" />然后在现有的 Markdown 解析库解析文章并生成 HTML 后再次对HTML内容进行解析,目前主流语言都有现成的库很方便地解析HTML内容, 下面以 Go 为例
Markdown渲染
比如有一篇文章,我想要添加网易云音乐的某首歌到文章内部,当使用现有的Markdown库对内容进行渲染
1package main 2 3import ( 4 "github.com/russross/blackfriday/v2" 5) 6 7func main() { 8 content := `# Hello World 9<shortcode _name="163music" id="xxx" /> 10This is one of my favorite **music**. 11 ` 12 d := blackfriday.Run(content) 13 fmt.Println(string(d)) 14}
输出结果
1<h1>Hello World</h1> 2 3<p><shortcode _name="163music" id="xxx" /> 4 This is one of my favorite <strong>music</strong>.</p>
查找HTML标签
可以使用 golang.org/x/net/html 对HTML内容进行解析, 并查找到 shortcode
1package main 2 3import ( 4 "bytes" 5 "fmt" 6 "github.com/russross/blackfriday/v2" 7 "golang.org/x/net/html" 8 "text/template" 9) 10 11func main() { 12 // ... 13 var ( 14 w bytes.Buffer 15 z = html.NewTokenizer(bytes.NewReader(d)) 16 ) 17 for { 18 next := z.Next() 19 if next == html.ErrorToken { 20 break 21 } 22 23 token := z.Token() 24 switch next { 25 case html.StartTagToken, html.SelfClosingTagToken: 26 // 找到shortcode标签 27 if token.Data == "shortcode" { 28 29 } 30 } 31 w.WriteString(token.String()) 32 } 33 fmt.Println(w.String()) 34}
替换HTML
首先把需要替换的 iframe 转换成模版
1replace := `<iframe 2frameborder="no" border="0" 3marginwidth="0" marginheight="0" 4width=330 height=86 5src="//music.163.com/outchain/player?type=2&id={{ .id }}&auto=0&height=66"></iframe>` 6 7tmpl, err := template.New("test").Parse(replace) 8if err != nil { 9 panic(err) 10}
然后根据标签参数替换原来的HTML
1// 获取该标签的参数 2attrs := make(map[string]string) 3for _, attr := range token.Attr { 4 attrs[attr.Key] = attr.Val 5} 6// 确认是163music 7if attrs["_name"] == "163music" { 8 // 获取替换的内容 9 var buf bytes.Buffer 10 err := tmpl.Execute(&buf, attrs) 11 if err != nil { 12 panic(err) 13 } 14 w.WriteString(buf.String()) 15 continue 16}
最后的输出结果
1<h1>Hello World</h1> 2 3<p><iframe 4 frameborder="no" border="0" 5 marginwidth="0" marginheight="0" 6 width=330 height=86 7 src="//music.163.com/outchain/player?type=2&id=xxx&auto=0&height=66"></iframe> 8 This is one of my favorite <strong>music</strong>.</p>
结尾
目前snow的 shortcode 功能就是根据第二种方式实现的,具体可以参考 hooks/shortcode/shortcode.go, 利用此特性甚至还可以实现类似 Hugo 中的 Markdown Render Hook 功能,用于自定义 img, a 等标签
