Logs
erockAqGV
created pr with ps-26
on erockAqGV
added ps-27
on erockAqGV
changed status
on {"status":"open"}
erockAqGV
changed status
on {"status":"closed"}
Patchsets
Diff ↕
feat(prose): live journal
Eric Bower <me@erock.io>
The goal is to be able to use posts as live journals where reader can receive updates to docs via rss.
db/db.go | 1 + go.mod | 1 + go.sum | 8 ++++++++ prose/api.go | 48 +++++++++++++++++++++++----------------------- prose/scp_hooks.go | 5 +++++ 5 files changed, 39 insertions(+), 24 deletions(-)
1From 866976a24d3a7963012ffb9cc28bd99364505f71 Mon Sep 17 00:00:00 2001
2From: Eric Bower <me@erock.io>
3Date: Thu, 1 Aug 2024 10:24:47 -0400
4Subject: [PATCH] feat(prose): live journal
5
6The goal is to be able to use posts as live journals where reader can
7receive updates to docs via rss.
8---
9 db/db.go | 1 +
10 go.mod | 1 +
11 go.sum | 8 ++++++++
12 prose/api.go | 48 +++++++++++++++++++++++-----------------------
13 prose/scp_hooks.go | 5 +++++
14 5 files changed, 39 insertions(+), 24 deletions(-)
15
16diff --git a/db/db.go b/db/db.go
17index 8cee29c..2c82dc4 100644
18--- a/db/db.go
19+++ b/db/db.go
20@@ -32,6 +32,7 @@ type User struct {
21 type PostData struct {
22 ImgPath string `json:"img_path"`
23 LastDigest *time.Time `json:"last_digest"`
24+ Diff string `json:"diff"`
25 }
26
27 // Make the Attrs struct implement the driver.Valuer interface. This method
28diff --git a/go.mod b/go.mod
29index b7084ba..549e3ad 100644
30--- a/go.mod
31+++ b/go.mod
32@@ -37,6 +37,7 @@ require (
33 github.com/picosh/tunkit v0.0.0-20240709033345-8315d4f3cd0e
34 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
35 github.com/sendgrid/sendgrid-go v3.14.0+incompatible
36+ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
37 github.com/simplesurance/go-ip-anonymizer v0.0.0-20200429124537-35a880f8e87d
38 github.com/x-way/crawlerdetect v0.2.21
39 github.com/yuin/goldmark v1.7.1
40diff --git a/go.sum b/go.sum
41index 1980154..2577975 100644
42--- a/go.sum
43+++ b/go.sum
44@@ -158,8 +158,11 @@ github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuV
45 github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
46 github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
47 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
48+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
49 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
50 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
51+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
52+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
53 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
54 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
55 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
56@@ -265,6 +268,8 @@ github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekuei
57 github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
58 github.com/sendgrid/sendgrid-go v3.14.0+incompatible h1:KDSasSTktAqMJCYClHVE94Fcif2i7P7wzISv1sU6DUA=
59 github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
60+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
61+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
62 github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
63 github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
64 github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
65@@ -276,6 +281,7 @@ github.com/simplesurance/go-ip-anonymizer v0.0.0-20200429124537-35a880f8e87d/go.
66 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
67 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
68 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
69+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
70 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
71 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
72 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
73@@ -393,10 +399,12 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
74 google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
75 google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
76 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
77+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
78 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
79 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
80 gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
81 gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
82+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
83 gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
84 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
85 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
86diff --git a/prose/api.go b/prose/api.go
87index 1d703d8..7643e0e 100644
88--- a/prose/api.go
89+++ b/prose/api.go
90@@ -160,7 +160,7 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
91 }
92
93 tag := r.URL.Query().Get("tag")
94- pager := &db.Pager{Num: 1000, Page: 0}
95+ pager := &db.Pager{Num: 250, Page: 0}
96 var posts []*db.Post
97 var p *db.Paginate[*db.Post]
98 if tag == "" {
99@@ -170,6 +170,13 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
100 }
101 posts = p.Data
102
103+ byUpdated := strings.Contains(r.URL.Path, "live")
104+ if byUpdated {
105+ slices.SortFunc(posts, func(a *db.Post, b *db.Post) int {
106+ return b.UpdatedAt.Compare(*a.UpdatedAt)
107+ })
108+ }
109+
110 if err != nil {
111 logger.Error(err.Error())
112 http.Error(w, "could not fetch posts for blog", http.StatusInternalServerError)
113@@ -653,6 +660,13 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
114 curl := shared.CreateURLFromRequest(cfg, r)
115 blogUrl := cfg.FullBlogURL(curl, username)
116
117+ byUpdated := strings.Contains(r.URL.Path, "live")
118+ if byUpdated {
119+ slices.SortFunc(posts, func(a *db.Post, b *db.Post) int {
120+ return b.UpdatedAt.Compare(*a.UpdatedAt)
121+ })
122+ }
123+
124 feed := &feeds.Feed{
125 Id: blogUrl,
126 Title: headerTxt.Title,
127@@ -691,12 +705,18 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
128 }
129
130 realUrl := cfg.FullPostURL(curl, post.Username, post.Slug)
131+ feedId := realUrl
132+
133+ if byUpdated {
134+ feedId = fmt.Sprintf("%s:%s", realUrl, post.UpdatedAt.Format(time.RFC3339))
135+ }
136
137 item := &feeds.Item{
138- Id: realUrl,
139+ Id: feedId,
140 Title: shared.FilenameToTitle(post.Filename, post.Title),
141 Link: &feeds.Link{Href: realUrl},
142 Content: tpl.String(),
143+ Updated: *post.UpdatedAt,
144 Created: *post.CreatedAt,
145 Description: post.Description,
146 }
147@@ -849,36 +869,16 @@ func createMainRoutes(staticRoutes []shared.Route) []shared.Route {
148 staticRoutes...,
149 )
150
151- routes = append(
152- routes,
153- shared.NewRoute("GET", "/rss", rssHandler),
154- shared.NewRoute("GET", "/rss.xml", rssHandler),
155- shared.NewRoute("GET", "/atom.xml", rssHandler),
156- shared.NewRoute("GET", "/feed.xml", rssHandler),
157-
158- shared.NewRoute("GET", "/([^/]+)", blogHandler),
159- shared.NewRoute("GET", "/([^/]+)/rss", rssBlogHandler),
160- shared.NewRoute("GET", "/([^/]+)/rss.xml", rssBlogHandler),
161- shared.NewRoute("GET", "/([^/]+)/atom.xml", rssBlogHandler),
162- shared.NewRoute("GET", "/([^/]+)/atom", rssBlogHandler),
163- shared.NewRoute("GET", "/([^/]+)/blog/index.xml", rssBlogHandler),
164- shared.NewRoute("GET", "/([^/]+)/feed.xml", rssBlogHandler),
165- shared.NewRoute("GET", "/([^/]+)/_styles.css", blogStyleHandler),
166- shared.NewRoute("GET", "/raw/([^/]+)/(.+)", postRawHandler),
167- shared.NewRoute("GET", "/([^/]+)/(.+)/(.+)", imgs.ImgRequest),
168- shared.NewRoute("GET", "/([^/]+)/(.+.(?:jpg|jpeg|png|gif|webp|svg))$", imgs.ImgRequest),
169- shared.NewRoute("GET", "/([^/]+)/i", imgs.ImgsListHandler),
170- shared.NewRoute("GET", "/([^/]+)/(.+)", postHandler),
171- )
172-
173 return routes
174 }
175
176 func createSubdomainRoutes(staticRoutes []shared.Route) []shared.Route {
177 routes := []shared.Route{
178 shared.NewRoute("GET", "/", blogHandler),
179+ shared.NewRoute("GET", "/live", blogHandler),
180 shared.NewRoute("GET", "/_styles.css", blogStyleHandler),
181 shared.NewRoute("GET", "/rss", rssBlogHandler),
182+ shared.NewRoute("GET", "/live/rss", rssBlogHandler),
183 shared.NewRoute("GET", "/rss.xml", rssBlogHandler),
184 shared.NewRoute("GET", "/atom.xml", rssBlogHandler),
185 shared.NewRoute("GET", "/feed.xml", rssBlogHandler),
186diff --git a/prose/scp_hooks.go b/prose/scp_hooks.go
187index 577790c..3cb64e6 100644
188--- a/prose/scp_hooks.go
189+++ b/prose/scp_hooks.go
190@@ -10,6 +10,7 @@ import (
191 "github.com/picosh/pico/db"
192 "github.com/picosh/pico/filehandlers"
193 "github.com/picosh/pico/shared"
194+ "github.com/sergi/go-diff/diffmatchpatch"
195 )
196
197 type MarkdownHooks struct {
198@@ -62,6 +63,10 @@ func (p *MarkdownHooks) FileMeta(s ssh.Session, data *filehandlers.PostMetaData)
199 data.Tags = parsedText.Tags
200 data.Description = parsedText.Description
201
202+ dmp := diffmatchpatch.New()
203+ diffs := dmp.DiffMain(data.Cur.Text, data.Text, false)
204+ data.Data.Diff = dmp.DiffPrettyText(diffs)
205+
206 if parsedText.PublishAt != nil && !parsedText.PublishAt.IsZero() {
207 data.PublishAt = parsedText.MetaData.PublishAt
208 }
209
210base-commit: 0bd744d728324024fe7735c06e37a3e228403bc6
211--
2122.45.2
213
ps-26
by
erockAqGV
on Diff ↕
chore: diff in post
Eric Bower <me@erock.io>
filehandlers/post_handler.go | 1 + prose/api.go | 11 +++++++++++ prose/html/post.page.tmpl | 9 ++++++++- prose/scp_hooks.go | 31 ++++++++++++++++++++++++++++--- shared/mdparser.go | 7 +++++++ 5 files changed, 55 insertions(+), 4 deletions(-)
1From b82b2f7db8755e20ddfbc119f1f78e5b6f984518 Mon Sep 17 00:00:00 2001
2From: Eric Bower <me@erock.io>
3Date: Thu, 1 Aug 2024 11:29:31 -0400
4Subject: [PATCH 2/2] chore: diff in post
5
6---
7 filehandlers/post_handler.go | 1 +
8 prose/api.go | 11 +++++++++++
9 prose/html/post.page.tmpl | 9 ++++++++-
10 prose/scp_hooks.go | 31 ++++++++++++++++++++++++++++---
11 shared/mdparser.go | 7 +++++++
12 5 files changed, 55 insertions(+), 4 deletions(-)
13
14diff --git a/filehandlers/post_handler.go b/filehandlers/post_handler.go
15index 4e10b66..30e00cc 100644
16--- a/filehandlers/post_handler.go
17+++ b/filehandlers/post_handler.go
18@@ -139,6 +139,7 @@ func (h *ScpUploadHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
19
20 if post != nil {
21 metadata.Cur = post
22+ metadata.Data = post.Data
23 metadata.Post.PublishAt = post.PublishAt
24 }
25
26diff --git a/prose/api.go b/prose/api.go
27index 7643e0e..60aa52a 100644
28--- a/prose/api.go
29+++ b/prose/api.go
30@@ -83,6 +83,7 @@ type PostPageData struct {
31 Footer template.HTML
32 Favicon template.URL
33 Unlisted bool
34+ Diff template.HTML
35 }
36
37 type TransparencyPageData struct {
38@@ -399,6 +400,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
39 favicon = readmeParsed.Favicon
40 }
41
42+ diff := ""
43 post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
44 if err == nil {
45 parsedText, err := shared.ParseText(post.Text)
46@@ -414,6 +416,14 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
47 ogImageCard = parsedText.ImageCard
48 }
49
50+ // if parsedText.Live {
51+ if post.Data.Diff != "" {
52+ str := "```diff\n" + post.Data.Diff + "\n```"
53+ diffParsed, _ := shared.ParseText(str)
54+ diff = diffParsed.Html
55+ }
56+ // }
57+
58 // track visit
59 view, err := shared.AnalyticsVisitFromRequest(r, user.ID, cfg.Secret)
60 if err == nil {
61@@ -451,6 +461,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
62 Favicon: template.URL(favicon),
63 Footer: footerHTML,
64 Unlisted: unlisted,
65+ Diff: template.HTML(diff),
66 }
67 } else {
68 // TODO: HACK to support imgs slugs inside prose
69diff --git a/prose/html/post.page.tmpl b/prose/html/post.page.tmpl
70index 3811a4c..e667978 100644
71--- a/prose/html/post.page.tmpl
72+++ b/prose/html/post.page.tmpl
73@@ -62,9 +62,16 @@
74 </div>
75 </header>
76 <main>
77+ {{if .Diff}}
78+ <details>
79+ <summary>diff</summary>
80+ <div class="md">{{.Diff}}</div>
81+ </details>
82+ {{end}}
83+
84 <article class="md">
85 {{.Contents}}
86- <div id="post-footer">{{.Footer}}</div>
87+ <div id="post-footer" class="box">{{.Footer}}</div>
88 </article>
89 </main>
90 {{template "footer" .}}
91diff --git a/prose/scp_hooks.go b/prose/scp_hooks.go
92index 3cb64e6..707bb61 100644
93--- a/prose/scp_hooks.go
94+++ b/prose/scp_hooks.go
95@@ -1,6 +1,7 @@
96 package prose
97
98 import (
99+ "bytes"
100 "fmt"
101 "strings"
102
103@@ -47,6 +48,26 @@ func (p *MarkdownHooks) FileValidate(s ssh.Session, data *filehandlers.PostMetaD
104 return true, nil
105 }
106
107+func diffText(diffs []diffmatchpatch.Diff) string {
108+ var buff bytes.Buffer
109+ for _, diff := range diffs {
110+ text := diff.Text
111+
112+ switch diff.Type {
113+ case diffmatchpatch.DiffInsert:
114+ _, _ = buff.WriteString("+")
115+ _, _ = buff.WriteString(text)
116+ case diffmatchpatch.DiffDelete:
117+ _, _ = buff.WriteString("-")
118+ _, _ = buff.WriteString(text)
119+ case diffmatchpatch.DiffEqual:
120+ _, _ = buff.WriteString(text)
121+ }
122+ }
123+
124+ return buff.String()
125+}
126+
127 func (p *MarkdownHooks) FileMeta(s ssh.Session, data *filehandlers.PostMetaData) error {
128 parsedText, err := shared.ParseText(data.Text)
129 if err != nil {
130@@ -63,9 +84,13 @@ func (p *MarkdownHooks) FileMeta(s ssh.Session, data *filehandlers.PostMetaData)
131 data.Tags = parsedText.Tags
132 data.Description = parsedText.Description
133
134- dmp := diffmatchpatch.New()
135- diffs := dmp.DiffMain(data.Cur.Text, data.Text, false)
136- data.Data.Diff = dmp.DiffPrettyText(diffs)
137+ if data.Cur.Text == data.Text {
138+ } else {
139+ dmp := diffmatchpatch.New()
140+ diffs := dmp.DiffMain(data.Cur.Text, data.Text, false)
141+ fmt.Println(diffs)
142+ data.Data.Diff = diffText(diffs)
143+ }
144
145 if parsedText.PublishAt != nil && !parsedText.PublishAt.IsZero() {
146 data.PublishAt = parsedText.MetaData.PublishAt
147diff --git a/shared/mdparser.go b/shared/mdparser.go
148index 54f234b..1ddf0b1 100644
149--- a/shared/mdparser.go
150+++ b/shared/mdparser.go
151@@ -40,6 +40,7 @@ type MetaData struct {
152 ImageCard string
153 Favicon string
154 Hidden bool
155+ Live bool
156 }
157
158 type ParsedText struct {
159@@ -303,6 +304,12 @@ func ParseText(text string) (*ParsedText, error) {
160 }
161 parsed.MetaData.Favicon = favicon
162
163+ live, err := toBool(metaData["live"])
164+ if err != nil {
165+ return &parsed, fmt.Errorf("front-matter field (%s): %w", "live", err)
166+ }
167+ parsed.MetaData.Live = live
168+
169 var publishAt *time.Time = nil
170 date, err := toString(metaData["date"])
171 if err != nil {
172--
1732.45.2
174
ps-27
by
erockAqGV
on feat(prose): live journal
Eric Bower <me@erock.io>
The goal is to be able to use posts as live journals where reader can receive updates to docs via rss.
db/db.go | 1 + go.mod | 1 + go.sum | 8 ++++++++ prose/api.go | 48 +++++++++++++++++++++++----------------------- prose/scp_hooks.go | 5 +++++ 5 files changed, 39 insertions(+), 24 deletions(-)
1From 866976a24d3a7963012ffb9cc28bd99364505f71 Mon Sep 17 00:00:00 2001
2From: Eric Bower <me@erock.io>
3Date: Thu, 1 Aug 2024 10:24:47 -0400
4Subject: [PATCH 1/2] feat(prose): live journal
5
6The goal is to be able to use posts as live journals where reader can
7receive updates to docs via rss.
8---
9 db/db.go | 1 +
10 go.mod | 1 +
11 go.sum | 8 ++++++++
12 prose/api.go | 48 +++++++++++++++++++++++-----------------------
13 prose/scp_hooks.go | 5 +++++
14 5 files changed, 39 insertions(+), 24 deletions(-)
15
16diff --git a/db/db.go b/db/db.go
17index 8cee29c..2c82dc4 100644
18--- a/db/db.go
19+++ b/db/db.go
20@@ -32,6 +32,7 @@ type User struct {
21 type PostData struct {
22 ImgPath string `json:"img_path"`
23 LastDigest *time.Time `json:"last_digest"`
24+ Diff string `json:"diff"`
25 }
26
27 // Make the Attrs struct implement the driver.Valuer interface. This method
28diff --git a/go.mod b/go.mod
29index b7084ba..549e3ad 100644
30--- a/go.mod
31+++ b/go.mod
32@@ -37,6 +37,7 @@ require (
33 github.com/picosh/tunkit v0.0.0-20240709033345-8315d4f3cd0e
34 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
35 github.com/sendgrid/sendgrid-go v3.14.0+incompatible
36+ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3
37 github.com/simplesurance/go-ip-anonymizer v0.0.0-20200429124537-35a880f8e87d
38 github.com/x-way/crawlerdetect v0.2.21
39 github.com/yuin/goldmark v1.7.1
40diff --git a/go.sum b/go.sum
41index 1980154..2577975 100644
42--- a/go.sum
43+++ b/go.sum
44@@ -158,8 +158,11 @@ github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuV
45 github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
46 github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
47 github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
48+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
49 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
50 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
51+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
52+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
53 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
54 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
55 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
56@@ -265,6 +268,8 @@ github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekuei
57 github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
58 github.com/sendgrid/sendgrid-go v3.14.0+incompatible h1:KDSasSTktAqMJCYClHVE94Fcif2i7P7wzISv1sU6DUA=
59 github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
60+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
61+github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
62 github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
63 github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
64 github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
65@@ -276,6 +281,7 @@ github.com/simplesurance/go-ip-anonymizer v0.0.0-20200429124537-35a880f8e87d/go.
66 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
67 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
68 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
69+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
70 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
71 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
72 github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
73@@ -393,10 +399,12 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
74 google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
75 google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
76 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
77+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
78 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
79 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
80 gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
81 gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
82+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
83 gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
84 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
85 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
86diff --git a/prose/api.go b/prose/api.go
87index 1d703d8..7643e0e 100644
88--- a/prose/api.go
89+++ b/prose/api.go
90@@ -160,7 +160,7 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
91 }
92
93 tag := r.URL.Query().Get("tag")
94- pager := &db.Pager{Num: 1000, Page: 0}
95+ pager := &db.Pager{Num: 250, Page: 0}
96 var posts []*db.Post
97 var p *db.Paginate[*db.Post]
98 if tag == "" {
99@@ -170,6 +170,13 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
100 }
101 posts = p.Data
102
103+ byUpdated := strings.Contains(r.URL.Path, "live")
104+ if byUpdated {
105+ slices.SortFunc(posts, func(a *db.Post, b *db.Post) int {
106+ return b.UpdatedAt.Compare(*a.UpdatedAt)
107+ })
108+ }
109+
110 if err != nil {
111 logger.Error(err.Error())
112 http.Error(w, "could not fetch posts for blog", http.StatusInternalServerError)
113@@ -653,6 +660,13 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
114 curl := shared.CreateURLFromRequest(cfg, r)
115 blogUrl := cfg.FullBlogURL(curl, username)
116
117+ byUpdated := strings.Contains(r.URL.Path, "live")
118+ if byUpdated {
119+ slices.SortFunc(posts, func(a *db.Post, b *db.Post) int {
120+ return b.UpdatedAt.Compare(*a.UpdatedAt)
121+ })
122+ }
123+
124 feed := &feeds.Feed{
125 Id: blogUrl,
126 Title: headerTxt.Title,
127@@ -691,12 +705,18 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
128 }
129
130 realUrl := cfg.FullPostURL(curl, post.Username, post.Slug)
131+ feedId := realUrl
132+
133+ if byUpdated {
134+ feedId = fmt.Sprintf("%s:%s", realUrl, post.UpdatedAt.Format(time.RFC3339))
135+ }
136
137 item := &feeds.Item{
138- Id: realUrl,
139+ Id: feedId,
140 Title: shared.FilenameToTitle(post.Filename, post.Title),
141 Link: &feeds.Link{Href: realUrl},
142 Content: tpl.String(),
143+ Updated: *post.UpdatedAt,
144 Created: *post.CreatedAt,
145 Description: post.Description,
146 }
147@@ -849,36 +869,16 @@ func createMainRoutes(staticRoutes []shared.Route) []shared.Route {
148 staticRoutes...,
149 )
150
151- routes = append(
152- routes,
153- shared.NewRoute("GET", "/rss", rssHandler),
154- shared.NewRoute("GET", "/rss.xml", rssHandler),
155- shared.NewRoute("GET", "/atom.xml", rssHandler),
156- shared.NewRoute("GET", "/feed.xml", rssHandler),
157-
158- shared.NewRoute("GET", "/([^/]+)", blogHandler),
159- shared.NewRoute("GET", "/([^/]+)/rss", rssBlogHandler),
160- shared.NewRoute("GET", "/([^/]+)/rss.xml", rssBlogHandler),
161- shared.NewRoute("GET", "/([^/]+)/atom.xml", rssBlogHandler),
162- shared.NewRoute("GET", "/([^/]+)/atom", rssBlogHandler),
163- shared.NewRoute("GET", "/([^/]+)/blog/index.xml", rssBlogHandler),
164- shared.NewRoute("GET", "/([^/]+)/feed.xml", rssBlogHandler),
165- shared.NewRoute("GET", "/([^/]+)/_styles.css", blogStyleHandler),
166- shared.NewRoute("GET", "/raw/([^/]+)/(.+)", postRawHandler),
167- shared.NewRoute("GET", "/([^/]+)/(.+)/(.+)", imgs.ImgRequest),
168- shared.NewRoute("GET", "/([^/]+)/(.+.(?:jpg|jpeg|png|gif|webp|svg))$", imgs.ImgRequest),
169- shared.NewRoute("GET", "/([^/]+)/i", imgs.ImgsListHandler),
170- shared.NewRoute("GET", "/([^/]+)/(.+)", postHandler),
171- )
172-
173 return routes
174 }
175
176 func createSubdomainRoutes(staticRoutes []shared.Route) []shared.Route {
177 routes := []shared.Route{
178 shared.NewRoute("GET", "/", blogHandler),
179+ shared.NewRoute("GET", "/live", blogHandler),
180 shared.NewRoute("GET", "/_styles.css", blogStyleHandler),
181 shared.NewRoute("GET", "/rss", rssBlogHandler),
182+ shared.NewRoute("GET", "/live/rss", rssBlogHandler),
183 shared.NewRoute("GET", "/rss.xml", rssBlogHandler),
184 shared.NewRoute("GET", "/atom.xml", rssBlogHandler),
185 shared.NewRoute("GET", "/feed.xml", rssBlogHandler),
186diff --git a/prose/scp_hooks.go b/prose/scp_hooks.go
187index 577790c..3cb64e6 100644
188--- a/prose/scp_hooks.go
189+++ b/prose/scp_hooks.go
190@@ -10,6 +10,7 @@ import (
191 "github.com/picosh/pico/db"
192 "github.com/picosh/pico/filehandlers"
193 "github.com/picosh/pico/shared"
194+ "github.com/sergi/go-diff/diffmatchpatch"
195 )
196
197 type MarkdownHooks struct {
198@@ -62,6 +63,10 @@ func (p *MarkdownHooks) FileMeta(s ssh.Session, data *filehandlers.PostMetaData)
199 data.Tags = parsedText.Tags
200 data.Description = parsedText.Description
201
202+ dmp := diffmatchpatch.New()
203+ diffs := dmp.DiffMain(data.Cur.Text, data.Text, false)
204+ data.Data.Diff = dmp.DiffPrettyText(diffs)
205+
206 if parsedText.PublishAt != nil && !parsedText.PublishAt.IsZero() {
207 data.PublishAt = parsedText.MetaData.PublishAt
208 }
209
210base-commit: 0bd744d728324024fe7735c06e37a3e228403bc6
211--
2122.45.2
213
chore: diff in post
Eric Bower <me@erock.io>
filehandlers/post_handler.go | 1 + prose/api.go | 11 +++++++++++ prose/html/post.page.tmpl | 9 ++++++++- prose/scp_hooks.go | 31 ++++++++++++++++++++++++++++--- shared/mdparser.go | 7 +++++++ 5 files changed, 55 insertions(+), 4 deletions(-)
1From b82b2f7db8755e20ddfbc119f1f78e5b6f984518 Mon Sep 17 00:00:00 2001
2From: Eric Bower <me@erock.io>
3Date: Thu, 1 Aug 2024 11:29:31 -0400
4Subject: [PATCH 2/2] chore: diff in post
5
6---
7 filehandlers/post_handler.go | 1 +
8 prose/api.go | 11 +++++++++++
9 prose/html/post.page.tmpl | 9 ++++++++-
10 prose/scp_hooks.go | 31 ++++++++++++++++++++++++++++---
11 shared/mdparser.go | 7 +++++++
12 5 files changed, 55 insertions(+), 4 deletions(-)
13
14diff --git a/filehandlers/post_handler.go b/filehandlers/post_handler.go
15index 4e10b66..30e00cc 100644
16--- a/filehandlers/post_handler.go
17+++ b/filehandlers/post_handler.go
18@@ -139,6 +139,7 @@ func (h *ScpUploadHandler) Write(s ssh.Session, entry *utils.FileEntry) (string,
19
20 if post != nil {
21 metadata.Cur = post
22+ metadata.Data = post.Data
23 metadata.Post.PublishAt = post.PublishAt
24 }
25
26diff --git a/prose/api.go b/prose/api.go
27index 7643e0e..60aa52a 100644
28--- a/prose/api.go
29+++ b/prose/api.go
30@@ -83,6 +83,7 @@ type PostPageData struct {
31 Footer template.HTML
32 Favicon template.URL
33 Unlisted bool
34+ Diff template.HTML
35 }
36
37 type TransparencyPageData struct {
38@@ -399,6 +400,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
39 favicon = readmeParsed.Favicon
40 }
41
42+ diff := ""
43 post, err := dbpool.FindPostWithSlug(slug, user.ID, cfg.Space)
44 if err == nil {
45 parsedText, err := shared.ParseText(post.Text)
46@@ -414,6 +416,14 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
47 ogImageCard = parsedText.ImageCard
48 }
49
50+ // if parsedText.Live {
51+ if post.Data.Diff != "" {
52+ str := "```diff\n" + post.Data.Diff + "\n```"
53+ diffParsed, _ := shared.ParseText(str)
54+ diff = diffParsed.Html
55+ }
56+ // }
57+
58 // track visit
59 view, err := shared.AnalyticsVisitFromRequest(r, user.ID, cfg.Secret)
60 if err == nil {
61@@ -451,6 +461,7 @@ func postHandler(w http.ResponseWriter, r *http.Request) {
62 Favicon: template.URL(favicon),
63 Footer: footerHTML,
64 Unlisted: unlisted,
65+ Diff: template.HTML(diff),
66 }
67 } else {
68 // TODO: HACK to support imgs slugs inside prose
69diff --git a/prose/html/post.page.tmpl b/prose/html/post.page.tmpl
70index 3811a4c..e667978 100644
71--- a/prose/html/post.page.tmpl
72+++ b/prose/html/post.page.tmpl
73@@ -62,9 +62,16 @@
74 </div>
75 </header>
76 <main>
77+ {{if .Diff}}
78+ <details>
79+ <summary>diff</summary>
80+ <div class="md">{{.Diff}}</div>
81+ </details>
82+ {{end}}
83+
84 <article class="md">
85 {{.Contents}}
86- <div id="post-footer">{{.Footer}}</div>
87+ <div id="post-footer" class="box">{{.Footer}}</div>
88 </article>
89 </main>
90 {{template "footer" .}}
91diff --git a/prose/scp_hooks.go b/prose/scp_hooks.go
92index 3cb64e6..707bb61 100644
93--- a/prose/scp_hooks.go
94+++ b/prose/scp_hooks.go
95@@ -1,6 +1,7 @@
96 package prose
97
98 import (
99+ "bytes"
100 "fmt"
101 "strings"
102
103@@ -47,6 +48,26 @@ func (p *MarkdownHooks) FileValidate(s ssh.Session, data *filehandlers.PostMetaD
104 return true, nil
105 }
106
107+func diffText(diffs []diffmatchpatch.Diff) string {
108+ var buff bytes.Buffer
109+ for _, diff := range diffs {
110+ text := diff.Text
111+
112+ switch diff.Type {
113+ case diffmatchpatch.DiffInsert:
114+ _, _ = buff.WriteString("+")
115+ _, _ = buff.WriteString(text)
116+ case diffmatchpatch.DiffDelete:
117+ _, _ = buff.WriteString("-")
118+ _, _ = buff.WriteString(text)
119+ case diffmatchpatch.DiffEqual:
120+ _, _ = buff.WriteString(text)
121+ }
122+ }
123+
124+ return buff.String()
125+}
126+
127 func (p *MarkdownHooks) FileMeta(s ssh.Session, data *filehandlers.PostMetaData) error {
128 parsedText, err := shared.ParseText(data.Text)
129 if err != nil {
130@@ -63,9 +84,13 @@ func (p *MarkdownHooks) FileMeta(s ssh.Session, data *filehandlers.PostMetaData)
131 data.Tags = parsedText.Tags
132 data.Description = parsedText.Description
133
134- dmp := diffmatchpatch.New()
135- diffs := dmp.DiffMain(data.Cur.Text, data.Text, false)
136- data.Data.Diff = dmp.DiffPrettyText(diffs)
137+ if data.Cur.Text == data.Text {
138+ } else {
139+ dmp := diffmatchpatch.New()
140+ diffs := dmp.DiffMain(data.Cur.Text, data.Text, false)
141+ fmt.Println(diffs)
142+ data.Data.Diff = diffText(diffs)
143+ }
144
145 if parsedText.PublishAt != nil && !parsedText.PublishAt.IsZero() {
146 data.PublishAt = parsedText.MetaData.PublishAt
147diff --git a/shared/mdparser.go b/shared/mdparser.go
148index 54f234b..1ddf0b1 100644
149--- a/shared/mdparser.go
150+++ b/shared/mdparser.go
151@@ -40,6 +40,7 @@ type MetaData struct {
152 ImageCard string
153 Favicon string
154 Hidden bool
155+ Live bool
156 }
157
158 type ParsedText struct {
159@@ -303,6 +304,12 @@ func ParseText(text string) (*ParsedText, error) {
160 }
161 parsed.MetaData.Favicon = favicon
162
163+ live, err := toBool(metaData["live"])
164+ if err != nil {
165+ return &parsed, fmt.Errorf("front-matter field (%s): %w", "live", err)
166+ }
167+ parsed.MetaData.Live = live
168+
169 var publishAt *time.Time = nil
170 date, err := toString(metaData["date"])
171 if err != nil {
172--
1732.45.2
174