packages feed

hakyllbars-1.0.0.1: src/Hakyllbars/Field/Date.hs

module Hakyllbars.Field.Date
  ( DateConfig (..),
    defaultDateConfigWith,
    dateFields,
    dateFormatField,
    dateField,
    publishedField,
    updatedField,
    getLastModifiedDate,
    isPublishedField,
    isUpdatedField,
    dateFromMetadata,
    normalizedDateTimeFormat,
    parseTimeM',
  )
where

import Data.Aeson.Key qualified as Key
import Data.Aeson.KeyMap qualified as KeyMap
import Data.List (tails)
import Data.String.Utils
import Hakyllbars.Common
import Hakyllbars.Context
import Hakyllbars.Util

data DateConfig = DateConfig
  { -- | The locale to use for date formatting.
    dateConfigLocale :: TimeLocale,
    -- | The current time (or time at which the site generator is running).
    dateConfigCurrentTime :: ZonedTime,
    -- | The format to use for long dates (i.e. date with time).
    dateConfigDateLongFormat :: String,
    -- | The format to use for short dates (i.e. date without time).
    dateConfigDateShortFormat :: String,
    -- | The format to use for time only.
    dateConfigTimeFormat :: String,
    -- | The format to use for machine-readable dates.
    dateConfigRobotDateFormat :: String,
    -- | The format to use for machine-readable times.
    dateConfigRobotTimeFormat :: String
  }

-- | Creates a default date configuration with the given locale and current time.
defaultDateConfigWith :: TimeLocale -> ZonedTime -> DateConfig
defaultDateConfigWith locale currentTime =
  DateConfig
    { dateConfigLocale = locale,
      dateConfigCurrentTime = currentTime,
      dateConfigDateLongFormat = "%B %e, %Y %l:%M %P %EZ",
      dateConfigDateShortFormat = "%B %e, %Y",
      dateConfigTimeFormat = "%l:%M %p %EZ",
      dateConfigRobotDateFormat = "%Y-%m-%d",
      dateConfigRobotTimeFormat = "%Y-%m-%dT%H:%M:%S%Ez"
    }

-- | Creates a default date fields configuration with the given date config.
dateFields :: DateConfig -> Context a
dateFields config =
  mconcat
    [ dateField "date" (dateConfigLocale config) (dateConfigCurrentTime config),
      publishedField "published" (dateConfigLocale config),
      updatedField "updated" (dateConfigLocale config),
      isPublishedField "isPublished",
      isUpdatedField "isUpdated",
      constField "longDate" (dateConfigDateLongFormat config),
      constField "shortDate" (dateConfigDateShortFormat config),
      constField "timeOnly" (dateConfigTimeFormat config),
      constField "robotDate" (dateConfigRobotDateFormat config),
      constField "robotTime" (dateConfigRobotTimeFormat config),
      constField "rfc822" rfc822DateFormat,
      dateFormatField "dateAs" (dateConfigLocale config)
    ]

-- | Gets a date formatted with the given format.
dateFormatField :: String -> TimeLocale -> Context a
dateFormatField key timeLocale = functionField2 key f
  where
    f (dateFormat :: String) (dateString :: String) = do
      date <- deserializeTime dateString
      return $ formatTime timeLocale dateFormat date
    deserializeTime = parseTimeM' timeLocale normalizedDateTimeFormat

-- | Gets the date relative to the configured time locale and current time from the "date" or "published" fields.
dateField :: String -> TimeLocale -> ZonedTime -> Context a
dateField key timeLocale currentTime = field key f
  where
    f item = do
      metadata <- lift . getMetadata $ itemIdentifier item
      tplWithCall key . lift $
        do
          let maybeDateString = dateFromMetadata timeLocale ["date", "published"] metadata
          maybe (dateFromFilePath timeLocale item) return maybeDateString
            <|> return (formatTime timeLocale "%Y-%m-%dT%H:%M:%S%Ez" currentTime)

-- | Gets the published date of an item from the metadata fields "published" or "date".
publishedField :: String -> TimeLocale -> Context a
publishedField key timeLocale = field key f
  where
    f =
      lift
        . getMetadata
        . itemIdentifier
        >=> tplWithCall key
        . lift
        . maybe (noResult $ "Tried published field " ++ show key) return
        . dateFromMetadata timeLocale ["published", "date"]

