diff --git a/cmd/blog/main.go b/cmd/blog/main.go index 1937f9e..f4de630 100644 --- a/cmd/blog/main.go +++ b/cmd/blog/main.go @@ -51,5 +51,5 @@ func main() { jsondb.SetDebug(true) } - app.ListenAndServe(fAddress) + app.Run(fAddress) } diff --git a/core/admin.go b/core/admin.go index ad0b1b1..8b142db 100644 --- a/core/admin.go +++ b/core/admin.go @@ -1,7 +1,6 @@ package core import ( - "fmt" "io/ioutil" "net/http" "net/url" @@ -11,10 +10,9 @@ import ( "strings" "github.com/gorilla/mux" - "github.com/kirsle/blog/jsondb/caches/null" - "github.com/kirsle/blog/jsondb/caches/redis" "github.com/kirsle/blog/core/internal/forms" "github.com/kirsle/blog/core/internal/models/settings" + "github.com/kirsle/blog/core/internal/render" "github.com/urfave/negroni" ) @@ -33,13 +31,13 @@ func (b *Blog) AdminRoutes(r *mux.Router) { // AdminHandler is the admin landing page. func (b *Blog) AdminHandler(w http.ResponseWriter, r *http.Request) { - b.RenderTemplate(w, r, "admin/index", nil) + b.RenderTemplate(w, r, "admin/index", render.Vars{}) } // FileTree holds information about files in the document roots. type FileTree struct { UserRoot bool // false = CoreRoot - Files []Filepath + Files []render.Filepath } // EditorHandler lets you edit web pages from the frontend. @@ -127,7 +125,7 @@ func (b *Blog) editorFileList(w http.ResponseWriter, r *http.Request) { for i, root := range []string{b.UserRoot, b.DocumentRoot} { tree := FileTree{ UserRoot: i == 0, - Files: []Filepath{}, + Files: []render.Filepath{}, } filepath.Walk(root, func(path string, f os.FileInfo, err error) error { @@ -155,7 +153,7 @@ func (b *Blog) editorFileList(w http.ResponseWriter, r *http.Request) { return nil } - tree.Files = append(tree.Files, Filepath{ + tree.Files = append(tree.Files, render.Filepath{ Absolute: abs, Relative: rel, Basename: filepath.Base(path), @@ -224,24 +222,7 @@ func (b *Blog) SettingsHandler(w http.ResponseWriter, r *http.Request) { } else { // Save the settings. settings.Save() - - // Reset Redis configuration. - if settings.Redis.Enabled { - cache, err := redis.New( - fmt.Sprintf("%s:%d", settings.Redis.Host, settings.Redis.Port), - settings.Redis.DB, - settings.Redis.Prefix, - ) - if err != nil { - b.Flash(w, r, "Error connecting to Redis: %s", err) - b.Cache = null.New() - } else { - b.Cache = cache - } - } else { - b.Cache = null.New() - } - b.DB.Cache = b.Cache + b.Configure() b.FlashAndReload(w, r, "Settings have been saved!") return diff --git a/core/auth.go b/core/auth.go index e0babce..d601480 100644 --- a/core/auth.go +++ b/core/auth.go @@ -6,6 +6,7 @@ import ( "github.com/gorilla/mux" "github.com/kirsle/blog/core/internal/forms" + "github.com/kirsle/blog/core/internal/log" "github.com/kirsle/blog/core/internal/models/users" ) diff --git a/core/blog.go b/core/blog.go index b1b73f6..8028596 100644 --- a/core/blog.go +++ b/core/blog.go @@ -13,11 +13,13 @@ import ( "github.com/gorilla/feeds" "github.com/gorilla/mux" + "github.com/kirsle/blog/core/internal/log" "github.com/kirsle/blog/core/internal/markdown" "github.com/kirsle/blog/core/internal/models/comments" "github.com/kirsle/blog/core/internal/models/posts" "github.com/kirsle/blog/core/internal/models/settings" "github.com/kirsle/blog/core/internal/models/users" + "github.com/kirsle/blog/core/internal/render" "github.com/urfave/negroni" ) @@ -306,12 +308,20 @@ func (b *Blog) RenderIndex(r *http.Request, tag, privacy string) template.HTML { // Render the blog index partial. var output bytes.Buffer - v := map[string]interface{}{ - "PreviousPage": previousPage, - "NextPage": nextPage, - "View": view, + v := render.Vars{ + Data: map[interface{}]interface{}{ + "PreviousPage": previousPage, + "NextPage": nextPage, + "View": view, + }, } - b.RenderPartialTemplate(&output, "blog/index.partial", v, false, nil) + v = b.LoadDefaults(v, r) + render.PartialTemplate(&output, "blog/index.partial", render.Config{ + Request: r, + Vars: &v, + WithLayout: false, + Functions: b.TemplateFuncs(nil, r, nil), + }) return template.HTML(output.String()) } @@ -329,14 +339,20 @@ func (b *Blog) RenderTags(r *http.Request, indexView bool) template.HTML { } var output bytes.Buffer - v := struct { - IndexView bool - Tags []posts.Tag - }{ - IndexView: indexView, - Tags: tags, + v := render.Vars{ + Data: map[interface{}]interface{}{ + "IndexView": indexView, + "Tags": tags, + }, } - b.RenderPartialTemplate(&output, "blog/tags.partial", v, false, nil) + v = b.LoadDefaults(v, r) + render.PartialTemplate(&output, "blog/tags.partial", render.Config{ + Request: r, + Vars: &v, + WithLayout: false, + Functions: b.TemplateFuncs(nil, nil, nil), + }) + // b.RenderPartialTemplate(&output, r, "blog/tags.partial", v, false, nil) return template.HTML(output.String()) } @@ -417,7 +433,7 @@ func (b *Blog) viewPost(w http.ResponseWriter, r *http.Request, fragment string) // RenderPost renders a blog post as a partial template and returns the HTML. // If indexView is true, the blog headers will be hyperlinked to the dedicated // entry view page. -func (b *Blog) RenderPost(p *posts.Post, indexView bool, numComments int) template.HTML { +func (b *Blog) RenderPost(r *http.Request, p *posts.Post, indexView bool, numComments int) template.HTML { // Look up the author's information. author, err := users.LoadReadonly(p.AuthorID) if err != nil { @@ -445,16 +461,18 @@ func (b *Blog) RenderPost(p *posts.Post, indexView bool, numComments int) templa rendered = template.HTML(p.Body) } - meta := PostMeta{ - Post: p, - Rendered: rendered, - Author: author, - IndexView: indexView, - Snipped: snipped, - NumComments: numComments, + meta := render.Vars{ + Data: map[interface{}]interface{}{ + "Post": p, + "Rendered": rendered, + "Author": author, + "IndexView": indexView, + "Snipped": snipped, + "NumComments": numComments, + }, } output := bytes.Buffer{} - err = b.RenderPartialTemplate(&output, "blog/entry.partial", meta, false, nil) + err = b.RenderPartialTemplate(&output, r, "blog/entry.partial", meta, false, nil) if err != nil { return template.HTML(fmt.Sprintf("[template error in blog/entry.partial: %s]", err.Error())) } diff --git a/core/comments.go b/core/comments.go index 521521d..40066e8 100644 --- a/core/comments.go +++ b/core/comments.go @@ -11,9 +11,11 @@ import ( "github.com/google/uuid" "github.com/gorilla/mux" "github.com/gorilla/sessions" + "github.com/kirsle/blog/core/internal/log" "github.com/kirsle/blog/core/internal/markdown" "github.com/kirsle/blog/core/internal/models/comments" "github.com/kirsle/blog/core/internal/models/users" + "github.com/kirsle/blog/core/internal/render" ) // CommentRoutes attaches the comment routes to the app. @@ -91,14 +93,14 @@ func (b *Blog) RenderComments(session *sessions.Session, csrfToken, url, subject } // Get the template snippet. - filepath, err := b.ResolvePath("comments/comments.partial") + filepath, err := render.ResolvePath("comments/comments.partial") if err != nil { log.Error(err.Error()) return template.HTML("[error: missing comments/comments.partial]") } // And the comment view partial. - entryPartial, err := b.ResolvePath("comments/entry.partial") + entryPartial, err := render.ResolvePath("comments/entry.partial") if err != nil { log.Error(err.Error()) return template.HTML("[error: missing comments/entry.partial]") diff --git a/core/core.go b/core/core.go index 5a48ecb..60da7e7 100644 --- a/core/core.go +++ b/core/core.go @@ -8,11 +8,13 @@ import ( "github.com/gorilla/mux" "github.com/gorilla/sessions" + "github.com/kirsle/blog/core/internal/log" "github.com/kirsle/blog/core/internal/markdown" "github.com/kirsle/blog/core/internal/models/comments" "github.com/kirsle/blog/core/internal/models/posts" "github.com/kirsle/blog/core/internal/models/settings" "github.com/kirsle/blog/core/internal/models/users" + "github.com/kirsle/blog/core/internal/render" "github.com/kirsle/blog/jsondb" "github.com/kirsle/blog/jsondb/caches" "github.com/kirsle/blog/jsondb/caches/null" @@ -41,28 +43,43 @@ type Blog struct { // New initializes the Blog application. func New(documentRoot, userRoot string) *Blog { - blog := &Blog{ + return &Blog{ DocumentRoot: documentRoot, UserRoot: userRoot, DB: jsondb.New(filepath.Join(userRoot, ".private")), Cache: null.New(), } +} +// Run quickly configures and starts the HTTP server. +func (b *Blog) Run(address string) { + b.Configure() + b.SetupHTTP() + b.ListenAndServe(address) +} + +// Configure initializes (or reloads) the blog's configuration, and binds the +// settings in sub-packages. +func (b *Blog) Configure() { // Load the site config, or start with defaults if not found. - settings.DB = blog.DB + settings.DB = b.DB config, err := settings.Load() if err != nil { config = settings.Defaults() } + // Bind configs in sub-packages. + render.UserRoot = &b.UserRoot + render.DocumentRoot = &b.DocumentRoot + // Initialize the session cookie store. - blog.store = sessions.NewCookieStore([]byte(config.Security.SecretKey)) + b.store = sessions.NewCookieStore([]byte(config.Security.SecretKey)) users.HashCost = config.Security.HashCost // Initialize the rest of the models. - posts.DB = blog.DB - users.DB = blog.DB - comments.DB = blog.DB + posts.DB = b.DB + users.DB = b.DB + comments.DB = b.DB // Redis cache? if config.Redis.Enabled { @@ -76,41 +93,42 @@ func New(documentRoot, userRoot string) *Blog { if err != nil { log.Error("Redis init error: %s", err.Error()) } else { - blog.Cache = cache - blog.DB.Cache = cache + b.Cache = cache + b.DB.Cache = cache markdown.Cache = cache } } +} +// SetupHTTP initializes the Negroni middleware engine and registers routes. +func (b *Blog) SetupHTTP() { // Initialize the router. r := mux.NewRouter() - r.HandleFunc("/initial-setup", blog.SetupHandler) - blog.AuthRoutes(r) - blog.AdminRoutes(r) - blog.ContactRoutes(r) - blog.BlogRoutes(r) - blog.CommentRoutes(r) + r.HandleFunc("/initial-setup", b.SetupHandler) + b.AuthRoutes(r) + b.AdminRoutes(r) + b.ContactRoutes(r) + b.BlogRoutes(r) + b.CommentRoutes(r) // GitHub Flavored Markdown CSS. r.Handle("/css/gfm.css", http.StripPrefix("/css", http.FileServer(gfmstyle.Assets))) - r.PathPrefix("/").HandlerFunc(blog.PageHandler) - r.NotFoundHandler = http.HandlerFunc(blog.PageHandler) + r.PathPrefix("/").HandlerFunc(b.PageHandler) + r.NotFoundHandler = http.HandlerFunc(b.PageHandler) n := negroni.New( negroni.NewRecovery(), negroni.NewLogger(), - negroni.HandlerFunc(blog.SessionLoader), - negroni.HandlerFunc(blog.CSRFMiddleware), - negroni.HandlerFunc(blog.AuthMiddleware), + negroni.HandlerFunc(b.SessionLoader), + negroni.HandlerFunc(b.CSRFMiddleware), + negroni.HandlerFunc(b.AuthMiddleware), ) n.UseHandler(r) // Keep references handy elsewhere in the app. - blog.n = n - blog.r = r - - return blog + b.n = n + b.r = r } // ListenAndServe begins listening on the given bind address. diff --git a/core/initial-setup.go b/core/initial-setup.go index 43afda5..a99e656 100644 --- a/core/initial-setup.go +++ b/core/initial-setup.go @@ -5,13 +5,15 @@ import ( "github.com/gorilla/sessions" "github.com/kirsle/blog/core/internal/forms" + "github.com/kirsle/blog/core/internal/log" "github.com/kirsle/blog/core/internal/models/settings" "github.com/kirsle/blog/core/internal/models/users" + "github.com/kirsle/blog/core/internal/render" ) // SetupHandler is the initial blog setup route. func (b *Blog) SetupHandler(w http.ResponseWriter, r *http.Request) { - vars := &Vars{ + vars := render.Vars{ Form: forms.Setup{}, } diff --git a/core/internal/log/log.go b/core/internal/log/log.go new file mode 100644 index 0000000..c7f4ea7 --- /dev/null +++ b/core/internal/log/log.go @@ -0,0 +1,30 @@ +// Package log implements the common logging engine for the blog. +package log + +import "github.com/kirsle/golog" + +var log *golog.Logger + +func init() { + log = golog.GetLogger("blog") + log.Configure(&golog.Config{ + Colors: golog.ExtendedColor, + Theme: golog.DarkTheme, + }) +} + +func Debug(m string, v ...interface{}) { + log.Debug(m, v...) +} + +func Info(m string, v ...interface{}) { + log.Info(m, v...) +} + +func Warn(m string, v ...interface{}) { + log.Warn(m, v...) +} + +func Error(m string, v ...interface{}) { + log.Error(m, v...) +} diff --git a/core/internal/markdown/markdown.go b/core/internal/markdown/markdown.go index f48b748..aa148b9 100644 --- a/core/internal/markdown/markdown.go +++ b/core/internal/markdown/markdown.go @@ -11,8 +11,8 @@ import ( "regexp" "strings" + "github.com/kirsle/blog/core/internal/log" "github.com/kirsle/blog/jsondb/caches" - "github.com/kirsle/golog" "github.com/microcosm-cc/bluemonday" "github.com/shurcooL/github_flavored_markdown" ) @@ -35,12 +35,6 @@ var ( reDecodeBlock = regexp.MustCompile(`\[?FENCED_CODE_%d_BLOCK?\]`) ) -var log *golog.Logger - -func init() { - log = golog.GetLogger("blog") -} - // A container for parsed code blocks. type codeBlock struct { placeholder int @@ -100,7 +94,10 @@ func RenderTrustedMarkdown(input string) string { // Substitute fenced codes back in. for _, block := range codeBlocks { - highlighted, _ := Pygmentize(block.language, block.source) + highlighted, err := Pygmentize(block.language, block.source) + if err != nil { + log.Error("Pygmentize error: %s", err) + } html = strings.Replace(html, fmt.Sprintf("[?FENCED_CODE_%d_BLOCK?]", block.placeholder), highlighted, @@ -127,11 +124,10 @@ func Pygmentize(language, source string) (string, error) { hash := fmt.Sprintf("%x", h.Sum(nil)) cacheKey := "pygmentize:" + hash - _ = cacheKey // Do we have it cached? - // if cached, err := b.Cache.Get(cacheKey); err == nil && len(cached) > 0 { - // return string(cached), nil - // } + if cached, err := Cache.Get(cacheKey); err == nil && len(cached) > 0 { + return string(cached), nil + } // Defer to the `pygmentize` command bin := "pygmentize" @@ -154,10 +150,10 @@ func Pygmentize(language, source string) (string, error) { } result = out.String() - // err := b.Cache.Set(cacheKey, []byte(result), 60*60*24) // cool md5's don't change - // if err != nil { - // log.Error("Couldn't cache Pygmentize output: %s", err) - // } + err := Cache.Set(cacheKey, []byte(result), 60*60*24) // cool md5's don't change + if err != nil { + log.Error("Couldn't cache Pygmentize output: %s", err) + } return result, nil } diff --git a/core/internal/render/resolve_paths.go b/core/internal/render/resolve_paths.go new file mode 100644 index 0000000..6f3b280 --- /dev/null +++ b/core/internal/render/resolve_paths.go @@ -0,0 +1,92 @@ +package render + +import ( + "errors" + "os" + "path/filepath" + "strings" + + "github.com/kirsle/blog/core/internal/log" +) + +// Blog configuration bindings. +var ( + UserRoot *string + DocumentRoot *string +) + +// Filepath represents a file discovered in the document roots, and maintains +// both its relative and absolute components. +type Filepath struct { + // Canonicalized URI version of the file resolved on disk, + // possible with a file extension injected. + // (i.e. "/about" -> "about.html") + URI string + Basename string + Relative string // Relative path including document root (i.e. "root/about.html") + Absolute string // Absolute path on disk (i.e. "/opt/blog/root/about.html") +} + +func (f Filepath) String() string { + return f.Relative +} + +// ResolvePath matches a filesystem path to a relative request URI. +// +// This checks the UserRoot first and then the DocumentRoot. This way the user +// may override templates from the core app's document root. +func ResolvePath(path string) (Filepath, error) { + // Strip leading slashes. + if path[0] == '/' { + path = strings.TrimPrefix(path, "/") + } + + // If you need to debug this function, edit this block. + debug := func(tmpl string, args ...interface{}) { + if false { + log.Debug(tmpl, args...) + } + } + + debug("Resolving filepath for URI: %s", path) + for _, root := range []string{*UserRoot, *DocumentRoot} { + if len(root) == 0 { + continue + } + + // Resolve the file path. + relPath := filepath.Join(root, path) + absPath, err := filepath.Abs(relPath) + basename := filepath.Base(relPath) + if err != nil { + log.Error("%v", err) + } + + debug("Expected filepath: %s", absPath) + + // Found an exact hit? + if stat, err := os.Stat(absPath); !os.IsNotExist(err) && !stat.IsDir() { + debug("Exact filepath found: %s", absPath) + return Filepath{path, basename, relPath, absPath}, nil + } + + // Try some supported suffixes. + suffixes := []string{ + ".gohtml", + ".html", + "/index.gohtml", + "/index.html", + ".md", + "/index.md", + } + for _, suffix := range suffixes { + test := absPath + suffix + if stat, err := os.Stat(test); !os.IsNotExist(err) && !stat.IsDir() { + debug("Filepath found via suffix %s: %s", suffix, test) + return Filepath{path + suffix, basename + suffix, relPath + suffix, test}, nil + } + } + } + + return Filepath{}, errors.New("not found") +} diff --git a/core/internal/render/templates.go b/core/internal/render/templates.go new file mode 100644 index 0000000..3367a92 --- /dev/null +++ b/core/internal/render/templates.go @@ -0,0 +1,159 @@ +package render + +import ( + "html/template" + "io" + "net/http" + "strings" + "time" + + "github.com/kirsle/blog/core/internal/forms" + "github.com/kirsle/blog/core/internal/log" + "github.com/kirsle/blog/core/internal/models/users" +) + +// Config provides the settings and injectables for rendering templates. +type Config struct { + // Refined and raw variables for the templates. + Vars *Vars // Normal RenderTemplate's + + // Wrap the template with the `.layout.gohtml` + WithLayout bool + + // Inject your own functions for the Go templates. + Functions map[string]interface{} + + Request *http.Request +} + +// Vars is an interface to implement by the templates to pass their own custom +// variables in. It auto-loads global template variables (site name, etc.) +// when the template is rendered. +type Vars struct { + // Global, "constant" template variables. + SetupNeeded bool + Title string + Path string + LoggedIn bool + CurrentUser *users.User + CSRF string + Editable bool // page is editable + Request *http.Request + RequestTime time.Time + RequestDuration time.Duration + + // Configuration variables + NoLayout bool // don't wrap in .layout.html, just render the template + + // Common template variables. + Message string + Flashes []string + Error error + Data map[interface{}]interface{} + Form forms.Form +} + +// PartialTemplate handles rendering a Go template to a writer, without +// doing anything extra to the vars or dealing with net/http. This is ideal for +// rendering partials, such as comment partials. +// +// This will wrap the template in `.layout.gohtml` by default. To render just +// a bare template on its own, i.e. for partial templates, create a Vars struct +// with `Vars{NoIndex: true}` +func PartialTemplate(w io.Writer, path string, C Config) error { + if C.Request == nil { + panic("render.RenderPartialTemplate(): The *http.Request is nil!?") + } + + // v interface{}, withLayout bool, functions map[string]interface{}) error { + var ( + layout Filepath + templateName string + err error + ) + + // Find the file path to the template. + filepath, err := ResolvePath(path) + if err != nil { + log.Error("RenderTemplate(%s): file not found", path) + return err + } + + // Get the layout template. + if C.WithLayout { + templateName = "layout" + layout, err = ResolvePath(".layout") + if err != nil { + log.Error("RenderTemplate(%s): layout template not found", path) + return err + } + } else { + templateName = filepath.Basename + } + + // The comment entry partial. + commentEntry, err := ResolvePath("comments/entry.partial") + if err != nil { + log.Error("RenderTemplate(%s): comments/entry.partial not found") + return err + } + + // Template functions. + funcmap := template.FuncMap{ + "StringsJoin": strings.Join, + "Now": time.Now, + "TemplateName": func() string { + return filepath.URI + }, + } + if C.Functions != nil { + for name, fn := range C.Functions { + funcmap[name] = fn + } + } + + // Useful template functions. + t := template.New(filepath.Absolute).Funcs(funcmap) + + // Parse the template files. The layout comes first because it's the wrapper + // and allows the filepath template to set the page title. + var templates []string + if C.WithLayout { + templates = append(templates, layout.Absolute) + } + t, err = t.ParseFiles(append(templates, commentEntry.Absolute, filepath.Absolute)...) + if err != nil { + log.Error(err.Error()) + return err + } + + err = t.ExecuteTemplate(w, templateName, C.Vars) + if err != nil { + log.Error("Template parsing error: %s", err) + return err + } + + return nil +} + +// Template responds with an HTML template. +// +// The vars will be massaged a bit to load the global defaults (such as the +// website title and user login status), the user's session may be updated with +// new CSRF token, and other such things. If you just want to render a template +// without all that nonsense, use RenderPartialTemplate. +func Template(w http.ResponseWriter, path string, C Config) error { + if C.Request == nil { + panic("render.RenderTemplate(): The *http.Request is nil!?") + } + + w.Header().Set("Content-Type", "text/html; encoding=UTF-8") + PartialTemplate(w, path, Config{ + Request: C.Request, + Vars: C.Vars, + WithLayout: true, + Functions: C.Functions, + }) + + return nil +} diff --git a/core/internal/responses/responses.go b/core/internal/responses/responses.go new file mode 100644 index 0000000..1c0e193 --- /dev/null +++ b/core/internal/responses/responses.go @@ -0,0 +1,66 @@ +package responses + +import ( + "net/http" + + "github.com/kirsle/blog/core/internal/log" + "github.com/kirsle/blog/core/internal/render" +) + +// Redirect sends an HTTP redirect response. +func Redirect(w http.ResponseWriter, location string) { + w.Header().Set("Location", location) + w.WriteHeader(http.StatusFound) +} + +// NotFound sends a 404 response. +func NotFound(w http.ResponseWriter, r *http.Request, message ...string) { + if len(message) == 0 { + message = []string{"The page you were looking for was not found."} + } + + w.WriteHeader(http.StatusNotFound) + err := render.RenderTemplate(w, r, ".errors/404", &render.Vars{ + Message: message[0], + }) + if err != nil { + log.Error(err.Error()) + w.Write([]byte("Unrecoverable template error for NotFound()")) + } +} + +// Forbidden sends an HTTP 403 Forbidden response. +func Forbidden(w http.ResponseWriter, r *http.Request, message ...string) { + w.WriteHeader(http.StatusForbidden) + err := render.RenderTemplate(w, r, ".errors/403", &render.Vars{ + Message: message[0], + }) + if err != nil { + log.Error(err.Error()) + w.Write([]byte("Unrecoverable template error for Forbidden()")) + } +} + +// Error sends an HTTP 500 Internal Server Error response. +func Error(w http.ResponseWriter, r *http.Request, message ...string) { + w.WriteHeader(http.StatusInternalServerError) + err := render.RenderTemplate(w, r, ".errors/500", &render.Vars{ + Message: message[0], + }) + if err != nil { + log.Error(err.Error()) + w.Write([]byte("Unrecoverable template error for Error()")) + } +} + +// BadRequest sends an HTTP 400 Bad Request. +func BadRequest(w http.ResponseWriter, r *http.Request, message ...string) { + w.WriteHeader(http.StatusBadRequest) + err := render.RenderTemplate(w, r, ".errors/400", &render.Vars{ + Message: message[0], + }) + if err != nil { + log.Error(err.Error()) + w.Write([]byte("Unrecoverable template error for BadRequest()")) + } +} diff --git a/core/log.go b/core/log.go deleted file mode 100644 index 8189ada..0000000 --- a/core/log.go +++ /dev/null @@ -1,13 +0,0 @@ -package core - -import "github.com/kirsle/golog" - -var log *golog.Logger - -func init() { - log = golog.GetLogger("blog") - log.Configure(&golog.Config{ - Colors: golog.ExtendedColor, - Theme: golog.DarkTheme, - }) -} diff --git a/core/mail.go b/core/mail.go index 6c0f750..f6f5552 100644 --- a/core/mail.go +++ b/core/mail.go @@ -8,9 +8,11 @@ import ( "net/url" "strings" + "github.com/kirsle/blog/core/internal/log" "github.com/kirsle/blog/core/internal/markdown" "github.com/kirsle/blog/core/internal/models/comments" "github.com/kirsle/blog/core/internal/models/settings" + "github.com/kirsle/blog/core/internal/render" "github.com/microcosm-cc/bluemonday" gomail "gopkg.in/gomail.v2" ) @@ -36,7 +38,7 @@ func (b *Blog) SendEmail(email Email) { } // Resolve the template. - tmpl, err := b.ResolvePath(email.Template) + tmpl, err := render.ResolvePath(email.Template) if err != nil { log.Error("SendEmail: %s", err.Error()) return diff --git a/core/middleware.go b/core/middleware.go index db552c6..5c86749 100644 --- a/core/middleware.go +++ b/core/middleware.go @@ -8,6 +8,7 @@ import ( "github.com/google/uuid" "github.com/gorilla/sessions" + "github.com/kirsle/blog/core/internal/log" "github.com/kirsle/blog/core/internal/models/users" ) @@ -38,6 +39,10 @@ func (b *Blog) SessionLoader(w http.ResponseWriter, r *http.Request, next http.H // Session returns the current request's session. func (b *Blog) Session(r *http.Request) *sessions.Session { + if r == nil { + panic("Session(*http.Request) with a nil argument!?") + } + ctx := r.Context() if session, ok := ctx.Value(sessionKey).(*sessions.Session); ok { return session diff --git a/core/pages.go b/core/pages.go index 6024ca0..1b7444b 100644 --- a/core/pages.go +++ b/core/pages.go @@ -1,15 +1,13 @@ package core import ( - "errors" "html/template" "io/ioutil" "net/http" - "os" - "path/filepath" "strings" "github.com/kirsle/blog/core/internal/markdown" + "github.com/kirsle/blog/core/internal/render" ) // PageHandler is the catch-all route handler, for serving static web pages. @@ -30,7 +28,7 @@ func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) { } // Search for a file that matches their URL. - filepath, err := b.ResolvePath(path) + filepath, err := render.ResolvePath(path) if err != nil { // See if it resolves as a blog entry. err = b.viewPost(w, r, strings.TrimLeft(path, "/")) @@ -42,7 +40,7 @@ func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) { // Is it a template file? if strings.HasSuffix(filepath.URI, ".gohtml") { - b.RenderTemplate(w, r, filepath.URI, nil) + b.RenderTemplate(w, r, filepath.URI, render.Vars{}) return } @@ -69,79 +67,3 @@ func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, filepath.Absolute) } - -// Filepath represents a file discovered in the document roots, and maintains -// both its relative and absolute components. -type Filepath struct { - // Canonicalized URI version of the file resolved on disk, - // possible with a file extension injected. - // (i.e. "/about" -> "about.html") - URI string - Basename string - Relative string // Relative path including document root (i.e. "root/about.html") - Absolute string // Absolute path on disk (i.e. "/opt/blog/root/about.html") -} - -func (f Filepath) String() string { - return f.Relative -} - -// ResolvePath matches a filesystem path to a relative request URI. -// -// This checks the UserRoot first and then the DocumentRoot. This way the user -// may override templates from the core app's document root. -func (b *Blog) ResolvePath(path string) (Filepath, error) { - // Strip leading slashes. - if path[0] == '/' { - path = strings.TrimPrefix(path, "/") - } - - // If you need to debug this function, edit this block. - debug := func(tmpl string, args ...interface{}) { - if false { - log.Debug(tmpl, args...) - } - } - - debug("Resolving filepath for URI: %s", path) - for _, root := range []string{b.UserRoot, b.DocumentRoot} { - if len(root) == 0 { - continue - } - - // Resolve the file path. - relPath := filepath.Join(root, path) - absPath, err := filepath.Abs(relPath) - basename := filepath.Base(relPath) - if err != nil { - log.Error("%v", err) - } - - debug("Expected filepath: %s", absPath) - - // Found an exact hit? - if stat, err := os.Stat(absPath); !os.IsNotExist(err) && !stat.IsDir() { - debug("Exact filepath found: %s", absPath) - return Filepath{path, basename, relPath, absPath}, nil - } - - // Try some supported suffixes. - suffixes := []string{ - ".gohtml", - ".html", - "/index.gohtml", - "/index.html", - ".md", - "/index.md", - } - for _, suffix := range suffixes { - test := absPath + suffix - if stat, err := os.Stat(test); !os.IsNotExist(err) && !stat.IsDir() { - debug("Filepath found via suffix %s: %s", suffix, test) - return Filepath{path + suffix, basename + suffix, relPath + suffix, test}, nil - } - } - } - - return Filepath{}, errors.New("not found") -} diff --git a/core/responses.go b/core/responses.go index 92e0ebb..02f7720 100644 --- a/core/responses.go +++ b/core/responses.go @@ -3,6 +3,9 @@ package core import ( "fmt" "net/http" + + "github.com/kirsle/blog/core/internal/log" + "github.com/kirsle/blog/core/internal/render" ) // Flash adds a flash message to the user's session. @@ -37,7 +40,7 @@ func (b *Blog) NotFound(w http.ResponseWriter, r *http.Request, message ...strin } w.WriteHeader(http.StatusNotFound) - err := b.RenderTemplate(w, r, ".errors/404", &Vars{ + err := b.RenderTemplate(w, r, ".errors/404", render.Vars{ Message: message[0], }) if err != nil { @@ -49,7 +52,7 @@ func (b *Blog) NotFound(w http.ResponseWriter, r *http.Request, message ...strin // Forbidden sends an HTTP 403 Forbidden response. func (b *Blog) Forbidden(w http.ResponseWriter, r *http.Request, message ...string) { w.WriteHeader(http.StatusForbidden) - err := b.RenderTemplate(w, r, ".errors/403", &Vars{ + err := b.RenderTemplate(w, r, ".errors/403", render.Vars{ Message: message[0], }) if err != nil { @@ -61,7 +64,7 @@ func (b *Blog) Forbidden(w http.ResponseWriter, r *http.Request, message ...stri // Error sends an HTTP 500 Internal Server Error response. func (b *Blog) Error(w http.ResponseWriter, r *http.Request, message ...string) { w.WriteHeader(http.StatusInternalServerError) - err := b.RenderTemplate(w, r, ".errors/500", &Vars{ + err := b.RenderTemplate(w, r, ".errors/500", render.Vars{ Message: message[0], }) if err != nil { @@ -73,7 +76,7 @@ func (b *Blog) Error(w http.ResponseWriter, r *http.Request, message ...string) // BadRequest sends an HTTP 400 Bad Request. func (b *Blog) BadRequest(w http.ResponseWriter, r *http.Request, message ...string) { w.WriteHeader(http.StatusBadRequest) - err := b.RenderTemplate(w, r, ".errors/400", &Vars{ + err := b.RenderTemplate(w, r, ".errors/400", render.Vars{ Message: message[0], }) if err != nil { diff --git a/core/templates.go b/core/templates.go index e79c6e5..3c36eea 100644 --- a/core/templates.go +++ b/core/templates.go @@ -10,6 +10,7 @@ import ( "github.com/kirsle/blog/core/internal/forms" "github.com/kirsle/blog/core/internal/models/settings" "github.com/kirsle/blog/core/internal/models/users" + "github.com/kirsle/blog/core/internal/render" ) // Vars is an interface to implement by the templates to pass their own custom @@ -41,20 +42,20 @@ type Vars struct { // NewVars initializes a Vars struct with the custom Data map initialized. // You may pass in an initial value for this map if you want. -func NewVars(data ...map[interface{}]interface{}) *Vars { +func NewVars(data ...map[interface{}]interface{}) render.Vars { var value map[interface{}]interface{} if len(data) > 0 { value = data[0] } else { value = make(map[interface{}]interface{}) } - return &Vars{ + return render.Vars{ Data: value, } } // LoadDefaults combines template variables with default, globally available vars. -func (v *Vars) LoadDefaults(b *Blog, r *http.Request) { +func (b *Blog) LoadDefaults(v render.Vars, r *http.Request) render.Vars { // Get the site settings. s, err := settings.Load() if err != nil { @@ -72,12 +73,9 @@ func (v *Vars) LoadDefaults(b *Blog, r *http.Request) { user, err := b.CurrentUser(r) v.CurrentUser = user v.LoggedIn = err == nil -} -// // TemplateVars is an interface that describes the template variable struct. -// type TemplateVars interface { -// LoadDefaults(*Blog, *http.Request) -// } + return v +} // RenderPartialTemplate handles rendering a Go template to a writer, without // doing anything extra to the vars or dealing with net/http. This is ideal for @@ -86,78 +84,14 @@ func (v *Vars) LoadDefaults(b *Blog, r *http.Request) { // This will wrap the template in `.layout.gohtml` by default. To render just // a bare template on its own, i.e. for partial templates, create a Vars struct // with `Vars{NoIndex: true}` -func (b *Blog) RenderPartialTemplate(w io.Writer, path string, v interface{}, withLayout bool, functions map[string]interface{}) error { - var ( - layout Filepath - templateName string - err error - ) - - // Find the file path to the template. - filepath, err := b.ResolvePath(path) - if err != nil { - log.Error("RenderTemplate(%s): file not found", path) - return err - } - - // Get the layout template. - if withLayout { - templateName = "layout" - layout, err = b.ResolvePath(".layout") - if err != nil { - log.Error("RenderTemplate(%s): layout template not found", path) - return err - } - } else { - templateName = filepath.Basename - } - - // The comment entry partial. - commentEntry, err := b.ResolvePath("comments/entry.partial") - if err != nil { - log.Error("RenderTemplate(%s): comments/entry.partial not found") - return err - } - - // Template functions. - funcmap := template.FuncMap{ - "StringsJoin": strings.Join, - "Now": time.Now, - "RenderIndex": b.RenderIndex, - "RenderPost": b.RenderPost, - "RenderTags": b.RenderTags, - "TemplateName": func() string { - return filepath.URI - }, - } - if functions != nil { - for name, fn := range functions { - funcmap[name] = fn - } - } - - // Useful template functions. - t := template.New(filepath.Absolute).Funcs(funcmap) - - // Parse the template files. The layout comes first because it's the wrapper - // and allows the filepath template to set the page title. - var templates []string - if withLayout { - templates = append(templates, layout.Absolute) - } - t, err = t.ParseFiles(append(templates, commentEntry.Absolute, filepath.Absolute)...) - if err != nil { - log.Error(err.Error()) - return err - } - - err = t.ExecuteTemplate(w, templateName, v) - if err != nil { - log.Error("Template parsing error: %s", err) - return err - } - - return nil +func (b *Blog) RenderPartialTemplate(w io.Writer, r *http.Request, path string, v render.Vars, withLayout bool, functions map[string]interface{}) error { + v = b.LoadDefaults(v, r) + return render.PartialTemplate(w, path, render.Config{ + Request: r, + Vars: &v, + WithLayout: withLayout, + Functions: b.TemplateFuncs(nil, nil, functions), + }) } // RenderTemplate responds with an HTML template. @@ -166,12 +100,13 @@ func (b *Blog) RenderPartialTemplate(w io.Writer, path string, v interface{}, wi // website title and user login status), the user's session may be updated with // new CSRF token, and other such things. If you just want to render a template // without all that nonsense, use RenderPartialTemplate. -func (b *Blog) RenderTemplate(w http.ResponseWriter, r *http.Request, path string, vars *Vars) error { - // Inject globally available variables. - if vars == nil { - vars = &Vars{} +func (b *Blog) RenderTemplate(w http.ResponseWriter, r *http.Request, path string, vars render.Vars) error { + if r == nil { + panic("core.RenderTemplate(): the *http.Request is nil!?") } - vars.LoadDefaults(b, r) + + // Inject globally available variables. + vars = b.LoadDefaults(vars, r) // Add any flashed messages from the endpoint controllers. session := b.Session(r) @@ -187,14 +122,34 @@ func (b *Blog) RenderTemplate(w http.ResponseWriter, r *http.Request, path strin vars.CSRF = b.GenerateCSRFToken(w, r, session) vars.Editable = !strings.HasPrefix(path, "admin/") - w.Header().Set("Content-Type", "text/html; encoding=UTF-8") - b.RenderPartialTemplate(w, path, vars, true, template.FuncMap{ + return render.Template(w, path, render.Config{ + Request: r, + Vars: &vars, + Functions: b.TemplateFuncs(w, r, nil), + }) +} + +// TemplateFuncs returns the common template function map. +func (b *Blog) TemplateFuncs(w http.ResponseWriter, r *http.Request, inject map[string]interface{}) map[string]interface{} { + fn := map[string]interface{}{ + "RenderIndex": b.RenderIndex, + "RenderPost": b.RenderPost, + "RenderTags": b.RenderTags, "RenderComments": func(subject string, ids ...string) template.HTML { + if w == nil || r == nil { + return template.HTML("[RenderComments Error: need both http.ResponseWriter and http.Request]") + } + session := b.Session(r) csrf := b.GenerateCSRFToken(w, r, session) return b.RenderComments(session, csrf, r.URL.Path, subject, ids...) }, - }) + } - return nil + if inject != nil { + for k, v := range inject { + fn[k] = v + } + } + return fn } diff --git a/root/blog/entry.gohtml b/root/blog/entry.gohtml index 46a094a..35364ed 100644 --- a/root/blog/entry.gohtml +++ b/root/blog/entry.gohtml @@ -2,7 +2,7 @@ {{ define "content" }} {{ $p := .Data.Post }} -{{ RenderPost $p false 0 }} +{{ RenderPost .Request $p false 0 }} {{ if and .LoggedIn .CurrentUser.Admin }} diff --git a/root/blog/entry.partial.gohtml b/root/blog/entry.partial.gohtml index 65a4715..8aadab7 100644 --- a/root/blog/entry.partial.gohtml +++ b/root/blog/entry.partial.gohtml @@ -1,7 +1,8 @@ -{{ $a := .Author }} -{{ $p := .Post }} +{{ $a := .Data.Author }} +{{ $p := .Data.Post }} +{{ $d := .Data }} -{{ if .IndexView }} +{{ if $d.IndexView }} {{ $p.Title }} {{ else }}

{{ $p.Title }}

@@ -27,9 +28,9 @@
- {{ .Rendered }} + {{ $d.Rendered }} - {{ if .Snipped }} + {{ if $d.Snipped }}

Read more...

@@ -45,9 +46,9 @@ {{ end }} -{{ if .IndexView }} +{{ if $d.IndexView }} - {{ .NumComments }} comment{{ if ne .NumComments 1 }}s{{ end }} + {{ $d.NumComments }} comment{{ if ne $d.NumComments 1 }}s{{ end }} | Permalink diff --git a/root/blog/index.partial.gohtml b/root/blog/index.partial.gohtml index ecf161b..0782916 100644 --- a/root/blog/index.partial.gohtml +++ b/root/blog/index.partial.gohtml @@ -1,18 +1,23 @@ +{{ $PreviousPage := .Data.PreviousPage }} +{{ $NextPage := .Data.NextPage }} +{{ $View := .Data.View }}
    - {{ if .PreviousPage }} -
  • Earlier
  • + {{ if $PreviousPage }} +
  • Earlier
  • {{ end }} - {{ if .NextPage }} -
  • Older
  • + {{ if $NextPage }} +
  • Older
  • {{ end }}
-{{ range .View }} +{{ $r := .Request }} + +{{ range $View }} {{ $p := .Post }} - {{ RenderPost $p true .NumComments }} + {{ RenderPost $r $p true .NumComments }} {{ if and $.LoggedIn $.CurrentUser.Admin }}
@@ -31,11 +36,11 @@
    - {{ if .PreviousPage }} -
  • Earlier
  • + {{ if $PreviousPage }} +
  • Earlier
  • {{ end }} - {{ if .NextPage }} -
  • Older
  • + {{ if $NextPage }} +
  • Older
  • {{ end }}
diff --git a/root/blog/tags.partial.gohtml b/root/blog/tags.partial.gohtml index ba0c8fc..5c7fecf 100644 --- a/root/blog/tags.partial.gohtml +++ b/root/blog/tags.partial.gohtml @@ -1,16 +1,16 @@ -{{ if .IndexView }} +{{ if .Data.IndexView }} Sorted by most frequently used: {{ else }} diff --git a/root/bluez/theme.css b/root/bluez/theme.css index 97e8237..3660175 100644 --- a/root/bluez/theme.css +++ b/root/bluez/theme.css @@ -24,6 +24,18 @@ h6, .h6 { color: #333; } +blockquote { + border-left: 2px solid #FF0000; + padding: 0 10px; + margin: 4px 6px; +} +blockquote blockquote { + border-left-color: #FF9900; +} +blockquote blockquote blockquote { + border-left-color: #CCCC00; +} + /* * Bootstrap tweaks and overrides */ diff --git a/root/comments/entry.partial.gohtml b/root/comments/entry.partial.gohtml index 3521194..05cf2da 100644 --- a/root/comments/entry.partial.gohtml +++ b/root/comments/entry.partial.gohtml @@ -101,7 +101,7 @@ class="form-control">{{ .Body }} You may format your message using - Markdown + GitHub Flavored Markdown syntax.
diff --git a/root/index.gohtml b/root/index.gohtml index fa80aa6..480d97f 100644 --- a/root/index.gohtml +++ b/root/index.gohtml @@ -8,4 +8,5 @@

{{ RenderIndex .Request "" "" }} + {{ end }} diff --git a/root/markdown.gohtml b/root/markdown.gohtml new file mode 100644 index 0000000..20be153 --- /dev/null +++ b/root/markdown.gohtml @@ -0,0 +1,712 @@ +{{ define "title" }}Markdown Cheatsheet{{ end }} +{{ define "content" }} + +

Markdown Cheatsheet

+ +

This is a simple reference sheet for Markdown syntax. The de facto place to find Markdown syntax is at +https://daringfireball.net/projects/markdown/syntax +but the examples here are more nicely presented.

+ +

This page just serves as a cheat sheet for Markdown syntax and their results. For descriptive paragraphs +explaining the syntax, see the page linked above.

+ +

This website uses GitHub Flavored Markdown, an +extension of Markdown that supports fenced code blocks, tables, and other features.

+ + + +

Block Elements

+ + +

Paragraphs and Line Breaks

+ +

A paragraph is defined as a group of lines of text separated from other groups +by at least one blank line. A hard return inside a paragraph doesn't get rendered +in the output.

+ +

Headers

+ +

There are two methods to declare a header in Markdown: "underline" it by +writing === or --- on the line directly below the +heading (for <h1> and <h2>, respectively), +or by prefixing the heading with # symbols. Usually the latter +option is the easiest, and you can get more levels of headers this way.

+ + + + + + + + + + + + + + + + + + +
Markdown SyntaxOutput
+ + This is an H1
+ =============

+ + This is an H2
+ ------------- +
+
+

This is an H1

+

This is an H2

+
+ + # This is an H1
+ ## This is an H2
+ #### This is an H4 +
+
+

This is an H1

+

This is an H2

+

This is an H4

+
+ +

Blockquotes

+ +

Prefix a line of text with > to "quote" it -- like in +"e-mail syntax."

+ +

You may have multiple layers of quotes by using multiple > +symbols.

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Markdown SyntaxOutput
+ + > This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
+ > consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
+ > Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.
+ >
+ > Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
+ > id sem consectetuer libero luctus adipiscing. +
+
+
+

This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet, + consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. + Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.

+ +

Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse + id sem consectetuer libero luctus adipiscing.

+
+
+ + > This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,
+ consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.
+ Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.

+ + > Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
+ id sem consectetuer libero luctus adipiscing. +
+
+
+

This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet, + consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus. + Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.

+ +

Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse + id sem consectetuer libero luctus adipiscing.

+
+
+ + > This is the first level of quoting.
+ >
+ >> This is nested blockquote.
+ >>> A third level.
+ >
+ > Back to the first level. +
+
+
+ This is the first level of quoting. +
+ This is nested blockquote. +
+ A third level. +
+
+ Back to the first level. +
+
+ + > ## This is a header.
+ >
+ > 1. This is the first list item.
+ > 2. This is the second list item.
+ >
+ >Here's some example code:
+ >
+ >    return shell_exec("echo $input | $markdown_script"); +
+
+
+

This is a header.

+
    +
  1. This is the first list item.
  2. +
  3. This is the second list item.
  4. +
+ Here's some example code: +
return shell_exec("echo $input | $markdown_script");
+
+
+ + +

Lists

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Markdown SyntaxOutput
+ + * Red
+ * Green
+ * Blue +
+
+
    +
  • Red
  • +
  • Green
  • +
  • Blue
  • +
+
+ + + Red
+ + Green
+ + Blue +
+
+
    +
  • Red
  • +
  • Green
  • +
  • Blue
  • +
+
+ + - Red
+ - Green
+ - Blue +
+
+
    +
  • Red
  • +
  • Green
  • +
  • Blue
  • +
+
+ + 1. Bird
+ 2. McHale
+ 3. Parish +
+
+
    +
  1. Bird
  2. +
  3. McHale
  4. +
  5. Parish
  6. +
+
+ + 1.  This is a list item with two paragraphs. Lorem ipsum dolor
+     sit amet, consectetuer adipiscing elit. Aliquam hendrerit
+     mi posuere lectus.

+ +     Vestibulum enim wisi, viverra nec, fringilla in, laoreet
+     vitae, risus. Donec sit amet nisl. Aliquam semper ipsum
+     sit amet velit.

+ + 2.  Suspendisse id sem consectetuer libero luctus adipiscing. + +

+
    +
  1. This is a list item with two paragraphs. Lorem ipsum dolor + sit amet, consectetuer adipiscing elit. Aliquam hendrerit + mi posuere lectus.

    + + Vestibulum enim wisi, viverra nec, fringilla in, laoreet + vitae, risus. Donec sit amet nisl. Aliquam semper ipsum + sit amet velit.

  2. + +
  3. Suspendisse id sem consectetuer libero luctus adipiscing.
  4. +
+
+ + +

Code Blocks

+ +The typical Markdown way to write a code block is to indent each line of a paragraph with at +least 4 spaces or 1 tab character. The Rophako CMS also uses GitHub-style code blocks, where +you can use three backticks before and after the code block and then you don't need to indent +each line of the code (makes copying/pasting easier!)

+ +Like GitHub-flavored Markdown, with a fenced code block you can also specify a programming +language to get syntax highlighting for the code.

+ + + + + + + + + + + + + + + + + + + + + + +
Markdown SyntaxOutput
+ + This is a normal paragraph.

+ +     This is a code block. + +

+ This is a normal paragraph.

+ +

This is a code block
+
+ + This is a normal paragraph.

+ + ```
+ This is a GitHub style "fenced code block".
+ ``` +
+

+ This is a normal paragraph.

+ +

This is a GitHub style "fenced code block".
+
+ + ```javascript
+ document.writeln("Hello world.");
+ ``` +
+
+
document.writeln("Hello world.");
+
+ + +

Horizontal Rules

+ + + + + + + + + + + + + + +
Markdown SyntaxOutput
+ + * * *

+ ***

+ *****

+ - - -

+ --------------------------- + +

+

+


+


+


+


+
+ + + +

Span Elements

+ + +

Links

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Markdown SyntaxOutput
+ + This is [an example](http://example.com/ "Title") inline link.

+ [This link](http://example.net/) has no title attribute. + +

+ This is an example inline link.

+ This link has no title attribute. +

+ + See my [About](/about) page for details. + + + See my About page for details. +
+ + This is [an example][id] reference-style link.

+ [id]: http://example.com/ "Optional Title Here" + +

+ This is an example reference-style link. +
+ + This is an example of an implicit reference-style link: search [Google][] for more.

+ [Google]: http://google.com/ + +

+ This is an example of an implicit reference-style link: search Google for more. +
+ + I get 10 times more traffic from [Google] [1] than from
+ [Yahoo] [2] or [Bing] [3].

+ + [1]: http://google.com/ "Google"
+ [2]: http://search.yahoo.com/ "Yahoo Search"
+ [3]: http://bing.com/ "Bing" +
+

+ I get 10 times more traffic from Google than from + Yahoo or + Bing. +
+ + +

Emphasis

+ + + + + + + + + + + + + + + + + + + + + + +
Markdown SyntaxOutput
+ + *single asterisks*

+ _single underscores_

+ **double asterisks**

+ __double underscores__ + +

+ single asterisks

+ single underscores

+ double asterisks

+ double underscores +

+ + un*frigging*believable + + + unfriggingbelievable +
+ + \*this text is surrounded by literal asterisks\* + + + *this text is surrounded by literal asterisks* +
+ + +

Code

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Markdown SyntaxOutput
+ + Use the `printf()` function. + + + Use the printf() function. +
+ + ``There is a literal backtick (`) here.`` + + + There is a literal backtick (`) here. +
+ + A single backtick in a code span: `` ` ``

+ A backtick-delimited string in a code span: `` `foo` `` + +

+ A single backtick in a code span: `

+ A backtick-delimited string in a code span: `foo` +

+ Please don't use any `<blink>` tags. + + Please don't use any <blink> tags. +
+ `&#8212;` is the decimal-encoded equivalent of `&mdash;`. + + &#8212; is the decimal-encoded equivalent of + &mdash;. +
+ + +

Images

+ + + + + + + + + + + + + + + + + + + + + + +
Markdown SyntaxOutput
+ ![Alt text](/static/avatars/default.png) + + Alt text +
+ ![Alt text](/static/avatars/default.png "Optional title") + + Alt text +
+ + ![Alt text][id]

+ [id]: /static/avatars/default.png "Optional title attribute" + +

+ Alt text +
+ + +

Miscellaneous

+ + +

Automatic Links

+ +E-mail links get automatically converted into a random mess of HTML attributes to +attempt to thwart e-mail harvesting spam bots.

+ + + + + + + + + + + + + + + + + + +
Markdown SyntaxOutput
+ <http://example.com/> + + http://example.com/ +
+ <address@example.com> + + address@example.com

+ + (Source: <a href="&#109;&#97;&#105;&#108;&#116;&#111;&#58; &#97;&#100;&#100;&#114;&#101;&#115;&#115;&#64; &#101;&#120;&#97;&#109;&#112;&#108; &#101;&#46;&#99;&#111;&#109;">&#97; &#100;&#100;&#114;&#101;&#115;&#115; &#64;&#101;&#120;&#97;&#109;&#112; &#108;&#101;&#46;&#99;&#111; &#109;</a>) +

+ + +

Backslash Escapes

+ +Use backslash characters to escape any other special characters in the Markdown syntax. For example, +\* to insert a literal asterisk so that it doesn't get mistaken for e.g. emphasized text, +a list item, etc.

+ +Markdown provides backslash escapes for the following characters:

+ +

\   backslash
+`   backtick
+*   asterisk
+_   underscore
+{}  curly braces
+[]  square brackets
+()  parenthesis
+#   hash mark
++   plus sign
+-   minus sign (hyphen)
+.   dot
+!   exclamation mark
+ +{{ end }}