├── fixtures └── hello.tpl ├── .travis.yml ├── LICENSE ├── hot_test.go ├── README.md └── hot.go /fixtures/hello.tpl: -------------------------------------------------------------------------------- 1 | hello -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.4 4 | - 1.6 5 | install: 6 | - go get -v 7 | script: 8 | - go test 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Geofrey Ernest 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /hot_test.go: -------------------------------------------------------------------------------- 1 | package hot 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "path/filepath" 7 | "sync" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func testHot(conf *Config, t *testing.T) { 13 | var wg sync.WaitGroup 14 | wg.Add(1) 15 | go func(w *sync.WaitGroup) { 16 | defer w.Done() 17 | tpl, err := New(conf) 18 | if err != nil { 19 | t.Error(err) 20 | return 21 | } 22 | body := "hello {{.Name}}" 23 | name := filepath.Join(conf.Dir, "hello.tpl") 24 | err = ioutil.WriteFile(name, []byte(body), 0600) 25 | if err != nil { 26 | t.Error(err) 27 | return 28 | } 29 | time.Sleep(time.Second) 30 | 31 | data := make(map[string]interface{}) 32 | data["Name"] = "gernest" 33 | buf := &bytes.Buffer{} 34 | err = tpl.Execute(buf, "hello.tpl", data) 35 | if err != nil { 36 | t.Error(err) 37 | return 38 | } 39 | message := "hello gernest" 40 | if buf.String() != message { 41 | t.Errorf("expected %s got %s", message, buf.String()) 42 | } 43 | }(&wg) 44 | wg.Wait() 45 | } 46 | 47 | func TestHot(t *testing.T) { 48 | conf := &Config{ 49 | Watch: true, 50 | BaseName: "hot", 51 | Dir: "fixtures", 52 | FilesExtension: []string{".tpl", ".html", ".tmpl"}, 53 | } 54 | testHot(conf, t) 55 | name := filepath.Join(conf.Dir, "hello.tpl") 56 | ioutil.WriteFile(name, []byte("hello"), 0600) 57 | } 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hot [![Build Status](https://travis-ci.org/gernest/hot.svg)](https://travis-ci.org/gernest/hot) 2 | 3 | Hot is a library for rendering hot golang templates. This means with `hot` you won't need to reload your application everytime you edit your templates.Hot watches for file changes in your templates directory and reloads everytime you make changes to your files. 4 | 5 | hot renders go templates using `html/template` package. 6 | 7 | # Installation 8 | 9 | go get github.com/gernest/hot 10 | 11 | # Usage 12 | 13 | Just pass the configuration object to `hot.New` 14 | 15 | ```go 16 | package main 17 | 18 | import ( 19 | "os" 20 | 21 | "github.com/gernest/hot" 22 | ) 23 | 24 | func main() { 25 | config := &hot.Config{ 26 | Watch: true, 27 | BaseName: "hot", 28 | Dir: "fixtures", 29 | FilesExtension: []string{".tpl", ".html", ".tmpl"}, 30 | } 31 | 32 | tpl, err := hot.New(config) 33 | if err != nil { 34 | panic(err) 35 | } 36 | 37 | // execute the template named "hello.tpl 38 | tpl.Execute(os.Stdout, "hello.tpl", nil) 39 | } 40 | 41 | ``` 42 | 43 | Note that the fixtures directory should exist and there is a template file `hello.tpl` in it , `hot` will be watching any changes to the files inside this directory. 44 | 45 | # configuration 46 | 47 | The `hot.Config` object is used to configure the hot template 48 | 49 | property| details 50 | --------|--------- 51 | Watch| If set to true, the hot reload is enabled 52 | BaseName| Is the root template name, e.g "base", "hot" etc 53 | Dir| The directory in which you keep your templates 54 | FilesExtension| Supported file extensions. These are the ones parsed in the template. 55 | Funcs| (optional) A map of names to functions that can be used inside your templates. ([more information](https://golang.org/pkg/text/template/#FuncMap)) 56 | LeftDelim| left template delimiter e.g {{ 57 | RightDelim| rignt template delimiter e.g }} 58 | 59 | 60 | 61 | # Contributing 62 | 63 | Start with clicking the star button to make the author and his neighbors happy. Then fork the repository and submit a pull request for whatever change you want to be added to this project. 64 | 65 | If you have any questions, just open an issue. 66 | 67 | # Author 68 | Geofrey Ernest 69 | 70 | Twitter : [@gernesti](https://twitter.com/gernesti) 71 | 72 | 73 | # Licence 74 | 75 | This project is released under the MIT licence. See [LICENCE](LICENCE) for more details. 76 | -------------------------------------------------------------------------------- /hot.go: -------------------------------------------------------------------------------- 1 | package hot 2 | 3 | import ( 4 | "fmt" 5 | "html/template" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "path/filepath" 10 | "strings" 11 | 12 | "gopkg.in/fsnotify.v1" 13 | ) 14 | 15 | type Config struct { 16 | Watch bool 17 | BaseName string 18 | Dir string 19 | Funcs template.FuncMap 20 | LeftDelim string 21 | RightDelim string 22 | FilesExtension []string 23 | Log io.Writer 24 | } 25 | 26 | type Template struct { 27 | tpl *template.Template 28 | cfg *Config 29 | watcher *fsnotify.Watcher 30 | closeChannel chan bool 31 | Out io.Writer 32 | } 33 | 34 | func New(cfg *Config) (*Template, error) { 35 | leftDelim, rightDelim := getDelims(cfg) 36 | tmpl := template.New(cfg.BaseName).Delims(leftDelim, rightDelim) 37 | if cfg.Funcs != nil { 38 | tmpl = template.New(cfg.BaseName).Funcs(cfg.Funcs).Delims(leftDelim, rightDelim) 39 | } 40 | tpl := &Template{ 41 | tpl: tmpl, 42 | cfg: cfg, 43 | Out: os.Stdout, 44 | } 45 | if cfg.Log != nil { 46 | tpl.Out = cfg.Log 47 | } 48 | tpl.Init() 49 | err := tpl.Load(cfg.Dir) 50 | if err != nil { 51 | return nil, err 52 | } 53 | return tpl, nil 54 | } 55 | 56 | func (t *Template) Init() { 57 | if t.cfg.Watch { 58 | watcher, err := fsnotify.NewWatcher() 59 | t.watcher = watcher 60 | if err != nil { 61 | fmt.Fprintln(t.Out, err) 62 | return 63 | } 64 | t.closeChannel = make(chan bool, 1) 65 | 66 | fmt.Fprintln(t.Out, "start watching ", t.cfg.Dir) 67 | err = watcher.Add(t.cfg.Dir) 68 | if err != nil { 69 | watcher.Close() 70 | fmt.Fprintln(t.Out, err) 71 | return 72 | } 73 | go func() { 74 | for { 75 | select { 76 | case <-t.closeChannel: 77 | return 78 | case evt := <-watcher.Events: 79 | fmt.Fprintf(t.Out, "%s: reloading... \n", evt.String()) 80 | t.Reload() 81 | } 82 | 83 | } 84 | }() 85 | } 86 | } 87 | 88 | func (t *Template) Close() { 89 | t.closeChannel <- true 90 | t.watcher.Close() 91 | } 92 | 93 | func (t *Template) Reload() { 94 | tpl := *t.tpl 95 | leftDelim, rightDelim := getDelims(t.cfg) 96 | t.tpl = template.New(t.cfg.BaseName).Delims(leftDelim, rightDelim) 97 | if t.cfg.Funcs != nil { 98 | t.tpl = template.New(t.cfg.BaseName).Funcs(t.cfg.Funcs).Delims(leftDelim, rightDelim) 99 | } 100 | err := t.Load(t.cfg.Dir) 101 | if err != nil { 102 | fmt.Fprintln(t.Out, err.Error()) 103 | t.tpl = &tpl 104 | } 105 | } 106 | 107 | func (t *Template) Load(dir string) error { 108 | fmt.Fprintln(t.Out, "loading...", dir) 109 | return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 110 | if err != nil { 111 | return err 112 | } 113 | if info.IsDir() { 114 | return nil 115 | } 116 | 117 | extension := filepath.Ext(path) 118 | found := false 119 | for _, ext := range t.cfg.FilesExtension { 120 | if ext == extension { 121 | found = true 122 | break 123 | } 124 | } 125 | if !found { 126 | return nil 127 | } 128 | 129 | data, err := ioutil.ReadFile(path) 130 | if err != nil { 131 | return err 132 | } 133 | 134 | // We remove the directory name from the path 135 | // this means if we have directory foo, with file bar.tpl 136 | // full path for bar file foo/bar.tpl 137 | // we trim the foo part and remain with /bar.tpl 138 | name := path[len(dir):] 139 | 140 | name = filepath.ToSlash(name) 141 | 142 | name = strings.TrimPrefix(name, "/") // case we missed the opening slash 143 | 144 | tpl := t.tpl.New(name) 145 | _, err = tpl.Parse(string(data)) 146 | if err != nil { 147 | return err 148 | } 149 | return nil 150 | }) 151 | } 152 | 153 | func (t *Template) Execute(w io.Writer, name string, ctx interface{}) error { 154 | return t.tpl.ExecuteTemplate(w, name, ctx) 155 | } 156 | 157 | func getDelims(cfg *Config) (leftDelim string, rightDelim string) { 158 | if cfg.LeftDelim != "" { 159 | leftDelim = cfg.LeftDelim 160 | } else { 161 | leftDelim = "{{" 162 | } 163 | if cfg.RightDelim != "" { 164 | rightDelim = cfg.RightDelim 165 | } else { 166 | rightDelim = "}}" 167 | } 168 | return 169 | } 170 | --------------------------------------------------------------------------------