summaryrefslogtreecommitdiff
path: root/Text/OPML/Conduit/Render.hs
blob: 97862035f03cdc9c90d1e4122a40b0677b95e344 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
{-# LANGUAGE FlexibleContexts  #-}
{-# LANGUAGE OverloadedLists   #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeFamilies      #-}
-- | Streaming renderer for the OPML 2.0 standard.
module Text.OPML.Conduit.Render
  ( -- * Renderers
    renderOpmlHead
  , renderOpmlOutline
  , renderOpml
  ) where

-- {{{ Imports
import           Control.Monad

import           Data.Conduit
import           Data.List.NonEmpty     hiding (filter, map)
import           Data.Monoid
import           Data.NonNull
import           Data.String
import           Data.Text              (Text, intercalate, pack, toLower)
import           Data.Text.Encoding
import           Data.Time.Clock
import           Data.Time.LocalTime
import           Data.Time.RFC822
import           Data.Tree
import           Data.Version
import           Data.XML.Types

import           Lens.Simple

import           Prelude                hiding (foldr, lookup, show)
import qualified Prelude                (show)

import           Text.OPML.Lens
import           Text.OPML.Types
import           Text.XML.Stream.Render

import           URI.ByteString
-- }}}

show :: (Show a, IsString t) => a -> t
show = fromString . Prelude.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 . show

formatURI :: URI -> Text
formatURI = decodeUtf8 . serializeURIRef'

-- | Render the @\<head\>@ section.
renderOpmlHead :: (Monad m) => OpmlHead -> Source m Event
renderOpmlHead input = tag "head" mempty $ do
  forM_ (input^.opmlCreatedL)       $ tag "dateCreated" mempty . content . formatTime
  forM_ (input^.modifiedL)          $ tag "dateModified" mempty . content . formatTime
  forM_ (input^.docsL)              $ tag "docs" mempty . content . formatURI
  unless (null es)                  $ tag "expansionState" mempty . content . intercalate "," $ show <$> es
  unless (empty email)              $ tag "ownerEmail" mempty $ content email
  forM_ (input^.ownerIdL)           $ tag "ownerId" mempty . content . formatURI
  unless (empty name)               $ tag "ownerName" mempty $ content name
  unless (empty title)              $ tag "title" mempty $ content title
  forM_ (input^.vertScrollStateL)   $ tag "vertScrollState" mempty . content . show
  forM_ (input^.windowBottomL)      $ tag "windowBottom" mempty . content . show
  forM_ (input^.windowLeftL)        $ tag "windowLeft" mempty . content . show
  forM_ (input^.windowRightL)       $ tag "windowRight" mempty . content . show
  forM_ (input^.windowTopL)         $ tag "windowTop" mempty . content . show
  where es = input ^.. expansionStateL
        email = input ^. ownerEmailL
        name = input ^. ownerNameL
        title = input ^. opmlTitleL

-- | 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" (formatURI uri)
          OpmlOutlineSubscription b s -> baseAttr b <> subscriptionAttr s
        baseAttr b = attr "text" (toNullable $ b^.textL)
                     <> optionalAttr "isComment" (formatBool <$> b^.isCommentL)
                     <> optionalAttr "isBreakpoint" (formatBool <$> b^.isBreakpointL)
                     <> optionalAttr "created" (formatTime <$> b^.outlineCreatedL)
                     <> optionalAttr "category" (formatCategories $ b^.categoriesL)
        subscriptionAttr s = attr "type" "rss"
                             <> attr "xmlUrl" (formatURI $ s^.xmlUriL)
                             <> optionalAttr "htmlUrl" (formatURI <$> s^.htmlUriL)
                             <> optionalAttr "description" (toMaybe $ s^.descriptionL)
                             <> optionalAttr "language" (toMaybe $ s^.languageL)
                             <> optionalAttr "title" (toMaybe $ s^.subscriptionTitleL)
                             <> optionalAttr "version" (toMaybe $ s^.subscriptionVersionL)

-- | Render the top-level @\<opml\>@ section.
renderOpml :: (Monad m) => Opml -> Source m Event
renderOpml opml = tag "opml" (attr "version" . pack . showVersion $ opml^.opmlVersionL) $ do
  renderOpmlHead $ opml^.opmlHeadL
  tag "body" mempty . mapM_ renderOpmlOutline $ opml^.opmlOutlinesL