A webmail client. Forked from https://git.sr.ht/~migadu/alps
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

178 lines
3.9 KiB

  1. package alps
  2. import (
  3. "fmt"
  4. "html/template"
  5. "io"
  6. "io/ioutil"
  7. "os"
  8. "github.com/labstack/echo/v4"
  9. )
  10. const themesDir = "themes"
  11. // GlobalRenderData contains data available in all templates.
  12. type GlobalRenderData struct {
  13. Path string
  14. LoggedIn bool
  15. // if logged in
  16. Username string
  17. // additional plugin-specific data
  18. Extra map[string]interface{}
  19. }
  20. // BaseRenderData is the base type for templates. It should be extended with
  21. // additional template-specific fields:
  22. //
  23. // type MyRenderData struct {
  24. // BaseRenderData
  25. // // add additional fields here
  26. // }
  27. type BaseRenderData struct {
  28. GlobalData GlobalRenderData
  29. // additional plugin-specific data
  30. Extra map[string]interface{}
  31. }
  32. // Global implements RenderData.
  33. func (brd *BaseRenderData) Global() *GlobalRenderData {
  34. return &brd.GlobalData
  35. }
  36. // RenderData is implemented by template data structs. It can be used to inject
  37. // additional data to all templates.
  38. type RenderData interface {
  39. // GlobalData returns a pointer to the global render data.
  40. Global() *GlobalRenderData
  41. }
  42. // NewBaseRenderData initializes a new BaseRenderData.
  43. //
  44. // It can be used by routes to pre-fill the base data:
  45. //
  46. // type MyRenderData struct {
  47. // BaseRenderData
  48. // // add additional fields here
  49. // }
  50. //
  51. // data := &MyRenderData{
  52. // BaseRenderData: *alps.NewBaseRenderData(ctx),
  53. // // other fields...
  54. // }
  55. func NewBaseRenderData(ctx *Context) *BaseRenderData {
  56. global := GlobalRenderData{Extra: make(map[string]interface{})}
  57. if ctx.Session != nil {
  58. global.LoggedIn = true
  59. global.Username = ctx.Session.username
  60. }
  61. global.Path = ctx.Request().URL.String()
  62. return &BaseRenderData{
  63. GlobalData: global,
  64. Extra: make(map[string]interface{}),
  65. }
  66. }
  67. type renderer struct {
  68. logger echo.Logger
  69. defaultTheme string
  70. base *template.Template
  71. themes map[string]*template.Template
  72. }
  73. func (r *renderer) Render(w io.Writer, name string, data interface{}, ectx echo.Context) error {
  74. // ectx is the raw *echo.context, not our own *Context
  75. ctx := ectx.Get("context").(*Context)
  76. var renderData RenderData
  77. if data == nil {
  78. renderData = &struct{ BaseRenderData }{*NewBaseRenderData(ctx)}
  79. } else {
  80. var ok bool
  81. renderData, ok = data.(RenderData)
  82. if !ok {
  83. return fmt.Errorf("data passed to template %q doesn't implement RenderData", name)
  84. }
  85. }
  86. for _, plugin := range ctx.Server.plugins {
  87. if err := plugin.Inject(ctx, name, renderData); err != nil {
  88. return fmt.Errorf("failed to run plugin %q: %v", plugin.Name(), err)
  89. }
  90. }
  91. // TODO: per-user theme selection
  92. t := r.base
  93. if r.defaultTheme != "" {
  94. t = r.themes[r.defaultTheme]
  95. }
  96. return t.ExecuteTemplate(w, name, data)
  97. }
  98. func loadTheme(name string, base *template.Template) (*template.Template, error) {
  99. theme, err := base.Clone()
  100. if err != nil {
  101. return nil, err
  102. }
  103. theme, err = theme.ParseGlob(themesDir + "/" + name + "/*.html")
  104. if err != nil {
  105. return nil, err
  106. }
  107. return theme, nil
  108. }
  109. func (r *renderer) Load(plugins []Plugin) error {
  110. base := template.New("")
  111. for _, p := range plugins {
  112. if err := p.LoadTemplate(base); err != nil {
  113. return fmt.Errorf("failed to load template for plugin %q: %v", p.Name(), err)
  114. }
  115. }
  116. themes := make(map[string]*template.Template)
  117. files, err := ioutil.ReadDir(themesDir)
  118. if err != nil && !os.IsNotExist(err) {
  119. return err
  120. }
  121. for _, fi := range files {
  122. if !fi.IsDir() {
  123. continue
  124. }
  125. r.logger.Printf("Loading theme %q", fi.Name())
  126. var err error
  127. if themes[fi.Name()], err = loadTheme(fi.Name(), base); err != nil {
  128. return fmt.Errorf("failed to load theme %q: %v", fi.Name(), err)
  129. }
  130. }
  131. if r.defaultTheme != "" {
  132. if _, ok := themes[r.defaultTheme]; !ok {
  133. return fmt.Errorf("failed to find default theme %q", r.defaultTheme)
  134. }
  135. }
  136. r.base = base
  137. r.themes = themes
  138. return nil
  139. }
  140. func newRenderer(logger echo.Logger, defaultTheme string) *renderer {
  141. return &renderer{
  142. logger: logger,
  143. defaultTheme: defaultTheme,
  144. }
  145. }