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