You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
234 lines
4.6 KiB
234 lines
4.6 KiB
package gmi
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"embed"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"mime"
|
|
"net/url"
|
|
"path"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"git.sr.ht/~adnano/go-gemini"
|
|
"github.com/mediocregopher/blog.mediocregopher.com/srv/post"
|
|
"github.com/mediocregopher/mediocre-go-lib/v2/mctx"
|
|
gmnhg "github.com/tdemin/gmnhg"
|
|
)
|
|
|
|
//go:embed tpl
|
|
var tplFS embed.FS
|
|
|
|
type rendererGetPostsRes struct {
|
|
Posts []post.StoredPost
|
|
HasMore bool
|
|
}
|
|
|
|
type rendererGetPostSeriesNextPreviousRes struct {
|
|
Next *post.StoredPost
|
|
Previous *post.StoredPost
|
|
}
|
|
|
|
type renderer struct {
|
|
url *url.URL
|
|
postStore post.Store
|
|
httpPublicURL *url.URL
|
|
}
|
|
|
|
func (r renderer) GetPosts(page, count int) (rendererGetPostsRes, error) {
|
|
posts, hasMore, err := r.postStore.Get(page, count)
|
|
return rendererGetPostsRes{posts, hasMore}, err
|
|
}
|
|
|
|
func (r renderer) GetPostByID(id string) (post.StoredPost, error) {
|
|
p, err := r.postStore.GetByID(id)
|
|
if err != nil {
|
|
return post.StoredPost{}, fmt.Errorf("fetching post %q: %w", id, err)
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
func (r renderer) GetPostSeriesNextPrevious(p post.StoredPost) (rendererGetPostSeriesNextPreviousRes, error) {
|
|
|
|
seriesPosts, err := r.postStore.GetBySeries(p.Series)
|
|
if err != nil {
|
|
return rendererGetPostSeriesNextPreviousRes{}, fmt.Errorf(
|
|
"fetching posts for series %q: %w", p.Series, err,
|
|
)
|
|
}
|
|
|
|
var (
|
|
res rendererGetPostSeriesNextPreviousRes
|
|
foundThis bool
|
|
)
|
|
|
|
for i := range seriesPosts {
|
|
|
|
seriesPost := seriesPosts[i]
|
|
|
|
if seriesPost.ID == p.ID {
|
|
foundThis = true
|
|
continue
|
|
}
|
|
|
|
if !foundThis {
|
|
res.Next = &seriesPost
|
|
continue
|
|
}
|
|
|
|
res.Previous = &seriesPost
|
|
break
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
func (r renderer) PostBody(p post.StoredPost) (string, error) {
|
|
|
|
preprocessFuncs := post.PreprocessFunctions{
|
|
BlogURL: func(path string) string {
|
|
return filepath.Join("/", path)
|
|
},
|
|
AssetURL: func(id string) string {
|
|
return filepath.Join("/assets", id)
|
|
},
|
|
PostURL: func(id string) string {
|
|
return filepath.Join("/posts", id)
|
|
},
|
|
StaticURL: func(path string) string {
|
|
httpPublicURL := *r.httpPublicURL
|
|
httpPublicURL.Path = filepath.Join(httpPublicURL.Path, "/static", path)
|
|
return httpPublicURL.String()
|
|
},
|
|
Image: func(args ...string) (string, error) {
|
|
|
|
var (
|
|
id = args[0]
|
|
descr = "Image"
|
|
)
|
|
|
|
if len(args) > 1 {
|
|
descr = args[1]
|
|
}
|
|
|
|
return fmt.Sprintf("=> %s %s", filepath.Join("/assets", id), descr), nil
|
|
},
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
if err := p.PreprocessBody(buf, preprocessFuncs); err != nil {
|
|
return "", fmt.Errorf("preprocessing post body: %w", err)
|
|
}
|
|
|
|
bodyBytes := buf.Bytes()
|
|
|
|
if p.Format == post.FormatMarkdown {
|
|
|
|
gemtextBodyBytes, err := gmnhg.RenderMarkdown(bodyBytes, 0)
|
|
if err != nil {
|
|
return "", fmt.Errorf("converting from markdown: %w", err)
|
|
}
|
|
|
|
bodyBytes = gemtextBodyBytes
|
|
}
|
|
|
|
return string(bodyBytes), nil
|
|
}
|
|
|
|
func (r renderer) GetQueryValue(key, def string) string {
|
|
v := r.url.Query().Get(key)
|
|
if v == "" {
|
|
v = def
|
|
}
|
|
return v
|
|
}
|
|
|
|
func (r renderer) GetQueryIntValue(key string, def int) (int, error) {
|
|
vStr := r.GetQueryValue(key, strconv.Itoa(def))
|
|
return strconv.Atoi(vStr)
|
|
}
|
|
|
|
func (r renderer) Add(a, b int) int { return a + b }
|
|
|
|
func (a *api) tplHandler() (gemini.Handler, error) {
|
|
|
|
allTpls := template.New("")
|
|
|
|
err := fs.WalkDir(tplFS, "tpl", func(path string, d fs.DirEntry, err error) error {
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if d.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
body, err := fs.ReadFile(tplFS, path)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
name := strings.TrimPrefix(path, "tpl/")
|
|
|
|
allTpls, err = allTpls.New(name).Parse(string(body))
|
|
if err != nil {
|
|
return fmt.Errorf("parsing %q as template: %w", path, err)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing templates: %w", err)
|
|
}
|
|
|
|
return gemini.HandlerFunc(func(
|
|
ctx context.Context,
|
|
rw gemini.ResponseWriter,
|
|
r *gemini.Request,
|
|
) {
|
|
|
|
tplPath := strings.TrimPrefix(r.URL.Path, "/")
|
|
mimeType := mime.TypeByExtension(path.Ext(r.URL.Path))
|
|
|
|
ctx = mctx.Annotate(ctx,
|
|
"url", r.URL,
|
|
"tplPath", tplPath,
|
|
"mimeType", mimeType,
|
|
)
|
|
|
|
tpl := allTpls.Lookup(tplPath)
|
|
|
|
if tpl == nil {
|
|
rw.WriteHeader(gemini.StatusNotFound, "Page not found, sorry!")
|
|
return
|
|
}
|
|
|
|
if mimeType != "" {
|
|
rw.SetMediaType(mimeType)
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
err := tpl.Execute(buf, renderer{
|
|
url: r.URL,
|
|
postStore: a.params.PostStore,
|
|
httpPublicURL: a.params.HTTPPublicURL,
|
|
})
|
|
|
|
if err != nil {
|
|
a.params.Logger.Error(ctx, "rendering error", err)
|
|
rw.WriteHeader(gemini.StatusTemporaryFailure, err.Error())
|
|
return
|
|
}
|
|
|
|
io.Copy(rw, buf)
|
|
}), nil
|
|
}
|
|
|