-
-
Notifications
You must be signed in to change notification settings - Fork 89
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: Codec for article slugs #128
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,120 @@ | ||||||||
module Data.Slug | ||||||||
( Slug | ||||||||
, generate | ||||||||
, parse | ||||||||
, toString | ||||||||
, truncate | ||||||||
) where | ||||||||
|
||||||||
import Prelude | ||||||||
|
||||||||
import Data.Argonaut.Decode (class DecodeJson, JsonDecodeError(..), decodeJson) | ||||||||
import Data.Argonaut.Encode (class EncodeJson, encodeJson) | ||||||||
import Data.Array as Array | ||||||||
import Data.CodePoint.Unicode (isLatin1) | ||||||||
import Data.Either (note) | ||||||||
import Data.Maybe (Maybe(..)) | ||||||||
import Data.String as String | ||||||||
import Data.String.CodePoints (codePointFromChar, fromCodePointArray, toCodePointArray) | ||||||||
import Data.String.Pattern (Pattern(..), Replacement(..)) | ||||||||
|
||||||||
-- | A `Slug` represents a string value which is guaranteed to have the | ||||||||
-- | following qualities: | ||||||||
-- | | ||||||||
-- | - it is not empty | ||||||||
-- | - it consists of groups of characters separated by `-` dashes, | ||||||||
-- | where the slug cannot begin or end with a dash, and there can | ||||||||
-- | never be two dashes in a row. | ||||||||
-- | | ||||||||
-- | Example: `Slug "this-is-an-article-slug"` | ||||||||
newtype Slug = Slug String | ||||||||
|
||||||||
derive newtype instance Eq Slug | ||||||||
derive newtype instance Ord Slug | ||||||||
derive newtype instance Semigroup Slug | ||||||||
|
||||||||
instance Show Slug where | ||||||||
show (Slug str) = "(Slug " <> str <> ")" | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should ditch these instances since they aren’t used here (I generally don’t use Show, and we use codecs instead of JSON instances) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I removed the |
||||||||
|
||||||||
instance EncodeJson Slug where | ||||||||
encodeJson (Slug s) = encodeJson s | ||||||||
|
||||||||
instance DecodeJson Slug where | ||||||||
decodeJson = note (TypeMismatch "Slug") <<< parse <=< decodeJson | ||||||||
|
||||||||
-- | Create a `Slug` from a string. This will transform the input string | ||||||||
-- | to be a valid slug (if it is possible to do so) by separating words | ||||||||
-- | with `-` dashes, ensuring the string does not begin or end with a | ||||||||
-- | dash, and ensuring there are never two dashes in a row. | ||||||||
-- | | ||||||||
-- | Slugs are usually created for article titles and other resources | ||||||||
-- | which need a human-readable resource name in a URL. | ||||||||
-- | | ||||||||
-- | ```purescript | ||||||||
-- | > Slug.generate "My article title!" | ||||||||
-- | > Just (Slug "My-article-title") | ||||||||
-- | | ||||||||
-- | > Slug.generate "¬¬¬{}¬¬¬" | ||||||||
-- | > Nothing | ||||||||
-- | ``` | ||||||||
generate :: String -> Maybe Slug | ||||||||
generate s = do | ||||||||
let arr = words $ onlyLatin1 $ stripApostrophes s | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Copied from https://github.com/thomashoneyman/purescript-slug/blob/0142716b49758829d5b48244fe1e9f87ea935b77/src/Slug.purs#L63 removing |
||||||||
if Array.null arr then | ||||||||
Nothing | ||||||||
else | ||||||||
Just $ Slug $ String.joinWith "-" arr | ||||||||
where | ||||||||
-- Strip apostrophes to avoid unnecessary word breaks | ||||||||
stripApostrophes = String.replaceAll (Pattern "'") (Replacement "") | ||||||||
|
||||||||
-- Replace non-Latin 1 characters with spaces to be stripped later. | ||||||||
onlyLatin1 = | ||||||||
fromCodePointArray | ||||||||
<<< map (\x -> if isLatin1 x then x else codePointFromChar ' ') | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Copied from https://github.com/thomashoneyman/purescript-slug/blob/0142716b49758829d5b48244fe1e9f87ea935b77/src/Slug.purs#L75, removing |
||||||||
<<< toCodePointArray | ||||||||
|
||||||||
-- Split on whitespace | ||||||||
words = Array.filter (not String.null) <<< String.split (Pattern " ") | ||||||||
|
||||||||
-- | Parse a valid slug (as a string) into a `Slug`. This will fail if the | ||||||||
-- | string is not a valid slug and does not provide the same behavior as | ||||||||
-- | `generate`. | ||||||||
-- | | ||||||||
-- | ```purescript | ||||||||
-- | > Slug.parse "my-article-title" | ||||||||
-- | > Just (Slug "my-article-title") | ||||||||
-- | | ||||||||
-- | > Slug.parse "My article" | ||||||||
-- | > Nothing | ||||||||
-- | ``` | ||||||||
parse :: String -> Maybe Slug | ||||||||
parse str = generate str >>= check | ||||||||
where | ||||||||
check slug@(Slug s) | ||||||||
| s == str = Just slug | ||||||||
| otherwise = Nothing | ||||||||
|
||||||||
-- | Unwrap a `Slug` into the string contained within, without performing | ||||||||
-- | any transformations. | ||||||||
-- | | ||||||||
-- | ```purescript | ||||||||
-- | > Slug.toString (mySlug :: Slug) | ||||||||
-- | > "my-slug-i-generated" | ||||||||
-- | ``` | ||||||||
toString :: Slug -> String | ||||||||
toString (Slug s) = s | ||||||||
|
||||||||
-- | Ensure a `Slug` is no longer than a given number of characters. If the last | ||||||||
-- | character is a dash, it will also be removed. Providing a non-positive | ||||||||
-- | number as the length will return `Nothing`. | ||||||||
-- | | ||||||||
-- | ```purescript | ||||||||
-- | > Slug.create "My article title is long!" >>= Slug.truncate 3 | ||||||||
-- | > Just (Slug "My") | ||||||||
-- | ``` | ||||||||
truncate :: Int -> Slug -> Maybe Slug | ||||||||
truncate n (Slug s) | ||||||||
| n < 1 = Nothing | ||||||||
| n >= String.length s = Just (Slug s) | ||||||||
| otherwise = generate $ String.take n s | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was copied from https://github.com/thomashoneyman/purescript-slug/blob/0142716b49758829d5b48244fe1e9f87ea935b77/src/Slug.purs#L21-L31, removing the following guarantees: "every character with a defined notion of case is lower-cased" and "it consists of alphanumeric groups of characters".