mirror of
https://github.com/caddyserver/caddy.git
synced 2025-01-13 22:51:08 -05:00
markdown: working version of template integration. Awaiting static site generation and tests.
This commit is contained in:
parent
434ec7b6ea
commit
0bfdb50ade
4 changed files with 124 additions and 82 deletions
|
@ -6,6 +6,7 @@ import (
|
||||||
"github.com/mholt/caddy/middleware"
|
"github.com/mholt/caddy/middleware"
|
||||||
"github.com/mholt/caddy/middleware/markdown"
|
"github.com/mholt/caddy/middleware/markdown"
|
||||||
"github.com/russross/blackfriday"
|
"github.com/russross/blackfriday"
|
||||||
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Markdown configures a new Markdown middleware instance.
|
// Markdown configures a new Markdown middleware instance.
|
||||||
|
@ -34,6 +35,8 @@ func markdownParse(c *Controller) ([]markdown.Config, error) {
|
||||||
for c.Next() {
|
for c.Next() {
|
||||||
md := markdown.Config{
|
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
|
// Get the path scope
|
||||||
|
@ -61,6 +64,23 @@ func markdownParse(c *Controller) ([]markdown.Config, error) {
|
||||||
return mdconfigs, c.ArgErr()
|
return mdconfigs, c.ArgErr()
|
||||||
}
|
}
|
||||||
md.Scripts = append(md.Scripts, c.Val())
|
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:
|
default:
|
||||||
return mdconfigs, c.Err("Expected valid markdown configuration property")
|
return mdconfigs, c.Err("Expected valid markdown configuration property")
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,9 @@
|
||||||
package markdown
|
package markdown
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/mholt/caddy/middleware"
|
"github.com/mholt/caddy/middleware"
|
||||||
|
@ -51,7 +49,10 @@ type Config struct {
|
||||||
Scripts []string
|
Scripts []string
|
||||||
|
|
||||||
// Map of registered templates
|
// Map of registered templates
|
||||||
Templates map[string] string
|
Templates map[string]string
|
||||||
|
|
||||||
|
// Static files
|
||||||
|
StaticFiles map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeHTTP implements the http.Handler interface.
|
// 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
|
return http.StatusInternalServerError, err
|
||||||
}
|
}
|
||||||
|
|
||||||
content := blackfriday.Markdown(body, m.Renderer, 0)
|
html, err := Process(md, fpath, body)
|
||||||
|
if err != nil {
|
||||||
var scripts, styles string
|
return http.StatusInternalServerError, err
|
||||||
for _, style := range m.Styles {
|
|
||||||
styles += strings.Replace(cssTemplate, "{{url}}", style, 1) + "\r\n"
|
|
||||||
}
|
}
|
||||||
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))
|
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
|
// Didn't qualify to serve as markdown; pass-thru
|
||||||
return md.Next.ServeHTTP(w, r)
|
return md.Next.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
htmlTemplate = `<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>{{title}}</title>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
{{css}}
|
|
||||||
{{js}}
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
{{body}}
|
|
||||||
</body>
|
|
||||||
</html>`
|
|
||||||
cssTemplate = `<link rel="stylesheet" href="{{url}}">`
|
|
||||||
jsTemplate = `<script src="{{url}}"></script>`
|
|
||||||
)
|
|
||||||
|
|
|
@ -17,12 +17,16 @@ var (
|
||||||
|
|
||||||
// Metadata stores a page's metadata
|
// Metadata stores a page's metadata
|
||||||
type Metadata struct {
|
type Metadata struct {
|
||||||
|
Title string
|
||||||
Template string
|
Template string
|
||||||
Variables map[string]interface{}
|
Variables map[string]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load loads parsed metadata into m
|
// Load loads parsed metadata into m
|
||||||
func (m *Metadata) load(parsedMap map[string]interface{}) {
|
func (m *Metadata) load(parsedMap map[string]interface{}) {
|
||||||
|
if template, ok := parsedMap["title"]; ok {
|
||||||
|
m.Title, _ = template.(string)
|
||||||
|
}
|
||||||
if template, ok := parsedMap["template"]; ok {
|
if template, ok := parsedMap["template"]; ok {
|
||||||
m.Template, _ = template.(string)
|
m.Template, _ = template.(string)
|
||||||
}
|
}
|
||||||
|
@ -37,6 +41,7 @@ type MetadataParser interface {
|
||||||
// Identifiers
|
// Identifiers
|
||||||
Opening() []byte
|
Opening() []byte
|
||||||
Closing() []byte
|
Closing() []byte
|
||||||
|
|
||||||
Parse([]byte) error
|
Parse([]byte) error
|
||||||
Metadata() Metadata
|
Metadata() Metadata
|
||||||
}
|
}
|
||||||
|
@ -121,12 +126,12 @@ func (y *YAMLMetadataParser) Metadata() Metadata {
|
||||||
return y.metadata
|
return y.metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
// Opening returns the opening identifier TOML metadata
|
// Opening returns the opening identifier YAML metadata
|
||||||
func (y *YAMLMetadataParser) Opening() []byte {
|
func (y *YAMLMetadataParser) Opening() []byte {
|
||||||
return []byte("---")
|
return []byte("---")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Closing returns the closing identifier TOML metadata
|
// Closing returns the closing identifier YAML metadata
|
||||||
func (y *YAMLMetadataParser) Closing() []byte {
|
func (y *YAMLMetadataParser) Closing() []byte {
|
||||||
return []byte("---")
|
return []byte("---")
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,41 +5,44 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/russross/blackfriday"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Process the contents of a page.
|
// Process the contents of a page.
|
||||||
// It parses the metadata if any and uses the template if found
|
// 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)
|
metadata, markdown, err := extractMetadata(b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
var tmpl []byte
|
||||||
if metadata.Template != "" {
|
if metadata.Template != "" {
|
||||||
if t, ok := c.Templates[metadata.Template]; ok {
|
if t, ok := c.Templates[metadata.Template]; ok {
|
||||||
tmpl, err = loadTemplate(t)
|
tmpl, err = ioutil.ReadFile(t)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// if no template loaded
|
|
||||||
// use default template
|
|
||||||
if tmpl == nil {
|
|
||||||
tmpl = []byte(htmlTemplate)
|
|
||||||
}
|
|
||||||
|
|
||||||
// process markdown
|
// process markdown
|
||||||
if markdown, err = processMarkdown(markdown, metadata.Variables); err != nil {
|
markdown = blackfriday.Markdown(markdown, c.Renderer, 0)
|
||||||
return nil, err
|
// set it as body for template
|
||||||
}
|
metadata.Variables["body"] = string(markdown)
|
||||||
|
|
||||||
tmpl = bytes.Replace(tmpl, []byte("{{body}}"), markdown, 1)
|
return processTemplate(c, fpath, tmpl, metadata)
|
||||||
|
|
||||||
return tmpl, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractMetadata extracts metadata content from a page.
|
// extractMetadata extracts metadata content from a page.
|
||||||
|
@ -70,12 +73,22 @@ func extractMetadata(b []byte) (metadata Metadata, markdown []byte, err error) {
|
||||||
line := scanner.Bytes()
|
line := scanner.Bytes()
|
||||||
// closing identifier found
|
// closing identifier found
|
||||||
if bytes.Equal(bytes.TrimSpace(line), parser.Closing()) {
|
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 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.Write(line)
|
||||||
|
buf.WriteString("\r\n")
|
||||||
}
|
}
|
||||||
return metadata, nil, fmt.Errorf("Metadata not closed. '%v' not found", string(parser.Closing()))
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadTemplate(tmpl string) ([]byte, error) {
|
func processTemplate(c Config, fpath string, tmpl []byte, metadata Metadata) ([]byte, error) {
|
||||||
b, err := ioutil.ReadFile(tmpl)
|
// 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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !bytes.Contains(b, []byte("{{body}}")) {
|
if err = t.Execute(b, metadata.Variables); err != nil {
|
||||||
return nil, fmt.Errorf("template missing {{body}}")
|
return nil, err
|
||||||
}
|
}
|
||||||
return b, nil
|
return b.Bytes(), nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func processMarkdown(b []byte, vars map[string]interface{}) ([]byte, error) {
|
func defaultTemplate(c Config, metadata Metadata, fpath string) []byte {
|
||||||
buf := &bytes.Buffer{}
|
// else, use default template
|
||||||
t, err := template.New("markdown").Parse(string(b))
|
var scripts, styles bytes.Buffer
|
||||||
if err != nil {
|
for _, style := range c.Styles {
|
||||||
return nil, err
|
styles.WriteString(strings.Replace(cssTemplate, "{{url}}", style, 1))
|
||||||
|
styles.WriteString("\r\n")
|
||||||
}
|
}
|
||||||
if err := t.Execute(buf, vars); err != nil {
|
for _, script := range c.Scripts {
|
||||||
return nil, err
|
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 = `<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>{{title}}</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
{{css}}
|
||||||
|
{{js}}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{{.body}}
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
cssTemplate = `<link rel="stylesheet" href="{{url}}">`
|
||||||
|
jsTemplate = `<script src="{{url}}"></script>`
|
||||||
|
|
||||||
|
DefaultTemplate = "defaultTemplate"
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in a new issue