1
0
mirror of https://github.com/danog/gojekyll.git synced 2024-12-03 13:07:53 +01:00

Use Liquids.Drop

This commit is contained in:
Oliver Steele 2017-07-03 09:37:14 -04:00
parent 757b3da7f9
commit 966decdeb4
11 changed files with 273 additions and 168 deletions

View File

@ -14,6 +14,7 @@ import (
"github.com/osteele/gojekyll/pages" "github.com/osteele/gojekyll/pages"
"github.com/osteele/gojekyll/server" "github.com/osteele/gojekyll/server"
"github.com/osteele/gojekyll/sites" "github.com/osteele/gojekyll/sites"
"github.com/osteele/liquid"
) )
// main sets this // main sets this
@ -57,39 +58,6 @@ func serveCommand(site *sites.Site) error {
return server.Run(*open, printSetting) return server.Run(*open, printSetting)
} }
func varsCommand(site *sites.Site) error {
printSetting("Variables:", "")
siteData := site.SiteVariables()
// The YAML representation including collections is impractically large for debugging.
// (Actually it's circular, which the yaml package can't handle.)
// Neuter it. This destroys it as Liquid data, but that's okay in this context.
for _, c := range site.Collections {
siteData[c.Name] = fmt.Sprintf("<elided page data for %d items>", len(siteData[c.Name].([]interface{})))
}
var data map[string]interface{}
switch {
case *siteVariable:
data = siteData
case *dataVariable:
data = siteData["data"].(map[string]interface{})
if *variablePath != "" {
data = data[*variablePath].(map[string]interface{})
}
default:
page, err := pageFromPathOrRoute(site, *variablePath)
if err != nil {
return err
}
data = page.PageVariables()
}
b, err := yaml.Marshal(data)
if err != nil {
return err
}
fmt.Println(string(b))
return nil
}
func routesCommand(site *sites.Site) error { func routesCommand(site *sites.Site) error {
printSetting("Routes:", "") printSetting("Routes:", "")
urls := []string{} urls := []string{}
@ -138,3 +106,32 @@ func pageFromPathOrRoute(s *sites.Site, path string) (pages.Document, error) {
return page, nil return page, nil
} }
} }
func varsCommand(site *sites.Site) error {
printSetting("Variables:", "")
siteData := site.SiteVariables()
// The YAML representation including collections is impractically large for debugging.
// Neuter it. This destroys it as Liquid data, but that's okay in this context.
// for _, c := range site.Collections {
// siteData[c.Name] = fmt.Sprintf("<elided page data for %d items>", len(siteData[c.Name].([]pages.Page)))
// }
var data interface{}
switch {
case *siteVariable:
data = siteData
case *dataVariable:
data = siteData["data"]
default:
page, err := pageFromPathOrRoute(site, *variablePath)
if err != nil {
return err
}
data = page.(liquid.Drop).ToLiquid()
}
b, err := yaml.Marshal(data)
if err != nil {
return err
}
fmt.Println(string(b))
return nil
}

View File

