Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
191 lines
4.4 KiB
Go
191 lines
4.4 KiB
Go
// Copyright © 2023 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package template
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"embed"
|
|
htemplate "html/template"
|
|
"io"
|
|
"io/fs"
|
|
"path/filepath"
|
|
"text/template"
|
|
|
|
"github.com/ory/x/fetcher"
|
|
"github.com/ory/x/httpx"
|
|
|
|
"github.com/Masterminds/sprig/v3"
|
|
lru "github.com/hashicorp/golang-lru/v2"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
//go:embed courier/builtin/templates/*
|
|
var templates embed.FS
|
|
|
|
var Cache, _ = lru.New[string, Template](16)
|
|
|
|
type Template interface {
|
|
Execute(wr io.Writer, data interface{}) error
|
|
}
|
|
|
|
type templateDependencies interface {
|
|
httpx.ClientProvider
|
|
}
|
|
|
|
func loadBuiltInTemplate(filesystem fs.FS, name string, html bool) (Template, error) {
|
|
if t, found := Cache.Get(name); found {
|
|
return t, nil
|
|
}
|
|
|
|
file, err := filesystem.Open(name)
|
|
if err != nil {
|
|
// try to fallback to bundled templates
|
|
var fallbackErr error
|
|
file, fallbackErr = templates.Open(filepath.Join("courier/builtin/templates", name))
|
|
if fallbackErr != nil {
|
|
// return original error from os.DirFS
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
}
|
|
|
|
defer func() { _ = file.Close() }()
|
|
|
|
var b bytes.Buffer
|
|
if _, err := io.Copy(&b, file); err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
|
|
var tpl Template
|
|
if html {
|
|
t, err := htemplate.New(name).Funcs(sprig.HtmlFuncMap()).Parse(b.String())
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
tpl = t
|
|
} else {
|
|
t, err := template.New(name).Funcs(sprig.TxtFuncMap()).Parse(b.String())
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
tpl = t
|
|
}
|
|
|
|
_ = Cache.Add(name, tpl)
|
|
return tpl, nil
|
|
}
|
|
|
|
func loadRemoteTemplate(ctx context.Context, d templateDependencies, url string, html bool) (t Template, err error) {
|
|
if t, found := Cache.Get(url); found {
|
|
return t, nil
|
|
}
|
|
|
|
f := fetcher.NewFetcher(fetcher.WithClient(d.HTTPClient(ctx)))
|
|
bb, err := f.FetchContext(ctx, url)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
b := bb.Bytes()
|
|
|
|
if html {
|
|
t, err = htemplate.New(url).Funcs(sprig.HermeticHtmlFuncMap()).Parse(string(b))
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
} else {
|
|
t, err = template.New(url).Funcs(sprig.HermeticTxtFuncMap()).Parse(string(b))
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
}
|
|
Cache.Add(url, t)
|
|
|
|
return t, nil
|
|
}
|
|
|
|
func loadTemplate(filesystem fs.FS, name, pattern string, html bool) (Template, error) {
|
|
if t, found := Cache.Get(name); found {
|
|
return t, nil
|
|
}
|
|
|
|
matches, _ := fs.Glob(filesystem, name)
|
|
// make sure the file exists in the fs, otherwise fallback to built in templates
|
|
if matches == nil {
|
|
return loadBuiltInTemplate(filesystem, name, html)
|
|
}
|
|
|
|
glob := name
|
|
if pattern != "" {
|
|
// pattern matching is used when we have more than one gotmpl for different use cases, such as i18n support
|
|
// e.g. some_template/template_name* will match some_template/template_name.body.en_US.gotmpl
|
|
matches, _ = fs.Glob(filesystem, pattern)
|
|
// set the glob string to match patterns
|
|
if matches != nil {
|
|
glob = pattern
|
|
}
|
|
}
|
|
|
|
var tpl Template
|
|
if html {
|
|
t, err := htemplate.New(filepath.Base(name)).Funcs(sprig.HermeticHtmlFuncMap()).ParseFS(filesystem, glob)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
tpl = t
|
|
} else {
|
|
t, err := template.New(filepath.Base(name)).Funcs(sprig.HermeticTxtFuncMap()).ParseFS(filesystem, glob)
|
|
if err != nil {
|
|
return nil, errors.WithStack(err)
|
|
}
|
|
tpl = t
|
|
}
|
|
|
|
_ = Cache.Add(name, tpl)
|
|
return tpl, nil
|
|
}
|
|
|
|
func LoadText(ctx context.Context, d templateDependencies, filesystem fs.FS, name, pattern string, model interface{}, remoteURL string) (string, error) {
|
|
var t Template
|
|
var err error
|
|
if remoteURL != "" {
|
|
t, err = loadRemoteTemplate(ctx, d, remoteURL, false)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
} else {
|
|
t, err = loadTemplate(filesystem, name, pattern, false)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
var b bytes.Buffer
|
|
if err := t.Execute(&b, model); err != nil {
|
|
return "", err
|
|
}
|
|
return b.String(), nil
|
|
}
|
|
|
|
func LoadHTML(ctx context.Context, d templateDependencies, filesystem fs.FS, name, pattern string, model interface{}, remoteURL string) (string, error) {
|
|
var t Template
|
|
var err error
|
|
if remoteURL != "" {
|
|
t, err = loadRemoteTemplate(ctx, d, remoteURL, true)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
} else {
|
|
t, err = loadTemplate(filesystem, name, pattern, true)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
}
|
|
|
|
var b bytes.Buffer
|
|
if err := t.Execute(&b, model); err != nil {
|
|
return "", err
|
|
}
|
|
return b.String(), nil
|
|
}
|