packages feed

rss-conduit 0.3.0.1 → 0.3.1.0

raw patch · 4 files changed

+401/−17 lines, 4 filesdep −conduit-extraPVP ok

version bump matches the API change (PVP)

Dependencies removed: conduit-extra

API changes (from Hackage documentation)

+ Text.RSS1.Conduit.Parse: rss1ChannelItems :: MonadThrow m => ConduitM Event o m (Maybe [Text])
+ Text.RSS1.Conduit.Parse: rss1Document :: MonadThrow m => ConduitM Event o m (Maybe RssDocument)
+ Text.RSS1.Conduit.Parse: rss1Image :: (MonadThrow m) => ConduitM Event o m (Maybe RssImage)
+ Text.RSS1.Conduit.Parse: rss1Item :: MonadThrow m => ConduitM Event o m (Maybe RssItem)
+ Text.RSS1.Conduit.Parse: rss1TextInput :: (MonadThrow m) => ConduitM Event o m (Maybe RssTextInput)

Files

README.md view
@@ -1,10 +1,11 @@ # rss-conduit -This [Haskell][hsk] library implements a streaming parser/renderer for the [RSS 2.0 syndication format][rss], based on [conduit][cdt]s.+This [Haskell][hsk] library implements a streaming parser/renderer for the [RSS 2.0 syndication format][rss], and a streaming parser for the [RSS 1.0 syndication format][rss1], based on [conduit][cdt]s.  Parsers are lenient as much as possible. E.g. unexpected tags are simply ignored.   [rss]: http://cyber.law.harvard.edu/rss/rss.html+[rss1]: http://web.resource.org/rss/1.0/spec [cdt]: https://hackage.haskell.org/package/conduit [hsk]: https://haskell.org
+ Text/RSS1/Conduit/Parse.hs view
@@ -0,0 +1,254 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes        #-}+{-# LANGUAGE TemplateHaskell   #-}+-- | Streaming parsers for the RSS 1.0 standard.+module Text.RSS1.Conduit.Parse+  ( -- * Top-level+    rss1Document+    -- * Elements+  , rss1ChannelItems+  , rss1Image+  , rss1Item+  , rss1TextInput+  ) where++-- {{{ Imports+import           Text.RSS.Types++import           Conduit                hiding (throwM)++import           Control.Exception.Safe as Exception+import           Control.Monad+import           Control.Monad.Fix++import           Data.Conduit+import           Data.Text              as Text+import           Data.Text.Encoding+import           Data.Time.Clock+import           Data.Time.LocalTime+import           Data.Time.RFC3339+import           Data.Version+import           Data.XML.Types++import           Lens.Simple++import           Text.XML.Stream.Parse++import           URI.ByteString+-- }}}++-- {{{ Util+asDate :: (MonadThrow m) => Text -> m UTCTime+asDate text = maybe (throw $ InvalidTime text) (return . zonedTimeToUTC) $ parseTimeRFC3339 text++asRssURI :: (MonadThrow m) => Text -> m RssURI+asRssURI t = case (parseURI' t, parseRelativeRef' t) of+  (Right u, _) -> return $ RssURI u+  (_, Right u) -> return $ RssURI u+  (_, Left e)  -> throwM $ InvalidURI e+  where parseURI' = parseURI laxURIParserOptions . encodeUtf8+        parseRelativeRef' = parseRelativeRef laxURIParserOptions . encodeUtf8++nullURI :: RssURI+nullURI = RssURI $ RelativeRef Nothing "" (Query []) Nothing++headRequiredC :: MonadThrow m => Text -> Consumer a m a+headRequiredC e = maybe (throw $ MissingElement e) return =<< headC++projectC :: Monad m => Fold a a' b b' -> Conduit a m b+projectC prism = fix $ \recurse -> do+  item <- await+  case (item, item ^? (_Just . prism)) of+    (_, Just a) -> yield a >> recurse+    (Just _, _) -> recurse+    _           -> return ()+++contentTag :: MonadThrow m => Text -> AttrParser a -> (a -> ConduitM Event o m b) -> ConduitM Event o m (Maybe b)+contentTag string = tag' (matching (== contentName string))++dcTag :: MonadThrow m => Text -> AttrParser a -> (a -> ConduitM Event o m b) -> ConduitM Event o m (Maybe b)+dcTag string = tag' (matching (== dcName string))++rdfTag :: MonadThrow m => Text -> AttrParser a -> (a -> ConduitM Event o m b) -> ConduitM Event o m (Maybe b)+rdfTag string = tag' (matching (== rdfName string))++rss1Tag :: MonadThrow m => Text -> AttrParser a -> (a -> ConduitM Event o m b) -> ConduitM Event o m (Maybe b)+rss1Tag string = tag' (matching (== rss1Name string))++contentName :: Text -> Name+contentName string = Name string (Just "http://purl.org/rss/1.0/modules/content/") (Just "content")++dcName :: Text -> Name+dcName string = Name string (Just "http://purl.org/dc/elements/1.1/") (Just "dc")++rdfName :: Text -> Name+rdfName string = Name string (Just "http://www.w3.org/1999/02/22-rdf-syntax-ns#") (Just "rdf")++rss1Name :: Text -> Name+rss1Name string = Name string (Just "http://purl.org/rss/1.0/") Nothing+-- }}}+++data TextInputPiece = TextInputTitle Text | TextInputDescription Text+                    | TextInputName Text | TextInputLink RssURI++makeTraversals ''TextInputPiece++-- | Parse a @\<textinput\>@ element.+rss1TextInput :: (MonadThrow m) => ConduitM Event o m (Maybe RssTextInput)+rss1TextInput = rss1Tag "textinput" attributes $ \uri -> (manyYield' (choose piece) =$= parser uri) <* many ignoreAnyTreeContent where+  parser uri = getZipConduit $ RssTextInput+    <$> ZipConduit (projectC _TextInputTitle =$= headRequiredC "Missing <title> element")+    <*> ZipConduit (projectC _TextInputDescription =$= headRequiredC "Missing <description> element")+    <*> ZipConduit (projectC _TextInputName =$= headRequiredC "Missing <name> element")+    <*> ZipConduit (projectC _TextInputLink =$= headDefC uri)  -- Lenient+  piece = [ fmap TextInputTitle <$> rss1Tag "title" ignoreAttrs (const content)+          , fmap TextInputDescription <$> rss1Tag "description" ignoreAttrs (const content)+          , fmap TextInputName <$> rss1Tag "name" ignoreAttrs (const content)+          , fmap TextInputLink <$> rss1Tag "link" ignoreAttrs (const $ content >>= asRssURI)+          ]+  attributes = (requireAttr (rdfName "about") >>= asRssURI) <* ignoreAttrs+++data ItemPiece = ItemTitle Text | ItemLink RssURI | ItemDescription Text | ItemCreator Text | ItemDate UTCTime | ItemContent Text++makeTraversals ''ItemPiece++-- | Parse an @\<item\>@ element.+rss1Item :: MonadThrow m => ConduitM Event o m (Maybe RssItem)+rss1Item = rss1Tag "item" attributes $ \uri -> (manyYield' (choose piece) =$= parser uri) <* many ignoreAnyTreeContent where+  parser uri = getZipConduit $ RssItem+    <$> ZipConduit (projectC _ItemTitle =$= headDefC mempty)+    <*> (Just <$> ZipConduit (projectC _ItemLink =$= headDefC uri))+    <*> ZipConduit (projectC _ItemDescription =$= headDefC mempty)+    <*> ZipConduit (projectC _ItemCreator =$= headDefC mempty)+    <*> pure mempty+    <*> pure mzero+    <*> pure mempty+    <*> pure mzero+    <*> ZipConduit (projectC _ItemDate =$= headC)+    <*> pure mzero+  piece = [ fmap ItemTitle <$> rss1Tag "title" ignoreAttrs (const content)+          , fmap ItemLink <$> rss1Tag "link" ignoreAttrs (const $ content >>= asRssURI)+          , fmap ItemDescription <$> (rss1Tag "description" ignoreAttrs (const content) `orE` contentTag "encoded" ignoreAttrs (const content))+          , fmap ItemCreator <$> dcTag "creator" ignoreAttrs (const content)+          , fmap ItemDate <$> dcTag "date" ignoreAttrs (const $ content >>= asDate)+          ]+  attributes = (requireAttr (rdfName "about") >>= asRssURI) <* ignoreAttrs+++data ImagePiece = ImageUri RssURI | ImageTitle Text | ImageLink RssURI++makeTraversals ''ImagePiece++-- | Parse an @\<image\>@ element.+rss1Image :: (MonadThrow m) => ConduitM Event o m (Maybe RssImage)+rss1Image = rss1Tag "image" attributes $ \uri -> (manyYield' (choose piece) =$= parser uri) <* many ignoreAnyTreeContent where+  parser uri = getZipConduit $ RssImage+    <$> ZipConduit (projectC _ImageUri =$= headDefC uri)  -- Lenient+    <*> ZipConduit (projectC _ImageTitle =$= headDefC "Unnamed image")  -- Lenient+    <*> ZipConduit (projectC _ImageLink =$= headDefC nullURI)  -- Lenient+    <*> pure mzero+    <*> pure mzero+    <*> pure mempty+  piece = [ fmap ImageUri <$> rss1Tag "url" ignoreAttrs (const $ content >>= asRssURI)+          , fmap ImageTitle <$> rss1Tag "title" ignoreAttrs (const content)+          , fmap ImageLink <$> rss1Tag "link" ignoreAttrs (const $ content >>= asRssURI)+          ]+  attributes = (requireAttr (rdfName "about") >>= asRssURI) <* ignoreAttrs+++-- | Parse an @\<items\>@ element.+rss1ChannelItems :: MonadThrow m => ConduitM Event o m (Maybe [Text])+rss1ChannelItems = fmap join $ rss1Tag "items" ignoreAttrs $ const $ rdfTag "Seq" ignoreAttrs $ const $ many $ rdfTag "li" attributes return where+  attributes = requireAttr (rdfName "resource") <* ignoreAttrs+++data Rss1Channel = Rss1Channel+  { channelId'          :: RssURI+  , channelTitle'       :: Text+  , channelLink'        :: RssURI+  , channelDescription' :: Text+  , channelItems'       :: [Text]+  , channelImage'       :: Maybe RssImage+  , channelTextInput'   :: Maybe RssURI+  }++data ChannelPiece = ChannelTitle Text+  | ChannelLink RssURI+  | ChannelDescription Text+  | ChannelImage RssImage+  | ChannelItems [Text]+  | ChannelTextInput RssURI++makeTraversals ''ChannelPiece+++-- | Parse a @\<channel\>@ element.+rss1Channel :: MonadThrow m => ConduitM Event o m (Maybe Rss1Channel)+rss1Channel = rss1Tag "channel" attributes $ \channelId -> (manyYield' (choose piece) =$= parser channelId) <* many ignoreAnyTreeContent where+  parser channelId = getZipConduit $ Rss1Channel channelId+    <$> ZipConduit (projectC _ChannelTitle =$= headRequiredC "Missing <title> element")+    <*> ZipConduit (projectC _ChannelLink =$= headRequiredC "Missing <link> element")+    <*> ZipConduit (projectC _ChannelDescription =$= headDefC "")  -- Lenient+    <*> ZipConduit (projectC _ChannelItems =$= concatC =$= sinkList)+    <*> ZipConduit (projectC _ChannelImage =$= headC)+    <*> ZipConduit (projectC _ChannelTextInput =$= headC)+  piece = [ fmap ChannelTitle <$> rss1Tag "title" ignoreAttrs (const content)+          , fmap ChannelLink <$> rss1Tag "link" ignoreAttrs (const $ content >>= asRssURI)+          , fmap ChannelDescription <$> rss1Tag "description" ignoreAttrs (const content)+          , fmap ChannelItems <$> rss1ChannelItems+          , fmap ChannelImage <$> rss1Image+          , fmap ChannelTextInput <$> rss1Tag "textinput" (requireAttr (rdfName "resource") >>= asRssURI) return+          ]+  attributes = (requireAttr (rdfName "about") >>= asRssURI) <* ignoreAttrs+++data Rss1Document = Rss1Document Rss1Channel (Maybe RssImage) [RssItem] (Maybe RssTextInput)++rss1ToRss2 :: Rss1Document -> RssDocument+rss1ToRss2 (Rss1Document channel image items textInput) = RssDocument+  (Version [1] [])+  (channelTitle' channel)+  (channelLink' channel)+  (channelDescription' channel)+  items+  mempty+  mempty+  mempty+  mempty+  mzero+  mzero+  mzero+  mempty+  mzero+  mzero+  mzero+  image+  mempty+  textInput+  mempty+  mempty++data DocumentPiece = DocumentChannel Rss1Channel+  | DocumentImage RssImage+  | DocumentItem RssItem+  | DocumentTextInput RssTextInput++makeTraversals ''DocumentPiece+++-- | Parse an @\<RDF\>@ element.+rss1Document :: MonadThrow m => ConduitM Event o m (Maybe RssDocument)+rss1Document = fmap (fmap rss1ToRss2) $ rdfTag "RDF" ignoreAttrs $ const $ (manyYield' (choose piece) =$= parser) <* many ignoreAnyTreeContent where+  parser = getZipConduit $ Rss1Document+    <$> ZipConduit (projectC _DocumentChannel =$= headRequiredC "Missing <channel> element")+    <*> ZipConduit (projectC _DocumentImage =$= headC)+    <*> ZipConduit (projectC _DocumentItem =$= sinkList)+    <*> ZipConduit (projectC _DocumentTextInput =$= headC)+  piece = [ fmap DocumentChannel <$> rss1Channel+          , fmap DocumentImage <$> rss1Image+          , fmap DocumentItem <$> rss1Item+          , fmap DocumentTextInput <$> rss1TextInput+          ]
rss-conduit.cabal view
@@ -1,6 +1,6 @@ name:                rss-conduit-version:             0.3.0.1-synopsis:            Streaming parser/renderer for the RSS 2.0 standard.+version:             0.3.1.0+synopsis:            Streaming parser/renderer for the RSS standard. description:         Cf README file. license:             PublicDomain license-file:        LICENSE@@ -17,6 +17,7 @@  library   exposed-modules:+    Text.RSS1.Conduit.Parse     Text.RSS.Conduit.Parse     Text.RSS.Conduit.Render     Text.RSS.Lens@@ -49,7 +50,7 @@     , base >= 4.8 && < 5     , bytestring     , conduit-    , conduit-extra+    , conduit-combinators     , data-default     , safe-exceptions     , hlint
test/Main.hs view
@@ -4,12 +4,13 @@ -- {{{ Imports import           Arbitrary +import           Conduit+ import           Control.Exception.Safe       as Exception import           Control.Monad.Trans.Resource  import           Data.Char import           Data.Conduit-import           Data.Conduit.Binary import           Data.Conduit.List import           Data.Default import           Data.Version@@ -26,6 +27,7 @@ import           Text.RSS.Conduit.Render      as Renderer import           Text.RSS.Lens import           Text.RSS.Types+import           Text.RSS1.Conduit.Parse      as Parser import           Text.XML.Stream.Parse        as XML hiding (choose) import           Text.XML.Stream.Render @@ -45,15 +47,20 @@ unitTests = testGroup "Unit tests"   [ skipHoursCase   , skipDaysCase-  , textInputCase-  , imageCase+  , rss1TextInputCase+  , rss2TextInputCase+  , rss1ImageCase+  , rss2ImageCase   , categoryCase   , cloudCase   , guidCase   , enclosureCase   , sourceCase-  , itemCase-  , documentCase+  , rss1ItemCase+  , rss2ItemCase+  , rss1ChannelItemsCase+  , rss1DocumentCase+  , rss2DocumentCase   ]  properties :: TestTree@@ -93,8 +100,23 @@                 , "</skipDays>"                 ] -textInputCase :: TestTree-textInputCase = testCase "<textInput> element" $ do+rss1TextInputCase :: TestTree+rss1TextInputCase = testCase "RSS1 <textinput> element" $ do+  result <- runResourceT . runConduit $ sourceList input =$= XML.parseText' def =$= force "ERROR" rss1TextInput+  result^.textInputTitleL @?= "Search XML.com"+  result^.textInputDescriptionL @?= "Search XML.com's XML collection"+  result^.textInputNameL @?= "s"+  result^.textInputLinkL @=? RssURI (URI (Scheme "http") (Just (Authority Nothing (Host "search.xml.com") Nothing)) "" (Query []) Nothing)+  where input = [ "<textinput xmlns=\"http://purl.org/rss/1.0/\" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" rdf:about=\"http://search.xml.com\">"+                , "<title>Search XML.com</title>"+                , "<description>Search XML.com's XML collection</description>"+                , "<name>s</name>"+                , "<link>http://search.xml.com</link>"+                , "</textinput>"+                ]++rss2TextInputCase :: TestTree+rss2TextInputCase = testCase "RSS2 <textInput> element" $ do   result <- runResourceT . runConduit $ sourceList input =$= XML.parseText' def =$= force "ERROR" rssTextInput   result^.textInputTitleL @?= "Title"   result^.textInputDescriptionL @?= "Description"@@ -108,8 +130,23 @@                 , "</textInput>"                 ] -imageCase :: TestTree-imageCase = testCase "<image> element" $ do+rss1ImageCase :: TestTree+rss1ImageCase = testCase "RSS1 <image> element" $ do+  result <- runResourceT . runConduit $ sourceList input =$= XML.parseText' def =$= force "ERROR" rss1Image+  result^.imageUriL @?= RssURI (URI (Scheme "http") (Just (Authority Nothing (Host "xml.com") Nothing)) "/universal/images/xml_tiny.gif" (Query []) Nothing)+  result^.imageTitleL @?= "XML.com"+  result^.imageLinkL @?= RssURI (URI (Scheme "http") (Just (Authority Nothing (Host "www.xml.com") Nothing)) "" (Query []) Nothing)+  where input = [ "<image xmlns=\"http://purl.org/rss/1.0/\" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" rdf:about=\"http://xml.com/universal/images/xml_tiny.gif\">"+                , "<url>http://xml.com/universal/images/xml_tiny.gif</url>"+                , "<title>XML.com</title>"+                , "<ignored>Ignored</ignored>"+                , "<link>http://www.xml.com</link>"+                , "<ignored>Ignored</ignored>"+                , "</image>"+                ]++rss2ImageCase :: TestTree+rss2ImageCase = testCase "RSS2 <image> element" $ do   result <- runResourceT . runConduit $ sourceList input =$= XML.parseText' def =$= force "ERROR" rssImage   result^.imageUriL @?= RssURI (URI (Scheme "http") (Just (Authority Nothing (Host "image.ext") Nothing)) "" (Query []) Nothing)   result^.imageTitleL @?= "Title"@@ -174,8 +211,26 @@                 ]         uri = RssURI (URI (Scheme "http") (Just (Authority Nothing (Host "www.tomalak.org") Nothing)) "/links2.xml" (Query []) Nothing) -itemCase :: TestTree-itemCase = testCase "<item> element" $ do+rss1ItemCase :: TestTree+rss1ItemCase = testCase "RSS1 <item> element" $ do+  result <- runResourceT . runConduit $ sourceList input =$= XML.parseText' def =$= force "ERROR" rss1Item+  result^.itemTitleL @?= "Processing Inclusions with XSLT"+  result^.itemLinkL @?= Just link+  result^.itemDescriptionL @?= "Processing document inclusions with general XML tools can be problematic. This article proposes a way of preserving inclusion information through SAX-based processing."+  where input = [ "<item xmlns=\"http://purl.org/rss/1.0/\" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" rdf:about=\"http://xml.com/pub/2000/08/09/xslt/xslt.html\">"+                , "<title>Processing Inclusions with XSLT</title>"+                , "<description>Processing document inclusions with general XML tools can be"+                , " problematic. This article proposes a way of preserving inclusion"+                , " information through SAX-based processing."+                , "</description>"+                , "<link>http://xml.com/pub/2000/08/09/xslt/xslt.html</link>"+                , "<sometag>Some content in unknown tag, should be ignored.</sometag>"+                , "</item>"+                ]+        link = RssURI (URI (Scheme "http") (Just (Authority Nothing (Host "xml.com") Nothing)) "/pub/2000/08/09/xslt/xslt.html" (Query []) Nothing)++rss2ItemCase :: TestTree+rss2ItemCase = testCase "RSS2 <item> element" $ do   result <- runResourceT . runConduit $ sourceList input =$= XML.parseText' def =$= force "ERROR" rssItem   result^.itemTitleL @?= "Example entry"   result^.itemLinkL @?= Just link@@ -193,8 +248,81 @@                 ]         link = RssURI (URI (Scheme "http") (Just (Authority Nothing (Host "www.example.com") Nothing)) "/blog/post/1" (Query []) Nothing) -documentCase :: TestTree-documentCase = testCase "<rss> element" $ do++rss1ChannelItemsCase :: TestTree+rss1ChannelItemsCase = testCase "RSS1 <items> element" $ do+  result <- runResourceT . runConduit $ sourceList input =$= XML.parseText' def =$= force "ERROR" rss1ChannelItems+  result @?= [resource1, resource2]+  where input = [ "<items xmlns=\"http://purl.org/rss/1.0/\" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">"+                , "<rdf:Seq>"+                , "<rdf:li rdf:resource=\"http://xml.com/pub/2000/08/09/xslt/xslt.html\" />"+                , "<rdf:li rdf:resource=\"http://xml.com/pub/2000/08/09/rdfdb/index.html\" />"+                , "</rdf:Seq>"+                , "</items>"+                ]+        resource1 = "http://xml.com/pub/2000/08/09/xslt/xslt.html"+        resource2 = "http://xml.com/pub/2000/08/09/rdfdb/index.html"++rss1DocumentCase :: TestTree+rss1DocumentCase = testCase "<rdf> element" $ do+  result <- runResourceT . runConduit $ sourceList input =$= XML.parseText' def =$= force "ERROR" rss1Document+  result^.documentVersionL @?= Version [1] []+  result^.channelTitleL @?= "XML.com"+  result^.channelDescriptionL @?= "XML.com features a rich mix of information and services for the XML community."+  result^.channelLinkL @?= link+  result^?channelImageL._Just.imageTitleL @?= Just "XML.com"+  result^?channelImageL._Just.imageLinkL @?= Just imageLink+  result^?channelImageL._Just.imageUriL @?= Just imageUri+  length (result^..channelItemsL) @?= 2+  result^?channelTextInputL._Just.textInputTitleL @?= Just "Search XML.com"+  result^?channelTextInputL._Just.textInputDescriptionL @?= Just "Search XML.com's XML collection"+  result^?channelTextInputL._Just.textInputNameL @?= Just "s"+  result^?channelTextInputL._Just.textInputLinkL @?= Just textInputLink+  where input = [ "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>"+                , "<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns=\"http://purl.org/rss/1.0/\">"+                , "<channel rdf:about=\"http://www.xml.com/xml/news.rss\">"+                , "<title>XML.com</title>"+                , "<link>http://xml.com/pub</link>"+                , "<description>XML.com features a rich mix of information and services for the XML community.</description>"+                , "<image rdf:resource=\"http://xml.com/universal/images/xml_tiny.gif\" />"+                , "<items>"+                , "<rdf:Seq>"+                , "<rdf:li rdf:resource=\"http://xml.com/pub/2000/08/09/xslt/xslt.html\" />"+                , "<rdf:li rdf:resource=\"http://xml.com/pub/2000/08/09/rdfdb/index.html\" />"+                , "</rdf:Seq>"+                , "</items>"+                , "</channel>"+                , "<image rdf:about=\"http://xml.com/universal/images/xml_tiny.gif\">"+                , "<title>XML.com</title>"+                , "<link>http://www.xml.com</link>"+                , "<url>http://xml.com/universal/images/xml_tiny.gif</url>"+                , "</image>"+                , "<item rdf:about=\"http://xml.com/pub/2000/08/09/xslt/xslt.html\">"+                , "<title>Processing Inclusions with XSLT</title>"+                , "<link>http://xml.com/pub/2000/08/09/xslt/xslt.html</link>"+                , "<description>Processing document inclusions with general XML tools can be problematic. This article proposes a way of preserving inclusion information through SAX-based processing.</description>"+                , "</item>"+                , "<item rdf:about=\"http://xml.com/pub/2000/08/09/xslt/xslt.html\">"+                , "<title>Putting RDF to Work</title>"+                , "<link>http://xml.com/pub/2000/08/09/rdfdb/index.html</link>"+                , "<description>Tool and API support for the Resource Description Framework is slowly coming of age. Edd Dumbill takes a look at RDFDB, one of the most exciting new RDF toolkits.</description>"+                , "</item>"+                , "<textinput rdf:about=\"http://search.xml.com\">"+                , "<title>Search XML.com</title>"+                , "<description>Search XML.com's XML collection</description>"+                , "<name>s</name>"+                , "<link>http://search.xml.com</link>"+                , "</textinput>"+                , "</rdf:RDF>"+                ]+        link = RssURI (URI (Scheme "http") (Just (Authority Nothing (Host "xml.com") Nothing)) "/pub" (Query []) Nothing)+        imageLink = RssURI (URI (Scheme "http") (Just (Authority Nothing (Host "www.xml.com") Nothing)) "" (Query []) Nothing)+        imageUri = RssURI (URI (Scheme "http") (Just (Authority Nothing (Host "xml.com") Nothing)) "/universal/images/xml_tiny.gif" (Query []) Nothing)+        textInputLink = RssURI (URI (Scheme "http") (Just (Authority Nothing (Host "search.xml.com") Nothing)) "" (Query []) Nothing)+++rss2DocumentCase :: TestTree+rss2DocumentCase = testCase "<rss> element" $ do   result <- runResourceT . runConduit $ sourceList input =$= XML.parseText' def =$= force "ERROR" rssDocument   result^.documentVersionL @?= Version [2] []   result^.channelTitleL @?= "RSS Title"