From 25847a6192ee854f9efc38758e05a6162aeb5f44 Mon Sep 17 00:00:00 2001 From: Abiola Ibrahim Date: Wed, 6 May 2015 03:37:29 +0100 Subject: [PATCH 1/7] markdown: added template codes. awaiting integration and tests --- middleware/markdown/markdown.go | 3 + middleware/markdown/metadata.go | 132 ++++++++++++++++++++++++++++++++ middleware/markdown/process.go | 115 ++++++++++++++++++++++++++++ 3 files changed, 250 insertions(+) create mode 100644 middleware/markdown/metadata.go create mode 100644 middleware/markdown/process.go diff --git a/middleware/markdown/markdown.go b/middleware/markdown/markdown.go index 70c3f825..5c34cfc2 100644 --- a/middleware/markdown/markdown.go +++ b/middleware/markdown/markdown.go @@ -49,6 +49,9 @@ type Config struct { // List of JavaScript files to load for each markdown file Scripts []string + + // Map of registered templates + Templates map[string] string } // ServeHTTP implements the http.Handler interface. diff --git a/middleware/markdown/metadata.go b/middleware/markdown/metadata.go new file mode 100644 index 00000000..83919fb4 --- /dev/null +++ b/middleware/markdown/metadata.go @@ -0,0 +1,132 @@ +package markdown + +import ( + "encoding/json" + + "github.com/BurntSushi/toml" + "gopkg.in/yaml.v2" +) + +var ( + parsers = []MetadataParser{ + &JSONMetadataParser{}, + &TOMLMetadataParser{}, + &YAMLMetadataParser{}, + } +) + +// Metadata stores a page's metadata +type Metadata struct { + Template string + Variables map[string]interface{} +} + +// Load loads parsed metadata into m +func (m *Metadata) load(parsedMap map[string]interface{}) { + if template, ok := parsedMap["template"]; ok { + m.Template, _ = template.(string) + } + if variables, ok := parsedMap["variables"]; ok { + m.Variables, _ = variables.(map[string]interface{}) + } +} + +// MetadataParser parses the page metadata +// into Metadata +type MetadataParser interface { + // Identifiers + Opening() []byte + Closing() []byte + Parse([]byte) error + Metadata() Metadata +} + +// JSONMetadataParser is the MetdataParser for JSON +type JSONMetadataParser struct { + metadata Metadata +} + +// Parse parses b into metadata +func (j *JSONMetadataParser) Parse(b []byte) error { + m := make(map[string]interface{}) + if err := json.Unmarshal(b, &m); err != nil { + return err + } + j.metadata.load(m) + return nil +} + +// Metadata returns the metadata parsed by this parser +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 parses b into metadata +func (t *TOMLMetadataParser) Parse(b []byte) error { + m := make(map[string]interface{}) + if err := toml.Unmarshal(b, &m); err != nil { + return err + } + t.metadata.load(m) + return nil +} + +// Metadata returns the metadata parsed by this parser +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 MetdataParser for YAML +type YAMLMetadataParser struct { + metadata Metadata +} + +// Parse parses b into metadata +func (y *YAMLMetadataParser) Parse(b []byte) error { + m := make(map[string]interface{}) + if err := yaml.Unmarshal(b, &m); err != nil { + return err + } + y.metadata.load(m) + return nil +} + +// Metadata returns the metadata parsed by this parser +func (y *YAMLMetadataParser) Metadata() Metadata { + return y.metadata +} + +// Opening returns the opening identifier TOML metadata +func (y *YAMLMetadataParser) Opening() []byte { + return []byte("---") +} + +// Closing returns the closing identifier TOML metadata +func (y *YAMLMetadataParser) Closing() []byte { + return []byte("---") +} diff --git a/middleware/markdown/process.go b/middleware/markdown/process.go new file mode 100644 index 00000000..68db4188 --- /dev/null +++ b/middleware/markdown/process.go @@ -0,0 +1,115 @@ +package markdown + +import ( + "bufio" + "bytes" + "fmt" + "io/ioutil" + "text/template" +) + +// 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) { + metadata, markdown, err := extractMetadata(b) + if err != nil { + return nil, err + } + // if metadata template is included + var tmpl []byte + if metadata.Template != "" { + if t, ok := c.Templates[metadata.Template]; ok { + tmpl, err = loadTemplate(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 + } + + tmpl = bytes.Replace(tmpl, []byte("{{body}}"), markdown, 1) + + return tmpl, nil +} + +// extractMetadata extracts metadata content from a page. +// it returns the metadata, the remaining bytes (markdown), +// and an error if any +func extractMetadata(b []byte) (metadata Metadata, markdown []byte, err error) { + b = bytes.TrimSpace(b) + reader := bytes.NewBuffer(b) + scanner := bufio.NewScanner(reader) + var parser MetadataParser + // if scanner.Scan() && + // Read first line + if scanner.Scan() { + line := scanner.Bytes() + parser = findParser(line) + // if no parser found + // assume metadata not present + if parser == nil { + return metadata, b, nil + } + } + + // buffer for metadata contents + buf := bytes.Buffer{} + + // Read remaining lines until closing identifier is found + for scanner.Scan() { + line := scanner.Bytes() + // closing identifier found + if bytes.Equal(bytes.TrimSpace(line), parser.Closing()) { + if err := parser.Parse(buf.Bytes()); err != nil { + return metadata, nil, err + } + return parser.Metadata(), reader.Bytes(), nil + } + buf.Write(line) + } + return metadata, nil, fmt.Errorf("Metadata not closed. '%v' not found", string(parser.Closing())) +} + +// findParser locates the parser for an opening identifier +func findParser(line []byte) MetadataParser { + line = bytes.TrimSpace(line) + for _, parser := range parsers { + if bytes.Equal(parser.Opening(), line) { + return parser + } + } + return nil +} + +func loadTemplate(tmpl string) ([]byte, error) { + b, err := ioutil.ReadFile(tmpl) + if err != nil { + return nil, err + } + if !bytes.Contains(b, []byte("{{body}}")) { + return nil, fmt.Errorf("template missing {{body}}") + } + return b, 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 + } + if err := t.Execute(buf, vars); err != nil { + return nil, err + } + return buf.Bytes(), nil +} From 0bfdb50adeccd3477dfb77f0811c2a18432e23b4 Mon Sep 17 00:00:00 2001 From: Abiola Ibrahim Date: Thu, 7 May 2015 00:19:02 +0100 Subject: [PATCH 2/7] markdown: working version of template integration. Awaiting static site generation and tests. --- config/setup/markdown.go | 22 +++++- middleware/markdown/markdown.go | 56 ++------------- middleware/markdown/metadata.go | 9 ++- middleware/markdown/process.go | 119 ++++++++++++++++++++++++-------- 4 files changed, 124 insertions(+), 82 deletions(-) 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" +) From ba613a1567d219b6b340c57873d3f4b999317823 Mon Sep 17 00:00:00 2001 From: Abiola Ibrahim Date: Thu, 7 May 2015 13:45:27 +0100 Subject: [PATCH 3/7] markdown: template integration done. awaiting documentation and test --- config/setup/markdown.go | 4 ++- middleware/markdown/markdown.go | 38 ++++++++++++++++++-- middleware/markdown/process.go | 61 +++++++++++++++++++++++++++++---- 3 files changed, 93 insertions(+), 10 deletions(-) diff --git a/config/setup/markdown.go b/config/setup/markdown.go index 8e296ef4..bfe96300 100644 --- a/config/setup/markdown.go +++ b/config/setup/markdown.go @@ -2,11 +2,12 @@ package setup import ( "net/http" + "path" + "path/filepath" "github.com/mholt/caddy/middleware" "github.com/mholt/caddy/middleware/markdown" "github.com/russross/blackfriday" - "path/filepath" ) // Markdown configures a new Markdown middleware instance. @@ -37,6 +38,7 @@ func markdownParse(c *Controller) ([]markdown.Config, error) { Renderer: blackfriday.HtmlRenderer(0, "", ""), Templates: make(map[string]string), StaticFiles: make(map[string]string), + StaticDir: path.Join(c.Root, markdown.StaticDir), } // Get the path scope diff --git a/middleware/markdown/markdown.go b/middleware/markdown/markdown.go index f4eab69e..d7bdcc39 100644 --- a/middleware/markdown/markdown.go +++ b/middleware/markdown/markdown.go @@ -31,6 +31,16 @@ type Markdown struct { IndexFiles []string } +// Helper function to check if a file is an index file +func (m Markdown) IsIndexFile(file string) bool { + for _, f := range m.IndexFiles { + if f == file { + return true + } + } + return false +} + // Config stores markdown middleware configurations. type Config struct { // Markdown renderer @@ -53,6 +63,9 @@ type Config struct { // Static files StaticFiles map[string]string + + // Static files directory + StaticDir string } // ServeHTTP implements the http.Handler interface. @@ -77,18 +90,37 @@ func (md Markdown) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error return http.StatusNotFound, nil } + fs, err := f.Stat() + if err != nil { + return http.StatusNotFound, nil + } + + // if static site is generated, attempt to use it + if filepath, ok := m.StaticFiles[fpath]; 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().UnixNano() < fs1.ModTime().UnixNano() { + if html, err := ioutil.ReadFile(filepath); err == nil { + w.Write(html) + return http.StatusOK, nil + } + } + } + } + body, err := ioutil.ReadAll(f) if err != nil { return http.StatusInternalServerError, err } - html, err := Process(md, fpath, body) + html, err := md.process(m, fpath, body) if err != nil { return http.StatusInternalServerError, err } - w.Write([]byte(html)) - + w.Write(html) return http.StatusOK, nil } } diff --git a/middleware/markdown/process.go b/middleware/markdown/process.go index 207b8733..a9fa702a 100644 --- a/middleware/markdown/process.go +++ b/middleware/markdown/process.go @@ -9,12 +9,19 @@ import ( "text/template" "github.com/russross/blackfriday" + "log" + "os" "strings" ) -// Process the contents of a page. +const ( + DefaultTemplate = "defaultTemplate" + StaticDir = ".caddy_static" +) + +// process the contents of a page. // It parses the metadata if any and uses the template if found -func Process(c Config, fpath string, b []byte) ([]byte, error) { +func (md Markdown) process(c Config, fpath string, b []byte) ([]byte, error) { metadata, markdown, err := extractMetadata(b) if err != nil { return nil, err @@ -42,7 +49,7 @@ func Process(c Config, fpath string, b []byte) ([]byte, error) { // set it as body for template metadata.Variables["body"] = string(markdown) - return processTemplate(c, fpath, tmpl, metadata) + return md.processTemplate(c, fpath, tmpl, metadata) } // extractMetadata extracts metadata content from a page. @@ -104,7 +111,7 @@ func findParser(line []byte) MetadataParser { return nil } -func processTemplate(c Config, fpath string, tmpl []byte, metadata Metadata) ([]byte, error) { +func (md Markdown) processTemplate(c Config, fpath string, tmpl []byte, metadata Metadata) ([]byte, error) { // if template is specified // replace parse the template if tmpl != nil { @@ -119,6 +126,14 @@ func processTemplate(c Config, fpath string, tmpl []byte, metadata Metadata) ([] if err = t.Execute(b, metadata.Variables); err != nil { return nil, err } + + // generate static page + if err = md.generatePage(c, fpath, b.Bytes()); err != nil { + // if static page generation fails, + // nothing fatal, only log the error. + log.Println(err) + } + return b.Bytes(), nil } @@ -154,6 +169,42 @@ func defaultTemplate(c Config, metadata Metadata, fpath string) []byte { return html } +func (md Markdown) generatePage(c Config, fpath string, content []byte) error { + // should not happen + // must be set on init + if c.StaticDir == "" { + return fmt.Errorf("Static directory not set") + } + + // 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 + } + } + + filePath := filepath.Join(c.StaticDir, fpath) + + // If it is index file, use the directory instead + if md.IsIndexFile(filepath.Base(fpath)) { + filePath, _ = filepath.Split(filePath) + } + if err := os.MkdirAll(filePath, os.FileMode(0755)); err != nil { + return err + } + + // generate index.html file in the directory + filePath = filepath.Join(filePath, "index.html") + err := ioutil.WriteFile(filePath, content, os.FileMode(0755)) + if err != nil { + return err + } + + c.StaticFiles[fpath] = filePath + return nil +} + const ( htmlTemplate = ` @@ -169,6 +220,4 @@ const ( ` cssTemplate = `` jsTemplate = `` - - DefaultTemplate = "defaultTemplate" ) From 0fccd3707d59fe68fa2c045443d9207244d8b5a9 Mon Sep 17 00:00:00 2001 From: Abiola Ibrahim Date: Fri, 8 May 2015 16:20:07 +0100 Subject: [PATCH 4/7] markdown: documentation done. awaiting test --- middleware/markdown/markdown.go | 4 +- middleware/markdown/metadata.go | 39 +++++++---- middleware/markdown/process.go | 116 +++++++++++++++++--------------- 3 files changed, 91 insertions(+), 68 deletions(-) diff --git a/middleware/markdown/markdown.go b/middleware/markdown/markdown.go index d7bdcc39..7600780c 100644 --- a/middleware/markdown/markdown.go +++ b/middleware/markdown/markdown.go @@ -61,10 +61,10 @@ type Config struct { // Map of registered templates Templates map[string]string - // Static files + // Map of request URL to static files generated StaticFiles map[string]string - // Static files directory + // Directory to store static files StaticDir string } diff --git a/middleware/markdown/metadata.go b/middleware/markdown/metadata.go index bdaf6c06..38660586 100644 --- a/middleware/markdown/metadata.go +++ b/middleware/markdown/metadata.go @@ -17,12 +17,17 @@ var ( // Metadata stores a page's metadata type Metadata struct { - Title string - Template string + // Page title + Title string + + // Page template + Template string + + // Variables to be used with Template Variables map[string]interface{} } -// Load loads parsed metadata into m +// load loads parsed values in parsedMap into Metadata func (m *Metadata) load(parsedMap map[string]interface{}) { if template, ok := parsedMap["title"]; ok { m.Title, _ = template.(string) @@ -35,14 +40,19 @@ func (m *Metadata) load(parsedMap map[string]interface{}) { } } -// MetadataParser parses the page metadata -// into Metadata +// MetadataParser is a an interface that must be satisfied by each parser type MetadataParser interface { - // Identifiers + // Opening identifier Opening() []byte + + // Closing identifier Closing() []byte + // Parse the metadata Parse([]byte) error + + // Parsed metadata. + // Should be called after a call to Parse returns no error Metadata() Metadata } @@ -51,7 +61,7 @@ type JSONMetadataParser struct { metadata Metadata } -// Parse parses b into metadata +// Parse the metadata func (j *JSONMetadataParser) Parse(b []byte) error { m := make(map[string]interface{}) if err := json.Unmarshal(b, &m); err != nil { @@ -61,7 +71,8 @@ func (j *JSONMetadataParser) Parse(b []byte) error { return nil } -// Metadata returns the metadata parsed by this parser +// Parsed metadata. +// Should be called after a call to Parse returns no error func (j *JSONMetadataParser) Metadata() Metadata { return j.metadata } @@ -81,7 +92,7 @@ type TOMLMetadataParser struct { metadata Metadata } -// Parse parses b into metadata +// Parse the metadata func (t *TOMLMetadataParser) Parse(b []byte) error { m := make(map[string]interface{}) if err := toml.Unmarshal(b, &m); err != nil { @@ -91,7 +102,8 @@ func (t *TOMLMetadataParser) Parse(b []byte) error { return nil } -// Metadata returns the metadata parsed by this parser +// Parsed metadata. +// Should be called after a call to Parse returns no error func (t *TOMLMetadataParser) Metadata() Metadata { return t.metadata } @@ -106,12 +118,12 @@ func (t *TOMLMetadataParser) Closing() []byte { return []byte("+++") } -// YAMLMetadataParser is the MetdataParser for YAML +// YAMLMetadataParser is the MetadataParser for YAML type YAMLMetadataParser struct { metadata Metadata } -// Parse parses b into metadata +// Parse the metadata func (y *YAMLMetadataParser) Parse(b []byte) error { m := make(map[string]interface{}) if err := yaml.Unmarshal(b, &m); err != nil { @@ -121,7 +133,8 @@ func (y *YAMLMetadataParser) Parse(b []byte) error { return nil } -// Metadata returns the metadata parsed by this parser +// Parsed metadata. +// Should be called after a call to Parse returns no error func (y *YAMLMetadataParser) Metadata() Metadata { return y.metadata } diff --git a/middleware/markdown/process.go b/middleware/markdown/process.go index a9fa702a..d775bf8b 100644 --- a/middleware/markdown/process.go +++ b/middleware/markdown/process.go @@ -19,9 +19,9 @@ const ( StaticDir = ".caddy_static" ) -// process the contents of a page. -// It parses the metadata if any and uses the template if found -func (md Markdown) process(c Config, fpath string, b []byte) ([]byte, error) { +// process processes the contents of a page. +// It parses the metadata (if any) and uses the template (if found) +func (md Markdown) process(c Config, requestPath string, b []byte) ([]byte, error) { metadata, markdown, err := extractMetadata(b) if err != nil { return nil, err @@ -46,10 +46,11 @@ func (md Markdown) process(c Config, fpath string, b []byte) ([]byte, error) { // process markdown markdown = blackfriday.Markdown(markdown, c.Renderer, 0) + // set it as body for template metadata.Variables["body"] = string(markdown) - return md.processTemplate(c, fpath, tmpl, metadata) + return md.processTemplate(c, requestPath, tmpl, metadata) } // extractMetadata extracts metadata content from a page. @@ -60,12 +61,12 @@ func extractMetadata(b []byte) (metadata Metadata, markdown []byte, err error) { reader := bytes.NewBuffer(b) scanner := bufio.NewScanner(reader) var parser MetadataParser - // if scanner.Scan() && + // Read first line if scanner.Scan() { line := scanner.Bytes() parser = findParser(line) - // if no parser found + // if no parser found, // assume metadata not present if parser == nil { return metadata, b, nil @@ -78,7 +79,8 @@ func extractMetadata(b []byte) (metadata Metadata, markdown []byte, err error) { // Read remaining lines until closing identifier is found for scanner.Scan() { line := scanner.Bytes() - // closing identifier found + + // if closing identifier found if bytes.Equal(bytes.TrimSpace(line), parser.Closing()) { // parse the metadata err := parser.Parse(buf.Bytes()) @@ -97,10 +99,12 @@ func extractMetadata(b []byte) (metadata Metadata, markdown []byte, err error) { buf.Write(line) buf.WriteString("\r\n") } + + // closing identifier not found return metadata, nil, fmt.Errorf("Metadata not closed. '%v' not found", string(parser.Closing())) } -// findParser locates the parser for an opening identifier +// findParser finds the parser using line that contains opening identifier func findParser(line []byte) MetadataParser { line = bytes.TrimSpace(line) for _, parser := range parsers { @@ -111,13 +115,16 @@ func findParser(line []byte) MetadataParser { return nil } -func (md Markdown) 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) +// processTemplate processes a template given a requestPath, +// template (tmpl) and metadata +func (md Markdown) processTemplate(c Config, requestPath string, tmpl []byte, metadata Metadata) ([]byte, error) { + // if template is not specified, + // use the default template + if tmpl == nil { + tmpl = defaultTemplate(c, metadata, requestPath) } + // process the template b := &bytes.Buffer{} t, err := template.New("").Parse(string(tmpl)) if err != nil { @@ -128,7 +135,7 @@ func (md Markdown) processTemplate(c Config, fpath string, tmpl []byte, metadata } // generate static page - if err = md.generatePage(c, fpath, b.Bytes()); err != nil { + if err = md.generatePage(c, requestPath, b.Bytes()); err != nil { // if static page generation fails, // nothing fatal, only log the error. log.Println(err) @@ -138,8 +145,47 @@ func (md Markdown) processTemplate(c Config, fpath string, tmpl []byte, metadata } -func defaultTemplate(c Config, metadata Metadata, fpath string) []byte { - // else, use default template +// generatePage generates a static html page from the markdown in content. +func (md Markdown) generatePage(c Config, requestPath string, content []byte) error { + // should not happen, + // must be set on Markdown init. + if c.StaticDir == "" { + return fmt.Errorf("Static directory not set") + } + + // 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 + } + } + + filePath := filepath.Join(c.StaticDir, 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(0755)); err != nil { + return err + } + + // generate index.html file in the directory + filePath = filepath.Join(filePath, "index.html") + err := ioutil.WriteFile(filePath, content, os.FileMode(0755)) + if err != nil { + return err + } + + c.StaticFiles[requestPath] = filePath + 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)) @@ -153,7 +199,7 @@ func defaultTemplate(c Config, metadata Metadata, fpath string) []byte { // Title is first line (length-limited), otherwise filename title := metadata.Title if title == "" { - title = filepath.Base(fpath) + title = filepath.Base(requestPath) if body, _ := metadata.Variables["body"].([]byte); len(body) > 128 { title = string(body[:128]) } else if len(body) > 0 { @@ -169,42 +215,6 @@ func defaultTemplate(c Config, metadata Metadata, fpath string) []byte { return html } -func (md Markdown) generatePage(c Config, fpath string, content []byte) error { - // should not happen - // must be set on init - if c.StaticDir == "" { - return fmt.Errorf("Static directory not set") - } - - // 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 - } - } - - filePath := filepath.Join(c.StaticDir, fpath) - - // If it is index file, use the directory instead - if md.IsIndexFile(filepath.Base(fpath)) { - filePath, _ = filepath.Split(filePath) - } - if err := os.MkdirAll(filePath, os.FileMode(0755)); err != nil { - return err - } - - // generate index.html file in the directory - filePath = filepath.Join(filePath, "index.html") - err := ioutil.WriteFile(filePath, content, os.FileMode(0755)) - if err != nil { - return err - } - - c.StaticFiles[fpath] = filePath - return nil -} - const ( htmlTemplate = ` From 48a12c605ab462e68ba7ac8670a79cfe3a2144ce Mon Sep 17 00:00:00 2001 From: Abiola Ibrahim Date: Fri, 8 May 2015 23:45:31 +0100 Subject: [PATCH 5/7] markdown: Added template support. --- middleware/markdown/metadata.go | 69 ++++++++++- middleware/markdown/metadata_test.go | 166 +++++++++++++++++++++++++++ middleware/markdown/process.go | 71 +----------- 3 files changed, 237 insertions(+), 69 deletions(-) create mode 100644 middleware/markdown/metadata_test.go diff --git a/middleware/markdown/metadata.go b/middleware/markdown/metadata.go index 38660586..3e3e48be 100644 --- a/middleware/markdown/metadata.go +++ b/middleware/markdown/metadata.go @@ -1,7 +1,10 @@ package markdown import ( + "bufio" + "bytes" "encoding/json" + "fmt" "github.com/BurntSushi/toml" "gopkg.in/yaml.v2" @@ -79,12 +82,12 @@ func (j *JSONMetadataParser) Metadata() Metadata { // Opening returns the opening identifier JSON metadata func (j *JSONMetadataParser) Opening() []byte { - return []byte("{") + return []byte(":::") } // Closing returns the closing identifier JSON metadata func (j *JSONMetadataParser) Closing() []byte { - return []byte("}") + return []byte(":::") } // TOMLMetadataParser is the MetadataParser for TOML @@ -148,3 +151,65 @@ func (y *YAMLMetadataParser) Opening() []byte { func (y *YAMLMetadataParser) Closing() []byte { return []byte("---") } + +// extractMetadata extracts metadata content from a page. +// it returns the metadata, the remaining bytes (markdown), +// and an error if any +func extractMetadata(b []byte) (metadata Metadata, markdown []byte, err error) { + b = bytes.TrimSpace(b) + reader := bytes.NewBuffer(b) + scanner := bufio.NewScanner(reader) + var parser MetadataParser + + // Read first line + if scanner.Scan() { + line := scanner.Bytes() + parser = findParser(line) + // if no parser found, + // assume metadata not present + if parser == nil { + return metadata, b, nil + } + } + + // buffer for metadata contents + buf := bytes.Buffer{} + + // Read remaining lines until closing identifier is found + for scanner.Scan() { + line := scanner.Bytes() + + // if closing identifier found + if bytes.Equal(bytes.TrimSpace(line), parser.Closing()) { + // parse the metadata + err := parser.Parse(buf.Bytes()) + if err != nil { + return metadata, nil, err + } + // 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") + } + + // closing identifier not found + return metadata, nil, fmt.Errorf("Metadata not closed. '%v' not found", string(parser.Closing())) +} + +// findParser finds the parser using line that contains opening identifier +func findParser(line []byte) MetadataParser { + line = bytes.TrimSpace(line) + for _, parser := range parsers { + if bytes.Equal(parser.Opening(), line) { + return parser + } + } + return nil +} diff --git a/middleware/markdown/metadata_test.go b/middleware/markdown/metadata_test.go new file mode 100644 index 00000000..987c89f8 --- /dev/null +++ b/middleware/markdown/metadata_test.go @@ -0,0 +1,166 @@ +package markdown + +import ( + "bytes" + "fmt" + "reflect" + "testing" +) + +var TOML = [4]string{` +title = "A title" +template = "default" +[variables] +name = "value" +`, + `+++ +title = "A title" +template = "default" +[variables] +name = "value" ++++ +`, + `+++ +title = "A title" +template = "default" +[variables] +name = "value" + `, + `title = "A title" template = "default" [variables] name = "value"`, +} + +var YAML = [4]string{` +title : A title +template : default +variables : +- name : value +`, + `--- +title : A title +template : default +variables : +- name : value +--- +`, + `--- +title : A title +template : default +variables : +- name : value +`, + `title : A title template : default variables : name : value`, +} +var JSON = [4]string{` +{ + "title" : "A title", + "template" : "default", + "variables" : { + "name" : "value" + } +} +`, + `::: +{ + "title" : "A title", + "template" : "default", + "variables" : { + "name" : "value" + } +} +:::`, + `::: +{ + "title" : "A title", + "template" : "default", + "variables" : { + "name" : "value" + } +} +`, + ` + ::: +{{ + "title" : "A title", + "template" : "default", + "variables" : { + "name" : "value" + } +} +::: + `, +} + +func check(t *testing.T, err error) { + if err != nil { + t.Fatal(err) + } +} + +func TestParsers(t *testing.T) { + expected := Metadata{ + Title: "A title", + Template: "default", + Variables: map[string]interface{}{"name": "value"}, + } + compare := func(m Metadata) bool { + if m.Title != expected.Title { + return false + } + if m.Template != expected.Template { + return false + } + for k, v := range m.Variables { + if v != expected.Variables[k] { + return false + } + } + return true + } + + data := []struct { + parser MetadataParser + testData [4]string + name string + }{ + {&JSONMetadataParser{}, JSON, "json"}, + {&YAMLMetadataParser{}, YAML, "yaml"}, + {&TOMLMetadataParser{}, TOML, "toml"}, + } + + for _, v := range data { + // metadata without identifiers + err := v.parser.Parse([]byte(v.testData[0])) + check(t, err) + if !compare(v.parser.Metadata()) { + t.Fatalf("Expected %v, found %v for %v", expected, v.parser.Metadata().Variables, v.name) + } + + // metadata with identifiers + metadata, _, err := extractMetadata([]byte(v.testData[1])) + check(t, err) + if !compare(metadata) { + t.Fatalf("Expected %v, found %v for %v", expected, metadata.Variables, 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)) + } + } + + // metadata without closing identifier + if _, _, err := extractMetadata([]byte(v.testData[2])); err == nil { + t.Fatalf("Expected error for missing closing identifier for %v", v.name) + } + + // invalid metadata + if err := v.parser.Parse([]byte(v.testData[3])); err == nil { + t.Fatalf("Expected error for invalid metadata for %v", v.name) + } + } + +} diff --git a/middleware/markdown/process.go b/middleware/markdown/process.go index d775bf8b..5bf9badf 100644 --- a/middleware/markdown/process.go +++ b/middleware/markdown/process.go @@ -1,17 +1,16 @@ package markdown import ( - "bufio" "bytes" "fmt" "io/ioutil" + "log" + "os" "path/filepath" + "strings" "text/template" "github.com/russross/blackfriday" - "log" - "os" - "strings" ) const ( @@ -48,73 +47,11 @@ func (md Markdown) process(c Config, requestPath string, b []byte) ([]byte, erro markdown = blackfriday.Markdown(markdown, c.Renderer, 0) // set it as body for template - metadata.Variables["body"] = string(markdown) + metadata.Variables["markdown"] = string(markdown) return md.processTemplate(c, requestPath, tmpl, metadata) } -// extractMetadata extracts metadata content from a page. -// it returns the metadata, the remaining bytes (markdown), -// and an error if any -func extractMetadata(b []byte) (metadata Metadata, markdown []byte, err error) { - b = bytes.TrimSpace(b) - reader := bytes.NewBuffer(b) - scanner := bufio.NewScanner(reader) - var parser MetadataParser - - // Read first line - if scanner.Scan() { - line := scanner.Bytes() - parser = findParser(line) - // if no parser found, - // assume metadata not present - if parser == nil { - return metadata, b, nil - } - } - - // buffer for metadata contents - buf := bytes.Buffer{} - - // Read remaining lines until closing identifier is found - for scanner.Scan() { - line := scanner.Bytes() - - // if closing identifier found - if bytes.Equal(bytes.TrimSpace(line), parser.Closing()) { - // parse the metadata - err := parser.Parse(buf.Bytes()) - if err != nil { - return metadata, nil, err - } - // 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") - } - - // closing identifier not found - return metadata, nil, fmt.Errorf("Metadata not closed. '%v' not found", string(parser.Closing())) -} - -// findParser finds the parser using line that contains opening identifier -func findParser(line []byte) MetadataParser { - line = bytes.TrimSpace(line) - for _, parser := range parsers { - if bytes.Equal(parser.Opening(), line) { - return parser - } - } - return nil -} - // processTemplate processes a template given a requestPath, // template (tmpl) and metadata func (md Markdown) processTemplate(c Config, requestPath string, tmpl []byte, metadata Metadata) ([]byte, error) { From 6ce83aad2b8f8ab8a9da2dd6484a44a853d727e0 Mon Sep 17 00:00:00 2001 From: Abiola Ibrahim Date: Sat, 9 May 2015 00:54:39 +0100 Subject: [PATCH 6/7] markdown: Refactor fixes --- middleware/markdown/metadata.go | 18 +++++++++++------- middleware/markdown/process.go | 4 ++-- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/middleware/markdown/metadata.go b/middleware/markdown/metadata.go index 3e3e48be..8734663a 100644 --- a/middleware/markdown/metadata.go +++ b/middleware/markdown/metadata.go @@ -162,14 +162,18 @@ func extractMetadata(b []byte) (metadata Metadata, markdown []byte, err error) { var parser MetadataParser // Read first line - if scanner.Scan() { - line := scanner.Bytes() - parser = findParser(line) - // if no parser found, + if !scanner.Scan() { + // if no line is read, // assume metadata not present - if parser == nil { - return metadata, b, nil - } + return metadata, b, nil + } + + line := scanner.Bytes() + parser = findParser(line) + // if no parser found, + // assume metadata not present + if parser == nil { + return metadata, b, nil } // buffer for metadata contents diff --git a/middleware/markdown/process.go b/middleware/markdown/process.go index 5bf9badf..9741bd17 100644 --- a/middleware/markdown/process.go +++ b/middleware/markdown/process.go @@ -137,7 +137,7 @@ func defaultTemplate(c Config, metadata Metadata, requestPath string) []byte { title := metadata.Title if title == "" { title = filepath.Base(requestPath) - if body, _ := metadata.Variables["body"].([]byte); len(body) > 128 { + if body, _ := metadata.Variables["markdown"].([]byte); len(body) > 128 { title = string(body[:128]) } else if len(body) > 0 { title = string(body) @@ -162,7 +162,7 @@ const ( {{js}} - {{.body}} + {{.markdown}} ` cssTemplate = `` From 2f5e2f39cb244294dddf8580444558443d146499 Mon Sep 17 00:00:00 2001 From: Abiola Ibrahim Date: Sat, 9 May 2015 11:49:54 +0100 Subject: [PATCH 7/7] markdown: remove identifier from Json metadata --- middleware/markdown/metadata.go | 84 +++++++++++++++++----------- middleware/markdown/metadata_test.go | 47 ++++++++-------- middleware/markdown/process.go | 18 +++++- 3 files changed, 90 insertions(+), 59 deletions(-) diff --git a/middleware/markdown/metadata.go b/middleware/markdown/metadata.go index 8734663a..11b7f32a 100644 --- a/middleware/markdown/metadata.go +++ b/middleware/markdown/metadata.go @@ -51,8 +51,10 @@ type MetadataParser interface { // Closing identifier Closing() []byte - // Parse the metadata - Parse([]byte) error + // 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 @@ -65,13 +67,25 @@ type JSONMetadataParser struct { } // Parse the metadata -func (j *JSONMetadataParser) Parse(b []byte) error { +func (j *JSONMetadataParser) Parse(b []byte) ([]byte, error) { m := make(map[string]interface{}) - if err := json.Unmarshal(b, &m); err != nil { - return err + + // Read the preceding JSON object + decoder := json.NewDecoder(bytes.NewReader(b)) + if err := decoder.Decode(&m); err != nil { + return b, err } + j.metadata.load(m) - return nil + + // Retrieve remaining bytes after decoding + buf := make([]byte, len(b)) + n, err := decoder.Buffered().Read(buf) + if err != nil { + return b, err + } + + return buf[:n], nil } // Parsed metadata. @@ -82,12 +96,12 @@ func (j *JSONMetadataParser) Metadata() Metadata { // Opening returns the opening identifier JSON metadata func (j *JSONMetadataParser) Opening() []byte { - return []byte(":::") + return []byte("{") } // Closing returns the closing identifier JSON metadata func (j *JSONMetadataParser) Closing() []byte { - return []byte(":::") + return []byte("}") } // TOMLMetadataParser is the MetadataParser for TOML @@ -96,13 +110,17 @@ type TOMLMetadataParser struct { } // Parse the metadata -func (t *TOMLMetadataParser) Parse(b []byte) error { +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 err + return markdown, err } t.metadata.load(m) - return nil + return markdown, nil } // Parsed metadata. @@ -127,13 +145,17 @@ type YAMLMetadataParser struct { } // Parse the metadata -func (y *YAMLMetadataParser) Parse(b []byte) error { +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 err + return markdown, err } y.metadata.load(m) - return nil + return markdown, nil } // Parsed metadata. @@ -154,26 +176,23 @@ func (y *YAMLMetadataParser) Closing() []byte { // extractMetadata extracts metadata content from a page. // it returns the metadata, the remaining bytes (markdown), -// and an error if any -func extractMetadata(b []byte) (metadata Metadata, markdown []byte, err error) { +// and an error if any. +// Useful for MetadataParser with defined identifiers (YAML, TOML) +func extractMetadata(parser MetadataParser, b []byte) (metadata []byte, markdown []byte, err error) { b = bytes.TrimSpace(b) reader := bytes.NewBuffer(b) scanner := bufio.NewScanner(reader) - var parser MetadataParser // Read first line if !scanner.Scan() { // if no line is read, // assume metadata not present - return metadata, b, nil + return nil, b, nil } - line := scanner.Bytes() - parser = findParser(line) - // if no parser found, - // assume metadata not present - if parser == nil { - return metadata, b, nil + line := bytes.TrimSpace(scanner.Bytes()) + if !bytes.Equal(line, parser.Opening()) { + return nil, b, fmt.Errorf("Wrong identifier") } // buffer for metadata contents @@ -185,11 +204,7 @@ func extractMetadata(b []byte) (metadata Metadata, markdown []byte, err error) { // if closing identifier found if bytes.Equal(bytes.TrimSpace(line), parser.Closing()) { - // parse the metadata - err := parser.Parse(buf.Bytes()) - if err != nil { - return metadata, nil, err - } + // get the scanner to return remaining bytes scanner.Split(func(data []byte, atEOF bool) (int, []byte, error) { return len(data), data, nil @@ -197,18 +212,23 @@ func extractMetadata(b []byte) (metadata Metadata, markdown []byte, err error) { // scan the remaining bytes scanner.Scan() - return parser.Metadata(), scanner.Bytes(), nil + return buf.Bytes(), scanner.Bytes(), nil } buf.Write(line) buf.WriteString("\r\n") } // closing identifier not found - return metadata, nil, fmt.Errorf("Metadata not closed. '%v' not found", string(parser.Closing())) + return buf.Bytes(), nil, fmt.Errorf("Metadata not closed. '%v' not found", string(parser.Closing())) } // findParser finds the parser using line that contains opening identifier -func findParser(line []byte) MetadataParser { +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) { diff --git a/middleware/markdown/metadata_test.go b/middleware/markdown/metadata_test.go index 987c89f8..68659bf4 100644 --- a/middleware/markdown/metadata_test.go +++ b/middleware/markdown/metadata_test.go @@ -4,6 +4,7 @@ import ( "bytes" "fmt" "reflect" + "strings" "testing" ) @@ -19,6 +20,7 @@ template = "default" [variables] name = "value" +++ +Page content `, `+++ title = "A title" @@ -41,6 +43,7 @@ template : default variables : - name : value --- +Page content `, `--- title : A title @@ -51,34 +54,30 @@ variables : `title : A title template : default variables : name : value`, } var JSON = [4]string{` -{ "title" : "A title", "template" : "default", "variables" : { "name" : "value" } -} `, - `::: -{ - "title" : "A title", - "template" : "default", - "variables" : { - "name" : "value" - } -} -:::`, - `::: -{ + `{ "title" : "A title", "template" : "default", "variables" : { "name" : "value" } } +Page content +`, + ` +{ + "title" : "A title", + "template" : "default", + "variables" : { + "name" : "value" + } `, ` - ::: {{ "title" : "A title", "template" : "default", @@ -86,7 +85,6 @@ var JSON = [4]string{` "name" : "value" } } -::: `, } @@ -129,17 +127,18 @@ func TestParsers(t *testing.T) { for _, v := range data { // metadata without identifiers - err := v.parser.Parse([]byte(v.testData[0])) + if _, err := v.parser.Parse([]byte(v.testData[0])); err == nil { + 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 !compare(v.parser.Metadata()) { t.Fatalf("Expected %v, found %v for %v", expected, v.parser.Metadata().Variables, v.name) } - - // metadata with identifiers - metadata, _, err := extractMetadata([]byte(v.testData[1])) - check(t, err) - if !compare(metadata) { - t.Fatalf("Expected %v, found %v for %v", expected, metadata.Variables, 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 @@ -153,12 +152,12 @@ func TestParsers(t *testing.T) { } // metadata without closing identifier - if _, _, err := extractMetadata([]byte(v.testData[2])); err == nil { + if _, err := v.parser.Parse([]byte(v.testData[2])); err == nil { t.Fatalf("Expected error for missing closing identifier for %v", v.name) } // invalid metadata - if err := v.parser.Parse([]byte(v.testData[3])); err == nil { + if md, err = v.parser.Parse([]byte(v.testData[3])); err == nil { t.Fatalf("Expected error for invalid metadata for %v", v.name) } } diff --git a/middleware/markdown/process.go b/middleware/markdown/process.go index 9741bd17..ffcd456d 100644 --- a/middleware/markdown/process.go +++ b/middleware/markdown/process.go @@ -21,10 +21,22 @@ const ( // process processes the contents of a page. // It parses the metadata (if any) and uses the template (if found) func (md Markdown) process(c Config, requestPath string, b []byte) ([]byte, error) { - metadata, markdown, err := extractMetadata(b) - if err != nil { - return nil, err + var metadata = Metadata{} + var markdown []byte + var err error + + // find parser compatible with page contents + parser := findParser(b) + + // if found, assume metadata present and parse. + if parser != nil { + markdown, err = parser.Parse(b) + 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 {