-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy pathrenderer.go
159 lines (140 loc) · 4.25 KB
/
renderer.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
package wikilink
import (
"bytes"
"fmt"
"io"
"path/filepath"
"sync"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/util"
)
// Renderer renders wikilinks as HTML.
//
// Install it on your goldmark Markdown object with Extender, or directly on a
// goldmark Renderer by using the WithNodeRenderers option.
//
// wikilinkRenderer := util.Prioritized(&wikilink.Renderer{...}, 199)
// goldmarkRenderer.AddOptions(renderer.WithNodeRenderers(wikilinkRenderer))
type Renderer struct {
// Resolver determines destinations for wikilink pages.
//
// If a Resolver returns an empty destination, the Renderer will skip
// the link and render just its contents. That is, instead of,
//
// <a href="foo">bar</a>
//
// The renderer will render just the following.
//
// bar
//
// Defaults to DefaultResolver if unspecified.
Resolver Resolver
once sync.Once // guards init
// hasDest records whether a node had a destination when we resolved
// it. This is needed to decide whether a closing </a> must be added
// when exiting a Node render.
hasDest sync.Map // *Node => struct{}
}
func (r *Renderer) init() {
r.once.Do(func() {
if r.Resolver == nil {
r.Resolver = DefaultResolver
}
})
}
// RegisterFuncs registers wikilink rendering functions with the provided
// goldmark registerer. This teaches goldmark to call us when it encounters a
// wikilink in the AST.
func (r *Renderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
reg.Register(Kind, r.Render)
}
// Render renders the provided Node. It must be a Wikilink [Node].
//
// goldmark will call this method if this renderer was registered with it
// using the WithNodeRenderers option.
//
// All nodes will be rendered as links (with <a> tags),
// except for embed links (![[..]]) that refer to images.
// Those will be rendered as images (with <img> tags).
func (r *Renderer) Render(w util.BufWriter, src []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
r.init()
n, ok := node.(*Node)
if !ok {
return ast.WalkStop, fmt.Errorf("unexpected node %T, expected *wikilink.Node", node)
}
if entering {
return r.enter(w, n, src)
}
r.exit(w, n)
return ast.WalkContinue, nil
}
func (r *Renderer) enter(w util.BufWriter, n *Node, src []byte) (ast.WalkStatus, error) {
dest, err := r.Resolver.ResolveWikilink(n)
if err != nil {
return ast.WalkStop, fmt.Errorf("resolve %q: %w", n.Target, err)
}
if len(dest) == 0 {
return ast.WalkContinue, nil
}
img := resolveAsImage(n)
if !img {
r.hasDest.Store(n, struct{}{})
_, _ = w.WriteString(`<a href="`)
_, _ = w.Write(util.URLEscape(dest, true /* resolve references */))
_, _ = w.WriteString(`">`)
return ast.WalkContinue, nil
}
_, _ = w.WriteString(`<img src="`)
_, _ = w.Write(util.URLEscape(dest, true /* resolve references */))
// The label portion of the link becomes the alt text
// only if it isn't the same as the target.
// This way, [[foo.jpg]] does not become alt="foo.jpg",
// but [[foo.jpg|bar]] does become alt="bar".
if n.ChildCount() == 1 {
label := nodeText(src, n.FirstChild())
if !bytes.Equal(label, n.Target) {
_, _ = w.WriteString(`" alt="`)
_, _ = w.Write(util.EscapeHTML(label))
}
}
_, _ = w.WriteString(`">`)
return ast.WalkSkipChildren, nil
}
func (r *Renderer) exit(w util.BufWriter, n *Node) {
if _, ok := r.hasDest.LoadAndDelete(n); ok {
_, _ = w.WriteString("</a>")
}
}
// returns true if the wikilink should be resolved to an image node
func resolveAsImage(n *Node) bool {
if !n.Embed {
return false
}
filename := string(n.Target)
switch ext := filepath.Ext(filename); ext {
// Common image file types taken from
// https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types
case ".apng", ".avif", ".gif", ".jpg", ".jpeg", ".jfif", ".pjpeg", ".pjp", ".png", ".svg", ".webp":
return true
default:
return false
}
}
func nodeText(src []byte, n ast.Node) []byte {
var buf bytes.Buffer
writeNodeText(src, &buf, n)
return buf.Bytes()
}
func writeNodeText(src []byte, dst io.Writer, n ast.Node) {
switch n := n.(type) {
case *ast.Text:
_, _ = dst.Write(n.Segment.Value(src))
case *ast.String:
_, _ = dst.Write(n.Value)
default:
for c := n.FirstChild(); c != nil; c = c.NextSibling() {
writeNodeText(src, dst, c)
}
}
}