Branch markdown into its own subpackage

This commit is contained in:
Noah 2018-02-09 19:01:56 -08:00
parent 3d5147ff4c
commit aabcf59181
8 changed files with 49 additions and 34 deletions

View File

@ -13,6 +13,7 @@ import (
"github.com/gorilla/feeds" "github.com/gorilla/feeds"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/kirsle/blog/core/internal/markdown"
"github.com/kirsle/blog/core/internal/models/comments" "github.com/kirsle/blog/core/internal/models/comments"
"github.com/kirsle/blog/core/internal/models/posts" "github.com/kirsle/blog/core/internal/models/posts"
"github.com/kirsle/blog/core/internal/models/settings" "github.com/kirsle/blog/core/internal/models/settings"
@ -439,7 +440,7 @@ func (b *Blog) RenderPost(p *posts.Post, indexView bool, numComments int) templa
// Render the post to HTML. // Render the post to HTML.
var rendered template.HTML var rendered template.HTML
if p.ContentType == string(MARKDOWN) { if p.ContentType == string(MARKDOWN) {
rendered = template.HTML(b.RenderTrustedMarkdown(p.Body)) rendered = template.HTML(markdown.RenderTrustedMarkdown(p.Body))
} else { } else {
rendered = template.HTML(p.Body) rendered = template.HTML(p.Body)
} }
@ -490,7 +491,7 @@ func (b *Blog) EditBlog(w http.ResponseWriter, r *http.Request) {
switch r.FormValue("submit") { switch r.FormValue("submit") {
case "preview": case "preview":
if post.ContentType == string(MARKDOWN) { if post.ContentType == string(MARKDOWN) {
v.Data["preview"] = template.HTML(b.RenderTrustedMarkdown(post.Body)) v.Data["preview"] = template.HTML(markdown.RenderTrustedMarkdown(post.Body))
} else { } else {
v.Data["preview"] = template.HTML(post.Body) v.Data["preview"] = template.HTML(post.Body)
} }

View File

@ -11,6 +11,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/kirsle/blog/core/internal/markdown"
"github.com/kirsle/blog/core/internal/models/comments" "github.com/kirsle/blog/core/internal/models/comments"
"github.com/kirsle/blog/core/internal/models/users" "github.com/kirsle/blog/core/internal/models/users"
) )
@ -62,7 +63,7 @@ func (b *Blog) RenderComments(session *sessions.Session, csrfToken, url, subject
// Render all the comments in the thread. // Render all the comments in the thread.
userMap := map[int]*users.User{} userMap := map[int]*users.User{}
for _, c := range thread.Comments { for _, c := range thread.Comments {
c.HTML = template.HTML(b.RenderMarkdown(c.Body)) c.HTML = template.HTML(markdown.RenderMarkdown(c.Body))
c.ThreadID = thread.ID c.ThreadID = thread.ID
c.OriginURL = url c.OriginURL = url
c.CSRF = csrfToken c.CSRF = csrfToken
@ -203,7 +204,7 @@ func (b *Blog) CommentHandler(w http.ResponseWriter, r *http.Request) {
c.Email = currentUser.Email c.Email = currentUser.Email
c.LoadAvatar() c.LoadAvatar()
} }
c.HTML = template.HTML(b.RenderMarkdown(c.Body)) c.HTML = template.HTML(markdown.RenderMarkdown(c.Body))
case "post": case "post":
if err := c.Validate(); err != nil { if err := c.Validate(); err != nil {
v.Error = err v.Error = err

View File

@ -10,6 +10,7 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/kirsle/blog/core/internal/forms" "github.com/kirsle/blog/core/internal/forms"
"github.com/kirsle/blog/core/internal/markdown"
"github.com/kirsle/blog/core/internal/models/settings" "github.com/kirsle/blog/core/internal/models/settings"
) )
@ -47,7 +48,7 @@ func (b *Blog) ContactRoutes(r *mux.Router) {
Template: ".email/contact.gohtml", Template: ".email/contact.gohtml",
Data: map[string]interface{}{ Data: map[string]interface{}{
"Name": form.Name, "Name": form.Name,
"Message": template.HTML(b.RenderMarkdown(form.Message)), "Message": template.HTML(markdown.RenderMarkdown(form.Message)),
"Email": form.Email, "Email": form.Email,
}, },
}) })

View File

@ -1,3 +1,4 @@
// Package core implements the core source code of kirsle/blog.
package core package core
import ( import (
@ -7,14 +8,15 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
"github.com/kirsle/blog/jsondb/caches" "github.com/kirsle/blog/core/internal/markdown"
"github.com/kirsle/blog/jsondb/caches/null"
"github.com/kirsle/blog/jsondb/caches/redis"
"github.com/kirsle/blog/jsondb"
"github.com/kirsle/blog/core/internal/models/comments" "github.com/kirsle/blog/core/internal/models/comments"
"github.com/kirsle/blog/core/internal/models/posts" "github.com/kirsle/blog/core/internal/models/posts"
"github.com/kirsle/blog/core/internal/models/settings" "github.com/kirsle/blog/core/internal/models/settings"
"github.com/kirsle/blog/core/internal/models/users" "github.com/kirsle/blog/core/internal/models/users"
"github.com/kirsle/blog/jsondb"
"github.com/kirsle/blog/jsondb/caches"
"github.com/kirsle/blog/jsondb/caches/null"
"github.com/kirsle/blog/jsondb/caches/redis"
"github.com/shurcooL/github_flavored_markdown/gfmstyle" "github.com/shurcooL/github_flavored_markdown/gfmstyle"
"github.com/urfave/negroni" "github.com/urfave/negroni"
) )
@ -76,6 +78,7 @@ func New(documentRoot, userRoot string) *Blog {
} else { } else {
blog.Cache = cache blog.Cache = cache
blog.DB.Cache = cache blog.DB.Cache = cache
markdown.Cache = cache
} }
} }

