packages feed

imm-2.1.2.0: src/write-file/Main.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE UnicodeSyntax #-}

-- Write a file from the input RSS/Atom item.
--
--  Meant to be used as a callback for imm.
--  {{{ Imports

import Data.Aeson
import Data.ByteString.Builder (Builder, toLazyByteString)
import Data.ByteString.Lazy (getContents, writeFile)
import qualified Data.Text as Text (null, replace, unpack)
import Data.Time
import Imm.Callback
import Imm.Feed
import Imm.Link
import Imm.Pretty
import Options.Applicative
import System.Directory (createDirectoryIfMissing)
import System.FilePath
import Text.Blaze.Html.Renderer.Utf8
import Text.Blaze.Html5 (Html, docTypeHtml, preEscapedToHtml, (!))
import qualified Text.Blaze.Html5 as H
import qualified Text.Blaze.Html5.Attributes as H (charset, href)
import URI.ByteString.Extended

-- }}}

data CliOptions = CliOptions
  { _directory ∷ FilePath
  , _dryRun ∷ Bool
  }
  deriving (Eq, Ord, Read, Show)

parseOptions ∷ MonadIO m ⇒ m CliOptions
parseOptions = io $ execParser $ info (cliOptions <**> helper) $ progDesc description
 where
  description = "Write a file for each new RSS/Atom item. An intermediate folder will be created for each feed."

cliOptions ∷ Parser CliOptions
cliOptions =
  CliOptions
    <$> strOption (long "directory" <> short 'd' <> metavar "PATH" <> help "Root directory where files will be created.")
    <*> switch (long "dry-run" <> help "Disable all I/Os, except for logs.")

main ∷ IO ()
main = do
  CliOptions directory dryRun ← parseOptions
  input ← getContents <&> eitherDecode

  case input ∷ Either String CallbackMessage of
    Right (CallbackMessage feedLocation feedDefinition item) → do
      let content = defaultFileContent feedDefinition item
          filePath = defaultFilePath directory feedLocation feedDefinition item
      putStrLn filePath
      unless dryRun $ do
        createDirectoryIfMissing True $ takeDirectory filePath
        writeFile filePath $ toLazyByteString content
    Left e → putStrLn ("Invalid input: " <> e) >> exitFailure
  return ()

-- * Default behavior

-- | Generate a path @<root>/<feed designator>/<element date>-<element title>.html@, where @<root>@ is the first argument
defaultFilePath ∷ FilePath → FeedLocation → FeedDefinition → FeedItem → FilePath
defaultFilePath root feedLocation feedDefinition element = makeValid $ root </> feedFolder </> fileName <.> "html"
 where
  FeedLocation feedUri _ = feedLocation
  feedFolder = if Text.null title then uriToFolder feedUri else toString title
  uriToFolder uri =
    uri
      & uriAuthority
      <&> authorityHost
      <&> hostBS
      <&> decodeUtf8
      & fromMaybe "unknown-host"
      & sanitize
      & Text.unpack
  date = maybe "" (formatTime defaultTimeLocale "%F-") $ _itemDate element
  fileName = date <> toString (sanitize $ _itemTitle element)
  title = sanitize $ _feedTitle feedDefinition
  sanitize =
    appEndo (mconcat [Endo $ Text.replace (toText [s]) "_" | s ← pathSeparators])
      >>> Text.replace "." "_"
      >>> Text.replace "?" "_"
      >>> Text.replace "!" "_"
      >>> Text.replace "#" "_"

-- | Generate an HTML page, with a title, a header and an article that contains the feed element
defaultFileContent ∷ FeedDefinition → FeedItem → Builder
defaultFileContent feedDefinition element = renderHtmlBuilder $
  docTypeHtml $ do
    H.head $ do
      H.meta ! H.charset "utf-8"
      H.title $ convertText $ _feedTitle feedDefinition <> " | " <> _itemTitle element
    H.body $ do
      H.h1 $ convertText $ _feedTitle feedDefinition
      H.article $ do
        H.header $ do
          defaultArticleTitle feedDefinition element
          defaultArticleAuthor feedDefinition element
          defaultArticleDate feedDefinition element
        defaultBody feedDefinition element

-- * Low-level helpers

defaultArticleTitle ∷ FeedDefinition → FeedItem → Html
defaultArticleTitle _ item =
  H.h2 $
    maybe id (\link → H.a ! href (_linkURI link)) (getMainLink item) $
      convertText $
        _itemTitle item

defaultArticleAuthor ∷ FeedDefinition → FeedItem → Html
defaultArticleAuthor _ item = H.address $ do
  "Published by "
  forM_ (_itemAuthors item) $ \author → do
    convertDoc $ pretty author
    ", "

defaultArticleDate ∷ FeedDefinition → FeedItem → Html
defaultArticleDate _ element = forM_ (_itemDate element) $ \date → H.p $ " on " >> H.time (convertDoc $ prettyTime date)

-- | Generate the HTML content for a given feed element
defaultBody ∷ FeedDefinition → FeedItem → Html
defaultBody _ item = do
  unless (null links) $
    H.p $ do
      "Related links:"
      H.ul $ forM_ links $ \uri → H.li (H.a ! href uri $ convertURI uri)
  H.p $ preEscapedToHtml $ _itemContent item
 where
  links = _linkURI <$> _itemLinks item

href ∷ AnyURI → H.Attribute
href = H.href . convertURI

convertURI ∷ IsString t ⇒ AnyURI → t
convertURI = convertText . decodeUtf8 . withAnyURI serializeURIRef'

convertText ∷ IsString t ⇒ Text → t
convertText = fromString . toString

convertDoc ∷ IsString t ⇒ Doc a → t
convertDoc = show