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

footnote.go 14 kB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. // Copyright 2019 Yusuke Inuzuka
  2. // Copyright 2019 The Gitea Authors. All rights reserved.
  3. // Use of this source code is governed by a MIT-style
  4. // license that can be found in the LICENSE file.
  5. // Most of what follows is a subtly changed version of github.com/yuin/goldmark/extension/footnote.go
  6. package common
  7. import (
  8. "bytes"
  9. "fmt"
  10. "os"
  11. "strconv"
  12. "unicode"
  13. "github.com/yuin/goldmark"
  14. "github.com/yuin/goldmark/ast"
  15. "github.com/yuin/goldmark/parser"
  16. "github.com/yuin/goldmark/renderer"
  17. "github.com/yuin/goldmark/renderer/html"
  18. "github.com/yuin/goldmark/text"
  19. "github.com/yuin/goldmark/util"
  20. )
  21. // CleanValue will clean a value to make it safe to be an id
  22. // This function is quite different from the original goldmark function
  23. // and more closely matches the output from the shurcooL sanitizer
  24. // In particular Unicode letters and numbers are a lot more than a-zA-Z0-9...
  25. func CleanValue(value []byte) []byte {
  26. value = bytes.TrimSpace(value)
  27. rs := bytes.Runes(value)
  28. result := make([]rune, 0, len(rs))
  29. needsDash := false
  30. for _, r := range rs {
  31. switch {
  32. case unicode.IsLetter(r) || unicode.IsNumber(r):
  33. if needsDash && len(result) > 0 {
  34. result = append(result, '-')
  35. }
  36. needsDash = false
  37. result = append(result, unicode.ToLower(r))
  38. default:
  39. needsDash = true
  40. }
  41. }
  42. return []byte(string(result))
  43. }
  44. // Most of what follows is a subtly changed version of github.com/yuin/goldmark/extension/footnote.go
  45. // A FootnoteLink struct represents a link to a footnote of Markdown
  46. // (PHP Markdown Extra) text.
  47. type FootnoteLink struct {
  48. ast.BaseInline
  49. Index int
  50. Name []byte
  51. }
  52. // Dump implements Node.Dump.
  53. func (n *FootnoteLink) Dump(source []byte, level int) {
  54. m := map[string]string{}
  55. m["Index"] = fmt.Sprintf("%v", n.Index)
  56. m["Name"] = fmt.Sprintf("%v", n.Name)
  57. ast.DumpHelper(n, source, level, m, nil)
  58. }
  59. // KindFootnoteLink is a NodeKind of the FootnoteLink node.
  60. var KindFootnoteLink = ast.NewNodeKind("GiteaFootnoteLink")
  61. // Kind implements Node.Kind.
  62. func (n *FootnoteLink) Kind() ast.NodeKind {
  63. return KindFootnoteLink
  64. }
  65. // NewFootnoteLink returns a new FootnoteLink node.
  66. func NewFootnoteLink(index int, name []byte) *FootnoteLink {
  67. return &FootnoteLink{
  68. Index: index,
  69. Name: name,
  70. }
  71. }
  72. // A FootnoteBackLink struct represents a link to a footnote of Markdown
  73. // (PHP Markdown Extra) text.
  74. type FootnoteBackLink struct {
  75. ast.BaseInline
  76. Index int
  77. Name []byte
  78. }
  79. // Dump implements Node.Dump.
  80. func (n *FootnoteBackLink) Dump(source []byte, level int) {
  81. m := map[string]string{}
  82. m["Index"] = fmt.Sprintf("%v", n.Index)
  83. m["Name"] = fmt.Sprintf("%v", n.Name)
  84. ast.DumpHelper(n, source, level, m, nil)
  85. }
  86. // KindFootnoteBackLink is a NodeKind of the FootnoteBackLink node.
  87. var KindFootnoteBackLink = ast.NewNodeKind("GiteaFootnoteBackLink")
  88. // Kind implements Node.Kind.
  89. func (n *FootnoteBackLink) Kind() ast.NodeKind {
  90. return KindFootnoteBackLink
  91. }
  92. // NewFootnoteBackLink returns a new FootnoteBackLink node.
  93. func NewFootnoteBackLink(index int, name []byte) *FootnoteBackLink {
  94. return &FootnoteBackLink{
  95. Index: index,
  96. Name: name,
  97. }
  98. }
  99. // A Footnote struct represents a footnote of Markdown
  100. // (PHP Markdown Extra) text.
  101. type Footnote struct {
  102. ast.BaseBlock
  103. Ref []byte
  104. Index int
  105. Name []byte
  106. }
  107. // Dump implements Node.Dump.
  108. func (n *Footnote) Dump(source []byte, level int) {
  109. m := map[string]string{}
  110. m["Index"] = fmt.Sprintf("%v", n.Index)
  111. m["Ref"] = fmt.Sprintf("%s", n.Ref)
  112. m["Name"] = fmt.Sprintf("%v", n.Name)
  113. ast.DumpHelper(n, source, level, m, nil)
  114. }
  115. // KindFootnote is a NodeKind of the Footnote node.
  116. var KindFootnote = ast.NewNodeKind("GiteaFootnote")
  117. // Kind implements Node.Kind.
  118. func (n *Footnote) Kind() ast.NodeKind {
  119. return KindFootnote
  120. }
  121. // NewFootnote returns a new Footnote node.
  122. func NewFootnote(ref []byte) *Footnote {
  123. return &Footnote{
  124. Ref: ref,
  125. Index: -1,
  126. Name: ref,
  127. }
  128. }
  129. // A FootnoteList struct represents footnotes of Markdown
  130. // (PHP Markdown Extra) text.
  131. type FootnoteList struct {
  132. ast.BaseBlock
  133. Count int
  134. }
  135. // Dump implements Node.Dump.
  136. func (n *FootnoteList) Dump(source []byte, level int) {
  137. m := map[string]string{}
  138. m["Count"] = fmt.Sprintf("%v", n.Count)
  139. ast.DumpHelper(n, source, level, m, nil)
  140. }
  141. // KindFootnoteList is a NodeKind of the FootnoteList node.
  142. var KindFootnoteList = ast.NewNodeKind("GiteaFootnoteList")
  143. // Kind implements Node.Kind.
  144. func (n *FootnoteList) Kind() ast.NodeKind {
  145. return KindFootnoteList
  146. }
  147. // NewFootnoteList returns a new FootnoteList node.
  148. func NewFootnoteList() *FootnoteList {
  149. return &FootnoteList{
  150. Count: 0,
  151. }
  152. }
  153. var footnoteListKey = parser.NewContextKey()
  154. type footnoteBlockParser struct {
  155. }
  156. var defaultFootnoteBlockParser = &footnoteBlockParser{}
  157. // NewFootnoteBlockParser returns a new parser.BlockParser that can parse
  158. // footnotes of the Markdown(PHP Markdown Extra) text.
  159. func NewFootnoteBlockParser() parser.BlockParser {
  160. return defaultFootnoteBlockParser
  161. }
  162. func (b *footnoteBlockParser) Trigger() []byte {
  163. return []byte{'['}
  164. }
  165. func (b *footnoteBlockParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) {
  166. line, segment := reader.PeekLine()
  167. pos := pc.BlockOffset()
  168. if pos < 0 || line[pos] != '[' {
  169. return nil, parser.NoChildren
  170. }
  171. pos++
  172. if pos > len(line)-1 || line[pos] != '^' {
  173. return nil, parser.NoChildren
  174. }
  175. open := pos + 1
  176. closes := 0
  177. closure := util.FindClosure(line[pos+1:], '[', ']', false, false)
  178. closes = pos + 1 + closure
  179. next := closes + 1
  180. if closure > -1 {
  181. if next >= len(line) || line[next] != ':' {
  182. return nil, parser.NoChildren
  183. }
  184. } else {
  185. return nil, parser.NoChildren
  186. }
  187. padding := segment.Padding
  188. label := reader.Value(text.NewSegment(segment.Start+open-padding, segment.Start+closes-padding))
  189. if util.IsBlank(label) {
  190. return nil, parser.NoChildren
  191. }
  192. item := NewFootnote(label)
  193. pos = next + 1 - padding
  194. if pos >= len(line) {
  195. reader.Advance(pos)
  196. return item, parser.NoChildren
  197. }
  198. reader.AdvanceAndSetPadding(pos, padding)
  199. return item, parser.HasChildren
  200. }
  201. func (b *footnoteBlockParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State {
  202. line, _ := reader.PeekLine()
  203. if util.IsBlank(line) {
  204. return parser.Continue | parser.HasChildren
  205. }
  206. childpos, padding := util.IndentPosition(line, reader.LineOffset(), 4)
  207. if childpos < 0 {
  208. return parser.Close
  209. }
  210. reader.AdvanceAndSetPadding(childpos, padding)
  211. return parser.Continue | parser.HasChildren
  212. }
  213. func (b *footnoteBlockParser) Close(node ast.Node, reader text.Reader, pc parser.Context) {
  214. var list *FootnoteList
  215. if tlist := pc.Get(footnoteListKey); tlist != nil {
  216. list = tlist.(*FootnoteList)
  217. } else {
  218. list = NewFootnoteList()
  219. pc.Set(footnoteListKey, list)
  220. node.Parent().InsertBefore(node.Parent(), node, list)
  221. }
  222. node.Parent().RemoveChild(node.Parent(), node)
  223. list.AppendChild(list, node)
  224. }
  225. func (b *footnoteBlockParser) CanInterruptParagraph() bool {
  226. return true
  227. }
  228. func (b *footnoteBlockParser) CanAcceptIndentedLine() bool {
  229. return false
  230. }
  231. type footnoteParser struct {
  232. }
  233. var defaultFootnoteParser = &footnoteParser{}
  234. // NewFootnoteParser returns a new parser.InlineParser that can parse
  235. // footnote links of the Markdown(PHP Markdown Extra) text.
  236. func NewFootnoteParser() parser.InlineParser {
  237. return defaultFootnoteParser
  238. }
  239. func (s *footnoteParser) Trigger() []byte {
  240. // footnote syntax probably conflict with the image syntax.
  241. // So we need trigger this parser with '!'.
  242. return []byte{'!', '['}
  243. }
  244. func (s *footnoteParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node {
  245. line, segment := block.PeekLine()
  246. pos := 1
  247. if len(line) > 0 && line[0] == '!' {
  248. pos++
  249. }
  250. if pos >= len(line) || line[pos] != '^' {
  251. return nil
  252. }
  253. pos++
  254. if pos >= len(line) {
  255. return nil
  256. }
  257. open := pos
  258. closure := util.FindClosure(line[pos:], '[', ']', false, false)
  259. if closure < 0 {
  260. return nil
  261. }
  262. closes := pos + closure
  263. value := block.Value(text.NewSegment(segment.Start+open, segment.Start+closes))
  264. block.Advance(closes + 1)
  265. var list *FootnoteList
  266. if tlist := pc.Get(footnoteListKey); tlist != nil {
  267. list = tlist.(*FootnoteList)
  268. }
  269. if list == nil {
  270. return nil
  271. }
  272. index := 0
  273. name := []byte{}
  274. for def := list.FirstChild(); def != nil; def = def.NextSibling() {
  275. d := def.(*Footnote)
  276. if bytes.Equal(d.Ref, value) {
  277. if d.Index < 0 {
  278. list.Count++
  279. d.Index = list.Count
  280. val := CleanValue(d.Name)
  281. if len(val) == 0 {
  282. val = []byte(strconv.Itoa(d.Index))
  283. }
  284. d.Name = pc.IDs().Generate(val, KindFootnote)
  285. }
  286. index = d.Index
  287. name = d.Name
  288. break
  289. }
  290. }
  291. if index == 0 {
  292. return nil
  293. }
  294. return NewFootnoteLink(index, name)
  295. }
  296. type footnoteASTTransformer struct {
  297. }
  298. var defaultFootnoteASTTransformer = &footnoteASTTransformer{}
  299. // NewFootnoteASTTransformer returns a new parser.ASTTransformer that
  300. // insert a footnote list to the last of the document.
  301. func NewFootnoteASTTransformer() parser.ASTTransformer {
  302. return defaultFootnoteASTTransformer
  303. }
  304. func (a *footnoteASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
  305. var list *FootnoteList
  306. if tlist := pc.Get(footnoteListKey); tlist != nil {
  307. list = tlist.(*FootnoteList)
  308. } else {
  309. return
  310. }
  311. pc.Set(footnoteListKey, nil)
  312. for footnote := list.FirstChild(); footnote != nil; {
  313. var container ast.Node = footnote
  314. next := footnote.NextSibling()
  315. if fc := container.LastChild(); fc != nil && ast.IsParagraph(fc) {
  316. container = fc
  317. }
  318. footnoteNode := footnote.(*Footnote)
  319. index := footnoteNode.Index
  320. name := footnoteNode.Name
  321. if index < 0 {
  322. list.RemoveChild(list, footnote)
  323. } else {
  324. container.AppendChild(container, NewFootnoteBackLink(index, name))
  325. }
  326. footnote = next
  327. }
  328. list.SortChildren(func(n1, n2 ast.Node) int {
  329. if n1.(*Footnote).Index < n2.(*Footnote).Index {
  330. return -1
  331. }
  332. return 1
  333. })
  334. if list.Count <= 0 {
  335. list.Parent().RemoveChild(list.Parent(), list)
  336. return
  337. }
  338. node.AppendChild(node, list)
  339. }
  340. // FootnoteHTMLRenderer is a renderer.NodeRenderer implementation that
  341. // renders FootnoteLink nodes.
  342. type FootnoteHTMLRenderer struct {
  343. html.Config
  344. }
  345. // NewFootnoteHTMLRenderer returns a new FootnoteHTMLRenderer.
  346. func NewFootnoteHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
  347. r := &FootnoteHTMLRenderer{
  348. Config: html.NewConfig(),
  349. }
  350. for _, opt := range opts {
  351. opt.SetHTMLOption(&r.Config)
  352. }
  353. return r
  354. }
  355. // RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
  356. func (r *FootnoteHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
  357. reg.Register(KindFootnoteLink, r.renderFootnoteLink)
  358. reg.Register(KindFootnoteBackLink, r.renderFootnoteBackLink)
  359. reg.Register(KindFootnote, r.renderFootnote)
  360. reg.Register(KindFootnoteList, r.renderFootnoteList)
  361. }
  362. func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
  363. if entering {
  364. n := node.(*FootnoteLink)
  365. n.Dump(source, 0)
  366. is := strconv.Itoa(n.Index)
  367. _, _ = w.WriteString(`<sup id="fnref:`)
  368. _, _ = w.Write(n.Name)
  369. _, _ = w.WriteString(`"><a href="#fn:`)
  370. _, _ = w.Write(n.Name)
  371. _, _ = w.WriteString(`" class="footnote-ref" role="doc-noteref">`)
  372. _, _ = w.WriteString(is)
  373. _, _ = w.WriteString(`</a></sup>`)
  374. }
  375. return ast.WalkContinue, nil
  376. }
  377. func (r *FootnoteHTMLRenderer) renderFootnoteBackLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
  378. if entering {
  379. n := node.(*FootnoteBackLink)
  380. fmt.Fprintf(os.Stdout, "source:\n%s\n", string(n.Text(source)))
  381. _, _ = w.WriteString(` <a href="#fnref:`)
  382. _, _ = w.Write(n.Name)
  383. _, _ = w.WriteString(`" class="footnote-backref" role="doc-backlink">`)
  384. _, _ = w.WriteString("&#x21a9;&#xfe0e;")
  385. _, _ = w.WriteString(`</a>`)
  386. }
  387. return ast.WalkContinue, nil
  388. }
  389. func (r *FootnoteHTMLRenderer) renderFootnote(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
  390. n := node.(*Footnote)
  391. if entering {
  392. fmt.Fprintf(os.Stdout, "source:\n%s\n", string(n.Text(source)))
  393. _, _ = w.WriteString(`<li id="fn:`)
  394. _, _ = w.Write(n.Name)
  395. _, _ = w.WriteString(`" role="doc-endnote"`)
  396. if node.Attributes() != nil {
  397. html.RenderAttributes(w, node, html.ListItemAttributeFilter)
  398. }
  399. _, _ = w.WriteString(">\n")
  400. } else {
  401. _, _ = w.WriteString("</li>\n")
  402. }
  403. return ast.WalkContinue, nil
  404. }
  405. func (r *FootnoteHTMLRenderer) renderFootnoteList(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
  406. tag := "div"
  407. if entering {
  408. _, _ = w.WriteString("<")
  409. _, _ = w.WriteString(tag)
  410. _, _ = w.WriteString(` class="footnotes" role="doc-endnotes"`)
  411. if node.Attributes() != nil {
  412. html.RenderAttributes(w, node, html.GlobalAttributeFilter)
  413. }
  414. _ = w.WriteByte('>')
  415. if r.Config.XHTML {
  416. _, _ = w.WriteString("\n<hr />\n")
  417. } else {
  418. _, _ = w.WriteString("\n<hr>\n")
  419. }
  420. _, _ = w.WriteString("<ol>\n")
  421. } else {
  422. _, _ = w.WriteString("</ol>\n")
  423. _, _ = w.WriteString("</")
  424. _, _ = w.WriteString(tag)
  425. _, _ = w.WriteString(">\n")
  426. }
  427. return ast.WalkContinue, nil
  428. }
  429. type footnoteExtension struct{}
  430. // FootnoteExtension represents the Gitea Footnote
  431. var FootnoteExtension = &footnoteExtension{}
  432. // Extend extends the markdown converter with the Gitea Footnote parser
  433. func (e *footnoteExtension) Extend(m goldmark.Markdown) {
  434. m.Parser().AddOptions(
  435. parser.WithBlockParsers(
  436. util.Prioritized(NewFootnoteBlockParser(), 999),
  437. ),
  438. parser.WithInlineParsers(
  439. util.Prioritized(NewFootnoteParser(), 101),
  440. ),
  441. parser.WithASTTransformers(
  442. util.Prioritized(NewFootnoteASTTransformer(), 999),
  443. ),
  444. )
  445. m.Renderer().AddOptions(renderer.WithNodeRenderers(
  446. util.Prioritized(NewFootnoteHTMLRenderer(), 500),
  447. ))
  448. }