code.gitea.io/gitea@v1.19.3/modules/markup/markdown/goldmark.go (about)

     1  // Copyright 2019 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package markdown
     5  
     6  import (
     7  	"bytes"
     8  	"fmt"
     9  	"regexp"
    10  	"strings"
    11  
    12  	"code.gitea.io/gitea/modules/container"
    13  	"code.gitea.io/gitea/modules/markup"
    14  	"code.gitea.io/gitea/modules/markup/common"
    15  	"code.gitea.io/gitea/modules/setting"
    16  	"code.gitea.io/gitea/modules/svg"
    17  	giteautil "code.gitea.io/gitea/modules/util"
    18  
    19  	"github.com/microcosm-cc/bluemonday/css"
    20  	"github.com/yuin/goldmark/ast"
    21  	east "github.com/yuin/goldmark/extension/ast"
    22  	"github.com/yuin/goldmark/parser"
    23  	"github.com/yuin/goldmark/renderer"
    24  	"github.com/yuin/goldmark/renderer/html"
    25  	"github.com/yuin/goldmark/text"
    26  	"github.com/yuin/goldmark/util"
    27  )
    28  
    29  var byteMailto = []byte("mailto:")
    30  
    31  // ASTTransformer is a default transformer of the goldmark tree.
    32  type ASTTransformer struct{}
    33  
    34  // Transform transforms the given AST tree.
    35  func (g *ASTTransformer) Transform(node *ast.Document, reader text.Reader, pc parser.Context) {
    36  	firstChild := node.FirstChild()
    37  	createTOC := false
    38  	ctx := pc.Get(renderContextKey).(*markup.RenderContext)
    39  	rc := pc.Get(renderConfigKey).(*RenderConfig)
    40  	if rc.yamlNode != nil {
    41  		metaNode := rc.toMetaNode()
    42  		if metaNode != nil {
    43  			node.InsertBefore(node, firstChild, metaNode)
    44  		}
    45  		createTOC = rc.TOC
    46  		ctx.TableOfContents = make([]markup.Header, 0, 100)
    47  	}
    48  
    49  	attentionMarkedBlockquotes := make(container.Set[*ast.Blockquote])
    50  	_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
    51  		if !entering {
    52  			return ast.WalkContinue, nil
    53  		}
    54  
    55  		switch v := n.(type) {
    56  		case *ast.Heading:
    57  			for _, attr := range v.Attributes() {
    58  				if _, ok := attr.Value.([]byte); !ok {
    59  					v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value)))
    60  				}
    61  			}
    62  			text := n.Text(reader.Source())
    63  			header := markup.Header{
    64  				Text:  util.BytesToReadOnlyString(text),
    65  				Level: v.Level,
    66  			}
    67  			if id, found := v.AttributeString("id"); found {
    68  				header.ID = util.BytesToReadOnlyString(id.([]byte))
    69  			}
    70  			ctx.TableOfContents = append(ctx.TableOfContents, header)
    71  		case *ast.Image:
    72  			// Images need two things:
    73  			//
    74  			// 1. Their src needs to munged to be a real value
    75  			// 2. If they're not wrapped with a link they need a link wrapper
    76  
    77  			// Check if the destination is a real link
    78  			link := v.Destination
    79  			if len(link) > 0 && !markup.IsLink(link) {
    80  				prefix := pc.Get(urlPrefixKey).(string)
    81  				if pc.Get(isWikiKey).(bool) {
    82  					prefix = giteautil.URLJoin(prefix, "wiki", "raw")
    83  				}
    84  				prefix = strings.Replace(prefix, "/src/", "/media/", 1)
    85  
    86  				lnk := strings.TrimLeft(string(link), "/")
    87  
    88  				lnk = giteautil.URLJoin(prefix, lnk)
    89  				link = []byte(lnk)
    90  			}
    91  			v.Destination = link
    92  
    93  			parent := n.Parent()
    94  			// Create a link around image only if parent is not already a link
    95  			if _, ok := parent.(*ast.Link); !ok && parent != nil {
    96  				next := n.NextSibling()
    97  
    98  				// Create a link wrapper
    99  				wrap := ast.NewLink()
   100  				wrap.Destination = link
   101  				wrap.Title = v.Title
   102  				wrap.SetAttributeString("target", []byte("_blank"))
   103  
   104  				// Duplicate the current image node
   105  				image := ast.NewImage(ast.NewLink())
   106  				image.Destination = link
   107  				image.Title = v.Title
   108  				for _, attr := range v.Attributes() {
   109  					image.SetAttribute(attr.Name, attr.Value)
   110  				}
   111  				for child := v.FirstChild(); child != nil; {
   112  					next := child.NextSibling()
   113  					image.AppendChild(image, child)
   114  					child = next
   115  				}
   116  
   117  				// Append our duplicate image to the wrapper link
   118  				wrap.AppendChild(wrap, image)
   119  
   120  				// Wire in the next sibling
   121  				wrap.SetNextSibling(next)
   122  
   123  				// Replace the current node with the wrapper link
   124  				parent.ReplaceChild(parent, n, wrap)
   125  
   126  				// But most importantly ensure the next sibling is still on the old image too
   127  				v.SetNextSibling(next)
   128  			}
   129  		case *ast.Link:
   130  			// Links need their href to munged to be a real value
   131  			link := v.Destination
   132  			if len(link) > 0 && !markup.IsLink(link) &&
   133  				link[0] != '#' && !bytes.HasPrefix(link, byteMailto) {
   134  				// special case: this is not a link, a hash link or a mailto:, so it's a
   135  				// relative URL
   136  				lnk := string(link)
   137  				if pc.Get(isWikiKey).(bool) {
   138  					lnk = giteautil.URLJoin("wiki", lnk)
   139  				}
   140  				link = []byte(giteautil.URLJoin(pc.Get(urlPrefixKey).(string), lnk))
   141  			}
   142  			if len(link) > 0 && link[0] == '#' {
   143  				link = []byte("#user-content-" + string(link)[1:])
   144  			}
   145  			v.Destination = link
   146  		case *ast.List:
   147  			if v.HasChildren() {
   148  				children := make([]ast.Node, 0, v.ChildCount())
   149  				child := v.FirstChild()
   150  				for child != nil {
   151  					children = append(children, child)
   152  					child = child.NextSibling()
   153  				}
   154  				v.RemoveChildren(v)
   155  
   156  				for _, child := range children {
   157  					listItem := child.(*ast.ListItem)
   158  					if !child.HasChildren() || !child.FirstChild().HasChildren() {
   159  						v.AppendChild(v, child)
   160  						continue
   161  					}
   162  					taskCheckBox, ok := child.FirstChild().FirstChild().(*east.TaskCheckBox)
   163  					if !ok {
   164  						v.AppendChild(v, child)
   165  						continue
   166  					}
   167  					newChild := NewTaskCheckBoxListItem(listItem)
   168  					newChild.IsChecked = taskCheckBox.IsChecked
   169  					newChild.SetAttributeString("class", []byte("task-list-item"))
   170  					v.AppendChild(v, newChild)
   171  				}
   172  			}
   173  		case *ast.Text:
   174  			if v.SoftLineBreak() && !v.HardLineBreak() {
   175  				renderMetas := pc.Get(renderMetasKey).(map[string]string)
   176  				mode := renderMetas["mode"]
   177  				if mode != "document" {
   178  					v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments)
   179  				} else {
   180  					v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments)
   181  				}
   182  			}
   183  		case *ast.CodeSpan:
   184  			colorContent := n.Text(reader.Source())
   185  			if css.ColorHandler(strings.ToLower(string(colorContent))) {
   186  				v.AppendChild(v, NewColorPreview(colorContent))
   187  			}
   188  		case *ast.Emphasis:
   189  			// check if inside blockquote for attention, expected hierarchy is
   190  			// Emphasis < Paragraph < Blockquote
   191  			blockquote, isInBlockquote := n.Parent().Parent().(*ast.Blockquote)
   192  			if isInBlockquote && !attentionMarkedBlockquotes.Contains(blockquote) {
   193  				fullText := string(n.Text(reader.Source()))
   194  				if fullText == AttentionNote || fullText == AttentionWarning {
   195  					v.SetAttributeString("class", []byte("attention-"+strings.ToLower(fullText)))
   196  					v.Parent().InsertBefore(v.Parent(), v, NewAttention(fullText))
   197  					attentionMarkedBlockquotes.Add(blockquote)
   198  				}
   199  			}
   200  		}
   201  		return ast.WalkContinue, nil
   202  	})
   203  
   204  	if createTOC && len(ctx.TableOfContents) > 0 {
   205  		lang := rc.Lang
   206  		if len(lang) == 0 {
   207  			lang = setting.Langs[0]
   208  		}
   209  		tocNode := createTOCNode(ctx.TableOfContents, lang)
   210  		if tocNode != nil {
   211  			node.InsertBefore(node, firstChild, tocNode)
   212  		}
   213  	}
   214  
   215  	if len(rc.Lang) > 0 {
   216  		node.SetAttributeString("lang", []byte(rc.Lang))
   217  	}
   218  }
   219  
   220  type prefixedIDs struct {
   221  	values container.Set[string]
   222  }
   223  
   224  // Generate generates a new element id.
   225  func (p *prefixedIDs) Generate(value []byte, kind ast.NodeKind) []byte {
   226  	dft := []byte("id")
   227  	if kind == ast.KindHeading {
   228  		dft = []byte("heading")
   229  	}
   230  	return p.GenerateWithDefault(value, dft)
   231  }
   232  
   233  // Generate generates a new element id.
   234  func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte {
   235  	result := common.CleanValue(value)
   236  	if len(result) == 0 {
   237  		result = dft
   238  	}
   239  	if !bytes.HasPrefix(result, []byte("user-content-")) {
   240  		result = append([]byte("user-content-"), result...)
   241  	}
   242  	if p.values.Add(util.BytesToReadOnlyString(result)) {
   243  		return result
   244  	}
   245  	for i := 1; ; i++ {
   246  		newResult := fmt.Sprintf("%s-%d", result, i)
   247  		if p.values.Add(newResult) {
   248  			return []byte(newResult)
   249  		}
   250  	}
   251  }
   252  
   253  // Put puts a given element id to the used ids table.
   254  func (p *prefixedIDs) Put(value []byte) {
   255  	p.values.Add(util.BytesToReadOnlyString(value))
   256  }
   257  
   258  func newPrefixedIDs() *prefixedIDs {
   259  	return &prefixedIDs{
   260  		values: make(container.Set[string]),
   261  	}
   262  }
   263  
   264  // NewHTMLRenderer creates a HTMLRenderer to render
   265  // in the gitea form.
   266  func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
   267  	r := &HTMLRenderer{
   268  		Config: html.NewConfig(),
   269  	}
   270  	for _, opt := range opts {
   271  		opt.SetHTMLOption(&r.Config)
   272  	}
   273  	return r
   274  }
   275  
   276  // HTMLRenderer is a renderer.NodeRenderer implementation that
   277  // renders gitea specific features.
   278  type HTMLRenderer struct {
   279  	html.Config
   280  }
   281  
   282  // RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
   283  func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
   284  	reg.Register(ast.KindDocument, r.renderDocument)
   285  	reg.Register(KindDetails, r.renderDetails)
   286  	reg.Register(KindSummary, r.renderSummary)
   287  	reg.Register(KindIcon, r.renderIcon)
   288  	reg.Register(ast.KindCodeSpan, r.renderCodeSpan)
   289  	reg.Register(KindAttention, r.renderAttention)
   290  	reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem)
   291  	reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
   292  }
   293  
   294  // renderCodeSpan renders CodeSpan elements (like goldmark upstream does) but also renders ColorPreview elements.
   295  // See #21474 for reference
   296  func (r *HTMLRenderer) renderCodeSpan(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) {
   297  	if entering {
   298  		if n.Attributes() != nil {
   299  			_, _ = w.WriteString("<code")
   300  			html.RenderAttributes(w, n, html.CodeAttributeFilter)
   301  			_ = w.WriteByte('>')
   302  		} else {
   303  			_, _ = w.WriteString("<code>")
   304  		}
   305  		for c := n.FirstChild(); c != nil; c = c.NextSibling() {
   306  			switch v := c.(type) {
   307  			case *ast.Text:
   308  				segment := v.Segment
   309  				value := segment.Value(source)
   310  				if bytes.HasSuffix(value, []byte("\n")) {
   311  					r.Writer.RawWrite(w, value[:len(value)-1])
   312  					r.Writer.RawWrite(w, []byte(" "))
   313  				} else {
   314  					r.Writer.RawWrite(w, value)
   315  				}
   316  			case *ColorPreview:
   317  				_, _ = w.WriteString(fmt.Sprintf(`<span class="color-preview" style="background-color: %v"></span>`, string(v.Color)))
   318  			}
   319  		}
   320  		return ast.WalkSkipChildren, nil
   321  	}
   322  	_, _ = w.WriteString("</code>")
   323  	return ast.WalkContinue, nil
   324  }
   325  
   326  // renderAttention renders a quote marked with i.e. "> **Note**" or "> **Warning**" with a corresponding svg
   327  func (r *HTMLRenderer) renderAttention(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   328  	if entering {
   329  		_, _ = w.WriteString(`<span class="attention-icon attention-`)
   330  		n := node.(*Attention)
   331  		_, _ = w.WriteString(strings.ToLower(n.AttentionType))
   332  		_, _ = w.WriteString(`">`)
   333  
   334  		var octiconType string
   335  		switch n.AttentionType {
   336  		case AttentionNote:
   337  			octiconType = "info"
   338  		case AttentionWarning:
   339  			octiconType = "alert"
   340  		}
   341  		_, _ = w.WriteString(string(svg.RenderHTML("octicon-" + octiconType)))
   342  	} else {
   343  		_, _ = w.WriteString("</span>\n")
   344  	}
   345  	return ast.WalkContinue, nil
   346  }
   347  
   348  func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   349  	n := node.(*ast.Document)
   350  
   351  	if val, has := n.AttributeString("lang"); has {
   352  		var err error
   353  		if entering {
   354  			_, err = w.WriteString("<div")
   355  			if err == nil {
   356  				_, err = w.WriteString(fmt.Sprintf(` lang=%q`, val))
   357  			}
   358  			if err == nil {
   359  				_, err = w.WriteRune('>')
   360  			}
   361  		} else {
   362  			_, err = w.WriteString("</div>")
   363  		}
   364  
   365  		if err != nil {
   366  			return ast.WalkStop, err
   367  		}
   368  	}
   369  
   370  	return ast.WalkContinue, nil
   371  }
   372  
   373  func (r *HTMLRenderer) renderDetails(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   374  	var err error
   375  	if entering {
   376  		_, err = w.WriteString("<details>")
   377  	} else {
   378  		_, err = w.WriteString("</details>")
   379  	}
   380  
   381  	if err != nil {
   382  		return ast.WalkStop, err
   383  	}
   384  
   385  	return ast.WalkContinue, nil
   386  }
   387  
   388  func (r *HTMLRenderer) renderSummary(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   389  	var err error
   390  	if entering {
   391  		_, err = w.WriteString("<summary>")
   392  	} else {
   393  		_, err = w.WriteString("</summary>")
   394  	}
   395  
   396  	if err != nil {
   397  		return ast.WalkStop, err
   398  	}
   399  
   400  	return ast.WalkContinue, nil
   401  }
   402  
   403  var validNameRE = regexp.MustCompile("^[a-z ]+$")
   404  
   405  func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   406  	if !entering {
   407  		return ast.WalkContinue, nil
   408  	}
   409  
   410  	n := node.(*Icon)
   411  
   412  	name := strings.TrimSpace(strings.ToLower(string(n.Name)))
   413  
   414  	if len(name) == 0 {
   415  		// skip this
   416  		return ast.WalkContinue, nil
   417  	}
   418  
   419  	if !validNameRE.MatchString(name) {
   420  		// skip this
   421  		return ast.WalkContinue, nil
   422  	}
   423  
   424  	var err error
   425  	_, err = w.WriteString(fmt.Sprintf(`<i class="icon %s"></i>`, name))
   426  
   427  	if err != nil {
   428  		return ast.WalkStop, err
   429  	}
   430  
   431  	return ast.WalkContinue, nil
   432  }
   433  
   434  func (r *HTMLRenderer) renderTaskCheckBoxListItem(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   435  	n := node.(*TaskCheckBoxListItem)
   436  	if entering {
   437  		if n.Attributes() != nil {
   438  			_, _ = w.WriteString("<li")
   439  			html.RenderAttributes(w, n, html.ListItemAttributeFilter)
   440  			_ = w.WriteByte('>')
   441  		} else {
   442  			_, _ = w.WriteString("<li>")
   443  		}
   444  		_, _ = w.WriteString(`<input type="checkbox" disabled=""`)
   445  		segments := node.FirstChild().Lines()
   446  		if segments.Len() > 0 {
   447  			segment := segments.At(0)
   448  			_, _ = w.WriteString(fmt.Sprintf(` data-source-position="%d"`, segment.Start))
   449  		}
   450  		if n.IsChecked {
   451  			_, _ = w.WriteString(` checked=""`)
   452  		}
   453  		if r.XHTML {
   454  			_, _ = w.WriteString(` />`)
   455  		} else {
   456  			_ = w.WriteByte('>')
   457  		}
   458  		fc := n.FirstChild()
   459  		if fc != nil {
   460  			if _, ok := fc.(*ast.TextBlock); !ok {
   461  				_ = w.WriteByte('\n')
   462  			}
   463  		}
   464  	} else {
   465  		_, _ = w.WriteString("</li>\n")
   466  	}
   467  	return ast.WalkContinue, nil
   468  }
   469  
   470  func (r *HTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   471  	return ast.WalkContinue, nil
   472  }