Move template rendering into sub-package
This commit is contained in:
parent
aabcf59181
commit
e393b1880f
|
@ -51,5 +51,5 @@ func main() {
|
||||||
jsondb.SetDebug(true)
|
jsondb.SetDebug(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.ListenAndServe(fAddress)
|
app.Run(fAddress)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -11,10 +10,9 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"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/forms"
|
||||||
"github.com/kirsle/blog/core/internal/models/settings"
|
"github.com/kirsle/blog/core/internal/models/settings"
|
||||||
|
"github.com/kirsle/blog/core/internal/render"
|
||||||
"github.com/urfave/negroni"
|
"github.com/urfave/negroni"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -33,13 +31,13 @@ func (b *Blog) AdminRoutes(r *mux.Router) {
|
||||||
|
|
||||||
// AdminHandler is the admin landing page.
|
// AdminHandler is the admin landing page.
|
||||||
func (b *Blog) AdminHandler(w http.ResponseWriter, r *http.Request) {
|
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.
|
// FileTree holds information about files in the document roots.
|
||||||
type FileTree struct {
|
type FileTree struct {
|
||||||
UserRoot bool // false = CoreRoot
|
UserRoot bool // false = CoreRoot
|
||||||
Files []Filepath
|
Files []render.Filepath
|
||||||
}
|
}
|
||||||
|
|
||||||
// EditorHandler lets you edit web pages from the frontend.
|
// 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} {
|
for i, root := range []string{b.UserRoot, b.DocumentRoot} {
|
||||||
tree := FileTree{
|
tree := FileTree{
|
||||||
UserRoot: i == 0,
|
UserRoot: i == 0,
|
||||||
Files: []Filepath{},
|
Files: []render.Filepath{},
|
||||||
}
|
}
|
||||||
|
|
||||||
filepath.Walk(root, func(path string, f os.FileInfo, err error) error {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
tree.Files = append(tree.Files, Filepath{
|
tree.Files = append(tree.Files, render.Filepath{
|
||||||
Absolute: abs,
|
Absolute: abs,
|
||||||
Relative: rel,
|
Relative: rel,
|
||||||
Basename: filepath.Base(path),
|
Basename: filepath.Base(path),
|
||||||
|
@ -224,24 +222,7 @@ func (b *Blog) SettingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
} else {
|
} else {
|
||||||
// Save the settings.
|
// Save the settings.
|
||||||
settings.Save()
|
settings.Save()
|
||||||
|
b.Configure()
|
||||||
// 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.FlashAndReload(w, r, "Settings have been saved!")
|
b.FlashAndReload(w, r, "Settings have been saved!")
|
||||||
return
|
return
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/kirsle/blog/core/internal/forms"
|
"github.com/kirsle/blog/core/internal/forms"
|
||||||
|
"github.com/kirsle/blog/core/internal/log"
|
||||||
"github.com/kirsle/blog/core/internal/models/users"
|
"github.com/kirsle/blog/core/internal/models/users"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
60
core/blog.go
60
core/blog.go
|
@ -13,11 +13,13 @@ import (
|
||||||
|
|
||||||
"github.com/gorilla/feeds"
|
"github.com/gorilla/feeds"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/kirsle/blog/core/internal/log"
|
||||||
"github.com/kirsle/blog/core/internal/markdown"
|
"github.com/kirsle/blog/core/internal/markdown"
|
||||||
"github.com/kirsle/blog/core/internal/models/comments"
|
"github.com/kirsle/blog/core/internal/models/comments"
|
||||||
"github.com/kirsle/blog/core/internal/models/posts"
|
"github.com/kirsle/blog/core/internal/models/posts"
|
||||||
"github.com/kirsle/blog/core/internal/models/settings"
|
"github.com/kirsle/blog/core/internal/models/settings"
|
||||||
"github.com/kirsle/blog/core/internal/models/users"
|
"github.com/kirsle/blog/core/internal/models/users"
|
||||||
|
"github.com/kirsle/blog/core/internal/render"
|
||||||
"github.com/urfave/negroni"
|
"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.
|
// Render the blog index partial.
|
||||||
var output bytes.Buffer
|
var output bytes.Buffer
|
||||||
v := map[string]interface{}{
|
v := render.Vars{
|
||||||
"PreviousPage": previousPage,
|
Data: map[interface{}]interface{}{
|
||||||
"NextPage": nextPage,
|
"PreviousPage": previousPage,
|
||||||
"View": view,
|
"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())
|
return template.HTML(output.String())
|
||||||
}
|
}
|
||||||
|
@ -329,14 +339,20 @@ func (b *Blog) RenderTags(r *http.Request, indexView bool) template.HTML {
|
||||||
}
|
}
|
||||||
|
|
||||||
var output bytes.Buffer
|
var output bytes.Buffer
|
||||||
v := struct {
|
v := render.Vars{
|
||||||
IndexView bool
|
Data: map[interface{}]interface{}{
|
||||||
Tags []posts.Tag
|
"IndexView": indexView,
|
||||||
}{
|
"Tags": tags,
|
||||||
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())
|
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.
|
// 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
|
// If indexView is true, the blog headers will be hyperlinked to the dedicated
|
||||||
// entry view page.
|
// 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.
|
// Look up the author's information.
|
||||||
author, err := users.LoadReadonly(p.AuthorID)
|
author, err := users.LoadReadonly(p.AuthorID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -445,16 +461,18 @@ func (b *Blog) RenderPost(p *posts.Post, indexView bool, numComments int) templa
|
||||||
rendered = template.HTML(p.Body)
|
rendered = template.HTML(p.Body)
|
||||||
}
|
}
|
||||||
|
|
||||||
meta := PostMeta{
|
meta := render.Vars{
|
||||||
Post: p,
|
Data: map[interface{}]interface{}{
|
||||||
Rendered: rendered,
|
"Post": p,
|
||||||
Author: author,
|
"Rendered": rendered,
|
||||||
IndexView: indexView,
|
"Author": author,
|
||||||
Snipped: snipped,
|
"IndexView": indexView,
|
||||||
NumComments: numComments,
|
"Snipped": snipped,
|
||||||
|
"NumComments": numComments,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
output := bytes.Buffer{}
|
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 {
|
if err != nil {
|
||||||
return template.HTML(fmt.Sprintf("[template error in blog/entry.partial: %s]", err.Error()))
|
return template.HTML(fmt.Sprintf("[template error in blog/entry.partial: %s]", err.Error()))
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,9 +11,11 @@ import (
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
|
"github.com/kirsle/blog/core/internal/log"
|
||||||
"github.com/kirsle/blog/core/internal/markdown"
|
"github.com/kirsle/blog/core/internal/markdown"
|
||||||
"github.com/kirsle/blog/core/internal/models/comments"
|
"github.com/kirsle/blog/core/internal/models/comments"
|
||||||
"github.com/kirsle/blog/core/internal/models/users"
|
"github.com/kirsle/blog/core/internal/models/users"
|
||||||
|
"github.com/kirsle/blog/core/internal/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CommentRoutes attaches the comment routes to the app.
|
// 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.
|
// Get the template snippet.
|
||||||
filepath, err := b.ResolvePath("comments/comments.partial")
|
filepath, err := render.ResolvePath("comments/comments.partial")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err.Error())
|
log.Error(err.Error())
|
||||||
return template.HTML("[error: missing comments/comments.partial]")
|
return template.HTML("[error: missing comments/comments.partial]")
|
||||||
}
|
}
|
||||||
|
|
||||||
// And the comment view partial.
|
// And the comment view partial.
|
||||||
entryPartial, err := b.ResolvePath("comments/entry.partial")
|
entryPartial, err := render.ResolvePath("comments/entry.partial")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err.Error())
|
log.Error(err.Error())
|
||||||
return template.HTML("[error: missing comments/entry.partial]")
|
return template.HTML("[error: missing comments/entry.partial]")
|
||||||
|
|
64
core/core.go
64
core/core.go
|
@ -8,11 +8,13 @@ import (
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
|
"github.com/kirsle/blog/core/internal/log"
|
||||||
"github.com/kirsle/blog/core/internal/markdown"
|
"github.com/kirsle/blog/core/internal/markdown"
|
||||||
"github.com/kirsle/blog/core/internal/models/comments"
|
"github.com/kirsle/blog/core/internal/models/comments"
|
||||||
"github.com/kirsle/blog/core/internal/models/posts"
|
"github.com/kirsle/blog/core/internal/models/posts"
|
||||||
"github.com/kirsle/blog/core/internal/models/settings"
|
"github.com/kirsle/blog/core/internal/models/settings"
|
||||||
"github.com/kirsle/blog/core/internal/models/users"
|
"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"
|
||||||
"github.com/kirsle/blog/jsondb/caches"
|
"github.com/kirsle/blog/jsondb/caches"
|
||||||
"github.com/kirsle/blog/jsondb/caches/null"
|
"github.com/kirsle/blog/jsondb/caches/null"
|
||||||
|
@ -41,28 +43,43 @@ type Blog struct {
|
||||||
|
|
||||||
// New initializes the Blog application.
|
// New initializes the Blog application.
|
||||||
func New(documentRoot, userRoot string) *Blog {
|
func New(documentRoot, userRoot string) *Blog {
|
||||||
blog := &Blog{
|
return &Blog{
|
||||||
DocumentRoot: documentRoot,
|
DocumentRoot: documentRoot,
|
||||||
UserRoot: userRoot,
|
UserRoot: userRoot,
|
||||||
DB: jsondb.New(filepath.Join(userRoot, ".private")),
|
DB: jsondb.New(filepath.Join(userRoot, ".private")),
|
||||||
Cache: null.New(),
|
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.
|
// Load the site config, or start with defaults if not found.
|
||||||
settings.DB = blog.DB
|
settings.DB = b.DB
|
||||||
config, err := settings.Load()
|
config, err := settings.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
config = settings.Defaults()
|
config = settings.Defaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bind configs in sub-packages.
|
||||||
|
render.UserRoot = &b.UserRoot
|
||||||
|
render.DocumentRoot = &b.DocumentRoot
|
||||||
|
|
||||||
// Initialize the session cookie store.
|
// 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
|
users.HashCost = config.Security.HashCost
|
||||||
|
|
||||||
// Initialize the rest of the models.
|
// Initialize the rest of the models.
|
||||||
posts.DB = blog.DB
|
posts.DB = b.DB
|
||||||
users.DB = blog.DB
|
users.DB = b.DB
|
||||||
comments.DB = blog.DB
|
comments.DB = b.DB
|
||||||
|
|
||||||
// Redis cache?
|
// Redis cache?
|
||||||
if config.Redis.Enabled {
|
if config.Redis.Enabled {
|
||||||
|
@ -76,41 +93,42 @@ func New(documentRoot, userRoot string) *Blog {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Redis init error: %s", err.Error())
|
log.Error("Redis init error: %s", err.Error())
|
||||||
} else {
|
} else {
|
||||||
blog.Cache = cache
|
b.Cache = cache
|
||||||
blog.DB.Cache = cache
|
b.DB.Cache = cache
|
||||||
markdown.Cache = cache
|
markdown.Cache = cache
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupHTTP initializes the Negroni middleware engine and registers routes.
|
||||||
|
func (b *Blog) SetupHTTP() {
|
||||||
// Initialize the router.
|
// Initialize the router.
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
r.HandleFunc("/initial-setup", blog.SetupHandler)
|
r.HandleFunc("/initial-setup", b.SetupHandler)
|
||||||
blog.AuthRoutes(r)
|
b.AuthRoutes(r)
|
||||||
blog.AdminRoutes(r)
|
b.AdminRoutes(r)
|
||||||
blog.ContactRoutes(r)
|
b.ContactRoutes(r)
|
||||||
blog.BlogRoutes(r)
|
b.BlogRoutes(r)
|
||||||
blog.CommentRoutes(r)
|
b.CommentRoutes(r)
|
||||||
|
|
||||||
// GitHub Flavored Markdown CSS.
|
// GitHub Flavored Markdown CSS.
|
||||||
r.Handle("/css/gfm.css", http.StripPrefix("/css", http.FileServer(gfmstyle.Assets)))
|
r.Handle("/css/gfm.css", http.StripPrefix("/css", http.FileServer(gfmstyle.Assets)))
|
||||||
|
|
||||||
r.PathPrefix("/").HandlerFunc(blog.PageHandler)
|
r.PathPrefix("/").HandlerFunc(b.PageHandler)
|
||||||
r.NotFoundHandler = http.HandlerFunc(blog.PageHandler)
|
r.NotFoundHandler = http.HandlerFunc(b.PageHandler)
|
||||||
|
|
||||||
n := negroni.New(
|
n := negroni.New(
|
||||||
negroni.NewRecovery(),
|
negroni.NewRecovery(),
|
||||||
negroni.NewLogger(),
|
negroni.NewLogger(),
|
||||||
negroni.HandlerFunc(blog.SessionLoader),
|
negroni.HandlerFunc(b.SessionLoader),
|
||||||
negroni.HandlerFunc(blog.CSRFMiddleware),
|
negroni.HandlerFunc(b.CSRFMiddleware),
|
||||||
negroni.HandlerFunc(blog.AuthMiddleware),
|
negroni.HandlerFunc(b.AuthMiddleware),
|
||||||
)
|
)
|
||||||
n.UseHandler(r)
|
n.UseHandler(r)
|
||||||
|
|
||||||
// Keep references handy elsewhere in the app.
|
// Keep references handy elsewhere in the app.
|
||||||
blog.n = n
|
b.n = n
|
||||||
blog.r = r
|
b.r = r
|
||||||
|
|
||||||
return blog
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListenAndServe begins listening on the given bind address.
|
// ListenAndServe begins listening on the given bind address.
|
||||||
|
|
|
@ -5,13 +5,15 @@ import (
|
||||||
|
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
"github.com/kirsle/blog/core/internal/forms"
|
"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/settings"
|
||||||
"github.com/kirsle/blog/core/internal/models/users"
|
"github.com/kirsle/blog/core/internal/models/users"
|
||||||
|
"github.com/kirsle/blog/core/internal/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SetupHandler is the initial blog setup route.
|
// SetupHandler is the initial blog setup route.
|
||||||
func (b *Blog) SetupHandler(w http.ResponseWriter, r *http.Request) {
|
func (b *Blog) SetupHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := &Vars{
|
vars := render.Vars{
|
||||||
Form: forms.Setup{},
|
Form: forms.Setup{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
30
core/internal/log/log.go
Normal file
30
core/internal/log/log.go
Normal 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...)
|
||||||
|
}
|
|
@ -11,8 +11,8 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/kirsle/blog/core/internal/log"
|
||||||
"github.com/kirsle/blog/jsondb/caches"
|
"github.com/kirsle/blog/jsondb/caches"
|
||||||
"github.com/kirsle/golog"
|
|
||||||
"github.com/microcosm-cc/bluemonday"
|
"github.com/microcosm-cc/bluemonday"
|
||||||
"github.com/shurcooL/github_flavored_markdown"
|
"github.com/shurcooL/github_flavored_markdown"
|
||||||
)
|
)
|
||||||
|
@ -35,12 +35,6 @@ var (
|
||||||
reDecodeBlock = regexp.MustCompile(`\[?FENCED_CODE_%d_BLOCK?\]`)
|
reDecodeBlock = regexp.MustCompile(`\[?FENCED_CODE_%d_BLOCK?\]`)
|
||||||
)
|
)
|
||||||
|
|
||||||
var log *golog.Logger
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
log = golog.GetLogger("blog")
|
|
||||||
}
|
|
||||||
|
|
||||||
// A container for parsed code blocks.
|
// A container for parsed code blocks.
|
||||||
type codeBlock struct {
|
type codeBlock struct {
|
||||||
placeholder int
|
placeholder int
|
||||||
|
@ -100,7 +94,10 @@ func RenderTrustedMarkdown(input string) string {
|
||||||
|
|
||||||
// Substitute fenced codes back in.
|
// Substitute fenced codes back in.
|
||||||
for _, block := range codeBlocks {
|
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,
|
html = strings.Replace(html,
|
||||||
fmt.Sprintf("[?FENCED_CODE_%d_BLOCK?]", block.placeholder),
|
fmt.Sprintf("[?FENCED_CODE_%d_BLOCK?]", block.placeholder),
|
||||||
highlighted,
|
highlighted,
|
||||||
|
@ -127,11 +124,10 @@ func Pygmentize(language, source string) (string, error) {
|
||||||
hash := fmt.Sprintf("%x", h.Sum(nil))
|
hash := fmt.Sprintf("%x", h.Sum(nil))
|
||||||
cacheKey := "pygmentize:" + hash
|
cacheKey := "pygmentize:" + hash
|
||||||
|
|
||||||
_ = cacheKey
|
|
||||||
// Do we have it cached?
|
// Do we have it cached?
|
||||||
// if cached, err := b.Cache.Get(cacheKey); err == nil && len(cached) > 0 {
|
if cached, err := Cache.Get(cacheKey); err == nil && len(cached) > 0 {
|
||||||
// return string(cached), nil
|
return string(cached), nil
|
||||||
// }
|
}
|
||||||
|
|
||||||
// Defer to the `pygmentize` command
|
// Defer to the `pygmentize` command
|
||||||
bin := "pygmentize"
|
bin := "pygmentize"
|
||||||
|
@ -154,10 +150,10 @@ func Pygmentize(language, source string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
result = out.String()
|
result = out.String()
|
||||||
// err := b.Cache.Set(cacheKey, []byte(result), 60*60*24) // cool md5's don't change
|
err := Cache.Set(cacheKey, []byte(result), 60*60*24) // cool md5's don't change
|
||||||
// if err != nil {
|
if err != nil {
|
||||||
// log.Error("Couldn't cache Pygmentize output: %s", err)
|
log.Error("Couldn't cache Pygmentize output: %s", err)
|
||||||
// }
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
92
core/internal/render/resolve_paths.go
Normal file
92
core/internal/render/resolve_paths.go
Normal 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")
|
||||||
|
}
|
159
core/internal/render/templates.go
Normal file
159
core/internal/render/templates.go
Normal 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
|
||||||
|
}
|
66
core/internal/responses/responses.go
Normal file
66
core/internal/responses/responses.go
Normal 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()"))
|
||||||
|
}
|
||||||
|
}
|
13
core/log.go
13
core/log.go
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -8,9 +8,11 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/kirsle/blog/core/internal/log"
|
||||||
"github.com/kirsle/blog/core/internal/markdown"
|
"github.com/kirsle/blog/core/internal/markdown"
|
||||||
"github.com/kirsle/blog/core/internal/models/comments"
|
"github.com/kirsle/blog/core/internal/models/comments"
|
||||||
"github.com/kirsle/blog/core/internal/models/settings"
|
"github.com/kirsle/blog/core/internal/models/settings"
|
||||||
|
"github.com/kirsle/blog/core/internal/render"
|
||||||
"github.com/microcosm-cc/bluemonday"
|
"github.com/microcosm-cc/bluemonday"
|
||||||
gomail "gopkg.in/gomail.v2"
|
gomail "gopkg.in/gomail.v2"
|
||||||
)
|
)
|
||||||
|
@ -36,7 +38,7 @@ func (b *Blog) SendEmail(email Email) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve the template.
|
// Resolve the template.
|
||||||
tmpl, err := b.ResolvePath(email.Template)
|
tmpl, err := render.ResolvePath(email.Template)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("SendEmail: %s", err.Error())
|
log.Error("SendEmail: %s", err.Error())
|
||||||
return
|
return
|
||||||
|
|
|
@ -8,6 +8,7 @@ import (
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
|
"github.com/kirsle/blog/core/internal/log"
|
||||||
"github.com/kirsle/blog/core/internal/models/users"
|
"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.
|
// Session returns the current request's session.
|
||||||
func (b *Blog) Session(r *http.Request) *sessions.Session {
|
func (b *Blog) Session(r *http.Request) *sessions.Session {
|
||||||
|
if r == nil {
|
||||||
|
panic("Session(*http.Request) with a nil argument!?")
|
||||||
|
}
|
||||||
|
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
if session, ok := ctx.Value(sessionKey).(*sessions.Session); ok {
|
if session, ok := ctx.Value(sessionKey).(*sessions.Session); ok {
|
||||||
return session
|
return session
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"html/template"
|
"html/template"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/kirsle/blog/core/internal/markdown"
|
"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.
|
// 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.
|
// Search for a file that matches their URL.
|
||||||
filepath, err := b.ResolvePath(path)
|
filepath, err := render.ResolvePath(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// See if it resolves as a blog entry.
|
// See if it resolves as a blog entry.
|
||||||
err = b.viewPost(w, r, strings.TrimLeft(path, "/"))
|
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?
|
// Is it a template file?
|
||||||
if strings.HasSuffix(filepath.URI, ".gohtml") {
|
if strings.HasSuffix(filepath.URI, ".gohtml") {
|
||||||
b.RenderTemplate(w, r, filepath.URI, nil)
|
b.RenderTemplate(w, r, filepath.URI, render.Vars{})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,79 +67,3 @@ func (b *Blog) PageHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
http.ServeFile(w, r, filepath.Absolute)
|
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")
|
|
||||||
}
|
|
||||||
|
|
|
@ -3,6 +3,9 @@ package core
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"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.
|
// 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)
|
w.WriteHeader(http.StatusNotFound)
|
||||||
err := b.RenderTemplate(w, r, ".errors/404", &Vars{
|
err := b.RenderTemplate(w, r, ".errors/404", render.Vars{
|
||||||
Message: message[0],
|
Message: message[0],
|
||||||
})
|
})
|
||||||
if err != nil {
|
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.
|
// Forbidden sends an HTTP 403 Forbidden response.
|
||||||
func (b *Blog) Forbidden(w http.ResponseWriter, r *http.Request, message ...string) {
|
func (b *Blog) Forbidden(w http.ResponseWriter, r *http.Request, message ...string) {
|
||||||
w.WriteHeader(http.StatusForbidden)
|
w.WriteHeader(http.StatusForbidden)
|
||||||
err := b.RenderTemplate(w, r, ".errors/403", &Vars{
|
err := b.RenderTemplate(w, r, ".errors/403", render.Vars{
|
||||||
Message: message[0],
|
Message: message[0],
|
||||||
})
|
})
|
||||||
if err != nil {
|
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.
|
// Error sends an HTTP 500 Internal Server Error response.
|
||||||
func (b *Blog) Error(w http.ResponseWriter, r *http.Request, message ...string) {
|
func (b *Blog) Error(w http.ResponseWriter, r *http.Request, message ...string) {
|
||||||
w.WriteHeader(http.StatusInternalServerError)
|
w.WriteHeader(http.StatusInternalServerError)
|
||||||
err := b.RenderTemplate(w, r, ".errors/500", &Vars{
|
err := b.RenderTemplate(w, r, ".errors/500", render.Vars{
|
||||||
Message: message[0],
|
Message: message[0],
|
||||||
})
|
})
|
||||||
if err != nil {
|
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.
|
// BadRequest sends an HTTP 400 Bad Request.
|
||||||
func (b *Blog) BadRequest(w http.ResponseWriter, r *http.Request, message ...string) {
|
func (b *Blog) BadRequest(w http.ResponseWriter, r *http.Request, message ...string) {
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
err := b.RenderTemplate(w, r, ".errors/400", &Vars{
|
err := b.RenderTemplate(w, r, ".errors/400", render.Vars{
|
||||||
Message: message[0],
|
Message: message[0],
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"github.com/kirsle/blog/core/internal/forms"
|
"github.com/kirsle/blog/core/internal/forms"
|
||||||
"github.com/kirsle/blog/core/internal/models/settings"
|
"github.com/kirsle/blog/core/internal/models/settings"
|
||||||
"github.com/kirsle/blog/core/internal/models/users"
|
"github.com/kirsle/blog/core/internal/models/users"
|
||||||
|
"github.com/kirsle/blog/core/internal/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Vars is an interface to implement by the templates to pass their own custom
|
// 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.
|
// NewVars initializes a Vars struct with the custom Data map initialized.
|
||||||
// You may pass in an initial value for this map if you want.
|
// 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{}
|
var value map[interface{}]interface{}
|
||||||
if len(data) > 0 {
|
if len(data) > 0 {
|
||||||
value = data[0]
|
value = data[0]
|
||||||
} else {
|
} else {
|
||||||
value = make(map[interface{}]interface{})
|
value = make(map[interface{}]interface{})
|
||||||
}
|
}
|
||||||
return &Vars{
|
return render.Vars{
|
||||||
Data: value,
|
Data: value,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadDefaults combines template variables with default, globally available vars.
|
// 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.
|
// Get the site settings.
|
||||||
s, err := settings.Load()
|
s, err := settings.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -72,12 +73,9 @@ func (v *Vars) LoadDefaults(b *Blog, r *http.Request) {
|
||||||
user, err := b.CurrentUser(r)
|
user, err := b.CurrentUser(r)
|
||||||
v.CurrentUser = user
|
v.CurrentUser = user
|
||||||
v.LoggedIn = err == nil
|
v.LoggedIn = err == nil
|
||||||
}
|
|
||||||
|
|
||||||
// // TemplateVars is an interface that describes the template variable struct.
|
return v
|
||||||
// type TemplateVars interface {
|
}
|
||||||
// LoadDefaults(*Blog, *http.Request)
|
|
||||||
// }
|
|
||||||
|
|
||||||
// RenderPartialTemplate handles rendering a Go template to a writer, without
|
// 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
|
// 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
|
// 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
|
// a bare template on its own, i.e. for partial templates, create a Vars struct
|
||||||
// with `Vars{NoIndex: true}`
|
// with `Vars{NoIndex: true}`
|
||||||
func (b *Blog) RenderPartialTemplate(w io.Writer, path string, v interface{}, withLayout bool, functions map[string]interface{}) error {
|
func (b *Blog) RenderPartialTemplate(w io.Writer, r *http.Request, path string, v render.Vars, withLayout bool, functions map[string]interface{}) error {
|
||||||
var (
|
v = b.LoadDefaults(v, r)
|
||||||
layout Filepath
|
return render.PartialTemplate(w, path, render.Config{
|
||||||
templateName string
|
Request: r,
|
||||||
err error
|
Vars: &v,
|
||||||
)
|
WithLayout: withLayout,
|
||||||
|
Functions: b.TemplateFuncs(nil, nil, functions),
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RenderTemplate responds with an HTML template.
|
// 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
|
// 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
|
// new CSRF token, and other such things. If you just want to render a template
|
||||||
// without all that nonsense, use RenderPartialTemplate.
|
// without all that nonsense, use RenderPartialTemplate.
|
||||||
func (b *Blog) RenderTemplate(w http.ResponseWriter, r *http.Request, path string, vars *Vars) error {
|
func (b *Blog) RenderTemplate(w http.ResponseWriter, r *http.Request, path string, vars render.Vars) error {
|
||||||
// Inject globally available variables.
|
if r == nil {
|
||||||
if vars == nil {
|
panic("core.RenderTemplate(): the *http.Request is nil!?")
|
||||||
vars = &Vars{}
|
|
||||||
}
|
}
|
||||||
vars.LoadDefaults(b, r)
|
|
||||||
|
// Inject globally available variables.
|
||||||
|
vars = b.LoadDefaults(vars, r)
|
||||||
|
|
||||||
// Add any flashed messages from the endpoint controllers.
|
// Add any flashed messages from the endpoint controllers.
|
||||||
session := b.Session(r)
|
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.CSRF = b.GenerateCSRFToken(w, r, session)
|
||||||
vars.Editable = !strings.HasPrefix(path, "admin/")
|
vars.Editable = !strings.HasPrefix(path, "admin/")
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; encoding=UTF-8")
|
return render.Template(w, path, render.Config{
|
||||||
b.RenderPartialTemplate(w, path, vars, true, template.FuncMap{
|
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 {
|
"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)
|
session := b.Session(r)
|
||||||
csrf := b.GenerateCSRFToken(w, r, session)
|
csrf := b.GenerateCSRFToken(w, r, session)
|
||||||
return b.RenderComments(session, csrf, r.URL.Path, subject, ids...)
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
{{ define "content" }}
|
{{ define "content" }}
|
||||||
|
|
||||||
{{ $p := .Data.Post }}
|
{{ $p := .Data.Post }}
|
||||||
{{ RenderPost $p false 0 }}
|
{{ RenderPost .Request $p false 0 }}
|
||||||
|
|
||||||
{{ if and .LoggedIn .CurrentUser.Admin }}
|
{{ if and .LoggedIn .CurrentUser.Admin }}
|
||||||
<small>
|
<small>
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
{{ $a := .Author }}
|
{{ $a := .Data.Author }}
|
||||||
{{ $p := .Post }}
|
{{ $p := .Data.Post }}
|
||||||
|
{{ $d := .Data }}
|
||||||
|
|
||||||
{{ if .IndexView }}
|
{{ if $d.IndexView }}
|
||||||
<a class="h1 blog-title" href="/{{ $p.Fragment }}">{{ $p.Title }}</a>
|
<a class="h1 blog-title" href="/{{ $p.Fragment }}">{{ $p.Title }}</a>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<h1 class="blog-title">{{ $p.Title }}</h1>
|
<h1 class="blog-title">{{ $p.Title }}</h1>
|
||||||
|
@ -27,9 +28,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="markdown mb-4">
|
<div class="markdown mb-4">
|
||||||
{{ .Rendered }}
|
{{ $d.Rendered }}
|
||||||
|
|
||||||
{{ if .Snipped }}
|
{{ if $d.Snipped }}
|
||||||
<p>
|
<p>
|
||||||
<a href="/{{ $p.Fragment }}#snip">Read more...</a>
|
<a href="/{{ $p.Fragment }}#snip">Read more...</a>
|
||||||
</p>
|
</p>
|
||||||
|
@ -45,9 +46,9 @@
|
||||||
</ul>
|
</ul>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ if .IndexView }}
|
{{ if $d.IndexView }}
|
||||||
<em class="text-muted">
|
<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>
|
<a href="/{{ $p.Fragment }}">Permalink</a>
|
||||||
</em>
|
</em>
|
||||||
|
|
|
@ -1,18 +1,23 @@
|
||||||
|
{{ $PreviousPage := .Data.PreviousPage }}
|
||||||
|
{{ $NextPage := .Data.NextPage }}
|
||||||
|
{{ $View := .Data.View }}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col text-right">
|
<div class="col text-right">
|
||||||
<ul class="list-inline">
|
<ul class="list-inline">
|
||||||
{{ if .PreviousPage }}
|
{{ if $PreviousPage }}
|
||||||
<li class="list-inline-item"><a href="?page={{ .PreviousPage }}">Earlier</a></li>
|
<li class="list-inline-item"><a href="?page={{ $PreviousPage }}">Earlier</a></li>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ if .NextPage }}
|
{{ if $NextPage }}
|
||||||
<li class="list-inline-item"><a href="?page={{ .NextPage }}">Older</a></li>
|
<li class="list-inline-item"><a href="?page={{ $NextPage }}">Older</a></li>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{ range .View }}
|
{{ $r := .Request }}
|
||||||
|
|
||||||
|
{{ range $View }}
|
||||||
{{ $p := .Post }}
|
{{ $p := .Post }}
|
||||||
{{ RenderPost $p true .NumComments }}
|
{{ RenderPost $r $p true .NumComments }}
|
||||||
|
|
||||||
{{ if and $.LoggedIn $.CurrentUser.Admin }}
|
{{ if and $.LoggedIn $.CurrentUser.Admin }}
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
|
@ -31,11 +36,11 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col text-right">
|
<div class="col text-right">
|
||||||
<ul class="list-inline">
|
<ul class="list-inline">
|
||||||
{{ if .PreviousPage }}
|
{{ if $PreviousPage }}
|
||||||
<li class="list-inline-item"><a href="?page={{ .PreviousPage }}">Earlier</a></li>
|
<li class="list-inline-item"><a href="?page={{ $PreviousPage }}">Earlier</a></li>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ if .NextPage }}
|
{{ if $NextPage }}
|
||||||
<li class="list-inline-item"><a href="?page={{ .NextPage }}">Older</a></li>
|
<li class="list-inline-item"><a href="?page={{ $NextPage }}">Older</a></li>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
{{ if .IndexView }}
|
{{ if .Data.IndexView }}
|
||||||
Sorted by most frequently used:
|
Sorted by most frequently used:
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{{ range .Tags }}
|
{{ range .Data.Tags }}
|
||||||
<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>
|
</ul>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<ul>
|
<ul>
|
||||||
{{ range $i, $t := .Tags }}
|
{{ range $i, $t := .Data.Tags }}
|
||||||
{{ if le $i 20 }}
|
{{ 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 }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -24,6 +24,18 @@ h6, .h6 {
|
||||||
color: #333;
|
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
|
* Bootstrap tweaks and overrides
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -101,7 +101,7 @@
|
||||||
class="form-control">{{ .Body }}</textarea>
|
class="form-control">{{ .Body }}</textarea>
|
||||||
<small id="bodyHelp" class="form-text text-muted">
|
<small id="bodyHelp" class="form-text text-muted">
|
||||||
You may format your message using
|
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.
|
syntax.
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,4 +8,5 @@
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{{ RenderIndex .Request "" "" }}
|
{{ RenderIndex .Request "" "" }}
|
||||||
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
712
root/markdown.gohtml
Normal file
712
root/markdown.gohtml
Normal 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><h1></code> and <code><h2></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>></code> to "quote" it -- like in
|
||||||
|
"e-mail syntax."</p>
|
||||||
|
|
||||||
|
<p>You may have multiple layers of quotes by using multiple <code>></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>
|
||||||
|
> 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>
|
||||||
|
> 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>
|
||||||
|
> 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>
|
||||||
|
|
||||||
|
> 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>
|
||||||
|
> This is the first level of quoting.<br>
|
||||||
|
><br>
|
||||||
|
>> This is nested blockquote.<br>
|
||||||
|
>>> A third level.<br>
|
||||||
|
><br>
|
||||||
|
> 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>
|
||||||
|
> ## This is a header.<br>
|
||||||
|
><br>
|
||||||
|
> 1. This is the first list item.<br>
|
||||||
|
> 2. This is the second list item.<br>
|
||||||
|
><br>
|
||||||
|
>Here's some example code:<br>
|
||||||
|
><br>
|
||||||
|
> 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. This is a list item with two paragraphs. Lorem ipsum dolor<br>
|
||||||
|
sit amet, consectetuer adipiscing elit. Aliquam hendrerit<br>
|
||||||
|
mi posuere lectus.<p>
|
||||||
|
|
||||||
|
Vestibulum enim wisi, viverra nec, fringilla in, laoreet<br>
|
||||||
|
vitae, risus. Donec sit amet nisl. Aliquam semper ipsum<br>
|
||||||
|
sit amet velit.<p>
|
||||||
|
|
||||||
|
2. 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>
|
||||||
|
|
||||||
|
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">"Hello world."</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 `<blink>` tags.</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
Please don't use any <code><blink></code> tags.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code>`&#8212;` is the decimal-encoded equivalent of `&mdash;`.</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<code>&#8212;</code> is the decimal-encoded equivalent of
|
||||||
|
<code>&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><http://example.com/></code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="http://example.com/">http://example.com/</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<code><address@example.com></code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="mailto:address@example.com">address@example.com</a><p>
|
||||||
|
|
||||||
|
(Source: <code><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></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 }}
|
Loading…
Reference in New Issue
Block a user