From 8d7e708d98a3a46ba3ba08f9c8deeb4838bb8ca5 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Fri, 17 May 2024 23:37:43 +0200 Subject: Render posts completely using common rendering methods The aim is to reduce reliance on custom logic in the handlers for every protocol, eventually outsourcing all of it into `render.Methods`, leaving each protocol to simply direct calls to the correct template. --- src/render/methods.go | 226 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 src/render/methods.go (limited to 'src/render/methods.go') diff --git a/src/render/methods.go b/src/render/methods.go new file mode 100644 index 0000000..6dd9332 --- /dev/null +++ b/src/render/methods.go @@ -0,0 +1,226 @@ +package render + +import ( + "bytes" + "context" + "fmt" + "html/template" + "net/url" + "path" + "path/filepath" + "strconv" + "strings" + + "dev.mediocregopher.com/mediocre-blog.git/src/gmi/gemtext" + "dev.mediocregopher.com/mediocre-blog.git/src/post" + "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/html" + "github.com/gomarkdown/markdown/parser" + gmnhg "github.com/tdemin/gmnhg" +) + +type ctxKey string + +const ( + ctxKeyPost ctxKey = "post" +) + +// WithPost sets the post on a Context, such that it can be retrieved using +// the GetThisPost method. +func WithPost(ctx context.Context, p post.StoredPost) context.Context { + return context.WithValue(ctx, ctxKeyPost, p) +} + +// GetPostsRes are the fields returned from the GetPosts method. +type GetPostsRes struct { + Posts []post.StoredPost + HasMore bool +} + +// GetPostSeriesNextPreviousRes are the fields returned from the +// GetPostSeriesNextPreviousRes method. +type GetPostSeriesNextPreviousRes struct { + Next *post.StoredPost + Previous *post.StoredPost +} + +// Methods carries methods which are designed to be accessible from a template. +type Methods struct { + ctx context.Context + url *url.URL + publicURL *url.URL + geminiGatewayURL *url.URL + postStore post.Store + preprocessFuncs post.PreprocessFunctions + + thisPost *post.StoredPost // cache +} + +// NewMethods initializes a Methods using its required dependencies. +func NewMethods( + ctx context.Context, + url *url.URL, + publicURL *url.URL, + geminiGatewayURL *url.URL, + postStore post.Store, + preprocessFuncs post.PreprocessFunctions, +) *Methods { + return &Methods{ + ctx, + url, + publicURL, + geminiGatewayURL, + postStore, + preprocessFuncs, + nil, // thisPost + } +} + +func (m *Methods) GetPosts(page, count int) (GetPostsRes, error) { + posts, hasMore, err := m.postStore.Get(page, count) + return GetPostsRes{posts, hasMore}, err +} + +func (m *Methods) GetThisPost() (p post.StoredPost, err error) { + if m.thisPost != nil { + return *m.thisPost, nil + } + + defer func() { + m.thisPost = &p + }() + + if p, ok := m.ctx.Value(ctxKeyPost).(post.StoredPost); ok { + return p, nil + } + + id := path.Base(m.url.Path) + id = strings.TrimSuffix(id, path.Ext(id)) + + return m.postStore.GetByID(id) +} + +func (m *Methods) GetPostByID(id string) (post.StoredPost, error) { + p, err := m.postStore.GetByID(id) + if err != nil { + return post.StoredPost{}, fmt.Errorf("fetching post %q: %w", id, err) + } + return p, nil +} + +func (m *Methods) GetPostSeriesNextPrevious( + p post.StoredPost, +) ( + GetPostSeriesNextPreviousRes, error, +) { + + seriesPosts, err := m.postStore.GetBySeries(p.Series) + if err != nil { + return GetPostSeriesNextPreviousRes{}, fmt.Errorf( + "fetching posts for series %q: %w", p.Series, err, + ) + } + + var ( + res GetPostSeriesNextPreviousRes + 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 (m *Methods) PostGemtextBody(p post.StoredPost) (string, error) { + + buf := new(bytes.Buffer) + + if err := p.PreprocessBody(buf, m.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 (m *Methods) PostHTMLBody(p post.StoredPost) (template.HTML, error) { + bodyBuf := new(bytes.Buffer) + + if err := p.PreprocessBody(bodyBuf, m.preprocessFuncs); err != nil { + return "", fmt.Errorf("preprocessing post body: %w", err) + } + + if p.Format == post.FormatGemtext { + + prevBodyBuf := bodyBuf + bodyBuf = new(bytes.Buffer) + + err := gemtext.ToMarkdown( + bodyBuf, prevBodyBuf, m.geminiGatewayURL, + ) + + if err != nil { + return "", fmt.Errorf("converting gemtext to markdown: %w", err) + } + } + + // this helps the markdown renderer properly parse pages which end in a + // `` tag... I don't know why. + _, _ = bodyBuf.WriteString("\n") + + parserExt := parser.CommonExtensions | parser.AutoHeadingIDs + parser := parser.NewWithExtensions(parserExt) + + htmlFlags := html.HrefTargetBlank + htmlRenderer := html.NewRenderer(html.RendererOptions{Flags: htmlFlags}) + + renderedBody := markdown.ToHTML(bodyBuf.Bytes(), parser, htmlRenderer) + return template.HTML(renderedBody), nil +} + +func (m *Methods) GetQueryValue(key, def string) string { + v := m.url.Query().Get(key) + if v == "" { + v = def + } + return v +} + +func (m *Methods) GetQueryIntValue(key string, def int) (int, error) { + vStr := m.GetQueryValue(key, strconv.Itoa(def)) + return strconv.Atoi(vStr) +} + +func (m *Methods) GetPath() (string, error) { + basePath := filepath.Join("/", m.publicURL.Path) // in case it's empty + return filepath.Rel(basePath, m.url.Path) +} + +func (m *Methods) Add(a, b int) int { return a + b } -- cgit v1.2.3