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