pico

created pr with pico/102.1 on 2026-01-04T04:57:34Z · by c8ef7d19
added pico/102.2 on 2026-01-04T15:06:18Z · by c8ef7d19
1: e265f5e ! 1: ff673f6 feat(prose): lists are back bby
added pico/102.3 on 2026-01-04T15:36:38Z · by c8ef7d19
1: ff673f6 ! 1: c86f5a4 feat(prose): lists are back bby
cmds
checkout latest patchset:
ssh pr.pico.sh pull pico:102 | git am -3
checkout specific patchset revision:
ssh pr.pico.sh pull pico:102 [rev] | git am -3
add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pico:102
add comment to patch request:
ssh pr.pico.sh comment pico:102 "lgtm!"
+43 -15 pkg/apps/prose/api.go #
......@@ -90,6 +90,7 @@ type PostPageData struct {
9090 Diff template.HTML
9191 UpdatedAtISO string
9292 UpdatedAt string
93+ List *shared.ListParsedText
9394 }
9495
9596 type HeaderTxt struct {
......@@ -427,22 +428,47 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
427428 post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
428429 if err == nil {
429430 logger.Info("post found", "id", post.ID, "filename", post.FileSize)
430- parsedText, err := shared.ParseText(post.Text)
431- if err != nil {
432- logger.Error("find post with slug", "err", err.Error())
433- }
431+ ext := filepath.Ext(post.Filename)
432+ contents := template.HTML("")
433+ tags := []string{}
434+ unlisted := false
435+ var list *shared.ListParsedText
436+
437+ switch ext {
438+ case ".lxt":
439+ list = shared.ListParseText(post.Text)
440+
441+ // if parsedText.Image != "" {
442+ // ogImage = parsedText.Image
443+ // }
444+ //
445+ // if parsedText.ImageCard != "" {
446+ // ogImageCard = parsedText.ImageCard
447+ // }
448+ tags = list.Tags
449+
450+ if post.Hidden || post.PublishAt.After(time.Now()) {
451+ unlisted = true
452+ }
453+ case ".md":
454+ parsedText, err := shared.ParseText(post.Text)
455+ if err != nil {
456+ logger.Error("could not parse md text", "err", err.Error())
457+ }
434458
435- if parsedText.Image != "" {
436- ogImage = parsedText.Image
437- }
459+ if parsedText.Image != "" {
460+ ogImage = parsedText.Image
461+ }
438462
439- if parsedText.ImageCard != "" {
440- ogImageCard = parsedText.ImageCard
441- }
463+ if parsedText.ImageCard != "" {
464+ ogImageCard = parsedText.ImageCard
465+ }
466+ tags = parsedText.Tags
442467
443- unlisted := false
444- if post.Hidden || post.PublishAt.After(time.Now()) {
445- unlisted = true
468+ if post.Hidden || post.PublishAt.After(time.Now()) {
469+ unlisted = true
470+ }
471+ contents = template.HTML(parsedText.Html)
446472 }
447473
448474 data = PostPageData{
......@@ -459,10 +485,10 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
459485 UpdatedAtISO: post.UpdatedAt.Format(time.RFC3339),
460486 Username: username,
461487 BlogName: blogName,
462- Contents: template.HTML(parsedText.Html),
488+ Contents: contents,
463489 HasCSS: hasCSS,
464490 CssURL: template.URL(cfg.CssURL(username)),
465- Tags: parsedText.Tags,
491+ Tags: tags,
466492 Image: template.URL(ogImage),
467493 ImageCard: ogImageCard,
468494 Favicon: template.URL(favicon),
......@@ -470,6 +496,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
470496 Unlisted: unlisted,
471497 Diff: template.HTML(diff),
472498 WithStyles: withStyles,
499+ List: list,
473500 }
474501 } else {
475502 logger.Info("post not found")
......@@ -522,6 +549,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
522549 }
523550
524551 ts, err := shared.RenderTemplate(cfg, []string{
552+ cfg.StaticPath("html/list.partial.tmpl"),
525553 cfg.StaticPath("html/post.page.tmpl"),
526554 })
527555
+1 -0 pkg/apps/prose/config.go #
......@@ -28,6 +28,7 @@ func NewConfigSite(service string) *shared.ConfigSite {
2828 Space: "prose",
2929 AllowedExt: []string{
3030 ".md",
31+ ".lxt",
3132 ".jpg",
3233 ".jpeg",
3334 ".png",
+51 -0 pkg/apps/prose/html/list.partial.tmpl #
......@@ -0,0 +1,51 @@
1+{{define "list"}}
2+{{$indent := 0}}
3+{{$mod := 0}}
4+<ul style="list-style-type: {{.ListType}};">
5+ {{range .Items}}
6+ {{if lt $indent .Indent}}
7+ <ul style="list-style-type: {{$.ListType}};">
8+ {{else if gt $indent .Indent}}
9+
10+ {{$mod = minus $indent .Indent}}
11+ {{range $y := intRange 1 $mod}}
12+ </li></ul>
13+ {{end}}
14+
15+ {{else}}
16+ </li>
17+ {{end}}
18+ {{$indent = .Indent}}
19+
20+ {{if .IsText}}
21+ {{if .Value}}
22+ <li>{{.Value}}
23+ {{end}}
24+ {{end}}
25+
26+ {{if .IsURL}}
27+ <li><a href="{{.URL}}">{{.Value}}</a>
28+ {{end}}
29+
30+ {{if .IsImg}}
31+ <li><img src="{{.URL}}" alt="{{.Value}}" />
32+ {{end}}
33+
34+ {{if .IsBlock}}
35+ <li><blockquote>{{.Value}}</blockquote>
36+ {{end}}
37+
38+ {{if .IsHeaderOne}}
39+ </ul><h2 class="text-xl font-bold">{{.Value}}</h2><ul style="list-style-type: {{$.ListType}};">
40+ {{end}}
41+
42+ {{if .IsHeaderTwo}}
43+ </ul><h3 class="text-lg font-bold">{{.Value}}</h3><ul style="list-style-type: {{$.ListType}};">
44+ {{end}}
45+
46+ {{if .IsPre}}
47+ <li><pre>{{.Value}}</pre>
48+ {{end}}
49+ {{end}}
50+</ul>
51+{{end}}
+5 -1 pkg/apps/prose/html/post.page.tmpl #
......@@ -65,7 +65,11 @@
6565 </header>
6666 <main>
6767 <article class="md">
68- {{.Contents}}
68+ {{if .List}}
69+ {{template "list" .List}}
70+ {{else}}
71+ {{.Contents}}
72+ {{end}}
6973
7074 <div class="tags">
7175 {{range .Tags}}
+43 -1 pkg/apps/prose/scp_hooks.go #
......@@ -2,6 +2,7 @@ package prose
22
33 import (
44 "fmt"
5+ "path/filepath"
56 "strings"
67
78 "slices"
......@@ -62,7 +63,29 @@ func (p *MarkdownHooks) FileValidate(s *pssh.SSHServerConnSession, data *filehan
6263 return true, nil
6364 }
6465
65-func (p *MarkdownHooks) FileMeta(s *pssh.SSHServerConnSession, data *filehandlers.PostMetaData) error {
66+func (p *MarkdownHooks) metaLxt(data *filehandlers.PostMetaData) error {
67+ parsedText := shared.ListParseText(data.Text)
68+
69+ if parsedText.Title == "" {
70+ data.Title = utils.ToUpper(data.Slug)
71+ } else {
72+ data.Title = parsedText.Title
73+ }
74+
75+ data.Aliases = parsedText.Aliases
76+ data.Tags = parsedText.Tags
77+ data.Description = parsedText.Description
78+
79+ if parsedText.PublishAt != nil && !parsedText.PublishAt.IsZero() {
80+ data.PublishAt = parsedText.PublishAt
81+ }
82+
83+ isHiddenFilename := slices.Contains(p.Cfg.HiddenPosts, data.Filename)
84+ data.Hidden = parsedText.Hidden || isHiddenFilename
85+ return nil
86+}
87+
88+func (p *MarkdownHooks) metaMd(data *filehandlers.PostMetaData) error {
6689 parsedText, err := shared.ParseText(data.Text)
6790 if err != nil {
6891 return fmt.Errorf("%s: %w", data.Filename, err)
......@@ -84,6 +107,25 @@ func (p *MarkdownHooks) FileMeta(s *pssh.SSHServerConnSession, data *filehandler
84107
85108 isHiddenFilename := slices.Contains(p.Cfg.HiddenPosts, data.Filename)
86109 data.Hidden = parsedText.Hidden || isHiddenFilename
110+ return nil
111+}
112+
113+func (p *MarkdownHooks) FileMeta(s *pssh.SSHServerConnSession, data *filehandlers.PostMetaData) error {
114+ ext := filepath.Ext(data.Filename)
115+ switch ext {
116+ case ".lxt":
117+ err := p.metaLxt(data)
118+ if err != nil {
119+ return fmt.Errorf("%s: %w", data.Filename, err)
120+ }
121+ case ".md":
122+ err := p.metaMd(data)
123+ if err != nil {
124+ return fmt.Errorf("%s: %w", data.Filename, err)
125+ }
126+ default:
127+ return fmt.Errorf("%s: invalid filetype", data.Filename)
128+ }
87129
88130 return nil
89131 }
+1 -0 pkg/apps/prose/ssh.go #
......@@ -55,6 +55,7 @@ func StartSshServer() {
5555 ".md": filehandlers.NewScpPostHandler(dbh, cfg, hooks),
5656 ".txt": filehandlers.NewScpPostHandler(dbh, cfg, hooks),
5757 ".css": filehandlers.NewScpPostHandler(dbh, cfg, hooks),
58+ ".lxt": filehandlers.NewScpPostHandler(dbh, cfg, hooks),
5859 "fallback": uploadimgs.NewUploadImgHandler(dbh, cfg, st),
5960 }
6061 handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap)
+23 -6 pkg/shared/listparser.go #
......@@ -34,12 +34,17 @@ type ListItem struct {
3434 }
3535
3636 type ListMetaData struct {
37- PublishAt *time.Time
38- Title string
39- Description string
40- Layout string
41- Tags []string
42- ListType string // https://developer.mozilla.org/en-US/docs/Web/CSS/list-style-type
37+ // prose
38+ PublishAt *time.Time
39+ Title string
40+ Description string
41+ Layout string
42+ Tags []string
43+ Hidden bool
44+ Aliases []string
45+ ListType string // https://developer.mozilla.org/en-US/docs/Web/CSS/list-style-type
46+
47+ // feeds
4348 DigestInterval string
4449 Cron string
4550 Email string
......@@ -103,12 +108,24 @@ func TokenToMetaField(meta *ListMetaData, token *SplitToken) error {
103108 meta.Description = token.Value
104109 case "list_type":
105110 meta.ListType = token.Value
111+ case "hidden":
112+ if token.Value == "true" {
113+ meta.Hidden = true
114+ } else {
115+ meta.Hidden = false
116+ }
106117 case "tags":
107118 tags := strings.Split(token.Value, ",")
108119 meta.Tags = make([]string, 0)
109120 for _, tag := range tags {
110121 meta.Tags = append(meta.Tags, strings.TrimSpace(tag))
111122 }
123+ case "aliases":
124+ aliases := strings.Split(token.Value, ",")
125+ meta.Aliases = make([]string, 0)
126+ for _, alias := range aliases {
127+ meta.Aliases = append(meta.Aliases, strings.TrimSpace(alias))
128+ }
112129 case "layout":
113130 meta.Layout = token.Value
114131 case "digest_interval":
Back to top