package revel
import (
"bytes"
"errors"
"fmt"
"github.com/xeonx/timeago"
"html"
"html/template"
"reflect"
"strings"
"time"
)
var (
// The functions available for use in the templates.
TemplateFuncs = map[string]interface{}{
"url": ReverseURL,
"set": func(viewArgs map[string]interface{}, key string, value interface{}) template.JS {
viewArgs[key] = value
return template.JS("")
},
"append": func(viewArgs map[string]interface{}, key string, value interface{}) template.JS {
if viewArgs[key] == nil {
viewArgs[key] = []interface{}{value}
} else {
viewArgs[key] = append(viewArgs[key].([]interface{}), value)
}
return template.JS("")
},
"field": NewField,
"firstof": func(args ...interface{}) interface{} {
for _, val := range args {
switch val.(type) {
case nil:
continue
case string:
if val == "" {
continue
}
return val
default:
return val
}
}
return nil
},
"option": func(f *Field, val interface{}, label string) template.HTML {
selected := ""
if f.Flash() == val || (f.Flash() == "" && f.Value() == val) {
selected = " selected"
}
return template.HTML(fmt.Sprintf(``,
html.EscapeString(fmt.Sprintf("%v", val)), selected, html.EscapeString(label)))
},
"radio": func(f *Field, val string) template.HTML {
checked := ""
if f.Flash() == val {
checked = " checked"
}
return template.HTML(fmt.Sprintf(``,
html.EscapeString(f.Name), html.EscapeString(val), checked))
},
"checkbox": func(f *Field, val string) template.HTML {
checked := ""
if f.Flash() == val {
checked = " checked"
}
return template.HTML(fmt.Sprintf(``,
html.EscapeString(f.Name), html.EscapeString(val), checked))
},
// Pads the given string with 's up to the given width.
"pad": func(str string, width int) template.HTML {
if len(str) >= width {
return template.HTML(html.EscapeString(str))
}
return template.HTML(html.EscapeString(str) + strings.Repeat(" ", width-len(str)))
},
"errorClass": func(name string, viewArgs map[string]interface{}) template.HTML {
errorMap, ok := viewArgs["errors"].(map[string]*ValidationError)
if !ok || errorMap == nil {
templateLog.Warn("errorClass: Called 'errorClass' without 'errors' in the view args.")
return template.HTML("")
}
valError, ok := errorMap[name]
if !ok || valError == nil {
return template.HTML("")
}
return template.HTML(ErrorCSSClass)
},
"msg": func(viewArgs map[string]interface{}, message string, args ...interface{}) template.HTML {
str, ok := viewArgs[CurrentLocaleViewArg].(string)
if !ok {
return ""
}
return template.HTML(MessageFunc(str, message, args...))
},
// Replaces newlines with
"nl2br": func(text string) template.HTML {
return template.HTML(strings.Replace(template.HTMLEscapeString(text), "\n", "
", -1))
},
// Skips sanitation on the parameter. Do not use with dynamic data.
"raw": func(text string) template.HTML {
return template.HTML(text)
},
// Pluralize, a helper for pluralizing words to correspond to data of dynamic length.
// items - a slice of items, or an integer indicating how many items there are.
// pluralOverrides - optional arguments specifying the output in the
// singular and plural cases. by default "" and "s"
"pluralize": func(items interface{}, pluralOverrides ...string) string {
singular, plural := "", "s"
if len(pluralOverrides) >= 1 {
singular = pluralOverrides[0]
if len(pluralOverrides) == 2 {
plural = pluralOverrides[1]
}
}
switch v := reflect.ValueOf(items); v.Kind() {
case reflect.Int:
if items.(int) != 1 {
return plural
}
case reflect.Slice:
if v.Len() != 1 {
return plural
}
default:
templateLog.Error("pluralize: unexpected type: ", "value", v)
}
return singular
},
// Format a date according to the application's default date(time) format.
"date": func(date time.Time) string {
return date.Format(DateFormat)
},
"datetime": func(date time.Time) string {
return date.Format(DateTimeFormat)
},
// Fetch an object from the session.
"session": func(key string, viewArgs map[string]interface{}) interface{} {
if viewArgs != nil {
if c, found := viewArgs["_controller"]; found {
if v, err := c.(*Controller).Session.Get(key); err == nil {
return v
} else {
templateLog.Errorf("template.session, key %s error %v", key, err)
}
} else {
templateLog.Warnf("template.session, key %s requested without controller", key)
}
} else {
templateLog.Warnf("template.session, key %s requested passing in view args", key)
}
return ""
},
"slug": Slug,
"even": func(a int) bool { return (a % 2) == 0 },
// Using https://github.com/xeonx/timeago
"timeago": TimeAgo,
"i18ntemplate": func(args ...interface{}) (template.HTML, error) {
templateName, lang := "", ""
var viewArgs interface{}
switch len(args) {
case 0:
templateLog.Error("i18ntemplate: No arguments passed to template call")
case 1:
// Assume only the template name is passed in
templateName = args[0].(string)
case 2:
// Assume template name and viewArgs is passed in
templateName = args[0].(string)
viewArgs = args[1]
// Try to extract language from the view args
if viewargsmap, ok := viewArgs.(map[string]interface{}); ok {
lang, _ = viewargsmap[CurrentLocaleViewArg].(string)
}
default:
// Assume third argument is the region
templateName = args[0].(string)
viewArgs = args[1]
lang, _ = args[2].(string)
if len(args) > 3 {
templateLog.Error("i18ntemplate: Received more parameters then needed for", "template", templateName)
}
}
var buf bytes.Buffer
// Get template
tmpl, err := MainTemplateLoader.TemplateLang(templateName, lang)
if err == nil {
err = tmpl.Render(&buf, viewArgs)
} else {
templateLog.Error("i18ntemplate: Failed to render i18ntemplate ", "name", templateName, "error", err)
}
return template.HTML(buf.String()), err
},
}
)
/////////////////////
// Template functions
/////////////////////
// ReverseURL returns a url capable of invoking a given controller method:
// "Application.ShowApp 123" => "/app/123"
func ReverseURL(args ...interface{}) (template.URL, error) {
if len(args) == 0 {
return "", errors.New("no arguments provided to reverse route")
}
action := args[0].(string)
if action == "Root" {
return template.URL(AppRoot), nil
}
pathData, found := splitActionPath(nil, action, true)
if !found {
return "", fmt.Errorf("reversing '%s', expected 'Controller.Action'", action)
}
// Look up the types.
if pathData.TypeOfController == nil {
return "", fmt.Errorf("Failed reversing %s: controller not found %#v", action, pathData)
}
// Note method name is case insensitive search
methodType := pathData.TypeOfController.Method(pathData.MethodName)
if methodType == nil {
return "", errors.New("revel/controller: In " + action + " failed to find function " + pathData.MethodName)
}
if len(methodType.Args) < len(args)-1 {
return "", fmt.Errorf("reversing %s: route defines %d args, but received %d",
action, len(methodType.Args), len(args)-1)
}
// Unbind the arguments.
argsByName := make(map[string]string)
// Bind any static args first
fixedParams := len(pathData.FixedParamsByName)
for i, argValue := range args[1:] {
Unbind(argsByName, methodType.Args[i+fixedParams].Name, argValue)
}
return template.URL(MainRouter.Reverse(args[0].(string), argsByName).URL), nil
}
func Slug(text string) string {
separator := "-"
text = strings.ToLower(text)
text = invalidSlugPattern.ReplaceAllString(text, "")
text = whiteSpacePattern.ReplaceAllString(text, separator)
text = strings.Trim(text, separator)
return text
}
var timeAgoLangs = map[string]timeago.Config{}
func TimeAgo(args ...interface{}) string {
datetime := time.Now()
lang := ""
var viewArgs interface{}
switch len(args) {
case 0:
templateLog.Error("TimeAgo: No arguments passed to timeago")
case 1:
// only the time is passed in
datetime = args[0].(time.Time)
case 2:
// time and region is passed in
datetime = args[0].(time.Time)
switch v := reflect.ValueOf(args[1]); v.Kind() {
case reflect.String:
// second params type string equals region
lang, _ = args[1].(string)
case reflect.Map:
// second params type map equals viewArgs
viewArgs = args[1]
if viewargsmap, ok := viewArgs.(map[string]interface{}); ok {
lang, _ = viewargsmap[CurrentLocaleViewArg].(string)
}
default:
templateLog.Error("TimeAgo: unexpected type: ", "value", v)
}
default:
// Assume third argument is the region
datetime = args[0].(time.Time)
if reflect.ValueOf(args[1]).Kind() != reflect.Map {
templateLog.Error("TimeAgo: unexpected type", "value", args[1])
}
if reflect.ValueOf(args[2]).Kind() != reflect.String {
templateLog.Error("TimeAgo: unexpected type: ", "value", args[2])
}
viewArgs = args[1]
lang, _ = args[2].(string)
if len(args) > 3 {
templateLog.Error("TimeAgo: Received more parameters then needed for timeago")
}
}
if lang == "" {
lang, _ = Config.String(defaultLanguageOption)
if lang == "en" {
timeAgoLangs[lang] = timeago.English
}
}
_, ok := timeAgoLangs[lang]
if !ok {
timeAgoLangs[lang] = timeago.Config{
PastPrefix: "",
PastSuffix: " " + MessageFunc(lang, "ago"),
FuturePrefix: MessageFunc(lang, "in") + " ",
FutureSuffix: "",
Periods: []timeago.FormatPeriod{
{time.Second, MessageFunc(lang, "about a second"), MessageFunc(lang, "%d seconds")},
{time.Minute, MessageFunc(lang, "about a minute"), MessageFunc(lang, "%d minutes")},
{time.Hour, MessageFunc(lang, "about an hour"), MessageFunc(lang, "%d hours")},
{timeago.Day, MessageFunc(lang, "one day"), MessageFunc(lang, "%d days")},
{timeago.Month, MessageFunc(lang, "one month"), MessageFunc(lang, "%d months")},
{timeago.Year, MessageFunc(lang, "one year"), MessageFunc(lang, "%d years")},
},
Zero: MessageFunc(lang, "about a second"),
Max: 73 * time.Hour,
DefaultLayout: "2006-01-02",
}
}
return timeAgoLangs[lang].Format(datetime)
}