From 58033d8e3717ed6cf1d94494c03050677d3420d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20P=C3=A9rez?= Date: Wed, 21 Aug 2024 22:39:00 +0200 Subject: [PATCH 1/6] feat: :tada: nace gorender --- README.md | 106 +++++++++++++++++++ example/go.mod | 20 ++++ example/go.sum | 30 ++++++ example/main.go | 37 +++++++ example/template/base.html | 22 ++++ example/template/fragment.html | 9 ++ example/template/pages/page.html | 7 ++ form.go | 95 +++++++++++++++++ functions.go | 71 +++++++++++++ go.mod | 19 ++++ go.sum | 30 ++++++ pages.go | 130 ++++++++++++++++++++++++ render.go | 168 +++++++++++++++++++++++++++++++ 13 files changed, 744 insertions(+) create mode 100644 README.md create mode 100644 example/go.mod create mode 100644 example/go.sum create mode 100644 example/main.go create mode 100644 example/template/base.html create mode 100644 example/template/fragment.html create mode 100644 example/template/pages/page.html create mode 100644 form.go create mode 100644 functions.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 pages.go create mode 100644 render.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..275c391 --- /dev/null +++ b/README.md @@ -0,0 +1,106 @@ +# gorender + +Simple y minimalista librería para procesar plantillas utilizando la librería +estándar de Go `html/template`. + +## Características + +- Procesamiento de plantillas utilizando `html/template`. +- Soporte para caché de plantillas. +- Posibilidad de añadir funciones personalizadas a las plantillas. +- Configuración sencilla con opciones por defecto que se pueden sobreescribir. +Inspirado en `Gin`. + +## Instalación + +```bash +go get github.com/zepyrshut/gorender +``` + +## Uso mínimo + +Las plantillas deben tener la siguiente estructura, observa que las páginas a +procesar están dentro de `pages`. Los demás componentes como bases y fragmentos +pueden estar en el directorio raíz o dentro de un directorio. + +Puedes cambiar el nombre del directorio `template` y `pages`. Ejemplo en la +siguiente sección. + +``` +template/ +├── pages/ +│ └── page.html +├── base.html +└── fragment.html +``` + +```go +import ( + "github.com/zepyrshut/gorender" +) + +func main() { + ren := gorender.New() + + // ... + + td := &gorender.TemplateData{} + ren.Template(w, r, "index.html", td) + + // ... +} +``` + +## Personalización + +> Recuerda que si habilitas el caché, no podrás ver los cambios que realices +> durante el desarrollo. + +```go +func dummyFunc() string { + return "dummy" +} + +func main() { + + customFuncs := template.FuncMap{ + "dummyFunc": dummyFunc, + } + + renderOpts := &gorender.Render{ + EnableCache: true, + TemplatesPath: "template/path", + PageTemplatesPath: "template/path/pages", + Functions: customFuncs, + } + + ren := gorender.New(gorender.WithRenderOptions(renderOpts)) + + // ... + + td := &gorender.TemplateData{} + ren.Template(w, r, "index.html", td) + + // ... +} +``` +## Agradecimientos + +- [Protección CSRF justinas/nosurf](https://github.com/justinas/nosurf) +- [Valicación go-playground/validator](https://github.com/go-playground/validator) + +## Descargo de responsabilidad + +Esta librería fue creada para usar las plantillas en mis proyectos privados, es +posible que también solucione su problema. Sin embargo, no ofrezco ninguna +garantía de que funcione para todos los casos de uso, tenga el máximo +rendimiento o esté libre de errores. + +Si decides integrarla en tu proyecto, te recomiendo que la pruebes para +asegurarte de que cumple con tus expectativas y requisitos. + +Si encuentras problemas o tienes sugerencias de mejora, puedes colocar tus +aportaciones a través de _issues_ o _pull requests_ en el repositorio. Estaré +encantado de ayudarte. + + diff --git a/example/go.mod b/example/go.mod new file mode 100644 index 0000000..441ffa6 --- /dev/null +++ b/example/go.mod @@ -0,0 +1,20 @@ +module gorender/example + +go 1.23.0 + +replace github.com/zepyrshut/gorender => ../ + +require github.com/zepyrshut/gorender v0.0.0-00010101000000-000000000000 + +require ( + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.22.0 // indirect + github.com/justinas/nosurf v1.1.1 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + golang.org/x/crypto v0.19.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/example/go.sum b/example/go.sum new file mode 100644 index 0000000..d6f9aad --- /dev/null +++ b/example/go.sum @@ -0,0 +1,30 @@ +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/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= +github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk= +github.com/justinas/nosurf v1.1.1/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +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/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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/example/main.go b/example/main.go new file mode 100644 index 0000000..14e6ce1 --- /dev/null +++ b/example/main.go @@ -0,0 +1,37 @@ +package main + +import ( + "fmt" + "net/http" + "text/template" + + "github.com/zepyrshut/gorender" +) + +func dummyFunc() string { + return "dummy function" +} + +func main() { + newFuncs := template.FuncMap{ + "dummyFunc": dummyFunc, + } + + renderOpts := &gorender.Render{ + EnableCache: true, + TemplatesPath: "template", + PageTemplatesPath: "template/pages", + Functions: newFuncs, + } + + ren := gorender.New(gorender.WithRenderOptions(renderOpts)) + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + td := &gorender.TemplateData{} + + ren.Template(w, r, "page.html", td) + }) + + fmt.Println("Server running on port 8080") + http.ListenAndServe(":8080", nil) +} diff --git a/example/template/base.html b/example/template/base.html new file mode 100644 index 0000000..ea051d3 --- /dev/null +++ b/example/template/base.html @@ -0,0 +1,22 @@ +{{ define "base" }} + + + + + + + gorender example + + + + +

