packages feed

shakebook-0.13.1.0: test/Spec.hs

{-# LANGUAGE TemplateHaskell #-}

import           Data.List.Split
import           Path.Extensions
import           RIO
import qualified RIO.ByteString.Lazy as LBS
import           RIO.List
import qualified RIO.Text            as T
import           Shakebook
import qualified Shakebook.IxSet     as Ix
import qualified Shakebook.Tuple     as C
import           Test.Tasty
import           Test.Tasty.Golden

-- Config --

sourceDir :: Path Rel Dir
sourceDir = $(mkRelDir "test/site")

outputDir :: Path Rel Dir
outputDir = $(mkRelDir "test/public")

templatesDir :: Path Rel Dir
templatesDir = $(mkRelDir "templates")

baseUrl :: Text
baseUrl = "http://blanky.test"

siteTitle :: Text
siteTitle = "Blanky Site"

tableOfContents :: Cofree [] (Path Rel File)
tableOfContents = $(mkRelFile "docs/index.md") :< [
                    $(mkRelFile "docs/1/index.md") :< []
                  , $(mkRelFile "docs/2/index.md") :< [
                  $(mkRelFile "docs/2/champ.md") :< []
                    ]
                  ]

numRecentPosts :: Int
numRecentPosts = 3

numPageNeighbours :: Int
numPageNeighbours = 3

postsPerPage :: Int
postsPerPage = 5

mySocial :: [Record Link]
mySocial = [ "twitter" :|: "http://twitter.com/blanky-site-nowhere"
           , "youtube" :|: "http://youtube.com/blanky-site-nowhere"
           , "gitlab"  :|: "http://gitlab.com/blanky-site-nowhere"
           ]

postsRoot :: Text
postsRoot = "/posts/"

tagRoot :: Tag -> Text
tagRoot x = "/posts/tags/" <> unTag x <> "/"

monthRoot :: YearMonth -> Text
monthRoot x = "/posts/months/" <> (defaultMonthUrlFormat . fromYearMonth $ x) <> "/"

pageRoot :: Text -> Int -> Text
pageRoot x i = x <> "pages/" <> (T.pack . show $ i) <> "/"

enrichment :: Record Enrichment
enrichment = val @"social"        mySocial
          :& val @"cdn-imports"   defaultCdnImports
          :& val @"highlighting"  defaultHighlighting
          :& val @"site-title"    siteTitle
          :& RNil

type PostSet = Ix.IxSet '[Tag, Posted, YearMonth] (Record (Routed Stage1Post))

-- Loaders

loadMarkdownWith :: (ShakeValue (Record (Compdoc a)), MonadSB r m) => JsonFormat Void (Record a) -> Path Rel File -> m (Record (Compdoc a))
loadMarkdownWith f x = cacheAction ("loader" :: Text, x) $ do
  logInfo $ "Loading " <> displayShow (toFilePath x)
  readMarkdownFile defaultMarkdownReaderOptions defaultHtml5WriterOptions f x

postIndex :: MonadSB r m => Path Rel Dir -> [FilePattern] -> m PostSet
postIndex = batchLoadIndex $ \x -> do
              k <- loadMarkdownWith rawPostMetaJsonFormat x
              u <- deriveUrl x
              return $ u :*: stage1PostExtras k <+> k

docTree :: MonadSB r m => Path Rel Dir -> Cofree [] (Path Rel File) -> m (Cofree [] (Record (Routed RawDoc)))
docTree dir toc' = forM (dir </$> toc') $ \x -> do
                     k <- loadMarkdownWith rawDocMetaJsonFormat x
                     u <- deriveUrl x
                     return $ u :*: k

-- Extras

deriveUrl :: MonadThrow m => Path Rel File -> m Text
deriveUrl = fmap toGroundedUrl . withHtmlExtension <=< stripProperPrefix sourceDir

