diff options
author | Steve <nilslice@gmail.com> | 2016-12-19 11:19:53 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2016-12-19 11:19:53 -0800 |
commit | 3791fadda7b761ffba38c567da29e2e71acd1dfb (patch) | |
tree | 79d810f9aafa1868ee0760983937470d0eea3db8 /system/item | |
parent | b20c5bdee38682edc851e646d815a34689c3c923 (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.go | 209 | ||||
-rw-r--r-- | system/item/types.go | 21 |
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{}) |