module Text.XML.Stream.Parse
(
parseBytes
, parseBytesPos
, parseText'
, parseText
, parseTextPos
, detectUtf
, parseFile
, parseLBS
, ParseSettings
, def
, DecodeEntities
, psDecodeEntities
, psRetainNamespaces
, decodeXmlEntities
, decodeHtmlEntities
, tag
, tagPredicate
, tagName
, tagNoAttr
, content
, contentMaybe
, AttrParser
, attr
, requireAttr
, optionalAttr
, requireAttrRaw
, optionalAttrRaw
, ignoreAttrs
, orE
, choose
, many
, force
, XmlException (..)
, PositionRange
, EventPos
) where
import qualified Control.Applicative as A
import Control.Monad.Trans.Resource (MonadResource, MonadThrow (..),
monadThrow)
import Data.Attoparsec.Text (Parser, anyChar, char, manyTill,
skipWhile, string, takeWhile,
takeWhile1, try)
import Data.Conduit.Attoparsec (PositionRange, conduitParser)
import Data.XML.Types (Content (..), Event (..),
ExternalID (..),
Instruction (..), Name (..))
import Blaze.ByteString.Builder (fromWord32be, toByteString)
import Control.Applicative (Alternative (empty, (<|>)),
Applicative (..), (<$>))
import Control.Arrow ((***))
import Control.Exception (Exception (..), SomeException)
import Control.Monad (ap, guard, liftM, void)
import Control.Monad.Trans.Class (lift)
import qualified Data.ByteString as S
import qualified Data.ByteString.Lazy as L
import Data.Char (isSpace)
import Data.Conduit
import Data.Conduit.Binary (sourceFile)
import qualified Data.Conduit.Internal as CI
import qualified Data.Conduit.List as CL
import qualified Data.Conduit.Text as CT
import Data.Default (Default (..))
import Data.List (foldl')
import qualified Data.Map as Map
import Data.Maybe (fromMaybe, isNothing)
import Data.Text (Text, pack)
import qualified Data.Text as T
import qualified Data.Text as TS
import Data.Text.Encoding (decodeUtf32BEWith)
import Data.Text.Encoding.Error (ignore)
import Data.Text.Read (Reader, decimal, hexadecimal)
import Data.Typeable (Typeable)
import Data.Word (Word32)
import Filesystem.Path.CurrentOS (FilePath, encodeString)
import Prelude hiding (FilePath, takeWhile)
import Text.XML.Stream.Token
type Ents = [(Text, Text)]
tokenToEvent :: ParseSettings -> Ents -> [NSLevel] -> Token -> (Ents, [NSLevel], [Event])
tokenToEvent _ es n (TokenBeginDocument _) = (es, n, [])
tokenToEvent _ es n (TokenInstruction i) = (es, n, [EventInstruction i])
tokenToEvent ps es n (TokenBeginElement name as isClosed _) =
(es, n', if isClosed then [begin, end] else [begin])
where
l0 = case n of
[] -> NSLevel Nothing Map.empty
x:_ -> x
(as', l') = foldl' go (id, l0) as
go (front, l) (TName kpref kname, val) =
(addNS front, l'')
where
isPrefixed = kpref == Just "xmlns"
isUnprefixed = isNothing kpref && kname == "xmlns"
addNS
| not (psRetainNamespaces ps) && (isPrefixed || isUnprefixed) = id
| otherwise = (((tname, map resolve val):) .)
where
tname
| isPrefixed = TName Nothing ("xmlns:" `T.append` kname)
| otherwise = TName kpref kname
l''
| isPrefixed =
l { prefixes = Map.insert kname (contentsToText val)
$ prefixes l }
| isUnprefixed =
l { defaultNS = if T.null $ contentsToText val
then Nothing
else Just $ contentsToText val }
| otherwise = l
resolve (ContentEntity e)
| Just t <- lookup e es = ContentText t
resolve c = c
n' = if isClosed then n else l' : n
fixAttName (name', val) = (tnameToName True l' name', val)
elementName = tnameToName False l' name
begin = EventBeginElement elementName $ map fixAttName $ as' []
end = EventEndElement elementName
tokenToEvent _ es n (TokenEndElement name) =
(es, n', [EventEndElement $ tnameToName False l name])
where
(l, n') =
case n of
[] -> (NSLevel Nothing Map.empty, [])
x:xs -> (x, xs)
tokenToEvent _ es n (TokenContent (ContentEntity e))
| Just t <- lookup e es = (es, n, [EventContent $ ContentText t])
tokenToEvent _ es n (TokenContent c) = (es, n, [EventContent c])
tokenToEvent _ es n (TokenComment c) = (es, n, [EventComment c])
tokenToEvent _ es n (TokenDoctype t eid es') = (es ++ es', n, [EventBeginDoctype t eid, EventEndDoctype])
tokenToEvent _ es n (TokenCDATA t) = (es, n, [EventCDATA t])
tnameToName :: Bool -> NSLevel -> TName -> Name
tnameToName _ _ (TName (Just "xml") name) =
Name name (Just "http://www.w3.org/XML/1998/namespace") (Just "xml")
tnameToName isAttr (NSLevel def' _) (TName Nothing name) =
Name name (if isAttr then Nothing else def') Nothing
tnameToName _ (NSLevel _ m) (TName (Just pref) name) =
case Map.lookup pref m of
Just ns -> Name name (Just ns) (Just pref)
Nothing -> Name name Nothing (Just pref)
detectUtf :: MonadThrow m => Conduit S.ByteString m TS.Text
detectUtf =
conduit id
where
conduit front = await >>= maybe (return ()) (push front)
push front bss =
either conduit (\(bss', continue) -> leftover bss' >> continue)
(getEncoding front bss)
getEncoding front bs'
| S.length bs < 4 =
Left (bs `S.append`)
| otherwise =
Right (bsOut, CT.decode codec)
where
bs = front bs'
bsOut = S.append (S.drop toDrop x) y
(x, y) = S.splitAt 4 bs
(toDrop, codec) =
case S.unpack x of
[0x00, 0x00, 0xFE, 0xFF] -> (4, CT.utf32_be)
[0xFF, 0xFE, 0x00, 0x00] -> (4, CT.utf32_le)
0xFE : 0xFF: _ -> (2, CT.utf16_be)
0xFF : 0xFE: _ -> (2, CT.utf16_le)
0xEF : 0xBB: 0xBF : _ -> (3, CT.utf8)
[0x00, 0x00, 0x00, 0x3C] -> (0, CT.utf32_be)
[0x3C, 0x00, 0x00, 0x00] -> (0, CT.utf32_le)
[0x00, 0x3C, 0x00, 0x3F] -> (0, CT.utf16_be)
[0x3C, 0x00, 0x3F, 0x00] -> (0, CT.utf16_le)
_ -> (0, CT.utf8)
type EventPos = (Maybe PositionRange, Event)
parseBytes :: MonadThrow m
=> ParseSettings
-> Conduit S.ByteString m Event
parseBytes = mapOutput snd . parseBytesPos
parseBytesPos :: MonadThrow m
=> ParseSettings
-> Conduit S.ByteString m EventPos
parseBytesPos ps = detectUtf =$= parseTextPos ps
dropBOM :: Monad m => Conduit TS.Text m TS.Text
dropBOM =
await >>= maybe (return ()) push
where
push t =
case T.uncons t of
Nothing -> dropBOM
Just (c, cs) ->
let output
| c == '\xfeef' = cs
| otherwise = t
in yield output >> idConduit
idConduit = await >>= maybe (return ()) (\x -> yield x >> idConduit)
parseText' :: MonadThrow m
=> ParseSettings
-> Conduit TS.Text m Event
parseText' = mapOutput snd . parseTextPos
parseText :: MonadThrow m
=> ParseSettings
-> Conduit TS.Text m EventPos
parseText = parseTextPos
parseTextPos :: MonadThrow m
=> ParseSettings
-> Conduit TS.Text m EventPos
parseTextPos de =
dropBOM
=$= tokenize
=$= toEventC de
=$= addBeginEnd
where
tokenize = conduitToken de
addBeginEnd = yield (Nothing, EventBeginDocument) >> addEnd
addEnd = await >>= maybe
(yield (Nothing, EventEndDocument))
(\e -> yield e >> addEnd)
toEventC :: Monad m => ParseSettings -> Conduit (PositionRange, Token) m EventPos
toEventC ps =
go [] []
where
go !es !levels =
await >>= maybe (return ()) push
where
push (position, token) =
mapM_ (yield . (,) (Just position)) events >> go es' levels'
where
(es', levels', events) = tokenToEvent ps es levels token
data ParseSettings = ParseSettings
{ psDecodeEntities :: DecodeEntities
, psRetainNamespaces :: Bool
}
instance Default ParseSettings where
def = ParseSettings
{ psDecodeEntities = decodeXmlEntities
, psRetainNamespaces = False
}
conduitToken :: MonadThrow m => ParseSettings -> Conduit TS.Text m (PositionRange, Token)
conduitToken = conduitParser . parseToken . psDecodeEntities
parseToken :: DecodeEntities -> Parser Token
parseToken de = (char '<' >> parseLt) <|> TokenContent <$> parseContent de False False
where
parseLt =
(char '?' >> parseInstr) <|>
(char '!' >> (parseComment <|> parseCdata <|> parseDoctype)) <|>
parseBegin <|>
(char '/' >> parseEnd)
parseInstr = do
name <- parseIdent
if name == "xml"
then do
as <- A.many $ parseAttribute de
skipSpace
char' '?'
char' '>'
newline <|> return ()
return $ TokenBeginDocument as
else do
skipSpace
x <- T.pack <$> manyTill anyChar (try $ string "?>")
return $ TokenInstruction $ Instruction name x
parseComment = do
char' '-'
char' '-'
c <- T.pack <$> manyTill anyChar (string "-->")
return $ TokenComment c
parseCdata = do
_ <- string "[CDATA["
t <- T.pack <$> manyTill anyChar (string "]]>")
return $ TokenCDATA t
parseDoctype = do
_ <- string "DOCTYPE"
skipSpace
name <- parseName
let i =
case name of
TName Nothing x -> x
TName (Just x) y -> T.concat [x, ":", y]
skipSpace
eid <- fmap Just parsePublicID <|>
fmap Just parseSystemID <|>
return Nothing
skipSpace
ents <- (do
char' '['
ents <- parseEntities id
skipSpace
return ents) <|> return []
char' '>'
newline <|> return ()
return $ TokenDoctype i eid ents
parseEntities front =
(char ']' >> return (front [])) <|>
(parseEntity >>= \e -> parseEntities (front . (e:))) <|>
(char '<' >> parseEntities front) <|>
(skipWhile (\t -> t /= ']' && t /= '<') >> parseEntities front)
parseEntity = try $ do
_ <- string "<!ENTITY"
skipSpace
i <- parseIdent
t <- quotedText
skipSpace
char' '>'
return (i, t)
parsePublicID = do
_ <- string "PUBLIC"
x <- quotedText
y <- quotedText
return $ PublicID x y
parseSystemID = do
_ <- string "SYSTEM"
x <- quotedText
return $ SystemID x
quotedText = do
skipSpace
between '"' <|> between '\''
between c = do
char' c
x <- takeWhile (/=c)
char' c
return x
parseEnd = do
skipSpace
n <- parseName
skipSpace
char' '>'
return $ TokenEndElement n
parseBegin = do
skipSpace
n <- parseName
as <- A.many $ parseAttribute de
skipSpace
isClose <- (char '/' >> skipSpace >> return True) <|> return False
char' '>'
return $ TokenBeginElement n as isClose 0
parseAttribute :: DecodeEntities -> Parser TAttribute
parseAttribute de = do
skipSpace
key <- parseName
skipSpace
char' '='
skipSpace
val <- squoted <|> dquoted
return (key, val)
where
squoted = char '\'' *> manyTill (parseContent de False True) (char '\'')
dquoted = char '"' *> manyTill (parseContent de True False) (char '"')
parseName :: Parser TName
parseName =
name <$> parseIdent <*> A.optional (char ':' >> parseIdent)
where
name i1 Nothing = TName Nothing i1
name i1 (Just i2) = TName (Just i1) i2
parseIdent :: Parser Text
parseIdent =
takeWhile1 valid
where
valid '&' = False
valid '<' = False
valid '>' = False
valid ':' = False
valid '?' = False
valid '=' = False
valid '"' = False
valid '\'' = False
valid '/' = False
valid c = not $ isXMLSpace c
parseContent :: DecodeEntities
-> Bool
-> Bool
-> Parser Content
parseContent de breakDouble breakSingle =
parseEntity <|> parseText'
where
parseEntity = do
char' '&'
t <- takeWhile1 (/= ';')
char' ';'
return $ de t
parseText' = do
bs <- takeWhile1 valid
return $ ContentText bs
valid '"' = not breakDouble
valid '\'' = not breakSingle
valid '&' = False
valid '<' = False
valid _ = True
skipSpace :: Parser ()
skipSpace = skipWhile isXMLSpace
isXMLSpace :: Char -> Bool
isXMLSpace ' ' = True
isXMLSpace '\t' = True
isXMLSpace '\r' = True
isXMLSpace '\n' = True
isXMLSpace _ = False
newline :: Parser ()
newline = ((char '\r' >> char '\n') <|> char '\n') >> return ()
char' :: Char -> Parser ()
char' = void . char
data ContentType =
Ignore | IsContent Text | IsError String | NotContent
contentMaybe :: MonadThrow m => Consumer Event m (Maybe Text)
contentMaybe = do
x <- CL.peek
case pc' x of
Ignore -> CL.drop 1 >> contentMaybe
IsContent t -> CL.drop 1 >> fmap Just (takeContents (t:))
IsError e -> lift $ monadThrow $ XmlException e x
NotContent -> return Nothing
where
pc' Nothing = NotContent
pc' (Just x) = pc x
pc (EventContent (ContentText t)) = IsContent t
pc (EventContent (ContentEntity e)) = IsError $ "Unknown entity: " ++ show e
pc (EventCDATA t) = IsContent t
pc EventBeginElement{} = NotContent
pc EventEndElement{} = NotContent
pc EventBeginDocument{} = Ignore
pc EventEndDocument = Ignore
pc EventBeginDoctype{} = Ignore
pc EventEndDoctype = Ignore
pc EventInstruction{} = Ignore
pc EventComment{} = Ignore
takeContents front = do
x <- CL.peek
case pc' x of
Ignore -> CL.drop 1 >> takeContents front
IsContent t -> CL.drop 1 >> takeContents (front . (:) t)
IsError e -> lift $ monadThrow $ XmlException e x
NotContent -> return $ T.concat $ front []
content :: MonadThrow m => Consumer Event m Text
content = fromMaybe T.empty <$> contentMaybe
tag :: MonadThrow m
=> (Name -> Maybe a)
-> (a -> AttrParser b)
-> (b -> CI.ConduitM Event o m c)
-> CI.ConduitM Event o m (Maybe c)
tag checkName attrParser f = do
x <- dropWS
case x of
Just (EventBeginElement name as) ->
case checkName name of
Just y ->
case runAttrParser' (attrParser y) as of
Left e -> lift $ monadThrow e
Right z -> do
CL.drop 1
z' <- f z
a <- dropWS
case a of
Just (EventEndElement name')
| name == name' -> CL.drop 1 >> return (Just z')
_ -> lift $ monadThrow $ XmlException ("Expected end tag for: " ++ show name) a
Nothing -> return Nothing
_ -> return Nothing
where
dropWS = do
x <- CL.peek
let isWS =
case x of
Just EventBeginDocument -> True
Just EventEndDocument -> True
Just EventBeginDoctype{} -> True
Just EventEndDoctype -> True
Just EventInstruction{} -> True
Just EventBeginElement{} -> False
Just EventEndElement{} -> False
Just (EventContent (ContentText t))
| T.all isSpace t -> True
| otherwise -> False
Just (EventContent ContentEntity{}) -> False
Just EventComment{} -> True
Just EventCDATA{} -> False
Nothing -> False
if isWS then CL.drop 1 >> dropWS else return x
runAttrParser' p as =
case runAttrParser p as of
Left e -> Left e
Right ([], x) -> Right x
Right (attr, _) -> Left $ toException $ UnparsedAttributes attr
tagPredicate :: MonadThrow m
=> (Name -> Bool)
-> AttrParser a
-> (a -> CI.ConduitM Event o m b)
-> CI.ConduitM Event o m (Maybe b)
tagPredicate p attrParser = tag (guard . p) (const attrParser)
tagName :: MonadThrow m
=> Name
-> AttrParser a
-> (a -> CI.ConduitM Event o m b)
-> CI.ConduitM Event o m (Maybe b)
tagName name = tagPredicate (== name)
tagNoAttr :: MonadThrow m
=> Name
-> CI.ConduitM Event o m a
-> CI.ConduitM Event o m (Maybe a)
tagNoAttr name f = tagName name (return ()) $ const f
orE :: Monad m
=> Consumer Event m (Maybe a)
-> Consumer Event m (Maybe a)
-> Consumer Event m (Maybe a)
orE a b =
a >>= \x -> maybe b (const $ return x) x
choose :: Monad m
=> [Consumer Event m (Maybe a)]
-> Consumer Event m (Maybe a)
choose [] = return Nothing
choose (i:is) =
i >>= maybe (choose is) (return . Just)
force :: MonadThrow m
=> String
-> m (Maybe a)
-> m a
force msg i = i >>= maybe (throwM $ XmlException msg Nothing) return
parseFile :: MonadResource m
=> ParseSettings
-> FilePath
-> Producer m Event
parseFile ps fp = sourceFile (encodeString fp) =$= parseBytes ps
parseLBS :: MonadThrow m
=> ParseSettings
-> L.ByteString
-> Producer m Event
parseLBS ps lbs = CL.sourceList (L.toChunks lbs) =$= parseBytes ps
data XmlException = XmlException
{ xmlErrorMessage :: String
, xmlBadInput :: Maybe Event
}
| InvalidEndElement Name
| InvalidEntity Text
| UnparsedAttributes [(Name, [Content])]
deriving (Show, Typeable)
instance Exception XmlException
newtype AttrParser a = AttrParser { runAttrParser :: [(Name, [Content])] -> Either SomeException ([(Name, [Content])], a) }
instance Monad AttrParser where
return a = AttrParser $ \as -> Right (as, a)
(AttrParser f) >>= g = AttrParser $ \as ->
either Left (\(as', f') -> runAttrParser (g f') as') (f as)
instance Functor AttrParser where
fmap = liftM
instance Applicative AttrParser where
pure = return
(<*>) = ap
instance Alternative AttrParser where
empty = AttrParser $ const $ Left $ toException $ XmlException "AttrParser.empty" Nothing
AttrParser f <|> AttrParser g = AttrParser $ \x ->
either (const $ g x) Right (f x)
instance MonadThrow AttrParser where
throwM = AttrParser . const . throwM
optionalAttrRaw :: ((Name, [Content]) -> Maybe b) -> AttrParser (Maybe b)
optionalAttrRaw f =
AttrParser $ go id
where
go front [] = Right (front [], Nothing)
go front (a:as) =
maybe (go (front . (:) a) as)
(\b -> Right (front as, Just b))
(f a)
requireAttrRaw :: String -> ((Name, [Content]) -> Maybe b) -> AttrParser b
requireAttrRaw msg f = optionalAttrRaw f >>=
maybe (AttrParser $ const $ Left $ toException $ XmlException msg Nothing)
return
attr :: Name -> AttrParser (Maybe Text)
attr = optionalAttr
requireAttr :: Name -> AttrParser Text
requireAttr n = force ("Missing attribute: " ++ show n) $ attr n
optionalAttr :: Name -> AttrParser (Maybe Text)
optionalAttr n = optionalAttrRaw
(\(x, y) -> if x == n then Just (contentsToText y) else Nothing)
contentsToText :: [Content] -> Text
contentsToText =
T.concat . map toText
where
toText (ContentText t) = t
toText (ContentEntity e) = T.concat ["&", e, ";"]
ignoreAttrs :: AttrParser ()
ignoreAttrs = AttrParser $ const $ Right ([], ())
many :: Monad m
=> Consumer Event m (Maybe a)
-> Consumer Event m [a]
many i =
go id
where
go front = i >>=
maybe (return $ front [])
(\y -> go $ front . (:) y)
type DecodeEntities = Text -> Content
decodeXmlEntities :: DecodeEntities
decodeXmlEntities "lt" = ContentText "<"
decodeXmlEntities "gt" = ContentText ">"
decodeXmlEntities "amp" = ContentText "&"
decodeXmlEntities "quot" = ContentText "\""
decodeXmlEntities "apos" = ContentText "'"
decodeXmlEntities t = let backup = ContentEntity t in
case T.uncons t of
Just ('#', t') ->
case T.uncons t' of
Just ('x', t'')
| T.length t'' > 6 -> backup
| otherwise -> decodeChar hexadecimal backup t''
_
| T.length t' > 7 -> backup
| otherwise -> decodeChar decimal backup t'
_ -> backup
decodeHtmlEntities :: DecodeEntities
decodeHtmlEntities t =
case decodeXmlEntities t of
x@ContentText{} -> x
backup@ContentEntity{} ->
maybe backup ContentText $ Map.lookup t htmlEntities
htmlEntities :: Map.Map T.Text T.Text
htmlEntities = Map.fromList
$ map (pack *** pack)
[ ("nbsp", "\160")
, ("iexcl", "\161")
, ("cent", "\162")
, ("pound", "\163")
, ("curren", "\164")
, ("yen", "\165")
, ("brvbar", "\166")
, ("sect", "\167")
, ("uml", "\168")
, ("copy", "\169")
, ("ordf", "\170")
, ("laquo", "\171")
, ("not", "\172")
, ("shy", "\173")
, ("reg", "\174")
, ("macr", "\175")
, ("deg", "\176")
, ("plusmn", "\177")
, ("sup2", "\178")
, ("sup3", "\179")
, ("acute", "\180")
, ("micro", "\181")
, ("para", "\182")
, ("middot", "\183")
, ("cedil", "\184")
, ("sup1", "\185")
, ("ordm", "\186")
, ("raquo", "\187")
, ("frac14", "\188")
, ("frac12", "\189")
, ("frac34", "\190")
, ("iquest", "\191")
, ("Agrave", "\192")
, ("Aacute", "\193")
, ("Acirc", "\194")
, ("Atilde", "\195")
, ("Auml", "\196")
, ("Aring", "\197")
, ("AElig", "\198")
, ("Ccedil", "\199")
, ("Egrave", "\200")
, ("Eacute", "\201")
, ("Ecirc", "\202")
, ("Euml", "\203")
, ("Igrave", "\204")
, ("Iacute", "\205")
, ("Icirc", "\206")
, ("Iuml", "\207")
, ("ETH", "\208")
, ("Ntilde", "\209")
, ("Ograve", "\210")
, ("Oacute", "\211")
, ("Ocirc", "\212")
, ("Otilde", "\213")
, ("Ouml", "\214")
, ("times", "\215")
, ("Oslash", "\216")
, ("Ugrave", "\217")
, ("Uacute", "\218")
, ("Ucirc", "\219")
, ("Uuml", "\220")
, ("Yacute", "\221")
, ("THORN", "\222")
, ("szlig", "\223")
, ("agrave", "\224")
, ("aacute", "\225")
, ("acirc", "\226")
, ("atilde", "\227")
, ("auml", "\228")
, ("aring", "\229")
, ("aelig", "\230")
, ("ccedil", "\231")
, ("egrave", "\232")
, ("eacute", "\233")
, ("ecirc", "\234")
, ("euml", "\235")
, ("igrave", "\236")
, ("iacute", "\237")
, ("icirc", "\238")
, ("iuml", "\239")
, ("eth", "\240")
, ("ntilde", "\241")
, ("ograve", "\242")
, ("oacute", "\243")
, ("ocirc", "\244")
, ("otilde", "\245")
, ("ouml", "\246")
, ("divide", "\247")
, ("oslash", "\248")
, ("ugrave", "\249")
, ("uacute", "\250")
, ("ucirc", "\251")
, ("uuml", "\252")
, ("yacute", "\253")
, ("thorn", "\254")
, ("yuml", "\255")
, ("OElig", "\338")
, ("oelig", "\339")
, ("Scaron", "\352")
, ("scaron", "\353")
, ("Yuml", "\376")
, ("fnof", "\402")
, ("circ", "\710")
, ("tilde", "\732")
, ("Alpha", "\913")
, ("Beta", "\914")
, ("Gamma", "\915")
, ("Delta", "\916")
, ("Epsilon", "\917")
, ("Zeta", "\918")
, ("Eta", "\919")
, ("Theta", "\920")
, ("Iota", "\921")
, ("Kappa", "\922")
, ("Lambda", "\923")
, ("Mu", "\924")
, ("Nu", "\925")
, ("Xi", "\926")
, ("Omicron", "\927")
, ("Pi", "\928")
, ("Rho", "\929")
, ("Sigma", "\931")
, ("Tau", "\932")
, ("Upsilon", "\933")
, ("Phi", "\934")
, ("Chi", "\935")
, ("Psi", "\936")
, ("Omega", "\937")
, ("alpha", "\945")
, ("beta", "\946")
, ("gamma", "\947")
, ("delta", "\948")
, ("epsilon", "\949")
, ("zeta", "\950")
, ("eta", "\951")
, ("theta", "\952")
, ("iota", "\953")
, ("kappa", "\954")
, ("lambda", "\955")
, ("mu", "\956")
, ("nu", "\957")
, ("xi", "\958")
, ("omicron", "\959")
, ("pi", "\960")
, ("rho", "\961")
, ("sigmaf", "\962")
, ("sigma", "\963")
, ("tau", "\964")
, ("upsilon", "\965")
, ("phi", "\966")
, ("chi", "\967")
, ("psi", "\968")
, ("omega", "\969")
, ("thetasym", "\977")
, ("upsih", "\978")
, ("piv", "\982")
, ("ensp", "\8194")
, ("emsp", "\8195")
, ("thinsp", "\8201")
, ("zwnj", "\8204")
, ("zwj", "\8205")
, ("lrm", "\8206")
, ("rlm", "\8207")
, ("ndash", "\8211")
, ("mdash", "\8212")
, ("lsquo", "\8216")
, ("rsquo", "\8217")
, ("sbquo", "\8218")
, ("ldquo", "\8220")
, ("rdquo", "\8221")
, ("bdquo", "\8222")
, ("dagger", "\8224")
, ("Dagger", "\8225")
, ("bull", "\8226")
, ("hellip", "\8230")
, ("permil", "\8240")
, ("prime", "\8242")
, ("Prime", "\8243")
, ("lsaquo", "\8249")
, ("rsaquo", "\8250")
, ("oline", "\8254")
, ("frasl", "\8260")
, ("euro", "\8364")
, ("image", "\8465")
, ("weierp", "\8472")
, ("real", "\8476")
, ("trade", "\8482")
, ("alefsym", "\8501")
, ("larr", "\8592")
, ("uarr", "\8593")
, ("rarr", "\8594")
, ("darr", "\8595")
, ("harr", "\8596")
, ("crarr", "\8629")
, ("lArr", "\8656")
, ("uArr", "\8657")
, ("rArr", "\8658")
, ("dArr", "\8659")
, ("hArr", "\8660")
, ("forall", "\8704")
, ("part", "\8706")
, ("exist", "\8707")
, ("empty", "\8709")
, ("nabla", "\8711")
, ("isin", "\8712")
, ("notin", "\8713")
, ("ni", "\8715")
, ("prod", "\8719")
, ("sum", "\8721")
, ("minus", "\8722")
, ("lowast", "\8727")
, ("radic", "\8730")
, ("prop", "\8733")
, ("infin", "\8734")
, ("ang", "\8736")
, ("and", "\8743")
, ("or", "\8744")
, ("cap", "\8745")
, ("cup", "\8746")
, ("int", "\8747")
, ("there4", "\8756")
, ("sim", "\8764")
, ("cong", "\8773")
, ("asymp", "\8776")
, ("ne", "\8800")
, ("equiv", "\8801")
, ("le", "\8804")
, ("ge", "\8805")
, ("sub", "\8834")
, ("sup", "\8835")
, ("nsub", "\8836")
, ("sube", "\8838")
, ("supe", "\8839")
, ("oplus", "\8853")
, ("otimes", "\8855")
, ("perp", "\8869")
, ("sdot", "\8901")
, ("lceil", "\8968")
, ("rceil", "\8969")
, ("lfloor", "\8970")
, ("rfloor", "\8971")
, ("lang", "\9001")
, ("rang", "\9002")
, ("loz", "\9674")
, ("spades", "\9824")
, ("clubs", "\9827")
, ("hearts", "\9829")
, ("diams", "\9830")
]
decodeChar :: Reader Word32 -> Content -> Text -> Content
decodeChar readNum backup = either (const backup) toContent . readNum
where
toContent (num, extra) | T.null extra =
case decodeUtf32BEWith ignore . toByteString $ fromWord32be num of
c | T.length c == 1 -> ContentText c
| otherwise -> backup
toContent _ = backup