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:
parent
c1785d79a4
commit
6b21291798
Binary file not shown.
|
@ -1,16 +1,23 @@
|
||||||
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",
|
||||||
BaseUrl: "http://handmade.local:9001",
|
PrivateAddr: ":9002",
|
||||||
LogLevel: zerolog.TraceLevel,
|
BaseUrl: "http://handmade.local:9001",
|
||||||
|
LogLevel: zerolog.TraceLevel,
|
||||||
Postgres: PostgresConfig{
|
Postgres: PostgresConfig{
|
||||||
User: "hmn",
|
User: "hmn",
|
||||||
Password: "password",
|
Password: "password",
|
||||||
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,
|
||||||
},
|
},
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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]
|
||||||
`
|
`
|
||||||
|
|
|
@ -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) {}
|
|
@ -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
|
||||||
|
|
|
@ -8,9 +8,10 @@
|
||||||
<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)
|
||||||
go.run(result.instance);
|
.then(result => {
|
||||||
});
|
go.run(result.instance);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
|
@ -110,6 +111,9 @@
|
||||||
MathJax.typeset();
|
MathJax.typeset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
goLoaded.then(() => {
|
||||||
|
updatePreview();
|
||||||
|
});
|
||||||
tf.addEventListener('input', () => {
|
tf.addEventListener('input', () => {
|
||||||
updatePreview();
|
updatePreview();
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue