Skip to content

Commit 868e755

Browse files
committed
Add dedicated HTML previews for posts
1 parent 70cb859 commit 868e755

17 files changed

Lines changed: 149 additions & 39 deletions

File tree

src/admintools/adminproject.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func addCreateProjectCommand(projectCommand *cobra.Command) {
3434
personal, _ := cmd.Flags().GetBool("personal")
3535
userIDs, _ := cmd.Flags().GetIntSlice("userids")
3636

37-
descParsed := parsing.ParseMarkdown(description, parsing.ForumRealMarkdown)
37+
descParsed := parsing.ParseMarkdown(description, parsing.PostMarkdown)
3838

3939
ctx := context.Background()
4040
conn := db.NewConn()

src/hmndata/threads_and_posts_helper.go

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -829,16 +829,24 @@ func CreatePostVersion(ctx context.Context, tx pgx.Tx, postId int, unparsedConte
829829
unparsedContent = unparsedContent[:maxPostContentLength-1]
830830
}
831831

832-
parsed := parsing.ParseMarkdown(unparsedContent, parsing.ForumRealMarkdown)
833-
ip := net.ParseIP(ipString)
834-
835-
const previewMaxLength = 100
836-
parsedPlaintext := parsing.ParseMarkdown(unparsedContent, parsing.PlaintextMarkdown)
837-
preview := parsedPlaintext
838-
if len(preview) > previewMaxLength-1 {
839-
preview = preview[:previewMaxLength-1] + "…"
832+
htmlContent := parsing.ParseMarkdown(unparsedContent, parsing.PostMarkdown)
833+
834+
const plaintextPreviewMaxLength = 100
835+
const htmlPreviewMaxLength = 600
836+
var plaintextPreview, htmlPreview string
837+
{
838+
plaintextPreview = parsing.ParseMarkdown(unparsedContent, parsing.PlaintextMarkdown)
839+
if len(plaintextPreview) > plaintextPreviewMaxLength-1 {
840+
plaintextPreview = plaintextPreview[:plaintextPreviewMaxLength-1] + "…"
841+
}
842+
}
843+
{
844+
previewMD := unparsedContent[:min(htmlPreviewMaxLength, len(unparsedContent))]
845+
htmlPreview = parsing.ParseMarkdown(previewMD, parsing.PostPreviewMarkdown)
840846
}
841847

848+
ip := net.ParseIP(ipString)
849+
842850
// Create post version
843851
err := tx.QueryRow(ctx,
844852
`
@@ -849,7 +857,7 @@ func CreatePostVersion(ctx context.Context, tx pgx.Tx, postId int, unparsedConte
849857
`,
850858
postId,
851859
unparsedContent,
852-
parsed,
860+
htmlContent,
853861
ip,
854862
time.Now(),
855863
editReason,
@@ -864,11 +872,12 @@ func CreatePostVersion(ctx context.Context, tx pgx.Tx, postId int, unparsedConte
864872
`
865873
---- Update post to new version
866874
UPDATE post
867-
SET current_id = $1, preview = $2
868-
WHERE id = $3
875+
SET current_id = $1, preview = $2, preview_html = $3
876+
WHERE id = $4
869877
`,
870878
versionId,
871-
preview,
879+
plaintextPreview,
880+
htmlPreview,
872881
postId,
873882
)
874883
if err != nil {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package migrations
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
"git.handmade.network/hmn/hmn/src/migration/types"
8+
"github.com/jackc/pgx/v5"
9+
)
10+
11+
func init() {
12+
registerMigration(AddHTMLPreview{})
13+
}
14+
15+
type AddHTMLPreview struct{}
16+
17+
func (m AddHTMLPreview) Version() types.MigrationVersion {
18+
return types.MigrationVersion(time.Date(2026, 3, 6, 17, 9, 58, 0, time.UTC))
19+
}
20+
21+
func (m AddHTMLPreview) Name() string {
22+
return "AddHTMLPreview"
23+
}
24+
25+
func (m AddHTMLPreview) Description() string {
26+
return "Adds an explicit HTML preview to post versions"
27+
}
28+
29+
func (m AddHTMLPreview) Up(ctx context.Context, tx pgx.Tx) error {
30+
_, err := tx.Exec(ctx,
31+
`
32+
ALTER TABLE post
33+
ADD COLUMN preview_html TEXT NOT NULL DEFAULT '';
34+
`,
35+
)
36+
return err
37+
}
38+
39+
func (m AddHTMLPreview) Down(ctx context.Context, tx pgx.Tx) error {
40+
_, err := tx.Exec(ctx,
41+
`
42+
ALTER TABLE post
43+
DROP COLUMN preview_html;
44+
`,
45+
)
46+
return err
47+
}

src/migration/seed.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ func seedProject(ctx context.Context, tx pgx.Tx, input models.Project, owners []
281281
`,
282282
utils.OrDefault(input.ID, latestProjectId+1),
283283
input.Slug, input.Name, input.Blurb,
284-
input.Description, parsing.ParseMarkdown(input.Description, parsing.ForumRealMarkdown),
284+
input.Description, parsing.ParseMarkdown(input.Description, parsing.PostMarkdown),
285285
input.Color1, input.Color2,
286286
input.Featured, input.Personal, utils.OrDefault(input.Lifecycle, models.ProjectLifecycleActive), input.Hidden,
287287
input.ForumEnabled, input.BlogEnabled,

src/models/post.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ type Post struct {
1919
PostDate time.Time `db:"postdate"`
2020
Deleted bool `db:"deleted"`
2121

22-
Preview string `db:"preview"`
23-
ReadOnly bool `db:"readonly"`
22+
PreviewPlaintext string `db:"preview"`
23+
PreviewHTML string `db:"preview_html"`
24+
ReadOnly bool `db:"readonly"`
2425
}
2526

2627
type PostVersion struct {

src/parsing/parsing.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,17 @@ import (
1212
"github.com/yuin/goldmark/util"
1313
)
1414

15-
// Used for rendering real-time previews of post content.
16-
var ForumPreviewMarkdown = makeGoldmark(
15+
// Used for rendering HTML previews of posts across the site.
16+
var PostPreviewMarkdown = makeGoldmark(
17+
false,
18+
goldmark.WithExtensions(makeGoldmarkExtensions(MarkdownOptions{
19+
Previews: true,
20+
Embeds: false,
21+
})...),
22+
)
23+
24+
// Used for rendering real-time previews of post content while editing.
25+
var PostEditPreviewMarkdown = makeGoldmark(
1726
false,
1827
goldmark.WithExtensions(makeGoldmarkExtensions(MarkdownOptions{
1928
Previews: true,
@@ -22,7 +31,7 @@ var ForumPreviewMarkdown = makeGoldmark(
2231
)
2332

2433
// Used for generating the final HTML for a post.
25-
var ForumRealMarkdown = makeGoldmark(
34+
var PostMarkdown = makeGoldmark(
2635
false,
2736
goldmark.WithExtensions(makeGoldmarkExtensions(MarkdownOptions{
2837
Previews: false,

src/parsing/parsing_test.go

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ import (
1010
func TestMarkdown(t *testing.T) {
1111
t.Run("fenced code blocks", func(t *testing.T) {
1212
t.Run("multiple lines", func(t *testing.T) {
13-
html := ParseMarkdown("```\nmultiple lines\n\tof code\n```", ForumRealMarkdown)
13+
html := ParseMarkdown("```\nmultiple lines\n\tof code\n```", PostMarkdown)
1414
t.Log(html)
1515
assert.Equal(t, 1, strings.Count(html, "<pre"))
1616
assert.Contains(t, html, `class="hmn-code"`)
1717
assert.Contains(t, html, "multiple lines\n\tof code")
1818
})
1919
t.Run("multiple lines with language", func(t *testing.T) {
20-
html := ParseMarkdown("```go\nfunc main() {\n\tfmt.Println(\"Hello, world!\")\n}\n```", ForumRealMarkdown)
20+
html := ParseMarkdown("```go\nfunc main() {\n\tfmt.Println(\"Hello, world!\")\n}\n```", PostMarkdown)
2121
t.Log(html)
2222
assert.Equal(t, 1, strings.Count(html, "<pre"))
2323
assert.Contains(t, html, `class="hmn-code"`)
@@ -27,10 +27,52 @@ func TestMarkdown(t *testing.T) {
2727
})
2828
}
2929

30+
func TestMarkdownVariants(t *testing.T) {
31+
md := `
32+
Here's a cool image that I really like a lot.
33+
34+
![picture of a dog](coolimage.png)
35+
36+
And here's my favorite YouTube video:
37+
38+
https://youtu.be/dQw4w9WgXcQ
39+
40+
I hope you like it as much as I do.
41+
`
42+
t.Run("Real post Markdown", func(t *testing.T) {
43+
html := ParseMarkdown(md, PostMarkdown)
44+
t.Log(html)
45+
assert.Contains(t, html, `<img src="coolimage.png"`)
46+
assert.Contains(t, html, "<iframe")
47+
})
48+
t.Run("Post edit preview Markdown", func(t *testing.T) {
49+
html := ParseMarkdown(md, PostEditPreviewMarkdown)
50+
t.Log(html)
51+
assert.Contains(t, html, `<img src="coolimage.png"`)
52+
assert.Contains(t, html, `<img src="https://img.youtube.com/vi/dQw4w9WgXcQ/hqdefault.jpg"`)
53+
assert.NotContains(t, html, "<iframe")
54+
})
55+
t.Run("Post preview Markdown", func(t *testing.T) {
56+
html := ParseMarkdown(md, PostPreviewMarkdown)
57+
t.Log(html)
58+
assert.Contains(t, html, `<img src="coolimage.png"`)
59+
assert.Contains(t, html, `<a href="https://youtu.be/dQw4w9WgXcQ"`)
60+
assert.NotContains(t, html, "<iframe")
61+
})
62+
t.Run("Plaintext Markdown", func(t *testing.T) {
63+
html := ParseMarkdown(md, PlaintextMarkdown)
64+
t.Log(html)
65+
assert.NotContains(t, html, `<img`)
66+
assert.NotContains(t, html, `<a`)
67+
assert.NotContains(t, html, "<iframe")
68+
assert.NotContains(t, html, "\n", "Plain text markdown is intended for OpenGraph descriptions and therefore shouldn't contain newlines")
69+
})
70+
}
71+
3072
func TestBBCode(t *testing.T) {
3173
t.Run("[code]", func(t *testing.T) {
3274
t.Run("one line", func(t *testing.T) {
33-
html := ParseMarkdown("[code]Just some code, you know?[/code]", ForumRealMarkdown)
75+
html := ParseMarkdown("[code]Just some code, you know?[/code]", PostMarkdown)
3476
t.Log(html)
3577
assert.Equal(t, 1, strings.Count(html, "<pre"))
3678
assert.Contains(t, html, `class="hmn-code"`)
@@ -41,7 +83,7 @@ func TestBBCode(t *testing.T) {
4183
Multiline code
4284
with an indent
4385
[/code]`
44-
html := ParseMarkdown(bbcode, ForumRealMarkdown)
86+
html := ParseMarkdown(bbcode, PostMarkdown)
4587
t.Log(html)
4688
assert.Equal(t, 1, strings.Count(html, "<pre"))
4789
assert.Contains(t, html, `class="hmn-code"`)
@@ -54,7 +96,7 @@ func main() {
5496
fmt.Println("Hello, world!")
5597
}
5698
[/code]`
57-
html := ParseMarkdown(bbcode, ForumRealMarkdown)
99+
html := ParseMarkdown(bbcode, PostMarkdown)
58100
t.Log(html)
59101
assert.Equal(t, 1, strings.Count(html, "<pre"))
60102
assert.Contains(t, html, "Println")
@@ -66,7 +108,7 @@ func main() {
66108
func TestSharlock(t *testing.T) {
67109
t.Skipf("This doesn't pass right now because parts of Sharlock's original source read as indented code blocks, or depend on different line break behavior.")
68110
t.Run("sanity check", func(t *testing.T) {
69-
result := ParseMarkdown(sharlock, ForumRealMarkdown)
111+
result := ParseMarkdown(sharlock, PostMarkdown)
70112

71113
for line := range strings.SplitSeq(result, "\n") {
72114
assert.NotContains(t, line, "[b]")
@@ -85,6 +127,6 @@ func TestSharlock(t *testing.T) {
85127

86128
func BenchmarkSharlock(b *testing.B) {
87129
for i := 0; i < b.N; i++ {
88-
ParseMarkdown(sharlock, ForumRealMarkdown)
130+
ParseMarkdown(sharlock, PostMarkdown)
89131
}
90132
}

src/templates/mapping.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ func PostToTemplate(p *models.Post, author *models.User) Post {
2222

2323
// Urls not set here. They vary per thread type. Set 'em yourself!
2424

25-
Preview: p.Preview,
26-
ReadOnly: p.ReadOnly,
25+
Preview: p.PreviewPlaintext,
26+
PreviewHTML: template.HTML(p.PreviewHTML),
27+
ReadOnly: p.ReadOnly,
2728

2829
Author: UserToTemplate(author),
2930
// No content. A lot of the time we don't have this handy and don't need it. See AddContentVersion.

src/templates/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ type Post struct {
127127
ReplyUrl string
128128

129129
Preview string
130+
PreviewHTML template.HTML
130131
ReadOnly bool
131132
ThreadLocked bool
132133

src/website/admin.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ func AdminApprovalQueue(c *RequestContext) ResponseData {
206206
}
207207
timelineItem := PostToTimelineItem(hmndata.UrlContextForProject(&p.Project), lineageBuilder, &p.Post, &p.Thread, p.ThreadOwner, &p.Author)
208208
timelineItem.OwnerAvatarUrl = ""
209-
timelineItem.Description = template.HTML(p.CurrentVersion.TextParsed)
209+
timelineItem.Description = template.HTML(p.Post.PreviewHTML)
210210
userData.Timeline = append(userData.Timeline, timelineItem)
211211
}
212212

0 commit comments

Comments
 (0)