github.com/anakojm/hugo-katex@v0.0.0-20231023141351-42d6f5de9c0b/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/common/types/hstring"
    21  	"github.com/gohugoio/hugo/markup/converter/hooks"
    22  	"github.com/gohugoio/hugo/markup/goldmark/goldmark_config"
    23  	"github.com/gohugoio/hugo/markup/goldmark/images"
    24  	"github.com/gohugoio/hugo/markup/goldmark/internal/render"
    25  	"github.com/gohugoio/hugo/markup/internal/attributes"
    26  
    27  	"github.com/yuin/goldmark"
    28  	"github.com/yuin/goldmark/ast"
    29  	"github.com/yuin/goldmark/renderer"
    30  	"github.com/yuin/goldmark/renderer/html"
    31  	"github.com/yuin/goldmark/util"
    32  )
    33  
    34  var _ renderer.SetOptioner = (*hookedRenderer)(nil)
    35  
    36  func newLinkRenderer(cfg goldmark_config.Config) renderer.NodeRenderer {
    37  	r := &hookedRenderer{
    38  		linkifyProtocol: []byte(cfg.Extensions.LinkifyProtocol),
    39  		Config: html.Config{
    40  			Writer: html.DefaultWriter,
    41  		},
    42  	}
    43  	return r
    44  }
    45  
    46  func newLinks(cfg goldmark_config.Config) goldmark.Extender {
    47  	return &links{cfg: cfg}
    48  }
    49  
    50  type linkContext struct {
    51  	page        any
    52  	destination string
    53  	title       string
    54  	text        hstring.RenderedString
    55  	plainText   string
    56  	*attributes.AttributesHolder
    57  }
    58  
    59  func (ctx linkContext) Destination() string {
    60  	return ctx.destination
    61  }
    62  
    63  func (ctx linkContext) Page() any {
    64  	return ctx.page
    65  }
    66  
    67  func (ctx linkContext) Text() hstring.RenderedString {
    68  	return ctx.text
    69  }
    70  
    71  func (ctx linkContext) PlainText() string {
    72  	return ctx.plainText
    73  }
    74  
    75  func (ctx linkContext) Title() string {
    76  	return ctx.title
    77  }
    78  
    79  type imageLinkContext struct {
    80  	linkContext
    81  	ordinal int
    82  	isBlock bool
    83  }
    84  
    85  func (ctx imageLinkContext) IsBlock() bool {
    86  	return ctx.isBlock
    87  }
    88  
    89  func (ctx imageLinkContext) Ordinal() int {
    90  	return ctx.ordinal
    91  }
    92  
    93  type headingContext struct {
    94  	page      any
    95  	level     int
    96  	anchor    string
    97  	text      hstring.RenderedString
    98  	plainText string
    99  	*attributes.AttributesHolder
   100  }
   101  
   102  func (ctx headingContext) Page() any {
   103  	return ctx.page
   104  }
   105  
   106  func (ctx headingContext) Level() int {
   107  	return ctx.level
   108  }
   109  
   110  func (ctx headingContext) Anchor() string {
   111  	return ctx.anchor
   112  }
   113  
   114  func (ctx headingContext) Text() hstring.RenderedString {
   115  	return ctx.text
   116  }
   117  
   118  func (ctx headingContext) PlainText() string {
   119  	return ctx.plainText
   120  }
   121  
   122  type hookedRenderer struct {
   123  	linkifyProtocol []byte
   124  	html.Config
   125  }
   126  
   127  func (r *hookedRenderer) SetOption(name renderer.OptionName, value any) {
   128  	r.Config.SetOption(name, value)
   129  }
   130  
   131  // RegisterFuncs implements NodeRenderer.RegisterFuncs.
   132  func (r *hookedRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
   133  	reg.Register(ast.KindLink, r.renderLink)
   134  	reg.Register(ast.KindAutoLink, r.renderAutoLink)
   135  	reg.Register(ast.KindImage, r.renderImage)
   136  	reg.Register(ast.KindHeading, r.renderHeading)
   137  }
   138  
   139  func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   140  	n := node.(*ast.Image)
   141  	var lr hooks.LinkRenderer
   142  
   143  	ctx, ok := w.(*render.Context)
   144  	if ok {
   145  		h := ctx.RenderContext().GetRenderer(hooks.ImageRendererType, nil)
   146  		ok = h != nil
   147  		if ok {
   148  			lr = h.(hooks.LinkRenderer)
   149  		}
   150  	}
   151  
   152  	if !ok {
   153  		return r.renderImageDefault(w, source, node, entering)
   154  	}
   155  
   156  	if entering {
   157  		// Store the current pos so we can capture the rendered text.
   158  		ctx.PushPos(ctx.Buffer.Len())
   159  		return ast.WalkContinue, nil
   160  	}
   161  
   162  	pos := ctx.PopPos()
   163  	text := ctx.Buffer.Bytes()[pos:]
   164  	ctx.Buffer.Truncate(pos)
   165  
   166  	var (
   167  		isBlock bool
   168  		ordinal int
   169  	)
   170  	if b, ok := n.AttributeString(images.AttrIsBlock); ok && b.(bool) {
   171  		isBlock = true
   172  	}
   173  	if n, ok := n.AttributeString(images.AttrOrdinal); ok {
   174  		ordinal = n.(int)
   175  	}
   176  
   177  	// We use the attributes to signal from the parser whether the image is in
   178  	// a block context or not.
   179  	// We may find a better way to do that, but for now, we'll need to remove any
   180  	// internal attributes before rendering.
   181  	attrs := r.filterInternalAttributes(n.Attributes())
   182  
   183  	err := lr.RenderLink(
   184  		ctx.RenderContext().Ctx,
   185  		w,
   186  		imageLinkContext{
   187  			linkContext: linkContext{
   188  				page:             ctx.DocumentContext().Document,
   189  				destination:      string(n.Destination),
   190  				title:            string(n.Title),
   191  				text:             hstring.RenderedString(text),
   192  				plainText:        string(n.Text(source)),
   193  				AttributesHolder: attributes.New(attrs, attributes.AttributesOwnerGeneral),
   194  			},
   195  			ordinal: ordinal,
   196  			isBlock: isBlock,
   197  		},
   198  	)
   199  
   200  	ctx.AddIdentity(lr)
   201  
   202  	return ast.WalkContinue, err
   203  }
   204  
   205  func (r *hookedRenderer) filterInternalAttributes(attrs []ast.Attribute) []ast.Attribute {
   206  	n := 0
   207  	for _, x := range attrs {
   208  		if !bytes.HasPrefix(x.Name, []byte(internalAttrPrefix)) {
   209  			attrs[n] = x
   210  			n++
   211  		}
   212  	}
   213  	return attrs[:n]
   214  }
   215  
   216  // Fall back to the default Goldmark render funcs. Method below borrowed from:
   217  // https://github.com/yuin/goldmark/blob/b611cd333a492416b56aa8d94b04a67bf0096ab2/renderer/html/html.go#L404
   218  func (r *hookedRenderer) renderImageDefault(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   219  	if !entering {
   220  		return ast.WalkContinue, nil
   221  	}
   222  	n := node.(*ast.Image)
   223  	_, _ = w.WriteString("<img src=\"")
   224  	if r.Unsafe || !html.IsDangerousURL(n.Destination) {
   225  		_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
   226  	}
   227  	_, _ = w.WriteString(`" alt="`)
   228  	_, _ = w.Write(nodeToHTMLText(n, source))
   229  	_ = w.WriteByte('"')
   230  	if n.Title != nil {
   231  		_, _ = w.WriteString(` title="`)
   232  		r.Writer.Write(w, n.Title)
   233  		_ = w.WriteByte('"')
   234  	}
   235  	if n.Attributes() != nil {
   236  		attrs := r.filterInternalAttributes(n.Attributes())
   237  		attributes.RenderASTAttributes(w, attrs...)
   238  	}
   239  	if r.XHTML {
   240  		_, _ = w.WriteString(" />")
   241  	} else {
   242  		_, _ = w.WriteString(">")
   243  	}
   244  	return ast.WalkSkipChildren, nil
   245  }
   246  
   247  func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   248  	n := node.(*ast.Link)
   249  	var lr hooks.LinkRenderer
   250  
   251  	ctx, ok := w.(*render.Context)
   252  	if ok {
   253  		h := ctx.RenderContext().GetRenderer(hooks.LinkRendererType, nil)
   254  		ok = h != nil
   255  		if ok {
   256  			lr = h.(hooks.LinkRenderer)
   257  		}
   258  	}
   259  
   260  	if !ok {
   261  		return r.renderLinkDefault(w, source, node, entering)
   262  	}
   263  
   264  	if entering {
   265  		// Store the current pos so we can capture the rendered text.
   266  		ctx.PushPos(ctx.Buffer.Len())
   267  		return ast.WalkContinue, nil
   268  	}
   269  
   270  	pos := ctx.PopPos()
   271  	text := ctx.Buffer.Bytes()[pos:]
   272  	ctx.Buffer.Truncate(pos)
   273  
   274  	err := lr.RenderLink(
   275  		ctx.RenderContext().Ctx,
   276  		w,
   277  		linkContext{
   278  			page:             ctx.DocumentContext().Document,
   279  			destination:      string(n.Destination),
   280  			title:            string(n.Title),
   281  			text:             hstring.RenderedString(text),
   282  			plainText:        string(n.Text(source)),
   283  			AttributesHolder: attributes.Empty,
   284  		},
   285  	)
   286  
   287  	// TODO(bep) I have a working branch that fixes these rather confusing identity types,
   288  	// but for now it's important that it's not .GetIdentity() that's added here,
   289  	// to make sure we search the entire chain on changes.
   290  	ctx.AddIdentity(lr)
   291  
   292  	return ast.WalkContinue, err
   293  }
   294  
   295  // Fall back to the default Goldmark render funcs. Method below borrowed from:
   296  // https://github.com/yuin/goldmark/blob/b611cd333a492416b56aa8d94b04a67bf0096ab2/renderer/html/html.go#L404
   297  func (r *hookedRenderer) renderLinkDefault(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   298  	n := node.(*ast.Link)
   299  	if entering {
   300  		_, _ = w.WriteString("<a href=\"")
   301  		if r.Unsafe || !html.IsDangerousURL(n.Destination) {
   302  			_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
   303  		}
   304  		_ = w.WriteByte('"')
   305  		if n.Title != nil {
   306  			_, _ = w.WriteString(` title="`)
   307  			r.Writer.Write(w, n.Title)
   308  			_ = w.WriteByte('"')
   309  		}
   310  		_ = w.WriteByte('>')
   311  	} else {
   312  		_, _ = w.WriteString("</a>")
   313  	}
   314  	return ast.WalkContinue, nil
   315  }
   316  
   317  func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   318  	if !entering {
   319  		return ast.WalkContinue, nil
   320  	}
   321  
   322  	n := node.(*ast.AutoLink)
   323  	var lr hooks.LinkRenderer
   324  
   325  	ctx, ok := w.(*render.Context)
   326  	if ok {
   327  		h := ctx.RenderContext().GetRenderer(hooks.LinkRendererType, nil)
   328  		ok = h != nil
   329  		if ok {
   330  			lr = h.(hooks.LinkRenderer)
   331  		}
   332  	}
   333  
   334  	if !ok {
   335  		return r.renderAutoLinkDefault(w, source, node, entering)
   336  	}
   337  
   338  	url := string(r.autoLinkURL(n, source))
   339  	label := string(n.Label(source))
   340  	if n.AutoLinkType == ast.AutoLinkEmail && !strings.HasPrefix(strings.ToLower(url), "mailto:") {
   341  		url = "mailto:" + url
   342  	}
   343  
   344  	err := lr.RenderLink(
   345  		ctx.RenderContext().Ctx,
   346  		w,
   347  		linkContext{
   348  			page:             ctx.DocumentContext().Document,
   349  			destination:      url,
   350  			text:             hstring.RenderedString(label),
   351  			plainText:        label,
   352  			AttributesHolder: attributes.Empty,
   353  		},
   354  	)
   355  
   356  	// TODO(bep) I have a working branch that fixes these rather confusing identity types,
   357  	// but for now it's important that it's not .GetIdentity() that's added here,
   358  	// to make sure we search the entire chain on changes.
   359  	ctx.AddIdentity(lr)
   360  
   361  	return ast.WalkContinue, err
   362  }
   363  
   364  // Fall back to the default Goldmark render funcs. Method below borrowed from:
   365  // https://github.com/yuin/goldmark/blob/5588d92a56fe1642791cf4aa8e9eae8227cfeecd/renderer/html/html.go#L439
   366  func (r *hookedRenderer) renderAutoLinkDefault(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   367  	n := node.(*ast.AutoLink)
   368  	if !entering {
   369  		return ast.WalkContinue, nil
   370  	}
   371  
   372  	_, _ = w.WriteString(`<a href="`)
   373  	url := r.autoLinkURL(n, source)
   374  	label := n.Label(source)
   375  	if n.AutoLinkType == ast.AutoLinkEmail && !bytes.HasPrefix(bytes.ToLower(url), []byte("mailto:")) {
   376  		_, _ = w.WriteString("mailto:")
   377  	}
   378  	_, _ = w.Write(util.EscapeHTML(util.URLEscape(url, false)))
   379  	if n.Attributes() != nil {
   380  		_ = w.WriteByte('"')
   381  		html.RenderAttributes(w, n, html.LinkAttributeFilter)
   382  		_ = w.WriteByte('>')
   383  	} else {
   384  		_, _ = w.WriteString(`">`)
   385  	}
   386  	_, _ = w.Write(util.EscapeHTML(label))
   387  	_, _ = w.WriteString(`</a>`)
   388  	return ast.WalkContinue, nil
   389  }
   390  
   391  func (r *hookedRenderer) autoLinkURL(n *ast.AutoLink, source []byte) []byte {
   392  	url := n.URL(source)
   393  	if len(n.Protocol) > 0 && !bytes.Equal(n.Protocol, r.linkifyProtocol) {
   394  		// The CommonMark spec says "http" is the correct protocol for links,
   395  		// but this doesn't make much sense (the fact that they should care about the rendered output).
   396  		// Note that n.Protocol is not set if protocol is provided by user.
   397  		url = append(r.linkifyProtocol, url[len(n.Protocol):]...)
   398  	}
   399  	return url
   400  }
   401  
   402  func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   403  	n := node.(*ast.Heading)
   404  	var hr hooks.HeadingRenderer
   405  
   406  	ctx, ok := w.(*render.Context)
   407  	if ok {
   408  		h := ctx.RenderContext().GetRenderer(hooks.HeadingRendererType, nil)
   409  		ok = h != nil
   410  		if ok {
   411  			hr = h.(hooks.HeadingRenderer)
   412  		}
   413  	}
   414  
   415  	if !ok {
   416  		return r.renderHeadingDefault(w, source, node, entering)
   417  	}
   418  
   419  	if entering {
   420  		// Store the current pos so we can capture the rendered text.
   421  		ctx.PushPos(ctx.Buffer.Len())
   422  		return ast.WalkContinue, nil
   423  	}
   424  
   425  	pos := ctx.PopPos()
   426  	text := ctx.Buffer.Bytes()[pos:]
   427  	ctx.Buffer.Truncate(pos)
   428  	// All ast.Heading nodes are guaranteed to have an attribute called "id"
   429  	// that is an array of bytes that encode a valid string.
   430  	anchori, _ := n.AttributeString("id")
   431  	anchor := anchori.([]byte)
   432  
   433  	err := hr.RenderHeading(
   434  		ctx.RenderContext().Ctx,
   435  		w,
   436  		headingContext{
   437  			page:             ctx.DocumentContext().Document,
   438  			level:            n.Level,
   439  			anchor:           string(anchor),
   440  			text:             hstring.RenderedString(text),
   441  			plainText:        string(n.Text(source)),
   442  			AttributesHolder: attributes.New(n.Attributes(), attributes.AttributesOwnerGeneral),
   443  		},
   444  	)
   445  
   446  	ctx.AddIdentity(hr)
   447  
   448  	return ast.WalkContinue, err
   449  }
   450  
   451  func (r *hookedRenderer) renderHeadingDefault(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
   452  	n := node.(*ast.Heading)
   453  	if entering {
   454  		_, _ = w.WriteString("<h")
   455  		_ = w.WriteByte("0123456"[n.Level])
   456  		if n.Attributes() != nil {
   457  			attributes.RenderASTAttributes(w, node.Attributes()...)
   458  		}
   459  		_ = w.WriteByte('>')
   460  	} else {
   461  		_, _ = w.WriteString("</h")
   462  		_ = w.WriteByte("0123456"[n.Level])
   463  		_, _ = w.WriteString(">\n")
   464  	}
   465  	return ast.WalkContinue, nil
   466  }
   467  
   468  type links struct {
   469  	cfg goldmark_config.Config
   470  }
   471  
   472  // Extend implements goldmark.Extender.
   473  func (e *links) Extend(m goldmark.Markdown) {
   474  	m.Renderer().AddOptions(renderer.WithNodeRenderers(
   475  		util.Prioritized(newLinkRenderer(e.cfg), 100),
   476  	))
   477  }
   478  
   479  // Borrowed from Goldmark.
   480  func nodeToHTMLText(n ast.Node, source []byte) []byte {
   481  	var buf bytes.Buffer
   482  	for c := n.FirstChild(); c != nil; c = c.NextSibling() {
   483  		if s, ok := c.(*ast.String); ok && s.IsCode() {
   484  			buf.Write(s.Text(source))
   485  		} else if !c.HasChildren() {
   486  			buf.Write(util.EscapeHTML(c.Text(source)))
   487  		} else {
   488  			buf.Write(nodeToHTMLText(c, source))
   489  		}
   490  	}
   491  	return buf.Bytes()
   492  }