1
0
mirror of https://github.com/danog/gojekyll.git synced 2024-11-27 15:44:40 +01:00
gojekyll/page.go

295 lines
7.3 KiB
Go
Raw Normal View History

package main
import (
"bytes"
"fmt"
2017-06-12 02:51:01 +02:00
"io"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"regexp"
2017-06-15 15:07:06 +02:00
yaml "gopkg.in/yaml.v2"
"github.com/acstech/liquid"
"github.com/russross/blackfriday"
)
var (
2017-06-15 16:17:21 +02:00
frontMatterMatcher = regexp.MustCompile(`(?s)^---\n(.+?\n)---\n`)
emptyFontMatterMatcher = regexp.MustCompile(`(?s)^---\n+---\n`)
)
2017-06-15 13:19:49 +02:00
// Page is a Jekyll page.
type Page interface {
Path() string
Site() *Site
Source() string
Static() bool
Published() bool
Permalink() string
2017-06-15 13:19:49 +02:00
TemplateObject() VariableMap
Write(io.Writer) error
DebugVariables() VariableMap
initPermalink() error
}
type pageFields struct {
site *Site
path string // this is the relative path
permalink string
2017-06-15 13:31:52 +02:00
frontMatter VariableMap
}
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)
}
func (p *pageFields) Site() *Site { return p.site }
2017-06-15 15:07:06 +02:00
// ReadPage reads a Page from a file, using defaults as the default front matter.
func ReadPage(site *Site, rel string, defaults VariableMap) (p Page, err error) {
2017-06-16 19:47:11 +02:00
magic, err := ReadFileMagic(filepath.Join(site.Source, rel))
2017-06-15 15:07:06 +02:00
if err != nil {
2017-06-16 19:47:11 +02:00
return
2017-06-15 15:07:06 +02:00
}
2017-06-16 19:47:11 +02:00
fields := pageFields{
site: site,
2017-06-16 19:47:11 +02:00
path: rel,
frontMatter: defaults,
}
2017-06-16 19:47:11 +02:00
if string(magic) == "---\n" {
p, err = readDynamicPage(fields, rel)
2017-06-15 15:07:06 +02:00
} else {
2017-06-16 19:47:11 +02:00
p = &StaticPage{fields}
}
if p != nil {
2017-06-16 19:47:11 +02:00
// Compute this after creating the page, in order to pick up the front matter.
err := p.initPermalink()
if err != nil {
return nil, err
}
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.
type StaticPage struct {
pageFields
}
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.
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.
func (p *DynamicPage) Static() bool { return false }
func readDynamicPage(fields pageFields, rel string) (p *DynamicPage, err error) {
2017-06-16 19:47:11 +02:00
data, err := ioutil.ReadFile(filepath.Join(site.Source, rel))
2017-06-15 17:51:40 +02:00
if err != nil {
2017-06-16 19:47:11 +02:00
return
2017-06-15 17:51:40 +02:00
}
2017-06-16 19:47:11 +02:00
data = bytes.Replace(data, []byte("\r"), []byte("\n"), -1)
frontMatter, err := readFrontMatter(&data)
if err != nil {
return
2017-06-15 17:51:40 +02:00
}
2017-06-16 19:47:11 +02:00
fields.frontMatter = mergeVariableMaps(fields.frontMatter, frontMatter)
return &DynamicPage{
pageFields: fields,
2017-06-16 19:47:11 +02:00
Content: data,
}, nil
2017-06-15 17:51:40 +02:00
}
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]
}
// 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 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,
"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
"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-15 13:31:52 +02:00
for k, v := range p.frontMatter {
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
default:
data[k] = v
}
}
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-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.
func (p *DynamicPage) DebugVariables() VariableMap {
2017-06-15 13:19:49 +02:00
return p.TemplateVariables()
}
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) {
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-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.
2017-06-16 19:47:11 +02:00
func (p *DynamicPage) Write(w io.Writer) (err error) {
2017-06-15 17:30:44 +02:00
body, err := parseAndApplyTemplate(p.Content, p.TemplateVariables())
2017-06-12 02:30:25 +02:00
if err != nil {
2017-06-16 19:47:11 +02:00
err = &os.PathError{Op: "Liquid Error", Path: p.Source(), Err: err}
return
2017-06-12 02:30:25 +02:00
}
if p.Site().IsMarkdown(p.path) {
2017-06-12 02:51:01 +02:00
body = blackfriday.MarkdownCommon(body)
2017-06-16 19:47:11 +02:00
body, err = p.applyLayout(p.frontMatter, body)
if err != nil {
return
}
}
if p.Site().IsSassPath(p.path) {
2017-06-16 19:47:11 +02:00
return p.writeSass(w, body)
2017-06-12 02:30:25 +02:00
}
2017-06-16 19:47:11 +02:00
_, err = w.Write(body)
return
}