{-# OPTIONS -fno-warn-incomplete-patterns #-} -------------------------------------------------------------------- -- | -- Module : Text.Feed.Constructor -- Copyright : (c) Galois, Inc. 2008, -- (c) Sigbjorn Finne 2009- -- License : BSD3 -- -- Maintainer: Sigbjorn Finne -- Stability : provisional -- Description: Module for an abstraction layer between different kinds of feeds. -- -------------------------------------------------------------------- module Text.Feed.Constructor ( FeedKind(..) , newFeed -- :: FeedKind -> Feed , feedFromRSS -- :: RSS -> Feed , feedFromAtom -- :: Atom.Feed -> Feed , feedFromRDF -- :: RSS1.Feed -> Feed , feedFromXML -- :: Element -> Feed , getFeedKind -- :: Feed -> FeedKind , FeedSetter -- type _ a = a -> Feed -> Feed , addItem -- :: FeedSetter Item , withFeedTitle -- :: FeedSetter Text , withFeedHome -- :: FeedSetter URLString , withFeedHTML -- :: FeedSetter URLString , withFeedDescription -- :: FeedSetter Text , withFeedPubDate -- :: FeedSetter DateString , withFeedLastUpdate -- :: FeedSetter DateString , withFeedDate -- :: FeedSetter DateString , withFeedLogoLink -- :: FeedSetter URLString , withFeedLanguage -- :: FeedSetter Text , withFeedCategories -- :: FeedSetter [(Text, Maybe Text)] , withFeedGenerator -- :: FeedSetter Text , withFeedItems -- :: FeedSetter [Item] , newItem -- :: FeedKind -> Item , getItemKind -- :: Item -> FeedKind , atomEntryToItem -- :: Atom.Entry -> Item , rssItemToItem -- :: RSS.Item -> Item , rdfItemToItem -- :: RSS1.Item -> Item , ItemSetter -- type _ a = a -> Item -> Item , withItemTitle -- :: ItemSetter Text , withItemLink -- :: ItemSetter URLString , withItemPubDate -- :: ItemSetter DateString , withItemDate -- :: ItemSetter DateString , withItemAuthor -- :: ItemSetter Text , withItemCommentLink -- :: ItemSetter Text , withItemEnclosure -- :: Text -> Maybe Text -> ItemSetter Integer , withItemFeedLink -- :: Text -> ItemSetter Text , withItemId -- :: Bool -> ItemSetter Text , withItemCategories -- :: ItemSetter [(Text, Maybe Text)] , withItemDescription -- :: ItemSetter Text , withItemRights -- :: ItemSetter Text ) where import Prelude.Compat import Text.Feed.Types as Feed.Types import Text.Atom.Feed as Atom import Text.DublinCore.Types import Text.RSS.Syntax as RSS import Text.RSS1.Syntax as RSS1 import Data.XML.Compat import Data.XML.Types as XML import Data.Char (toLower) import Data.Maybe (fromMaybe, mapMaybe) import Data.Text (Text, pack) -- ToDo: -- -- - complete set of constructors over feeds -- - provide a unified treatment of date string reps. -- (i.e., I know they differ across formats, but ignorant what -- the constraints are at the moment.) -- | Construct an empty feed document, intending to output it in -- the 'fk' feed format. newFeed :: FeedKind -> Feed.Types.Feed newFeed fk = case fk of AtomKind -> AtomFeed (Atom.nullFeed "feed-id-not-filled-in" (TextString "dummy-title") "dummy-and-bogus-update-date") RSSKind mbV -> let def = RSS.nullRSS "dummy-title" "default-channel-url" in RSSFeed $ maybe def (\v -> def {RSS.rssVersion = v}) mbV RDFKind mbV -> let def = RSS1.nullFeed "default-channel-url" "dummy-title" in RSS1Feed $ maybe def (\v -> def {RSS1.feedVersion = v}) mbV feedFromRSS :: RSS.RSS -> Feed.Types.Feed feedFromRSS = RSSFeed feedFromAtom :: Atom.Feed -> Feed.Types.Feed feedFromAtom = AtomFeed feedFromRDF :: RSS1.Feed -> Feed.Types.Feed feedFromRDF = RSS1Feed feedFromXML :: XML.Element -> Feed.Types.Feed feedFromXML = XMLFeed getFeedKind :: Feed.Types.Feed -> FeedKind getFeedKind f = case f of Feed.Types.AtomFeed {} -> AtomKind Feed.Types.RSSFeed r -> RSSKind (case RSS.rssVersion r of "2.0" -> Nothing v -> Just v) Feed.Types.RSS1Feed r -> RDFKind (case RSS1.feedVersion r of "1.0" -> Nothing v -> Just v) Feed.Types.XMLFeed {} -> RSSKind (Just "2.0") -- for now, just a hunch.. addItem :: Feed.Types.Item -> Feed.Types.Feed -> Feed.Types.Feed addItem it f = case (it, f) of (Feed.Types.AtomItem e, Feed.Types.AtomFeed fe) -> Feed.Types.AtomFeed fe {Atom.feedEntries = e : Atom.feedEntries fe} (Feed.Types.RSSItem e, Feed.Types.RSSFeed r) -> Feed.Types.RSSFeed r {RSS.rssChannel = (RSS.rssChannel r) {RSS.rssItems = e : RSS.rssItems (RSS.rssChannel r)}} (Feed.Types.RSS1Item e, Feed.Types.RSS1Feed r) -- note: do not update the channel item URIs at this point; -- will delay doing so until serialization. -> Feed.Types.RSS1Feed r {RSS1.feedItems = e : RSS1.feedItems r} _ -> error "addItem: currently unable to automatically convert items from one feed type to another" withFeedItems :: FeedSetter [Feed.Types.Item] withFeedItems is fe = foldr addItem (case fe of Feed.Types.AtomFeed f -> Feed.Types.AtomFeed f {Atom.feedEntries = []} Feed.Types.RSSFeed f -> Feed.Types.RSSFeed f {rssChannel = (rssChannel f) {rssItems = []}} Feed.Types.RSS1Feed f -> Feed.Types.RSS1Feed f {feedItems = []}) is newItem :: FeedKind -> Feed.Types.Item newItem fk = case fk of AtomKind -> Feed.Types.AtomItem $ Atom.nullEntry "entry-id-not-filled-in" (TextString "dummy-entry-title") "dummy-and-bogus-entry-update-date" RSSKind {} -> Feed.Types.RSSItem $ RSS.nullItem "dummy-rss-item-title" RDFKind {} -> Feed.Types.RSS1Item $ RSS1.nullItem "dummy-item-uri" "dummy-item-title" "dummy-item-link" getItemKind :: Feed.Types.Item -> FeedKind getItemKind f = case f of Feed.Types.AtomItem {} -> AtomKind Feed.Types.RSSItem {} -> RSSKind (Just "2.0") -- good guess.. Feed.Types.RSS1Item {} -> RDFKind (Just "1.0") Feed.Types.XMLItem {} -> RSSKind (Just "2.0") type FeedSetter a = a -> Feed.Types.Feed -> Feed.Types.Feed withFeedTitle :: FeedSetter Text withFeedTitle tit fe = case fe of Feed.Types.AtomFeed f -> Feed.Types.AtomFeed f {feedTitle = TextString tit} Feed.Types.RSSFeed f -> Feed.Types.RSSFeed f {rssChannel = (rssChannel f) {rssTitle = tit}} Feed.Types.RSS1Feed f -> Feed.Types.RSS1Feed f {feedChannel = (feedChannel f) {channelTitle = tit}} Feed.Types.XMLFeed f -> Feed.Types.XMLFeed $ mapMaybeChildren (\e -> if elementName e == "channel" then Just (mapMaybeChildren (\e2 -> if elementName e2 == "title" then Just (unode "title" tit) else Nothing) e) else Nothing) f withFeedHome :: FeedSetter URLString withFeedHome url fe = case fe of Feed.Types.AtomFeed f -> Feed.Types.AtomFeed f {feedLinks = newSelf : Atom.feedLinks f} -- ToDo: fix, the element is for the HTML home of the channel, not the -- location of the feed itself. Struggling to find if there is a common way -- to represent this outside of RSS 2.0 standard elements.. Feed.Types.RSSFeed f -> Feed.Types.RSSFeed f {rssChannel = (rssChannel f) {rssLink = url}} Feed.Types.RSS1Feed f -> Feed.Types.RSS1Feed f {feedChannel = (feedChannel f) {channelURI = url}} Feed.Types.XMLFeed f -> Feed.Types.XMLFeed $ mapMaybeChildren (\e -> if elementName e == "channel" then Just (mapMaybeChildren (\e2 -> if elementName e2 == "link" then Just (unode "link" url) else Nothing) e) else Nothing) f where newSelf = (nullLink url) {linkRel = Just (Left "self"), linkType = Just "application/atom+xml"} -- | 'withFeedHTML' sets the URL where an HTML version of the -- feed is published. withFeedHTML :: FeedSetter URLString withFeedHTML url fe = case fe of Feed.Types.AtomFeed f -> Feed.Types.AtomFeed f {feedLinks = newAlt : Atom.feedLinks f} Feed.Types.RSSFeed f -> Feed.Types.RSSFeed f {rssChannel = (rssChannel f) {rssLink = url}} Feed.Types.RSS1Feed f -> Feed.Types.RSS1Feed f {feedChannel = (feedChannel f) {channelLink = url}} Feed.Types.XMLFeed f -> Feed.Types.XMLFeed $ mapMaybeChildren (\e -> if elementName e == "channel" then Just (mapMaybeChildren (\e2 -> if elementName e2 == "link" then Just (unode "link" url) else Nothing) e) else Nothing) f where newAlt = (nullLink url) {linkRel = Just (Left "alternate"), linkType = Just "text/html"} -- | 'withFeedHTML' sets the URL where an HTML version of the -- feed is published. withFeedDescription :: FeedSetter Text withFeedDescription desc fe = case fe of Feed.Types.AtomFeed f -> Feed.Types.AtomFeed f {feedSubtitle = Just (TextString desc)} Feed.Types.RSSFeed f -> Feed.Types.RSSFeed f {rssChannel = (rssChannel f) {rssDescription = desc}} Feed.Types.RSS1Feed f -> Feed.Types.RSS1Feed f {feedChannel = (feedChannel f) {channelDesc = desc}} Feed.Types.XMLFeed f -> Feed.Types.XMLFeed $ mapMaybeChildren (\e -> if elementName e == "channel" then Just (mapMaybeChildren (\e2 -> if elementName e2 == "description" then Just (unode "description" desc) else Nothing) e) else Nothing) f withFeedPubDate :: FeedSetter Text withFeedPubDate dateStr fe = case fe of Feed.Types.AtomFeed f -> Feed.Types.AtomFeed f {feedUpdated = dateStr} Feed.Types.RSSFeed f -> Feed.Types.RSSFeed f {rssChannel = (rssChannel f) {rssPubDate = Just dateStr}} Feed.Types.RSS1Feed f -> Feed.Types.RSS1Feed $ case break isDate $ RSS1.channelDC (RSS1.feedChannel f) of (as, dci:bs) -> f { RSS1.feedChannel = (RSS1.feedChannel f) {RSS1.channelDC = as ++ dci {dcText = dateStr} : bs} } (_, []) -> f { RSS1.feedChannel = (RSS1.feedChannel f) { RSS1.channelDC = DCItem {dcElt = DC_Date, dcText = dateStr} : RSS1.channelDC (RSS1.feedChannel f) } } Feed.Types.XMLFeed f -> Feed.Types.XMLFeed $ mapMaybeChildren (\e -> if elementName e == "channel" then Just (mapMaybeChildren (\e2 -> if elementName e2 == "pubDate" then Just (unode "pubDate" dateStr) else Nothing) e) else Nothing) f where isDate dc = dcElt dc == DC_Date withFeedLastUpdate :: FeedSetter DateString withFeedLastUpdate dateStr fe = case fe of Feed.Types.AtomFeed f -> Feed.Types.AtomFeed f {feedUpdated = dateStr} Feed.Types.RSSFeed f -> Feed.Types.RSSFeed f {rssChannel = (rssChannel f) {rssLastUpdate = Just dateStr}} Feed.Types.RSS1Feed f -> Feed.Types.RSS1Feed $ case break isDate $ RSS1.channelDC (RSS1.feedChannel f) of (as, dci:bs) -> f { RSS1.feedChannel = (RSS1.feedChannel f) {RSS1.channelDC = as ++ dci {dcText = dateStr} : bs} } (_, []) -> f { RSS1.feedChannel = (RSS1.feedChannel f) { RSS1.channelDC = DCItem {dcElt = DC_Date, dcText = dateStr} : RSS1.channelDC (RSS1.feedChannel f) } } Feed.Types.XMLFeed f -> Feed.Types.XMLFeed $ mapMaybeChildren (\e -> if elementName e == "channel" then Just (mapMaybeChildren (\e2 -> if elementName e2 == "lastUpdate" then Just (unode "lastUpdate" dateStr) else Nothing) e) else Nothing) f where isDate dc = dcElt dc == DC_Date -- | 'withFeedDate dt' is the composition of 'withFeedPubDate' -- and 'withFeedLastUpdate', setting both publication date and -- last update date to 'dt'. Notice that RSS2.0 is the only format -- supporting both pub and last-update. withFeedDate :: FeedSetter DateString withFeedDate dt f = withFeedPubDate dt (withFeedLastUpdate dt f) withFeedLogoLink :: URLString -> FeedSetter URLString withFeedLogoLink imgURL lnk fe = case fe of Feed.Types.AtomFeed f -> Feed.Types.AtomFeed f {feedLogo = Just imgURL, feedLinks = newSelf : Atom.feedLinks f} Feed.Types.RSSFeed f -> Feed.Types.RSSFeed f { rssChannel = (rssChannel f) {rssImage = Just $ RSS.nullImage imgURL (rssTitle (rssChannel f)) lnk} } Feed.Types.RSS1Feed f -> Feed.Types.RSS1Feed $ f { feedImage = Just $ RSS1.nullImage imgURL (RSS1.channelTitle (RSS1.feedChannel f)) lnk , feedChannel = (feedChannel f) {channelImageURI = Just imgURL} } Feed.Types.XMLFeed f -> Feed.Types.XMLFeed $ mapMaybeChildren (\e -> if elementName e == "channel" then Just (mapMaybeChildren (\e2 -> if elementName e2 == "image" then Just (unode "image" [unode "url" imgURL, unode "title" title, unode "link" lnk]) else Nothing) e) else Nothing) f where title = case fmap (findChild "title") (findChild "channel" f) of Just (Just e1) -> strContent e1 _ -> "feed_title" -- shouldn't happen.. where newSelf = (nullLink lnk) {linkRel = Just (Left "self"), linkType = Just "application/atom+xml"} withFeedLanguage :: FeedSetter Text withFeedLanguage lang fe = case fe of Feed.Types.AtomFeed f -> Feed.Types.AtomFeed f {Atom.feedAttrs = langAttr : Atom.feedAttrs f} where langAttr = (name, [ContentText lang]) name = Name {nameLocalName = "lang", nameNamespace = Nothing, namePrefix = Just "xml"} Feed.Types.RSSFeed f -> Feed.Types.RSSFeed f {rssChannel = (rssChannel f) {rssLanguage = Just lang}} Feed.Types.RSS1Feed f -> Feed.Types.RSS1Feed $ case break isLang $ RSS1.channelDC (RSS1.feedChannel f) of (as, dci:bs) -> f { RSS1.feedChannel = (RSS1.feedChannel f) {RSS1.channelDC = as ++ dci {dcText = lang} : bs} } (_, []) -> f { RSS1.feedChannel = (RSS1.feedChannel f) { RSS1.channelDC = DCItem {dcElt = DC_Language, dcText = lang} : RSS1.channelDC (RSS1.feedChannel f) } } Feed.Types.XMLFeed f -> Feed.Types.XMLFeed $ mapMaybeChildren (\e -> if elementName e == "channel" then Just (mapMaybeChildren (\e2 -> if elementName e2 == "language" then Just (unode "language" lang) else Nothing) e) else Nothing) f where isLang dc = dcElt dc == DC_Language withFeedCategories :: FeedSetter [(Text, Maybe Text)] withFeedCategories cats fe = case fe of Feed.Types.AtomFeed f -> Feed.Types.AtomFeed f { Atom.feedCategories = map (\(t, mb) -> (Atom.newCategory t) {Atom.catScheme = mb}) cats ++ feedCategories f } Feed.Types.RSSFeed f -> Feed.Types.RSSFeed f { rssChannel = (rssChannel f) { RSS.rssCategories = map (\(t, mb) -> (RSS.newCategory t) {RSS.rssCategoryDomain = mb}) cats ++ RSS.rssCategories (rssChannel f) } } Feed.Types.RSS1Feed f -> Feed.Types.RSS1Feed f { feedChannel = (feedChannel f) { RSS1.channelDC = map (\(t, _) -> DCItem {dcElt = DC_Subject, dcText = t}) cats ++ RSS1.channelDC (feedChannel f) } } Feed.Types.XMLFeed f -> Feed.Types.XMLFeed $ mapMaybeChildren (\e -> if elementName e == "channel" then Just (foldr (\(t, mb) acc -> addChild (unode "category" (maybe (: []) (\v x -> [mkAttr "domain" v, x]) mb (mkAttr "term" t))) acc) e cats) else Nothing) f withFeedGenerator :: FeedSetter (Text, Maybe URLString) withFeedGenerator (gen, mbURI) fe = case fe of Feed.Types.AtomFeed f -> Feed.Types.AtomFeed $ f {Atom.feedGenerator = Just ((Atom.nullGenerator gen) {Atom.genURI = mbURI})} Feed.Types.RSSFeed f -> Feed.Types.RSSFeed f {rssChannel = (rssChannel f) {rssGenerator = Just gen}} Feed.Types.RSS1Feed f -> Feed.Types.RSS1Feed $ case break isSource $ RSS1.channelDC (RSS1.feedChannel f) of (as, dci:bs) -> f { RSS1.feedChannel = (RSS1.feedChannel f) {RSS1.channelDC = as ++ dci {dcText = gen} : bs} } (_, []) -> f { RSS1.feedChannel = (RSS1.feedChannel f) { RSS1.channelDC = DCItem {dcElt = DC_Source, dcText = gen} : RSS1.channelDC (RSS1.feedChannel f) } } Feed.Types.XMLFeed f -> Feed.Types.XMLFeed $ mapMaybeChildren (\e -> if elementName e == "channel" then Just (mapMaybeChildren (\e2 -> if elementName e2 == "generator" then Just (unode "generator" gen) else Nothing) e) else Nothing) f where isSource dc = dcElt dc == DC_Source -- Item constructors (all the way to the end): atomEntryToItem :: Atom.Entry -> Feed.Types.Item atomEntryToItem = Feed.Types.AtomItem rssItemToItem :: RSS.RSSItem -> Feed.Types.Item rssItemToItem = Feed.Types.RSSItem rdfItemToItem :: RSS1.Item -> Feed.Types.Item rdfItemToItem = Feed.Types.RSS1Item type ItemSetter a = a -> Feed.Types.Item -> Feed.Types.Item -- | 'withItemPubDate dt' associates the creation\/ publication date 'dt' -- with a feed item. withItemPubDate :: ItemSetter DateString withItemPubDate dt fi = case fi of Feed.Types.AtomItem e -> Feed.Types.AtomItem e {Atom.entryUpdated = dt} Feed.Types.RSSItem i -> Feed.Types.RSSItem i {RSS.rssItemPubDate = Just dt} Feed.Types.RSS1Item i -> case break isDate $ RSS1.itemDC i of (as, dci:bs) -> Feed.Types.RSS1Item i {RSS1.itemDC = as ++ dci {dcText = dt} : bs} (_, []) -> Feed.Types.RSS1Item i {RSS1.itemDC = DCItem {dcElt = DC_Date, dcText = dt} : RSS1.itemDC i} Feed.Types.XMLItem i -> Feed.Types.XMLItem $ addChild (unode "pubDate" dt) $ filterChildren (\e -> elementName e /= "pubDate") i where isDate dc = dcElt dc == DC_Date -- | 'withItemDate' is a synonym for 'withItemPubDate'. withItemDate :: ItemSetter DateString withItemDate = withItemPubDate -- | 'withItemTitle myTitle' associates a new title, 'myTitle', -- with a feed item. withItemTitle :: ItemSetter Text withItemTitle tit fi = case fi of Feed.Types.AtomItem e -> Feed.Types.AtomItem e {Atom.entryTitle = TextString tit} Feed.Types.RSSItem i -> Feed.Types.RSSItem i {RSS.rssItemTitle = Just tit} Feed.Types.RSS1Item i -> Feed.Types.RSS1Item i {RSS1.itemTitle = tit} Feed.Types.XMLItem i -> Feed.Types.XMLItem $ addChild (unode "title" tit) $ filterChildren (\e -> elementName e /= "title") i -- | 'withItemAuthor auStr' associates new author info -- with a feed item. withItemAuthor :: ItemSetter Text withItemAuthor au fi = case fi of Feed.Types.AtomItem e -> Feed.Types.AtomItem e {Atom.entryAuthors = [nullPerson {personName = au, personURI = Just au}]} Feed.Types.RSSItem i -> Feed.Types.RSSItem i {RSS.rssItemAuthor = Just au} Feed.Types.RSS1Item i -> case break isAuthor $ RSS1.itemDC i of (as, dci:bs) -> Feed.Types.RSS1Item i {RSS1.itemDC = as ++ dci {dcText = au} : bs} (_, []) -> Feed.Types.RSS1Item i {RSS1.itemDC = DCItem {dcElt = DC_Creator, dcText = au} : RSS1.itemDC i} Feed.Types.XMLItem i -> Feed.Types.XMLItem $ addChild (unode "author" au) $ filterChildren (\e -> elementName e /= "author") i where isAuthor dc = dcElt dc == DC_Creator -- | 'withItemFeedLink name myFeed' associates the parent feed URL 'myFeed' -- with a feed item. It is labelled as 'name'. withItemFeedLink :: Text -> ItemSetter Text withItemFeedLink tit url fi = case fi of Feed.Types.AtomItem e -> Feed.Types.AtomItem e { Atom.entrySource = Just Atom.nullSource {sourceId = Just url, sourceTitle = Just (TextString tit)} } Feed.Types.RSSItem i -> Feed.Types.RSSItem i {RSS.rssItemSource = Just (RSS.nullSource url tit)} Feed.Types.RSS1Item i -> Feed.Types.RSS1Item i {RSS1.itemTitle = tit} Feed.Types.XMLItem i -> Feed.Types.XMLItem $ addChild (unode "source" ([mkAttr "url" url], tit)) $ filterChildren (\e -> elementName e /= "source") i -- | 'withItemCommentLink url' sets the URL reference to the comment page to 'url'. withItemCommentLink :: ItemSetter Text withItemCommentLink url fi = case fi of Feed.Types.AtomItem e -> Feed.Types.AtomItem e {Atom.entryLinks = ((nullLink url) {linkRel = Just (Left "replies")}) : Atom.entryLinks e} Feed.Types.RSSItem i -> Feed.Types.RSSItem i {RSS.rssItemComments = Just url} Feed.Types.RSS1Item i -> case break isRel $ RSS1.itemDC i of (as, dci:bs) -> Feed.Types.RSS1Item i {RSS1.itemDC = as ++ dci {dcText = url} : bs} (_, []) -> Feed.Types.RSS1Item i {RSS1.itemDC = DCItem {dcElt = DC_Relation, dcText = url} : RSS1.itemDC i} Feed.Types.XMLItem i -> Feed.Types.XMLItem $ addChild (unode "comments" url) $ filterChildren (\e -> elementName e /= "comments") i where isRel dc = dcElt dc == DC_Relation -- | 'withItemEnclosure url mbTy len' sets the URL reference to the comment page to 'url'. withItemEnclosure :: Text -> Maybe Text -> ItemSetter (Maybe Integer) withItemEnclosure url ty mb_len fi = case fi of Feed.Types.AtomItem e -> Feed.Types.AtomItem e { Atom.entryLinks = ((nullLink url) { linkRel = Just (Left "enclosure") , linkType = ty , linkLength = fmap (pack . show) mb_len }) : Atom.entryLinks e } Feed.Types.RSSItem i -> Feed.Types.RSSItem i {RSS.rssItemEnclosure = Just (nullEnclosure url mb_len (fromMaybe "text/html" ty))} Feed.Types.RSS1Item i -> Feed.Types.RSS1Item i { RSS1.itemContent = nullContentInfo {contentURI = Just url, contentFormat = ty} : RSS1.itemContent i } Feed.Types.XMLItem i -> Feed.Types.XMLItem $ addChild ((unode "enclosure" url) {elementAttributes = [mkAttr "length" "0", mkAttr "type" (fromMaybe "text/html" ty)]}) $ filterChildren (\e -> elementName e /= "enclosure") i -- | 'withItemId isURL id' associates new unique identifier with a feed item. -- If 'isURL' is 'True', then the id is assumed to point to a valid web resource. withItemId :: Bool -> ItemSetter Text withItemId isURL idS fi = case fi of Feed.Types.AtomItem e -> Feed.Types.AtomItem e {Atom.entryId = idS} Feed.Types.RSSItem i -> Feed.Types.RSSItem i {RSS.rssItemGuid = Just (nullGuid idS) {rssGuidPermanentURL = Just isURL}} Feed.Types.RSS1Item i -> case break isId $ RSS1.itemDC i of (as, dci:bs) -> Feed.Types.RSS1Item i {RSS1.itemDC = as ++ dci {dcText = idS} : bs} (_, []) -> Feed.Types.RSS1Item i {RSS1.itemDC = DCItem {dcElt = DC_Identifier, dcText = idS} : RSS1.itemDC i} Feed.Types.XMLItem i -> Feed.Types.XMLItem $ addChild (unode "guid" ([mkAttr "isPermaLink" (showBool isURL)], idS)) $ filterChildren (\e -> elementName e /= "guid") i where showBool x = pack $ map toLower (show x) isId dc = dcElt dc == DC_Identifier -- | 'withItemDescription desc' associates a new descriptive string (aka summary) -- with a feed item. withItemDescription :: ItemSetter Text withItemDescription desc fi = case fi of Feed.Types.AtomItem e -> Feed.Types.AtomItem e {Atom.entrySummary = Just (TextString desc)} Feed.Types.RSSItem i -> Feed.Types.RSSItem i {RSS.rssItemDescription = Just desc} Feed.Types.RSS1Item i -> Feed.Types.RSS1Item i {RSS1.itemDesc = Just desc} Feed.Types.XMLItem i -> Feed.Types.XMLItem $ addChild (unode "description" desc) $ filterChildren (\e -> elementName e /= "description") i -- | 'withItemRights rightStr' associates the rights information 'rightStr' -- with a feed item. withItemRights :: ItemSetter Text withItemRights desc fi = case fi of Feed.Types.AtomItem e -> Feed.Types.AtomItem e {Atom.entryRights = Just (TextString desc)} -- Note: per-item copyright information isn't supported by RSS2.0 (and earlier editions), -- you can only attach this at the feed/channel level. So, there's not much we can do -- except dropping the information on the floor here. (Rolling our own attribute or -- extension element is an option, but would prefer if someone else had started that -- effort already. Feed.Types.RSSItem {} -> fi Feed.Types.RSS1Item i -> case break ((== DC_Rights) . dcElt) $ RSS1.itemDC i of (as, dci:bs) -> Feed.Types.RSS1Item i {RSS1.itemDC = as ++ dci {dcText = desc} : bs} (_, []) -> Feed.Types.RSS1Item i {RSS1.itemDC = DCItem {dcElt = DC_Rights, dcText = desc} : RSS1.itemDC i} -- Since we're so far assuming that a shallow XML rep. of an item -- is of RSS2.0 ilk, pinning on the rights info is hard (see above.) Feed.Types.XMLItem {} -> fi -- | 'withItemTitle myLink' associates a new URL, 'myLink', -- with a feed item. withItemLink :: ItemSetter URLString withItemLink url fi = case fi of Feed.Types.AtomItem e -> Feed.Types.AtomItem e {Atom.entryLinks = replaceAlternate url (Atom.entryLinks e)} Feed.Types.RSSItem i -> Feed.Types.RSSItem i {RSS.rssItemLink = Just url} Feed.Types.RSS1Item i -> Feed.Types.RSS1Item i {RSS1.itemLink = url} Feed.Types.XMLItem i -> Feed.Types.XMLItem $ addChild (unode "link" url) $ filterChildren (\e -> elementName e /= "link") i where replaceAlternate _ [] = [] replaceAlternate x (lr:xs) | toStr (Atom.linkRel lr) == "alternate" = lr {Atom.linkHref = x} : xs | otherwise = lr : replaceAlternate x xs toStr Nothing = "" toStr (Just (Left x)) = x toStr (Just (Right x)) = x withItemCategories :: ItemSetter [(Text, Maybe Text)] withItemCategories cats fi = case fi of Feed.Types.AtomItem e -> Feed.Types.AtomItem e { Atom.entryCategories = map (\(t, mb) -> (Atom.newCategory t) {Atom.catScheme = mb}) cats ++ entryCategories e } Feed.Types.RSSItem i -> Feed.Types.RSSItem i { RSS.rssItemCategories = map (\(t, mb) -> (RSS.newCategory t) {RSS.rssCategoryDomain = mb}) cats ++ rssItemCategories i } Feed.Types.RSS1Item i -> Feed.Types.RSS1Item i { RSS1.itemDC = map (\(t, _) -> DCItem {dcElt = DC_Subject, dcText = t}) cats ++ RSS1.itemDC i } Feed.Types.XMLItem i -> Feed.Types.XMLItem $ foldr (\(t, mb) acc -> addChild (unode "category" (maybe (: []) (\v x -> [mkAttr "domain" v, x]) mb (mkAttr "term" t))) acc) i cats -- helpers.. filterChildren :: (XML.Element -> Bool) -> XML.Element -> XML.Element filterChildren pre e = case elementNodes e of [] -> e cs -> e {elementNodes = mapMaybe filterElt cs} where filterElt xe@(XML.NodeElement el) | pre el = Just xe | otherwise = Nothing filterElt xe = Just xe addChild :: XML.Element -> XML.Element -> XML.Element addChild a b = b {elementNodes = XML.NodeElement a : elementNodes b} mapMaybeChildren :: (XML.Element -> Maybe XML.Element) -> XML.Element -> XML.Element mapMaybeChildren f e = case elementNodes e of [] -> e cs -> e {elementNodes = map procElt cs} where procElt xe@(XML.NodeElement el) = case f el of Nothing -> xe Just el1 -> XML.NodeElement el1 procElt xe = xe