Use more writers and streaming when possible

This commit is contained in:
Jan-Lukas Else 2022-02-11 16:19:10 +01:00
parent 5771a945cf
commit 68daab64e3
15 changed files with 130 additions and 171 deletions

View File

@ -113,7 +113,7 @@ func (a *goBlog) toASNote(p *post) *asNote {
as.Type = "Note" as.Type = "Note"
} }
// Content // Content
as.Content = string(a.postHtml(p, true)) as.Content = a.postHtml(p, true)
// Attachments // Attachments
if images := p.Parameters[a.cfg.Micropub.PhotoParam]; len(images) > 0 { if images := p.Parameters[a.cfg.Micropub.PhotoParam]; len(images) > 0 {
for _, image := range images { for _, image := range images {

View File

@ -12,6 +12,7 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"go.goblog.app/app/pkgs/bufferpool"
"golang.org/x/sync/singleflight" "golang.org/x/sync/singleflight"
) )
@ -126,7 +127,10 @@ func (a *goBlog) checkLinks(w io.Writer, posts ...*post) error {
func (a *goBlog) allLinks(posts ...*post) (allLinks []*stringPair, err error) { func (a *goBlog) allLinks(posts ...*post) (allLinks []*stringPair, err error) {
for _, p := range posts { for _, p := range posts {
links, err := allLinksFromHTMLString(string(a.postHtml(p, true)), a.fullPostURL(p)) contentBuf := bufferpool.Get()
a.postHtmlToWriter(contentBuf, p, true)
links, err := allLinksFromHTML(contentBuf, a.fullPostURL(p))
bufferpool.Put(contentBuf)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -45,7 +45,7 @@ func (a *goBlog) serveEditorPreview(w http.ResponseWriter, r *http.Request) {
continue continue
} }
// Create preview // Create preview
preview, err := a.createMarkdownPreview(blog, message) preview, err := a.createMarkdownPreview(blog, string(message))
if err != nil { if err != nil {
preview = []byte(err.Error()) preview = []byte(err.Error())
} }
@ -57,12 +57,10 @@ func (a *goBlog) serveEditorPreview(w http.ResponseWriter, r *http.Request) {
} }
} }
func (a *goBlog) createMarkdownPreview(blog string, markdown []byte) (rendered []byte, err error) { func (a *goBlog) createMarkdownPreview(blog string, markdown string) (rendered []byte, err error) {
p := &post{ p := &post{
Content: string(markdown), Blog: blog,
Blog: blog, Content: markdown,
Path: "/editor/preview",
Published: localNowString(),
} }
err = a.computeExtraPostParameters(p) err = a.computeExtraPostParameters(p)
if err != nil { if err != nil {

View File

@ -8,6 +8,7 @@ import (
"github.com/araddon/dateparse" "github.com/araddon/dateparse"
"github.com/jlelse/feeds" "github.com/jlelse/feeds"
"go.goblog.app/app/pkgs/bufferpool"
"go.goblog.app/app/pkgs/contenttype" "go.goblog.app/app/pkgs/contenttype"
) )
@ -38,16 +39,18 @@ func (a *goBlog) generateFeed(blog string, f feedType, w http.ResponseWriter, r
}, },
} }
for _, p := range posts { for _, p := range posts {
content, _ := a.min.MinifyString(contenttype.HTML, a.feedHtml(p)) buf := bufferpool.Get()
a.feedHtml(buf, p)
feed.Add(&feeds.Item{ feed.Add(&feeds.Item{
Title: p.RenderedTitle, Title: p.RenderedTitle,
Link: &feeds.Link{Href: a.fullPostURL(p)}, Link: &feeds.Link{Href: a.fullPostURL(p)},
Description: a.postSummary(p), Description: a.postSummary(p),
Id: p.Path, Id: p.Path,
Content: content, Content: buf.String(),
Created: timeNoErr(dateparse.ParseLocal(p.Published)), Created: timeNoErr(dateparse.ParseLocal(p.Published)),
Updated: timeNoErr(dateparse.ParseLocal(p.Updated)), Updated: timeNoErr(dateparse.ParseLocal(p.Updated)),
}) })
bufferpool.Put(buf)
} }
var err error var err error
var feedBuffer bytes.Buffer var feedBuffer bytes.Buffer

View File

@ -2,7 +2,6 @@ package main
import ( import (
"bytes" "bytes"
"html/template"
"io" "io"
marktag "git.jlel.se/jlelse/goldmark-mark" marktag "git.jlel.se/jlelse/goldmark-mark"
@ -94,19 +93,6 @@ func (a *goBlog) renderMarkdownToWriter(w io.Writer, source string, absoluteLink
return err return err
} }
func (a *goBlog) renderMarkdownAsHTML(source string, absoluteLinks bool) (rendered template.HTML, err error) {
b, err := a.renderMarkdown(source, absoluteLinks)
if err != nil {
return "", err
}
return template.HTML(b), nil
}
func (a *goBlog) safeRenderMarkdownAsHTML(source string) template.HTML {
h, _ := a.renderMarkdownAsHTML(source, false)
return h
}
func (a *goBlog) renderText(s string) string { func (a *goBlog) renderText(s string) string {
if s == "" { if s == "" {
return "" return ""

View File

@ -79,11 +79,6 @@ func Test_markdown(t *testing.T) {
assert.Equal(t, "Tests", app.renderMdTitle("Test's")) assert.Equal(t, "Tests", app.renderMdTitle("Test's"))
assert.Equal(t, "😂", app.renderMdTitle(":joy:")) assert.Equal(t, "😂", app.renderMdTitle(":joy:"))
assert.Equal(t, "<b></b>", app.renderMdTitle("<b></b>")) assert.Equal(t, "<b></b>", app.renderMdTitle("<b></b>"))
// Template func
renderedText = string(app.safeRenderMarkdownAsHTML("[Relative](/relative)"))
assert.Contains(t, renderedText, `href="/relative"`)
}) })
} }

View File

@ -10,6 +10,7 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/spf13/cast" "github.com/spf13/cast"
"github.com/thoas/go-funk" "github.com/thoas/go-funk"
@ -397,7 +398,7 @@ func (a *goBlog) computeExtraPostParameters(p *post) error {
} }
if p.Published == "" && p.Section != "" { if p.Published == "" && p.Section != "" {
// Has no published date, but section -> published now // Has no published date, but section -> published now
p.Published = localNowString() p.Published = time.Now().Local().Format(time.RFC3339)
} }
// Add images not in content // Add images not in content
images := p.Parameters[a.cfg.Micropub.PhotoParam] images := p.Parameters[a.cfg.Micropub.PhotoParam]

View File

@ -4,12 +4,10 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"html/template"
"net/http" "net/http"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
@ -31,8 +29,6 @@ type post struct {
// Not persisted // Not persisted
Slug string Slug string
RenderedTitle string RenderedTitle string
renderCache map[bool]template.HTML
renderMutex sync.RWMutex
} }
type postStatus string type postStatus string

View File

@ -2,13 +2,13 @@ package main
import ( import (
"fmt" "fmt"
"html/template" "io"
"log"
"strings" "strings"
"time" "time"
gogeouri "git.jlel.se/jlelse/go-geouri" gogeouri "git.jlel.se/jlelse/go-geouri"
"github.com/araddon/dateparse" "github.com/araddon/dateparse"
"go.goblog.app/app/pkgs/bufferpool"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@ -34,69 +34,56 @@ func (p *post) firstParameter(parameter string) (result string) {
return return
} }
func (a *goBlog) postHtml(p *post, absolute bool) template.HTML { func (a *goBlog) postHtml(p *post, absolute bool) (res string) {
p.renderMutex.RLock() buf := bufferpool.Get()
// Check cache a.postHtmlToWriter(buf, p, absolute)
if r, ok := p.renderCache[absolute]; ok && r != "" { res = buf.String()
p.renderMutex.RUnlock() bufferpool.Put(buf)
return r return
}
p.renderMutex.RUnlock()
// No cache, build it...
p.renderMutex.Lock()
defer p.renderMutex.Unlock()
// Build HTML
var htmlBuilder strings.Builder
// Add audio to the top
for _, a := range p.Parameters[a.cfg.Micropub.AudioParam] {
htmlBuilder.WriteString(`<audio controls preload=none><source src="`)
htmlBuilder.WriteString(a)
htmlBuilder.WriteString(`"/></audio>`)
}
// Render markdown
htmlContent, err := a.renderMarkdown(p.Content, absolute)
if err != nil {
log.Fatal(err)
return ""
}
htmlBuilder.Write(htmlContent)
// Add bookmark links to the bottom
for _, l := range p.Parameters[a.cfg.Micropub.BookmarkParam] {
htmlBuilder.WriteString(`<p><a class=u-bookmark-of href="`)
htmlBuilder.WriteString(l)
htmlBuilder.WriteString(`" target=_blank rel=noopener>`)
htmlBuilder.WriteString(l)
htmlBuilder.WriteString(`</a></p>`)
}
// Cache
html := template.HTML(htmlBuilder.String())
if p.renderCache == nil {
p.renderCache = map[bool]template.HTML{}
}
p.renderCache[absolute] = html
return html
} }
func (a *goBlog) feedHtml(p *post) string { func (a *goBlog) postHtmlToWriter(w io.Writer, p *post, absolute bool) {
var htmlBuilder strings.Builder // Build HTML
hb := newHtmlBuilder(w)
// Add audio to the top
for _, a := range p.Parameters[a.cfg.Micropub.AudioParam] {
hb.writeElementOpen("audio", "controls", "preload", "none")
hb.writeElementOpen("source", "src", a)
hb.writeElementClose("source")
hb.writeElementClose("audio")
}
// Render markdown
_ = a.renderMarkdownToWriter(w, p.Content, absolute)
// Add bookmark links to the bottom
for _, l := range p.Parameters[a.cfg.Micropub.BookmarkParam] {
hb.writeElementOpen("p")
hb.writeElementOpen("a", "class", "u-bookmark-of", "href", l, "target", "_blank", "rel", "noopener noreferrer")
hb.writeEscaped(l)
hb.writeElementClose("a")
hb.writeElementClose("p")
}
}
func (a *goBlog) feedHtml(w io.Writer, p *post) {
hb := newHtmlBuilder(w)
// Add TTS audio to the top // Add TTS audio to the top
for _, a := range p.Parameters[ttsParameter] { for _, a := range p.Parameters[ttsParameter] {
htmlBuilder.WriteString(`<audio controls preload=none><source src="`) hb.writeElementOpen("audio", "controls", "preload", "none")
htmlBuilder.WriteString(a) hb.writeElementOpen("source", "src", a)
htmlBuilder.WriteString(`"/></audio>`) hb.writeElementClose("source")
hb.writeElementClose("audio")
} }
// Add post HTML // Add post HTML
htmlBuilder.WriteString(string(a.postHtml(p, true))) a.postHtmlToWriter(hb, p, true)
// Add link to interactions and comments // Add link to interactions and comments
blogConfig := a.cfg.Blogs[defaultIfEmpty(p.Blog, a.cfg.DefaultBlog)] blogConfig := a.cfg.Blogs[defaultIfEmpty(p.Blog, a.cfg.DefaultBlog)]
if cc := blogConfig.Comments; cc != nil && cc.Enabled { if cc := blogConfig.Comments; cc != nil && cc.Enabled {
htmlBuilder.WriteString(`<p><a href="`) hb.writeElementOpen("p")
htmlBuilder.WriteString(a.getFullAddress(p.Path)) hb.writeElementOpen("a", "href", a.getFullAddress(p.Path)+"#interactions")
htmlBuilder.WriteString(`#interactions">`) hb.writeEscaped(a.ts.GetTemplateStringVariant(blogConfig.Lang, "interactions"))
htmlBuilder.WriteString(a.ts.GetTemplateStringVariant(blogConfig.Lang, "interactions")) hb.writeElementClose("a")
htmlBuilder.WriteString(`</a></p>`) hb.writeElementClose("p")
} }
return htmlBuilder.String()
} }
const summaryDivider = "<!--more-->" const summaryDivider = "<!--more-->"
@ -106,11 +93,12 @@ func (a *goBlog) postSummary(p *post) (summary string) {
if summary != "" { if summary != "" {
return return
} }
html := string(a.postHtml(p, false)) if splitted := strings.Split(p.Content, summaryDivider); len(splitted) > 1 {
if splitted := strings.Split(html, summaryDivider); len(splitted) > 1 { rendered, _ := a.renderMarkdown(splitted[0], false)
summary = htmlText(splitted[0]) summary = htmlText(string(rendered))
} else { } else {
summary = strings.Split(htmlText(html), "\n\n")[0] rendered, _ := a.renderMarkdown(splitted[0], false)
summary = strings.Split(htmlText(string(rendered)), "\n\n")[0]
} }
return return
} }

2
tts.go
View File

@ -65,7 +65,7 @@ func (a *goBlog) createPostTTSAudio(p *post) error {
parts = append(parts, a.renderMdTitle(title)) parts = append(parts, a.renderMdTitle(title))
} }
// Add body split into paragraphs because of 5000 character limit // Add body split into paragraphs because of 5000 character limit
parts = append(parts, strings.Split(htmlText(string(a.postHtml(p, false))), "\n\n")...) parts = append(parts, strings.Split(htmlText(a.postHtml(p, false)), "\n\n")...)
// Create TTS audio for each part // Create TTS audio for each part
partsBuffers := make([]io.Reader, len(parts)) partsBuffers := make([]io.Reader, len(parts))

