pico
created pr with
102.1
added 102.2
1: e265f5e ! 1: ff673f6 feat(prose): lists are back bby
added 102.3
1: ff673f6 ! 1: c86f5a4 feat(prose): lists are back bby
cmds
checkout latest patchset:
ssh pr.pico.sh print 102 | git am -3checkout any patchset in a patch request:
ssh pr.pico.sh print 102.[rev] | git am -3add changes to patch request:
git format-patch main --stdout | ssh pr.pico.sh pr add 102
Patchset
102.3
feat(prose): lists are back bby
Eric Bower
2026-01-04T04:35:04ZSemantic diff summary
2 added,
9 modified,
0 signature changed,
1 removed
across 5 analyzed files
(3 files skipped: unsupported file type)
+109
-46
pkg/apps/prose/api.go
#
| ... | ... | @@ -427,22 +428,38 @@ func postHandler(w http.ResponseWriter, r *http.Request) { | |
| 427 | 428 | post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space) | |
| 428 | 429 | if err == nil { | |
| 429 | 430 | 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 | + | tags = list.Tags | |
| 441 | + | if post.Hidden || post.PublishAt.After(time.Now()) { | |
| 442 | + | unlisted = true | |
| 443 | + | } | |
| 444 | + | case ".md": | |
| 445 | + | parsedText, err := shared.ParseText(post.Text) | |
| 446 | + | if err != nil { | |
| 447 | + | logger.Error("could not parse md text", "err", err.Error()) | |
| 448 | + | } | |
| 434 | 449 | ||
| 435 | - | if parsedText.Image != "" { | |
| 436 | - | ogImage = parsedText.Image | |
| 437 | - | } | |
| 450 | + | if parsedText.Image != "" { | |
| 451 | + | ogImage = parsedText.Image | |
| 452 | + | } | |
| 438 | 453 | ||
| 439 | - | if parsedText.ImageCard != "" { | |
| 440 | - | ogImageCard = parsedText.ImageCard | |
| 441 | - | } | |
| 454 | + | if parsedText.ImageCard != "" { | |
| 455 | + | ogImageCard = parsedText.ImageCard | |
| 456 | + | } | |
| 457 | + | tags = parsedText.Tags | |
| 442 | 458 | ||
| 443 | - | unlisted := false | |
| 444 | - | if post.Hidden || post.PublishAt.After(time.Now()) { | |
| 445 | - | unlisted = true | |
| 459 | + | if post.Hidden || post.PublishAt.After(time.Now()) { | |
| 460 | + | unlisted = true | |
| 461 | + | } | |
| 462 | + | contents = template.HTML(parsedText.Html) | |
| 446 | 463 | } | |
| 447 | 464 | ||
| 448 | 465 | data = PostPageData{ |
| ... | ... | @@ -459,10 +476,10 @@ func postHandler(w http.ResponseWriter, r *http.Request) { | |
| 459 | 476 | UpdatedAtISO: post.UpdatedAt.Format(time.RFC3339), | |
| 460 | 477 | Username: username, | |
| 461 | 478 | BlogName: blogName, | |
| 462 | - | Contents: template.HTML(parsedText.Html), | |
| 479 | + | Contents: contents, | |
| 463 | 480 | HasCSS: hasCSS, | |
| 464 | 481 | CssURL: template.URL(cfg.CssURL(username)), | |
| 465 | - | Tags: parsedText.Tags, | |
| 482 | + | Tags: tags, | |
| 466 | 483 | Image: template.URL(ogImage), | |
| 467 | 484 | ImageCard: ogImageCard, | |
| 468 | 485 | Favicon: template.URL(favicon), |
| ... | ... | @@ -651,7 +670,10 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) { | |
| 651 | 670 | return | |
| 652 | 671 | } | |
| 653 | 672 | ||
| 654 | - | ts, err := template.ParseFiles(cfg.StaticPath("html/rss.page.tmpl")) | |
| 673 | + | ts, err := template.New("rss.page.tmpl").Funcs(shared.FuncMap).ParseFiles( | |
| 674 | + | cfg.StaticPath("html/list.partial.tmpl"), | |
| 675 | + | cfg.StaticPath("html/rss.page.tmpl"), | |
| 676 | + | ) | |
| 655 | 677 | if err != nil { | |
| 656 | 678 | logger.Error("template parse file", "err", err.Error()) | |
| 657 | 679 | http.Error(w, err.Error(), http.StatusInternalServerError) |
| ... | ... | @@ -700,27 +722,48 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) { | |
| 700 | 722 | if slices.Contains(cfg.HiddenPosts, post.Filename) { | |
| 701 | 723 | continue | |
| 702 | 724 | } | |
| 703 | - | parsed, err := shared.ParseText(post.Text) | |
| 704 | - | if err != nil { | |
| 705 | - | logger.Error("parse post text", "err", err.Error()) | |
| 706 | - | } | |
| 707 | 725 | ||
| 708 | - | footer, err := dbpool.FindPostWithFilename("_footer.md", user.ID, cfg.Space) | |
| 709 | - | var footerHTML string | |
| 710 | - | if err == nil { | |
| 711 | - | footerParsed, err := shared.ParseText(footer.Text) | |
| 726 | + | content := "" | |
| 727 | + | ext := filepath.Ext(post.Filename) | |
| 728 | + | fmt.Println("HERE", post.Filename, ext) | |
| 729 | + | switch ext { | |
| 730 | + | case ".md": | |
| 731 | + | parsed, err := shared.ParseText(post.Text) | |
| 712 | 732 | if err != nil { | |
| 713 | - | logger.Error("parse footer text", "err", err.Error()) | |
| 733 | + | logger.Error("parse post text", "err", err.Error()) | |
| 714 | 734 | } | |
| 715 | - | footerHTML = footerParsed.Html | |
| 716 | - | } | |
| 717 | 735 | ||
| 718 | - | var tpl bytes.Buffer | |
| 719 | - | data := &PostPageData{ | |
| 720 | - | Contents: template.HTML(parsed.Html + footerHTML), | |
| 721 | - | } | |
| 722 | - | if err := ts.Execute(&tpl, data); err != nil { | |
| 723 | - | continue | |
| 736 | + | footer, err := dbpool.FindPostWithFilename("_footer.md", user.ID, cfg.Space) | |
| 737 | + | var footerHTML string | |
| 738 | + | if err == nil { | |
| 739 | + | footerParsed, err := shared.ParseText(footer.Text) | |
| 740 | + | if err != nil { | |
| 741 | + | logger.Error("parse footer text", "err", err.Error()) | |
| 742 | + | } | |
| 743 | + | footerHTML = footerParsed.Html | |
| 744 | + | } | |
| 745 | + | ||
| 746 | + | var tpl bytes.Buffer | |
| 747 | + | data := &PostPageData{ | |
| 748 | + | Contents: template.HTML(parsed.Html + footerHTML), | |
| 749 | + | } | |
| 750 | + | if err := ts.Execute(&tpl, data); err != nil { | |
| 751 | + | logger.Error("md template", "err", err) | |
| 752 | + | continue | |
| 753 | + | } | |
| 754 | + | content = tpl.String() | |
| 755 | + | case ".lxt": | |
| 756 | + | parsed := shared.ListParseText(post.Text) | |
| 757 | + | var tpl bytes.Buffer | |
| 758 | + | data := &PostPageData{ | |
| 759 | + | List: parsed, | |
| 760 | + | } | |
| 761 | + | if err := ts.Execute(&tpl, data); err != nil { | |
| 762 | + | logger.Error("lxt template", "err", err) | |
| 763 | + | continue | |
| 764 | + | } | |
| 765 | + | content = tpl.String() | |
| 766 | + | ||
| 724 | 767 | } | |
| 725 | 768 | ||
| 726 | 769 | realUrl := cfg.FullPostURL(curl, post.Username, post.Slug) |
| ... | ... | @@ -730,7 +773,7 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) { | |
| 730 | 773 | Id: feedId, | |
| 731 | 774 | Title: utils.FilenameToTitle(post.Filename, post.Title), | |
| 732 | 775 | Link: &feeds.Link{Href: realUrl}, | |
| 733 | - | Content: tpl.String(), | |
| 776 | + | Content: content, | |
| 734 | 777 | Updated: *post.PublishAt, | |
| 735 | 778 | Created: *post.PublishAt, | |
| 736 | 779 | Description: post.Description, |
| ... | ... | @@ -769,7 +812,10 @@ func rssHandler(w http.ResponseWriter, r *http.Request) { | |
| 769 | 812 | return | |
| 770 | 813 | } | |
| 771 | 814 | ||
| 772 | - | ts, err := template.ParseFiles(cfg.StaticPath("html/rss.page.tmpl")) | |
| 815 | + | ts, err := template.New("rss.page.tmpl").Funcs(shared.FuncMap).ParseFiles( | |
| 816 | + | cfg.StaticPath("html/list.partial.tmpl"), | |
| 817 | + | cfg.StaticPath("html/rss.page.tmpl"), | |
| 818 | + | ) | |
| 773 | 819 | if err != nil { | |
| 774 | 820 | logger.Error("template parse file", "err", err.Error()) | |
| 775 | 821 | http.Error(w, err.Error(), http.StatusInternalServerError) |
| ... | ... | @@ -788,17 +834,34 @@ func rssHandler(w http.ResponseWriter, r *http.Request) { | |
| 788 | 834 | ||
| 789 | 835 | var feedItems []*feeds.Item | |
| 790 | 836 | for _, post := range pager.Data { | |
| 791 | - | parsed, err := shared.ParseText(post.Text) | |
| 792 | - | if err != nil { | |
| 793 | - | logger.Error(err.Error()) | |
| 794 | - | } | |
| 837 | + | content := "" | |
| 838 | + | ext := filepath.Ext(post.Filename) | |
| 839 | + | switch ext { | |
| 840 | + | case ".md": | |
| 841 | + | parsed, err := shared.ParseText(post.Text) | |
| 842 | + | if err != nil { | |
| 843 | + | logger.Error(err.Error()) | |
| 844 | + | } | |
| 845 | + | ||
| 846 | + | var tpl bytes.Buffer | |
| 847 | + | data := &PostPageData{ | |
| 848 | + | Contents: template.HTML(parsed.Html), | |
| 849 | + | } | |
| 850 | + | if err := ts.Execute(&tpl, data); err != nil { | |
| 851 | + | continue | |
| 852 | + | } | |
| 853 | + | content = tpl.String() | |
| 854 | + | case ".lxt": | |
| 855 | + | parsed := shared.ListParseText(post.Text) | |
| 856 | + | var tpl bytes.Buffer | |
| 857 | + | data := &PostPageData{ | |
| 858 | + | List: parsed, | |
| 859 | + | } | |
| 860 | + | if err := ts.Execute(&tpl, data); err != nil { | |
| 861 | + | continue | |
| 862 | + | } | |
| 863 | + | content = tpl.String() | |
| 795 | 864 | ||
| 796 | - | var tpl bytes.Buffer | |
| 797 | - | data := &PostPageData{ | |
| 798 | - | Contents: template.HTML(parsed.Html), | |
| 799 | - | } | |
| 800 | - | if err := ts.Execute(&tpl, data); err != nil { | |
| 801 | - | continue | |
| 802 | 865 | } | |
| 803 | 866 | ||
| 804 | 867 | realUrl := cfg.FullPostURL(curl, post.Username, post.Slug) |
| ... | ... | @@ -810,7 +873,7 @@ func rssHandler(w http.ResponseWriter, r *http.Request) { | |
| 810 | 873 | Id: realUrl, | |
| 811 | 874 | Title: post.Title, | |
| 812 | 875 | Link: &feeds.Link{Href: realUrl}, | |
| 813 | - | Content: tpl.String(), | |
| 876 | + | Content: content, | |
| 814 | 877 | Created: *post.PublishAt, | |
| 815 | 878 | Updated: *post.UpdatedAt, | |
| 816 | 879 | Description: post.Description, |
+1
-0
pkg/apps/prose/config.go
#
+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> | |
| 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
#
+5
-1
pkg/apps/prose/html/rss.page.tmpl
#
+43
-1
pkg/apps/prose/scp_hooks.go
#
| ... | ... | @@ -62,7 +63,29 @@ func (p *MarkdownHooks) FileValidate(s *pssh.SSHServerConnSession, data *filehan | |
| 62 | 63 | return true, nil | |
| 63 | 64 | } | |
| 64 | 65 | ||
| 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 { | |
| 66 | 89 | parsedText, err := shared.ParseText(data.Text) | |
| 67 | 90 | if err != nil { | |
| 68 | 91 | return fmt.Errorf("%s: %w", data.Filename, err) |
| ... | ... | @@ -84,6 +107,25 @@ func (p *MarkdownHooks) FileMeta(s *pssh.SSHServerConnSession, data *filehandler | |
| 84 | 107 | ||
| 85 | 108 | isHiddenFilename := slices.Contains(p.Cfg.HiddenPosts, data.Filename) | |
| 86 | 109 | 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 | + | } | |
| 87 | 129 | ||
| 88 | 130 | return nil | |
| 89 | 131 | } |
+1
-0
pkg/apps/prose/ssh.go
#
| ... | ... | @@ -55,6 +55,7 @@ func StartSshServer() { | |
| 55 | 55 | ".md": filehandlers.NewScpPostHandler(dbh, cfg, hooks), | |
| 56 | 56 | ".txt": filehandlers.NewScpPostHandler(dbh, cfg, hooks), | |
| 57 | 57 | ".css": filehandlers.NewScpPostHandler(dbh, cfg, hooks), | |
| 58 | + | ".lxt": filehandlers.NewScpPostHandler(dbh, cfg, hooks), | |
| 58 | 59 | "fallback": uploadimgs.NewUploadImgHandler(dbh, cfg, st), | |
| 59 | 60 | } | |
| 60 | 61 | handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap) |