]> Sergey Matveev's repositories - sgblog.git/blobdiff - cmd/sgblog/http.go
Use templates for code simplicity
[sgblog.git] / cmd / sgblog / http.go
index a4ad6713a42d3861a3c4da6d0ec6dcc500448b44..0c09f2b440af44e7bc7be2a5772005d949636e04 100644 (file)
@@ -35,7 +35,7 @@ import (
        "os"
        "strconv"
        "strings"
-       "time"
+       "text/template"
 
        "github.com/hjson/hjson-go"
        "go.stargrave.org/sgblog"
@@ -49,11 +49,106 @@ import (
 const (
        AtomPostsFeed    = "feed.atom"
        AtomCommentsFeed = "comments.atom"
+       TmplHTMLIndex    = `<html>
+<head>
+       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+       <meta name="generator" content="SGBlog {{.Version}}">
+       <title>{{.Cfg.Title}} ({{.Offset}}-{{.OffsetNext}})</title>
+       {{if .Cfg.CSS}}<link rel="stylesheet" type="text/css" href="{{.Cfg.CSS}}">{{end}}
+       {{if .Cfg.Webmaster}}<link rev="made" href="mailto:{{.Cfg.Webmaster}}">{{end -}}
+       {{- range .Cfg.GitURLs}}
+       <link rel="vcs-git" href="{{.}}" title="Git repository">{{end}}
+       <link rel="top" href="{{.Cfg.URLPrefix}}/" title="top">
+       <link rel="alternate" title="Posts feed" href="{{.Cfg.AtomBaseURL}}{{.Cfg.URLPrefix}}/{{.AtomPostsFeed}}" type="application/atom+xml">
+       {{if .CommentsEnabled}}<link rel="alternate" title="Comments feed" href="{{.Cfg.AtomBaseURL}}{{.Cfg.URLPrefix}}/{{.AtomCommentsFeed}}" type="application/atom+xml">{{end}}
+       {{if .Offset}}<link rel="prev" href="{{.Cfg.URLPrefix}}/{{if .OffsetPrev}}?offset={{.OffsetPrev}}{{end}}" title="prev">{{end}}
+       {{if not .LogEnded}}<link rel="next" href="{{.Cfg.URLPrefix}}/?offset={{.OffsetNext}}" title="next">{{end}}
+</head>
+<body>
+{{if .Cfg.AboutURL}}[<a href="{{.Cfg.AboutURL}}">about</a>]{{end}}
+{{block "links" .}}
+{{if .Offset}}[<a href="{{.Cfg.URLPrefix}}/{{if .OffsetPrev}}?offset={{.OffsetPrev}}{{end}}">prev</a>]{{end}}
+{{if not .LogEnded}}[<a href="{{.Cfg.URLPrefix}}/?offset={{.OffsetNext}}">next</a>]{{end}}
+{{end}}
+{{- $Cfg := .Cfg -}}
+{{- $yearPrev := 0 -}}
+{{- $monthPrev := 0 -}}
+{{- $dayPrev := 0}}
+<table border=1>
+<tr>
+       <th>N</th><th>When</th><th>Title</th>
+       <th size="5%"><a title="Lines">L</a></th>
+       <th size="5%"><a title="Comments">C</a></th>
+       <th>Linked to</th>
+</tr>{{range .Entries -}}
+{{- $yearCur := .Commit.Author.When.Year -}}
+{{- $monthCur := .Commit.Author.When.Month -}}
+{{- $dayCur := .Commit.Author.When.Day -}}
+{{- if or (ne $dayCur $dayPrev) (ne $monthCur $monthPrev) (ne $yearCur $yearPrev) -}}
+<tr><td colspan=6><center><tt>{{$yearCur | printf "%04d"}}-{{$monthCur | printf "%02d"}}-{{$dayCur | printf "%02d"}}</tt></center></td></tr>
+{{- $dayPrev = $dayCur}}{{$monthPrev = $monthCur}}{{$yearPrev = $yearCur -}}
+{{- end -}}
+<tr>
+       <td>{{.Num}}</td>
+       <td><tt>{{.Commit.Author.When.Hour | printf "%02d" -}}:{{- .Commit.Author.When.Minute | printf "%02d"}}</tt></td>
+       <td><a href="{{$Cfg.URLPrefix}}/{{.Commit.Hash.String}}">{{.Title}}</a></td>
+       <td>{{.LinesNum}}</td>
+       <td>{{if .CommentsNum}}{{.CommentsNum}}{{else}}&nbsp;{{end}}</td>
+       <td>{{range .DomainURLs}} {{.}} {{end}}</td>
+</tr>
+{{- end}}</table>
+{{template "links" .}}
+</body>
+</html>
+`
+       TmplHTMLEntry = `{{$Cfg := .Cfg}}<html>
+<head>
+       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+       <meta name="generator" content="SGBlog {{.Version}}">
+       <title>{{.Title}} ({{.When}})</title>
+       {{if .Cfg.CSS}}<link rel="stylesheet" type="text/css" href="{{.Cfg.CSS}}">{{end}}
+       {{if .Cfg.Webmaster}}<link rev="made" href="mailto:{{.Cfg.Webmaster}}">{{end -}}
+       {{- range .Cfg.GitURLs}}
+       <link rel="vcs-git" href="{{.}}" title="Git repository">{{end}}
+       <link rel="top" href="{{.Cfg.URLPrefix}}/" title="top">
+       <link rel="alternate" title="Comments feed" href="{{.AtomCommentsURL}}" type="application/atom+xml">
+       {{if .Parent}}<link rel="prev" href="{{.Cfg.URLPrefix}}/{{.Parent}}" title="prev">{{end}}
+</head>
+<body>
+{{if .Cfg.AboutURL}}[<a href="{{.Cfg.AboutURL}}">about</a>]{{end}}
+[<a href="{{.Cfg.URLPrefix}}/">index</a>]
+{{if .Parent}}[<a href="{{.Cfg.URLPrefix}}/{{.Parent}}">prev</a>]{{end}}
+[<tt><a title="When">{{.When}}</a></tt>]
+[<tt><a title="What">{{.Commit.Hash.String}}</a></tt>]
+
+<hr/>
+<h2>{{.Title}}</h2>
+<pre>
+{{range .Lines}}{{. | lineURLize $Cfg.URLPrefix}}
+{{end}}</pre>
+<hr/>
+
+{{if .NoteLines}}Note:<pre>
+{{range .NoteLines}}{{. | lineURLize $Cfg.URLPrefix}}
+{{end}}</pre>
+<hr/>{{end}}
+
+{{if .Cfg.CommentsEmail}}[<a href="mailto:{{.Cfg.CommentsEmail}}?subject={{.Commit.Hash.String}}">leave comment</a>]{{end}}
+
+<dl>{{range $idx, $comment := .Comments}}
+<dt><a name="comment{{$idx}}"><a href="#comment{{$idx}}">comment {{$idx}}</a>:</dt>
+<dd><pre>
+{{range $comment.HeaderLines}}{{.}}
+{{end}}{{range $comment.BodyLines}}{{. | lineURLize $Cfg.URLPrefix}}
+{{end}}</pre></dd>
+{{end}}</dl>
+
+</body>
+</html>
+`
 )
 
 var (
-       defaultLinks = []string{}
-
        renderableSchemes = map[string]struct{}{
                "ftp":    struct{}{},
                "gopher": struct{}{},
@@ -64,8 +159,18 @@ var (
 )
 
 type TableEntry struct {
-       commit      *object.Commit
-       commentsRaw []byte
+       Commit      *object.Commit
+       CommentsRaw []byte
+       Num         int
+       Title       string
+       LinesNum    int
+       CommentsNum int
+       DomainURLs  []string
+}
+
+type CommentEntry struct {
+       HeaderLines []string
+       BodyLines   []string
 }
 
 func makeA(href, text string) string {
@@ -92,13 +197,15 @@ func lineURLize(urlPrefix, line string) string {
                        cols[i] = makeA(col, col)
                        continue
                }
-               cols[i] = sha1DigestRe.ReplaceAllString(col, makeA(
-                       urlPrefix+"/$1", "$1",
-               ))
+               cols[i] = sha1DigestRe.ReplaceAllString(col, makeA(urlPrefix+"/$1", "$1"))
        }
        return strings.Join(cols, " ")
 }
 
+func lineURLizeInTemplate(urlPrefix, line interface{}) string {
+       return lineURLize(urlPrefix.(string), line.(string))
+}
+
 func startHeader(etag hash.Hash, gziped bool) string {
        lines := []string{
                "Content-Type: text/html; charset=UTF-8",
@@ -112,21 +219,6 @@ func startHeader(etag hash.Hash, gziped bool) string {
        return strings.Join(lines, "\n")
 }
 
-func startHTML(title string, additional []string) string {
-       return fmt.Sprintf(`<html>
-<head>
-       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-       <meta name="generator" content="SGBlog %s">
-       <title>%s</title>
-       %s
-</head>
-<body>
-`,
-               sgblog.Version, title,
-               strings.Join(append(defaultLinks, additional...), "\n   "),
-       )
-}
-
 func makeErr(err error) {
        fmt.Print("Content-Type: text/plain; charset=UTF-8\n\n")
        fmt.Println(err)
@@ -182,43 +274,32 @@ func serveHTTP() {
        if err != nil {
                panic(err)
        }
-       etagHash.Write([]byte("SGBLOG"))
-       etagHash.Write([]byte(sgblog.Version))
-       etagHash.Write([]byte(cfg.GitPath))
-       etagHash.Write([]byte(cfg.Branch))
-       etagHash.Write([]byte(cfg.Title))
-       etagHash.Write([]byte(cfg.URLPrefix))
-       etagHash.Write([]byte(cfg.AtomBaseURL))
-       etagHash.Write([]byte(cfg.AtomId))
-       etagHash.Write([]byte(cfg.AtomAuthor))
-
-       etagHashForWeb := [][]byte{}
-       if cfg.CSS != "" {
-               defaultLinks = append(defaultLinks, `<link rel="stylesheet" type="text/css" href="`+cfg.CSS+`">`)
-               etagHashForWeb = append(etagHashForWeb, []byte(cfg.CSS))
-       }
-       if cfg.Webmaster != "" {
-               defaultLinks = append(defaultLinks, `<link rev="made" href="mailto:`+cfg.Webmaster+`">`)
-               etagHashForWeb = append(etagHashForWeb, []byte(cfg.Webmaster))
+       for _, s := range []string{
+               "SGBLOG",
+               sgblog.Version,
+               cfg.GitPath,
+               cfg.Branch,
+               cfg.Title,
+               cfg.URLPrefix,
+               cfg.AtomBaseURL,
+               cfg.AtomId,
+               cfg.AtomAuthor,
+       } {
+               if _, err = etagHash.Write([]byte(s)); err != nil {
+                       panic(err)
+               }
        }
-       if cfg.AboutURL != "" {
-               etagHashForWeb = append(etagHashForWeb, []byte(cfg.AboutURL))
+       etagHashForWeb := []string{
+               cfg.CSS,
+               cfg.Webmaster,
+               cfg.AboutURL,
+               cfg.CommentsNotesRef,
+               cfg.CommentsEmail,
        }
        for _, gitURL := range cfg.GitURLs {
-               defaultLinks = append(defaultLinks, `<link rel="vcs-git" href="`+gitURL+`" title="Git repository">`)
-               etagHashForWeb = append(etagHashForWeb, []byte(gitURL))
-       }
-       if cfg.CommentsNotesRef != "" {
-               etagHashForWeb = append(etagHashForWeb, []byte(cfg.CommentsNotesRef))
-       }
-       if cfg.CommentsEmail != "" {
-               etagHashForWeb = append(etagHashForWeb, []byte(cfg.CommentsEmail))
+               etagHashForWeb = append(etagHashForWeb, gitURL)
        }
 
-       defaultLinks = append(defaultLinks, `<link rel="top" href="`+cfg.URLPrefix+`/" title="top">`)
-       atomPostsURL := cfg.AtomBaseURL + cfg.URLPrefix + "/" + AtomPostsFeed
-       atomCommentsURL := cfg.AtomBaseURL + cfg.URLPrefix + "/" + AtomCommentsFeed
-
        headHash, err := initRepo(cfg)
        if err != nil {
                makeErr(err)
@@ -283,7 +364,7 @@ func serveHTTP() {
                entries := make([]TableEntry, 0, PageEntries)
                logEnded := false
                for _, data := range etagHashForWeb {
-                       etagHash.Write(data)
+                       etagHash.Write([]byte(data))
                }
                etagHash.Write([]byte("INDEX"))
                for i := 0; i < PageEntries; i++ {
@@ -295,96 +376,57 @@ func serveHTTP() {
                        etagHash.Write(commit.Hash[:])
                        commentsRaw := getNote(commentsTree, commit.Hash)
                        etagHash.Write(commentsRaw)
-                       entries = append(entries, TableEntry{commit, commentsRaw})
+                       entries = append(entries, TableEntry{
+                               Commit:      commit,
+                               CommentsRaw: commentsRaw,
+                       })
                }
                checkETag(etagHash)
 
-               var table bytes.Buffer
-               table.WriteString(
-                       "<table border=1>\n" +
-                               "<th>N</th>" +
-                               "<th>When</th>" +
-                               "<th>Title</th>" +
-                               `<th size="5%"><a title="Lines">L</a></th>` +
-                               `<th size="5%"><a title="Comments">C</a></th>` +
-                               "<th>Linked to</th></tr>\n")
-               var yearPrev int
-               var monthPrev time.Month
-               var dayPrev int
-               for _, entry := range entries {
-                       yearCur, monthCur, dayCur := entry.commit.Author.When.Date()
-                       if dayCur != dayPrev || monthCur != monthPrev || yearCur != yearPrev {
-                               table.WriteString(fmt.Sprintf(
-                                       "<tr><td colspan=6><center><tt>%04d-%02d-%02d</tt></center></td></tr>\n",
-                                       yearCur, monthCur, dayCur,
-                               ))
-                               yearPrev, monthPrev, dayPrev = yearCur, monthCur, dayCur
-                       }
+               for i, entry := range entries {
                        commitN++
-                       lines := msgSplit(entry.commit.Message)
-                       domains := []string{}
+                       entry.Num = commitN
+                       lines := msgSplit(entry.Commit.Message)
+                       entry.Title = lines[0]
+                       entry.LinesNum = len(lines) - 2
                        for _, line := range lines[2:] {
-                               if u := urlParse(line); u == nil {
+                               u := urlParse(line)
+                               if u == nil {
                                        break
-                               } else {
-                                       domains = append(domains, makeA(line, u.Host))
                                }
+                               entry.DomainURLs = append(entry.DomainURLs, makeA(line, u.Host))
                        }
-                       var commentsValue string
-                       if l := len(parseComments(entry.commentsRaw)); l > 0 {
-                               commentsValue = strconv.Itoa(l)
-                       } else {
-                               commentsValue = "&nbsp;"
-                       }
-                       table.WriteString(fmt.Sprintf(
-                               "<tr><td>%d</td><td><tt>%02d:%02d</tt></td>"+
-                                       "<td>%s</td>"+
-                                       "<td>%d</td><td>%s</td>"+
-                                       "<td>%s</td></tr>\n",
-                               commitN,
-                               entry.commit.Author.When.Hour(),
-                               entry.commit.Author.When.Minute(),
-                               makeA(cfg.URLPrefix+"/"+entry.commit.Hash.String(), lines[0]),
-                               len(lines)-2,
-                               commentsValue,
-                               strings.Join(domains, " "),
-                       ))
+                       entry.CommentsNum = len(parseComments(entry.CommentsRaw))
+                       entries[i] = entry
                }
-               table.WriteString("</table>")
-
-               var href string
-               links := []string{`<link rel="alternate" title="Posts feed" href="` + atomPostsURL + `" type="application/atom+xml">`}
-               var refs bytes.Buffer
-               if commentsTree != nil {
-                       links = append(links, `<link rel="alternate" title="Comments feed" href="`+atomCommentsURL+`" type="application/atom+xml">`)
-               }
-               if offset > 0 {
-                       if offsetPrev := offset - PageEntries; offsetPrev > 0 {
-                               href = cfg.URLPrefix + "/?offset=" + strconv.Itoa(offsetPrev)
-                       } else {
-                               href = cfg.URLPrefix + "/"
-                       }
-                       links = append(links, `<link rel="prev" href="`+href+`" title="prev">`)
-                       refs.WriteString("\n" + makeA(href, "[prev]"))
-               }
-               if !logEnded {
-                       href = cfg.URLPrefix + "/?offset=" + strconv.Itoa(offset+PageEntries)
-                       links = append(links, `<link rel="next" href="`+href+`" title="next">`)
-                       refs.WriteString("\n" + makeA(href, "[next]"))
-               }
-
+               tmpl := template.Must(template.New("index").Parse(TmplHTMLIndex))
                os.Stdout.Write([]byte(startHeader(etagHash, gzipWriter != nil)))
-               out.Write([]byte(startHTML(
-                       fmt.Sprintf("%s (%d-%d)", cfg.Title, offset, offset+PageEntries),
-                       links,
-               )))
-               if cfg.AboutURL != "" {
-                       out.Write([]byte(fmt.Sprintf("[%s]", makeA(cfg.AboutURL, "about"))))
+               err = tmpl.Execute(out, struct {
+                       Version          string
+                       Cfg              *Cfg
+                       CommentsEnabled  bool
+                       AtomPostsFeed    string
+                       AtomCommentsFeed string
+                       Offset           int
+                       OffsetPrev       int
+                       OffsetNext       int
+                       LogEnded         bool
+                       Entries          []TableEntry
+               }{
+                       Version:          sgblog.Version,
+                       Cfg:              cfg,
+                       CommentsEnabled:  commentsTree != nil,
+                       AtomPostsFeed:    AtomPostsFeed,
+                       AtomCommentsFeed: AtomCommentsFeed,
+                       Offset:           offset,
+                       OffsetPrev:       offset - PageEntries,
+                       OffsetNext:       offset + PageEntries,
+                       LogEnded:         logEnded,
+                       Entries:          entries,
+               })
+               if err != nil {
+                       makeErr(err)
                }
-               out.Write(refs.Bytes())
-               out.Write(table.Bytes())
-               out.Write(refs.Bytes())
-               out.Write([]byte("\n"))
        } else if pathInfo == "/"+AtomPostsFeed {
                commit, err := repo.CommitObject(*headHash)
                if err != nil {
@@ -397,8 +439,11 @@ func serveHTTP() {
                        Title:   cfg.Title,
                        ID:      cfg.AtomId,
                        Updated: atom.Time(commit.Author.When),
-                       Link:    []atom.Link{{Rel: "self", Href: atomPostsURL}},
-                       Author:  &atom.Person{Name: cfg.AtomAuthor},
+                       Link: []atom.Link{{
+                               Rel:  "self",
+                               Href: cfg.AtomBaseURL + cfg.URLPrefix + "/" + AtomPostsFeed,
+                       }},
+                       Author: &atom.Person{Name: cfg.AtomAuthor},
                }
                repoLog, err := repo.Log(&git.LogOptions{From: *headHash})
                if err != nil {
@@ -450,8 +495,11 @@ func serveHTTP() {
                        Title:   cfg.Title + " comments",
                        ID:      "urn:uuid:" + bytes2uuid(idHasher.Sum(nil)),
                        Updated: atom.Time(commit.Author.When),
-                       Link:    []atom.Link{{Rel: "self", Href: atomCommentsURL}},
-                       Author:  &atom.Person{Name: cfg.AtomAuthor},
+                       Link: []atom.Link{{
+                               Rel:  "self",
+                               Href: cfg.AtomBaseURL + cfg.URLPrefix + "/" + AtomCommentsFeed,
+                       }},
+                       Author: &atom.Person{Name: cfg.AtomAuthor},
                }
                repoLog, err := repo.Log(&git.LogOptions{From: commentsRef.Hash()})
                if err != nil {
@@ -490,11 +538,10 @@ func serveHTTP() {
                        idHasher.Write(commit.Hash[:])
                        idHasher.Write([]byte(commentN))
                        feed.Entry = append(feed.Entry, &atom.Entry{
-                               Title: strings.Join([]string{
-                                       "Comment ", commentN,
-                                       " for \"", msgSplit(commit.Message)[0],
-                                       "\" by ", from,
-                               }, ""),
+                               Title: fmt.Sprintf(
+                                       "Comment %s for \"%s\" by %s",
+                                       commentN, msgSplit(commit.Message)[0], from,
+                               ),
                                Author: &atom.Person{Name: from},
                                ID:     "urn:uuid:" + bytes2uuid(idHasher.Sum(nil)),
                                Link: []atom.Link{{
@@ -524,11 +571,11 @@ func serveHTTP() {
                        makeErr(err)
                }
                for _, data := range etagHashForWeb {
-                       etagHash.Write(data)
+                       etagHash.Write([]byte(data))
                }
                etagHash.Write([]byte("ENTRY"))
                etagHash.Write(commit.Hash[:])
-               atomCommentsURL = strings.Join([]string{
+               atomCommentsURL := strings.Join([]string{
                        cfg.AtomBaseURL, cfg.URLPrefix, "/",
                        commit.Hash.String(), "/", AtomCommentsFeed,
                }, "")
@@ -583,10 +630,7 @@ func serveHTTP() {
                                idHasher.Write(commit.Hash[:])
                                idHasher.Write([]byte(comment.n))
                                feed.Entry = append(feed.Entry, &atom.Entry{
-                                       Title: strings.Join([]string{
-                                               "Comment", comment.n,
-                                               "by", comment.from,
-                                       }, " "),
+                                       Title:  fmt.Sprintf("Comment %s by %s", comment.n, comment.from),
                                        Author: &atom.Person{Name: comment.from},
                                        ID:     "urn:uuid:" + bytes2uuid(idHasher.Sum(nil)),
                                        Link: []atom.Link{{
@@ -616,64 +660,55 @@ func serveHTTP() {
                notesRaw := getNote(notesTree, commit.Hash)
                etagHash.Write(notesRaw)
                checkETag(etagHash)
+
                lines := msgSplit(commit.Message)
                title := lines[0]
                when := commit.Author.When.Format(sgblog.WhenFmt)
-               os.Stdout.Write([]byte(startHeader(etagHash, gzipWriter != nil)))
-               links := []string{`<link rel="alternate" title="Comments feed" href="` + atomCommentsURL + `" type="application/atom+xml">`}
                var parent string
                if len(commit.ParentHashes) > 0 {
                        parent = commit.ParentHashes[0].String()
-                       links = append(links, `<link rel="prev" href="`+cfg.URLPrefix+"/"+parent+`" title="prev">`)
-               }
-               out.Write([]byte(startHTML(fmt.Sprintf("%s (%s)", title, when), links)))
-               if cfg.AboutURL != "" {
-                       out.Write([]byte(fmt.Sprintf("[%s]\n", makeA(cfg.AboutURL, "about"))))
-               }
-               out.Write([]byte(fmt.Sprintf("[%s]\n", makeA(cfg.URLPrefix+"/", "index"))))
-               if parent != "" {
-                       out.Write([]byte(fmt.Sprintf("[%s]\n", makeA(cfg.URLPrefix+"/"+parent, "prev"))))
                }
-               out.Write([]byte(fmt.Sprintf(
-                       "[<tt><a title=\"When\">%s</a></tt>]\n"+
-                               "[<tt><a title=\"What\">%s</a></tt>]\n"+
-                               "<hr/>\n<h2>%s</h2>\n<pre>\n",
-                       when, commit.Hash.String(), title,
-               )))
-               for _, line := range lines[2:] {
-                       out.Write([]byte(lineURLize(cfg.URLPrefix, line) + "\n"))
+               commentsParsed := parseComments(commentsRaw)
+               comments := make([]CommentEntry, 0, len(commentsParsed))
+               for _, comment := range commentsParsed {
+                       lines := strings.Split(comment, "\n")
+                       comments = append(comments, CommentEntry{lines[:3], lines[3:]})
                }
-               out.Write([]byte("</pre>\n<hr/>\n"))
+               var notesLines []string
                if len(notesRaw) > 0 {
-                       out.Write([]byte("Note:<pre>\n"))
-                       for _, line := range strings.Split(string(notesRaw), "\n") {
-                               out.Write([]byte(lineURLize(cfg.URLPrefix, line) + "\n"))
-                       }
-                       out.Write([]byte("</pre>\n<hr/>\n"))
-               }
-               if cfg.CommentsEmail != "" {
-                       out.Write([]byte("[" + makeA(
-                               "mailto:"+cfg.CommentsEmail+"?subject="+commit.Hash.String(),
-                               "leave comment",
-                       ) + "]\n"))
+                       notesLines = strings.Split(string(notesRaw), "\n")
                }
-               out.Write([]byte("<dl>\n"))
-               for i, comment := range parseComments(commentsRaw) {
-                       out.Write([]byte(fmt.Sprintf(
-                               "<dt><a name=\"comment%d\"><a href=\"#comment%d\">comment %d</a>:"+
-                                       "</dt>\n<dd><pre>\n",
-                               i, i, i,
-                       )))
-                       lines = strings.Split(comment, "\n")
-                       for _, line := range lines[:3] {
-                               out.Write([]byte(line + "\n"))
-                       }
-                       for _, line := range lines[3:] {
-                               out.Write([]byte(lineURLize(cfg.URLPrefix, line) + "\n"))
-                       }
-                       out.Write([]byte("</pre></dd>\n"))
+
+               tmpl := template.New("entry")
+               tmpl = tmpl.Funcs(template.FuncMap{"lineURLize": lineURLizeInTemplate})
+               tmpl = template.Must(tmpl.Parse(TmplHTMLEntry))
+               os.Stdout.Write([]byte(startHeader(etagHash, gzipWriter != nil)))
+               err = tmpl.Execute(out, struct {
+                       Version         string
+                       Cfg             *Cfg
+                       Title           string
+                       When            string
+                       AtomCommentsURL string
+                       Parent          string
+                       Commit          *object.Commit
+                       Lines           []string
+                       NoteLines       []string
+                       Comments        []CommentEntry
+               }{
+                       Version:         sgblog.Version,
+                       Cfg:             cfg,
+                       Title:           title,
+                       When:            when,
+                       AtomCommentsURL: atomCommentsURL,
+                       Parent:          parent,
+                       Commit:          commit,
+                       Lines:           lines[2:],
+                       NoteLines:       notesLines,
+                       Comments:        comments,
+               })
+               if err != nil {
+                       makeErr(err)
                }
-               out.Write([]byte("</dl>\n"))
        } else {
                makeErr(errors.New("unknown URL action"))
        }