Compare commits

...

7 Commits

6 changed files with 577 additions and 12 deletions

View File

@@ -1,20 +1,30 @@
version: "2"
run:
timeout: 5m
modules-download-mode: readonly
linters:
disable-all: true
default: none
enable:
- errcheck
- goimports
- gofumpt
- revive # golint
- gosimple
- govet
- ineffassign
- revive
- staticcheck
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$
issues:
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0
formatters:
enable:
- gofumpt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

6
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1734424634,
"narHash": "sha256-cHar1vqHOOyC7f1+tVycPoWTfKIaqkoe1Q6TnKzuti4=",
"lastModified": 1760878510,
"narHash": "sha256-K5Osef2qexezUfs0alLvZ7nQFTGS9DL2oTVsIXsqLgs=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "d3c42f187194c26d9f0309a8ecc469d6c878ce33",
"rev": "5e2a59a5b1a82f89f2c7e598302a9cacebb72a67",
"type": "github"
},
"original": {

283
xtemplate/hot.go Normal file
View File

@@ -0,0 +1,283 @@
package xtemplate
import (
"errors"
"fmt"
htmltemplate "html/template"
"io"
"io/fs"
"slices"
"text/template"
"text/template/parse"
)
func NewHot[T template.Template | htmltemplate.Template](name string) *HotTemplate {
return Hot(New[T](name))
}
func Hot(t Template) *HotTemplate {
return &HotTemplate{template: t}
}
type HotTemplate struct {
template Template
err error
methodQueue []struct {
name methodName
args []any
}
}
var _ Template = (*HotTemplate)(nil)
func (t *HotTemplate) AddParseTree(name string, tree *parse.Tree) (Template, error) {
return t.queue(methodAddParseTree, []any{name, tree})
}
func (t *HotTemplate) Clone() (Template, error) {
return t.clone()
}
func (t *HotTemplate) DefinedTemplates() string {
temp, err := t.run()
if err != nil {
return ""
}
return temp.DefinedTemplates()
}
func (t *HotTemplate) Delims(left, right string) Template {
t, _ = t.queue(methodDelims, []any{left, right})
return t
}
func (t *HotTemplate) Execute(wr io.Writer, data any) error {
temp, err := t.run()
if err != nil {
return errors.Join(ErrHotRun, err)
}
return temp.Execute(wr, data)
}
func (t *HotTemplate) ExecuteTemplate(wr io.Writer, name string, data any) error {
temp, err := t.run()
if err != nil {
return errors.Join(ErrHotRun, err)
}
return temp.ExecuteTemplate(wr, name, data)
}
func (t *HotTemplate) Funcs(funcMap template.FuncMap) Template {
t, _ = t.queue(methodFuncs, funcMap)
return t
}
func (t *HotTemplate) Lookup(name string) Template {
temp, err := t.run()
if err != nil {
return nil
}
return temp.Lookup(name)
}
func (t *HotTemplate) Name() string {
temp, err := t.run()
if err != nil {
return ""
}
return temp.Name()
}
func (t *HotTemplate) New(name string) Template {
temp, err := t.run()
if err != nil {
return nil
}
return Hot(temp.New(name))
}
func (t *HotTemplate) Option(opt ...string) Template {
t, _ = t.queue(methodOption, opt)
return t
}
func (t *HotTemplate) Parse(text string) (Template, error) {
return t.queue(methodParse, text)
}
func (t *HotTemplate) ParseFS(fs fs.FS, patterns ...string) (Template, error) {
return t.queue(methodParseFS, fs, patterns)
}
func (t *HotTemplate) ParseFiles(filenames ...string) (Template, error) {
return t.queue(methodParseFiles, filenames)
}
func (t *HotTemplate) ParseGlob(pattern string) (Template, error) {
return t.queue(methodParseGlob, pattern)
}
func (t *HotTemplate) Templates() []Template {
temp, err := t.run()
if err != nil {
return nil
}
ts := temp.Templates()
hts := make([]Template, len(ts))
for i := range ts {
hts[i] = Hot(ts[i])
}
return hts
}
func (t *HotTemplate) queue(method methodName, args ...any) (*HotTemplate, error) {
tc, err := t.clone()
if err != nil {
// HACK: Storing the error so it can be returned on methods that can
t.err = errors.Join(ErrHotQueue, err)
return t, err
}
tc.methodQueue = append(t.methodQueue, struct {
name methodName
args []any
}{
name: method,
args: args,
})
return tc, nil
}
func (t *HotTemplate) clone() (*HotTemplate, error) {
temp, err := t.template.Clone()
if err != nil {
return nil, fmt.Errorf("HotTemplate: unable to clone template: %w", err)
}
return &HotTemplate{
template: temp,
methodQueue: slices.Clone(t.methodQueue),
}, nil
}
func (t *HotTemplate) run() (Template, error) {
if t.err != nil {
return t, fmt.Errorf("HotTemplate: template has a past error: %w", t.err)
}
temp, err := t.template.Clone()
if err != nil {
return temp, fmt.Errorf("HotTemplate: unable to clone template: %w", err)
}
for _, method := range t.methodQueue {
switch method.name {
case methodAddParseTree:
name, ok := method.args[0].(string)
if !ok {
err = fmt.Errorf("HotTemplate: first argument is not of type string")
break
}
tree, ok := method.args[1].(*parse.Tree)
if !ok {
err = fmt.Errorf("HotTemplate: second argument is not of type *parse.Tree")
break
}
temp, err = temp.AddParseTree(name, tree)
case methodDelims:
left, ok := method.args[0].(string)
if !ok {
err = fmt.Errorf("HotTemplate: first argument is not of type string")
break
}
right, ok := method.args[1].(string)
if !ok {
err = fmt.Errorf("HotTemplate: second argument is not of type string")
break
}
temp = temp.Delims(left, right)
case methodFuncs:
funcMap, ok := method.args[0].(template.FuncMap)
if !ok {
err = fmt.Errorf("HotTemplate: first argument is not of type template.FuncMap")
break
}
temp = temp.Funcs(funcMap)
case methodOption:
opt, ok := method.args[0].([]string)
if !ok {
err = fmt.Errorf("HotTemplate: first argument is not of type []string")
break
}
temp = temp.Option(opt...)
case methodParse:
text, ok := method.args[0].(string)
if !ok {
err = fmt.Errorf("HotTemplate: first argument is not of type string")
break
}
temp, err = temp.Parse(text)
case methodParseFS:
fs, ok := method.args[0].(fs.FS)
if !ok {
err = fmt.Errorf("HotTemplate: first argument is not of type fs.FS")
break
}
patterns, ok := method.args[0].([]string)
if !ok {
err = fmt.Errorf("HotTemplate: second argument is not of type []string")
break
}
temp, err = temp.ParseFS(fs, patterns...)
case methodParseFiles:
filenames, ok := method.args[0].([]string)
if !ok {
err = fmt.Errorf("HotTemplate: first argument is not of type []string")
break
}
temp, err = temp.ParseFiles(filenames...)
case methodParseGlob:
pattern, ok := method.args[0].(string)
if !ok {
err = fmt.Errorf("HotTemplate: first argument is not of type string")
break
}
temp, err = temp.ParseGlob(pattern)
default:
err = fmt.Errorf("HotTemplate: method %q does not exist", method.name)
}
if err != nil {
return temp, fmt.Errorf("HotTemplate: failed to run method %q with arguments %v: %w",
method.name, method.args, err)
}
}
return temp, nil
}
const (
methodAddParseTree methodName = "AddParseTree"
methodDelims methodName = "Delims"
methodFuncs methodName = "Funcs"
methodOption methodName = "Option"
methodParse methodName = "Parse"
methodParseFS methodName = "ParseFS"
methodParseFiles methodName = "ParseFiles"
methodParseGlob methodName = "ParseGlob"
)
type methodName string
var (
ErrHotRun = errors.New("HotTemplate: unable to run queued methods")
ErrHotQueue = errors.New("HotTemplate: unable to queue method")
)

89
xtemplate/html.go Normal file
View File

@@ -0,0 +1,89 @@
package xtemplate
import (
"html/template"
"io"
"io/fs"
"text/template/parse"
)
type htmlTemplate struct {
html *template.Template
}
var _ Template = (*htmlTemplate)(nil)
func (t *htmlTemplate) AddParseTree(name string, tree *parse.Tree) (Template, error) {
temp, err := t.html.AddParseTree(name, tree)
return &htmlTemplate{temp}, err
}
func (t *htmlTemplate) Clone() (Template, error) {
temp, err := t.html.Clone()
return &htmlTemplate{temp}, err
}
func (t *htmlTemplate) Delims(left, right string) Template {
return &htmlTemplate{t.html.Delims(left, right)}
}
func (t *htmlTemplate) DefinedTemplates() string {
return t.html.DefinedTemplates()
}
func (t *htmlTemplate) Execute(wr io.Writer, data any) error {
return t.html.Execute(wr, data)
}
func (t *htmlTemplate) ExecuteTemplate(wr io.Writer, name string, data any) error {
return t.html.ExecuteTemplate(wr, name, data)
}
func (t *htmlTemplate) Funcs(funcMap template.FuncMap) Template {
return &htmlTemplate{t.html.Funcs(funcMap)}
}
func (t *htmlTemplate) Lookup(name string) Template {
return &htmlTemplate{t.html.Lookup(name)}
}
func (t *htmlTemplate) Name() string {
return t.html.Name()
}
func (t *htmlTemplate) New(name string) Template {
return &htmlTemplate{t.html.New(name)}
}
func (t *htmlTemplate) Option(opt ...string) Template {
return &htmlTemplate{t.html.Option(opt...)}
}
func (t *htmlTemplate) Parse(text string) (Template, error) {
temp, err := t.html.Parse(text)
return &htmlTemplate{temp}, err
}
func (t *htmlTemplate) ParseFS(fs fs.FS, patterns ...string) (Template, error) {
temp, err := t.html.ParseFS(fs, patterns...)
return &htmlTemplate{temp}, err
}
func (t *htmlTemplate) ParseFiles(filenames ...string) (Template, error) {
temp, err := t.html.ParseFiles(filenames...)
return &htmlTemplate{temp}, err
}
func (t *htmlTemplate) ParseGlob(pattern string) (Template, error) {
temp, err := t.html.ParseGlob(pattern)
return &htmlTemplate{temp}, err
}
func (t *htmlTemplate) Templates() []Template {
htmltemps := t.html.Templates()
temps := make([]Template, len(htmltemps))
for i := range len(temps) {
temps[i] = &htmlTemplate{htmltemps[i]}
}
return temps
}

89
xtemplate/text.go Normal file
View File

@@ -0,0 +1,89 @@
package xtemplate
import (
"io"
"io/fs"
"text/template"
"text/template/parse"
)
type textTemplate struct {
text *template.Template
}
var _ Template = (*textTemplate)(nil)
func (t *textTemplate) AddParseTree(name string, tree *parse.Tree) (Template, error) {
temp, err := t.text.AddParseTree(name, tree)
return &textTemplate{temp}, err
}
func (t *textTemplate) Clone() (Template, error) {
temp, err := t.text.Clone()
return &textTemplate{temp}, err
}
func (t *textTemplate) Delims(left, right string) Template {
return &textTemplate{t.text.Delims(left, right)}
}
func (t *textTemplate) DefinedTemplates() string {
return t.text.DefinedTemplates()
}
func (t *textTemplate) Execute(wr io.Writer, data any) error {
return t.text.Execute(wr, data)
}
func (t *textTemplate) ExecuteTemplate(wr io.Writer, name string, data any) error {
return t.text.ExecuteTemplate(wr, name, data)
}
func (t *textTemplate) Funcs(funcMap template.FuncMap) Template {
return &textTemplate{t.text.Funcs(funcMap)}
}
func (t *textTemplate) Lookup(name string) Template {
return &textTemplate{t.text.Lookup(name)}
}
func (t *textTemplate) Name() string {
return t.text.Name()
}
func (t *textTemplate) New(name string) Template {
return &textTemplate{t.text.New(name)}
}
func (t *textTemplate) Option(opt ...string) Template {
return &textTemplate{t.text.Option(opt...)}
}
func (t *textTemplate) Parse(text string) (Template, error) {
temp, err := t.text.Parse(text)
return &textTemplate{temp}, err
}
func (t *textTemplate) ParseFS(fs fs.FS, patterns ...string) (Template, error) {
temp, err := t.text.ParseFS(fs, patterns...)
return &textTemplate{temp}, err
}
func (t *textTemplate) ParseFiles(filenames ...string) (Template, error) {
temp, err := t.text.ParseFiles(filenames...)
return &textTemplate{temp}, err
}
func (t *textTemplate) ParseGlob(pattern string) (Template, error) {
temp, err := t.text.ParseGlob(pattern)
return &textTemplate{temp}, err
}
func (t *textTemplate) Templates() []Template {
texttemps := t.text.Templates()
temps := make([]Template, len(texttemps))
for i := range len(temps) {
temps[i] = &textTemplate{texttemps[i]}
}
return temps
}

94
xtemplate/xtemplate.go Normal file
View File

@@ -0,0 +1,94 @@
package xtemplate
import (
htmltemplate "html/template"
"io"
"io/fs"
"text/template"
"text/template/parse"
)
func Must(t Template, err error) Template {
if err != nil {
panic(err)
}
return t
}
func New[T template.Template | htmltemplate.Template](name string) Template {
var t T
switch any(t).(type) {
case template.Template:
return &textTemplate{template.New(name)}
case htmltemplate.Template:
return &htmlTemplate{htmltemplate.New(name)}
default:
panic("Incorrect T type")
}
}
func ParseFS[T template.Template | htmltemplate.Template](fs fs.FS, patterns ...string) (Template, error) {
var t T
switch any(t).(type) {
case template.Template:
temp, err := template.ParseFS(fs, patterns...)
return &textTemplate{temp}, err
case htmltemplate.Template:
temp, err := htmltemplate.ParseFS(fs, patterns...)
return &htmlTemplate{temp}, err
default:
panic("Incorrect T type")
}
}
func ParseFiles[T template.Template | htmltemplate.Template](filenames ...string) (Template, error) {
var t T
switch any(t).(type) {
case template.Template:
temp, err := template.ParseFiles(filenames...)
return &textTemplate{temp}, err
case htmltemplate.Template:
temp, err := htmltemplate.ParseFiles(filenames...)
return &htmlTemplate{temp}, err
default:
panic("Incorrect T type")
}
}
func ParseGlob[T template.Template | htmltemplate.Template](pattern string) (Template, error) {
var t T
switch any(t).(type) {
case template.Template:
temp, err := template.ParseGlob(pattern)
return &textTemplate{temp}, err
case htmltemplate.Template:
temp, err := htmltemplate.ParseGlob(pattern)
return &htmlTemplate{temp}, err
default:
panic("Incorrect T type")
}
}
type Template interface {
AddParseTree(name string, tree *parse.Tree) (Template, error)
Clone() (Template, error)
DefinedTemplates() string
Delims(left, right string) Template
Funcs(funcMap template.FuncMap) Template
Lookup(name string) Template
Name() string
New(name string) Template
Option(opt ...string) Template
Parse(text string) (Template, error)
ParseFS(fs fs.FS, patterns ...string) (Template, error)
ParseFiles(filenames ...string) (Template, error)
ParseGlob(pattern string) (Template, error)
Templates() []Template
Templater
}
type Templater interface {
Execute(wr io.Writer, data any) error
ExecuteTemplate(wr io.Writer, name string, data any) error
}