├── testdata ├── sample │ ├── media │ │ ├── media2 │ │ └── midia1 │ ├── _themes │ │ └── blue │ │ │ ├── post.html │ │ │ ├── static │ │ │ └── css │ │ │ │ └── style.css │ │ │ ├── page.html │ │ │ ├── index.html │ │ │ └── home.html │ ├── 1.4.md │ ├── 1.0.md │ ├── 1.3.md │ ├── _bongo.yml │ └── 1.2.md └── data │ └── post.md ├── .gitignore ├── bongo_test.go ├── utils.go ├── .travis.yml ├── Makefile ├── cmd ├── docs │ └── docs.go └── bongo │ └── main.go ├── loader.go ├── README.md ├── LICENCE ├── front_test.go ├── bongo.go ├── models.go ├── front.go ├── doc.go ├── docs └── doc.md └── render.go /testdata/sample/media/media2: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/sample/media/midia1: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/sample/_themes/blue/post.html: -------------------------------------------------------------------------------- 1 | {{.Page.HTML}} -------------------------------------------------------------------------------- /testdata/sample/_themes/blue/static/css/style.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/sample/1.4.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: chapter four 3 | --- 4 | 5 | Begin 6 | -------------------------------------------------------------------------------- /testdata/sample/1.0.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: chapter one 3 | section: blog 4 | --- 5 | 6 | Genesis -------------------------------------------------------------------------------- /testdata/sample/1.3.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Chapter three 3 | section: blog 4 | --- 5 | 6 | Once upon a time -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | .idea 3 | testdata/sample/_site 4 | dist 5 | cmd/bongo/bongo 6 | docs/_site 7 | docs/gh-pages -------------------------------------------------------------------------------- /testdata/sample/_bongo.yml: -------------------------------------------------------------------------------- 1 | author: gernest 2 | title: Selling alien technologies 3 | subtitle: A dead man from Tanzania 4 | theme: blue 5 | static: 6 | - media -------------------------------------------------------------------------------- /testdata/sample/_themes/blue/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /testdata/sample/_themes/blue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | my index 9 | 10 | -------------------------------------------------------------------------------- /bongo_test.go: -------------------------------------------------------------------------------- 1 | package bongo 2 | 3 | import "testing" 4 | 5 | func TestApp(t *testing.T) { 6 | app := New() 7 | err := app.Run("testdata/sample") 8 | if err != nil { 9 | t.Error(err) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /testdata/sample/_themes/blue/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | This is blue 9 | 10 | 11 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package bongo 2 | 3 | import "path/filepath" 4 | 5 | //HasExt hecks if the file has any mathing extension 6 | func HasExt(file string, exts ...string) bool { 7 | fext := filepath.Ext(file) 8 | if len(exts) > 0 { 9 | for _, ext := range exts { 10 | if ext == fext { 11 | return true 12 | } 13 | } 14 | } 15 | return false 16 | } 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.7 4 | before_install: 5 | - go get -t -v 6 | - go get ./cmd/bongo 7 | - go get github.com/axw/gocov/gocov 8 | - go get github.com/mattn/goveralls 9 | - if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi 10 | script: 11 | - $HOME/gopath/bin/goveralls -service=travis-ci -repotoken=$COVERALLS -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | cmd_dir:=cmd/bongo 2 | .PHONY: all dist 3 | ifeq "$(origin APP_VER)" "undefined" 4 | APP_VER=0.1 5 | endif 6 | all: test 7 | @go vet 8 | @golint 9 | @cd $(cmd_dir)&&go build 10 | 11 | clean: 12 | @cd $(cmd_dir)&&go clean 13 | 14 | deps: 15 | @go get github.com/mitchellh/gox 16 | 17 | dist: 18 | -@rm -r dist 19 | @gox -output="dist/{{.Dir}}v$(APP_VER)_{{.OS}}_{{.Arch}}/{{.Dir}}" ./cmd/bongo 20 | 21 | test: 22 | @go test 23 | 24 | install: 25 | @cd $(cmd_dir)&&go install 26 | 27 | gh-pages: 28 | @go run cmd/docs/docs.go 29 | 30 | preview-docs: 31 | @./${cmd_dir}/bongo serve --source docs -------------------------------------------------------------------------------- /cmd/docs/docs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/gernest/bongo" 11 | ) 12 | 13 | func main() { 14 | docsDir := "docs" 15 | app := bongo.New() 16 | if err := app.Run(docsDir); err != nil { 17 | log.Fatal(err) 18 | } 19 | 20 | docsSite := filepath.Join(docsDir, bongo.OutputDir) 21 | ghPages := filepath.Join(docsDir, "gh-pages", "bongo") 22 | 23 | err := filepath.Walk(docsSite, func(path string, info os.FileInfo, err error) error { 24 | if err != nil { 25 | return err 26 | } 27 | if info.IsDir() { 28 | return nil 29 | } 30 | out := strings.TrimPrefix(path, docsSite) 31 | dest := filepath.Join(ghPages, out) 32 | 33 | os.MkdirAll(filepath.Dir(dest), 0755) 34 | 35 | b, err := ioutil.ReadFile(path) 36 | if err != nil { 37 | return err 38 | } 39 | err = ioutil.WriteFile(dest, b, info.Mode()) 40 | if err != nil { 41 | return err 42 | } 43 | return nil 44 | }) 45 | if err != nil { 46 | log.Println(err) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /loader.go: -------------------------------------------------------------------------------- 1 | package bongo 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | ) 8 | 9 | var supportedExtensions = []string{".md", ".MD", "..markdown"} 10 | 11 | //DefaultLoader is the default FileLoader implementation 12 | type DefaultLoader struct{} 13 | 14 | // NewLoader returns default FileLoader implementation. 15 | func NewLoader() *DefaultLoader { 16 | return &DefaultLoader{} 17 | } 18 | 19 | // Load loads files found in the base path for processing. 20 | func (d DefaultLoader) Load(base string) ([]string, error) { 21 | return func(base string) ([]string, error) { 22 | var rst []string 23 | err := filepath.Walk(base, func(path string, info os.FileInfo, err error) error { 24 | switch { 25 | case err != nil: 26 | return err 27 | case info.IsDir(): 28 | return nil 29 | case !HasExt(path, supportedExtensions...): 30 | return nil 31 | case strings.Contains(path, OutputDir): 32 | return nil 33 | } 34 | rst = append(rst, path) 35 | return nil 36 | }) 37 | if err != nil { 38 | return nil, err 39 | } 40 | return rst, nil 41 | 42 | }(base) 43 | 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bongo [![Build Status](https://travis-ci.org/gernest/bongo.svg)](https://travis-ci.org/gernest/bongo) [![Coverage Status](https://coveralls.io/repos/gernest/bongo/badge.svg?branch=master&service=github)](https://coveralls.io/github/gernest/bongo?branch=master) [![GoDoc](https://godoc.org/github.com/gernest/bongo?status.svg)](https://godoc.org/github.com/gernest/bongo) 2 | 3 | An elegant static site generator. 4 | 5 | # Features 6 | * Fast. (yes, speed as a feature) 7 | * Flexible. You can assemble your own static generator. 8 | * Simple to use. 9 | * Themes support. 10 | * Minimalistic. 11 | 12 | # Installation 13 | 14 | The command line app 15 | 16 | ```bash 17 | go get github.com/gernest/bongo/cmd/bongo 18 | ``` 19 | 20 | The library 21 | ```bash 22 | go get github.com/gernest/bongo 23 | ``` 24 | 25 | 26 | ### Documentation 27 | 28 | For Installation and Usage see [documentation](http://godoc.org/github.com/gernest/bongo) 29 | 30 | 31 | # Contributing 32 | Just fork, and submit a pull request. 33 | 34 | ## Licence 35 | This project is released under MIT licence see [LICENCE](LICENCE) for more details. 36 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 geofrey ernest 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /front_test.go: -------------------------------------------------------------------------------- 1 | package bongo 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | var jsonPost = `+++ 11 | { 12 | "title":"bongo" 13 | } 14 | +++ 15 | 16 | # Body 17 | Over my dead body` 18 | 19 | var yamlPost = `--- 20 | title: chapter two 21 | section: blog 22 | --- 23 | A brave new world 24 | ` 25 | 26 | func TestJSONMatter(t *testing.T) { 27 | m := NewJSON("+++") 28 | 29 | f, b, err := m.Parse(strings.NewReader(jsonPost)) 30 | if err != nil { 31 | t.Error(err) 32 | } 33 | if b == nil { 34 | t.Error("expected body got nil instead") 35 | } 36 | if f == nil { 37 | t.Fatal("expecetd front") 38 | } 39 | if _, ok := f["title"]; !ok { 40 | t.Error("expeced title") 41 | } 42 | } 43 | 44 | func TestYAMLMatter(t *testing.T) { 45 | m := NewYAML("---") 46 | f, b, err := m.Parse(strings.NewReader(yamlPost)) 47 | if err != nil { 48 | t.Error(err) 49 | } 50 | if b == nil { 51 | t.Error("expected body got nil instead") 52 | } 53 | if f == nil { 54 | t.Fatal("expecetd front") 55 | } 56 | if _, ok := f["title"]; !ok { 57 | t.Error("expeced title") 58 | } 59 | 60 | // body, _ := ioutil.ReadAll(b) 61 | // t.Error(string(body)) 62 | } 63 | 64 | func TestLargeFile(t *testing.T) { 65 | data, err := ioutil.ReadFile("testdata/data/post.md") 66 | if err != nil { 67 | t.Error(err) 68 | } 69 | m := NewYAML("---") 70 | f, b, err := m.Parse(bytes.NewReader(data)) 71 | if err != nil { 72 | t.Error(err) 73 | } 74 | if b == nil { 75 | t.Error("expected body got nil instead") 76 | } 77 | if f == nil { 78 | t.Fatal("expecetd front") 79 | } 80 | if _, ok := f["title"]; !ok { 81 | t.Error("expeced title") 82 | } 83 | // body, _ := ioutil.ReadAll(b) 84 | // t.Error(string(body)) 85 | } 86 | -------------------------------------------------------------------------------- /bongo.go: -------------------------------------------------------------------------------- 1 | package bongo 2 | 3 | import "os" 4 | 5 | type defaultApp struct { 6 | DefaultLoader 7 | *Matter 8 | *DefaultRenderer 9 | } 10 | 11 | func newDefaultApp() *defaultApp { 12 | app := &defaultApp{} 13 | app.Matter = NewYAML() 14 | app.DefaultRenderer = NewDefaultRenderer() 15 | return app 16 | } 17 | 18 | //App is the main bongo application 19 | type App struct { 20 | gene Generator 21 | } 22 | 23 | //New creates a new App which uses default Generator implementation 24 | func New() *App { 25 | return NewApp(newDefaultApp()) 26 | } 27 | 28 | //NewApp creates a new app, that uses g as the generator 29 | func NewApp(g Generator) *App { 30 | return &App{gene: g} 31 | } 32 | 33 | // Run runs the app 34 | func (g *App) Run(root string) error { 35 | files, err := g.gene.Load(root) 36 | if err != nil { 37 | return err 38 | } 39 | pages := make(PageList, len(files)) 40 | send := make(chan *Page) 41 | errs := make(chan error) 42 | for _, f := range files { 43 | go func(file string) { 44 | f, err := os.Open(file) 45 | if err != nil { 46 | errs <- err 47 | return 48 | } 49 | defer f.Close() 50 | front, body, err := g.gene.Parse(f) 51 | if err != nil { 52 | errs <- err 53 | return 54 | } 55 | stat, err := f.Stat() 56 | if err != nil { 57 | errs <- err 58 | return 59 | } 60 | send <- &Page{Path: file, Body: body, Data: front, ModTime: stat.ModTime()} 61 | }(f) 62 | } 63 | n := 0 64 | var fish error 65 | END: 66 | for { 67 | select { 68 | case pg := <-send: 69 | pages[n] = pg 70 | n++ 71 | case perr := <-errs: 72 | fish = perr 73 | break END 74 | 75 | default: 76 | if len(files) <= n { 77 | break END 78 | } 79 | } 80 | 81 | } 82 | if fish != nil { 83 | return fish 84 | } 85 | 86 | // run before rendering 87 | err = g.gene.Before(root) 88 | if err != nil { 89 | return nil 90 | } 91 | err = g.gene.Render(root, pages) 92 | if err != nil { 93 | Rollback(root) // roll back before exiting 94 | return err 95 | } 96 | 97 | // run after rendering 98 | err = g.gene.After(root) 99 | if err != nil { 100 | return err 101 | } 102 | return nil 103 | 104 | } 105 | -------------------------------------------------------------------------------- /cmd/bongo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/gernest/bongo" 10 | 11 | "gopkg.in/fsnotify.v1" 12 | 13 | "github.com/urfave/cli" 14 | ) 15 | 16 | var ( 17 | authors = []cli.Author{ 18 | {"Geofrey Ernest", "geofreyernest@live.com"}, 19 | } 20 | sourceFlagName = "source" 21 | appName = "bongo" 22 | version = "0.1.1" 23 | ) 24 | 25 | func buildFlags() []cli.Flag { 26 | return []cli.Flag{ 27 | cli.StringFlag{ 28 | Name: sourceFlagName, 29 | Usage: "sets the path to the project soucce files", 30 | EnvVar: "PROJECT_SOURCE", 31 | }, 32 | } 33 | } 34 | 35 | func build(ctx *cli.Context) { 36 | wd, _ := os.Getwd() 37 | src := wd 38 | if f := ctx.String(sourceFlagName); f != "" { 39 | src = f 40 | } 41 | app := bongo.New() 42 | err := app.Run(src) 43 | if err != nil { 44 | log.Println(err) 45 | } 46 | 47 | } 48 | 49 | func serve(ctx *cli.Context) { 50 | wd, _ := os.Getwd() 51 | src := wd 52 | if f := ctx.String(sourceFlagName); f != "" { 53 | src = f 54 | } 55 | app := bongo.New() 56 | err := app.Run(src) 57 | if err != nil { 58 | log.Println(err) 59 | } 60 | files, err := bongo.NewLoader().Load(src) 61 | if err != nil { 62 | log.Fatal(err) 63 | } 64 | watch, err := fsnotify.NewWatcher() 65 | if err != nil { 66 | log.Fatal(err) 67 | } 68 | defer watch.Close() 69 | for _, file := range files { 70 | watch.Add(file) 71 | } 72 | go func() { 73 | dir := filepath.Join(src, bongo.OutputDir) 74 | log.Println("serving website", dir, " at http://localhost:8000") 75 | log.Fatal(http.ListenAndServe(":8000", http.FileServer(http.Dir(dir)))) 76 | }() 77 | for { 78 | select { 79 | case event := <-watch.Events: 80 | if event.Op&(fsnotify.Rename|fsnotify.Create|fsnotify.Write) > 0 { 81 | log.Printf("detected change %s Rebuilding...\n", event.Name) 82 | app.Run(src) 83 | } 84 | case err := <-watch.Errors: 85 | if err != nil { 86 | log.Println(err) 87 | } 88 | default: 89 | continue 90 | } 91 | } 92 | 93 | } 94 | 95 | func main() { 96 | app := cli.NewApp() 97 | app.Name = appName 98 | app.Usage = "Eleant static website generator" 99 | app.Authors = authors 100 | app.Version = version 101 | app.Commands = []cli.Command{ 102 | cli.Command{ 103 | Name: "build", 104 | ShortName: "b", 105 | Usage: "build site", 106 | Description: "build site", 107 | Action: build, 108 | Flags: buildFlags(), 109 | }, 110 | cli.Command{ 111 | Name: "serve", 112 | ShortName: "s", 113 | Usage: "builds and serves the project", 114 | Description: "serves site", 115 | Action: serve, 116 | Flags: buildFlags(), 117 | }, 118 | } 119 | app.Run(os.Args) 120 | } 121 | -------------------------------------------------------------------------------- /models.go: -------------------------------------------------------------------------------- 1 | package bongo 2 | 3 | import ( 4 | "html/template" 5 | "io" 6 | "io/ioutil" 7 | "sort" 8 | "time" 9 | 10 | "github.com/a8m/mark" 11 | ) 12 | 13 | const ( 14 | 15 | //DefaultView is the default template view 16 | // for pages 17 | DefaultView = "post" 18 | 19 | //OutputDir is the name of the directory where generated files are saved 20 | OutputDir = "_site" 21 | 22 | //DefaultExt is the default extensinon name for output files 23 | DefaultExt = ".html" 24 | 25 | //DefaultPerm is the default permissinon for generated files 26 | DefaultPerm = 0600 27 | 28 | //DefaultPageKey is the key used to store current page in template 29 | // context data. 30 | DefaultPageKey = "Page" 31 | 32 | //CurrentSectionKey is the key used to store the current section value in the 33 | // template context 34 | CurrentSectionKey = "CurrentSection" 35 | 36 | //AllSectionsKey is the key used to store all sections in the template context data 37 | AllSectionsKey = "Sections" 38 | 39 | //DefaultConfigFile is the default configuraton file for abongo based project 40 | DefaultConfigFile = "_bongo.yml" 41 | 42 | //SiteConfigKey is the key used to store site wide configuration 43 | SiteConfigKey = "Site" 44 | 45 | //ThemeKey is the key used to store the name of the theme to be used 46 | ThemeKey = "theme" 47 | 48 | //ThemeDir is the directory where themes are installed 49 | ThemeDir = "_themes" 50 | 51 | //DefaultTheme the name of the default theme 52 | DefaultTheme = "gh" 53 | 54 | //StaticDir directory for static assets 55 | StaticDir = "static" 56 | 57 | cssDir = "css" 58 | defaultSection = "home" 59 | pageSection = "section" 60 | modTime = "timeStamp" 61 | ) 62 | 63 | //DefaultTpl is the defaut templates 64 | var DefaultTpl = struct { 65 | Home, Index, Page, Post string 66 | }{ 67 | "home.html", 68 | "index.html", 69 | "page.html", 70 | "post.html", 71 | } 72 | 73 | type ( 74 | // PageList is a collection of pages 75 | PageList []*Page 76 | 77 | // Page is a represantation of text document 78 | Page struct { 79 | Path string 80 | Body io.Reader 81 | ModTime time.Time 82 | Data interface{} 83 | } 84 | 85 | //FileLoader loads files needed for processing. 86 | // the filepaths can be relative or absolute. 87 | FileLoader interface { 88 | Load(string) ([]string, error) 89 | } 90 | 91 | //FrontMatter extracts frontmatter from a text file 92 | FrontMatter interface { 93 | Parse(io.Reader) (front map[string]interface{}, body io.Reader, err error) 94 | } 95 | 96 | //Renderer generates static pages 97 | Renderer interface { 98 | Before(root string) error 99 | Render(root string, pages PageList, opts ...interface{}) error 100 | After(root string) error 101 | } 102 | 103 | //Generator is a static site generator 104 | Generator interface { 105 | FileLoader 106 | FrontMatter 107 | Renderer 108 | } 109 | ) 110 | 111 | //HTML returns body text as html. 112 | func (p *Page) HTML() template.HTML { 113 | b, _ := ioutil.ReadAll(p.Body) 114 | return template.HTML(mark.New(string(b), mark.DefaultOptions()).Render()) 115 | } 116 | 117 | // 118 | // 119 | // Sort Implementation for Pagelist 120 | // 121 | // 122 | 123 | func (p PageList) Len() int { return len(p) } 124 | func (p PageList) Less(i, j int) bool { return p[i].ModTime.Before(p[j].ModTime) } 125 | func (p PageList) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 126 | 127 | // GetAllSections filter the pagelist for any section informations 128 | // it returns a map of all the sections with the pages matching the 129 | // section attached as a pagelist. 130 | func GetAllSections(p PageList) map[string]PageList { 131 | sections := make(map[string]PageList) 132 | for k := range p { 133 | page := p[k] 134 | data := page.Data.(map[string]interface{}) 135 | section := defaultSection 136 | 137 | if sec, ok := data[pageSection]; ok { 138 | switch sec.(type) { 139 | case string: 140 | section = sec.(string) 141 | } 142 | } 143 | if sdata, ok := sections[section]; ok { 144 | sdata = append(sdata, page) 145 | sections[section] = sdata 146 | continue 147 | } 148 | pList := make(PageList, 1) 149 | pList[0] = page 150 | sections[section] = pList 151 | } 152 | 153 | // sort the result before returning 154 | for key := range sections { 155 | sort.Sort(sections[key]) 156 | } 157 | return sections 158 | } 159 | -------------------------------------------------------------------------------- /front.go: -------------------------------------------------------------------------------- 1 | package bongo 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/json" 7 | "errors" 8 | "io" 9 | "strings" 10 | 11 | "gopkg.in/yaml.v2" 12 | ) 13 | 14 | var ( 15 | //ErrIsEmpty is an error indicating no front matter was found 16 | ErrIsEmpty = errors.New("an empty file") 17 | 18 | //ErrUnknownDelim is returned when the delimiters are not known by the 19 | //FrontMatter implementation. 20 | ErrUnknownDelim = errors.New("unknown delim") 21 | 22 | defaultDelim = "---" 23 | ) 24 | 25 | type ( 26 | //HandlerFunc is an interface for a function that process front matter text. 27 | HandlerFunc func(string) (map[string]interface{}, error) 28 | ) 29 | 30 | //Matter is all what matters here. 31 | type Matter struct { 32 | handlers map[string]HandlerFunc 33 | delim string 34 | lastDelim bool 35 | lastIndex int 36 | } 37 | 38 | func newMatter() *Matter { 39 | return &Matter{handlers: make(map[string]HandlerFunc)} 40 | } 41 | 42 | //NewYAML returns a new FrontMatter implementation with support for yaml frontmatter. 43 | // default delimiters is --- 44 | func NewYAML(opts ...string) *Matter { 45 | delim := defaultDelim 46 | if len(opts) > 0 { 47 | delim = opts[0] 48 | } 49 | m := newMatter() 50 | m.Handle(delim, YAMLHandler) 51 | return m 52 | } 53 | 54 | //NewJSON returns a new FrontMatter implementation with support for json frontmatter. 55 | // default delimiters is --- 56 | func NewJSON(opts ...string) *Matter { 57 | delim := defaultDelim 58 | if len(opts) > 0 { 59 | delim = opts[0] 60 | } 61 | m := newMatter() 62 | m.Handle(delim, JSONHandler) 63 | return m 64 | } 65 | 66 | //Handle registers a handler for the given frontmatter delimiter 67 | func (m *Matter) Handle(delim string, fn HandlerFunc) { 68 | m.handlers[delim] = fn 69 | } 70 | 71 | // Parse parses the input and extract the frontmatter 72 | func (m *Matter) Parse(input io.Reader) (front map[string]interface{}, body io.Reader, err error) { 73 | return m.parse(input) 74 | } 75 | func (m *Matter) parse(input io.Reader) (front map[string]interface{}, body io.Reader, err error) { 76 | var getFront = func(f string) string { 77 | return strings.TrimSpace(strings.TrimPrefix(strings.TrimSuffix(f, m.delim), m.delim)) 78 | } 79 | f, body, err := m.splitFront(input) 80 | if err != nil { 81 | return nil, nil, err 82 | } 83 | 84 | h := m.handlers[f[:3]] 85 | front, err = h(getFront(f)) 86 | if err != nil { 87 | return nil, nil, err 88 | } 89 | 90 | return front, body, nil 91 | 92 | } 93 | func sniffDelim(input []byte) (string, error) { 94 | if len(input) < 4 { 95 | return "", ErrIsEmpty 96 | } 97 | return string(input[:3]), nil 98 | } 99 | 100 | func (m *Matter) splitFront(input io.Reader) (front string, body io.Reader, err error) { 101 | s := bufio.NewScanner(input) 102 | s.Split(m.split) 103 | var ( 104 | f string 105 | b string 106 | ) 107 | n := 0 108 | for s.Scan() { 109 | if n == 0 { 110 | f = s.Text() 111 | } else { 112 | b = b + s.Text() 113 | } 114 | n++ 115 | 116 | } 117 | return f, strings.NewReader(b), s.Err() 118 | } 119 | 120 | //split implements bufio.SplitFunc for spliting fron matter from the body text. 121 | func (m *Matter) split(data []byte, atEOF bool) (advance int, token []byte, err error) { 122 | if atEOF && len(data) == 0 { 123 | return 0, nil, nil 124 | } 125 | if m.delim == "" { 126 | delim, err := sniffDelim(data) 127 | if err != nil { 128 | return 0, nil, err 129 | } 130 | m.delim = delim 131 | } 132 | if _, ok := m.handlers[m.delim]; !ok { 133 | return 0, nil, ErrUnknownDelim 134 | } 135 | if x := bytes.Index(data, []byte(m.delim)); x >= 0 { 136 | // check the next delim index 137 | if next := bytes.Index(data[x+len(m.delim):], []byte(m.delim)); next > 0 { 138 | if !m.lastDelim { 139 | m.lastDelim = true 140 | m.lastIndex = next + len(m.delim) 141 | return next + len(m.delim)*2, dropSpace(data[x : next+len(m.delim)]), nil 142 | } 143 | } 144 | } 145 | if atEOF { 146 | return len(data), data, nil 147 | } 148 | return 0, nil, nil 149 | } 150 | 151 | func dropSpace(d []byte) []byte { 152 | return bytes.TrimSpace(d) 153 | } 154 | 155 | //JSONHandler implements HandlerFunc interface. It extracts front matter data from the given 156 | // string argument by interpreting it as a json string. 157 | func JSONHandler(front string) (map[string]interface{}, error) { 158 | var rst interface{} 159 | err := json.Unmarshal([]byte(front), &rst) 160 | if err != nil { 161 | return nil, err 162 | } 163 | return rst.(map[string]interface{}), nil 164 | } 165 | 166 | //YAMLHandler decodes ymal string into a go map[string]interface{} 167 | func YAMLHandler(front string) (map[string]interface{}, error) { 168 | out := make(map[string]interface{}) 169 | err := yaml.Unmarshal([]byte(front), out) 170 | if err != nil { 171 | return nil, err 172 | } 173 | return out, nil 174 | } 175 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package bongo is an elegant static website generator. It is designed to be simple minimal 3 | and easy to use. 4 | 5 | Bongo comes in two flavors. The commandline applicaion and the library. 6 | 7 | Commandline 8 | 9 | The commandline application can be found in cmd/bongo directory 10 | 11 | and you can install it via go get like this 12 | 13 | go get github.com/gernest/bongo/cmd/bongo 14 | 15 | Or just download the latest binary here https://github.com/gernest/bongo/releases/latest 16 | 17 | To build your project foo. 18 | 19 | * You can specify the path to foo 20 | 21 | bongo build --source path/to/foo 22 | 23 | * You can run at the root of foo 24 | 25 | cd path/to/foo 26 | 27 | bongo build 28 | 29 | To serve your project locally. This will run a local server at port http://localhost:8000. 30 | The project will be rebuilt if any markdown file changes. 31 | 32 | * You can specify the path to foo 33 | 34 | bongo serve --source path/to/foo 35 | 36 | * You can run at the root of foo 37 | 38 | cd path/to/foo 39 | 40 | bongo serve 41 | 42 | 43 | The generated website will be in the directory _site at the root of your foo project. 44 | 45 | 46 | The Website Project Structure 47 | 48 | There is no restriction on how you arrange your project. If you have a project foo. 49 | It will be somewhare in a directory named foo. You can see the example in testdata/sample directory. 50 | 51 | 52 | Bongo only process markdown files found in your project root.Supported file extensions 53 | for the markdown files are 54 | 55 | .md , .MD , .mdown, and .markdown 56 | 57 | This means you can put your markdown files in any nested directories inside your project 58 | and bongo will process them without any problem. Bongo support github flavored markdown 59 | 60 | Optionaly, you can add sitewide configuration file `_bongo.yml` at the root of your project. 61 | The configuration is in yaml format. And there are a few settings you can change. 62 | 63 | static 64 | This is a list of static directories(relative from the project root). If defined 65 | the directories will be copied to the output directory as is. 66 | 67 | title 68 | The string representing the title of the project. 69 | 70 | subtitle 71 | The string representing subtitle of the project 72 | 73 | theme 74 | The name of the theme to use. Note that, bongo comes with a default theme called gh. 75 | Only if you have a theme installed in the _themes directory at the root of your project 76 | will you neeed to specify this. 77 | 78 | 79 | Themes 80 | 81 | There is loose restrictions in the how to create your own theme. What matters is that you have 82 | the following templates. 83 | 84 | index.html 85 | - used to render index pages for sections 86 | 87 | home.html 88 | - used to render home page 89 | 90 | page.html 91 | - used to render arbitrary pages 92 | 93 | post.html 94 | - used to render the posts 95 | 96 | 97 | These templates can be used in project, by setting the view value of frontmatter. For instance 98 | if I set view to post, then post.html will be used on that particular file. 99 | 100 | IMPORTANT: All static contents should be placed in a diretory named static at the root of the 101 | theme. They will be copied to the output directory unchanged. 102 | 103 | All themes custom themes should live under the _theme directory at the project root. Please 104 | see testdata/sample/_themes for an example. 105 | 106 | 107 | Frontmatter 108 | 109 | Bongo support frontmatter. And it is recomended every post(your markdown file) should have 110 | a frontmatter. For convenience, only YAML frontmatter is supported by default. And you can 111 | add it at the beginning of your file like this. 112 | 113 | --- 114 | title: chapter one 115 | section: blog 116 | --- 117 | 118 | Your post contents goes here. 119 | 120 | Important frontmatter settings, 121 | 122 | title 123 | -The title of the post 124 | 125 | section 126 | - this acts as a category of sort. You can specify any section that the 127 | post will reside. 128 | 129 | Sections are in the form of relative directory paths. for instance the following 130 | are valid sections blog, blog/funny, blog/happy, blog/stuffs. 131 | 132 | If you specify section as blog/golang. bongo will put the generated html files in the 133 | folder named blog/golang. And you can referance your post by blog/golang/mypost.html. 134 | where mypost is the name of your markdown file. 135 | 136 | The default section is home. 137 | 138 | view 139 | - specifies the template to render the content.Defaults to post. 140 | 141 | 142 | 143 | The Library 144 | 145 | Bongo is modular, and uses interfaces to define its components.The most important interface is 146 | the Generator interface. 147 | 148 | So, you can implement your own Generator interface, and pass it to the bongo library to have your 149 | own static website generator with your own rules. 150 | 151 | I challenge you, to try implementing different Generators. Or, implement different components of the 152 | generator interface. I have default implementations shipped with bongo. 153 | 154 | */ 155 | package bongo 156 | -------------------------------------------------------------------------------- /docs/doc.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Elegant static website generation with Go 3 | section: blog 4 | --- 5 | 6 | bongo is an elegant static website generator. It is designed to be simple minimal 7 | and easy to use. 8 | 9 | Bongo comes in two flavors. The commandline applicaion and the library. 10 | 11 | Commandline 12 | 13 | The commandline application can be found in cmd/bongo directory 14 | 15 | and you can install it via go get like this 16 | 17 | go get github.com/gernest/bongo/cmd/bongo 18 | 19 | Or just download the latest binary here https://github.com/gernest/bongo/releases/latest 20 | 21 | To build your project foo. 22 | 23 | * You can specify the path to foo 24 | 25 | bongo build --source path/to/foo 26 | 27 | * You can run at the root of foo 28 | 29 | cd path/to/foo 30 | 31 | bongo build 32 | 33 | To serve your project locally. This will run a local server at port http://localhost:8000. 34 | The project will be rebuilt if any markdown file changes. 35 | 36 | * You can specify the path to foo 37 | 38 | bongo serve --source path/to/foo 39 | 40 | * You can run at the root of foo 41 | 42 | cd path/to/foo 43 | 44 | bongo serve 45 | 46 | 47 | The generated website will be in the directory _site at the root of your foo project. 48 | 49 | 50 | The Website Project Structure 51 | 52 | There is no restriction on how you arrange your project. If you have a project foo. 53 | It will be somewhare in a directory named foo. You can see the example in testdata/sample directory. 54 | 55 | 56 | Bongo only process markdown files found in your project root.Supported file extensions 57 | for the markdown files are 58 | 59 | .md , .MD , .mdown, and .markdown 60 | 61 | This means you can put your markdown files in any nested directories inside your project 62 | and bongo will process them without any problem. Bongo support github flavored markdown 63 | 64 | Optionaly, you can add sitewide configuration file `_bongo.yml` at the root of your project. 65 | The configuration is in yaml format. And there are a few settings you can change. 66 | 67 | static 68 | This is a list of static directories(relative from the project root). If defined 69 | the directories will be copied to the output directory as is. 70 | 71 | title 72 | The string representing the title of the project. 73 | 74 | subtitle 75 | The string representing subtitle of the project 76 | 77 | theme 78 | The name of the theme to use. Note that, bongo comes with a default theme called gh. 79 | Only if you have a theme installed in the _themes directory at the root of your project 80 | will you neeed to specify this. 81 | 82 | 83 | Themes 84 | 85 | There is loose restrictions in the how to create your own theme. What matters is that you have 86 | the following templates. 87 | 88 | index.html 89 | - used to render index pages for sections 90 | 91 | home.html 92 | - used to render home page 93 | 94 | page.html 95 | - used to render arbitrary pages 96 | 97 | post.html 98 | - used to render the posts 99 | 100 | 101 | These templates can be used in project, by setting the view value of frontmatter. For instance 102 | if I set view to post, then post.html will be used on that particular file. 103 | 104 | IMPORTANT: All static contents should be placed in a diretory named static at the root of the 105 | theme. They will be copied to the output directory unchanged. 106 | 107 | All themes custom themes should live under the _theme directory at the project root. Please 108 | see testdata/sample/_themes for an example. 109 | 110 | 111 | Frontmatter 112 | 113 | Bongo support frontmatter. And it is recomended every post(your markdown file) should have 114 | a frontmatter. For convenience, only YAML frontmatter is supported by default. And you can 115 | add it at the beginning of your file like this. 116 | 117 | --- 118 | title: chapter one 119 | section: blog 120 | --- 121 | 122 | Your post contents goes here. 123 | 124 | Important frontmatter settings, 125 | 126 | title 127 | -The title of the post 128 | 129 | section 130 | - this acts as a category of sort. You can specify any section that the 131 | post will reside. 132 | 133 | Sections are in the form of relative directory paths. for instance the following 134 | are valid sections blog, blog/funny, blog/happy, blog/stuffs. 135 | 136 | If you specify section as blog/golang. bongo will put the generated html files in the 137 | folder named blog/golang. And you can referance your post by blog/golang/mypost.html. 138 | where mypost is the name of your markdown file. 139 | 140 | The default section is home. 141 | 142 | view 143 | - specifies the template to render the content.Defaults to post. 144 | 145 | 146 | 147 | The Library 148 | 149 | Bongo is modular, and uses interfaces to define its components.The most important interface is 150 | the Generator interface. 151 | 152 | So, you can implement your own Generator interface, and pass it to the bongo library to have your 153 | own static website generator with your own rules. 154 | 155 | I challenge you, to try implementing different Generators. Or, implement different components of the 156 | generator interface. I have default implementations shipped with bongo. 157 | 158 | 159 | -------------------------------------------------------------------------------- /testdata/sample/1.2.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: chapter two 3 | section: blog 4 | --- 5 | 6 | ## How the infastructure works 7 | 8 | * The smsd daemon checks for rexeived sms, If it finds any, It stores to the databsse backend in our case `postgresql` and the name of the database will be in the `.gammu-smsdrc` configuration file. 9 | 10 | * After saving the sms, the daemon executes `pesambili` binary 11 | 12 | * `pesambili` binary calls post request to the `sweetjesus` localserver running on port 8000. 13 | 14 | * The `sweetjesus` server when receives the ping at the `/mpesa` endpoint, it assumes there is an inbox, goes to the sms daemon server database and collects all inbox messages and process them. 15 | 16 | * Well, we should make sure processed messages are moved to another table to avoid mayhem 17 | 18 | It is beyond the scope of this post to go in great details how each component work, I will rather focus on how to work with each compoent to make the system tick. Oh! well, I will try to dig a bit about important bits though. 19 | 20 | 21 | ### Installing and configuring gammu-smsd 22 | ______ 23 | 24 | The first challenge is to install and configure gammu-smsd. SMSD stands for SMS daemon. 25 | as the name suggests, it runs cron jobs on top of gammu. Gammu is the software 26 | for accessing functionality of mobile phones in the desktop computers. 27 | 28 | I am using linux mint, and I assume you are a technical reader, so enough of talking 29 | Time to write some code 30 | 31 | Install gammu, wammu, and gammu-smsd 32 | 33 | sudo apt-get install wammu gammu gammu-smsd 34 | 35 | Noye that wammu is optional, but its good to use wammu configuring the devices. When the 36 | installation is complete, 37 | 38 | Start wammu with privillege 39 | 40 | sudo wammu 41 | 42 | Now the wammu window will appear, and follow The instruction to configure your device 43 | I am using a ZTE modem and it is mounted at `/dv/tty/usb2`. 44 | 45 | When you have finished configuring gammu then a file `.gammurc` will be created at the home 46 | path of your pc. 47 | 48 | $ cat .gammurc 49 | [gammu] 50 | port=/dev/ttyUSB2 51 | connection=at 52 | name=Vodafone_Modem 53 | 54 | I have given the name `vodacom_modem` to my modem, you can name whatever you want. 55 | 56 | So, the Real part was making gammu-smsd to work. 57 | 58 | First create a database `smsd` I am using postgres database so I did like this 59 | 60 | $ createdb -O postgres smsd 61 | 62 | The default user is postgres, so I create a database names `smsd` and give ownership 63 | to postgres. 64 | 65 | Now, there is a modified postgresl schema gist here [ gammu-smsd postgres schema](https://gist.github.com/gernest/a8b9f1fc8474bd92c75c), download it 66 | and upack somewhere you can access. 67 | 68 | Navigate to the psql.sql file 69 | 70 | $ cd path/to/psql/file 71 | 72 | 73 | Execute the following command 74 | 75 | $ psql -d smsd -f pgsql.sql 76 | 77 | You will see output on the terminal showing what tables are creates 78 | 79 | Ok, time to write a gammu-smsd configuration. 80 | First navigate back home. 81 | 82 | $ cd /path/to/home 83 | 84 | Orjust type 85 | 86 | $ cd ~ 87 | 88 | Okay, I will use linux magic to copy fontent of our `.gammurc` file we created 89 | ealier to our new `.gammu-smsdrc` file 90 | 91 | $ cat .gammurc > .gammu-smsdrc 92 | 93 | Okay, we will need now to edit our .gammu-smsdrc 94 | 95 | $ nano .gammu-smsdrc 96 | 97 | Edit it to look like this 98 | 99 | [gammu] 100 | port=/dev/ttyUSB2 101 | connection=at 102 | name=Vodafone_Modem 103 | 104 | [smsd] 105 | service=sql 106 | driver=native_pgsql 107 | host=localhost 108 | database=smsd 109 | user=postgres 110 | password=postgres 111 | logfile=/home/YOUR_USERNAME/sms.log 112 | 113 | 114 | You can change the values whatever you like, and everything is clear, Note that 115 | I am running a test database with user postgres and password postgres so you can put whatever you 116 | like. And dont forget to replace `YOUR_USERNAME` with your pc username 117 | 118 | Okay, we almost done, time to start our service. 119 | 120 | $ gammu-smsd -c .gammu-smsdrc 121 | 122 | You will see a path to log file, and If nothing else is printed just know we have configured everything right. 123 | 124 | What gammu-smsd will do is listen for incoming sms and after receiving them stores them in the database. Smsd has many cool stuffs, but In only interested in processing the received sms which are stores in the inbox table. 125 | 126 | 127 | 128 | ### The `Pesambili` script 129 | 130 | Well, there is a hook on the `smsd` daemon called `onReceive` . It tells the daemon what to call when there is a received sms. Shell scripting is the way to go, but I wanted to focus more on go, so instead of writing a shell script, I wrote just a wrapper in go, In the hood there is a `curl` command invoked hence `pesambili` go package. 131 | 132 | You can install this package by this command 133 | 134 | $ go get github.com/gernest/pesambili 135 | 136 | If I have time I will remove the `curl` dependency and use `net/http` package instead. You should note that, I assume you have added `$GOSRC/bin` to your system path environment variables(I mean you have a working `go` environment) 137 | 138 | __suorce code__ for `pesambili` is available here [github.com/gernest/pesambili](https://github.com/gernest/pesambili) 139 | 140 | 141 | ### The `sweetjesus` server 142 | 143 | This is a very crucial part of the system, Its the place where actual work is done. This server uses the `net/http` package. It listens on port `8000` and register only one handler at the endpoint `/mpesa`. 144 | 145 | So, the only varid connection to the server is through the `url` `http://localhost:8000/mpesa`. For the moment, the server only accepts post request, and funny enough it doesn't give a shit about the received data. 146 | 147 | The point of the request is just to make it know that there is an sms in our inbox. 148 | 149 | You can install this server by running the following command. 150 | 151 | $ go get github.com/gernest/sweetjesus 152 | 153 | There is still room for improvement though, so contributions are welcome 154 | 155 | __source code__ for `sweetjesus` server is available here [github.com/gernest/sweetjesus](https://github.com/gernest/sweetjesus) 156 | 157 | ### The `jesus` library 158 | 159 | This is backbone of the `sweetjesus` server. I Contains the utility functions, the table structures and many other goodies. Its in an awkward state right now but I think its design makes sense. 160 | 161 | There is a great deal in parsing and extracting information from a received m-pesa sms. So instead of creating a lexer and parser, I just rolled an ad-hoc implementation for extracting the data in the `processor.go` file. 162 | 163 | I will try to improve stuffs when I get the time. The part that I will be happy to receive contributions is for the `FilterFunctions`. I will write more about the filter functions when I write a a dedicated post about the library. 164 | 165 | _Be Warned_ This is a one week hack, please dont blame me for anything weird in it, but dont be selfish to report any bugs 166 | 167 | You can install this by the following command 168 | 169 | $ go get github.com/gernst/jesus 170 | 171 | __source code__ for `jesus` packageis available here [github.com/gernest/jesus](https://github.com/gernest/jesus) 172 | -------------------------------------------------------------------------------- /testdata/data/post.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Implementing M-PESA processing system in GO 3 | section: blog 4 | --- 5 | 6 | ## How the infastructure works 7 | 8 | * The smsd daemon checks for rexeived sms, If it finds any, It stores to the databsse backend in our case `postgresql` and the name of the database will be in the `.gammu-smsdrc` configuration file. 9 | 10 | * After saving the sms, the daemon executes `pesambili` binary 11 | 12 | * `pesambili` binary calls post request to the `sweetjesus` localserver running on port 8000. 13 | 14 | * The `sweetjesus` server when receives the ping at the `/mpesa` endpoint, it assumes there is an inbox, goes to the sms daemon server database and collects all inbox messages and process them. 15 | 16 | * Well, we should make sure processed messages are moved to another table to avoid mayhem 17 | 18 | It is beyond the scope of this post to go in great details how each component work, I will rather focus on how to work with each compoent to make the system tick. Oh! well, I will try to dig a bit about important bits though. 19 | 20 | 21 | ### Installing and configuring gammu-smsd 22 | ______ 23 | 24 | The first challenge is to install and configure gammu-smsd. SMSD stands for SMS daemon. 25 | as the name suggests, it runs cron jobs on top of gammu. Gammu is the software 26 | for accessing functionality of mobile phones in the desktop computers. 27 | 28 | I am using linux mint, and I assume you are a technical reader, so enough of talking 29 | Time to write some code 30 | 31 | Install gammu, wammu, and gammu-smsd 32 | 33 | sudo apt-get install wammu gammu gammu-smsd 34 | 35 | Noye that wammu is optional, but its good to use wammu configuring the devices. When the 36 | installation is complete, 37 | 38 | Start wammu with privillege 39 | 40 | sudo wammu 41 | 42 | Now the wammu window will appear, and follow The instruction to configure your device 43 | I am using a ZTE modem and it is mounted at `/dv/tty/usb2`. 44 | 45 | When you have finished configuring gammu then a file `.gammurc` will be created at the home 46 | path of your pc. 47 | 48 | $ cat .gammurc 49 | [gammu] 50 | port=/dev/ttyUSB2 51 | connection=at 52 | name=Vodafone_Modem 53 | 54 | I have given the name `vodacom_modem` to my modem, you can name whatever you want. 55 | 56 | So, the Real part was making gammu-smsd to work. 57 | 58 | First create a database `smsd` I am using postgres database so I did like this 59 | 60 | $ createdb -O postgres smsd 61 | 62 | The default user is postgres, so I create a database names `smsd` and give ownership 63 | to postgres. 64 | 65 | Now, there is a modified postgresl schema gist here [ gammu-smsd postgres schema](https://gist.github.com/gernest/a8b9f1fc8474bd92c75c), download it 66 | and upack somewhere you can access. 67 | 68 | Navigate to the psql.sql file 69 | 70 | $ cd path/to/psql/file 71 | 72 | 73 | Execute the following command 74 | 75 | $ psql -d smsd -f pgsql.sql 76 | 77 | You will see output on the terminal showing what tables are creates 78 | 79 | Ok, time to write a gammu-smsd configuration. 80 | First navigate back home. 81 | 82 | $ cd /path/to/home 83 | 84 | Orjust type 85 | 86 | $ cd ~ 87 | 88 | Okay, I will use linux magic to copy fontent of our `.gammurc` file we created 89 | ealier to our new `.gammu-smsdrc` file 90 | 91 | $ cat .gammurc > .gammu-smsdrc 92 | 93 | Okay, we will need now to edit our .gammu-smsdrc 94 | 95 | $ nano .gammu-smsdrc 96 | 97 | Edit it to look like this 98 | 99 | [gammu] 100 | port=/dev/ttyUSB2 101 | connection=at 102 | name=Vodafone_Modem 103 | 104 | [smsd] 105 | service=sql 106 | driver=native_pgsql 107 | host=localhost 108 | database=smsd 109 | user=postgres 110 | password=postgres 111 | logfile=/home/YOUR_USERNAME/sms.log 112 | 113 | 114 | You can change the values whatever you like, and everything is clear, Note that 115 | I am running a test database with user postgres and password postgres so you can put whatever you 116 | like. And dont forget to replace `YOUR_USERNAME` with your pc username 117 | 118 | Okay, we almost done, time to start our service. 119 | 120 | $ gammu-smsd -c .gammu-smsdrc 121 | 122 | You will see a path to log file, and If nothing else is printed just know we have configured everything right. 123 | 124 | What gammu-smsd will do is listen for incoming sms and after receiving them stores them in the database. Smsd has many cool stuffs, but In only interested in processing the received sms which are stores in the inbox table. 125 | 126 | 127 | 128 | ### The `Pesambili` script 129 | 130 | Well, there is a hook on the `smsd` daemon called `onReceive` . It tells the daemon what to call when there is a received sms. Shell scripting is the way to go, but I wanted to focus more on go, so instead of writing a shell script, I wrote just a wrapper in go, In the hood there is a `curl` command invoked hence `pesambili` go package. 131 | 132 | You can install this package by this command 133 | 134 | $ go get github.com/gernest/pesambili 135 | 136 | If I have time I will remove the `curl` dependency and use `net/http` package instead. You should note that, I assume you have added `$GOSRC/bin` to your system path environment variables(I mean you have a working `go` environment) 137 | 138 | __suorce code__ for `pesambili` is available here [github.com/gernest/pesambili](https://github.com/gernest/pesambili) 139 | 140 | 141 | ### The `sweetjesus` server 142 | 143 | This is a very crucial part of the system, Its the place where actual work is done. This server uses the `net/http` package. It listens on port `8000` and register only one handler at the endpoint `/mpesa`. 144 | 145 | So, the only varid connection to the server is through the `url` `http://localhost:8000/mpesa`. For the moment, the server only accepts post request, and funny enough it doesn't give a shit about the received data. 146 | 147 | The point of the request is just to make it know that there is an sms in our inbox. 148 | 149 | You can install this server by running the following command. 150 | 151 | $ go get github.com/gernest/sweetjesus 152 | 153 | There is still room for improvement though, so contributions are welcome 154 | 155 | __source code__ for `sweetjesus` server is available here [github.com/gernest/sweetjesus](https://github.com/gernest/sweetjesus) 156 | 157 | ### The `jesus` library 158 | 159 | This is backbone of the `sweetjesus` server. I Contains the utility functions, the table structures and many other goodies. Its in an awkward state right now but I think its design makes sense. 160 | 161 | There is a great deal in parsing and extracting information from a received m-pesa sms. So instead of creating a lexer and parser, I just rolled an ad-hoc implementation for extracting the data in the `processor.go` file. 162 | 163 | I will try to improve stuffs when I get the time. The part that I will be happy to receive contributions is for the `FilterFunctions`. I will write more about the filter functions when I write a a dedicated post about the library. 164 | 165 | _Be Warned_ This is a one week hack, please dont blame me for anything weird in it, but dont be selfish to report any bugs 166 | 167 | You can install this by the following command 168 | 169 | $ go get github.com/gernst/jesus 170 | 171 | __source code__ for `jesus` packageis available here [github.com/gernest/jesus](https://github.com/gernest/jesus) 172 | -------------------------------------------------------------------------------- /render.go: -------------------------------------------------------------------------------- 1 | package bongo 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "html/template" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | 13 | "github.com/Unknwon/com" 14 | "github.com/gernest/gh" 15 | "gopkg.in/yaml.v2" 16 | ) 17 | 18 | var ( 19 | defaultTemplates *template.Template 20 | defaultTheme = "gh" 21 | defalutTplExtensions = []string{".html", ".tpl", ".tmpl"} 22 | ) 23 | 24 | const ( 25 | indexPage = "index.html" 26 | statusPassed = iota 27 | statusFailed 28 | statusComplete 29 | ) 30 | 31 | type buildStatus struct { 32 | Status int 33 | Message string 34 | } 35 | 36 | func newStatus(code int, msg string) *buildStatus { 37 | return &buildStatus{Status: code, Message: msg} 38 | } 39 | 40 | func init() { 41 | defaultTemplates = template.New("bongo") 42 | for _, n := range gh.AssetNames() { 43 | if filepath.Ext(n) != ".html" { 44 | continue 45 | } 46 | tx := defaultTemplates.New(filepath.Join("gh", n)) 47 | d, err := gh.Asset(n) 48 | if err != nil { 49 | panic(err) 50 | } 51 | _, err = tx.Parse(string(d)) 52 | if err != nil { 53 | panic(err) 54 | } 55 | } 56 | log.SetFlags(log.Lshortfile) 57 | } 58 | 59 | //DefaultRenderer is the default REnderer implementation 60 | type DefaultRenderer struct { 61 | config map[string]interface{} 62 | rendr *template.Template 63 | root string 64 | } 65 | 66 | // Before loads configurations and prepare rendering stuffs 67 | func (d *DefaultRenderer) Before(root string) error { 68 | cfg, rendr, err := load(root) 69 | if err != nil { 70 | return err 71 | } 72 | d.config = cfg 73 | d.rendr = rendr 74 | d.root = root 75 | return nil 76 | } 77 | 78 | // Render builds a static site 79 | func (d *DefaultRenderer) Render(root string, pages PageList, opts ...interface{}) error { 80 | baseInfo, err := os.Stat(root) 81 | if err != nil { 82 | return err 83 | } 84 | buf := &bytes.Buffer{} 85 | 86 | buildDIr := filepath.Join(root, OutputDir) 87 | if err = prepareBuild(baseInfo, buildDIr); err != nil { 88 | return err 89 | } 90 | themeName := d.getTheme() 91 | 92 | allsections := GetAllSections(pages) 93 | for key := range allsections { 94 | setionPages := allsections[key] 95 | view := DefaultView 96 | data := make(map[string]interface{}) 97 | 98 | data[CurrentSectionKey] = setionPages 99 | data[AllSectionsKey] = allsections 100 | 101 | for _, page := range setionPages { 102 | switch page.Data.(type) { 103 | case map[string]interface{}: 104 | if v, ok := page.Data.(map[string]interface{})[DefaultView]; ok { 105 | view = v.(string) 106 | } 107 | } 108 | data[DefaultPageKey] = page 109 | 110 | buf.Reset() 111 | rerr := d.rendr.ExecuteTemplate(buf, fmt.Sprintf("%s/%s.html", themeName, view), data) 112 | if rerr != nil { 113 | break 114 | } 115 | 116 | destFileName := strings.Replace(filepath.Base(page.Path), filepath.Ext(page.Path), DefaultExt, -1) 117 | destDir := filepath.Join(buildDIr, key) 118 | os.MkdirAll(destDir, baseInfo.Mode()) 119 | destFile := filepath.Join(destDir, destFileName) 120 | 121 | ioerr := ioutil.WriteFile(destFile, buf.Bytes(), DefaultPerm) 122 | if ioerr != nil { 123 | break 124 | } 125 | 126 | } 127 | 128 | // write the index page for the section. 129 | buf.Reset() 130 | 131 | rerr := d.rendr.ExecuteTemplate(buf, filepath.Join(themeName, DefaultTpl.Index), data) 132 | if rerr != nil { 133 | return rerr 134 | } 135 | os.MkdirAll(filepath.Join(buildDIr, key), baseInfo.Mode()) 136 | 137 | destIndexFile := filepath.Join(buildDIr, key, indexPage) 138 | ioerr := ioutil.WriteFile(destIndexFile, buf.Bytes(), DefaultPerm) 139 | if ioerr != nil { 140 | return ioerr 141 | } 142 | 143 | } 144 | 145 | // write home page. 146 | buf.Reset() 147 | 148 | data := make(map[string]interface{}) 149 | data[AllSectionsKey] = allsections 150 | data[SiteConfigKey] = d.config 151 | 152 | rerr := d.rendr.ExecuteTemplate(buf, filepath.Join(themeName, DefaultTpl.Home), data) 153 | if rerr != nil { 154 | return rerr 155 | } 156 | 157 | homePage := filepath.Join(buildDIr, indexPage) 158 | ioerr := ioutil.WriteFile(homePage, buf.Bytes(), DefaultPerm) 159 | if ioerr != nil { 160 | return ioerr 161 | } 162 | return nil 163 | } 164 | 165 | func (d *DefaultRenderer) getTheme() string { 166 | return d.config[ThemeKey].(string) 167 | } 168 | 169 | // After copies relevant static files to the generated site 170 | func (d *DefaultRenderer) After(root string) error { 171 | return d.copyStatic() 172 | } 173 | 174 | func (d *DefaultRenderer) copyStatic() error { 175 | theme := d.getTheme() 176 | out := d.getOutputDir() 177 | switch theme { 178 | case defaultTheme: 179 | info, _ := os.Stat(d.root) 180 | for _, f := range gh.AssetNames() { 181 | if filepath.Ext(f) == ".html" { 182 | continue 183 | } 184 | base := filepath.Join(out, filepath.Dir(f)) 185 | os.MkdirAll(base, info.Mode()) 186 | b, err := gh.Asset(f) 187 | if err != nil { 188 | return err 189 | } 190 | err = ioutil.WriteFile(filepath.Join(out, f), b, DefaultPerm) 191 | if err != nil { 192 | log.Println(err, base) 193 | return err 194 | } 195 | } 196 | return nil 197 | default: 198 | // we copy the static directory in the current theme directory 199 | staticDir := filepath.Join(d.root, ThemeDir, theme, StaticDir) 200 | if com.IsExist(staticDir) { 201 | err := com.CopyDir(staticDir, filepath.Join(d.root, OutputDir, StaticDir)) 202 | if err != nil { 203 | return err 204 | } 205 | } 206 | 207 | } 208 | 209 | // if we have the static set on the config file we use it. 210 | if staticDirective, ok := d.config[StaticDir]; ok { 211 | switch staticDirective.(type) { 212 | case []interface{}: 213 | sd := staticDirective.([]interface{}) 214 | for _, f := range sd { 215 | dir := f.(string) 216 | err := com.CopyDir(filepath.Join(d.root, dir), filepath.Join(out, dir)) 217 | if err != nil { 218 | return err 219 | } 220 | } 221 | } 222 | } 223 | return nil 224 | } 225 | 226 | func (d *DefaultRenderer) getOutputDir() string { 227 | return filepath.Join(d.root, OutputDir) 228 | } 229 | 230 | //NewDefaultRenderer returns default Renderer implementation 231 | func NewDefaultRenderer() *DefaultRenderer { 232 | return &DefaultRenderer{config: make(map[string]interface{})} 233 | } 234 | 235 | func prepareBuild(baseInfo os.FileInfo, buildDir string) error { 236 | // If there is already a built project we remove it and start afresh 237 | info, err := os.Stat(buildDir) 238 | if err != nil { 239 | if os.IsNotExist(err) { 240 | oerr := os.MkdirAll(buildDir, baseInfo.Mode()) 241 | if oerr != nil { 242 | return fmt.Errorf("create build dir at %s %v", buildDir, err) 243 | } 244 | } 245 | } else { 246 | oerr := os.RemoveAll(buildDir) 247 | if oerr != nil { 248 | return fmt.Errorf("cleaning %s %v", buildDir, oerr) 249 | } 250 | oerr = os.MkdirAll(buildDir, info.Mode()) 251 | if oerr != nil { 252 | return fmt.Errorf("creating %s %v", buildDir, oerr) 253 | } 254 | } 255 | return nil 256 | } 257 | 258 | //Rollback delets the build directory 259 | func Rollback(root string) { 260 | buildDIr := filepath.Join(root, OutputDir) 261 | os.RemoveAll(buildDIr) 262 | } 263 | 264 | func loadTheme(base, name string, tpl *template.Template) error { 265 | themesDir := filepath.Join(base, ThemeDir) 266 | return filepath.Walk(filepath.Join(themesDir, name), func(path string, info os.FileInfo, err error) error { 267 | if err != nil { 268 | return err 269 | } 270 | if info.IsDir() || !HasExt(path, defalutTplExtensions...) { 271 | return nil 272 | } 273 | b, err := ioutil.ReadFile(path) 274 | if err != nil { 275 | return err 276 | } 277 | n := strings.TrimPrefix(path, themesDir) 278 | tx := tpl.New(n[1:]) 279 | _, err = tx.Parse(string(b)) 280 | if err != nil { 281 | return err 282 | } 283 | return nil 284 | }) 285 | 286 | } 287 | 288 | func load(root string) (map[string]interface{}, *template.Template, error) { 289 | cfg := loadConfig(root) 290 | tpl := template.New("bongo") 291 | if cfg != nil { 292 | if tName, ok := cfg[ThemeKey]; ok { 293 | terr := loadTheme(root, tName.(string), tpl) 294 | if terr != nil { 295 | return nil, nil, terr 296 | } 297 | return cfg, tpl, nil 298 | } 299 | cfg[ThemeKey] = defaultTheme 300 | return cfg, defaultTemplates, nil 301 | } 302 | c := make(map[string]interface{}) 303 | c[ThemeKey] = defaultTheme 304 | return c, defaultTemplates, nil 305 | } 306 | 307 | func loadConfig(root string) map[string]interface{} { 308 | configPath := filepath.Join(root, DefaultConfigFile) 309 | b, err := ioutil.ReadFile(configPath) 310 | if err != nil { 311 | return nil 312 | } 313 | m := make(map[string]interface{}) 314 | err = yaml.Unmarshal(b, m) 315 | if err != nil { 316 | log.Fatalf("loading config %v \n", err) 317 | } 318 | return m 319 | } 320 | --------------------------------------------------------------------------------