gitea源码

footnote.go 13KB

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