solving conflicts

This commit is contained in:
Pedro Pérez 2024-10-17 20:58:06 +02:00
commit 254b2ce0f3
11 changed files with 793 additions and 0 deletions

4
.gitignore vendored
View File

@ -1 +1,5 @@
<<<<<<< HEAD
.idea/
=======
.idea/
>>>>>>> excel2struct/main

BIN
Book1.xlsx Normal file

Binary file not shown.

36
README.md Normal file
View File

@ -0,0 +1,36 @@
# excel2struct
Convierte una hoja de excel compatible con la librería [Excelize](https://github.com/qax-os/excelize) a un tipo estructurado de Go. La primera fila debe coincidir con la etiqueta XLSX, sensible a las mayúsculas.
| Id | Nombre | Apellidos | Email | Género | Balance |
|----|--------|-----------|--------------------------|--------|---------|
| 1 | Caryl | Kimbrough | ckimbrough0@fotki.com | true | 571.08 |
| 2 | Robin | Bozward | rbozward1@thetimes.co.uk | true | 2162.89 |
| 3 | Tabbie | Kaygill | tkaygill2@is.gd | false | 703.94 |
```go
type User struct {
Id int `xlsx:"Id"`
Name string `xlsx:"Nombre"`
LastName string `xlsx:"Apellidos"`
Email string `xlsx:"Email"`
Gender bool `xlsx:"Género"`
Balance float32 `xlsx:"Balance"`
}
```
```go
func main() {
data := exceltostruct.Convert[User]("Book1.xlsx", "Sheet1")
fmt.Println(data)
}
```
```bash
[{1 Caryl Kimbrough ckimbrough0@fotki.com true 571.08} {2 Robin Bozward rbozward1@thetimes.co.uk true 2162.89} {3 Tabbie Kaygill tkaygill2@is.gd false 703.94}]
```
Donde el primer parámetro es la ruta donde está ubicada la hoja de cálculo y la segunda el nombre de la hoja.
Tipos compatibles: **int**, **float32**, **bool** y **string**.

63
exceltostruct.go Normal file
View File

@ -0,0 +1,63 @@
package exceltostruct
import (
"github.com/xuri/excelize/v2"
"reflect"
"strconv"
)
func Convert[T any](bookPath, sheetName string) (dataExcel []T) {
f, _ := excelize.OpenFile(bookPath)
rows, _ := f.GetRows(sheetName)
firstRow := map[string]int{}
for i, row := range rows[0] {
firstRow[row] = i
}
t := new(T)
dataExcel = make([]T, 0, len(rows)-1)
for _, row := range rows[1:] {
v := reflect.ValueOf(t)
if v.Kind() == reflect.Pointer {
v = v.Elem()
}
for i := 0; i < v.NumField(); i++ {
tag := v.Type().Field(i).Tag.Get("xlsx")
objType := v.Field(i).Type().String()
if j, ok := firstRow[tag]; ok {
field := v.Field(i)
if len(row) > j {
d := row[j]
elementConverted := convertType(objType, d)
field.Set(reflect.ValueOf(elementConverted))
}
}
}
dataExcel = append(dataExcel, *t)
}
return dataExcel
}
func convertType(objType string, value string) any {
switch objType {
case "int":
valueInt, _ := strconv.Atoi(value)
return valueInt
case "bool":
valueBool, _ := strconv.ParseBool(value)
return valueBool
case "float32":
valueFloat, _ := strconv.ParseFloat(value, 32)
return float32(valueFloat)
case "string":
return value
}
return value
}

17
go.mod Normal file
View File

@ -0,0 +1,17 @@
module github.com/zepyrshut/excel2struct
go 1.20
require github.com/xuri/excelize/v2 v2.7.0
require (
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/richardlehane/mscfb v1.0.4 // indirect
github.com/richardlehane/msoleps v1.0.3 // indirect
github.com/stretchr/testify v1.8.2 // indirect
github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 // indirect
github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 // indirect
golang.org/x/crypto v0.7.0 // indirect
golang.org/x/net v0.8.0 // indirect
golang.org/x/text v0.8.0 // indirect
)

65
go.sum Normal file
View File

@ -0,0 +1,65 @@
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/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
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/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM=
github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk=
github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
github.com/richardlehane/msoleps v1.0.3 h1:aznSZzrwYRl3rLKRT3gUk9am7T/mLNSnJINvN0AQoVM=
github.com/richardlehane/msoleps v1.0.3/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg=
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/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 h1:6932x8ltq1w4utjmfMPVj09jdMlkY0aiA6+Skbtl3/c=
github.com/xuri/efp v0.0.0-20220603152613-6918739fd470/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI=
github.com/xuri/excelize/v2 v2.7.0 h1:Hri/czwyRCW6f6zrCDWXcXKshlq4xAZNpNOpdfnFhEw=
github.com/xuri/excelize/v2 v2.7.0/go.mod h1:ebKlRoS+rGyLMyUx3ErBECXs/HNYqyj+PbkkKRK5vSI=
github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22 h1:OAmKAfT06//esDdpi/DZ8Qsdt4+M5+ltca05dA5bG2M=
github.com/xuri/nfp v0.0.0-20220409054826-5e722a1d9e22/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY=
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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=

107
gorender/README.md Normal file
View File

@ -0,0 +1,107 @@
# 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.

95
gorender/form.go Normal file
View File

@ -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)
}

71
gorender/functions.go Normal file
View File

@ -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
}

153
gorender/pages.go Normal file
View File

@ -0,0 +1,153 @@
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
}

182
gorender/render.go Normal file
View File

@ -0,0 +1,182 @@
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
}