summaryrefslogtreecommitdiff
path: root/system/item
diff options
context:
space:
mode:
authorSteve <nilslice@gmail.com>2016-12-19 11:19:53 -0800
committerGitHub <noreply@github.com>2016-12-19 11:19:53 -0800
commit3791fadda7b761ffba38c567da29e2e71acd1dfb (patch)
tree79d810f9aafa1868ee0760983937470d0eea3db8 /system/item
parentb20c5bdee38682edc851e646d815a34689c3c923 (diff)
[addons] Creating foundation for plugin-like system "Addons" (#24)
* adding addons dir and sample addon which enables the use of a new input element in forms for referencing other content. "addons" is a conceptual plugin-like feature, similar to wordpress "plugins" dir, but not as sophisticated
Diffstat (limited to 'system/item')
-rw-r--r--system/item/item.go209
-rw-r--r--system/item/types.go21
2 files changed, 230 insertions, 0 deletions
diff --git a/system/item/item.go b/system/item/item.go
new file mode 100644
index 0000000..a813669
--- /dev/null
+++ b/system/item/item.go
@@ -0,0 +1,209 @@
+package item
+
+import (
+ "fmt"
+ "net/http"
+ "regexp"
+ "strings"
+ "unicode"
+
+ uuid "github.com/satori/go.uuid"
+ "golang.org/x/text/transform"
+ "golang.org/x/text/unicode/norm"
+)
+
+// Sluggable makes a struct locatable by URL with it's own path
+// As an Item implementing Sluggable, slugs may overlap. If this is an issue,
+// make your content struct (or one which imbeds Item) implement Sluggable
+// and it will override the slug created by Item's SetSlug with your struct's
+type Sluggable interface {
+ SetSlug(string)
+ ItemSlug() string
+}
+
+// Identifiable enables a struct to have its ID set/get. Typically this is done
+// to set an ID to -1 indicating it is new for DB inserts, since by default
+// a newly initialized struct would have an ID of 0, the int zero-value, and
+// BoltDB's starting key per bucket is 0, thus overwriting the first record.
+type Identifiable interface {
+ ItemID() int
+ SetItemID(int)
+ UniqueID() uuid.UUID
+ String() string
+}
+
+// Sortable ensures data is sortable by time
+type Sortable interface {
+ Time() int64
+ Touch() int64
+}
+
+// Hookable provides our user with an easy way to intercept or add functionality
+// to the different lifecycles/events a struct may encounter. Item implements
+// Hookable with no-ops so our user can override only whichever ones necessary.
+type Hookable interface {
+ BeforeSave(req *http.Request) error
+ AfterSave(req *http.Request) error
+
+ BeforeDelete(req *http.Request) error
+ AfterDelete(req *http.Request) error
+
+ BeforeApprove(req *http.Request) error
+ AfterApprove(req *http.Request) error
+
+ BeforeReject(req *http.Request) error
+ AfterReject(req *http.Request) error
+}
+
+// Item should only be embedded into content type structs.
+type Item struct {
+ UUID uuid.UUID `json:"uuid"`
+ ID int `json:"id"`
+ Slug string `json:"slug"`
+ Timestamp int64 `json:"timestamp"`
+ Updated int64 `json:"updated"`
+}
+
+// Time partially implements the Sortable interface
+func (i Item) Time() int64 {
+ return i.Timestamp
+}
+
+// Touch partially implements the Sortable interface
+func (i Item) Touch() int64 {
+ return i.Updated
+}
+
+// SetSlug sets the item's slug for its URL
+func (i *Item) SetSlug(slug string) {
+ i.Slug = slug
+}
+
+// ItemSlug sets the item's slug for its URL
+func (i *Item) ItemSlug() string {
+ return i.Slug
+}
+
+// ItemID gets the Item's ID field
+// partially implements the Identifiable interface
+func (i Item) ItemID() int {
+ return i.ID
+}
+
+// SetItemID sets the Item's ID field
+// partially implements the Identifiable interface
+func (i *Item) SetItemID(id int) {
+ i.ID = id
+}
+
+// UniqueID gets the Item's UUID field
+// partially implements the Identifiable interface
+func (i Item) UniqueID() uuid.UUID {
+ return i.UUID
+}
+
+// String formats an Item into a printable value
+// partially implements the Identifiable interface
+func (i Item) String() string {
+ return fmt.Sprintf("Item ID: %s", i.UniqueID())
+}
+
+// BeforeSave is a no-op to ensure structs which embed Item implement Hookable
+func (i Item) BeforeSave(req *http.Request) error {
+ return nil
+}
+
+// AfterSave is a no-op to ensure structs which embed Item implement Hookable
+func (i Item) AfterSave(req *http.Request) error {
+ return nil
+}
+
+// BeforeDelete is a no-op to ensure structs which embed Item implement Hookable
+func (i Item) BeforeDelete(req *http.Request) error {
+ return nil
+}
+
+// AfterDelete is a no-op to ensure structs which embed Item implement Hookable
+func (i Item) AfterDelete(req *http.Request) error {
+ return nil
+}
+
+// BeforeApprove is a no-op to ensure structs which embed Item implement Hookable
+func (i Item) BeforeApprove(req *http.Request) error {
+ return nil
+}
+
+// AfterApprove is a no-op to ensure structs which embed Item implement Hookable
+func (i Item) AfterApprove(req *http.Request) error {
+ return nil
+}
+
+// BeforeReject is a no-op to ensure structs which embed Item implement Hookable
+func (i Item) BeforeReject(req *http.Request) error {
+ return nil
+}
+
+// AfterReject is a no-op to ensure structs which embed Item implement Hookable
+func (i Item) AfterReject(req *http.Request) error {
+ return nil
+}
+
+// Slug returns a URL friendly string from the title of a post item
+func Slug(i Identifiable) (string, error) {
+ // get the name of the post item
+ name := strings.TrimSpace(i.String())
+
+ // 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("-"))
+
+ str := strings.Replace(string(src), "'", "", -1)
+ str = strings.Replace(str, `"`, "", -1)
+
+ t := transform.Chain(norm.NFD, transform.RemoveFunc(isMn), norm.NFC)
+ slug, _, err := transform.String(t, str)
+ if err != nil {
+ return "", err
+ }
+
+ return strings.TrimSpace(slug), nil
+}
diff --git a/system/item/types.go b/system/item/types.go
new file mode 100644
index 0000000..33e9ced
--- /dev/null
+++ b/system/item/types.go
@@ -0,0 +1,21 @@
+package item
+
+const (
+ // ErrTypeNotRegistered means content type isn't registered (not found in Types map)
+ ErrTypeNotRegistered = `Error:
+There is no type registered for %[1]s
+
+Add this to the file which defines %[1]s{} in the 'content' package:
+
+
+ func init() {
+ item.Types["%[1]s"] = func() interface{} { return new(%[1]s) }
+ }
+
+
+`
+)
+
+// Types is a map used to reference a type name to its actual Editable type
+// mainly for lookups in /admin route based utilities
+var Types = make(map[string]func() interface{})