github.com/gitbundle/modules@v0.0.0-20231025071548-85b91c5c3b01/markup/markdown/goldmark.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  package markdown
     7  
     8  import (
     9  	"bytes"
    10  	"fmt"
    11  	"regexp"
    12  	"strings"
    13  
    14  	"github.com/gitbundle/modules/markup"
    15  	"github.com/gitbundle/modules/markup/common"
    16  	"github.com/gitbundle/modules/setting"
    17  	giteautil "github.com/gitbundle/modules/util"
    18  
    19  	meta "github.com/yuin/goldmark-meta"
    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  	metaData := meta.GetItems(pc)
    37  	firstChild := node.FirstChild()
    38  	createTOC := false
    39  	ctx := pc.Get(renderContextKey).(*markup.RenderContext)
    40  	rc := &RenderConfig{
    41  		Meta: "table",
    42  		Icon: "table",
    43  		Lang: "",
    44  	}
    45  
    46  	if metaData != nil {
    47  		rc.ToRenderConfig(metaData)
    48  
    49  		metaNode := rc.toMetaNode(metaData)
    50  		if metaNode != nil {
    51  			node.InsertBefore(node, firstChild, metaNode)
    52  		}
    53  		createTOC = rc.TOC
    54  		ctx.TableOfContents = make([]markup.Header, 0, 100)
    55  	}
    56  
    57  	_ = ast.Walk(node, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
    58  		if !entering {
    59  			return ast.WalkContinue, nil
    60  		}
    61  
    62  		switch v := n.(type) {
    63  		case *ast.Heading:
    64  			for _, attr := range v.Attributes() {
    65  				if _, ok := attr.Value.([]byte); !ok {
    66  					v.SetAttribute(attr.Name, []byte(fmt.Sprintf("%v", attr.Value)))
    67  				}
    68  			}
    69  			text := n.Text(reader.Source())
    70  			header := markup.Header{
    71  				Text:  util.BytesToReadOnlyString(text),
    72  				Level: v.Level,
    73  			}
    74  			if id, found := v.AttributeString("id"); found {
    75  				header.ID = util.BytesToReadOnlyString(id.([]byte))
    76  			}
    77  			ctx.TableOfContents = append(ctx.TableOfContents, header)
    78  		case *ast.Image:
    79  			// Images need two things:
    80  			//
    81  			// 1. Their src needs to munged to be a real value
    82  			// 2. If they're not wrapped with a link they need a link wrapper
    83  
    84  			// Check if the destination is a real link
    85  			link := v.Destination
    86  			if len(link) > 0 && !markup.IsLink(link) {
    87  				prefix := pc.Get(urlPrefixKey).(string)
    88  				if pc.Get(isWikiKey).(bool) {
    89  					prefix = giteautil.URLJoin(prefix, "wiki", "raw")
    90  				}
    91  				prefix = strings.Replace(prefix, "/src/", "/media/", 1)
    92  
    93  				lnk := strings.TrimLeft(string(link), "/")
    94  
    95  				lnk = giteautil.URLJoin(prefix, lnk)
    96  				link = []byte(lnk)
    97  			}
    98  			v.Destination = link
    99  
   100  			parent := n.Parent()
   101  			// Create a link around image only if parent is not already a link
   102  			if _, ok := parent.(*ast.Link); !ok && parent != nil {
   103  				next := n.NextSibling()
   104  
   105  				// Create a link wrapper
   106  				wrap := ast.NewLink()
   107  				wrap.Destination = link
   108  				wrap.Title = v.Title
   109  				wrap.SetAttributeString("target", []byte("_blank"))
   110  
   111  				// Duplicate the current image node
   112  				image := ast.NewImage(ast.NewLink())
   113  				image.Destination = link
   114  				image.Title = v.Title
   115  				for _, attr := range v.Attributes() {
   116  					image.SetAttribute(attr.Name, attr.Value)
   117  				}
   118  				for child := v.FirstChild(); child != nil; {
   119  					next := child.NextSibling()
   120  					image.AppendChild(image, child)
   121  					child = next
   122  				}
   123  
   124  				// Append our duplicate image to the wrapper link
   125  				wrap.AppendChild(wrap, image)
   126  
   127  				// Wire in the next sibling
   128  				wrap.SetNextSibling(next)
   129  
   130  				// Replace the current node with the wrapper link
   131  				parent.ReplaceChild(parent, n, wrap)
   132  
   133  				// But most importantly ensure the next sibling is still on the old image too
   134  				v.SetNextSibling(next)
   135  			}
   136  		case *ast.Link:
   137  			// Links need their href to munged to be a real value
   138  			link := v.Destination
   139  			if len(link) > 0 && !markup.IsLink(link) &&
   140  				link[0] != '#' && !bytes.HasPrefix(link, byteMailto) {
   141  				// special case: this is not a link, a hash link or a mailto:, so it's a
   142  				// relative URL
   143  				lnk := string(link)
   144  				if pc.Get(isWikiKey).(bool) {
   145  					lnk = giteautil.URLJoin("wiki", lnk)
   146  				}
   147  				link = []byte(giteautil.URLJoin(pc.Get(urlPrefixKey).(string), lnk))
   148  			}
   149  			if len(link) > 0 && link[0] == '#' {
   150  				link = []byte("#user-content-" + string(link)[1:])
   151  			}
   152  			v.Destination = link
   153  		case *ast.List:
   154  			if v.HasChildren() {
   155  				children := make([]ast.Node, 0, v.ChildCount())
   156  				child := v.FirstChild()
   157  				for child != nil {
   158  					children = append(children, child)
   159  					child = child.NextSibling()
   160  				}
   161  				v.RemoveChildren(v)
   162  
   163  				for _, child := range children {
   164  					listItem := child.(*ast.ListItem)
   165  					if !child.HasChildren() || !child.FirstChild().HasChildren() {
   166  						v.AppendChild(v, child)
   167  						continue
   168  					}
   169  					taskCheckBox, ok := child.FirstChild().FirstChild().(*east.TaskCheckBox)
   170  					if !ok {
   171  						v.AppendChild(v, child)
   172  						continue
   173  					}
   174  					newChild := NewTaskCheckBoxListItem(listItem)
   175  					newChild.IsChecked = taskCheckBox.IsChecked
   176  					newChild.SetAttributeString("class", []byte("task-list-item"))
   177  					v.AppendChild(v, newChild)
   178  				}
   179  			}
   180  		case *ast.Text:
   181  			if v.SoftLineBreak() && !v.HardLineBreak() {
   182  				renderMetas := pc.Get(renderMetasKey).(map[string]string)
   183  				mode := renderMetas["mode"]
   184  				if mode != "document" {
   185  					v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInComments)
   186  				} else {
   187  					v.SetHardLineBreak(setting.Markdown.EnableHardLineBreakInDocuments)
   188  				}
   189  			}
   190  		}
   191  		return ast.WalkContinue, nil
   192  	})
   193  
   194  	if createTOC && len(ctx.TableOfContents) > 0 {
   195  		lang := rc.Lang
   196  		if len(lang) == 0 {
   197  			lang = setting.Langs[0]
   198  		}
   199  		tocNode := createTOCNode(ctx.TableOfContents, lang)
   200  		if tocNode != nil {
   201  			node.InsertBefore(node, firstChild, tocNode)
   202  		}
   203  	}
   204  
   205  	if len(rc.Lang) > 0 {
   206  		node.SetAttributeString("lang", []byte(rc.Lang))
   207  	}
   208  }
   209  
   210  type prefixedIDs struct {
   211  	values map[string]bool
   212  }
   213  
   214  // Generate generates a new element id.
   215  func (p *prefixedIDs) Generate(value []byte, kind ast.NodeKind) []byte {
   216  	dft := []byte("id")
   217  	if kind == ast.KindHeading {
   218  		dft = []byte("heading")
   219  	}
   220  	return p.GenerateWithDefault(value, dft)
   221  }
   222  
   223  // Generate generates a new element id.
   224  func (p *prefixedIDs) GenerateWithDefault(value, dft []byte) []byte {
   225  	result := common.CleanValue(value)
   226  	if len(result) == 0 {
   227  		result = dft
   228  	}
   229  	if !bytes.HasPrefix(result, []byte("user-content-")) {
   230  		result = append([]byte("user-content-"), result...)
   231  	}
   232  	if _, ok := p.values[util.BytesToReadOnlyString(result)]; !ok {
   233  		p.values[util.BytesToReadOnlyString(result)] = true
   234  		return result
   235  	}
   236  	for i := 1; ; i++ {
   237  		newResult := fmt.Sprintf("%s-%d", result, i)
   238  		if _, ok := p.values[newResult]; !ok {
   239  			p.values[newResult] = true
   240  			return []byte(newResult)
   241  		}
   242  	}
   243  }
   244  
   245  // Put puts a given element id to the used ids table.
   246  func (p *prefixedIDs) Put(value []byte) {
   247  	p.values[util.BytesToReadOnlyString(value)] = true
   248  }
   249  
   250  func newPrefixedIDs() *prefixedIDs {
   251  	return &prefixedIDs{
   252  		values: map[string]bool{},
   253  	}
   254  }
   255  
   256  // NewHTMLRenderer creates a HTMLRenderer to render
   257  // in the gitea form.
   258  func NewHTMLRenderer(opts ...html.Option) renderer.NodeRenderer {
   259  	r := &HTMLRenderer{
   260  		Config: html.NewConfig(),
   261  	}
   262  	for _, opt := range opts {
   263  		opt.SetHTMLOption(&r.Config)
   264  	}
   265  	return r
   266  }
   267  
   268  // HTMLRenderer is a renderer.NodeRenderer implementation that
   269  // renders gitea specific features.
   270  type HTMLRenderer struct {
   271  	html.Config
   272  }
   273  
   274  // RegisterFuncs implements renderer.NodeRenderer.RegisterFuncs.
   275  func (r *HTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
   276  	reg.Register(ast.KindDocument, r.renderDocument)
   277  	reg.Register(KindDetails, r.renderDetails)
   278  	reg.Register(KindSummary, r.renderSummary)
   279  	reg.Register(KindIcon, r.renderIcon)
   280  	reg.Register(KindTaskCheckBoxListItem, r.renderTaskCheckBoxListItem)
   281  	reg.Register(east.KindTaskCheckBox, r.renderTaskCheckBox)
   282  }
   283  
   284  func (r *HTMLRenderer) renderDocument(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   285  	n := node.(*ast.Document)
   286  
   287  	if val, has := n.AttributeString("lang"); has {
   288  		var err error
   289  		if entering {
   290  			_, err = w.WriteString("<div")
   291  			if err == nil {
   292  				_, err = w.WriteString(fmt.Sprintf(` lang=%q`, val))
   293  			}
   294  			if err == nil {
   295  				_, err = w.WriteRune('>')
   296  			}
   297  		} else {
   298  			_, err = w.WriteString("</div>")
   299  		}
   300  
   301  		if err != nil {
   302  			return ast.WalkStop, err
   303  		}
   304  	}
   305  
   306  	return ast.WalkContinue, nil
   307  }
   308  
   309  func (r *HTMLRenderer) renderDetails(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   310  	var err error
   311  	if entering {
   312  		_, err = w.WriteString("<details>")
   313  	} else {
   314  		_, err = w.WriteString("</details>")
   315  	}
   316  
   317  	if err != nil {
   318  		return ast.WalkStop, err
   319  	}
   320  
   321  	return ast.WalkContinue, nil
   322  }
   323  
   324  func (r *HTMLRenderer) renderSummary(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   325  	var err error
   326  	if entering {
   327  		_, err = w.WriteString("<summary>")
   328  	} else {
   329  		_, err = w.WriteString("</summary>")
   330  	}
   331  
   332  	if err != nil {
   333  		return ast.WalkStop, err
   334  	}
   335  
   336  	return ast.WalkContinue, nil
   337  }
   338  
   339  var validNameRE = regexp.MustCompile("^[a-z ]+$")
   340  
   341  func (r *HTMLRenderer) renderIcon(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   342  	if !entering {
   343  		return ast.WalkContinue, nil
   344  	}
   345  
   346  	n := node.(*Icon)
   347  
   348  	name := strings.TrimSpace(strings.ToLower(string(n.Name)))
   349  
   350  	if len(name) == 0 {
   351  		// skip this
   352  		return ast.WalkContinue, nil
   353  	}
   354  
   355  	if !validNameRE.MatchString(name) {
   356  		// skip this
   357  		return ast.WalkContinue, nil
   358  	}
   359  
   360  	var err error
   361  	_, err = w.WriteString(fmt.Sprintf(`<i class="icon %s"></i>`, name))
   362  
   363  	if err != nil {
   364  		return ast.WalkStop, err
   365  	}
   366  
   367  	return ast.WalkContinue, nil
   368  }
   369  
   370  func (r *HTMLRenderer) renderTaskCheckBoxListItem(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   371  	n := node.(*TaskCheckBoxListItem)
   372  	if entering {
   373  		if n.Attributes() != nil {
   374  			_, _ = w.WriteString("<li")
   375  			html.RenderAttributes(w, n, html.ListItemAttributeFilter)
   376  			_ = w.WriteByte('>')
   377  		} else {
   378  			_, _ = w.WriteString("<li>")
   379  		}
   380  		_, _ = w.WriteString(`<input type="checkbox" disabled=""`)
   381  		segments := node.FirstChild().Lines()
   382  		if segments.Len() > 0 {
   383  			segment := segments.At(0)
   384  			_, _ = w.WriteString(fmt.Sprintf(` data-source-position="%d"`, segment.Start))
   385  		}
   386  		if n.IsChecked {
   387  			_, _ = w.WriteString(` checked=""`)
   388  		}
   389  		if r.XHTML {
   390  			_, _ = w.WriteString(` />`)
   391  		} else {
   392  			_ = w.WriteByte('>')
   393  		}
   394  		fc := n.FirstChild()
   395  		if fc != nil {
   396  			if _, ok := fc.(*ast.TextBlock); !ok {
   397  				_ = w.WriteByte('\n')
   398  			}
   399  		}
   400  	} else {
   401  		_, _ = w.WriteString("</li>\n")
   402  	}
   403  	return ast.WalkContinue, nil
   404  }
   405  
   406  func (r *HTMLRenderer) renderTaskCheckBox(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   407  	return ast.WalkContinue, nil
   408  }