mainPageExtras :: Ix.IsIndexOf Posted ixs => Ix.IxSet ixs (Record xs) -> Record (FRecentPosts xs : '[])
mainPageExtras = C.singleton . mostRecentPosted numRecentPosts

stage1PostExtras :: Record RawPost -> Record Stage1PostExtras
stage1PostExtras x = view fPosted x
                 :*: (map (deriveTagLink tagRoot . Tag) . view fTags $ x)
                 :*: (defaultDeriveTeaser . view fContent $ x)
                 :*: RNil

createBlogNav :: PostSet -> Cofree [] (Record Link)
createBlogNav xs = ("Blog" :*: postsRoot :*: RNil)
                :< Ix.toDescCofreeList
                    (C.fanout (defaultPrettyMonthFormat . fromYearMonth) monthRoot)
                    (C.fanout (view fTitle) (view fUrl))
                    (Down . view fPosted)
                    xs

-- Stache Builders

myBuildPage :: (MonadAction m, RMap x, RecordToJsonObject x, RecordFromJson x)
            => PName -> Rec (JsonField e) x -> Record x -> Path Rel File -> m ()
myBuildPage t f x out = do
  k <- compileMustacheDir' t (sourceDir </> templatesDir)
  let l' = renderMustache' k (enrichedRecordJsonFormat f) (enrichment <+> x)
  writeFile' out l'

buildFromRouteF t f x = do
  out <- fromGroundedUrlF $ view fUrl x
  myBuildPage t f (rcast x) (outputDir </> out)

buildFromRouteD t f x = do
  out <- deriveIndexHtmlF $ view fUrl x
  myBuildPage t f (rcast x) (outputDir </> out)

-- Rules

docsRules :: MonadSB r m => Path Rel Dir -> Cofree [] (Path Rel File) -> m ()
docsRules dir toc = do
  as <- docTree dir toc
  let nav = createDocNav as
  let as' = as =>> \(x :< xs) -> val @"toc" nav :& val @"subsections" (extract <$> xs) :& x
  forM_ as' $ buildFromRouteF "docs" finalDocJsonFields

mainPageRules :: MonadSB r m => PostSet -> m ()
mainPageRules postsIx = do
  x <- loadMarkdownWith rawSingleMetaJsonFormat $ sourceDir </> $(mkRelFile "index.md")
  let x' = mainPageExtras postsIx <+> x
  myBuildPage "index" mainPageJsonFields x' $ outputDir </> $(mkRelFile "index.html")

deriveIndexHtmlF :: MonadThrow m => Text -> m (Path Rel File)
deriveIndexHtmlF = fmap (</> $(mkRelFile "index.html")) . fromGroundedUrlD

postIndexRules :: MonadSB r m => Cofree [] (Record Link) -> Text -> PostSet -> Text -> m ()
postIndexRules nav title postset root = do
  ys <- indexPagesBy (Proxy @Posted) postsPerPage postset
  let ys' = flip fmap ys $ \x -> val @"url" (pageRoot root $ view fPageNo x) :& x
  sequence_ $ ys' =>> \xs' -> do
    let x = extract xs'
    ps <- toHtmlFragmentM $ renderPageLinks numPageNeighbours ys'
    let x' = val @"page-links" ps :& val @"toc" nav :& val @"title" title :& x
    buildFromRouteD "post-list" postIndexPageJsonFields x'
  k' <- deriveIndexHtmlF $ view fUrl $ extract $ seek 0 ys'
  s' <- deriveIndexHtmlF root
  copyFileChanged (outputDir </> k') (outputDir </> s')

postRules :: MonadSB r m => Path Rel Dir -> [FilePattern] -> m PostSet
postRules dir fp = cacheAction ("build" :: T.Text, (dir, fp)) $ do
  postsIx <- postIndex dir fp
  let nav = createBlogNav postsIx
  let postsZ = Ix.toDescList (Proxy @Posted) postsIx
  let postsZ' = fmap (\x -> val @"toc" nav :& x) postsZ
  forM_ postsZ' $ buildFromRouteF "post" finalPostJsonFields
  postIndexRules nav "Posts" postsIx postsRoot
  forM_ (Ix.indexKeys postsIx) $ \t@(Tag t') ->
    postIndexRules nav ("Posts tagged " <> t') (postsIx Ix.@+ [t]) (tagRoot t)
  forM_ (Ix.indexKeys postsIx) \ym@(YearMonth _) ->
    postIndexRules nav ("Posts from " <> defaultPrettyMonthFormat (fromYearMonth ym)) (postsIx Ix.@+ [ym]) (monthRoot ym)
  return postsIx

sitemapRules :: MonadSB r m => PostSet -> Path Rel File -> m ()
sitemapRules xs out = cacheAction ("sitemap" :: T.Text, out) $
  LBS.writeFile (toFilePath $ outputDir </> out) $ renderSitemap $ Sitemap $ asSitemapUrl baseUrl <$> Ix.toList xs

buildRules :: MonadSB r m => m ()
buildRules = do
  xs <- postRules sourceDir ["posts/*.md"]
  mainPageRules xs
  docsRules sourceDir tableOfContents
  sitemapRules xs $(mkRelFile "sitemap.xml")

tests :: [FilePath] -> TestTree
tests xs = testGroup "Rendering Tests" $
  map (\x -> goldenVsFile x x (replace "golden" "public" x) (return ())) xs where
    replace fr to' = intercalate to' . splitOn fr

main :: IO ()
main = do
  let shOpts = shakeOptions { shakeLintInside = ["\\"]}
  runLoggedShakeForward shOpts buildRules
  findByExtension [".html", ".xml"] "test/golden" >>= defaultMain . tests