Move template rendering into sub-package

pull/4/head
Noah 2018-02-10 10:08:45 -08:00
parent aabcf59181
commit e393b1880f
26 changed files with 1271 additions and 301 deletions

View File

@ -51,5 +51,5 @@ func main() {
jsondb.SetDebug(true)
}
app.ListenAndServe(fAddress)
app.Run(fAddress)
}

View File

@ -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

View File

@ -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"
)

View File

@ -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()))
}

View File

@ -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]")

View File

@ -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.

View File

@ -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{},
}

30
core/internal/log/log.go Normal file
View File

@ -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...)
}

View File

@ -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
}

View File

@ -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")
}

View File

@ -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
}

View File

@ -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()"))
}
}

View File

@ -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,
})
}

View File

@ -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

View File

@ -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

View File

@ -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")
}

View File

@ -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 {

View File

@ -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
}

View File

@ -2,7 +2,7 @@
{{ define "content" }}
{{ $p := .Data.Post }}
{{ RenderPost $p false 0 }}
{{ RenderPost .Request $p false 0 }}
{{ if and .LoggedIn .CurrentUser.Admin }}
<small>

View File

@ -1,7 +1,8 @@
{{ $a := .Author }}
{{ $p := .Post }}
{{ $a := .Data.Author }}
{{ $p := .Data.Post }}
{{ $d := .Data }}
{{ if .IndexView }}
{{ if $d.IndexView }}
<a class="h1 blog-title" href="/{{ $p.Fragment }}">{{ $p.Title }}</a>
{{ else }}
<h1 class="blog-title">{{ $p.Title }}</h1>
@ -27,9 +28,9 @@
</div>
<div class="markdown mb-4">
{{ .Rendered }}
{{ $d.Rendered }}
{{ if .Snipped }}
{{ if $d.Snipped }}
<p>
<a href="/{{ $p.Fragment }}#snip">Read more...</a>
</p>
@ -45,9 +46,9 @@
</ul>
{{ end }}
{{ if .IndexView }}
{{ if $d.IndexView }}
<em class="text-muted">
<a href="/{{ $p.Fragment }}#comments">{{ .NumComments }} comment{{ if ne .NumComments 1 }}s{{ end }}</a>
<a href="/{{ $p.Fragment }}#comments">{{ $d.NumComments }} comment{{ if ne $d.NumComments 1 }}s{{ end }}</a>
|
<a href="/{{ $p.Fragment }}">Permalink</a>
</em>

View File

@ -1,18 +1,23 @@
{{ $PreviousPage := .Data.PreviousPage }}
{{ $NextPage := .Data.NextPage }}
{{ $View := .Data.View }}
<div class="row">
<div class="col text-right">
<ul class="list-inline">
{{ if .PreviousPage }}
<li class="list-inline-item"><a href="?page={{ .PreviousPage }}">Earlier</a></li>
{{ if $PreviousPage }}
<li class="list-inline-item"><a href="?page={{ $PreviousPage }}">Earlier</a></li>
{{ end }}
{{ if .NextPage }}
<li class="list-inline-item"><a href="?page={{ .NextPage }}">Older</a></li>
{{ if $NextPage }}
<li class="list-inline-item"><a href="?page={{ $NextPage }}">Older</a></li>
{{ end }}
</div>
</div>
{{ range .View }}
{{ $r := .Request }}
{{ range $View }}
{{ $p := .Post }}
{{ RenderPost $p true .NumComments }}
{{ RenderPost $r $p true .NumComments }}
{{ if and $.LoggedIn $.CurrentUser.Admin }}
<div class="mb-4">
@ -31,11 +36,11 @@
<div class="row">
<div class="col text-right">
<ul class="list-inline">
{{ if .PreviousPage }}
<li class="list-inline-item"><a href="?page={{ .PreviousPage }}">Earlier</a></li>
{{ if $PreviousPage }}
<li class="list-inline-item"><a href="?page={{ $PreviousPage }}">Earlier</a></li>
{{ end }}
{{ if .NextPage }}
<li class="list-inline-item"><a href="?page={{ .NextPage }}">Older</a></li>
{{ if $NextPage }}
<li class="list-inline-item"><a href="?page={{ $NextPage }}">Older</a></li>
{{ end }}
</div>
</div>

View File

