opml-conduit-0.2.0.0: Text/OPML/Stream/Render.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies #-}
-- | Streaming renderer for the OPML 2.0 standard.
module Text.OPML.Stream.Render
( -- * Renderers
renderOpmlHead
, renderOpmlOutline
, renderOpml
) where
-- {{{ Imports
import Control.Lens.At
import Control.Lens.Getter
import Control.Monad
import Data.Conduit
import Data.List.NonEmpty hiding (filter, map)
import Data.Monoid
import Data.NonNull
import Data.Text (Text, intercalate, pack, toLower)
import Data.Time.Clock
import Data.Time.LocalTime
import Data.Time.RFC822
import Data.Tree
import Data.Version
import Data.XML.Types
import Prelude hiding (foldr, lookup)
import Text.OPML.Types
import Text.XML.Stream.Render
-- }}}
tshow :: (Show a) => a -> Text
tshow = pack . show
empty :: (Eq s, Monoid s) => s -> Bool
empty t = t == mempty
toMaybe :: (Eq s, Monoid s) => s -> Maybe s
toMaybe s | s == mempty = mempty
| otherwise = Just s
formatTime :: UTCTime -> Text
formatTime = formatTimeRFC822 . utcToZonedTime utc
formatCategories :: [NonEmpty (NonNull Text)] -> Maybe Text
formatCategories = toMaybe . intercalate "," . map (intercalate "/" . toList . fmap toNullable)
formatBool :: Bool -> Text
formatBool = toLower . tshow
-- | Render the @\<head\>@ section.
renderOpmlHead :: (Monad m) => OpmlHead -> Source m Event
renderOpmlHead input = tag "head" mempty $ do
forM_ (input^.opmlCreated_) $ tag "dateCreated" mempty . content . formatTime
forM_ (input^.modified_) $ tag "dateModified" mempty . content . formatTime
forM_ (input^.docs_) $ tag "docs" mempty . content . tshow
unless (null es) . tag "expansionState" mempty . content . intercalate "," $ tshow <$> es
unless (empty email) $ tag "ownerEmail" mempty $ content email
forM_ (input^.ownerId_) $ tag "ownerId" mempty . content . tshow
unless (empty name) $ tag "ownerName" mempty $ content name
unless (empty title) $ tag "title" mempty $ content title
forM_ (input^.vertScrollState_) $ tag "vertScrollState" mempty . content . tshow
forM_ (input^.window_.at Bottom') $ tag "windowBottom" mempty . content . tshow
forM_ (input^.window_.at Left') $ tag "windowLeft" mempty . content . tshow
forM_ (input^.window_.at Right') $ tag "windowRight" mempty . content . tshow
forM_ (input^.window_.at Top') $ tag "windowTop" mempty . content . tshow
where es = input ^. expansionState_
email = input ^. ownerEmail_
name = input ^. ownerName_
title = input ^. opmlTitle_
-- | Render an @\<outline\>@ section.
renderOpmlOutline :: (Monad m) => Tree OpmlOutline -> Source m Event
renderOpmlOutline (Node outline subOutlines) = tag "outline" attributes $ mapM_ renderOpmlOutline subOutlines
where attributes = case outline of
OpmlOutlineGeneric b t -> baseAttr b <> optionalAttr "type" (toMaybe t)
OpmlOutlineLink b uri -> baseAttr b <> attr "type" "link" <> attr "url" (tshow uri)
OpmlOutlineSubscription b s -> baseAttr b <> subscriptionAttr s
baseAttr b = attr "text" (toNullable $ b^.text_)
<> optionalAttr "isComment" (formatBool <$> b^.isComment_)
<> optionalAttr "isBreakpoint" (formatBool <$> b^.isBreakpoint_)
<> optionalAttr "created" (formatTime <$> b^.outlineCreated_)
<> optionalAttr "category" (formatCategories $ b^.categories_)
subscriptionAttr s = attr "type" "rss"
<> attr "xmlUrl" (tshow $ s^.xmlUri_)
<> optionalAttr "htmlUrl" (tshow <$> s^.htmlUri_)
<> optionalAttr "description" (toMaybe $ s^.description_)
<> optionalAttr "language" (toMaybe $ s^.language_)
<> optionalAttr "title" (toMaybe $ s^.subscriptionTitle_)
<> optionalAttr "version" (toMaybe $ s^.subscriptionVersion_)
-- | Render the top-level @\<opml\>@ section.
renderOpml :: (Monad m) => Opml -> Source m Event
renderOpml opml = tag "opml" (attr "version" . pack . showVersion $ opml^.opmlVersion_) $ do
renderOpmlHead $ opml^.head_
tag "body" mempty . mapM_ renderOpmlOutline $ opml^.outlines_