summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--editor/editor.go41
-rw-r--r--editor/element.go115
-rw-r--r--post.go93
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{},
+ }
+}
diff --git a/post.go b/post.go
new file mode 100644
index 0000000..3ae0b92
--- /dev/null
+++ b/post.go
@@ -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)
+
+}