packages feed

willow-0.1.0.0: src/Web/Willow/Common/Encoding/EucJp.hs

{-|
Description:    Character translation functions to and from the EUC-JP encoding scheme.

Copyright:      (c) 2020 Sam May
License:        MPL-2.0
Maintainer:     ag.eitilt@gmail.com

Stability:      experimental
Portability:    portable
-}
module Web.Willow.Common.Encoding.EucJp
    ( -- * Decoder
      decoder
    , decodeIndex0208
      -- * Encoder
    , encoder
    , encodeIndex0208
    ) where


import qualified Control.Applicative as A

import qualified Data.ByteString.Short as BS.SH
import qualified Data.Char as C
import qualified Data.Maybe as Y
import qualified Data.Tuple as U
import qualified Data.Word as W

import Web.Willow.Common.Encoding.Common
import Web.Willow.Common.Parser
import Web.Willow.Common.Parser.Switch


-- | __Encoding:__
--      @[EUC-JP decoder]
--      (https://encoding.spec.whatwg.org/#euc-jp-decoder)@
-- 
-- Decodes a 'Char' from a binary stream encoded with the 'EucJp' encoding
-- scheme, or returns 'Left' if the stream starts with an invalid byte
-- sequence.
decoder :: TextBuilder
decoder = next >>= switch
    [ If_ (== 0x8E) $ do
        second <- next
        if range 0xA1 0xDF second
            then emit [0x8E, second] . toEnum . (+ 0xFEC0) $ fromIntegral second
            else if isAsciiByte second
                then push second *> decoderFailure1 0x8E
                else decoderFailure [0x8E, second]
    , If_ (== 0x8F) $ do
        second <- next
        if range 0xA1 0xFE second
            then decodeSecond True second
            else if isAsciiByte second
                then push second *> decoderFailure1 0x8F
                else decoderFailure [0x8F, second]
    , If isAsciiByte toUnicode1
    , If (range 0xA1 0xFE) $ decodeSecond False
    , Else decoderFailure1
    ]

-- | __Encoding:__
--      @[EUC-JP decoder]
--      (https://encoding.spec.whatwg.org/#euc-jp-decoder)@
--      step 5
-- 
-- Process a multi-byte sequence according to either the @jis0208@ ('False') or
-- the @jis0212@ ('True') encoding scheme.
-- 
-- Fails on an ASCII byte, which should be read on its own.
decodeSecond :: Bool -> W.Word8 -> TextBuilder
decodeSecond is0212 lead = next >>= switch
    [ If (range 0xA1 0xFE) $ \trail ->
        maybe (decoderFailure $ bs ++ [trail]) (emit $ bs ++ [trail]) . decodeIndex $
            (fromIntegral lead - 0xA1) * 94 + fromIntegral trail - 0xA1
    , If_ (not . isAsciiByte) A.empty
    ]
  where (decodeIndex, bs)
            | is0212 = (decodeIndex0212, [0x8F, lead])
            | otherwise = (decodeIndex0208, [lead])


-- | __Encoding:__
--      @[EUC-JP encoder]
--      (https://encoding.spec.whatwg.org/#euc-jp-encoder)@
-- 
-- Encode the first 'Char' in a string according to the 'EucJp'/@jis02080@
-- encoding scheme, or return that same character if that scheme doesn't define
-- a binary representation for it.
encoder :: BinaryBuilder
encoder = next >>= switch
    [ If C.isAscii fromAscii
    , If_ (== '\xA5') $ return (pure $ BS.SH.pack [0x5C])
    , If_ (== '\x203E') $ return (pure $ BS.SH.pack [0x7E])
    , If_ (== '\x2212') $ encodeTwoByte (Just '\x2212') '\xFF0D'
    , If (range '\xFF61' '\xFF9F') $ \char ->
        return . pure $ BS.SH.pack [0x8E, fromIntegral $ fromEnum char - 0xFEC0]
    , Else $ encodeTwoByte Nothing
    ]

-- | __Encoding:__
--      @[EUC-JP encoder]
--      (https://encoding.spec.whatwg.org/#euc-jp-encoder)@
--      steps 7-11
-- 
-- Encode a multi-byte character according to the 'EucJp'/@jis02080@ encoding,
-- or return that same character that scheme doesn't define a binary
-- representation for it.
-- 
-- Never fails or consumes input.
encodeTwoByte :: Maybe Char -> Char -> BinaryBuilder
encodeTwoByte err char = case encodeIndex0208 char of
    Just index ->
        let (lead, trail) = divMod index 94
        in  return . pure $ BS.SH.pack [fromIntegral lead + 0xA1, fromIntegral trail + 0xA1]
    Nothing -> encoderFailure $ Y.fromMaybe char err


-- | Look for a character in the jis0208 encoding at the given index.
decodeIndex0208 :: Word -> Maybe Char
decodeIndex0208 index =
    lookupMemoizedIndex decodeIndex0208M (Just encodeIndex0208M) index readDecodeIndex0208

-- | Memoization table to save lookup time in the over-large jis0208 index.
decodeIndex0208M :: DecoderMemoTable
decodeIndex0208M = newMemoizationTable
{-# NOINLINE decodeIndex0208M #-}

-- | Read the character at a given offset from the jis0208 index.  Note that
-- this is a heavy function, and should be cached whenever possible.
readDecodeIndex0208 :: Word -> Maybe Char
readDecodeIndex0208 index = search index $ loadIndex "jis0208"


-- | Look for a character in the jis0212 encoding at the given index.
decodeIndex0212 :: Word -> Maybe Char
decodeIndex0212 index = lookupMemoizedIndex decodeIndex0212M Nothing index readDecodeIndex0212

-- | Memoization table to save lookup time in the over-large jis0212 index.
decodeIndex0212M :: DecoderMemoTable
decodeIndex0212M = newMemoizationTable
{-# NOINLINE decodeIndex0212M #-}

-- | Read the character at a given offset from the jis0212 index.  Note that
-- this is a heavy function, and should be cached whenever possible.
readDecodeIndex0212 :: Word -> Maybe Char
readDecodeIndex0212 index = search index $ loadIndex "jis0212"


-- | Look for the index of a given character in the jis0208 encoding.
encodeIndex0208 :: Char -> Maybe Word
encodeIndex0208 char =
    lookupMemoizedIndex encodeIndex0208M (Just decodeIndex0208M) char readEncodeIndex0208

-- | Memoization table to save lookup time in the over-large jis0208 index.
encodeIndex0208M :: EncoderMemoTable
encodeIndex0208M = newMemoizationTable
{-# NOINLINE encodeIndex0208M #-}

-- | Find the offset of a given character in the jis0208 index.  Note that this
-- is a heavy function, and should be cached whenever possible.
readEncodeIndex0208 :: Char -> Maybe Word
readEncodeIndex0208 char = search char . map U.swap $ loadIndex "jis0208"