diff --git a/.drone.yml b/.drone.yml
new file mode 100644
index 0000000..97b9960
--- /dev/null
+++ b/.drone.yml
@@ -0,0 +1,24 @@
+kind: pipeline
+name: default
+
+steps:
+- name: golangci-lint
+ image: golangci/golangci-lint:v1.49
+ volumes:
+ - name: deps
+ path: /go/src/mod
+ commands:
+ - golangci-lint run --timeout 5m
+
+- name: test
+ image: golang
+ volumes:
+ - name: deps
+ path: /go/src/mod
+ commands:
+ - go test ./...
+
+volumes:
+- name: deps
+ temp: {}
+
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..4c318db
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,42 @@
+linters-settings:
+ dupl:
+ threshold: 100
+ funlen:
+ lines: 100
+ statements: 50
+ goconst:
+ min-len: 2
+ min-occurrences: 2
+ gocyclo:
+ min-complexity: 15
+ golint:
+ min-confidence: 0
+ govet:
+ check-shadowing: true
+ lll:
+ line-length: 140
+ maligned:
+ suggest-new: true
+ misspell:
+ locale: US
+ varnamelen:
+ min-name-length: 2
+
+linters:
+ enable-all: true
+ disable:
+ - maligned
+ - exhaustivestruct
+ - golint
+ - structcheck
+ - varcheck
+ - interfacer
+ - deadcode
+ - nosnakecase
+ - scopelint
+ - ifshort
+
+ - gochecknoglobals
+ - ireturn
+ - exhaustruct
+ - gochecknoinits
\ No newline at end of file
diff --git a/engine/encode.go b/engine/encode.go
new file mode 100644
index 0000000..669b5bb
--- /dev/null
+++ b/engine/encode.go
@@ -0,0 +1,38 @@
+package engine
+
+import (
+ "context"
+ "fmt"
+ "io"
+
+ "gitoa.ru/go-4devs/mime"
+ "gitoa.ru/go-4devs/templating/render"
+)
+
+func NewEncode(name string, encode func(w io.Writer, data interface{}) error, ext ...mime.Ext) Encode {
+ return Encode{
+ name: name,
+ encode: encode,
+ formats: ext,
+ }
+}
+
+type Encode struct {
+ encode func(w io.Writer, data interface{}) error
+ name string
+ formats []mime.Ext
+}
+
+func (e Encode) Support(_ context.Context, reference render.Reference) bool {
+ return Support(reference, e.name, e.formats...)
+}
+
+func (e Encode) Load(context.Context, render.Reference) (render.Execute, error) {
+ return func(_ context.Context, wr io.Writer, data interface{}, _ render.Params) error {
+ if err := e.encode(wr, data); err != nil {
+ return fmt.Errorf("%s engine:%w", e.name, err)
+ }
+
+ return nil
+ }, nil
+}
diff --git a/engine/encode_test.go b/engine/encode_test.go
new file mode 100644
index 0000000..c12e2d7
--- /dev/null
+++ b/engine/encode_test.go
@@ -0,0 +1,27 @@
+package engine_test
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "io"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "gitoa.ru/go-4devs/templating/engine"
+ "gitoa.ru/go-4devs/templating/render"
+)
+
+func TestEncodeLoad(t *testing.T) {
+ t.Parallel()
+
+ ctx := context.Background()
+ buff := bytes.Buffer{}
+
+ exec, err := engine.NewEncode("json", func(w io.Writer, v interface{}) error {
+ return json.NewEncoder(w).Encode(v)
+ }).Load(ctx, render.NewReference("any"))
+ require.NoError(t, err)
+ require.NoError(t, exec(ctx, &buff, map[string]string{"name": "json data"}, nil))
+ require.Equal(t, "{\"name\":\"json data\"}\n", buff.String())
+}
diff --git a/engine/engine.go b/engine/engine.go
new file mode 100644
index 0000000..99580bb
--- /dev/null
+++ b/engine/engine.go
@@ -0,0 +1,125 @@
+package engine
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "gitoa.ru/go-4devs/mime"
+ "gitoa.ru/go-4devs/templating/loader"
+ "gitoa.ru/go-4devs/templating/render"
+)
+
+var _ render.Engine = (*Engine)(nil)
+
+var (
+ ErrNotSupport = errors.New("not support")
+ ErrDuplicate = errors.New("duplicate")
+ ErrNotFound = errors.New("not found")
+)
+
+type (
+ Option func(*Engine)
+ Parse func(loader.Source) (Template, error)
+)
+
+type Cache interface {
+ Set(ctx context.Context, tpl Template) error
+ Get(ctx context.Context, name string) (Template, error)
+ List(_ context.Context) []Template
+}
+
+func WithTemplates(tpls ...Template) Option {
+ ctx := context.Background()
+
+ return func(l *Engine) {
+ for _, tpl := range tpls {
+ _ = l.tpls.Set(ctx, tpl)
+ }
+ }
+}
+
+func WithLoader(load loader.Loader) Option {
+ return func(l *Engine) {
+ l.load = load
+ }
+}
+
+func WithFormats(formats ...mime.Ext) Option {
+ return func(e *Engine) {
+ e.formats = formats
+ }
+}
+
+func New(name string, parse Parse, opts ...Option) *Engine {
+ engine := Engine{
+ name: name,
+ parce: parse,
+ tpls: NewTemplates(),
+ load: loader.Empty(),
+ }
+
+ for _, opt := range opts {
+ opt(&engine)
+ }
+
+ return &engine
+}
+
+type Engine struct {
+ name string
+ formats []mime.Ext
+ tpls Cache
+ load loader.Loader
+ parce Parse
+}
+
+func (l Engine) WithLoader(load loader.Loader) *Engine {
+ return New(l.name, l.parce, WithTemplates(l.tpls.List(context.Background())...), WithLoader(load))
+}
+
+func (l Engine) Add(ctx context.Context, tpls ...Template) error {
+ for idx, tpl := range tpls {
+ if err := l.tpls.Set(ctx, tpl); err != nil {
+ return fmt.Errorf("engine add template[%d] with name %s: %w", idx, tpl.Name(), err)
+ }
+ }
+
+ return nil
+}
+
+func (l Engine) Support(ctx context.Context, tpl render.Reference) bool {
+ return Support(tpl, l.name, l.formats...)
+}
+
+func (l Engine) Load(ctx context.Context, reference render.Reference) (render.Execute, error) {
+ var (
+ tpl Template
+ err error
+ )
+
+ tpl, err = l.tpls.Get(ctx, reference.Name())
+ if err == nil {
+ return tpl.Execute, nil
+ }
+
+ if !errors.Is(err, ErrNotFound) {
+ return nil, fmt.Errorf("load get:%w", err)
+ }
+
+ source, err := l.load.Load(ctx, reference)
+ if err != nil {
+ return nil, fmt.Errorf("load source:%w", err)
+ }
+
+ tpl, err = l.parce(source)
+ if err != nil {
+ return nil, fmt.Errorf("load parce:%w", err)
+ }
+
+ if err := l.tpls.Set(ctx, tpl); err != nil {
+ return nil, fmt.Errorf("load set:%w", err)
+ }
+
+ return tpl.Execute, nil
+}
diff --git a/engine/suport.go b/engine/suport.go
new file mode 100644
index 0000000..2aad50d
--- /dev/null
+++ b/engine/suport.go
@@ -0,0 +1,10 @@
+package engine
+
+import (
+ "gitoa.ru/go-4devs/mime"
+ "gitoa.ru/go-4devs/templating/render"
+)
+
+func Support(reference render.Reference, name string, formats ...mime.Ext) bool {
+ return (reference.Engine != "" && reference.IsEngine(name)) || (len(formats) != 0 && reference.IsFromat(formats...))
+}
diff --git a/engine/template.go b/engine/template.go
new file mode 100644
index 0000000..ef9c57c
--- /dev/null
+++ b/engine/template.go
@@ -0,0 +1,80 @@
+package engine
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "sync"
+
+ "gitoa.ru/go-4devs/templating/render"
+)
+
+type Template interface {
+ Name() string
+ Execute(ctx context.Context, w io.Writer, data interface{}, param render.Params) error
+}
+
+func NewTemplates() Templates {
+ return Templates{
+ list: make(map[string]Template),
+ mu: &sync.RWMutex{},
+ }
+}
+
+type Templates struct {
+ list map[string]Template
+ mu *sync.RWMutex
+}
+
+func (t Templates) Get(_ context.Context, name string) (Template, error) {
+ t.mu.RLock()
+ defer t.mu.RUnlock()
+
+ if tpl, ok := t.list[name]; ok {
+ return tpl, nil
+ }
+
+ return nil, fmt.Errorf("templates get:%w", ErrNotFound)
+}
+
+func (t Templates) Set(_ context.Context, tpl Template) error {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+
+ if _, ok := t.list[tpl.Name()]; ok {
+ return fmt.Errorf("templates set:%w", ErrDuplicate)
+ }
+
+ t.list[tpl.Name()] = tpl
+
+ return nil
+}
+
+func (t Templates) List(_ context.Context) []Template {
+ list := make([]Template, 0, len(t.list))
+ for _, tpl := range t.list {
+ list = append(list, tpl)
+ }
+
+ return list
+}
+
+func NewTemplate(name string, execute func(w io.Writer, data interface{}) error) ExecTemplate {
+ return ExecTemplate{
+ name: name,
+ execute: execute,
+ }
+}
+
+type ExecTemplate struct {
+ name string
+ execute func(w io.Writer, data interface{}) error
+}
+
+func (t ExecTemplate) Name() string {
+ return t.name
+}
+
+func (t ExecTemplate) Execute(_ context.Context, w io.Writer, data interface{}, _ render.Params) error {
+ return t.execute(w, data)
+}
diff --git a/execute.go b/execute.go
new file mode 100644
index 0000000..683a979
--- /dev/null
+++ b/execute.go
@@ -0,0 +1,38 @@
+package templating
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "sync"
+
+ "gitoa.ru/go-4devs/templating/parser"
+ "gitoa.ru/go-4devs/templating/render"
+)
+
+var (
+ exec = render.New(parser.Name)
+ mu = sync.Mutex{}
+)
+
+func SetParser(parser render.Parser) {
+ mu.Lock()
+ defer mu.Unlock()
+
+ exec = exec.WithParser(parser)
+}
+
+func AddEngine(engine ...render.Engine) {
+ mu.Lock()
+ defer mu.Unlock()
+
+ exec.Add(engine...)
+}
+
+func Execute(ctx context.Context, wr io.Writer, name string, data interface{}, opts ...render.Option) error {
+ if err := exec.Execute(ctx, wr, name, data, opts...); err != nil {
+ return fmt.Errorf("templating engine:%w", err)
+ }
+
+ return nil
+}
diff --git a/execute_example_test.go b/execute_example_test.go
new file mode 100644
index 0000000..cb4c780
--- /dev/null
+++ b/execute_example_test.go
@@ -0,0 +1,75 @@
+package templating_test
+
+import (
+ "context"
+ html "html/template"
+ "log"
+ "os"
+ text "text/template"
+
+ "gitoa.ru/go-4devs/templating"
+ "gitoa.ru/go-4devs/templating/gohtml"
+ "gitoa.ru/go-4devs/templating/gotxt"
+ "gitoa.ru/go-4devs/templating/render"
+)
+
+type Data struct {
+ Name string
+}
+
+func ExampleExecute() {
+ ctx := context.Background()
+ data := Data{Name: "andrey"}
+
+ gohtml.Must(html.New("example.html").Parse(`Hello {{ .Name }}!`))
+
+ err := templating.Execute(ctx, os.Stdout, "example.html.gohtml", data)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ // Output:
+ // Hello andrey!
+}
+
+func ExampleExecute_withOption() {
+ ctx := context.Background()
+ data := Data{Name: "world"}
+
+ gohtml.Must(html.New("example_with_option.html").Parse(`Hello {{ .Name }}!`))
+
+ err := templating.Execute(ctx, os.Stdout, "example_with_option.html", data, render.WithEngine(gohtml.Name))
+ if err != nil {
+ log.Fatalln(err)
+ }
+ // Output:
+ // Hello <b>world</b>!
+}
+
+func ExampleExecute_text() {
+ ctx := context.Background()
+ data := Data{Name: "text"}
+
+ gotxt.Must(text.New("example.txt").Parse(`Hello {{ .Name }}!`))
+
+ err := templating.Execute(ctx, os.Stdout, "example.txt.gotxt", data)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ // Output:
+ // Hello text!
+}
+
+func ExampleExecute_withoutEngine() {
+ ctx := context.Background()
+ data := Data{Name: "engine"}
+
+ gotxt.Must(text.New("example_engine.txt").Parse(`Text {{ .Name }}!`))
+ gohtml.Must(html.New("example_engine.html").Parse(`Html {{ .Name }}!`))
+
+ err := templating.Execute(ctx, os.Stdout, "example_engine.html", data)
+ if err != nil {
+ log.Fatalln(err)
+ }
+ // Output:
+ // Html engine!
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..94f76f4
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,18 @@
+module gitoa.ru/go-4devs/templating
+
+go 1.17
+
+require (
+ github.com/stretchr/testify v1.8.0
+ gitoa.ru/go-4devs/encoding/json v0.0.1
+ gitoa.ru/go-4devs/mime v0.0.1
+)
+
+require (
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/kr/pretty v0.1.0 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ gitoa.ru/go-4devs/encoding v0.0.1 // indirect
+ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..9280ccf
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,27 @@
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+gitoa.ru/go-4devs/encoding v0.0.1 h1:K93gf7fCwbjkl4yh+eVRDyQ/lNRJPH9Ryd4qWlTKMm4=
+gitoa.ru/go-4devs/encoding v0.0.1/go.mod h1:7dsdRnHdwk82P+cH1qSBwT1dAhZVYnMmGHw+zc9nOAI=
+gitoa.ru/go-4devs/encoding/json v0.0.1 h1:N6QHhTXaOzNTb3TnPVxGPiAI0vmWt5OPEBRBlU3PDmo=
+gitoa.ru/go-4devs/encoding/json v0.0.1/go.mod h1:T+sKiDPkVJnUrmZJjbP8vQvF52lmCfyH92G3G2zNpMQ=
+gitoa.ru/go-4devs/mime v0.0.1 h1:WKcEhe1g5l7gMHWuC7xxeir9zIPYmlNKRjmnOj0kciA=
+gitoa.ru/go-4devs/mime v0.0.1/go.mod h1:t+UFgjBNqSdLMYzV3xbKoLWX7dZgcqxEzcjAFh4Y8Fs=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/gohtml/engine/engine.go b/gohtml/engine/engine.go
new file mode 100644
index 0000000..2675524
--- /dev/null
+++ b/gohtml/engine/engine.go
@@ -0,0 +1,25 @@
+package engine
+
+import (
+ "fmt"
+ html "html/template"
+
+ "gitoa.ru/go-4devs/mime"
+ "gitoa.ru/go-4devs/templating/engine"
+ "gitoa.ru/go-4devs/templating/loader"
+)
+
+const Name = "gohtml"
+
+func Parser(source loader.Source) (engine.Template, error) {
+ tpl, err := html.New(source.Name()).Parse(source.Data())
+ if err != nil {
+ return nil, fmt.Errorf("parse html:%w", engine.ErrNotSupport)
+ }
+
+ return engine.NewTemplate(tpl.Name(), tpl.Execute), nil
+}
+
+func New(opts ...engine.Option) *engine.Engine {
+ return engine.New(Name, Parser, append(opts, engine.WithFormats(mime.ExtHTML))...)
+}
diff --git a/gohtml/engine/engine_test.go b/gohtml/engine/engine_test.go
new file mode 100644
index 0000000..7514259
--- /dev/null
+++ b/gohtml/engine/engine_test.go
@@ -0,0 +1,23 @@
+package engine_test
+
+import (
+ "bytes"
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "gitoa.ru/go-4devs/templating/gohtml/engine"
+ "gitoa.ru/go-4devs/templating/loader"
+)
+
+func TestParser(t *testing.T) {
+ t.Parallel()
+
+ ctx := context.Background()
+ buff := bytes.Buffer{}
+
+ tpl, err := engine.Parser(loader.NewSource(t.Name(), `engine:{{.name}}`))
+ require.NoError(t, err)
+ require.NoError(t, tpl.Execute(ctx, &buff, map[string]string{"name": "gohtml"}, nil))
+ require.Equal(t, "engine:gohtml", buff.String())
+}
diff --git a/gohtml/html.go b/gohtml/html.go
new file mode 100644
index 0000000..22898bf
--- /dev/null
+++ b/gohtml/html.go
@@ -0,0 +1,43 @@
+package gohtml
+
+import (
+ "context"
+ "fmt"
+ html "html/template"
+
+ "gitoa.ru/go-4devs/templating"
+ "gitoa.ru/go-4devs/templating/engine"
+ gohtml "gitoa.ru/go-4devs/templating/gohtml/engine"
+ "gitoa.ru/go-4devs/templating/loader"
+)
+
+const Name = gohtml.Name
+
+var htm = gohtml.New()
+
+func init() {
+ templating.AddEngine(htm)
+}
+
+// Loader set new loader. This function is not thread-safe.
+func Loader(in loader.Loader) {
+ htm = htm.WithLoader(in)
+}
+
+func Must(template *html.Template, err error) {
+ MustRegister(html.Must(template, err))
+}
+
+func MustRegister(template *html.Template) {
+ if rerr := Register(template); rerr != nil {
+ panic(rerr)
+ }
+}
+
+func Register(template *html.Template) error {
+ if err := htm.Add(context.Background(), engine.NewTemplate(template.Name(), template.Execute)); err != nil {
+ return fmt.Errorf("gohtml loader:%w", err)
+ }
+
+ return nil
+}
diff --git a/gotxt/engine/engine.go b/gotxt/engine/engine.go
new file mode 100644
index 0000000..811f1aa
--- /dev/null
+++ b/gotxt/engine/engine.go
@@ -0,0 +1,25 @@
+package engine
+
+import (
+ "fmt"
+ text "text/template"
+
+ "gitoa.ru/go-4devs/mime"
+ "gitoa.ru/go-4devs/templating/engine"
+ "gitoa.ru/go-4devs/templating/loader"
+)
+
+const Name = "gotxt"
+
+func Parser(source loader.Source) (engine.Template, error) {
+ tpl, err := text.New(source.Name()).Parse(source.Data())
+ if err != nil {
+ return nil, fmt.Errorf("parse text:%w", engine.ErrNotSupport)
+ }
+
+ return engine.NewTemplate(tpl.Name(), tpl.Execute), nil
+}
+
+func New(opts ...engine.Option) *engine.Engine {
+ return engine.New(Name, Parser, append(opts, engine.WithFormats(mime.ExtTxt, mime.ExtText))...)
+}
diff --git a/gotxt/text.go b/gotxt/text.go
new file mode 100644
index 0000000..a7825bb
--- /dev/null
+++ b/gotxt/text.go
@@ -0,0 +1,43 @@
+package gotxt
+
+import (
+ "context"
+ "fmt"
+ text "text/template"
+
+ "gitoa.ru/go-4devs/templating"
+ "gitoa.ru/go-4devs/templating/engine"
+ gotxt "gitoa.ru/go-4devs/templating/gotxt/engine"
+ "gitoa.ru/go-4devs/templating/loader"
+)
+
+const Name = gotxt.Name
+
+var txt = gotxt.New()
+
+func init() {
+ templating.AddEngine(txt)
+}
+
+// Loader set new loader. This function is not thread-safe.
+func Loader(in loader.Loader) {
+ txt = txt.WithLoader(in)
+}
+
+func Must(template *text.Template, err error) {
+ MustRegister(text.Must(template, err))
+}
+
+func MustRegister(template *text.Template) {
+ if rerr := Register(template); rerr != nil {
+ panic(rerr)
+ }
+}
+
+func Register(template *text.Template) error {
+ if err := txt.Add(context.Background(), engine.NewTemplate(template.Name(), template.Execute)); err != nil {
+ return fmt.Errorf("gotext loader:%w", err)
+ }
+
+ return nil
+}
diff --git a/json/engine/engine.go b/json/engine/engine.go
new file mode 100644
index 0000000..c565533
--- /dev/null
+++ b/json/engine/engine.go
@@ -0,0 +1,13 @@
+package engine
+
+import (
+ "gitoa.ru/go-4devs/encoding/json"
+ "gitoa.ru/go-4devs/mime"
+ "gitoa.ru/go-4devs/templating/engine"
+)
+
+const Name = "json"
+
+func New() engine.Encode {
+ return engine.NewEncode(Name, json.Encode, mime.ExtJSON)
+}
diff --git a/json/json.go b/json/json.go
new file mode 100644
index 0000000..9c91baf
--- /dev/null
+++ b/json/json.go
@@ -0,0 +1,12 @@
+package json
+
+import (
+ "gitoa.ru/go-4devs/templating"
+ json "gitoa.ru/go-4devs/templating/json/engine"
+)
+
+const Name = json.Name
+
+func init() {
+ templating.AddEngine(json.New())
+}
diff --git a/loader/chain/loader.go b/loader/chain/loader.go
new file mode 100644
index 0000000..6eb8a37
--- /dev/null
+++ b/loader/chain/loader.go
@@ -0,0 +1,29 @@
+package chain
+
+import (
+ "context"
+ "errors"
+ "fmt"
+
+ "gitoa.ru/go-4devs/templating/loader"
+ "gitoa.ru/go-4devs/templating/render"
+)
+
+var _ loader.Loader = (Loaders)(nil)
+
+type Loaders []loader.Loader
+
+func (l Loaders) Load(ctx context.Context, template render.Reference) (loader.Source, error) {
+ for idx, load := range l {
+ sourse, err := load.Load(ctx, template)
+ if err == nil {
+ return sourse, nil
+ }
+
+ if !errors.Is(err, loader.ErrNotFound) {
+ return loader.Source{}, fmt.Errorf("chain[%d]:%w", idx, err)
+ }
+ }
+
+ return loader.Source{}, fmt.Errorf("chains(%d):%w", len(l), loader.ErrNotFound)
+}
diff --git a/loader/empty.go b/loader/empty.go
new file mode 100644
index 0000000..9a7d58e
--- /dev/null
+++ b/loader/empty.go
@@ -0,0 +1,20 @@
+package loader
+
+import (
+ "context"
+ "fmt"
+
+ "gitoa.ru/go-4devs/templating/render"
+)
+
+var _empty = empty{}
+
+type empty struct{}
+
+func (empty) Load(context.Context, render.Reference) (Source, error) {
+ return Source{}, fmt.Errorf("empty: %w", ErrNotFound)
+}
+
+func Empty() Loader {
+ return _empty
+}
diff --git a/loader/loader.go b/loader/loader.go
new file mode 100644
index 0000000..f72f1e5
--- /dev/null
+++ b/loader/loader.go
@@ -0,0 +1,34 @@
+package loader
+
+import (
+ "context"
+ "errors"
+
+ "gitoa.ru/go-4devs/templating/render"
+)
+
+var ErrNotFound = errors.New("not found")
+
+type Loader interface {
+ Load(ctx context.Context, r render.Reference) (Source, error)
+}
+
+func NewSource(name, data string) Source {
+ return Source{
+ name: name,
+ data: data,
+ }
+}
+
+type Source struct {
+ name string
+ data string
+}
+
+func (s Source) Name() string {
+ return s.name
+}
+
+func (s Source) Data() string {
+ return s.data
+}
diff --git a/parser/name.go b/parser/name.go
new file mode 100644
index 0000000..0bb3736
--- /dev/null
+++ b/parser/name.go
@@ -0,0 +1,43 @@
+package parser
+
+import (
+ "context"
+ "path"
+ "strings"
+
+ "gitoa.ru/go-4devs/mime"
+ "gitoa.ru/go-4devs/templating/render"
+)
+
+const (
+ nameWithExt = 2
+ nameWithEngine = 3
+ countOption = 2
+)
+
+// Name parse name by .. or ..
+func Name(_ context.Context, name string, opts ...render.Option) (render.Reference, error) {
+ options := make([]render.Option, 0, countOption)
+ tplName := strings.ToLower(name)
+
+ base := path.Base(tplName)
+ el := strings.SplitN(base, ".", nameWithEngine)
+
+ var ext mime.Ext
+
+ switch len(el) {
+ case nameWithEngine:
+ ext = mime.ExtFromString(el[1])
+ options = append(options, render.WithEngine(el[2]))
+ tplName = strings.TrimRight(tplName, "."+el[1]+"."+el[2])
+ case nameWithExt:
+ ext = mime.ExtFromString(el[1])
+ tplName = strings.TrimRight(tplName, "."+el[1])
+ }
+
+ if !ext.Is(mime.ExtUnrecognized) {
+ options = append(options, render.WithFormat(ext))
+ }
+
+ return render.NewReference(tplName, append(options, opts...)...), nil
+}
diff --git a/render/reference.go b/render/reference.go
new file mode 100644
index 0000000..aa1e873
--- /dev/null
+++ b/render/reference.go
@@ -0,0 +1,78 @@
+package render
+
+import "gitoa.ru/go-4devs/mime"
+
+type Option func(*Reference)
+
+func WithParam(name string, val interface{}) Option {
+ return func(r *Reference) {
+ r.Params[name] = val
+ }
+}
+
+func WithEngine(name string) Option {
+ return func(r *Reference) {
+ r.Engine = name
+ }
+}
+
+func WithFormat(ext mime.Ext) Option {
+ return func(r *Reference) {
+ r.Format = ext
+ }
+}
+
+func NewReference(name string, opts ...Option) Reference {
+ ref := Reference{
+ name: name,
+ }
+
+ for _, opt := range opts {
+ opt(&ref)
+ }
+
+ return ref
+}
+
+type Reference struct {
+ name string
+ Engine string
+ Format mime.Ext
+ Params Params
+}
+
+func (r Reference) Name() string {
+ if !r.Format.Is(mime.ExtUnrecognized) {
+ return r.name + "." + r.Format.String()
+ }
+
+ return r.name
+}
+
+func (r Reference) String() string {
+ if r.Engine != "" {
+ return r.name + "." + r.Format.String() + "." + r.Engine
+ }
+
+ return r.name + "." + r.Format.String()
+}
+
+func (r Reference) IsEngine(engines ...string) bool {
+ for _, engine := range engines {
+ if engine == r.Engine {
+ return true
+ }
+ }
+
+ return false
+}
+
+func (r Reference) IsFromat(formats ...mime.Ext) bool {
+ return r.Format.Is(formats...)
+}
+
+type Params map[string]interface{}
+
+func (p Params) Get(name string) interface{} {
+ return p[name]
+}
diff --git a/render/render.go b/render/render.go
new file mode 100644
index 0000000..9bdac6e
--- /dev/null
+++ b/render/render.go
@@ -0,0 +1,77 @@
+package render
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+)
+
+var ErrNotFound = errors.New("not found")
+
+func New(parser Parser, engines ...Engine) *Render {
+ return &Render{
+ parser: parser,
+ engines: engines,
+ }
+}
+
+type Render struct {
+ engines []Engine
+ parser Parser
+}
+
+type Execute func(ctx context.Context, wr io.Writer, data interface{}, params Params) error
+
+type Engine interface {
+ Support(context.Context, Reference) bool
+ Load(ctx context.Context, reference Reference) (Execute, error)
+}
+
+type Parser func(ctx context.Context, name string, opts ...Option) (Reference, error)
+
+// Add add engine. This function is not thread-safe.
+func (e *Render) Add(engine ...Engine) {
+ e.engines = append(e.engines, engine...)
+}
+
+func (e *Render) WithParser(parser Parser) *Render {
+ return New(parser, e.engines...)
+}
+
+func (e *Render) Load(ctx context.Context, reference Reference) (Execute, error) {
+ for _, engine := range e.engines {
+ if engine.Support(ctx, reference) {
+ exec, err := engine.Load(ctx, reference)
+ if err != nil {
+ return nil, fmt.Errorf("render load:%w", err)
+ }
+
+ return exec, nil
+ }
+ }
+
+ return nil, fmt.Errorf("loader:%w", ErrNotFound)
+}
+
+func (e *Render) Parse(ctx context.Context, name string, opts ...Option) (Reference, error) {
+ return e.parser(ctx, name, opts...)
+}
+
+func (e *Render) Execute(ctx context.Context, wr io.Writer, name string, data interface{}, opts ...Option) error {
+ reference, rerr := e.parser(ctx, name, opts...)
+ if rerr != nil {
+ return fmt.Errorf("parse: %w", rerr)
+ }
+
+ execute, err := e.Load(ctx, reference)
+ if err != nil {
+ return fmt.Errorf("engine load:%w", err)
+ }
+
+ if err := execute(ctx, wr, data, reference.Params); err != nil {
+ return fmt.Errorf("render execute:%w", err)
+ }
+
+ return nil
+}