diff --git a/middleware/markdown/markdown.go b/middleware/markdown/markdown.go index 70c3f825a..5c34cfc26 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 000000000..83919fb4a --- /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 000000000..68db4188f --- /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 +}