1
0
mirror of https://github.com/danog/gojekyll.git synced 2024-11-30 06:59:04 +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/server"
"github.com/osteele/gojekyll/sites"
"github.com/osteele/liquid"
)
// main sets this
@ -57,39 +58,6 @@ func serveCommand(site *sites.Site) error {
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 {
printSetting("Routes:", "")
urls := []string{}
@ -138,3 +106,32 @@ func pageFromPathOrRoute(s *sites.Site, path string) (pages.Document, error) {
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 (
"path/filepath"
"sort"
"github.com/osteele/gojekyll/config"
"github.com/osteele/gojekyll/constants"
"github.com/osteele/gojekyll/pages"
"github.com/osteele/gojekyll/templates"
"github.com/osteele/liquid/generics"
)
// Collection is a Jekyll collection https://jekyllrb.com/docs/collections/.
@ -64,30 +64,39 @@ func (c *Collection) Pages() []pages.Page {
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
// value of the collection.
func (c *Collection) TemplateVariable(ctx pages.RenderingContext, includeContent bool) ([]interface{}, error) {
pages := []interface{}{}
for _, p := range c.Pages() {
v := p.PageVariables()
if includeContent {
c, err := p.Content(ctx)
func (c *Collection) TemplateVariable(ctx pages.RenderingContext, includeContent bool) ([]pages.Page, error) {
if includeContent {
for _, p := range c.Pages() {
_, err := p.Content(ctx)
if err != nil {
return nil, err
}
v = templates.MergeVariableMaps(v, map[string]interface{}{
"content": string(c),
})
}
pages = append(pages, v)
}
pages := c.Pages()
if c.IsPostsCollection() {
generics.SortByProperty(pages, "date", true)
reversed := make([]interface{}, len(pages))
for i, v := range pages {
reversed[len(pages)-1-i] = v
}
pages = reversed
sort.Sort(pagesByDate{pages})
}
return pages, nil
}

View File

@ -4,13 +4,6 @@ import (
"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.
func LeftPad(s string, n int) string {
if n <= len(s) {
@ -23,6 +16,38 @@ func LeftPad(s string, n int) string {
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.
func StringArrayToMap(strings []string) map[string]bool {
stringMap := map[string]bool{}

View File

@ -1,11 +1,34 @@
package helpers
import (
"fmt"
"regexp"
"testing"
"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) {
require.Equal(t, "abc", Slugify("abc"))
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, "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) {
input := []string{"a", "b", "c"}

View File

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

View File

@ -6,11 +6,13 @@ import (
"path"
"path/filepath"
"reflect"
"sort"
"strings"
"time"
"github.com/osteele/gojekyll/helpers"
"github.com/osteele/gojekyll/templates"
"github.com/osteele/liquid/generics"
)
// 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}
}
// Compute this after creating the page, in order to pick up the front matter.
err = p.initPermalink()
if err != nil {
if err = p.setPermalink(); err != nil {
return nil, err
}
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
func (f *file) PageVariables() map[string]interface{} {
func (f *file) ToLiquid() interface{} {
var (
relpath = "/" + filepath.ToSlash(f.relpath)
base = path.Base(relpath)
@ -88,26 +89,34 @@ func (f *file) PageVariables() map[string]interface{} {
})
}
func (f *file) categories() []string {
if v, found := f.frontMatter["categories"]; found {
switch v := v.(type) {
case string:
return strings.Fields(v)
case []interface{}:
sl := make([]string, len(v))
for i, s := range v {
switch s := s.(type) {
case fmt.Stringer:
sl[i] = s.String()
default:
sl[i] = fmt.Sprint(s)
}
}
return sl
default:
fmt.Printf("%T", v)
panic("unimplemented")
}
}
return []string{templates.VariableMap(f.frontMatter).String("category", "")}
// MarshalYAML is part of the yaml.Marshaler interface
// The variables subcommand uses this.
func (f *file) MarshalYAML() (interface{}, error) {
return f.ToLiquid(), nil
}
// Categories is in the File interface
func (f *file) Categories() []string {
return sortedStringValue(f.frontMatter["categories"])
}
// Categories is in the File interface
func (f *file) Tags() []string {
return sortedStringValue(f.frontMatter["tags"])
}
func sortedStringValue(field interface{}) []string {
out := []string{}
switch value := field.(type) {
case string:
out = strings.Fields(value)
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 (
"bytes"
"fmt"
"io"
"io/ioutil"
"path/filepath"
"time"
"github.com/osteele/gojekyll/helpers"
"github.com/osteele/gojekyll/templates"
"github.com/osteele/liquid/generics"
)
// Page is a post or collection page.
type Page interface {
Document
Content(rc RenderingContext) ([]byte, error)
PostDate() time.Time
}
type page struct {
@ -42,8 +46,8 @@ func newPage(filename string, f file) (*page, error) {
}, nil
}
// PageVariables returns the attributes of the template page object.
func (p *page) PageVariables() map[string]interface{} {
// ToLiquid is in the liquid.Drop interface.
func (p *page) ToLiquid() interface{} {
var (
relpath = p.relpath
ext = filepath.Ext(relpath)
@ -69,8 +73,8 @@ func (p *page) PageVariables() map[string]interface{} {
"title": base, // TODO capitalize
// TODO excerpt category? categories tags
// TODO slug
"categories": []string{},
"tags": []string{},
"categories": p.Categories(),
"tags": p.Tags(),
// TODO Only present in collection pages https://jekyllrb.com/docs/collections/#documents
"relative_path": p.Path(),
@ -89,17 +93,53 @@ func (p *page) PageVariables() map[string]interface{} {
data[k] = v
}
}
if p.content != nil {
data["content"] = string(*p.content)
// TODO excerpt
}
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
func (p *page) TemplateContext(rc RenderingContext) map[string]interface{} {
return map[string]interface{}{
"page": p.PageVariables(),
"page": p,
"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.
func (p *page) Write(rc RenderingContext, w io.Writer) error {
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"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
@ -38,65 +37,53 @@ var permalinkDateVariables = map[string]string{
var templateVariableMatcher = regexp.MustCompile(`:\w+\b`)
// See https://jekyllrb.com/docs/permalinks/#template-variables
func (p *file) permalinkTemplateVariables() map[string]string {
func (f *file) permalinkVariables() map[string]string {
var (
relpath = strings.TrimPrefix(p.relpath, p.container.PathPrefix())
root = helpers.TrimExt(relpath)
name = filepath.Base(root)
categories = p.categories()
relpath = strings.TrimPrefix(f.relpath, f.container.PathPrefix())
root = helpers.TrimExt(relpath)
name = filepath.Base(root)
fm = f.frontMatter
bindings = templates.VariableMap(fm)
slug = bindings.String("slug", helpers.Slugify(name))
)
sort.Strings(categories)
bindings := templates.VariableMap(p.frontMatter)
// TODO recognize category; list
vs := map[string]string{
"categories": strings.Join(categories, "/"),
vars := map[string]string{
"categories": strings.Join(f.Categories(), "/"),
"collection": bindings.String("collection", ""),
"name": helpers.Slugify(name),
"path": "/" + root,
"slug": bindings.String("slug", helpers.Slugify(name)),
"title": bindings.String("slug", helpers.Slugify(name)),
// The following aren't documented, but is evident
"output_ext": p.OutputExt(),
"y_day": strconv.Itoa(p.fileModTime.YearDay()),
"path": "/" + root, // TODO are we removing and then adding this?
"slug": slug,
"title": slug,
// The following aren't documented, but are evident
"output_ext": f.OutputExt(),
"y_day": strconv.Itoa(f.fileModTime.YearDay()),
}
for name, f := range permalinkDateVariables {
vs[name] = p.fileModTime.Format(f)
for k, v := range permalinkDateVariables {
vars[k] = f.fileModTime.Format(v)
}
return vs
return vars
}
func (p *file) expandPermalink() (s string, err error) {
pattern := templates.VariableMap(p.frontMatter).String("permalink", constants.DefaultPermalinkPattern)
func (f *file) computePermalink(vars map[string]string) (src string, err error) {
pattern := templates.VariableMap(f.frontMatter).String("permalink", constants.DefaultPermalinkPattern)
if p, found := PermalinkStyles[pattern]; found {
pattern = p
}
templateVariables := p.permalinkTemplateVariables()
// The ReplaceAllStringFunc callback signals errors via panic.
// 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 {
templateVariables := f.permalinkVariables()
s, err := helpers.SafeReplaceAllStringFunc(templateVariableMatcher, pattern, func(m string) (string, error) {
varname := m[1:]
value, found := templateVariables[varname]
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
}
// The permalink is computed once instead of on demand, so that subsequent
// access needn't check for an error.
func (p *file) initPermalink() (err error) {
p.permalink, err = p.expandPermalink()
func (f *file) setPermalink() (err error) {
f.permalink, err = f.computePermalink(f.permalinkVariables())
return
}

View File

@ -10,16 +10,6 @@ import (
"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 }
var tests = []pathTest{
@ -53,17 +43,17 @@ func TestExpandPermalinkPattern(t *testing.T) {
)
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)
switch ext {
case ".md", ".markdown":
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")
require.NoError(t, err)
p.fileModTime = t0
return p.expandPermalink()
return p.computePermalink(p.permalinkVariables())
}
runTests := func(tests []pathTest) {

View File

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