You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

footnote.go 9.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. package extension
  2. import (
  3. "bytes"
  4. "github.com/yuin/goldmark"
  5. gast "github.com/yuin/goldmark/ast"
  6. "github.com/yuin/goldmark/extension/ast"
  7. "github.com/yuin/goldmark/parser"
  8. "github.com/yuin/goldmark/renderer"
  9. "github.com/yuin/goldmark/renderer/html"
  10. "github.com/yuin/goldmark/text"
  11. "github.com/yuin/goldmark/util"
  12. "strconv"
  13. )
  14. var footnoteListKey = parser.NewContextKey()
  15. type footnoteBlockParser struct {
  16. }
  17. var defaultFootnoteBlockParser = &footnoteBlockParser{}
  18. // NewFootnoteBlockParser returns a new parser.BlockParser that can parse
  19. // footnotes of the Markdown(PHP Markdown Extra) text.
  20. func NewFootnoteBlockParser() parser.BlockParser {
  21. return defaultFootnoteBlockParser
  22. }
  23. func (b *footnoteBlockParser) Trigger() []byte {
  24. return []byte{'['}
  25. }
  26. func (b *footnoteBlockParser) Open(parent gast.Node, reader text.Reader, pc parser.Context) (gast.Node, parser.State) {
  27. line, segment := reader.PeekLine()
  28. pos := pc.BlockOffset()
  29. if pos < 0 || line[pos] != '[' {
  30. return nil, parser.NoChildren
  31. }
  32. pos++
  33. if pos > len(line)-1 || line[pos] != '^' {
  34. return nil, parser.NoChildren
  35. }
  36. open := pos + 1
  37. closes := 0
  38. closure := util.FindClosure(line[pos+1:], '[', ']', false, false)
  39. closes = pos + 1 + closure
  40. next := closes + 1
  41. if closure > -1 {
  42. if next >= len(line) || line[next] != ':' {
  43. return nil, parser.NoChildren
  44. }
  45. } else {
  46. return nil, parser.NoChildren
  47. }
  48. padding := segment.Padding
  49. label := reader.Value(text.NewSegment(segment.Start+open-padding, segment.Start+closes-padding))
  50. if util.IsBlank(label) {
  51. return nil, parser.NoChildren
  52. }
  53. item := ast.NewFootnote(label)
  54. pos = next + 1 - padding
  55. if pos >= len(line) {
  56. reader.Advance(pos)
  57. return item, parser.NoChildren
  58. }
  59. reader.AdvanceAndSetPadding(pos, padding)
  60. return item, parser.HasChildren
  61. }
  62. func (b *footnoteBlockParser) Continue(node gast.Node, reader text.Reader, pc parser.Context) parser.State {
  63. line, _ := reader.PeekLine()
  64. if util.IsBlank(line) {
  65. return parser.Continue | parser.HasChildren
  66. }
  67. childpos, padding := util.IndentPosition(line, reader.LineOffset(), 4)
  68. if childpos < 0 {
  69. return parser.Close
  70. }
  71. reader.AdvanceAndSetPadding(childpos, padding)
  72. return parser.Continue | parser.HasChildren
  73. }
  74. func (b *footnoteBlockParser) Close(node gast.Node, reader text.Reader, pc parser.Context) {
  75. var list *ast.FootnoteList
  76. if tlist := pc.Get(footnoteListKey); tlist != nil {
  77. list = tlist.(*ast.FootnoteList)
  78. } else {
  79. list = ast.NewFootnoteList()
  80. pc.Set(footnoteListKey, list)
  81. node.Parent().InsertBefore(node.Parent(), node, list)
  82. }
  83. node.Parent().RemoveChild(node.Parent(), node)
  84. list.AppendChild(list, node)
  85. }
  86. func (b *footnoteBlockParser) CanInterruptParagraph() bool {
  87. return true
  88. }
  89. func (b *footnoteBlockParser) CanAcceptIndentedLine() bool {
  90. return false
  91. }
  92. type footnoteParser struct {
  93. }
  94. var defaultFootnoteParser = &footnoteParser{}
  95. // NewFootnoteParser returns a new parser.InlineParser that can parse
  96. // footnote links of the Markdown(PHP Markdown Extra) text.
  97. func NewFootnoteParser() parser.InlineParser {
  98. return defaultFootnoteParser
  99. }
  100. func (s *footnoteParser) Trigger() []byte {
  101. // footnote syntax probably conflict with the image syntax.
  102. // So we need trigger this parser with '!'.
  103. return []byte{'!', '['}
  104. }
  105. func (s *footnoteParser) Parse(parent gast.Node, block text.Reader, pc parser.Context) gast.Node {
  106. line, segment := block.PeekLine()
  107. pos := 1
  108. if len(line) > 0 && line[0] == '!' {
  109. pos++
  110. }
  111. if pos >= len(line) || line[pos] != '^' {
  112. return nil
  113. }
  114. pos++
  115. if pos >= len(line) {
  116. return nil
  117. }
  118. open := pos
  119. closure := util.FindClosure(line[pos:], '[', ']', false, false)
  120. if closure < 0 {
  121. return nil
  122. }
  123. closes := pos + closure
  124. value := block.Value(text.NewSegment(segment.Start+open, segment.Start+closes))
  125. block.Advance(closes + 1)
  126. var list *ast.FootnoteList
  127. if tlist := pc.Get(footnoteListKey); tlist != nil {
  128. list = tlist.(*ast.FootnoteList)
  129. }
  130. if list == nil {
  131. return nil
  132. }
  133. index := 0
  134. for def := list.FirstChild(); def != nil; def = def.NextSibling() {
  135. d := def.(*ast.Footnote)
  136. if bytes.Equal(d.Ref, value) {
  137. if d.Index < 0 {
  138. list.Count += 1
  139. d.Index = list.Count
  140. }
  141. index = d.Index
  142. break
  143. }
  144. }
  145. if index == 0 {
  146. return nil
  147. }
  148. return ast.NewFootnoteLink(index)
  149. }
  150. type footnoteASTTransformer struct {
  151. }
  152. var defaultFootnoteASTTransformer = &footnoteASTTransformer{}
  153. // NewFootnoteASTTransformer returns a new parser.ASTTransformer that
  154. // insert a footnote list to the last of the document.
  155. func NewFootnoteASTTransformer() parser.ASTTransformer {
  156. return defaultFootnoteASTTransformer
  157. }
  158. func (a *footnoteASTTransformer) Transform(node *gast.Document, reader text.Reader, pc parser.Context) {
  159. var list *ast.FootnoteList
  160. if tlist := pc.Get(footnoteListKey); tlist != nil {
  161. list = tlist.(*ast.FootnoteList)
  162. } else {
  163. return
  164. }
  165. pc.Set(footnoteListKey, nil)
  166. for footnote := list.FirstChild(); footnote != nil; {
  167. var container gast.Node = footnote
  168. next := footnote.NextSibling()
  169. if fc := container.LastChild(); fc != nil && gast.IsParagraph(fc) {
  170. container = fc
  171. }
  172. index := footnote.(*ast.Footnote).Index
  173. if index < 0 {
  174. list.RemoveChild(list, footnote)
  175. } else {
  176. container.AppendChild(container, ast.NewFootnoteBackLink(index))
  177. }
  178. footnote = next
  179. }
  180. list.SortChildren(func(n1, n2 gast.Node) int {
  181. if n1.(*ast.Footnote).Index < n2.(*ast.Footnote).Index {
  182. return -1
  183. }
  184. return 1
  185. })
  186. if list.Count <= 0 {
  187. list.Parent().RemoveChild(list.Parent(), list)
  188. return
  189. }
  190. node.AppendChild(node, list)
  191. }
  192. // FootnoteHTMLRenderer is a renderer.NodeRenderer implementation that
  193. // renders FootnoteLink nodes.
  194. type FootnoteHTMLRenderer struct {
  195. html.Config
  196. }
  197. // NewFootnoteHTMLRenderer returns a new FootnoteHTMLRenderer.
  198. func NewFootnoteHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
  199. r := &FootnoteHTMLRenderer{
  200. Config: html.NewConfig(),
  201. }
  202. for _, opt := range opts {
  203. opt.SetHTMLOption(&r.Config)
  204. }
  205. return r
  206. }
  207. // RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
  208. func (r *FootnoteHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
  209. reg.Register(ast.KindFootnoteLink, r.renderFootnoteLink)
  210. reg.Register(ast.KindFootnoteBackLink, r.renderFootnoteBackLink)
  211. reg.Register(ast.KindFootnote, r.renderFootnote)
  212. reg.Register(ast.KindFootnoteList, r.renderFootnoteList)
  213. }
  214. func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
  215. if entering {
  216. n := node.(*ast.FootnoteLink)
  217. is := strconv.Itoa(n.Index)
  218. _, _ = w.WriteString(`<sup id="fnref:`)
  219. _, _ = w.WriteString(is)
  220. _, _ = w.WriteString(`"><a href="#fn:`)
  221. _, _ = w.WriteString(is)
  222. _, _ = w.WriteString(`" class="footnote-ref" role="doc-noteref">`)
  223. _, _ = w.WriteString(is)
  224. _, _ = w.WriteString(`</a></sup>`)
  225. }
  226. return gast.WalkContinue, nil
  227. }
  228. func (r *FootnoteHTMLRenderer) renderFootnoteBackLink(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
  229. if entering {
  230. n := node.(*ast.FootnoteBackLink)
  231. is := strconv.Itoa(n.Index)
  232. _, _ = w.WriteString(` <a href="#fnref:`)
  233. _, _ = w.WriteString(is)
  234. _, _ = w.WriteString(`" class="footnote-backref" role="doc-backlink">`)
  235. _, _ = w.WriteString("&#x21a9;&#xfe0e;")
  236. _, _ = w.WriteString(`</a>`)
  237. }
  238. return gast.WalkContinue, nil
  239. }
  240. func (r *FootnoteHTMLRenderer) renderFootnote(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
  241. n := node.(*ast.Footnote)
  242. is := strconv.Itoa(n.Index)
  243. if entering {
  244. _, _ = w.WriteString(`<li id="fn:`)
  245. _, _ = w.WriteString(is)
  246. _, _ = w.WriteString(`" role="doc-endnote"`)
  247. if node.Attributes() != nil {
  248. html.RenderAttributes(w, node, html.ListItemAttributeFilter)
  249. }
  250. _, _ = w.WriteString(">\n")
  251. } else {
  252. _, _ = w.WriteString("</li>\n")
  253. }
  254. return gast.WalkContinue, nil
  255. }
  256. func (r *FootnoteHTMLRenderer) renderFootnoteList(w util.BufWriter, source []byte, node gast.Node, entering bool) (gast.WalkStatus, error) {
  257. tag := "section"
  258. if r.Config.XHTML {
  259. tag = "div"
  260. }
  261. if entering {
  262. _, _ = w.WriteString("<")
  263. _, _ = w.WriteString(tag)
  264. _, _ = w.WriteString(` class="footnotes" role="doc-endnotes"`)
  265. if node.Attributes() != nil {
  266. html.RenderAttributes(w, node, html.GlobalAttributeFilter)
  267. }
  268. _ = w.WriteByte('>')
  269. if r.Config.XHTML {
  270. _, _ = w.WriteString("\n<hr />\n")
  271. } else {
  272. _, _ = w.WriteString("\n<hr>\n")
  273. }
  274. _, _ = w.WriteString("<ol>\n")
  275. } else {
  276. _, _ = w.WriteString("</ol>\n")
  277. _, _ = w.WriteString("</")
  278. _, _ = w.WriteString(tag)
  279. _, _ = w.WriteString(">\n")
  280. }
  281. return gast.WalkContinue, nil
  282. }
  283. type footnote struct {
  284. }
  285. // Footnote is an extension that allow you to use PHP Markdown Extra Footnotes.
  286. var Footnote = &footnote{}
  287. func (e *footnote) Extend(m goldmark.Markdown) {
  288. m.Parser().AddOptions(
  289. parser.WithBlockParsers(
  290. util.Prioritized(NewFootnoteBlockParser(), 999),
  291. ),
  292. parser.WithInlineParsers(
  293. util.Prioritized(NewFootnoteParser(), 101),
  294. ),
  295. parser.WithASTTransformers(
  296. util.Prioritized(NewFootnoteASTTransformer(), 999),
  297. ),
  298. )
  299. m.Renderer().AddOptions(renderer.WithNodeRenderers(
  300. util.Prioritized(NewFootnoteHTMLRenderer(), 500),
  301. ))
  302. }