willow-0.1.0.0: src/Web/Willow/Common/Encoding/Iso2022Jp.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE Trustworthy #-}
{-|
Description: Character translation functions to and from the ISO-2022-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.Iso2022Jp
( Iso2022JpMode ( .. )
-- * Decoder
, decoder
, Iso2022JpDecoderState ( .. )
, defaultIso2022JpDecoderState
, Iso2022TextBuilder
-- * Encoder
, encoder
, Iso2022JpEncoderState ( .. )
, defaultIso2022JpEncoderState
, Iso2022BinaryBuilder
) where
import qualified Control.Applicative as A
import qualified Control.Monad as N
import qualified Data.ByteString as BS
import qualified Data.ByteString.Short as BS.SH
import qualified Data.Char as C
import qualified Data.Maybe as Y
import qualified Data.Vector as V
import Control.Applicative ( (<|>) )
import Data.Functor ( ($>) )
import Data.Vector ( (!?) )
import Web.Willow.Common.Encoding.Common
import Web.Willow.Common.Parser
import Web.Willow.Common.Parser.Switch
import Web.Willow.Common.Encoding.EucJp
( decodeIndex0208, encodeIndex0208 )
-- | 'Iso2022Jp' is a "sticky-shift" encoding, where the escape sequences
-- change the behaviour of every following character until the next escape; it
-- therefore needs to track which character mode it is currently in.
-- Additionally, escape sequences need to be separated by at least one real
-- character, and while that could be ensured with pure code, we may as well
-- add it to the state data being tracked anyway.
data Iso2022JpDecoderState = Iso2022JpDecoderState
{ decoderMode :: Iso2022JpMode
-- ^ The set of character mappings currently "loaded" into the
-- algorithm.
, decoderErrorOnEscape :: Bool
-- ^ Whether an output character is /required/ ('True') or if an escape
-- sequence is allowed ('False').
}
deriving ( Eq, Show, Read )
-- | 'Iso2022Jp' is a "sticky-shift" encoding, where the escape sequences
-- change the behaviour of every following character until the next escape; it
-- therefore needs to track which character mode it is currently in.
newtype Iso2022JpEncoderState = Iso2022JpEncoderState
{ encoderMode :: Iso2022JpMode
-- ^ The set of character mappings currently "loaded" into the
-- algorithm.
}
deriving ( Eq, Show, Read )
-- | The various 'Char'-byte mappings which may be loaded by the 'Iso2022Jp'
-- algorithms.
data Iso2022JpMode
= Ascii
-- ^ The basic, seven-bit ASCII set.
| Roman
-- ^ As 'Ascii', but replacing backslash (@\'\\'@) with yen (@\'¥'@)
-- and tilde (@'~'@) with overline (@\'‾'@).
| Katakana
-- ^ The standard block of halfwidth katakana located in Unicode
-- between @\\xFF61@ and @\\xFF9F@.
--
-- Note that this has identical behaviour to 'Multibyte' when passed to
-- 'encoder'; halfwidth katakana are instead encoded as fullwidth.
| Multibyte
-- ^ An encoding of the full jis0208 set into a seven-bit-compatible
-- transmission scheme.
deriving ( Eq, Ord, Bounded, Enum, Show, Read )
-- | The default initial state to kickstart the 'Iso2022Jp' decoder.
defaultIso2022JpDecoderState :: Iso2022JpDecoderState
defaultIso2022JpDecoderState = Iso2022JpDecoderState
{ decoderMode = Ascii
, decoderErrorOnEscape = False
}
-- | The default initial state to kickstart the 'Iso2022Jp' encoder.
defaultIso2022JpEncoderState :: Iso2022JpEncoderState
defaultIso2022JpEncoderState = Iso2022JpEncoderState
{ encoderMode = Ascii
}
-- | Shorthand type for parser combinators written for the 'Iso2022Jp' decoding
-- algorithm.
type Iso2022TextBuilder = StateTextBuilder Iso2022JpDecoderState
-- | __Encoding:__
-- @[ISO-2022-JP decoder]
-- (https://encoding.spec.whatwg.org/#iso-2022-jp-decoder)@
--
-- Decodes a 'Char' from a binary stream encoded with the 'Iso2022Jp' encoding
-- scheme, or returns 'Left' if the stream starts with an invalid byte
-- sequence.
decoder :: Iso2022TextBuilder
decoder = do
state <- getDecoderState
char <- case decoderMode state of
Ascii -> decoderAscii
Roman -> decoderRoman
Katakana -> decoderKatakana
Multibyte -> decoderMultibyte
N.unless (char == Right "") clearDecoderOutput
return char
-- | __Encoding:__
-- @[ISO-2022-JP decoder]
-- (https://encoding.spec.whatwg.org/#iso-2022-jp-decoder)@
-- @ASCII@ case
--
-- Process the head of a binary stream according to the 'Ascii' character set.
decoderAscii :: Iso2022TextBuilder
decoderAscii = next >>= switch
[ If (`elem` [0x0E, 0x0F]) decoderFailure1
, If_ (== toByte '\ESC') decoderEscape
, If isAsciiByte toUnicode1
, Else decoderFailure1
]
-- | __Encoding:__
-- @[ISO-2022-JP decoder]
-- (https://encoding.spec.whatwg.org/#iso-2022-jp-decoder)@
-- @Roman@ case
--
-- Process the head of a binary stream according to the 'Roman' character set.
decoderRoman :: Iso2022TextBuilder
decoderRoman = next >>= switch
[ If (`elem` [0x0E, 0x0F]) decoderFailure1
, If_ (== toByte '\ESC') decoderEscape
, If_ (== toByte '\\') $ emit [toByte '\\'] '\x00A5'
, If_ (== toByte '~') $ emit [toByte '~'] '\x203E'
, If isAsciiByte toUnicode1
, Else decoderFailure1
]
-- | __Encoding:__
-- @[ISO-2022-JP decoder]
-- (https://encoding.spec.whatwg.org/#iso-2022-jp-decoder)@
-- @katakana@ case
--
-- Process the head of a binary stream according to the 'Katakana' character
-- set.
decoderKatakana :: Iso2022TextBuilder
decoderKatakana = next >>= switch
[ If_ (== toByte '\ESC') decoderEscape
, If (range 0x21 0x5F) $ \b -> emit [b] . toEnum . (+ 0xFF40) $ fromIntegral b
, Else decoderFailure1
]
-- | __Encoding:__
-- @[ISO-2022-JP decoder]
-- (https://encoding.spec.whatwg.org/#iso-2022-jp-decoder)@
-- @lead byte@ and @trail byte@ cases
--
-- Process the head of a binary stream according to the 'Multibyte' character
-- set.
decoderMultibyte :: Iso2022TextBuilder
decoderMultibyte = next >>= switch
[ If_ (== toByte '\ESC') decoderEscape
, If (range 0x21 0x7E) $ \b -> decoderMultibyte' b <|> decoderFailure1 b
, Else decoderFailure1
]
where decoderMultibyte' lead = next >>= switch
[ If_ (== toByte '\ESC') $ push (toByte '\ESC') *> decoderFailure1 lead
, If (range 0x21 0x7E) $ \second ->
maybe (decoderFailure [lead, second]) (emit [lead, second]) . decodeIndex0208 $
(fromIntegral lead - 0x21) * 94 + fromIntegral second - 0x21
, Else $ decoderFailure2 lead
]
-- | __Encoding:__
-- @[ISO-2022-JP decoder]
-- (https://encoding.spec.whatwg.org/#iso-2022-jp-decoder)@
-- @Escape start@ and @Escape@ cases
--
-- Process the head of a binary stream as an escape sequence transitioning from
-- one character set to another.
decoderEscape :: Iso2022TextBuilder
decoderEscape = do
err <- decoderErrorOnEscape <$> getDecoderState
nextChunk 2 >>= switch
[ If (== "$@") $ switchMode err Multibyte
, If (== "$B") $ switchMode err Multibyte
, If (== "(B") $ switchMode err Ascii
, If (== "(J") $ switchMode err Roman
, If (== "(I") $ switchMode err Katakana
, Else $ \bs -> do
pushChunk bs
clearDecoderOutput
decoderFailure1 $ toByte '\ESC'
]
where switchMode err mode bs = do
modifyDecoderState $ \state -> state
{ decoderMode = mode
, decoderErrorOnEscape = True
}
if err
then decoderFailure $ BS.unpack bs
else return $ pure []
-- | Mark that some character or error output has occurred, and so allow an
-- escape sequence without complaint.
clearDecoderOutput :: StateDecoder Iso2022JpDecoderState ()
clearDecoderOutput = modifyDecoderState $ \s -> s
{ decoderErrorOnEscape = False
}
-- | Shorthand type for parser combinators written for the 'Iso2022Jp' encoding
-- algorithm.
type Iso2022BinaryBuilder = StateBinaryBuilder Iso2022JpEncoderState
-- | __Encoding:__
-- @[ISO-2022-JP encoder]
-- (https://encoding.spec.whatwg.org/#iso-2022-jp-encoder)@
--
-- Encode the first 'Char' in a string according to the 'Iso2022Jp' encoding
-- scheme, or return that same character if that scheme doesn't define a binary
-- representation for it.
encoder :: Iso2022BinaryBuilder
encoder = do
state <- getEncoderState
bs' <- A.optional $ case encoderMode state of
Ascii -> encoderAscii
Roman -> encoderRoman
Katakana -> encoderMultibyte True
Multibyte -> encoderMultibyte True
case bs' of
Nothing | encoderMode state == Ascii -> A.empty
Nothing -> setEncoderMode Ascii
Just bs -> return bs
-- | __Encoding:__
-- @[ISO-2022-JP encoder]
-- (https://encoding.spec.whatwg.org/#iso-2022-jp-encoder)@
-- steps 3-4, 7-12
--
-- Process the head of a 'Data.Text.Lazy.Text' string starting in the 'Ascii'
-- character set, insert the appropriate escape sequence if a mode transition
-- is required, or return 'Left' if all else fails.
encoderAscii :: Iso2022BinaryBuilder
encoderAscii = next >>= \c -> switch
[ If_ (`elem` ['\SO', '\SI', '\ESC']) $ encoderFailure '\xFFFD'
, If C.isAscii fromAscii
, If_ (`elem` ['\x00A5', '\x203E']) $ push c *> setEncoderMode Roman
] c <|> encoderFallback c
-- | __Encoding:__
-- @[ISO-2022-JP encoder]
-- (https://encoding.spec.whatwg.org/#iso-2022-jp-encoder)@
-- steps 5-6, 8-12
--
-- Process the head of a 'Data.Text.Lazy.Text' string starting in the 'Roman'
-- character set, insert the appropriate escape sequence if a mode transition
-- is required, or return 'Left' if all else fails.
encoderRoman :: Iso2022BinaryBuilder
encoderRoman = next >>= \c -> switch
[ If_ (`elem` ['\SO', '\SI', '\ESC']) $ encoderFailure '\xFFFD'
, If_ (`elem` ['\\', '~']) $ push c *> setEncoderMode Ascii
, If C.isAscii fromAscii
, If_ (== '\x00A5') $ return (pure "\\")
, If_ (== '\x203E') $ return (pure "~")
] c <|> encoderFallback c
-- | __Encoding:__
-- @[ISO-2022-JP encoder]
-- (https://encoding.spec.whatwg.org/#iso-2022-jp-encoder)@
-- steps 8-12
--
-- Attempt to parse the character in (specifically) the 'Multibyte' mode, and
-- return 'Left' if that fails.
encoderFallback :: Char -> Iso2022BinaryBuilder
encoderFallback c = A.optional encoderFallback' >>= maybe (encoderFailure c) return
where encoderFallback' = do
push c
escape <- setEncoderMode Multibyte
_ <- lookAhead $ encoderMultibyte False
return escape
-- | __Encoding:__
-- @[ISO-2022-JP encoder]
-- (https://encoding.spec.whatwg.org/#iso-2022-jp-encoder)@
-- steps 6-15
--
-- Process the head of a 'Data.Text.Lazy.Text' string starting in the
-- 'Multibyte' character set, insert the appropriate escape sequence if a mode
-- transition is required, or return 'Left' if all else fails.
encoderMultibyte
:: Bool
-- ^ Whether the encoder is allowed to switch to another mode, or
-- whether it's being used as a fallback and so must remain in
-- 'Multibyte'.
-> Iso2022BinaryBuilder
encoderMultibyte allowRecovery = next >>= \c -> switch
[ If_ C.isAscii $ push c *> setEncoderMode' Ascii
, If_ (`elem` ['\x00A5', '\x203E']) $ push c *> setEncoderMode' Roman
, If_ (== '\x2212') $ indexBytes c (encodeIndex0208 '\xFF0D')
, If (range '\xFF61' '\xFF9F') $ indexBytes c . encodeIndex0208 . remapKatakana
, Else $ indexBytes c . encodeIndex0208
] c
where setEncoderMode' s
| allowRecovery = setEncoderMode s
| otherwise = A.empty
indexBytes c Nothing = do
state <- getEncoderState
if encoderMode state `elem` [Katakana, Multibyte]
then push c *> setEncoderMode' Ascii
else encoderFailure c
indexBytes _ (Just code) = return . pure $
BS.SH.pack [fromIntegral lead + 0x21, fromIntegral trail + 0x21]
where (lead, trail) = divMod code 94
-- | Transition the encoder from one character set to another, emitting the
-- appropriate binary escape sequence.
setEncoderMode :: Iso2022JpMode -> Iso2022BinaryBuilder
setEncoderMode mode = modifyEncoderState setEncoderMode' $> pure esc
where esc = case mode of
Ascii -> "\ESC(B"
Roman -> "\ESC(J"
Katakana -> "\ESC$B"
Multibyte -> "\ESC$B"
setEncoderMode' state = state
{ encoderMode = mode
}
-- | __Encoding:__
-- @[ISO-2022-JP encoder]
-- (https://encoding.spec.whatwg.org/#iso-2022-jp-encoder)@
-- step 9
--
-- Expand halfwidth katakana into their (more appropriate) fullwidth forms.
remapKatakana :: Char -> Char
remapKatakana c = Y.fromMaybe '\xFFFD' $ indexKatakana !? (fromEnum c - 0xFF61)
-- | The mapping between halfwidth and fullwidth katakana, in an easily-indexed
-- structure.
indexKatakana :: V.Vector Char
indexKatakana = V.fromList . map snd $ loadIndex "iso-2022-jp-katakana"