diff --git a/activitystreams/attachment.go b/activitystreams/attachment.go index d221169..f8763e3 100644 --- a/activitystreams/attachment.go +++ b/activitystreams/attachment.go @@ -1,33 +1,47 @@ package activitystreams import ( "mime" "strings" ) type Attachment struct { Type AttachmentType `json:"type"` URL string `json:"url"` MediaType string `json:"mediaType"` Name string `json:"name"` } type AttachmentType string -const AttachImage AttachmentType = "Image" +const ( + AttachImage AttachmentType = "Image" + AttachDocument AttachmentType = "Document" +) // NewImageAttachment creates a new Attachment from the given URL, setting the // correct type and automatically detecting the MediaType based on the file // extension. func NewImageAttachment(url string) Attachment { - var imgType string + return newAttachment(url, AttachImage) +} + +// NewDocumentAttachment creates a new Attachment from the given URL, setting the +// correct type and automatically detecting the MediaType based on the file +// extension. +func NewDocumentAttachment(url string) Attachment { + return newAttachment(url, AttachDocument) +} + +func newAttachment(url string, attachType AttachmentType) Attachment { + var fileType string extIdx := strings.LastIndexByte(url, '.') if extIdx > -1 { - imgType = mime.TypeByExtension(url[extIdx:]) + fileType = mime.TypeByExtension(url[extIdx:]) } return Attachment{ - Type: AttachImage, + Type: attachType, URL: url, - MediaType: imgType, + MediaType: fileType, } } diff --git a/activitystreams/attachment_test.go b/activitystreams/attachment_test.go index c96d1cd..76a524b 100644 --- a/activitystreams/attachment_test.go +++ b/activitystreams/attachment_test.go @@ -1,40 +1,64 @@ package activitystreams import ( "reflect" "testing" ) func TestNewImageAttachment(t *testing.T) { type args struct { url string } tests := []struct { name string args args - want *Attachment + want Attachment }{ - {name: "good svg", args: args{"https://writefreely.org/img/writefreely.svg"}, want: &Attachment{ + {name: "good svg", args: args{"https://writefreely.org/img/writefreely.svg"}, want: Attachment{ Type: "Image", URL: "https://writefreely.org/img/writefreely.svg", MediaType: "image/svg+xml", }}, - {name: "good png", args: args{"https://i.snap.as/12345678.png"}, want: &Attachment{ + {name: "good png", args: args{"https://i.snap.as/12345678.png"}, want: Attachment{ Type: "Image", URL: "https://i.snap.as/12345678.png", MediaType: "image/png", }}, - {name: "no extension", args: args{"https://i.snap.as/12345678"}, want: &Attachment{ + {name: "no extension", args: args{"https://i.snap.as/12345678"}, want: Attachment{ Type: "Image", URL: "https://i.snap.as/12345678", MediaType: "", }}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := NewImageAttachment(tt.args.url); !reflect.DeepEqual(got, tt.want) { t.Errorf("NewImageAttachment() = %v, want %v", got, tt.want) } }) } } + +func TestNewDocumentAttachment(t *testing.T) { + type args struct { + url string + } + tests := []struct { + name string + args args + want Attachment + }{ + {name: "mp3", args: args{"https://listen.as/matt/abc.mp3"}, want: Attachment{ + Type: "Document", + URL: "https://listen.as/matt/abc.mp3", + MediaType: "audio/mpeg", + }}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewDocumentAttachment(tt.args.url); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewDocumentAttachment() = %+v, want %+v", got, tt.want) + } + }) + } +} diff --git a/category/category.go b/category/category.go new file mode 100644 index 0000000..f60dc4b --- /dev/null +++ b/category/category.go @@ -0,0 +1,30 @@ +// Package category supports post categories +package category + +import ( + "errors" + "github.com/writeas/slug" +) + +var ( + ErrNotFound = errors.New("category doesn't exist") +) + +// Category represents a post tag with additional metadata, like a title and slug. +type Category struct { + ID int64 `json:"-"` + Hashtag string `json:"hashtag"` + Slug string `json:"slug"` + Title string `json:"title"` +} + +// NewCategory creates a Category you can insert into the database, based on a hashtag. It automatically breaks up the +// hashtag by words, based on capitalization, for both the title and a URL-friendly slug. +func NewCategory(hashtag string) *Category { + title := titleFromHashtag(hashtag) + return &Category{ + Hashtag: hashtag, + Slug: slug.Make(title), + Title: title, + } +} diff --git a/category/tags.go b/category/tags.go new file mode 100644 index 0000000..9b99248 --- /dev/null +++ b/category/tags.go @@ -0,0 +1,26 @@ +package category + +import ( + "strings" + "unicode" +) + +// titleFromHashtag generates an all-lowercase title, with spaces inserted based on initial capitalization -- e.g. +// "MyWordyTag" becomes "my wordy tag". +func titleFromHashtag(hashtag string) string { + var t strings.Builder + var prev rune + for i, c := range hashtag { + if unicode.IsUpper(c) { + if i > 0 && !unicode.IsUpper(prev) { + // Insert space if previous rune wasn't also uppercase (e.g. an abbreviation) + t.WriteRune(' ') + } + t.WriteRune(unicode.ToLower(c)) + } else { + t.WriteRune(c) + } + prev = c + } + return t.String() +} diff --git a/category/tags_test.go b/category/tags_test.go new file mode 100644 index 0000000..9889cfc --- /dev/null +++ b/category/tags_test.go @@ -0,0 +1,31 @@ +package category + +import "testing" + +func TestTitleFromHashtag(t *testing.T) { + tests := []struct { + name string + hashtag string + expTitle string + }{ + {"proper noun", "Jane", "jane"}, + {"full name", "JaneDoe", "jane doe"}, + {"us words", "unitedStates", "united states"}, + {"usa", "USA", "usa"}, + {"us monoword", "unitedstates", "unitedstates"}, + {"100dto", "100DaysToOffload", "100 days to offload"}, + {"iphone", "iPhone", "iphone"}, + {"ilike", "iLikeThis", "i like this"}, + {"abird", "aBird", "a bird"}, + {"all caps", "URGENT", "urgent"}, + {"smartphone", "スマートフォン", "スマートフォン"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + res := titleFromHashtag(test.hashtag) + if res != test.expTitle { + t.Fatalf("#%s: got '%s' expected '%s'", test.hashtag, res, test.expTitle) + } + }) + } +} diff --git a/go.mod b/go.mod index 0ea6e4d..5170100 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,19 @@ module github.com/writeas/web-core go 1.10 require ( github.com/gofrs/uuid v3.3.0+incompatible github.com/kylemcc/twitter-text-go v0.0.0-20180726194232-7f582f6736ec - github.com/microcosm-cc/bluemonday v1.0.2 + github.com/microcosm-cc/bluemonday v1.0.5 + github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/writeas/go-strip-markdown v2.0.1+incompatible github.com/writeas/impart v1.1.1 github.com/writeas/openssl-go v1.0.0 github.com/writeas/saturday v1.7.1 + github.com/writeas/slug v1.2.0 golang.org/x/crypto v0.0.0-20200109152110-61a87790db17 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 // indirect ) diff --git a/go.sum b/go.sum index 81be546..e02806f 100644 --- a/go.sum +++ b/go.sum @@ -1,35 +1,44 @@ +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/chris-ramon/douceur v0.2.0 h1:IDMEdxlEUUBYBKE4z/mJnFyVXox+MjuEVDJNN27glkU= +github.com/chris-ramon/douceur v0.2.0/go.mod h1:wDW5xjJdeoMm1mRt4sD4c/LbF/mWdEpRXQKjTR8nIBE= github.com/gofrs/uuid v3.3.0+incompatible h1:8K4tyRfvU1CYPgJsveYFQMhpFd/wXNM7iK6rR7UHz84= github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kylemcc/twitter-text-go v0.0.0-20180726194232-7f582f6736ec h1:ZXWuspqypleMuJy4bzYEqlMhJnGAYpLrWe5p7W3CdvI= github.com/kylemcc/twitter-text-go v0.0.0-20180726194232-7f582f6736ec/go.mod h1:voECJzdraJmolzPBgL9Z7ANwXf4oMXaTCsIkdiPpR/g= -github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s= -github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= +github.com/microcosm-cc/bluemonday v1.0.5 h1:cF59UCKMmmUgqN1baLvqU/B1ZsMori+duLVTLpgiG3w= +github.com/microcosm-cc/bluemonday v1.0.5/go.mod h1:8iwZnFn2CDDNZ0r6UXhF4xawGvzaqzCRa1n3/lO3W2w= +github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ= +github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/writeas/go-strip-markdown v2.0.1+incompatible h1:IIqxTM5Jr7RzhigcL6FkrCNfXkvbR+Nbu1ls48pXYcw= github.com/writeas/go-strip-markdown v2.0.1+incompatible/go.mod h1:Rsyu10ZhbEK9pXdk8V6MVnZmTzRG0alMNLMwa0J01fE= github.com/writeas/impart v1.1.1 h1:RyA9+CqbdbDuz53k+nXCWUY+NlEkdyw6+nWanxSBl5o= github.com/writeas/impart v1.1.1/go.mod h1:g0MpxdnTOHHrl+Ca/2oMXUHJ0PcRAEWtkCzYCJUXC9Y= github.com/writeas/openssl-go v1.0.0 h1:YXM1tDXeYOlTyJjoMlYLQH1xOloUimSR1WMF8kjFc5o= github.com/writeas/openssl-go v1.0.0/go.mod h1:WsKeK5jYl0B5y8ggOmtVjbmb+3rEGqSD25TppjJnETA= github.com/writeas/saturday v1.7.1 h1:lYo1EH6CYyrFObQoA9RNWHVlpZA5iYL5Opxo7PYAnZE= github.com/writeas/saturday v1.7.1/go.mod h1:ETE1EK6ogxptJpAgUbcJD0prAtX48bSloie80+tvnzQ= +github.com/writeas/slug v1.2.0 h1:EMQ+cwLiOcA6EtFwUgyw3Ge18x9uflUnOnR6bp/J+/g= +github.com/writeas/slug v1.2.0/go.mod h1:RE8shOqQP3YhsfsQe0L3RnuejfQ4Mk+JjY5YJQFubfQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200109152110-61a87790db17 h1:nVJ3guKA9qdkEQ3TUdXI9QSINo2CUPM/cySEvw2w8I0= golang.org/x/crypto v0.0.0-20200109152110-61a87790db17/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0 h1:POO/ycCATvegFmVuPpQzZFJ+pGZeX22Ufu6fibxDVjU= gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=