render.go

v1.4.2
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 with version from go.mod.
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
				importLinks[alias] = "/" + strings.TrimPrefix(path, host+"/")
300
			} else {
301
				importLinks[alias] = pkgDevURL(path, doc.DepVersions)
302
			}
303
		}
304
305
		// Convert godoc types to template types.
306
		imports := importLinks
307
		for _, c := range doc.Consts {
308
			data.Consts = append(data.Consts, convertValue(c, symbols, imports))
309
		}
310
		for _, v := range doc.Vars {
311
			data.Vars = append(data.Vars, convertValue(v, symbols, imports))
312
		}
313
		for _, f := range doc.Funcs {
314
			data.Funcs = append(data.Funcs, convertFunc(f, symbols, imports))
315
		}
316
		for _, t := range doc.Types {
317
			data.Types = append(data.Types, convertType(t, symbols, imports))
318
		}
319
	}
320
321
	return data
322
}
323
324
func hlDecl(s string, symbols map[string]string, imports map[string]string) template.HTML {
325
	return template.HTML(godoc.HighlightDeclLinked(s, symbols, imports))
326
}
327
328
func convertValue(v godoc.ValueDoc, symbols, imports map[string]string) ValueData {
329
	return ValueData{
330
		Names: v.Names,
331
		Doc:   template.HTML(v.Doc),
332
		Decl:  hlDecl(v.Decl, symbols, imports),
333
	}
334
}
335
336
func convertFunc(f godoc.FuncDoc, symbols, imports map[string]string) FuncData {
337
	return FuncData{
338
		Name:       f.Name,
339
		Doc:        template.HTML(f.Doc),
340
		Decl:       hlDecl(f.Decl, symbols, imports),
341
		Recv:       f.Recv,
342
		SourceFile: f.SourceFile,
343
		SourceLine: f.SourceLine,
344
	}
345
}
346
347
func convertType(t godoc.TypeDoc, symbols, imports map[string]string) TypeData {
348
	td := TypeData{
349
		Name:       t.Name,
350
		Doc:        template.HTML(t.Doc),
351
		Decl:       hlDecl(t.Decl, symbols, imports),
352
		SourceFile: t.SourceFile,
353
		SourceLine: t.SourceLine,
354
	}
355
	for _, c := range t.Consts {
356
		td.Consts = append(td.Consts, convertValue(c, symbols, imports))
357
	}
358
	for _, v := range t.Vars {
359
		td.Vars = append(td.Vars, convertValue(v, symbols, imports))
360
	}
361
	for _, f := range t.Funcs {
362
		td.Funcs = append(td.Funcs, convertFunc(f, symbols, imports))
363
	}
364
	for _, m := range t.Methods {
365
		td.Methods = append(td.Methods, convertFunc(m, symbols, imports))
366
	}
367
	return td
368
}
369
370
// TrustedHTML converts a string to template.HTML for pre-sanitized content.
371
func TrustedHTML(s string) template.HTML {
372
	return template.HTML(s)
373
}
374
375
func mustRead(name string) string {
376
	data, err := content.ReadFile(name)
377
	if err != nil {
378
		panic("dochtml: " + err.Error())
379
	}
380
	return strings.TrimSpace(string(data))
381
}
382
383
// pkgDevURL builds a pkg.go.dev URL for an import path, with version if known.
384
// For stdlib packages, uses the Go version. For third-party, finds the module
385
// version from go.mod.
386
func pkgDevURL(importPath string, depVersions map[string]string) string {
387
	if depVersions == nil {
388
		return "https://pkg.go.dev/" + importPath
389
	}
390
391
	// Check if this is a stdlib package (no dots in first path element).
392
	firstSlash := strings.Index(importPath, "/")
393
	firstElem := importPath
394
	if firstSlash > 0 {
395
		firstElem = importPath[:firstSlash]
396
	}
397
	if !strings.Contains(firstElem, ".") {
398
		// Stdlib package — use Go version.
399
		if goVer, ok := depVersions["go"]; ok {
400
			return "https://pkg.go.dev/" + importPath + "@go" + goVer
401
		}
402
		return "https://pkg.go.dev/" + importPath
403
	}
404
405
	// Third-party: find the longest matching module path in depVersions.
406
	// e.g., for "github.com/foo/bar/baz", try "github.com/foo/bar/baz",
407
	// then "github.com/foo/bar", then "github.com/foo".
408
	path := importPath
409
	for path != "" {
410
		if ver, ok := depVersions[path]; ok {
411
			return "https://pkg.go.dev/" + importPath + "@" + ver
412
		}
413
		slash := strings.LastIndex(path, "/")
414
		if slash < 0 {
415
			break
416
		}
417
		path = path[:slash]
418
	}
419
420
	return "https://pkg.go.dev/" + importPath
421
}
422

Source Files