diff options
-rw-r--r-- | editor/editor.go | 41 | ||||
-rw-r--r-- | editor/element.go | 115 | ||||
-rw-r--r-- | post.go | 93 |
3 files changed, 249 insertions, 0 deletions
diff --git a/editor/editor.go b/editor/editor.go new file mode 100644 index 0000000..11dc064 --- /dev/null +++ b/editor/editor.go @@ -0,0 +1,41 @@ +// Package editor enables users to create edit views from their content +// structs so that admins can manage content +package editor + +import "bytes" + +// Editable ensures data is editable +type Editable interface { + Editor() *Editor + NewViewBuffer() + Render() []byte +} + +// Editor is a view containing fields to manage content +type Editor struct { + ViewBuf *bytes.Buffer +} + +// Field is used to create the editable view for a field +// within a particular content struct +type Field struct { + View []byte +} + +// New takes editable content and any number of Field funcs to describe the edit +// page for any content struct added by a user +func New(post Editable, fields ...Field) ([]byte, error) { + post.NewViewBuffer() + + editor := post.Editor() + + for _, f := range fields { + addFieldToEditorView(editor, f) + } + + return post.Render(), nil +} + +func addFieldToEditorView(e *Editor, f Field) { + e.ViewBuf.Write(f.View) +} diff --git a/editor/element.go b/editor/element.go new file mode 100644 index 0000000..519ce5d --- /dev/null +++ b/editor/element.go @@ -0,0 +1,115 @@ +package editor + +import ( + "bytes" + "reflect" +) + +// Input returns the []byte of an <input> HTML element with a label. +// IMPORTANT: +// The `fieldName` argument will cause a panic if it is not exactly the string +// form of the struct field that this editor input is representing +func Input(fieldName string, p interface{}, attrs map[string]string) []byte { + var wrapInLabel = true + label, found := attrs["label"] + if !found { + wrapInLabel = false + label = "" + } + + e := newElement("input", label, fieldName, p, attrs) + + return domElementSelfClose(e, wrapInLabel) +} + +// Textarea returns the []byte of a <textarea> HTML element with a label. +// IMPORTANT: +// The `fieldName` argument will cause a panic if it is not exactly the string +// form of the struct field that this editor input is representing +func Textarea(fieldName string, p interface{}, attrs map[string]string) []byte { + var wrapInLabel = true + label, found := attrs["label"] + if !found { + wrapInLabel = false + label = "" + } + + e := newElement("textarea", label, fieldName, p, attrs) + + return domElement(e, wrapInLabel) +} + +type element struct { + TagName string + Attrs map[string]string + Name string + label string + data []byte + viewBuf *bytes.Buffer +} + +// domElementSelfClose is a special DOM element which is parsed as a +// self-closing tag and thus needs to be created differently +func domElementSelfClose(e *element, wrapInLabel bool) []byte { + if wrapInLabel { + e.viewBuf.Write([]byte(`<label>` + e.label + `</label>`)) + } + e.viewBuf.Write([]byte(`<` + e.TagName + ` value="`)) + e.viewBuf.Write(append(e.data, []byte(`" `)...)) + + for attr, value := range e.Attrs { + e.viewBuf.Write([]byte(attr + `="` + string(value) + `"`)) + } + e.viewBuf.Write([]byte(` />`)) + + return e.viewBuf.Bytes() +} + +// domElement creates a DOM element +func domElement(e *element, wrapInLabel bool) []byte { + if wrapInLabel { + e.viewBuf.Write([]byte(`<label>` + e.label + `</label>`)) + } + e.viewBuf.Write([]byte(`<` + e.TagName + ` `)) + + for attr, value := range e.Attrs { + e.viewBuf.Write([]byte(attr + `="` + string(value) + `"`)) + } + e.viewBuf.Write([]byte(` >`)) + + e.viewBuf.Write([]byte(e.data)) + e.viewBuf.Write([]byte(`</` + e.TagName + `>`)) + + return e.viewBuf.Bytes() +} + +func tagNameFromStructField(name string, post interface{}) string { + field, ok := reflect.TypeOf(post).Elem().FieldByName(name) + if !ok { + panic("Couldn't get struct field for: " + name + ". Make sure you pass the right field name to editor field elements.") + } + + tag, ok := field.Tag.Lookup("json") + if !ok { + panic("Couldn't get json struct tag for: " + name + ". Struct fields for content types must have 'json' tags.") + } + + return tag +} + +func valueFromStructField(name string, post interface{}) []byte { + field := reflect.Indirect(reflect.ValueOf(post)).FieldByName(name) + + return field.Bytes() +} + +func newElement(tagName, label, fieldName string, p interface{}, attrs map[string]string) *element { + return &element{ + TagName: tagName, + Attrs: attrs, + Name: tagNameFromStructField(fieldName, p), + label: label, + data: valueFromStructField(fieldName, p), + viewBuf: &bytes.Buffer{}, + } +} @@ -0,0 +1,93 @@ +package main + +import ( + "bytes" + "fmt" + "net/http" + + "github.com/nilslice/cms/editor" +) + +// Post is the generic content struct +type Post struct { + editor editor.Editor + + Title []byte `json:"title"` + Content []byte `json:"content"` + Author []byte `json:"author"` + Timestamp []byte `json:"timestamp"` +} + +// Editor partially implements editor.Editable +func (p *Post) Editor() *editor.Editor { + return &p.editor +} + +// NewViewBuffer partially implements editor.Editable +func (p *Post) NewViewBuffer() { + p.editor.ViewBuf = &bytes.Buffer{} +} + +// Render partially implements editor.Editable +func (p *Post) Render() []byte { + return p.editor.ViewBuf.Bytes() +} + +// EditView writes a buffer of html to edit a Post +func (p Post) EditView() ([]byte, error) { + view, err := editor.New(&p, + editor.Field{ + View: editor.Input("Title", &p, map[string]string{ + "label": "Post Title", + "type": "text", + "placeholder": "Enter your Post Title here", + }), + }, + editor.Field{ + View: editor.Textarea("Content", &p, map[string]string{ + "label": "Content", + "placeholder": "Add the content of your post here", + }), + }, + editor.Field{ + View: editor.Input("Author", &p, map[string]string{ + "label": "Author", + "type": "text", + "placeholder": "Enter the author name here", + }), + }, + editor.Field{ + View: editor.Input("Timestamp", &p, map[string]string{ + "label": "Publish Date", + "type": "date", + }), + }, + ) + + if err != nil { + return nil, fmt.Errorf("Failed to render Post editor view: %s", err.Error()) + } + + return view, nil +} + +func (p Post) ServeHTTP(res http.ResponseWriter, req *http.Request) { + res.Header().Set("Content-type", "text/html") + resp, err := p.EditView() + if err != nil { + fmt.Println(err) + } + res.Write(resp) +} + +func main() { + p := Post{ + Content: []byte("<h3>H</h3>ello. My name is <em>Steve</em>."), + Title: []byte("Profound introduction"), + Author: []byte("Steve Manuel"), + Timestamp: []byte("2016-09-16"), + } + + http.ListenAndServe(":8080", p) + +} |