Add plain-text post previews

I opted to do this by making a new markdown renderer that only outputs
plain text, no HTML. This feels a lot more sane to me than trying to
strip HTML out of already-parsed stuff. The tradeoff right now is that
some content just doesn't show up at all, notably bbcode content. I
doubt anyone will care.
This commit is contained in:
Ben Visness 2021-07-05 13:34:51 -05:00
parent c1785d79a4
commit 6b21291798
8 changed files with 111 additions and 44 deletions

Binary file not shown.

View File

@ -1,8 +1,14 @@
package config package config
import (
"github.com/jackc/pgx/v4"
"github.com/rs/zerolog"
)
var Config = HMNConfig{ var Config = HMNConfig{
Env: Dev, Env: Dev,
Addr: ":9001", Addr: ":9001",
PrivateAddr: ":9002",
BaseUrl: "http://handmade.local:9001", BaseUrl: "http://handmade.local:9001",
LogLevel: zerolog.TraceLevel, LogLevel: zerolog.TraceLevel,
Postgres: PostgresConfig{ Postgres: PostgresConfig{
@ -11,6 +17,7 @@ var Config = HMNConfig{
Hostname: "handmade.local", Hostname: "handmade.local",
Port: 5454, Port: 5454,
DbName: "hmn", DbName: "hmn",
LogLevel: pgx.LogLevelTrace,
MinConn: 2, // Keep these low for dev, high for production MinConn: 2, // Keep these low for dev, high for production
MaxConn: 2, MaxConn: 2,
}, },

View File

@ -9,37 +9,23 @@ import (
"github.com/yuin/goldmark/util" "github.com/yuin/goldmark/util"
) )
var previewMarkdown = goldmark.New( // Used for rendering real-time previews of post content.
goldmark.WithExtensions( var PreviewMarkdown = goldmark.New(
extension.GFM, goldmark.WithExtensions(makeGoldmarkExtensions(true)...),
highlightExtension,
SpoilerExtension{},
EmbedExtension{
Preview: true,
},
MathjaxExtension{},
BBCodeExtension{
Preview: true,
},
),
)
var realMarkdown = goldmark.New(
goldmark.WithExtensions(
extension.GFM,
highlightExtension,
SpoilerExtension{},
EmbedExtension{},
MathjaxExtension{},
BBCodeExtension{},
),
) )
func ParsePostInput(source string, preview bool) string { // Used for generating the final HTML for a post.
md := realMarkdown var RealMarkdown = goldmark.New(
if preview { goldmark.WithExtensions(makeGoldmarkExtensions(false)...),
md = previewMarkdown )
}
// Used for generating plain-text previews of posts.
var PlaintextMarkdown = goldmark.New(
goldmark.WithExtensions(makeGoldmarkExtensions(false)...),
goldmark.WithRenderer(plaintextRenderer{}),
)
func ParsePostInput(source string, md goldmark.Markdown) string {
var buf bytes.Buffer var buf bytes.Buffer
if err := md.Convert([]byte(source), &buf); err != nil { if err := md.Convert([]byte(source), &buf); err != nil {
panic(err) panic(err)
@ -48,6 +34,21 @@ func ParsePostInput(source string, preview bool) string {
return buf.String() return buf.String()
} }
func makeGoldmarkExtensions(preview bool) []goldmark.Extender {
return []goldmark.Extender{
extension.GFM,
highlightExtension,
SpoilerExtension{},
EmbedExtension{
Preview: preview,
},
MathjaxExtension{},
BBCodeExtension{
Preview: preview,
},
}
}
var highlightExtension = highlighting.NewHighlighting( var highlightExtension = highlighting.NewHighlighting(
highlighting.WithFormatOptions(HMNChromaOptions...), highlighting.WithFormatOptions(HMNChromaOptions...),
highlighting.WithWrapperRenderer(func(w util.BufWriter, context highlighting.CodeBlockContext, entering bool) { highlighting.WithWrapperRenderer(func(w util.BufWriter, context highlighting.CodeBlockContext, entering bool) {

View File

@ -10,14 +10,14 @@ import (
func TestMarkdown(t *testing.T) { func TestMarkdown(t *testing.T) {
t.Run("fenced code blocks", func(t *testing.T) { t.Run("fenced code blocks", func(t *testing.T) {
t.Run("multiple lines", func(t *testing.T) { t.Run("multiple lines", func(t *testing.T) {
html := ParsePostInput("```\nmultiple lines\n\tof code\n```", false) html := ParsePostInput("```\nmultiple lines\n\tof code\n```", RealMarkdown)
t.Log(html) t.Log(html)
assert.Equal(t, 1, strings.Count(html, "<pre")) assert.Equal(t, 1, strings.Count(html, "<pre"))
assert.Contains(t, html, `class="hmn-code"`) assert.Contains(t, html, `class="hmn-code"`)
assert.Contains(t, html, "multiple lines\n\tof code") assert.Contains(t, html, "multiple lines\n\tof code")
}) })
t.Run("multiple lines with language", func(t *testing.T) { t.Run("multiple lines with language", func(t *testing.T) {
html := ParsePostInput("```go\nfunc main() {\n\tfmt.Println(\"Hello, world!\")\n}\n```", false) html := ParsePostInput("```go\nfunc main() {\n\tfmt.Println(\"Hello, world!\")\n}\n```", RealMarkdown)
t.Log(html) t.Log(html)
assert.Equal(t, 1, strings.Count(html, "<pre")) assert.Equal(t, 1, strings.Count(html, "<pre"))
assert.Contains(t, html, `class="hmn-code"`) assert.Contains(t, html, `class="hmn-code"`)
@ -30,7 +30,7 @@ func TestMarkdown(t *testing.T) {
func TestBBCode(t *testing.T) { func TestBBCode(t *testing.T) {
t.Run("[code]", func(t *testing.T) { t.Run("[code]", func(t *testing.T) {
t.Run("one line", func(t *testing.T) { t.Run("one line", func(t *testing.T) {
html := ParsePostInput("[code]Just some code, you know?[/code]", false) html := ParsePostInput("[code]Just some code, you know?[/code]", RealMarkdown)
t.Log(html) t.Log(html)
assert.Equal(t, 1, strings.Count(html, "<pre")) assert.Equal(t, 1, strings.Count(html, "<pre"))
assert.Contains(t, html, `class="hmn-code"`) assert.Contains(t, html, `class="hmn-code"`)
@ -41,7 +41,7 @@ func TestBBCode(t *testing.T) {
Multiline code Multiline code
with an indent with an indent
[/code]` [/code]`
html := ParsePostInput(bbcode, false) html := ParsePostInput(bbcode, RealMarkdown)
t.Log(html) t.Log(html)
assert.Equal(t, 1, strings.Count(html, "<pre")) assert.Equal(t, 1, strings.Count(html, "<pre"))
assert.Contains(t, html, `class="hmn-code"`) assert.Contains(t, html, `class="hmn-code"`)
@ -54,7 +54,7 @@ func main() {
fmt.Println("Hello, world!") fmt.Println("Hello, world!")
} }
[/code]` [/code]`
html := ParsePostInput(bbcode, false) html := ParsePostInput(bbcode, RealMarkdown)
t.Log(html) t.Log(html)
assert.Equal(t, 1, strings.Count(html, "<pre")) assert.Equal(t, 1, strings.Count(html, "<pre"))
assert.Contains(t, html, "Println") assert.Contains(t, html, "Println")
@ -114,4 +114,7 @@ func main() {
[td]Body 1[/td] [td]Body 2[/td] [td]Body 1[/td] [td]Body 2[/td]
[/tr] [/tr]
[/table] [/table]
[youtube]https://www.youtube.com/watch?v=0J8G9qNT7gQ[/youtube]
[youtube]https://youtu.be/0J8G9qNT7gQ[/youtube]
` `

46
src/parsing/renderer.go Normal file
View File

@ -0,0 +1,46 @@
package parsing
import (
"io"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/renderer"
)
type plaintextRenderer struct{}
var _ renderer.Renderer = plaintextRenderer{}
func (r plaintextRenderer) Render(w io.Writer, source []byte, n ast.Node) error {
return ast.Walk(n, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
switch n.Kind() {
case ast.KindText:
n := n.(*ast.Text)
_, err := w.Write(n.Text(source))
if err != nil {
return ast.WalkContinue, err
}
if n.SoftLineBreak() {
_, err := w.Write([]byte(" "))
if err != nil {
return ast.WalkContinue, err
}
}
case ast.KindParagraph:
_, err := w.Write([]byte(" "))
if err != nil {
return ast.WalkContinue, err
}
}
return ast.WalkContinue, nil
})
}
func (r plaintextRenderer) AddOptions(...renderer.Option) {}

View File

@ -8,7 +8,7 @@ import (
func main() { func main() {
js.Global().Set("parseMarkdown", js.FuncOf(func(this js.Value, args []js.Value) interface{} { js.Global().Set("parseMarkdown", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return parsing.ParsePostInput(args[0].String(), true) return parsing.ParsePostInput(args[0].String(), parsing.PreviewMarkdown)
})) }))
var done chan bool var done chan bool

View File

@ -8,7 +8,8 @@
<script src="{{ static "go_wasm_exec.js" }}"></script> <script src="{{ static "go_wasm_exec.js" }}"></script>
<script> <script>
const go = new Go(); const go = new Go();
WebAssembly.instantiateStreaming(fetch("{{ static "parsing.wasm" }}"), go.importObject).then(result => { const goLoaded = WebAssembly.instantiateStreaming(fetch("{{ static "parsing.wasm" }}"), go.importObject)
.then(result => {
go.run(result.instance); go.run(result.instance);
}); });
</script> </script>
@ -110,6 +111,9 @@
MathJax.typeset(); MathJax.typeset();
} }
goLoaded.then(() => {
updatePreview();
});
tf.addEventListener('input', () => { tf.addEventListener('input', () => {
updatePreview(); updatePreview();
}); });

View File

@ -581,11 +581,17 @@ func ForumNewThreadSubmit(c *RequestContext) ResponseData {
sticky = true sticky = true
} }
parsed := parsing.ParsePostInput(unparsed, false) parsed := parsing.ParsePostInput(unparsed, parsing.RealMarkdown)
now := time.Now() now := time.Now()
ip := net.ParseIP(c.Req.RemoteAddr) ip := net.ParseIP(c.Req.RemoteAddr)
const previewMaxLength = 100
parsedPlaintext := parsing.ParsePostInput(unparsed, parsing.PlaintextMarkdown)
preview := parsedPlaintext
if len(preview) > previewMaxLength-1 {
preview = preview[:previewMaxLength-1] + "…"
}
// Create thread // Create thread
var threadId int var threadId int
err = tx.QueryRow(c.Context(), err = tx.QueryRow(c.Context(),
@ -615,7 +621,7 @@ func ForumNewThreadSubmit(c *RequestContext) ResponseData {
now, now,
currentCatId, currentCatId,
threadId, threadId,
"lol", // TODO: Actual previews preview,
-1, -1,
c.CurrentUser.ID, c.CurrentUser.ID,
models.CatKindForum, models.CatKindForum,