Files
core/restream/replace/replace.go

195 lines
4.9 KiB
Go

package replace
import (
"regexp"
"strings"
"github.com/datarhei/core/v16/glob"
"github.com/datarhei/core/v16/restream/app"
)
type ReplaceFunc func(params map[string]string, config *app.Config, section string) string
type Replacer interface {
// RegisterReplaceFunc registers a function for replacing for a specific placeholder.
// If a parameter is not of a placeholder is not present, default values can be provided.
RegisterReplaceFunc(placeholder string, replacer ReplaceFunc, defaults map[string]string)
// Replace replaces all occurences of placeholder in str with value. The placeholder is of the
// form {placeholder}. It is possible to escape a characters in value with \\ by appending a ^
// and the character to escape to the placeholder name, e.g. {placeholder^:} to escape ":".
// A placeholder may also have parameters of the form {placeholder,key1=value1,key2=value2}.
// If the value is an empty string, the registered replacer functions will be searched for that
// placeholder. If no function is found, the placeholder will be replaced by the empty string.
// A placeholder name may consist on of the letters a-z and ':'. The placeholder may contain
// a glob pattern to find the appropriate template.
Replace(str, placeholder, value string, vars map[string]string, config *app.Config, section string) string
}
type replace struct {
fn ReplaceFunc
defaults map[string]string
}
type replacer struct {
replacers map[string]replace
re *regexp.Regexp
templateRe *regexp.Regexp
}
// New returns a Replacer
func New() Replacer {
r := &replacer{
replacers: make(map[string]replace),
re: regexp.MustCompile(`{([a-z:]+)(?:\^(.))?(?:,(.*?))?}`),
templateRe: regexp.MustCompile(`{([a-z:]+)}`),
}
return r
}
func (r *replacer) RegisterReplaceFunc(placeholder string, replaceFn ReplaceFunc, defaults map[string]string) {
r.replacers[placeholder] = replace{
fn: replaceFn,
defaults: defaults,
}
}
func (r *replacer) Replace(str, placeholder, value string, vars map[string]string, config *app.Config, section string) string {
str = r.re.ReplaceAllStringFunc(str, func(match string) string {
matches := r.re.FindStringSubmatch(match)
if ok, _ := glob.Match(placeholder, matches[1], ':'); !ok {
return match
}
placeholder := matches[1]
// We need a copy from the value
v := value
var repl replace = replace{
fn: func(map[string]string, *app.Config, string) string { return v },
}
if len(v) == 0 {
// Check for a registered template
t, ok := r.replacers[placeholder]
if ok {
repl = t
}
}
params := r.parseParametes(matches[3], vars, repl.defaults)
v = repl.fn(params, config, section)
if len(matches[2]) != 0 {
// If there's a character to escape, we also have to escape the
// escape character, but only if it is different from the character
// to escape.
if matches[2] != "\\" {
v = strings.ReplaceAll(v, "\\", "\\\\\\")
}
v = strings.ReplaceAll(v, matches[2], "\\\\"+matches[2])
}
return strings.Replace(match, match, v, 1)
})
return str
}
// parseParametes parses the parameters of a placeholder. The params string is a comma-separated
// string of key=value pairs. The key and values can be escaped with an \.
// The provided defaults will be used as basis. Any parsed key/value from the params might overwrite
// the default value. Any variables in the values will be replaced by their value from the
// vars parameter.
func (r *replacer) parseParametes(params string, vars map[string]string, defaults map[string]string) map[string]string {
reSpace := regexp.MustCompile(`^\s+`)
p := make(map[string]string)
if len(params) == 0 && len(defaults) == 0 {
return p
}
// Copy the defaults
for key, value := range defaults {
for name, v := range vars {
value = strings.ReplaceAll(value, "$"+name, v)
}
p[key] = value
}
// taken from net/url.ParseQuery
for params != "" {
var key string
key, params, _ = cut([]rune(params), ',')
key = reSpace.ReplaceAllString(key, "") // Remove all leading spaces
if key == "" {
continue
}
key, value, _ := cut([]rune(key), '=')
value = unescape([]rune(value))
for name, v := range vars {
value = strings.ReplaceAll(value, "$"+name, v)
}
p[key] = value
}
return p
}
func cut(s []rune, sep rune) (before, after string, found bool) {
if i := index(s, sep); i >= 0 {
return string(s[:i]), string(s[i+1:]), true
}
return string(s), "", false
}
func index(s []rune, sep rune) int {
ignoreSep := false
for i, c := range s {
if c == rune('\\') {
ignoreSep = true
continue
}
if c == sep && !ignoreSep {
return i
}
ignoreSep = false
}
return -1
}
func unescape(e []rune) string {
r := make([]rune, len(e))
ignore := false
i := 0
for _, c := range e {
if c == rune('\\') && !ignore {
ignore = true
continue
}
ignore = false
r[i] = c
i++
}
return string(r[:i])
}