// Package interpol provides utility functions for doing format-string like // string interpolation using named parameters. // Currently, a template only accepts variable placeholders delimited by brace // characters (eg. "Hello {foo} {bar}"). package interpol import ( "bytes" "errors" "io" "strings" ) // Errors returned when formatting templates. var ( ErrUnexpectedClose = errors.New("interpol: unexpected close in template") ErrExpectingClose = errors.New("interpol: expecting close in template") ErrKeyNotFound = errors.New("interpol: key not found") ErrReadByteFailed = errors.New("interpol: read byte failed") ) // Func receives the placeholder key and writes to the io.Writer. If an error // happens, the function can return an error, in which case the interpolation // will be aborted. type Func func(key string, w io.Writer) error // New creates a new interpolator with the given list of options. // You can use options such as the ones returned by WithTemplate, WithFormat // and WithOutput. func New(opts ...Option) *Interpolator { opts2 := &Options{} setOptions(opts, newOptionSetter(opts2)) return NewWithOptions(opts2) } // NewWithOptions creates a new interpolator with the given options. func NewWithOptions(opts *Options) *Interpolator { return &Interpolator{ template: templateReader(opts), output: outputWriter(opts), format: opts.Format, rb: make([]rune, 0, 64), start: -1, closing: false, } } // Interpolator interpolates Template to Output, according to Format. type Interpolator struct { template io.RuneReader output runeWriter format Func rb []rune start int closing bool } // Interpolate reads runes from Template and writes them to Output, with the // exception of placeholders which are passed to Format. func (i *Interpolator) Interpolate() error { for pos := 0; ; pos++ { r, _, err := i.template.ReadRune() if err != nil { if err == io.EOF { break } return err } if err := i.parse(r, pos); err != nil { return err } } return i.finish() } func (i *Interpolator) parse(r rune, pos int) error { switch r { case '{': return i.open(pos) case '}': return i.close() default: return i.append(r) } } func (i *Interpolator) open(pos int) error { if i.closing { return ErrUnexpectedClose } if i.start >= 0 { if _, err := i.output.WriteRune('{'); err != nil { return err } i.start = -1 } else { i.start = pos + 1 } return nil } func (i *Interpolator) close() error { if i.start >= 0 { if err := i.format(string(i.rb), i.output); err != nil { return err } i.rb = i.rb[:0] i.start = -1 } else if i.closing { i.closing = false if _, err := i.output.WriteRune('}'); err != nil { return err } } else { i.closing = true } return nil } func (i *Interpolator) append(r rune) error { if i.closing { return ErrUnexpectedClose } if i.start < 0 { _, err := i.output.WriteRune(r) return err } i.rb = append(i.rb, r) return nil } func (i *Interpolator) finish() error { if i.start >= 0 { return ErrExpectingClose } if i.closing { return ErrUnexpectedClose } return nil } // WithFunc interpolates the specified template with replacements using the // given function. func WithFunc(template string, format Func) (string, error) { buffer := bytes.NewBuffer(make([]byte, 0, len(template))) opts := &Options{ Template: strings.NewReader(template), Output: buffer, Format: format, } i := NewWithOptions(opts) if err := i.Interpolate(); err != nil { return "", err } return buffer.String(), nil } // WithMap interpolates the specified template with replacements using the // given map. If a placeholder is used for which a value is not found, an error // is returned. func WithMap(template string, m map[string]string) (string, error) { format := func(key string, w io.Writer) error { value, ok := m[key] if !ok { return ErrKeyNotFound } _, err := w.Write([]byte(value)) return err } return WithFunc(template, format) }