detached cd54f11

yaml parser and languages WIP

commit
cd54f111ea1e71e7751233b588477a9fc2ad54bd
author
bnema · · 2023-09-02 05:12 UTC
parent
df8fa86
signature
unsigned
7 files · +181 -42
M cmd/main.go +89 -29
@@ -3,57 +3,60 @@ package main
33 import (
44 "fmt"
55 "html/template"
6+ "io/fs"
67 "net/http"
7- "strings"
8+ "os"
89
910 "github.com/labstack/echo/v4"
11+ "github.com/labstack/echo/v4/middleware"
12+ "github.com/rs/zerolog"
1013 "gogs.bnema.dev/gordon-echo/internal/ui"
1114 "gogs.bnema.dev/gordon-echo/pkg/utils"
1215 )
1316
14-const trailedAdminPath = "/admin/"
17+// getRenderer returns a new renderer for the given template
18+func getRenderer(filename string, fs fs.FS) (*utils.Renderer, error) {
19+ tmpl, err := template.New(filename).ParseFS(fs, filename)
20+ if err != nil {
21+ return nil, err
22+ }
23+ return &utils.Renderer{
24+ Template: tmpl,
25+ }, nil
26+}
1527
1628 // bindStaticAdminUI serves the static admin UI
1729 func bindStaticAdminUI(e *echo.Echo) error {
18- tmpl, err := template.New("index").ParseFS(ui.PublicFS, "index.html")
30+ // Load static website data from YAML
31+ staticData, err := utils.LoadDataFromYAML()
1932 if err != nil {
20- return fmt.Errorf("failed to parse index.html: %w", err)
33+ return err
2134 }
2235
23- renderer := &utils.Renderer{
24- Template: tmpl,
25- ParseError: err,
26- }
36+ // Main handler for /admin
37+ e.GET("/admin/:lang", func(c echo.Context) error {
38+ lang := c.Param("lang")
2739
28- // redirect to trailing slash to ensure that relative urls will still work properly
29- e.GET(
30- strings.TrimRight(trailedAdminPath, "/"),
31- func(c echo.Context) error {
32- return c.Redirect(http.StatusTemporaryRedirect, strings.TrimLeft(trailedAdminPath, "/"))
33- },
34- )
35-
36- // Main handler for /admin/
37- e.GET(trailedAdminPath, func(c echo.Context) error {
38- data := map[string]interface{}{
39- "website": map[string]interface{}{
40- "title": "Hello, World!",
41- },
40+ switch lang {
41+ case "fr":
42+ staticData.CurrentLang = staticData.FR
43+ default: // default to English if no match
44+ staticData.CurrentLang = staticData.EN
45+ }
46+
47+ renderer, err := getRenderer("index.html", ui.PublicFS)
48+ if err != nil {
49+ return err
4250 }
4351
44- html, err := renderer.Render(data)
52+ html, err := renderer.Render(staticData)
4553 if err != nil {
4654 return c.String(http.StatusInternalServerError, fmt.Sprintf("Failed to render template: %v", err))
4755 }
4856
4957 return c.HTML(http.StatusOK, html)
5058 })
51- // serves static files from the /ui/dist directory
52- // (similar to echo.StaticFS but with gzip middleware enabled)
53- e.GET(
54- trailedAdminPath+"*",
55- echo.StaticDirectoryHandler(ui.PublicFS, false),
56- )
59+
5760 e.GET("/htmx", func(c echo.Context) error {
5861 fragment := c.Request().Header.Get("X-Fragment")
5962
@@ -70,14 +73,71 @@ func bindStaticAdminUI(e *echo.Echo) error {
7073 // Handle other fragments or default behavior
7174 return c.String(http.StatusBadRequest, "Invalid fragment")
7275 })
76+
77+ // !!! IMPORTANT !!! This must be the last route (except for the 404 route)
78+ e.GET("/*", func(c echo.Context) error {
79+ // Set cache headers
80+ c.Response().Header().Set("Cache-Control", "public, max-age=86400")
81+
82+ // Proceed with serving the file
83+ return echo.StaticDirectoryHandler(ui.PublicFS, false)(c)
84+ })
85+
86+ // // 404 handler
87+ // e.HTTPErrorHandler = func(err error, c echo.Context) {
88+ // if he, ok := err.(*echo.HTTPError); ok {
89+ // if he.Code == http.StatusNotFound {
90+ // // 404.html
91+ // renderer, err := getRenderer("404.html", ui.TemplateFS)
92+ // if err != nil {
93+ // c.String(http.StatusInternalServerError, fmt.Sprintf("Failed to render template: %v", err))
94+ // }
95+
96+ // data := map[string]interface{}{
97+ // "website": map[string]interface{}{
98+ // "title": "Page Not Found",
99+ // },
100+ // }
101+
102+ // html, err := renderer.Render(data)
103+ // if err != nil {
104+ // c.String(http.StatusInternalServerError, fmt.Sprintf("Failed to render template: %v", err))
105+ // }
106+
107+ // c.HTML(http.StatusNotFound, html)
108+ // return
109+ // }
110+ // }
111+
112+ // e.DefaultHTTPErrorHandler(err, c)
113+ // }
114+
73115 return nil
116+
74117 }
75118
76119 func main() {
120+ logger := zerolog.New(os.Stdout)
77121 e := echo.New()
78122
123+ e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
124+ LogURI: true,
125+ LogStatus: true,
126+ LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
127+ logger.Info().
128+ Str("URI", v.URI).
129+ Int("status", v.Status).
130+ Msg("request")
131+
132+ return nil
133+ },
134+ }))
135+
79136 // use func bindStaticAdminUI to serve the static admin UI
80137 bindStaticAdminUI(e)
138+ if err := bindStaticAdminUI(e); err != nil {
139+ e.Logger.Fatal(err)
140+ }
81141
82142 e.Logger.Fatal(e.Start(":1323"))
83143
@@ -3,57 +3,60 @@ package main
3 import (3 import (
4 "fmt"4 "fmt"
5 "html/template"5 "html/template"
6+ "io/fs"
6 "net/http"7 "net/http"
7- "strings"8+ "os"
8 9
9 "github.com/labstack/echo/v4"10 "github.com/labstack/echo/v4"
11+ "github.com/labstack/echo/v4/middleware"
12+ "github.com/rs/zerolog"
10 "gogs.bnema.dev/gordon-echo/internal/ui"13 "gogs.bnema.dev/gordon-echo/internal/ui"
11 "gogs.bnema.dev/gordon-echo/pkg/utils"14 "gogs.bnema.dev/gordon-echo/pkg/utils"
12 )15 )
13 16
14-const trailedAdminPath = "/admin/"17+// getRenderer returns a new renderer for the given template
18+func getRenderer(filename string, fs fs.FS) (*utils.Renderer, error) {
19+ tmpl, err := template.New(filename).ParseFS(fs, filename)
20+ if err != nil {
21+ return nil, err
22+ }
23+ return &utils.Renderer{
24+ Template: tmpl,
25+ }, nil
26+}
15 27
16 // bindStaticAdminUI serves the static admin UI28 // bindStaticAdminUI serves the static admin UI
17 func bindStaticAdminUI(e *echo.Echo) error {29 func bindStaticAdminUI(e *echo.Echo) error {
18- tmpl, err := template.New("index").ParseFS(ui.PublicFS, "index.html")30+ // Load static website data from YAML
31+ staticData, err := utils.LoadDataFromYAML()
19 if err != nil {32 if err != nil {
20- return fmt.Errorf("failed to parse index.html: %w", err)33+ return err
21 }34 }
22 35
23- renderer := &utils.Renderer{36+ // Main handler for /admin
24- Template: tmpl,37+ e.GET("/admin/:lang", func(c echo.Context) error {
25- ParseError: err,38+ lang := c.Param("lang")
26- }
27 39
28- // redirect to trailing slash to ensure that relative urls will still work properly40+ switch lang {
29- e.GET(41+ case "fr":
30- strings.TrimRight(trailedAdminPath, "/"),42+ staticData.CurrentLang = staticData.FR
31- func(c echo.Context) error {43+ default: // default to English if no match
32- return c.Redirect(http.StatusTemporaryRedirect, strings.TrimLeft(trailedAdminPath, "/"))44+ staticData.CurrentLang = staticData.EN
33- },45+ }
34- )46+
35-47+ renderer, err := getRenderer("index.html", ui.PublicFS)
36- // Main handler for /admin/48+ if err != nil {
37- e.GET(trailedAdminPath, func(c echo.Context) error {49+ return err
38- data := map[string]interface{}{
39- "website": map[string]interface{}{
40- "title": "Hello, World!",
41- },
42 }50 }
43 51
44- html, err := renderer.Render(data)52+ html, err := renderer.Render(staticData)
45 if err != nil {53 if err != nil {
46 return c.String(http.StatusInternalServerError, fmt.Sprintf("Failed to render template: %v", err))54 return c.String(http.StatusInternalServerError, fmt.Sprintf("Failed to render template: %v", err))
47 }55 }
48 56
49 return c.HTML(http.StatusOK, html)57 return c.HTML(http.StatusOK, html)
50 })58 })
51- // serves static files from the /ui/dist directory59+
52- // (similar to echo.StaticFS but with gzip middleware enabled)
53- e.GET(
54- trailedAdminPath+"*",
55- echo.StaticDirectoryHandler(ui.PublicFS, false),
56- )
57 e.GET("/htmx", func(c echo.Context) error {60 e.GET("/htmx", func(c echo.Context) error {
58 fragment := c.Request().Header.Get("X-Fragment")61 fragment := c.Request().Header.Get("X-Fragment")
59 62
@@ -70,14 +73,71 @@ func bindStaticAdminUI(e *echo.Echo) error {
70 // Handle other fragments or default behavior73 // Handle other fragments or default behavior
71 return c.String(http.StatusBadRequest, "Invalid fragment")74 return c.String(http.StatusBadRequest, "Invalid fragment")
72 })75 })
76+
77+ // !!! IMPORTANT !!! This must be the last route (except for the 404 route)
78+ e.GET("/*", func(c echo.Context) error {
79+ // Set cache headers
80+ c.Response().Header().Set("Cache-Control", "public, max-age=86400")
81+
82+ // Proceed with serving the file
83+ return echo.StaticDirectoryHandler(ui.PublicFS, false)(c)
84+ })
85+
86+ // // 404 handler
87+ // e.HTTPErrorHandler = func(err error, c echo.Context) {
88+ // if he, ok := err.(*echo.HTTPError); ok {
89+ // if he.Code == http.StatusNotFound {
90+ // // 404.html
91+ // renderer, err := getRenderer("404.html", ui.TemplateFS)
92+ // if err != nil {
93+ // c.String(http.StatusInternalServerError, fmt.Sprintf("Failed to render template: %v", err))
94+ // }
95+
96+ // data := map[string]interface{}{
97+ // "website": map[string]interface{}{
98+ // "title": "Page Not Found",
99+ // },
100+ // }
101+
102+ // html, err := renderer.Render(data)
103+ // if err != nil {
104+ // c.String(http.StatusInternalServerError, fmt.Sprintf("Failed to render template: %v", err))
105+ // }
106+
107+ // c.HTML(http.StatusNotFound, html)
108+ // return
109+ // }
110+ // }
111+
112+ // e.DefaultHTTPErrorHandler(err, c)
113+ // }
114+
73 return nil115 return nil
116+
74 }117 }
75 118
76 func main() {119 func main() {
120+ logger := zerolog.New(os.Stdout)
77 e := echo.New()121 e := echo.New()
78 122
123+ e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
124+ LogURI: true,
125+ LogStatus: true,
126+ LogValuesFunc: func(c echo.Context, v middleware.RequestLoggerValues) error {
127+ logger.Info().
128+ Str("URI", v.URI).
129+ Int("status", v.Status).
130+ Msg("request")
131+
132+ return nil
133+ },
134+ }))
135+
79 // use func bindStaticAdminUI to serve the static admin UI136 // use func bindStaticAdminUI to serve the static admin UI
80 bindStaticAdminUI(e)137 bindStaticAdminUI(e)
138+ if err := bindStaticAdminUI(e); err != nil {
139+ e.Logger.Fatal(err)
140+ }
81 141
82 e.Logger.Fatal(e.Start(":1323"))142 e.Logger.Fatal(e.Start(":1323"))
83 143
M go.mod +10 -4
@@ -2,19 +2,25 @@ module gogs.bnema.dev/gordon-echo
22
33 go 1.21.0
44
5-require github.com/labstack/echo/v4 v4.11.1
5+require (
6+ github.com/PuerkitoBio/goquery v1.8.1
7+ github.com/labstack/echo/v4 v4.11.1
8+)
69
710 require (
8- github.com/PuerkitoBio/goquery v1.8.1 // indirect
911 github.com/andybalholm/cascadia v1.3.1 // indirect
10- github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect
12+ github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
13+ github.com/google/safehtml v0.1.0 // indirect
14+ golang.org/x/time v0.3.0 // indirect
15+ gopkg.in/yaml.v3 v3.0.1 // indirect
1116 )
1217
1318 require (
14- github.com/labstack/echo v3.3.10+incompatible
19+ github.com/go-chi/chi v1.5.4
1520 github.com/labstack/gommon v0.4.0 // indirect
1621 github.com/mattn/go-colorable v0.1.13 // indirect
1722 github.com/mattn/go-isatty v0.0.19 // indirect
23+ github.com/rs/zerolog v1.30.0
1824 github.com/valyala/bytebufferpool v1.0.0 // indirect
1925 github.com/valyala/fasttemplate v1.2.2 // indirect
2026 golang.org/x/crypto v0.11.0 // indirect
@@ -2,19 +2,25 @@ module gogs.bnema.dev/gordon-echo
2 2
3 go 1.21.03 go 1.21.0
4 4
5-require github.com/labstack/echo/v4 v4.11.15+require (
6+ github.com/PuerkitoBio/goquery v1.8.1
7+ github.com/labstack/echo/v4 v4.11.1
8+)
6 9
7 require (10 require (
8- github.com/PuerkitoBio/goquery v1.8.1 // indirect
9 github.com/andybalholm/cascadia v1.3.1 // indirect11 github.com/andybalholm/cascadia v1.3.1 // indirect
10- github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect12+ github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
13+ github.com/google/safehtml v0.1.0 // indirect
14+ golang.org/x/time v0.3.0 // indirect
15+ gopkg.in/yaml.v3 v3.0.1 // indirect
11 )16 )
12 17
13 require (18 require (
14- github.com/labstack/echo v3.3.10+incompatible19+ github.com/go-chi/chi v1.5.4
15 github.com/labstack/gommon v0.4.0 // indirect20 github.com/labstack/gommon v0.4.0 // indirect
16 github.com/mattn/go-colorable v0.1.13 // indirect21 github.com/mattn/go-colorable v0.1.13 // indirect
17 github.com/mattn/go-isatty v0.0.19 // indirect22 github.com/mattn/go-isatty v0.0.19 // indirect
23+ github.com/rs/zerolog v1.30.0
18 github.com/valyala/bytebufferpool v1.0.0 // indirect24 github.com/valyala/bytebufferpool v1.0.0 // indirect
19 github.com/valyala/fasttemplate v1.2.2 // indirect25 github.com/valyala/fasttemplate v1.2.2 // indirect
20 golang.org/x/crypto v0.11.0 // indirect26 golang.org/x/crypto v0.11.0 // indirect
M go.sum +21 -4 generated file: collapsed
A internal/ui/public/assets/imgs/gordon-mascot.png +0 -0 binary file: no text diff

binary file: no text diff

A internal/ui/templates/strings.yaml +9 -0
@@ -0,0 +1,9 @@
1+en:
2+ website:
3+ title: Admin Dashboard
4+ user: John Doe
5+
6+fr:
7+ website:
8+ title: Tableau de bord administrateur
9+ user: Jean Dupont⏎̸
@@ -0,0 +1,9 @@
1+en:
2+ website:
3+ title: Admin Dashboard
4+ user: John Doe
5+
6+fr:
7+ website:
8+ title: Tableau de bord administrateur
9+ user: Jean Dupont⏎̸
A pkg/utils/parser.go +49 -0
@@ -0,0 +1,49 @@
1+package utils
2+
3+import (
4+ "fmt"
5+ "io"
6+
7+ "gogs.bnema.dev/gordon-echo/internal/ui"
8+ "gopkg.in/yaml.v3"
9+)
10+
11+type Data struct {
12+ EN StaticData `yaml:"en"`
13+ FR StaticData `yaml:"fr"`
14+ CurrentLang StaticData `yaml:"-"`
15+ Website struct {
16+ Title string `yaml:"title"`
17+ User string `yaml:"user"`
18+ } `yaml:"website"`
19+}
20+
21+type StaticData struct {
22+ Title string `yaml:"title"`
23+ User string `yaml:"user"`
24+}
25+
26+// LoadDataFromYAML loads the data from the given YAML file
27+func LoadDataFromYAML() (Data, error) {
28+ var data Data
29+
30+ // Open the YAML file inside the ui/components directory
31+ file, err := ui.TemplateFS.Open("strings.yaml")
32+ if err != nil {
33+ return data, fmt.Errorf("failed to open strings.yaml: %w", err)
34+ }
35+ defer file.Close()
36+
37+ // Read the content of the file
38+ content, err := io.ReadAll(file)
39+ if err != nil {
40+ return data, fmt.Errorf("failed to read strings.yaml: %w", err)
41+ }
42+
43+ // Unmarshal the YAML content into the Data struct
44+ if err := yaml.Unmarshal(content, &data); err != nil {
45+ return data, fmt.Errorf("failed to unmarshal strings.yaml: %w", err)
46+ }
47+
48+ return data, nil
49+}
@@ -0,0 +1,49 @@
1+package utils
2+
3+import (
4+ "fmt"
5+ "io"
6+
7+ "gogs.bnema.dev/gordon-echo/internal/ui"
8+ "gopkg.in/yaml.v3"
9+)
10+
11+type Data struct {
12+ EN StaticData `yaml:"en"`
13+ FR StaticData `yaml:"fr"`
14+ CurrentLang StaticData `yaml:"-"`
15+ Website struct {
16+ Title string `yaml:"title"`
17+ User string `yaml:"user"`
18+ } `yaml:"website"`
19+}
20+
21+type StaticData struct {
22+ Title string `yaml:"title"`
23+ User string `yaml:"user"`
24+}
25+
26+// LoadDataFromYAML loads the data from the given YAML file
27+func LoadDataFromYAML() (Data, error) {
28+ var data Data
29+
30+ // Open the YAML file inside the ui/components directory
31+ file, err := ui.TemplateFS.Open("strings.yaml")
32+ if err != nil {
33+ return data, fmt.Errorf("failed to open strings.yaml: %w", err)
34+ }
35+ defer file.Close()
36+
37+ // Read the content of the file
38+ content, err := io.ReadAll(file)
39+ if err != nil {
40+ return data, fmt.Errorf("failed to read strings.yaml: %w", err)
41+ }
42+
43+ // Unmarshal the YAML content into the Data struct
44+ if err := yaml.Unmarshal(content, &data); err != nil {
45+ return data, fmt.Errorf("failed to unmarshal strings.yaml: %w", err)
46+ }
47+
48+ return data, nil
49+}
M pkg/utils/render.go +3 -5
@@ -11,9 +11,8 @@ type Renderer struct {
1111 ParseError error
1212 }
1313
14-// Render executes the template with the specified data as the dot object
15-// and returns the result as plain string.
16-func (r *Renderer) Render(data any) (string, error) {
14+// Render function renders the template with the given data
15+func (r *Renderer) Render(data interface{}) (string, error) {
1716 if r.ParseError != nil {
1817 return "", r.ParseError
1918 }
@@ -23,10 +22,9 @@ func (r *Renderer) Render(data any) (string, error) {
2322 }
2423
2524 buf := new(bytes.Buffer)
26-
2725 err := r.Template.Execute(buf, data)
2826 if err != nil {
29- return "", errors.New("failed to execute template: " + err.Error())
27+ return "", err
3028 }
3129
3230 return buf.String(), nil
@@ -11,9 +11,8 @@ type Renderer struct {
11 ParseError error11 ParseError error
12 }12 }
13 13
14-// Render executes the template with the specified data as the dot object14+// Render function renders the template with the given data
15-// and returns the result as plain string.15+func (r *Renderer) Render(data interface{}) (string, error) {
16-func (r *Renderer) Render(data any) (string, error) {
17 if r.ParseError != nil {16 if r.ParseError != nil {
18 return "", r.ParseError17 return "", r.ParseError
19 }18 }
@@ -23,10 +22,9 @@ func (r *Renderer) Render(data any) (string, error) {
23 }22 }
24 23
25 buf := new(bytes.Buffer)24 buf := new(bytes.Buffer)
26-
27 err := r.Template.Execute(buf, data)25 err := r.Template.Execute(buf, data)
28 if err != nil {26 if err != nil {
29- return "", errors.New("failed to execute template: " + err.Error())27+ return "", err
30 }28 }
31 29
32 return buf.String(), nil30 return buf.String(), nil

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close