github.com/kovansky/hugo@v0.92.3-0.20220224232819-63076e4ff19f/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  
    20  	"github.com/gohugoio/hugo/markup/converter/hooks"
    21  	"github.com/gohugoio/hugo/markup/goldmark/internal/render"
    22  	"github.com/gohugoio/hugo/markup/internal/attributes"
    23  
    24  	"github.com/yuin/goldmark"
    25  	"github.com/yuin/goldmark/ast"
    26  	"github.com/yuin/goldmark/renderer"
    27  	"github.com/yuin/goldmark/renderer/html"
    28  	"github.com/yuin/goldmark/util"
    29  )
    30  
    31  var _ renderer.SetOptioner = (*hookedRenderer)(nil)
    32  
    33  func newLinkRenderer() renderer.NodeRenderer {
    34  	r := &hookedRenderer{
    35  		Config: html.Config{
    36  			Writer: html.DefaultWriter,
    37  		},
    38  	}
    39  	return r
    40  }
    41  
    42  func newLinks() goldmark.Extender {
    43  	return &links{}
    44  }
    45  
    46  type linkContext struct {
    47  	page        interface{}
    48  	destination string
    49  	title       string
    50  	text        string
    51  	plainText   string
    52  }
    53  
    54  func (ctx linkContext) Destination() string {
    55  	return ctx.destination
    56  }
    57  
    58  func (ctx linkContext) Resolved() bool {
    59  	return false
    60  }
    61  
    62  func (ctx linkContext) Page() interface{} {
    63  	return ctx.page
    64  }
    65  
    66  func (ctx linkContext) Text() string {
    67  	return ctx.text
    68  }
    69  
    70  func (ctx linkContext) PlainText() string {
    71  	return ctx.plainText
    72  }
    73  
    74  func (ctx linkContext) Title() string {
    75  	return ctx.title
    76  }
    77  
    78  type headingContext struct {
    79  	page      interface{}
    80  	level     int
    81  	anchor    string
    82  	text      string
    83  	plainText string
    84  	*attributes.AttributesHolder
    85  }
    86  
    87  func (ctx headingContext) Page() interface{} {
    88  	return ctx.page
    89  }
    90  
    91  func (ctx headingContext) Level() int {
    92  	return ctx.level
    93  }
    94  
    95  func (ctx headingContext) Anchor() string {
    96  	return ctx.anchor
    97  }
    98  
    99  func (ctx headingContext) Text() string {
   100  	return ctx.text
   101  }
   102  
   103  func (ctx headingContext) PlainText() string {
   104  	return ctx.plainText
   105  }
   106  
   107  type hookedRenderer struct {
   108  	html.Config
   109  }
   110  
   111  func (r *hookedRenderer) SetOption(name renderer.OptionName, value interface{}) {
   112  	r.Config.SetOption(name, value)
   113  }
   114  
   115  // RegisterFuncs implements NodeRenderer.RegisterFuncs.
   116  func (r *hookedRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
   117  	reg.Register(ast.KindLink, r.renderLink)
   118  	reg.Register(ast.KindAutoLink, r.renderAutoLink)
   119  	reg.Register(ast.KindImage, r.renderImage)
   120  	reg.Register(ast.KindHeading, r.renderHeading)
   121  }
   122  
   123  func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   124  	n := node.(*ast.Image)
   125  	var lr hooks.LinkRenderer
   126  
   127  	ctx, ok := w.(*render.Context)
   128  	if ok {
   129  		h := ctx.RenderContext().GetRenderer(hooks.ImageRendererType, nil)
   130  		ok = h != nil
   131  		if ok {
   132  			lr = h.(hooks.LinkRenderer)
   133  		}
   134  	}
   135  
   136  	if !ok {
   137  		return r.renderImageDefault(w, source, node, entering)
   138  	}
   139  
   140  	if entering {
   141  		// Store the current pos so we can capture the rendered text.
   142  		ctx.PushPos(ctx.Buffer.Len())
   143  		return ast.WalkContinue, nil
   144  	}
   145  
   146  	pos := ctx.PopPos()
   147  	text := ctx.Buffer.Bytes()[pos:]
   148  	ctx.Buffer.Truncate(pos)
   149  
   150  	err := lr.RenderLink(
   151  		w,
   152  		linkContext{
   153  			page:        ctx.DocumentContext().Document,
   154  			destination: string(n.Destination),
   155  			title:       string(n.Title),
   156  			text:        string(text),
   157  			plainText:   string(n.Text(source)),
   158  		},
   159  	)
   160  
   161  	ctx.AddIdentity(lr)
   162  
   163  	return ast.WalkContinue, err
   164  }
   165  
   166  // Fall back to the default Goldmark render funcs. Method below borrowed from:
   167  // https://github.com/yuin/goldmark/blob/b611cd333a492416b56aa8d94b04a67bf0096ab2/renderer/html/html.go#L404
   168  func (r *hookedRenderer) renderImageDefault(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   169  	if !entering {
   170  		return ast.WalkContinue, nil
   171  	}
   172  	n := node.(*ast.Image)
   173  	_, _ = w.WriteString("<img src=\"")
   174  	if r.Unsafe || !html.IsDangerousURL(n.Destination) {
   175  		_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
   176  	}
   177  	_, _ = w.WriteString(`" alt="`)
   178  	_, _ = w.Write(n.Text(source))
   179  	_ = w.WriteByte('"')
   180  	if n.Title != nil {
   181  		_, _ = w.WriteString(` title="`)
   182  		r.Writer.Write(w, n.Title)
   183  		_ = w.WriteByte('"')
   184  	}
   185  	if r.XHTML {
   186  		_, _ = w.WriteString(" />")
   187  	} else {
   188  		_, _ = w.WriteString(">")
   189  	}
   190  	return ast.WalkSkipChildren, nil
   191  }
   192  
   193  func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   194  	n := node.(*ast.Link)
   195  	var lr hooks.LinkRenderer
   196  
   197  	ctx, ok := w.(*render.Context)
   198  	if ok {
   199  		h := ctx.RenderContext().GetRenderer(hooks.LinkRendererType, nil)
   200  		ok = h != nil
   201  		if ok {
   202  			lr = h.(hooks.LinkRenderer)
   203  		}
   204  	}
   205  
   206  	if !ok {
   207  		return r.renderLinkDefault(w, source, node, entering)
   208  	}
   209  
   210  	if entering {
   211  		// Store the current pos so we can capture the rendered text.
   212  		ctx.PushPos(ctx.Buffer.Len())
   213  		return ast.WalkContinue, nil
   214  	}
   215  
   216  	pos := ctx.PopPos()
   217  	text := ctx.Buffer.Bytes()[pos:]
   218  	ctx.Buffer.Truncate(pos)
   219  
   220  	err := lr.RenderLink(
   221  		w,
   222  		linkContext{
   223  			page:        ctx.DocumentContext().Document,
   224  			destination: string(n.Destination),
   225  			title:       string(n.Title),
   226  			text:        string(text),
   227  			plainText:   string(n.Text(source)),
   228  		},
   229  	)
   230  
   231  	// TODO(bep) I have a working branch that fixes these rather confusing identity types,
   232  	// but for now it's important that it's not .GetIdentity() that's added here,
   233  	// to make sure we search the entire chain on changes.
   234  	ctx.AddIdentity(lr)
   235  
   236  	return ast.WalkContinue, err
   237  }
   238  
   239  // Fall back to the default Goldmark render funcs. Method below borrowed from:
   240  // https://github.com/yuin/goldmark/blob/b611cd333a492416b56aa8d94b04a67bf0096ab2/renderer/html/html.go#L404
   241  func (r *hookedRenderer) renderLinkDefault(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   242  	n := node.(*ast.Link)
   243  	if entering {
   244  		_, _ = w.WriteString("<a href=\"")
   245  		if r.Unsafe || !html.IsDangerousURL(n.Destination) {
   246  			_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
   247  		}
   248  		_ = w.WriteByte('"')
   249  		if n.Title != nil {
   250  			_, _ = w.WriteString(` title="`)
   251  			r.Writer.Write(w, n.Title)
   252  			_ = w.WriteByte('"')
   253  		}
   254  		_ = w.WriteByte('>')
   255  	} else {
   256  		_, _ = w.WriteString("</a>")
   257  	}
   258  	return ast.WalkContinue, nil
   259  }
   260  
   261  func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   262  	if !entering {
   263  		return ast.WalkContinue, nil
   264  	}
   265  
   266  	n := node.(*ast.AutoLink)
   267  	var lr hooks.LinkRenderer
   268  
   269  	ctx, ok := w.(*render.Context)
   270  	if ok {
   271  		h := ctx.RenderContext().GetRenderer(hooks.LinkRendererType, nil)
   272  		ok = h != nil
   273  		if ok {
   274  			lr = h.(hooks.LinkRenderer)
   275  		}
   276  	}
   277  
   278  	if !ok {
   279  		return r.renderAutoLinkDefault(w, source, node, entering)
   280  	}
   281  
   282  	url := string(n.URL(source))
   283  	label := string(n.Label(source))
   284  	if n.AutoLinkType == ast.AutoLinkEmail && !strings.HasPrefix(strings.ToLower(url), "mailto:") {
   285  		url = "mailto:" + url
   286  	}
   287  
   288  	err := lr.RenderLink(
   289  		w,
   290  		linkContext{
   291  			page:        ctx.DocumentContext().Document,
   292  			destination: url,
   293  			text:        label,
   294  			plainText:   label,
   295  		},
   296  	)
   297  
   298  	// TODO(bep) I have a working branch that fixes these rather confusing identity types,
   299  	// but for now it's important that it's not .GetIdentity() that's added here,
   300  	// to make sure we search the entire chain on changes.
   301  	ctx.AddIdentity(lr)
   302  
   303  	return ast.WalkContinue, err
   304  }
   305  
   306  // Fall back to the default Goldmark render funcs. Method below borrowed from:
   307  // https://github.com/yuin/goldmark/blob/5588d92a56fe1642791cf4aa8e9eae8227cfeecd/renderer/html/html.go#L439
   308  func (r *hookedRenderer) renderAutoLinkDefault(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   309  	n := node.(*ast.AutoLink)
   310  	if !entering {
   311  		return ast.WalkContinue, nil
   312  	}
   313  	_, _ = w.WriteString(`<a href="`)
   314  	url := n.URL(source)
   315  	label := n.Label(source)
   316  	if n.AutoLinkType == ast.AutoLinkEmail && !bytes.HasPrefix(bytes.ToLower(url), []byte("mailto:")) {
   317  		_, _ = w.WriteString("mailto:")
   318  	}
   319  	_, _ = w.Write(util.EscapeHTML(util.URLEscape(url, false)))
   320  	if n.Attributes() != nil {
   321  		_ = w.WriteByte('"')
   322  		html.RenderAttributes(w, n, html.LinkAttributeFilter)
   323  		_ = w.WriteByte('>')
   324  	} else {
   325  		_, _ = w.WriteString(`">`)
   326  	}
   327  	_, _ = w.Write(util.EscapeHTML(label))
   328  	_, _ = w.WriteString(`</a>`)
   329  	return ast.WalkContinue, nil
   330  }
   331  
   332  func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   333  	n := node.(*ast.Heading)
   334  	var hr hooks.HeadingRenderer
   335  
   336  	ctx, ok := w.(*render.Context)
   337  	if ok {
   338  		h := ctx.RenderContext().GetRenderer(hooks.HeadingRendererType, nil)
   339  		ok = h != nil
   340  		if ok {
   341  			hr = h.(hooks.HeadingRenderer)
   342  		}
   343  	}
   344  
   345  	if !ok {
   346  		return r.renderHeadingDefault(w, source, node, entering)
   347  	}
   348  
   349  	if entering {
   350  		// Store the current pos so we can capture the rendered text.
   351  		ctx.PushPos(ctx.Buffer.Len())
   352  		return ast.WalkContinue, nil
   353  	}
   354  
   355  	pos := ctx.PopPos()
   356  	text := ctx.Buffer.Bytes()[pos:]
   357  	ctx.Buffer.Truncate(pos)
   358  	// All ast.Heading nodes are guaranteed to have an attribute called "id"
   359  	// that is an array of bytes that encode a valid string.
   360  	anchori, _ := n.AttributeString("id")
   361  	anchor := anchori.([]byte)
   362  
   363  	err := hr.RenderHeading(
   364  		w,
   365  		headingContext{
   366  			page:             ctx.DocumentContext().Document,
   367  			level:            n.Level,
   368  			anchor:           string(anchor),
   369  			text:             string(text),
   370  			plainText:        string(n.Text(source)),
   371  			AttributesHolder: attributes.New(n.Attributes(), attributes.AttributesOwnerGeneral),
   372  		},
   373  	)
   374  
   375  	ctx.AddIdentity(hr)
   376  
   377  	return ast.WalkContinue, err
   378  }
   379  
   380  func (r *hookedRenderer) renderHeadingDefault(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   381  	n := node.(*ast.Heading)
   382  	if entering {
   383  		_, _ = w.WriteString("<h")
   384  		_ = w.WriteByte("0123456"[n.Level])
   385  		if n.Attributes() != nil {
   386  			attributes.RenderASTAttributes(w, node.Attributes()...)
   387  		}
   388  		_ = w.WriteByte('>')
   389  	} else {
   390  		_, _ = w.WriteString("</h")
   391  		_ = w.WriteByte("0123456"[n.Level])
   392  		_, _ = w.WriteString(">\n")
   393  	}
   394  	return ast.WalkContinue, nil
   395  }
   396  
   397  type links struct{}
   398  
   399  // Extend implements goldmark.Extender.
   400  func (e *links) Extend(m goldmark.Markdown) {
   401  	m.Renderer().AddOptions(renderer.WithNodeRenderers(
   402  		util.Prioritized(newLinkRenderer(), 100),
   403  	))
   404  }