+{-# LANGUAGE DeriveFunctor #-}
+
+module Webc.Compiler where
+
+import Control.Applicative (Applicative (..))
+import Control.Monad (Monad (..), forM_, when)
+import Data.Bool
+import Data.ByteString.Lazy qualified as BSL
+import Data.Either (Either (..))
+import Data.Eq (Eq (..))
+import Data.Foldable (null, toList)
+import Data.Function (($), (.))
+import Data.Functor (Functor (..), (<$>))
+import Data.Maybe (Maybe (..))
+import Data.Ord (Ord (..))
+import Data.Semigroup (Semigroup (..))
+import Data.Text qualified as Text
+import GHC.Base (error)
+import GHC.Stack (HasCallStack)
+import Network.URI.Slug (Slug)
+import Network.URI.Slug qualified as URI
+import Symantic (
+ Optionable (..),
+ ProductFunctor (..),
+ SumFunctor (..),
+ )
+import Symantic.Classes (Iso (..), IsoFunctor (..))
+import System.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExist, doesFileExist, doesPathExist {-, getCurrentDirectory-})
+import System.FilePath (FilePath, takeDirectory, (</>))
+import System.FilePath qualified as FilePath
+import System.FilePattern.Directory (getDirectoryFiles)
+import System.IO (IO, hPutStrLn, stderr)
+import Text.Show (Show (..))
+import Webc.Classes hiding ((</>))
+import Webc.MIME (PlainText)
+
+-- * The 'Compiler' interpreter
+
+newtype Compiler a = Compiler
+ { unCompiler :: [Comp a]
+ }
+ deriving (Functor, Show)
+
+instance Applicative Compiler where
+ pure = Compiler . pure . pure
+ Compiler f <*> Compiler x = Compiler $ (<*>) <$> f <*> x
+
+-- instance Monad Compiler where
+-- return = pure
+-- Compiler x >>= f = Compiler (x >>=)
+
+data CompilerConf = CompilerConf
+ { compilerConfSource :: FilePath
+ , compilerConfDest :: FilePath
+ }
+
+compile :: Show a => Renderable a => Compiler a -> CompilerConf -> IO ()
+compile comp CompilerConf{..} = do
+ createDirectoryIfMissing True compilerConfDest
+ let routes = unCompiler comp
+ when (null routes) $
+ error "no routes, nothing to compile"
+ forM_ routes $ \c@Comp{..} -> do
+ hPutStrLn stderr $ "route: " <> show c
+ let routePath = pathOfSlugs compSlugs
+ in case render c of
+ Left staticPath ->
+ doesPathExist (compilerConfSource </> staticPath) >>= \case
+ True -> do
+ -- TODO: In current branch, we don't expect this to be a directory.
+ -- Although the user may pass it, but review before merge.
+ hPutStrLn stderr $
+ "staticCopy: "
+ <> show
+ ( (compilerConfSource </> staticPath)
+ , (compilerConfDest </> staticPath)
+ )
+ copyDirRecursively
+ routePath
+ (compilerConfSource </> staticPath)
+ (compilerConfDest </> staticPath)
+ False ->
+ return ()
+ -- log LevelWarn $ toText $ "? " <> staticPath <> " (missing)"
+ Right (bs, ext) -> do
+ hPutStrLn stderr $ "mkdir: " <> show (takeDirectory (compilerConfDest </> routePath))
+ createDirectoryIfMissing True (takeDirectory (compilerConfDest </> routePath))
+ hPutStrLn stderr $ "write: " <> show (compilerConfDest </> routePath FilePath.<.> ext)
+ BSL.writeFile (compilerConfDest </> routePath FilePath.<.> ext) bs
+
+-- ** Class 'Renderable'
+class Renderable a where
+ render :: Comp a -> Either FilePath (BSL.ByteString, FilePath)
+instance Renderable () where
+ render Comp{..} = Left $ pathOfSlugs compSlugs
+
+pathOfSlugs :: [Slug] -> FilePath
+pathOfSlugs s = Text.unpack $ Text.intercalate "/" $ URI.encodeSlug <$> s
+
+-- ** Type 'Comp'
+data Comp a = Comp
+ { compSlugs :: [Slug] -- TODO: Endo? Seq?
+ , compValue :: a
+ , compExt :: Maybe FilePath
+ }
+ deriving (Eq, Ord, Show, Functor)
+instance Applicative Comp where
+ pure compValue =
+ Comp
+ { compSlugs = []
+ , compValue
+ , compExt = Nothing
+ }
+ f <*> x =
+ Comp
+ { compSlugs = compSlugs f <> compSlugs x
+ , compValue = compValue f (compValue x)
+ , compExt = compExt f <> compExt x
+ }
+
+instance IsoFunctor Compiler where
+ (<%>) Iso{..} = (a2b <$>)
+instance ProductFunctor Compiler where
+ (<.>) = liftA2 (,)
+ (<.) = (<*)
+ (.>) = (*>)
+instance SumFunctor Compiler where
+ x <+> y =
+ Compiler $
+ (<>)
+ ((Left <$>) <$> unCompiler x)
+ ((Right <$>) <$> unCompiler y)
+instance Optionable Compiler where
+ optional x =
+ Compiler $
+ Comp{compSlugs = [], compValue = Nothing, compExt = Nothing} :
+ ((Just <$>) <$> unCompiler x)
+instance Slugable Compiler where
+ literalSlug s = Compiler [Comp{compSlugs = [s], compValue = (), compExt = Nothing}]
+ chooseSlug ss =
+ Compiler $
+ [ Comp{compSlugs = [s], compValue = s, compExt = Nothing}
+ | s <- toList ss
+ ]
+instance ContentTypeable PlainText () Compiler where
+ contentType =
+ Compiler
+ [ Comp
+ { compSlugs = []
+ , compValue = ()
+ , compExt = Just "txt"
+ }
+ ]
+
+-- instance Repeatable Compiler where
+-- many0 (Compiler x) =
+-- Compiler $
+-- ((\Comp{} -> Comp [] []) <$> x)
+-- <> ((\(Comp s a) -> Comp s [a]) <$> x)
+-- <> ((\(Comp s a) -> Comp (s <> s) [a, a]) <$> x)
+-- many1 (Compiler x) =
+-- Compiler $
+-- ((\(Comp s a) -> Comp s [a]) <$> x)
+-- <> ((\(Comp s a) -> Comp (s <> s) [a, a]) <$> x)
+-- instance Endable Compiler where
+-- end = Compiler [Comp [] ()]
+-- instance Capturable Compiler where
+-- captureSlug n = Compiler $ [Comp [n] n]
+-- instance Constantable c Compiler where
+-- constant = pure
+
+{-
+let (staticPaths, generatedPaths) =
+ lefts &&& rights $
+ routes <&> \r ->
+ case render model r of
+ AssetStatic fp -> Left (r, fp)
+ AssetComperated _fmt s -> Right (encodeRoute model r, s)
+paths <- forM generatedPaths $ \(relPath, !s) -> do
+ let fp = dest </> relPath
+ log LevelInfo $ toText $ "W " <> fp
+ liftIO $ do
+ createDirectoryIfMissing True (takeDirectory fp)
+ writeFileLBS fp s
+ pure fp
+forM_ staticPaths $ \(r, staticPath) -> do
+ liftIO (doesPathExist staticPath) >>= \case
+ True ->
+ -- TODO: In current branch, we don't expect this to be a directory.
+ -- Although the user may pass it, but review before merge.
+ copyDirRecursively (encodeRoute model r) staticPath dest
+ False ->
+ log LevelWarn $ toText $ "? " <> staticPath <> " (missing)"
+-}
+{-
+instance Slugable Compiler where
+ literalSlug s = Compiler $ return $ Endo (s :)
+ captureSlug _n = Compiler $ MT.ReaderT $ \s -> return $ Endo (s :)
+instance ProductFunctor Compiler where
+ Compiler x <.> Compiler y = Compiler $
+ MT.ReaderT $ \env ->
+ MT.withReaderT (\env -> env{compilerArgs = b2a (compilerArgs env)})
+ return $
+ MT.runReaderT x a
+ <> MT.runReaderT y b
+-}
+
+--deriving stock (Eq, Show, Ord, Comperic)
+
+{-
+
+safeForLarge' :: Int -> IO ()
+safeForLarge' n = flip finally (cleanup tmpfile) $ do
+ cleanup tmpfile
+ lgrset <- newFileLoggerSet defaultBufSize tmpfile
+ let xs = toLogStr $ BS.pack $ take (abs n) (cycle ['a'..'z'])
+ lf = "x"
+ pushLogStr lgrset $ xs <> lf
+ flushLogStr lgrset
+ rmLoggerSet lgrset
+ bs <- BS.readFile tmpfile
+ bs `shouldBe` BS.pack (take (abs n) (cycle ['a'..'z']) <> "x")
+ where
+ tmpfile = "test/temp"
+
+cleanup :: FilePath -> IO ()
+cleanup file = do
+ exist <- doesFileExist file
+ when exist $ removeFile file
+
+logAllMsgs :: IO ()
+logAllMsgs = logAll "LICENSE" `finally` cleanup tmpfile
+ where
+ tmpfile = "test/temp"
+ logAll file = do
+ cleanup tmpfile
+ lgrset <- newFileLoggerSet 512 tmpfile
+ src <- BS.readFile file
+ let bs = (<> "\n") . toLogStr <$> BS.lines src
+ mapM_ (pushLogStr lgrset) bs
+ flushLogStr lgrset
+ rmLoggerSet lgrset
+ dst <- BS.readFile tmpfile
+ dst `shouldBe` src
+
+compile ::
+ forall model m route.
+ ( -- MonadIO m,
+ -- MonadUnliftIO m,
+ -- MonadLoggerIO m,
+ -- Show r,
+ HasCallStack
+ ) =>
+ FilePath ->
+ model ->
+ (model -> route -> Asset BSL.ByteString) ->
+ -- | List of generated files.
+ IO ()
+compile dest model render = do
+ dirExists <- doesDirectoryExist dest
+ when (not dirExists) $ do
+ error $ "Destination does not exist: " <> dest
+ let routes = allRoutes model
+ when (null routes) $
+ error "allRoutes is empty; nothing to compile"
+ -- log LevelInfo $ "Writing " <> show (length routes) <> " routes"
+ let staticPaths =
+ (<$> routes) $ \route ->
+ case render model route of
+ AssetStatic fp -> Left (route, fp)
+ AssetComperated _fmt s -> Right (encodeRoute model route, s)
+ forM_ staticPaths $ \(route, staticPath) -> do
+ (doesPathExist staticPath) >>= \case
+ True ->
+ -- TODO: In current branch, we don't expect this to be a directory.
+ -- Although the user may pass it, but review before merge.
+ copyDirRecursively (encodeRoute model route) staticPath dest
+ False ->
+ return ()
+ -- log LevelWarn $ toText $ "? " <> staticPath <> " (missing)"
+
+log :: MonadLogger m => LogLevel -> Text -> m ()
+log = logWithoutLoc "Comperate"
+
+-- | Disable birdbrained hacks from GitHub to disable surprises like,
+-- https://github.com/jekyll/jekyll/issues/55
+noBirdbrainedJekyll :: (MonadIO m, MonadLoggerIO m) => FilePath -> m ()
+noBirdbrainedJekyll dest = do
+ let nojekyll = dest </> ".nojekyll"
+ liftIO (doesFileExist nojekyll) >>= \case
+ True -> pure ()
+ False -> do
+ log LevelInfo $ "Disabling Jekyll by writing " <> toText nojekyll
+ writeFileLBS nojekyll ""
+
+newtype StaticAssetMissing = StaticAssetMissing FilePath
+ deriving stock (Show)
+ deriving anyclass (Exception)
+
+-}
+copyDirRecursively ::
+ ( --MonadIO m,
+ --MonadUnliftIO m,
+ --MonadLoggerIO m,
+ HasCallStack
+ ) =>
+ -- | Source file path relative to CWD
+ FilePath ->
+ -- | Absolute path to source file to copy.
+ FilePath ->
+ -- | Directory *under* which the source file/dir will be copied
+ FilePath ->
+ IO ()
+copyDirRecursively srcRel srcAbs destParent = do
+ doesFileExist srcAbs >>= \case
+ True -> do
+ let b = destParent </> srcRel
+ --log LevelInfo $ toText $ "C " <> b
+ copyFileCreatingParents srcAbs b
+ False ->
+ doesDirectoryExist srcAbs >>= \case
+ False ->
+ return ()
+ -- throw $ StaticAssetMissing srcAbs
+ True -> do
+ fs <- getDirectoryFiles srcAbs ["**"]
+ forM_ fs $ \fp -> do
+ let a = srcAbs </> fp
+ b = destParent </> srcRel </> fp
+ -- log LevelInfo $ toText $ "C " <> b
+ copyFileCreatingParents a b
+ where
+ copyFileCreatingParents a b = do
+ createDirectoryIfMissing True (takeDirectory b)
+ copyFile a b