summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSteve Manuel <nilslice@gmail.com>2016-09-22 17:47:14 -0700
committerSteve Manuel <nilslice@gmail.com>2016-09-22 17:47:14 -0700
commit27c175f6c626175598ee0a3d5c7b750a84d583e2 (patch)
tree563cd0451d81464afb1269d7f703ddb8e4d9d9f0
parentf31c13d76e5de8ab0420ecaf0f570e52f6218066 (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.go144
-rw-r--r--cmd/cms/main.go58
-rw-r--r--cmd/cms/server.go7
-rw-r--r--content/item.go4
-rw-r--r--content/post.go16
-rw-r--r--management/editor/editor.go6
-rw-r--r--management/manager/process.go68
-rw-r--r--system/admin/admin.go14
-rw-r--r--system/admin/auth.go11
-rw-r--r--system/admin/config.go5
-rw-r--r--system/db/query.go30
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 {