diff --git a/.gitignore b/.gitignore
index 2405b4b1..61f32858 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,6 +27,10 @@ content/themes/*
content/https/*
!content/https/README.md
+# Plugins
+content/plugins/*
+!content/plugins/README.md
+
# Go
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
diff --git a/content/plugins/README.md b/content/plugins/README.md
new file mode 100644
index 00000000..0f2106c3
--- /dev/null
+++ b/content/plugins/README.md
@@ -0,0 +1,5 @@
+# content/plugins
+
+Place your plugins here.
+
+Read https://github.com/kabukky/journey/wiki/Creating-a-Journey-Plugin for a tutorial on how to create your own Journey plugin.
\ No newline at end of file
diff --git a/filenames/filenames.go b/filenames/filenames.go
index 5b036637..4fa7d1f8 100644
--- a/filenames/filenames.go
+++ b/filenames/filenames.go
@@ -12,13 +12,14 @@ var (
// Initialization of the working directory - needed to load relative assets
_ = initializeWorkingDirectory()
- // For assets that are created or changed while running journey
+ // For assets that are created, changed, our user-provided while running journey
ConfigFilename = filepath.Join(flags.CustomPath, "config.json")
LogFilename = filepath.Join(flags.CustomPath, "log.txt")
DatabaseFilename = filepath.Join(flags.CustomPath, "content", "data", "journey.db")
ThemesFilepath = filepath.Join(flags.CustomPath, "content", "themes")
ImagesFilepath = filepath.Join(flags.CustomPath, "content", "images")
ContentFilepath = filepath.Join(flags.CustomPath, "content")
+ PluginsFilepath = filepath.Join(flags.CustomPath, "content", "plugins")
// For https
HttpsCertFilename = filepath.Join(flags.CustomPath, "content", "https", "cert.pem")
diff --git a/helpers/files.go b/helpers/files.go
new file mode 100644
index 00000000..3894d66a
--- /dev/null
+++ b/helpers/files.go
@@ -0,0 +1,18 @@
+package helpers
+
+import (
+ "os"
+ "path/filepath"
+)
+
+func GetFilenameWithoutExtension(path string) string {
+ return filepath.Base(path)[0 : len(filepath.Base(path))-len(filepath.Ext(path))]
+}
+
+func IsDirectory(path string) bool {
+ fileInfo, err := os.Stat(path)
+ if err != nil {
+ return false
+ }
+ return fileInfo.IsDir()
+}
diff --git a/main.go b/main.go
index 7dafd232..8d3ec638 100644
--- a/main.go
+++ b/main.go
@@ -6,6 +6,8 @@ import (
"github.com/kabukky/journey/configuration"
"github.com/kabukky/journey/database"
"github.com/kabukky/journey/filenames"
+ "github.com/kabukky/journey/flags"
+ "github.com/kabukky/journey/plugins"
"github.com/kabukky/journey/server"
"github.com/kabukky/journey/templates"
"log"
@@ -34,17 +36,20 @@ func checkHttpsCertificates() {
func main() {
// Setup
+ var err error
// GOMAXPROCS - Maybe not needed
runtime.GOMAXPROCS(runtime.NumCPU())
- // Write log to file
- logFile, err := os.OpenFile(filenames.LogFilename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
- if err != nil {
- log.Fatal("Error: Couldn't open log file: " + err.Error())
+ // Write log to file if Journey is not in dev mode
+ if !flags.IsInDevMode {
+ logFile, err := os.OpenFile(filenames.LogFilename, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
+ if err != nil {
+ log.Fatal("Error: Couldn't open log file: " + err.Error())
+ }
+ defer logFile.Close()
+ log.SetOutput(logFile)
}
- defer logFile.Close()
- log.SetOutput(logFile)
// Configuration is read from config.json by loading the configuration package
@@ -62,6 +67,14 @@ func main() {
return
}
+ // Plugins
+ err = plugins.Load()
+ if err == nil {
+ // Close LuaPool at the end
+ defer plugins.LuaPool.Shutdown()
+ log.Println("Plugins loaded.")
+ }
+
// HTTP(S) Server
// Determine the kind of https support (as set in the config.json)
switch configuration.Config.HttpsUsage {
diff --git a/plugins/conversion.go b/plugins/conversion.go
new file mode 100644
index 00000000..73ae2c57
--- /dev/null
+++ b/plugins/conversion.go
@@ -0,0 +1,71 @@
+package plugins
+
+import (
+ "github.com/kabukky/journey/structure"
+ "github.com/yuin/gopher-lua"
+)
+
+func convertPost(vm *lua.LState, structurePost *structure.Post) *lua.LTable {
+ post := vm.NewTable()
+ post.RawSet(lua.LString("id"), lua.LNumber(structurePost.Id))
+ post.RawSet(lua.LString("uuid"), lua.LString(structurePost.Uuid))
+ post.RawSet(lua.LString("title"), lua.LString(structurePost.Title))
+ post.RawSet(lua.LString("slug"), lua.LString(structurePost.Slug))
+ post.RawSet(lua.LString("markdown"), lua.LString(structurePost.Markdown))
+ post.RawSet(lua.LString("html"), lua.LString(structurePost.Html))
+ post.RawSet(lua.LString("isfeatured"), lua.LBool(structurePost.IsFeatured))
+ post.RawSet(lua.LString("ispage"), lua.LBool(structurePost.IsPage))
+ post.RawSet(lua.LString("ispublished"), lua.LBool(structurePost.IsPublished))
+ post.RawSet(lua.LString("date"), lua.LNumber(structurePost.Date.Unix()))
+ post.RawSet(lua.LString("image"), lua.LString(structurePost.Image))
+ return post
+}
+
+func convertUser(vm *lua.LState, structureUser *structure.User) *lua.LTable {
+ user := vm.NewTable()
+ user.RawSet(lua.LString("id"), lua.LNumber(structureUser.Id))
+ user.RawSet(lua.LString("name"), lua.LString(structureUser.Name))
+ user.RawSet(lua.LString("slug"), lua.LString(structureUser.Slug))
+ user.RawSet(lua.LString("email"), lua.LString(structureUser.Email))
+ user.RawSet(lua.LString("image"), lua.LString(structureUser.Image))
+ user.RawSet(lua.LString("cover"), lua.LString(structureUser.Cover))
+ user.RawSet(lua.LString("bio"), lua.LString(structureUser.Bio))
+ user.RawSet(lua.LString("website"), lua.LString(structureUser.Website))
+ user.RawSet(lua.LString("location"), lua.LString(structureUser.Location))
+ user.RawSet(lua.LString("role"), lua.LNumber(structureUser.Role))
+ return user
+}
+
+func convertTags(vm *lua.LState, structureTags []structure.Tag) *lua.LTable {
+ table := make([]*lua.LTable, 0)
+ for index, _ := range structureTags {
+ tag := vm.NewTable()
+ tag.RawSet(lua.LString("id"), lua.LNumber(structureTags[index].Id))
+ tag.RawSet(lua.LString("name"), lua.LString(structureTags[index].Name))
+ tag.RawSet(lua.LString("slug"), lua.LString(structureTags[index].Slug))
+ table = append(table, tag)
+ }
+ return makeTable(vm, table)
+}
+
+func convertBlog(vm *lua.LState, structureBlog *structure.Blog) *lua.LTable {
+ blog := vm.NewTable()
+ blog.RawSet(lua.LString("url"), lua.LString(structureBlog.Url))
+ blog.RawSet(lua.LString("title"), lua.LString(structureBlog.Title))
+ blog.RawSet(lua.LString("description"), lua.LString(structureBlog.Description))
+ blog.RawSet(lua.LString("logo"), lua.LString(structureBlog.Logo))
+ blog.RawSet(lua.LString("cover"), lua.LString(structureBlog.Cover))
+ blog.RawSet(lua.LString("assetpath"), lua.LString(structureBlog.AssetPath))
+ blog.RawSet(lua.LString("postcount"), lua.LNumber(structureBlog.PostCount))
+ blog.RawSet(lua.LString("postsperpage"), lua.LNumber(structureBlog.PostsPerPage))
+ blog.RawSet(lua.LString("activetheme"), lua.LString(structureBlog.ActiveTheme))
+ return blog
+}
+
+func makeTable(vm *lua.LState, tables []*lua.LTable) *lua.LTable {
+ table := vm.NewTable()
+ for index, _ := range tables {
+ table.Append(tables[index])
+ }
+ return table
+}
diff --git a/plugins/execution.go b/plugins/execution.go
new file mode 100644
index 00000000..cdee812e
--- /dev/null
+++ b/plugins/execution.go
@@ -0,0 +1,26 @@
+package plugins
+
+import (
+ "github.com/kabukky/journey/structure"
+ "github.com/yuin/gopher-lua"
+ "log"
+)
+
+func Execute(name string, values *structure.RequestData) ([]byte, error) {
+ // Retrieve the lua state
+ vm := values.PluginVMs[name]
+ // Execute plugin
+ err := vm.CallByParam(lua.P{Fn: vm.GetGlobal(name), NRet: 1, Protect: true})
+ if err != nil {
+ log.Println("Error while executing plugin for helper "+name+":", err)
+ // Since the vm threw an error, close all vms and don't put the map back into the pool
+ for _, luavm := range values.PluginVMs {
+ luavm.Close()
+ }
+ values.PluginVMs = nil
+ return []byte{}, err
+ }
+ // Get return value from vm
+ ret := vm.ToString(-1)
+ return []byte(ret), nil
+}
diff --git a/plugins/loading.go b/plugins/loading.go
new file mode 100644
index 00000000..0bda87e8
--- /dev/null
+++ b/plugins/loading.go
@@ -0,0 +1,134 @@
+package plugins
+
+import (
+ "errors"
+ "github.com/kabukky/journey/filenames"
+ "github.com/kabukky/journey/structure"
+ "github.com/yuin/gopher-lua"
+ "log"
+ "os"
+ "path/filepath"
+)
+
+func Load() error {
+ // Reset LuaPool for a fresh start
+ LuaPool = nil
+ // Make map
+ nameMap := make(map[string]string, 0)
+ err := filepath.Walk(filenames.PluginsFilepath, func(filePath string, info os.FileInfo, err error) error {
+ if !info.IsDir() && filepath.Ext(filePath) == ".lua" {
+ // Check if the lua file is a plugin entry point by executing it
+ helperNames, err := getHelperNames(filePath)
+ if err != nil {
+ return err
+ }
+ // Add all file names of helpers to the name map
+ for _, helperName := range helperNames {
+ absPath, err := filepath.Abs(filePath)
+ if err != nil {
+ log.Println("Error while determining absolute path to lua file:", err)
+ return err
+ }
+ nameMap[helperName] = absPath
+ }
+ }
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+ if len(nameMap) == 0 {
+ return errors.New("No plugins were loaded.")
+ }
+ // If plugins were loaded, create LuaPool and assign name map to LuaPool
+ LuaPool = newLuaPool()
+ LuaPool.m.Lock()
+ defer LuaPool.m.Unlock()
+ LuaPool.files = nameMap
+ return nil
+}
+
+func getHelperNames(fileName string) ([]string, error) {
+ // Make a slice to hold all helper names
+ helperList := make([]string, 0)
+ // Create a new lua state
+ vm := lua.NewState()
+ defer vm.Close()
+ // Set up vm functions
+ values := &structure.RequestData{}
+ absDir, err := filepath.Abs(fileName)
+ if err != nil {
+ log.Println("Error while determining absolute path to lua file:", err)
+ return helperList, err
+ }
+ setUpVm(vm, values, absDir)
+ // Execute plugin
+ // TODO: Is there a better way to just load the file? We only need to execute the register function (see below)
+ err = vm.DoFile(absDir)
+ if err != nil {
+ // TODO: We are not returning upon error here. Keep it like this?
+ log.Println("Error while loading plugin:", err)
+ }
+ err = vm.CallByParam(lua.P{Fn: vm.GetGlobal("register"), NRet: 1, Protect: true})
+ if err != nil {
+ // Fail silently since this is probably just a lua file without a register function
+ return helperList, nil
+ }
+ // Get return value
+ table := vm.ToTable(-1)
+ // Check if return value is a table
+ if table != nil {
+ // Iterate the table for every helper name to be registered
+ table.ForEach(func(key lua.LValue, value lua.LValue) {
+ if str, ok := value.(lua.LString); ok {
+ if string(str) != "" {
+ helperList = append(helperList, string(str))
+ }
+ }
+ })
+ }
+ return helperList, nil
+}
+
+// Creates all methods that can be used from Lua.
+func setUpVm(vm *lua.LState, values *structure.RequestData, absPathToLuaFile string) {
+ luaPath := filepath.Dir(absPathToLuaFile)
+ // Function to get the directory of the current file (to add to LUA_PATH in Lua)
+ vm.SetGlobal("getCurrentDir", vm.NewFunction(func(vm *lua.LState) int {
+ vm.Push(lua.LString(luaPath))
+ return 1 // Number of results
+ }))
+ // Function to print to the log
+ vm.SetGlobal("print", vm.NewFunction(func(vm *lua.LState) int {
+ log.Println(vm.Get(-1).String())
+ return 0 // Number of results
+ }))
+ // Function to get number of posts in values
+ vm.SetGlobal("getNumberOfPosts", vm.NewFunction(func(vm *lua.LState) int {
+ vm.Push(lua.LNumber(len(values.Posts)))
+ return 1 // Number of results
+ }))
+ // Function to get a post by its index
+ vm.SetGlobal("getPost", vm.NewFunction(func(vm *lua.LState) int {
+ postIndex := vm.ToInt(-1)
+ vm.Push(convertPost(vm, &values.Posts[postIndex-1]))
+ return 1 // Number of results
+ }))
+ // Function to get a user by post
+ vm.SetGlobal("getAuthorForPost", vm.NewFunction(func(vm *lua.LState) int {
+ postIndex := vm.ToInt(-1)
+ vm.Push(convertUser(vm, values.Posts[postIndex-1].Author))
+ return 1 // Number of results
+ }))
+ // Function to get tags by post
+ vm.SetGlobal("getTagsForPost", vm.NewFunction(func(vm *lua.LState) int {
+ postIndex := vm.ToInt(-1)
+ vm.Push(convertTags(vm, values.Posts[postIndex-1].Tags))
+ return 1 // Number of results
+ }))
+ // Function to get blog
+ vm.SetGlobal("getBlog", vm.NewFunction(func(vm *lua.LState) int {
+ vm.Push(convertBlog(vm, values.Blog))
+ return 1 // Number of results
+ }))
+}
diff --git a/plugins/luapool.go b/plugins/luapool.go
new file mode 100644
index 00000000..27d22848
--- /dev/null
+++ b/plugins/luapool.go
@@ -0,0 +1,65 @@
+package plugins
+
+import (
+ "github.com/kabukky/journey/structure"
+ "github.com/yuin/gopher-lua"
+ "sync"
+)
+
+// Global LState pool
+var LuaPool *lStatePool
+
+type lStatePool struct {
+ m sync.Mutex
+ files map[string]string
+ saved []map[string]*lua.LState
+}
+
+func (pl *lStatePool) Get(values *structure.RequestData) map[string]*lua.LState {
+ pl.m.Lock()
+ defer pl.m.Unlock()
+ n := len(pl.saved)
+ if n == 0 {
+ x := pl.New()
+ // Since these are new lua states, do the lua file.
+ for key, value := range x {
+ setUpVm(value, values, LuaPool.files[key])
+ value.DoFile(LuaPool.files[key])
+ }
+ return x
+ }
+ x := pl.saved[n-1]
+ // Set the new values for this request in every lua state
+ for key, value := range x {
+ setUpVm(value, values, LuaPool.files[key])
+ }
+ pl.saved = pl.saved[0 : n-1]
+ return x
+}
+
+func (pl *lStatePool) New() map[string]*lua.LState {
+ stateMap := make(map[string]*lua.LState, 0)
+ for key, _ := range LuaPool.files {
+ L := lua.NewState()
+ stateMap[key] = L
+ }
+ return stateMap
+}
+
+func (pl *lStatePool) Put(L map[string]*lua.LState) {
+ pl.m.Lock()
+ defer pl.m.Unlock()
+ pl.saved = append(pl.saved, L)
+}
+
+func (pl *lStatePool) Shutdown() {
+ for _, stateMap := range pl.saved {
+ for _, value := range stateMap {
+ value.Close()
+ }
+ }
+}
+
+func newLuaPool() *lStatePool {
+ return &lStatePool{saved: make([]map[string]*lua.LState, 0, 4)}
+}
diff --git a/templates/helper.go b/structure/helper.go
similarity index 71%
rename from templates/helper.go
rename to structure/helper.go
index 4347b69f..bc1cf232 100644
--- a/templates/helper.go
+++ b/structure/helper.go
@@ -1,8 +1,4 @@
-package templates
-
-import (
- "github.com/kabukky/journey/structure"
-)
+package structure
// Helpers are created during parsing of the theme (template files). Helpers should never be altered during template execution (Helpers are shared across all requests).
type Helper struct {
@@ -12,6 +8,6 @@ type Helper struct {
Position int
Block []byte
Children []Helper
- Function func(*Helper, *structure.RequestData) []byte
+ Function func(*Helper, *RequestData) []byte
BodyHelper *Helper
}
diff --git a/structure/requestdata.go b/structure/requestdata.go
index 378f9d05..6f46567e 100644
--- a/structure/requestdata.go
+++ b/structure/requestdata.go
@@ -1,7 +1,12 @@
package structure
+import (
+ "github.com/yuin/gopher-lua"
+)
+
// RequestData: used for template/helper execution. Contains data specific to the incoming request.
type RequestData struct {
+ PluginVMs map[string]*lua.LState
Posts []Post
Blog *Blog
CurrentTag *Tag
diff --git a/templates/generation.go b/templates/generation.go
index 9e8dd49f..f5f088e8 100644
--- a/templates/generation.go
+++ b/templates/generation.go
@@ -6,8 +6,10 @@ import (
"github.com/kabukky/journey/database"
"github.com/kabukky/journey/filenames"
"github.com/kabukky/journey/flags"
+ "github.com/kabukky/journey/helpers"
+ "github.com/kabukky/journey/plugins"
"github.com/kabukky/journey/structure"
- "gopkg.in/fsnotify.v1"
+ "github.com/kabukky/journey/watcher"
"io/ioutil"
"log"
"os"
@@ -16,17 +18,13 @@ import (
"strings"
)
-// For watching the theme directory for changes
-var themeFileWatcher *fsnotify.Watcher
-var watchedDirectories []string
-
// For parsing of the theme files
var openTag = []byte("{{")
var closeTag = []byte("}}")
var twoPartArgumentChecker = regexp.MustCompile("(\\S+?)\\s*?=\\s*?['\"](.*?)['\"]")
var quoteTagChecker = regexp.MustCompile("(.*?)[\"'](.+?)[\"']$")
-func getFunction(name string) func(*Helper, *structure.RequestData) []byte {
+func getFunction(name string) func(*structure.Helper, *structure.RequestData) []byte {
if helperFuctions[name] != nil {
return helperFuctions[name]
} else {
@@ -34,8 +32,8 @@ func getFunction(name string) func(*Helper, *structure.RequestData) []byte {
}
}
-func createHelper(helperName []byte, unescaped bool, startPos int, block []byte, children []Helper, elseHelper *Helper) *Helper {
- var helper *Helper
+func createHelper(helperName []byte, unescaped bool, startPos int, block []byte, children []structure.Helper, elseHelper *structure.Helper) *structure.Helper {
+ var helper *structure.Helper
// Check for =arguments
twoPartArgumentResult := twoPartArgumentChecker.FindAllSubmatch(helperName, -1)
twoPartArguments := make([][]byte, 0)
@@ -79,11 +77,11 @@ func createHelper(helperName []byte, unescaped bool, startPos int, block []byte,
return helper
}
-func makeHelper(tag string, unescaped bool, startPos int, block []byte, children []Helper) *Helper {
- return &Helper{Name: tag, Arguments: nil, Unescaped: unescaped, Position: startPos, Block: block, Children: children, Function: getFunction(tag)}
+func makeHelper(tag string, unescaped bool, startPos int, block []byte, children []structure.Helper) *structure.Helper {
+ return &structure.Helper{Name: tag, Arguments: nil, Unescaped: unescaped, Position: startPos, Block: block, Children: children, Function: getFunction(tag)}
}
-func findHelper(data []byte, allHelpers []Helper) ([]byte, []Helper) {
+func findHelper(data []byte, allHelpers []structure.Helper) ([]byte, []structure.Helper) {
startPos := bytes.Index(data, openTag)
endPos := bytes.Index(data, closeTag)
if startPos != -1 && endPos != -1 {
@@ -109,7 +107,7 @@ func findHelper(data []byte, allHelpers []Helper) ([]byte, []Helper) {
// Check if block
if bytes.HasPrefix(helperName, []byte("#")) {
helperName = helperName[len([]byte("#")):] //remove '#' from helperName
- var helper Helper
+ var helper structure.Helper
data, helper = findBlock(data, helperName, unescaped, startPos) //only use the data string after the opening tag
allHelpers = append(allHelpers, helper)
return findHelper(data, allHelpers)
@@ -121,7 +119,7 @@ func findHelper(data []byte, allHelpers []Helper) ([]byte, []Helper) {
}
}
-func findBlock(data []byte, helperName []byte, unescaped bool, startPos int) ([]byte, Helper) {
+func findBlock(data []byte, helperName []byte, unescaped bool, startPos int) ([]byte, structure.Helper) {
arguments := bytes.Fields(helperName)
tag := arguments[0] // Get only the first tag (e.g. 'if' in 'if @blog.cover')
arguments = arguments[1:]
@@ -141,7 +139,7 @@ func findBlock(data []byte, helperName []byte, unescaped bool, startPos int) ([]
block := data[startPos:closePositions[positionIndex][0]]
parts := [][]byte{data[:startPos], data[closePositions[positionIndex][1]:]}
data = bytes.Join(parts, []byte(""))
- children := make([]Helper, 0)
+ children := make([]structure.Helper, 0)
block, children = findHelper(block, children)
// Handle else (search children for else helper)
for index, child := range children {
@@ -165,9 +163,9 @@ func findBlock(data []byte, helperName []byte, unescaped bool, startPos int) ([]
return data, *helper
}
-func compileTemplate(data []byte, name string) *Helper {
- baseHelper := Helper{Name: name, Arguments: nil, Unescaped: false, Position: 0, Block: []byte{}, Children: nil, Function: getFunction(name)}
- allHelpers := make([]Helper, 0)
+func compileTemplate(data []byte, name string) *structure.Helper {
+ baseHelper := structure.Helper{Name: name, Arguments: nil, Unescaped: false, Position: 0, Block: []byte{}, Children: nil, Function: getFunction(name)}
+ allHelpers := make([]structure.Helper, 0)
data, allHelpers = findHelper(data, allHelpers)
baseHelper.Block = data
baseHelper.Children = allHelpers
@@ -180,12 +178,12 @@ func compileTemplate(data []byte, name string) *Helper {
return &baseHelper
}
-func createTemplateFromFile(filename string) (*Helper, error) {
+func createTemplateFromFile(filename string) (*structure.Helper, error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
- fileNameWithoutExtension := filepath.Base(filename)[0 : len(filepath.Base(filename))-len(filepath.Ext(filename))]
+ fileNameWithoutExtension := helpers.GetFilenameWithoutExtension(filename)
// Check if a helper with the same name is already in the map
if compiledTemplates.m[fileNameWithoutExtension] != nil {
return nil, errors.New("Error: Conflicting .hbs name '" + fileNameWithoutExtension + "'. A theme file of the same name already exists.")
@@ -214,7 +212,7 @@ func Generate() error {
}
// Compile all template files
// First clear compiledTemplates map (theme could have been changed)
- compiledTemplates.m = make(map[string]*Helper)
+ compiledTemplates.m = make(map[string]*structure.Helper)
currentThemePath := filepath.Join(filenames.ThemesFilepath, *activeTheme)
// Check if the theme folder exists
if _, err := os.Stat(currentThemePath); os.IsNotExist(err) {
@@ -232,75 +230,14 @@ func Generate() error {
if _, ok := compiledTemplates.m["post"]; !ok {
return errors.New("Couldn't compile template 'post'. Is post.hbs missing?")
}
- // If the dev flag is set, watch the theme directory for changes
+ // If the dev flag is set, watch the theme directory and the plugin directoy for changes
+ // TODO: It seems unclean to do the watching of the plugins in the templates package. Move this somewhere else.
if flags.IsInDevMode {
- err = watchThemeDirectory(currentThemePath)
+ // Create watcher
+ err = watcher.Watch([]string{currentThemePath, filenames.PluginsFilepath}, map[string]func() error{".hbs": Generate, ".lua": plugins.Load})
if err != nil {
return err
}
}
return nil
}
-
-func watchThemeDirectory(currentThemePath string) error {
- // Prepare watcher to generate the theme on changes to the files
- if themeFileWatcher == nil {
- var err error
- themeFileWatcher, err = createThemeFileWatcher()
- if err != nil {
- return err
- }
- } else {
- // Remove all current directories from watcher
- for _, dir := range watchedDirectories {
- err := themeFileWatcher.Remove(dir)
- if err != nil {
- return err
- }
- }
- }
- watchedDirectories = make([]string, 0)
- // Watch all subdirectories in theme directory
- err := filepath.Walk(currentThemePath, func(filePath string, info os.FileInfo, err error) error {
- if info.IsDir() {
- err := themeFileWatcher.Add(filePath)
- if err != nil {
- return err
- }
- watchedDirectories = append(watchedDirectories, filePath)
- }
- return nil
- })
- if err != nil {
- return err
- }
- return nil
-}
-
-func createThemeFileWatcher() (*fsnotify.Watcher, error) {
- watcher, err := fsnotify.NewWatcher()
- if err != nil {
- return nil, err
- }
- go func() {
- for {
- select {
- case event := <-watcher.Events:
- if event.Op&fsnotify.Write == fsnotify.Write && filepath.Ext(event.Name) == ".hbs" {
- go Generate()
- }
- case err := <-watcher.Errors:
- log.Println("Error while watching theme directory.", err)
- }
- }
- }()
- return watcher, nil
-}
-
-func isDirectory(path string) bool {
- fileInfo, err := os.Stat(path)
- if err != nil {
- return false
- }
- return fileInfo.IsDir()
-}
diff --git a/templates/handlebars.go b/templates/handlebars.go
index fd8161d6..105cfea0 100644
--- a/templates/handlebars.go
+++ b/templates/handlebars.go
@@ -5,6 +5,7 @@ import (
"github.com/kabukky/journey/conversion"
"github.com/kabukky/journey/database"
"github.com/kabukky/journey/filenames"
+ "github.com/kabukky/journey/plugins"
"github.com/kabukky/journey/structure"
"html"
"log"
@@ -18,12 +19,26 @@ import (
var jqueryCodeForFooter = []byte("")
// Helper fuctions
-func nullFunc(helper *Helper, values *structure.RequestData) []byte {
- log.Println("Warning: This helper is not implemented:", helper.Name)
+func nullFunc(helper *structure.Helper, values *structure.RequestData) []byte {
+ // Check if the helper was defined in a plugin
+ if plugins.LuaPool != nil {
+ // Get a state map to execute and attach it to the requestdata
+ if values.PluginVMs == nil {
+ values.PluginVMs = plugins.LuaPool.Get(values)
+ }
+ if values.PluginVMs[helper.Name] != nil {
+ pluginResult, err := plugins.Execute(helper.Name, values)
+ if err != nil {
+ return []byte{}
+ }
+ return evaluateEscape(pluginResult, helper.Unescaped)
+ }
+ }
+ //log.Println("Warning: This helper is not implemented:", helper.Name)
return []byte{}
}
-func paginationDotTotalFunc(helper *Helper, values *structure.RequestData) []byte {
+func paginationDotTotalFunc(helper *structure.Helper, values *structure.RequestData) []byte {
if values.CurrentTemplate == 0 { // index
return []byte(strconv.FormatInt(values.Blog.PostCount, 10))
} else if values.CurrentTemplate == 3 { // author
@@ -44,7 +59,7 @@ func paginationDotTotalFunc(helper *Helper, values *structure.RequestData) []byt
return []byte{}
}
-func pluralFunc(helper *Helper, values *structure.RequestData) []byte {
+func pluralFunc(helper *structure.Helper, values *structure.RequestData) []byte {
countString := string(helper.Arguments[0].Function(helper, values))
if countString == "" {
log.Println("Couldn't get count in plural helper")
@@ -68,14 +83,14 @@ func pluralFunc(helper *Helper, values *structure.RequestData) []byte {
return []byte{}
}
-func prevFunc(helper *Helper, values *structure.RequestData) []byte {
+func prevFunc(helper *structure.Helper, values *structure.RequestData) []byte {
if values.CurrentIndexPage > 1 {
return []byte{1}
}
return []byte{}
}
-func nextFunc(helper *Helper, values *structure.RequestData) []byte {
+func nextFunc(helper *structure.Helper, values *structure.RequestData) []byte {
var count int64
var err error
if values.CurrentTemplate == 0 { // index
@@ -100,11 +115,11 @@ func nextFunc(helper *Helper, values *structure.RequestData) []byte {
return []byte{}
}
-func pageFunc(helper *Helper, values *structure.RequestData) []byte {
+func pageFunc(helper *structure.Helper, values *structure.RequestData) []byte {
return []byte(strconv.Itoa(values.CurrentIndexPage))
}
-func pagesFunc(helper *Helper, values *structure.RequestData) []byte {
+func pagesFunc(helper *structure.Helper, values *structure.RequestData) []byte {
var count int64
var err error
if values.CurrentTemplate == 0 { // index
@@ -126,7 +141,7 @@ func pagesFunc(helper *Helper, values *structure.RequestData) []byte {
return []byte(strconv.FormatInt(maxPages, 10))
}
-func page_urlFunc(helper *Helper, values *structure.RequestData) []byte {
+func page_urlFunc(helper *structure.Helper, values *structure.RequestData) []byte {
if len(helper.Arguments) != 0 {
if helper.Arguments[0].Name == "prev" || helper.Arguments[0].Name == "pagination.prev" {
if values.CurrentIndexPage > 1 {
@@ -198,21 +213,21 @@ func page_urlFunc(helper *Helper, values *structure.RequestData) []byte {
return []byte{}
}
-func extendFunc(helper *Helper, values *structure.RequestData) []byte {
+func extendFunc(helper *structure.Helper, values *structure.RequestData) []byte {
if len(helper.Arguments) != 0 {
return []byte(helper.Arguments[0].Name)
}
return []byte{}
}
-func featuredFunc(helper *Helper, values *structure.RequestData) []byte {
+func featuredFunc(helper *structure.Helper, values *structure.RequestData) []byte {
if values.Posts[values.CurrentPostIndex].IsFeatured {
return []byte{1}
}
return []byte{}
}
-func body_classFunc(helper *Helper, values *structure.RequestData) []byte {
+func body_classFunc(helper *structure.Helper, values *structure.RequestData) []byte {
if values.CurrentTemplate == 1 { // post
// TODO: is there anything else that needs to get output here?
var buffer bytes.Buffer
@@ -254,17 +269,17 @@ func body_classFunc(helper *Helper, values *structure.RequestData) []byte {
return []byte("post-template")
}
-func ghost_headFunc(helper *Helper, values *structure.RequestData) []byte {
+func ghost_headFunc(helper *structure.Helper, values *structure.RequestData) []byte {
// TODO: Implement
return []byte{}
}
-func ghost_footFunc(helper *Helper, values *structure.RequestData) []byte {
+func ghost_footFunc(helper *structure.Helper, values *structure.RequestData) []byte {
// TODO: This seems to just output a jquery link in ghost. Keep for compatibility?
return jqueryCodeForFooter
}
-func meta_titleFunc(helper *Helper, values *structure.RequestData) []byte {
+func meta_titleFunc(helper *structure.Helper, values *structure.RequestData) []byte {
if values.CurrentTemplate == 1 { // post or page
return evaluateEscape(values.Posts[values.CurrentPostIndex].Title, helper.Unescaped)
} else if values.CurrentTemplate == 3 { // author
@@ -286,7 +301,7 @@ func meta_titleFunc(helper *Helper, values *structure.RequestData) []byte {
return evaluateEscape(values.Blog.Title, helper.Unescaped)
}
-func meta_descriptionFunc(helper *Helper, values *structure.RequestData) []byte {
+func meta_descriptionFunc(helper *structure.Helper, values *structure.RequestData) []byte {
// TODO: Finish this
if values.CurrentTemplate != 1 { // not post
return evaluateEscape(values.Blog.Description, helper.Unescaped)
@@ -295,11 +310,11 @@ func meta_descriptionFunc(helper *Helper, values *structure.RequestData) []byte
return []byte{}
}
-func bodyFunc(helper *Helper, values *structure.RequestData) []byte {
+func bodyFunc(helper *structure.Helper, values *structure.RequestData) []byte {
return helper.Block
}
-func insertFunc(helper *Helper, values *structure.RequestData) []byte {
+func insertFunc(helper *structure.Helper, values *structure.RequestData) []byte {
if len(helper.Arguments) != 0 {
if templateHelper, ok := compiledTemplates.m[helper.Arguments[0].Name]; ok {
return executeHelper(templateHelper, values, values.CurrentHelperContext)
@@ -308,14 +323,14 @@ func insertFunc(helper *Helper, values *structure.RequestData) []byte {
return []byte{}
}
-func encodeFunc(helper *Helper, values *structure.RequestData) []byte {
+func encodeFunc(helper *structure.Helper, values *structure.RequestData) []byte {
if len(helper.Arguments) != 0 {
return []byte(url.QueryEscape(string(helper.Arguments[0].Function(&helper.Arguments[0], values))))
}
return []byte{}
}
-func authorFunc(helper *Helper, values *structure.RequestData) []byte {
+func authorFunc(helper *structure.Helper, values *structure.RequestData) []byte {
// Check if helper is block helper
if len(helper.Block) != 0 {
return executeHelper(helper, values, 3) // context = author
@@ -333,7 +348,7 @@ func authorFunc(helper *Helper, values *structure.RequestData) []byte {
return buffer.Bytes()
}
-func authorDotNameFunc(helper *Helper, values *structure.RequestData) []byte {
+func authorDotNameFunc(helper *structure.Helper, values *structure.RequestData) []byte {
var buffer bytes.Buffer
buffer.WriteString(" 0 {
return []byte{1}
}
return []byte{}
}
-func tagsFunc(helper *Helper, values *structure.RequestData) []byte {
+func tagsFunc(helper *structure.Helper, values *structure.RequestData) []byte {
if len(values.Posts[values.CurrentPostIndex].Tags) > 0 {
separator := ", "
suffix := ""
@@ -449,7 +464,7 @@ func tagsFunc(helper *Helper, values *structure.RequestData) []byte {
return []byte{}
}
-func post_classFunc(helper *Helper, values *structure.RequestData) []byte {
+func post_classFunc(helper *structure.Helper, values *structure.RequestData) []byte {
var buffer bytes.Buffer
buffer.WriteString("post")
if values.Posts[values.CurrentPostIndex].IsFeatured {
@@ -465,7 +480,7 @@ func post_classFunc(helper *Helper, values *structure.RequestData) []byte {
return evaluateEscape(buffer.Bytes(), helper.Unescaped)
}
-func urlFunc(helper *Helper, values *structure.RequestData) []byte {
+func urlFunc(helper *structure.Helper, values *structure.RequestData) []byte {
var buffer bytes.Buffer
if len(helper.Arguments) != 0 {
arguments := processArguments(helper.Arguments)
@@ -492,16 +507,16 @@ func urlFunc(helper *Helper, values *structure.RequestData) []byte {
return []byte{}
}
-func titleFunc(helper *Helper, values *structure.RequestData) []byte {
+func titleFunc(helper *structure.Helper, values *structure.RequestData) []byte {
return evaluateEscape(values.Posts[values.CurrentPostIndex].Title, helper.Unescaped)
}
-func contentFunc(helper *Helper, values *structure.RequestData) []byte {
+func contentFunc(helper *structure.Helper, values *structure.RequestData) []byte {
// TODO: is content always unescaped? seems like it...
return values.Posts[values.CurrentPostIndex].Html
}
-func excerptFunc(helper *Helper, values *structure.RequestData) []byte {
+func excerptFunc(helper *structure.Helper, values *structure.RequestData) []byte {
if values.CurrentHelperContext == 1 { // post
if len(helper.Arguments) != 0 {
arguments := processArguments(helper.Arguments)
@@ -540,7 +555,7 @@ func excerptFunc(helper *Helper, values *structure.RequestData) []byte {
return []byte{}
}
-func dateFunc(helper *Helper, values *structure.RequestData) []byte {
+func dateFunc(helper *structure.Helper, values *structure.RequestData) []byte {
showPublicationDate := false
timeFormat := "MMM Do, YYYY" // Default time format
// If in scope of a post, change default to published date
@@ -570,7 +585,7 @@ func dateFunc(helper *Helper, values *structure.RequestData) []byte {
return evaluateEscape(formatDate(timeFormat, &date), helper.Unescaped)
}
-func atFirstFunc(helper *Helper, values *structure.RequestData) []byte {
+func atFirstFunc(helper *structure.Helper, values *structure.RequestData) []byte {
if values.CurrentHelperContext == 1 { // post
if values.CurrentPostIndex == 0 {
return []byte{1}
@@ -586,7 +601,7 @@ func atFirstFunc(helper *Helper, values *structure.RequestData) []byte {
return []byte{}
}
-func atLastFunc(helper *Helper, values *structure.RequestData) []byte {
+func atLastFunc(helper *structure.Helper, values *structure.RequestData) []byte {
if values.CurrentHelperContext == 1 { // post
if values.CurrentPostIndex == (len(values.Posts) - 1) {
return []byte{1}
@@ -602,7 +617,7 @@ func atLastFunc(helper *Helper, values *structure.RequestData) []byte {
return []byte{}
}
-func atEvenFunc(helper *Helper, values *structure.RequestData) []byte {
+func atEvenFunc(helper *structure.Helper, values *structure.RequestData) []byte {
if values.CurrentHelperContext == 1 { // post
// First post (index 0) needs to be odd
if values.CurrentPostIndex%2 == 1 {
@@ -620,7 +635,7 @@ func atEvenFunc(helper *Helper, values *structure.RequestData) []byte {
return []byte{}
}
-func atOddFunc(helper *Helper, values *structure.RequestData) []byte {
+func atOddFunc(helper *structure.Helper, values *structure.RequestData) []byte {
if values.CurrentHelperContext == 1 { // post
// First post (index 0) needs to be odd
if values.CurrentPostIndex%2 == 0 {
@@ -638,7 +653,7 @@ func atOddFunc(helper *Helper, values *structure.RequestData) []byte {
return []byte{}
}
-func nameFunc(helper *Helper, values *structure.RequestData) []byte {
+func nameFunc(helper *structure.Helper, values *structure.RequestData) []byte {
// If tag (commented out the code for generating a link. Ghost doesn't seem to do that either.
if values.CurrentHelperContext == 2 { // tag
//var buffer bytes.Buffer
@@ -664,7 +679,7 @@ func nameFunc(helper *Helper, values *structure.RequestData) []byte {
return evaluateEscape(values.Posts[values.CurrentPostIndex].Author.Name, helper.Unescaped)
}
-func tagDotNameFunc(helper *Helper, values *structure.RequestData) []byte {
+func tagDotNameFunc(helper *structure.Helper, values *structure.RequestData) []byte {
if len(values.CurrentTag.Name) != 0 {
return evaluateEscape(values.CurrentTag.Name, helper.Unescaped)
} else {
@@ -672,7 +687,7 @@ func tagDotNameFunc(helper *Helper, values *structure.RequestData) []byte {
}
}
-func tagDotSlugFunc(helper *Helper, values *structure.RequestData) []byte {
+func tagDotSlugFunc(helper *structure.Helper, values *structure.RequestData) []byte {
if values.CurrentTag.Slug != "" {
return evaluateEscape([]byte(values.CurrentTag.Slug), helper.Unescaped)
} else {
@@ -680,7 +695,7 @@ func tagDotSlugFunc(helper *Helper, values *structure.RequestData) []byte {
}
}
-func paginationFunc(helper *Helper, values *structure.RequestData) []byte {
+func paginationFunc(helper *structure.Helper, values *structure.RequestData) []byte {
if template, ok := compiledTemplates.m["pagination"]; ok { // If the theme has a pagination.hbs
return executeHelper(template, values, values.CurrentHelperContext)
}
@@ -762,11 +777,11 @@ func paginationFunc(helper *Helper, values *structure.RequestData) []byte {
}
}
-func idFunc(helper *Helper, values *structure.RequestData) []byte {
+func idFunc(helper *structure.Helper, values *structure.RequestData) []byte {
return []byte(strconv.FormatInt(values.Posts[values.CurrentPostIndex].Id, 10))
}
-func assetFunc(helper *Helper, values *structure.RequestData) []byte {
+func assetFunc(helper *structure.Helper, values *structure.RequestData) []byte {
if len(helper.Arguments) != 0 {
var buffer bytes.Buffer
buffer.Write(values.Blog.AssetPath)
@@ -776,7 +791,7 @@ func assetFunc(helper *Helper, values *structure.RequestData) []byte {
return []byte{}
}
-func foreachFunc(helper *Helper, values *structure.RequestData) []byte {
+func foreachFunc(helper *structure.Helper, values *structure.RequestData) []byte {
if len(helper.Arguments) != 0 {
switch helper.Arguments[0].Name {
case "posts":
@@ -804,7 +819,7 @@ func foreachFunc(helper *Helper, values *structure.RequestData) []byte {
return []byte{}
}
-func ifFunc(helper *Helper, values *structure.RequestData) []byte {
+func ifFunc(helper *structure.Helper, values *structure.RequestData) []byte {
if len(helper.Arguments) != 0 {
if len(helper.Arguments[0].Function(&helper.Arguments[0], values)) != 0 {
// If the evaluation is true, execute the if helper
@@ -821,7 +836,7 @@ func ifFunc(helper *Helper, values *structure.RequestData) []byte {
return []byte{}
}
-func unlessFunc(helper *Helper, values *structure.RequestData) []byte {
+func unlessFunc(helper *structure.Helper, values *structure.RequestData) []byte {
if len(helper.Arguments) != 0 {
if len(helper.Arguments[0].Function(&helper.Arguments[0], values)) == 0 {
// If the evaluation is false, execute the unless helper
@@ -831,11 +846,11 @@ func unlessFunc(helper *Helper, values *structure.RequestData) []byte {
return []byte{}
}
-func atBlogDotTitleFunc(helper *Helper, values *structure.RequestData) []byte {
+func atBlogDotTitleFunc(helper *structure.Helper, values *structure.RequestData) []byte {
return evaluateEscape(values.Blog.Title, helper.Unescaped)
}
-func atBlogDotUrlFunc(helper *Helper, values *structure.RequestData) []byte {
+func atBlogDotUrlFunc(helper *structure.Helper, values *structure.RequestData) []byte {
var buffer bytes.Buffer
// Write // in front of url to be protocol agnostic
buffer.WriteString("//")
@@ -843,15 +858,15 @@ func atBlogDotUrlFunc(helper *Helper, values *structure.RequestData) []byte {
return evaluateEscape(buffer.Bytes(), helper.Unescaped)
}
-func atBlogDotLogoFunc(helper *Helper, values *structure.RequestData) []byte {
+func atBlogDotLogoFunc(helper *structure.Helper, values *structure.RequestData) []byte {
return evaluateEscape(values.Blog.Logo, helper.Unescaped)
}
-func atBlogDotCoverFunc(helper *Helper, values *structure.RequestData) []byte {
+func atBlogDotCoverFunc(helper *structure.Helper, values *structure.RequestData) []byte {
return evaluateEscape(values.Blog.Cover, helper.Unescaped)
}
-func atBlogDotDescriptionFunc(helper *Helper, values *structure.RequestData) []byte {
+func atBlogDotDescriptionFunc(helper *structure.Helper, values *structure.RequestData) []byte {
return evaluateEscape(values.Blog.Description, helper.Unescaped)
}
@@ -862,7 +877,7 @@ func evaluateEscape(value []byte, unescaped bool) []byte {
return []byte(html.EscapeString(string(value)))
}
-func processArguments(arguments []Helper) map[string]string {
+func processArguments(arguments []structure.Helper) map[string]string {
argumentsMap := make(map[string]string)
for index, _ := range arguments {
// Separate = arguments and put them in map
diff --git a/templates/helperfunctions.go b/templates/helperfunctions.go
index 151c7d8f..687a0729 100644
--- a/templates/helperfunctions.go
+++ b/templates/helperfunctions.go
@@ -4,7 +4,7 @@ import (
"github.com/kabukky/journey/structure"
)
-var helperFuctions = map[string]func(*Helper, *structure.RequestData) []byte{
+var helperFuctions = map[string]func(*structure.Helper, *structure.RequestData) []byte{
// Null function
"null": nullFunc,
diff --git a/templates/templates.go b/templates/templates.go
index d2f9c99e..37536e24 100644
--- a/templates/templates.go
+++ b/templates/templates.go
@@ -5,6 +5,8 @@ import (
"errors"
"github.com/kabukky/journey/database"
"github.com/kabukky/journey/filenames"
+ "github.com/kabukky/journey/helpers"
+ "github.com/kabukky/journey/plugins"
"github.com/kabukky/journey/structure"
"github.com/kabukky/journey/structure/methods"
"net/http"
@@ -14,10 +16,10 @@ import (
type Templates struct {
sync.RWMutex
- m map[string]*Helper
+ m map[string]*structure.Helper
}
-func newTemplates() *Templates { return &Templates{m: make(map[string]*Helper)} }
+func newTemplates() *Templates { return &Templates{m: make(map[string]*structure.Helper)} }
// Global compiled templates - thread safe and accessible from all packages
var compiledTemplates = newTemplates()
@@ -45,6 +47,10 @@ func ShowPostTemplate(writer http.ResponseWriter, slug string) error {
}
}
_, err = writer.Write(executeHelper(compiledTemplates.m["post"], &requestData, 1)) // context = post
+ if requestData.PluginVMs != nil {
+ // Put the lua state map back into the pool
+ plugins.LuaPool.Put(requestData.PluginVMs)
+ }
return err
}
@@ -73,6 +79,10 @@ func ShowAuthorTemplate(writer http.ResponseWriter, slug string, page int) error
} else {
_, err = writer.Write(executeHelper(compiledTemplates.m["index"], &requestData, 0)) // context = index
}
+ if requestData.PluginVMs != nil {
+ // Put the lua state map back into the pool
+ plugins.LuaPool.Put(requestData.PluginVMs)
+ }
return err
}
@@ -101,6 +111,10 @@ func ShowTagTemplate(writer http.ResponseWriter, slug string, page int) error {
} else {
_, err = writer.Write(executeHelper(compiledTemplates.m["index"], &requestData, 0)) // context = index
}
+ if requestData.PluginVMs != nil {
+ // Put the lua state map back into the pool
+ plugins.LuaPool.Put(requestData.PluginVMs)
+ }
return err
}
@@ -121,6 +135,10 @@ func ShowIndexTemplate(writer http.ResponseWriter, page int) error {
}
requestData := structure.RequestData{Posts: posts, Blog: blog, CurrentIndexPage: page, CurrentTemplate: 0} // CurrentTemplate = index
_, err = writer.Write(executeHelper(compiledTemplates.m["index"], &requestData, 0)) // context = index
+ if requestData.PluginVMs != nil {
+ // Put the lua state map back into the pool
+ plugins.LuaPool.Put(requestData.PluginVMs)
+ }
return err
}
@@ -128,14 +146,14 @@ func GetAllThemes() []string {
themes := make([]string, 0)
files, _ := filepath.Glob(filepath.Join(filenames.ThemesFilepath, "*"))
for _, file := range files {
- if isDirectory(file) {
+ if helpers.IsDirectory(file) {
themes = append(themes, filepath.Base(file))
}
}
return themes
}
-func executeHelper(helper *Helper, values *structure.RequestData, context int) []byte {
+func executeHelper(helper *structure.Helper, values *structure.RequestData, context int) []byte {
// Set context and set it back to the old value once fuction returns
defer setCurrentHelperContext(values, values.CurrentHelperContext)
values.CurrentHelperContext = context
@@ -143,7 +161,7 @@ func executeHelper(helper *Helper, values *structure.RequestData, context int) [
block := helper.Block
indexTracker := 0
extended := false
- var extendHelper *Helper
+ var extendHelper *structure.Helper
for index, child := range helper.Children {
// Handle extend helper
if index == 0 && child.Name == "!<" {
diff --git a/watcher/watcher.go b/watcher/watcher.go
new file mode 100644
index 00000000..81ce5c84
--- /dev/null
+++ b/watcher/watcher.go
@@ -0,0 +1,76 @@
+package watcher
+
+import (
+ "gopkg.in/fsnotify.v1"
+ "log"
+ "os"
+ "path/filepath"
+)
+
+var watcher *fsnotify.Watcher
+var watchedDirectories []string
+
+func Watch(paths []string, extensionsFunctions map[string]func() error) error {
+ // Prepare watcher to generate the theme on changes to the files
+ if watcher == nil {
+ var err error
+ watcher, err = createWatcher(extensionsFunctions)
+ if err != nil {
+ return err
+ }
+ } else {
+ // Remove all current directories from watcher
+ for _, dir := range watchedDirectories {
+ err := watcher.Remove(dir)
+ if err != nil {
+ return err
+ }
+ }
+ }
+ watchedDirectories = make([]string, 0)
+ // Watch all subdirectories in the given paths
+ for _, path := range paths {
+ err := filepath.Walk(path, func(filePath string, info os.FileInfo, err error) error {
+ if info.IsDir() {
+ err := watcher.Add(filePath)
+ if err != nil {
+ return err
+ }
+ watchedDirectories = append(watchedDirectories, filePath)
+ }
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func createWatcher(extensionsFunctions map[string]func() error) (*fsnotify.Watcher, error) {
+ watcher, err := fsnotify.NewWatcher()
+ if err != nil {
+ return nil, err
+ }
+ go func() {
+ for {
+ select {
+ case event := <-watcher.Events:
+ if event.Op&fsnotify.Write == fsnotify.Write {
+ for key, value := range extensionsFunctions {
+ if filepath.Ext(event.Name) == key {
+ // Call the function associated with this file extension
+ err := value()
+ if err != nil {
+ log.Panic("Error while reloading theme or plugins:", err)
+ }
+ }
+ }
+ }
+ case err := <-watcher.Errors:
+ log.Println("Error while watching theme directory.", err)
+ }
+ }
+ }()
+ return watcher, nil
+}