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 }