render.go

v1.3.6
Doc Versions Source
1
package dochtml
2
3
import (
4
	"embed"
5
	"html/template"
6
	"io"
7
	"io/fs"
8
	"net/http"
9
	"sort"
10
	"strings"
11
12
	"go.bigb.es/curator/internal/godoc"
13
)
14
15
//go:embed templates/* static/*
16
var content embed.FS
17
18
// StaticFS returns the embedded static assets filesystem.
19
func StaticFS() http.FileSystem {
20
	sub, _ := fs.Sub(content, "static")
21
	return http.FS(sub)
22
}
23
24
// IndexEntry represents an entry in the package index.
25
type IndexEntry struct {
26
	Anchor   string
27
	Text     string
28
	Children []IndexEntry // constructors and methods (for types)
29
}
30
31
// ModulePageData is the template data for module/package pages.
32
type ModulePageData struct {
33
	Host         string
34
	ImportPrefix string
35
	ModuleName   string
36
	VCS          string
37
	Repo         string
38
	Version      string
39
	SubPath      string
40
	Versions     []string
41
42
	// Package doc fields.
43
	Doc         template.HTML
44
	Synopsis    string
45
	PackageName string
46
	Index       []IndexEntry
47
	Consts      []ValueData
48
	Vars        []ValueData
49
	Funcs       []FuncData
50
	Types       []TypeData
51
	SubPackages []godoc.SubPkgSummary
52
	SourceFiles []string
53
}
54
55
// ValueData wraps godoc.ValueDoc for templates.
56
type ValueData struct {
57
	Names []string
58
	Doc   template.HTML
59
	Decl  template.HTML
60
}
61
62
// FuncData wraps godoc.FuncDoc for templates.
63
type FuncData struct {
64
	Name       string
65
	Doc        template.HTML
66
	Decl       template.HTML
67
	Recv       string
68
	SourceFile string
69
	SourceLine int
70
}
71
72
// TypeData wraps godoc.TypeDoc for templates.
73
type TypeData struct {
74
	Name       string
75
	Doc        template.HTML
76
	Decl       template.HTML
77
	Consts     []ValueData
78
	Vars       []ValueData
79
	Funcs      []FuncData
80
	Methods    []FuncData
81
	SourceFile string
82
	SourceLine int
83
}
84
85
// SourcePageData is the template data for source file view.
86
type SourcePageData struct {
87
	Host              string
88
	ImportPrefix      string
89
	ModuleName        string
90
	VCS               string
91
	Repo              string
92
	Version           string
93
	SubPath           string
94
	FileName          string
95
	HighlightedSource template.HTML
96
	SourceFiles       []string
97
}
98
99
// VersionsPageData is the template data for the versions list.
100
type VersionsPageData struct {
101
	Host           string
102
	ImportPrefix   string
103
	ModuleName     string
104
	VCS            string
105
	Repo           string
106
	Versions       []string
107
	CurrentVersion string
108
}
109
110
// Renderer handles HTML rendering of documentation pages.
111
type Renderer struct {
112
	moduleTmpl   *template.Template
113
	sourceTmpl   *template.Template
114
	versionsTmpl *template.Template
115
}
116
117
// NewRenderer creates a renderer with parsed templates.
118
func NewRenderer() (*Renderer, error) {
119
	funcs := template.FuncMap{
120
		"hasPrefix": strings.HasPrefix,
121
	}
122
	base, err := template.New("base").Funcs(funcs).Parse(mustRead("templates/base.html"))
123
	if err != nil {
124
		return nil, err
125
	}
126
127
	moduleTmpl, err := template.Must(base.Clone()).Parse(mustRead("templates/module.html"))
128
	if err != nil {
129
		return nil, err
130
	}
131
132
	sourceTmpl, err := template.Must(base.Clone()).Parse(mustRead("templates/source.html"))
133
	if err != nil {
134
		return nil, err
135
	}
136
137
	versionsTmpl, err := template.Must(base.Clone()).Parse(mustRead("templates/versions.html"))
138
	if err != nil {
139
		return nil, err
140
	}
141
142
	return &Renderer{
143
		moduleTmpl:   moduleTmpl,
144
		sourceTmpl:   sourceTmpl,
145
		versionsTmpl: versionsTmpl,
146
	}, nil
147
}
148
149
// RenderModule renders a module/package documentation page.
150
func (r *Renderer) RenderModule(w io.Writer, data *ModulePageData) error {
151
	return r.moduleTmpl.Execute(w, data)
152
}
153
154
// RenderSource renders a source code view page.
155
func (r *Renderer) RenderSource(w io.Writer, data *SourcePageData) error {
156
	return r.sourceTmpl.Execute(w, data)
157
}
158
159
// RenderVersions renders a version list page.
160
func (r *Renderer) RenderVersions(w io.Writer, data *VersionsPageData) error {
161
	return r.versionsTmpl.Execute(w, data)
162
}
163
164
// BuildModulePageData creates template data from a PackageDoc.
165
func BuildModulePageData(host, modName, version, subpath, vcs, repo string, doc *godoc.PackageDoc, versions []string, subPkgs []godoc.SubPkgSummary) *ModulePageData {
166
	data := &ModulePageData{
167
		Host:         host,
168
		ImportPrefix: host + "/" + modName,
169
		ModuleName:   modName,
170
		VCS:          vcs,
171
		Repo:         repo,
172
		Version:      version,
173
		SubPath:      subpath,
174
		Versions:     versions,
175
		SubPackages:  subPkgs,
176
	}
177
178
	if doc != nil {
179
		data.Doc = template.HTML(doc.Doc)
180
		data.Synopsis = doc.Synopsis
181
		data.PackageName = doc.Name
182
		data.SourceFiles = doc.Files
183
184
		// Build symbol map for cross-referencing in declarations.
185
		symbols := make(map[string]string)
186
		for _, t := range doc.Types {
187
			symbols[t.Name] = "#" + t.Name
188
			for _, c := range t.Consts {
189
				for _, name := range c.Names {
190
					symbols[name] = "#" + name
191
				}
192
			}
193
			for _, v := range t.Vars {
194
				for _, name := range v.Names {
195
					symbols[name] = "#" + name
196
				}
197
			}
198
			for _, f := range t.Funcs {
199
				symbols[f.Name] = "#" + f.Name
200
			}
201
		}
202
		for _, f := range doc.Funcs {
203
			symbols[f.Name] = "#" + f.Name
204
		}
205
		for _, c := range doc.Consts {
206
			for _, name := range c.Names {
207
				symbols[name] = "#" + name
208
			}
209
		}
210
		for _, v := range doc.Vars {
211
			for _, name := range v.Names {
212
				symbols[name] = "#" + name
213
			}
214
		}
215
216
		// Build hierarchical index: funcs sorted, then types sorted
217
		// with constructors and methods as children.
218
		var funcIndex []IndexEntry
219
		for _, f := range doc.Funcs {
220
			funcIndex = append(funcIndex, IndexEntry{
221
				Anchor: f.Name,
222
				Text:   f.ShortDecl,
223
			})
224
		}
225
		sort.Slice(funcIndex, func(i, j int) bool {
226
			return funcIndex[i].Text < funcIndex[j].Text
227
		})
228
229
		var typeIndex []IndexEntry
230
		for _, t := range doc.Types {
231
			entry := IndexEntry{
232
				Anchor: t.Name,
233
				Text:   "type " + t.Name,
234
			}
235
236
			// Constructors (sorted).
237
			var constructors []IndexEntry
238
			for _, f := range t.Funcs {
239
				constructors = append(constructors, IndexEntry{
240
					Anchor: f.Name,
241
					Text:   f.ShortDecl,
242
				})
243
			}
244
			sort.Slice(constructors, func(i, j int) bool {
245
				return constructors[i].Text < constructors[j].Text
246
			})
247
			entry.Children = append(entry.Children, constructors...)
248
249
			// Type-associated constants (sorted).
250
			var consts []IndexEntry
251
			for _, c := range t.Consts {
252
				for _, name := range c.Names {
253
					consts = append(consts, IndexEntry{
254
						Anchor: name,
255
						Text:   "const " + name,
256
					})
257
				}
258
			}
259
			for _, v := range t.Vars {
260
				for _, name := range v.Names {
261
					consts = append(consts, IndexEntry{
262
						Anchor: name,
263
						Text:   "var " + name,
264
					})
265
				}
266
			}
267
			sort.Slice(consts, func(i, j int) bool {
268
				return consts[i].Text < consts[j].Text
269
			})
270
			entry.Children = append(entry.Children, consts...)
271
272
			// Methods (sorted).
273
			var methods []IndexEntry
274
			for _, m := range t.Methods {
275
				methods = append(methods, IndexEntry{
276
					Anchor: t.Name + "." + m.Name,
277
					Text:   m.ShortDecl,
278
				})
279
			}
280
			sort.Slice(methods, func(i, j int) bool {
281
				return methods[i].Text < methods[j].Text
282
			})
283
			entry.Children = append(entry.Children, methods...)
284
			typeIndex = append(typeIndex, entry)
285
		}
286
		sort.Slice(typeIndex, func(i, j int) bool {
287
			return typeIndex[i].Text < typeIndex[j].Text
288
		})
289
290
		data.Index = append(funcIndex, typeIndex...)
291
292
		// Build import link map: alias → base doc URL.
293
		// Local imports (under our host) link to our own docs.
294
		// External imports link to pkg.go.dev.
295
		importLinks := make(map[string]string, len(doc.Imports))
296
		for alias, path := range doc.Imports {
297
			if strings.HasPrefix(path, host+"/") {
298
				// Local module: link to our doc page.
299
				// e.g., "go.bigb.es/curator/internal/config" → "/curator/internal/config" (TODO: version?)
300
				importLinks[alias] = "/" + strings.TrimPrefix(path, host+"/")
301
			} else {
302
				importLinks[alias] = "https://pkg.go.dev/" + path
303
			}
304
		}
305
306
		// Convert godoc types to template types.
307
		imports := importLinks
308
		for _, c := range doc.Consts {
309
			data.Consts = append(data.Consts, convertValue(c, symbols, imports))
310
		}
311
		for _, v := range doc.Vars {
312
			data.Vars = append(data.Vars, convertValue(v, symbols, imports))
313
		}
314
		for _, f := range doc.Funcs {
315
			data.Funcs = append(data.Funcs, convertFunc(f, symbols, imports))
316
		}
317
		for _, t := range doc.Types {
318
			data.Types = append(data.Types, convertType(t, symbols, imports))
319
		}
320
	}
321
322
	return data
323
}
324
325
func hlDecl(s string, symbols map[string]string, imports map[string]string) template.HTML {
326
	return template.HTML(godoc.HighlightDeclLinked(s, symbols, imports))
327
}
328
329
func convertValue(v godoc.ValueDoc, symbols, imports map[string]string) ValueData {
330
	return ValueData{
331
		Names: v.Names,
332
		Doc:   template.HTML(v.Doc),
333
		Decl:  hlDecl(v.Decl, symbols, imports),
334
	}
335
}
336
337
func convertFunc(f godoc.FuncDoc, symbols, imports map[string]string) FuncData {
338
	return FuncData{
339
		Name:       f.Name,
340
		Doc:        template.HTML(f.Doc),
341
		Decl:       hlDecl(f.Decl, symbols, imports),
342
		Recv:       f.Recv,
343
		SourceFile: f.SourceFile,
344
		SourceLine: f.SourceLine,
345
	}
346
}
347
348
func convertType(t godoc.TypeDoc, symbols, imports map[string]string) TypeData {
349
	td := TypeData{
350
		Name:       t.Name,
351
		Doc:        template.HTML(t.Doc),
352
		Decl:       hlDecl(t.Decl, symbols, imports),
353
		SourceFile: t.SourceFile,
354
		SourceLine: t.SourceLine,
355
	}
356
	for _, c := range t.Consts {
357
		td.Consts = append(td.Consts, convertValue(c, symbols, imports))
358
	}
359
	for _, v := range t.Vars {
360
		td.Vars = append(td.Vars, convertValue(v, symbols, imports))
361
	}
362
	for _, f := range t.Funcs {
363
		td.Funcs = append(td.Funcs, convertFunc(f, symbols, imports))
364
	}
365
	for _, m := range t.Methods {
366
		td.Methods = append(td.Methods, convertFunc(m, symbols, imports))
367
	}
368
	return td
369
}
370
371
// TrustedHTML converts a string to template.HTML for pre-sanitized content.
372
func TrustedHTML(s string) template.HTML {
373
	return template.HTML(s)
374
}
375
376
func mustRead(name string) string {
377
	data, err := content.ReadFile(name)
378
	if err != nil {
379
		panic("dochtml: " + err.Error())
380
	}
381
	return strings.TrimSpace(string(data))
382
}
383

Source Files