- commit
- 63b93d9
- parent
- bb7b4f2
- author
- Eric Bower
- date
- 2026-01-03 23:35:04 -0500 EST
feat(prose): lists are back bby
8 files changed,
+253,
-58
+114,
-46
1@@ -90,6 +90,7 @@ type PostPageData struct {
2 Diff template.HTML
3 UpdatedAtISO string
4 UpdatedAt string
5+ List *shared.ListParsedText
6 }
7
8 type HeaderTxt struct {
9@@ -427,22 +428,43 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
10 post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
11 if err == nil {
12 logger.Info("post found", "id", post.ID, "filename", post.FileSize)
13- parsedText, err := shared.ParseText(post.Text)
14- if err != nil {
15- logger.Error("find post with slug", "err", err.Error())
16- }
17+ ext := filepath.Ext(post.Filename)
18+ contents := template.HTML("")
19+ tags := []string{}
20+ unlisted := false
21+ var list *shared.ListParsedText
22
23- if parsedText.Image != "" {
24- ogImage = parsedText.Image
25- }
26+ switch ext {
27+ case ".lxt":
28+ list = shared.ListParseText(post.Text)
29
30- if parsedText.ImageCard != "" {
31- ogImageCard = parsedText.ImageCard
32- }
33+ tags = list.Tags
34+ if list.Image != "" {
35+ ogImage = list.Image
36+ }
37+ if list.ImageCard != "" {
38+ ogImageCard = list.ImageCard
39+ }
40+ if post.Hidden || post.PublishAt.After(time.Now()) {
41+ unlisted = true
42+ }
43+ case ".md":
44+ parsedText, err := shared.ParseText(post.Text)
45+ if err != nil {
46+ logger.Error("could not parse md text", "err", err.Error())
47+ }
48
49- unlisted := false
50- if post.Hidden || post.PublishAt.After(time.Now()) {
51- unlisted = true
52+ tags = parsedText.Tags
53+ if parsedText.Image != "" {
54+ ogImage = parsedText.Image
55+ }
56+ if parsedText.ImageCard != "" {
57+ ogImageCard = parsedText.ImageCard
58+ }
59+ if post.Hidden || post.PublishAt.After(time.Now()) {
60+ unlisted = true
61+ }
62+ contents = template.HTML(parsedText.Html)
63 }
64
65 data = PostPageData{
66@@ -459,10 +481,10 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
67 UpdatedAtISO: post.UpdatedAt.Format(time.RFC3339),
68 Username: username,
69 BlogName: blogName,
70- Contents: template.HTML(parsedText.Html),
71+ Contents: contents,
72 HasCSS: hasCSS,
73 CssURL: template.URL(cfg.CssURL(username)),
74- Tags: parsedText.Tags,
75+ Tags: tags,
76 Image: template.URL(ogImage),
77 ImageCard: ogImageCard,
78 Favicon: template.URL(favicon),
79@@ -470,6 +492,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
80 Unlisted: unlisted,
81 Diff: template.HTML(diff),
82 WithStyles: withStyles,
83+ List: list,
84 }
85 } else {
86 logger.Info("post not found")
87@@ -522,6 +545,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
88 }
89
90 ts, err := shared.RenderTemplate(cfg, []string{
91+ cfg.StaticPath("html/list.partial.tmpl"),
92 cfg.StaticPath("html/post.page.tmpl"),
93 })
94
95@@ -651,7 +675,10 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
96 return
97 }
98
99- ts, err := template.ParseFiles(cfg.StaticPath("html/rss.page.tmpl"))
100+ ts, err := template.New("rss.page.tmpl").Funcs(shared.FuncMap).ParseFiles(
101+ cfg.StaticPath("html/list.partial.tmpl"),
102+ cfg.StaticPath("html/rss.page.tmpl"),
103+ )
104 if err != nil {
105 logger.Error("template parse file", "err", err.Error())
106 http.Error(w, err.Error(), http.StatusInternalServerError)
107@@ -700,27 +727,48 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
108 if slices.Contains(cfg.HiddenPosts, post.Filename) {
109 continue
110 }
111- parsed, err := shared.ParseText(post.Text)
112- if err != nil {
113- logger.Error("parse post text", "err", err.Error())
114- }
115
116- footer, err := dbpool.FindPostWithFilename("_footer.md", user.ID, cfg.Space)
117- var footerHTML string
118- if err == nil {
119- footerParsed, err := shared.ParseText(footer.Text)
120+ content := ""
121+ ext := filepath.Ext(post.Filename)
122+ fmt.Println("HERE", post.Filename, ext)
123+ switch ext {
124+ case ".md":
125+ parsed, err := shared.ParseText(post.Text)
126 if err != nil {
127- logger.Error("parse footer text", "err", err.Error())
128+ logger.Error("parse post text", "err", err.Error())
129 }
130- footerHTML = footerParsed.Html
131- }
132
133- var tpl bytes.Buffer
134- data := &PostPageData{
135- Contents: template.HTML(parsed.Html + footerHTML),
136- }
137- if err := ts.Execute(&tpl, data); err != nil {
138- continue
139+ footer, err := dbpool.FindPostWithFilename("_footer.md", user.ID, cfg.Space)
140+ var footerHTML string
141+ if err == nil {
142+ footerParsed, err := shared.ParseText(footer.Text)
143+ if err != nil {
144+ logger.Error("parse footer text", "err", err.Error())
145+ }
146+ footerHTML = footerParsed.Html
147+ }
148+
149+ var tpl bytes.Buffer
150+ data := &PostPageData{
151+ Contents: template.HTML(parsed.Html + footerHTML),
152+ }
153+ if err := ts.Execute(&tpl, data); err != nil {
154+ logger.Error("md template", "err", err)
155+ continue
156+ }
157+ content = tpl.String()
158+ case ".lxt":
159+ parsed := shared.ListParseText(post.Text)
160+ var tpl bytes.Buffer
161+ data := &PostPageData{
162+ List: parsed,
163+ }
164+ if err := ts.Execute(&tpl, data); err != nil {
165+ logger.Error("lxt template", "err", err)
166+ continue
167+ }
168+ content = tpl.String()
169+
170 }
171
172 realUrl := cfg.FullPostURL(curl, post.Username, post.Slug)
173@@ -730,7 +778,7 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
174 Id: feedId,
175 Title: utils.FilenameToTitle(post.Filename, post.Title),
176 Link: &feeds.Link{Href: realUrl},
177- Content: tpl.String(),
178+ Content: content,
179 Updated: *post.PublishAt,
180 Created: *post.PublishAt,
181 Description: post.Description,
182@@ -769,7 +817,10 @@ func rssHandler(w http.ResponseWriter, r *http.Request) {
183 return
184 }
185
186- ts, err := template.ParseFiles(cfg.StaticPath("html/rss.page.tmpl"))
187+ ts, err := template.New("rss.page.tmpl").Funcs(shared.FuncMap).ParseFiles(
188+ cfg.StaticPath("html/list.partial.tmpl"),
189+ cfg.StaticPath("html/rss.page.tmpl"),
190+ )
191 if err != nil {
192 logger.Error("template parse file", "err", err.Error())
193 http.Error(w, err.Error(), http.StatusInternalServerError)
194@@ -788,17 +839,34 @@ func rssHandler(w http.ResponseWriter, r *http.Request) {
195
196 var feedItems []*feeds.Item
197 for _, post := range pager.Data {
198- parsed, err := shared.ParseText(post.Text)
199- if err != nil {
200- logger.Error(err.Error())
201- }
202+ content := ""
203+ ext := filepath.Ext(post.Filename)
204+ switch ext {
205+ case ".md":
206+ parsed, err := shared.ParseText(post.Text)
207+ if err != nil {
208+ logger.Error(err.Error())
209+ }
210+
211+ var tpl bytes.Buffer
212+ data := &PostPageData{
213+ Contents: template.HTML(parsed.Html),
214+ }
215+ if err := ts.Execute(&tpl, data); err != nil {
216+ continue
217+ }
218+ content = tpl.String()
219+ case ".lxt":
220+ parsed := shared.ListParseText(post.Text)
221+ var tpl bytes.Buffer
222+ data := &PostPageData{
223+ List: parsed,
224+ }
225+ if err := ts.Execute(&tpl, data); err != nil {
226+ continue
227+ }
228+ content = tpl.String()
229
230- var tpl bytes.Buffer
231- data := &PostPageData{
232- Contents: template.HTML(parsed.Html),
233- }
234- if err := ts.Execute(&tpl, data); err != nil {
235- continue
236 }
237
238 realUrl := cfg.FullPostURL(curl, post.Username, post.Slug)
239@@ -810,7 +878,7 @@ func rssHandler(w http.ResponseWriter, r *http.Request) {
240 Id: realUrl,
241 Title: post.Title,
242 Link: &feeds.Link{Href: realUrl},
243- Content: tpl.String(),
244+ Content: content,
245 Created: *post.PublishAt,
246 Updated: *post.UpdatedAt,
247 Description: post.Description,
+1,
-0
1@@ -28,6 +28,7 @@ func NewConfigSite(service string) *shared.ConfigSite {
2 Space: "prose",
3 AllowedExt: []string{
4 ".md",
5+ ".lxt",
6 ".jpg",
7 ".jpeg",
8 ".png",
+51,
-0
1@@ -0,0 +1,51 @@
2+{{define "list"}}
3+{{$indent := 0}}
4+{{$mod := 0}}
5+<ul style="list-style-type: {{.ListType}};">
6+ {{range .Items}}
7+ {{if lt $indent .Indent}}
8+ <ul>
9+ {{else if gt $indent .Indent}}
10+
11+ {{$mod = minus $indent .Indent}}
12+ {{range $y := intRange 1 $mod}}
13+ </li></ul>
14+ {{end}}
15+
16+ {{else}}
17+ </li>
18+ {{end}}
19+ {{$indent = .Indent}}
20+
21+ {{if .IsText}}
22+ {{if .Value}}
23+ <li>{{.Value}}
24+ {{end}}
25+ {{end}}
26+
27+ {{if .IsURL}}
28+ <li><a href="{{.URL}}">{{.Value}}</a>
29+ {{end}}
30+
31+ {{if .IsImg}}
32+ <li><img src="{{.URL}}" alt="{{.Value}}" />
33+ {{end}}
34+
35+ {{if .IsBlock}}
36+ <li><blockquote>{{.Value}}</blockquote>
37+ {{end}}
38+
39+ {{if .IsHeaderOne}}
40+ </ul><h2 class="text-xl font-bold">{{.Value}}</h2><ul style="list-style-type: {{$.ListType}};">
41+ {{end}}
42+
43+ {{if .IsHeaderTwo}}
44+ </ul><h3 class="text-lg font-bold">{{.Value}}</h3><ul style="list-style-type: {{$.ListType}};">
45+ {{end}}
46+
47+ {{if .IsPre}}
48+ <li><pre>{{.Value}}</pre>
49+ {{end}}
50+ {{end}}
51+</ul>
52+{{end}}
+5,
-1
1@@ -65,7 +65,11 @@
2 </header>
3 <main>
4 <article class="md">
5- {{.Contents}}
6+ {{if .List}}
7+ {{template "list" .List}}
8+ {{else}}
9+ {{.Contents}}
10+ {{end}}
11
12 <div class="tags">
13 {{range .Tags}}
+5,
-1
1@@ -1 +1,5 @@
2-{{.Contents}}
3+{{if .List}}
4+ {{template "list" .List}}
5+{{else}}
6+ {{.Contents}}
7+{{end}}
+43,
-2
1@@ -2,6 +2,7 @@ package prose
2
3 import (
4 "fmt"
5+ "path/filepath"
6 "strings"
7
8 "slices"
9@@ -62,7 +63,28 @@ func (p *MarkdownHooks) FileValidate(s *pssh.SSHServerConnSession, data *filehan
10 return true, nil
11 }
12
13-func (p *MarkdownHooks) FileMeta(s *pssh.SSHServerConnSession, data *filehandlers.PostMetaData) error {
14+func (p *MarkdownHooks) metaLxt(data *filehandlers.PostMetaData) error {
15+ parsedText := shared.ListParseText(data.Text)
16+
17+ if parsedText.Title == "" {
18+ data.Title = utils.ToUpper(data.Slug)
19+ } else {
20+ data.Title = parsedText.Title
21+ }
22+
23+ data.Aliases = parsedText.Aliases
24+ data.Tags = parsedText.Tags
25+ data.Description = parsedText.Description
26+
27+ if parsedText.PublishAt != nil && !parsedText.PublishAt.IsZero() {
28+ data.PublishAt = parsedText.PublishAt
29+ }
30+ data.Hidden = parsedText.Hidden
31+
32+ return nil
33+}
34+
35+func (p *MarkdownHooks) metaMd(data *filehandlers.PostMetaData) error {
36 parsedText, err := shared.ParseText(data.Text)
37 if err != nil {
38 return fmt.Errorf("%s: %w", data.Filename, err)
39@@ -81,9 +103,28 @@ func (p *MarkdownHooks) FileMeta(s *pssh.SSHServerConnSession, data *filehandler
40 if parsedText.PublishAt != nil && !parsedText.PublishAt.IsZero() {
41 data.PublishAt = parsedText.PublishAt
42 }
43+ data.Hidden = parsedText.Hidden
44+
45+ return nil
46+}
47+
48+func (p *MarkdownHooks) FileMeta(s *pssh.SSHServerConnSession, data *filehandlers.PostMetaData) error {
49+ ext := filepath.Ext(data.Filename)
50+ switch ext {
51+ case ".lxt":
52+ err := p.metaLxt(data)
53+ if err != nil {
54+ return fmt.Errorf("%s: %w", data.Filename, err)
55+ }
56+ case ".md":
57+ err := p.metaMd(data)
58+ if err != nil {
59+ return fmt.Errorf("%s: %w", data.Filename, err)
60+ }
61+ }
62
63 isHiddenFilename := slices.Contains(p.Cfg.HiddenPosts, data.Filename)
64- data.Hidden = parsedText.Hidden || isHiddenFilename
65+ data.Hidden = data.Hidden || isHiddenFilename
66
67 return nil
68 }
+1,
-0
1@@ -55,6 +55,7 @@ func StartSshServer() {
2 ".md": filehandlers.NewScpPostHandler(dbh, cfg, hooks),
3 ".txt": filehandlers.NewScpPostHandler(dbh, cfg, hooks),
4 ".css": filehandlers.NewScpPostHandler(dbh, cfg, hooks),
5+ ".lxt": filehandlers.NewScpPostHandler(dbh, cfg, hooks),
6 "fallback": uploadimgs.NewUploadImgHandler(dbh, cfg, st),
7 }
8 handler := filehandlers.NewFileHandlerRouter(cfg, dbh, fileMap)
1@@ -34,12 +34,19 @@ type ListItem struct {
2 }
3
4 type ListMetaData struct {
5- PublishAt *time.Time
6- Title string
7- Description string
8- Layout string
9- Tags []string
10- ListType string // https://developer.mozilla.org/en-US/docs/Web/CSS/list-style-type
11+ // prose
12+ Aliases []string
13+ Description string
14+ Hidden bool
15+ Image string
16+ ImageCard string
17+ Layout string
18+ ListType string // https://developer.mozilla.org/en-US/docs/Web/CSS/list-style-type
19+ PublishAt *time.Time
20+ Tags []string
21+ Title string
22+
23+ // feeds
24 DigestInterval string
25 Cron string
26 Email string
27@@ -101,14 +108,30 @@ func TokenToMetaField(meta *ListMetaData, token *SplitToken) error {
28 meta.Title = token.Value
29 case "description":
30 meta.Description = token.Value
31+ case "image":
32+ meta.Image = token.Value
33+ case "image_card":
34+ meta.ImageCard = token.Value
35 case "list_type":
36 meta.ListType = token.Value
37+ case "hidden":
38+ if token.Value == "true" {
39+ meta.Hidden = true
40+ } else {
41+ meta.Hidden = false
42+ }
43 case "tags":
44 tags := strings.Split(token.Value, ",")
45 meta.Tags = make([]string, 0)
46 for _, tag := range tags {
47 meta.Tags = append(meta.Tags, strings.TrimSpace(tag))
48 }
49+ case "aliases":
50+ aliases := strings.Split(token.Value, ",")
51+ meta.Aliases = make([]string, 0)
52+ for _, alias := range aliases {
53+ meta.Aliases = append(meta.Aliases, strings.TrimSpace(alias))
54+ }
55 case "layout":
56 meta.Layout = token.Value
57 case "digest_interval":
58@@ -206,10 +229,12 @@ func ListParseText(text string) *ListParsedText {
59 textItems := SplitByNewline(text)
60 items := []*ListItem{}
61 meta := ListMetaData{
62+ Aliases: []string{},
63+ InlineContent: true,
64+ Layout: "default",
65 ListType: "disc",
66+ PublishAt: &time.Time{},
67 Tags: []string{},
68- Layout: "default",
69- InlineContent: true,
70 }
71 pre := false
72 skip := false