1 {-# LANGUAGE DeriveFunctor #-}
3 module Webc.Compiler where
5 import Control.Applicative (Applicative (..))
6 import Control.Monad (Monad (..), forM_, when)
8 import Data.ByteString.Lazy qualified as BSL
9 import Data.Either (Either (..))
10 import Data.Eq (Eq (..))
11 import Data.Foldable (null, toList)
12 import Data.Function (($), (.))
13 import Data.Functor (Functor (..), (<$>))
14 import Data.Maybe (Maybe (..))
15 import Data.Ord (Ord (..))
16 import Data.Semigroup (Semigroup (..))
17 import Data.Text qualified as Text
18 import GHC.Base (error)
19 import GHC.Stack (HasCallStack)
20 import Network.URI.Slug (Slug)
21 import Network.URI.Slug qualified as URI
27 import Symantic.Classes (Iso (..), IsoFunctor (..))
28 import System.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExist, doesFileExist, doesPathExist {-, getCurrentDirectory-})
29 import System.FilePath (FilePath, takeDirectory, (</>))
30 import System.FilePath qualified as FilePath
31 import System.FilePattern.Directory (getDirectoryFiles)
32 import System.IO (IO, hPutStrLn, stderr)
33 import Text.Show (Show (..))
34 import Webc.Classes hiding ((</>))
35 import Webc.MIME (PlainText)
37 -- * The 'Compiler' interpreter
39 newtype Compiler a = Compiler
40 { unCompiler :: [Comp a]
42 deriving (Functor, Show)
44 instance Applicative Compiler where
45 pure = Compiler . pure . pure
46 Compiler f <*> Compiler x = Compiler $ (<*>) <$> f <*> x
48 -- instance Monad Compiler where
50 -- Compiler x >>= f = Compiler (x >>=)
52 data CompilerConf = CompilerConf
53 { compilerConfSource :: FilePath
54 , compilerConfDest :: FilePath
57 compile :: Show a => Renderable a => Compiler a -> CompilerConf -> IO ()
58 compile comp CompilerConf{..} = do
59 createDirectoryIfMissing True compilerConfDest
60 let routes = unCompiler comp
62 error "no routes, nothing to compile"
63 forM_ routes $ \c@Comp{..} -> do
64 hPutStrLn stderr $ "route: " <> show c
65 let routePath = pathOfSlugs compSlugs
68 doesPathExist (compilerConfSource </> staticPath) >>= \case
70 -- TODO: In current branch, we don't expect this to be a directory.
71 -- Although the user may pass it, but review before merge.
75 ( (compilerConfSource </> staticPath)
76 , (compilerConfDest </> staticPath)
80 (compilerConfSource </> staticPath)
81 (compilerConfDest </> staticPath)
84 -- log LevelWarn $ toText $ "? " <> staticPath <> " (missing)"
86 hPutStrLn stderr $ "mkdir: " <> show (takeDirectory (compilerConfDest </> routePath))
87 createDirectoryIfMissing True (takeDirectory (compilerConfDest </> routePath))
88 hPutStrLn stderr $ "write: " <> show (compilerConfDest </> routePath FilePath.<.> ext)
89 BSL.writeFile (compilerConfDest </> routePath FilePath.<.> ext) bs
91 -- ** Class 'Renderable'
92 class Renderable a where
93 render :: Comp a -> Either FilePath (BSL.ByteString, FilePath)
94 instance Renderable () where
95 render Comp{..} = Left $ pathOfSlugs compSlugs
97 pathOfSlugs :: [Slug] -> FilePath
98 pathOfSlugs s = Text.unpack $ Text.intercalate "/" $ URI.encodeSlug <$> s
102 { compSlugs :: [Slug] -- TODO: Endo? Seq?
104 , compExt :: Maybe FilePath
106 deriving (Eq, Ord, Show, Functor)
107 instance Applicative Comp where
116 { compSlugs = compSlugs f <> compSlugs x
117 , compValue = compValue f (compValue x)
118 , compExt = compExt f <> compExt x
121 instance IsoFunctor Compiler where
122 (<%>) Iso{..} = (a2b <$>)
123 instance ProductFunctor Compiler where
127 instance SumFunctor Compiler where
131 ((Left <$>) <$> unCompiler x)
132 ((Right <$>) <$> unCompiler y)
133 instance Optionable Compiler where
136 Comp{compSlugs = [], compValue = Nothing, compExt = Nothing} :
137 ((Just <$>) <$> unCompiler x)
138 instance Slugable Compiler where
139 literalSlug s = Compiler [Comp{compSlugs = [s], compValue = (), compExt = Nothing}]
142 [ Comp{compSlugs = [s], compValue = s, compExt = Nothing}
145 instance ContentTypeable PlainText () Compiler where
151 , compExt = Just "txt"
155 -- instance Repeatable Compiler where
156 -- many0 (Compiler x) =
158 -- ((\Comp{} -> Comp [] []) <$> x)
159 -- <> ((\(Comp s a) -> Comp s [a]) <$> x)
160 -- <> ((\(Comp s a) -> Comp (s <> s) [a, a]) <$> x)
161 -- many1 (Compiler x) =
163 -- ((\(Comp s a) -> Comp s [a]) <$> x)
164 -- <> ((\(Comp s a) -> Comp (s <> s) [a, a]) <$> x)
165 -- instance Endable Compiler where
166 -- end = Compiler [Comp [] ()]
167 -- instance Capturable Compiler where
168 -- captureSlug n = Compiler $ [Comp [n] n]
169 -- instance Constantable c Compiler where
173 let (staticPaths, generatedPaths) =
176 case render model r of
177 AssetStatic fp -> Left (r, fp)
178 AssetComperated _fmt s -> Right (encodeRoute model r, s)
179 paths <- forM generatedPaths $ \(relPath, !s) -> do
180 let fp = dest </> relPath
181 log LevelInfo $ toText $ "W " <> fp
183 createDirectoryIfMissing True (takeDirectory fp)
186 forM_ staticPaths $ \(r, staticPath) -> do
187 liftIO (doesPathExist staticPath) >>= \case
189 -- TODO: In current branch, we don't expect this to be a directory.
190 -- Although the user may pass it, but review before merge.
191 copyDirRecursively (encodeRoute model r) staticPath dest
193 log LevelWarn $ toText $ "? " <> staticPath <> " (missing)"
196 instance Slugable Compiler where
197 literalSlug s = Compiler $ return $ Endo (s :)
198 captureSlug _n = Compiler $ MT.ReaderT $ \s -> return $ Endo (s :)
199 instance ProductFunctor Compiler where
200 Compiler x <.> Compiler y = Compiler $
202 MT.withReaderT (\env -> env{compilerArgs = b2a (compilerArgs env)})
208 --deriving stock (Eq, Show, Ord, Comperic)
212 safeForLarge' :: Int -> IO ()
213 safeForLarge' n = flip finally (cleanup tmpfile) $ do
215 lgrset <- newFileLoggerSet defaultBufSize tmpfile
216 let xs = toLogStr $ BS.pack $ take (abs n) (cycle ['a'..'z'])
218 pushLogStr lgrset $ xs <> lf
221 bs <- BS.readFile tmpfile
222 bs `shouldBe` BS.pack (take (abs n) (cycle ['a'..'z']) <> "x")
224 tmpfile = "test/temp"
226 cleanup :: FilePath -> IO ()
228 exist <- doesFileExist file
229 when exist $ removeFile file
232 logAllMsgs = logAll "LICENSE" `finally` cleanup tmpfile
234 tmpfile = "test/temp"
237 lgrset <- newFileLoggerSet 512 tmpfile
238 src <- BS.readFile file
239 let bs = (<> "\n") . toLogStr <$> BS.lines src
240 mapM_ (pushLogStr lgrset) bs
243 dst <- BS.readFile tmpfile
247 forall model m route.
256 (model -> route -> Asset BSL.ByteString) ->
257 -- | List of generated files.
259 compile dest model render = do
260 dirExists <- doesDirectoryExist dest
261 when (not dirExists) $ do
262 error $ "Destination does not exist: " <> dest
263 let routes = allRoutes model
265 error "allRoutes is empty; nothing to compile"
266 -- log LevelInfo $ "Writing " <> show (length routes) <> " routes"
268 (<$> routes) $ \route ->
269 case render model route of
270 AssetStatic fp -> Left (route, fp)
271 AssetComperated _fmt s -> Right (encodeRoute model route, s)
272 forM_ staticPaths $ \(route, staticPath) -> do
273 (doesPathExist staticPath) >>= \case
275 -- TODO: In current branch, we don't expect this to be a directory.
276 -- Although the user may pass it, but review before merge.
277 copyDirRecursively (encodeRoute model route) staticPath dest
280 -- log LevelWarn $ toText $ "? " <> staticPath <> " (missing)"
282 log :: MonadLogger m => LogLevel -> Text -> m ()
283 log = logWithoutLoc "Comperate"
285 -- | Disable birdbrained hacks from GitHub to disable surprises like,
286 -- https://github.com/jekyll/jekyll/issues/55
287 noBirdbrainedJekyll :: (MonadIO m, MonadLoggerIO m) => FilePath -> m ()
288 noBirdbrainedJekyll dest = do
289 let nojekyll = dest </> ".nojekyll"
290 liftIO (doesFileExist nojekyll) >>= \case
293 log LevelInfo $ "Disabling Jekyll by writing " <> toText nojekyll
294 writeFileLBS nojekyll ""
296 newtype StaticAssetMissing = StaticAssetMissing FilePath
297 deriving stock (Show)
298 deriving anyclass (Exception)
301 copyDirRecursively ::
307 -- | Source file path relative to CWD
309 -- | Absolute path to source file to copy.
311 -- | Directory *under* which the source file/dir will be copied
314 copyDirRecursively srcRel srcAbs destParent = do
315 doesFileExist srcAbs >>= \case
317 let b = destParent </> srcRel
318 --log LevelInfo $ toText $ "C " <> b
319 copyFileCreatingParents srcAbs b
321 doesDirectoryExist srcAbs >>= \case
324 -- throw $ StaticAssetMissing srcAbs
326 fs <- getDirectoryFiles srcAbs ["**"]
328 let a = srcAbs </> fp
329 b = destParent </> srcRel </> fp
330 -- log LevelInfo $ toText $ "C " <> b
331 copyFileCreatingParents a b
333 copyFileCreatingParents a b = do
334 createDirectoryIfMissing True (takeDirectory b)