├── 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 [](https://travis-ci.org/gernest/bongo) [](https://coveralls.io/github/gernest/bongo?branch=master) [](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 |
--------------------------------------------------------------------------------