mirror of
https://github.com/caddyserver/caddy.git
synced 2025-01-05 18:44:58 +03:00
commit
c23c6d9cb4
21 changed files with 934 additions and 1372 deletions
|
@ -2,9 +2,7 @@ package setup
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/markdown"
|
||||
|
@ -25,25 +23,6 @@ func Markdown(c *Controller) (middleware.Middleware, error) {
|
|||
IndexFiles: []string{"index.md"},
|
||||
}
|
||||
|
||||
// Sweep the whole path at startup to at least generate link index, maybe generate static site
|
||||
c.Startup = append(c.Startup, func() error {
|
||||
for i := range mdconfigs {
|
||||
cfg := mdconfigs[i]
|
||||
|
||||
// Generate link index and static files (if enabled)
|
||||
if err := markdown.GenerateStatic(md, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Watch file changes for static site generation if not in development mode.
|
||||
if !cfg.Development {
|
||||
markdown.Watch(md, cfg, markdown.DefaultInterval)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return func(next middleware.Handler) middleware.Handler {
|
||||
md.Next = next
|
||||
return md
|
||||
|
@ -55,9 +34,9 @@ func markdownParse(c *Controller) ([]*markdown.Config, error) {
|
|||
|
||||
for c.Next() {
|
||||
md := &markdown.Config{
|
||||
Renderer: blackfriday.HtmlRenderer(0, "", ""),
|
||||
Templates: make(map[string]string),
|
||||
StaticFiles: make(map[string]string),
|
||||
Renderer: blackfriday.HtmlRenderer(0, "", ""),
|
||||
Extensions: make(map[string]struct{}),
|
||||
Template: markdown.GetDefaultTemplate(),
|
||||
}
|
||||
|
||||
// Get the path scope
|
||||
|
@ -80,7 +59,9 @@ func markdownParse(c *Controller) ([]*markdown.Config, error) {
|
|||
|
||||
// If no extensions were specified, assume some defaults
|
||||
if len(md.Extensions) == 0 {
|
||||
md.Extensions = []string{".md", ".markdown", ".mdown"}
|
||||
md.Extensions[".md"] = struct{}{}
|
||||
md.Extensions[".markdown"] = struct{}{}
|
||||
md.Extensions[".mdown"] = struct{}{}
|
||||
}
|
||||
|
||||
mdconfigs = append(mdconfigs, md)
|
||||
|
@ -92,11 +73,9 @@ func markdownParse(c *Controller) ([]*markdown.Config, error) {
|
|||
func loadParams(c *Controller, mdc *markdown.Config) error {
|
||||
switch c.Val() {
|
||||
case "ext":
|
||||
exts := c.RemainingArgs()
|
||||
if len(exts) == 0 {
|
||||
return c.ArgErr()
|
||||
for _, ext := range c.RemainingArgs() {
|
||||
mdc.Extensions[ext] = struct{}{}
|
||||
}
|
||||
mdc.Extensions = append(mdc.Extensions, exts...)
|
||||
return nil
|
||||
case "css":
|
||||
if !c.NextArg() {
|
||||
|
@ -113,41 +92,32 @@ func loadParams(c *Controller, mdc *markdown.Config) error {
|
|||
case "template":
|
||||
tArgs := c.RemainingArgs()
|
||||
switch len(tArgs) {
|
||||
case 0:
|
||||
default:
|
||||
return c.ArgErr()
|
||||
case 1:
|
||||
if _, ok := mdc.Templates[markdown.DefaultTemplate]; ok {
|
||||
return c.Err("only one default template is allowed, use alias.")
|
||||
}
|
||||
fpath := filepath.ToSlash(filepath.Clean(c.Root + string(filepath.Separator) + tArgs[0]))
|
||||
mdc.Templates[markdown.DefaultTemplate] = fpath
|
||||
|
||||
if err := markdown.SetTemplate(mdc.Template, "", fpath); err != nil {
|
||||
c.Errf("default template parse error: %v", err)
|
||||
}
|
||||
return nil
|
||||
case 2:
|
||||
fpath := filepath.ToSlash(filepath.Clean(c.Root + string(filepath.Separator) + tArgs[1]))
|
||||
mdc.Templates[tArgs[0]] = fpath
|
||||
|
||||
if err := markdown.SetTemplate(mdc.Template, tArgs[0], fpath); err != nil {
|
||||
c.Errf("template parse error: %v", err)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
case "templatedir":
|
||||
if !c.NextArg() {
|
||||
return c.ArgErr()
|
||||
}
|
||||
case "sitegen":
|
||||
if c.NextArg() {
|
||||
mdc.StaticDir = path.Join(c.Root, c.Val())
|
||||
} else {
|
||||
mdc.StaticDir = path.Join(c.Root, markdown.DefaultStaticDir)
|
||||
_, err := mdc.Template.ParseGlob(c.Val())
|
||||
if err != nil {
|
||||
c.Errf("template load error: %v", err)
|
||||
}
|
||||
if c.NextArg() {
|
||||
// only 1 argument allowed
|
||||
return c.ArgErr()
|
||||
}
|
||||
return nil
|
||||
case "dev":
|
||||
if c.NextArg() {
|
||||
mdc.Development = strings.ToLower(c.Val()) == "true"
|
||||
} else {
|
||||
mdc.Development = true
|
||||
}
|
||||
if c.NextArg() {
|
||||
// only 1 argument allowed
|
||||
return c.ArgErr()
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -3,11 +3,9 @@ package setup
|
|||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"text/template"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/markdown"
|
||||
|
@ -37,84 +35,14 @@ func TestMarkdown(t *testing.T) {
|
|||
if myHandler.Configs[0].PathScope != "/blog" {
|
||||
t.Errorf("Expected /blog as the Path Scope")
|
||||
}
|
||||
if fmt.Sprint(myHandler.Configs[0].Extensions) != fmt.Sprint([]string{".md", ".markdown", ".mdown"}) {
|
||||
t.Errorf("Expected .md, .markdown, and .mdown as default extensions")
|
||||
if len(myHandler.Configs[0].Extensions) != 3 {
|
||||
t.Error("Expected 3 markdown extensions")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownStaticGen(t *testing.T) {
|
||||
c := NewTestController(`markdown /blog {
|
||||
ext .md
|
||||
template tpl_with_include.html
|
||||
sitegen
|
||||
}`)
|
||||
|
||||
c.Root = "./testdata"
|
||||
mid, err := Markdown(c)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no errors, got: %v", err)
|
||||
}
|
||||
|
||||
if mid == nil {
|
||||
t.Fatal("Expected middleware, was nil instead")
|
||||
}
|
||||
|
||||
for _, start := range c.Startup {
|
||||
err := start()
|
||||
if err != nil {
|
||||
t.Errorf("Startup error: %v", err)
|
||||
for _, key := range []string{".md", ".markdown", ".mdown"} {
|
||||
if ext, ok := myHandler.Configs[0].Extensions[key]; !ok {
|
||||
t.Errorf("Expected extensions to contain %v", ext)
|
||||
}
|
||||
}
|
||||
|
||||
next := middleware.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
t.Fatalf("Next shouldn't be called")
|
||||
return 0, nil
|
||||
})
|
||||
hndlr := mid(next)
|
||||
mkdwn, ok := hndlr.(markdown.Markdown)
|
||||
if !ok {
|
||||
t.Fatalf("Was expecting a markdown.Markdown but got %T", hndlr)
|
||||
}
|
||||
|
||||
expectedStaticFiles := map[string]string{"/blog/first_post.md": "testdata/generated_site/blog/first_post.md/index.html"}
|
||||
if fmt.Sprint(expectedStaticFiles) != fmt.Sprint(mkdwn.Configs[0].StaticFiles) {
|
||||
t.Fatalf("Test expected StaticFiles to be %s, but got %s",
|
||||
fmt.Sprint(expectedStaticFiles), fmt.Sprint(mkdwn.Configs[0].StaticFiles))
|
||||
}
|
||||
|
||||
filePath := "testdata/generated_site/blog/first_post.md/index.html"
|
||||
if _, err := os.Stat(filePath); err != nil {
|
||||
t.Fatalf("An error occured when getting the file information: %v", err)
|
||||
}
|
||||
|
||||
html, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
t.Fatalf("An error occured when getting the file content: %v", err)
|
||||
}
|
||||
|
||||
expectedBody := []byte(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>first_post</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Header title</h1>
|
||||
|
||||
<h1>Test h1</h1>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
|
||||
if !bytes.Equal(html, expectedBody) {
|
||||
t.Fatalf("Expected file content: %s got: %s", string(expectedBody), string(html))
|
||||
}
|
||||
|
||||
fp := filepath.Join(c.Root, markdown.DefaultStaticDir)
|
||||
if err = os.RemoveAll(fp); err != nil {
|
||||
t.Errorf("Error while removing the generated static files: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkdownParse(t *testing.T) {
|
||||
|
@ -129,22 +57,30 @@ func TestMarkdownParse(t *testing.T) {
|
|||
css /resources/css/blog.css
|
||||
js /resources/js/blog.js
|
||||
}`, false, []markdown.Config{{
|
||||
PathScope: "/blog",
|
||||
Extensions: []string{".md", ".txt"},
|
||||
Styles: []string{"/resources/css/blog.css"},
|
||||
Scripts: []string{"/resources/js/blog.js"},
|
||||
PathScope: "/blog",
|
||||
Extensions: map[string]struct{}{
|
||||
".md": {},
|
||||
".txt": {},
|
||||
},
|
||||
Styles: []string{"/resources/css/blog.css"},
|
||||
Scripts: []string{"/resources/js/blog.js"},
|
||||
Template: markdown.GetDefaultTemplate(),
|
||||
}}},
|
||||
{`markdown /blog {
|
||||
ext .md
|
||||
template tpl_with_include.html
|
||||
sitegen
|
||||
}`, false, []markdown.Config{{
|
||||
PathScope: "/blog",
|
||||
Extensions: []string{".md"},
|
||||
Templates: map[string]string{markdown.DefaultTemplate: "testdata/tpl_with_include.html"},
|
||||
StaticDir: markdown.DefaultStaticDir,
|
||||
PathScope: "/blog",
|
||||
Extensions: map[string]struct{}{
|
||||
".md": {},
|
||||
},
|
||||
Template: markdown.GetDefaultTemplate(),
|
||||
}}},
|
||||
}
|
||||
// Setup the extra template
|
||||
tmpl := tests[1].expectedMarkdownConfig[0].Template
|
||||
markdown.SetTemplate(tmpl, "", "./testdata/tpl_with_include.html")
|
||||
|
||||
for i, test := range tests {
|
||||
c := NewTestController(test.inputMarkdownConfig)
|
||||
c.Root = "./testdata"
|
||||
|
@ -174,11 +110,47 @@ func TestMarkdownParse(t *testing.T) {
|
|||
t.Errorf("Test %d expected %dth Markdown Config Scripts to be %s , but got %s",
|
||||
i, j, fmt.Sprint(test.expectedMarkdownConfig[j].Scripts), fmt.Sprint(actualMarkdownConfig.Scripts))
|
||||
}
|
||||
if fmt.Sprint(actualMarkdownConfig.Templates) != fmt.Sprint(test.expectedMarkdownConfig[j].Templates) {
|
||||
t.Errorf("Test %d expected %dth Markdown Config Templates to be %s , but got %s",
|
||||
i, j, fmt.Sprint(test.expectedMarkdownConfig[j].Templates), fmt.Sprint(actualMarkdownConfig.Templates))
|
||||
if ok, tx, ty := equalTemplates(actualMarkdownConfig.Template, test.expectedMarkdownConfig[j].Template); !ok {
|
||||
t.Errorf("Test %d the %dth Markdown Config Templates did not match, expected %s to be %s", i, j, tx, ty)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func equalTemplates(i, j *template.Template) (bool, string, string) {
|
||||
// Just in case :)
|
||||
if i == j {
|
||||
return true, "", ""
|
||||
}
|
||||
|
||||
// We can't do much here, templates can't really be compared. However,
|
||||
// we can execute the templates and compare their outputs to be reasonably
|
||||
// sure that they're the same.
|
||||
|
||||
// This is exceedingly ugly.
|
||||
ctx := middleware.Context{
|
||||
Root: http.Dir("./testdata"),
|
||||
}
|
||||
|
||||
md := markdown.Data{
|
||||
Context: ctx,
|
||||
Doc: make(map[string]string),
|
||||
DocFlags: make(map[string]bool),
|
||||
Styles: []string{"style1"},
|
||||
Scripts: []string{"js1"},
|
||||
}
|
||||
md.Doc["title"] = "some title"
|
||||
md.Doc["body"] = "some body"
|
||||
|
||||
bufi := new(bytes.Buffer)
|
||||
bufj := new(bytes.Buffer)
|
||||
|
||||
if err := i.Execute(bufi, md); err != nil {
|
||||
return false, fmt.Sprintf("%v", err), ""
|
||||
}
|
||||
if err := j.Execute(bufj, md); err != nil {
|
||||
return false, "", fmt.Sprintf("%v", err)
|
||||
}
|
||||
|
||||
return bytes.Equal(bufi.Bytes(), bufj.Bytes()), string(bufi.Bytes()), string(bufj.Bytes())
|
||||
}
|
||||
|
|
|
@ -184,7 +184,11 @@ func (c Context) Markdown(filename string) (string, error) {
|
|||
return "", err
|
||||
}
|
||||
renderer := blackfriday.HtmlRenderer(0, "", "")
|
||||
extns := blackfriday.EXTENSION_TABLES | blackfriday.EXTENSION_FENCED_CODE | blackfriday.EXTENSION_STRIKETHROUGH | blackfriday.EXTENSION_DEFINITION_LISTS
|
||||
extns := 0
|
||||
extns |= blackfriday.EXTENSION_TABLES
|
||||
extns |= blackfriday.EXTENSION_FENCED_CODE
|
||||
extns |= blackfriday.EXTENSION_STRIKETHROUGH
|
||||
extns |= blackfriday.EXTENSION_DEFINITION_LISTS
|
||||
markdown := blackfriday.Markdown([]byte(body), renderer, extns)
|
||||
|
||||
return string(markdown), nil
|
||||
|
|
|
@ -1,146 +0,0 @@
|
|||
package markdown
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
)
|
||||
|
||||
// GenerateStatic generate static files and link index from markdowns.
|
||||
// It only generates static files if it is enabled (cfg.StaticDir
|
||||
// must be set).
|
||||
func GenerateStatic(md Markdown, cfg *Config) error {
|
||||
// Generate links since they may be needed, even without sitegen.
|
||||
generated, err := generateLinks(md, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// No new file changes, return.
|
||||
if !generated {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If static site generation is enabled, generate the site.
|
||||
if cfg.StaticDir != "" {
|
||||
if err := generateStaticHTML(md, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type linkGenerator struct {
|
||||
gens map[*Config]*linkGen
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
var generator = linkGenerator{gens: make(map[*Config]*linkGen)}
|
||||
|
||||
// generateLinks generates links to all markdown files ordered by newest date.
|
||||
// This blocks until link generation is done. When called by multiple goroutines,
|
||||
// the first caller starts the generation and others only wait.
|
||||
// It returns if generation is done and any error that occurred.
|
||||
func generateLinks(md Markdown, cfg *Config) (bool, error) {
|
||||
generator.Lock()
|
||||
|
||||
// if link generator exists for config and running, wait.
|
||||
if g, ok := generator.gens[cfg]; ok {
|
||||
if g.started() {
|
||||
g.addWaiter()
|
||||
generator.Unlock()
|
||||
g.Wait()
|
||||
// another goroutine has done the generation.
|
||||
return false, g.lastErr
|
||||
}
|
||||
}
|
||||
|
||||
g := &linkGen{}
|
||||
generator.gens[cfg] = g
|
||||
generator.Unlock()
|
||||
|
||||
generated := g.generateLinks(md, cfg)
|
||||
g.discardWaiters()
|
||||
return generated, g.lastErr
|
||||
}
|
||||
|
||||
// generateStaticHTML generates static HTML files from markdowns.
|
||||
func generateStaticHTML(md Markdown, cfg *Config) error {
|
||||
// If generated site already exists, clear it out
|
||||
_, err := os.Stat(cfg.StaticDir)
|
||||
if err == nil {
|
||||
err := os.RemoveAll(cfg.StaticDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
fp := filepath.Join(md.Root, cfg.PathScope)
|
||||
|
||||
return filepath.Walk(fp, func(path string, info os.FileInfo, err error) error {
|
||||
for _, ext := range cfg.Extensions {
|
||||
if !info.IsDir() && strings.HasSuffix(info.Name(), ext) {
|
||||
// Load the file
|
||||
body, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the relative path as if it were a HTTP request,
|
||||
// then prepend with "/" (like a real HTTP request)
|
||||
reqPath, err := filepath.Rel(md.Root, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqPath = filepath.ToSlash(reqPath)
|
||||
reqPath = "/" + reqPath
|
||||
|
||||
// Create empty requests and url to cater for template values.
|
||||
req, _ := http.NewRequest("", "/", nil)
|
||||
urlVar, _ := url.Parse("/")
|
||||
|
||||
// Generate the static file
|
||||
ctx := middleware.Context{Root: md.FileSys, Req: req, URL: urlVar}
|
||||
_, err = md.Process(cfg, reqPath, body, ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
break // don't try other file extensions
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// computeDirHash computes an hash on static directory of c.
|
||||
func computeDirHash(md Markdown, c *Config) (string, error) {
|
||||
dir := filepath.Join(md.Root, c.PathScope)
|
||||
if _, err := os.Stat(dir); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hashString := ""
|
||||
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if !info.IsDir() && c.IsValidExt(filepath.Ext(path)) {
|
||||
hashString += fmt.Sprintf("%v%v%v%v", info.ModTime(), info.Name(), info.Size(), path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sum := md5.Sum([]byte(hashString))
|
||||
return hex.EncodeToString(sum[:]), nil
|
||||
}
|
|
@ -3,12 +3,13 @@
|
|||
package markdown
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/russross/blackfriday"
|
||||
|
@ -33,16 +34,6 @@ type Markdown struct {
|
|||
IndexFiles []string
|
||||
}
|
||||
|
||||
// IsIndexFile checks to see if a file is an index file
|
||||
func (md Markdown) IsIndexFile(file string) bool {
|
||||
for _, f := range md.IndexFiles {
|
||||
if f == file {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Config stores markdown middleware configurations.
|
||||
type Config struct {
|
||||
// Markdown renderer
|
||||
|
@ -52,7 +43,7 @@ type Config struct {
|
|||
PathScope string
|
||||
|
||||
// List of extensions to consider as markdown files
|
||||
Extensions []string
|
||||
Extensions map[string]struct{}
|
||||
|
||||
// List of style sheets to load for each markdown file
|
||||
Styles []string
|
||||
|
@ -60,116 +51,121 @@ type Config struct {
|
|||
// List of JavaScript files to load for each markdown file
|
||||
Scripts []string
|
||||
|
||||
// Map of registered templates
|
||||
Templates map[string]string
|
||||
|
||||
// Map of request URL to static files generated
|
||||
StaticFiles map[string]string
|
||||
|
||||
// Links to all markdown pages ordered by date.
|
||||
Links []PageLink
|
||||
|
||||
// Stores a directory hash to check for changes.
|
||||
linksHash string
|
||||
|
||||
// Directory to store static files
|
||||
StaticDir string
|
||||
|
||||
// If in development mode. i.e. Actively editing markdown files.
|
||||
Development bool
|
||||
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
// IsValidExt checks to see if an extension is a valid markdown extension
|
||||
// for config.
|
||||
func (c *Config) IsValidExt(ext string) bool {
|
||||
for _, e := range c.Extensions {
|
||||
if e == ext {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
// Template(s) to render with
|
||||
Template *template.Template
|
||||
}
|
||||
|
||||
// ServeHTTP implements the http.Handler interface.
|
||||
func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||
for _, cfg := range md.Configs {
|
||||
if !middleware.Path(r.URL.Path).Matches(cfg.PathScope) {
|
||||
continue
|
||||
var cfg *Config
|
||||
for _, c := range md.Configs {
|
||||
if middleware.Path(r.URL.Path).Matches(c.PathScope) { // not negated
|
||||
cfg = c
|
||||
break // or goto
|
||||
}
|
||||
}
|
||||
if cfg == nil {
|
||||
return md.Next.ServeHTTP(w, r) // exit early
|
||||
}
|
||||
|
||||
// We only deal with HEAD/GET
|
||||
switch r.Method {
|
||||
case http.MethodGet, http.MethodHead:
|
||||
default:
|
||||
return http.StatusMethodNotAllowed, nil
|
||||
}
|
||||
|
||||
var dirents []os.FileInfo
|
||||
var lastModTime time.Time
|
||||
fpath := r.URL.Path
|
||||
if idx, ok := middleware.IndexFile(md.FileSys, fpath, md.IndexFiles); ok {
|
||||
// We're serving a directory index file, which may be a markdown
|
||||
// file with a template. Let's grab a list of files this directory
|
||||
// URL points to, and pass that in to any possible template invocations,
|
||||
// so that templates can customize the look and feel of a directory.
|
||||
fdp, err := md.FileSys.Open(fpath)
|
||||
switch {
|
||||
case err == nil: // nop
|
||||
case os.IsPermission(err):
|
||||
return http.StatusForbidden, err
|
||||
case os.IsExist(err):
|
||||
return http.StatusNotFound, nil
|
||||
default: // did we run out of FD?
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
defer fdp.Close()
|
||||
|
||||
// Grab a possible set of directory entries. Note, we do not check
|
||||
// for errors here (unreadable directory, for example). It may
|
||||
// still be useful to have a directory template file, without the
|
||||
// directory contents being present. Note, the directory's last
|
||||
// modification is also present here (entry ".").
|
||||
dirents, _ = fdp.Readdir(-1)
|
||||
for _, d := range dirents {
|
||||
lastModTime = latest(lastModTime, d.ModTime())
|
||||
}
|
||||
|
||||
fpath := r.URL.Path
|
||||
if idx, ok := middleware.IndexFile(md.FileSys, fpath, md.IndexFiles); ok {
|
||||
fpath = idx
|
||||
}
|
||||
// Set path to found index file
|
||||
fpath = idx
|
||||
}
|
||||
|
||||
for _, ext := range cfg.Extensions {
|
||||
if strings.HasSuffix(fpath, ext) {
|
||||
f, err := md.FileSys.Open(fpath)
|
||||
if err != nil {
|
||||
if os.IsPermission(err) {
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
return http.StatusNotFound, nil
|
||||
}
|
||||
// If not supported extension, pass on it
|
||||
if _, ok := cfg.Extensions[path.Ext(fpath)]; !ok {
|
||||
return md.Next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
fs, err := f.Stat()
|
||||
if err != nil {
|
||||
return http.StatusNotFound, nil
|
||||
}
|
||||
// At this point we have a supported extension/markdown
|
||||
f, err := md.FileSys.Open(fpath)
|
||||
switch {
|
||||
case err == nil: // nop
|
||||
case os.IsPermission(err):
|
||||
return http.StatusForbidden, err
|
||||
case os.IsExist(err):
|
||||
return http.StatusNotFound, nil
|
||||
default: // did we run out of FD?
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
// if development is set, scan directory for file changes for links.
|
||||
if cfg.Development {
|
||||
if err := GenerateStatic(md, cfg); err != nil {
|
||||
log.Printf("[ERROR] markdown: on-demand site generation error: %v", err)
|
||||
}
|
||||
}
|
||||
if fs, err := f.Stat(); err != nil {
|
||||
return http.StatusGone, nil
|
||||
} else {
|
||||
lastModTime = latest(lastModTime, fs.ModTime())
|
||||
}
|
||||
|
||||
cfg.RLock()
|
||||
filepath, ok := cfg.StaticFiles[fpath]
|
||||
cfg.RUnlock()
|
||||
// if static site is generated, attempt to use it
|
||||
if ok {
|
||||
if fs1, err := os.Stat(filepath); err == nil {
|
||||
// if markdown has not been modified since static page
|
||||
// generation, serve the static page
|
||||
if fs.ModTime().Before(fs1.ModTime()) {
|
||||
if html, err := ioutil.ReadFile(filepath); err == nil {
|
||||
middleware.SetLastModifiedHeader(w, fs1.ModTime())
|
||||
w.Write(html)
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
if os.IsPermission(err) {
|
||||
return http.StatusForbidden, err
|
||||
}
|
||||
return http.StatusNotFound, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx := middleware.Context{
|
||||
Root: md.FileSys,
|
||||
Req: r,
|
||||
URL: r.URL,
|
||||
}
|
||||
html, err := cfg.Markdown(title(fpath), f, dirents, ctx)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(int64(len(html)), 10))
|
||||
middleware.SetLastModifiedHeader(w, lastModTime)
|
||||
if r.Method == http.MethodGet {
|
||||
w.Write(html)
|
||||
}
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
ctx := middleware.Context{
|
||||
Root: md.FileSys,
|
||||
Req: r,
|
||||
URL: r.URL,
|
||||
}
|
||||
html, err := md.Process(cfg, fpath, body, ctx)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
// latest returns the latest time.Time
|
||||
func latest(t ...time.Time) time.Time {
|
||||
var last time.Time
|
||||
|
||||
middleware.SetLastModifiedHeader(w, fs.ModTime())
|
||||
w.Write(html)
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
for _, tt := range t {
|
||||
if tt.After(last) {
|
||||
last = tt
|
||||
}
|
||||
}
|
||||
|
||||
// Didn't qualify to serve as markdown; pass-thru
|
||||
return md.Next.ServeHTTP(w, r)
|
||||
return last
|
||||
}
|
||||
|
||||
// title gives a backup generated title for a page
|
||||
func title(p string) string {
|
||||
return strings.TrimRight(path.Base(p), path.Ext(p))
|
||||
}
|
||||
|
|
|
@ -2,13 +2,14 @@ package markdown
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"log"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
|
@ -16,61 +17,55 @@ import (
|
|||
)
|
||||
|
||||
func TestMarkdown(t *testing.T) {
|
||||
templates := make(map[string]string)
|
||||
templates[DefaultTemplate] = "testdata/markdown_tpl.html"
|
||||
rootDir := "./testdata"
|
||||
|
||||
f := func(filename string) string {
|
||||
return filepath.ToSlash(rootDir + string(filepath.Separator) + filename)
|
||||
}
|
||||
|
||||
md := Markdown{
|
||||
Root: "./testdata",
|
||||
FileSys: http.Dir("./testdata"),
|
||||
Root: rootDir,
|
||||
FileSys: http.Dir(rootDir),
|
||||
Configs: []*Config{
|
||||
{
|
||||
Renderer: blackfriday.HtmlRenderer(0, "", ""),
|
||||
PathScope: "/blog",
|
||||
Extensions: []string{".md"},
|
||||
Styles: []string{},
|
||||
Scripts: []string{},
|
||||
Templates: templates,
|
||||
StaticDir: DefaultStaticDir,
|
||||
StaticFiles: make(map[string]string),
|
||||
},
|
||||
{
|
||||
Renderer: blackfriday.HtmlRenderer(0, "", ""),
|
||||
PathScope: "/docflags",
|
||||
Extensions: []string{".md"},
|
||||
Styles: []string{},
|
||||
Scripts: []string{},
|
||||
Templates: map[string]string{
|
||||
DefaultTemplate: "testdata/docflags/template.txt",
|
||||
Renderer: blackfriday.HtmlRenderer(0, "", ""),
|
||||
PathScope: "/blog",
|
||||
Extensions: map[string]struct{}{
|
||||
".md": {},
|
||||
},
|
||||
StaticDir: DefaultStaticDir,
|
||||
StaticFiles: make(map[string]string),
|
||||
Styles: []string{},
|
||||
Scripts: []string{},
|
||||
Template: setDefaultTemplate(f("markdown_tpl.html")),
|
||||
},
|
||||
{
|
||||
Renderer: blackfriday.HtmlRenderer(0, "", ""),
|
||||
PathScope: "/log",
|
||||
Extensions: []string{".md"},
|
||||
Styles: []string{"/resources/css/log.css", "/resources/css/default.css"},
|
||||
Scripts: []string{"/resources/js/log.js", "/resources/js/default.js"},
|
||||
Templates: make(map[string]string),
|
||||
StaticDir: DefaultStaticDir,
|
||||
StaticFiles: make(map[string]string),
|
||||
},
|
||||
{
|
||||
Renderer: blackfriday.HtmlRenderer(0, "", ""),
|
||||
PathScope: "/og",
|
||||
Extensions: []string{".md"},
|
||||
Styles: []string{},
|
||||
Scripts: []string{},
|
||||
Templates: templates,
|
||||
StaticDir: "testdata/og_static",
|
||||
StaticFiles: map[string]string{"/og/first.md": "testdata/og_static/og/first.md/index.html"},
|
||||
Links: []PageLink{
|
||||
{
|
||||
Title: "first",
|
||||
Summary: "",
|
||||
Date: time.Now(),
|
||||
URL: "/og/first.md",
|
||||
},
|
||||
Renderer: blackfriday.HtmlRenderer(0, "", ""),
|
||||
PathScope: "/docflags",
|
||||
Extensions: map[string]struct{}{
|
||||
".md": {},
|
||||
},
|
||||
Styles: []string{},
|
||||
Scripts: []string{},
|
||||
Template: setDefaultTemplate(f("docflags/template.txt")),
|
||||
},
|
||||
{
|
||||
Renderer: blackfriday.HtmlRenderer(0, "", ""),
|
||||
PathScope: "/log",
|
||||
Extensions: map[string]struct{}{
|
||||
".md": {},
|
||||
},
|
||||
Styles: []string{"/resources/css/log.css", "/resources/css/default.css"},
|
||||
Scripts: []string{"/resources/js/log.js", "/resources/js/default.js"},
|
||||
Template: GetDefaultTemplate(),
|
||||
},
|
||||
{
|
||||
Renderer: blackfriday.HtmlRenderer(0, "", ""),
|
||||
PathScope: "/og",
|
||||
Extensions: map[string]struct{}{
|
||||
".md": {},
|
||||
},
|
||||
Styles: []string{},
|
||||
Scripts: []string{},
|
||||
Template: setDefaultTemplate(f("markdown_tpl.html")),
|
||||
},
|
||||
},
|
||||
IndexFiles: []string{"index.html"},
|
||||
|
@ -80,14 +75,6 @@ func TestMarkdown(t *testing.T) {
|
|||
}),
|
||||
}
|
||||
|
||||
for i := range md.Configs {
|
||||
c := md.Configs[i]
|
||||
if err := GenerateStatic(md, c); err != nil {
|
||||
t.Fatalf("Error: %v", err)
|
||||
}
|
||||
Watch(md, c, time.Millisecond*100)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", "/blog/test.md", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create HTTP request: %v", err)
|
||||
|
@ -163,11 +150,9 @@ DocFlags.var_bool true`
|
|||
<title>Markdown test 2</title>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="/resources/css/log.css">
|
||||
<link rel="stylesheet" href="/resources/css/default.css">
|
||||
|
||||
<link rel="stylesheet" href="/resources/css/default.css">
|
||||
<script src="/resources/js/log.js"></script>
|
||||
<script src="/resources/js/default.js"></script>
|
||||
|
||||
<script src="/resources/js/default.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Welcome on the blog</h2>
|
||||
|
@ -192,9 +177,9 @@ DocFlags.var_bool true`
|
|||
}
|
||||
rec = httptest.NewRecorder()
|
||||
currenttime := time.Now().Local().Add(-time.Second)
|
||||
err = os.Chtimes("testdata/og/first.md", currenttime, currenttime)
|
||||
_ = os.Chtimes("testdata/og/first.md", currenttime, currenttime)
|
||||
currenttime = time.Now().Local()
|
||||
err = os.Chtimes("testdata/og_static/og/first.md/index.html", currenttime, currenttime)
|
||||
_ = os.Chtimes("testdata/og_static/og/first.md/index.html", currenttime, currenttime)
|
||||
time.Sleep(time.Millisecond * 200)
|
||||
|
||||
md.ServeHTTP(rec, req)
|
||||
|
@ -219,52 +204,6 @@ Welcome to title!
|
|||
if !equalStrings(respBody, expectedBody) {
|
||||
t.Fatalf("Expected body: %v got: %v", expectedBody, respBody)
|
||||
}
|
||||
|
||||
expectedLinks := []string{
|
||||
"/blog/test.md",
|
||||
"/docflags/test.md",
|
||||
"/log/test.md",
|
||||
}
|
||||
|
||||
for i, c := range md.Configs[:2] {
|
||||
log.Printf("Test number: %d, configuration links: %v, config: %v", i, c.Links, c)
|
||||
if c.Links[0].URL != expectedLinks[i] {
|
||||
t.Fatalf("Expected %v got %v", expectedLinks[i], c.Links[0].URL)
|
||||
}
|
||||
}
|
||||
|
||||
// attempt to trigger race conditions
|
||||
var w sync.WaitGroup
|
||||
f := func() {
|
||||
req, err := http.NewRequest("GET", "/log/test.md", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not create HTTP request: %v", err)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
md.ServeHTTP(rec, req)
|
||||
w.Done()
|
||||
}
|
||||
for i := 0; i < 5; i++ {
|
||||
w.Add(1)
|
||||
go f()
|
||||
}
|
||||
w.Wait()
|
||||
|
||||
f = func() {
|
||||
GenerateStatic(md, md.Configs[0])
|
||||
w.Done()
|
||||
}
|
||||
for i := 0; i < 5; i++ {
|
||||
w.Add(1)
|
||||
go f()
|
||||
}
|
||||
w.Wait()
|
||||
|
||||
if err = os.RemoveAll(DefaultStaticDir); err != nil {
|
||||
t.Errorf("Error while removing the generated static files: %v", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func equalStrings(s1, s2 string) bool {
|
||||
|
@ -280,3 +219,12 @@ func equalStrings(s1, s2 string) bool {
|
|||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func setDefaultTemplate(filename string) *template.Template {
|
||||
buf, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return template.Must(GetDefaultTemplate().Parse(string(buf)))
|
||||
}
|
||||
|
|
|
@ -1,242 +0,0 @@
|
|||
package markdown
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// Metadata stores a page's metadata
|
||||
type Metadata struct {
|
||||
// Page title
|
||||
Title string
|
||||
|
||||
// Page template
|
||||
Template string
|
||||
|
||||
// Publish date
|
||||
Date time.Time
|
||||
|
||||
// Variables to be used with Template
|
||||
Variables map[string]string
|
||||
|
||||
// Flags to be used with Template
|
||||
Flags map[string]bool
|
||||
}
|
||||
|
||||
// load loads parsed values in parsedMap into Metadata
|
||||
func (m *Metadata) load(parsedMap map[string]interface{}) {
|
||||
if title, ok := parsedMap["title"]; ok {
|
||||
m.Title, _ = title.(string)
|
||||
}
|
||||
if template, ok := parsedMap["template"]; ok {
|
||||
m.Template, _ = template.(string)
|
||||
}
|
||||
if date, ok := parsedMap["date"].(string); ok {
|
||||
if t, err := time.Parse(timeLayout, date); err == nil {
|
||||
m.Date = t
|
||||
}
|
||||
}
|
||||
// store everything as a variable
|
||||
for key, val := range parsedMap {
|
||||
switch v := val.(type) {
|
||||
case string:
|
||||
m.Variables[key] = v
|
||||
case bool:
|
||||
m.Flags[key] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MetadataParser is a an interface that must be satisfied by each parser
|
||||
type MetadataParser interface {
|
||||
// Opening identifier
|
||||
Opening() []byte
|
||||
|
||||
// Closing identifier
|
||||
Closing() []byte
|
||||
|
||||
// Parse the metadata.
|
||||
// Returns the remaining page contents (Markdown)
|
||||
// after extracting metadata
|
||||
Parse([]byte) ([]byte, error)
|
||||
|
||||
// Parsed metadata.
|
||||
// Should be called after a call to Parse returns no error
|
||||
Metadata() Metadata
|
||||
}
|
||||
|
||||
// JSONMetadataParser is the MetadataParser for JSON
|
||||
type JSONMetadataParser struct {
|
||||
metadata Metadata
|
||||
}
|
||||
|
||||
// Parse the metadata
|
||||
func (j *JSONMetadataParser) Parse(b []byte) ([]byte, error) {
|
||||
b, markdown, err := extractMetadata(j, b)
|
||||
if err != nil {
|
||||
return markdown, err
|
||||
}
|
||||
m := make(map[string]interface{})
|
||||
|
||||
// Read the preceding JSON object
|
||||
decoder := json.NewDecoder(bytes.NewReader(b))
|
||||
if err := decoder.Decode(&m); err != nil {
|
||||
return markdown, err
|
||||
}
|
||||
j.metadata.load(m)
|
||||
|
||||
return markdown, nil
|
||||
}
|
||||
|
||||
// Metadata returns parsed metadata. It should be called
|
||||
// only after a call to Parse returns without error.
|
||||
func (j *JSONMetadataParser) Metadata() Metadata {
|
||||
return j.metadata
|
||||
}
|
||||
|
||||
// Opening returns the opening identifier JSON metadata
|
||||
func (j *JSONMetadataParser) Opening() []byte {
|
||||
return []byte("{")
|
||||
}
|
||||
|
||||
// Closing returns the closing identifier JSON metadata
|
||||
func (j *JSONMetadataParser) Closing() []byte {
|
||||
return []byte("}")
|
||||
}
|
||||
|
||||
// TOMLMetadataParser is the MetadataParser for TOML
|
||||
type TOMLMetadataParser struct {
|
||||
metadata Metadata
|
||||
}
|
||||
|
||||
// Parse the metadata
|
||||
func (t *TOMLMetadataParser) Parse(b []byte) ([]byte, error) {
|
||||
b, markdown, err := extractMetadata(t, b)
|
||||
if err != nil {
|
||||
return markdown, err
|
||||
}
|
||||
m := make(map[string]interface{})
|
||||
if err := toml.Unmarshal(b, &m); err != nil {
|
||||
return markdown, err
|
||||
}
|
||||
t.metadata.load(m)
|
||||
return markdown, nil
|
||||
}
|
||||
|
||||
// Metadata returns parsed metadata. It should be called
|
||||
// only after a call to Parse returns without error.
|
||||
func (t *TOMLMetadataParser) Metadata() Metadata {
|
||||
return t.metadata
|
||||
}
|
||||
|
||||
// Opening returns the opening identifier TOML metadata
|
||||
func (t *TOMLMetadataParser) Opening() []byte {
|
||||
return []byte("+++")
|
||||
}
|
||||
|
||||
// Closing returns the closing identifier TOML metadata
|
||||
func (t *TOMLMetadataParser) Closing() []byte {
|
||||
return []byte("+++")
|
||||
}
|
||||
|
||||
// YAMLMetadataParser is the MetadataParser for YAML
|
||||
type YAMLMetadataParser struct {
|
||||
metadata Metadata
|
||||
}
|
||||
|
||||
// Parse the metadata
|
||||
func (y *YAMLMetadataParser) Parse(b []byte) ([]byte, error) {
|
||||
b, markdown, err := extractMetadata(y, b)
|
||||
if err != nil {
|
||||
return markdown, err
|
||||
}
|
||||
|
||||
m := make(map[string]interface{})
|
||||
if err := yaml.Unmarshal(b, &m); err != nil {
|
||||
return markdown, err
|
||||
}
|
||||
y.metadata.load(m)
|
||||
return markdown, nil
|
||||
}
|
||||
|
||||
// Metadata returns parsed metadata. It should be called
|
||||
// only after a call to Parse returns without error.
|
||||
func (y *YAMLMetadataParser) Metadata() Metadata {
|
||||
return y.metadata
|
||||
}
|
||||
|
||||
// Opening returns the opening identifier YAML metadata
|
||||
func (y *YAMLMetadataParser) Opening() []byte {
|
||||
return []byte("---")
|
||||
}
|
||||
|
||||
// Closing returns the closing identifier YAML metadata
|
||||
func (y *YAMLMetadataParser) Closing() []byte {
|
||||
return []byte("---")
|
||||
}
|
||||
|
||||
// extractMetadata separates metadata content from from markdown content in b.
|
||||
// It returns the metadata, the remaining bytes (markdown), and an error, if any.
|
||||
func extractMetadata(parser MetadataParser, b []byte) (metadata []byte, markdown []byte, err error) {
|
||||
b = bytes.TrimSpace(b)
|
||||
openingLine := parser.Opening()
|
||||
closingLine := parser.Closing()
|
||||
if !bytes.HasPrefix(b, openingLine) {
|
||||
return nil, b, fmt.Errorf("first line missing expected metadata identifier")
|
||||
}
|
||||
metaStart := len(openingLine)
|
||||
if _, ok := parser.(*JSONMetadataParser); ok {
|
||||
metaStart = 0
|
||||
}
|
||||
metaEnd := bytes.Index(b[metaStart:], closingLine)
|
||||
if metaEnd == -1 {
|
||||
return nil, nil, fmt.Errorf("metadata not closed ('%s' not found)", parser.Closing())
|
||||
}
|
||||
metaEnd += metaStart
|
||||
if _, ok := parser.(*JSONMetadataParser); ok {
|
||||
metaEnd += len(closingLine)
|
||||
}
|
||||
metadata = b[metaStart:metaEnd]
|
||||
markdown = b[metaEnd:]
|
||||
if _, ok := parser.(*JSONMetadataParser); !ok {
|
||||
markdown = b[metaEnd+len(closingLine):]
|
||||
}
|
||||
return metadata, markdown, nil
|
||||
}
|
||||
|
||||
// findParser finds the parser using line that contains opening identifier
|
||||
func findParser(b []byte) MetadataParser {
|
||||
var line []byte
|
||||
// Read first line
|
||||
if _, err := fmt.Fscanln(bytes.NewReader(b), &line); err != nil {
|
||||
return nil
|
||||
}
|
||||
line = bytes.TrimSpace(line)
|
||||
for _, parser := range parsers() {
|
||||
if bytes.Equal(parser.Opening(), line) {
|
||||
return parser
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func newMetadata() Metadata {
|
||||
return Metadata{
|
||||
Variables: make(map[string]string),
|
||||
Flags: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// parsers returns all available parsers
|
||||
func parsers() []MetadataParser {
|
||||
return []MetadataParser{
|
||||
&JSONMetadataParser{metadata: newMetadata()},
|
||||
&TOMLMetadataParser{metadata: newMetadata()},
|
||||
&YAMLMetadataParser{metadata: newMetadata()},
|
||||
}
|
||||
}
|
158
middleware/markdown/metadata/metadata.go
Normal file
158
middleware/markdown/metadata/metadata.go
Normal file
|
@ -0,0 +1,158 @@
|
|||
package metadata
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// Date format YYYY-MM-DD HH:MM:SS or YYYY-MM-DD
|
||||
timeLayout = []string{
|
||||
`2006-01-02 15:04:05-0700`,
|
||||
`2006-01-02 15:04:05`,
|
||||
`2006-01-02`,
|
||||
}
|
||||
)
|
||||
|
||||
// Metadata stores a page's metadata
|
||||
type Metadata struct {
|
||||
// Page title
|
||||
Title string
|
||||
|
||||
// Page template
|
||||
Template string
|
||||
|
||||
// Publish date
|
||||
Date time.Time
|
||||
|
||||
// Variables to be used with Template
|
||||
Variables map[string]string
|
||||
|
||||
// Flags to be used with Template
|
||||
Flags map[string]bool
|
||||
}
|
||||
|
||||
// NewMetadata() returns a new Metadata struct, loaded with the given map
|
||||
func NewMetadata(parsedMap map[string]interface{}) Metadata {
|
||||
md := Metadata{
|
||||
Variables: make(map[string]string),
|
||||
Flags: make(map[string]bool),
|
||||
}
|
||||
md.load(parsedMap)
|
||||
|
||||
return md
|
||||
}
|
||||
|
||||
// load loads parsed values in parsedMap into Metadata
|
||||
func (m *Metadata) load(parsedMap map[string]interface{}) {
|
||||
|
||||
// Pull top level things out
|
||||
if title, ok := parsedMap["title"]; ok {
|
||||
m.Title, _ = title.(string)
|
||||
}
|
||||
if template, ok := parsedMap["template"]; ok {
|
||||
m.Template, _ = template.(string)
|
||||
}
|
||||
if date, ok := parsedMap["date"].(string); ok {
|
||||
for _, layout := range timeLayout {
|
||||
if t, err := time.Parse(layout, date); err == nil {
|
||||
m.Date = t
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Store everything as a flag or variable
|
||||
for key, val := range parsedMap {
|
||||
switch v := val.(type) {
|
||||
case bool:
|
||||
m.Flags[key] = v
|
||||
case string:
|
||||
m.Variables[key] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MetadataParser is a an interface that must be satisfied by each parser
|
||||
type MetadataParser interface {
|
||||
// Initialize a parser
|
||||
Init(b *bytes.Buffer) bool
|
||||
|
||||
// Type of metadata
|
||||
Type() string
|
||||
|
||||
// Parsed metadata.
|
||||
Metadata() Metadata
|
||||
|
||||
// Raw markdown.
|
||||
Markdown() []byte
|
||||
}
|
||||
|
||||
// GetParser returns a parser for the given data
|
||||
func GetParser(buf []byte) MetadataParser {
|
||||
for _, p := range parsers() {
|
||||
b := bytes.NewBuffer(buf)
|
||||
if p.Init(b) {
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// parsers returns all available parsers
|
||||
func parsers() []MetadataParser {
|
||||
return []MetadataParser{
|
||||
&TOMLMetadataParser{},
|
||||
&YAMLMetadataParser{},
|
||||
&JSONMetadataParser{},
|
||||
|
||||
// This one must be last
|
||||
&NoneMetadataParser{},
|
||||
}
|
||||
}
|
||||
|
||||
// Split out prefixed/suffixed metadata with given delimiter
|
||||
func splitBuffer(b *bytes.Buffer, delim string) (*bytes.Buffer, *bytes.Buffer) {
|
||||
scanner := bufio.NewScanner(b)
|
||||
|
||||
// Read and check first line
|
||||
if !scanner.Scan() {
|
||||
return nil, nil
|
||||
}
|
||||
if string(bytes.TrimSpace(scanner.Bytes())) != delim {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Accumulate metadata, until delimiter
|
||||
meta := bytes.NewBuffer(nil)
|
||||
for scanner.Scan() {
|
||||
if string(bytes.TrimSpace(scanner.Bytes())) == delim {
|
||||
break
|
||||
}
|
||||
if _, err := meta.Write(scanner.Bytes()); err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
if _, err := meta.WriteRune('\n'); err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
// Make sure we saw closing delimiter
|
||||
if string(bytes.TrimSpace(scanner.Bytes())) != delim {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// The rest is markdown
|
||||
markdown := new(bytes.Buffer)
|
||||
for scanner.Scan() {
|
||||
if _, err := markdown.Write(scanner.Bytes()); err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
if _, err := markdown.WriteRune('\n'); err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
return meta, markdown
|
||||
}
|
53
middleware/markdown/metadata/metadata_json.go
Normal file
53
middleware/markdown/metadata/metadata_json.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
package metadata
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
// JSONMetadataParser is the MetadataParser for JSON
|
||||
type JSONMetadataParser struct {
|
||||
metadata Metadata
|
||||
markdown *bytes.Buffer
|
||||
}
|
||||
|
||||
func (j *JSONMetadataParser) Type() string {
|
||||
return "JSON"
|
||||
}
|
||||
|
||||
// Parse metadata/markdown file
|
||||
func (j *JSONMetadataParser) Init(b *bytes.Buffer) bool {
|
||||
m := make(map[string]interface{})
|
||||
|
||||
err := json.Unmarshal(b.Bytes(), &m)
|
||||
if err != nil {
|
||||
var offset int
|
||||
|
||||
if jerr, ok := err.(*json.SyntaxError); !ok {
|
||||
return false
|
||||
} else {
|
||||
offset = int(jerr.Offset)
|
||||
}
|
||||
|
||||
m = make(map[string]interface{})
|
||||
err = json.Unmarshal(b.Next(offset-1), &m)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
j.metadata = NewMetadata(m)
|
||||
j.markdown = bytes.NewBuffer(b.Bytes())
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Metadata returns parsed metadata. It should be called
|
||||
// only after a call to Parse returns without error.
|
||||
func (j *JSONMetadataParser) Metadata() Metadata {
|
||||
return j.metadata
|
||||
}
|
||||
|
||||
func (j *JSONMetadataParser) Markdown() []byte {
|
||||
return j.markdown.Bytes()
|
||||
}
|
39
middleware/markdown/metadata/metadata_none.go
Normal file
39
middleware/markdown/metadata/metadata_none.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package metadata
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
// TOMLMetadataParser is the MetadataParser for TOML
|
||||
type NoneMetadataParser struct {
|
||||
metadata Metadata
|
||||
markdown *bytes.Buffer
|
||||
}
|
||||
|
||||
func (n *NoneMetadataParser) Type() string {
|
||||
return "None"
|
||||
}
|
||||
|
||||
// Parse metadata/markdown file
|
||||
func (n *NoneMetadataParser) Init(b *bytes.Buffer) bool {
|
||||
m := make(map[string]interface{})
|
||||
n.metadata = NewMetadata(m)
|
||||
n.markdown = bytes.NewBuffer(b.Bytes())
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Parse the metadata
|
||||
func (n *NoneMetadataParser) Parse(b []byte) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Metadata returns parsed metadata. It should be called
|
||||
// only after a call to Parse returns without error.
|
||||
func (n *NoneMetadataParser) Metadata() Metadata {
|
||||
return n.metadata
|
||||
}
|
||||
|
||||
func (n *NoneMetadataParser) Markdown() []byte {
|
||||
return n.markdown.Bytes()
|
||||
}
|
|
@ -1,9 +1,7 @@
|
|||
package markdown
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
@ -164,56 +162,52 @@ func TestParsers(t *testing.T) {
|
|||
testData [5]string
|
||||
name string
|
||||
}{
|
||||
{&JSONMetadataParser{metadata: newMetadata()}, JSON, "json"},
|
||||
{&YAMLMetadataParser{metadata: newMetadata()}, YAML, "yaml"},
|
||||
{&TOMLMetadataParser{metadata: newMetadata()}, TOML, "toml"},
|
||||
{&JSONMetadataParser{}, JSON, "JSON"},
|
||||
{&YAMLMetadataParser{}, YAML, "YAML"},
|
||||
{&TOMLMetadataParser{}, TOML, "TOML"},
|
||||
}
|
||||
|
||||
for _, v := range data {
|
||||
// metadata without identifiers
|
||||
if _, err := v.parser.Parse([]byte(v.testData[0])); err == nil {
|
||||
if v.parser.Init(bytes.NewBufferString(v.testData[0])) {
|
||||
t.Fatalf("Expected error for invalid metadata for %v", v.name)
|
||||
}
|
||||
|
||||
// metadata with identifiers
|
||||
md, err := v.parser.Parse([]byte(v.testData[1]))
|
||||
check(t, err)
|
||||
if !v.parser.Init(bytes.NewBufferString(v.testData[1])) {
|
||||
t.Fatalf("Metadata failed to initialize, type %v", v.parser.Type())
|
||||
}
|
||||
md := v.parser.Markdown()
|
||||
if !compare(v.parser.Metadata()) {
|
||||
t.Fatalf("Expected %v, found %v for %v", expected, v.parser.Metadata(), v.name)
|
||||
}
|
||||
if "Page content" != strings.TrimSpace(string(md)) {
|
||||
t.Fatalf("Expected %v, found %v for %v", "Page content", string(md), v.name)
|
||||
}
|
||||
|
||||
var line []byte
|
||||
fmt.Fscanln(bytes.NewReader([]byte(v.testData[1])), &line)
|
||||
if parser := findParser(line); parser == nil {
|
||||
t.Fatalf("Parser must be found for %v", v.name)
|
||||
} else {
|
||||
if reflect.TypeOf(parser) != reflect.TypeOf(v.parser) {
|
||||
t.Fatalf("parsers not equal. %v != %v", reflect.TypeOf(parser), reflect.TypeOf(v.parser))
|
||||
}
|
||||
// Check that we find the correct metadata parser type
|
||||
if p := GetParser([]byte(v.testData[1])); p.Type() != v.name {
|
||||
t.Fatalf("Wrong parser found, expected %v, found %v", v.name, p.Type())
|
||||
}
|
||||
|
||||
// metadata without closing identifier
|
||||
if _, err := v.parser.Parse([]byte(v.testData[2])); err == nil {
|
||||
t.Fatalf("Expected error for missing closing identifier for %v", v.name)
|
||||
if v.parser.Init(bytes.NewBufferString(v.testData[2])) {
|
||||
t.Fatalf("Expected error for missing closing identifier for %v parser", v.name)
|
||||
}
|
||||
|
||||
// invalid metadata
|
||||
if _, err = v.parser.Parse([]byte(v.testData[3])); err == nil {
|
||||
if v.parser.Init(bytes.NewBufferString(v.testData[3])) {
|
||||
t.Fatalf("Expected error for invalid metadata for %v", v.name)
|
||||
}
|
||||
|
||||
// front matter but no body
|
||||
if _, err = v.parser.Parse([]byte(v.testData[4])); err != nil {
|
||||
if !v.parser.Init(bytes.NewBufferString(v.testData[4])) {
|
||||
t.Fatalf("Unexpected error for valid metadata but no body for %v", v.name)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestLargeBody(t *testing.T) {
|
||||
|
||||
var JSON = `{
|
||||
"template": "chapter"
|
||||
}
|
||||
|
@ -232,24 +226,36 @@ Mycket olika byggnader har man i de nordiska rikena: pyramidformiga, kilformiga,
|
|||
template : chapter
|
||||
---
|
||||
|
||||
Mycket olika byggnader har man i de nordiska rikena: pyramidformiga, kilformiga, välvda, runda och fyrkantiga. De pyramidformiga består helt enkelt av träribbor, som upptill löper samman och nedtill bildar en vidare krets; de är avsedda att användas av hantverkarna under sommaren, för att de inte ska plågas av solen, på samma gång som de besväras av rök och eld. De kilformiga husen är i regel försedda med höga tak, för att de täta och tunga snömassorna fortare ska kunna blåsa av och inte tynga ned taken. Dessa är täckta av björknäver, tegel eller kluvet spån av furu - för kådans skull -, gran, ek eller bok; taken på de förmögnas hus däremot med plåtar av koppar eller bly, i likhet med kyrktaken. Valvbyggnaderna uppförs ganska konstnärligt till skydd mot våldsamma vindar och snöfall, görs av sten eller trä, och är avsedda för olika alldagliga viktiga ändamål. Liknande byggnader kan finnas i stormännens gårdar där de används som förvaringsrum för husgeråd och jordbruksredskap. De runda byggnaderna - som för övrigt är de högst sällsynta - används av konstnärer, som vid sitt arbete behöver ett jämnt fördelat ljus från taket. Vanligast är de fyrkantiga husen, vars grova bjälkar är synnerligen väl hopfogade i hörnen - ett sant mästerverk av byggnadskonst; även dessa har fönster högt uppe i taken, för att dagsljuset skall kunna strömma in och ge alla därinne full belysning. Stenhusen har dörröppningar i förhållande till byggnadens storlek, men smala fönstergluggar, som skydd mot den stränga kölden, frosten och snön. Vore de större och vidare, såsom fönstren i Italien, skulle husen i följd av den fint yrande snön, som röres upp av den starka blåsten, precis som dammet av virvelvinden, snart nog fyllas med massor av snö och inte kunna stå emot dess tryck, utan störta samman.
|
||||
|
||||
`
|
||||
var NONE = `
|
||||
|
||||
Mycket olika byggnader har man i de nordiska rikena: pyramidformiga, kilformiga, välvda, runda och fyrkantiga. De pyramidformiga består helt enkelt av träribbor, som upptill löper samman och nedtill bildar en vidare krets; de är avsedda att användas av hantverkarna under sommaren, för att de inte ska plågas av solen, på samma gång som de besväras av rök och eld. De kilformiga husen är i regel försedda med höga tak, för att de täta och tunga snömassorna fortare ska kunna blåsa av och inte tynga ned taken. Dessa är täckta av björknäver, tegel eller kluvet spån av furu - för kådans skull -, gran, ek eller bok; taken på de förmögnas hus däremot med plåtar av koppar eller bly, i likhet med kyrktaken. Valvbyggnaderna uppförs ganska konstnärligt till skydd mot våldsamma vindar och snöfall, görs av sten eller trä, och är avsedda för olika alldagliga viktiga ändamål. Liknande byggnader kan finnas i stormännens gårdar där de används som förvaringsrum för husgeråd och jordbruksredskap. De runda byggnaderna - som för övrigt är de högst sällsynta - används av konstnärer, som vid sitt arbete behöver ett jämnt fördelat ljus från taket. Vanligast är de fyrkantiga husen, vars grova bjälkar är synnerligen väl hopfogade i hörnen - ett sant mästerverk av byggnadskonst; även dessa har fönster högt uppe i taken, för att dagsljuset skall kunna strömma in och ge alla därinne full belysning. Stenhusen har dörröppningar i förhållande till byggnadens storlek, men smala fönstergluggar, som skydd mot den stränga kölden, frosten och snön. Vore de större och vidare, såsom fönstren i Italien, skulle husen i följd av den fint yrande snön, som röres upp av den starka blåsten, precis som dammet av virvelvinden, snart nog fyllas med massor av snö och inte kunna stå emot dess tryck, utan störta samman.
|
||||
|
||||
`
|
||||
var expectedBody = `Mycket olika byggnader har man i de nordiska rikena: pyramidformiga, kilformiga, välvda, runda och fyrkantiga. De pyramidformiga består helt enkelt av träribbor, som upptill löper samman och nedtill bildar en vidare krets; de är avsedda att användas av hantverkarna under sommaren, för att de inte ska plågas av solen, på samma gång som de besväras av rök och eld. De kilformiga husen är i regel försedda med höga tak, för att de täta och tunga snömassorna fortare ska kunna blåsa av och inte tynga ned taken. Dessa är täckta av björknäver, tegel eller kluvet spån av furu - för kådans skull -, gran, ek eller bok; taken på de förmögnas hus däremot med plåtar av koppar eller bly, i likhet med kyrktaken. Valvbyggnaderna uppförs ganska konstnärligt till skydd mot våldsamma vindar och snöfall, görs av sten eller trä, och är avsedda för olika alldagliga viktiga ändamål. Liknande byggnader kan finnas i stormännens gårdar där de används som förvaringsrum för husgeråd och jordbruksredskap. De runda byggnaderna - som för övrigt är de högst sällsynta - används av konstnärer, som vid sitt arbete behöver ett jämnt fördelat ljus från taket. Vanligast är de fyrkantiga husen, vars grova bjälkar är synnerligen väl hopfogade i hörnen - ett sant mästerverk av byggnadskonst; även dessa har fönster högt uppe i taken, för att dagsljuset skall kunna strömma in och ge alla därinne full belysning. Stenhusen har dörröppningar i förhållande till byggnadens storlek, men smala fönstergluggar, som skydd mot den stränga kölden, frosten och snön. Vore de större och vidare, såsom fönstren i Italien, skulle husen i följd av den fint yrande snön, som röres upp av den starka blåsten, precis som dammet av virvelvinden, snart nog fyllas med massor av snö och inte kunna stå emot dess tryck, utan störta samman.
|
||||
`
|
||||
|
||||
data := []struct {
|
||||
parser MetadataParser
|
||||
pType string
|
||||
testData string
|
||||
name string
|
||||
}{
|
||||
{&JSONMetadataParser{metadata: newMetadata()}, JSON, "json"},
|
||||
{&YAMLMetadataParser{metadata: newMetadata()}, YAML, "yaml"},
|
||||
{&TOMLMetadataParser{metadata: newMetadata()}, TOML, "toml"},
|
||||
{"JSON", JSON},
|
||||
{"TOML", TOML},
|
||||
{"YAML", YAML},
|
||||
{"None", NONE},
|
||||
}
|
||||
for _, v := range data {
|
||||
// metadata without identifiers
|
||||
if md, err := v.parser.Parse([]byte(v.testData)); err != nil || strings.TrimSpace(string(md)) != strings.TrimSpace(expectedBody) {
|
||||
t.Fatalf("Error not expected and/or markdown not equal for %v", v.name)
|
||||
p := GetParser([]byte(v.testData))
|
||||
if v.pType != p.Type() {
|
||||
t.Fatalf("Wrong parser type, expected %v, got %v", v.pType, p.Type())
|
||||
}
|
||||
md := p.Markdown()
|
||||
if strings.TrimSpace(string(md)) != strings.TrimSpace(expectedBody) {
|
||||
t.Log("Provided:", v.testData)
|
||||
t.Log("Returned:", p.Markdown())
|
||||
t.Fatalf("Error, mismatched body in expected type %v, matched type %v", v.pType, p.Type())
|
||||
}
|
||||
}
|
||||
}
|
44
middleware/markdown/metadata/metadata_toml.go
Normal file
44
middleware/markdown/metadata/metadata_toml.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package metadata
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
// TOMLMetadataParser is the MetadataParser for TOML
|
||||
type TOMLMetadataParser struct {
|
||||
metadata Metadata
|
||||
markdown *bytes.Buffer
|
||||
}
|
||||
|
||||
func (t *TOMLMetadataParser) Type() string {
|
||||
return "TOML"
|
||||
}
|
||||
|
||||
// Parse metadata/markdown file
|
||||
func (t *TOMLMetadataParser) Init(b *bytes.Buffer) bool {
|
||||
meta, data := splitBuffer(b, "+++")
|
||||
if meta == nil || data == nil {
|
||||
return false
|
||||
}
|
||||
t.markdown = data
|
||||
|
||||
m := make(map[string]interface{})
|
||||
if err := toml.Unmarshal(meta.Bytes(), &m); err != nil {
|
||||
return false
|
||||
}
|
||||
t.metadata = NewMetadata(m)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Metadata returns parsed metadata. It should be called
|
||||
// only after a call to Parse returns without error.
|
||||
func (t *TOMLMetadataParser) Metadata() Metadata {
|
||||
return t.metadata
|
||||
}
|
||||
|
||||
func (t *TOMLMetadataParser) Markdown() []byte {
|
||||
return t.markdown.Bytes()
|
||||
}
|
43
middleware/markdown/metadata/metadata_yaml.go
Normal file
43
middleware/markdown/metadata/metadata_yaml.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package metadata
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// YAMLMetadataParser is the MetadataParser for YAML
|
||||
type YAMLMetadataParser struct {
|
||||
metadata Metadata
|
||||
markdown *bytes.Buffer
|
||||
}
|
||||
|
||||
func (y *YAMLMetadataParser) Type() string {
|
||||
return "YAML"
|
||||
}
|
||||
|
||||
func (y *YAMLMetadataParser) Init(b *bytes.Buffer) bool {
|
||||
meta, data := splitBuffer(b, "---")
|
||||
if meta == nil || data == nil {
|
||||
return false
|
||||
}
|
||||
y.markdown = data
|
||||
|
||||
m := make(map[string]interface{})
|
||||
if err := yaml.Unmarshal(meta.Bytes(), &m); err != nil {
|
||||
return false
|
||||
}
|
||||
y.metadata = NewMetadata(m)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Metadata returns parsed metadata. It should be called
|
||||
// only after a call to Parse returns without error.
|
||||
func (y *YAMLMetadataParser) Metadata() Metadata {
|
||||
return y.metadata
|
||||
}
|
||||
|
||||
func (y *YAMLMetadataParser) Markdown() []byte {
|
||||
return y.markdown.Bytes()
|
||||
}
|
|
@ -1,169 +0,0 @@
|
|||
package markdown
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/russross/blackfriday"
|
||||
)
|
||||
|
||||
const (
|
||||
// Date format YYYY-MM-DD HH:MM:SS
|
||||
timeLayout = `2006-01-02 15:04:05`
|
||||
|
||||
// Maximum length of page summary.
|
||||
summaryLen = 500
|
||||
)
|
||||
|
||||
// PageLink represents a statically generated markdown page.
|
||||
type PageLink struct {
|
||||
Title string
|
||||
Summary string
|
||||
Date time.Time
|
||||
URL string
|
||||
}
|
||||
|
||||
// byDate sorts PageLink by newest date to oldest.
|
||||
type byDate []PageLink
|
||||
|
||||
func (p byDate) Len() int { return len(p) }
|
||||
func (p byDate) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||
func (p byDate) Less(i, j int) bool { return p[i].Date.After(p[j].Date) }
|
||||
|
||||
type linkGen struct {
|
||||
generating bool
|
||||
waiters int
|
||||
lastErr error
|
||||
sync.RWMutex
|
||||
sync.WaitGroup
|
||||
}
|
||||
|
||||
func (l *linkGen) addWaiter() {
|
||||
l.WaitGroup.Add(1)
|
||||
l.waiters++
|
||||
}
|
||||
|
||||
func (l *linkGen) discardWaiters() {
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
for i := 0; i < l.waiters; i++ {
|
||||
l.Done()
|
||||
}
|
||||
}
|
||||
|
||||
func (l *linkGen) started() bool {
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
return l.generating
|
||||
}
|
||||
|
||||
// generateLinks generate links to markdown files if there are file changes.
|
||||
// It returns true when generation is done and false otherwise.
|
||||
func (l *linkGen) generateLinks(md Markdown, cfg *Config) bool {
|
||||
l.Lock()
|
||||
l.generating = true
|
||||
l.Unlock()
|
||||
|
||||
fp := filepath.Join(md.Root, cfg.PathScope) // path to scan for .md files
|
||||
|
||||
// If the file path to scan for Markdown files (fp) does
|
||||
// not exist, there are no markdown files to scan for.
|
||||
if _, err := os.Stat(fp); os.IsNotExist(err) {
|
||||
l.Lock()
|
||||
l.lastErr = err
|
||||
l.generating = false
|
||||
l.Unlock()
|
||||
return false
|
||||
}
|
||||
|
||||
hash, err := computeDirHash(md, cfg)
|
||||
|
||||
// same hash, return.
|
||||
if err == nil && hash == cfg.linksHash {
|
||||
l.Lock()
|
||||
l.generating = false
|
||||
l.Unlock()
|
||||
return false
|
||||
} else if err != nil {
|
||||
log.Printf("[ERROR] markdown: Hash error: %v", err)
|
||||
}
|
||||
|
||||
cfg.Links = []PageLink{}
|
||||
|
||||
cfg.Lock()
|
||||
l.lastErr = filepath.Walk(fp, func(path string, info os.FileInfo, err error) error {
|
||||
for _, ext := range cfg.Extensions {
|
||||
if info.IsDir() || !strings.HasSuffix(info.Name(), ext) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Load the file
|
||||
body, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Get the relative path as if it were a HTTP request,
|
||||
// then prepend with "/" (like a real HTTP request)
|
||||
reqPath, err := filepath.Rel(md.Root, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqPath = "/" + filepath.ToSlash(reqPath)
|
||||
|
||||
// Make the summary
|
||||
parser := findParser(body)
|
||||
if parser == nil {
|
||||
// no metadata, ignore.
|
||||
continue
|
||||
}
|
||||
summaryRaw, err := parser.Parse(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
summary := blackfriday.Markdown(summaryRaw, SummaryRenderer{}, 0)
|
||||
|
||||
// truncate summary to maximum length
|
||||
if len(summary) > summaryLen {
|
||||
summary = summary[:summaryLen]
|
||||
|
||||
// trim to nearest word
|
||||
lastSpace := bytes.LastIndex(summary, []byte(" "))
|
||||
if lastSpace != -1 {
|
||||
summary = summary[:lastSpace]
|
||||
}
|
||||
}
|
||||
|
||||
metadata := parser.Metadata()
|
||||
|
||||
cfg.Links = append(cfg.Links, PageLink{
|
||||
Title: metadata.Title,
|
||||
URL: reqPath,
|
||||
Date: metadata.Date,
|
||||
Summary: string(summary),
|
||||
})
|
||||
|
||||
break // don't try other file extensions
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
// sort by newest date
|
||||
sort.Sort(byDate(cfg.Links))
|
||||
|
||||
cfg.linksHash = hash
|
||||
cfg.Unlock()
|
||||
|
||||
l.Lock()
|
||||
l.generating = false
|
||||
l.Unlock()
|
||||
return true
|
||||
}
|
|
@ -1,216 +1,74 @@
|
|||
package markdown
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/markdown/metadata"
|
||||
"github.com/mholt/caddy/middleware/markdown/summary"
|
||||
"github.com/russross/blackfriday"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultTemplate is the default template.
|
||||
DefaultTemplate = "defaultTemplate"
|
||||
// DefaultStaticDir is the default static directory.
|
||||
DefaultStaticDir = "generated_site"
|
||||
)
|
||||
|
||||
// Data represents a markdown document.
|
||||
type Data struct {
|
||||
middleware.Context
|
||||
Doc map[string]string
|
||||
DocFlags map[string]bool
|
||||
Links []PageLink
|
||||
type FileInfo struct {
|
||||
os.FileInfo
|
||||
ctx middleware.Context
|
||||
}
|
||||
|
||||
// Include "overrides" the embedded middleware.Context's Include()
|
||||
// method so that included files have access to d's fields.
|
||||
func (d Data) Include(filename string) (string, error) {
|
||||
return middleware.ContextInclude(filename, d, d.Root)
|
||||
func (f FileInfo) Summarize(wordcount int) (string, error) {
|
||||
fp, err := f.ctx.Root.Open(f.Name())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
buf, err := ioutil.ReadAll(fp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(summary.Markdown(buf, wordcount)), nil
|
||||
}
|
||||
|
||||
// Process processes the contents of a page in b. It parses the metadata
|
||||
// Markdown processes the contents of a page in b. It parses the metadata
|
||||
// (if any) and uses the template (if found).
|
||||
func (md Markdown) Process(c *Config, requestPath string, b []byte, ctx middleware.Context) ([]byte, error) {
|
||||
var metadata = newMetadata()
|
||||
var markdown []byte
|
||||
var err error
|
||||
|
||||
// find parser compatible with page contents
|
||||
parser := findParser(b)
|
||||
|
||||
if parser == nil {
|
||||
// if not found, assume whole file is markdown (no front matter)
|
||||
markdown = b
|
||||
} else {
|
||||
// if found, assume metadata present and parse.
|
||||
markdown, err = parser.Parse(b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metadata = parser.Metadata()
|
||||
func (c *Config) Markdown(title string, r io.Reader, dirents []os.FileInfo, ctx middleware.Context) ([]byte, error) {
|
||||
body, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if template is not specified, check if Default template is set
|
||||
if metadata.Template == "" {
|
||||
if _, ok := c.Templates[DefaultTemplate]; ok {
|
||||
metadata.Template = DefaultTemplate
|
||||
}
|
||||
}
|
||||
|
||||
// if template is set, load it
|
||||
var tmpl []byte
|
||||
if metadata.Template != "" {
|
||||
if t, ok := c.Templates[metadata.Template]; ok {
|
||||
tmpl, err = ioutil.ReadFile(t)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
parser := metadata.GetParser(body)
|
||||
markdown := parser.Markdown()
|
||||
mdata := parser.Metadata()
|
||||
|
||||
// process markdown
|
||||
extns := blackfriday.EXTENSION_TABLES | blackfriday.EXTENSION_FENCED_CODE | blackfriday.EXTENSION_STRIKETHROUGH | blackfriday.EXTENSION_DEFINITION_LISTS
|
||||
markdown = blackfriday.Markdown(markdown, c.Renderer, extns)
|
||||
extns := 0
|
||||
extns |= blackfriday.EXTENSION_TABLES
|
||||
extns |= blackfriday.EXTENSION_FENCED_CODE
|
||||
extns |= blackfriday.EXTENSION_STRIKETHROUGH
|
||||
extns |= blackfriday.EXTENSION_DEFINITION_LISTS
|
||||
html := blackfriday.Markdown(markdown, c.Renderer, extns)
|
||||
|
||||
// set it as body for template
|
||||
metadata.Variables["body"] = string(markdown)
|
||||
title := metadata.Title
|
||||
if title == "" {
|
||||
title = filepath.Base(requestPath)
|
||||
var extension = filepath.Ext(requestPath)
|
||||
title = title[0 : len(title)-len(extension)]
|
||||
}
|
||||
metadata.Variables["title"] = title
|
||||
mdata.Variables["body"] = string(html)
|
||||
|
||||
return md.processTemplate(c, requestPath, tmpl, metadata, ctx)
|
||||
}
|
||||
|
||||
// processTemplate processes a template given a requestPath,
|
||||
// template (tmpl) and metadata
|
||||
func (md Markdown) processTemplate(c *Config, requestPath string, tmpl []byte, metadata Metadata, ctx middleware.Context) ([]byte, error) {
|
||||
// if template is not specified,
|
||||
// use the default template
|
||||
if tmpl == nil {
|
||||
tmpl = defaultTemplate(c, metadata, requestPath)
|
||||
// fixup title
|
||||
mdata.Variables["title"] = mdata.Title
|
||||
if mdata.Variables["title"] == "" {
|
||||
mdata.Variables["title"] = title
|
||||
}
|
||||
|
||||
// process the template
|
||||
b := new(bytes.Buffer)
|
||||
t, err := template.New("").Parse(string(tmpl))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mdData := Data{
|
||||
Context: ctx,
|
||||
Doc: metadata.Variables,
|
||||
DocFlags: metadata.Flags,
|
||||
Links: c.Links,
|
||||
}
|
||||
|
||||
c.RLock()
|
||||
err = t.Execute(b, mdData)
|
||||
c.RUnlock()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// generate static page
|
||||
if err = md.generatePage(c, requestPath, b.Bytes()); err != nil {
|
||||
// if static page generation fails, nothing fatal, only log the error.
|
||||
// TODO: Report (return) this non-fatal error, but don't log it here?
|
||||
log.Println("[ERROR] markdown: Render:", err)
|
||||
}
|
||||
|
||||
return b.Bytes(), nil
|
||||
|
||||
}
|
||||
|
||||
// generatePage generates a static html page from the markdown in content if c.StaticDir
|
||||
// is a non-empty value, meaning that the user enabled static site generation.
|
||||
func (md Markdown) generatePage(c *Config, requestPath string, content []byte) error {
|
||||
// Only generate the page if static site generation is enabled
|
||||
if c.StaticDir != "" {
|
||||
// if static directory is not existing, create it
|
||||
if _, err := os.Stat(c.StaticDir); err != nil {
|
||||
err := os.MkdirAll(c.StaticDir, os.FileMode(0755))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// massage possible files
|
||||
files := []FileInfo{}
|
||||
for _, ent := range dirents {
|
||||
file := FileInfo{
|
||||
FileInfo: ent,
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
// the URL will always use "/" as a path separator,
|
||||
// convert that to a native path to support OS that
|
||||
// use different path separators
|
||||
filePath := filepath.Join(c.StaticDir, filepath.FromSlash(requestPath))
|
||||
|
||||
// If it is index file, use the directory instead
|
||||
if md.IsIndexFile(filepath.Base(requestPath)) {
|
||||
filePath, _ = filepath.Split(filePath)
|
||||
}
|
||||
|
||||
// Create the directory in case it is not existing
|
||||
if err := os.MkdirAll(filePath, os.FileMode(0744)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// generate index.html file in the directory
|
||||
filePath = filepath.Join(filePath, "index.html")
|
||||
err := ioutil.WriteFile(filePath, content, os.FileMode(0664))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Lock()
|
||||
c.StaticFiles[requestPath] = filepath.ToSlash(filePath)
|
||||
c.Unlock()
|
||||
files = append(files, file)
|
||||
}
|
||||
|
||||
return nil
|
||||
return execTemplate(c, mdata, files, ctx)
|
||||
}
|
||||
|
||||
// defaultTemplate constructs a default template.
|
||||
func defaultTemplate(c *Config, metadata Metadata, requestPath string) []byte {
|
||||
var scripts, styles bytes.Buffer
|
||||
for _, style := range c.Styles {
|
||||
styles.WriteString(strings.Replace(cssTemplate, "{{url}}", style, 1))
|
||||
styles.WriteString("\r\n")
|
||||
}
|
||||
for _, script := range c.Scripts {
|
||||
scripts.WriteString(strings.Replace(jsTemplate, "{{url}}", script, 1))
|
||||
scripts.WriteString("\r\n")
|
||||
}
|
||||
|
||||
// Title is first line (length-limited), otherwise filename
|
||||
title, _ := metadata.Variables["title"]
|
||||
|
||||
html := []byte(htmlTemplate)
|
||||
html = bytes.Replace(html, []byte("{{title}}"), []byte(title), 1)
|
||||
html = bytes.Replace(html, []byte("{{css}}"), styles.Bytes(), 1)
|
||||
html = bytes.Replace(html, []byte("{{js}}"), scripts.Bytes(), 1)
|
||||
|
||||
return html
|
||||
}
|
||||
|
||||
const (
|
||||
htmlTemplate = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{title}}</title>
|
||||
<meta charset="utf-8">
|
||||
{{css}}
|
||||
{{js}}
|
||||
</head>
|
||||
<body>
|
||||
{{.Doc.body}}
|
||||
</body>
|
||||
</html>`
|
||||
cssTemplate = `<link rel="stylesheet" href="{{url}}">`
|
||||
jsTemplate = `<script src="{{url}}"></script>`
|
||||
)
|
||||
|
|
|
@ -1,139 +0,0 @@
|
|||
package markdown
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
)
|
||||
|
||||
// SummaryRenderer represents a summary renderer.
|
||||
type SummaryRenderer struct{}
|
||||
|
||||
// Block-level callbacks
|
||||
|
||||
// BlockCode is the code tag callback.
|
||||
func (r SummaryRenderer) BlockCode(out *bytes.Buffer, text []byte, lang string) {}
|
||||
|
||||
// BlockQuote is the quote tag callback.
|
||||
func (r SummaryRenderer) BlockQuote(out *bytes.Buffer, text []byte) {}
|
||||
|
||||
// BlockHtml is the HTML tag callback.
|
||||
func (r SummaryRenderer) BlockHtml(out *bytes.Buffer, text []byte) {}
|
||||
|
||||
// Header is the header tag callback.
|
||||
func (r SummaryRenderer) Header(out *bytes.Buffer, text func() bool, level int, id string) {}
|
||||
|
||||
// HRule is the horizontal rule tag callback.
|
||||
func (r SummaryRenderer) HRule(out *bytes.Buffer) {}
|
||||
|
||||
// List is the list tag callback.
|
||||
func (r SummaryRenderer) List(out *bytes.Buffer, text func() bool, flags int) {
|
||||
// TODO: This is not desired (we'd rather not write lists as part of summary),
|
||||
// but see this issue: https://github.com/russross/blackfriday/issues/189
|
||||
marker := out.Len()
|
||||
if !text() {
|
||||
out.Truncate(marker)
|
||||
}
|
||||
out.Write([]byte{' '})
|
||||
}
|
||||
|
||||
// ListItem is the list item tag callback.
|
||||
func (r SummaryRenderer) ListItem(out *bytes.Buffer, text []byte, flags int) {}
|
||||
|
||||
// Paragraph is the paragraph tag callback.
|
||||
func (r SummaryRenderer) Paragraph(out *bytes.Buffer, text func() bool) {
|
||||
marker := out.Len()
|
||||
if !text() {
|
||||
out.Truncate(marker)
|
||||
}
|
||||
out.Write([]byte{' '})
|
||||
}
|
||||
|
||||
// Table is the table tag callback.
|
||||
func (r SummaryRenderer) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) {}
|
||||
|
||||
// TableRow is the table row tag callback.
|
||||
func (r SummaryRenderer) TableRow(out *bytes.Buffer, text []byte) {}
|
||||
|
||||
// TableHeaderCell is the table header cell tag callback.
|
||||
func (r SummaryRenderer) TableHeaderCell(out *bytes.Buffer, text []byte, flags int) {}
|
||||
|
||||
// TableCell is the table cell tag callback.
|
||||
func (r SummaryRenderer) TableCell(out *bytes.Buffer, text []byte, flags int) {}
|
||||
|
||||
// Footnotes is the foot notes tag callback.
|
||||
func (r SummaryRenderer) Footnotes(out *bytes.Buffer, text func() bool) {}
|
||||
|
||||
// FootnoteItem is the footnote item tag callback.
|
||||
func (r SummaryRenderer) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) {}
|
||||
|
||||
// TitleBlock is the title tag callback.
|
||||
func (r SummaryRenderer) TitleBlock(out *bytes.Buffer, text []byte) {}
|
||||
|
||||
// Span-level callbacks
|
||||
|
||||
// AutoLink is the autolink tag callback.
|
||||
func (r SummaryRenderer) AutoLink(out *bytes.Buffer, link []byte, kind int) {}
|
||||
|
||||
// CodeSpan is the code span tag callback.
|
||||
func (r SummaryRenderer) CodeSpan(out *bytes.Buffer, text []byte) {
|
||||
out.Write([]byte("`"))
|
||||
out.Write(text)
|
||||
out.Write([]byte("`"))
|
||||
}
|
||||
|
||||
// DoubleEmphasis is the double emphasis tag callback.
|
||||
func (r SummaryRenderer) DoubleEmphasis(out *bytes.Buffer, text []byte) {
|
||||
out.Write(text)
|
||||
}
|
||||
|
||||
// Emphasis is the emphasis tag callback.
|
||||
func (r SummaryRenderer) Emphasis(out *bytes.Buffer, text []byte) {
|
||||
out.Write(text)
|
||||
}
|
||||
|
||||
// Image is the image tag callback.
|
||||
func (r SummaryRenderer) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) {}
|
||||
|
||||
// LineBreak is the line break tag callback.
|
||||
func (r SummaryRenderer) LineBreak(out *bytes.Buffer) {}
|
||||
|
||||
// Link is the link tag callback.
|
||||
func (r SummaryRenderer) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) {
|
||||
out.Write(content)
|
||||
}
|
||||
|
||||
// RawHtmlTag is the raw HTML tag callback.
|
||||
func (r SummaryRenderer) RawHtmlTag(out *bytes.Buffer, tag []byte) {}
|
||||
|
||||
// TripleEmphasis is the triple emphasis tag callback.
|
||||
func (r SummaryRenderer) TripleEmphasis(out *bytes.Buffer, text []byte) {
|
||||
out.Write(text)
|
||||
}
|
||||
|
||||
// StrikeThrough is the strikethrough tag callback.
|
||||
func (r SummaryRenderer) StrikeThrough(out *bytes.Buffer, text []byte) {}
|
||||
|
||||
// FootnoteRef is the footnote ref tag callback.
|
||||
func (r SummaryRenderer) FootnoteRef(out *bytes.Buffer, ref []byte, id int) {}
|
||||
|
||||
// Low-level callbacks
|
||||
|
||||
// Entity callback.
|
||||
func (r SummaryRenderer) Entity(out *bytes.Buffer, entity []byte) {
|
||||
out.Write(entity)
|
||||
}
|
||||
|
||||
// NormalText callback.
|
||||
func (r SummaryRenderer) NormalText(out *bytes.Buffer, text []byte) {
|
||||
out.Write(text)
|
||||
}
|
||||
|
||||
// Header and footer
|
||||
|
||||
// DocumentHeader callback.
|
||||
func (r SummaryRenderer) DocumentHeader(out *bytes.Buffer) {}
|
||||
|
||||
// DocumentFooter callback.
|
||||
func (r SummaryRenderer) DocumentFooter(out *bytes.Buffer) {}
|
||||
|
||||
// GetFlags returns zero.
|
||||
func (r SummaryRenderer) GetFlags() int { return 0 }
|
153
middleware/markdown/summary/render.go
Normal file
153
middleware/markdown/summary/render.go
Normal file
|
@ -0,0 +1,153 @@
|
|||
package summary
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/russross/blackfriday"
|
||||
)
|
||||
|
||||
// Ensure we implement the Blackfriday Markdown Renderer interface
|
||||
var _ blackfriday.Renderer = (*renderer)(nil)
|
||||
|
||||
// renderer renders Markdown to plain-text meant for listings and excerpts,
|
||||
// and implements the blackfriday.Renderer interface.
|
||||
//
|
||||
// Many of the methods are stubs with no output to prevent output of HTML markup.
|
||||
type renderer struct{}
|
||||
|
||||
// Blocklevel callbacks
|
||||
|
||||
// Stub BlockCode is the code tag callback.
|
||||
func (r renderer) BlockCode(out *bytes.Buffer, text []byte, land string) {}
|
||||
|
||||
// Stub BlockQuote is teh quote tag callback.
|
||||
func (r renderer) BlockQuote(out *bytes.Buffer, text []byte) {}
|
||||
|
||||
// Stub BlockHtml is the HTML tag callback.
|
||||
func (r renderer) BlockHtml(out *bytes.Buffer, text []byte) {}
|
||||
|
||||
// Stub Header is the header tag callback.
|
||||
func (r renderer) Header(out *bytes.Buffer, text func() bool, level int, id string) {}
|
||||
|
||||
// Stub HRule is the horizontal rule tag callback.
|
||||
func (r renderer) HRule(out *bytes.Buffer) {}
|
||||
|
||||
// List is the list tag callback.
|
||||
func (r renderer) List(out *bytes.Buffer, text func() bool, flags int) {
|
||||
// TODO: This is not desired (we'd rather not write lists as part of summary),
|
||||
// but see this issue: https://github.com/russross/blackfriday/issues/189
|
||||
marker := out.Len()
|
||||
if !text() {
|
||||
out.Truncate(marker)
|
||||
}
|
||||
out.Write([]byte{' '})
|
||||
}
|
||||
|
||||
// Stub ListItem is the list item tag callback.
|
||||
func (r renderer) ListItem(out *bytes.Buffer, text []byte, flags int) {}
|
||||
|
||||
// Paragraph is the paragraph tag callback. This renders simple paragraph text
|
||||
// into plain text, such that summaries can be easily generated.
|
||||
func (r renderer) Paragraph(out *bytes.Buffer, text func() bool) {
|
||||
marker := out.Len()
|
||||
if !text() {
|
||||
out.Truncate(marker)
|
||||
}
|
||||
out.Write([]byte{' '})
|
||||
}
|
||||
|
||||
// Stub Table is the table tag callback.
|
||||
func (r renderer) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) {}
|
||||
|
||||
// Stub TableRow is the table row tag callback.
|
||||
func (r renderer) TableRow(out *bytes.Buffer, text []byte) {}
|
||||
|
||||
// Stub TableHeaderCell is the table header cell tag callback.
|
||||
func (r renderer) TableHeaderCell(out *bytes.Buffer, text []byte, flags int) {}
|
||||
|
||||
// Stub TableCell is the table cell tag callback.
|
||||
func (r renderer) TableCell(out *bytes.Buffer, text []byte, flags int) {}
|
||||
|
||||
// Stub Footnotes is the foot notes tag callback.
|
||||
func (r renderer) Footnotes(out *bytes.Buffer, text func() bool) {}
|
||||
|
||||
// Stub FootnoteItem is the footnote item tag callback.
|
||||
func (r renderer) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) {}
|
||||
|
||||
// Stub TitleBlock is the title tag callback.
|
||||
func (r renderer) TitleBlock(out *bytes.Buffer, text []byte) {}
|
||||
|
||||
// Spanlevel callbacks
|
||||
|
||||
// Stub AutoLink is the autolink tag callback.
|
||||
func (r renderer) AutoLink(out *bytes.Buffer, link []byte, kind int) {}
|
||||
|
||||
// CodeSpan is the code span tag callback. Outputs a simple Markdown version
|
||||
// of the code span.
|
||||
func (r renderer) CodeSpan(out *bytes.Buffer, text []byte) {
|
||||
out.Write([]byte("`"))
|
||||
out.Write(text)
|
||||
out.Write([]byte("`"))
|
||||
}
|
||||
|
||||
// DoubleEmphasis is the double emphasis tag callback. Outputs a simple
|
||||
// plain-text version of the input.
|
||||
func (r renderer) DoubleEmphasis(out *bytes.Buffer, text []byte) {
|
||||
out.Write(text)
|
||||
}
|
||||
|
||||
// Emphasis is the emphasis tag callback. Outputs a simple plain-text
|
||||
// version of the input.
|
||||
func (r renderer) Emphasis(out *bytes.Buffer, text []byte) {
|
||||
out.Write(text)
|
||||
}
|
||||
|
||||
// Stub Image is the image tag callback.
|
||||
func (r renderer) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) {}
|
||||
|
||||
// Stub LineBreak is the line break tag callback.
|
||||
func (r renderer) LineBreak(out *bytes.Buffer) {}
|
||||
|
||||
// Link is the link tag callback. Outputs a sipmle plain-text version
|
||||
// of the input.
|
||||
func (r renderer) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) {
|
||||
out.Write(content)
|
||||
}
|
||||
|
||||
// Stub RawHtmlTag is the raw HTML tag callback.
|
||||
func (r renderer) RawHtmlTag(out *bytes.Buffer, tag []byte) {}
|
||||
|
||||
// TripleEmphasis is the triple emphasis tag callback. Outputs a simple plain-text
|
||||
// version of the input.
|
||||
func (r renderer) TripleEmphasis(out *bytes.Buffer, text []byte) {
|
||||
out.Write(text)
|
||||
}
|
||||
|
||||
// Stub StrikeThrough is the strikethrough tag callback.
|
||||
func (r renderer) StrikeThrough(out *bytes.Buffer, text []byte) {}
|
||||
|
||||
// Stub FootnoteRef is the footnote ref tag callback.
|
||||
func (r renderer) FootnoteRef(out *bytes.Buffer, ref []byte, id int) {}
|
||||
|
||||
// Lowlevel callbacks
|
||||
|
||||
// Entity callback. Outputs a simple plain-text version of the input.
|
||||
func (r renderer) Entity(out *bytes.Buffer, entity []byte) {
|
||||
out.Write(entity)
|
||||
}
|
||||
|
||||
// NormalText callback. Outputs a simple plain-text version of the input.
|
||||
func (r renderer) NormalText(out *bytes.Buffer, text []byte) {
|
||||
out.Write(text)
|
||||
}
|
||||
|
||||
// Header and footer
|
||||
|
||||
// Stub DocumentHeader callback.
|
||||
func (r renderer) DocumentHeader(out *bytes.Buffer) {}
|
||||
|
||||
// Stub DocumentFooter callback.
|
||||
func (r renderer) DocumentFooter(out *bytes.Buffer) {}
|
||||
|
||||
// Stub GetFlags returns zero.
|
||||
func (r renderer) GetFlags() int { return 0 }
|
18
middleware/markdown/summary/summary.go
Normal file
18
middleware/markdown/summary/summary.go
Normal file
|
@ -0,0 +1,18 @@
|
|||
package summary
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/russross/blackfriday"
|
||||
)
|
||||
|
||||
// Markdown formats input using a plain-text renderer, and
|
||||
// then returns up to the first `wordcount` words as a summary.
|
||||
func Markdown(input []byte, wordcount int) []byte {
|
||||
words := bytes.Fields(blackfriday.Markdown(input, renderer{}, 0))
|
||||
if wordcount > len(words) {
|
||||
wordcount = len(words)
|
||||
}
|
||||
|
||||
return bytes.Join(words[0:wordcount], []byte{' '})
|
||||
}
|
88
middleware/markdown/template.go
Normal file
88
middleware/markdown/template.go
Normal file
|
@ -0,0 +1,88 @@
|
|||
package markdown
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"text/template"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/markdown/metadata"
|
||||
)
|
||||
|
||||
// Data represents a markdown document.
|
||||
type Data struct {
|
||||
middleware.Context
|
||||
Doc map[string]string
|
||||
DocFlags map[string]bool
|
||||
Styles []string
|
||||
Scripts []string
|
||||
Files []FileInfo
|
||||
}
|
||||
|
||||
// Include "overrides" the embedded middleware.Context's Include()
|
||||
// method so that included files have access to d's fields.
|
||||
// Note: using {{template 'template-name' .}} instead might be better.
|
||||
func (d Data) Include(filename string) (string, error) {
|
||||
return middleware.ContextInclude(filename, d, d.Root)
|
||||
}
|
||||
|
||||
// execTemplate executes a template given a requestPath, template, and metadata
|
||||
func execTemplate(c *Config, mdata metadata.Metadata, files []FileInfo, ctx middleware.Context) ([]byte, error) {
|
||||
mdData := Data{
|
||||
Context: ctx,
|
||||
Doc: mdata.Variables,
|
||||
DocFlags: mdata.Flags,
|
||||
Styles: c.Styles,
|
||||
Scripts: c.Scripts,
|
||||
Files: files,
|
||||
}
|
||||
|
||||
b := new(bytes.Buffer)
|
||||
if err := c.Template.ExecuteTemplate(b, mdata.Template, mdData); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func SetTemplate(t *template.Template, name, filename string) error {
|
||||
|
||||
// Read template
|
||||
buf, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update if exists
|
||||
if tt := t.Lookup(name); tt != nil {
|
||||
_, err = tt.Parse(string(buf))
|
||||
return err
|
||||
}
|
||||
|
||||
// Allocate new name if not
|
||||
_, err = t.New(name).Parse(string(buf))
|
||||
return err
|
||||
}
|
||||
|
||||
func GetDefaultTemplate() *template.Template {
|
||||
return template.Must(template.New("").Parse(defaultTemplate))
|
||||
}
|
||||
|
||||
const (
|
||||
defaultTemplate = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{{.Doc.title}}</title>
|
||||
<meta charset="utf-8">
|
||||
{{- range .Styles}}
|
||||
<link rel="stylesheet" href="{{.}}">
|
||||
{{- end}}
|
||||
{{- range .Scripts}}
|
||||
<script src="{{.}}"></script>
|
||||
{{- end}}
|
||||
</head>
|
||||
<body>
|
||||
{{.Doc.body}}
|
||||
</body>
|
||||
</html>`
|
||||
)
|
|
@ -1,42 +0,0 @@
|
|||
package markdown
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DefaultInterval is the default interval at which the markdown watcher
|
||||
// checks for changes.
|
||||
const DefaultInterval = time.Second * 60
|
||||
|
||||
// Watch monitors the configured markdown directory for changes. It calls GenerateLinks
|
||||
// when there are changes.
|
||||
func Watch(md Markdown, c *Config, interval time.Duration) (stopChan chan struct{}) {
|
||||
return TickerFunc(interval, func() {
|
||||
if err := GenerateStatic(md, c); err != nil {
|
||||
log.Printf("[ERROR] markdown: Re-generating static site: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TickerFunc runs f at interval. A message to the returned channel will stop the
|
||||
// executing goroutine.
|
||||
func TickerFunc(interval time.Duration, f func()) chan struct{} {
|
||||
stopChan := make(chan struct{})
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
go func() {
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
f()
|
||||
case <-stopChan:
|
||||
ticker.Stop()
|
||||
break loop
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return stopChan
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
package markdown
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWatcher(t *testing.T) {
|
||||
expected := "12345678"
|
||||
interval := time.Millisecond * 100
|
||||
i := 0
|
||||
out := ""
|
||||
syncChan := make(chan struct{})
|
||||
stopChan := TickerFunc(interval, func() {
|
||||
i++
|
||||
out += fmt.Sprint(i)
|
||||
syncChan <- struct{}{}
|
||||
})
|
||||
sleepInSync(8, syncChan, stopChan)
|
||||
if out != expected {
|
||||
t.Fatalf("Expected to have prefix %v, found %v", expected, out)
|
||||
}
|
||||
out = ""
|
||||
i = 0
|
||||
var mu sync.Mutex
|
||||
stopChan = TickerFunc(interval, func() {
|
||||
i++
|
||||
mu.Lock()
|
||||
out += fmt.Sprint(i)
|
||||
mu.Unlock()
|
||||
syncChan <- struct{}{}
|
||||
})
|
||||
sleepInSync(9, syncChan, stopChan)
|
||||
mu.Lock()
|
||||
res := out
|
||||
mu.Unlock()
|
||||
if !strings.HasPrefix(res, expected) || res == expected {
|
||||
t.Fatalf("expected (%v) must be a proper prefix of out(%v).", expected, out)
|
||||
}
|
||||
}
|
||||
|
||||
func sleepInSync(times int, syncChan chan struct{}, stopChan chan struct{}) {
|
||||
for i := 0; i < times; i++ {
|
||||
<-syncChan
|
||||
}
|
||||
stopChan <- struct{}{}
|
||||
}
|
Loading…
Reference in a new issue