View File

@ -1,4 +1,5 @@
package core // Package markdown implements a GitHub Flavored Markdown renderer.
package markdown
import ( import (
"bytes" "bytes"
@ -10,17 +11,23 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/kirsle/blog/jsondb/caches"
"github.com/kirsle/golog"
"github.com/microcosm-cc/bluemonday" "github.com/microcosm-cc/bluemonday"
"github.com/shurcooL/github_flavored_markdown" "github.com/shurcooL/github_flavored_markdown"
) )
// Regexps for Markdown use cases. // Regexps for Markdown use cases.
var ( var (
// Plug your own Redis cacher in.
Cache caches.Cacher
// Match title from the first `# h1` heading. // Match title from the first `# h1` heading.
reMarkdownTitle = regexp.MustCompile(`(?m:^#([^#\r\n]+)$)`) reMarkdownTitle = regexp.MustCompile(`(?m:^#([^#\r\n]+)$)`)
// Match fenced code blocks with languages defined. // Match fenced code blocks with languages defined.
reFencedCode = regexp.MustCompile("```" + `([a-z]*)[\r\n]([\s\S]*?)[\r\n]\s*` + "```") reFencedCode = regexp.MustCompile("```" + `([a-z]*)[\r\n]([\s\S]*?)[\r\n]\s*` + "```")
reFencedCodeClass = regexp.MustCompile("^highlight highlight-[a-zA-Z0-9]+$")
// Regexp to match fenced code blocks in rendered Markdown HTML. // Regexp to match fenced code blocks in rendered Markdown HTML.
// Tweak this if you change Markdown engines later. // Tweak this if you change Markdown engines later.
@ -28,6 +35,12 @@ var (
reDecodeBlock = regexp.MustCompile(`\[?FENCED_CODE_%d_BLOCK?\]`) reDecodeBlock = regexp.MustCompile(`\[?FENCED_CODE_%d_BLOCK?\]`)
) )
var log *golog.Logger
func init() {
log = golog.GetLogger("blog")
}
// A container for parsed code blocks. // A container for parsed code blocks.
type codeBlock struct { type codeBlock struct {
placeholder int placeholder int
@ -51,8 +64,8 @@ func TitleFromMarkdown(body string) (string, error) {
// RenderMarkdown renders markdown to HTML, safely. It uses blackfriday to // RenderMarkdown renders markdown to HTML, safely. It uses blackfriday to
// render Markdown to HTML and then Bluemonday to sanitize the resulting HTML. // render Markdown to HTML and then Bluemonday to sanitize the resulting HTML.
func (b *Blog) RenderMarkdown(input string) string { func RenderMarkdown(input string) string {
unsafe := []byte(b.RenderTrustedMarkdown(input)) unsafe := []byte(RenderTrustedMarkdown(input))
// Sanitize HTML, but allow fenced code blocks to not get mangled in user // Sanitize HTML, but allow fenced code blocks to not get mangled in user
// submitted comments. // submitted comments.
@ -65,7 +78,7 @@ func (b *Blog) RenderMarkdown(input string) string {
// RenderTrustedMarkdown renders markdown to HTML, but without applying // RenderTrustedMarkdown renders markdown to HTML, but without applying
// bluemonday filtering afterward. This is for blog posts and website // bluemonday filtering afterward. This is for blog posts and website
// Markdown pages, not for user-submitted comments or things. // Markdown pages, not for user-submitted comments or things.
func (b *Blog) RenderTrustedMarkdown(input string) string { func RenderTrustedMarkdown(input string) string {
// Find and hang on to fenced code blocks. // Find and hang on to fenced code blocks.
codeBlocks := []codeBlock{} codeBlocks := []codeBlock{}
matches := reFencedCode.FindAllStringSubmatch(input, -1) matches := reFencedCode.FindAllStringSubmatch(input, -1)
@ -87,7 +100,7 @@ func (b *Blog) RenderTrustedMarkdown(input string) string {
// Substitute fenced codes back in. // Substitute fenced codes back in.
for _, block := range codeBlocks { for _, block := range codeBlocks {
highlighted, _ := b.Pygmentize(block.language, block.source) highlighted, _ := Pygmentize(block.language, block.source)
html = strings.Replace(html, html = strings.Replace(html,
fmt.Sprintf("[?FENCED_CODE_%d_BLOCK?]", block.placeholder), fmt.Sprintf("[?FENCED_CODE_%d_BLOCK?]", block.placeholder),
highlighted, highlighted,
@ -105,7 +118,7 @@ func (b *Blog) RenderTrustedMarkdown(input string) string {
// //
// The rendered result is cached in Redis if available, because the CLI // The rendered result is cached in Redis if available, because the CLI
// call takes ~0.6s which is slow if you're rendering a lot of code blocks. // call takes ~0.6s which is slow if you're rendering a lot of code blocks.
func (b *Blog) Pygmentize(language, source string) (string, error) { func Pygmentize(language, source string) (string, error) {
var result string var result string
// Hash the source for the cache key. // Hash the source for the cache key.
@ -114,10 +127,11 @@ func (b *Blog) Pygmentize(language, source string) (string, error) {
hash := fmt.Sprintf("%x", h.Sum(nil)) hash := fmt.Sprintf("%x", h.Sum(nil))
cacheKey := "pygmentize:" + hash cacheKey := "pygmentize:" + hash
_ = cacheKey
// Do we have it cached? // Do we have it cached?
if cached, err := b.Cache.Get(cacheKey); err == nil && len(cached) > 0 { // if cached, err := b.Cache.Get(cacheKey); err == nil && len(cached) > 0 {
return string(cached), nil // return string(cached), nil
} // }
// Defer to the `pygmentize` command // Defer to the `pygmentize` command
bin := "pygmentize" bin := "pygmentize"
@ -140,10 +154,10 @@ func (b *Blog) Pygmentize(language, source string) (string, error) {
} }
result = out.String() result = out.String()
err := b.Cache.Set(cacheKey, []byte(result), 60*60*24) // cool md5's don't change // err := b.Cache.Set(cacheKey, []byte(result), 60*60*24) // cool md5's don't change
if err != nil { // if err != nil {
log.Error("Couldn't cache Pygmentize output: %s", err) // log.Error("Couldn't cache Pygmentize output: %s", err)
} // }
return result, nil return result, nil
} }

View File

@ -8,6 +8,7 @@ import (
"net/url" "net/url"
"strings" "strings"
"github.com/kirsle/blog/core/internal/markdown"
"github.com/kirsle/blog/core/internal/models/comments" "github.com/kirsle/blog/core/internal/models/comments"
"github.com/kirsle/blog/core/internal/models/settings" "github.com/kirsle/blog/core/internal/models/settings"
"github.com/microcosm-cc/bluemonday" "github.com/microcosm-cc/bluemonday"
@ -107,7 +108,7 @@ func (b *Blog) NotifyComment(c *comments.Comment) {
Data: map[string]interface{}{ Data: map[string]interface{}{
"Name": c.Name, "Name": c.Name,
"Subject": c.Subject, "Subject": c.Subject,
"Body": template.HTML(b.RenderMarkdown(c.Body)), "Body": template.HTML(markdown.RenderMarkdown(c.Body)),
"URL": strings.Trim(s.Site.URL, "/") + c.OriginURL, "URL": strings.Trim(s.Site.URL, "/") + c.OriginURL,
"QuickDelete": fmt.Sprintf("%s/comments/quick-delete?t=%s&d=%s", "QuickDelete": fmt.Sprintf("%s/comments/quick-delete?t=%s&d=%s",
strings.Trim(s.Site.URL, "/"), strings.Trim(s.Site.URL, "/"),

View File

@ -8,6 +8,8 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/kirsle/blog/core/internal/markdown"
) )
// PageHandler is the catch-all route handler, for serving static web pages. // PageHandler is the catch-all route handler, for serving static web pages.
@ -54,8 +56,8 @@ func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) {
// Render it to HTML and find out its title. // Render it to HTML and find out its title.
body := string(source) body := string(source)
html := b.RenderTrustedMarkdown(body) html := markdown.RenderTrustedMarkdown(body)
title, _ := TitleFromMarkdown(body) title, _ := markdown.TitleFromMarkdown(body)
b.RenderTemplate(w, r, ".markdown", NewVars(map[interface{}]interface{}{ b.RenderTemplate(w, r, ".markdown", NewVars(map[interface{}]interface{}{
"Title": title, "Title": title,

View File

@ -1,8 +0,0 @@
package core
import "regexp"
var (
// CSS classes for Markdown fenced code blocks
reFencedCodeClass = regexp.MustCompile("^highlight highlight-[a-zA-Z0-9]+$")
)