diff --git a/src/gmi/tpl.go b/src/gmi/tpl.go index 7959b8e..7f41993 100644 --- a/src/gmi/tpl.go +++ b/src/gmi/tpl.go @@ -8,6 +8,7 @@ import ( "io" "io/fs" "net/url" + "path/filepath" "strconv" "strings" "text/template" @@ -83,6 +84,45 @@ func (r renderer) GetPostSeriesNextPrevious(p post.StoredPost) (rendererGetPostS 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 { + return filepath.Join("/static", path) + }, + 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) + } + + return buf.String(), nil +} + func (r renderer) GetQueryValue(key, def string) string { v := r.url.Query().Get(key) if v == "" { diff --git a/src/gmi/tpl/posts/post.gmi b/src/gmi/tpl/posts/post.gmi index 4f58c84..7d1719b 100644 --- a/src/gmi/tpl/posts/post.gmi +++ b/src/gmi/tpl/posts/post.gmi @@ -6,7 +6,7 @@ > {{ $post.Description }} {{ end -}} -{{ $post.Body }} +{{ .PostBody $post }} -------------------------------------------------------------------------------- diff --git a/src/http/posts.go b/src/http/posts.go index eff0eaa..cae8f43 100644 --- a/src/http/posts.go +++ b/src/http/posts.go @@ -21,68 +21,43 @@ import ( "github.com/mediocregopher/mediocre-go-lib/v2/mctx" ) -func (a *api) parsePostBody(p post.Post) (*txttpl.Template, error) { - tpl := txttpl.New("root") - tpl = tpl.Funcs(txttpl.FuncMap(a.tplFuncs())) - - tpl = txttpl.Must(tpl.New("image.html").Parse(mustReadTplFile("image.html"))) - - if p.Format == post.FormatMarkdown { - tpl = tpl.Funcs(txttpl.FuncMap{ - "Image": func(id string) (string, error) { - - tplPayload := struct { - ID string - Descr string - Resizable bool - }{ - ID: id, - // I could use variadic args to make this work, I think - Descr: "TODO: proper alt text", - Resizable: isImgResizable(id), - } - - buf := new(bytes.Buffer) - if err := tpl.ExecuteTemplate(buf, "image.html", tplPayload); err != nil { - return "", err - } - - return buf.String(), nil - }, - }) - } +func (a *api) postPreprocessFuncImage(args ...string) (string, error) { + + var ( + id = args[0] + descr = "TODO" + ) - if p.Format == post.FormatGemtext { - tpl = tpl.Funcs(txttpl.FuncMap{ - "Image": func(id, descr string) (string, error) { - - tplPayload := struct { - ID string - Descr string - Resizable bool - }{ - ID: id, - Descr: descr, - Resizable: isImgResizable(id), - } - - buf := new(bytes.Buffer) - if err := tpl.ExecuteTemplate(buf, "image.html", tplPayload); err != nil { - return "", err - } - - return buf.String(), nil - }, - }) + if len(args) > 1 { + descr = args[1] } - tpl, err := tpl.New(p.ID + "-body.html").Parse(p.Body) + tpl := txttpl.New("image.html") - if err != nil { - return nil, err + tpl.Funcs(txttpl.FuncMap{ + "AssetURL": func(id string) string { + return a.assetURL(id, false) + }, + }) + + tpl = txttpl.Must(tpl.Parse(mustReadTplFile("image.html"))) + + tplPayload := struct { + ID string + Descr string + Resizable bool + }{ + ID: id, + Descr: descr, + Resizable: isImgResizable(id), + } + + buf := new(bytes.Buffer) + if err := tpl.ExecuteTemplate(buf, "image.html", tplPayload); err != nil { + return "", err } - return tpl, nil + return buf.String(), nil } type postTplPayload struct { @@ -93,15 +68,27 @@ type postTplPayload struct { func (a *api) postToPostTplPayload(storedPost post.StoredPost) (postTplPayload, error) { - bodyTpl, err := a.parsePostBody(storedPost.Post) - if err != nil { - return postTplPayload{}, fmt.Errorf("parsing post body as template: %w", err) + preprocessFuncs := post.PreprocessFunctions{ + BlogURL: func(path string) string { + return a.blogURL(path, false) + }, + AssetURL: func(id string) string { + return a.assetURL(id, false) + }, + PostURL: func(id string) string { + return a.postURL(id, false) + }, + StaticURL: func(path string) string { + path = filepath.Join("static", path) + return a.blogURL(path, false) + }, + Image: a.postPreprocessFuncImage, } bodyBuf := new(bytes.Buffer) - if err := bodyTpl.Execute(bodyBuf, nil); err != nil { - return postTplPayload{}, fmt.Errorf("executing post body as template: %w", err) + if err := storedPost.PreprocessBody(bodyBuf, preprocessFuncs); err != nil { + return postTplPayload{}, fmt.Errorf("preprocessing post body: %w", err) } if storedPost.Format == post.FormatGemtext { diff --git a/src/post/preprocess.go b/src/post/preprocess.go new file mode 100644 index 0000000..4d4be9d --- /dev/null +++ b/src/post/preprocess.go @@ -0,0 +1,66 @@ +package post + +import ( + "fmt" + "io" + "text/template" +) + +// PreprocessFunctions are functions which can be used by posts themselves to +// interleave dynamic content into their bodies. Usually this is used for +// properly constructing URLs, but also for things like displaying images. +type PreprocessFunctions struct { + + // BlogURL returns the given string, rooted to the blog's base url (which + // may or may not include path components itself). + // + // The given path should not have a leading slash. + BlogURL func(path string) string + + // AssetURL returns the URL of the asset with the given ID. + AssetURL func(id string) string + + // PostURL returns the URL of the post with the given ID. + PostURL func(id string) string + + // StaticURL returns the URL of a file being served from the static + // directory. The given path should _not_ include the prefixed 'static/' + // path element. + StaticURL func(path string) string + + // Image returns a string which should be inlined into the post body in + // order to display an. + // + // The first argument to Image _must_ be the ID of an image asset. The + // second argument _may_ be a description of the image which will be used as + // alt text, or possibly displayed to the user with the image. + Image func(args ...string) (string, error) +} + +// PreprocessBody interprets the Post's Body as a text template which may use +// any of the functions found in PreprocessFunctions (all must be set). It +// executes the template and writes the result to the given writer. +func (p Post) PreprocessBody(into io.Writer, funcs PreprocessFunctions) error { + + tpl := template.New("") + + tpl.Funcs(template.FuncMap{ + "BlogURL": funcs.BlogURL, + "AssetURL": funcs.AssetURL, + "PostURL": funcs.PostURL, + "StaticURL": funcs.StaticURL, + "Image": funcs.Image, + }) + + tpl, err := tpl.Parse(p.Body) + + if err != nil { + return fmt.Errorf("parsing post body as template: %w", err) + } + + if err := tpl.Execute(into, nil); err != nil { + return fmt.Errorf("executing post body as template: %w", err) + } + + return nil +}