├── .travis.yml ├── LICENCE ├── README.md ├── front.go ├── front_test.go └── testdata ├── front ├── body.md ├── empty.md └── json.md └── sample.yml /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.6 4 | before_install: 5 | - go get github.com/axw/gocov/gocov 6 | - go get github.com/mattn/goveralls 7 | - if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi 8 | script: 9 | - $HOME/gopath/bin/goveralls -service=travis-ci -repotoken=$COVERALLS 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # front [![Build Status](https://travis-ci.org/gernest/front.svg)](https://travis-ci.org/gernest/front) [![GoDoc](https://godoc.org/github.com/gernest/front?status.svg)](https://godoc.org/github.com/gernest/front)[![Coverage Status](https://coveralls.io/repos/gernest/front/badge.svg?branch=master&service=github)](https://coveralls.io/github/gernest/front?branch=master) 2 | 3 | extracts frontmatter from text files with ease. 4 | 5 | ## Features 6 | * Custom delimiters. You are free to register any delimiter of your choice. Provided its a three character string. e.g `+++`, `$$$`, `---`, `%%%` 7 | * Custom Handlers. Anything that implements `HandlerFunc` can be used to decode the values from the frontmatter text, you can see the `JSONHandler` for how to implement one. 8 | * Support YAML frontmatter 9 | * Support JSON frontmatter. 10 | 11 | ## Installation 12 | 13 | go get github.com/gernest/front 14 | 15 | ## How to use 16 | 17 | ```go 18 | package main 19 | 20 | import ( 21 | "fmt" 22 | "strings" 23 | 24 | "github.com/gernest/front" 25 | ) 26 | 27 | var txt = `+++ 28 | { 29 | "title":"front" 30 | } 31 | +++ 32 | 33 | # Body 34 | Over my dead body 35 | ` 36 | 37 | func main() { 38 | m := front.NewMatter() 39 | m.Handle("+++", front.JSONHandler) 40 | f, body, err := m.Parse(strings.NewReader(txt)) 41 | if err != nil { 42 | panic(err) 43 | } 44 | 45 | fmt.Printf("The front matter is:\n%#v\n", f) 46 | fmt.Printf("The body is:\n%q\n", body) 47 | } 48 | ``` 49 | 50 | Please see the tests formore details 51 | 52 | ## Licence 53 | 54 | This project is under the MIT Licence. See the [LICENCE](LICENCE) file for the full license text. 55 | 56 | -------------------------------------------------------------------------------- /front.go: -------------------------------------------------------------------------------- 1 | // Package front is a frontmatter extraction library. 2 | package front 3 | 4 | import ( 5 | "bufio" 6 | "bytes" 7 | "encoding/json" 8 | "errors" 9 | "io" 10 | "strings" 11 | 12 | "gopkg.in/yaml.v2" 13 | ) 14 | 15 | var ( 16 | //ErrIsEmpty is an error indicating no front matter was found 17 | ErrIsEmpty = errors.New("front: an empty file") 18 | 19 | //ErrUnknownDelim is returned when the delimiters are not known by the 20 | //FrontMatter implementation. 21 | ErrUnknownDelim = errors.New("front: unknown delim") 22 | ) 23 | 24 | type ( 25 | //HandlerFunc is an interface for a function that process front matter text. 26 | HandlerFunc func(string) (map[string]interface{}, error) 27 | ) 28 | 29 | //Matter is all what matters here. 30 | type Matter struct { 31 | handlers map[string]HandlerFunc 32 | } 33 | 34 | //NewMatter creates a new Matter instance 35 | func NewMatter() *Matter { 36 | return &Matter{handlers: make(map[string]HandlerFunc)} 37 | } 38 | 39 | //Handle registers a handler for the given frontmatter delimiter 40 | func (m *Matter) Handle(delim string, fn HandlerFunc) { 41 | m.handlers[delim] = fn 42 | } 43 | 44 | // Parse parses the input and extract the frontmatter 45 | func (m *Matter) Parse(input io.Reader) (front map[string]interface{}, body string, err error) { 46 | return m.parse(input) 47 | } 48 | func (m *Matter) parse(input io.Reader) (front map[string]interface{}, body string, err error) { 49 | var getFront = func(f string) string { 50 | return strings.TrimSpace(f[3:]) 51 | } 52 | f, body, err := m.splitFront(input) 53 | if err != nil { 54 | return nil, "", err 55 | } else if len(f) < 3 { 56 | return map[string]interface{}{}, body, nil 57 | } 58 | h := m.handlers[f[:3]] 59 | front, err = h(getFront(f)) 60 | if err != nil { 61 | return nil, "", err 62 | } 63 | return front, body, nil 64 | 65 | } 66 | func sniffDelim(input []byte) (string, error) { 67 | if len(input) < 4 { 68 | return "", ErrIsEmpty 69 | } 70 | return string(input[:3]), nil 71 | } 72 | 73 | func (m *Matter) splitFront(input io.Reader) (front, body string, err error) { 74 | bufsize := 1024 * 1024 75 | buf := make([]byte, bufsize) 76 | 77 | s := bufio.NewScanner(input) 78 | // Necessary so we can handle larger than default 4096b buffer 79 | s.Buffer(buf, bufsize) 80 | 81 | rst := make([]string, 2) 82 | s.Split(m.split) 83 | n := 0 84 | for s.Scan() { 85 | if n == 0 { 86 | rst[0] = s.Text() 87 | } else if n == 1 { 88 | rst[1] = s.Text() 89 | } 90 | n++ 91 | } 92 | if err = s.Err(); err != nil { 93 | return 94 | } 95 | return rst[0], rst[1], nil 96 | } 97 | 98 | //split implements bufio.SplitFunc for spliting front matter from the body text. 99 | func (m *Matter) split(data []byte, atEOF bool) (advance int, token []byte, err error) { 100 | if atEOF && len(data) == 0 { 101 | return 0, nil, nil 102 | } 103 | delim, err := sniffDelim(data) 104 | if err != nil { 105 | return 0, nil, err 106 | } 107 | if _, ok := m.handlers[delim]; !ok { 108 | return 0, nil, ErrUnknownDelim 109 | } 110 | if x := bytes.Index(data, []byte(delim)); x >= 0 { 111 | // check the next delim index 112 | if next := bytes.Index(data[x+len(delim):], []byte(delim)); next > 0 { 113 | return next + len(delim), dropSpace(data[:next+len(delim)]), nil 114 | } 115 | return len(data), dropSpace(data[x+len(delim):]), nil 116 | } 117 | if atEOF { 118 | return len(data), data, nil 119 | } 120 | return 0, nil, nil 121 | } 122 | 123 | func dropSpace(d []byte) []byte { 124 | return bytes.TrimSpace(d) 125 | } 126 | 127 | //JSONHandler implements HandlerFunc interface. It extracts front matter data from the given 128 | // string argument by interpreting it as a json string. 129 | func JSONHandler(front string) (map[string]interface{}, error) { 130 | var rst interface{} 131 | err := json.Unmarshal([]byte(front), &rst) 132 | if err != nil { 133 | return nil, err 134 | } 135 | return rst.(map[string]interface{}), nil 136 | } 137 | 138 | //YAMLHandler decodes ymal string into a go map[string]interface{} 139 | func YAMLHandler(front string) (map[string]interface{}, error) { 140 | out := make(map[string]interface{}) 141 | err := yaml.Unmarshal([]byte(front), out) 142 | if err != nil { 143 | return nil, err 144 | } 145 | return out, nil 146 | } 147 | -------------------------------------------------------------------------------- /front_test.go: -------------------------------------------------------------------------------- 1 | package front 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "testing" 7 | ) 8 | 9 | func TestMatter(t *testing.T) { 10 | bodyData, err := ioutil.ReadFile("testdata/front/body.md") 11 | if err != nil { 12 | t.Error(err) 13 | } 14 | m := NewMatter() 15 | m.Handle("+++", JSONHandler) 16 | b, err := ioutil.ReadFile("testdata/front/json.md") 17 | if err != nil { 18 | t.Error(err) 19 | } 20 | front, body, err := m.Parse(bytes.NewReader(b)) 21 | if err != nil { 22 | t.Error(err) 23 | } 24 | if body != string(bodyData) { 25 | t.Errorf("expected %s got %s", string(bodyData), body) 26 | } 27 | if _, ok := front["title"]; !ok { 28 | t.Error("expected front matter to contain title got nil instead") 29 | } 30 | } 31 | 32 | func TestYAMLHandler(t *testing.T) { 33 | data, err := ioutil.ReadFile("testdata/sample.yml") 34 | if err != nil { 35 | t.Fatal(err) 36 | } 37 | f, err := YAMLHandler(string(data)) 38 | if err != nil { 39 | t.Errorf("handling yaml %v", err) 40 | } 41 | if _, ok := f["language"]; !ok { 42 | t.Errorf("expected language got nil instead") 43 | } 44 | } 45 | 46 | func TestEmptyFile(t *testing.T) { 47 | data, err := ioutil.ReadFile("testdata/front/empty.md") 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | 52 | m := NewMatter() 53 | m.Handle("+++", JSONHandler) 54 | front, body, err := m.Parse(bytes.NewReader(data)) 55 | if err != nil { 56 | t.Error(err) 57 | } 58 | if len(front) != 0 { 59 | t.Fatal("front was not empty") 60 | } 61 | if len(body) != 0 { 62 | t.Fatal("body was not empty") 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /testdata/front/body.md: -------------------------------------------------------------------------------- 1 | # Body 2 | Over my dead body -------------------------------------------------------------------------------- /testdata/front/empty.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gernest/front/8a0b0a782d0a110a3c3be00169f5f823dfbd1f6e/testdata/front/empty.md -------------------------------------------------------------------------------- /testdata/front/json.md: -------------------------------------------------------------------------------- 1 | +++ 2 | { 3 | "title":"bongo" 4 | } 5 | +++ 6 | 7 | # Body 8 | Over my dead body -------------------------------------------------------------------------------- /testdata/sample.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.2 4 | - 1.3 5 | - release 6 | - tip 7 | before_install: 8 | - go get github.com/axw/gocov/gocov 9 | - go get github.com/mattn/goveralls 10 | - if ! go get code.google.com/p/go.tools/cmd/cover; then go get golang.org/x/tools/cmd/cover; fi 11 | script: 12 | - $HOME/gopath/bin/goveralls -service=travis-ci -repotoken=$COVERALLS --------------------------------------------------------------------------------