github.com/graemephi/kahugo@v0.62.3-0.20211121071557-d78c0423784d/markup/goldmark/render_hooks.go (about)

     1  // Copyright 2019 The Hugo Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package goldmark
    15  
    16  import (
    17  	"bytes"
    18  	"strings"
    19  	"sync"
    20  
    21  	"github.com/spf13/cast"
    22  
    23  	"github.com/gohugoio/hugo/markup/converter/hooks"
    24  
    25  	"github.com/yuin/goldmark"
    26  	"github.com/yuin/goldmark/ast"
    27  	"github.com/yuin/goldmark/renderer"
    28  	"github.com/yuin/goldmark/renderer/html"
    29  	"github.com/yuin/goldmark/util"
    30  )
    31  
    32  var _ renderer.SetOptioner = (*hookedRenderer)(nil)
    33  
    34  func newLinkRenderer() renderer.NodeRenderer {
    35  	r := &hookedRenderer{
    36  		Config: html.Config{
    37  			Writer: html.DefaultWriter,
    38  		},
    39  	}
    40  	return r
    41  }
    42  
    43  func newLinks() goldmark.Extender {
    44  	return &links{}
    45  }
    46  
    47  type attributesHolder struct {
    48  	// What we get from Goldmark.
    49  	astAttributes []ast.Attribute
    50  
    51  	// What we send to the the render hooks.
    52  	attributesInit sync.Once
    53  	attributes     map[string]string
    54  }
    55  
    56  func (a *attributesHolder) Attributes() map[string]string {
    57  	a.attributesInit.Do(func() {
    58  		a.attributes = make(map[string]string)
    59  		for _, attr := range a.astAttributes {
    60  			a.attributes[string(attr.Name)] = string(util.EscapeHTML(attr.Value.([]byte)))
    61  		}
    62  	})
    63  	return a.attributes
    64  }
    65  
    66  type linkContext struct {
    67  	page        interface{}
    68  	destination string
    69  	title       string
    70  	text        string
    71  	plainText   string
    72  }
    73  
    74  func (ctx linkContext) Destination() string {
    75  	return ctx.destination
    76  }
    77  
    78  func (ctx linkContext) Resolved() bool {
    79  	return false
    80  }
    81  
    82  func (ctx linkContext) Page() interface{} {
    83  	return ctx.page
    84  }
    85  
    86  func (ctx linkContext) Text() string {
    87  	return ctx.text
    88  }
    89  
    90  func (ctx linkContext) PlainText() string {
    91  	return ctx.plainText
    92  }
    93  
    94  func (ctx linkContext) Title() string {
    95  	return ctx.title
    96  }
    97  
    98  type headingContext struct {
    99  	page      interface{}
   100  	level     int
   101  	anchor    string
   102  	text      string
   103  	plainText string
   104  	*attributesHolder
   105  }
   106  
   107  func (ctx headingContext) Page() interface{} {
   108  	return ctx.page
   109  }
   110  
   111  func (ctx headingContext) Level() int {
   112  	return ctx.level
   113  }
   114  
   115  func (ctx headingContext) Anchor() string {
   116  	return ctx.anchor
   117  }
   118  
   119  func (ctx headingContext) Text() string {
   120  	return ctx.text
   121  }
   122  
   123  func (ctx headingContext) PlainText() string {
   124  	return ctx.plainText
   125  }
   126  
   127  type hookedRenderer struct {
   128  	html.Config
   129  }
   130  
   131  func (r *hookedRenderer) SetOption(name renderer.OptionName, value interface{}) {
   132  	r.Config.SetOption(name, value)
   133  }
   134  
   135  // RegisterFuncs implements NodeRenderer.RegisterFuncs.
   136  func (r *hookedRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
   137  	reg.Register(ast.KindLink, r.renderLink)
   138  	reg.Register(ast.KindAutoLink, r.renderAutoLink)
   139  	reg.Register(ast.KindImage, r.renderImage)
   140  	reg.Register(ast.KindHeading, r.renderHeading)
   141  }
   142  
   143  func (r *hookedRenderer) renderAttributesForNode(w util.BufWriter, node ast.Node) {
   144  	renderAttributes(w, false, node.Attributes()...)
   145  }
   146  
   147  var (
   148  
   149  	// Attributes with special meaning that does not make sense to render in HTML.
   150  	attributeExcludes = map[string]bool{
   151  		"linenos":     true,
   152  		"hl_lines":    true,
   153  		"linenostart": true,
   154  	}
   155  )
   156  
   157  func renderAttributes(w util.BufWriter, skipClass bool, attributes ...ast.Attribute) {
   158  	for _, attr := range attributes {
   159  		if skipClass && bytes.Equal(attr.Name, []byte("class")) {
   160  			continue
   161  		}
   162  
   163  		if attributeExcludes[string(attr.Name)] {
   164  			continue
   165  		}
   166  
   167  		_, _ = w.WriteString(" ")
   168  		_, _ = w.Write(attr.Name)
   169  		_, _ = w.WriteString(`="`)
   170  
   171  		switch v := attr.Value.(type) {
   172  		case []byte:
   173  			_, _ = w.Write(util.EscapeHTML(v))
   174  		default:
   175  			w.WriteString(cast.ToString(v))
   176  		}
   177  
   178  		_ = w.WriteByte('"')
   179  	}
   180  }
   181  
   182  func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   183  	n := node.(*ast.Image)
   184  	var h hooks.Renderers
   185  
   186  	ctx, ok := w.(*renderContext)
   187  	if ok {
   188  		h = ctx.RenderContext().RenderHooks
   189  		ok = h.ImageRenderer != nil
   190  	}
   191  
   192  	if !ok {
   193  		return r.renderImageDefault(w, source, node, entering)
   194  	}
   195  
   196  	if entering {
   197  		// Store the current pos so we can capture the rendered text.
   198  		ctx.pos = ctx.Buffer.Len()
   199  		return ast.WalkContinue, nil
   200  	}
   201  
   202  	text := ctx.Buffer.Bytes()[ctx.pos:]
   203  	ctx.Buffer.Truncate(ctx.pos)
   204  
   205  	err := h.ImageRenderer.RenderLink(
   206  		w,
   207  		linkContext{
   208  			page:        ctx.DocumentContext().Document,
   209  			destination: string(n.Destination),
   210  			title:       string(n.Title),
   211  			text:        string(text),
   212  			plainText:   string(n.Text(source)),
   213  		},
   214  	)
   215  
   216  	ctx.AddIdentity(h.ImageRenderer)
   217  
   218  	return ast.WalkContinue, err
   219  }
   220  
   221  // Fall back to the default Goldmark render funcs. Method below borrowed from:
   222  // https://github.com/yuin/goldmark/blob/b611cd333a492416b56aa8d94b04a67bf0096ab2/renderer/html/html.go#L404
   223  func (r *hookedRenderer) renderImageDefault(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   224  	if !entering {
   225  		return ast.WalkContinue, nil
   226  	}
   227  	n := node.(*ast.Image)
   228  	_, _ = w.WriteString("<img src=\"")
   229  	if r.Unsafe || !html.IsDangerousURL(n.Destination) {
   230  		_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
   231  	}
   232  	_, _ = w.WriteString(`" alt="`)
   233  	_, _ = w.Write(n.Text(source))
   234  	_ = w.WriteByte('"')
   235  	if n.Title != nil {
   236  		_, _ = w.WriteString(` title="`)
   237  		r.Writer.Write(w, n.Title)
   238  		_ = w.WriteByte('"')
   239  	}
   240  	if r.XHTML {
   241  		_, _ = w.WriteString(" />")
   242  	} else {
   243  		_, _ = w.WriteString(">")
   244  	}
   245  	return ast.WalkSkipChildren, nil
   246  }
   247  
   248  func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   249  	n := node.(*ast.Link)
   250  	var h hooks.Renderers
   251  
   252  	ctx, ok := w.(*renderContext)
   253  	if ok {
   254  		h = ctx.RenderContext().RenderHooks
   255  		ok = h.LinkRenderer != nil
   256  	}
   257  
   258  	if !ok {
   259  		return r.renderLinkDefault(w, source, node, entering)
   260  	}
   261  
   262  	if entering {
   263  		// Store the current pos so we can capture the rendered text.
   264  		ctx.pos = ctx.Buffer.Len()
   265  		return ast.WalkContinue, nil
   266  	}
   267  
   268  	text := ctx.Buffer.Bytes()[ctx.pos:]
   269  	ctx.Buffer.Truncate(ctx.pos)
   270  
   271  	err := h.LinkRenderer.RenderLink(
   272  		w,
   273  		linkContext{
   274  			page:        ctx.DocumentContext().Document,
   275  			destination: string(n.Destination),
   276  			title:       string(n.Title),
   277  			text:        string(text),
   278  			plainText:   string(n.Text(source)),
   279  		},
   280  	)
   281  
   282  	// TODO(bep) I have a working branch that fixes these rather confusing identity types,
   283  	// but for now it's important that it's not .GetIdentity() that's added here,
   284  	// to make sure we search the entire chain on changes.
   285  	ctx.AddIdentity(h.LinkRenderer)
   286  
   287  	return ast.WalkContinue, err
   288  }
   289  
   290  // Fall back to the default Goldmark render funcs. Method below borrowed from:
   291  // https://github.com/yuin/goldmark/blob/b611cd333a492416b56aa8d94b04a67bf0096ab2/renderer/html/html.go#L404
   292  func (r *hookedRenderer) renderLinkDefault(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   293  	n := node.(*ast.Link)
   294  	if entering {
   295  		_, _ = w.WriteString("<a href=\"")
   296  		if r.Unsafe || !html.IsDangerousURL(n.Destination) {
   297  			_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
   298  		}
   299  		_ = w.WriteByte('"')
   300  		if n.Title != nil {
   301  			_, _ = w.WriteString(` title="`)
   302  			r.Writer.Write(w, n.Title)
   303  			_ = w.WriteByte('"')
   304  		}
   305  		_ = w.WriteByte('>')
   306  	} else {
   307  		_, _ = w.WriteString("</a>")
   308  	}
   309  	return ast.WalkContinue, nil
   310  }
   311  
   312  func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   313  	if !entering {
   314  		return ast.WalkContinue, nil
   315  	}
   316  
   317  	n := node.(*ast.AutoLink)
   318  	var h hooks.Renderers
   319  
   320  	ctx, ok := w.(*renderContext)
   321  	if ok {
   322  		h = ctx.RenderContext().RenderHooks
   323  		ok = h.LinkRenderer != nil
   324  	}
   325  
   326  	if !ok {
   327  		return r.renderAutoLinkDefault(w, source, node, entering)
   328  	}
   329  
   330  	url := string(n.URL(source))
   331  	label := string(n.Label(source))
   332  	if n.AutoLinkType == ast.AutoLinkEmail && !strings.HasPrefix(strings.ToLower(url), "mailto:") {
   333  		url = "mailto:" + url
   334  	}
   335  
   336  	err := h.LinkRenderer.RenderLink(
   337  		w,
   338  		linkContext{
   339  			page:        ctx.DocumentContext().Document,
   340  			destination: url,
   341  			text:        label,
   342  			plainText:   label,
   343  		},
   344  	)
   345  
   346  	// TODO(bep) I have a working branch that fixes these rather confusing identity types,
   347  	// but for now it's important that it's not .GetIdentity() that's added here,
   348  	// to make sure we search the entire chain on changes.
   349  	ctx.AddIdentity(h.LinkRenderer)
   350  
   351  	return ast.WalkContinue, err
   352  }
   353  
   354  // Fall back to the default Goldmark render funcs. Method below borrowed from:
   355  // https://github.com/yuin/goldmark/blob/5588d92a56fe1642791cf4aa8e9eae8227cfeecd/renderer/html/html.go#L439
   356  func (r *hookedRenderer) renderAutoLinkDefault(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   357  	n := node.(*ast.AutoLink)
   358  	if !entering {
   359  		return ast.WalkContinue, nil
   360  	}
   361  	_, _ = w.WriteString(`<a href="`)
   362  	url := n.URL(source)
   363  	label := n.Label(source)
   364  	if n.AutoLinkType == ast.AutoLinkEmail && !bytes.HasPrefix(bytes.ToLower(url), []byte("mailto:")) {
   365  		_, _ = w.WriteString("mailto:")
   366  	}
   367  	_, _ = w.Write(util.EscapeHTML(util.URLEscape(url, false)))
   368  	if n.Attributes() != nil {
   369  		_ = w.WriteByte('"')
   370  		html.RenderAttributes(w, n, html.LinkAttributeFilter)
   371  		_ = w.WriteByte('>')
   372  	} else {
   373  		_, _ = w.WriteString(`">`)
   374  	}
   375  	_, _ = w.Write(util.EscapeHTML(label))
   376  	_, _ = w.WriteString(`</a>`)
   377  	return ast.WalkContinue, nil
   378  }
   379  
   380  func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   381  	n := node.(*ast.Heading)
   382  	var h hooks.Renderers
   383  
   384  	ctx, ok := w.(*renderContext)
   385  	if ok {
   386  		h = ctx.RenderContext().RenderHooks
   387  		ok = h.HeadingRenderer != nil
   388  	}
   389  
   390  	if !ok {
   391  		return r.renderHeadingDefault(w, source, node, entering)
   392  	}
   393  
   394  	if entering {
   395  		// Store the current pos so we can capture the rendered text.
   396  		ctx.pos = ctx.Buffer.Len()
   397  		return ast.WalkContinue, nil
   398  	}
   399  
   400  	text := ctx.Buffer.Bytes()[ctx.pos:]
   401  	ctx.Buffer.Truncate(ctx.pos)
   402  	// All ast.Heading nodes are guaranteed to have an attribute called "id"
   403  	// that is an array of bytes that encode a valid string.
   404  	anchori, _ := n.AttributeString("id")
   405  	anchor := anchori.([]byte)
   406  
   407  	err := h.HeadingRenderer.RenderHeading(
   408  		w,
   409  		headingContext{
   410  			page:             ctx.DocumentContext().Document,
   411  			level:            n.Level,
   412  			anchor:           string(anchor),
   413  			text:             string(text),
   414  			plainText:        string(n.Text(source)),
   415  			attributesHolder: &attributesHolder{astAttributes: n.Attributes()},
   416  		},
   417  	)
   418  
   419  	ctx.AddIdentity(h.HeadingRenderer)
   420  
   421  	return ast.WalkContinue, err
   422  }
   423  
   424  func (r *hookedRenderer) renderHeadingDefault(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   425  	n := node.(*ast.Heading)
   426  	if entering {
   427  		_, _ = w.WriteString("<h")
   428  		_ = w.WriteByte("0123456"[n.Level])
   429  		if n.Attributes() != nil {
   430  			r.renderAttributesForNode(w, node)
   431  		}
   432  		_ = w.WriteByte('>')
   433  	} else {
   434  		_, _ = w.WriteString("</h")
   435  		_ = w.WriteByte("0123456"[n.Level])
   436  		_, _ = w.WriteString(">\n")
   437  	}
   438  	return ast.WalkContinue, nil
   439  }
   440  
   441  type links struct {
   442  }
   443  
   444  // Extend implements goldmark.Extender.
   445  func (e *links) Extend(m goldmark.Markdown) {
   446  	m.Renderer().AddOptions(renderer.WithNodeRenderers(
   447  		util.Prioritized(newLinkRenderer(), 100),
   448  	))
   449  }