diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 8ce4906..0000000 --- a/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -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 diff --git a/README.md b/README.md deleted file mode 100644 index 43017bb..0000000 --- a/README.md +++ /dev/null @@ -1,107 +0,0 @@ -# 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. -- 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`. - -## 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 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. - -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/form.go b/form.go deleted file mode 100644 index 0a2f46e..0000000 --- a/form.go +++ /dev/null @@ -1,95 +0,0 @@ -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 deleted file mode 100644 index d00def8..0000000 --- a/functions.go +++ /dev/null @@ -1,71 +0,0 @@ -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 deleted file mode 100644 index ccdaa6b..0000000 --- a/go.mod +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index d6f9aad..0000000 --- a/go.sum +++ /dev/null @@ -1,30 +0,0 @@ -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 deleted file mode 100644 index 555a5c6..0000000 --- a/pages.go +++ /dev/null @@ -1,153 +0,0 @@ -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 - // 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] -} - -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 deleted file mode 100644 index 9fb9416..0000000 --- a/render.go +++ /dev/null @@ -1,182 +0,0 @@ -package gorender - -import ( - "bytes" - "errors" - "html/template" - "io/fs" - "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{} - // 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 - // 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 { - slog.Error("error creating template cache:", "error", 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 { - slog.Error("error executing template:", "error", err) - return err - } - - _, err = buf.WriteTo(w) - if err != nil { - slog.Error("error writing template to browser:", "error", err) - } - - return nil -} - -func findHTMLFiles(root string) ([]string, error) { - var files []string - - err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - - 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) - } - - 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 -}