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