packages feed

morley-1.19.1: src/Morley/Util/Text.hs

-- SPDX-FileCopyrightText: 2021 Oxhead Alpha
-- SPDX-License-Identifier: LicenseRef-MIT-OA

module Morley.Util.Text
  ( headToLower
  , surround
  , dquotes
  , stripFieldPrefix
  , dropPrefix
  , toCamel
  , Manip.toPascal
  , toSnake
  , toSpinal
  , lowerCaseCluster
  ) where

import Data.Char (isLower, isUpper, toLower)
import Data.Text qualified as T
import Data.Text.Manipulate qualified as Manip

-- | Leads first character of text to lower case.
--
-- For empty text this will throw an error.
headToLower :: HasCallStack => Text -> Text
headToLower txt = case T.uncons txt of
  Nothing -> error "Empty text"
  Just (c, cs) -> T.cons (toLower c) cs

surround :: Semigroup a => a -> a -> a -> a
surround pre post content = pre <> content <> post

dquotes :: (Semigroup a, IsString a) => a -> a
dquotes = surround "\"" "\""

-- | Drops the field name prefix from a field.
--
-- We assume a convention of the prefix always being non-uppercase, and the
-- first letter of the actual field name being uppercase.
dropPrefix :: Text -> Text
dropPrefix = T.dropWhile (not . isUpper)

-- | Transform text to @camelCase@.
--
-- If the text starts with a single uppercase letter, it is lowercased. If it
-- starts with a cluster of non-lowercase letters, all but the last in the
-- cluster are lowercased.
--
-- >>> toCamel "MyField"
-- "myField"
-- >>> toCamel "USPosition"
-- "usPosition"
-- >>> toCamel "FA2Config"
-- "fa2Config"
toCamel :: Text -> Text
toCamel = Manip.toCamel . lowerCaseCluster

-- | Transform text to @snake_case@.
--
-- Non-lowercase clusters starting with an uppercase letter are treated as
-- separate words, except the last letter in the cluster.
--
-- >>> toSnake "MyField"
-- "my_field"
-- >>> toSnake "USPosition"
-- "us_position"
-- >>> toSnake "FA2Config"
-- "fa2_config"
-- >>> toSnake "MyUSPosition"
-- "my_us_position"
-- >>> toSnake "MyFie123d"
-- "my_fie123d"
toSnake :: Text -> Text
toSnake = T.intercalate "_" . fmap (Manip.toSnake . lowerCaseCluster) . Manip.splitWords

-- | Transform text to @spinal-case@.
--
-- Non-lowercase clusters starting with an uppercase letter are treated as
-- separate words, except the last letter in the cluster.
--
-- >>> toSpinal "MyField"
-- "my-field"
-- >>> toSpinal "USPosition"
-- "us-position"
-- >>> toSpinal "FA2Config"
-- "fa2-config"
-- >>> toSpinal "MyUSPosition"
-- "my-us-position"
-- >>> toSpinal "MyFie123d"
-- "my-fie123d"
toSpinal :: Text -> Text
toSpinal = T.intercalate "-" . fmap (Manip.toSpinal . lowerCaseCluster) . Manip.splitWords

-- | If text starts with a cluster of non-lowercase letters, lowercase them except
-- the last one.
--
-- >>> lowerCaseCluster "USPosition"
-- "usPosition"
-- >>> lowerCaseCluster "Position"
-- "Position"
-- >>> lowerCaseCluster "FA2Config"
-- "fa2Config"
lowerCaseCluster :: Text -> Text
lowerCaseCluster x = fromMaybe x do
  (c1, xs) <- T.uncons x
  (c2, _) <- T.uncons xs
  if notLower c1 && notLower c2
  then do
    let (ucs, rest) = T.span notLower x
    (ucs', lastuc) <- T.unsnoc ucs
    Just $ T.map toLower ucs' <> T.cons lastuc rest
  else Nothing
  where
    notLower = not . isLower

-- | Cut fields prefixes which we use according to the style guide.
--
-- >>> stripFieldPrefix "cmMyField"
-- "myField"
stripFieldPrefix :: Text -> Text
stripFieldPrefix = toCamel . dropPrefix