packages feed

dtd (empty) → 0.4.0

raw patch · 9 files changed

+1568/−0 lines, 9 filesdep +attoparsecdep +attoparsec-conduitdep +basesetup-changed

Dependencies added: attoparsec, attoparsec-conduit, base, blaze-builder, conduit, containers, lifted-base, monad-control, network, resourcet, text, transformers, uri-conduit, xml-catalog, xml-conduit, xml-types

Files

+ Data/DTD/Cache.hs view
@@ -0,0 +1,207 @@+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE FlexibleContexts #-}
+module Data.DTD.Cache
+    ( DTDCache
+    , applyDTD
+    , applyDTD_
+    , newDTDCache
+    , newDTDCacheFile
+    , loadAttrMap
+    , UnresolvedEntity (..)
+    ) where
+
+import qualified Text.XML as X
+import Text.XML.Cursor
+import qualified Data.XML.Types as XU
+import qualified Data.Map as Map
+import Data.Text (Text)
+import qualified Data.Text as T
+import qualified Data.Text.Lazy as TL
+import Text.XML.Catalog
+import Network.URI.Conduit
+import Network.URI.Conduit.File
+import qualified Data.DTD.Types as D
+import Data.DTD.Parse (readEID, uriToEID)
+import qualified Data.Conduit as C
+import qualified Data.Conduit.List as CL
+import Control.Monad.IO.Class (MonadIO (liftIO))
+import qualified Data.IORef as I
+import Control.Exception (Exception, throwIO, SomeException)
+import Data.Typeable (Typeable)
+import Control.Monad.Trans.Control (MonadBaseControl)
+import qualified Network.URI as NU
+import Control.Exception.Lifted (try)
+import Control.Monad (liftM)
+
+toMaps :: [D.DTDComponent] -> (EntityMap, AttrMap)
+toMaps =
+    foldr go (Map.empty, Map.empty)
+  where
+    go (D.DTDEntityDecl (D.InternalGeneralEntityDecl k v)) (e, a) = (Map.insert k v e, a)
+    go (D.DTDAttList (D.AttList lname atts)) (e, a) =
+        (e, Map.unionWith Map.union (Map.singleton (X.Name lname Nothing Nothing) (Map.unions $ map go' atts)) a)
+    go _ m = m
+
+    go' (D.AttDecl lname _ def) =
+        case def of
+            D.AttFixed t -> Map.singleton name $ Fixed t
+            D.AttDefaultValue t -> Map.singleton name $ Def t
+            _ -> Map.empty
+      where
+        name = X.Name lname Nothing Nothing
+
+data DTDCache = DTDCache
+    { _dcCache :: I.IORef (Map.Map PubSys (EntityMap, AttrMap))
+    , _dcCatalog :: Catalog
+    , _dcSchemeMap :: SchemeMap
+    }
+
+newDTDCache :: MonadIO m' => Catalog -> SchemeMap -> m' DTDCache
+newDTDCache c sm = do
+    x <- liftIO $ I.newIORef Map.empty
+    return $ DTDCache x c sm
+
+newDTDCacheFile :: MonadIO m => FilePath -> m DTDCache
+newDTDCacheFile fp = do
+    uri <- liftIO $ decodeString fp
+    c <- loadCatalog (toSchemeMap [fileScheme]) uri
+    newDTDCache c (toSchemeMap [fileScheme])
+
+loadSchemaAttrMap :: MonadIO m => DTDCache -> Text -> m (EntityMap, AttrMap)
+loadSchemaAttrMap (DTDCache icache catalog sm) uri0 = do
+    res <- liftIO $ fmap (Map.lookup pubsys) $ I.readIORef icache
+    case res of
+        Just dtd -> return dtd
+        Nothing -> liftIO $ do
+            res' <- load uri0
+            let maps = (Map.empty, res')
+            I.atomicModifyIORef icache $ \m ->
+                (Map.insert pubsys maps m, ())
+            return maps
+  where
+    pubsys = Public uri0
+
+    load uri =
+        case resolveURI catalog Nothing (X.PublicID uri uri) of
+            Nothing -> throwIO $ UnknownSchemaURI uri
+            Just uri' -> do
+                doc <- C.runResourceT $ readURI sm uri' C.$$ X.sinkDoc X.def
+                let c = fromDocument doc
+                let includes =
+                        (c $// element "{http://www.w3.org/2001/XMLSchema}include" >=> attribute "schemaLocation") ++
+                        (c $// element "{http://www.w3.org/2001/XMLSchema}redefine" >=> attribute "schemaLocation")
+                ms1 <- mapM load includes
+                let ms2 = c $// element "{http://www.w3.org/2001/XMLSchema}element" >=> go
+                let ms = ms1 ++ map (uncurry Map.singleton) ms2
+                return $ Map.unionsWith Map.union ms
+
+    go c = do
+        name <- attribute "name" c
+        let attrs = c $// element "{http://www.w3.org/2001/XMLSchema}attribute" >=> goA
+        return (X.Name name Nothing Nothing, Map.fromList attrs)
+
+    goA c = do
+        ref <- attribute "ref" c
+        def <- attribute "default" c
+        return (X.Name ref Nothing Nothing, Def def)
+
+loadAttrMap :: MonadIO m => DTDCache -> X.ExternalID -> m (EntityMap, AttrMap)
+loadAttrMap (DTDCache icache catalog sm) ext = do
+    res <- liftIO $ fmap (Map.lookup pubsys) $ I.readIORef icache
+    case res of
+        Just dtd -> return dtd
+        Nothing ->
+            case Map.lookup pubsys catalog of
+                Nothing -> liftIO $ throwIO $ UnknownExternalID ext
+                Just uri -> do
+                    ecomps <- liftIO $ try $ C.runResourceT $ readEID catalog (uriToEID uri) sm C.$$ CL.consume
+                    comps <- either (liftIO . throwIO . CannotLoadDTD (toNetworkURI uri)) return ecomps
+                    let maps = toMaps comps
+                    liftIO $ I.atomicModifyIORef icache $ \m ->
+                        (Map.insert pubsys maps m, ())
+                    return maps
+  where
+    pubsys =
+        case ext of
+            X.SystemID t -> System t
+            X.PublicID t _ -> Public t
+
+data UnknownExternalID = UnknownExternalID X.ExternalID
+                       | CannotLoadDTD NU.URI SomeException
+                       | UnknownSchemaURI Text
+    deriving (Show, Typeable)
+instance Exception UnknownExternalID
+
+data UnresolvedEntity = UnresolvedEntity Text
+    deriving (Show, Typeable)
+instance Exception UnresolvedEntity
+
+applyDTD_ :: (MonadBaseControl IO m, MonadIO m)
+          => DTDCache -> XU.Document -> m X.Document
+applyDTD_ dc doc = applyDTD dc doc >>= either (liftIO . throwIO) return
+
+applyDTD :: (MonadBaseControl IO m, MonadIO m)
+         => DTDCache -> XU.Document -> m (Either UnresolvedEntity X.Document)
+applyDTD dc doc@(XU.Document pro@(X.Prologue _ mdoctype _) root epi) = do
+    mattrs <-
+        case mdoctype of
+            Just (X.Doctype _ (Just extid)) -> liftM Just $ loadAttrMap dc extid
+            _ ->
+                case lookup "{http://www.w3.org/2001/XMLSchema-instance}noNamespaceSchemaLocation" $ XU.elementAttributes root of
+                    Just [XU.ContentText uri] -> liftM Just $ loadSchemaAttrMap dc uri
+                    _ -> return Nothing
+    case mattrs of
+        Nothing -> return $ goD (Map.empty, Map.empty) doc
+        Just attrs ->
+            case go attrs root of
+                Left e -> return $ Left e
+                Right root' -> return $ Right $ X.Document pro root' epi
+  where
+    go :: (EntityMap, AttrMap) -> XU.Element -> Either UnresolvedEntity X.Element
+    go (ents, attrs) (XU.Element name as ns) = do
+        as' <- mapM (resolveAttr ents) as
+        ns' <- mapM gon ns
+        Right $ X.Element name (as'' as') ns'
+      where
+        as'' as' =
+            case Map.lookup name attrs of
+                Nothing -> as'
+                Just x -> foldr goa as' $ Map.toList x
+        gon (XU.NodeElement e) = fmap X.NodeElement $ go (ents, attrs) e
+        gon (XU.NodeComment t) = Right $ X.NodeComment t
+        gon (XU.NodeInstruction t) = Right $ X.NodeInstruction t
+        gon (XU.NodeContent (XU.ContentText t)) = Right $ X.NodeContent t
+        gon (XU.NodeContent (XU.ContentEntity t)) = fmap X.NodeContent $ getEntity ents t
+
+    goa (name, Fixed t) as = (name, t) : filter (\(n, _) -> name /= n) as
+    goa (name, Def t) as =
+        case lookup name as of
+            Nothing -> (name, t) : as
+            Just _ -> as
+
+    goD attrs (XU.Document a r b) =
+        case go attrs r of
+            Left e -> Left e
+            Right root' -> Right $ X.Document a root' b
+
+    resolveAttr ents (k, v) = fmap (\ts -> (k, T.concat ts)) $ mapM (resolveAttr' ents) v
+    resolveAttr' _ (XU.ContentText t) = Right t
+    resolveAttr' ents (XU.ContentEntity t) = getEntity ents t
+
+data Att = Def Text | Fixed Text
+
+type AttrMap = Map.Map X.Name (Map.Map X.Name Att)
+type EntityMap = Map.Map Text Text
+
+getEntity :: EntityMap -> Text -> Either UnresolvedEntity Text
+getEntity ents t = maybe (Left $ UnresolvedEntity t) Right $ do
+    raw <- Map.lookup t ents
+    case X.parseText X.def $ TL.fromChunks ["<dummy>", raw, "</dummy>"] of
+        Right (X.Document _ (X.Element _ _ nodes) _) -> toContent nodes
+        Left{} -> Nothing
+  where
+    toContent = fmap T.concat . mapM toContent'
+    toContent' (X.NodeContent t') = Just t'
+    toContent' _ = Nothing
+ Data/DTD/Parse.hs view
@@ -0,0 +1,295 @@+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE RankNTypes #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE FlexibleContexts #-}
+module Data.DTD.Parse
+    ( readFile_
+    , enumFile
+    , filePathToEID
+    , uriToEID
+    , readEID
+    ) where
+
+import Data.DTD.Types
+import Data.XML.Types (ExternalID (SystemID))
+import qualified Data.DTD.Types.Unresolved as U
+import qualified Data.DTD.Parse.Unresolved as UP
+import Control.Exception (Exception, SomeException, throwIO)
+import qualified Control.Exception.Lifted as Lifted
+import qualified Data.Conduit.List as CL
+import Control.Monad.Trans.Class (MonadTrans, lift)
+import qualified Data.Text as T
+import qualified Data.Map as Map
+import Data.Typeable (Typeable)
+import Control.Monad.Trans.Reader (ReaderT, runReaderT, ask)
+import Text.XML.Catalog (resolveURI, Catalog)
+import Network.URI.Conduit (URI, SchemeMap, readURI, toNetworkURI, toSchemeMap)
+import Network.URI.Conduit.File (decodeString, fileScheme)
+import qualified Data.Conduit as C
+import Text.XML.Stream.Parse (detectUtf)
+import Data.Conduit.Attoparsec (sinkParser)
+import Control.Applicative ((*>), (<*), (<|>), (<$>), many)
+import qualified Data.IORef as I
+import Control.Monad.Trans.Control (MonadBaseControl)
+import qualified Data.Attoparsec.Text as A
+import Control.Monad (liftM)
+import Control.Monad.IO.Class (MonadIO (liftIO))
+
+type ResolveMonad m = ReaderT ResolveReader m
+
+readEID :: (C.MonadResource m, MonadBaseControl IO m)
+        => Catalog
+        -> ExternalID
+        -> SchemeMap
+        -> C.Source m DTDComponent
+readEID catalog eid sm =
+    C.SourceM pull (return ())
+  where
+    pull =
+        case resolveURI catalog Nothing eid of
+            Nothing -> liftIO $ throwIO $ CannotResolveExternalID eid
+            Just uri -> do
+                istate <- liftIO $ I.newIORef initState
+                let rr = ResolveReader catalog uri istate sm
+                return $ readerToEnum rr
+
+readerToEnum :: (MonadBaseControl IO m, C.MonadThrow m, MonadIO m, C.MonadResource m)
+             => ResolveReader -> C.Source m DTDComponent
+readerToEnum rr =
+    addCatch src0
+  where
+    src0 =
+        readURI (rrSchemeMap rr) (rrBase rr)
+                C.$= detectUtf
+                C.$= streamUnresolved
+                C.$= CL.concatMap id
+                C.$= resolveEnum rr
+    addCatch = id -- FIXME
+    {-
+    addCatch src = C.Source
+        { C.sourcePull = do
+            res <- C.sourcePull src `Lifted.catch` (lift . throw rr)
+            return $ case res of
+                C.Open src' val -> C.Open (addCatch src') val
+                C.Closed -> C.Closed
+        , C.sourceClose = return ()
+        }
+        -}
+
+throw :: C.MonadThrow m => ResolveReader -> SomeException -> m a
+throw rr e =
+    C.monadThrow $ OccuredAt (show $ toNetworkURI $ rrBase rr) (ResolveOther e)
+
+readFile_ :: FilePath -> IO [DTDComponent]
+readFile_ fp = C.runResourceT $ enumFile fp C.$$ CL.consume
+
+enumFile :: (MonadBaseControl IO m, C.MonadResource m) => FilePath -> C.Source m DTDComponent
+enumFile fp =
+    C.SourceM pull (return ())
+  where
+    pull = do
+        eid <- filePathToEID fp
+        return $ readEID Map.empty eid $ toSchemeMap [fileScheme]
+
+filePathToEID :: MonadIO m => FilePath -> m ExternalID
+filePathToEID = liftM uriToEID . liftIO . decodeString
+
+uriToEID :: URI -> ExternalID
+uriToEID = SystemID . T.pack . show . toNetworkURI
+
+streamUnresolved :: C.MonadThrow m => C.Conduit T.Text m [U.DTDComponent]
+streamUnresolved =
+    C.sequenceSink () $ const $ C.Emit () . return <$> sinkParser p
+  where
+    p = (UP.ws >> UP.skipWS >> return []) <|>
+        (UP.textDecl >> return []) <|>
+        (UP.dtdComponent >>= return . return) <|>
+        (A.endOfInput >> return [])
+
+resolveEnum :: (MonadBaseControl IO m, C.MonadThrow m, MonadIO m, C.MonadUnsafeIO m)
+            => ResolveReader
+            -> C.Conduit U.DTDComponent m DTDComponent
+resolveEnum rr =
+      C.transConduit (evalStateT' rr)
+    $ CL.concatMapM resolvef
+
+evalStateT' :: Monad m
+            => ResolveReader
+            -> ReaderT ResolveReader m a
+            -> m a
+evalStateT' rr m = do
+    a <- runReaderT m rr
+    return a
+
+data ResolveState = ResolveState
+    { rsRefText :: Map.Map U.PERef T.Text
+    , rsRefEid :: Map.Map U.PERef ExternalID
+    }
+    deriving Show
+
+data ResolveReader = ResolveReader
+    { rrCatalog :: Catalog
+    , rrBase :: URI
+    , rrState :: I.IORef ResolveState
+    , rrSchemeMap :: SchemeMap
+    }
+
+get :: MonadIO m => ReaderT ResolveReader m ResolveState
+get = do
+    rr <- ask
+    liftIO $ I.readIORef $ rrState rr
+
+put :: MonadIO m => ResolveState -> ReaderT ResolveReader m ()
+put rs = do
+    rr <- ask
+    liftIO $ I.writeIORef (rrState rr) rs
+
+modify :: MonadIO m => (ResolveState -> ResolveState) -> ReaderT ResolveReader m ()
+modify f = get >>= put . f
+
+initState :: ResolveState
+initState = ResolveState Map.empty Map.empty
+
+resolvef :: (MonadBaseControl IO m, C.MonadThrow m, MonadIO m, C.MonadUnsafeIO m)
+         => U.DTDComponent
+         -> ResolveMonad m [DTDComponent]
+
+-- passthrough, no modification needed
+resolvef (U.DTDNotation x) = return [DTDNotation x]
+resolvef (U.DTDInstruction x) = return [DTDInstruction x]
+resolvef (U.DTDComment x) = return [DTDComment x]
+resolvef (U.DTDEntityDecl (U.ExternalGeneralEntityDecl a b c)) =
+    return [DTDEntityDecl $ ExternalGeneralEntityDecl a b c]
+
+-- look up the EntityValues
+resolvef (U.DTDEntityDecl (U.InternalGeneralEntityDecl a b)) = do
+    rs <- get
+    case resolveEntityValue rs b of
+        Left e -> throwError' e
+        Right t -> return [DTDEntityDecl $ InternalGeneralEntityDecl a t]
+
+-- store external entities
+resolvef (U.DTDEntityDecl (U.ExternalParameterEntityDecl name eid)) = do
+    modify $ \rs -> rs { rsRefEid = insertNoReplace name eid $ rsRefEid rs }
+    return []
+
+-- store internal entities
+resolvef (U.DTDEntityDecl (U.InternalParameterEntityDecl name vals)) = do
+    rs <- get
+    t <- either throwError' return $ resolveEntityValue rs vals
+    put $ rs { rsRefText = insertNoReplace name t $ rsRefText rs }
+    return []
+
+-- pull in perefs
+resolvef (U.DTDPERef p) = do
+    rs <- get
+    case Map.lookup p $ rsRefEid rs of
+        Nothing -> throwError' $ UnknownPERef p
+        Just eid -> do
+            rr <- ask
+            case resolveURI (rrCatalog rr) (Just $ rrBase rr) eid of
+                Nothing -> throwError' $ CannotResolveExternalID eid
+                Just uri -> do
+                    let rr' = rr { rrBase = uri }
+                    C.runResourceT $ readerToEnum rr' C.$$ CL.consume
+
+-- element declarations
+resolvef (U.DTDElementDecl (U.ElementDecl name' c)) = do
+    name <- either resolvePERefText return name'
+    c' <-
+        case c of
+            U.ContentEmpty -> return ContentEmpty
+            U.ContentAny -> return ContentAny
+            U.ContentElement ev -> resolveContentModel ev
+            U.ContentMixed cm -> return $ ContentMixed cm
+            U.ContentPERef p -> do
+                t <- resolvePERefText p
+                case runPartial $ A.parse (UP.skipWS *> UP.contentDecl <* UP.skipWS) t of
+                    A.Done "" x ->
+                        case x of
+                            U.ContentPERef{} -> throwError' $ RecursiveContentDeclPERef p
+                            U.ContentEmpty -> return ContentEmpty
+                            U.ContentAny -> return ContentAny
+                            U.ContentElement cm -> resolveContentModel cm
+                            U.ContentMixed cm -> return $ ContentMixed cm
+                    x -> throwError' $ InvalidContentDecl p t x
+    return [DTDElementDecl $ ElementDecl name c']
+
+-- attribute list
+resolvef (U.DTDAttList (U.AttList name' xs)) = do
+    name <- either resolvePERefText return name'
+    ys <- mapM resolveAttDeclPERef xs
+    return [DTDAttList $ AttList name $ concat ys]
+
+resolveAttDeclPERef :: (MonadIO m, C.MonadThrow m) => U.AttDeclPERef -> ResolveMonad m [AttDecl]
+resolveAttDeclPERef (U.ADPDecl (U.AttDecl name typ def)) = do
+    typ' <-
+        case typ of
+            U.ATPType t -> return t
+            U.ATPPERef p -> do
+                t <- resolvePERefText p
+                case runPartial $ A.parse UP.attType $ T.strip t `T.append` " " of
+                    A.Done "" x -> return x
+                    x -> throwError' $ InvalidAttType p t x
+    return [AttDecl name typ' def]
+resolveAttDeclPERef (U.ADPPERef p) = do
+    t <- resolvePERefText p
+    case runPartial $ A.parse (many UP.attDecl) $ T.strip t of
+        A.Done "" x -> liftM concat $ mapM (resolveAttDeclPERef . U.ADPDecl) x
+        x -> throwError' $ InvalidAttDecl p t x
+
+throwError' :: C.MonadThrow m => ResolveException' -> ResolveMonad m a
+throwError' e = do
+    uri <- liftM rrBase ask
+    lift $ C.monadThrow $ OccuredAt (show $ toNetworkURI uri) e
+
+runPartial :: A.Result t -> A.Result t
+runPartial (A.Partial f) = f ""
+runPartial r = r
+
+resolvePERefText :: (MonadIO m, C.MonadThrow m) => U.PERef -> ResolveMonad m T.Text
+resolvePERefText p = do
+    rs <- get
+    maybe (throwError' $ UnknownPERefText p) return $ Map.lookup p $ rsRefText rs
+
+resolveEntityValue :: ResolveState -> [U.EntityValue] -> Either ResolveException' T.Text
+resolveEntityValue rs evs =
+    fmap T.concat $ mapM go evs
+  where
+    go (U.EntityText t) = Right t
+    go (U.EntityPERef p) =
+        case Map.lookup p $ rsRefText rs of
+            Nothing -> Left $ UnknownPERefValue p
+            Just t -> Right t
+
+resolveContentModel :: (MonadIO m, C.MonadThrow m) => [U.EntityValue] -> ResolveMonad m ContentDecl
+resolveContentModel ev = do
+    rs <- get
+    text <- either throwError' return $ resolveEntityValue rs ev
+    case runPartial $ A.parse UP.contentModel $ T.strip text of
+        A.Done "" x -> return $ ContentElement x
+        x -> throwError' $ InvalidContentModel text x
+
+data ResolveException = OccuredAt String ResolveException'
+  deriving (Show, Typeable)
+instance Exception ResolveException
+
+data ResolveException'
+    = UnknownPERef U.PERef
+    | UnknownPERefValue U.PERef
+    | UnknownPERefText U.PERef
+    | CannotResolveExternalID ExternalID
+    | InvalidContentDecl U.PERef T.Text (A.Result U.ContentDecl)
+    | InvalidContentModel T.Text (A.Result U.ContentModel)
+    | InvalidAttDecl U.PERef T.Text (A.Result [U.AttDecl])
+    | InvalidAttType U.PERef T.Text (A.Result U.AttType)
+    | RecursiveContentDeclPERef U.PERef
+    | ResolveOther SomeException
+  deriving (Show, Typeable)
+instance Exception ResolveException'
+
+insertNoReplace :: Ord k => k -> v -> Map.Map k v -> Map.Map k v
+insertNoReplace k v m =
+    case Map.lookup k m of
+        Nothing -> Map.insert k v m
+        Just{} -> m
+ Data/DTD/Parse/Unresolved.hs view
@@ -0,0 +1,357 @@+{-# LANGUAGE OverloadedStrings #-}
+------------------------------------------------------------------------------
+-- |
+-- Module      :  Data.XML.DTD.Parse
+-- Copyright   :  Suite Solutions Ltd., Israel 2011
+--
+-- Maintainer  :  Yitzchak Gale <gale@sefer.org>
+-- Portability :  portable
+--
+-- This module provides a "Data.Attoparsec.Text" parser for XML
+-- Document Type Declaration (DTD) documents. A higher-level interface
+-- that implements parameter entity resolution is also provided.
+
+{-
+Copyright (c) 2011 Suite Solutions Ltd., Israel. All rights reserved.
+
+For licensing information, see the BSD3-style license in the file
+license.txt that was originally distributed by the author together
+with this file.
+-}
+
+module Data.DTD.Parse.Unresolved
+  ( -- * Parsing a DTD
+    dtd
+
+    -- * Top-level DTD structure
+  , textDecl
+  , dtdComponent
+
+    -- * Entity declarations and references
+  , entityDecl
+  , entityValue
+  , pERef
+  , notation
+  , notationSrc
+
+    -- * Element declarations
+  , elementDecl
+  , contentDecl
+  , contentModel
+  , repeatChar
+
+    -- * Attribute declarations
+  , attList
+  , attDecl
+  , attDeclPERef
+  , attType
+  , attDefault
+
+    -- * Declarations of comments and processing instructions
+  , instruction
+  , comment
+
+    -- * Parsing combinators for general DTD syntax
+  , externalID
+  , name
+  , nameSS
+  , quoted
+  , skipWS
+  , ws
+  ) 
+  where
+
+import Data.DTD.Types.Unresolved
+import Data.XML.Types (ExternalID(PublicID, SystemID),
+  Instruction(Instruction))
+import Data.Attoparsec.Text (Parser, try, satisfy, takeTill,
+  anyChar, char, digit)
+import qualified Data.Attoparsec.Text as A -- for takeWhile
+import Data.Attoparsec.Combinator (manyTill, choice, sepBy1)
+import Data.Functor ((<$>))
+import Control.Applicative (pure, optional, (<*>), (<*), (*>), (<|>),
+    Applicative, many)
+import Control.Monad (guard)
+import Data.Text (Text)
+import Data.Char (isSpace)
+import qualified Data.Text as T
+
+(<*.) :: Parser a -> T.Text -> Parser a
+a <*. b = a <* A.string b
+
+(.*>) :: T.Text -> Parser a -> Parser a
+a .*> b = A.string a *> b
+
+-- | A pre-parsed component of the DTD. Pre-parsing separates
+-- components that need parameter entity replacement from those that
+-- do not.
+data PreParse =
+     PPERef PERef
+   | PInstruction Instruction
+   | PComment Text
+   | PMarkup [MarkupText]
+  deriving (Eq, Show)
+
+-- | Markup text is interspersed quoted 'Text', unquoted 'Text', and
+-- parameter entity references.
+data MarkupText = MTUnquoted Text | MTQuoted Text | MTPERef PERef
+  deriving (Eq, Show)
+
+-- | Parse a DTD. Parameter entity substitution is not supported by
+-- this parser, so parameter entities cannot appear in places where a
+-- valid DTD syntax production cannot be determined without resolving
+-- them.
+dtd :: Parser DTD
+dtd = DTD <$> (skipWS *> optional (textDecl <* skipWS)) <*>
+      many (dtdComponent <* skipWS)
+
+-- | Parse an @?xml@ text declaration at the beginning of a 'DTD'.
+textDecl :: Parser DTDTextDecl
+textDecl = do
+    "<?" .*> xml *> ws *> skipWS
+    enc1 <- optional $ try encoding
+    ver  <- optional $ try (maybeSpace version enc1)
+    enc  <- maybe (maybeSpace encoding ver) return enc1
+    skipWS *> "?>" .*> pure (DTDTextDecl ver enc)
+  where
+    xml = (A.string "X" <|> A.string "x") *>
+          (A.string "M" <|> A.string "m") *>
+          (A.string "L" <|> A.string "l")
+    version = attr "version" $ const versionNum
+    versionNum = T.append <$> A.string "1." <*> (T.singleton <$> digit)
+    encoding = attr "encoding" $ takeTill . (==)
+    attr name' val = try (attrQ '"' name' val) <|> attrQ '\'' name' val
+    attrQ q name' val = name' .*> skipWS *> "=" .*> skipWS *>
+                       char q *> val q <* char q
+    maybeSpace p = maybe p (const $ ws *> skipWS *> p)
+
+-- | Parse a single component of a 'DTD'. Conditional sections are
+-- currently not supported.
+dtdComponent :: Parser DTDComponent
+dtdComponent = choice $ map try
+  [ DTDPERef       <$> pERef
+  , DTDEntityDecl  <$> entityDecl
+  , DTDElementDecl <$> elementDecl
+  , DTDAttList     <$> attList
+  , DTDNotation    <$> notation
+  , DTDInstruction <$> instruction
+  ] ++ -- no try needed for last choice
+  [ DTDComment     <$> comment
+  ]
+
+-- | Parse a processing instruction.
+instruction :: Parser Instruction
+instruction = Instruction <$> ("<?" .*> skipWS *> nameSS) <*>
+                              idata <*. "?>"
+  where
+    -- Break the content into chunks beginning with '?' so we
+    -- can find the '?>' at the end. The first chunk might not
+    -- begin with '?'.
+    idata = T.concat . concat <$> manyTillS chunk (A.string "?>")
+    chunk = list2 . T.singleton <$> anyChar <*> takeTill (== '?')
+
+-- | Parse an entity declaration.
+entityDecl :: Parser EntityDecl
+entityDecl = "<!ENTITY" .*> ws *> skipWS *>
+                choice [try internalParam, try externalParam,
+                        try internalGen,   externalGen]
+              <* skipWS <*. ">"
+  where
+    internalParam = InternalParameterEntityDecl <$>
+                      (param *> nameSS) <*> entityValue
+    externalParam = ExternalParameterEntityDecl <$>
+                      (param *> nameSS) <*> externalID
+    internalGen = InternalGeneralEntityDecl <$> nameSS <*> entityValue
+    externalGen = ExternalGeneralEntityDecl <$>
+                    nameSS <*> externalID <*> optional (try ndata)
+    param = "%" .*> ws *> skipWS
+    ndata = skipWS *> "NDATA" .*> ws *> skipWS *> name
+
+-- | Parse a DTD name. We are much more liberal than the spec: we
+-- allow any characters that will not interfere with other DTD
+-- syntax. This parser subsumes both @Name@ and @NmToken@ in the spec,
+-- and more.
+name :: Parser Text
+name = nonNull $ takeTill notNameChar
+  where
+    notNameChar c = isSpace c || c `elem` syntaxChars
+    syntaxChars = "()[]<>!%&;'\"?*+|,="
+    nonNull parser = do
+      text <- parser
+      guard . not . T.null $ text
+      return text
+
+-- | Parse a DTD 'name' followed by optional white space.
+nameSS :: Parser Text
+nameSS = name <* skipWS
+
+nameSSP :: Parser (Either PERef Text)
+nameSSP = ((Left <$> pERef) <|> (Right <$> name)) <* skipWS
+
+-- | Parse an entity value. An entity value is a quoted string
+-- possibly containing parameter entity references.
+entityValue :: Parser [EntityValue]
+entityValue = try (quotedVal '"') <|> quotedVal '\''
+  where
+    quotedVal q = char q *> manyTill (content q) (char q)
+    content q =  EntityPERef <$> try pERef <|> EntityText <$> text q
+    text q = takeTill $ \c -> c == '%' || c == q
+
+entityValueUnquoted :: Parser [EntityValue]
+entityValueUnquoted = many $
+    ((EntityPERef <$> pERef) <|>
+    (EntityText <$> (A.takeWhile1 $ not . flip elem "%>")))
+
+-- | Parse a parameter entity reference
+pERef :: Parser PERef
+pERef = "%" .*> name <*. ";"
+
+-- | Parse the declaration of an element.
+elementDecl :: Parser ElementDecl
+elementDecl = ElementDecl <$> ("<!ELEMENT" .*> ws *> skipWS *> nameSSP) <*>
+                              contentDecl <* skipWS <*. ">"
+
+-- | Parse the content that can occur in an element.
+contentDecl :: Parser ContentDecl
+contentDecl = choice $ map try
+    [ pure ContentEmpty <*. "EMPTY"
+    , pure ContentAny   <*. "ANY"
+    ,      ContentMixed <$> pcdata
+    , ContentPERef <$> pERef
+    ] ++
+    [      ContentElement <$> entityValueUnquoted
+    ]
+  where
+    pcdata = "(" .*> skipWS *> "#PCDATA" .*> skipWS *>
+             (try tags <|> noTagsNoStar)
+    tags = many ("|" .*> skipWS *> nameSS) <*. ")*"
+    noTagsNoStar = ")" .*> pure []
+
+-- | Parse the model of structured content for an element.
+contentModel :: Parser ContentModel
+contentModel = choice $ map (<*> repeatChar)
+    [ CMChoice <$> try (cmList '|')
+    , CMSeq    <$> try (cmList ',')
+    , CMName   <$> name
+    ]
+  where
+    cmList sep = "(" .*> skipWS *>
+      ((contentModel <* skipWS) `sepBy1` (char sep *> skipWS)) <*. ")"
+
+-- | Parse a repetition character.
+repeatChar :: Parser Repeat
+repeatChar = choice
+  [ char '?' *> pure ZeroOrOne
+  , char '*' *> pure ZeroOrMore
+  , char '+' *> pure OneOrMore
+  ,             pure One
+  ]
+
+-- | Parse a list of attribute declarations for an element.
+attList :: Parser AttList
+attList = AttList <$> ("<!ATTLIST" .*> ws *> skipWS *> nameSSP) <*>
+                       many attDeclPERef <*. ">"
+
+attDeclPERef :: Parser AttDeclPERef
+attDeclPERef = (ADPPERef <$> pERef <* skipWS) <|> (ADPDecl <$> attDecl)
+
+-- | Parse the three-part declaration of an attribute.
+attDecl :: Parser AttDecl
+attDecl = AttDecl <$>
+           nameSS <*> attTypePERef <* skipWS <*> attDefault <* skipWS
+
+attTypePERef :: Parser AttTypePERef
+attTypePERef = (ATPPERef <$> pERef) <|> (ATPType <$> attType)
+
+-- | Parse the type of an attribute.
+attType :: Parser AttType
+attType = choice $ map try
+    -- The ws is required by the spec, and needed by the parser to be
+    -- able to distinguish between ID and IDREF, and NMTOKEN and
+    -- NMTOKENS.
+    [ "CDATA"    .*> ws *> pure AttStringType
+    , "ID"       .*> ws *> pure AttIDType
+    , "IDREF"    .*> ws *> pure AttIDRefType
+    , "IDREFS"   .*> ws *> pure AttIDRefsType
+    , "ENTITY"   .*> ws *> pure AttEntityType
+    , "ENTITIES" .*> ws *> pure AttEntitiesType
+    , "NMTOKEN"  .*> ws *> pure AttNmTokenType
+    , "NMTOKENS" .*> ws *> pure AttNmTokensType
+    ,  AttEnumType <$> enumType
+    ] ++
+    [ AttNotationType <$> notationType
+    ]
+  where
+    enumType = nameList
+    notationType = "NOTATION" .*> ws *> skipWS *> nameList
+    nameList = "(" .*> skipWS *>
+               (nameSS `sepBy1` ("|" .*> skipWS)) <*. ")"
+
+-- | Parse a default value specification for an attribute.
+attDefault :: Parser AttDefault
+attDefault = choice $ map try
+    [ "#REQUIRED" .*> pure AttRequired
+    , "#IMPLIED"  .*> pure AttImplied
+    , AttFixed <$> ("#FIXED" .*> ws *> skipWS *> quoted)
+    ] ++
+    [ AttDefaultValue <$> quoted
+    ]
+
+-- | A single-quoted or double-quoted string. The quotation marks are
+-- dropped.
+quoted :: Parser Text
+quoted = quotedWith '"' <|> quotedWith '\''
+  where
+    quotedWith q = char q *> takeTill (== q) <* char q
+
+-- | Parse a declaration of a notation.
+notation :: Parser Notation
+notation = Notation <$>
+  ("<!NOTATION" .*> ws *> skipWS *> name) <* ws <* skipWS <*>
+  notationSrc <*. ">"
+
+-- | Parse a source for a notation.
+notationSrc :: Parser NotationSource
+notationSrc = try system <|> public
+  where
+    system = NotationSysID <$>
+      ("SYSTEM" .*> ws *> skipWS *> quoted <* ws <* skipWS)
+    public = mkPublic <$>
+      ("PUBLIC" .*> ws *> skipWS *> quoted) <*>
+      optional (try $ ws *> skipWS *> quoted) <* skipWS
+    mkPublic pubID = maybe (NotationPubID pubID) (NotationPubSysID pubID)
+
+-- | Parse an external ID.
+externalID :: Parser ExternalID
+externalID = try system <|> public
+  where
+    system = SystemID <$> ("SYSTEM" .*> ws *> skipWS *> quoted)
+    public = PublicID <$> ("PUBLIC" .*> ws *> skipWS *> quoted) <*
+                          ws <* skipWS <*> quoted
+
+-- | Parse a comment
+comment :: Parser Text
+comment = "<!--" .*> (T.concat . concat <$> manyTillS chunk (A.string "--")) <*. ">"
+  where
+    chunk = list2 . T.singleton <$> anyChar <*> takeTill (== '-')
+
+-- | Definition of white space characters, from the XML specification.
+isXMLSpace :: Char -> Bool
+isXMLSpace = (`elem` "\x20\x9\xD\xA")
+
+-- | Parse one character of white space.
+ws :: Parser Char
+ws = satisfy isXMLSpace
+
+-- | Skip zero or more characters of white space
+skipWS :: Parser ()
+skipWS = A.skipWhile isXMLSpace
+
+-- | Type-specialized version of manyTill, so we can use the 'IsString'
+-- instance for 'Parser' 'Text' with it.
+manyTillS :: Parser a -> Parser Text -> Parser [a]
+manyTillS = manyTill
+
+-- | Create a two-element list.
+list2 :: a -> a -> [a]
+list2 x y = [x, y]
+ Data/DTD/Render.hs view
@@ -0,0 +1,239 @@+{-# LANGUAGE OverloadedStrings #-}
+------------------------------------------------------------------------------
+-- |
+-- Module      :  Data.XML.DTD.Render
+-- Copyright   :  Suite Solutions Ltd., Israel 2011
+--
+-- Maintainer  :  Yitzchak Gale <gale@sefer.org>
+-- Portability :  portable
+--
+-- A "Blaze.ByteString.Builder" renderer for XML Document Type
+-- Declaration (DTD) documents.
+
+{-
+Copyright (c) 2011 Suite Solutions Ltd., Israel. All rights reserved.
+
+For licensing information, see the BSD3-style license in the file
+license.txt that was originally distributed by the author together
+with this file.
+-}
+
+module Data.DTD.Render
+  ( -- * DTD structure
+    buildDTD
+  , buildDTDTextDecl
+  , buildDTDComponent
+
+    -- * Entity declarations and references
+  , buildEntityDecl
+
+    -- * Element declarations
+  , buildElementDecl
+  , buildContentDecl
+  , buildContentModel
+  , buildRepeat
+
+    -- * Attribute declarations
+  , buildAttList
+  , buildAttDecl
+  , buildAttType
+  , buildAttDefault
+
+    -- * Notation declarations
+  , buildNotation
+  , buildNotationSource
+
+    -- * Comments and processing instructions
+  , buildInstruction
+  , buildComment
+
+    -- * Builder combinators for general DTD syntax
+  , buildExternalID
+  , buildList
+  , buildChoice
+  , buildMaybe
+  , newline
+  , space
+  , quote
+  , pbracket
+  , parens
+  ) 
+  where
+
+import Blaze.ByteString.Builder (Builder)
+import Blaze.ByteString.Builder.Char.Utf8 (fromText, fromChar)
+import Data.DTD.Types
+import Data.XML.Types (ExternalID(..), Instruction(..))
+import Data.Text (Text)
+import Data.Monoid (Monoid(..))
+import Data.List (intersperse)
+import System.IO (nativeNewline, Newline(CRLF))
+-- No instance Semigroup Builder yet, so <> defined here manually.
+--import Data.Semigroup ((<>))
+(<>) :: Monoid m => m -> m -> m
+(<>) = mappend
+
+-- | Build an optional item.
+buildMaybe :: (a -> Builder) -> Maybe a -> Builder
+buildMaybe = maybe mempty
+
+-- | Build a newline.
+newline :: Builder
+newline = fromText $ case nativeNewline of
+                       CRLF   -> "\r\n"
+                       _      -> "\n"
+
+-- | Build a space.
+space :: Builder
+space = fromChar ' '
+
+-- | Build a quoted string.
+quote :: Builder -> Builder
+quote = (fromChar '"' <>) . (<> fromChar '"')
+
+-- | Build a string quoted by angle brackets, with an exclamation mark.
+pbracket :: Builder -> Builder
+pbracket = (fromText "<!" <>) . (<> fromChar '>')
+
+-- | Build a string surround by parantheses.
+parens :: Builder -> Builder
+parens = (fromChar '(' <>) . (<> fromChar ')')
+
+-- | Build a list of items
+buildList :: Text -> (a -> Builder) -> [a] -> Builder
+buildList sep build =
+  parens . mconcat . intersperse (fromText sep) . map build
+
+-- | Build a choice expression.
+buildChoice :: (a -> Builder) -> [a] -> Builder
+buildChoice = buildList " | "
+
+-- | A 'Builder' for a 'DTD'.
+buildDTD :: DTD -> Builder
+buildDTD (DTD decl cmps) = buildMaybe buildDTDTextDecl decl <>
+  mconcat (map ((<> newline) . buildDTDComponent) cmps)
+
+-- | A 'Builder' for a 'DTDTextDecl'.
+buildDTDTextDecl :: DTDTextDecl -> Builder
+buildDTDTextDecl (DTDTextDecl ver enc) = fromText "<?xml " <>
+  buildMaybe
+    ((fromText "version=" <>) . (<> space) . quote . fromText) ver <>
+  fromText "encoding=" <> quote (fromText enc) <> fromText "?>" <> newline
+
+-- | A 'Builder' for a 'DTDComponent'.
+buildDTDComponent :: DTDComponent -> Builder
+buildDTDComponent (DTDEntityDecl d)  = buildEntityDecl d
+buildDTDComponent (DTDElementDecl d) = buildElementDecl d
+buildDTDComponent (DTDAttList a)     = buildAttList a
+buildDTDComponent (DTDNotation n)    = buildNotation n
+buildDTDComponent (DTDInstruction i) = buildInstruction i
+buildDTDComponent (DTDComment c)     = buildComment c
+
+-- | A 'Builder' for an 'EntityDecl'.
+buildEntityDecl :: EntityDecl -> Builder
+buildEntityDecl d = pbracket $ fromText "ENTITY " <> pct <> name <> val
+  where
+    name = fromText (entityDeclName d) <> space
+    (pct, val) = case d of
+      InternalGeneralEntityDecl   _ val    -> (mempty, quote $ fromText val)
+      ExternalGeneralEntityDecl   _ eid nt -> (mempty, ege eid nt)
+    pctBld = fromText "% "
+    ege eid nt = buildExternalID eid <>
+      buildMaybe ((fromText " NDATA " <>) . quote . fromText) nt
+
+-- | A 'Builder' for an 'ExternalID'.
+buildExternalID :: ExternalID -> Builder
+buildExternalID (SystemID sys)     = fromText "SYSTEM " <>
+                                     quote (fromText sys)
+buildExternalID (PublicID pub sys) = fromText "PUBLIC " <>
+                                     quote (fromText pub) <> space <>
+                                     quote (fromText sys)
+
+-- | A 'Builder' for an 'ElementDecl'.
+buildElementDecl :: ElementDecl -> Builder
+buildElementDecl (ElementDecl name content) = pbracket $
+  fromText "ELEMENT " <> fromText name <> space <> buildContentDecl content
+
+-- | A 'Builder' for a 'ContentDecl'.
+buildContentDecl :: ContentDecl -> Builder
+buildContentDecl ContentEmpty         = fromText "EMPTY"
+buildContentDecl ContentAny           = fromText "ANY"
+buildContentDecl (ContentElement cm)  = buildContentModel cm
+buildContentDecl (ContentMixed names) =
+  buildChoice fromText ("#PCDATA" : names) <> fromChar '*'
+
+-- | A 'Builder' for a 'ContentModel'.
+buildContentModel :: ContentModel -> Builder
+buildContentModel (CMName nam rpt) = parens $ fromText nam <> buildRepeat rpt
+buildContentModel cm               = buildCM cm
+  where
+    buildCM (CMName name  rpt) = fromText name <> buildRepeat rpt
+    buildCM (CMChoice cms rpt) = cp buildChoice      cms rpt
+    buildCM (CMSeq    cms rpt) = cp (buildList ", ") cms rpt
+    cp f cms rpt = f buildCM cms <> buildRepeat rpt
+
+-- | A 'Builder' for a 'Repeat'.
+buildRepeat :: Repeat -> Builder
+buildRepeat One        = mempty
+buildRepeat ZeroOrOne  = fromChar '?'
+buildRepeat ZeroOrMore = fromChar '*'
+buildRepeat OneOrMore  = fromChar '+'
+
+-- | A 'Builder' for an 'AttList'.
+buildAttList :: AttList -> Builder
+buildAttList (AttList name decls) = pbracket $
+  fromText "ATTLIST " <> fromText name <> mconcat
+  (map ((newline <>) . (fromText "          " <>) . buildAttDecl) decls)
+
+-- | A 'Builder' for an 'AttDecl'.
+buildAttDecl :: AttDecl -> Builder
+buildAttDecl (AttDecl name typ dflt) = fromText name <> space <>
+  buildAttType typ <> space <> buildAttDefault dflt
+
+-- | A 'Builder' for an 'AttType'.
+buildAttType :: AttType -> Builder
+buildAttType AttStringType        = fromText "CDATA"
+buildAttType AttIDType            = fromText "ID"
+buildAttType AttIDRefType         = fromText "IDREF"
+buildAttType AttIDRefsType        = fromText "IDREFS"
+buildAttType AttEntityType        = fromText "ENTITY"
+buildAttType AttEntitiesType      = fromText "ENTITIES"
+buildAttType AttNmTokenType       = fromText "NMTOKEN"
+buildAttType AttNmTokensType      = fromText "NMTOKENS"
+buildAttType (AttEnumType vs)     = buildChoice fromText vs
+buildAttType (AttNotationType ns) = fromText "NOTATION " <>
+                                    buildChoice fromText ns
+
+-- | A 'Builder' for an 'AttDefault'.
+buildAttDefault :: AttDefault -> Builder
+buildAttDefault AttRequired           = fromText "#REQUIRED"
+buildAttDefault AttImplied            = fromText "#IMPLIED"
+buildAttDefault (AttDefaultValue val) = quote (fromText val)
+buildAttDefault (AttFixed val)        = fromText "#FIXED " <>
+                                        quote (fromText val)
+
+-- | A 'Builder' for a 'Notation'.
+buildNotation :: Notation -> Builder
+buildNotation (Notation name src) = pbracket $
+  fromText "NOTATION " <> fromText name <> space <> buildNotationSource src
+
+-- | A 'Builder' for a 'NotationSource'.
+buildNotationSource :: NotationSource -> Builder
+buildNotationSource (NotationSysID sys)        = fromText "SYSTEM " <>
+                                                 quote (fromText sys)
+buildNotationSource (NotationPubID pub)        = fromText "PUBLIC " <>
+                                                 quote (fromText pub)
+buildNotationSource (NotationPubSysID pub sys) = fromText "PUBLIC " <>
+                                                 quote (fromText pub) <>
+                                                 space <>
+                                                 quote (fromText sys)
+
+-- | A 'Builder' for an 'Instruction'.
+buildInstruction :: Instruction -> Builder
+buildInstruction (Instruction trgt dat) =
+  fromText "<?" <> fromText trgt <> space <> fromText dat <> fromText "?>"
+
+-- | A 'Builder' for a comment. The comment text cannot be null,
+-- cannot contain two consecutive '-', and cannot end in '-'.
+buildComment :: Text -> Builder
+buildComment cmt = pbracket $ fromText "--" <> fromText cmt <> fromText "--"
+ Data/DTD/Types.hs view
@@ -0,0 +1,107 @@+{-# LANGUAGE DeriveDataTypeable #-}
+module Data.DTD.Types
+  ( -- * DTD structure
+    DTD (..)
+  , DTDTextDecl (..)
+  , DTDComponent (..)
+
+    -- * Entity declarations and references
+  , EntityDecl (..)
+
+    -- * Element declarations
+  , ElementDecl (..)
+  , ContentDecl (..)
+  , ContentModel (..)
+  , Repeat (..)
+
+    -- * Attribute declarations
+  , AttList (..)
+  , AttDecl (..)
+  , AttType (..)
+  , AttDefault (..)
+
+    -- * Notation declarations
+  , Notation (..)
+  , NotationSource (..)
+  )
+  where
+
+import Data.DTD.Types.Unresolved
+    ( DTDTextDecl (..)
+    , ContentModel (..)
+    , Repeat (..)
+    , AttType (..)
+    , AttDefault (..)
+    , Notation (..)
+    , NotationSource (..)
+    )
+import Data.Typeable (Typeable)
+import Data.Text (Text)
+import Data.XML.Types (ExternalID, Instruction)
+
+-- | A list of attribute declarations for an element.
+data AttList =
+     AttList
+       { attListElementName :: Text -- ^ The name of the element to
+                                    -- which the attribute
+                                    -- declarations apply
+       , attListDecls :: [AttDecl]
+       }
+  deriving (Show, Eq, Typeable)
+
+-- | The content that can occur in an element.
+data ContentDecl =
+     ContentEmpty                   -- ^ No content
+   | ContentAny                     -- ^ Unrestricted content
+   | ContentElement ContentModel    -- ^ Structured element content
+   | ContentMixed [Text]            -- ^ A mixture of text and elements
+  deriving (Show, Eq, Typeable)
+
+-- | A declaration of an element.
+data ElementDecl =
+     ElementDecl
+      { eltDeclName :: Text
+      , eltDeclContent :: ContentDecl
+      }
+  deriving (Show, Eq, Typeable)
+
+data EntityDecl =
+     InternalGeneralEntityDecl
+       { entityDeclName :: Text
+       , entityDeclValue :: Text
+       }
+   | ExternalGeneralEntityDecl
+       { entityDeclName :: Text
+       , entityDeclID :: ExternalID
+       , entityDeclNotation :: Maybe Text
+       }                                  -- ^ An external general
+                                          -- entity is unparsed if a
+                                          -- notation is specified.
+  deriving (Show, Eq, Typeable)
+
+-- | The kinds of components that can appear in a 'DTD'.
+data DTDComponent =
+     DTDEntityDecl EntityDecl   -- ^ Entity declaration
+   | DTDElementDecl ElementDecl -- ^ Element declaration
+   | DTDAttList AttList        -- ^ List of attribute declarions for
+                                -- an element
+   | DTDNotation Notation       -- ^ A notation declaration
+   | DTDInstruction Instruction -- ^ A processing instruction
+   | DTDComment Text            -- ^ A comment
+  deriving (Show, Eq, Typeable)
+
+-- | A 'DTD' is a sequence components in any order.
+data DTD = DTD
+             { dtdTextDecl :: Maybe DTDTextDecl
+             , dtdComponents :: [DTDComponent]
+             }
+  deriving (Show, Eq, Typeable)
+
+-- | A declaration of an attribute that can occur in an element.
+data AttDecl =
+     AttDecl
+       { attDeclName :: Text           -- ^ The name of the attribute
+       , attDeclType :: AttType   -- ^ The type of the attribute
+       , attDeclDefault :: AttDefault  -- ^ The default value specification
+       }
+  deriving (Show, Eq)
+ Data/DTD/Types/Unresolved.hs view
@@ -0,0 +1,289 @@+------------------------------------------------------------------------------
+-- |
+-- Module      :  Data.XML.DTD.Types
+-- Copyright   :  Suite Solutions Ltd., Israel 2011
+--
+-- Maintainer  :  Yitzchak Gale <gale@sefer.org>
+-- Portability :  portable
+--
+-- This module provides types to represent an XML Document Type
+-- Declaration (DTD) as defined in W3C specifications
+-- (<http://www.w3.org/XML/Core/#Publications>). It is intended to be
+-- compatible with and extend the set of types in "Data.XML.Types"
+-- provided by the xml-types package.
+--
+-- Following the philosophy of @Data.XML.Types@, the types in this
+-- module are not intended to be a strict and complete representation
+-- of the model in the W3C specifications; rather, they are intended
+-- to be convenient and type-safe for the kinds of processing of DTDs
+-- that are commonly done in practice. As such, this model is
+-- compatible with both Version 1.0 and Version 1.1 of the XML
+-- specification.
+--
+-- Therefore, these types are not suitable for type-level validation
+-- of the syntax of a DTD. For example: these types are more
+-- lenient than the specs about the characters that are allowed in
+-- various locations in a DTD; entities of various kinds only appear
+-- as distinct syntactic elements in places where they are commonly
+-- needed when processing DTDs; etc.
+--
+-- Conditional sections are not represented in these types. They
+-- should be handled directly by parsers and renderers, if needed.
+
+{-
+Copyright (c) 2011 Suite Solutions Ltd., Israel. All rights reserved.
+
+For licensing information, see the BSD3-style license in the file
+license.txt that was originally distributed by the author together
+with this file.
+-}
+
+module Data.DTD.Types.Unresolved
+  ( -- * DTD structure
+    DTD (..)
+  , DTDTextDecl (..)
+  , DTDComponent (..)
+
+    -- * Entity declarations and references
+  , EntityDecl (..)
+  , EntityValue (..)
+  , PERef
+
+    -- * Element declarations
+  , ElementDecl (..)
+  , ContentDecl (..)
+  , ContentModel (..)
+  , Repeat (..)
+
+    -- * Attribute declarations
+  , AttList (..)
+  , AttDecl (..)
+  , AttDeclPERef (..)
+  , AttType (..)
+  , AttTypePERef (..)
+  , AttDefault (..)
+
+    -- * Notation declarations
+  , Notation (..)
+  , NotationSource (..)
+  ) 
+  where
+
+import Data.Text (Text)
+import Data.Typeable ( Typeable, TypeRep, typeOf
+                     , mkTyConApp, mkTyCon)
+import Data.XML.Types (ExternalID, Instruction)
+
+-- | A 'DTD' is a sequence components in any order.
+data DTD = DTD
+             { dtdTextDecl :: Maybe DTDTextDecl
+             , dtdComponents :: [DTDComponent]
+             }
+  deriving (Show, Eq)
+
+instance Typeable DTD where
+  typeOf = typeString "DTD"
+
+-- | The @?xml@ text declaration at the beginning of a DTD.
+data DTDTextDecl =
+     DTDTextDecl
+       { dtdXMLVersion :: Maybe Text
+       , dtdEncoding :: Text
+       }
+  deriving (Show, Eq)
+
+instance Typeable DTDTextDecl where
+  typeOf = typeString "DTDTextDecl"
+
+-- | The kinds of components that can appear in a 'DTD'.
+data DTDComponent =
+     DTDEntityDecl EntityDecl   -- ^ Entity declaration
+   | DTDElementDecl ElementDecl -- ^ Element declaration
+   | DTDAttList AttList        -- ^ List of attribute declarions for
+                                -- an element
+   | DTDNotation Notation       -- ^ A notation declaration
+   | DTDPERef PERef             -- ^ A parameter entity reference in
+                                -- the top-level flow of the DTD
+   | DTDInstruction Instruction -- ^ A processing instruction
+   | DTDComment Text            -- ^ A comment
+  deriving (Show, Eq)
+
+instance Typeable DTDComponent where
+  typeOf = typeString "DTDComponent"
+
+-- | A declaration of an entity. An entity is a textual substitution
+-- variable. General entities can be referenced in an XML document
+-- conforming to the DTD, and parameter entities can be referenced in
+-- the DTD itself. The value of an unparsed entity is not specified in
+-- the DTD; it is specified by external syntax declared as a notation
+-- elsewhere in the DTD.
+data EntityDecl =
+     InternalGeneralEntityDecl
+       { entityDeclName :: Text
+       , entityDeclValue :: [EntityValue]
+       }
+   | ExternalGeneralEntityDecl
+       { entityDeclName :: Text
+       , entityDeclID :: ExternalID
+       , entityDeclNotation :: Maybe Text
+       }                                  -- ^ An external general
+                                          -- entity is unparsed if a
+                                          -- notation is specified.
+   | InternalParameterEntityDecl
+       { entityDeclName :: Text
+       , entityDeclValue :: [EntityValue]
+       }
+   | ExternalParameterEntityDecl
+       { entityDeclName :: Text
+       , entityDeclID :: ExternalID
+       }
+  deriving (Show, Eq)
+
+instance Typeable EntityDecl where
+  typeOf = typeString "EntityDecl"
+
+-- | The value of an internal entity may contain references to
+-- parameter entities; these references need to be resolved to obtain
+-- the actual replacement value of the entity. So we represent the
+-- value as a mixture of parameter entity references and free text.
+data EntityValue =
+     EntityText Text
+   | EntityPERef PERef
+  deriving (Show, Eq)
+
+instance Typeable EntityValue where
+  typeOf = typeString "EntityValue"
+
+-- | A parameter entity reference. It contains the name of the
+-- parameter entity that is being referenced.
+type PERef = Text
+
+-- | A declaration of an element.
+data ElementDecl =
+     ElementDecl
+      { eltDeclName :: Either PERef Text
+      , eltDeclContent :: ContentDecl
+      }
+  deriving (Show, Eq)
+
+instance Typeable ElementDecl where
+  typeOf = typeString "ElementDecl"
+
+-- | The content that can occur in an element.
+data ContentDecl =
+     ContentEmpty                   -- ^ No content
+   | ContentAny                     -- ^ Unrestricted content
+   | ContentElement [EntityValue]   -- ^ Structured element content
+   | ContentMixed [Text]            -- ^ A mixture of text and elements
+   | ContentPERef PERef
+  deriving (Show, Eq)
+
+instance Typeable ContentDecl where
+  typeOf = typeString "ContentDecl"
+
+-- | A model of structured content for an element.
+data ContentModel =
+     CMName Text Repeat             -- ^ Element name
+   | CMChoice [ContentModel] Repeat -- ^ Choice, delimited by @\"|\"@
+   | CMSeq [ContentModel] Repeat    -- ^ Sequence, delimited by @\",\"@
+  deriving (Show, Eq)
+
+instance Typeable ContentModel where
+  typeOf = typeString "ContentModel"
+
+-- | The number of times a production of content model syntax can
+-- repeat.
+data Repeat = One | ZeroOrOne | ZeroOrMore | OneOrMore
+  deriving (Show, Eq)
+
+instance Typeable Repeat where
+  typeOf = typeString "Repeat"
+
+-- | A list of attribute declarations for an element.
+data AttList =
+     AttList
+       { attListElementName :: Either PERef Text -- ^ The name of the element to
+                                    -- which the attribute
+                                    -- declarations apply
+       , attListDecls :: [AttDeclPERef]
+       }
+  deriving (Show, Eq)
+
+instance Typeable AttList where
+  typeOf = typeString "AttList"
+
+data AttDeclPERef = ADPDecl AttDecl | ADPPERef PERef
+    deriving (Show, Eq)
+
+-- | A declaration of an attribute that can occur in an element.
+data AttDecl =
+     AttDecl
+       { attDeclName :: Text           -- ^ The name of the attribute
+       , attDeclType :: AttTypePERef   -- ^ The type of the attribute
+       , attDeclDefault :: AttDefault  -- ^ The default value specification
+       }
+  deriving (Show, Eq)
+
+instance Typeable AttDecl where
+  typeOf = typeString "AttDecl"
+
+data AttTypePERef = ATPType AttType | ATPPERef PERef
+    deriving (Show, Eq)
+
+-- | The type of value that an attribute can take.
+data AttType =
+     AttStringType           -- ^ Any text
+   | AttIDType               -- ^ A unique ID
+   | AttIDRefType            -- ^ A reference to an ID
+   | AttIDRefsType           -- ^ One or more references to IDs
+   | AttEntityType           -- ^ An unparsed external entity
+   | AttEntitiesType         -- ^ One or more unparsed external entities
+   | AttNmTokenType          -- ^ A name-like token
+   | AttNmTokensType         -- ^ One or more name-like tokens
+   | AttEnumType [Text]      -- ^ One of the given values
+   | AttNotationType [Text]  -- ^ Specified by external syntax
+                             -- declared as a notation
+  deriving (Show, Eq)
+
+instance Typeable AttType where
+  typeOf = typeString "AttType"
+
+-- | A default value specification for an attribute.
+data AttDefault =
+     AttRequired          -- ^ No default value; the attribute must always
+                          -- be supplied
+   | AttImplied           -- ^ No default value; the attribute is optional
+   | AttFixed Text        -- ^ When supplied, the attribute must have the
+                          -- given value
+   | AttDefaultValue Text -- ^ The attribute has the given default value
+                          -- when not supplied
+  deriving (Show, Eq)
+
+instance Typeable AttDefault where
+  typeOf = typeString "AttDefault"
+
+-- | A declaration of a notation.
+data Notation =
+     Notation
+       { notationName :: Text,
+         notationSource :: NotationSource
+       }
+  deriving (Show, Eq)
+
+instance Typeable Notation where
+  typeOf = typeString "Notation"
+
+-- | A source for a notation. We do not use the usual 'ExternalID'
+-- type here, because for notations it is only optional, not required,
+-- for a public ID to be accompanied also by a system ID.
+data NotationSource =
+     NotationSysID Text         -- ^ A system ID
+   | NotationPubID Text         -- ^ A public ID
+   | NotationPubSysID Text Text -- ^ A public ID with a system ID
+  deriving (Show, Eq)
+
+instance Typeable NotationSource where
+  typeOf = typeString "NotationSource"
+
+typeString :: String -> a -> TypeRep
+typeString str _ = mkTyConApp (mkTyCon ("Data.XML.DTD.Types." ++ str)) []
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c)2011, Michael Snoyman
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+    * Redistributions of source code must retain the above copyright
+      notice, this list of conditions and the following disclaimer.
+
+    * Redistributions in binary form must reproduce the above
+      copyright notice, this list of conditions and the following
+      disclaimer in the documentation and/or other materials provided
+      with the distribution.
+
+    * Neither the name of Michael Snoyman nor the names of other
+      contributors may be used to endorse or promote products derived
+      from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.lhs view
@@ -0,0 +1,7 @@+#!/usr/bin/env runhaskell
+
+> module Main where
+> import Distribution.Simple
+
+> main :: IO ()
+> main = defaultMain
+ dtd.cabal view
@@ -0,0 +1,37 @@+Name:                dtd
+Version:             0.4.0
+Synopsis:            Parse and render DTD files
+Homepage:            http://github.com/snoyberg/xml
+License:             BSD3
+License-file:        LICENSE
+Author:              Michael Snoyman
+Maintainer:          michaels@suite-sol.com
+Category:            Text
+Build-type:          Simple
+Cabal-version:       >=1.6
+Description:         Parse and render DTD files
+
+Library
+  Exposed-modules:     Data.DTD.Types.Unresolved
+                       Data.DTD.Parse.Unresolved
+                       Data.DTD.Types
+                       Data.DTD.Parse
+                       Data.DTD.Render
+                       Data.DTD.Cache
+  Build-depends:       base                     >= 4          && < 5
+                     , text                     >= 0.11       && < 1.0
+                     , containers               >= 0.2        && < 0.5
+                     , xml-conduit              >= 0.6        && < 0.7
+                     , uri-conduit              >= 0.3        && < 0.4
+                     , transformers             >= 0.2        && < 0.3
+                     , xml-types                >= 0.3        && < 0.4
+                     , attoparsec               >= 0.10       && < 0.11
+                     , monad-control            >= 0.3        && < 0.4
+                     , xml-catalog              >= 0.6        && < 0.7
+                     , blaze-builder            >= 0.3        && < 0.4
+                     , network                  >= 2.2        && < 2.4
+                     , resourcet                >= 0.3        && < 0.4
+                     , conduit                  >= 0.3        && < 0.4
+                     , attoparsec-conduit       >= 0.3        && < 0.4
+                     , lifted-base              >= 0.1        && < 0.2
+  Ghc-options: -Wall