{-# LANGUAGE OverloadedStrings #-} {-| Internal implementation of Pencil's environment. -} module Pencil.Env.Internal where import qualified Pencil.Parser as P import Data.Text.Encoding (encodeUtf8) import qualified Data.HashMap.Strict as H import qualified Data.Maybe as M import qualified Data.Text as T import qualified Data.Time.Clock as TC import qualified Data.Time.Format as TF import qualified Data.Vector as V import qualified Data.Yaml as A -- | Represents the data types found in an environment. -- -- This includes at least 'Data.Aeson' 'Data.Aeson.Types.Value' types, plus other -- useful ones. data Value = VNull -- JSON null | VText T.Text | VBool Bool | VDateTime TC.UTCTime | VArray [Value] | VEnvList [Env] | VNodes [P.PNode] deriving (Eq, Show) -- | Environment map of variables to 'Value's. type Env = H.HashMap T.Text Value -- | Converts an Aeson 'Aeson.Value' to a Pencil 'Value'. toValue :: A.Value -> Maybe Value toValue A.Null = Just VNull toValue (A.Bool b) = Just $ VBool b toValue (A.String s) = -- See if coercible to datetime case toDateTime (T.unpack s) of Nothing -> Just $ VText s Just dt -> Just $ VDateTime dt toValue (A.Array arr) = Just $ VArray (V.toList (V.mapMaybe toValue arr)) toValue _ = Nothing -- | Accepted format is ISO 8601 (YYYY-MM-DD), optionally with an appended "THH:MM:SS". -- Examples: -- -- * 2010-01-30 -- * 2010-01-30T09:08:00 -- toDateTime :: String -> Maybe TC.UTCTime toDateTime s = -- Try to parse "YYYY-MM-DD" case parseIso8601 Nothing s of -- Try to parse "YYYY-MM-DDTHH:MM:SS" Nothing -> parseIso8601 (Just "%H:%M:%S") s Just dt -> Just dt -- | Helper for 'TF.parseTimeM' using ISO 8601. YYYY-MM-DDTHH:MM:SS and -- YYYY-MM-DD formats. -- -- https://hackage.haskell.org/package/time-1.9/docs/Data-Time-Format.html#v:iso8601DateFormat parseIso8601 :: Maybe String -> String -> Maybe TC.UTCTime parseIso8601 f = TF.parseTimeM True TF.defaultTimeLocale (TF.iso8601DateFormat f) -- | Gets the nodes from the env, from the @this.nodes@ variable. Returns empty -- list if this variable is missing. getNodes :: Env -> [P.PNode] getNodes env = case H.lookup "this.nodes" env of Just (VNodes nodes) -> nodes _ -> [] -- | Find preamble node, and load as an Env. If no preamble is found, return a -- blank Env. findEnv :: [P.PNode] -> Env findEnv nodes = aesonToEnv $ M.fromMaybe H.empty (P.findPreambleText nodes >>= (A.decodeThrow . encodeUtf8 . T.strip)) -- | Converts an Aeson Object to an Env. aesonToEnv :: A.Object -> Env aesonToEnv = H.foldlWithKey' maybeInsertIntoEnv H.empty -- | Convert known Aeson 'Aeson.Value' into a Pencil -- 'Pencil.Env.Internal.Value', and insert into the env. If there is no -- conversion possible, the env is not modified. maybeInsertIntoEnv :: Env -> T.Text -> A.Value -> Env maybeInsertIntoEnv env k v = case toValue v of Nothing -> env Just d -> H.insert k d env -- | Gets the rendered content from the env, from the @this.content@ variable. -- Returns empty is the variable is missing (e.g. has not been rendered). getContent :: Env -> Maybe T.Text getContent env = case H.lookup "this.content" env of Just (VText content) -> return content _ -> Nothing -- | Renders environment value for human consumption. This is the default one. toText :: Value -> T.Text toText VNull = "null" toText (VText t) = t toText (VArray arr) = T.unwords $ map toText arr toText (VBool b) = if b then "true" else "false" toText (VEnvList envs) = T.unwords $ map (T.unwords . map toText . H.elems) envs toText (VDateTime dt) = -- December 30, 2017 T.pack $ TF.formatTime TF.defaultTimeLocale "%B %e, %Y" dt toText (VNodes nodes) = P.renderNodes nodes -- | A version of 'toText' that renders 'Value' acceptable for an RSS feed. -- -- * Dates are rendered in the RFC 822 format. -- * Everything else defaults to the 'toText' implementation. -- -- You'll probably want to also use 'Pencil.Content.Internal.escapeXml' to render an RSS feed. -- toTextRss :: Value -> T.Text toTextRss (VDateTime dt) = T.pack $ TF.formatTime TF.defaultTimeLocale rfc822DateFormat dt toTextRss v = toText v -- | RFC 822 date format. -- -- Helps to pass https://validator.w3.org/feed/check.cgi. -- -- Same as https://hackage.haskell.org/package/time/docs/Data-Time-Format.html#v:rfc822DateFormat -- but no padding for the day section, so that single-digit days only has one space preceeding it. -- -- Also changed to spit out the offset timezone (+0000) because the default was spitting out "UTC" -- which is not valid RFC 822. Weird, since the defaultTimeLocal source and docs show that it won't -- use "UTC": -- https://hackage.haskell.org/package/time/docs/Data-Time-Format.html#v:defaultTimeLocale -- rfc822DateFormat :: String rfc822DateFormat = "%a, %d %b %Y %H:%M:%S %z"