dashboard / erock/pico / feat(prose): lists are back bby #102 rss

open · opened on 2026-01-04T04:57:34Z by erock
Help
checkout latest patchset:
ssh pr.pico.sh print pr-102 | git am -3
checkout any patchset in a patch request:
ssh pr.pico.sh print ps-X | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 102
add review to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add --review 102
accept PR:
ssh pr.pico.sh pr accept 102
close PR:
ssh pr.pico.sh pr close 102
Timeline Patchsets
+43 -15 pkg/apps/prose/api.go link
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
diff --git a/pkg/apps/prose/api.go b/pkg/apps/prose/api.go
index 7225ca6..eaef804 100644
--- a/pkg/apps/prose/api.go
+++ b/pkg/apps/prose/api.go
@@ -90,6 +90,7 @@ type PostPageData struct {
 	Diff         template.HTML
 	UpdatedAtISO string
 	UpdatedAt    string
+	List         *shared.ListParsedText
 }
 
 type HeaderTxt struct {
@@ -427,22 +428,47 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
 	post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
 	if err == nil {
 		logger.Info("post found", "id", post.ID, "filename", post.FileSize)
-		parsedText, err := shared.ParseText(post.Text)
-		if err != nil {
-			logger.Error("find post with slug", "err", err.Error())
-		}
+		ext := filepath.Ext(post.Filename)
+		contents := template.HTML("")
+		tags := []string{}
+		unlisted := false
+		var list *shared.ListParsedText
+
+		switch ext {
+		case ".lxt":
+			list = shared.ListParseText(post.Text)
+
+			// if parsedText.Image != "" {
+			// 	ogImage = parsedText.Image
+			// }
+			//
+			// if parsedText.ImageCard != "" {
+			// 	ogImageCard = parsedText.ImageCard
+			// }
+			tags = list.Tags
+
+			if post.Hidden || post.PublishAt.After(time.Now()) {
+				unlisted = true
+			}
+		case ".md":
+			parsedText, err := shared.ParseText(post.Text)
+			if err != nil {
+				logger.Error("could not parse md text", "err", err.Error())
+			}
 
-		if parsedText.Image != "" {
-			ogImage = parsedText.Image
-		}
+			if parsedText.Image != "" {
+				ogImage = parsedText.Image
+			}
 
-		if parsedText.ImageCard != "" {
-			ogImageCard = parsedText.ImageCard
-		}
+			if parsedText.ImageCard != "" {
+				ogImageCard = parsedText.ImageCard
+			}
+			tags = parsedText.Tags
 
-		unlisted := false
-		if post.Hidden || post.PublishAt.After(time.Now()) {
-			unlisted = true
+			if post.Hidden || post.PublishAt.After(time.Now()) {
+				unlisted = true
+			}
+			contents = template.HTML(parsedText.Html)
 		}
 
 		data = PostPageData{
@@ -459,10 +485,10 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
 			UpdatedAtISO: post.UpdatedAt.Format(time.RFC3339),
 			Username:     username,
 			BlogName:     blogName,
-			Contents:     template.HTML(parsedText.Html),
+			Contents:     contents,
 			HasCSS:       hasCSS,
 			CssURL:       template.URL(cfg.CssURL(username)),
-			Tags:         parsedText.Tags,
+			Tags:         tags,
 			Image:        template.URL(ogImage),
 			ImageCard:    ogImageCard,
 			Favicon:      template.URL(favicon),
@@ -470,6 +496,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
 			Unlisted:     unlisted,
 			Diff:         template.HTML(diff),
 			WithStyles:   withStyles,
+			List:         list,
 		}
 	} else {
 		logger.Info("post not found")
@@ -522,6 +549,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
 	}
 
 	ts, err := shared.RenderTemplate(cfg, []string{
+		cfg.StaticPath("html/list.partial.tmpl"),
 		cfg.StaticPath("html/post.page.tmpl"),
 	})
 
+1 -0 pkg/apps/prose/config.go link
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
diff --git a/pkg/apps/prose/config.go b/pkg/apps/prose/config.go
index 6b86a0e..8ffb4c3 100644
--- a/pkg/apps/prose/config.go
+++ b/pkg/apps/prose/config.go
@@ -28,6 +28,7 @@ func NewConfigSite(service string) *shared.ConfigSite {
 		Space:    "prose",
 		AllowedExt: []string{
 			".md",
+			".lxt",
 			".jpg",
 			".jpeg",
 			".png",
+51 -0 pkg/apps/prose/html/list.partial.tmpl link
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
diff --git a/pkg/apps/prose/html/list.partial.tmpl b/pkg/apps/prose/html/list.partial.tmpl
new file mode 100644
index 0000000..fc3f08e
--- /dev/null
+++ b/pkg/apps/prose/html/list.partial.tmpl
@@ -0,0 +1,51 @@
+{{define "list"}}
+{{$indent := 0}}
+{{$mod := 0}}
+<ul style="list-style-type: {{.ListType}};">
+    {{range .Items}}
+        {{if lt $indent .Indent}}
+        <ul>
+        {{else if gt $indent .Indent}}
+
+        {{$mod = minus $indent .Indent}}
+        {{range $y := intRange 1 $mod}}
+        </li></ul>
+        {{end}}
+
+        {{else}}
+        </li>
+        {{end}}
+        {{$indent = .Indent}}
+
+        {{if .IsText}}
+            {{if .Value}}
+            <li>{{.Value}}
+            {{end}}
+        {{end}}
+
+        {{if .IsURL}}
+        <li><a href="{{.URL}}">{{.Value}}</a>
+        {{end}}
+
+        {{if .IsImg}}
+        <li><img src="{{.URL}}" alt="{{.Value}}" />
+        {{end}}
+
+        {{if .IsBlock}}
+        <li><blockquote>{{.Value}}</blockquote>
+        {{end}}
+
+        {{if .IsHeaderOne}}
+        </ul><h2 class="text-xl font-bold">{{.Value}}</h2><ul style="list-style-type: {{$.ListType}};">
+        {{end}}
+
+        {{if .IsHeaderTwo}}
+        </ul><h3 class="text-lg font-bold">{{.Value}}</h3><ul style="list-style-type: {{$.ListType}};">
+        {{end}}
+
+        {{if .IsPre}}
+        <li><pre>{{.Value}}</pre>
+        {{end}}
+    {{end}}
+</ul>
+{{end}}
+5 -1 pkg/apps/prose/html/post.page.tmpl link
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
diff --git a/pkg/apps/prose/html/post.page.tmpl b/pkg/apps/prose/html/post.page.tmpl
index 8caa8f5..f0335e4 100644
--- a/pkg/apps/prose/html/post.page.tmpl
+++ b/pkg/apps/prose/html/post.page.tmpl
@@ -65,7 +65,11 @@
 </header>
 <main>
     <article class="md">
-        {{.Contents}}
+        {{if .List}}
+          {{template "list" .List}}
+        {{else}}
+          {{.Contents}}
+        {{end}}
 
         <div class="tags">
           {{range .Tags}}
+43 -1 pkg/apps/prose/scp_hooks.go link
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
diff --git a/pkg/apps/prose/scp_hooks.go b/pkg/apps/prose/scp_hooks.go
index 6387e04..526e95d 100644
--- a/pkg/apps/prose/scp_hooks.go
+++ b/pkg/apps/prose/scp_hooks.go
@@ -2,6 +2,7 @@ package prose
 
 import (
 	"fmt"
+	"path/filepath"
 	"strings"
 
 	"slices"
@@ -62,7 +63,29 @@ func (p *MarkdownHooks) FileValidate(s *pssh.SSHServerConnSession, data *filehan
 	return true, nil
 }
 
-func (p *MarkdownHooks) FileMeta(s *pssh.SSHServerConnSession, data *filehandlers.PostMetaData) error {
+func (p *MarkdownHooks) metaLxt(data *filehandlers.PostMetaData) error {
+	parsedText := shared.ListParseText(data.Text)
+
+	if parsedText.Title == "" {
+		data.Title = utils.ToUpper(data.Slug)
+	} else {
+		data.Title = parsedText.Title
+	}
+
+	data.Aliases = parsedText.Aliases
+	data.Tags = parsedText.Tags
+	data.Description = parsedText.Description
+
+	if parsedText.PublishAt != nil && !parsedText.PublishAt.IsZero() {
+		data.PublishAt = parsedText.PublishAt
+	}
+
+	isHiddenFilename := slices.Contains(p.Cfg.HiddenPosts, data.Filename)
+	data.Hidden = parsedText.Hidden || isHiddenFilename
+	return nil
+}
+
+func (p *MarkdownHooks) metaMd(data *filehandlers.PostMetaData) error {
 	parsedText, err := shared.ParseText(data.Text)
 	if err != nil {
 		return fmt.Errorf("%s: %w", data.Filename, err)
@@ -84,6 +107,25 @@ func (p *MarkdownHooks) FileMeta(s *pssh.SSHServerConnSession, data *filehandler
 
 	isHiddenFilename := slices.Contains(p.Cfg.HiddenPosts, data.Filename)
 	data.Hidden = parsedText.Hidden || isHiddenFilename
+	return nil
+}
+
+func (p *MarkdownHooks) FileMeta(s *pssh.SSHServerConnSession, data *filehandlers.PostMetaData) error {
+	ext := filepath.Ext(data.Filename)
+	switch ext {
+	case ".lxt":
+		err := p.metaLxt(data)
+		if err != nil {
+			return fmt.Errorf("%s: %w", data.Filename, err)
+		}
+	case ".md":
+		err := p.metaMd(data)
+		if err != nil {
+			return fmt.Errorf("%s: %w", data.Filename, err)
+		}
+	default:
+		return fmt.Errorf("%s: invalid filetype", data.Filename)
+	}
 
 	return nil
 }
+1 -0 pkg/apps/prose/ssh.go link
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
diff --git a/pkg/apps/prose/ssh.go b/pkg/apps/prose/ssh.go
index 709f8d8..0ea4e00 100644
--- a/pkg/apps/prose/ssh.go
+++ b/pkg/apps/prose/ssh.go
@@ -55,6 +55,7 @@ func StartSshServer() {
 		".md":      filehandlers.NewScpPostHandler(dbh, cfg, hooks),
 		".txt":     filehandlers.NewScpPostHandler(dbh, cfg, hooks),
 		".css":     filehandlers.NewScpPostHandler(dbh, cfg, hooks),
+		".lxt":     filehandlers.NewScpPostHandler(dbh, cfg, hooks),
 		"fallback": uploadimgs.NewUploadImgHandler(dbh, cfg, st),
 	}
 	handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap)
+23 -6 pkg/shared/listparser.go link
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
diff --git a/pkg/shared/listparser.go b/pkg/shared/listparser.go
index 59ee741..89f369d 100644
--- a/pkg/shared/listparser.go
+++ b/pkg/shared/listparser.go
@@ -34,12 +34,17 @@ type ListItem struct {
 }
 
 type ListMetaData struct {
-	PublishAt      *time.Time
-	Title          string
-	Description    string
-	Layout         string
-	Tags           []string
-	ListType       string // https://developer.mozilla.org/en-US/docs/Web/CSS/list-style-type
+	// prose
+	PublishAt   *time.Time
+	Title       string
+	Description string
+	Layout      string
+	Tags        []string
+	Hidden      bool
+	Aliases     []string
+	ListType    string // https://developer.mozilla.org/en-US/docs/Web/CSS/list-style-type
+
+	// feeds
 	DigestInterval string
 	Cron           string
 	Email          string
@@ -103,12 +108,24 @@ func TokenToMetaField(meta *ListMetaData, token *SplitToken) error {
 		meta.Description = token.Value
 	case "list_type":
 		meta.ListType = token.Value
+	case "hidden":
+		if token.Value == "true" {
+			meta.Hidden = true
+		} else {
+			meta.Hidden = false
+		}
 	case "tags":
 		tags := strings.Split(token.Value, ",")
 		meta.Tags = make([]string, 0)
 		for _, tag := range tags {
 			meta.Tags = append(meta.Tags, strings.TrimSpace(tag))
 		}
+	case "aliases":
+		aliases := strings.Split(token.Value, ",")
+		meta.Aliases = make([]string, 0)
+		for _, alias := range aliases {
+			meta.Aliases = append(meta.Aliases, strings.TrimSpace(alias))
+		}
 	case "layout":
 		meta.Layout = token.Value
 	case "digest_interval":