packages feed

slab-0.0.1.0: src/Slab/PreProcess.hs

{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE RecordWildCards #-}

module Slab.PreProcess
  ( Context (..)
  , preprocessFile
  , preprocessFileE
  ) where

import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Except (ExceptT, runExceptT, throwE)
import Data.Aeson qualified as Aeson
import Data.ByteString.Lazy qualified as BL
import Data.Text qualified as T
import Data.Text.IO qualified as T
import Slab.Error qualified as Error
import Slab.Parse qualified as Parse
import Slab.Syntax
import System.Directory (doesFileExist)
import System.FilePath (takeDirectory, takeExtension, (</>))

--------------------------------------------------------------------------------
data Context = Context
  { ctxStartPath :: FilePath
  }

--------------------------------------------------------------------------------

-- | Similar to `parseFile` but pre-process the include statements.
preprocessFile :: FilePath -> IO (Either Error.Error [Block])
preprocessFile = runExceptT . preprocessFileE

preprocessFileE :: FilePath -> ExceptT Error.Error IO [Block]
preprocessFileE path = do
  nodes <- Parse.parseFileE path
  let ctx =
        Context
          { ctxStartPath = path
          }
  preprocess ctx nodes

--------------------------------------------------------------------------------

-- Process include statements (i.e. read the given path and parse its content
-- recursively).
preprocess :: Context -> [Block] -> ExceptT Error.Error IO [Block]
preprocess ctx nodes = mapM (preproc ctx) nodes

preproc :: Context -> Block -> ExceptT Error.Error IO Block
preproc ctx@Context {..} = \case
  node@BlockDoctype -> pure node
  BlockElem name mdot attrs nodes -> do
    nodes' <- preprocess ctx nodes
    pure $ BlockElem name mdot attrs nodes'
  node@(BlockText _ _) -> pure node
  BlockInclude mname path _ -> do
    let includedPath = takeDirectory ctxStartPath </> path
        slabExt = takeExtension includedPath == ".slab"
    exists <- liftIO $ doesFileExist includedPath
    if
        | exists && (not slabExt || mname == Just "escape-html") -> do
            -- Include the file content as-is.
            content <- liftIO $ T.readFile includedPath
            let node = Parse.parserTextInclude content
            pure $ BlockInclude mname path (Just [node])
        | exists -> do
            -- Parse and process the .slab file.
            nodes' <- preprocessFileE includedPath
            pure $ BlockInclude mname path (Just nodes')
        | otherwise ->
            throwE $ Error.PreProcessError $ "File " <> T.pack includedPath <> " doesn't exist"
  BlockFragmentDef name params nodes -> do
    nodes' <- preprocess ctx nodes
    pure $ BlockFragmentDef name params nodes'
  BlockFragmentCall name values nodes -> do
    nodes' <- preprocess ctx nodes
    pure $ BlockFragmentCall name values nodes'
  node@(BlockFor _ _ _ _) -> pure node
  node@(BlockComment _ _) -> pure node
  node@(BlockFilter _ _) -> pure node
  node@(BlockRawElem _ _) -> pure node
  BlockDefault name nodes -> do
    nodes' <- preprocess ctx nodes
    pure $ BlockDefault name nodes'
  BlockImport path _ args -> do
    -- An import is treated like an include used to define a fragment, then
    -- directly calling that fragment.
    let includedPath = takeDirectory ctxStartPath </> path
        slabExt = takeExtension includedPath == ".slab"
    exists <- liftIO $ doesFileExist includedPath
    if
        | exists && not slabExt ->
            throwE $ Error.PreProcessError $ "Extends requires a .slab file"
        | exists -> do
            -- Parse and process the .slab file.
            body <- preprocessFileE includedPath
            args' <- mapM (preproc ctx) args
            pure $ BlockImport path (Just body) args'
        | otherwise ->
            throwE $ Error.PreProcessError $ "File " <> T.pack includedPath <> " doesn't exist"
  node@(BlockRun _ _) -> pure node
  BlockReadJson name path _ -> do
    let path' = takeDirectory ctxStartPath </> path
    content <- liftIO $ BL.readFile path'
    case Aeson.eitherDecode content of
      Right val ->
        pure $ BlockReadJson name path $ Just val
      Left err ->
        throwE $ Error.PreProcessError $ "Can't decode JSON: " <> T.pack err
  node@(BlockAssignVar _ _) -> pure node
  BlockIf cond as bs -> do
    -- File inclusion is done right away, without checking the condition.
    as' <- preprocess ctx as
    bs' <- preprocess ctx bs
    pure $ BlockIf cond as' bs'
  BlockList nodes -> do
    nodes' <- preprocess ctx nodes
    pure $ BlockList nodes'
  node@(BlockCode _) -> pure node