mini-2.2.0.0: src/Mini/String/XML.hs
-- | XML 1.0 (Fifth Edition): <https://www.w3.org/TR/2008/REC-xml-20081126>
module Mini.String.XML (
-- * Types
Document (
Document
),
Element (
Element
),
Node (
NodeCData,
NodeComment,
NodeElement,
NodePI
),
Misc (
MiscComment,
MiscPI,
MiscSpace
),
PI (
PI
),
Attributes (
Attributes
),
EType (
EType
),
CData (
CData
),
Key (
Key
),
Value (
Value
),
Comment (
Comment
),
Target (
Target
),
Instruction (
Instruction
),
Space (
Space
),
-- * Parsers
document,
-- * Encoding
encode,
decode,
) where
import Control.Applicative (
empty,
many,
optional,
some,
(<|>),
)
import Data.Bits (
shiftL,
)
import Data.Bool (
bool,
)
import Data.Char (
digitToInt,
)
import Mini.Data.Map (
Map,
)
import qualified Mini.Data.Map as Map (
foldrWith,
insert,
member,
)
import Mini.Hash.Class (
Hashable,
toBytes,
)
import Mini.Transformers.Parser (
ParserT,
accept,
noneOf,
oneOf,
reject,
sat,
string,
symbol,
till,
)
import Prelude (
Char,
Eq,
Maybe,
Monad,
Ord,
Show,
String,
concatMap,
dropWhile,
foldl,
fromEnum,
maybe,
mempty,
null,
pure,
show,
toEnum,
($),
(&&),
(*),
(*>),
(+),
(.),
(<$),
(<$>),
(<*),
(<*>),
(<=),
(<>),
(==),
(>=),
(>>=),
(||),
)
-- Types
-- | An XML document
data Document = Document [Misc] Element [Misc]
deriving (Eq, Ord)
instance Show Document where
show (Document ms e ms') = concatMap show ms <> show e <> concatMap show ms'
instance Hashable Document where
toBytes = toBytes . show
-- | An element
data Element = Element EType Attributes [Node]
deriving (Eq, Ord)
instance Show Element where
show (Element t as ns) =
"<"
<> show t
<> show as
<> bool
( ">"
<> concatMap show ns
<> "</"
<> show t
<> ">"
)
("/>")
(null ns)
instance Hashable Element where
toBytes = toBytes . show
-- | An element node
data Node
= NodeCData CData
| NodeComment Comment
| NodeElement Element
| NodePI PI
deriving (Eq, Ord)
instance Show Node where
show (NodeCData cd) = show cd
show (NodeComment c) = show c
show (NodeElement e) = show e
show (NodePI p) = show p
instance Hashable Node where
toBytes = toBytes . show
-- | A miscellaneous component
data Misc
= MiscComment Comment
| MiscPI PI
| MiscSpace Space
deriving (Eq, Ord)
instance Show Misc where
show (MiscComment c) = show c
show (MiscPI p) = show p
show (MiscSpace s) = show s
instance Hashable Misc where
toBytes = toBytes . show
-- | Element attributes
newtype Attributes = Attributes (Map Key Value)
deriving (Eq, Ord)
-- | Values are shown within double quotes
instance Show Attributes where
show (Attributes as) =
Map.foldrWith
(\k v b -> " " <> show k <> "=\"" <> show v <> "\"" <> b)
[]
as
instance Hashable Attributes where
toBytes = toBytes . show
-- | A processing instruction
data PI = PI Target (Maybe Instruction)
deriving (Eq, Ord)
instance Show PI where
show (PI t i) = "<?" <> show t <> maybe "" ((' ' :) . show) i <> "?>"
instance Hashable PI where
toBytes = toBytes . show
-- | An element type
newtype EType = EType String
deriving (Eq, Ord)
instance Show EType where
show (EType t) = t
instance Hashable EType where
toBytes = toBytes . show
-- | Character data
newtype CData = CData String
deriving (Eq, Ord)
-- | Encodes @<&@ into predefined entities
instance Show CData where
show (CData cd) = concatMap enc cd
where
enc '&' = "&"
enc '<' = "<"
enc c = [c]
instance Hashable CData where
toBytes = toBytes . show
-- | An attribute key
newtype Key = Key String
deriving (Eq, Ord)
instance Show Key where
show (Key k) = k
instance Hashable Key where
toBytes = toBytes . show
-- | An attribute value
newtype Value
= -- | without the surrounding quotes
Value String
deriving (Eq, Ord)
-- | Encodes @<&"@ into predefined entities
instance Show Value where
show (Value v) = concatMap enc v
where
enc '&' = "&"
enc '<' = "<"
enc '"' = """
enc c = [c]
instance Hashable Value where
toBytes = toBytes . show
-- | A comment
newtype Comment
= -- | without the leading @"<!--"@ and trailing @"-->"@
Comment String
deriving (Eq, Ord)
instance Show Comment where
show (Comment c) = "<!--" <> c <> "-->"
instance Hashable Comment where
toBytes = toBytes . show
-- | A PI target
newtype Target = Target String
deriving (Eq, Ord)
instance Show Target where
show (Target t) = t
instance Hashable Target where
toBytes = toBytes . show
-- | A PI instruction
newtype Instruction = Instruction String
deriving (Eq, Ord)
instance Show Instruction where
show (Instruction i) = i
instance Hashable Instruction where
toBytes = toBytes . show
-- | Miscellaneous whitespace
newtype Space = Space String
deriving (Eq, Ord)
instance Show Space where
show (Space s) = s
instance Hashable Space where
toBytes = toBytes . show
-- Parsers
-- | Parse an XML document.
--
-- Enforces well-formedness constraints /Element Type Match/, /Unique Att Spec/,
-- and /Legal Character/.
--
-- Rejects documents containing a document type declaration or non-predefined
-- entity references.
--
-- Normalizes line breaks according to section 2.11 /End-of-Line Handling/.
--
-- Decodes predefined entity references and numeric references.
document :: (Monad m) => ParserT Char m Document
document = Document <$> prolog <*> element <*> many misc
-- Encoding
-- | Turn a character into a predefined or decimal reference
encode :: Char -> String
encode '&' = "&"
encode '<' = "<"
encode '>' = ">"
encode '"' = """
encode '\'' = "'"
encode c = "&#" <> show (fromEnum c) <> ";"
-- | Parse a predefined or numeric reference into a legal character
decode :: (Monad m) => ParserT Char m Char
decode = symbol '&' *> (entityRef <|> charRef) <* symbol ';'
where
entityRef =
('&' <$ string "amp")
<|> ('<' <$ string "lt")
<|> ('>' <$ string "gt")
<|> ('"' <$ string "quot")
<|> ('\'' <$ string "apos")
charRef = symbol '#' *> (dec <|> hex)
dec =
some (oneOf ['0' .. '9'])
>>= ( \n ->
bool
empty
(pure $ toEnum n)
$ isLegal n
)
. foldl (\b a -> digitToInt a + b * 10) 0
. dropWhile (== '0')
hex =
(symbol 'x' *> some (oneOf $ ['0' .. '9'] <> ['a' .. 'f'] <> ['A' .. 'F']))
>>= ( \n ->
bool
empty
(pure $ toEnum n)
$ isLegal n
)
. foldl (\b a -> digitToInt a + (b `shiftL` 4)) 0
. dropWhile (== '0')
isLegal =
( \n ->
n == 0x9
|| n == 0xA
|| n == 0xD
|| n >= 0x20 && n <= 0xD7FF
|| n >= 0xE000 && n <= 0xFFFD
|| n >= 0x10000 && n <= 0x10FFFF
)
. fromEnum
-- Helpers
char :: (Monad m) => ParserT Char m Char
char =
('\n' <$ string "\r\n")
<|> ('\n' <$ symbol '\r')
<|> ( sat $
( \n ->
n == 0x9
|| n == 0xA
|| n >= 0x20 && n <= 0xD7FF
|| n >= 0xE000 && n <= 0xFFFD
|| n >= 0x10000 && n <= 0x10FFFF
)
. fromEnum
)
space :: (Monad m) => ParserT Char m Space
space =
Space
<$> ( some . sat $
(\n -> n == 0x20 || n == 0x9 || n == 0xD || n == 0xA) . fromEnum
)
nameStartChar :: (Monad m) => ParserT Char m Char
nameStartChar =
symbol ':'
<|> oneOf ['A' .. 'Z']
<|> symbol '_'
<|> oneOf ['a' .. 'z']
<|> sat
( ( \n ->
n >= 0xC0 && n <= 0xD6
|| n >= 0xD8 && n <= 0xF6
|| n >= 0xF8 && n <= 0x2FF
|| n >= 0x370 && n <= 0x37D
|| n >= 0x37F && n <= 0x1FFF
|| n >= 0x200C && n <= 0x200D
|| n >= 0x2070 && n <= 0x218F
|| n >= 0x2C00 && n <= 0x2FEF
|| n >= 0x3001 && n <= 0xD7FF
|| n >= 0xF900 && n <= 0xFDCF
|| n >= 0xFDF0 && n <= 0xFFFD
|| n >= 0x10000 && n <= 0xEFFFF
)
. fromEnum
)
nameChar :: (Monad m) => ParserT Char m Char
nameChar =
nameStartChar
<|> symbol '-'
<|> symbol '.'
<|> oneOf ['0' .. '9']
<|> sat
( ( \n ->
n == 0xB7
|| n >= 0x0300 && n <= 0x036F
|| n >= 0x203F && n <= 0x2040
)
. fromEnum
)
name :: (Monad m) => ParserT Char m String
name = (:) <$> nameStartChar <*> many nameChar
value :: (Monad m) => ParserT Char m Value
value =
Value
<$> ( (doubleQuoted $ many (noneOf "<&\"" <|> decode))
<|> (singleQuoted $ many (noneOf "<&'" <|> decode))
)
cData :: (Monad m) => ParserT Char m CData
cData = CData <$> (some $ reject (string "]]>") *> exclude "<&")
comment :: (Monad m) => ParserT Char m Comment
comment =
Comment
<$> ( string "<!--"
*> many (reject (string "--") *> char)
<* string "-->"
)
pi :: (Monad m) => ParserT Char m PI
pi =
PI
<$> (string "<?" *> target)
<*> optional
(Instruction <$> (space *> many (reject (string "?>") *> char)))
<* string "?>"
target :: (Monad m) => ParserT Char m Target
target = Target <$> (reject (oneOf "Xx" *> oneOf "Mm" *> oneOf "Ll") *> name)
cdSect :: (Monad m) => ParserT Char m CData
cdSect = CData <$> (string "<![CDATA[" *> (char `till` string "]]>"))
-- NOTE: rejecting doctypedecl
prolog :: (Monad m) => ParserT Char m [Misc]
prolog = optional xmlDecl *> many misc
xmlDecl :: (Monad m) => ParserT Char m ()
xmlDecl =
()
<$ ( string "<?xml"
*> versionInfo
*> optional encodingDecl
*> optional sdDecl
*> optional space
*> string "?>"
)
versionInfo :: (Monad m) => ParserT Char m ()
versionInfo =
()
<$ ( space
*> string "version"
*> eq
*> (doubleQuoted versionNum <|> singleQuoted versionNum)
)
eq :: (Monad m) => ParserT Char m Char
eq = optional space *> symbol '=' <* optional space
versionNum :: (Monad m) => ParserT Char m ()
versionNum = () <$ (string "1." *> some (oneOf ['0' .. '9']))
misc :: (Monad m) => ParserT Char m Misc
misc =
(MiscComment <$> comment)
<|> (MiscPI <$> pi)
<|> (MiscSpace <$> space)
sdDecl :: (Monad m) => ParserT Char m ()
sdDecl =
()
<$ ( space
*> string "standalone"
*> eq
*> ( doubleQuoted (string "yes" <|> string "no")
<|> singleQuoted (string "yes" <|> string "no")
)
)
element :: (Monad m) => ParserT Char m Element
element = do
t <- symbol '<' *> (EType <$> name)
as <- attributes
(Element t as [] <$ string "/>")
<|> ( do
ns <- symbol '>' *> nodes
t' <-
string "</"
*> (EType <$> name)
<* optional space
<* symbol '>'
bool
empty
(pure $ Element t as ns)
$ t == t'
)
attributes :: (Monad m) => ParserT Char m Attributes
attributes = (many (space *> attribute) <* optional space) >>= go mempty
where
go t ((k, v) : as) = bool (go (Map.insert k v t) as) empty $ k `Map.member` t
go t [] = pure $ Attributes t
attribute :: (Monad m) => ParserT Char m (Key, Value)
attribute = (,) <$> (Key <$> name) <* eq <*> value
nodes :: (Monad m) => ParserT Char m [Node]
nodes = go <$> many node
where
go (NodeCData (CData cd) : NodeCData (CData cd') : rest) =
NodeCData (CData $ cd <> cd') : go rest
go (n : ns) = n : go ns
go [] = []
node :: (Monad m) => ParserT Char m Node
node =
( NodeCData
<$> ( cData
<|> (CData <$> (pure <$> decode))
<|> cdSect
)
)
<|> (NodeComment <$> comment)
<|> (NodeElement <$> element)
<|> (NodePI <$> pi)
encodingDecl :: (Monad m) => ParserT Char m ()
encodingDecl =
()
<$ ( space
*> string "encoding"
*> eq
*> (doubleQuoted encName <|> singleQuoted encName)
)
encName :: (Monad m) => ParserT Char m ()
encName =
()
<$ ( (:)
<$> oneOf (['A' .. 'Z'] <> ['a' .. 'z'])
<*> many
( oneOf
( ['A' .. 'Z']
<> ['a' .. 'z']
<> ['0' .. '9']
<> "._-"
)
)
)
doubleQuoted :: (Monad m) => ParserT Char m a -> ParserT Char m a
doubleQuoted p = symbol '"' *> p <* symbol '"'
singleQuoted :: (Monad m) => ParserT Char m a -> ParserT Char m a
singleQuoted p = symbol '\'' *> p <* symbol '\''
exclude :: (Monad m) => String -> ParserT Char m Char
exclude str = accept (noneOf str) *> char