~erock/pico.sh

feat: images can now be uploaded from all services v1 PROPOSED

Eric Bower: 1
 feat: images can now be uploaded from all services

 7 files changed, 138 insertions(+), 71 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~erock/pico.sh/patches/34799/mbox | git am -3
Learn more about email & git

[PATCH v1] feat: images can now be uploaded from all services Export this patch

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