packages feed

kitchen-sink-0.1.0.0: src/KitchenSink/Engine/SiteConfig.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}

module KitchenSink.Engine.SiteConfig where

import Data.Aeson (FromJSON, ToJSON)
import Data.Maybe (fromMaybe, isNothing)
import Data.Text qualified as Text
import GHC.Generics (Generic)
import Prelude ((&&))

import KitchenSink.Layout.Blog.Metadata (FooterColumn (..), FooterSpec (..), HomeLinkSpec (..), MenuItem (..), defaultHomeLink)
import KitchenSink.Prelude

data LinkedSite = LinkedSite
    { baseURL :: Text
    , siteType :: Text
    , siteTitle :: Text
    }
    deriving (Generic, Show)
instance FromJSON LinkedSite
instance ToJSON LinkedSite

-- | The optional @homeLink@ object of @kitchen-sink.json@: the label (default
-- @"Home"@) and an optional icon image of the link back to the site root.
data HomeLink = HomeLink
    { label :: Maybe Text
    , icon :: Maybe Text
    }
    deriving (Generic, Show)
instance FromJSON HomeLink
instance ToJSON HomeLink

-- | A plain link of the @menu@ or of a @footer@ column.
data NavLink = NavLink
    { label :: Text
    , url :: Text
    }
    deriving (Generic, Show)
instance FromJSON NavLink
instance ToJSON NavLink

-- | An entry of the @menu@ array of @kitchen-sink.json@: a link, optionally
-- with a sub-menu of plain links (one level only).
data NavEntry = NavEntry
    { label :: Text
    , url :: Text
    , children :: Maybe [NavLink]
    }
    deriving (Generic, Show)
instance FromJSON NavEntry
instance ToJSON NavEntry

data FooterColumnConfig = FooterColumnConfig
    { heading :: Maybe Text
    , links :: [NavLink]
    }
    deriving (Generic, Show)
instance FromJSON FooterColumnConfig
instance ToJSON FooterColumnConfig

-- | The optional @footer@ object of @kitchen-sink.json@: columns of links and
-- a legal line.
data FooterConfig = FooterConfig
    { columns :: Maybe [FooterColumnConfig]
    , legal :: Maybe Text
    }
    deriving (Generic, Show)
instance FromJSON FooterConfig
instance ToJSON FooterConfig

data SiteInfo = SiteInfo
    { title :: Text
    , publishURL :: Text
    , twitterLogin :: Maybe Text
    , linkedSites :: Maybe [LinkedSite]
    , basePath :: Maybe Text
    , homeLink :: Maybe HomeLink
    , menu :: Maybe [NavEntry]
    , footer :: Maybe FooterConfig
    }
    deriving (Generic, Show)
instance FromJSON SiteInfo
instance ToJSON SiteInfo

defaultSiteInfo :: SiteInfo
defaultSiteInfo =
    SiteInfo "invalid siteconfig!" "/" Nothing Nothing Nothing Nothing Nothing Nothing

-- | The home link to render, falling back to the defaults for whatever the
-- configuration leaves out.
resolveHomeLink :: SiteInfo -> HomeLinkSpec
resolveHomeLink config =
    HomeLinkSpec
        (fromMaybe (homeLabel defaultHomeLink) (config.homeLink >>= (.label)))
        (config.homeLink >>= (.icon))

-- | The header menu to render: empty when the configuration has none.
resolveMenu :: SiteInfo -> [MenuItem]
resolveMenu config = fmap toItem (fromMaybe [] config.menu)
  where
    toItem e = MenuItem e.label e.url (fmap linkItem (fromMaybe [] e.children))

-- | The footer to render: 'Nothing' when the configuration has no columns and
-- no legal line.
resolveFooter :: SiteInfo -> Maybe FooterSpec
resolveFooter config = do
    f <- config.footer
    let cols = fmap toColumn (fromMaybe [] f.columns)
    if null cols && isNothing f.legal
        then Nothing
        else Just (FooterSpec cols f.legal)
  where
    toColumn c = FooterColumn c.heading (fmap linkItem c.links)

linkItem :: NavLink -> MenuItem
linkItem l = MenuItem l.label l.url []

-- | Normalizes the configured @basePath@ (e.g., @Just "tramaj"@, @Just
-- "/tramaj/"@, @Nothing@) into a prefix ready to prepend to the
-- root-relative URLs generated throughout the site: no trailing slash, a
-- single leading slash when non-empty, and @""@ when hosted at the domain
-- root. This is what makes a GitHub Pages *project* page (served under
-- @https:\/\/user.github.io\/reponame\/@ rather than the domain root) work.
normalizedBasePath :: SiteInfo -> Text
normalizedBasePath config =
    case Text.dropWhileEnd (== '/') . dropLeadingSlashes <$> basePath config of
        Nothing -> ""
        Just "" -> ""
        Just p -> "/" <> p
  where
    dropLeadingSlashes = Text.dropWhile (== '/')