andrey1s
2 years ago
24 changed files with 969 additions and 0 deletions
@ -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: {} |
|||
|
@ -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 |
@ -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 |
|||
} |
@ -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()) |
|||
} |
@ -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 |
|||
} |
@ -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...)) |
|||
} |
@ -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) |
|||
} |
@ -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 |
|||
} |
@ -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: "<b>world</b>"} |
|||
|
|||
gohtml.Must(html.New("example_with_option.html").Parse(`Hello <s>{{ .Name }}</s>!`)) |
|||
|
|||
err := templating.Execute(ctx, os.Stdout, "example_with_option.html", data, render.WithEngine(gohtml.Name)) |
|||
if err != nil { |
|||
log.Fatalln(err) |
|||
} |
|||
// Output:
|
|||
// Hello <s><b>world</b></s>!
|
|||
} |
|||
|
|||
func ExampleExecute_text() { |
|||
ctx := context.Background() |
|||
data := Data{Name: "<u>text</u>"} |
|||
|
|||
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 <u>text</u>!
|
|||
} |
|||
|
|||
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!
|
|||
} |
@ -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 |
|||
) |
@ -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= |
@ -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))...) |
|||
} |
@ -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()) |
|||
} |
@ -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 |
|||
} |
@ -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))...) |
|||
} |
@ -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 |
|||
} |
@ -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) |
|||
} |
@ -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()) |
|||
} |
@ -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) |
|||
} |
@ -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 |
|||
} |
@ -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 |
|||
} |
@ -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 <tpl name>.<format>.<engine> or <tpl name>.<format>.
|
|||
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 |
|||
} |
@ -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] |
|||
} |
@ -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 |
|||
} |
Loading…
Reference in new issue