blob: 28495baf50345216d17580ca72ab81e8caca9018 [file] [log] [blame]
package template
import (
"fmt"
"regexp"
"strings"
)
var delimiter = "\\$"
var substitution = "[_a-z][_a-z0-9]*(?::?-[^}]+)?"
var patternString = fmt.Sprintf(
"%s(?i:(?P<escaped>%s)|(?P<named>%s)|{(?P<braced>%s)}|(?P<invalid>))",
delimiter, delimiter, substitution, substitution,
)
var pattern = regexp.MustCompile(patternString)
// InvalidTemplateError is returned when a variable template is not in a valid
// format
type InvalidTemplateError struct {
Template string
}
func (e InvalidTemplateError) Error() string {
return fmt.Sprintf("Invalid template: %#v", e.Template)
}
// Mapping is a user-supplied function which maps from variable names to values.
// Returns the value as a string and a bool indicating whether
// the value is present, to distinguish between an empty string
// and the absence of a value.
type Mapping func(string) (string, bool)
// Substitute variables in the string with their values
func Substitute(template string, mapping Mapping) (result string, err *InvalidTemplateError) {
result = pattern.ReplaceAllStringFunc(template, func(substring string) string {
matches := pattern.FindStringSubmatch(substring)
groups := make(map[string]string)
for i, name := range pattern.SubexpNames() {
if i != 0 {
groups[name] = matches[i]
}
}
substitution := groups["named"]
if substitution == "" {
substitution = groups["braced"]
}
if substitution != "" {
// Soft default (fall back if unset or empty)
if strings.Contains(substitution, ":-") {
name, defaultValue := partition(substitution, ":-")
value, ok := mapping(name)
if !ok || value == "" {
return defaultValue
}
return value
}
// Hard default (fall back if-and-only-if empty)
if strings.Contains(substitution, "-") {
name, defaultValue := partition(substitution, "-")
value, ok := mapping(name)
if !ok {
return defaultValue
}
return value
}
// No default (fall back to empty string)
value, ok := mapping(substitution)
if !ok {
return ""
}
return value
}
if escaped := groups["escaped"]; escaped != "" {
return escaped
}
err = &InvalidTemplateError{Template: template}
return ""
})
return result, err
}
// Split the string at the first occurrence of sep, and return the part before the separator,
// and the part after the separator.
//
// If the separator is not found, return the string itself, followed by an empty string.
func partition(s, sep string) (string, string) {
if strings.Contains(s, sep) {
parts := strings.SplitN(s, sep, 2)
return parts[0], parts[1]
}
return s, ""
}