// Copyright 2019 Yusuke Inuzuka // Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT // Most of what follows is a subtly changed version of github.com/yuin/goldmark/extension/footnote.go package common import ( "bytes" "fmt" "strconv" "unicode" "github.com/yuin/goldmark" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/renderer" "github.com/yuin/goldmark/renderer/html" "github.com/yuin/goldmark/text" "github.com/yuin/goldmark/util" ) // CleanValue will clean a value to make it safe to be an id // This function is quite different from the original goldmark function // and more closely matches the output from the shurcooL sanitizer // In particular Unicode letters and numbers are a lot more than a-zA-Z0-9... func CleanValue(value []byte) []byte { value = bytes.TrimSpace(value) rs := bytes.Runes(value) result := make([]rune, 0, len(rs)) needsDash := false for _, r := range rs { switch { case unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_': if needsDash && len(result) > 0 { result = append(result, '-') } needsDash = false result = append(result, unicode.ToLower(r)) default: needsDash = true } } return []byte(string(result)) } // Most of what follows is a subtly changed version of github.com/yuin/goldmark/extension/footnote.go // A FootnoteLink struct represents a link to a footnote of Markdown // (PHP Markdown Extra) text. type FootnoteLink struct { ast.BaseInline Index int Name []byte } // Dump implements Node.Dump. func (n *FootnoteLink) Dump(source []byte, level int) { m := map[string]string{} m["Index"] = fmt.Sprintf("%v", n.Index) m["Name"] = fmt.Sprintf("%v", n.Name) ast.DumpHelper(n, source, level, m, nil) } // KindFootnoteLink is a NodeKind of the FootnoteLink node. var KindFootnoteLink = ast.NewNodeKind("GiteaFootnoteLink") // Kind implements Node.Kind. func (n *FootnoteLink) Kind() ast.NodeKind { return KindFootnoteLink } // NewFootnoteLink returns a new FootnoteLink node. func NewFootnoteLink(index int, name []byte) *FootnoteLink { return &FootnoteLink{ Index: index, Name: name, } } // A FootnoteBackLink struct represents a link to a footnote of Markdown // (PHP Markdown Extra) text. type FootnoteBackLink struct { ast.BaseInline Index int Name []byte } // Dump implements Node.Dump. func (n *FootnoteBackLink) Dump(source []byte, level int) { m := map[string]string{} m["Index"] = fmt.Sprintf("%v", n.Index) m["Name"] = fmt.Sprintf("%v", n.Name) ast.DumpHelper(n, source, level, m, nil) } // KindFootnoteBackLink is a NodeKind of the FootnoteBackLink node. var KindFootnoteBackLink = ast.NewNodeKind("GiteaFootnoteBackLink") // Kind implements Node.Kind. func (n *FootnoteBackLink) Kind() ast.NodeKind { return KindFootnoteBackLink } // NewFootnoteBackLink returns a new FootnoteBackLink node. func NewFootnoteBackLink(index int, name []byte) *FootnoteBackLink { return &FootnoteBackLink{ Index: index, Name: name, } } // A Footnote struct represents a footnote of Markdown // (PHP Markdown Extra) text. type Footnote struct { ast.BaseBlock Ref []byte Index int Name []byte } // Dump implements Node.Dump. func (n *Footnote) Dump(source []byte, level int) { m := map[string]string{} m["Index"] = strconv.Itoa(n.Index) m["Ref"] = string(n.Ref) m["Name"] = string(n.Name) ast.DumpHelper(n, source, level, m, nil) } // KindFootnote is a NodeKind of the Footnote node. var KindFootnote = ast.NewNodeKind("GiteaFootnote") // Kind implements Node.Kind. func (n *Footnote) Kind() ast.NodeKind { return KindFootnote } // NewFootnote returns a new Footnote node. func NewFootnote(ref []byte) *Footnote { return &Footnote{ Ref: ref, Index: -1, Name: ref, } } // A FootnoteList struct represents footnotes of Markdown // (PHP Markdown Extra) text. type FootnoteList struct { ast.BaseBlock Count int } // Dump implements Node.Dump. func (n *FootnoteList) Dump(source []byte, level int) { m := map[string]string{} m["Count"] = fmt.Sprintf("%v", n.Count) ast.DumpHelper(n, source, level, m, nil) } // KindFootnoteList is a NodeKind of the FootnoteList node. var KindFootnoteList = ast.NewNodeKind("GiteaFootnoteList") // Kind implements Node.Kind. func (n *FootnoteList) Kind() ast.NodeKind { return KindFootnoteList } // NewFootnoteList returns a new FootnoteList node. func NewFootnoteList() *FootnoteList { return &FootnoteList{ Count: 0, } } var footnoteListKey = parser.NewContextKey() type footnoteBlockParser struct{} var defaultFootnoteBlockParser = &footnoteBlockParser{} // NewFootnoteBlockParser returns a new parser.BlockParser that can parse // footnotes of the Markdown(PHP Markdown Extra) text. func NewFootnoteBlockParser() parser.BlockParser { return defaultFootnoteBlockParser } func (b *footnoteBlockParser) Trigger() []byte { return []byte{'['} } func (b *footnoteBlockParser) Open(parent ast.Node, reader text.Reader, pc parser.Context) (ast.Node, parser.State) { line, segment := reader.PeekLine() pos := pc.BlockOffset() if pos < 0 || line[pos] != '[' { return nil, parser.NoChildren } pos++ if pos > len(line)-1 || line[pos] != '^' { return nil, parser.NoChildren } open := pos + 1 closure := util.FindClosure(line[pos+1:], '[', ']', false, false) //nolint closes := pos + 1 + closure next := closes + 1 if closure > -1 { if next >= len(line) || line[next] != ':' { return nil, parser.NoChildren } } else { return nil, parser.NoChildren } padding := segment.Padding label := reader.Value(text.NewSegment(segment.Start+open-padding, segment.Start+closes-padding)) if util.IsBlank(label) { return nil, parser.NoChildren } item := NewFootnote(label) pos = next + 1 - padding if pos >= len(line) { reader.Advance(pos) return item, parser.NoChildren } reader.AdvanceAndSetPadding(pos, padding) return item, parser.HasChildren } func (b *footnoteBlockParser) Continue(node ast.Node, reader text.Reader, pc parser.Context) parser.State { line, _ := reader.PeekLine() if util.IsBlank(line) { return parser.Continue | parser.HasChildren } childpos, padding := util.IndentPosition(line, reader.LineOffset(), 4) if childpos < 0 { return parser.Close } reader.AdvanceAndSetPadding(childpos, padding) return parser.Continue | parser.HasChildren } func (b *footnoteBlockParser) Close(node ast.Node, reader text.Reader, pc parser.Context) { var list *FootnoteList if tlist := pc.Get(footnoteListKey); tlist != nil { list = tlist.(*FootnoteList) } else { list = NewFootnoteList() pc.Set(footnoteListKey, list) node.Parent().InsertBefore(node.Parent(), node, list) } node.Parent().RemoveChild(node.Parent(), node) list.AppendChild(list, node) } func (b *footnoteBlockParser) CanInterruptParagraph() bool { return true } func (b *footnoteBlockParser) CanAcceptIndentedLine() bool { return false } type footnoteParser struct{} var defaultFootnoteParser = &footnoteParser{} // NewFootnoteParser returns a new parser.InlineParser that can parse // footnote links of the Markdown(PHP Markdown Extra) text. func NewFootnoteParser() parser.InlineParser { return defaultFootnoteParser } func (s *footnoteParser) Trigger() []byte { // footnote syntax probably conflict with the image syntax. // So we need trigger this parser with '!'. return []byte{'!', '['} } func (s *footnoteParser) Parse(parent ast.Node, block text.Reader, pc parser.Context) ast.Node { line, segment := block.PeekLine() pos := 1 if len(line) > 0 && line[0] == '!' { pos++ } if pos >= len(line) || line[pos] != '^' { return nil } pos++ if pos >= len(line) { return nil } open := pos closure := util.FindClosure(line[pos:], '[', ']', false, false) //nolint if closure < 0 { return nil } closes := pos + closure value := block.Value(text.NewSegment(segment.Start+open, segment.Start+closes)) block.Advance(closes + 1) var list *FootnoteList if tlist := pc.Get(footnoteListKey); tlist != nil { list = tlist.(*FootnoteList) } if list == nil { return nil } index := 0 name := []byte{} for def := list.FirstChild(); def != nil; def = def.NextSibling() { d := def.(*Footnote) if bytes.Equal(d.Ref, value) { if d.Index < 0 { list.Count++ d.Index = list.Count val := CleanValue(d.Name) if len(val) == 0 { val = []byte(strconv.Itoa(d.Index)) } d.Name = pc.IDs().Generate(val, KindFootnote) } index = d.Index name = d.Name break } } if index == 0 { return nil } return NewFootnoteLink(index, name) } type footnoteASTTransformer struct{} var defaultFootnoteASTTransformer = &footnoteASTTransformer{} // NewFootnoteASTTransformer returns a new parser.ASTTransformer that // insert a footnote list to the last of the document. func NewFootnoteASTTransformer() parser.ASTTransformer { return defaultFootnoteASTTransformer } func (a *footnoteASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) { var list *FootnoteList if tlist := pc.Get(footnoteListKey); tlist != nil { list = tlist.(*FootnoteList) } else { return } pc.Set(footnoteListKey, nil) for footnote := list.FirstChild(); footnote != nil; { container := footnote next := footnote.NextSibling() if fc := container.LastChild(); fc != nil && ast.IsParagraph(fc) { container = fc } footnoteNode := footnote.(*Footnote) index := footnoteNode.Index name := footnoteNode.Name if index < 0 { list.RemoveChild(list, footnote) } else { container.AppendChild(container, NewFootnoteBackLink(index, name)) } footnote = next } list.SortChildren(func(n1, n2 ast.Node) int { if n1.(*Footnote).Index < n2.(*Footnote).Index { return -1 } return 1 }) if list.Count <= 0 { list.Parent().RemoveChild(list.Parent(), list) return } node.AppendChild(node, list) } // FootnoteHTMLRenderer is a renderer.NodeRenderer implementation that // renders FootnoteLink nodes. type FootnoteHTMLRenderer struct { html.Config } // NewFootnoteHTMLRenderer returns a new FootnoteHTMLRenderer. func NewFootnoteHTMLRenderer(opts ...html.Option) renderer.NodeRenderer { r := &FootnoteHTMLRenderer{ Config: html.NewConfig(), } for _, opt := range opts { opt.SetHTMLOption(&r.Config) } return r } // RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs. func (r *FootnoteHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) { reg.Register(KindFootnoteLink, r.renderFootnoteLink) reg.Register(KindFootnoteBackLink, r.renderFootnoteBackLink) reg.Register(KindFootnote, r.renderFootnote) reg.Register(KindFootnoteList, r.renderFootnoteList) } func (r *FootnoteHTMLRenderer) renderFootnoteLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { if entering { n := node.(*FootnoteLink) is := strconv.Itoa(n.Index) _, _ = w.WriteString(`<sup id="fnref:`) _, _ = w.Write(n.Name) _, _ = w.WriteString(`"><a href="#fn:`) _, _ = w.Write(n.Name) _, _ = w.WriteString(`" class="footnote-ref" role="doc-noteref">`) _, _ = w.WriteString(is) _, _ = w.WriteString(`</a></sup>`) } return ast.WalkContinue, nil } func (r *FootnoteHTMLRenderer) renderFootnoteBackLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { if entering { n := node.(*FootnoteBackLink) _, _ = w.WriteString(` <a href="#fnref:`) _, _ = w.Write(n.Name) _, _ = w.WriteString(`" class="footnote-backref" role="doc-backlink">`) _, _ = w.WriteString("↩︎") _, _ = w.WriteString(`</a>`) } return ast.WalkContinue, nil } func (r *FootnoteHTMLRenderer) renderFootnote(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { n := node.(*Footnote) if entering { _, _ = w.WriteString(`<li id="fn:`) _, _ = w.Write(n.Name) _, _ = w.WriteString(`" role="doc-endnote"`) if node.Attributes() != nil { html.RenderAttributes(w, node, html.ListItemAttributeFilter) } _, _ = w.WriteString(">\n") } else { _, _ = w.WriteString("</li>\n") } return ast.WalkContinue, nil } func (r *FootnoteHTMLRenderer) renderFootnoteList(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) { tag := "div" if entering { _, _ = w.WriteString("<") _, _ = w.WriteString(tag) _, _ = w.WriteString(` class="footnotes" role="doc-endnotes"`) if node.Attributes() != nil { html.RenderAttributes(w, node, html.GlobalAttributeFilter) } _ = w.WriteByte('>') if r.Config.XHTML { _, _ = w.WriteString("\n<hr />\n") } else { _, _ = w.WriteString("\n<hr>\n") } _, _ = w.WriteString("<ol>\n") } else { _, _ = w.WriteString("</ol>\n") _, _ = w.WriteString("</") _, _ = w.WriteString(tag) _, _ = w.WriteString(">\n") } return ast.WalkContinue, nil } type footnoteExtension struct{} // FootnoteExtension represents the Gitea Footnote var FootnoteExtension = &footnoteExtension{} // Extend extends the markdown converter with the Gitea Footnote parser func (e *footnoteExtension) Extend(m goldmark.Markdown) { m.Parser().AddOptions( parser.WithBlockParsers( util.Prioritized(NewFootnoteBlockParser(), 999), ), parser.WithInlineParsers( util.Prioritized(NewFootnoteParser(), 101), ), parser.WithASTTransformers( util.Prioritized(NewFootnoteASTTransformer(), 999), ), ) m.Renderer().AddOptions(renderer.WithNodeRenderers( util.Prioritized(NewFootnoteHTMLRenderer(), 500), )) }