{-# LANGUAGE FlexibleContexts, TypeSynonymInstances, FlexibleInstances, TypeFamilies, MultiParamTypeClasses, BangPatterns,
UndecidableInstances, OverlappingInstances #-}
-- | This module provides combinators for generating XML documents.
--
-- As an example, suppose you want to generate the following XML document:
--
-- >
-- >
-- > Stefan
-- > Judith
-- >
--
-- Then you could use the following Haskell code:
--
--
-- @
-- let people = [(\"Stefan\", \"32\"), (\"Judith\", \"4\")]
-- in 'doc' 'defaultDocInfo' $
-- 'xelem' \"people\" $
-- 'xelems' $ map (\(name, age) -> 'xelem' \"person\" ('xattr' \"age\" age '<#>' 'xtext' name)) people
-- @
module Text.XML.Generator (
-- * General
Xml
-- * Documents
, Doc, DocInfo(..), doc, defaultDocInfo
-- * Namespaces
, Namespace, Prefix, Uri
, namespace, noNamespace, defaultNamespace
-- * Elements
, Elem, MkElem(xelem), MkEmptyElem(xelemEmpty), AddChildren
, xelems, noElems, xelemWithText, (<>), (<#>)
-- * Attributes
, Attr, MkAttr(xattr, xattrRaw)
, xattrs, noAttrs
-- * Text
, RawTextContent, TextContent
, xtext, xtextRaw, xentityRef
-- * Other
, xempty , Misc(xprocessingInstruction, xcomment)
-- * Rendering
, xrender
, XmlOutput(fromBuilder), Renderable
-- * XHTML documents
, xhtmlFramesetDocInfo, xhtmlStrictDocInfo, xhtmlTransitionalDocInfo
, xhtmlRootElem
) where
{-
TODO:
- documentation
-}
import Prelude hiding (elem)
import Control.Monad.Reader (Reader(..), ask, asks, runReader)
import qualified Data.Map as Map
import qualified Data.ByteString.Lazy as BSL
import Data.Monoid hiding (mconcat)
import Blaze.ByteString.Builder
import qualified Blaze.ByteString.Builder as Blaze
import Blaze.ByteString.Builder.Char.Utf8
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BSL
import Data.Char (isPrint, ord)
import qualified Data.String as S
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
--
-- Basic definitions
--
-- | A piece of XML at the element level.
newtype Elem = Elem { unElem :: Builder }
-- | A piece of XML at the attribute level.
newtype Attr = Attr { unAttr :: Builder }
-- | A piece of XML at the document level.
newtype Doc = Doc { unDoc :: Builder }
-- | Namespace prefix.
type Prefix = String
-- | Namespace URI.
type Uri = String -- must not be empty
-- | Type for representing presence or absence of an XML namespace.
data Namespace
= NoNamespace
| DefaultNamespace
| QualifiedNamespace Prefix Uri
deriving (Show, Eq)
-- | Constructs a qualified XML namespace.
-- The given URI must not be the empty string.
namespace :: Prefix -> Uri -> Namespace
namespace p u = if null u
then error "Text.XML.Generator.ns: namespace URI must not be empty"
else QualifiedNamespace p u
-- | A 'Namespace' value denoting the absence of any XML namespace information.
noNamespace :: Namespace
noNamespace = NoNamespace
-- | A 'Namespace' value denoting the default namespace.
--
-- * For elements, this is the namespace currently mapped to the empty prefix.
--
-- * For attributes, the default namespace does not carry any namespace information.
defaultNamespace :: Namespace
defaultNamespace = DefaultNamespace
data NsEnv = NsEnv { ne_namespaceMap :: Map.Map Prefix Uri
, ne_noNamespaceInUse :: Bool }
emptyNsEnv :: NsEnv
emptyNsEnv = NsEnv Map.empty False
-- | The type @Xml t@ represent a piece of XML of type @t@, where @t@
-- is usually one of 'Elem', 'Attr', or 'Doc'.
newtype Xml t = Xml { unXml :: Reader NsEnv (t, NsEnv) }
runXml :: NsEnv -> Xml t -> (t, NsEnv)
runXml nsEnv (Xml x) = runReader x nsEnv
-- | An empty, polymorphic piece of XML.
xempty :: Renderable t => Xml t
xempty = Xml $
do env <- ask
return (mkRenderable mempty, env)
--
-- Document
--
-- | The 'DocInfo' type contains all information of an XML document except the root element.
data DocInfo
= DocInfo
{ docInfo_standalone :: Bool -- ^ Value of the @standalone@ attribute in the @\@ header
, docInfo_docType :: Maybe String -- ^ Document type (N.B.: rendering does not escape this value)
, docInfo_preMisc :: Xml Doc -- ^ Content before the root element
, docInfo_postMisc :: Xml Doc -- ^ Content after the root element
}
-- | The default document info (standalone, without document type, without content before/after the root element).
defaultDocInfo :: DocInfo
defaultDocInfo = DocInfo { docInfo_standalone = True
, docInfo_docType = Nothing
, docInfo_preMisc = xempty
, docInfo_postMisc = xempty }
-- | Constructs an XML document from a 'DocInfo' value and the root element.
doc :: DocInfo -> Xml Elem -> Xml Doc
doc di rootElem = Xml $
do let prologBuf = fromString "
fromString (if standalone then "yes" else "no") <>
fromString "\"?>\n" <>
case mDocType of
Nothing -> mempty
Just s -> fromString s <> fromString "\n"
env <- ask
let Doc preBuf = fst $ runXml env preMisc
Elem elemBuf = fst $ runXml env rootElem
Doc postBuf = fst $ runXml env postMisc
return $ (Doc $ prologBuf `mappend` preBuf `mappend` elemBuf `mappend` postBuf, env)
where
standalone = docInfo_standalone di
mDocType = docInfo_docType di
preMisc = docInfo_preMisc di
postMisc = docInfo_postMisc di
--
-- Text content
--
-- | Construction of text content not subject to escaping.
class RawTextContent t where
rawTextBuilder :: t -> Builder
-- | Construction of text content subject to escaping.
class RawTextContent t => TextContent t where
escape :: t -> t
textBuilder :: TextContent t => t -> Builder
textBuilder = rawTextBuilder . escape
instance RawTextContent String where
rawTextBuilder = fromString
instance TextContent String where
escape = genericEscape foldr showString showChar
instance RawTextContent T.Text where
rawTextBuilder = fromText
instance TextContent T.Text where
escape = genericEscape T.foldr T.append T.cons
instance RawTextContent TL.Text where
rawTextBuilder = fromLazyText
instance TextContent TL.Text where
escape = genericEscape TL.foldr TL.append TL.cons
instance RawTextContent BS.ByteString where
rawTextBuilder = fromByteString
instance RawTextContent BSL.ByteString where
rawTextBuilder = fromLazyByteString
--
-- Attributes
--
-- | Class providing methods for constructing XML attributes.
--
-- The 'String' instance of this class constructs an attribute with a name
-- in the default namespace, the 'Namespace' instance allows customization
-- of namespaces.
class MkAttr n t where
type MkAttrRes n t
-- | Construct an attribute by escaping its value
xattr :: TextContent t => n -> MkAttrRes n t
-- | Construct an attribute without escaping its value.
-- /Note:/ attribute values are quoted with double quotes.
xattrRaw :: RawTextContent t => n -> MkAttrRes n t
instance MkAttr String t where
type MkAttrRes String t = t -> Xml Attr
xattr = xattrQ DefaultNamespace
xattrRaw = xattrQRaw DefaultNamespace
instance MkAttr Namespace t where
type MkAttrRes Namespace t = String -> t -> Xml Attr
xattr = xattrQ
xattrRaw = xattrQRaw
-- value is escaped
xattrQ :: TextContent t => Namespace -> String -> t -> Xml Attr
xattrQ ns key value = xattrQRaw' ns key (textBuilder value)
-- value is NOT escaped
xattrQRaw :: RawTextContent t => Namespace -> String -> t -> Xml Attr
xattrQRaw ns key value = xattrQRaw' ns key (rawTextBuilder value)
xattrQRaw' :: Namespace -> String -> Builder -> Xml Attr
xattrQRaw' ns' key valueBuilder = Xml $
do uriMap' <- ask
let (mDecl, prefix, uriMap) = extendNsEnv True uriMap' ns'
nsDeclBuilder =
case mDecl of
Nothing -> mempty
Just (p, u) ->
let uriBuilder = fromString u
prefixBuilder =
if null p then mempty else colonBuilder `mappend` fromString p
in spaceBuilder `mappend` nsDeclStartBuilder
`mappend` prefixBuilder `mappend` startBuilder `mappend` uriBuilder
`mappend` endBuilder
prefixBuilder =
if null prefix
then spaceBuilder
else spaceBuilder `mappend` fromString prefix `mappend` colonBuilder
builder = nsDeclBuilder `mappend` prefixBuilder `mappend`
keyBuilder `mappend` startBuilder `mappend`
valueBuilder `mappend` endBuilder
return $ (Attr builder, uriMap)
where
spaceBuilder = fromString " "
keyBuilder = fromString key
startBuilder = fromString "=\""
endBuilder = fromString "\""
nsDeclStartBuilder = fromString "xmlns"
colonBuilder = fromString ":"
-- | Merges a list of attributes into a single piece of XML at the attribute level.
xattrs :: [Xml Attr] -> Xml Attr
xattrs = foldr mappend noAttrs
-- | The empty attribute list.
noAttrs :: Xml Attr
noAttrs = xempty
instance Monoid (Xml Attr) where
mempty = noAttrs
mappend x1 x2 = Xml $
do env <- ask
let (Attr b1, env') = runXml env x1
let (Attr b2, env'') = runXml env' x2
return $ (Attr $ b1 `mappend` b2, env'')
--
-- Elements
--
-- | Class for adding children to an element.
--
-- The various instances of this class allow the addition of different kinds
-- of children.
class AddChildren c where
addChildren :: c -> NsEnv -> Builder
instance AddChildren (Xml Attr) where
addChildren attrs uriMap =
let (Attr builder', _) = runXml uriMap attrs
in builder' <> fromString "\n>"
instance AddChildren (Xml Elem) where
addChildren elems uriMap =
let (Elem builder', _) = runXml uriMap elems
in fromString "\n>" `mappend` builder'
instance AddChildren (Xml Attr, Xml Elem) where
addChildren (attrs, elems) uriMap =
let (Attr builder, uriMap') = runXml uriMap attrs
(Elem builder', _) = runXml uriMap' elems
in builder `mappend` fromString "\n>" `mappend` builder'
instance TextContent t => AddChildren t where
addChildren t _ = fromChar '>' <> textBuilder t
instance AddChildren () where
addChildren _ _ = fromChar '>'
-- | Class providing methods for constructing XML elements.
--
-- The 'String' instance of this class constructs an element in the
-- default namespace, the 'Namespace' instance allows customization of
-- namespaces.
class AddChildren c => MkElem n c where
type MkElemRes n c
xelem :: n -> MkElemRes n c
instance AddChildren c => MkElem String c where
type MkElemRes String c = c -> Xml Elem
xelem = xelemQ DefaultNamespace
instance AddChildren c => MkElem Namespace c where
type MkElemRes Namespace c = String -> c -> Xml Elem
xelem = xelemQ
-- | Class providing a method for constructing XML elements without children.
--
-- The 'String' instance of this class constructs an element in the
-- default namespace, the 'Namespace' instance allows customization of
-- namespaces.
class MkEmptyElem n where
type MkEmptyElemRes n
xelemEmpty :: n -> MkEmptyElemRes n
instance MkEmptyElem String where
type MkEmptyElemRes String = Xml Elem
xelemEmpty name = xelemQ DefaultNamespace name (mempty :: Xml Elem)
instance MkEmptyElem Namespace where
type MkEmptyElemRes Namespace = String -> Xml Elem
xelemEmpty ns name = xelemQ ns name (mempty :: Xml Elem)
xelemQ :: AddChildren c => Namespace -> String -> c -> Xml Elem
xelemQ ns' name children = Xml $
do oldUriMap <- ask
let (mDecl, prefix,!uriMap) = oldUriMap `seq` extendNsEnv False oldUriMap ns'
let elemNameBuilder =
if null prefix
then fromString name
else fromString prefix `mappend` fromString ":" `mappend` fromString name
let nsDeclBuilder =
case mDecl of
Nothing -> mempty
Just (p, u) ->
let prefixBuilder =
if null p then mempty else fromChar ':' `mappend` fromString p
in fromString " xmlns" `mappend` prefixBuilder `mappend` fromString "=\""
`mappend` fromString u `mappend` fromString "\""
let b1 = fromString "<"
let b2 = b1 `mappend` elemNameBuilder `mappend` nsDeclBuilder
let b3 = b2 `mappend` addChildren children uriMap
let builderOut = Elem (b3 `mappend` fromString "" `mappend` elemNameBuilder `mappend` fromString "\n>")
return (builderOut, oldUriMap)
-- | Merges a list of elements into a single piece of XML at the element level.
xelems :: [Xml Elem] -> Xml Elem
xelems = foldr mappend noElems
-- | No elements at all.
noElems :: Xml Elem
noElems = xempty
-- | The expression @xelemWithText n t@ constructs an XML element with name @n@ and text content @t@.
xelemWithText :: (TextContent t) => String -> t -> Xml Elem
xelemWithText n t = xelem n (xtext t)
instance Monoid (Xml Elem) where
mempty = noElems
mappend x1 x2 = Xml $
do env <- ask
let (Elem b1, env') = runXml env x1
(Elem b2, env'') = runXml env' x2
return (Elem $ b1 `mappend` b2, env'')
--
-- Other XML constructs
--
-- | Constructs a text node by escaping the given argument.
xtext :: TextContent t => t -> Xml Elem
xtext content = Xml $
do env <- ask
return (Elem $ textBuilder content, env)
-- | Constructs a text node /without/ escaping the given argument.
xtextRaw :: RawTextContent t => t -> Xml Elem
xtextRaw content = Xml $
do env <- ask
return (Elem $ rawTextBuilder content, env)
-- | Constructs a reference to the named entity.
-- /Note:/ no escaping is performed on the name of the entity
xentityRef :: String -> Xml Elem
xentityRef name = Xml $
do env <- ask
return (Elem $ fromChar '&' <> fromString name <> fromChar ';', env)
-- | Class providing methods for adding processing instructions and comments.
class Renderable t => Misc t where
-- | Constructs a processing instruction with the given target and content.
-- /Note:/ Rendering does not perform escaping on the target and the content.
xprocessingInstruction :: String -> String -> Xml t
xprocessingInstruction target content = Xml $
do env <- ask
return (mkRenderable $
fromString "" <>
fromString target <>
fromChar ' ' <>
fromString content <>
fromString "?>",
env)
-- | Constructs an XML comment.
-- /Note:/ No escaping is performed on the text of the comment.
xcomment :: String -> Xml t
xcomment content = Xml $
do env <- ask
return (mkRenderable $
fromString "",
env)
instance Misc Elem
instance Misc Doc
--
-- Operators
--
infixl 6 <>
-- | Shortcut for the 'mappend' functions of monoids. Used to concatenate elements, attributes
-- and text nodes.
(<>) :: Monoid t => t -> t -> t
(<>) = mappend
infixl 5 <#>
-- | Shortcut for coonstructing pairs. Used in combination with 'xelem' for separating child-attributes
-- from child-elements.
(<#>) :: a -> b -> (a, b)
(<#>) x y = (x, y)
--
-- Rendering
--
-- | Instances of the @XmlOutput@ class may serve as target of serializing an XML document.
class XmlOutput t where
-- | Creates the target type from a 'Builder'.
fromBuilder :: Builder -> t
instance XmlOutput Builder where
fromBuilder b = b
instance XmlOutput BS.ByteString where
fromBuilder = toByteString
instance XmlOutput BSL.ByteString where
fromBuilder = toLazyByteString
-- | Any type subject to rendering must implement this type class.
class Renderable t where
builder :: t -> Builder
mkRenderable :: Builder -> t
instance Renderable Elem where
builder (Elem b) = b
mkRenderable = Elem
instance Renderable Attr where
builder (Attr b) = b
mkRenderable = Attr
instance Renderable Doc where
builder (Doc b) = b
mkRenderable = Doc
-- | Renders a given piece of XML.
xrender :: (Renderable r, XmlOutput t) => Xml r -> t
xrender r = fromBuilder $ builder r'
where
r' = fst $ runXml emptyNsEnv r
--
-- Utilities
--
extendNsEnv :: Bool -> NsEnv -> Namespace -> (Maybe (Prefix, Uri), Prefix, NsEnv)
extendNsEnv isAttr env ns =
case ns of
NoNamespace
| isAttr -> (Nothing, "", env)
| otherwise ->
case Map.lookup "" (ne_namespaceMap env) of
Nothing -> -- empty prefix not in use
(Nothing, "", env { ne_noNamespaceInUse = True })
Just uri -> -- empty prefix mapped to uri
(Just ("", ""), "", env { ne_namespaceMap = Map.delete "" (ne_namespaceMap env)
, ne_noNamespaceInUse = True })
DefaultNamespace ->
(Nothing, "", env)
QualifiedNamespace p' u ->
let p = if null p' && (isAttr || ne_noNamespaceInUse env) then "_" else p'
(mDecl, prefix, newMap) = genValidPrefix (ne_namespaceMap env) p u
in (mDecl, prefix, env { ne_namespaceMap = newMap })
where
genValidPrefix map prefix uri =
case Map.lookup prefix map of
Nothing -> (Just (prefix, uri), prefix, Map.insert prefix uri map)
Just foundUri ->
if foundUri == uri
then (Nothing, prefix, map)
else genValidPrefix map ('_':prefix) uri
{-# SPECIALIZE INLINE genericEscape ::
((Char -> String -> String) -> String -> String -> String)
-> (String -> String -> String)
-> (Char -> String -> String)
-> String
-> String #-}
{-# SPECIALIZE INLINE genericEscape ::
((Char -> T.Text -> T.Text) -> T.Text -> T.Text -> T.Text)
-> (T.Text -> T.Text -> T.Text)
-> (Char -> T.Text -> T.Text)
-> T.Text
-> T.Text #-}
{-# SPECIALIZE INLINE genericEscape ::
((Char -> TL.Text -> TL.Text) -> TL.Text -> TL.Text -> TL.Text)
-> (TL.Text -> TL.Text -> TL.Text)
-> (Char -> TL.Text -> TL.Text)
-> TL.Text
-> TL.Text #-}
genericEscape :: (S.IsString s)
=> ((Char -> s -> s) -> s -> s -> s)
-> (s -> s -> s)
-> (Char -> s -> s)
-> s
-> s
genericEscape foldr showString' showChar x = foldr escChar (S.fromString "") x
where
-- copied from xml-light
escChar c = case c of
'<' -> showString "<"
'>' -> showString ">"
'&' -> showString "&"
'"' -> showString """
-- we use ' instead of ' because IE apparently has difficulties
-- rendering ' in xhtml.
-- Reported by Rohan Drape .
'\'' -> showString "'"
-- XXX: Is this really wortherd?
-- We could deal with these issues when we convert characters to bytes.
_ | (oc <= 0x7f && isPrint c) || c == '\n' || c == '\r' -> showChar c
| otherwise -> showString "" . showString (show oc) . showChar ';'
where oc = ord c
showString = showString' . S.fromString
--
-- XHTML
--
-- | Document type for XHTML 1.0 strict.
xhtmlDoctypeStrict :: String
xhtmlDoctypeStrict =
""
-- | Document info for XHTML 1.0 strict.
xhtmlStrictDocInfo :: DocInfo
xhtmlStrictDocInfo = defaultDocInfo { docInfo_docType = Just xhtmlDoctypeStrict }
-- | Document type for XHTML 1.0 transitional.
xhtmlDoctypeTransitional :: String
xhtmlDoctypeTransitional =
""
-- | Document info for XHTML 1.0 transitional.
xhtmlTransitionalDocInfo :: DocInfo
xhtmlTransitionalDocInfo = defaultDocInfo { docInfo_docType = Just xhtmlDoctypeTransitional }
-- | Document type for XHTML 1.0 frameset.
xhtmlDoctypeFrameset :: String
xhtmlDoctypeFrameset =
""
-- | Document info for XHTML 1.0 frameset.
xhtmlFramesetDocInfo :: DocInfo
xhtmlFramesetDocInfo = defaultDocInfo { docInfo_docType = Just xhtmlDoctypeFrameset }
-- | Constructs the root element of an XHTML document.
xhtmlRootElem :: String -> Xml Elem -> Xml Elem
xhtmlRootElem lang children =
xelem (namespace "" "http://www.w3.org/1999/xhtml") "html"
(xattr "xml:lang" lang <>
xattr "lang" lang <#>
children)