code.gitea.io/gitea@v1.22.3/modules/markup/common/footnote.go (about)

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