Render all non-login using new method to use less allocations and memory

This commit is contained in:
Jan-Lukas Else 2022-01-20 18:22:10 +01:00
parent 049fee72bd
commit aa319b70fb
37 changed files with 1611 additions and 997 deletions

View File

@ -62,12 +62,12 @@ func (a *goBlog) authMiddleware(next http.Handler) http.Handler {
_ = r.ParseForm()
b = []byte(r.PostForm.Encode())
}
a.render(w, r, templateLogin, &renderData{
Data: map[string]interface{}{
"loginmethod": r.Method,
"loginheaders": base64.StdEncoding.EncodeToString(h),
"loginbody": base64.StdEncoding.EncodeToString(b),
"totp": a.cfg.User.TOTP != "",
a.renderNew(w, r, a.renderLogin, &renderData{
Data: &loginRenderData{
loginMethod: r.Method,
loginHeaders: base64.StdEncoding.EncodeToString(h),
loginBody: base64.StdEncoding.EncodeToString(b),
totp: a.cfg.User.TOTP != "",
},
})
})

View File

@ -29,13 +29,13 @@ func (a *goBlog) serveBlogroll(w http.ResponseWriter, r *http.Request) {
}
c := bc.Blogroll
can := bc.getRelativePath(defaultIfEmpty(c.Path, defaultBlogrollPath))
a.render(w, r, templateBlogroll, &renderData{
a.renderNew(w, r, a.renderBlogroll, &renderData{
Canonical: a.getFullAddress(can),
Data: map[string]interface{}{
"Title": c.Title,
"Description": c.Description,
"Outlines": outlines,
"Download": can + ".opml",
Data: &blogrollRenderData{
title: c.Title,
description: c.Description,
outlines: outlines.([]*opml.Outline),
download: can + ".opml",
},
})
}

View File

@ -25,10 +25,10 @@ func (a *goBlog) initBlogStats() {
func (a *goBlog) serveBlogStats(w http.ResponseWriter, r *http.Request) {
_, bc := a.getBlog(r)
canonical := bc.getRelativePath(defaultIfEmpty(bc.BlogStats.Path, defaultBlogStatsPath))
a.render(w, r, templateBlogStats, &renderData{
a.renderNew(w, r, a.renderBlogStats, &renderData{
Canonical: a.getFullAddress(canonical),
Data: map[string]interface{}{
"TableUrl": canonical + blogStatsTablePath,
Data: &blogStatsRenderData{
tableUrl: canonical + blogStatsTablePath,
},
})
}
@ -43,7 +43,7 @@ func (a *goBlog) serveBlogStatsTable(w http.ResponseWriter, r *http.Request) {
return
}
// Render
a.render(w, r, templateBlogStatsTable, &renderData{
a.renderNew(w, r, a.renderBlogStatsTable, &renderData{
Data: data,
})
}

View File

@ -65,12 +65,12 @@ func (a *goBlog) captchaMiddleware(next http.Handler) http.Handler {
// Render captcha
_ = ses.Save(r, w)
w.Header().Set("Cache-Control", "no-store,max-age=0")
a.renderWithStatusCode(w, r, http.StatusUnauthorized, templateCaptcha, &renderData{
Data: map[string]string{
"captchamethod": r.Method,
"captchaheaders": base64.StdEncoding.EncodeToString(h),
"captchabody": base64.StdEncoding.EncodeToString(b),
"captchaid": captchaId,
a.renderNewWithStatusCode(w, r, http.StatusUnauthorized, a.renderCaptcha, &renderData{
Data: &captchaRenderData{
captchaMethod: r.Method,
captchaHeaders: base64.StdEncoding.EncodeToString(h),
captchaBody: base64.StdEncoding.EncodeToString(b),
captchaId: captchaId,
},
})
})

View File

@ -41,7 +41,7 @@ func (a *goBlog) serveComment(w http.ResponseWriter, r *http.Request) {
return
}
_, bc := a.getBlog(r)
a.render(w, r, templateComment, &renderData{
a.renderNew(w, r, a.renderComment, &renderData{
Canonical: a.getFullAddress(bc.getRelativePath(path.Join(commentPath, strconv.Itoa(id)))),
Data: comment,
})

View File

@ -17,11 +17,11 @@ const defaultContactPath = "/contact"
func (a *goBlog) serveContactForm(w http.ResponseWriter, r *http.Request) {
_, bc := a.getBlog(r)
cc := bc.Contact
a.render(w, r, templateContact, &renderData{
Data: map[string]interface{}{
"title": cc.Title,
"description": cc.Description,
"privacy": cc.PrivacyPolicy,
a.renderNew(w, r, a.renderContact, &renderData{
Data: &contactRenderData{
title: cc.Title,
description: cc.Description,
privacy: cc.PrivacyPolicy,
},
})
}
@ -63,9 +63,9 @@ func (a *goBlog) sendContactSubmission(w http.ResponseWriter, r *http.Request) {
// Send notification
a.sendNotification(message.String())
// Give feedback
a.render(w, r, templateContact, &renderData{
Data: map[string]interface{}{
"sent": true,
a.renderNew(w, r, a.renderContact, &renderData{
Data: &contactRenderData{
sent: true,
},
})
}

View File

@ -73,7 +73,7 @@ func (a *goBlog) createMarkdownPreview(blog string, markdown []byte) (rendered [
// Render post
var hb htmlBuilder
a.renderEditorPreview(&hb, a.cfg.Blogs[blog], p)
rendered = []byte(hb.String())
rendered = hb.Bytes()
return
}

View File

@ -8,11 +8,6 @@ import (
"go.goblog.app/app/pkgs/contenttype"
)
type errorData struct {
Title string
Message string
}
func (a *goBlog) serve404(w http.ResponseWriter, r *http.Request) {
a.serveError(w, r, fmt.Sprintf("%s was not found", r.RequestURI), http.StatusNotFound)
}
@ -40,8 +35,8 @@ func (a *goBlog) serveError(w http.ResponseWriter, r *http.Request, message stri
http.Error(w, message, status)
return
}
a.renderWithStatusCode(w, r, status, templateError, &renderData{
Data: &errorData{
a.renderNewWithStatusCode(w, r, status, a.renderError, &renderData{
Data: &errorRenderData{
Title: fmt.Sprintf("%d %s", status, http.StatusText(status)),
Message: message,
},

View File

@ -25,10 +25,10 @@ func (a *goBlog) serveGeoMap(w http.ResponseWriter, r *http.Request) {
}
if len(allPostsWithLocation) == 0 {
a.render(w, r, templateGeoMap, &renderData{
a.renderNew(w, r, a.renderGeoMap, &renderData{
Canonical: canonical,
Data: map[string]interface{}{
"nolocations": true,
Data: &geoMapRenderData{
noLocations: true,
},
})
return
@ -85,14 +85,14 @@ func (a *goBlog) serveGeoMap(w http.ResponseWriter, r *http.Request) {
tracksJson = string(tracksJsonBytes)
}
a.render(w, r, templateGeoMap, &renderData{
a.renderNew(w, r, a.renderGeoMap, &renderData{
Canonical: canonical,
Data: map[string]interface{}{
"locations": locationsJson,
"tracks": tracksJson,
"attribution": a.getMapAttribution(),
"minzoom": a.getMinZoom(),
"maxzoom": a.getMaxZoom(),
Data: &geoMapRenderData{
locations: locationsJson,
tracks: tracksJson,
attribution: a.getMapAttribution(),
minZoom: a.getMinZoom(),
maxZoom: a.getMaxZoom(),
},
})
}

2
go.mod
View File

@ -46,7 +46,7 @@ require (
github.com/spf13/cast v1.4.1
github.com/spf13/viper v1.10.1
github.com/stretchr/testify v1.7.0
github.com/tdewolff/minify/v2 v2.9.28
github.com/tdewolff/minify/v2 v2.9.29
github.com/thoas/go-funk v0.9.1
github.com/tkrajina/gpxgo v1.2.0
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80

4
go.sum
View File

@ -415,8 +415,8 @@ github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQ
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
github.com/tdewolff/minify/v2 v2.9.28 h1:70GT62rRyLcH4jbkTFGcy6rktHKGiaSNZb/h8pnuQYw=
github.com/tdewolff/minify/v2 v2.9.28/go.mod h1:6XAjcHM46pFcRE0eztigFPm0Q+Cxsw8YhEWT+rDkcZM=
github.com/tdewolff/minify/v2 v2.9.29 h1:QMVJaCJzWL0mXS33cX792YD074xz4lOhkyBS8hAzYAY=
github.com/tdewolff/minify/v2 v2.9.29/go.mod h1:6XAjcHM46pFcRE0eztigFPm0Q+Cxsw8YhEWT+rDkcZM=
github.com/tdewolff/parse/v2 v2.5.27 h1:PL3LzzXaOpmdrknnOlIeO2muIBHAwiKp6TxN1RbU5gI=
github.com/tdewolff/parse/v2 v2.5.27/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho=
github.com/tdewolff/test v1.0.6 h1:76mzYJQ83Op284kMT+63iCNCI7NEERsIN8dLM+RiKr4=

View File

@ -254,7 +254,7 @@ details summary {
height: 400px;
}
#post-actions {
#post-actions, #posteditactions {
@extend .p;
display: flex;
flex-wrap: wrap;

View File

@ -39,7 +39,7 @@ func (m *Minifier) Get() *minify.M {
func (m *Minifier) Write(w io.Writer, mediatype string, b []byte) (int, error) {
mw := m.Get().Writer(mediatype, w)
defer func() { _ = mw.Close() }()
defer mw.Close()
return mw.Write(b)
}

View File

@ -73,16 +73,16 @@ func (a *goBlog) servePost(w http.ResponseWriter, r *http.Request) {
if canonical == "" {
canonical = a.fullPostURL(p)
}
template := templatePost
renderMethod := a.renderPost
if p.Path == a.getRelativePath(p.Blog, "") {
template = templateStaticHome
renderMethod = a.renderStaticHome
}
w.Header().Add("Link", fmt.Sprintf("<%s>; rel=shortlink", a.shortPostURL(p)))
status := http.StatusOK
if strings.HasSuffix(string(p.Status), statusDeletedSuffix) {
status = http.StatusGone
}
a.renderWithStatusCode(w, r, status, template, &renderData{
a.renderNewWithStatusCode(w, r, status, renderMethod, &renderData{
BlogString: p.Blog,
Canonical: canonical,
Data: p,
@ -201,23 +201,22 @@ func (a *goBlog) serveDate(w http.ResponseWriter, r *http.Request) {
}
var title, dPath strings.Builder
if year != 0 {
ys := fmt.Sprintf("%0004d", year)
title.WriteString(ys)
dPath.WriteString(ys)
_, _ = fmt.Fprintf(&title, "%0004d", year)
_, _ = fmt.Fprintf(&dPath, "%0004d", year)
} else {
title.WriteString("XXXX")
dPath.WriteString("x")
_, _ = title.WriteString("XXXX")
_, _ = dPath.WriteString("x")
}
if month != 0 {
title.WriteString(fmt.Sprintf("-%02d", month))
dPath.WriteString(fmt.Sprintf("/%02d", month))
_, _ = fmt.Fprintf(&title, "-%02d", month)
_, _ = fmt.Fprintf(&dPath, "/%02d", month)
} else if day != 0 {
title.WriteString("-XX")
dPath.WriteString("/x")
_, _ = title.WriteString("-XX")
_, _ = dPath.WriteString("/x")
}
if day != 0 {
title.WriteString(fmt.Sprintf("-%02d", day))
dPath.WriteString(fmt.Sprintf("/%02d", day))
_, _ = fmt.Fprintf(&title, "-%02d", day)
_, _ = fmt.Fprintf(&dPath, "/%02d", day)
}
_, bc := a.getBlog(r)
a.serveIndex(w, r.WithContext(context.WithValue(r.Context(), indexConfigKey, &indexConfig{
@ -339,18 +338,18 @@ func (a *goBlog) serveIndex(w http.ResponseWriter, r *http.Request) {
if summaryTemplate == "" {
summaryTemplate = defaultSummary
}
a.render(w, r, templateIndex, &renderData{
a.renderNew(w, r, a.renderIndex, &renderData{
Canonical: a.getFullAddress(path),
Data: map[string]interface{}{
"Title": title,
"Description": description,
"Posts": posts,
"HasPrev": hasPrev,
"HasNext": hasNext,
"First": path,
"Prev": prevPath,
"Next": nextPath,
"SummaryTemplate": summaryTemplate,
Data: &indexRenderData{
title: title,
description: description,
posts: posts,
hasPrev: hasPrev,
hasNext: hasNext,
first: path,
prev: prevPath,
next: nextPath,
summaryTemplate: summaryTemplate,
},
})
}

View File

@ -17,25 +17,11 @@ const (
templatesExt = ".gohtml"
templateBase = "base"
templatePost = "post"
templateError = "error"
templateIndex = "index"
templateTaxonomy = "taxonomy"
templateSearch = "search"
templateEditor = "editor"
templateEditorFiles = "editorfiles"
templateLogin = "login"
templateStaticHome = "statichome"
templateBlogStats = "blogstats"
templateBlogStatsTable = "blogstatstable"
templateComment = "comment"
templateCaptcha = "captcha"
templateCommentsAdmin = "commentsadmin"
templateNotificationsAdmin = "notificationsadmin"
templateWebmentionAdmin = "webmentionadmin"
templateBlogroll = "blogroll"
templateGeoMap = "geomap"
templateContact = "contact"
templateIndieAuth = "indieauth"
)
@ -45,49 +31,10 @@ func (a *goBlog) initRendering() error {
"md": a.safeRenderMarkdownAsHTML,
"mdtitle": a.renderMdTitle,
"html": wrapStringAsHTML,
// Post specific
"content": a.postHtml,
"shorturl": a.shortPostURL,
"gettrack": a.getTrack,
// Code based rendering
"posttax": func(p *post, b *configBlog) template.HTML {
"tor": func(rd *renderData) template.HTML {
var hb htmlBuilder
a.renderPostTax(&hb, p, b)
return hb.html()
},
"postmeta": func(p *post, b *configBlog, typ string) template.HTML {
var hb htmlBuilder
a.renderPostMeta(&hb, p, b, typ)
return hb.html()
},
"oldcontentwarning": func(p *post, b *configBlog) template.HTML {
var hb htmlBuilder
a.renderOldContentWarning(&hb, p, b)
return hb.html()
},
"interactions": func(b *configBlog, c string) template.HTML {
var hb htmlBuilder
a.renderInteractions(&hb, b, c)
return hb.html()
},
"author": func() template.HTML {
var hb htmlBuilder
a.renderAuthor(&hb)
return hb.html()
},
"postheadmeta": func(p *post, c string) template.HTML {
var hb htmlBuilder
a.renderPostHeadMeta(&hb, p, c)
return hb.html()
},
"tor": func(b *configBlog, torUsed bool, torAddress string) template.HTML {
var hb htmlBuilder
a.renderTorNotice(&hb, b, torUsed, torAddress)
return hb.html()
},
"summary": func(bc *configBlog, p *post, typ summaryTyp) template.HTML {
var hb htmlBuilder
a.renderSummary(&hb, bc, p, typ)
a.renderTorNotice(&hb, rd)
return hb.html()
},
// Others
@ -97,13 +44,11 @@ func (a *goBlog) initRendering() error {
"now": localNowString,
"asset": a.assetFileName,
"string": a.ts.GetTemplateStringVariantFunc(),
"urlize": urlize,
"absolute": a.getFullAddress,
"opensearch": openSearchUrl,
"mbytes": mBytesString,
"editortemplate": a.editorPostTemplate,
"editorpostdesc": a.editorPostDesc,
"ttsenabled": a.ttsEnabled,
}
baseTemplate, err := template.New("base").Funcs(templateFunctions).ParseFiles(path.Join(templatesDir, templateBase+templatesExt))
if err != nil {
@ -170,6 +115,24 @@ func (a *goBlog) renderWithStatusCode(w http.ResponseWriter, r *http.Request, st
}
}
func (a *goBlog) renderNew(w http.ResponseWriter, r *http.Request, f func(*htmlBuilder, *renderData), data *renderData) {
a.renderNewWithStatusCode(w, r, http.StatusOK, f, data)
}
func (a *goBlog) renderNewWithStatusCode(w http.ResponseWriter, r *http.Request, statusCode int, f func(*htmlBuilder, *renderData), data *renderData) {
// Check render data
a.checkRenderData(r, data)
// Set content type
w.Header().Set(contentType, contenttype.HTMLUTF8)
// Write status code
w.WriteHeader(statusCode)
// Render
minWriter := a.min.Get().Writer(contenttype.HTML, w)
defer minWriter.Close()
hb := newHtmlBuilder(minWriter)
f(hb, data)
}
func (a *goBlog) checkRenderData(r *http.Request, data *renderData) {
if data.app == nil {
data.app = a

View File

@ -26,7 +26,7 @@ func (a *goBlog) serveSearch(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, path.Join(servePath, searchEncode(q)), http.StatusFound)
return
}
a.render(w, r, templateSearch, &renderData{
a.renderNew(w, r, a.renderSearch, &renderData{
Canonical: a.getFullAddress(servePath),
})
}

View File

@ -20,11 +20,11 @@ func (a *goBlog) serveTaxonomy(w http.ResponseWriter, r *http.Request) {
a.serveError(w, r, err.Error(), http.StatusInternalServerError)
return
}
a.render(w, r, templateTaxonomy, &renderData{
a.renderNew(w, r, a.renderTaxonomy, &renderData{
Canonical: a.getFullAddress(r.URL.Path),
Data: map[string]interface{}{
"Taxonomy": tax,
"ValueGroups": groupStrings(allValues),
Data: &taxonomyRenderData{
taxonomy: tax,
valueGroups: groupStrings(allValues),
},
})
}

View File

@ -144,7 +144,7 @@ details summary > *:first-child {
border-bottom: 1px solid var(--primary, #000);
}
.p, #post-actions, table {
.p, #post-actions, #posteditactions, table {
display: block;
margin-top: 1em;
margin-bottom: 1em;
@ -214,12 +214,12 @@ details summary > *:first-child {
height: 400px;
}
#post-actions {
#post-actions, #posteditactions {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
#post-actions * {
#post-actions *, #posteditactions * {
text-align: center;
}

View File

@ -39,7 +39,7 @@
<nav>{{ range $i, $item := .Items }}{{ if ne $i 0 }} &bull; {{ end }}<a href="{{ $item.Link }}">{{ mdtitle $item.Title }}</a>{{ end }}</nav>
{{ end }}
<p translate="no">&copy; {{ dateformat now "2006" }} {{ with .User.Name }}{{ . }}{{ else }}{{ mdtitle .Blog.Title }}{{ end }}</p>
{{ tor .Blog .TorUsed .TorAddress }}
{{ tor . }}
</footer>
{{ if .EasterEgg }}<script defer src="{{ asset "js/easteregg.js" }}"></script>{{ end }}
</html>

View File

@ -1,29 +0,0 @@
{{ define "title" }}
<title>{{ mdtitle .Data.Title }} - {{ mdtitle .Blog.Title }}</title>
{{ end }}
{{ define "main" }}
<main>
{{ with .Data.Title }}<h1>{{ mdtitle . }}</h1>{{ end }}
{{ with .Data.Description }}{{ md . }}{{ end }}
<p><a href="{{ .Blog.RelativePath .Data.Download }}" class="button" download>{{ string .Blog.Lang "download" }}</a></p>
{{ $lang := .Blog.Lang }}
{{ range .Data.Outlines }}
{{ $title := .Title }}
{{ if not $title }}{{ $title = .Text }}{{ end }}
<h2 id="{{ urlize $title }}">{{ $title }} ({{ len .Outlines }})</h2>
<ul>
{{ range .Outlines }}
{{ $ct := .Title }}
{{ if not $ct }}{{ $ct = .Text }}{{ end }}
<li><a href="{{ .HTMLURL }}" target="_blank">{{ $ct }}</a> (<a href="{{ .XMLURL }}" target="_blank">{{ string $lang "feed" }}</a>)</li>
{{ end }}
</ul>
{{ end }}
</main>
{{ if .CommentsEnabled }}{{ interactions .Blog .Canonical }}{{ end }}
{{ end }}
{{ define "blogroll" }}
{{ template "base" . }}
{{ end }}

View File

@ -1,17 +0,0 @@
{{ define "title" }}
<title>{{ with .Blog.BlogStats.Title }}{{ mdtitle . }} - {{ end }}{{ mdtitle .Blog.Title }}</title>
{{ end }}
{{ define "main" }}
<main>
{{ with .Blog.BlogStats.Title }}<h1>{{ mdtitle . }}</h1>{{ end }}
{{ with .Blog.BlogStats.Description }}{{ md . }}{{ end }}
<p id="loading" data-table="{{.Data.TableUrl}}">{{ string .Blog.Lang "loading" }}</p>
<script defer src="{{ asset "js/blogstats.js" }}"></script>
</main>
{{ if .CommentsEnabled }}{{ interactions .Blog .Canonical }}{{ end }}
{{ end }}
{{ define "blogstats" }}
{{ template "base" . }}
{{ end }}

View File

@ -1,48 +0,0 @@
{{ define "blogstatstable" }}
<table>
<thead>
<tr>
<th class="tal">{{ string .Blog.Lang "year" }}</th>
<th class="tar">{{ string .Blog.Lang "posts" }}</th>
<th class="tar">~{{ string .Blog.Lang "chars" }}</th>
<th class="tar">~{{ string .Blog.Lang "words" }}</th>
<th class="tar">~{{ string .Blog.Lang "wordsperpost" }}</th>
</tr>
</thead>
<tbody>
{{ $months := .Data.Months }}
{{ range $year := .Data.Years }}
<tr class="statsyear" data-year="{{ $year.Name }}">
<td class="tal">{{ $year.Name }}</td>
<td class="tar">{{ $year.Posts }}</td>
<td class="tar">{{ $year.Chars }}</td>
<td class="tar">{{ $year.Words }}</td>
<td class="tar">{{ $year.WordsPerPost }}</td>
</tr>
{{ range $month := (index $months $year.Name) }}
<tr class="statsmonth hide" data-year="{{ $year.Name }}">
<td class="tal">{{ $year.Name }}-{{ $month.Name }}</td>
<td class="tar">{{ $month.Posts }}</td>
<td class="tar">{{ $month.Chars }}</td>
<td class="tar">{{ $month.Words }}</td>
<td class="tar">{{ $month.WordsPerPost }}</td>
</tr>
{{ end }}
{{ end }}
<tr>
<td class="tal">{{ string .Blog.Lang "withoutdate" }}</td>
<td class="tar">{{ .Data.NoDate.Posts }}</td>
<td class="tar">{{ .Data.NoDate.Chars }}</td>
<td class="tar">{{ .Data.NoDate.Words }}</td>
<td class="tar">{{ .Data.NoDate.WordsPerPost }}</td>
</tr>
<tr>
<td class="tal"><b>{{ string .Blog.Lang "total" }}</b></td>
<td class="tar">{{ .Data.Total.Posts }}</td>
<td class="tar">{{ .Data.Total.Chars }}</td>
<td class="tar">{{ .Data.Total.Words }}</td>
<td class="tar">{{ .Data.Total.WordsPerPost }}</td>
</tr>
</tbody>
</table>
{{ end }}

View File

@ -1,21 +0,0 @@
{{ define "title" }}
<title>{{ mdtitle .Blog.Title }}</title>
{{ end }}
{{ define "main" }}
<main>
<p><img src="/captcha/{{ .Data.captchaid }}.png" class="captchaimg"></p>
<form class="fw p" method="post">
<input type="hidden" name="captchaaction" value="captcha">
<input type="hidden" name="captchamethod" value="{{ .Data.captchamethod }}">
<input type="hidden" name="captchaheaders" value="{{ .Data.captchaheaders }}">
<input type="hidden" name="captchabody" value="{{ .Data.captchabody }}">
<input type="text" name="digits" placeholder="{{ string .Blog.Lang "captchainstructions" }}" required>
<input type="submit" value="{{ string .Blog.Lang "submit" }}">
</form>
</main>
{{ end }}
{{ define "captcha" }}
{{ template "base" . }}
{{ end }}

View File

@ -1,21 +0,0 @@
{{ define "title" }}
<title>{{ string .Blog.Lang "acommentby" }} {{ .Data.Name }}</title>
{{ end }}
{{ define "main" }}
<main class=h-entry>
<p><a class="u-in-reply-to" href="{{ absolute .Data.Target }}">{{ absolute .Data.Target }}</a></p>
<div class="p-author h-card p">
{{ string .Blog.Lang "acommentby" }}
{{ if .Data.Website }}<a href="{{ .Data.Website }}" class="p-name u-url" target="_blank" rel="nofollow noopener noreferrer ugc">{{ .Data.Name }}</a>{{ else }}<span class="p-name">{{ .Data.Name }}</span>{{ end }}:
</div>
<p class="e-content">
{{ html .Data.Comment }}
</p>
</main>
{{ if .CommentsEnabled }}{{ interactions .Blog .Canonical }}{{ end }}
{{ end }}
{{ define "comment" }}
{{ template "base" . }}
{{ end }}

View File

@ -1,30 +0,0 @@
{{ define "title" }}
<title>{{ with .Data.title }}{{ mdtitle . }} - {{ end }}{{ mdtitle .Blog.Title }}</title>
{{ end }}
{{ define "main" }}
<main>
{{ if .Data.sent }}
<p>{{ string .Blog.Lang "messagesent" }}</p>
{{ else }}
{{ with .Data.title }}<h1>{{ mdtitle . }}</h1>{{ end }}
{{ with .Data.description }}{{ md . }}{{ end }}
<form class="fw p" method="post">
<input type="text" name="name" placeholder="{{ string .Blog.Lang "nameopt" }}">
<input type="url" name="website" placeholder="{{ string .Blog.Lang "websiteopt" }}">
<input type="email" name="email" placeholder="{{ string .Blog.Lang "emailopt" }}">
<textarea name="message" required placeholder="{{ string .Blog.Lang "message" }}"></textarea>
{{ if .Data.privacy }}
{{ md .Data.privacy }}
<input type="submit" value="{{ string .Blog.Lang "contactagreesend" }}">
{{ else }}
<input type="submit" value="{{ string .Blog.Lang "contactsend" }}">
{{ end }}
</form>
{{ end }}
</main>
{{ end }}
{{ define "contact" }}
{{ template "base" . }}
{{ end }}

View File

@ -1,14 +0,0 @@
{{ define "title" }}
<title>{{ with .Data.Title }}{{ . }}{{end}}</title>
{{ end }}
{{ define "main" }}
<main>
{{ with .Data.Title }}<h1>{{ . }}</h1>{{ end }}
{{ with .Data.Message }}<p class="monospace">{{ . }}</p>{{ end }}
</main>
{{ end }}
{{ define "error" }}
{{ template "base" . }}
{{ end }}

View File

@ -1,29 +0,0 @@
{{ define "title" }}
<title>{{ mdtitle .Blog.Title }}</title>
{{ if not .Data.nolocations }}
<link rel="stylesheet" href="/-/leaflet/leaflet.css"/>
<script src="/-/leaflet/leaflet.js"></script>
{{ end }}
{{ end }}
{{ define "main" }}
<main>
{{ if .Data.nolocations }}
<p>{{ string .Blog.Lang "nolocations" }}</p>
{{ else }}
<div class="p" id="map"
data-locations="{{ .Data.locations }}"
data-tracks="{{ .Data.tracks }}"
data-minzoom={{ .Data.minzoom }}
data-maxzoom={{ .Data.maxzoom }}
data-attribution="{{ .Data.attribution }}"
></div>
<script defer src="{{ asset "js/geomap.js" }}"></script>
{{ end }}
</main>
{{ if .CommentsEnabled }}{{ interactions .Blog .Canonical }}{{ end }}
{{ end }}
{{ define "geomap" }}
{{ template "base" . }}
{{ end }}

View File

@ -1,36 +0,0 @@
{{ define "title" }}
<title>{{ with .Data.Title }}{{ mdtitle . }} - {{ end }}{{ mdtitle .Blog.Title }}</title>
<link rel="alternate" type="application/rss+xml" title="RSS{{ with .Data.Title }} ({{ mdtitle . }}){{ end }}" href="{{ .Data.First }}.rss"/>
<link rel="alternate" type="application/atom+xml" title="Atom{{ with .Data.Title }} ({{ mdtitle . }}){{ end }}" href="{{ .Data.First }}.atom"/>
<link rel="alternate" type="application/feed+json" title="JSON Feed{{ with .Data.Title }} ({{ mdtitle . }}){{ end }}" href="{{ .Data.First }}.json"/>
{{ end }}
{{ define "main" }}
<main class="h-feed">
{{ with .Data.Title }}<h1 class="p-name">{{ mdtitle . }}</h1>{{ end }}
{{ with .Data.Description }}{{ md . }}{{ end }}
{{ if (or .Data.Title .Data.Description) }}
<hr>
{{ end }}
{{ if .Data.Posts }}
{{ $summaryTemplate := .Data.SummaryTemplate }}
{{ $blog := .Blog }}
{{ range $i, $post := .Data.Posts }}
{{ summary $blog $post $summaryTemplate }}
{{ end }}
{{ else }}
<p>{{ string .Blog.Lang "noposts" }}</p>
{{ end }}
{{ if .Data.HasPrev }}
<p><a href="{{ .Data.Prev }}">{{ string .Blog.Lang "prev" }}</a></p>
{{ end }}
{{ if .Data.HasNext }}
<p><a href="{{ .Data.Next }}">{{ string .Blog.Lang "next" }}</a></p>
{{ end }}
{{ author }}
</main>
{{ end }}
{{ define "index" }}
{{ template "base" . }}
{{ end }}

View File

@ -1,26 +0,0 @@
{{ define "title" }}
<title>{{ string .Blog.Lang "login" }} - {{ mdtitle .Blog.Title }}</title>
{{ end }}
{{ define "main" }}
<main>
<h1>{{ string .Blog.Lang "login" }}</h1>
<form class="fw p" method="post">
<input type="hidden" name="loginaction" value="login">
<input type="hidden" name="loginmethod" value="{{ .Data.loginmethod }}">
<input type="hidden" name="loginheaders" value="{{ .Data.loginheaders }}">
<input type="hidden" name="loginbody" value="{{ .Data.loginbody }}">
<input type="text" name="username" autocomplete="username" placeholder="{{ string .Blog.Lang "username" }}">
<input type="password" name="password" autocomplete="current-password" placeholder="{{ string .Blog.Lang "password" }}">
{{ if .Data.totp }}
<input type="text" name="token" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code" placeholder="{{ string .Blog.Lang "totp" }}">
{{ end }}
<input type="submit" value="{{ string .Blog.Lang "login" }}">
</form>
{{ author }}
</main>
{{ end }}
{{ define "login" }}
{{ template "base" . }}
{{ end }}

View File

@ -1,77 +0,0 @@
{{ define "title" }}
<link rel="stylesheet" href="{{ asset "css/chroma.css" }}">
<title>{{ with .Data.RenderedTitle }}{{ . }} - {{end}}{{ mdtitle .Blog.Title }}</title>
{{ postheadmeta .Data .Canonical }}
{{ with shorturl .Data }}<link rel="shortlink" href="{{ . }}">{{ end }}
{{ if .Data.HasTrack }}
<link rel="stylesheet" href="/-/leaflet/leaflet.css"/>
<script src="/-/leaflet/leaflet.js"></script>
{{ end }}
{{ end }}
{{ define "main" }}
<main class=h-entry>
<article>
<data class="u-url hide" value="{{ absolute .Data.Path }}"></data>
{{ with .Data.RenderedTitle }}<h1 class=p-name>{{ . }}</h1>{{ end }}
{{ postmeta .Data .Blog "post" }}
<div id="post-actions">
<a href="https://www.addtoany.com/share#url={{ shorturl .Data }}{{ with .Data.RenderedTitle }}&title={{ . }}{{ end }}" target="_blank" rel="nofollow noopener noreferrer" class="button">{{ string .Blog.Lang "share" }}</a>
<a id="translateBtn" href="https://translate.google.com/translate?u={{ absolute .Data.Path }}" target="_blank" rel="nofollow noopener noreferrer" class="button">{{ string .Blog.Lang "translate" }}</a>
<script defer src="{{ asset "js/translate.js" }}"></script>
<button id="speakBtn" class="hide" data-speak="{{ string .Blog.Lang "speak" }}" data-stopspeak="{{ string .Blog.Lang "stopspeak" }}"></button>
<script defer src="{{ if .Data.TTS }}{{ asset "js/tts.js" }}{{ else }}{{ asset "js/speak.js" }}{{ end }}"></script>
</div>
{{ if .Data.TTS }}<div class="p hide" id="tts"><audio controls preload=none id="tts-audio"><source src="{{ .Data.TTS }}"/></audio></div>{{ end }}
{{ if .Data.Content }}
{{ oldcontentwarning .Data .Blog }}
<div class=e-content>{{ content .Data false }}</div>
{{ end }}
{{ if .Data.HasTrack }}
{{ $track := (gettrack .Data) }}
{{ if $track }}{{ if $track.HasPoints }}
{{ $lang := .Blog.Lang }}
<p>{{ with $track.Name }}<b>{{ . }}</b> {{ end }}{{ with $track.Kilometers }}🏁 {{ . }} {{ string $lang "kilometers" }} {{ end }}{{ with $track.Hours }}⌛ {{ . }}{{ end }}</p>
<div class="p" id="map" data-paths="{{ $track.PathsJSON }}" data-points="{{ $track.PointsJSON }}" data-minzoom={{ $track.MinZoom }} data-maxzoom={{ $track.MaxZoom }} data-attribution="{{ $track.MapAttribution }}"></div>
<script defer src="{{ asset "js/geotrack.js" }}"></script>
{{ end }}{{ end }}
{{ end }}
{{ posttax .Data .Blog }}
</article>
{{ author }}
</main>
{{ if .LoggedIn }}
<div id="posteditactions" class="p">
<form class="in" method="post" action="{{ .Blog.RelativePath "/editor" }}#update">
<input type="hidden" name="editoraction" value="loadupdate">
<input type="hidden" name="path" value="{{ .Data.Path }}">
<input type="submit" value="{{ string .Blog.Lang "update" }}">
</form>
<form class="in" method="post" action="{{ .Blog.RelativePath "/editor" }}">
<input type="hidden" name="action" value="delete">
<input type="hidden" name="url" value="{{ .Canonical }}">
<input type="submit" value="{{ string .Blog.Lang "delete" }}" class="confirm" data-confirmmessage="{{ string .Blog.Lang "confirmdelete" }}">
</form>
{{ if .Data.Deleted }}
<form class="in" method="post" action="{{ .Blog.RelativePath "/editor" }}">
<input type="hidden" name="action" value="undelete">
<input type="hidden" name="url" value="{{ .Canonical }}">
<input type="submit" value="{{ string .Blog.Lang "undelete" }}">
</form>
{{ end }}
{{ if ttsenabled }}
<form class="in" method="post" action="{{ .Blog.RelativePath "/editor" }}">
<input type="hidden" name="editoraction" value="tts">
<input type="hidden" name="url" value="{{ .Canonical }}">
<input type="submit" value="{{ string .Blog.Lang "gentts" }}">
</form>
{{ end }}
<script defer src="{{ asset "js/formconfirm.js" }}"></script>
</div>
{{ end }}
{{ if .CommentsEnabled }}{{ interactions .Blog .Canonical }}{{ end }}
{{ end }}
{{ define "post" }}
{{ template "base" . }}
{{ end }}

View File

@ -1,21 +0,0 @@
{{ define "title" }}
<title>{{ with .Blog.Search.Title }}{{ mdtitle . }} - {{ end }}{{ mdtitle .Blog.Title }}</title>
{{ end }}
{{ define "main" }}
<main>
{{ with .Blog.Search.Title }}<h1>{{ mdtitle . }}</h1>{{ end }}
{{ with .Blog.Search.Description }}{{ md . }}{{ end }}
{{ if (or .Blog.Search.Title .Blog.Search.Description) }}
<hr>
{{ end }}
<form class="fw p" method="post">
<input type="text" name="q" {{ with .Blog.Search.Placeholder }}placeholder="{{ mdtitle . }}"{{ end }}>
<input type="submit" value="🔍 {{ string .Blog.Lang "search" }}">
</form>
</main>
{{ end }}
{{ define "search" }}
{{ template "base" . }}
{{ end }}

View File

@ -1,27 +0,0 @@
{{ define "title" }}
<title>{{ mdtitle .Blog.Title }}</title>
{{ postheadmeta .Data .Canonical }}
{{ end }}
{{ define "main" }}
<main class=h-entry>
<article>
<data class="u-url hide" value="{{ absolute .Data.Path }}"></data>
{{ if .Data.Content }}<div class=e-content>{{ content .Data false }}</div>{{ end }}
</article>
{{ author }}
</main>
{{ if .LoggedIn }}
<div id="posteditactions" class="p">
<form class="in" method="post" action="{{ .Blog.RelativePath "/editor" }}#update">
<input type="hidden" name="editoraction" value="loadupdate">
<input type="hidden" name="path" value="{{ .Data.Path }}">
<input type="submit" value="{{ string .Blog.Lang "update" }}">
</form>
</div>
{{ end }}
{{ end }}
{{ define "statichome" }}
{{ template "base" . }}
{{ end }}

View File

@ -1,24 +0,0 @@
{{ define "title" }}
<title>{{ mdtitle .Data.Taxonomy.Title }} - {{ mdtitle .Blog.Title }}</title>
{{ end }}
{{ define "main" }}
<main>
{{ with .Data.Taxonomy.Title }}<h1>{{ mdtitle . }}</h1>{{ end }}
{{ with .Data.Taxonomy.Description }}{{ md . }}{{ end }}
{{ $blog := .Blog }}
{{ $taxonomy := .Data.Taxonomy.Name }}
{{ range $i, $valueGroup := .Data.ValueGroups }}
<h2>{{ $valueGroup.Identifier }}</h2>
<p>
{{ range $i, $value := $valueGroup.Strings }}
{{ if ne $i 0 }} &bull; {{ end }}<a href="{{ $blog.RelativePath ( printf "/%s/%s" $taxonomy (urlize .) ) }}">{{ . }}</a>
{{ end }}
</p>
{{ end }}
</main>
{{ end }}
{{ define "taxonomy" }}
{{ template "base" . }}
{{ end }}

1425
ui.go

File diff suppressed because it is too large Load Diff

386
uiComponents.go Normal file
View File

@ -0,0 +1,386 @@
package main
import "fmt"
type summaryTyp string
const (
defaultSummary summaryTyp = "summary"
photoSummary summaryTyp = "photosummary"
)
// post summary on index pages
func (a *goBlog) renderSummary(hb *htmlBuilder, bc *configBlog, p *post, typ summaryTyp) {
if bc == nil || p == nil {
return
}
if typ == "" {
typ = defaultSummary
}
// Start article
hb.writeElementOpen("article", "class", "h-entry border-bottom")
if p.Priority > 0 {
// Is pinned post
hb.writeElementOpen("p")
hb.writeEscaped("📌 ")
hb.writeEscaped(a.ts.GetTemplateStringVariant(bc.Lang, "pinned"))
hb.writeElementClose("p")
}
if p.RenderedTitle != "" {
// Has title
hb.writeElementOpen("h2", "class", "p-name")
hb.writeElementOpen("a", "class", "u-url", "href", p.Path)
hb.writeEscaped(p.RenderedTitle)
hb.writeElementClose("a")
hb.writeElementClose("h2")
}
// Show photos in photo summary
photos := a.photoLinks(p)
if typ == photoSummary && len(photos) > 0 {
for _, photo := range photos {
hb.write(string(a.safeRenderMarkdownAsHTML(fmt.Sprintf("![](%s)", photo))))
}
}
// Post meta
a.renderPostMeta(hb, p, bc, "summary")
if typ != photoSummary && a.showFull(p) {
// Show full content
hb.writeElementOpen("div", "class", "e-content")
hb.write(string(a.postHtml(p, false)))
hb.writeElementClose("div")
} else {
// Show summary
hb.writeElementOpen("p", "class", "p-summary")
hb.writeEscaped(a.postSummary(p))
hb.writeElementClose("p")
}
// Show link to full post
hb.writeElementOpen("p")
if len(photos) > 0 {
// Contains photos
hb.writeEscaped("🖼️ ")
}
hb.writeElementOpen("a", "class", "u-url", "href", p.Path)
hb.writeEscaped(a.ts.GetTemplateStringVariant(bc.Lang, "view"))
hb.writeElementClose("a")
hb.writeElementClose("p")
// Finish article
hb.writeElementClose("article")
}
// list of post taxonomy values (tags, series, etc.)
func (a *goBlog) renderPostTax(hb *htmlBuilder, p *post, b *configBlog) {
if b == nil || p == nil {
return
}
// Iterate over all taxonomies
for _, tax := range b.Taxonomies {
// Get all sorted taxonomy values for this post
if taxValues := sortedStrings(p.Parameters[tax.Name]); len(taxValues) > 0 {
// Start new paragraph
hb.writeElementOpen("p")
// Add taxonomy name
hb.writeElementOpen("strong")
hb.writeEscaped(a.renderMdTitle(tax.Title))
hb.writeElementClose("strong")
hb.write(": ")
// Add taxonomy values
for i, taxValue := range taxValues {
if i > 0 {
hb.write(", ")
}
hb.writeElementOpen(
"a",
"class", "p-category",
"rel", "tag",
"href", b.getRelativePath(fmt.Sprintf("/%s/%s", tax.Name, urlize(taxValue))),
)
hb.writeEscaped(a.renderMdTitle(taxValue))
hb.writeElementClose("a")
}
// End paragraph
hb.writeElementClose("p")
}
}
}
// post meta information.
// typ can be "summary", "post" or "preview".
func (a *goBlog) renderPostMeta(hb *htmlBuilder, p *post, b *configBlog, typ string) {
if b == nil || p == nil || typ != "summary" && typ != "post" && typ != "preview" {
return
}
if typ == "summary" || typ == "post" {
hb.writeElementOpen("div", "class", "p")
}
// Published time
if published := p.Published; published != "" {
hb.writeElementOpen("div")
hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "publishedon"))
hb.write(" ")
hb.writeElementOpen("time", "class", "dt-published", "datetime", dateFormat(published, "2006-01-02T15:04:05Z07:00"))
hb.writeEscaped(isoDateFormat(published))
hb.writeElementClose("time")
// Section
if p.Section != "" {
if section := b.Sections[p.Section]; section != nil {
hb.write(" in ") // TODO: Replace with a proper translation
hb.writeElementOpen("a", "href", b.getRelativePath(section.Name))
hb.writeEscaped(a.renderMdTitle(section.Title))
hb.writeElementClose("a")
}
}
hb.writeElementClose("div")
}
// Updated time
if updated := p.Updated; updated != "" {
hb.writeElementOpen("div")
hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "updatedon"))
hb.write(" ")
hb.writeElementOpen("time", "class", "dt-updated", "datetime", dateFormat(updated, "2006-01-02T15:04:05Z07:00"))
hb.writeEscaped(isoDateFormat(updated))
hb.writeElementClose("time")
hb.writeElementClose("div")
}
// IndieWeb Meta
// Reply ("u-in-reply-to")
if replyLink := a.replyLink(p); replyLink != "" {
hb.writeElementOpen("div")
hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "replyto"))
hb.writeEscaped(": ")
hb.writeElementOpen("a", "class", "u-in-reply-to", "rel", "noopener", "target", "_blank", "href", replyLink)
if replyTitle := a.replyTitle(p); replyTitle != "" {
hb.writeEscaped(replyTitle)
} else {
hb.writeEscaped(replyLink)
}
hb.writeElementClose("a")
hb.writeElementClose("div")
}
// Like ("u-like-of")
if likeLink := a.likeLink(p); likeLink != "" {
hb.writeElementOpen("div")
hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "likeof"))
hb.writeEscaped(": ")
hb.writeElementOpen("a", "class", "u-like-of", "rel", "noopener", "target", "_blank", "href", likeLink)
if likeTitle := a.likeTitle(p); likeTitle != "" {
hb.writeEscaped(likeTitle)
} else {
hb.writeEscaped(likeLink)
}
hb.writeElementClose("div")
}
// Geo
if geoURI := a.geoURI(p); geoURI != nil {
hb.writeElementOpen("div")
hb.writeEscaped("📍 ")
hb.writeElementOpen("a", "class", "p-location h-geo", "target", "_blank", "rel", "nofollow noopener noreferrer", "href", geoOSMLink(geoURI))
hb.writeElementOpen("span", "class", "p-name")
hb.writeEscaped(a.geoTitle(geoURI, b.Lang))
hb.writeElementClose("span")
hb.writeElementOpen("data", "class", "p-longitude", "value", fmt.Sprintf("%f", geoURI.Longitude))
hb.writeElementClose("data")
hb.writeElementOpen("data", "class", "p-latitude", "value", fmt.Sprintf("%f", geoURI.Latitude))
hb.writeElementClose("data")
hb.writeElementClose("a")
hb.writeElementClose("div")
}
// Post specific elements
if typ == "post" {
// Translations
if translations := a.postTranslations(p); len(translations) > 0 {
hb.writeElementOpen("div")
hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "translations"))
hb.writeEscaped(": ")
for i, translation := range translations {
if i > 0 {
hb.writeEscaped(", ")
}
hb.writeElementOpen("a", "translate", "no", "href", translation.Path)
hb.writeEscaped(translation.RenderedTitle)
hb.writeElementClose("a")
}
hb.writeElementClose("div")
}
// Short link
if shortLink := a.shortPostURL(p); shortLink != "" {
hb.writeElementOpen("div")
hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "shorturl"))
hb.writeEscaped(" ")
hb.writeElementOpen("a", "rel", "shortlink", "href", shortLink)
hb.writeEscaped(shortLink)
hb.writeElementClose("a")
hb.writeElementClose("div")
}
// Status
if p.Status != statusPublished {
hb.writeElementOpen("div")
hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "status"))
hb.writeEscaped(": ")
hb.writeEscaped(string(p.Status))
hb.writeElementClose("div")
}
}
if typ == "summary" || typ == "post" {
hb.writeElementClose("div")
}
}
// warning for old posts
func (a *goBlog) renderOldContentWarning(hb *htmlBuilder, p *post, b *configBlog) {
if b == nil || p == nil || !p.Old() {
return
}
hb.writeElementOpen("strong", "class", "p border-top border-bottom")
hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "oldcontent"))
hb.writeElementClose("strong")
}
func (a *goBlog) renderInteractions(hb *htmlBuilder, b *configBlog, canonical string) {
if b == nil || canonical == "" {
return
}
// Start accordion
hb.writeElementOpen("details", "class", "p", "id", "interactions")
hb.writeElementOpen("summary")
hb.writeElementOpen("strong")
hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "interactions"))
hb.writeElementClose("strong")
hb.writeElementClose("summary")
// Render mentions
var renderMentions func(m []*mention)
renderMentions = func(m []*mention) {
if len(m) == 0 {
return
}
hb.writeElementOpen("ul")
for _, mention := range m {
hb.writeElementOpen("li")
hb.writeElementOpen("a", "href", mention.Url, "target", "_blank", "rel", "nofollow noopener noreferrer ugc")
hb.writeEscaped(defaultIfEmpty(mention.Author, mention.Url))
hb.writeElementClose("a")
if mention.Title != "" {
hb.write(" ")
hb.writeElementOpen("strong")
hb.writeEscaped(mention.Title)
hb.writeElementClose("strong")
}
if mention.Content != "" {
hb.write(" ")
hb.writeElementOpen("i")
hb.writeEscaped(mention.Content)
hb.writeElementClose("i")
}
if len(mention.Submentions) > 0 {
renderMentions(mention.Submentions)
}
hb.writeElementClose("li")
}
hb.writeElementClose("ul")
}
renderMentions(a.db.getWebmentionsByAddress(canonical))
// Show form to send a webmention
hb.writeElementOpen("form", "class", "fw p", "method", "post", "action", "/webmention")
hb.writeElementOpen("label", "for", "wm-source", "class", "p")
hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "interactionslabel"))
hb.writeElementClose("label")
hb.writeElementOpen("input", "id", "wm-source", "type", "url", "name", "source", "placeholder", "URL", "required", "")
hb.writeElementOpen("input", "type", "hidden", "name", "target", "value", canonical)
hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(b.Lang, "send"))
hb.writeElementClose("form")
// Show form to create a new comment
hb.writeElementOpen("form", "class", "fw p", "method", "post", "action", "/comment")
hb.writeElementOpen("input", "type", "hidden", "name", "target", "value", canonical)
hb.writeElementOpen("input", "type", "text", "name", "name", "placeholder", a.ts.GetTemplateStringVariant(b.Lang, "nameopt"))
hb.writeElementOpen("input", "type", "url", "name", "website", "placeholder", a.ts.GetTemplateStringVariant(b.Lang, "websiteopt"))
hb.writeElementOpen("textarea", "name", "comment", "required", "", "placeholder", a.ts.GetTemplateStringVariant(b.Lang, "comment"))
hb.writeElementClose("textarea")
hb.writeElementOpen("input", "type", "submit", "value", a.ts.GetTemplateStringVariant(b.Lang, "docomment"))
hb.writeElementClose("form")
// Finish accordion
hb.writeElementClose("details")
}
// author h-card
func (a *goBlog) renderAuthor(hb *htmlBuilder) {
user := a.cfg.User
if user == nil {
return
}
hb.writeElementOpen("div", "class", "p-author h-card hide")
if user.Picture != "" {
hb.writeElementOpen("data", "class", "u-photo", "value", user.Picture)
hb.writeElementClose("data")
}
if user.Name != "" {
hb.writeElementOpen("a", "class", "p-name u-url", "rel", "me", "href", defaultIfEmpty(user.Link, "/"))
hb.writeEscaped(user.Name)
hb.writeElementClose("a")
}
hb.writeElementClose("div")
}
// head meta tags for a post
func (a *goBlog) renderPostHeadMeta(hb *htmlBuilder, p *post, canonical string) {
if p == nil {
return
}
if canonical != "" {
hb.writeElementOpen("meta", "property", "og:url", "content", canonical)
hb.writeElementOpen("meta", "property", "twitter:url", "content", canonical)
}
if p.RenderedTitle != "" {
hb.writeElementOpen("meta", "property", "og:title", "content", p.RenderedTitle)
hb.writeElementOpen("meta", "property", "twitter:title", "content", p.RenderedTitle)
}
if summary := a.postSummary(p); summary != "" {
hb.writeElementOpen("meta", "name", "description", "content", summary)
hb.writeElementOpen("meta", "property", "og:description", "content", summary)
hb.writeElementOpen("meta", "property", "twitter:description", "content", summary)
}
if p.Published != "" {
hb.writeElementOpen("meta", "itemprop", "datePublished", "content", dateFormat(p.Published, "2006-01-02T15:04:05-07:00"))
}
if p.Updated != "" {
hb.writeElementOpen("meta", "itemprop", "dateModified", "content", dateFormat(p.Updated, "2006-01-02T15:04:05-07:00"))
}
for _, img := range a.photoLinks(p) {
hb.writeElementOpen("meta", "itemprop", "image", "content", img)
hb.writeElementOpen("meta", "property", "og:image", "content", img)
hb.writeElementOpen("meta", "property", "twitter:image", "content", img)
}
}
// TOR notice in the footer
func (a *goBlog) renderTorNotice(hb *htmlBuilder, rd *renderData) {
if !a.cfg.Server.Tor || (!rd.TorUsed && rd.TorAddress == "") {
return
}
if rd.TorUsed {
hb.writeElementOpen("p", "id", "tor")
hb.writeEscaped("🔐 ")
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "connectedviator"))
hb.writeElementClose("p")
} else if rd.TorAddress != "" {
hb.writeElementOpen("p", "id", "tor")
hb.writeEscaped("🔓 ")
hb.writeElementOpen("a", "href", rd.TorAddress)
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "connectviator"))
hb.writeElementClose("a")
hb.writeEscaped(" ")
hb.writeElementOpen("a", "href", "https://www.torproject.org/", "target", "_blank", "rel", "nofollow noopener noreferrer")
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "whatistor"))
hb.writeElementClose("a")
hb.writeElementClose("p")
}
}
func (a *goBlog) renderTitleTag(hb *htmlBuilder, blog *configBlog, optionalTitle string) {
hb.writeElementOpen("title")
if optionalTitle != "" {
hb.writeEscaped(optionalTitle)
hb.writeEscaped(" - ")
}
hb.writeEscaped(a.renderMdTitle(blog.Title))
hb.writeElementClose("title")
}

98
uiHtmlBuilder.go Normal file
View File

@ -0,0 +1,98 @@
package main
import (
"bytes"
"fmt"
"html/template"
"io"
textTemplate "text/template"
)
type htmlBuilder struct {
w io.Writer
buf bytes.Buffer
}
func newHtmlBuilder(w io.Writer) *htmlBuilder {
return &htmlBuilder{
w: w,
}
}
func (h *htmlBuilder) getWriter() io.Writer {
if h.w != nil {
return h.w
}
return &h.buf
}
func (h *htmlBuilder) Write(p []byte) (int, error) {
return h.getWriter().Write(p)
}
func (h *htmlBuilder) WriteString(s string) (int, error) {
return io.WriteString(h.getWriter(), s)
}
func (h *htmlBuilder) Read(p []byte) (int, error) {
return h.buf.Read(p)
}
func (h *htmlBuilder) String() string {
return h.buf.String()
}
func (h *htmlBuilder) Bytes() []byte {
return h.buf.Bytes()
}
func (h *htmlBuilder) html() template.HTML {
return template.HTML(h.String())
}
func (h *htmlBuilder) write(s string) {
_, _ = h.WriteString(s)
}
func (h *htmlBuilder) writeHtml(s template.HTML) {
h.write(string(s))
}
func (h *htmlBuilder) writeEscaped(s string) {
textTemplate.HTMLEscape(h, []byte(s))
}
func (h *htmlBuilder) writeAttribute(attr string, val interface{}) {
h.write(` `)
h.write(attr)
h.write(`=`)
if valStr, ok := val.(string); ok {
h.write(`"`)
h.writeEscaped(valStr)
h.write(`"`)
} else {
h.writeEscaped(fmt.Sprint(val))
}
}
func (h *htmlBuilder) writeElementOpen(tag string, attrs ...interface{}) {
h.write(`<`)
h.write(tag)
for i := 0; i < len(attrs); i += 2 {
if i+2 > len(attrs) {
break
}
attrStr, ok := attrs[i].(string)
if !ok {
continue
}
h.writeAttribute(attrStr, attrs[i+1])
}
h.write(`>`)
}
func (h *htmlBuilder) writeElementClose(tag string) {
h.write(`</`)
h.write(tag)
h.write(`>`)
}

View File

@ -2,6 +2,7 @@ package main
import (
"html/template"
"io"
"os"
"strings"
"testing"
@ -11,6 +12,10 @@ import (
"github.com/stretchr/testify/require"
)
var _ io.Writer = &htmlBuilder{}
var _ io.StringWriter = &htmlBuilder{}
var _ io.Reader = &htmlBuilder{}
func Test_renderPostTax(t *testing.T) {
app := &goBlog{
cfg: createDefaultTestConfig(t),
@ -137,30 +142,3 @@ func Test_renderAuthor(t *testing.T) {
assert.Equal(t, template.HTML("<div class=\"p-author h-card hide\"><data class=\"u-photo\" value=\"https://example.com/picture.jpg\"></data><a class=\"p-name u-url\" rel=\"me\" href=\"/\">John Doe</a></div>"), res)
}
func Test_renderTorNotice(t *testing.T) {
app := &goBlog{
cfg: createDefaultTestConfig(t),
}
_ = app.initConfig()
_ = app.initDatabase(false)
app.initComponents(false)
app.cfg.Server.Tor = true
var hb htmlBuilder
app.renderTorNotice(&hb, app.cfg.Blogs["default"], true, "http://abc.onion:80/test")
res := hb.html()
_, err := goquery.NewDocumentFromReader(strings.NewReader(string(res)))
require.NoError(t, err)
assert.Equal(t, template.HTML("<p id=\"tor\">🔐 Connected via Tor.</p>"), res)
hb.Reset()
app.renderTorNotice(&hb, app.cfg.Blogs["default"], false, "http://abc.onion:80/test")
res = hb.html()
_, err = goquery.NewDocumentFromReader(strings.NewReader(string(res)))
require.NoError(t, err)
assert.Equal(t, template.HTML("<p id=\"tor\">🔓 <a href=\"http://abc.onion:80/test\">Connect via Tor.</a> <a href=\"https://www.torproject.org/\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">What is Tor?</a></p>"), res)
}