Move template rendering into sub-package
This commit is contained in:
parent
aabcf59181
commit
e393b1880f
|
@ -51,5 +51,5 @@ func main() {
|
|||
jsondb.SetDebug(true)
|
||||
}
|
||||
|
||||
app.ListenAndServe(fAddress)
|
||||
app.Run(fAddress)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
|
54
core/blog.go
54
core/blog.go
|
@ -13,11 +13,13 @@ import (
|
|||
|
||||
"github.com/gorilla/feeds"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/kirsle/blog/core/internal/log"
|
||||
"github.com/kirsle/blog/core/internal/markdown"
|
||||
"github.com/kirsle/blog/core/internal/models/comments"
|
||||
"github.com/kirsle/blog/core/internal/models/posts"
|
||||
"github.com/kirsle/blog/core/internal/models/settings"
|
||||
"github.com/kirsle/blog/core/internal/models/users"
|
||||
"github.com/kirsle/blog/core/internal/render"
|
||||
"github.com/urfave/negroni"
|
||||
)
|
||||
|
||||
|
@ -306,12 +308,20 @@ func (b *Blog) RenderIndex(r *http.Request, tag, privacy string) template.HTML {
|
|||
|
||||
// Render the blog index partial.
|
||||
var output bytes.Buffer
|
||||
v := map[string]interface{}{
|
||||
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()))
|
||||
}
|
||||
|
|
|
@ -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]")
|
||||
|
|
64
core/core.go
64
core/core.go
|
@ -8,11 +8,13 @@ import (
|
|||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/kirsle/blog/core/internal/log"
|
||||
"github.com/kirsle/blog/core/internal/markdown"
|
||||
"github.com/kirsle/blog/core/internal/models/comments"
|
||||
"github.com/kirsle/blog/core/internal/models/posts"
|
||||
"github.com/kirsle/blog/core/internal/models/settings"
|
||||
"github.com/kirsle/blog/core/internal/models/users"
|
||||
"github.com/kirsle/blog/core/internal/render"
|
||||
"github.com/kirsle/blog/jsondb"
|
||||
"github.com/kirsle/blog/jsondb/caches"
|
||||
"github.com/kirsle/blog/jsondb/caches/null"
|
||||
|
@ -41,28 +43,43 @@ type Blog struct {
|
|||
|
||||
// New initializes the Blog application.
|
||||
func New(documentRoot, userRoot string) *Blog {
|
||||
blog := &Blog{
|
||||
return &Blog{
|
||||
DocumentRoot: documentRoot,
|
||||
UserRoot: userRoot,
|
||||
DB: jsondb.New(filepath.Join(userRoot, ".private")),
|
||||
Cache: null.New(),
|
||||
}
|
||||
}
|
||||
|
||||
// Run quickly configures and starts the HTTP server.
|
||||
func (b *Blog) Run(address string) {
|
||||
b.Configure()
|
||||
b.SetupHTTP()
|
||||
b.ListenAndServe(address)
|
||||
}
|
||||
|
||||
// Configure initializes (or reloads) the blog's configuration, and binds the
|
||||
// settings in sub-packages.
|
||||
func (b *Blog) Configure() {
|
||||
// Load the site config, or start with defaults if not found.
|
||||
settings.DB = blog.DB
|
||||
settings.DB = b.DB
|
||||
config, err := settings.Load()
|
||||
if err != nil {
|
||||
config = settings.Defaults()
|
||||
}
|
||||
|
||||
// Bind configs in sub-packages.
|
||||
render.UserRoot = &b.UserRoot
|
||||
render.DocumentRoot = &b.DocumentRoot
|
||||
|
||||
// Initialize the session cookie store.
|
||||
blog.store = sessions.NewCookieStore([]byte(config.Security.SecretKey))
|
||||
b.store = sessions.NewCookieStore([]byte(config.Security.SecretKey))
|
||||
users.HashCost = config.Security.HashCost
|
||||
|
||||
// Initialize the rest of the models.
|
||||
posts.DB = blog.DB
|
||||
users.DB = blog.DB
|
||||
comments.DB = blog.DB
|
||||
posts.DB = b.DB
|
||||
users.DB = b.DB
|
||||
comments.DB = b.DB
|
||||
|
||||
// Redis cache?
|
||||
if config.Redis.Enabled {
|
||||
|
@ -76,41 +93,42 @@ func New(documentRoot, userRoot string) *Blog {
|
|||
if err != nil {
|
||||
log.Error("Redis init error: %s", err.Error())
|
||||
} else {
|
||||
blog.Cache = cache
|
||||
blog.DB.Cache = cache
|
||||
b.Cache = cache
|
||||
b.DB.Cache = cache
|
||||
markdown.Cache = cache
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetupHTTP initializes the Negroni middleware engine and registers routes.
|
||||
func (b *Blog) SetupHTTP() {
|
||||
// Initialize the router.
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/initial-setup", blog.SetupHandler)
|
||||
blog.AuthRoutes(r)
|
||||
blog.AdminRoutes(r)
|
||||
blog.ContactRoutes(r)
|
||||
blog.BlogRoutes(r)
|
||||
blog.CommentRoutes(r)
|
||||
r.HandleFunc("/initial-setup", b.SetupHandler)
|
||||
b.AuthRoutes(r)
|
||||
b.AdminRoutes(r)
|
||||
b.ContactRoutes(r)
|
||||
b.BlogRoutes(r)
|
||||
b.CommentRoutes(r)
|
||||
|
||||
// GitHub Flavored Markdown CSS.
|
||||
r.Handle("/css/gfm.css", http.StripPrefix("/css", http.FileServer(gfmstyle.Assets)))
|
||||
|
||||
r.PathPrefix("/").HandlerFunc(blog.PageHandler)
|
||||
r.NotFoundHandler = http.HandlerFunc(blog.PageHandler)
|
||||
r.PathPrefix("/").HandlerFunc(b.PageHandler)
|
||||
r.NotFoundHandler = http.HandlerFunc(b.PageHandler)
|
||||
|
||||
n := negroni.New(
|
||||
negroni.NewRecovery(),
|
||||
negroni.NewLogger(),
|
||||
negroni.HandlerFunc(blog.SessionLoader),
|
||||
negroni.HandlerFunc(blog.CSRFMiddleware),
|
||||
negroni.HandlerFunc(blog.AuthMiddleware),
|
||||
negroni.HandlerFunc(b.SessionLoader),
|
||||
negroni.HandlerFunc(b.CSRFMiddleware),
|
||||
negroni.HandlerFunc(b.AuthMiddleware),
|
||||
)
|
||||
n.UseHandler(r)
|
||||
|
||||
// Keep references handy elsewhere in the app.
|
||||
blog.n = n
|
||||
blog.r = r
|
||||
|
||||
return blog
|
||||
b.n = n
|
||||
b.r = r
|
||||
}
|
||||
|
||||
// ListenAndServe begins listening on the given bind address.
|
||||
|
|
|
@ -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
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"
|
||||
"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
|
||||
}
|
||||
|
|
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"
|
||||
"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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
{{ define "content" }}
|
||||
|
||||
{{ $p := .Data.Post }}
|
||||
{{ RenderPost $p false 0 }}
|
||||
{{ RenderPost .Request $p false 0 }}
|
||||
|
||||
{{ if and .LoggedIn .CurrentUser.Admin }}
|
||||
<small>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -8,4 +8,5 @@
|
|||
</p>
|
||||
|
||||
{{ RenderIndex .Request "" "" }}
|
||||
|
||||
{{ 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