diff --git a/config/setup/markdown.go b/config/setup/markdown.go index 3f9b967a..8e296ef4 100644 --- a/config/setup/markdown.go +++ b/config/setup/markdown.go @@ -6,6 +6,7 @@ import ( "github.com/mholt/caddy/middleware" "github.com/mholt/caddy/middleware/markdown" "github.com/russross/blackfriday" + "path/filepath" ) // Markdown configures a new Markdown middleware instance. @@ -33,7 +34,9 @@ func markdownParse(c *Controller) ([]markdown.Config, error) { for c.Next() { md := markdown.Config{ - Renderer: blackfriday.HtmlRenderer(0, "", ""), + Renderer: blackfriday.HtmlRenderer(0, "", ""), + Templates: make(map[string]string), + StaticFiles: make(map[string]string), } // Get the path scope @@ -61,6 +64,23 @@ func markdownParse(c *Controller) ([]markdown.Config, error) { return mdconfigs, c.ArgErr() } md.Scripts = append(md.Scripts, c.Val()) + case "template": + tArgs := c.RemainingArgs() + switch len(tArgs) { + case 0: + return mdconfigs, c.ArgErr() + case 1: + if _, ok := md.Templates[markdown.DefaultTemplate]; ok { + return mdconfigs, c.Err("only one default template is allowed, use alias.") + } + fpath := filepath.Clean(c.Root + string(filepath.Separator) + tArgs[0]) + md.Templates[markdown.DefaultTemplate] = fpath + case 2: + fpath := filepath.Clean(c.Root + string(filepath.Separator) + tArgs[1]) + md.Templates[tArgs[0]] = fpath + default: + return mdconfigs, c.ArgErr() + } default: return mdconfigs, c.Err("Expected valid markdown configuration property") } diff --git a/middleware/markdown/markdown.go b/middleware/markdown/markdown.go index 5c34cfc2..f4eab69e 100644 --- a/middleware/markdown/markdown.go +++ b/middleware/markdown/markdown.go @@ -3,11 +3,9 @@ package markdown import ( - "bytes" "io/ioutil" "net/http" "os" - "path" "strings" "github.com/mholt/caddy/middleware" @@ -51,7 +49,10 @@ type Config struct { Scripts []string // Map of registered templates - Templates map[string] string + Templates map[string]string + + // Static files + StaticFiles map[string]string } // ServeHTTP implements the http.Handler interface. @@ -81,36 +82,10 @@ func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error return http.StatusInternalServerError, err } - content := blackfriday.Markdown(body, m.Renderer, 0) - - var scripts, styles string - for _, style := range m.Styles { - styles += strings.Replace(cssTemplate, "{{url}}", style, 1) + "\r\n" + html, err := Process(md, fpath, body) + if err != nil { + return http.StatusInternalServerError, err } - for _, script := range m.Scripts { - scripts += strings.Replace(jsTemplate, "{{url}}", script, 1) + "\r\n" - } - - // Title is first line (length-limited), otherwise filename - title := path.Base(fpath) - newline := bytes.Index(body, []byte("\n")) - if newline > -1 { - firstline := body[:newline] - newTitle := strings.TrimSpace(string(firstline)) - if len(newTitle) > 1 { - if len(newTitle) > 128 { - title = newTitle[:128] - } else { - title = newTitle - } - } - } - - html := htmlTemplate - html = strings.Replace(html, "{{title}}", title, 1) - html = strings.Replace(html, "{{css}}", styles, 1) - html = strings.Replace(html, "{{js}}", scripts, 1) - html = strings.Replace(html, "{{body}}", string(content), 1) w.Write([]byte(html)) @@ -122,20 +97,3 @@ func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error // Didn't qualify to serve as markdown; pass-thru return md.Next.ServeHTTP(w, r) } - -const ( - htmlTemplate = ` - - - {{title}} - - {{css}} - {{js}} - - - {{body}} - -` - cssTemplate = `` - jsTemplate = `` -) diff --git a/middleware/markdown/metadata.go b/middleware/markdown/metadata.go index 83919fb4..bdaf6c06 100644 --- a/middleware/markdown/metadata.go +++ b/middleware/markdown/metadata.go @@ -17,12 +17,16 @@ var ( // Metadata stores a page's metadata type Metadata struct { + Title string Template string Variables map[string]interface{} } // Load loads parsed metadata into m func (m *Metadata) load(parsedMap map[string]interface{}) { + if template, ok := parsedMap["title"]; ok { + m.Title, _ = template.(string) + } if template, ok := parsedMap["template"]; ok { m.Template, _ = template.(string) } @@ -37,6 +41,7 @@ type MetadataParser interface { // Identifiers Opening() []byte Closing() []byte + Parse([]byte) error Metadata() Metadata } @@ -121,12 +126,12 @@ func (y *YAMLMetadataParser) Metadata() Metadata { return y.metadata } -// Opening returns the opening identifier TOML metadata +// Opening returns the opening identifier YAML metadata func (y *YAMLMetadataParser) Opening() []byte { return []byte("---") } -// Closing returns the closing identifier TOML metadata +// Closing returns the closing identifier YAML metadata func (y *YAMLMetadataParser) Closing() []byte { return []byte("---") } diff --git a/middleware/markdown/process.go b/middleware/markdown/process.go index 68db4188..207b8733 100644 --- a/middleware/markdown/process.go +++ b/middleware/markdown/process.go @@ -5,41 +5,44 @@ import ( "bytes" "fmt" "io/ioutil" + "path/filepath" "text/template" + + "github.com/russross/blackfriday" + "strings" ) // Process the contents of a page. // It parses the metadata if any and uses the template if found -func Process(c Config, b []byte) ([]byte, error) { +func Process(c Config, fpath string, b []byte) ([]byte, error) { metadata, markdown, err := extractMetadata(b) if err != nil { return nil, err } - // if metadata template is included + // 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 = loadTemplate(t) + tmpl, err = ioutil.ReadFile(t) } if err != nil { return nil, err } } - // if no template loaded - // use default template - if tmpl == nil { - tmpl = []byte(htmlTemplate) - } - // process markdown - if markdown, err = processMarkdown(markdown, metadata.Variables); err != nil { - return nil, err - } + markdown = blackfriday.Markdown(markdown, c.Renderer, 0) + // set it as body for template + metadata.Variables["body"] = string(markdown) - tmpl = bytes.Replace(tmpl, []byte("{{body}}"), markdown, 1) - - return tmpl, nil + return processTemplate(c, fpath, tmpl, metadata) } // extractMetadata extracts metadata content from a page. @@ -70,12 +73,22 @@ func extractMetadata(b []byte) (metadata Metadata, markdown []byte, err error) { line := scanner.Bytes() // closing identifier found if bytes.Equal(bytes.TrimSpace(line), parser.Closing()) { - if err := parser.Parse(buf.Bytes()); err != nil { + // parse the metadata + err := parser.Parse(buf.Bytes()) + if err != nil { return metadata, nil, err } - return parser.Metadata(), reader.Bytes(), nil + // get the scanner to return remaining bytes + scanner.Split(func(data []byte, atEOF bool) (int, []byte, error) { + return len(data), data, nil + }) + // scan the remaining bytes + scanner.Scan() + + return parser.Metadata(), scanner.Bytes(), nil } buf.Write(line) + buf.WriteString("\r\n") } return metadata, nil, fmt.Errorf("Metadata not closed. '%v' not found", string(parser.Closing())) } @@ -91,25 +104,71 @@ func findParser(line []byte) MetadataParser { return nil } -func loadTemplate(tmpl string) ([]byte, error) { - b, err := ioutil.ReadFile(tmpl) +func processTemplate(c Config, fpath string, tmpl []byte, metadata Metadata) ([]byte, error) { + // if template is specified + // replace parse the template + if tmpl != nil { + tmpl = defaultTemplate(c, metadata, fpath) + } + + b := &bytes.Buffer{} + t, err := template.New("").Parse(string(tmpl)) if err != nil { return nil, err } - if !bytes.Contains(b, []byte("{{body}}")) { - return nil, fmt.Errorf("template missing {{body}}") + if err = t.Execute(b, metadata.Variables); err != nil { + return nil, err } - return b, nil + return b.Bytes(), nil + } -func processMarkdown(b []byte, vars map[string]interface{}) ([]byte, error) { - buf := &bytes.Buffer{} - t, err := template.New("markdown").Parse(string(b)) - if err != nil { - return nil, err +func defaultTemplate(c Config, metadata Metadata, fpath string) []byte { + // else, use default template + var scripts, styles bytes.Buffer + for _, style := range c.Styles { + styles.WriteString(strings.Replace(cssTemplate, "{{url}}", style, 1)) + styles.WriteString("\r\n") } - if err := t.Execute(buf, vars); err != nil { - return nil, err + for _, script := range c.Scripts { + scripts.WriteString(strings.Replace(jsTemplate, "{{url}}", script, 1)) + scripts.WriteString("\r\n") } - return buf.Bytes(), nil + + // Title is first line (length-limited), otherwise filename + title := metadata.Title + if title == "" { + title = filepath.Base(fpath) + if body, _ := metadata.Variables["body"].([]byte); len(body) > 128 { + title = string(body[:128]) + } else if len(body) > 0 { + title = string(body) + } + } + + 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 = ` + + + {{title}} + + {{css}} + {{js}} + + + {{.body}} + +` + cssTemplate = `` + jsTemplate = `` + + DefaultTemplate = "defaultTemplate" +)