@ -1,16 +1,16 @@
{{ if .IndexView }}
{{ if .Data.IndexView }}
Sorted by most frequently used:
<ul>
{{ range .Tags }}
<li><a href="/tagged/{{ .Name }}">{{ .Name }}</a> ({{ .Count }})</li>
{{ range .Data.Tags }}
<li><a href="/tagged/{{ or .Name "Uncategorized" }}">{{ or .Name "Uncategorized" }}</a> ({{ .Count }})</li>
{{ end }}
</ul>
{{ else }}
<ul>
{{ range $i, $t := .Tags }}
{{ range $i, $t := .Data.Tags }}
{{ if le $i 20 }}
<li><a href="/tagged/{{ .Name }}">{{ .Name }}</a> ({{ .Count }})</li>
<li><a href="/tagged/{{ or .Name "Uncategorized" }}">{{ or .Name "Uncategorized" }}</a> ({{ .Count }})</li>
{{ end }}
{{ end }}
</ul>

View File

@ -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
*/

View File

@ -101,7 +101,7 @@
class="form-control">{{ .Body }}</textarea>
<small id="bodyHelp" class="form-text text-muted">
You may format your message using
<a href="https://daringfireball.net/projects/markdown/syntax">Markdown</a>
<a href="/markdown" target="_blank">GitHub Flavored Markdown</a>
syntax.
</small>
</div>

View File

@ -8,4 +8,5 @@
</p>
{{ RenderIndex .Request "" "" }}
{{ end }}

712
root/markdown.gohtml Normal file
View File

@ -0,0 +1,712 @@
{{ define "title" }}Markdown Cheatsheet{{ end }}
{{ define "content" }}
<h1>Markdown Cheatsheet</h1>
<p>This is a simple reference sheet for Markdown syntax. The de facto place to find Markdown syntax is at
<a href="https://daringfireball.net/projects/markdown/syntax">https://daringfireball.net/projects/markdown/syntax</a>
but the examples here are more nicely presented.</p>
<p>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.</p>
<p>This website uses <a href="https://github.github.com/gfm/">GitHub Flavored Markdown</a>, an
extension of Markdown that supports fenced code blocks, tables, and other features.</p>
<ul>
<li>
<a href="#block">Block Elements</a>
<ul>
<li><a href="#pbr">Paragraphs and Line Breaks</a></li>
<li><a href="#h1">Headers</a></li>
<li><a href="#bq">Blockquotes</a></li>
<li><a href="#ul">Lists</a></li>
<li><a href="#pre">Code Blocks</a></li>
<li><a href="#hr">Horizontal Rules</a></li>
</ul>
</li>
<li>
<a href="#span">Span Elements</a>
<ul>
<li><a href="#a">Links</a></li>
<li><a href="#em">Emphasis</a></li>
<li><a href="#code">Code</a></li>
<li><a href="#img">Images</a></li>
</ul>
</li>
<li>
<a href="#misc">Miscellaneous</a>
<ul>
<li><a href="#autolink">Automatic Links</a></li>
<li><a href="#escape">Backslash Escapes</a></li>
</ul>
</li>
</ul>
<h1 id="block">Block Elements</h1>
<a name="pbr"></a>
<h2>Paragraphs and Line Breaks</h2>
<p>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.</p>
<h2 id="h1">Headers</h2>
<p>There are two methods to declare a header in Markdown: "underline" it by
writing <code>===</code> or <code>---</code> on the line directly below the
heading (for <code>&lt;h1&gt;</code> and <code>&lt;h2&gt;</code>, respectively),
or by prefixing the heading with <code>#</code> symbols. Usually the latter
option is the easiest, and you can get more levels of headers this way.</p>
<table width="100%" class="table">
<thead>
<tr>
<th width="50%">Markdown Syntax</th>
<th width="50%">Output</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>
This is an H1<br>
=============<br><br>
This is an H2<br>
-------------
</code>
</td>
<td>
<h1>This is an H1</h1>
<h2>This is an H2</h2>
</td>
</tr>
<tr>
<td>
<code>
# This is an H1<br>
## This is an H2<br>
#### This is an H4
</code>
</td>
<td>
<h1>This is an H1</h1>
<h2>This is an H2</h2>
<h4>This is an H4</h4>
</td>
</tr>
</tbody>
</table>
<h3 id="bq">Blockquotes</h3>
<p>Prefix a line of text with <code>&gt;</code> to "quote" it -- like in
"e-mail syntax."</p>
<p>You may have multiple layers of quotes by using multiple <code>&gt;</code>
symbols.</p>
<table width="100%" class="table">
<thead>
<tr>
<th width="50%">Markdown Syntax</th>
<th width="50%">Output</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>
&gt; This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,<br>
&gt; consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.<br>
&gt; Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.<br>
&gt;<br>
&gt; Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse<br>
&gt; id sem consectetuer libero luctus adipiscing.
</code>
</td>
<td>
<blockquote>
<p>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.</p>
<p>Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
id sem consectetuer libero luctus adipiscing.</p>
</blockquote>
</td>
</tr>
<tr>
<td>
<code>
&gt; This is a blockquote with two paragraphs. Lorem ipsum dolor sit amet,<br>
consectetuer adipiscing elit. Aliquam hendrerit mi posuere lectus.<br>
Vestibulum enim wisi, viverra nec, fringilla in, laoreet vitae, risus.<br><br>
&gt; Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse<br>
id sem consectetuer libero luctus adipiscing.
</code>
</td>
<td>
<blockquote>
<p>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.<p>
<p>Donec sit amet nisl. Aliquam semper ipsum sit amet velit. Suspendisse
id sem consectetuer libero luctus adipiscing.</p>
</blockquote>
</td>
</tr>
<tr>
<td>
<code>
&gt; This is the first level of quoting.<br>
&gt;<br>
&gt;&gt; This is nested blockquote.<br>
&gt;&gt;&gt; A third level.<br>
&gt;<br>
&gt; Back to the first level.
</code>
</td>
<td>
<blockquote>
This is the first level of quoting.
<blockquote>
This is nested blockquote.
<blockquote>
A third level.
</blockquote>
</blockquote>
Back to the first level.
</blockquote>
</td>
</tr>
<tr>
<td>
<code>
&gt; ## This is a header.<br>
&gt;<br>
&gt; 1. This is the first list item.<br>
&gt; 2. This is the second list item.<br>
&gt;<br>
&gt;Here's some example code:<br>
&gt;<br>
&gt;&nbsp;&nbsp;&nbsp;&nbsp;return shell_exec("echo $input | $markdown_script");
</code>
</td>
<td>
<blockquote>
<h2>This is a header.</h2>
<ol>
<li>This is the first list item.</li>
<li>This is the second list item.</li>
</ol>
Here's some example code:
<pre>return shell_exec("echo $input | $markdown_script");</pre>
</blockquote>
</td>
</tr>
</tbody>
</table>
<a name="ul"></a>
<h2>Lists</h2>
<table width="100%" class="table">
<thead>
<tr>
<th width="50%">Markdown Syntax</th>
<th width="50%">Output</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>
* Red<br>
* Green<br>
* Blue
</code>
</td>
<td>
<ul>
<li>Red</li>
<li>Green</li>
<li>Blue</li>
</ul>
</td>
</tr>
<tr>
<td>
<code>
+ Red<br>
+ Green<br>
+ Blue
</code>
</td>
<td>
<ul>
<li>Red</li>
<li>Green</li>
<li>Blue</li>
</ul>
</td>
</tr>
<tr>
<td>
<code>
- Red<br>
- Green<br>
- Blue
</code>
</td>
<td>
<ul>
<li>Red</li>
<li>Green</li>
<li>Blue</li>
</ul>
</td>
</tr>
<tr>
<td>
<code>
1. Bird<br>
2. McHale<br>
3. Parish
</code>
</td>
<td>
<ol>
<li>Bird</li>
<li>McHale</li>
<li>Parish</li>
</ol>
</td>
</tr>
<tr>
<td>
<code>
1.&nbsp;&nbsp;This is a list item with two paragraphs. Lorem ipsum dolor<br>
&nbsp;&nbsp;&nbsp;&nbsp;sit amet, consectetuer adipiscing elit. Aliquam hendrerit<br>
&nbsp;&nbsp;&nbsp;&nbsp;mi posuere lectus.<p>
&nbsp;&nbsp;&nbsp;&nbsp;Vestibulum enim wisi, viverra nec, fringilla in, laoreet<br>
&nbsp;&nbsp;&nbsp;&nbsp;vitae, risus. Donec sit amet nisl. Aliquam semper ipsum<br>
&nbsp;&nbsp;&nbsp;&nbsp;sit amet velit.<p>
2.&nbsp;&nbsp;Suspendisse id sem consectetuer libero luctus adipiscing.
</code>
</td>
<td>
<ol>
<li>This is a list item with two paragraphs. Lorem ipsum dolor
sit amet, consectetuer adipiscing elit. Aliquam hendrerit
mi posuere lectus.<p>
Vestibulum enim wisi, viverra nec, fringilla in, laoreet
vitae, risus. Donec sit amet nisl. Aliquam semper ipsum
sit amet velit.</li>
<li>Suspendisse id sem consectetuer libero luctus adipiscing.</li>
</ol>
</td>
</tr>
</tbody>
</table>
<a name="pre"></a>
<h2>Code Blocks</h2>
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!)<p>
Like GitHub-flavored Markdown, with a fenced code block you can also specify a programming
language to get syntax highlighting for the code.<p>
<table width="100%" class="table">
<thead>
<tr>
<th width="50%">Markdown Syntax</th>
<th width="50%">Output</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>
This is a normal paragraph.<p>
&nbsp;&nbsp;&nbsp;&nbsp;This is a code block.
</code>
</td>
<td>
This is a normal paragraph.<p>
<pre>This is a code block</pre>
</td>
</tr>
<tr>
<td>
<code>
This is a normal paragraph.<p>
```<br>
This is a GitHub style "fenced code block".<br>
```
</code>
</td>
<td>
This is a normal paragraph.<p>
<pre>This is a GitHub style "fenced code block".</pre>
</td>
</tr>
<tr>
<td>
<code>
```javascript<br>
document.writeln("Hello world.");<br>
```
</code>
</td>
<td>
<div class="codehilite"><pre><span class="nb">document</span><span class="p">.</span><span class="nx">writeln</span><span class="p">(</span><span class="s2">&quot;Hello world.&quot;</span><span class="p">);</span></pre></div>
</td>
</tr>
</tbody>
</table>
<a name="hr"></a>
<h2>Horizontal Rules</h2>
<table width="100%" class="table">
<thead>
<tr>
<th width="50%">Markdown Syntax</th>
<th width="50%">Output</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>
* * *<p>
***<p>
*****<p>
- - -<p>
---------------------------
</code>
</td>
<td>
<hr><p>
<hr><p>
<hr><p>
<hr><p>
<hr>
</td>
</tr>
</tbody>
</table>
<a name="span"></a>
<h1>Span Elements</h1>
<a name="a"></a>
<h2>Links</h2>
<table width="100%" class="table">
<thead>
<tr>
<th width="50%">Markdown Syntax</th>
<th width="50%">Output</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>
This is [an example](http://example.com/ "Title") inline link.<p>
[This link](http://example.net/) has no title attribute.
</code>
</td>
<td>
This is <a href="http://example.com/" title="Title">an example</a> inline link.<p>
<a href="http://example.net/">This link</a> has no title attribute.
</td>
</tr>
<tr>
<td>
<code>
See my [About](/about) page for details.
</code>
</td>
<td>
See my <a href="/about">About</a> page for details.
</td>
</tr>
<tr>
<td>
<code>
This is [an example][id] reference-style link.<p>
[id]: http://example.com/ "Optional Title Here"
</code>
</td>
<td>
This is <a href="http://example.com/" title="Optional Title Here">an example</a> reference-style link.
</td>
</tr>
<tr>
<td>
<code>
This is an example of an implicit reference-style link: search [Google][] for more.<p>
[Google]: http://google.com/
</code>
</td>
<td>
This is an example of an implicit reference-style link: search <a href="http://google.com/">Google</a> for more.
</td>
</tr>
<tr>
<td>
<code>
I get 10 times more traffic from [Google] [1] than from<br>
[Yahoo] [2] or [Bing] [3].<p>
[1]: http://google.com/ "Google"<br>
[2]: http://search.yahoo.com/ "Yahoo Search"<br>
[3]: http://bing.com/ "Bing"
</code>
</td>
<td>
I get 10 times more traffic from <a href="http://google.com/" title="Google">Google</a> than from
<a href="http://search.yahoo.com/" title="Yahoo Search">Yahoo</a> or
<a href="http://bing.com/" title="Bing">Bing</a>.
</td>
</tr>
</tbody>
</table>
<a name="em"></a>
<h2>Emphasis</h2>
<table width="100%" class="table">
<thead>
<tr>
<th width="50%">Markdown Syntax</th>
<th width="50%">Output</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>
*single asterisks*<p>
_single underscores_<p>
**double asterisks**<p>
__double underscores__
</code>
</td>
<td>
<em>single asterisks</em><p>
<em>single underscores</em><p>
<strong>double asterisks</strong><p>
<strong>double underscores</strong>
</td>
</tr>
<tr>
<td>
<code>
un*frigging*believable
</code>
</td>
<td>
un<em>frigging</em>believable
</td>
</tr>
<tr>
<td>
<code>
\*this text is surrounded by literal asterisks\*
</code>
</td>
<td>
*this text is surrounded by literal asterisks*
</td>
</tr>
</tbody>
</table>
<a name="code"></a>
<h2>Code</h2>
<table width="100%" class="table">
<thead>
<tr>
<th width="50%">Markdown Syntax</th>
<th width="50%">Output</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>
Use the `printf()` function.
</code>
</td>
<td>
Use the <code>printf()</code> function.
</td>
</tr>
<tr>
<td>
<code>
``There is a literal backtick (`) here.``
</code>
</td>
<td>
<code>There is a literal backtick (`) here.</code>
</td>
</tr>
<tr>
<td>
<code>
A single backtick in a code span: `` ` ``<p>
A backtick-delimited string in a code span: `` `foo` ``
</code>
</td>
<td>
A single backtick in a code span: <code>`</code><p>
A backtick-delimited string in a code span: <code>`foo`</code>
</td>
</tr>
<tr>
<td>
<code>Please don't use any `&lt;blink&gt;` tags.</code>
</td>
<td>
Please don't use any <code>&lt;blink&gt;</code> tags.
</td>
</tr>
<tr>
<td>
<code>`&amp;#8212;` is the decimal-encoded equivalent of `&amp;mdash;`.</code>
</td>
<td>
<code>&amp;#8212;</code> is the decimal-encoded equivalent of
<code>&amp;mdash;</code>.
</td>
</tr>
</tbody>
</table>
<a name="img"></a>
<h2>Images</h2>
<table width="100%" class="table">
<thead>
<tr>
<th width="50%">Markdown Syntax</th>
<th width="50%">Output</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>![Alt text](/static/avatars/default.png)</code>
</td>
<td>
<img src="/static/avatars/default.png" alt="Alt text">
</td>
</tr>
<tr>
<td>
<code>![Alt text](/static/avatars/default.png "Optional title")</code>
</td>
<td>
<img src="/static/avatars/default.png" alt="Alt text" title="Optional title">
</td>
</tr>
<tr>
<td>
<code>
![Alt text][id]<p>
[id]: /static/avatars/default.png "Optional title attribute"
</code>
</td>
<td>
<img src="/static/avatars/default.png" alt="Alt text" title="Optional title attribute">
</td>
</tr>
</tbody>
</table>
<a name="misc"></a>
<h1>Miscellaneous</h1>
<a name="autolink"></a>
<h2>Automatic Links</h2>
E-mail links get automatically converted into a random mess of HTML attributes to
attempt to thwart e-mail harvesting spam bots.<p>
<table width="100%" class="table">
<thead>
<tr>
<th width="50%">Markdown Syntax</th>
<th width="50%">Output</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>&lt;http://example.com/&gt;</code>
</td>
<td>
<a href="http://example.com/">http://example.com/</a>
</td>
</tr>
<tr>
<td>
<code>&lt;address@example.com&gt;</code>
</td>
<td>
<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><p>
(Source: <code>&lt;a href="&amp;#109;&amp;#97;&amp;#105;&amp;#108;&amp;#116;&amp;#111;&amp;#58; &amp;#97;&amp;#100;&amp;#100;&amp;#114;&amp;#101;&amp;#115;&amp;#115;&amp;#64; &amp;#101;&amp;#120;&amp;#97;&amp;#109;&amp;#112;&amp;#108; &amp;#101;&amp;#46;&amp;#99;&amp;#111;&amp;#109;"&gt;&amp;#97; &amp;#100;&amp;#100;&amp;#114;&amp;#101;&amp;#115;&amp;#115; &amp;#64;&amp;#101;&amp;#120;&amp;#97;&amp;#109;&amp;#112; &amp;#108;&amp;#101;&amp;#46;&amp;#99;&amp;#111; &amp;#109;&lt;/a&gt;</code>)
</td>
</tr>
</tbody>
</table>
<a name="escape"></a>
<h2>Backslash Escapes</h2>
Use backslash characters to escape any other special characters in the Markdown syntax. For example,
<code>\*</code> to insert a literal asterisk so that it doesn't get mistaken for e.g. emphasized text,
a list item, etc.<p>
Markdown provides backslash escapes for the following characters:<p>
<pre>\ backslash
` backtick
* asterisk
_ underscore
{} curly braces
[] square brackets
() parenthesis
# hash mark
+ plus sign
- minus sign (hyphen)
. dot
! exclamation mark</pre>
{{ end }}