module Text.XML.Generator (
Xml
, Doc, DocInfo(..), doc, defaultDocInfo
, Namespace, Prefix, Uri, Name
, namespace, noNamespace, defaultNamespace
, Elem, xelem, xelemQ, xelemEmpty, xelemQEmpty, AddChildren
, xelems, noElems, xelemWithText, (<>), (<#>)
, Attr, xattr, xattrQ, xattrQRaw
, xattrs, noAttrs
, TextContent
, xtext, xtextRaw, xentityRef
, xempty , Misc(xprocessingInstruction, xcomment)
, xrender
, XmlOutput(fromBuilder), Renderable
, xhtmlFramesetDocInfo, xhtmlStrictDocInfo, xhtmlTransitionalDocInfo
, xhtmlRootElem
) where
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 qualified Data.Monoid as M
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
#if MIN_VERSION_base(4,9,0)
import Data.Semigroup
import Data.Monoid hiding (mconcat, (<>))
#else
import Data.Monoid hiding (mconcat)
#endif
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
#ifdef MIN_VERSION_base
#if MIN_VERSION_base(4,5,0)
#define BASE_AT_LEAST_4_5_0_0
#endif
#else
#if __GLASGOW_HASKELL__ >= 704
#define BASE_AT_LEAST_4_5_0_0
#endif
#endif
newtype Elem = Elem { unElem :: Builder }
newtype Attr = Attr { unAttr :: Builder }
newtype Doc = Doc { unDoc :: Builder }
type Prefix = T.Text
type Uri = T.Text
type Name = T.Text
nameBuilder :: Name -> Builder
nameBuilder = fromText
data Namespace
= NoNamespace
| DefaultNamespace
| QualifiedNamespace Prefix Uri
deriving (Show, Eq)
namespace :: Prefix -> Uri -> Namespace
namespace p u = if T.null u
then error "Text.XML.Generator.ns: namespace URI must not be empty"
else QualifiedNamespace p u
noNamespace :: Namespace
noNamespace = NoNamespace
defaultNamespace :: Namespace
defaultNamespace = DefaultNamespace
data NsEnv = NsEnv { ne_namespaceMap :: Map.Map Prefix Uri
, ne_noNamespaceInUse :: Bool }
emptyNsEnv :: NsEnv
emptyNsEnv = NsEnv Map.empty False
newtype Xml t = Xml { unXml :: Reader NsEnv (t, NsEnv) }
runXml :: NsEnv -> Xml t -> (t, NsEnv)
runXml nsEnv (Xml x) = runReader x nsEnv
xempty :: Renderable t => Xml t
xempty = Xml $
do env <- ask
return (mkRenderable mempty, env)
data DocInfo
= DocInfo
{ docInfo_standalone :: Bool
, docInfo_docType :: Maybe String
, docInfo_preMisc :: Xml Doc
, docInfo_postMisc :: Xml Doc
}
defaultDocInfo :: DocInfo
defaultDocInfo = DocInfo { docInfo_standalone = True
, docInfo_docType = Nothing
, docInfo_preMisc = xempty
, docInfo_postMisc = xempty }
doc :: DocInfo -> Xml Elem -> Xml Doc
doc di rootElem = Xml $
do let prologBuf = fromString "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"" <>
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
type TextContent = T.Text
textBuilder :: TextContent -> Builder
textBuilder = fromText . escapeText
xtext :: TextContent -> Xml Elem
xtext content = Xml $
do env <- ask
return (Elem $ textBuilder content, env)
xtextRaw :: Builder -> Xml Elem
xtextRaw content = Xml $
do env <- ask
return (Elem content, env)
xentityRef :: Name -> Xml Elem
xentityRef name = Xml $
do env <- ask
return (Elem $ fromChar '&' <> fromText name <> fromChar ';', env)
xattr :: Name -> TextContent -> Xml Attr
xattr = xattrQ DefaultNamespace
xattrQ :: Namespace -> Name -> TextContent -> Xml Attr
xattrQ ns key value = xattrQRaw' ns (nameBuilder key) (textBuilder value)
xattrQRaw :: Namespace -> Name -> Builder -> Xml Attr
xattrQRaw ns key value = xattrQRaw' ns (nameBuilder key) value
xattrQRaw' :: Namespace -> Builder -> 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 = fromText u
prefixBuilder =
if T.null p then mempty else colonBuilder `mappend` fromText p
in spaceBuilder `mappend` nsDeclStartBuilder
`mappend` prefixBuilder `mappend` startBuilder `mappend` uriBuilder
`mappend` endBuilder
prefixBuilder =
if T.null prefix
then spaceBuilder
else spaceBuilder `mappend` fromText prefix `mappend` colonBuilder
builder = nsDeclBuilder `mappend` prefixBuilder `mappend`
key `mappend` startBuilder `mappend`
valueBuilder `mappend` endBuilder
return $ (Attr builder, uriMap)
where
spaceBuilder = fromString " "
startBuilder = fromString "=\""
endBuilder = fromString "\""
nsDeclStartBuilder = fromString "xmlns"
colonBuilder = fromString ":"
xattrs :: [Xml Attr] -> Xml Attr
xattrs = M.mconcat
noAttrs :: Xml Attr
noAttrs = xempty
mappendAttr :: Xml Attr -> Xml Attr -> Xml Attr
mappendAttr 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'')
#if MIN_VERSION_base(4,9,0)
instance Semigroup (Xml Attr) where
(<>) = mappendAttr
instance Monoid (Xml Attr) where
mempty = noAttrs
#if !(MIN_VERSION_base(4,11,0))
mappend = (<>)
#endif
#else
instance Monoid (Xml Attr) where
mempty = noAttrs
mappend = mappendAttr
#endif
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 AddChildren (Xml Attr, [Xml Elem]) where
addChildren (attrs, elems) uriMap = addChildren (attrs, xelems elems) uriMap
instance AddChildren TextContent where
addChildren t _ = fromChar '>' <> textBuilder t
instance AddChildren String where
addChildren t _ = fromChar '>' <> fromString t
instance AddChildren () where
addChildren _ _ = fromChar '>'
xelem :: (AddChildren c) => Name -> c -> Xml Elem
xelem = xelemQ DefaultNamespace
xelemEmpty :: Name -> Xml Elem
xelemEmpty name = xelemQ DefaultNamespace name (mempty :: Xml Elem)
xelemQ :: (AddChildren c) => Namespace -> Name -> c -> Xml Elem
xelemQ ns' name children = Xml $
do oldUriMap <- ask
let (mDecl, prefix,!uriMap) = oldUriMap `seq` extendNsEnv False oldUriMap ns'
let elemNameBuilder =
if T.null prefix
then nameBuilder name
else fromText prefix `mappend` fromString ":" `mappend` nameBuilder name
let nsDeclBuilder =
case mDecl of
Nothing -> mempty
Just (p, u) ->
let prefixBuilder =
if T.null p then mempty else fromChar ':' `mappend` fromText p
in fromString " xmlns" `mappend` prefixBuilder `mappend` fromString "=\""
`mappend` fromText 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)
xelemQEmpty :: Namespace -> Name -> Xml Elem
xelemQEmpty ns name = xelemQ ns name (mempty :: Xml Elem)
xelems :: [Xml Elem] -> Xml Elem
xelems = M.mconcat
noElems :: Xml Elem
noElems = xempty
xelemWithText :: Name -> TextContent -> Xml Elem
xelemWithText n t = xelem n (xtext t)
mappendElem :: Xml Elem -> Xml Elem -> Xml Elem
mappendElem 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'')
#if MIN_VERSION_base(4,9,0)
instance Semigroup (Xml Elem) where
(<>) = mappendElem
instance Monoid (Xml Elem) where
mempty = noElems
#if !(MIN_VERSION_base(4,11,0))
mappend = (<>)
#endif
#else
instance Monoid (Xml Elem) where
mempty = noElems
mappend = mappendElem
#endif
class Renderable t => Misc t where
xprocessingInstruction :: String -> String -> Xml t
xprocessingInstruction target content = Xml $
do env <- ask
return (mkRenderable $
fromString "<?" <>
fromString target <>
fromChar ' ' <>
fromString content <>
fromString "?>",
env)
xcomment :: String -> Xml t
xcomment content = Xml $
do env <- ask
return (mkRenderable $
fromString "<!--" <>
fromString content <>
fromString "-->",
env)
instance Misc Elem
instance Misc Doc
#ifndef BASE_AT_LEAST_4_5_0_0
infixl 6 <>
(<>) :: Monoid t => t -> t -> t
(<>) = mappend
#endif
infixl 5 <#>
(<#>) :: a -> b -> (a, b)
(<#>) x y = (x, y)
class XmlOutput t where
fromBuilder :: Builder -> t
instance XmlOutput Builder where
fromBuilder b = b
instance XmlOutput BS.ByteString where
fromBuilder = toByteString
instance XmlOutput BSL.ByteString where
fromBuilder = toLazyByteString
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
xrender :: (Renderable r, XmlOutput t) => Xml r -> t
xrender r = fromBuilder $ builder r'
where
r' = fst $ runXml emptyNsEnv r
extendNsEnv :: Bool -> NsEnv -> Namespace -> (Maybe (Prefix, Uri), Prefix, NsEnv)
extendNsEnv isAttr env ns =
case ns of
NoNamespace
| isAttr -> (Nothing, T.empty, env)
| otherwise ->
case Map.lookup T.empty (ne_namespaceMap env) of
Nothing ->
(Nothing, T.empty, env { ne_noNamespaceInUse = True })
Just uri ->
(Just (T.empty, T.empty), T.empty, env { ne_namespaceMap = Map.delete T.empty (ne_namespaceMap env)
, ne_noNamespaceInUse = True })
DefaultNamespace ->
(Nothing, T.empty, env)
QualifiedNamespace p' u ->
let p = if T.null p' && (isAttr || ne_noNamespaceInUse env) then T.pack "_" 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 (T.cons '_' prefix) uri
escapeText :: T.Text -> T.Text
escapeText = T.foldr escChar T.empty
where
escChar c = case c of
'<' -> T.append (T.pack "<")
'>' -> T.append (T.pack ">")
'&' -> T.append (T.pack "&")
'"' -> T.append (T.pack """)
'\'' -> T.append (T.pack "'")
_ | (oc <= 0x7f && isPrint c) || c == '\n' || c == '\r' -> T.cons c
| otherwise -> T.append (T.pack "&#") . T.append (T.pack (show oc)) . T.cons ';'
where oc = ord c
xhtmlDoctypeStrict :: String
xhtmlDoctypeStrict =
"<!DOCTYPE html\n" ++
" PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\"\n" ++
" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">"
xhtmlStrictDocInfo :: DocInfo
xhtmlStrictDocInfo = defaultDocInfo { docInfo_docType = Just xhtmlDoctypeStrict }
xhtmlDoctypeTransitional :: String
xhtmlDoctypeTransitional =
"<!DOCTYPE html\n" ++
" PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n" ++
" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">"
xhtmlTransitionalDocInfo :: DocInfo
xhtmlTransitionalDocInfo = defaultDocInfo { docInfo_docType = Just xhtmlDoctypeTransitional }
xhtmlDoctypeFrameset :: String
xhtmlDoctypeFrameset =
"<!DOCTYPE html\n" ++
" PUBLIC \"-//W3C//DTD XHTML 1.0 Frameset//EN\"\n" ++
" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd\">"
xhtmlFramesetDocInfo :: DocInfo
xhtmlFramesetDocInfo = defaultDocInfo { docInfo_docType = Just xhtmlDoctypeFrameset }
xhtmlRootElem :: T.Text -> Xml Elem -> Xml Elem
xhtmlRootElem lang children =
xelemQ (namespace (T.pack "") (T.pack "http://www.w3.org/1999/xhtml")) (T.pack "html")
(xattr (T.pack "xml:lang") lang <>
xattr (T.pack "lang") lang <#>
children)