@ -2,12 +2,12 @@ package collections
import ( import (
"path/filepath" "path/filepath"
"sort"
"github.com/osteele/gojekyll/config" "github.com/osteele/gojekyll/config"
"github.com/osteele/gojekyll/constants" "github.com/osteele/gojekyll/constants"
"github.com/osteele/gojekyll/pages" "github.com/osteele/gojekyll/pages"
"github.com/osteele/gojekyll/templates" "github.com/osteele/gojekyll/templates"
"github.com/osteele/liquid/generics"
) )
// Collection is a Jekyll collection https://jekyllrb.com/docs/collections/. // Collection is a Jekyll collection https://jekyllrb.com/docs/collections/.
@ -64,30 +64,39 @@ func (c *Collection) Pages() []pages.Page {
return c.pages return c.pages
} }
type pagesByDate struct{ pages []pages.Page }
// Len is part of sort.Interface.
func (p pagesByDate) Len() int {
return len(p.pages)
}
// Less is part of sort.Interface.
func (p pagesByDate) Less(i, j int) bool {
a, b := p.pages[i].PostDate(), p.pages[j].PostDate()
return a.Before(b)
}
// Swap is part of sort.Interface.
func (p pagesByDate) Swap(i, j int) {
pages := p.pages
pages[i], pages[j] = pages[j], pages[i]
}
// TemplateVariable returns an array of page objects, for use as the template variable // TemplateVariable returns an array of page objects, for use as the template variable
// value of the collection. // value of the collection.
func (c *Collection) TemplateVariable(ctx pages.RenderingContext, includeContent bool) ([]interface{}, error) { func (c *Collection) TemplateVariable(ctx pages.RenderingContext, includeContent bool) ([]pages.Page, error) {
pages := []interface{}{} if includeContent {
for _, p := range c.Pages() { for _, p := range c.Pages() {
v := p.PageVariables() _, err := p.Content(ctx)
if includeContent {
c, err := p.Content(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
v = templates.MergeVariableMaps(v, map[string]interface{}{
"content": string(c),
})
} }
pages = append(pages, v)
} }
pages := c.Pages()
if c.IsPostsCollection() { if c.IsPostsCollection() {
generics.SortByProperty(pages, "date", true) sort.Sort(pagesByDate{pages})
reversed := make([]interface{}, len(pages))
for i, v := range pages {
reversed[len(pages)-1-i] = v
}
pages = reversed
} }
return pages, nil return pages, nil
} }

View File

@ -4,13 +4,6 @@ import (
"regexp" "regexp"
) )
var nonAlphanumericSequenceMatcher = regexp.MustCompile(`[^[:alnum:]]+`)
// Slugify replaces each sequence of non-alphanumerics by a single hyphen
func Slugify(s string) string {
return nonAlphanumericSequenceMatcher.ReplaceAllString(s, "-")
}
// LeftPad left-pads s with spaces to n wide. It's an alternative to http://left-pad.io. // LeftPad left-pads s with spaces to n wide. It's an alternative to http://left-pad.io.
func LeftPad(s string, n int) string { func LeftPad(s string, n int) string {
if n <= len(s) { if n <= len(s) {
@ -23,6 +16,38 @@ func LeftPad(s string, n int) string {
return string(ws) + s return string(ws) + s
} }
type replaceStringFuncError error
// SafeReplaceAllStringFunc is like regexp.ReplaceAllStringFunc but passes an
// an error back from the replacement function.
func SafeReplaceAllStringFunc(re *regexp.Regexp, src string, repl func(m string) (string, error)) (out string, err error) {
// The ReplaceAllStringFunc callback signals errors via panic.
// Turn them into return values.
defer func() {
if r := recover(); r != nil {
if e, ok := r.(replaceStringFuncError); ok {
err = e.(error)
} else {
panic(r)
}
}
}()
return re.ReplaceAllStringFunc(src, func(m string) string {
out, err := repl(m)
if err != nil {
panic(replaceStringFuncError(err))
}
return out
}), nil
}
var nonAlphanumericSequenceMatcher = regexp.MustCompile(`[^[:alnum:]]+`)
// Slugify replaces each sequence of non-alphanumerics by a single hyphen
func Slugify(s string) string {
return nonAlphanumericSequenceMatcher.ReplaceAllString(s, "-")
}
// StringArrayToMap creates a map for use as a set. // StringArrayToMap creates a map for use as a set.
func StringArrayToMap(strings []string) map[string]bool { func StringArrayToMap(strings []string) map[string]bool {
stringMap := map[string]bool{} stringMap := map[string]bool{}

View File

@ -1,11 +1,34 @@
package helpers package helpers
import ( import (
"fmt"
"regexp"
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestLeftPad(t *testing.T) {
require.Equal(t, "abc", LeftPad("abc", 0))
require.Equal(t, "abc", LeftPad("abc", 3))
require.Equal(t, " abc", LeftPad("abc", 6))
}
func TestSafeReplaceAllStringFunc(t *testing.T) {
re := regexp.MustCompile(`\w+`)
out, err := SafeReplaceAllStringFunc(re, "1 > 0", func(m string) (string, error) {
return fmt.Sprint(m == "1"), nil
})
require.NoError(t, err)
require.Equal(t, "true > false", out)
out, err = SafeReplaceAllStringFunc(re, "1 > 0", func(m string) (string, error) {
return "", fmt.Errorf("an expected error")
})
require.Error(t, err)
require.Equal(t, "an expected error", err.Error())
}
func TestSlugify(t *testing.T) { func TestSlugify(t *testing.T) {
require.Equal(t, "abc", Slugify("abc")) require.Equal(t, "abc", Slugify("abc"))
require.Equal(t, "ab-c", Slugify("ab.c")) require.Equal(t, "ab-c", Slugify("ab.c"))
@ -13,11 +36,6 @@ func TestSlugify(t *testing.T) {
require.Equal(t, "ab-c", Slugify("ab()[]c")) require.Equal(t, "ab-c", Slugify("ab()[]c"))
require.Equal(t, "ab123-cde-f-g", Slugify("ab123(cde)[]f.g")) require.Equal(t, "ab123-cde-f-g", Slugify("ab123(cde)[]f.g"))
} }
func TestLeftPad(t *testing.T) {
require.Equal(t, "abc", LeftPad("abc", 0))
require.Equal(t, "abc", LeftPad("abc", 3))
require.Equal(t, " abc", LeftPad("abc", 6))
}
func TestStringArrayToMap(t *testing.T) { func TestStringArrayToMap(t *testing.T) {
input := []string{"a", "b", "c"} input := []string{"a", "b", "c"}

View File

@ -4,10 +4,15 @@ import (
"io" "io"
"github.com/osteele/gojekyll/pipelines" "github.com/osteele/gojekyll/pipelines"
"github.com/osteele/liquid"
"gopkg.in/yaml.v2"
) )
// Document is a Jekyll page or file. // Document is a Jekyll page or file.
type Document interface { type Document interface {
liquid.Drop
yaml.Marshaler
// Paths // Paths
SiteRelPath() string // relative to the site source directory SiteRelPath() string // relative to the site source directory
Permalink() string // relative URL path Permalink() string // relative URL path
@ -18,11 +23,11 @@ type Document interface {
Static() bool Static() bool
Write(RenderingContext, io.Writer) error Write(RenderingContext, io.Writer) error
// Variables Categories() []string
PageVariables() map[string]interface{} Tags() []string
// Document initialization uses this. // Document initialization
initPermalink() error setPermalink() error
} }
// RenderingContext provides context information for rendering. // RenderingContext provides context information for rendering.

View File

@ -6,11 +6,13 @@ import (
"path" "path"
"path/filepath" "path/filepath"
"reflect" "reflect"
"sort"
"strings" "strings"
"time" "time"
"github.com/osteele/gojekyll/helpers" "github.com/osteele/gojekyll/helpers"
"github.com/osteele/gojekyll/templates" "github.com/osteele/gojekyll/templates"
"github.com/osteele/liquid/generics"
) )
// file is embedded in StaticFile and page // file is embedded in StaticFile and page
@ -63,16 +65,15 @@ func NewFile(filename string, c Container, relpath string, defaults map[string]i
p = &StaticFile{fields} p = &StaticFile{fields}
} }
// Compute this after creating the page, in order to pick up the front matter. // Compute this after creating the page, in order to pick up the front matter.
err = p.initPermalink() if err = p.setPermalink(); err != nil {
if err != nil {
return nil, err return nil, err
} }
return p, nil return p, nil
} }
// Variables returns the attributes of the template page object. // ToLiquid returns the attributes of the template page object.
// See https://jekyllrb.com/docs/variables/#page-variables // See https://jekyllrb.com/docs/variables/#page-variables
func (f *file) PageVariables() map[string]interface{} { func (f *file) ToLiquid() interface{} {
var ( var (
relpath = "/" + filepath.ToSlash(f.relpath) relpath = "/" + filepath.ToSlash(f.relpath)
base = path.Base(relpath) base = path.Base(relpath)
@ -88,26 +89,34 @@ func (f *file) PageVariables() map[string]interface{} {
}) })
} }
func (f *file) categories() []string { // MarshalYAML is part of the yaml.Marshaler interface
if v, found := f.frontMatter["categories"]; found { // The variables subcommand uses this.
switch v := v.(type) { func (f *file) MarshalYAML() (interface{}, error) {
case string: return f.ToLiquid(), nil
return strings.Fields(v) }
case []interface{}:
sl := make([]string, len(v)) // Categories is in the File interface
for i, s := range v { func (f *file) Categories() []string {
switch s := s.(type) { return sortedStringValue(f.frontMatter["categories"])
case fmt.Stringer: }
sl[i] = s.String()
default: // Categories is in the File interface
sl[i] = fmt.Sprint(s) func (f *file) Tags() []string {
} return sortedStringValue(f.frontMatter["tags"])
} }
return sl
default: func sortedStringValue(field interface{}) []string {
fmt.Printf("%T", v) out := []string{}
panic("unimplemented") switch value := field.(type) {
} case string:
} out = strings.Fields(value)
return []string{templates.VariableMap(f.frontMatter).String("category", "")} case []interface{}:
if c, e := generics.Convert(value, reflect.TypeOf(out)); e == nil {
out = c.([]string)
}
case []string:
out = value
}
sort.Strings(out)
return out
} }

View File

@ -2,18 +2,22 @@ package pages
import ( import (
"bytes" "bytes"
"fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"path/filepath" "path/filepath"
"time"
"github.com/osteele/gojekyll/helpers" "github.com/osteele/gojekyll/helpers"
"github.com/osteele/gojekyll/templates" "github.com/osteele/gojekyll/templates"
"github.com/osteele/liquid/generics"
) )
// Page is a post or collection page. // Page is a post or collection page.
type Page interface { type Page interface {
Document Document
Content(rc RenderingContext) ([]byte, error) Content(rc RenderingContext) ([]byte, error)
PostDate() time.Time
} }
type page struct { type page struct {
@ -42,8 +46,8 @@ func newPage(filename string, f file) (*page, error) {
}, nil }, nil
} }
// PageVariables returns the attributes of the template page object. // ToLiquid is in the liquid.Drop interface.
func (p *page) PageVariables() map[string]interface{} { func (p *page) ToLiquid() interface{} {
var ( var (
relpath = p.relpath relpath = p.relpath
ext = filepath.Ext(relpath) ext = filepath.Ext(relpath)
@ -69,8 +73,8 @@ func (p *page) PageVariables() map[string]interface{} {
"title": base, // TODO capitalize "title": base, // TODO capitalize
// TODO excerpt category? categories tags // TODO excerpt category? categories tags
// TODO slug // TODO slug
"categories": []string{}, "categories": p.Categories(),
"tags": []string{}, "tags": p.Tags(),
// TODO Only present in collection pages https://jekyllrb.com/docs/collections/#documents // TODO Only present in collection pages https://jekyllrb.com/docs/collections/#documents
"relative_path": p.Path(), "relative_path": p.Path(),
@ -89,17 +93,53 @@ func (p *page) PageVariables() map[string]interface{} {
data[k] = v data[k] = v
} }
} }
if p.content != nil {
data["content"] = string(*p.content)
// TODO excerpt
}
return data return data
} }
// MarshalYAML is part of the yaml.Marshaler interface
// The variables subcommand uses this.
func (p *page) MarshalYAML() (interface{}, error) {
return p.ToLiquid(), nil
}
// TemplateContext returns the local variables for template evaluation // TemplateContext returns the local variables for template evaluation
func (p *page) TemplateContext(rc RenderingContext) map[string]interface{} { func (p *page) TemplateContext(rc RenderingContext) map[string]interface{} {
return map[string]interface{}{ return map[string]interface{}{
"page": p.PageVariables(), "page": p,
"site": rc.SiteVariables(), "site": rc.SiteVariables(),
} }
} }
// // Categories is part of the Page interface.
// func (p *page) Categories() []string {
// return []string{}
// }
// Tags is part of the Page interface.
func (p *page) Tags() []string {
return []string{}
}
// PostDate is part of the Page interface.
func (p *page) PostDate() time.Time {
switch value := p.frontMatter["date"].(type) {
case time.Time:
return value
case string:
t, err := generics.ParseTime(value)
if err == nil {
return t
}
default:
panic(fmt.Sprintf("expected a date %v", value))
}
panic("read posts should have set this")
}
// Write applies Liquid and Markdown, as appropriate. // Write applies Liquid and Markdown, as appropriate.
func (p *page) Write(rc RenderingContext, w io.Writer) error { func (p *page) Write(rc RenderingContext, w io.Writer) error {
rp := rc.RenderingPipeline() rp := rc.RenderingPipeline()

30
pages/pages_test.go Normal file
View File

@ -0,0 +1,30 @@
package pages
import (
"path/filepath"
"testing"
"github.com/osteele/gojekyll/config"
"github.com/stretchr/testify/require"
)
type containerMock struct {
c config.Config
prefix string
}
func (c containerMock) OutputExt(p string) string { return filepath.Ext(p) }
func (c containerMock) PathPrefix() string { return c.prefix }
func TestPageCategories(t *testing.T) {
require.Equal(t, []string{"a", "b"}, sortedStringValue("b a"))
require.Equal(t, []string{"a", "b"}, sortedStringValue([]interface{}{"b", "a"}))
require.Equal(t, []string{"a", "b"}, sortedStringValue([]string{"b", "a"}))
require.Equal(t, []string{}, sortedStringValue(3))
c := containerMock{config.Default(), ""}
fm := map[string]interface{}{"categories": "b a"}
f := file{container: c, frontMatter: fm}
require.Equal(t, []string{"a", "b"}, f.Categories())
}

View File

@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"path/filepath" "path/filepath"
"regexp" "regexp"
"sort"
"strconv" "strconv"
"strings" "strings"
@ -38,65 +37,53 @@ var permalinkDateVariables = map[string]string{
var templateVariableMatcher = regexp.MustCompile(`:\w+\b`) var templateVariableMatcher = regexp.MustCompile(`:\w+\b`)
// See https://jekyllrb.com/docs/permalinks/#template-variables // See https://jekyllrb.com/docs/permalinks/#template-variables
func (p *file) permalinkTemplateVariables() map[string]string { func (f *file) permalinkVariables() map[string]string {
var ( var (
relpath = strings.TrimPrefix(p.relpath, p.container.PathPrefix()) relpath = strings.TrimPrefix(f.relpath, f.container.PathPrefix())
root = helpers.TrimExt(relpath) root = helpers.TrimExt(relpath)
name = filepath.Base(root) name = filepath.Base(root)
categories = p.categories() fm = f.frontMatter
bindings = templates.VariableMap(fm)
slug = bindings.String("slug", helpers.Slugify(name))
) )
sort.Strings(categories) vars := map[string]string{
bindings := templates.VariableMap(p.frontMatter) "categories": strings.Join(f.Categories(), "/"),
// TODO recognize category; list
vs := map[string]string{
"categories": strings.Join(categories, "/"),
"collection": bindings.String("collection", ""), "collection": bindings.String("collection", ""),
"name": helpers.Slugify(name), "name": helpers.Slugify(name),
"path": "/" + root, "path": "/" + root, // TODO are we removing and then adding this?
"slug": bindings.String("slug", helpers.Slugify(name)), "slug": slug,
"title": bindings.String("slug", helpers.Slugify(name)), "title": slug,
// The following aren't documented, but is evident // The following aren't documented, but are evident
"output_ext": p.OutputExt(), "output_ext": f.OutputExt(),
"y_day": strconv.Itoa(p.fileModTime.YearDay()), "y_day": strconv.Itoa(f.fileModTime.YearDay()),
} }
for name, f := range permalinkDateVariables { for k, v := range permalinkDateVariables {
vs[name] = p.fileModTime.Format(f) vars[k] = f.fileModTime.Format(v)
} }
return vs return vars
} }
func (p *file) expandPermalink() (s string, err error) { func (f *file) computePermalink(vars map[string]string) (src string, err error) {
pattern := templates.VariableMap(p.frontMatter).String("permalink", constants.DefaultPermalinkPattern) pattern := templates.VariableMap(f.frontMatter).String("permalink", constants.DefaultPermalinkPattern)
if p, found := PermalinkStyles[pattern]; found { if p, found := PermalinkStyles[pattern]; found {
pattern = p pattern = p
} }
templateVariables := p.permalinkTemplateVariables() templateVariables := f.permalinkVariables()
// The ReplaceAllStringFunc callback signals errors via panic. s, err := helpers.SafeReplaceAllStringFunc(templateVariableMatcher, pattern, func(m string) (string, error) {
// Turn them into return values.
defer func() {
if r := recover(); r != nil {
if e, ok := r.(error); ok {
err = e
} else {
panic(r)
}
}
}()
s = templateVariableMatcher.ReplaceAllStringFunc(pattern, func(m string) string {
varname := m[1:] varname := m[1:]
value, found := templateVariables[varname] value, found := templateVariables[varname]
if !found { if !found {
panic(fmt.Errorf("unknown variable %q in permalink template %q", varname, pattern)) return "", fmt.Errorf("unknown variable %q in permalink template %q", varname, pattern)
} }
return value return value, nil
}) })
if err != nil {
return "", err
}
return helpers.URLPathClean("/" + s), nil return helpers.URLPathClean("/" + s), nil
} }
// The permalink is computed once instead of on demand, so that subsequent func (f *file) setPermalink() (err error) {
// access needn't check for an error. f.permalink, err = f.computePermalink(f.permalinkVariables())
func (p *file) initPermalink() (err error) {
p.permalink, err = p.expandPermalink()
return return
} }

View File

@ -10,16 +10,6 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
type containerMock struct {
c config.Config
p string
}
func (c containerMock) AbsDir() string { return "" }
func (c containerMock) Config() config.Config { return c.c }
func (c containerMock) OutputExt(p string) string { return filepath.Ext(p) }
func (c containerMock) PathPrefix() string { return c.p }
type pathTest struct{ path, pattern, out string } type pathTest struct{ path, pattern, out string }
var tests = []pathTest{ var tests = []pathTest{
@ -53,17 +43,17 @@ func TestExpandPermalinkPattern(t *testing.T) {
) )
testPermalinkPattern := func(pattern, path string, data map[string]interface{}) (string, error) { testPermalinkPattern := func(pattern, path string, data map[string]interface{}) (string, error) {
vs := templates.MergeVariableMaps(data, map[string]interface{}{"permalink": pattern}) fm := templates.MergeVariableMaps(data, map[string]interface{}{"permalink": pattern})
ext := filepath.Ext(path) ext := filepath.Ext(path)
switch ext { switch ext {
case ".md", ".markdown": case ".md", ".markdown":
ext = ".html" ext = ".html"
} }
p := file{container: c, relpath: path, frontMatter: vs, outputExt: ext} p := file{container: c, relpath: path, frontMatter: fm, outputExt: ext}
t0, err := time.Parse(time.RFC3339, "2006-02-03T15:04:05Z") t0, err := time.Parse(time.RFC3339, "2006-02-03T15:04:05Z")
require.NoError(t, err) require.NoError(t, err)
p.fileModTime = t0 p.fileModTime = t0
return p.expandPermalink() return p.computePermalink(p.permalinkVariables())
} }
runTests := func(tests []pathTest) { runTests := func(tests []pathTest) {

View File

@ -1,9 +1,9 @@
package sites package sites
import ( import (
"fmt"
"time" "time"
"github.com/osteele/gojekyll/pages"
"github.com/osteele/gojekyll/templates" "github.com/osteele/gojekyll/templates"
"github.com/osteele/liquid/generics" "github.com/osteele/liquid/generics"
) )
@ -48,27 +48,22 @@ func (s *Site) setCollectionVariables(includeContent bool) error {
return nil return nil
} }
func (s *Site) setPostVariables(pages []interface{}) { func (s *Site) setPostVariables(ps []pages.Page) {
var ( var (
related = pages related = ps
categories = map[string][]interface{}{} categories = map[string][]pages.Page{}
tags = map[string][]interface{}{} tags = map[string][]pages.Page{}
) )
if len(related) > 10 { if len(related) > 10 {
related = related[:10] related = related[:10]
} }
for _, p := range pages { for _, p := range ps {
b := p.(map[string]interface{}) for _, k := range p.Categories() {
switch cs := b["categories"].(type) { ps, found := categories[k]
case []interface{}: if !found {
for _, c := range cs { ps = []pages.Page{}
key := fmt.Sprint(c)
ps, found := categories[key]
if !found {
ps = []interface{}{}
}
categories[key] = append(ps, p)
} }
categories[k] = append(ps, p)
} }
} }
s.siteVariables["categories"] = categories s.siteVariables["categories"] = categories