diff options
author | Steve Manuel <nilslice@gmail.com> | 2016-09-22 17:47:14 -0700 |
---|---|---|
committer | Steve Manuel <nilslice@gmail.com> | 2016-09-22 17:47:14 -0700 |
commit | 27c175f6c626175598ee0a3d5c7b750a84d583e2 (patch) | |
tree | 563cd0451d81464afb1269d7f703ddb8e4d9d9f0 | |
parent | f31c13d76e5de8ab0420ecaf0f570e52f6218066 (diff) |
adding a generator for custom post content types, slug for url based on title, main file to manage commands
-rw-r--r-- | cmd/cms/generator.go | 144 | ||||
-rw-r--r-- | cmd/cms/main.go | 58 | ||||
-rw-r--r-- | cmd/cms/server.go | 7 | ||||
-rw-r--r-- | content/item.go | 4 | ||||
-rw-r--r-- | content/post.go | 16 | ||||
-rw-r--r-- | management/editor/editor.go | 6 | ||||
-rw-r--r-- | management/manager/process.go | 68 | ||||
-rw-r--r-- | system/admin/admin.go | 14 | ||||
-rw-r--r-- | system/admin/auth.go | 11 | ||||
-rw-r--r-- | system/admin/config.go | 5 | ||||
-rw-r--r-- | system/db/query.go | 30 |
11 files changed, 354 insertions, 9 deletions
diff --git a/cmd/cms/generator.go b/cmd/cms/generator.go new file mode 100644 index 0000000..5ea296c --- /dev/null +++ b/cmd/cms/generator.go @@ -0,0 +1,144 @@ +package main + +import ( + "fmt" + "html/template" + "os" + "path/filepath" + "strings" +) + +func generateContentType(name string) error { + fileName := strings.ToLower(name) + ".go" + typeName := strings.ToUpper(string(name[0])) + string(name[1:]) + + // contain processed name an info for template + data := map[string]string{ + "name": typeName, + "initial": string(fileName[0]), + } + + // open file in ./content/ dir + // if exists, alert user of conflict + pwd, err := os.Getwd() + if err != nil { + return err + } + + contentDir := filepath.Join(pwd, "content") + filePath := filepath.Join(contentDir, fileName) + + if _, err := os.Stat(filePath); !os.IsNotExist(err) { + return fmt.Errorf("Please remove '%s' before executing this command.", fileName) + } + + // no file exists.. ok to write new one + file, err := os.Create(filePath) + defer file.Close() + if err != nil { + return err + } + + // execute template + tmpl := template.Must(template.New("content").Parse(contentTypeTmpl)) + err = tmpl.Execute(file, data) + if err != nil { + return err + } + + return nil +} + +const contentTypeTmpl = ` +package content + +import ( + "fmt" + + "github.com/nilslice/cms/management/editor" +) + +// {{ .name }} is the generic content struct +type {{ .name }} struct { + Item + editor editor.Editor + +/* + // all your custom fields must have json tags! + Title string ` + "`json:" + `"title"` + "`" + ` + Content string ` + "`json:" + `"content"` + "`" + ` + Author string ` + "`json:" + `"author"` + "`" + ` + Timestamp string ` + "`json:" + `"timestamp"` + "`" + ` +*/ +} + +func init() { + Types["{{ .name }}"] = func() interface{} { return new({{ .name }}) } +} + +// SetContentID partially implements editor.Editable +func ({{ .initial }} *{{ .name }}) SetContentID(id int) { {{ .initial }}.ID = id } + +// ContentID partially implements editor.Editable +func ({{ .initial }} *{{ .name }}) ContentID() int { return {{ .initial }}.ID } + +// ContentName partially implements editor.Editable +func ({{ .initial }} *{{ .name }}) ContentName() string { return {{ .initial }}.Title } + +// SetSlug partially implements editor.Editable +func ({{ .initial }} *{{ .name }}) SetSlug(slug string) { {{ .initial }}.Slug = slug } + +// Editor partially implements editor.Editable +func ({{ .initial }} *{{ .name }}) Editor() *editor.Editor { return &{{ .initial }}.editor } + +// MarshalEditor writes a buffer of html to edit a {{ .name }} and partially implements editor.Editable +func ({{ .initial }} *{{ .name }}) MarshalEditor() ([]byte, error) { +/* EXAMPLE CODE (from post.go, the default content type) + view, err := editor.Form({{ .initial }}, + editor.Field{ + // Take careful note that the first argument to these Input-like methods + // is the string version of each {{ .name }} struct tag, and must follow this pattern + // for auto-decoding and -encoding reasons. + View: editor.Input("Slug", {{ .initial }}, map[string]string{ + "label": "URL Path", + "type": "text", + "disabled": "true", + "placeholder": "Will be set automatically", + }), + }, + editor.Field{ + View: editor.Input("Title", {{ .initial }}, map[string]string{ + "label": "{{ .name }} Title", + "type": "text", + "placeholder": "Enter your {{ .name }} Title here", + }), + }, + editor.Field{ + View: editor.Textarea("Content", {{ .initial }}, map[string]string{ + "label": "Content", + "placeholder": "Add the content of your {{ .name }} here", + }), + }, + editor.Field{ + View: editor.Input("Author", {{ .initial }}, map[string]string{ + "label": "Author", + "type": "text", + "placeholder": "Enter the author name here", + }), + }, + editor.Field{ + View: editor.Input("Timestamp", {{ .initial }}, map[string]string{ + "label": "Publish Date", + "type": "date", + }), + }, + ) + + if err != nil { + return nil, fmt.Errorf("Failed to render {{ .name }} editor view: %s", err.Error()) + } + + return view, nil +*/ +} +` diff --git a/cmd/cms/main.go b/cmd/cms/main.go new file mode 100644 index 0000000..bf779c2 --- /dev/null +++ b/cmd/cms/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "flag" + "fmt" + "os" +) + +var usage = ` +$ cms <option> <params> + +Options: +generate, gen, g: + Generate a new content type file with boilerplat code to implement + the editor.Editable interface. Must be given one (1) parameter of + the name of the type for the new content. + + Example: + $ cms gen Review + +` + +func init() { + flag.Usage = func() { + fmt.Println(usage) + } +} + +func main() { + flag.Parse() + + args := flag.Args() + if len(args) < 1 { + flag.PrintDefaults() + os.Exit(0) + } + + fmt.Println(args) + switch args[0] { + case "generate", "gen", "g": + if len(args) < 2 { + flag.PrintDefaults() + os.Exit(0) + } + + name := args[1] + + err := generateContentType(name) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + case "serve", "s": + serve() + default: + flag.PrintDefaults() + } +} diff --git a/cmd/cms/server.go b/cmd/cms/server.go index d8fa224..d3037d2 100644 --- a/cmd/cms/server.go +++ b/cmd/cms/server.go @@ -13,7 +13,7 @@ import ( "github.com/nilslice/cms/system/db" ) -func main() { +func init() { http.HandleFunc("/admin", func(res http.ResponseWriter, req *http.Request) { adminView := admin.Admin(nil) @@ -76,6 +76,8 @@ func main() { res.WriteHeader(http.StatusInternalServerError) return } + } else { + post.(editor.Editable).SetContentID(-1) } m, err := manager.Manage(post.(editor.Editable), t) @@ -113,7 +115,8 @@ func main() { http.Redirect(res, req, desURL, http.StatusFound) } }) +} +func serve() { http.ListenAndServe(":8080", nil) - } diff --git a/content/item.go b/content/item.go index 8b834e4..08d6359 100644 --- a/content/item.go +++ b/content/item.go @@ -1,7 +1,7 @@ package content // Item should only be embedded into content type structs. -// Helper for DB-related actions type Item struct { - ID int `json:"id"` + ID int `json:"id"` + Slug string `json:"slug"` } diff --git a/content/post.go b/content/post.go index 4690f41..b12eb1d 100644 --- a/content/post.go +++ b/content/post.go @@ -21,18 +21,32 @@ func init() { Types["Post"] = func() interface{} { return new(Post) } } +// SetContentID partially implements editor.Editable +func (p *Post) SetContentID(id int) { p.ID = id } + // ContentID partially implements editor.Editable func (p *Post) ContentID() int { return p.ID } // ContentName partially implements editor.Editable func (p *Post) ContentName() string { return p.Title } +// SetSlug partially implements editor.Editable +func (p *Post) SetSlug(slug string) { p.Slug = slug } + // Editor partially implements editor.Editable func (p *Post) Editor() *editor.Editor { return &p.editor } // MarshalEditor writes a buffer of html to edit a Post and partially implements editor.Editable func (p *Post) MarshalEditor() ([]byte, error) { - view, err := editor.New(p, + view, err := editor.Form(p, + editor.Field{ + View: editor.Input("Slug", p, map[string]string{ + "label": "URL Path", + "type": "text", + "disabled": "true", + "placeholder": "Will be set automatically", + }), + }, editor.Field{ View: editor.Input("Title", p, map[string]string{ "label": "Post Title", diff --git a/management/editor/editor.go b/management/editor/editor.go index 0acf03e..f49c4e3 100644 --- a/management/editor/editor.go +++ b/management/editor/editor.go @@ -6,8 +6,10 @@ import "bytes" // Editable ensures data is editable type Editable interface { + SetContentID(id int) ContentID() int ContentName() string + SetSlug(slug string) Editor() *Editor MarshalEditor() ([]byte, error) } @@ -23,9 +25,9 @@ type Field struct { View []byte } -// New takes editable content and any number of Field funcs to describe the edit +// Form 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) { +func Form(post Editable, fields ...Field) ([]byte, error) { editor := post.Editor() editor.ViewBuf = &bytes.Buffer{} diff --git a/management/manager/process.go b/management/manager/process.go new file mode 100644 index 0000000..ddae024 --- /dev/null +++ b/management/manager/process.go @@ -0,0 +1,68 @@ +package manager + +import ( + "regexp" + "strings" + "unicode" + + "github.com/nilslice/cms/management/editor" + "golang.org/x/text/transform" + "golang.org/x/text/unicode/norm" +) + +// Slug returns a URL friendly string from the title of a post item +func Slug(e editor.Editable) (string, error) { + // get the name of the post item + name := e.ContentName() + + // filter out non-alphanumeric character or non-whitespace + slug, err := stringToSlug(name) + if err != nil { + return "", err + } + + return slug, nil +} + +func isMn(r rune) bool { + return unicode.Is(unicode.Mn, r) // Mn: nonspacing marks +} + +// modified version of: https://www.socketloop.com/tutorials/golang-format-strings-to-seo-friendly-url-example +func stringToSlug(s string) (string, error) { + src := []byte(strings.ToLower(s)) + + // convert all spaces to dash + rx := regexp.MustCompile("[[:space:]]") + src = rx.ReplaceAll(src, []byte("-")) + + // remove all blanks such as tab + rx = regexp.MustCompile("[[:blank:]]") + src = rx.ReplaceAll(src, []byte("")) + + rx = regexp.MustCompile("[!/:-@[-`{-~]") + src = rx.ReplaceAll(src, []byte("")) + + rx = regexp.MustCompile("/[^\x20-\x7F]/") + src = rx.ReplaceAll(src, []byte("")) + + rx = regexp.MustCompile("`&(amp;)?#?[a-z0-9]+;`i") + src = rx.ReplaceAll(src, []byte("-")) + + rx = regexp.MustCompile("`&([a-z])(acute|uml|circ|grave|ring|cedil|slash|tilde|caron|lig|quot|rsquo);`i") + src = rx.ReplaceAll(src, []byte("\\1")) + + rx = regexp.MustCompile("`[^a-z0-9]`i") + src = rx.ReplaceAll(src, []byte("-")) + + rx = regexp.MustCompile("`[-]+`") + src = rx.ReplaceAll(src, []byte("-")) + + t := transform.Chain(norm.NFD, transform.RemoveFunc(isMn), norm.NFC) + slug, _, err := transform.String(t, string(src)) + if err != nil { + return "", err + } + + return strings.TrimSpace(slug), nil +} diff --git a/system/admin/admin.go b/system/admin/admin.go index bbc34cf..8febbf7 100644 --- a/system/admin/admin.go +++ b/system/admin/admin.go @@ -4,6 +4,7 @@ package admin import ( "bytes" + "fmt" "html/template" "github.com/nilslice/cms/content" @@ -13,6 +14,17 @@ const adminHTML = `<!doctype html> <html> <head> <title>CMS</title> + <style type="text/css"> + label { + display: block; + margin-top: 11px; + } + input { + display: block; + margin-bottom: 11px; + padding: 2px; + } + </style> </head> <body> <h1><a href="/admin">CMS</a></h1> @@ -43,6 +55,8 @@ func Admin(manager []byte) []byte { Subview: template.HTML(manager), } + fmt.Println(a.Types) + buf := &bytes.Buffer{} tmpl := template.Must(template.New("admin").Parse(adminHTML)) tmpl.Execute(buf, a) diff --git a/system/admin/auth.go b/system/admin/auth.go new file mode 100644 index 0000000..153a31a --- /dev/null +++ b/system/admin/auth.go @@ -0,0 +1,11 @@ +package admin + +// Session ... +type Session struct { + User + token string +} + +// User ... +type User struct { +} diff --git a/system/admin/config.go b/system/admin/config.go new file mode 100644 index 0000000..e067299 --- /dev/null +++ b/system/admin/config.go @@ -0,0 +1,5 @@ +package admin + +// Config represents the confirgurable options of the system +type Config struct { +} diff --git a/system/db/query.go b/system/db/query.go index fd526f8..80a3412 100644 --- a/system/db/query.go +++ b/system/db/query.go @@ -12,6 +12,8 @@ import ( "github.com/boltdb/bolt" "github.com/gorilla/schema" "github.com/nilslice/cms/content" + "github.com/nilslice/cms/management/editor" + "github.com/nilslice/cms/management/manager" ) var store *bolt.DB @@ -22,6 +24,19 @@ func init() { if err != nil { log.Fatal(err) } + + // initialize db with all content type buckets + store.Update(func(tx *bolt.Tx) error { + for t := range content.Types { + _, err := tx.CreateBucketIfNotExists([]byte(t)) + if err != nil { + return err + } + } + + return nil + }) + } // Set inserts or updates values in the database. @@ -30,8 +45,13 @@ func Set(target string, data url.Values) (int, error) { t := strings.Split(target, ":") ns, id := t[0], t[1] - // check if content has an id, and if not get new one from target bucket - if len(id) == 0 { + // check if content id == -1 (indicating new post). + // if so, run an insert which will assign the next auto incremented int. + // this is done because boltdb begins its bucket auto increment value at 0, + // which is the zero-value of an int in the Item struct field for ID. + // this is a problem when the original first post (with auto ID = 0) gets + // overwritten by any new post, originally having no ID, defauting to 0. + if id == "-1" { return insert(ns, data) } @@ -125,6 +145,12 @@ func toJSON(ns string, data url.Values) ([]byte, error) { return nil, err } + slug, err := manager.Slug(post.(editor.Editable)) + if err != nil { + return nil, err + } + post.(editor.Editable).SetSlug(slug) + // marshall content struct to json for db storage j, err := json.Marshal(post) if err != nil { |