498 lines · 13.2 KB · Go
RawDownload
// 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("&#x21a9;&#xfe0e;")		_, _ = 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.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 Footnotevar FootnoteExtension = &footnoteExtension{} // Extend extends the markdown converter with the Gitea Footnote parserfunc (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),	))}