1
0
mirror of https://github.com/danog/gojekyll.git synced 2024-12-02 17:17:49 +01:00
gojekyll/filters/filters.go

304 lines
8.6 KiB
Go
Raw Normal View History

2017-07-01 01:40:52 +02:00
package filters
2017-06-11 20:57:25 +02:00
import (
2017-06-29 15:41:33 +02:00
"bytes"
2017-06-15 16:17:30 +02:00
"encoding/json"
2017-06-16 04:30:53 +02:00
"encoding/xml"
2017-06-30 18:21:55 +02:00
"fmt"
2017-07-01 04:38:27 +02:00
"math/rand"
2017-07-10 23:16:53 +02:00
"net/url"
2017-06-11 20:57:25 +02:00
"reflect"
2017-06-29 15:41:33 +02:00
"regexp"
"strings"
2017-06-16 04:30:53 +02:00
"time"
2017-06-11 20:57:25 +02:00
2017-07-01 01:37:31 +02:00
"github.com/osteele/gojekyll/config"
2017-07-12 15:52:40 +02:00
"github.com/osteele/gojekyll/utils"
2017-07-01 01:40:52 +02:00
"github.com/osteele/liquid"
2017-07-05 17:35:20 +02:00
"github.com/osteele/liquid/evaluator"
"github.com/osteele/liquid/expressions"
2017-06-28 22:55:15 +02:00
"github.com/russross/blackfriday"
2017-07-15 16:59:55 +02:00
libsass "github.com/wellington/go-libsass"
2017-06-11 20:57:25 +02:00
)
2017-07-01 05:10:58 +02:00
// AddJekyllFilters adds the Jekyll filters to the Liquid engine.
func AddJekyllFilters(e *liquid.Engine, c *config.Config) {
2017-06-30 18:21:55 +02:00
// array filters
2017-07-02 19:34:43 +02:00
e.RegisterFilter("array_to_sentence_string", arrayToSentenceStringFilter)
2017-07-07 00:50:56 +02:00
// TODO doc neither Liquid nor Jekyll docs this, but it appears to be present
2017-07-02 19:34:43 +02:00
e.RegisterFilter("filter", func(values []map[string]interface{}, key string) []interface{} {
var result []interface{}
2017-06-28 20:42:04 +02:00
for _, value := range values {
if _, ok := value[key]; ok {
2017-07-13 18:56:38 +02:00
result = append(result, value)
2017-06-28 20:42:04 +02:00
}
}
2017-07-13 18:56:38 +02:00
return result
2017-06-28 20:42:04 +02:00
})
2017-07-02 19:34:43 +02:00
e.RegisterFilter("group_by", groupByFilter)
2017-07-13 18:56:38 +02:00
e.RegisterFilter("group_by_exp", groupByExpFilter)
2017-07-02 19:34:43 +02:00
e.RegisterFilter("sample", func(array []interface{}) interface{} {
2017-07-01 04:38:27 +02:00
if len(array) == 0 {
return nil
}
return array[rand.Intn(len(array))]
})
2017-06-29 15:41:33 +02:00
// sort overrides the Liquid filter with one that takes parameters
2017-07-02 19:34:43 +02:00
e.RegisterFilter("sort", sortFilter)
e.RegisterFilter("where", whereFilter) // TODO test case
e.RegisterFilter("where_exp", whereExpFilter)
e.RegisterFilter("xml_escape", xml.Marshal)
e.RegisterFilter("push", func(array []interface{}, item interface{}) interface{} {
2017-07-05 17:35:20 +02:00
return append(array, evaluator.MustConvertItem(item, array))
2017-06-29 15:41:33 +02:00
})
2017-07-09 19:00:25 +02:00
e.RegisterFilter("pop", requireNonEmptyArray(func(array []interface{}) interface{} {
2017-07-07 00:50:56 +02:00
return array[0]
2017-07-09 19:00:25 +02:00
}))
e.RegisterFilter("shift", requireNonEmptyArray(func(array []interface{}) interface{} {
2017-07-07 00:50:56 +02:00
return array[len(array)-1]
2017-07-09 19:00:25 +02:00
}))
2017-07-02 19:34:43 +02:00
e.RegisterFilter("unshift", func(array []interface{}, item interface{}) interface{} {
2017-07-05 17:35:20 +02:00
return append([]interface{}{evaluator.MustConvertItem(item, array)}, array...)
2017-06-29 15:41:33 +02:00
})
2017-06-28 22:55:15 +02:00
// dates
2017-07-02 19:34:43 +02:00
e.RegisterFilter("date_to_rfc822", func(date time.Time) string {
2017-06-28 22:55:15 +02:00
return date.Format(time.RFC822)
// Out: Mon, 07 Nov 2008 13:07:54 -0800
})
2017-07-02 19:34:43 +02:00
e.RegisterFilter("date_to_string", func(date time.Time) string {
2017-06-28 22:55:15 +02:00
return date.Format("02 Jan 2006")
// Out: 07 Nov 2008
})
2017-07-02 19:34:43 +02:00
e.RegisterFilter("date_to_long_string", func(date time.Time) string {
2017-06-28 22:55:15 +02:00
return date.Format("02 January 2006")
// Out: 07 November 2008
})
2017-07-02 19:34:43 +02:00
e.RegisterFilter("date_to_xmlschema", func(date time.Time) string {
2017-06-28 22:55:15 +02:00
return date.Format("2006-01-02T15:04:05-07:00")
// Out: 2008-11-07T13:07:54-08:00
})
// strings
2017-07-02 19:34:43 +02:00
e.RegisterFilter("absolute_url", func(s string) string {
2017-07-12 15:52:40 +02:00
return utils.URLJoin(c.AbsoluteURL, c.BaseURL, s)
2017-06-28 22:55:15 +02:00
})
2017-07-02 19:34:43 +02:00
e.RegisterFilter("relative_url", func(s string) string {
2017-07-01 05:10:58 +02:00
return c.BaseURL + s
2017-06-28 22:55:15 +02:00
})
2017-07-02 19:34:43 +02:00
e.RegisterFilter("jsonify", json.Marshal)
2021-06-04 12:13:49 +02:00
e.RegisterFilter("markdownify", blackfriday.Run)
2017-07-02 19:34:43 +02:00
e.RegisterFilter("normalize_whitespace", func(s string) string {
2017-07-01 04:38:27 +02:00
// s = strings.Replace(s, "n", "N", -1)
wsPattern := regexp.MustCompile(`(?s:[\s\n]+)`)
return wsPattern.ReplaceAllString(s, " ")
})
2017-07-02 19:34:43 +02:00
e.RegisterFilter("slugify", func(s, mode string) string {
2017-07-01 04:38:27 +02:00
if mode == "" {
mode = "default"
}
p := map[string]string{
"raw": `\s+`,
"default": `[^[:alnum:]]+`,
"pretty": `[^[:alnum:]\._~!$&'()+,;=@]+`,
}[mode]
if p != "" {
s = regexp.MustCompile(p).ReplaceAllString(s, "-")
}
return strings.ToLower(s)
})
2017-07-02 19:34:43 +02:00
e.RegisterFilter("to_integer", func(n int) int { return n })
e.RegisterFilter("number_of_words", func(s string) int {
2017-06-29 15:41:33 +02:00
wordPattern := regexp.MustCompile(`\w+`) // TODO what's the Jekyll spec for a word?
m := wordPattern.FindAllStringIndex(s, -1)
if m == nil {
return 0
}
return len(m)
})
// string escapes
2017-07-10 23:16:53 +02:00
e.RegisterFilter("cgi_escape", url.QueryEscape)
2017-07-15 16:59:55 +02:00
e.RegisterFilter("sassify", unimplementedFilter("sassify"))
e.RegisterFilter("scssify", scssifyFilter)
2017-07-09 19:00:25 +02:00
e.RegisterFilter("smartify", smartifyFilter)
2017-07-10 23:16:53 +02:00
e.RegisterFilter("uri_escape", func(s string) string {
return regexp.MustCompile(`\?(.+?)=([^&]*)(?:\&(.+?)=([^&]*))*`).ReplaceAllStringFunc(s, func(m string) string {
pair := strings.SplitN(m, "=", 2)
return pair[0] + "=" + url.QueryEscape(pair[1])
})
})
2017-07-02 19:34:43 +02:00
e.RegisterFilter("xml_escape", func(s string) string {
2017-06-29 19:15:34 +02:00
// TODO can't handle maps
// eval https://github.com/clbanning/mxj
// adapt https://stackoverflow.com/questions/30928770/marshall-map-to-xml-in-go
2017-06-29 15:41:33 +02:00
buf := new(bytes.Buffer)
if err := xml.EscapeText(buf, []byte(s)); err != nil {
panic(err)
}
return buf.String()
})
2017-06-15 16:17:30 +02:00
}
2017-07-09 19:00:25 +02:00
// helpers
func requireNonEmptyArray(fn func([]interface{}) interface{}) func([]interface{}) interface{} {
return func(array []interface{}) interface{} {
if len(array) == 0 {
return nil
}
return fn(array)
}
}
2017-07-01 04:38:27 +02:00
func unimplementedFilter(name string) func(value interface{}) interface{} {
warned := false
return func(value interface{}) interface{} {
if !warned {
fmt.Println("warning: unimplemented filter:", name)
warned = true
}
return value
}
}
2017-07-09 19:00:25 +02:00
// array filters
2017-07-08 16:29:24 +02:00
func arrayToSentenceStringFilter(array []string, conjunction func(string) string) string {
conj := conjunction("and ")
switch len(array) {
case 1:
return array[0]
default:
rt := reflect.ValueOf(array)
ar := make([]string, rt.Len())
for i, v := range array {
ar[i] = v
if i == rt.Len()-1 {
ar[i] = conj + v
}
2017-06-16 04:30:53 +02:00
}
return strings.Join(ar, ", ")
2017-06-16 04:30:53 +02:00
}
}
func groupByExpFilter(array []map[string]interface{}, name string, expr expressions.Closure) ([]map[string]interface{}, error) {
2017-07-13 18:56:38 +02:00
rt := reflect.ValueOf(array)
if !(rt.Kind() != reflect.Array || rt.Kind() == reflect.Slice) {
return nil, nil
}
groups := map[interface{}][]interface{}{}
for i := 0; i < rt.Len(); i++ {
item := rt.Index(i).Interface()
key, err := expr.Bind(name, item).Evaluate()
if err != nil {
return nil, err
}
if group, found := groups[key]; found {
groups[key] = append(group, item)
} else {
groups[key] = []interface{}{item}
}
}
var result []map[string]interface{}
2017-07-13 18:56:38 +02:00
for k, v := range groups {
result = append(result, map[string]interface{}{"name": k, "items": v})
}
return result, nil
}
2017-06-30 18:21:55 +02:00
func groupByFilter(array []map[string]interface{}, property string) []map[string]interface{} {
rt := reflect.ValueOf(array)
if !(rt.Kind() != reflect.Array || rt.Kind() == reflect.Slice) {
2017-06-30 18:21:55 +02:00
return nil
}
groups := map[interface{}][]interface{}{}
for i := 0; i < rt.Len(); i++ {
irt := rt.Index(i)
if irt.Kind() == reflect.Map && irt.Type().Key().Kind() == reflect.String {
krt := irt.MapIndex(reflect.ValueOf(property))
if krt.IsValid() && krt.CanInterface() {
key := krt.Interface()
if group, found := groups[key]; found {
groups[key] = append(group, irt.Interface())
2017-06-30 18:21:55 +02:00
} else {
groups[key] = []interface{}{irt.Interface()}
2017-06-30 18:21:55 +02:00
}
}
}
}
var result []map[string]interface{}
2017-06-30 18:21:55 +02:00
for k, v := range groups {
2017-07-13 18:56:38 +02:00
result = append(result, map[string]interface{}{"name": k, "items": v})
2017-06-30 18:21:55 +02:00
}
2017-07-13 18:56:38 +02:00
return result
2017-06-30 18:21:55 +02:00
}
2017-07-08 16:29:24 +02:00
func sortFilter(array []interface{}, key interface{}, nilFirst func(bool) bool) []interface{} {
nf := nilFirst(true)
2017-07-13 18:56:38 +02:00
result := make([]interface{}, len(array))
copy(result, array)
2017-06-29 15:41:33 +02:00
if key == nil {
2017-07-13 18:56:38 +02:00
evaluator.Sort(result)
2017-06-29 15:41:33 +02:00
} else {
// TODO error if key is not a string
2017-07-13 18:56:38 +02:00
evaluator.SortByProperty(result, key.(string), nf)
2017-06-29 15:41:33 +02:00
}
2017-07-13 18:56:38 +02:00
return result
2017-06-29 15:41:33 +02:00
}
func whereExpFilter(array []interface{}, name string, expr expressions.Closure) ([]interface{}, error) {
2017-06-30 18:21:55 +02:00
rt := reflect.ValueOf(array)
if rt.Kind() != reflect.Array && rt.Kind() != reflect.Slice {
return nil, nil
2017-06-11 20:57:25 +02:00
}
var result []interface{}
for i := 0; i < rt.Len(); i++ {
item := rt.Index(i).Interface()
value, err := expr.Bind(name, item).Evaluate()
if err != nil {
return nil, err
}
if value != nil && value != false {
2017-07-13 18:56:38 +02:00
result = append(result, item)
}
2017-06-11 20:57:25 +02:00
}
2017-07-13 18:56:38 +02:00
return result, nil
}
2017-06-11 20:57:25 +02:00
2017-06-30 18:21:55 +02:00
func whereFilter(array []map[string]interface{}, key string, value interface{}) []interface{} {
rt := reflect.ValueOf(array)
if rt.Kind() != reflect.Array && rt.Kind() != reflect.Slice {
2017-06-29 15:41:33 +02:00
return nil
2017-06-11 20:57:25 +02:00
}
var result []interface{}
2017-06-11 20:57:25 +02:00
for i := 0; i < rt.Len(); i++ {
item := rt.Index(i)
if item.Kind() == reflect.Map && item.Type().Key().Kind() == reflect.String {
attr := item.MapIndex(reflect.ValueOf(key))
2017-06-30 18:21:55 +02:00
if attr.IsValid() && fmt.Sprint(attr) == value {
2017-07-13 18:56:38 +02:00
result = append(result, item.Interface())
}
2017-06-11 20:57:25 +02:00
}
}
2017-07-13 18:56:38 +02:00
return result
2017-06-11 20:57:25 +02:00
}
2017-07-15 16:59:55 +02:00
// string filters
func scssifyFilter(s string) (string, error) {
2017-08-18 17:07:01 +02:00
// this doesn't try to share context or setup with the rendering manager,
2017-07-15 16:59:55 +02:00
// and it doesn't minify
buf := new(bytes.Buffer)
comp, err := libsass.New(buf, bytes.NewBufferString(s))
if err != nil {
return "", err
}
// err = comp.Option(libsass.WithSyntax(libsass.SassSyntax))
if err != nil {
return "", err
}
err = comp.Run()
return buf.String(), err
}