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