{-# LANGUAGE MagicHash #-}
module Ecluse.Core.Text (
nonBlank,
stripTrailingSlash,
joinUrlPath,
urlFilename,
urlFilenameComponent,
isSafeComponent,
afterFirst,
LowerPrefix,
httpsPrefix,
httpPrefix,
lowerPrefixChars,
isPrefixOfLowered,
registryPath,
readDecimalText,
readHexText,
renderIso8601Utc,
displayExceptionT,
textStorageBytes,
) where
import Data.Array.Byte (ByteArray (..))
import Data.Char (isControl)
import Data.Text qualified as T
import Data.Text.Encoding qualified as TE
import Data.Text.Internal qualified as TI
import Data.Text.Lazy qualified as TL
import Data.Text.Lazy.Builder qualified as TB
import Data.Text.Lazy.Builder.Int qualified as TBI
import Data.Text.Read qualified as TR
import Data.Time (UTCTime (UTCTime), diffTimeToPicoseconds, toGregorian)
import Data.Time.Format.ISO8601 (iso8601Show)
import GHC.Exts (Int (I#), sizeofByteArray#)
import Network.HTTP.Types.URI (urlDecode)
nonBlank :: Text -> Maybe Text
nonBlank :: Text -> Maybe Text
nonBlank Text
t =
let trimmed :: Text
trimmed = Text -> Text
T.strip Text
t
in if Text -> Bool
T.null Text
trimmed then Maybe Text
forall a. Maybe a
Nothing else Text -> Maybe Text
forall a. a -> Maybe a
Just Text
trimmed
stripTrailingSlash :: Text -> Text
stripTrailingSlash :: Text -> Text
stripTrailingSlash = (Char -> Bool) -> Text -> Text
T.dropWhileEnd (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
'/')
joinUrlPath :: Text -> Text -> Text
joinUrlPath :: Text -> Text -> Text
joinUrlPath Text
b Text
path = Text -> Text
stripTrailingSlash Text
b Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
"/" Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> Text
path
urlFilename :: Text -> Maybe Text
urlFilename :: Text -> Maybe Text
urlFilename Text
url = do
let filename :: Text
filename = Text -> Text
urlFilenameComponent Text
url
Bool -> Maybe ()
forall (f :: * -> *). Alternative f => Bool -> f ()
guard (Text -> Bool
isSafeComponent Text
filename)
Bool -> Maybe () -> Maybe ()
forall (f :: * -> *). Applicative f => Bool -> f () -> f ()
when ((Char -> Bool) -> Text -> Bool
T.any (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
'%') Text
filename) (Maybe () -> Maybe ()) -> Maybe () -> Maybe ()
forall a b. (a -> b) -> a -> b
$ do
decoded <- Either UnicodeException Text -> Maybe Text
forall l r. Either l r -> Maybe r
rightToMaybe (ByteString -> Either UnicodeException Text
TE.decodeUtf8' (Bool -> ByteString -> ByteString
urlDecode Bool
False (Text -> ByteString
forall a b. ConvertUtf8 a b => a -> b
encodeUtf8 Text
filename)))
guard (isSafeComponent decoded)
Text -> Maybe Text
forall a. a -> Maybe a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Text
filename
urlFilenameComponent :: Text -> Text
urlFilenameComponent :: Text -> Text
urlFilenameComponent = (Char -> Bool) -> Text -> Text
T.takeWhileEnd (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
/= Char
'/') (Text -> Text) -> (Text -> Text) -> Text -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Char -> Bool) -> Text -> Text
T.takeWhile Char -> Bool
inPath
where
inPath :: Char -> Bool
inPath Char
ch = Char
ch Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
/= Char
'?' Bool -> Bool -> Bool
&& Char
ch Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
/= Char
'#'
isSafeComponent :: Text -> Bool
isSafeComponent :: Text -> Bool
isSafeComponent Text
c =
Bool -> Bool
not (Text -> Bool
T.null Text
c)
Bool -> Bool -> Bool
&& Text
c Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
/= Text
"."
Bool -> Bool -> Bool
&& Text
c Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
/= Text
".."
Bool -> Bool -> Bool
&& (Char -> Bool) -> Text -> Bool
T.all Char -> Bool
safeChar Text
c
where
safeChar :: Char -> Bool
safeChar Char
ch = Char
ch Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
/= Char
'/' Bool -> Bool -> Bool
&& Char
ch Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
/= Char
'\\' Bool -> Bool -> Bool
&& Bool -> Bool
not (Char -> Bool
isControl Char
ch)
afterFirst :: Text -> Text -> Text
afterFirst :: Text -> Text -> Text
afterFirst Text
needle Text
hay = Text -> Maybe Text -> Text
forall a. a -> Maybe a -> a
fromMaybe Text
hay (Text -> Text -> Maybe Text
T.stripPrefix Text
needle ((Text, Text) -> Text
forall a b. (a, b) -> b
snd (HasCallStack => Text -> Text -> (Text, Text)
Text -> Text -> (Text, Text)
T.breakOn Text
needle Text
hay)))
data LowerPrefix = LowerPrefix Int Text
deriving stock (MonthOfYear -> LowerPrefix -> ShowS
[LowerPrefix] -> ShowS
LowerPrefix -> String
(MonthOfYear -> LowerPrefix -> ShowS)
-> (LowerPrefix -> String)
-> ([LowerPrefix] -> ShowS)
-> Show LowerPrefix
forall a.
(MonthOfYear -> a -> ShowS)
-> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: MonthOfYear -> LowerPrefix -> ShowS
showsPrec :: MonthOfYear -> LowerPrefix -> ShowS
$cshow :: LowerPrefix -> String
show :: LowerPrefix -> String
$cshowList :: [LowerPrefix] -> ShowS
showList :: [LowerPrefix] -> ShowS
Show)
httpsPrefix :: LowerPrefix
httpsPrefix :: LowerPrefix
httpsPrefix = MonthOfYear -> Text -> LowerPrefix
LowerPrefix MonthOfYear
8 Text
"https://"
httpPrefix :: LowerPrefix
httpPrefix :: LowerPrefix
httpPrefix = MonthOfYear -> Text -> LowerPrefix
LowerPrefix MonthOfYear
7 Text
"http://"
lowerPrefixChars :: LowerPrefix -> Int
lowerPrefixChars :: LowerPrefix -> MonthOfYear
lowerPrefixChars (LowerPrefix MonthOfYear
chars Text
_) = MonthOfYear
chars
isPrefixOfLowered :: LowerPrefix -> Text -> Bool
isPrefixOfLowered :: LowerPrefix -> Text -> Bool
isPrefixOfLowered (LowerPrefix MonthOfYear
chars Text
prefix) Text
t = Text
lowered Text -> Text -> Bool
forall a. Eq a => a -> a -> Bool
== Text
prefix
where
lowered :: Text
lowered = MonthOfYear -> Text -> Text
T.take MonthOfYear
chars (Text -> Text
T.toLower (MonthOfYear -> Text -> Text
T.take MonthOfYear
chars Text
t))
registryPath :: Text -> Text
registryPath :: Text -> Text
registryPath Text
raw = (Char -> Bool) -> Text -> Text
T.dropWhile (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
/= Char
'/') (Text -> Text -> Text
afterFirst Text
"://" Text
raw)
readDecimalText :: (Integral a) => Text -> Maybe a
readDecimalText :: forall a. Integral a => Text -> Maybe a
readDecimalText = Reader a -> Text -> Maybe a
forall a. Reader a -> Text -> Maybe a
readWholly Reader a
forall a. Integral a => Reader a
TR.decimal
readHexText :: (Integral a) => Text -> Maybe a
readHexText :: forall a. Integral a => Text -> Maybe a
readHexText Text
t
| LowerPrefix -> Text -> Bool
isPrefixOfLowered LowerPrefix
hexPrefix Text
t = Maybe a
forall a. Maybe a
Nothing
| Bool
otherwise = Reader a -> Text -> Maybe a
forall a. Reader a -> Text -> Maybe a
readWholly Reader a
forall a. Integral a => Reader a
TR.hexadecimal Text
t
hexPrefix :: LowerPrefix
hexPrefix :: LowerPrefix
hexPrefix = MonthOfYear -> Text -> LowerPrefix
LowerPrefix MonthOfYear
2 Text
"0x"
readWholly :: TR.Reader a -> Text -> Maybe a
readWholly :: forall a. Reader a -> Text -> Maybe a
readWholly Reader a
textReader Text
t = case Reader a
textReader Text
t of
Right (a
n, Text
rest) | Text -> Bool
T.null Text
rest -> a -> Maybe a
forall a. a -> Maybe a
Just a
n
Either String (a, Text)
_ -> Maybe a
forall a. Maybe a
Nothing
renderIso8601Utc :: UTCTime -> Text
renderIso8601Utc :: UTCTime -> Text
renderIso8601Utc t :: UTCTime
t@(UTCTime Day
day DiffTime
dt)
| Integer
year Integer -> Integer -> Bool
forall a. Ord a => a -> a -> Bool
< Integer
0 Bool -> Bool -> Bool
|| Integer
year Integer -> Integer -> Bool
forall a. Ord a => a -> a -> Bool
> Integer
9999 Bool -> Bool -> Bool
|| Integer
picos Integer -> Integer -> Bool
forall a. Ord a => a -> a -> Bool
>= Integer
86_400_000_000_000_000 = String -> Text
forall a. ToText a => a -> Text
toText (UTCTime -> String
forall t. ISO8601 t => t -> String
iso8601Show UTCTime
t)
| Bool
otherwise =
LazyText -> Text
TL.toStrict (LazyText -> Text) -> (Builder -> LazyText) -> Builder -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Builder -> LazyText
TB.toLazyText (Builder -> Text) -> Builder -> Text
forall a b. (a -> b) -> a -> b
$
MonthOfYear -> Integer -> Builder
digits MonthOfYear
4 Integer
year
Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> Builder
"-"
Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> MonthOfYear -> Integer -> Builder
digits MonthOfYear
2 (MonthOfYear -> Integer
forall a b. (Integral a, Num b) => a -> b
fromIntegral MonthOfYear
month)
Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> Builder
"-"
Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> MonthOfYear -> Integer -> Builder
digits MonthOfYear
2 (MonthOfYear -> Integer
forall a b. (Integral a, Num b) => a -> b
fromIntegral MonthOfYear
dayOfMonth)
Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> Builder
"T"
Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> MonthOfYear -> Integer -> Builder
digits MonthOfYear
2 Integer
hh
Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> Builder
":"
Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> MonthOfYear -> Integer -> Builder
digits MonthOfYear
2 Integer
mm
Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> Builder
":"
Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> MonthOfYear -> Integer -> Builder
digits MonthOfYear
2 Integer
ss
Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> Builder
fraction
Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> Builder
"Z"
where
(Integer
year, MonthOfYear
month, MonthOfYear
dayOfMonth) = Day -> (Integer, MonthOfYear, MonthOfYear)
toGregorian Day
day
picos :: Integer
picos = DiffTime -> Integer
diffTimeToPicoseconds DiffTime
dt
(Integer
secondsOfDay, Integer
frac) = Integer
picos Integer -> Integer -> (Integer, Integer)
forall a. Integral a => a -> a -> (a, a)
`divMod` Integer
1_000_000_000_000
(Integer
hh, Integer
rem') = Integer
secondsOfDay Integer -> Integer -> (Integer, Integer)
forall a. Integral a => a -> a -> (a, a)
`divMod` Integer
3600
(Integer
mm, Integer
ss) = Integer
rem' Integer -> Integer -> (Integer, Integer)
forall a. Integral a => a -> a -> (a, a)
`divMod` Integer
60
fraction :: TB.Builder
fraction :: Builder
fraction
| Integer
frac Integer -> Integer -> Bool
forall a. Eq a => a -> a -> Bool
== Integer
0 = Builder
forall a. Monoid a => a
mempty
| Bool
otherwise =
Text -> Builder
TB.fromText (Text
"." Text -> Text -> Text
forall a. Semigroup a => a -> a -> a
<> (Char -> Bool) -> Text -> Text
T.dropWhileEnd (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
'0') (MonthOfYear -> Char -> Text -> Text
T.justifyRight MonthOfYear
12 Char
'0' (Integer -> Text
forall b a. (Show a, IsString b) => a -> b
show Integer
frac)))
digits :: Int -> Integer -> TB.Builder
digits :: MonthOfYear -> Integer -> Builder
digits MonthOfYear
width Integer
n =
let body :: String
body = Integer -> String
forall b a. (Show a, IsString b) => a -> b
show Integer
n :: String
pad :: MonthOfYear
pad = MonthOfYear
width MonthOfYear -> MonthOfYear -> MonthOfYear
forall a. Num a => a -> a -> a
- String -> MonthOfYear
forall a. [a] -> MonthOfYear
forall (t :: * -> *) a. Foldable t => t a -> MonthOfYear
length String
body
in String -> Builder
TB.fromString (MonthOfYear -> Char -> String
forall a. MonthOfYear -> a -> [a]
replicate MonthOfYear
pad Char
'0') Builder -> Builder -> Builder
forall a. Semigroup a => a -> a -> a
<> Integer -> Builder
forall a. Integral a => a -> Builder
TBI.decimal Integer
n
displayExceptionT :: (Exception e) => e -> Text
displayExceptionT :: forall e. Exception e => e -> Text
displayExceptionT = String -> Text
forall a. ToText a => a -> Text
toText (String -> Text) -> (e -> String) -> e -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. e -> String
forall e. Exception e => e -> String
displayException
textStorageBytes :: Text -> Int
textStorageBytes :: Text -> MonthOfYear
textStorageBytes (TI.Text (ByteArray ByteArray#
array) MonthOfYear
_ MonthOfYear
_) = Int# -> MonthOfYear
I# (ByteArray# -> Int#
sizeofByteArray# ByteArray#
array)