2017-06-10 21:38:09 +02:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"fmt"
|
2017-06-12 02:51:01 +02:00
|
|
|
"io"
|
2017-06-10 21:38:09 +02:00
|
|
|
"io/ioutil"
|
2017-06-11 17:51:25 +02:00
|
|
|
"os"
|
2017-06-10 21:38:09 +02:00
|
|
|
"path/filepath"
|
2017-06-15 02:44:22 +02:00
|
|
|
"reflect"
|
2017-06-10 21:38:09 +02:00
|
|
|
"regexp"
|
2017-06-13 14:54:35 +02:00
|
|
|
"strings"
|
2017-06-10 21:38:09 +02:00
|
|
|
|
2017-06-15 15:07:06 +02:00
|
|
|
yaml "gopkg.in/yaml.v2"
|
|
|
|
|
2017-06-10 21:38:09 +02:00
|
|
|
"github.com/acstech/liquid"
|
|
|
|
"github.com/russross/blackfriday"
|
|
|
|
)
|
|
|
|
|
2017-06-11 17:51:25 +02:00
|
|
|
var (
|
2017-06-15 16:17:21 +02:00
|
|
|
frontMatterMatcher = regexp.MustCompile(`(?s)^---\n(.+?\n)---\n`)
|
|
|
|
emptyFontMatterMatcher = regexp.MustCompile(`(?s)^---\n+---\n`)
|
2017-06-11 17:51:25 +02:00
|
|
|
)
|
|
|
|
|
2017-06-15 13:19:49 +02:00
|
|
|
// Page is a Jekyll page.
|
2017-06-15 02:44:22 +02:00
|
|
|
type Page interface {
|
|
|
|
Path() string
|
|
|
|
Source() string
|
|
|
|
Static() bool
|
|
|
|
Published() bool
|
|
|
|
Permalink() string
|
2017-06-15 13:19:49 +02:00
|
|
|
TemplateObject() VariableMap
|
2017-06-15 02:44:22 +02:00
|
|
|
Write(io.Writer) error
|
|
|
|
DebugVariables() VariableMap
|
2017-06-15 15:31:04 +02:00
|
|
|
|
|
|
|
setPermalink(string)
|
2017-06-15 02:44:22 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
type pageFields struct {
|
|
|
|
path string // this is the relative path
|
|
|
|
permalink string
|
2017-06-15 13:31:52 +02:00
|
|
|
frontMatter VariableMap
|
2017-06-10 21:38:09 +02:00
|
|
|
}
|
|
|
|
|
2017-06-15 02:44:22 +02:00
|
|
|
func (p *pageFields) String() string {
|
|
|
|
return fmt.Sprintf("%s{Path=%v, Permalink=%v}",
|
|
|
|
reflect.TypeOf(p).Name(), p.path, p.permalink)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *pageFields) Path() string { return p.path }
|
|
|
|
func (p *pageFields) Permalink() string { return p.permalink }
|
2017-06-15 15:01:31 +02:00
|
|
|
|
|
|
|
func (p *pageFields) Published() bool {
|
|
|
|
return p.frontMatter.Bool("published", true)
|
|
|
|
}
|
2017-06-15 02:44:22 +02:00
|
|
|
|
2017-06-16 04:32:10 +02:00
|
|
|
// The permalink is computed once instead of on demand, so that subsequent
|
|
|
|
// access needn't check for an error.
|
2017-06-15 15:31:04 +02:00
|
|
|
func (p *pageFields) setPermalink(permalink string) {
|
|
|
|
p.permalink = permalink
|
|
|
|
}
|
|
|
|
|
2017-06-15 15:07:06 +02:00
|
|
|
// ReadPage reads a Page from a file, using defaults as the default front matter.
|
|
|
|
func ReadPage(path string, defaults VariableMap) (p Page, err error) {
|
2017-06-15 15:31:04 +02:00
|
|
|
// TODO only read the first four bytes unless it's dynamic
|
2017-06-15 15:07:06 +02:00
|
|
|
source, err := ioutil.ReadFile(filepath.Join(site.Source, path))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2017-06-15 15:31:04 +02:00
|
|
|
data := pageFields{
|
|
|
|
path: path,
|
|
|
|
frontMatter: defaults,
|
|
|
|
}
|
|
|
|
if string(source[:4]) == "---\n" {
|
|
|
|
p, err = makeDynamicPage(&data, source)
|
2017-06-15 15:07:06 +02:00
|
|
|
} else {
|
2017-06-15 15:31:04 +02:00
|
|
|
p = &StaticPage{data}
|
|
|
|
}
|
|
|
|
if p != nil {
|
|
|
|
// Compute this after creating the page, to pick up the the front matter.
|
2017-06-16 04:32:10 +02:00
|
|
|
pattern := data.frontMatter.String("permalink", ":path:output_ext")
|
2017-06-15 15:31:04 +02:00
|
|
|
permalink, err := expandPermalinkPattern(pattern, data.path, data.frontMatter)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2017-06-16 04:32:10 +02:00
|
|
|
println(path, pattern, permalink)
|
2017-06-15 15:31:04 +02:00
|
|
|
p.setPermalink(permalink)
|
2017-06-15 15:07:06 +02:00
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *StaticPage) Write(w io.Writer) error {
|
|
|
|
source, err := ioutil.ReadFile(p.Source())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
_, err = w.Write(source)
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// TemplateObject returns the attributes of the template page object.
|
|
|
|
// See https://jekyllrb.com/docs/variables/#page-variables
|
|
|
|
func (p *pageFields) TemplateObject() VariableMap {
|
|
|
|
var (
|
|
|
|
path = "/" + p.path
|
|
|
|
base = filepath.Base(path)
|
|
|
|
ext = filepath.Ext(path)
|
|
|
|
)
|
|
|
|
|
|
|
|
return VariableMap{
|
|
|
|
"path": path,
|
|
|
|
"modified_time": 0, // TODO
|
|
|
|
"name": base,
|
|
|
|
"basename": base[:len(base)-len(ext)],
|
|
|
|
"extname": ext,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// DebugVariables returns a map that's useful to present during diagnostics.
|
|
|
|
// For a static page, this is just the page's template object attributes.
|
|
|
|
func (p *pageFields) DebugVariables() VariableMap {
|
|
|
|
return p.TemplateObject()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Source returns the file path of the page source.
|
|
|
|
func (p *pageFields) Source() string {
|
|
|
|
return filepath.Join(site.Source, p.path)
|
|
|
|
}
|
|
|
|
|
2017-06-15 13:19:49 +02:00
|
|
|
// StaticPage is a static page.
|
2017-06-15 02:44:22 +02:00
|
|
|
type StaticPage struct {
|
|
|
|
pageFields
|
2017-06-10 21:38:09 +02:00
|
|
|
}
|
|
|
|
|
2017-06-15 15:07:06 +02:00
|
|
|
// Static returns a bool indicating that the page is a static page.
|
|
|
|
func (p *StaticPage) Static() bool { return true }
|
|
|
|
|
|
|
|
// TemplateObject returns metadata for use in the representation of the page as a collection item
|
|
|
|
func (p *StaticPage) TemplateObject() VariableMap {
|
|
|
|
return mergeVariableMaps(p.frontMatter, p.pageFields.TemplateObject())
|
|
|
|
}
|
|
|
|
|
2017-06-15 13:19:49 +02:00
|
|
|
// DynamicPage is a static page, that includes frontmatter.
|
2017-06-15 02:44:22 +02:00
|
|
|
type DynamicPage struct {
|
|
|
|
pageFields
|
|
|
|
Content []byte
|
|
|
|
}
|
|
|
|
|
2017-06-15 17:51:40 +02:00
|
|
|
// Static returns a bool indicating that the page is a not static page.
|
2017-06-15 02:44:22 +02:00
|
|
|
func (p *DynamicPage) Static() bool { return false }
|
|
|
|
|
2017-06-15 15:31:04 +02:00
|
|
|
func makeDynamicPage(data *pageFields, source []byte) (*DynamicPage, error) {
|
2017-06-15 17:51:40 +02:00
|
|
|
frontMatter, err := readFrontMatter(&source)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
data.frontMatter = mergeVariableMaps(data.frontMatter, frontMatter)
|
|
|
|
p := &DynamicPage{
|
|
|
|
pageFields: *data,
|
|
|
|
Content: source,
|
|
|
|
}
|
|
|
|
return p, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func readFrontMatter(sourcePtr *[]byte) (frontMatter VariableMap, err error) {
|
|
|
|
var (
|
|
|
|
source = *sourcePtr
|
|
|
|
start = 0
|
|
|
|
)
|
2017-06-15 16:17:21 +02:00
|
|
|
if match := frontMatterMatcher.FindSubmatchIndex(source); match != nil {
|
|
|
|
start = match[1]
|
2017-06-15 17:51:40 +02:00
|
|
|
if err = yaml.Unmarshal(source[match[2]:match[3]], &frontMatter); err != nil {
|
|
|
|
return
|
2017-06-15 16:17:21 +02:00
|
|
|
}
|
|
|
|
} else if match := emptyFontMatterMatcher.FindSubmatchIndex(source); match != nil {
|
|
|
|
start = match[1]
|
|
|
|
}
|
2017-06-15 15:31:04 +02:00
|
|
|
// This fixes the line numbers for template errors
|
|
|
|
// TODO find a less hacky solution
|
2017-06-15 17:51:40 +02:00
|
|
|
*sourcePtr = append(
|
2017-06-15 16:17:21 +02:00
|
|
|
regexp.MustCompile(`[^\n\r]+`).ReplaceAllLiteral(source[:start], []byte{}),
|
|
|
|
source[start:]...)
|
2017-06-15 17:51:40 +02:00
|
|
|
return
|
2017-06-15 02:44:22 +02:00
|
|
|
}
|
|
|
|
|
2017-06-15 13:19:49 +02:00
|
|
|
// TemplateObject returns the attributes of the template page object.
|
|
|
|
func (p *DynamicPage) TemplateObject() VariableMap {
|
2017-06-15 15:01:42 +02:00
|
|
|
var (
|
|
|
|
path = p.path
|
|
|
|
ext = filepath.Ext(path)
|
|
|
|
root = p.path[:len(path)-len(ext)]
|
|
|
|
base = filepath.Base(root)
|
|
|
|
)
|
|
|
|
|
2017-06-14 23:41:15 +02:00
|
|
|
data := VariableMap{
|
2017-06-15 15:01:42 +02:00
|
|
|
"path": p.path,
|
2017-06-15 02:44:22 +02:00
|
|
|
"url": p.Permalink(),
|
2017-06-15 15:01:42 +02:00
|
|
|
// TODO content output
|
|
|
|
|
|
|
|
// not documented, but present in both collection and non-collection pages
|
|
|
|
"permalink": p.Permalink(),
|
|
|
|
|
|
|
|
// TODO only in non-collection pages:
|
|
|
|
// TODO dir
|
|
|
|
// TODO name
|
|
|
|
// TODO next previous
|
|
|
|
|
|
|
|
// TODO Documented as present in all pages, but de facto only defined for collection pages
|
|
|
|
"id": base,
|
|
|
|
"title": base, // TODO capitalize
|
|
|
|
// TODO date (of the collection?) 2017-06-15 07:44:21 -0400
|
|
|
|
// TODO excerpt category? categories tags
|
|
|
|
// TODO slug
|
|
|
|
|
|
|
|
// TODO Only present in collection pages https://jekyllrb.com/docs/collections/#documents
|
2017-06-15 02:44:22 +02:00
|
|
|
"relative_path": p.Path(),
|
2017-06-15 15:01:42 +02:00
|
|
|
// TODO collection(name)
|
|
|
|
|
|
|
|
// TODO undocumented; only present in collection pages:
|
|
|
|
"ext": ext,
|
2017-06-12 00:36:31 +02:00
|
|
|
}
|
2017-06-15 13:31:52 +02:00
|
|
|
for k, v := range p.frontMatter {
|
2017-06-12 23:12:40 +02:00
|
|
|
switch k {
|
2017-06-15 15:01:42 +02:00
|
|
|
// doc implies these aren't present, but they appear to be present in a collection page:
|
|
|
|
// case "layout", "published":
|
|
|
|
case "permalink":
|
|
|
|
// omit this, in order to use the value above
|
2017-06-12 23:12:40 +02:00
|
|
|
default:
|
|
|
|
data[k] = v
|
|
|
|
}
|
2017-06-12 00:36:31 +02:00
|
|
|
}
|
|
|
|
return data
|
|
|
|
}
|
|
|
|
|
2017-06-15 13:19:49 +02:00
|
|
|
// TemplateVariables returns the local variables for template evaluation
|
|
|
|
func (p *DynamicPage) TemplateVariables() VariableMap {
|
2017-06-14 23:41:15 +02:00
|
|
|
return VariableMap{
|
2017-06-15 13:19:49 +02:00
|
|
|
"page": p.TemplateObject(),
|
2017-06-14 19:20:52 +02:00
|
|
|
"site": site.Variables,
|
2017-06-12 23:12:40 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-15 13:19:49 +02:00
|
|
|
// DebugVariables returns a map that's useful to present during diagnostics.
|
|
|
|
// For a dynamic page, this is the local variable map that is used for template evaluation.
|
2017-06-15 02:44:22 +02:00
|
|
|
func (p *DynamicPage) DebugVariables() VariableMap {
|
2017-06-15 13:19:49 +02:00
|
|
|
return p.TemplateVariables()
|
2017-06-15 02:44:22 +02:00
|
|
|
}
|
2017-06-10 21:38:09 +02:00
|
|
|
|
2017-06-15 17:30:44 +02:00
|
|
|
// renderTemplate is a wrapper around liquid template.Render that turns panics into errors
|
|
|
|
func renderTemplate(template *liquid.Template, variables VariableMap) (bs []byte, err error) {
|
2017-06-13 17:00:24 +02:00
|
|
|
defer func() {
|
2017-06-15 17:30:44 +02:00
|
|
|
if r := recover(); r != nil {
|
|
|
|
if e, ok := r.(error); ok {
|
|
|
|
err = e
|
|
|
|
} else {
|
|
|
|
panic(r)
|
|
|
|
}
|
2017-06-13 17:00:24 +02:00
|
|
|
}
|
|
|
|
}()
|
2017-06-15 17:30:44 +02:00
|
|
|
writer := new(bytes.Buffer)
|
|
|
|
template.Render(writer, variables)
|
|
|
|
return writer.Bytes(), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// applyTemplate parses and then renders the template.
|
|
|
|
func parseAndApplyTemplate(bs []byte, variables VariableMap) ([]byte, error) {
|
|
|
|
template, err := liquid.Parse(bs, nil)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return renderTemplate(template, variables)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Write applies Liquid and Markdown, as appropriate.
|
|
|
|
func (p *DynamicPage) Write(w io.Writer) error {
|
|
|
|
body, err := parseAndApplyTemplate(p.Content, p.TemplateVariables())
|
2017-06-12 02:30:25 +02:00
|
|
|
if err != nil {
|
2017-06-13 14:54:35 +02:00
|
|
|
err := &os.PathError{Op: "Liquid Error", Path: p.Source(), Err: err}
|
2017-06-12 02:51:01 +02:00
|
|
|
return err
|
2017-06-12 02:30:25 +02:00
|
|
|
}
|
|
|
|
|
2017-06-15 02:44:22 +02:00
|
|
|
if isMarkdown(p.path) {
|
2017-06-12 02:51:01 +02:00
|
|
|
body = blackfriday.MarkdownCommon(body)
|
2017-06-12 02:30:25 +02:00
|
|
|
}
|
|
|
|
|
2017-06-15 17:51:40 +02:00
|
|
|
layoutFrontMatter := p.frontMatter
|
|
|
|
for {
|
|
|
|
layoutName := layoutFrontMatter.String("layout", "")
|
|
|
|
if layoutName == "" {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
template, err := site.FindLayout(layoutName, &layoutFrontMatter)
|
2017-06-15 17:30:44 +02:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
vars := mergeVariableMaps(p.TemplateVariables(), VariableMap{
|
|
|
|
"content": body,
|
2017-06-15 17:51:40 +02:00
|
|
|
"layout": layoutFrontMatter,
|
2017-06-15 17:30:44 +02:00
|
|
|
})
|
2017-06-15 17:51:40 +02:00
|
|
|
body, err = renderTemplate(template, vars)
|
2017-06-15 17:30:44 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-06-12 02:51:01 +02:00
|
|
|
_, err = w.Write(body)
|
|
|
|
return err
|
2017-06-12 02:30:25 +02:00
|
|
|
}
|
|
|
|
|
2017-06-13 14:54:35 +02:00
|
|
|
func isMarkdown(path string) bool {
|
|
|
|
ext := filepath.Ext(path)
|
2017-06-13 17:00:24 +02:00
|
|
|
return site.MarkdownExtensions()[strings.TrimLeft(ext, ".")]
|
2017-06-13 14:54:35 +02:00
|
|
|
}
|