Commit c23c6d9c authored by Matt Holt's avatar Matt Holt

Merge pull request #762 from weingart/md_changes

Markdown changes
parents 39030d9e 9697c47e
......@@ -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
......@@ -56,8 +35,8 @@ 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),
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
return nil
default:
return c.ArgErr()
if err := markdown.SetTemplate(mdc.Template, tArgs[0], fpath); err != nil {
c.Errf("template parse error: %v", err)
}
case "sitegen":
if c.NextArg() {
mdc.StaticDir = path.Join(c.Root, c.Val())
} else {
mdc.StaticDir = path.Join(c.Root, markdown.DefaultStaticDir)
return nil
}
if c.NextArg() {
// only 1 argument allowed
case "templatedir":
if !c.NextArg() {
return c.ArgErr()
}
return nil
case "dev":
if c.NextArg() {
mdc.Development = strings.ToLower(c.Val()) == "true"
} else {
mdc.Development = true
_, 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
......
......@@ -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,83 +35,13 @@ 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")
}
}
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)
if len(myHandler.Configs[0].Extensions) != 3 {
t.Error("Expected 3 markdown extensions")
}
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)
}
}
......@@ -130,21 +58,29 @@ func TestMarkdownParse(t *testing.T) {
js /resources/js/blog.js
}`, false, []markdown.Config{{
PathScope: "/blog",
Extensions: []string{".md", ".txt"},
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,
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
......
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,97 +51,86 @@ 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
// Template(s) to render with
Template *template.Template
}
// 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
// ServeHTTP implements the http.Handler interface.
func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
var cfg *Config
for _, c := range md.Configs {
if middleware.Path(r.URL.Path).Matches(c.PathScope) { // not negated
cfg = c
break // or goto
}
}
return false
}
if cfg == nil {
return md.Next.ServeHTTP(w, r) // exit early
}
// 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
// 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 {
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) {
// 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()
fs, err := f.Stat()
if err != nil {
return http.StatusNotFound, nil
// 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())
}
// 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)
}
// Set path to found index file
fpath = idx
}
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 not supported extension, pass on it
if _, ok := cfg.Extensions[path.Ext(fpath)]; !ok {
return md.Next.ServeHTTP(w, r)
}
if os.IsPermission(err) {
// 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()
body, err := ioutil.ReadAll(f)
if err != nil {
return http.StatusInternalServerError, err
if fs, err := f.Stat(); err != nil {
return http.StatusGone, nil
} else {
lastModTime = latest(lastModTime, fs.ModTime())
}
ctx := middleware.Context{
......@@ -158,18 +138,34 @@ func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error
Req: r,
URL: r.URL,
}
html, err := md.Process(cfg, fpath, body, ctx)
html, err := cfg.Markdown(title(fpath), f, dirents, ctx)
if err != nil {
return http.StatusInternalServerError, err
}
middleware.SetLastModifiedHeader(w, fs.ModTime())
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
}
return http.StatusOK, nil
}
// latest returns the latest time.Time
func latest(t ...time.Time) time.Time {
var last time.Time
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"},
Extensions: map[string]struct{}{
".md": {},
},
Styles: []string{},
Scripts: []string{},
Templates: templates,
StaticDir: DefaultStaticDir,
StaticFiles: make(map[string]string),
Template: setDefaultTemplate(f("markdown_tpl.html")),
},
{
Renderer: blackfriday.HtmlRenderer(0, "", ""),
PathScope: "/docflags",
Extensions: []string{".md"},
Extensions: map[string]struct{}{
".md": {},
},
Styles: []string{},
Scripts: []string{},
Templates: map[string]string{
DefaultTemplate: "testdata/docflags/template.txt",
},
StaticDir: DefaultStaticDir,
StaticFiles: make(map[string]string),
Template: setDefaultTemplate(f("docflags/template.txt")),
},
{
Renderer: blackfriday.HtmlRenderer(0, "", ""),
PathScope: "/log",
Extensions: []string{".md"},
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"},
Templates: make(map[string]string),
StaticDir: DefaultStaticDir,
StaticFiles: make(map[string]string),
Template: GetDefaultTemplate(),
},
{
Renderer: blackfriday.HtmlRenderer(0, "", ""),
PathScope: "/og",
Extensions: []string{".md"},
Extensions: map[string]struct{}{
".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",
},
},
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)))
}
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()},
}
}
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
}
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()
}
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()
}
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())
}
}
}
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()
}
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()
}
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
}
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
}
// 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)
type FileInfo struct {
os.FileInfo
ctx middleware.Context
}
// Process 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)
func (f FileInfo) Summarize(wordcount int) (string, error) {
fp, err := f.ctx.Root.Open(f.Name())
if err != nil {
return nil, err
}
metadata = parser.Metadata()
}
// if template is not specified, check if Default template is set
if metadata.Template == "" {
if _, ok := c.Templates[DefaultTemplate]; ok {
metadata.Template = DefaultTemplate
}
return "", err
}
defer fp.Close()
// 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)
}
buf, err := ioutil.ReadAll(fp)
if err != nil {
return nil, err
}
}
// process markdown
extns := blackfriday.EXTENSION_TABLES | blackfriday.EXTENSION_FENCED_CODE | blackfriday.EXTENSION_STRIKETHROUGH | blackfriday.EXTENSION_DEFINITION_LISTS
markdown = 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)]
return "", err
}
metadata.Variables["title"] = title
return md.processTemplate(c, requestPath, tmpl, metadata, ctx)
return string(summary.Markdown(buf, wordcount)), nil
}
// 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)
}
// 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()
// Markdown processes the contents of a page in b. It parses the metadata
// (if any) and uses the template (if found).
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
}
// 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
}
parser := metadata.GetParser(body)
markdown := parser.Markdown()
mdata := parser.Metadata()
// 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
}
}
// 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
}
// process markdown
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)
// 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
}
// set it as body for template
mdata.Variables["body"] = string(html)
c.Lock()
c.StaticFiles[requestPath] = filepath.ToSlash(filePath)
c.Unlock()
// fixup title
mdata.Variables["title"] = mdata.Title
if mdata.Variables["title"] == "" {
mdata.Variables["title"] = title
}
return nil
}
// 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")
// massage possible files
files := []FileInfo{}
for _, ent := range dirents {
file := FileInfo{
FileInfo: ent,
ctx: ctx,
}
for _, script := range c.Scripts {
scripts.WriteString(strings.Replace(jsTemplate, "{{url}}", script, 1))
scripts.WriteString("\r\n")
files = append(files, file)
}
// 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
return execTemplate(c, mdata, files, ctx)
}
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>`
)
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 }
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 }
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{' '})
}
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>`
)
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
}
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{}{}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment