packages feed

unicode-data-names-0.4.0: lib/Unicode/Char/General/Names.hs

-- |
-- Module      : Unicode.Char.General.Names
-- Copyright   : (c) 2022 Composewell Technologies and Contributors
-- License     : Apache-2.0
-- Maintainer  : streamly@composewell.com
-- Stability   : experimental
--
-- Unicode character names and name aliases with 'String' API.
-- See Unicode standard 15.0.0, section 4.8.
--
-- There are also two optional APIs:
--
-- * @ByteString@ API, which requires using the package flag @has-bytestring@.
-- * @Text@ API, which requires using the package flag @has-text@.
--
-- @since 0.1.0

module Unicode.Char.General.Names
    ( -- * Unicode version
      unicodeVersion

      -- * Name
    , name
    , nameOrAlias
    , nameOrLabel
    , correctedName

      -- * Name Aliases
    , NameAliases.NameAliasType(..)
    , nameAliases
    , nameAliasesByType
    , nameAliasesWithTypes

      -- * Label
    , label
    ) where

import Control.Applicative ((<|>))
import Control.Monad ((>=>))
import Foreign.C.String (peekCAStringLen)
import GHC.Exts (
    Addr#,
    Char (..),
    Char#,
    Int#,
    dataToTag#,
    indexCharOffAddr#,
    isTrue#,
    ord#,
    plusAddr#,
    quotRemInt#,
    (+#),
    (-#),
    (<#),
 )
import System.IO.Unsafe (unsafeDupablePerformIO)

import Unicode.Internal.Bits.Names (unpackNBytes#)
import qualified Unicode.Internal.Char.Label as Label
import Unicode.Internal.Char.Names.Version (unicodeVersion)
import qualified Unicode.Internal.Char.UnicodeData.DerivedName as DerivedName
import qualified Unicode.Internal.Char.UnicodeData.NameAliases as NameAliases

-- | Name of a character, if defined.
--
-- @since 0.1.0
name :: Char -> Maybe String
name (C# c#) = case DerivedName.name c# of
    (# name#, len# #) -> case len# of
        DerivedName.NoName -> Nothing
        DerivedName.CjkCompatibilityIdeograph -> Just n
            where
            !hex = showHex c#
            !n = 'C':'J':'K':' ':'C':'O':'M':'P':'A':'T':'I':'B':'I':'L':'I':'T':'Y':' ':'I':'D':'E':'O':'G':'R':'A':'P':'H':'-':hex
        DerivedName.CjkUnifiedIdeograph -> Just n
            where
            !hex = showHex c#
            !n = 'C':'J':'K':' ':'U':'N':'I':'F':'I':'E':'D':' ':'I':'D':'E':'O':'G':'R':'A':'P':'H':'-':hex
        DerivedName.TangutIdeograph -> Just n
            where
            !hex = showHex c#
            !n = 'T':'A':'N':'G':'U':'T':' ':'I':'D':'E':'O':'G':'R':'A':'P':'H':'-':hex
        _
            | isTrue# (len# <# DerivedName.HangulSyllable) -> let !n = unpack name# [] len# in Just n
            | otherwise ->
                let !rest = unpack name# [] (len# -# DerivedName.HangulSyllable)
                    !n = 'H':'A':'N':'G':'U':'L':' ':'S':'Y':'L':'L':'A':'B':'L':'E':' ':rest
                in Just n

{-# INLINE unpack #-}
unpack :: Addr# -> String -> Int# -> String
unpack !addr# !acc = \case
    0# -> acc
    !i -> unpack addr# acc' i'
        where
        !i' = i -# 1#
        !c'# = indexCharOffAddr# addr# i'
        !c' = C# c'#
        !acc' = c' : acc

-- [NOTE] We assume c# >= '\x1000' to avoid to check for padding
showHex :: Char# -> String
showHex !c# = showIt [] (quotRemInt# (ord# c#) 16#)
    where
    showIt !acc (# q, r #) = case q of
        0# -> acc'
        _  -> showIt acc' (quotRemInt# q 16#)
        where
        !c = C# (indexCharOffAddr# "0123456789ABCDEF"# r)
        !acc' = c : acc

-- | Returns /corrected/ name of a character (see 'NameAliases.Correction'),
-- if defined, otherwise returns its original 'name' if defined.
--
-- @since 0.1.0
{-# INLINE correctedName #-}
correctedName :: Char -> Maybe String
correctedName c@(C# c#) = corrected <|> name c
    where
    -- Assumption: fromEnum NameAliases.Correction == 0
    !corrected = case indexCharOffAddr# addr# 0# of
        '\xff'# -> Nothing -- no aliases
        '\x00'# -> Nothing -- no correction
        i#      ->
            let !n = unpackNBytes'# (addr# `plusAddr#` ord# i#)
            in Just n
    !addr# = NameAliases.nameAliases c#

-- | Returns a character’s 'name' if defined,
-- otherwise returns its first name alias if defined.
--
-- @since 0.1.0
nameOrAlias :: Char -> Maybe String
nameOrAlias c@(C# c#) = name c <|> case indexCharOffAddr# addr# 0# of
    '\xff'# -> Nothing -- no aliases
    '\x00'# -> let !n = go 1# in Just n
    _       -> let !n = go 0# in Just n
    where
    !addr# = NameAliases.nameAliases c#
    go t# = case ord# (indexCharOffAddr# (addr# `plusAddr#` t#) 0#) of
        -- No bound check for t#: there is at least one alias
        0# -> go (t# +# 1#)
        i# -> unpackNBytes'# (addr# `plusAddr#` i#)

-- | Returns a character’s 'name' if defined,
-- otherwise returns its label between angle brackets.
--
-- @since 0.4.0
nameOrLabel :: Char -> String
nameOrLabel c = case name c of
    Nothing -> '<' : label c ++ ">"
    Just n  -> n

-- | All name aliases of a character, if defined.
-- The names are listed in the original order of the UCD.
--
-- See 'nameAliasesWithTypes' for the detailed list by alias type.
--
-- @since 0.1.0
{-# INLINE nameAliases #-}
nameAliases :: Char -> [String]
nameAliases (C# c#) = case indexCharOffAddr# addr0# 0# of
    '\xff'# -> [] -- no aliases
    _       -> go (addr0# `plusAddr#` (NameAliases.MaxNameAliasType +# 1#))
        where
        go addr# = case ord# (indexCharOffAddr# addr# 0#) of
            0# -> case ord# (indexCharOffAddr# (addr# `plusAddr#` 1#) 0#) of
                -- End of list
                0# -> []
                -- skip empty entry
                l# ->
                    let !s = unpackNBytes# (addr# `plusAddr#` 2#) l#
                    in s : go (addr# `plusAddr#` (l# +# 2#))
            l# ->
                let !s = unpackNBytes# (addr# `plusAddr#` 1#) l#
                in s : go (addr# `plusAddr#` (l# +# 1#))
    where
    addr0# = NameAliases.nameAliases c#

-- | Name aliases of a character for a specific name alias type.
--
-- @since 0.1.0
{-# INLINE nameAliasesByType #-}
nameAliasesByType :: NameAliases.NameAliasType -> Char -> [String]
nameAliasesByType t (C# c#) = case indexCharOffAddr# addr# 0# of
    '\xff'# -> [] -- no aliases
    _       -> nameAliasesByType# addr# t
    where
    addr# = NameAliases.nameAliases c#

-- | Detailed character names aliases.
-- The names are listed in the original order of the UCD.
--
-- See 'nameAliases' if the alias type is not required.
--
-- @since 0.1.0
{-# INLINE nameAliasesWithTypes #-}
nameAliasesWithTypes :: Char -> [(NameAliases.NameAliasType, [String])]
nameAliasesWithTypes (C# c#) = case indexCharOffAddr# addr# 0# of
    '\xff'# -> [] -- no aliases
    '\x00'# -> foldr mk mempty [succ minBound..maxBound]
    _       -> foldr mk mempty [minBound..maxBound]
    where
    addr# = NameAliases.nameAliases c#
    mk t acc = case nameAliasesByType# addr# t of
        []  -> acc
        !as -> (t, as) : acc

{-# INLINE unpackNBytes'# #-}
unpackNBytes'# :: Addr# -> String
unpackNBytes'# addr# = unpackNBytes#
    (addr# `plusAddr#` 1#)
    (ord# (indexCharOffAddr# addr# 0#))

{-# INLINE nameAliasesByType# #-}
nameAliasesByType# :: Addr# -> NameAliases.NameAliasType -> [String]
nameAliasesByType# addr# t = case indexCharOffAddr# (addr# `plusAddr#` t#) 0# of
    '\0'# -> [] -- no aliases for this type
    i#    -> unpackCStrings# (addr# `plusAddr#` ord# i#)
    where t# = dataToTag# t

-- | 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.
--
-- >>> label '\0'
-- "control-0000"
-- >>> label 'a'
-- "UNDEFINED"
-- >>> label '\xffff'
-- "noncharacter-FFFF"
--
-- @since 0.4.0
label :: Char -> String
label = unsafeDupablePerformIO . (Label.label >=> peekCAStringLen)

{-# INLINE unpackCStrings# #-}
unpackCStrings# :: Addr# -> [String]
unpackCStrings# = go
    where
    go addr# = case ord# (indexCharOffAddr# addr# 0#) of
        0# -> []
        l# ->
            let !s = unpackNBytes# (addr# `plusAddr#` 1#) l#
            in s : go (addr# `plusAddr#` (l# +# 1#))