| 1 | package dochtml |
| 2 | |
| 3 | import ( |
| 4 | "embed" |
| 5 | "html/template" |
| 6 | "io" |
| 7 | "io/fs" |
| 8 | "net/http" |
| 9 | "strings" |
| 10 | |
| 11 | "example.com/curator/internal/godoc" |
| 12 | ) |
| 13 | |
| 14 | //go:embed templates/* static/* |
| 15 | var content embed.FS |
| 16 | |
| 17 | // StaticFS returns the embedded static assets filesystem. |
| 18 | func StaticFS() http.FileSystem { |
| 19 | sub, _ := fs.Sub(content, "static") |
| 20 | return http.FS(sub) |
| 21 | } |
| 22 | |
| 23 | // IndexEntry represents an entry in the package index. |
| 24 | type IndexEntry struct { |
| 25 | Anchor string |
| 26 | Text string |
| 27 | } |
| 28 | |
| 29 | // ModulePageData is the template data for module/package pages. |
| 30 | type ModulePageData struct { |
| 31 | Host string |
| 32 | ImportPrefix string |
| 33 | ModuleName string |
| 34 | VCS string |
| 35 | Repo string |
| 36 | Version string |
| 37 | SubPath string |
| 38 | Versions []string |
| 39 | |
| 40 | // Package doc fields. |
| 41 | Doc template.HTML |
| 42 | Synopsis string |
| 43 | PackageName string |
| 44 | Index []IndexEntry |
| 45 | Consts []ValueData |
| 46 | Vars []ValueData |
| 47 | Funcs []FuncData |
| 48 | Types []TypeData |
| 49 | SubPackages []godoc.SubPkgSummary |
| 50 | SourceFiles []string |
| 51 | } |
| 52 | |
| 53 | // ValueData wraps godoc.ValueDoc for templates. |
| 54 | type ValueData struct { |
| 55 | Names []string |
| 56 | Doc template.HTML |
| 57 | Decl string |
| 58 | } |
| 59 | |
| 60 | // FuncData wraps godoc.FuncDoc for templates. |
| 61 | type FuncData struct { |
| 62 | Name string |
| 63 | Doc template.HTML |
| 64 | Decl string |
| 65 | Recv string |
| 66 | } |
| 67 | |
| 68 | // TypeData wraps godoc.TypeDoc for templates. |
| 69 | type TypeData struct { |
| 70 | Name string |
| 71 | Doc template.HTML |
| 72 | Decl string |
| 73 | Consts []ValueData |
| 74 | Vars []ValueData |
| 75 | Funcs []FuncData |
| 76 | Methods []FuncData |
| 77 | } |
| 78 | |
| 79 | // SourcePageData is the template data for source file view. |
| 80 | type SourcePageData struct { |
| 81 | Host string |
| 82 | ImportPrefix string |
| 83 | ModuleName string |
| 84 | VCS string |
| 85 | Repo string |
| 86 | Version string |
| 87 | SubPath string |
| 88 | FileName string |
| 89 | HighlightedSource template.HTML |
| 90 | SourceFiles []string |
| 91 | } |
| 92 | |
| 93 | // VersionsPageData is the template data for the versions list. |
| 94 | type VersionsPageData struct { |
| 95 | Host string |
| 96 | ImportPrefix string |
| 97 | ModuleName string |
| 98 | VCS string |
| 99 | Repo string |
| 100 | Versions []string |
| 101 | CurrentVersion string |
| 102 | } |
| 103 | |
| 104 | // Renderer handles HTML rendering of documentation pages. |
| 105 | type Renderer struct { |
| 106 | moduleTmpl *template.Template |
| 107 | sourceTmpl *template.Template |
| 108 | versionsTmpl *template.Template |
| 109 | } |
| 110 | |
| 111 | // NewRenderer creates a renderer with parsed templates. |
| 112 | func NewRenderer() (*Renderer, error) { |
| 113 | base, err := template.New("base").Parse(mustRead("templates/base.html")) |
| 114 | if err != nil { |
| 115 | return nil, err |
| 116 | } |
| 117 | |
| 118 | moduleTmpl, err := template.Must(base.Clone()).Parse(mustRead("templates/module.html")) |
| 119 | if err != nil { |
| 120 | return nil, err |
| 121 | } |
| 122 | |
| 123 | sourceTmpl, err := template.Must(base.Clone()).Parse(mustRead("templates/source.html")) |
| 124 | if err != nil { |
| 125 | return nil, err |
| 126 | } |
| 127 | |
| 128 | versionsTmpl, err := template.Must(base.Clone()).Parse(mustRead("templates/versions.html")) |
| 129 | if err != nil { |
| 130 | return nil, err |
| 131 | } |
| 132 | |
| 133 | return &Renderer{ |
| 134 | moduleTmpl: moduleTmpl, |
| 135 | sourceTmpl: sourceTmpl, |
| 136 | versionsTmpl: versionsTmpl, |
| 137 | }, nil |
| 138 | } |
| 139 | |
| 140 | // RenderModule renders a module/package documentation page. |
| 141 | func (r *Renderer) RenderModule(w io.Writer, data *ModulePageData) error { |
| 142 | return r.moduleTmpl.Execute(w, data) |
| 143 | } |
| 144 | |
| 145 | // RenderSource renders a source code view page. |
| 146 | func (r *Renderer) RenderSource(w io.Writer, data *SourcePageData) error { |
| 147 | return r.sourceTmpl.Execute(w, data) |
| 148 | } |
| 149 | |
| 150 | // RenderVersions renders a version list page. |
| 151 | func (r *Renderer) RenderVersions(w io.Writer, data *VersionsPageData) error { |
| 152 | return r.versionsTmpl.Execute(w, data) |
| 153 | } |
| 154 | |
| 155 | // BuildModulePageData creates template data from a PackageDoc. |
| 156 | func BuildModulePageData(host, modName, version, subpath, vcs, repo string, doc *godoc.PackageDoc, versions []string, subPkgs []godoc.SubPkgSummary) *ModulePageData { |
| 157 | data := &ModulePageData{ |
| 158 | Host: host, |
| 159 | ImportPrefix: host + "/" + modName, |
| 160 | ModuleName: modName, |
| 161 | VCS: vcs, |
| 162 | Repo: repo, |
| 163 | Version: version, |
| 164 | SubPath: subpath, |
| 165 | Versions: versions, |
| 166 | SubPackages: subPkgs, |
| 167 | } |
| 168 | |
| 169 | if doc != nil { |
| 170 | data.Doc = template.HTML(doc.Doc) |
| 171 | data.Synopsis = doc.Synopsis |
| 172 | data.PackageName = doc.Name |
| 173 | data.SourceFiles = doc.Files |
| 174 | |
| 175 | // Build index. |
| 176 | var index []IndexEntry |
| 177 | for _, f := range doc.Funcs { |
| 178 | index = append(index, IndexEntry{ |
| 179 | Anchor: f.Name, |
| 180 | Text: f.ShortDecl, |
| 181 | }) |
| 182 | } |
| 183 | for _, t := range doc.Types { |
| 184 | index = append(index, IndexEntry{ |
| 185 | Anchor: t.Name, |
| 186 | Text: "type " + t.Name, |
| 187 | }) |
| 188 | for _, f := range t.Funcs { |
| 189 | index = append(index, IndexEntry{ |
| 190 | Anchor: f.Name, |
| 191 | Text: f.ShortDecl, |
| 192 | }) |
| 193 | } |
| 194 | for _, m := range t.Methods { |
| 195 | index = append(index, IndexEntry{ |
| 196 | Anchor: t.Name + "." + m.Name, |
| 197 | Text: m.ShortDecl, |
| 198 | }) |
| 199 | } |
| 200 | } |
| 201 | data.Index = index |
| 202 | |
| 203 | // Convert godoc types to template types. |
| 204 | for _, c := range doc.Consts { |
| 205 | data.Consts = append(data.Consts, convertValue(c)) |
| 206 | } |
| 207 | for _, v := range doc.Vars { |
| 208 | data.Vars = append(data.Vars, convertValue(v)) |
| 209 | } |
| 210 | for _, f := range doc.Funcs { |
| 211 | data.Funcs = append(data.Funcs, convertFunc(f)) |
| 212 | } |
| 213 | for _, t := range doc.Types { |
| 214 | data.Types = append(data.Types, convertType(t)) |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | return data |
| 219 | } |
| 220 | |
| 221 | func convertValue(v godoc.ValueDoc) ValueData { |
| 222 | return ValueData{ |
| 223 | Names: v.Names, |
| 224 | Doc: template.HTML(v.Doc), |
| 225 | Decl: v.Decl, |
| 226 | } |
| 227 | } |
| 228 | |
| 229 | func convertFunc(f godoc.FuncDoc) FuncData { |
| 230 | return FuncData{ |
| 231 | Name: f.Name, |
| 232 | Doc: template.HTML(f.Doc), |
| 233 | Decl: f.Decl, |
| 234 | Recv: f.Recv, |
| 235 | } |
| 236 | } |
| 237 | |
| 238 | func convertType(t godoc.TypeDoc) TypeData { |
| 239 | td := TypeData{ |
| 240 | Name: t.Name, |
| 241 | Doc: template.HTML(t.Doc), |
| 242 | Decl: t.Decl, |
| 243 | } |
| 244 | for _, c := range t.Consts { |
| 245 | td.Consts = append(td.Consts, convertValue(c)) |
| 246 | } |
| 247 | for _, v := range t.Vars { |
| 248 | td.Vars = append(td.Vars, convertValue(v)) |
| 249 | } |
| 250 | for _, f := range t.Funcs { |
| 251 | td.Funcs = append(td.Funcs, convertFunc(f)) |
| 252 | } |
| 253 | for _, m := range t.Methods { |
| 254 | td.Methods = append(td.Methods, convertFunc(m)) |
| 255 | } |
| 256 | return td |
| 257 | } |
| 258 | |
| 259 | // TrustedHTML converts a string to template.HTML for pre-sanitized content. |
| 260 | func TrustedHTML(s string) template.HTML { |
| 261 | return template.HTML(s) |
| 262 | } |
| 263 | |
| 264 | func mustRead(name string) string { |
| 265 | data, err := content.ReadFile(name) |
| 266 | if err != nil { |
| 267 | panic("dochtml: " + err.Error()) |
| 268 | } |
| 269 | return strings.TrimSpace(string(data)) |
| 270 | } |
| 271 | |