-- | Gets the updated date of an item from the metadata fields "updated", "published", or "date".
updatedField :: String -> TimeLocale -> Context a
updatedField key timeLocale = field key f
  where
    f =
      lift
        . getMetadata
        . itemIdentifier
        >=> tplWithCall key
        . lift
        . maybe (noResult $ "Tried updated field " ++ show key) return
        . dateFromMetadata timeLocale ["updated", "published", "date"]

-- | Gets the last modified date of an item from the metadata fields "updated", "published", or "date", or the file path
-- if it contains a date.
getLastModifiedDate :: TimeLocale -> Item a -> Compiler ZonedTime
getLastModifiedDate timeLocale item = do
  metadata <- getMetadata $ itemIdentifier item
  let maybeDateString = dateFromMetadata timeLocale ["updated", "published", "date"] metadata
  dateString <- maybe (dateFromFilePath timeLocale item) return maybeDateString
  parseTimeM' timeLocale "%Y-%m-%dT%H:%M:%S%Ez" dateString

-- | Gets a date from the given metadata fields.
dateFromMetadata ::
  -- | The time locale to use.
  TimeLocale ->
  -- | The list of metadata keys to search for.
  [String] ->
  Metadata ->
  Maybe String
dateFromMetadata timeLocale sourceKeys metadata =
  firstAlt $ findDate <$> sourceKeys
  where
    findDate sourceKey =
      serializeTime =<< lookupString sourceKey metadata
    serializeTime dateString = do
      date <- firstAlt (parse dateString <$> metadataDateFormats)
      return $ normalizedTime timeLocale date
    parse = flip $ parseTimeM True timeLocale

-- | Gets a date from the item's file path.
dateFromFilePath :: TimeLocale -> Item a -> Compiler String
dateFromFilePath timeLocale item =
  dateFromPath
    <|> noResult ("Could not find file path date from " ++ show (toFilePath $ itemIdentifier item))
  where
    dateFromPath =
      firstAlt $
        dateFromPath' . intercalate "-"
          <$> ( [take 3 $ split "-" fnCand | fnCand <- reverse paths]
                  ++ (fmap (take 3) <$> reverse (tails paths))
              )
    paths = splitDirectories $ dropExtension $ toFilePath $ itemIdentifier item
    dateFromPath' path = do
      debugCompiler $ "Trying to parse date from path " ++ show path
      date <- parseTimeM' timeLocale "%Y-%m-%d" path
      return $ normalizedTime timeLocale date

parseTimeM' :: (MonadFail m) => TimeLocale -> String -> String -> m ZonedTime
parseTimeM' = parseTimeM True

normalizedTime :: TimeLocale -> ZonedTime -> String
normalizedTime = flip formatTime normalizedDateTimeFormat

normalizedDateTimeFormat :: String
normalizedDateTimeFormat = "%Y-%m-%dT%H:%M:%S%Ez"

rfc822DateFormat :: String
rfc822DateFormat = "%a, %d %b %Y %H:%M:%S %Z"

-- | Supported date formats to read from metadata.
metadataDateFormats :: [String]
metadataDateFormats =
  [ "%Y-%m-%d",
    normalizedDateTimeFormat,
    "%Y-%m-%dT%H:%M:%S",
    "%Y-%m-%d %H:%M:%S %EZ",
    "%Y-%m-%d %H:%M:%S%Ez",
    "%Y-%m-%d %H:%M:%S",
    rfc822DateFormat,
    "%a, %d %b %Y %H:%M:%S",
    "%B %e, %Y %l:%M %p %EZ",
    "%B %e, %Y %l:%M %p",
    "%b %e, %Y %l:%M %p %EZ",
    "%b %e, %Y %l:%M %p",
    "%B %e, %Y",
    "%B %d, %Y",
    "%b %e, %Y",
    "%b %d, %Y"
  ]

-- | Gets whether the item is published.
isPublishedField :: String -> Context a
isPublishedField key = field key f
  where
    f item = lift do
      getMetadata (itemIdentifier item)
        <&> isJust
        . KeyMap.lookup (Key.fromString "published")

-- | Gets whether the item has been updated.
isUpdatedField :: String -> Context a
isUpdatedField key = field key f
  where
    f item = lift do
      getMetadata (itemIdentifier item)
        <&> isJust
        . KeyMap.lookup (Key.fromString "updated")