68
ui.go
View File

@ -2,7 +2,6 @@ package main
import ( import (
"fmt" "fmt"
"strings"
"time" "time"
"github.com/hacdias/indieauth" "github.com/hacdias/indieauth"
@ -20,7 +19,7 @@ func (a *goBlog) renderEditorPreview(hb *htmlBuilder, bc *configBlog, p *post) {
a.renderPostMeta(hb, p, bc, "preview") a.renderPostMeta(hb, p, bc, "preview")
if p.Content != "" { if p.Content != "" {
hb.writeElementOpen("div") hb.writeElementOpen("div")
hb.writeHtml(a.postHtml(p, true)) a.postHtmlToWriter(hb, p, true)
hb.writeElementClose("div") hb.writeElementClose("div")
} }
a.renderPostTax(hb, p, bc) a.renderPostTax(hb, p, bc)
@ -859,14 +858,11 @@ func (a *goBlog) renderPost(hb *htmlBuilder, rd *renderData) {
if !ok { if !ok {
return return
} }
postHtml := a.postHtml(p, false)
a.renderBase( a.renderBase(
hb, rd, hb, rd,
func(hb *htmlBuilder) { func(hb *htmlBuilder) {
a.renderTitleTag(hb, rd.Blog, p.RenderedTitle) a.renderTitleTag(hb, rd.Blog, p.RenderedTitle)
if strings.Contains(string(postHtml), "c-chroma") { hb.writeElementOpen("link", "rel", "stylesheet", "href", a.assetFileName("css/chroma.css"))
hb.writeElementOpen("link", "rel", "stylesheet", "href", a.assetFileName("css/chroma.css"))
}
a.renderPostHeadMeta(hb, p, rd.Canonical) a.renderPostHeadMeta(hb, p, rd.Canonical)
if su := a.shortPostURL(p); su != "" { if su := a.shortPostURL(p); su != "" {
hb.writeElementOpen("link", "rel", "shortlink", "href", su) hb.writeElementOpen("link", "rel", "shortlink", "href", su)
@ -924,7 +920,7 @@ func (a *goBlog) renderPost(hb *htmlBuilder, rd *renderData) {
if p.Content != "" { if p.Content != "" {
// Content // Content
hb.writeElementOpen("div", "class", "e-content") hb.writeElementOpen("div", "class", "e-content")
hb.writeHtml(postHtml) a.postHtmlToWriter(hb, p, false)
hb.writeElementClose("div") hb.writeElementClose("div")
} }
// GPS Track // GPS Track
@ -1027,7 +1023,7 @@ func (a *goBlog) renderStaticHome(hb *htmlBuilder, rd *renderData) {
if p.Content != "" { if p.Content != "" {
// Content // Content
hb.writeElementOpen("div", "class", "e-content") hb.writeElementOpen("div", "class", "e-content")
hb.writeHtml(a.postHtml(p, false)) a.postHtmlToWriter(hb, p, false)
hb.writeElementClose("div") hb.writeElementClose("div")
} }
// Author // Author
@ -1142,7 +1138,7 @@ func (a *goBlog) renderEditorFiles(hb *htmlBuilder, rd *renderData) {
usesString := a.ts.GetTemplateStringVariant(rd.Blog.Lang, "fileuses") usesString := a.ts.GetTemplateStringVariant(rd.Blog.Lang, "fileuses")
for i, f := range ef.files { for i, f := range ef.files {
hb.writeElementOpen("option", "value", f.Name) hb.writeElementOpen("option", "value", f.Name)
hb.writeEscaped(fmt.Sprintf("%s (%s), %s, ~%d %s", f.Name, isoDateFormat(f.Time.String()), mBytesString(f.Size), ef.uses[i], usesString)) hb.writeEscaped(fmt.Sprintf("%s (%s), %s, ~%d %s", f.Name, f.Time.Local().Format(isoDateFormat), mBytesString(f.Size), ef.uses[i], usesString))
hb.writeElementClose("option") hb.writeElementClose("option")
} }
hb.writeElementClose("select") hb.writeElementClose("select")
@ -1305,6 +1301,7 @@ func (a *goBlog) renderWebmentionAdmin(hb *htmlBuilder, rd *renderData) {
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "webmentions")) hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "webmentions"))
hb.writeElementClose("h1") hb.writeElementClose("h1")
// Notifications // Notifications
tdLocale := matchTimeDiffLocale(rd.Blog.Lang)
for _, m := range wrd.mentions { for _, m := range wrd.mentions {
hb.writeElementOpen("div", "id", fmt.Sprintf("mention-%d", m.ID), "class", "p") hb.writeElementOpen("div", "id", fmt.Sprintf("mention-%d", m.ID), "class", "p")
hb.writeElementOpen("p") hb.writeElementOpen("p")
@ -1330,7 +1327,7 @@ func (a *goBlog) renderWebmentionAdmin(hb *htmlBuilder, rd *renderData) {
hb.writeElementOpen("br") hb.writeElementOpen("br")
// Date // Date
hb.writeEscaped("Created: ") hb.writeEscaped("Created: ")
hb.writeEscaped(unixToLocalDateString(m.Created)) hb.writeEscaped(timediff.TimeDiff(time.Unix(m.Created, 0), timediff.WithLocale(tdLocale)))
hb.writeElementOpen("br") hb.writeElementOpen("br")
hb.writeElementOpen("br") hb.writeElementOpen("br")
// Author // Author
@ -1447,36 +1444,24 @@ func (a *goBlog) renderEditor(hb *htmlBuilder, rd *renderData) {
hb.writeElementOpen("h2") hb.writeElementOpen("h2")
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "posts")) hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "posts"))
hb.writeElementClose("h2") hb.writeElementClose("h2")
// Template
postsListLink := func(path, title string) {
hb.writeElementOpen("p")
hb.writeElementOpen("a", "href", rd.Blog.getRelativePath(path))
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, title))
hb.writeElementClose("a")
hb.writeElementClose("p")
}
// Drafts // Drafts
hb.writeElementOpen("p") postsListLink("/editor/drafts", "drafts")
hb.writeElementOpen("a", "href", rd.Blog.getRelativePath("/editor/drafts"))
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "drafts"))
hb.writeElementClose("a")
hb.writeElementClose("p")
// Private // Private
hb.writeElementOpen("p") postsListLink("/editor/private", "privateposts")
hb.writeElementOpen("a", "href", rd.Blog.getRelativePath("/editor/private"))
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "privateposts"))
hb.writeElementClose("a")
hb.writeElementClose("p")
// Unlisted // Unlisted
hb.writeElementOpen("p") postsListLink("/editor/unlisted", "unlistedposts")
hb.writeElementOpen("a", "href", rd.Blog.getRelativePath("/editor/unlisted"))
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "unlistedposts"))
hb.writeElementClose("a")
hb.writeElementClose("p")
// Scheduled // Scheduled
hb.writeElementOpen("p") postsListLink("/editor/scheduled", "scheduledposts")
hb.writeElementOpen("a", "href", rd.Blog.getRelativePath("/editor/scheduled"))
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "scheduledposts"))
hb.writeElementClose("a")
hb.writeElementClose("p")
// Deleted // Deleted
hb.writeElementOpen("p") postsListLink("/editor/deleted", "deletedposts")
hb.writeElementOpen("a", "href", rd.Blog.getRelativePath("/editor/deleted"))
hb.writeEscaped(a.ts.GetTemplateStringVariant(rd.Blog.Lang, "deletedposts"))
hb.writeElementClose("a")
hb.writeElementClose("p")
// Upload // Upload
hb.writeElementOpen("h2") hb.writeElementOpen("h2")
@ -1524,15 +1509,10 @@ func (a *goBlog) renderEditor(hb *htmlBuilder, rd *renderData) {
hb.writeElementClose("main") hb.writeElementClose("main")
// Scripts // Scripts
// Editor preview for _, script := range []string{"js/mdpreview.js", "js/geohelper.js", "js/formcache.js"} {
hb.writeElementOpen("script", "src", a.assetFileName("js/mdpreview.js"), "defer", "") hb.writeElementOpen("script", "src", a.assetFileName(script), "defer", "")
hb.writeElementClose("script") hb.writeElementClose("script")
// Geohelper }
hb.writeElementOpen("script", "src", a.assetFileName("js/geohelper.js"), "defer", "")
hb.writeElementClose("script")
// Formcache
hb.writeElementOpen("script", "src", a.assetFileName("js/formcache.js"), "defer", "")
hb.writeElementClose("script")
}, },
) )
} }

