Prose will also replace all relative paths to images with a url to their
imgs.sh blog.
---
This patch integration prose and imgs together so it's very easy to
display images on your blog.
filehandlers/imgs/md.go | 2 +-
imgs/api.go | 71 +++++++++--------------------------------
imgs/config.go | 9 ++++++
prose/api.go | 25 ++++++++-------
prose/scp_hooks.go | 2 +-
shared/config.go | 39 ++++++++++++++++++++++
shared/mdparser.go | 61 ++++++++++++++++++++++++++++++++++-
7 files changed, 138 insertions(+), 71 deletions(-)
diff --git a/filehandlers/imgs/md.go b/filehandlers/imgs/md.go
index 3f43fcb..130d466 100644
--- a/filehandlers/imgs/md.go
+++ b/filehandlers/imgs/md.go
@@ -29,7 +29,7 @@ func (h *UploadImgHandler) validateMd(data *PostMetaData) (bool, error) {
}
func (h *UploadImgHandler) metaMd(data *PostMetaData) error {
- parsedText, err := shared.ParseText(data.Text)
+ parsedText, err := shared.ParseText(data.Text, "")
// we return nil here because we don't want the file upload to fail
if err != nil {
return nil
diff --git a/imgs/api.go b/imgs/api.go
index 35008a9..08c2057 100644
--- a/imgs/api.go
+++ b/imgs/api.go
@@ -113,45 +113,6 @@ type MergePost struct {
var allTag = "all"
-func ImgURL(c *shared.ConfigSite, username string, slug string, onSubdomain bool, withUserName bool) string {
- fname := url.PathEscape(strings.TrimLeft(slug, "/"))
- if c.IsSubdomains() && onSubdomain {
- return fmt.Sprintf("%s://%s.%s/%s", c.Protocol, username, c.Domain, fname)
- }
-
- if withUserName {
- return fmt.Sprintf("/%s/%s", username, fname)
- }
-
- return fmt.Sprintf("/%s", fname)
-}
-
-func TagURL(c *shared.ConfigSite, username, tag string, onSubdomain, withUserName bool) string {
- tg := url.PathEscape(tag)
- if c.IsSubdomains() && onSubdomain {
- return fmt.Sprintf("%s://%s.%s/t/%s", c.Protocol, username, c.Domain, tg)
- }
-
- if withUserName {
- return fmt.Sprintf("/%s/t/%s", username, tg)
- }
-
- return fmt.Sprintf("/t/%s", tg)
-}
-
-func TagPostURL(c *shared.ConfigSite, username, tag, slug string, onSubdomain, withUserName bool) string {
- fname := url.PathEscape(strings.TrimLeft(slug, "/"))
- if c.IsSubdomains() && onSubdomain {
- return fmt.Sprintf("%s://%s.%s/%s/%s", c.Protocol, username, c.Domain, tag, fname)
- }
-
- if withUserName {
- return fmt.Sprintf("/%s/%s/%s", username, tag, fname)
- }
-
- return fmt.Sprintf("/%s/%s", tag, fname)
-}
-
func GetPostTitle(post *db.Post) string {
if post.Description == "" {
return post.Title
@@ -239,8 +200,8 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
for key, post := range tagMap {
postCollection = append(postCollection, &PostTagData{
Tag: key,
- URL: template.URL(TagURL(cfg, post.Username, key, onSubdomain, withUserName)),
- ImgURL: template.URL(ImgURL(cfg, post.Username, post.Filename, onSubdomain, withUserName)),
+ URL: template.URL(cfg.TagURL(post.Username, key, onSubdomain, withUserName)),
+ ImgURL: template.URL(cfg.ImgURL(post.Username, post.Filename, onSubdomain, withUserName)),
PublishAt: post.PublishAt,
})
}
@@ -370,8 +331,8 @@ func tagHandler(w http.ResponseWriter, r *http.Request) {
continue
}
mergedPosts = append(mergedPosts, TagPostData{
- URL: template.URL(TagPostURL(cfg, username, tag, post.Slug, onSubdomain, withUserName)),
- ImgURL: template.URL(ImgURL(cfg, username, post.Filename, onSubdomain, withUserName)),
+ URL: template.URL(cfg.TagPostURL(username, tag, post.Slug, onSubdomain, withUserName)),
+ ImgURL: template.URL(cfg.ImgURL(username, post.Filename, onSubdomain, withUserName)),
Caption: post.Title,
})
}
@@ -383,7 +344,7 @@ func tagHandler(w http.ResponseWriter, r *http.Request) {
Site: *cfg.GetSiteData(),
Tag: tag,
Posts: mergedPosts,
- URL: template.URL(TagURL(cfg, username, tag, onSubdomain, withUserName)),
+ URL: template.URL(cfg.TagURL(username, tag, onSubdomain, withUserName)),
}
ts, err := shared.RenderTemplate(cfg, []string{
@@ -460,8 +421,7 @@ func tagPostHandler(w http.ResponseWriter, r *http.Request) {
}
if i+1 < len(mergedPosts) {
- nextPage = TagPostURL(
- cfg,
+ nextPage = cfg.TagPostURL(
username,
tag,
mergedPosts[i+1].Slug,
@@ -471,8 +431,7 @@ func tagPostHandler(w http.ResponseWriter, r *http.Request) {
}
if i-1 >= 0 {
- prevPage = TagPostURL(
- cfg,
+ prevPage = cfg.TagPostURL(
username,
tag,
mergedPosts[i-1].Slug,
@@ -489,7 +448,7 @@ func tagPostHandler(w http.ResponseWriter, r *http.Request) {
return
}
- parsed, err := shared.ParseText(post.Text)
+ parsed, err := shared.ParseText(post.Text, cfg.ImgURL(username, "", true, false))
if err != nil {
logger.Error(err)
}
@@ -501,7 +460,7 @@ func tagPostHandler(w http.ResponseWriter, r *http.Request) {
tagLinks := make([]Link, 0, len(post.Tags))
for _, tag := range post.Tags {
tagLinks = append(tagLinks, Link{
- URL: template.URL(TagURL(cfg, username, tag, onSubdomain, withUserName)),
+ URL: template.URL(cfg.TagURL(username, tag, onSubdomain, withUserName)),
Text: tag,
})
}
@@ -519,7 +478,7 @@ func tagPostHandler(w http.ResponseWriter, r *http.Request) {
Username: username,
BlogName: blogName,
Contents: template.HTML(text),
- ImgURL: template.URL(ImgURL(cfg, username, post.Filename, onSubdomain, withUserName)),
+ ImgURL: template.URL(cfg.ImgURL(username, post.Filename, onSubdomain, withUserName)),
Tags: tagLinks,
PrevPage: template.URL(prevPage),
NextPage: template.URL(nextPage),
@@ -572,7 +531,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
var data PostPageData
post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
if err == nil {
- parsed, err := shared.ParseText(post.Text)
+ parsed, err := shared.ParseText(post.Text, cfg.ImgURL(username, "", true, false))
if err != nil {
logger.Error(err)
}
@@ -584,7 +543,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
tagLinks := make([]Link, 0, len(post.Tags))
for _, tag := range post.Tags {
tagLinks = append(tagLinks, Link{
- URL: template.URL(TagURL(cfg, username, tag, onSubdomain, withUserName)),
+ URL: template.URL(cfg.TagURL(username, tag, onSubdomain, withUserName)),
Text: tag,
})
}
@@ -602,7 +561,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
Username: username,
BlogName: blogName,
Contents: template.HTML(text),
- ImgURL: template.URL(ImgURL(cfg, username, post.Filename, onSubdomain, withUserName)),
+ ImgURL: template.URL(cfg.ImgURL(username, post.Filename, onSubdomain, withUserName)),
Tags: tagLinks,
}
} else {
@@ -722,7 +681,7 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
}
var tpl bytes.Buffer
data := &PostPageData{
- ImgURL: template.URL(ImgURL(cfg, username, post.Filename, onSubdomain, withUserName)),
+ ImgURL: template.URL(cfg.ImgURL(username, post.Filename, onSubdomain, withUserName)),
}
if err := ts.Execute(&tpl, data); err != nil {
continue
@@ -801,7 +760,7 @@ func rssHandler(w http.ResponseWriter, r *http.Request) {
for _, post := range pager.Data {
var tpl bytes.Buffer
data := &PostPageData{
- ImgURL: template.URL(ImgURL(cfg, post.Username, post.Filename, onSubdomain, withUserName)),
+ ImgURL: template.URL(cfg.ImgURL(post.Username, post.Filename, onSubdomain, withUserName)),
}
if err := ts.Execute(&tpl, data); err != nil {
continue
diff --git a/imgs/config.go b/imgs/config.go
index 7cce9dd..01cc736 100644
--- a/imgs/config.go
+++ b/imgs/config.go
@@ -7,6 +7,15 @@ import (
"git.sr.ht/~erock/pico/wish/cms/config"
)
+func ImgBaseURL(username string) string {
+ cfg := NewConfigSite()
+ if cfg.IsSubdomains() {
+ return fmt.Sprintf("%s://%s.%s", cfg.Protocol, username, cfg.Domain)
+ }
+
+ return "/"
+}
+
func NewConfigSite() *shared.ConfigSite {
domain := shared.GetEnv("IMGS_DOMAIN", "prose.sh")
email := shared.GetEnv("IMGS_EMAIL", "hello@prose.sh")
diff --git a/prose/api.go b/prose/api.go
index b3c1d50..489a67c 100644
--- a/prose/api.go
+++ b/prose/api.go
@@ -13,6 +13,7 @@ import (
"git.sr.ht/~erock/pico/db"
"git.sr.ht/~erock/pico/db/postgres"
+ "git.sr.ht/~erock/pico/imgs"
"git.sr.ht/~erock/pico/shared"
"github.com/gorilla/feeds"
"golang.org/x/exp/slices"
@@ -191,7 +192,7 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
if post.Filename == "_styles.css" && len(post.Text) > 0 {
hasCSS = true
} else if post.Filename == "_readme.md" {
- parsedText, err := shared.ParseText(post.Text)
+ parsedText, err := shared.ParseText(post.Text, imgs.ImgBaseURL(post.Username))
if err != nil {
logger.Error(err)
}
@@ -328,7 +329,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
var data PostPageData
post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
if err == nil {
- parsedText, err := shared.ParseText(post.Text)
+ parsedText, err := shared.ParseText(post.Text, imgs.ImgBaseURL(username))
if err != nil {
logger.Error(err)
}
@@ -336,7 +337,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
// we need the blog name from the readme unfortunately
readme, err := dbpool.FindPostWithFilename("_readme.md", user.ID, cfg.Space)
if err == nil {
- readmeParsed, err := shared.ParseText(readme.Text)
+ readmeParsed, err := shared.ParseText(readme.Text, imgs.ImgBaseURL(username))
if err != nil {
logger.Error(err)
}
@@ -540,9 +541,15 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
Title: GetBlogName(username),
}
+ hostDomain := strings.Split(r.Host, ":")[0]
+ appDomain := strings.Split(cfg.ConfigCms.Domain, ":")[0]
+
+ onSubdomain := cfg.IsSubdomains() && strings.Contains(hostDomain, appDomain)
+ withUserName := (!onSubdomain && hostDomain == appDomain) || !cfg.IsCustomdomains()
+
for _, post := range posts {
if post.Filename == "_readme.md" {
- parsedText, err := shared.ParseText(post.Text)
+ parsedText, err := shared.ParseText(post.Text, imgs.ImgBaseURL(post.Username))
if err != nil {
logger.Error(err)
}
@@ -558,12 +565,6 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
}
}
- hostDomain := strings.Split(r.Host, ":")[0]
- appDomain := strings.Split(cfg.ConfigCms.Domain, ":")[0]
-
- onSubdomain := cfg.IsSubdomains() && strings.Contains(hostDomain, appDomain)
- withUserName := (!onSubdomain && hostDomain == appDomain) || !cfg.IsCustomdomains()
-
feed := &feeds.Feed{
Title: headerTxt.Title,
Link: &feeds.Link{Href: cfg.FullBlogURL(username, onSubdomain, withUserName)},
@@ -577,7 +578,7 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
if slices.Contains(cfg.HiddenPosts, post.Filename) {
continue
}
- parsed, err := shared.ParseText(post.Text)
+ parsed, err := shared.ParseText(post.Text, imgs.ImgBaseURL(post.Username))
if err != nil {
logger.Error(err)
}
@@ -660,7 +661,7 @@ func rssHandler(w http.ResponseWriter, r *http.Request) {
var feedItems []*feeds.Item
for _, post := range pager.Data {
- parsed, err := shared.ParseText(post.Text)
+ parsed, err := shared.ParseText(post.Text, imgs.ImgBaseURL(post.Username))
if err != nil {
logger.Error(err)
}
diff --git a/prose/scp_hooks.go b/prose/scp_hooks.go
index 08dcb21..9e851f4 100644
--- a/prose/scp_hooks.go
+++ b/prose/scp_hooks.go
@@ -45,7 +45,7 @@ func (p *MarkdownHooks) FileValidate(data *filehandlers.PostMetaData) (bool, err
}
func (p *MarkdownHooks) FileMeta(data *filehandlers.PostMetaData) error {
- parsedText, err := shared.ParseText(data.Text)
+ parsedText, err := shared.ParseText(data.Text, "")
// we return nil here because we don't want the file upload to fail
if err != nil {
return nil
diff --git a/shared/config.go b/shared/config.go
index e77cc1f..89bac2a 100644
--- a/shared/config.go
+++ b/shared/config.go
@@ -143,6 +143,45 @@ func (c *ConfigSite) RawPostURL(username, slug string) string {
return fmt.Sprintf("/raw/%s/%s", username, fname)
}
+func (c *ConfigSite) ImgURL(username string, slug string, onSubdomain bool, withUserName bool) string {
+ fname := url.PathEscape(strings.TrimLeft(slug, "/"))
+ if c.IsSubdomains() && onSubdomain {
+ return fmt.Sprintf("%s://%s.%s/%s", c.Protocol, username, c.Domain, fname)
+ }
+
+ if withUserName {
+ return fmt.Sprintf("/%s/%s", username, fname)
+ }
+
+ return fmt.Sprintf("/%s", fname)
+}
+
+func (c *ConfigSite) TagURL(username, tag string, onSubdomain, withUserName bool) string {
+ tg := url.PathEscape(tag)
+ if c.IsSubdomains() && onSubdomain {
+ return fmt.Sprintf("%s://%s.%s/t/%s", c.Protocol, username, c.Domain, tg)
+ }
+
+ if withUserName {
+ return fmt.Sprintf("/%s/t/%s", username, tg)
+ }
+
+ return fmt.Sprintf("/t/%s", tg)
+}
+
+func (c *ConfigSite) TagPostURL(username, tag, slug string, onSubdomain, withUserName bool) string {
+ fname := url.PathEscape(strings.TrimLeft(slug, "/"))
+ if c.IsSubdomains() && onSubdomain {
+ return fmt.Sprintf("%s://%s.%s/%s/%s", c.Protocol, username, c.Domain, tag, fname)
+ }
+
+ if withUserName {
+ return fmt.Sprintf("/%s/%s/%s", username, tag, fname)
+ }
+
+ return fmt.Sprintf("/%s/%s", tag, fname)
+}
+
func CreateLogger() *zap.SugaredLogger {
logger, err := zap.NewProduction()
if err != nil {
diff --git a/shared/mdparser.go b/shared/mdparser.go
index 4129923..00cf593 100644
--- a/shared/mdparser.go
+++ b/shared/mdparser.go
@@ -12,9 +12,12 @@ import (
"github.com/yuin/goldmark"
highlighting "github.com/yuin/goldmark-highlighting"
meta "github.com/yuin/goldmark-meta"
+ "github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
+ "github.com/yuin/goldmark/renderer"
ghtml "github.com/yuin/goldmark/renderer/html"
+ "github.com/yuin/goldmark/util"
)
type Link struct {
@@ -107,7 +110,60 @@ func toTags(obj interface{}) ([]string, error) {
return arr, nil
}
-func ParseText(text string) (*ParsedText, error) {
+type ImgRender struct {
+ ghtml.Config
+ ImgURL func(url []byte) []byte
+}
+
+func NewImgsRenderer(url func([]byte) []byte) renderer.NodeRenderer {
+ return &ImgRender{
+ ImgURL: url,
+ }
+}
+
+func (r *ImgRender) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
+ reg.Register(ast.KindImage, r.renderImage)
+}
+
+func (r *ImgRender) renderImage(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
+ if !entering {
+ return ast.WalkContinue, nil
+ }
+ n := node.(*ast.Image)
+ _, _ = w.WriteString("<img src=\"")
+ if r.Unsafe || !ghtml.IsDangerousURL(n.Destination) {
+ dest := r.ImgURL(n.Destination)
+ _, _ = w.Write(util.EscapeHTML(util.URLEscape(dest, true)))
+ }
+ _, _ = w.WriteString(`" alt="`)
+ _, _ = w.Write(util.EscapeHTML(n.Text(source)))
+ _ = w.WriteByte('"')
+ if n.Title != nil {
+ _, _ = w.WriteString(` title="`)
+ r.Writer.Write(w, n.Title)
+ _ = w.WriteByte('"')
+ }
+ if n.Attributes() != nil {
+ ghtml.RenderAttributes(w, n, ghtml.ImageAttributeFilter)
+ }
+ if r.XHTML {
+ _, _ = w.WriteString(" />")
+ } else {
+ _, _ = w.WriteString(">")
+ }
+ return ast.WalkSkipChildren, nil
+}
+
+func createImgURL(absURL string) func([]byte) []byte {
+ return func(url []byte) []byte {
+ if url[0] != '/' {
+ return url
+ }
+ return []byte(fmt.Sprintf("%s%s", absURL, string(url)))
+ }
+}
+
+func ParseText(text string, absURL string) (*ParsedText, error) {
parsed := ParsedText{
MetaData: &MetaData{
Tags: []string{},
@@ -131,6 +187,9 @@ func ParseText(text string) (*ParsedText, error) {
),
goldmark.WithRendererOptions(
ghtml.WithUnsafe(),
+ renderer.WithNodeRenderers(
+ util.Prioritized(NewImgsRenderer(createImgURL(absURL)), 0),
+ ),
),
)
context := parser.NewContext()
--
2.37.1