@@ -14,6 +14,13 @@ Templates in `public/themes/<name>/*.html` override default templates in | |||||
`public/*.html`. Assets in `public/themes/<name>/assets/*` are served by the | `public/*.html`. Assets in `public/themes/<name>/assets/*` are served by the | ||||
HTTP server at `themes/<name>/assets/*`. | HTTP server at `themes/<name>/assets/*`. | ||||
## Plugins | |||||
Lua plugins are supported. They can be dropped in `plugins/*.lua`. | |||||
For now only a single hook is supported: `render(name, data)`. If defined, this | |||||
Lua function will be called prior to rendering a template. | |||||
## License | ## License | ||||
MIT | MIT |
@@ -12,7 +12,9 @@ require ( | |||||
github.com/mattn/go-colorable v0.1.4 // indirect | github.com/mattn/go-colorable v0.1.4 // indirect | ||||
github.com/mattn/go-isatty v0.0.10 // indirect | github.com/mattn/go-isatty v0.0.10 // indirect | ||||
github.com/valyala/fasttemplate v1.1.0 // indirect | github.com/valyala/fasttemplate v1.1.0 // indirect | ||||
github.com/yuin/gopher-lua v0.0.0-20191128022950-c6266f4fe8d7 | |||||
golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e // indirect | golang.org/x/crypto v0.0.0-20191202143827-86a70503ff7e // indirect | ||||
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 // indirect | golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 // indirect | ||||
golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 // indirect | golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 // indirect | ||||
layeh.com/gopher-luar v1.0.7 | |||||
) | ) |
@@ -1,3 +1,6 @@ | |||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= | |||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= | |||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= | |||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | ||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||||
@@ -50,6 +53,9 @@ github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8W | |||||
github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= | ||||
github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4= | github.com/valyala/fasttemplate v1.1.0 h1:RZqt0yGBsps8NGvLSGW804QQqCUYYLsaOjTVHy1Ocw4= | ||||
github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= | github.com/valyala/fasttemplate v1.1.0/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= | ||||
github.com/yuin/gopher-lua v0.0.0-20190206043414-8bfc7677f583/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= | |||||
github.com/yuin/gopher-lua v0.0.0-20191128022950-c6266f4fe8d7 h1:Y17pEjKgx2X0A69WQPGa8hx/Myzu+4NdUxlkZpbAYio= | |||||
github.com/yuin/gopher-lua v0.0.0-20191128022950-c6266f4fe8d7/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= | |||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | ||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= | ||||
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= | ||||
@@ -59,6 +65,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2eP | |||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= | ||||
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 h1:e6HwijUxhDe+hPNjZQQn9bA5PW3vNmnN64U2ZW759Lk= | golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 h1:e6HwijUxhDe+hPNjZQQn9bA5PW3vNmnN64U2ZW759Lk= | ||||
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | ||||
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | |||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | ||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | ||||
@@ -76,3 +83,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ | |||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= | ||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= | ||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= | ||||
layeh.com/gopher-luar v1.0.7 h1:53iv6CCkRs5wyofZ+qVXcyAYQOIG52s6pt4xkqZdq7k= | |||||
layeh.com/gopher-luar v1.0.7/go.mod h1:TPnIVCZ2RJBndm7ohXyaqfhzjlZ+OA2SZR/YwL8tECk= |
@@ -0,0 +1,78 @@ | |||||
package koushin | |||||
import ( | |||||
"fmt" | |||||
"path/filepath" | |||||
"github.com/labstack/echo/v4" | |||||
"github.com/yuin/gopher-lua" | |||||
"layeh.com/gopher-luar" | |||||
) | |||||
type Plugin interface { | |||||
Name() string | |||||
Render(name string, data interface{}) error | |||||
Close() error | |||||
} | |||||
type luaPlugin struct { | |||||
filename string | |||||
state *lua.LState | |||||
} | |||||
func (p *luaPlugin) Name() string { | |||||
return p.filename | |||||
} | |||||
func (p *luaPlugin) Render(name string, data interface{}) error { | |||||
global := p.state.GetGlobal("render") | |||||
if global == nil { | |||||
return nil | |||||
} | |||||
if err := p.state.CallByParam(lua.P{ | |||||
Fn: global, | |||||
NRet: 0, | |||||
Protect: true, | |||||
}, lua.LString(name), luar.New(p.state, data)); err != nil { | |||||
return err | |||||
} | |||||
return nil | |||||
} | |||||
func (p *luaPlugin) Close() error { | |||||
p.state.Close() | |||||
return nil | |||||
} | |||||
func loadLuaPlugin(filename string) (*luaPlugin, error) { | |||||
l := lua.NewState() | |||||
if err := l.DoFile(filename); err != nil { | |||||
return nil, err | |||||
} | |||||
return &luaPlugin{filename, l}, nil | |||||
} | |||||
func loadAllLuaPlugins(log echo.Logger) ([]Plugin, error) { | |||||
filenames, err := filepath.Glob("plugins/*.lua") | |||||
if err != nil { | |||||
return nil, fmt.Errorf("filepath.Glob failed: %v", err) | |||||
} | |||||
plugins := make([]Plugin, 0, len(filenames)) | |||||
for _, filename := range filenames { | |||||
log.Printf("Loading Lua plugin '%v'", filename) | |||||
p, err := loadLuaPlugin(filename) | |||||
if err != nil { | |||||
for _, p := range plugins { | |||||
p.Close() | |||||
} | |||||
return nil, fmt.Errorf("failed to load Lua plugin '%v': %v", filename, err) | |||||
} | |||||
plugins = append(plugins, p) | |||||
} | |||||
return plugins, nil | |||||
} |
@@ -28,6 +28,8 @@ type Server struct { | |||||
tls bool | tls bool | ||||
insecure bool | insecure bool | ||||
} | } | ||||
plugins []Plugin | |||||
} | } | ||||
func (s *Server) parseIMAPURL(imapURL string) error { | func (s *Server) parseIMAPURL(imapURL string) error { | ||||
@@ -131,6 +133,11 @@ func New(e *echo.Echo, options *Options) error { | |||||
return fmt.Errorf("failed to load templates: %v", err) | return fmt.Errorf("failed to load templates: %v", err) | ||||
} | } | ||||
s.plugins, err = loadAllLuaPlugins(e.Logger) | |||||
if err != nil { | |||||
return fmt.Errorf("failed to load plugins: %v", err) | |||||
} | |||||
e.HTTPErrorHandler = func(err error, c echo.Context) { | e.HTTPErrorHandler = func(err error, c echo.Context) { | ||||
code := http.StatusInternalServerError | code := http.StatusInternalServerError | ||||
if he, ok := err.(*echo.HTTPError); ok { | if he, ok := err.(*echo.HTTPError); ok { | ||||
@@ -145,6 +152,7 @@ func New(e *echo.Echo, options *Options) error { | |||||
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { | e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { | ||||
return func(ectx echo.Context) error { | return func(ectx echo.Context) error { | ||||
ctx := &context{Context: ectx, server: s} | ctx := &context{Context: ectx, server: s} | ||||
ctx.Set("context", ctx) | |||||
cookie, err := ctx.Cookie(cookieName) | cookie, err := ctx.Cookie(cookieName) | ||||
if err == http.ErrNoCookie { | if err == http.ErrNoCookie { | ||||
@@ -1,6 +1,7 @@ | |||||
package koushin | package koushin | ||||
import ( | import ( | ||||
"fmt" | |||||
"html/template" | "html/template" | ||||
"io" | "io" | ||||
"net/url" | "net/url" | ||||
@@ -13,7 +14,16 @@ type tmpl struct { | |||||
t *template.Template | t *template.Template | ||||
} | } | ||||
func (t *tmpl) Render(w io.Writer, name string, data interface{}, c echo.Context) error { | |||||
func (t *tmpl) Render(w io.Writer, name string, data interface{}, ectx echo.Context) error { | |||||
// ectx is the raw *echo.context, not our own *context | |||||
ctx := ectx.Get("context").(*context) | |||||
for _, plugin := range ctx.server.plugins { | |||||
if err := plugin.Render(name, data); err != nil { | |||||
return fmt.Errorf("failed to run plugin '%v': %v", plugin.Name(), err) | |||||
} | |||||
} | |||||
return t.t.ExecuteTemplate(w, name, data) | return t.t.ExecuteTemplate(w, name, data) | ||||
} | } | ||||