| 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 | } |
| 69 | |
| 70 | // TypeData wraps godoc.TypeDoc for templates. |
| 71 | type TypeData struct { |
| 72 | Name string |
| 73 | Doc template.HTML |
| 74 | Decl template.HTML |
| 75 | Consts []ValueData |
| 76 | Vars []ValueData |
| 77 | Funcs []FuncData |
| 78 | Methods []FuncData |
| 79 | } |
| 80 | |
| 81 | // SourcePageData is the template data for source file view. |
| 82 | type SourcePageData struct { |
| 83 | Host string |
| 84 | ImportPrefix string |
| 85 | ModuleName string |
| 86 | VCS string |
| 87 | Repo string |
| 88 | Version string |
| 89 | SubPath string |
| 90 | FileName string |
| 91 | HighlightedSource template.HTML |
| 92 | SourceFiles []string |
| 93 | } |
| 94 | |
| 95 | // VersionsPageData is the template data for the versions list. |
| 96 | type VersionsPageData struct { |
| 97 | Host string |
| 98 | ImportPrefix string |
| 99 | ModuleName string |
| 100 | VCS string |
| 101 | Repo string |
| 102 | Versions []string |
| 103 | CurrentVersion string |
| 104 | } |
| 105 | |
| 106 | // Renderer handles HTML rendering of documentation pages. |
| 107 | type Renderer struct { |
| 108 | moduleTmpl *template.Template |
| 109 | sourceTmpl *template.Template |
| 110 | versionsTmpl *template.Template |
| 111 | } |
| 112 | |
| 113 | // NewRenderer creates a renderer with parsed templates. |
| 114 | func NewRenderer() (*Renderer, error) { |
| 115 | base, err := template.New("base").Parse(mustRead("templates/base.html")) |
| 116 | if err != nil { |
| 117 | return nil, err |
| 118 | } |
| 119 | |
| 120 | moduleTmpl, err := template.Must(base.Clone()).Parse(mustRead("templates/module.html")) |
| 121 | if err != nil { |
| 122 | return nil, err |
| 123 | } |
| 124 | |
| 125 | sourceTmpl, err := template.Must(base.Clone()).Parse(mustRead("templates/source.html")) |
| 126 | if err != nil { |
| 127 | return nil, err |
| 128 | } |
| 129 | |
| 130 | versionsTmpl, err := template.Must(base.Clone()).Parse(mustRead("templates/versions.html")) |
| 131 | if err != nil { |
| 132 | return nil, err |
| 133 | } |
| 134 | |
| 135 | return &Renderer{ |
| 136 | moduleTmpl: moduleTmpl, |
| 137 | sourceTmpl: sourceTmpl, |
| 138 | versionsTmpl: versionsTmpl, |
| 139 | }, nil |
| 140 | } |
| 141 | |
| 142 | // RenderModule renders a module/package documentation page. |
| 143 | func (r *Renderer) RenderModule(w io.Writer, data *ModulePageData) error { |
| 144 | return r.moduleTmpl.Execute(w, data) |
| 145 | } |
| 146 | |
| 147 | // RenderSource renders a source code view page. |
| 148 | func (r *Renderer) RenderSource(w io.Writer, data *SourcePageData) error { |
| 149 | return r.sourceTmpl.Execute(w, data) |
| 150 | } |
| 151 | |
| 152 | // RenderVersions renders a version list page. |
| 153 | func (r *Renderer) RenderVersions(w io.Writer, data *VersionsPageData) error { |
| 154 | return r.versionsTmpl.Execute(w, data) |
| 155 | } |
| 156 | |
| 157 | // BuildModulePageData creates template data from a PackageDoc. |
| 158 | func BuildModulePageData(host, modName, version, subpath, vcs, repo string, doc *godoc.PackageDoc, versions []string, subPkgs []godoc.SubPkgSummary) *ModulePageData { |
| 159 | data := &ModulePageData{ |
| 160 | Host: host, |
| 161 | ImportPrefix: host + "/" + modName, |
| 162 | ModuleName: modName, |
| 163 | VCS: vcs, |
| 164 | Repo: repo, |
| 165 | Version: version, |
| 166 | SubPath: subpath, |
| 167 | Versions: versions, |
| 168 | SubPackages: subPkgs, |
| 169 | } |
| 170 | |
| 171 | if doc != nil { |
| 172 | data.Doc = template.HTML(doc.Doc) |
| 173 | data.Synopsis = doc.Synopsis |
| 174 | data.PackageName = doc.Name |
| 175 | data.SourceFiles = doc.Files |
| 176 | |
| 177 | // Build hierarchical index: funcs sorted, then types sorted |
| 178 | // with constructors and methods as children. |
| 179 | var funcIndex []IndexEntry |
| 180 | for _, f := range doc.Funcs { |
| 181 | funcIndex = append(funcIndex, IndexEntry{ |
| 182 | Anchor: f.Name, |
| 183 | Text: f.ShortDecl, |
| 184 | }) |
| 185 | } |
| 186 | sort.Slice(funcIndex, func(i, j int) bool { |
| 187 | return funcIndex[i].Text < funcIndex[j].Text |
| 188 | }) |
| 189 | |
| 190 | var typeIndex []IndexEntry |
| 191 | for _, t := range doc.Types { |
| 192 | entry := IndexEntry{ |
| 193 | Anchor: t.Name, |
| 194 | Text: "type " + t.Name, |
| 195 | } |
| 196 | |
| 197 | // Constructors (sorted). |
| 198 | var constructors []IndexEntry |
| 199 | for _, f := range t.Funcs { |
| 200 | constructors = append(constructors, IndexEntry{ |
| 201 | Anchor: f.Name, |
| 202 | Text: f.ShortDecl, |
| 203 | }) |
| 204 | } |
| 205 | sort.Slice(constructors, func(i, j int) bool { |
| 206 | return constructors[i].Text < constructors[j].Text |
| 207 | }) |
| 208 | entry.Children = append(entry.Children, constructors...) |
| 209 | |
| 210 | // Type-associated constants (sorted). |
| 211 | var consts []IndexEntry |
| 212 | for _, c := range t.Consts { |
| 213 | for _, name := range c.Names { |
| 214 | consts = append(consts, IndexEntry{ |
| 215 | Anchor: name, |
| 216 | Text: "const " + name, |
| 217 | }) |
| 218 | } |
| 219 | } |
| 220 | for _, v := range t.Vars { |
| 221 | for _, name := range v.Names { |
| 222 | consts = append(consts, IndexEntry{ |
| 223 | Anchor: name, |
| 224 | Text: "var " + name, |
| 225 | }) |
| 226 | } |
| 227 | } |
| 228 | sort.Slice(consts, func(i, j int) bool { |
| 229 | return consts[i].Text < consts[j].Text |
| 230 | }) |
| 231 | entry.Children = append(entry.Children, consts...) |
| 232 | |
| 233 | // Methods (sorted). |
| 234 | var methods []IndexEntry |
| 235 | for _, m := range t.Methods { |
| 236 | methods = append(methods, IndexEntry{ |
| 237 | Anchor: t.Name + "." + m.Name, |
| 238 | Text: m.ShortDecl, |
| 239 | }) |
| 240 | } |
| 241 | sort.Slice(methods, func(i, j int) bool { |
| 242 | return methods[i].Text < methods[j].Text |
| 243 | }) |
| 244 | entry.Children = append(entry.Children, methods...) |
| 245 | typeIndex = append(typeIndex, entry) |
| 246 | } |
| 247 | sort.Slice(typeIndex, func(i, j int) bool { |
| 248 | return typeIndex[i].Text < typeIndex[j].Text |
| 249 | }) |
| 250 | |
| 251 | data.Index = append(funcIndex, typeIndex...) |
| 252 | |
| 253 | // Convert godoc types to template types. |
| 254 | for _, c := range doc.Consts { |
| 255 | data.Consts = append(data.Consts, convertValue(c)) |
| 256 | } |
| 257 | for _, v := range doc.Vars { |
| 258 | data.Vars = append(data.Vars, convertValue(v)) |
| 259 | } |
| 260 | for _, f := range doc.Funcs { |
| 261 | data.Funcs = append(data.Funcs, convertFunc(f)) |
| 262 | } |
| 263 | for _, t := range doc.Types { |
| 264 | data.Types = append(data.Types, convertType(t)) |
| 265 | } |
| 266 | } |
| 267 | |
| 268 | return data |
| 269 | } |
| 270 | |
| 271 | func highlightDecl(s string) template.HTML { |
| 272 | return template.HTML(godoc.HighlightDecl(s)) |
| 273 | } |
| 274 | |
| 275 | func convertValue(v godoc.ValueDoc) ValueData { |
| 276 | return ValueData{ |
| 277 | Names: v.Names, |
| 278 | Doc: template.HTML(v.Doc), |
| 279 | Decl: highlightDecl(v.Decl), |
| 280 | } |
| 281 | } |
| 282 | |
| 283 | func convertFunc(f godoc.FuncDoc) FuncData { |
| 284 | return FuncData{ |
| 285 | Name: f.Name, |
| 286 | Doc: template.HTML(f.Doc), |
| 287 | Decl: highlightDecl(f.Decl), |
| 288 | Recv: f.Recv, |
| 289 | } |
| 290 | } |
| 291 | |
| 292 | func convertType(t godoc.TypeDoc) TypeData { |
| 293 | td := TypeData{ |
| 294 | Name: t.Name, |
| 295 | Doc: template.HTML(t.Doc), |
| 296 | Decl: highlightDecl(t.Decl), |
| 297 | } |
| 298 | for _, c := range t.Consts { |
| 299 | td.Consts = append(td.Consts, convertValue(c)) |
| 300 | } |
| 301 | for _, v := range t.Vars { |
| 302 | td.Vars = append(td.Vars, convertValue(v)) |
| 303 | } |
| 304 | for _, f := range t.Funcs { |
| 305 | td.Funcs = append(td.Funcs, convertFunc(f)) |
| 306 | } |
| 307 | for _, m := range t.Methods { |
| 308 | td.Methods = append(td.Methods, convertFunc(m)) |
| 309 | } |
| 310 | return td |
| 311 | } |
| 312 | |
| 313 | // TrustedHTML converts a string to template.HTML for pre-sanitized content. |
| 314 | func TrustedHTML(s string) template.HTML { |
| 315 | return template.HTML(s) |
| 316 | } |
| 317 | |
| 318 | func mustRead(name string) string { |
| 319 | data, err := content.ReadFile(name) |
| 320 | if err != nil { |
| 321 | panic("dochtml: " + err.Error()) |
| 322 | } |
| 323 | return strings.TrimSpace(string(data)) |
| 324 | } |
| 325 | |