Base

+ +
+ {{ block "content" . }}{{ end }} +
+ + + + +{{ end }} \ No newline at end of file diff --git a/example/template/fragment.html b/example/template/fragment.html new file mode 100644 index 0000000..70e943e --- /dev/null +++ b/example/template/fragment.html @@ -0,0 +1,9 @@ +{{ define "fragment" }} + +

Fragment

+ + +{{ dummyFunc }} + + +{{ end }} \ No newline at end of file diff --git a/example/template/pages/page.html b/example/template/pages/page.html new file mode 100644 index 0000000..5145c05 --- /dev/null +++ b/example/template/pages/page.html @@ -0,0 +1,7 @@ +{{ template "base" . }} +{{ define "content" }} + +

Page with fragment

+ +{{ template "fragment" . }} +{{ end }} \ No newline at end of file diff --git a/form.go b/form.go new file mode 100644 index 0000000..0a2f46e --- /dev/null +++ b/form.go @@ -0,0 +1,95 @@ +package gorender + +import ( + "strings" + + spanish "github.com/go-playground/locales/es" + ut "github.com/go-playground/universal-translator" + "github.com/go-playground/validator/v10" + esTranslations "github.com/go-playground/validator/v10/translations/es" +) + +type FormData struct { + HasErrors bool + Errors map[string]string + Values map[string]string +} + +func NewForm() FormData { + return FormData{ + HasErrors: false, + Errors: map[string]string{}, + Values: map[string]string{}, + } +} + +// AddError añade errores a la estructura FormData, es un mapa cuya clave es una +// cadena de carecteres. Hay que tener en cuenta que cuando se hace una +// validación, se llama a esta función cuya clave es el nombre del campo con lo +// cual si hay más de un error de validación se sobreescriben el anterior y sólo +// se muestra el último error. +func (fd *FormData) AddError(field, message string) { + fd.HasErrors = true + fd.Errors[field] = message +} + +func (fd *FormData) AddValue(field, value string) { + fd.Values[field] = value +} + +type ValidationError struct { + Field string + Reason string +} + +func (fd *FormData) ValidateStruct(s interface{}) (map[string]string, error) { + spanishTranslator := spanish.New() + uni := ut.New(spanishTranslator, spanishTranslator) + trans, _ := uni.GetTranslator("es") + validate := validator.New() + _ = esTranslations.RegisterDefaultTranslations(validate, trans) + errors := make(map[string]string) + var validationErrors []ValidationError + + err := validate.Struct(s) + if err != nil { + if _, ok := err.(*validator.InvalidValidationError); ok { + fd.AddError("form-error", "Error de validación de datos.") + return errors, err + } + + for _, err := range err.(validator.ValidationErrors) { + fieldName, _ := trans.T(err.Field()) + message := strings.Replace(err.Translate(trans), err.Field(), fieldName, -1) + + validationErrors = append(validationErrors, ValidationError{ + Field: strings.ToLower(err.Field()), + Reason: correctMessage(message), + }) + } + + for _, err := range validationErrors { + errors[err.Field] = err.Reason + } + + if len(errors) > 0 { + fd.Errors = errors + fd.HasErrors = true + } + + return errors, err + } + + return errors, nil +} + +func correctMessage(s string) string { + s = strings.TrimSpace(s) + runes := []rune(s) + runes[0] = []rune(strings.ToUpper(string(runes[0])))[0] + if runes[len(runes)-1] != '.' { + runes = append(runes, '.') + } + + return string(runes) +} diff --git a/functions.go b/functions.go new file mode 100644 index 0000000..d00def8 --- /dev/null +++ b/functions.go @@ -0,0 +1,71 @@ +package gorender + +import ( + "bufio" + "fmt" + "os" + "strings" +) + +func or(a, b string) bool { + if a == "" && b == "" { + return false + } + return true +} + +// containsErrors hace una función similar a "{{ with index ... }}" con el +// añadido de que puede pasarle más de un argumento y comprobar si alguno de +// ellos está en el mapa de errores. +// +// Ejemplo: +// +// {{ if containsErrors .FormData.Errors "name" "email" }} +// {{index .FormData.Errors "name" }} +// {{index .FormData.Errors "email" }} +// {{ end }} +func containsErrors(errors map[string]string, names ...string) bool { + for _, name := range names { + if _, ok := errors[name]; ok { + return true + } + } + return false +} + +func loadTranslations(language string) map[string]string { + translations := make(map[string]string) + filePath := fmt.Sprintf("%s.translate", language) + file, err := os.Open(filePath) + if err != nil { + fmt.Println("Error opening translation file:", err) + return translations + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + parts := strings.Split(line, "=") + if len(parts) == 2 { + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + translations[key] = value + } + } + + if err := scanner.Err(); err != nil { + fmt.Println("Error reading translation file:", err) + } + + return translations +} + +func translateKey(key string) string { + translations := loadTranslations("es_ES") + translated := translations[key] + if translated != "" { + return translated + } + return key +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ccdaa6b --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module github.com/zepyrshut/gorender + +go 1.23.0 + +require ( + github.com/go-playground/locales v0.14.1 + github.com/go-playground/universal-translator v0.18.1 + github.com/go-playground/validator/v10 v10.22.0 + github.com/justinas/nosurf v1.1.1 +) + +require ( + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + golang.org/x/crypto v0.19.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d6f9aad --- /dev/null +++ b/go.sum @@ -0,0 +1,30 @@ +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/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.22.0 h1:k6HsTZ0sTnROkhS//R0O+55JgM8C4Bx7ia+JlgcnOao= +github.com/go-playground/validator/v10 v10.22.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/justinas/nosurf v1.1.1 h1:92Aw44hjSK4MxJeMSyDa7jwuI9GR2J/JCQiaKvXXSlk= +github.com/justinas/nosurf v1.1.1/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +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/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +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/pages.go b/pages.go new file mode 100644 index 0000000..3f58218 --- /dev/null +++ b/pages.go @@ -0,0 +1,130 @@ +package gorender + +// Pages contiene la información de paginación. +type Pages struct { + // totalElements son la cantidad de elementos totales a paginar. Pueden ser + // total de filas o total de páginas de blog. + totalElements int + // showElements muestra la cantidad máxima de elementos a mostrar en una + // página. + showElements int + // currentPage es la página actual, utilizado como ayuda para mostrar la + // página activa. + currentPage int +} + +// Page contiene la información de una página. +type Page struct { + // number es el número de página. + number int + // active es un dato lógico que indica si la página es la actual. + active bool +} + +// NewPages crea un nuevo objeto para paginación. +func NewPages(totalElements, showElements, currentPage int) Pages { + if showElements <= 0 { + showElements = 1 + } + if currentPage <= 0 { + currentPage = 1 + } + p := Pages{totalElements, showElements, currentPage} + if p.currentPage > p.TotalPages() { + p.currentPage = p.TotalPages() + } + + return p +} + +// Limit devuelve la cantidad de elementos máximos a mostrar por página. +func (p *Pages) Limit() int { + return p.showElements +} + +// TotalPages devuelve la cantidad total de páginas. +func (p *Pages) TotalPages() int { + return (p.totalElements + p.showElements - 1) / p.showElements +} + +// IsFirst indica si la página actual es la primera. +func (p *Pages) IsFirst() bool { + return p.currentPage == 1 +} + +// IsLast indica si la página actual es la última. +func (p *Pages) IsLast() bool { + return p.currentPage == p.TotalPages() +} + +// HasPrevious indica si hay una página anterior. +func (p *Pages) HasPrevious() bool { + return p.currentPage > 1 +} + +// HasNext indica si hay una página siguiente. +func (p *Pages) HasNext() bool { + return p.currentPage < p.TotalPages() +} + +// Previous devuelve el número de la página anterior. +func (p *Pages) Previous() int { + return p.currentPage - 1 +} + +// Next devuelve el número de la página siguiente. +func (p *Pages) Next() int { + return p.currentPage + 1 +} + +func (p *Page) NumberOfPage() int { + return p.number +} + +// IsActive indica si la página es la actual. +func (p *Page) IsActive() bool { + return p.active +} + +// Pages devuelve un arreglo de páginas para mostrar en la paginación. El +// parametro pagesShow indica la cantidad de páginas a mostrar, asignable desde +// la plantilla. +func (p *Pages) Pages(pagesShow int) []*Page { + var pages []*Page + startPage := p.currentPage - (pagesShow / 2) + endPage := p.currentPage + (pagesShow/2 - 1) + + if startPage < 1 { + startPage = 1 + endPage = pagesShow + } + + if endPage > p.TotalPages() { + endPage = p.TotalPages() + startPage = p.TotalPages() - pagesShow + 1 + if startPage < 1 { + startPage = 1 + } + } + + for i := startPage; i <= endPage; i++ { + pages = append(pages, &Page{i, i == p.currentPage}) + } + return pages +} + +func PaginateArray[T any](items []T, currentPage, itemsPerPage int) []T { + totalItems := len(items) + + startIndex := (currentPage - 1) * itemsPerPage + endIndex := startIndex + itemsPerPage + + if startIndex > totalItems { + startIndex = totalItems + } + if endIndex > totalItems { + endIndex = totalItems + } + + return items[startIndex:endIndex] +} diff --git a/render.go b/render.go new file mode 100644 index 0000000..1213ecc --- /dev/null +++ b/render.go @@ -0,0 +1,168 @@ +package gorender + +import ( + "bytes" + "errors" + "fmt" + "html/template" + "io/fs" + "log" + "log/slog" + "net/http" + "path/filepath" + + "github.com/justinas/nosurf" +) + +type TemplateCache map[string]*template.Template + +type Render struct { + EnableCache bool + // TemplatesPath es la ruta donde se encuentran las plantillas de la + // aplicación, pueden ser bases, fragmentos o ambos. Lo que quieras. + TemplatesPath string + // PageTemplatesPath es la ruta donde se encuentran las plantillas de las + // páginas de la aplicación. Estas son las que van a ser llamadas para + // mostrar en pantalla. + PageTemplatesPath string + TemplateCache TemplateCache + Functions template.FuncMap +} + +type OptionFunc func(*Render) + +type TemplateData struct { + Data map[string]interface{} + // FeedbackData tiene como función mostrar los mensajes habituales de + // información, advertencia, éxito y error. No va implícitamente relacionado + // con los errores de validación de formularios pero pueden ser usados para + // ello. + FeedbackData map[string]string + // FormData es una estructura que contiene los errores de validación de los + // formularios además de los valores que se han introducido en los campos. + FormData FormData + CSRFToken string + Page Pages +} + +func WithRenderOptions(opts *Render) OptionFunc { + return func(re *Render) { + re.TemplatesPath = opts.TemplatesPath + re.PageTemplatesPath = opts.PageTemplatesPath + + if opts.Functions != nil { + for k, v := range opts.Functions { + re.Functions[k] = v + } + } + + if opts.EnableCache { + re.EnableCache = opts.EnableCache + re.TemplateCache, _ = re.CreateTemplateCache() + } + } +} + +func New(opts ...OptionFunc) *Render { + functions := template.FuncMap{ + "translateKey": translateKey, + "or": or, + "containsErrors": containsErrors, + } + + config := &Render{ + EnableCache: false, + TemplatesPath: "templates", + PageTemplatesPath: "templates/pages", + TemplateCache: TemplateCache{}, + Functions: functions, + } + + return config.Apply(opts...) +} + +func (re *Render) Apply(opts ...OptionFunc) *Render { + for _, opt := range opts { + opt(re) + } + + return re +} + +func AddDefaultData(td *TemplateData, r *http.Request) *TemplateData { + td.CSRFToken = nosurf.Token(r) + return td +} + +func (re *Render) Template(w http.ResponseWriter, r *http.Request, tmpl string, td *TemplateData) error { + var tc TemplateCache + var err error + + if re.EnableCache { + tc = re.TemplateCache + } else { + tc, err = re.CreateTemplateCache() + if err != nil { + log.Println("error creating template cache:", err) + return err + } + } + + t, ok := tc[tmpl] + if !ok { + return errors.New("can't get template from cache") + } + + buf := new(bytes.Buffer) + td = AddDefaultData(td, r) + err = t.Execute(buf, td) + if err != nil { + log.Println("error executing template:", err) + return err + } + + _, err = buf.WriteTo(w) + if err != nil { + log.Println("error writing template to browser:", err) + } + + return nil +} + +func (re *Render) CreateTemplateCache() (TemplateCache, error) { + myCache := TemplateCache{} + + pagesTemplates, err := filepath.Glob(fmt.Sprintf("%s/*.html", re.PageTemplatesPath)) + if err != nil { + return myCache, err + } + + files := []string{} + filepath.WalkDir(re.TemplatesPath, func(path string, file fs.DirEntry, err error) error { + if err != nil { + return err + } + if !file.IsDir() { + slog.Info("file found", "path", path) + files = append(files, path) + } + + return nil + }) + + for function, _ := range re.Functions { + slog.Info("function found", "function", function) + } + + for _, file := range pagesTemplates { + name := filepath.Base(file) + ts, err := template.New(name).Funcs(re.Functions).ParseFiles(append(files, file)...) + if err != nil { + return myCache, err + } + + myCache[name] = ts + } + + return myCache, nil +} From f2fd887f9b1d42361cfab51c5fc2ac51e14fa958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20P=C3=A9rez?= Date: Wed, 21 Aug 2024 22:42:46 +0200 Subject: [PATCH 2/6] =?UTF-8?q?docs:=20:page=5Ffacing=5Fup:=20a=C3=B1adida?= =?UTF-8?q?=20licencia=20MIT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8ce4906 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Pedro Pérez + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file From 53362dcafd1cb0925394b89f7b133089ecc68d9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20P=C3=A9rez?= Date: Wed, 21 Aug 2024 22:56:51 +0200 Subject: [PATCH 3/6] =?UTF-8?q?fix:=20:recycle:=20createTemplateCache=20pa?= =?UTF-8?q?sa=20a=20ser=20un=20m=C3=A9todo=20privado?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 ++- render.go | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 275c391..43017bb 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ estándar de Go `html/template`. - Procesamiento de plantillas utilizando `html/template`. - Soporte para caché de plantillas. +- Soporte para paginación de elementos como tablas o múltiples blogs. - Posibilidad de añadir funciones personalizadas a las plantillas. - Configuración sencilla con opciones por defecto que se pueden sobreescribir. Inspirado en `Gin`. @@ -92,7 +93,7 @@ func main() { ## Descargo de responsabilidad Esta librería fue creada para usar las plantillas en mis proyectos privados, es -posible que también solucione su problema. Sin embargo, no ofrezco ninguna +posible que también solucione tu problema. Sin embargo, no ofrezco ninguna garantía de que funcione para todos los casos de uso, tenga el máximo rendimiento o esté libre de errores. diff --git a/render.go b/render.go index 1213ecc..6bd394c 100644 --- a/render.go +++ b/render.go @@ -58,7 +58,7 @@ func WithRenderOptions(opts *Render) OptionFunc { if opts.EnableCache { re.EnableCache = opts.EnableCache - re.TemplateCache, _ = re.CreateTemplateCache() + re.TemplateCache, _ = re.createTemplateCache() } } } @@ -101,7 +101,7 @@ func (re *Render) Template(w http.ResponseWriter, r *http.Request, tmpl string, if re.EnableCache { tc = re.TemplateCache } else { - tc, err = re.CreateTemplateCache() + tc, err = re.createTemplateCache() if err != nil { log.Println("error creating template cache:", err) return err @@ -129,7 +129,7 @@ func (re *Render) Template(w http.ResponseWriter, r *http.Request, tmpl string, return nil } -func (re *Render) CreateTemplateCache() (TemplateCache, error) { +func (re *Render) createTemplateCache() (TemplateCache, error) { myCache := TemplateCache{} pagesTemplates, err := filepath.Glob(fmt.Sprintf("%s/*.html", re.PageTemplatesPath)) From f902581a4a0eea8bbe3bbfc987c8919489c9e2db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20P=C3=A9rez?= Date: Thu, 22 Aug 2024 20:44:17 +0200 Subject: [PATCH 4/6] =?UTF-8?q?refactor:=20:recycle:=20uso=20de=20slog=20y?= =?UTF-8?q?=20paso=20de=20funciones=20p=C3=BAblica=20a=20privadas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- render.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/render.go b/render.go index 6bd394c..f452270 100644 --- a/render.go +++ b/render.go @@ -6,7 +6,6 @@ import ( "fmt" "html/template" "io/fs" - "log" "log/slog" "net/http" "path/filepath" @@ -78,10 +77,10 @@ func New(opts ...OptionFunc) *Render { Functions: functions, } - return config.Apply(opts...) + return config.apply(opts...) } -func (re *Render) Apply(opts ...OptionFunc) *Render { +func (re *Render) apply(opts ...OptionFunc) *Render { for _, opt := range opts { opt(re) } @@ -89,7 +88,7 @@ func (re *Render) Apply(opts ...OptionFunc) *Render { return re } -func AddDefaultData(td *TemplateData, r *http.Request) *TemplateData { +func addDefaultData(td *TemplateData, r *http.Request) *TemplateData { td.CSRFToken = nosurf.Token(r) return td } @@ -103,7 +102,7 @@ func (re *Render) Template(w http.ResponseWriter, r *http.Request, tmpl string, } else { tc, err = re.createTemplateCache() if err != nil { - log.Println("error creating template cache:", err) + slog.Error("error creating template cache:", "error", err) return err } } @@ -114,16 +113,16 @@ func (re *Render) Template(w http.ResponseWriter, r *http.Request, tmpl string, } buf := new(bytes.Buffer) - td = AddDefaultData(td, r) + td = addDefaultData(td, r) err = t.Execute(buf, td) if err != nil { - log.Println("error executing template:", err) + slog.Error("error executing template:", "error", err) return err } _, err = buf.WriteTo(w) if err != nil { - log.Println("error writing template to browser:", err) + slog.Error("error writing template to browser:", "error", err) } return nil @@ -150,7 +149,7 @@ func (re *Render) createTemplateCache() (TemplateCache, error) { return nil }) - for function, _ := range re.Functions { + for function := range re.Functions { slog.Info("function found", "function", function) } From be1dc3ceff6dff44e1f7b2a0afdc529be5cd93b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20P=C3=A9rez?= Date: Thu, 22 Aug 2024 23:46:47 +0200 Subject: [PATCH 5/6] =?UTF-8?q?feat:=20:sparkles:=20a=C3=B1adido=20funci?= =?UTF-8?q?=C3=B3n=20de=20par=C3=A1metros=20de=20paginaci=C3=B3n=20y=20dat?= =?UTF-8?q?os=20de=20sesi=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pages.go | 23 +++++++++++++++++++++++ render.go | 2 ++ 2 files changed, 25 insertions(+) diff --git a/pages.go b/pages.go index 3f58218..555a5c6 100644 --- a/pages.go +++ b/pages.go @@ -1,5 +1,10 @@ package gorender +import ( + "net/http" + "strconv" +) + // Pages contiene la información de paginación. type Pages struct { // totalElements son la cantidad de elementos totales a paginar. Pueden ser @@ -128,3 +133,21 @@ func PaginateArray[T any](items []T, currentPage, itemsPerPage int) []T { return items[startIndex:endIndex] } + +func PaginationParams(r *http.Request) (int, int, int) { + limit := r.FormValue("limit") + if limit == "" { + limit = "50" + } + page := r.FormValue("page") + if page == "" || page == "0" { + page = "1" + } + + limitInt, _ := strconv.Atoi(limit) + pageInt, _ := strconv.Atoi(page) + offset := (pageInt - 1) * limitInt + actualPage := offset/limitInt + 1 + + return limitInt, offset, actualPage +} diff --git a/render.go b/render.go index f452270..a43a44b 100644 --- a/render.go +++ b/render.go @@ -32,6 +32,8 @@ type OptionFunc func(*Render) type TemplateData struct { Data map[string]interface{} + // SessionData contiene los datos de la sesión del usuario. + SessionData interface{} // FeedbackData tiene como función mostrar los mensajes habituales de // información, advertencia, éxito y error. No va implícitamente relacionado // con los errores de validación de formularios pero pueden ser usados para From 93dc58253bd28c980eb4086b47bd5f35f35283f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20P=C3=A9rez?= Date: Wed, 28 Aug 2024 20:49:45 +0200 Subject: [PATCH 6/6] =?UTF-8?q?feat:=20:sparkles:=20b=C3=BAsqueda=20de=20t?= =?UTF-8?q?odas=20las=20p=C3=A1ginas=20y=20fragmentos=20en=20todos=20los?= =?UTF-8?q?=20niveles=20de=20profundidad?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- render.go | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/render.go b/render.go index a43a44b..9fb9416 100644 --- a/render.go +++ b/render.go @@ -3,7 +3,6 @@ package gorender import ( "bytes" "errors" - "fmt" "html/template" "io/fs" "log/slog" @@ -130,27 +129,41 @@ func (re *Render) Template(w http.ResponseWriter, r *http.Request, tmpl string, return nil } -func (re *Render) createTemplateCache() (TemplateCache, error) { - myCache := TemplateCache{} +func findHTMLFiles(root string) ([]string, error) { + var files []string - pagesTemplates, err := filepath.Glob(fmt.Sprintf("%s/*.html", re.PageTemplatesPath)) - if err != nil { - return myCache, err - } - - files := []string{} - filepath.WalkDir(re.TemplatesPath, func(path string, file fs.DirEntry, err error) error { + err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { if err != nil { return err } - if !file.IsDir() { - slog.Info("file found", "path", path) + + if !d.IsDir() && filepath.Ext(path) == ".html" { files = append(files, path) } return nil }) + if err != nil { + return nil, err + } + + return files, nil +} + +func (re *Render) createTemplateCache() (TemplateCache, error) { + myCache := TemplateCache{} + + pagesTemplates, err := findHTMLFiles(re.PageTemplatesPath) + if err != nil { + return myCache, err + } + + files, err := findHTMLFiles(re.TemplatesPath) + if err != nil { + return myCache, err + } + for function := range re.Functions { slog.Info("function found", "function", function) }