Commit 63b93d9

Eric Bower  ·  2026-01-03 23:35:04 -0500 EST
parent bb7b4f2
feat(prose): lists are back bby
8 files changed,  +253, -58
+114, -46
......@@ -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,43 @@ 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
434436
435- if parsedText.Image != "" {
436- ogImage = parsedText.Image
437- }
437+ switch ext {
438+ case ".lxt":
439+ list = shared.ListParseText(post.Text)
438440
439- if parsedText.ImageCard != "" {
440- ogImageCard = parsedText.ImageCard
441- }
441+ tags = list.Tags
442+ if list.Image != "" {
443+ ogImage = list.Image
444+ }
445+ if list.ImageCard != "" {
446+ ogImageCard = list.ImageCard
447+ }
448+ if post.Hidden || post.PublishAt.After(time.Now()) {
449+ unlisted = true
450+ }
451+ case ".md":
452+ parsedText, err := shared.ParseText(post.Text)
453+ if err != nil {
454+ logger.Error("could not parse md text", "err", err.Error())
455+ }
442456
443- unlisted := false
444- if post.Hidden || post.PublishAt.After(time.Now()) {
445- unlisted = true
457+ tags = parsedText.Tags
458+ if parsedText.Image != "" {
459+ ogImage = parsedText.Image
460+ }
461+ if parsedText.ImageCard != "" {
462+ ogImageCard = parsedText.ImageCard
463+ }
464+ if post.Hidden || post.PublishAt.After(time.Now()) {
465+ unlisted = true
466+ }
467+ contents = template.HTML(parsedText.Html)
446468 }
447469
448470 data = PostPageData{
......@@ -459,10 +481,10 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
459481 UpdatedAtISO: post.UpdatedAt.Format(time.RFC3339),
460482 Username: username,
461483 BlogName: blogName,
462- Contents: template.HTML(parsedText.Html),
484+ Contents: contents,
463485 HasCSS: hasCSS,
464486 CssURL: template.URL(cfg.CssURL(username)),
465- Tags: parsedText.Tags,
487+ Tags: tags,
466488 Image: template.URL(ogImage),
467489 ImageCard: ogImageCard,
468490 Favicon: template.URL(favicon),
......@@ -470,6 +492,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
470492 Unlisted: unlisted,
471493 Diff: template.HTML(diff),
472494 WithStyles: withStyles,
495+ List: list,
473496 }
474497 } else {
475498 logger.Info("post not found")
......@@ -522,6 +545,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
522545 }
523546
524547 ts, err := shared.RenderTemplate(cfg, []string{
548+ cfg.StaticPath("html/list.partial.tmpl"),
525549 cfg.StaticPath("html/post.page.tmpl"),
526550 })
527551
......@@ -651,7 +675,10 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
651675 return
652676 }
653677
654- ts, err := template.ParseFiles(cfg.StaticPath("html/rss.page.tmpl"))
678+ ts, err := template.New("rss.page.tmpl").Funcs(shared.FuncMap).ParseFiles(
679+ cfg.StaticPath("html/list.partial.tmpl"),
680+ cfg.StaticPath("html/rss.page.tmpl"),
681+ )
655682 if err != nil {
656683 logger.Error("template parse file", "err", err.Error())
657684 http.Error(w, err.Error(), http.StatusInternalServerError)
......@@ -700,27 +727,48 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
700727 if slices.Contains(cfg.HiddenPosts, post.Filename) {
701728 continue
702729 }
703- parsed, err := shared.ParseText(post.Text)
704- if err != nil {
705- logger.Error("parse post text", "err", err.Error())
706- }
707730
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)
731+ content := ""
732+ ext := filepath.Ext(post.Filename)
733+ fmt.Println("HERE", post.Filename, ext)
734+ switch ext {
735+ case ".md":
736+ parsed, err := shared.ParseText(post.Text)
712737 if err != nil {
713- logger.Error("parse footer text", "err", err.Error())
738+ logger.Error("parse post text", "err", err.Error())
714739 }
715- footerHTML = footerParsed.Html
716- }
717740
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
741+ footer, err := dbpool.FindPostWithFilename("_footer.md", user.ID, cfg.Space)
742+ var footerHTML string
743+ if err == nil {
744+ footerParsed, err := shared.ParseText(footer.Text)
745+ if err != nil {
746+ logger.Error("parse footer text", "err", err.Error())
747+ }
748+ footerHTML = footerParsed.Html
749+ }
750+
751+ var tpl bytes.Buffer
752+ data := &PostPageData{
753+ Contents: template.HTML(parsed.Html + footerHTML),
754+ }
755+ if err := ts.Execute(&tpl, data); err != nil {
756+ logger.Error("md template", "err", err)
757+ continue
758+ }
759+ content = tpl.String()
760+ case ".lxt":
761+ parsed := shared.ListParseText(post.Text)
762+ var tpl bytes.Buffer
763+ data := &PostPageData{
764+ List: parsed,
765+ }
766+ if err := ts.Execute(&tpl, data); err != nil {
767+ logger.Error("lxt template", "err", err)
768+ continue
769+ }
770+ content = tpl.String()
771+
724772 }
725773
726774 realUrl := cfg.FullPostURL(curl, post.Username, post.Slug)
......@@ -730,7 +778,7 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
730778 Id: feedId,
731779 Title: utils.FilenameToTitle(post.Filename, post.Title),
732780 Link: &feeds.Link{Href: realUrl},
733- Content: tpl.String(),
781+ Content: content,
734782 Updated: *post.PublishAt,
735783 Created: *post.PublishAt,
736784 Description: post.Description,
......@@ -769,7 +817,10 @@ func rssHandler(w http.ResponseWriter, r *http.Request) {
769817 return
770818 }
771819
772- ts, err := template.ParseFiles(cfg.StaticPath("html/rss.page.tmpl"))
820+ ts, err := template.New("rss.page.tmpl").Funcs(shared.FuncMap).ParseFiles(
821+ cfg.StaticPath("html/list.partial.tmpl"),
822+ cfg.StaticPath("html/rss.page.tmpl"),
823+ )
773824 if err != nil {
774825 logger.Error("template parse file", "err", err.Error())
775826 http.Error(w, err.Error(), http.StatusInternalServerError)
......@@ -788,17 +839,34 @@ func rssHandler(w http.ResponseWriter, r *http.Request) {
788839
789840 var feedItems []*feeds.Item
790841 for _, post := range pager.Data {
791- parsed, err := shared.ParseText(post.Text)
792- if err != nil {
793- logger.Error(err.Error())
794- }
842+ content := ""
843+ ext := filepath.Ext(post.Filename)
844+ switch ext {
845+ case ".md":
846+ parsed, err := shared.ParseText(post.Text)
847+ if err != nil {
848+ logger.Error(err.Error())
849+ }
850+
851+ var tpl bytes.Buffer
852+ data := &PostPageData{
853+ Contents: template.HTML(parsed.Html),
854+ }
855+ if err := ts.Execute(&tpl, data); err != nil {
856+ continue
857+ }
858+ content = tpl.String()
859+ case ".lxt":
860+ parsed := shared.ListParseText(post.Text)
861+ var tpl bytes.Buffer
862+ data := &PostPageData{
863+ List: parsed,
864+ }
865+ if err := ts.Execute(&tpl, data); err != nil {
866+ continue
867+ }
868+ content = tpl.String()
795869
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
802870 }
803871
804872 realUrl := cfg.FullPostURL(curl, post.Username, post.Slug)
......@@ -810,7 +878,7 @@ func rssHandler(w http.ResponseWriter, r *http.Request) {
810878 Id: realUrl,
811879 Title: post.Title,
812880 Link: &feeds.Link{Href: realUrl},
813- Content: tpl.String(),
881+ Content: content,
814882 Created: *post.PublishAt,
815883 Updated: *post.UpdatedAt,
816884 Description: post.Description,
+1, -0
......@@ -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
......@@ -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
......@@ -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}}
+5, -1
......@@ -1 +1,5 @@
1-{{.Contents}}
1+{{if .List}}
2+ {{template "list" .List}}
3+{{else}}
4+ {{.Contents}}
5+{{end}}
+43, -2
......@@ -2,6 +2,7 @@ package prose
22
33 import (
44 "fmt"
5+ "path/filepath"
56 "strings"
67
78 "slices"
......@@ -62,7 +63,28 @@ 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+ data.Hidden = parsedText.Hidden
83+
84+ return nil
85+}
86+
87+func (p *MarkdownHooks) metaMd(data *filehandlers.PostMetaData) error {
6688 parsedText, err := shared.ParseText(data.Text)
6789 if err != nil {
6890 return fmt.Errorf("%s: %w", data.Filename, err)
......@@ -81,9 +103,28 @@ func (p *MarkdownHooks) FileMeta(s *pssh.SSHServerConnSession, data *filehandler
81103 if parsedText.PublishAt != nil && !parsedText.PublishAt.IsZero() {
82104 data.PublishAt = parsedText.PublishAt
83105 }
106+ data.Hidden = parsedText.Hidden
107+
108+ return nil
109+}
110+
111+func (p *MarkdownHooks) FileMeta(s *pssh.SSHServerConnSession, data *filehandlers.PostMetaData) error {
112+ ext := filepath.Ext(data.Filename)
113+ switch ext {
114+ case ".lxt":
115+ err := p.metaLxt(data)
116+ if err != nil {
117+ return fmt.Errorf("%s: %w", data.Filename, err)
118+ }
119+ case ".md":
120+ err := p.metaMd(data)
121+ if err != nil {
122+ return fmt.Errorf("%s: %w", data.Filename, err)
123+ }
124+ }
84125
85126 isHiddenFilename := slices.Contains(p.Cfg.HiddenPosts, data.Filename)
86- data.Hidden = parsedText.Hidden || isHiddenFilename
127+ data.Hidden = data.Hidden || isHiddenFilename
87128
88129 return nil
89130 }
+1, -0
......@@ -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)
+33, -8
......@@ -34,12 +34,19 @@ 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+ Aliases []string
39+ Description string
40+ Hidden bool
41+ Image string
42+ ImageCard string
43+ Layout string
44+ ListType string // https://developer.mozilla.org/en-US/docs/Web/CSS/list-style-type
45+ PublishAt *time.Time
46+ Tags []string
47+ Title string
48+
49+ // feeds
4350 DigestInterval string
4451 Cron string
4552 Email string
......@@ -101,14 +108,30 @@ func TokenToMetaField(meta *ListMetaData, token *SplitToken) error {
101108 meta.Title = token.Value
102109 case "description":
103110 meta.Description = token.Value
111+ case "image":
112+ meta.Image = token.Value
113+ case "image_card":
114+ meta.ImageCard = token.Value
104115 case "list_type":
105116 meta.ListType = token.Value
117+ case "hidden":
118+ if token.Value == "true" {
119+ meta.Hidden = true
120+ } else {
121+ meta.Hidden = false
122+ }
106123 case "tags":
107124 tags := strings.Split(token.Value, ",")
108125 meta.Tags = make([]string, 0)
109126 for _, tag := range tags {
110127 meta.Tags = append(meta.Tags, strings.TrimSpace(tag))
111128 }
129+ case "aliases":
130+ aliases := strings.Split(token.Value, ",")
131+ meta.Aliases = make([]string, 0)
132+ for _, alias := range aliases {
133+ meta.Aliases = append(meta.Aliases, strings.TrimSpace(alias))
134+ }
112135 case "layout":
113136 meta.Layout = token.Value
114137 case "digest_interval":
......@@ -206,10 +229,12 @@ func ListParseText(text string) *ListParsedText {
206229 textItems := SplitByNewline(text)
207230 items := []*ListItem{}
208231 meta := ListMetaData{
232+ Aliases: []string{},
233+ InlineContent: true,
234+ Layout: "default",
209235 ListType: "disc",
236+ PublishAt: &time.Time{},
210237 Tags: []string{},
211- Layout: "default",
212- InlineContent: true,
213238 }
214239 pre := false
215240 skip := false