github.com/gitbundle/modules@v0.0.0-20231025071548-85b91c5c3b01/markup/common/footnote.go (about)

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