glirc-2.40: src/Client/Image/MircFormatting.hs
{-# Language TemplateHaskell #-}
{-|
Module : Client.Image.MircFormatting
Description : Parser for mIRC's text formatting encoding
Copyright : (c) Eric Mertens, 2016
License : ISC
Maintainer : emertens@gmail.com
This module parses mIRC encoded text and generates VTY images.
-}
module Client.Image.MircFormatting
( parseIrcText
, parseIrcText'
, plainText
, controlImage
, mircColor
, mircColors
) where
import Client.Image.PackedImage as I
import Client.Image.Palette (Palette, palMonospace)
import Control.Applicative ((<|>), empty)
import Control.Lens (view, over, set, makeLensesFor)
import Data.Attoparsec.Text as Parse
import Data.Bits (Bits(xor))
import Data.Char (ord, chr, isControl, isHexDigit)
import Data.Maybe (fromMaybe, isNothing)
import Data.Text (Text)
import Data.Vector (Vector)
import Data.Vector qualified as Vector
import Graphics.Vty.Attributes
import Numeric (readHex)
makeLensesFor
[ ("attrForeColor", "foreColorLens")
, ("attrBackColor", "backColorLens")
, ("attrStyle" , "styleLens" )]
''Attr
-- | Parse mIRC encoded format characters and hide the control characters.
parseIrcText :: Palette -> Text -> Image'
parseIrcText = parseIrcText' False
-- | Parse mIRC encoded format characters and render the control characters
-- explicitly. This view is useful when inputting control characters to make
-- it clear where they are in the text.
parseIrcText' :: Bool -> Palette -> Text -> Image'
parseIrcText' explicit pal
= either plainText id
. parseOnly (pIrcLine pal explicit False defAttr)
data Segment = TextSegment Text | ControlSegment Char
pSegment :: Parser Segment
pSegment = TextSegment <$> takeWhile1 (not . isControl)
<|> ControlSegment <$> satisfy isControl
pIrcLine :: Palette -> Bool -> Bool -> Attr -> Parser Image'
pIrcLine pal explicit mono fmt =
do seg <- option Nothing (Just <$> pSegment)
case seg of
Nothing -> return mempty
Just (TextSegment txt) ->
do rest <- pIrcLine pal explicit mono fmt
return (text' fmt txt <> rest)
Just (ControlSegment '\^C') ->
do (numberText, colorNumbers) <- match (pColorNumbers pColorNumber)
rest <- pIrcLine pal explicit mono (applyColors colorNumbers fmt)
return $ if explicit
then controlImage '\^C'
<> text' defAttr numberText
<> rest
else rest
Just (ControlSegment '\^D') ->
do (numberText, colorNumbers) <- match (pColorNumbers pColorHex)
rest <- pIrcLine pal explicit mono (applyColors colorNumbers fmt)
return $ if explicit
then controlImage '\^D'
<> text' defAttr numberText
<> rest
else rest
Just (ControlSegment '\^Q')
| explicit -> (controlImage '\^Q' <>) <$> rest
| otherwise -> rest
where
rest = pIrcLine pal explicit (not mono)
(if mono then defAttr else view palMonospace pal)
Just (ControlSegment c)
-- always render control codes that we don't understand
| isNothing mbFmt' || explicit ->
do rest <- next
return (controlImage c <> rest)
| otherwise -> next
where
mbFmt' = applyControlEffect c fmt
next = pIrcLine pal explicit mono (fromMaybe fmt mbFmt')
pColorNumbers ::
Parser (MaybeDefault Color) ->
Parser (Maybe (MaybeDefault Color, Maybe (MaybeDefault Color)))
pColorNumbers color = option Nothing $
do fc <- color
bc <- optional (Parse.char ',' *> color)
return (Just (fc,bc))
pColorNumber :: Parser (MaybeDefault Color)
pColorNumber =
do d1 <- digit
ds <- option [] (return <$> digit)
case mircColor (read (d1:ds)) of
Just c -> pure c
Nothing -> empty
pColorHex :: Parser (MaybeDefault Color)
pColorHex = SetTo <$> (rgbColor' <$> p <*> p <*> p)
where
p = do x <- satisfy isHexDigit
y <- satisfy isHexDigit
pure (fst (head (readHex [x,y])))
optional :: Parser a -> Parser (Maybe a)
optional p = option Nothing (Just <$> p)
applyColors :: Maybe (MaybeDefault Color, Maybe (MaybeDefault Color)) -> Attr -> Attr
applyColors Nothing = set foreColorLens Default
. set backColorLens Default
applyColors (Just (c1, Nothing)) = set foreColorLens c1 -- preserve background
applyColors (Just (c1, Just c2)) = set foreColorLens c1
. set backColorLens c2
mircColor :: Int -> Maybe (MaybeDefault Color)
mircColor 99 = Just Default
mircColor i = SetTo <$> mircColors Vector.!? i
mircColors :: Vector Color
mircColors =
Vector.fromList $
[ white -- white
, black -- black
, blue -- blue
, green -- green
, red -- red
, rgbColor' 127 0 0 -- brown
, rgbColor' 156 0 156 -- purple
, rgbColor' 252 127 0 -- yellow
, yellow -- yellow
, brightGreen -- green
, cyan -- brightBlue
, brightCyan -- brightCyan
, brightBlue -- brightBlue
, rgbColor' 255 0 255 -- brightRed
, rgbColor' 127 127 127 -- brightBlack
, rgbColor' 210 210 210 -- brightWhite
] ++
map (Color240 . subtract 16) [ -- https://modern.ircdocs.horse/formatting.html#colors-16-98
052, 094, 100, 058,
022, 029, 023, 024, 017, 054, 053, 089, 088, 130,
142, 064, 028, 035, 030, 025, 018, 091, 090, 125,
124, 166, 184, 106, 034, 049, 037, 033, 019, 129,
127, 161, 196, 208, 226, 154, 046, 086, 051, 075,
021, 171, 201, 198, 203, 215, 227, 191, 083, 122,
087, 111, 063, 177, 207, 205, 217, 223, 229, 193,
157, 158, 159, 153, 147, 183, 219, 212, 016, 233,
235, 237, 239, 241, 244, 247, 250, 254, 231 ]
rgbColor' :: Int -> Int -> Int -> Color
rgbColor' = srgbColor -- fix the type to Int
applyControlEffect :: Char -> Attr -> Maybe Attr
applyControlEffect '\^B' attr = Just $! toggleStyle bold attr
applyControlEffect '\^V' attr = Just $! toggleStyle reverseVideo attr
applyControlEffect '\^_' attr = Just $! toggleStyle underline attr
applyControlEffect '\^^' attr = Just $! toggleStyle strikethrough attr
applyControlEffect '\^]' attr = Just $! toggleStyle italic attr
applyControlEffect '\^O' _ = Just defAttr
applyControlEffect _ _ = Nothing
toggleStyle :: Style -> Attr -> Attr
toggleStyle s1 = over styleLens $ \old ->
case old of
SetTo s2 -> SetTo (xor s1 s2)
_ -> SetTo s1
-- | Safely render a control character.
controlImage :: Char -> Image'
controlImage = I.char attr . controlName
where
attr = withStyle defAttr reverseVideo
controlName c
| c < '\128' = chr (0x40 `xor` ord c)
| otherwise = '!'
-- | Render a 'String' with default attributes and replacing all of the
-- control characters with reverse-video letters corresponding to caret
-- notation.
plainText :: String -> Image'
plainText "" = mempty
plainText xs =
case break isControl xs of
(first, "" ) -> I.string defAttr first
(first, cntl:rest) -> I.string defAttr first <>
controlImage cntl <>
plainText rest