render_hooks.go - hugo - [fork] hugo port for 9front
(HTM) git clone https://git.drkhsh.at/hugo.git
(DIR) Log
(DIR) Files
(DIR) Refs
(DIR) Submodules
(DIR) README
(DIR) LICENSE
---
render_hooks.go (14689B)
---
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 pageInner any
53 destination string
54 title string
55 text hstring.HTML
56 plainText string
57 *attributes.AttributesHolder
58 }
59
60 func (ctx linkContext) Destination() string {
61 return ctx.destination
62 }
63
64 func (ctx linkContext) Page() any {
65 return ctx.page
66 }
67
68 func (ctx linkContext) PageInner() any {
69 return ctx.pageInner
70 }
71
72 func (ctx linkContext) Text() hstring.HTML {
73 return ctx.text
74 }
75
76 func (ctx linkContext) PlainText() string {
77 return ctx.plainText
78 }
79
80 func (ctx linkContext) Title() string {
81 return ctx.title
82 }
83
84 type imageLinkContext struct {
85 linkContext
86 ordinal int
87 isBlock bool
88 }
89
90 func (ctx imageLinkContext) IsBlock() bool {
91 return ctx.isBlock
92 }
93
94 func (ctx imageLinkContext) Ordinal() int {
95 return ctx.ordinal
96 }
97
98 type headingContext struct {
99 page any
100 pageInner any
101 level int
102 anchor string
103 text hstring.HTML
104 plainText string
105 *attributes.AttributesHolder
106 }
107
108 func (ctx headingContext) Page() any {
109 return ctx.page
110 }
111
112 func (ctx headingContext) PageInner() any {
113 return ctx.pageInner
114 }
115
116 func (ctx headingContext) Level() int {
117 return ctx.level
118 }
119
120 func (ctx headingContext) Anchor() string {
121 return ctx.anchor
122 }
123
124 func (ctx headingContext) Text() hstring.HTML {
125 return ctx.text
126 }
127
128 func (ctx headingContext) PlainText() string {
129 return ctx.plainText
130 }
131
132 type hookedRenderer struct {
133 linkifyProtocol []byte
134 html.Config
135 }
136
137 func (r *hookedRenderer) SetOption(name renderer.OptionName, value any) {
138 r.Config.SetOption(name, value)
139 }
140
141 // RegisterFuncs implements NodeRenderer.RegisterFuncs.
142 func (r *hookedRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
143 reg.Register(ast.KindLink, r.renderLink)
144 reg.Register(ast.KindAutoLink, r.renderAutoLink)
145 reg.Register(ast.KindImage, r.renderImage)
146 reg.Register(ast.KindHeading, r.renderHeading)
147 }
148
149 func (r *hookedRenderer) renderImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
150 n := node.(*ast.Image)
151 var lr hooks.LinkRenderer
152
153 ctx, ok := w.(*render.Context)
154 if ok {
155 h := ctx.RenderContext().GetRenderer(hooks.ImageRendererType, nil)
156 ok = h != nil
157 if ok {
158 lr = h.(hooks.LinkRenderer)
159 }
160 }
161
162 if !ok {
163 return r.renderImageDefault(w, source, node, entering)
164 }
165
166 if entering {
167 // Store the current pos so we can capture the rendered text.
168 ctx.PushPos(ctx.Buffer.Len())
169 return ast.WalkContinue, nil
170 }
171
172 text := ctx.PopRenderedString()
173
174 var (
175 isBlock bool
176 ordinal int
177 )
178 if b, ok := n.AttributeString(images.AttrIsBlock); ok && b.(bool) {
179 isBlock = true
180 }
181 if n, ok := n.AttributeString(images.AttrOrdinal); ok {
182 ordinal = n.(int)
183 }
184
185 // We use the attributes to signal from the parser whether the image is in
186 // a block context or not.
187 // We may find a better way to do that, but for now, we'll need to remove any
188 // internal attributes before rendering.
189 attrs := r.filterInternalAttributes(n.Attributes())
190
191 page, pageInner := render.GetPageAndPageInner(ctx)
192
193 err := lr.RenderLink(
194 ctx.RenderContext().Ctx,
195 w,
196 imageLinkContext{
197 linkContext: linkContext{
198 page: page,
199 pageInner: pageInner,
200 destination: string(n.Destination),
201 title: string(n.Title),
202 text: hstring.HTML(text),
203 plainText: render.TextPlain(n, source),
204 AttributesHolder: attributes.New(attrs, attributes.AttributesOwnerGeneral),
205 },
206 ordinal: ordinal,
207 isBlock: isBlock,
208 },
209 )
210
211 return ast.WalkContinue, err
212 }
213
214 func (r *hookedRenderer) filterInternalAttributes(attrs []ast.Attribute) []ast.Attribute {
215 n := 0
216 for _, x := range attrs {
217 if !bytes.HasPrefix(x.Name, []byte(internalAttrPrefix)) {
218 attrs[n] = x
219 n++
220 }
221 }
222 return attrs[:n]
223 }
224
225 // Fall back to the default Goldmark render funcs. Method below borrowed from:
226 // https://github.com/yuin/goldmark
227 func (r *hookedRenderer) renderImageDefault(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
228 if !entering {
229 return ast.WalkContinue, nil
230 }
231 n := node.(*ast.Image)
232 _, _ = w.WriteString("<img src=\"")
233 if r.Unsafe || !html.IsDangerousURL(n.Destination) {
234 _, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
235 }
236 _, _ = w.WriteString(`" alt="`)
237 r.renderTexts(w, source, n)
238 _ = w.WriteByte('"')
239 if n.Title != nil {
240 _, _ = w.WriteString(` title="`)
241 r.Writer.Write(w, n.Title)
242 _ = w.WriteByte('"')
243 }
244 if n.Attributes() != nil {
245 html.RenderAttributes(w, n, html.ImageAttributeFilter)
246 }
247 if r.XHTML {
248 _, _ = w.WriteString(" />")
249 } else {
250 _, _ = w.WriteString(">")
251 }
252 return ast.WalkSkipChildren, nil
253 }
254
255 func (r *hookedRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
256 n := node.(*ast.Link)
257 var lr hooks.LinkRenderer
258
259 ctx, ok := w.(*render.Context)
260 if ok {
261 h := ctx.RenderContext().GetRenderer(hooks.LinkRendererType, nil)
262 ok = h != nil
263 if ok {
264 lr = h.(hooks.LinkRenderer)
265 }
266 }
267
268 if !ok {
269 return r.renderLinkDefault(w, source, node, entering)
270 }
271
272 if entering {
273 // Store the current pos so we can capture the rendered text.
274 ctx.PushPos(ctx.Buffer.Len())
275 return ast.WalkContinue, nil
276 }
277
278 text := ctx.PopRenderedString()
279
280 page, pageInner := render.GetPageAndPageInner(ctx)
281
282 err := lr.RenderLink(
283 ctx.RenderContext().Ctx,
284 w,
285 linkContext{
286 page: page,
287 pageInner: pageInner,
288 destination: string(n.Destination),
289 title: string(n.Title),
290 text: hstring.HTML(text),
291 plainText: render.TextPlain(n, source),
292 AttributesHolder: attributes.Empty,
293 },
294 )
295
296 return ast.WalkContinue, err
297 }
298
299 // Borrowed from Goldmark's HTML renderer.
300 func (r *hookedRenderer) renderTexts(w util.BufWriter, source []byte, n ast.Node) {
301 for c := n.FirstChild(); c != nil; c = c.NextSibling() {
302 if s, ok := c.(*ast.String); ok {
303 _, _ = r.renderString(w, source, s, true)
304 } else if t, ok := c.(*ast.Text); ok {
305 _, _ = r.renderText(w, source, t, true)
306 } else {
307 r.renderTexts(w, source, c)
308 }
309 }
310 }
311
312 // Borrowed from Goldmark's HTML renderer.
313 func (r *hookedRenderer) renderString(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
314 if !entering {
315 return ast.WalkContinue, nil
316 }
317 n := node.(*ast.String)
318 if n.IsCode() {
319 _, _ = w.Write(n.Value)
320 } else {
321 if n.IsRaw() {
322 r.Writer.RawWrite(w, n.Value)
323 } else {
324 r.Writer.Write(w, n.Value)
325 }
326 }
327 return ast.WalkContinue, nil
328 }
329
330 // Borrowed from Goldmark's HTML renderer.
331 func (r *hookedRenderer) renderText(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
332 if !entering {
333 return ast.WalkContinue, nil
334 }
335 n := node.(*ast.Text)
336 segment := n.Segment
337 if n.IsRaw() {
338 r.Writer.RawWrite(w, segment.Value(source))
339 } else {
340 value := segment.Value(source)
341 r.Writer.Write(w, value)
342 if n.HardLineBreak() || (n.SoftLineBreak() && r.HardWraps) {
343 if r.XHTML {
344 _, _ = w.WriteString("<br />\n")
345 } else {
346 _, _ = w.WriteString("<br>\n")
347 }
348 } else if n.SoftLineBreak() {
349 // TODO(bep) we use these methods a fallback to default rendering when no image/link hooks are defined.
350 // I don't think the below is relevant in these situations, but if so, we need to create a PR
351 // upstream to export softLineBreak.
352 /*if r.EastAsianLineBreaks != html.EastAsianLineBreaksNone && len(value) != 0 {
353 sibling := node.NextSibling()
354 if sibling != nil && sibling.Kind() == ast.KindText {
355 if siblingText := sibling.(*ast.Text).Value(source); len(siblingText) != 0 {
356 thisLastRune := util.ToRune(value, len(value)-1)
357 siblingFirstRune, _ := utf8.DecodeRune(siblingText)
358 if r.EastAsianLineBreaks.softLineBreak(thisLastRune, siblingFirstRune) {
359 _ = w.WriteByte('\n')
360 }
361 }
362 }
363 } else {
364 _ = w.WriteByte('\n')
365 }*/
366 _ = w.WriteByte('\n')
367 }
368 }
369 return ast.WalkContinue, nil
370 }
371
372 // Fall back to the default Goldmark render funcs. Method below borrowed from:
373 // https://github.com/yuin/goldmark/blob/b611cd333a492416b56aa8d94b04a67bf0096ab2/renderer/html/html.go#L404
374 func (r *hookedRenderer) renderLinkDefault(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
375 n := node.(*ast.Link)
376 if entering {
377 _, _ = w.WriteString("<a href=\"")
378 if r.Unsafe || !html.IsDangerousURL(n.Destination) {
379 _, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
380 }
381 _ = w.WriteByte('"')
382 if n.Title != nil {
383 _, _ = w.WriteString(` title="`)
384 r.Writer.Write(w, n.Title)
385 _ = w.WriteByte('"')
386 }
387 _ = w.WriteByte('>')
388 } else {
389 _, _ = w.WriteString("</a>")
390 }
391 return ast.WalkContinue, nil
392 }
393
394 func (r *hookedRenderer) renderAutoLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
395 if !entering {
396 return ast.WalkContinue, nil
397 }
398
399 n := node.(*ast.AutoLink)
400 var lr hooks.LinkRenderer
401
402 ctx, ok := w.(*render.Context)
403 if ok {
404 h := ctx.RenderContext().GetRenderer(hooks.LinkRendererType, nil)
405 ok = h != nil
406 if ok {
407 lr = h.(hooks.LinkRenderer)
408 }
409 }
410
411 if !ok {
412 return r.renderAutoLinkDefault(w, source, node, entering)
413 }
414
415 url := string(r.autoLinkURL(n, source))
416 label := string(n.Label(source))
417 if n.AutoLinkType == ast.AutoLinkEmail && !strings.HasPrefix(strings.ToLower(url), "mailto:") {
418 url = "mailto:" + url
419 }
420
421 page, pageInner := render.GetPageAndPageInner(ctx)
422
423 err := lr.RenderLink(
424 ctx.RenderContext().Ctx,
425 w,
426 linkContext{
427 page: page,
428 pageInner: pageInner,
429 destination: url,
430 text: hstring.HTML(label),
431 plainText: label,
432 AttributesHolder: attributes.Empty,
433 },
434 )
435
436 return ast.WalkContinue, err
437 }
438
439 // Fall back to the default Goldmark render funcs. Method below borrowed from:
440 // https://github.com/yuin/goldmark/blob/5588d92a56fe1642791cf4aa8e9eae8227cfeecd/renderer/html/html.go#L439
441 func (r *hookedRenderer) renderAutoLinkDefault(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
442 n := node.(*ast.AutoLink)
443 if !entering {
444 return ast.WalkContinue, nil
445 }
446
447 _, _ = w.WriteString(`<a href="`)
448 url := r.autoLinkURL(n, source)
449 label := n.Label(source)
450 if n.AutoLinkType == ast.AutoLinkEmail && !bytes.HasPrefix(bytes.ToLower(url), []byte("mailto:")) {
451 _, _ = w.WriteString("mailto:")
452 }
453 _, _ = w.Write(util.EscapeHTML(util.URLEscape(url, false)))
454 if n.Attributes() != nil {
455 _ = w.WriteByte('"')
456 html.RenderAttributes(w, n, html.LinkAttributeFilter)
457 _ = w.WriteByte('>')
458 } else {
459 _, _ = w.WriteString(`">`)
460 }
461 _, _ = w.Write(util.EscapeHTML(label))
462 _, _ = w.WriteString(`</a>`)
463 return ast.WalkContinue, nil
464 }
465
466 func (r *hookedRenderer) autoLinkURL(n *ast.AutoLink, source []byte) []byte {
467 url := n.URL(source)
468 if len(n.Protocol) > 0 && !bytes.Equal(n.Protocol, r.linkifyProtocol) {
469 // The CommonMark spec says "http" is the correct protocol for links,
470 // but this doesn't make much sense (the fact that they should care about the rendered output).
471 // Note that n.Protocol is not set if protocol is provided by user.
472 url = append(r.linkifyProtocol, url[len(n.Protocol):]...)
473 }
474 return url
475 }
476
477 func (r *hookedRenderer) renderHeading(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
478 n := node.(*ast.Heading)
479 var hr hooks.HeadingRenderer
480
481 ctx, ok := w.(*render.Context)
482 if ok {
483 h := ctx.RenderContext().GetRenderer(hooks.HeadingRendererType, nil)
484 ok = h != nil
485 if ok {
486 hr = h.(hooks.HeadingRenderer)
487 }
488 }
489
490 if !ok {
491 return r.renderHeadingDefault(w, source, node, entering)
492 }
493
494 if entering {
495 // Store the current pos so we can capture the rendered text.
496 ctx.PushPos(ctx.Buffer.Len())
497 return ast.WalkContinue, nil
498 }
499
500 text := ctx.PopRenderedString()
501
502 var anchor []byte
503 if anchori, ok := n.AttributeString("id"); ok {
504 anchor, _ = anchori.([]byte)
505 }
506
507 page, pageInner := render.GetPageAndPageInner(ctx)
508
509 err := hr.RenderHeading(
510 ctx.RenderContext().Ctx,
511 w,
512 headingContext{
513 page: page,
514 pageInner: pageInner,
515 level: n.Level,
516 anchor: string(anchor),
517 text: hstring.HTML(text),
518 plainText: render.TextPlain(n, source),
519 AttributesHolder: attributes.New(n.Attributes(), attributes.AttributesOwnerGeneral),
520 },
521 )
522
523 return ast.WalkContinue, err
524 }
525
526 func (r *hookedRenderer) renderHeadingDefault(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
527 n := node.(*ast.Heading)
528 if entering {
529 _, _ = w.WriteString("<h")
530 _ = w.WriteByte("0123456"[n.Level])
531 if n.Attributes() != nil {
532 attributes.RenderASTAttributes(w, node.Attributes()...)
533 }
534 _ = w.WriteByte('>')
535 } else {
536 _, _ = w.WriteString("</h")
537 _ = w.WriteByte("0123456"[n.Level])
538 _, _ = w.WriteString(">\n")
539 }
540 return ast.WalkContinue, nil
541 }
542
543 type links struct {
544 cfg goldmark_config.Config
545 }
546
547 // Extend implements goldmark.Extender.
548 func (e *links) Extend(m goldmark.Markdown) {
549 m.Renderer().AddOptions(renderer.WithNodeRenderers(
550 util.Prioritized(newLinkRenderer(e.cfg), 100),
551 ))
552 }