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 }