packages feed

unicode-data-names-0.4.0: lib/Unicode/Internal/Char/Label.hs

-- |
-- Module      : Unicode.Char
-- Copyright   : (c) 2024 Composewell Technologies and Contributors
-- License     : Apache-2.0
-- Maintainer  : streamly@composewell.com
-- Stability   : experimental

module Unicode.Internal.Char.Label
    ( label
    , addHexCodePoint
    , intToDigiT
    ) where

import Data.Char (ord)
import Data.Functor (($>))
import Foreign.C.String (CString, CStringLen)
import Foreign.C.Types (CChar (..))
import Foreign.Marshal (allocaArray, copyArray)
import Foreign.Storable (Storable (..))
import GHC.Exts (Int (..), Int#, Ptr (..), isTrue#, quotRemInt#, (+#), (-#), (<=#))
import Unicode.Char.General (CodePointType (..), codePointType)

-- | Returns the label of a code point if it has no character name, otherwise
-- returns @\"UNDEFINED\"@.
--
-- See subsection
-- [“Code Point Labels”](https://www.unicode.org/versions/Unicode15.0.0/ch04.pdf#G135248)
-- in section 4.8 “Name” of the Unicode Standard.
--
-- @since 0.4.0
label :: Char -> IO CStringLen
label c = case codePointType c of
    ControlType      -> mkLabel 8#  "control-"#
    PrivateUseType   -> mkLabel 12# "private-use-"#
    SurrogateType    -> mkLabel 10# "surrogate-"#
    NoncharacterType -> mkLabel 13# "noncharacter-"#
    ReservedType     -> mkLabel 9#  "reserved-"#
    _                -> pure (Ptr "UNDEFINED"#, 9)

    where

    mkLabel len s0 = allocaArray (I# len + 6) $ \s -> do
        copyArray s (Ptr s0) (I# len)
        len' <- addHexCodePoint s len len c
        pure (s, len')

-- | Appned the code point of a character using the Unicode Standard convention:
-- hexadecimal codepoint padded with zeros if inferior to 4 characters.
--
-- It is the responsability of the caller to provide a 'CString' that can hold
-- up to 6 characters from the provided index.
addHexCodePoint
    :: CString -- ^ Destination ASCII string
    -> Int#    -- ^ String length
    -> Int#    -- ^ Index
    -> Char    -- ^ Character which code point will be added to the string
    -> IO Int  -- ^ New size of the string
addHexCodePoint s len i0 c
    | isTrue# (cp# <=# 0x0000f#) = prependAt 3# <* pad0 0# <* pad0 1# <* pad0 2#
    | isTrue# (cp# <=# 0x000ff#) = prependAt 3# <* pad0 0# <* pad0 1#
    | isTrue# (cp# <=# 0x00fff#) = prependAt 3# <* pad0 0#
    | isTrue# (cp# <=# 0x0ffff#) = prependAt 3#
    | isTrue# (cp# <=# 0xfffff#) = prependAt 4#
    | otherwise                  = prependAt 5#
    where
    !(I# cp#) = ord c
    pad0 i = pokeElemOff s (I# (i0 +# i)) (CChar 0x30)
    prependAt i = go (i0 +# i) (quotRemInt# cp# 16#) $> I# (len +# i +# 1#)
    go i (# n#, d #) = do
        pokeElemOff s (I# i) (intToDigiT d)
        case n# of
            0# -> pure ()
            _  -> go (i -# 1#) (quotRemInt# n# 16#)

-- | Convert an 'Int#' in the range 0..15 to the corresponding single digit
-- 'CChar' in upper case.
--
-- Undefined for numbers outside the 0..15 range.
intToDigiT :: Int# -> CChar
intToDigiT i = if isTrue# (i <=# 9#)
    then fromIntegral (I# (0x30# +# i))
    else fromIntegral (I# (0x37# +# i))