hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

render_hooks.go (11470B)

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