View File

@ -1,6 +1,9 @@
package main package main
import "fmt" import (
"fmt"
"time"
)
type summaryTyp string type summaryTyp string
@ -46,7 +49,7 @@ func (a *goBlog) renderSummary(hb *htmlBuilder, bc *configBlog, p *post, typ sum
if typ != photoSummary && a.showFull(p) { if typ != photoSummary && a.showFull(p) {
// Show full content // Show full content
hb.writeElementOpen("div", "class", "e-content") hb.writeElementOpen("div", "class", "e-content")
hb.write(string(a.postHtml(p, false))) a.postHtmlToWriter(hb, p, false)
hb.writeElementClose("div") hb.writeElementClose("div")
} else { } else {
// Show summary // Show summary
@ -114,12 +117,12 @@ func (a *goBlog) renderPostMeta(hb *htmlBuilder, p *post, b *configBlog, typ str
hb.writeElementOpen("div", "class", "p") hb.writeElementOpen("div", "class", "p")
} }
// Published time // Published time
if published := p.Published; published != "" { if published := toLocalTime(p.Published); !published.IsZero() {
hb.writeElementOpen("div") hb.writeElementOpen("div")
hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "publishedon")) hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "publishedon"))
hb.write(" ") hb.write(" ")
hb.writeElementOpen("time", "class", "dt-published", "datetime", dateFormat(published, "2006-01-02T15:04:05Z07:00")) hb.writeElementOpen("time", "class", "dt-published", "datetime", published.Format(time.RFC3339))
hb.writeEscaped(isoDateFormat(published)) hb.writeEscaped(published.Format(isoDateFormat))
hb.writeElementClose("time") hb.writeElementClose("time")
// Section // Section
if p.Section != "" { if p.Section != "" {
@ -133,12 +136,12 @@ func (a *goBlog) renderPostMeta(hb *htmlBuilder, p *post, b *configBlog, typ str
hb.writeElementClose("div") hb.writeElementClose("div")
} }
// Updated time // Updated time
if updated := p.Updated; updated != "" { if updated := toLocalTime(p.Updated); !updated.IsZero() {
hb.writeElementOpen("div") hb.writeElementOpen("div")
hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "updatedon")) hb.writeEscaped(a.ts.GetTemplateStringVariant(b.Lang, "updatedon"))
hb.write(" ") hb.write(" ")
hb.writeElementOpen("time", "class", "dt-updated", "datetime", dateFormat(updated, "2006-01-02T15:04:05Z07:00")) hb.writeElementOpen("time", "class", "dt-updated", "datetime", updated.Format(time.RFC3339))
hb.writeEscaped(isoDateFormat(updated)) hb.writeEscaped(updated.Format(isoDateFormat))
hb.writeElementClose("time") hb.writeElementClose("time")
hb.writeElementClose("div") hb.writeElementClose("div")
} }
@ -335,11 +338,11 @@ func (a *goBlog) renderPostHeadMeta(hb *htmlBuilder, p *post, canonical string)
hb.writeElementOpen("meta", "property", "og:description", "content", summary) hb.writeElementOpen("meta", "property", "og:description", "content", summary)
hb.writeElementOpen("meta", "property", "twitter:description", "content", summary) hb.writeElementOpen("meta", "property", "twitter:description", "content", summary)
} }
if p.Published != "" { if published := toLocalTime(p.Published); !published.IsZero() {
hb.writeElementOpen("meta", "itemprop", "datePublished", "content", dateFormat(p.Published, "2006-01-02T15:04:05-07:00")) hb.writeElementOpen("meta", "itemprop", "datePublished", "content", published.Format(time.RFC3339))
} }
if p.Updated != "" { if updated := toLocalTime(p.Updated); !updated.IsZero() {
hb.writeElementOpen("meta", "itemprop", "dateModified", "content", dateFormat(p.Updated, "2006-01-02T15:04:05-07:00")) hb.writeElementOpen("meta", "itemprop", "dateModified", "content", updated.Format(time.RFC3339))
} }
for _, img := range a.photoLinks(p) { for _, img := range a.photoLinks(p) {
hb.writeElementOpen("meta", "itemprop", "image", "content", img) hb.writeElementOpen("meta", "itemprop", "image", "content", img)

View File

@ -2,7 +2,6 @@ package main
import ( import (
"fmt" "fmt"
"html/template"
"io" "io"
textTemplate "text/template" textTemplate "text/template"
) )
@ -33,10 +32,6 @@ func (h *htmlBuilder) write(s string) {
_, _ = h.WriteString(s) _, _ = h.WriteString(s)
} }
func (h *htmlBuilder) writeHtml(s template.HTML) {
h.write(string(s))
}
func (h *htmlBuilder) writeEscaped(s string) { func (h *htmlBuilder) writeEscaped(s string) {
textTemplate.HTMLEscape(h, []byte(s)) textTemplate.HTMLEscape(h, []byte(s))
} }

View File

@ -14,6 +14,7 @@ import (
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
"sync"
"time" "time"
"unicode" "unicode"
@ -164,25 +165,18 @@ func toUTC(s string) (string, error) {
return d.UTC().Format(time.RFC3339), nil return d.UTC().Format(time.RFC3339), nil
} }
func dateFormat(date string, format string) string { func toLocalTime(date string) time.Time {
if date == "" {
return time.Time{}
}
d, err := dateparse.ParseLocal(date) d, err := dateparse.ParseLocal(date)
if err != nil { if err != nil {
return "" return time.Time{}
} }
return d.Local().Format(format) return d.Local()
} }
func isoDateFormat(date string) string { const isoDateFormat = "2006-01-02"
return dateFormat(date, "2006-01-02")
}
func unixToLocalDateString(unix int64) string {
return time.Unix(unix, 0).Local().Format(time.RFC3339)
}
func localNowString() string {
return time.Now().Local().Format(time.RFC3339)
}
func utcNowString() string { func utcNowString() string {
return time.Now().UTC().Format(time.RFC3339) return time.Now().UTC().Format(time.RFC3339)
@ -366,7 +360,17 @@ func (valueOnlyContext) Err() error {
return nil return nil
} }
var timeDiffLocaleMap = map[string]tdl.Locale{}
var timeDiffLocaleMutex sync.RWMutex
func matchTimeDiffLocale(lang string) tdl.Locale { func matchTimeDiffLocale(lang string) tdl.Locale {
timeDiffLocaleMutex.RLock()
if locale, ok := timeDiffLocaleMap[lang]; ok {
return locale
}
timeDiffLocaleMutex.RUnlock()
timeDiffLocaleMutex.Lock()
defer timeDiffLocaleMutex.Unlock()
supportedLangs := []string{"en", "de", "es", "hi", "pt", "ru", "zh-CN"} supportedLangs := []string{"en", "de", "es", "hi", "pt", "ru", "zh-CN"}
supportedTags := []language.Tag{} supportedTags := []language.Tag{}
for _, lang := range supportedLangs { for _, lang := range supportedLangs {
@ -374,5 +378,7 @@ func matchTimeDiffLocale(lang string) tdl.Locale {
} }
matcher := language.NewMatcher(supportedTags) matcher := language.NewMatcher(supportedTags)
_, idx, _ := matcher.Match(language.Make(lang)) _, idx, _ := matcher.Match(language.Make(lang))
return tdl.Locale(supportedLangs[idx]) locale := tdl.Locale(supportedLangs[idx])
timeDiffLocaleMap[lang] = locale
return locale
} }

View File

@ -14,6 +14,7 @@ import (
"github.com/carlmjohnson/requests" "github.com/carlmjohnson/requests"
"github.com/thoas/go-funk" "github.com/thoas/go-funk"
"github.com/tomnomnom/linkheader" "github.com/tomnomnom/linkheader"
"go.goblog.app/app/pkgs/bufferpool"
) )
const postParamWebmention = "webmention" const postParamWebmention = "webmention"
@ -32,7 +33,10 @@ func (a *goBlog) sendWebmentions(p *post) error {
return nil return nil
} }
links := []string{} links := []string{}
contentLinks, err := allLinksFromHTML(strings.NewReader(string(a.postHtml(p, false))), a.fullPostURL(p)) contentBuf := bufferpool.Get()
a.postHtmlToWriter(contentBuf, p, false)
contentLinks, err := allLinksFromHTML(contentBuf, a.fullPostURL(p))
bufferpool.Put(contentBuf)
if err != nil { if err != nil {
return err return err
} }