jorge/markup/filters.go
2024-03-15 17:50:31 -03:00

228 lines
6.3 KiB
Go

package markup
import (
"bytes"
"fmt"
"net/url"
"path/filepath"
"reflect"
"regexp"
"strings"
"encoding/xml"
"time"
"github.com/elliotchance/orderedmap/v2"
"github.com/osteele/liquid"
"github.com/osteele/liquid/evaluator"
"github.com/osteele/liquid/expressions"
"github.com/yuin/goldmark"
"github.com/osteele/liquid/render"
)
// a lot of the filters and tags available at jekyll aren't default liquid manually adding them here
// copied from https://github.com/osteele/gojekyll/blob/f1794a874890bfb601cae767a0cce15d672e9058/filters/filters.go
// MIT License: https://github.com/osteele/gojekyll/blob/f1794a874890bfb601cae767a0cce15d672e9058/LICENSE
func loadJekyllFilters(e *liquid.Engine, siteUrl string, includesDir string) {
e.RegisterFilter("filter", filter)
e.RegisterFilter("group_by", groupByFilter)
e.RegisterFilter("group_by_exp", groupByExpFilter)
e.RegisterFilter("sort", sortFilter)
e.RegisterFilter("keys", keysFilter)
e.RegisterFilter("where", whereFilter)
e.RegisterFilter("where_exp", whereExpFilter)
e.RegisterFilter("normalize_whitespace", func(s string) string {
wsPattern := regexp.MustCompile(`(?s:[\s\n]+)`)
return wsPattern.ReplaceAllString(s, " ")
})
e.RegisterFilter("markdownify", func(s string) (string, error) {
// using goldmark here instead of balckfriday, to avoid an extra dependency
var buf bytes.Buffer
err := goldmark.Convert([]byte(s), &buf)
return buf.String(), err
})
e.RegisterFilter("xml_escape", func(s string) (string, error) {
// using goldmark here instead of balckfriday, to avoid an extra dependency
var buf bytes.Buffer
err := xml.EscapeText(&buf, []byte(s))
return buf.String(), err
})
e.RegisterFilter("absolute_url", func(path string) (string, error) {
parsed, err := url.Parse(path)
if err != nil {
return "", err
}
if parsed.IsAbs() {
return path, nil
}
return url.JoinPath(siteUrl, path)
})
e.RegisterFilter("date_to_rfc822", func(date time.Time) string {
return date.Format(time.RFC822)
// Out: Mon, 07 Nov 2008 13:07:54 -0800
})
e.RegisterFilter("date_to_string", func(date time.Time) string {
return date.Format("02 Jan 2006")
// Out: 07 Nov 2008
})
e.RegisterFilter("date_to_long_string", func(date time.Time) string {
return date.Format("02 January 2006")
// Out: 07 November 2008
})
e.RegisterFilter("date_to_xmlschema", func(date time.Time) string {
return date.Format("2006-01-02T15:04:05-07:00")
// Out: 2008-11-07T13:07:54-08:00
})
e.RegisterTag("include", func(rc render.Context) (string, error) {
return includeFromDir(includesDir, rc)
})
}
func filter(values []map[string]interface{}, key string) []interface{} {
var result []interface{}
for _, value := range values {
if _, ok := value[key]; ok {
result = append(result, value)
}
}
return result
}
func groupByExpFilter(array []map[string]interface{}, name string, expr expressions.Closure) ([]map[string]interface{}, error) {
rt := reflect.ValueOf(array)
if !(rt.Kind() != reflect.Array || rt.Kind() == reflect.Slice) {
return nil, nil
}
groups := orderedmap.NewOrderedMap[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.Get(key); found {
groups.Set(key, append(group, item))
} else {
groups.Set(key, []interface{}{item})
}
}
var result []map[string]interface{}
for _, k := range groups.Keys() {
v, _ := groups.Get(k)
result = append(result, map[string]interface{}{"name": k, "items": v})
}
return result, nil
}
// TODO use ordered map
func groupByFilter(array []map[string]interface{}, property string) []map[string]interface{} {
rt := reflect.ValueOf(array)
if !(rt.Kind() != reflect.Array || rt.Kind() == reflect.Slice) {
return nil
}
groups := orderedmap.NewOrderedMap[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.Get(key); found {
groups.Set(key, append(group, irt.Interface()))
} else {
groups.Set(key, []interface{}{irt.Interface()})
}
}
}
}
var result []map[string]interface{}
for _, k := range groups.Keys() {
v, _ := groups.Get(k)
result = append(result, map[string]interface{}{"name": k, "items": v})
}
return result
}
func keysFilter(m map[string]interface{}) []string {
keys := make([]string, len(m))
i := 0
for k := range m {
keys[i] = k
i++
}
return keys
}
func sortFilter(array []interface{}, key interface{}, nilFirst func(bool) bool) []interface{} {
nf := nilFirst(true)
result := make([]interface{}, len(array))
copy(result, array)
if key == nil {
evaluator.Sort(result)
} else {
// TODO error if key is not a string
evaluator.SortByProperty(result, key.(string), nf)
}
return result
}
func whereExpFilter(array []interface{}, name string, expr expressions.Closure) ([]interface{}, error) {
rt := reflect.ValueOf(array)
if rt.Kind() != reflect.Array && rt.Kind() != reflect.Slice {
return nil, nil
}
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 {
result = append(result, item)
}
}
return result, nil
}
func whereFilter(array []map[string]interface{}, key string, value interface{}) []interface{} {
rt := reflect.ValueOf(array)
if rt.Kind() != reflect.Array && rt.Kind() != reflect.Slice {
return nil
}
var result []interface{}
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))
if attr.IsValid() && fmt.Sprint(attr) == value {
result = append(result, item.Interface())
}
}
}
return result
}
func includeFromDir(dir string, rc render.Context) (string, error) {
argsline, err := rc.ExpandTagArg()
if err != nil {
return "", err
}
args := strings.Split(argsline, " ")
if err != nil {
return "", err
}
if len(args) != 1 {
return "", fmt.Errorf("parse error")
}
filename := filepath.Join(dir, args[0])
return rc.RenderFile(filename, map[string]interface{}{})
}