packages feed

slab 0.0.2.0 → 0.0.3.0

raw patch · 15 files changed

+656/−227 lines, 15 filesdep +servant-websocketsdep +websocketsPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: servant-websockets, websockets

API changes (from Hackage documentation)

- Slab.Command: Report :: FilePath -> Command
- Slab.Report: run :: FilePath -> IO ()
- Slab.Syntax: BlockAssignVar :: Text -> Expr -> Block
- Slab.Syntax: BlockReadJson :: Text -> FilePath -> Maybe Value -> Block
+ Slab.Command: ReportHeadings :: FilePath -> Command
+ Slab.Command: ReportPages :: FilePath -> Command
+ Slab.Command: RunNormal :: RunMode
+ Slab.Command: RunPassthrough :: RunMode
+ Slab.Command: data RunMode
+ Slab.Execute: Context :: FilePath -> RunMode -> Context
+ Slab.Execute: [ctxPath] :: Context -> FilePath
+ Slab.Execute: [ctxRunMode] :: Context -> RunMode
+ Slab.Execute: data Context
+ Slab.Render: extractTexts :: [Block] -> Text
+ Slab.Report: instance GHC.Classes.Eq Slab.Report.Heading
+ Slab.Report: instance GHC.Show.Show Slab.Report.Heading
+ Slab.Report: reportHeadings :: FilePath -> IO ()
+ Slab.Report: reportPages :: FilePath -> IO ()
+ Slab.Syntax: Arg :: Expr -> Attr
+ Slab.Syntax: BlockAssignVars :: [(Text, Expr)] -> Block
+ Slab.Syntax: DefinitionArg :: DefinitionUse
+ Slab.Syntax: DefinitionNormal :: DefinitionUse
+ Slab.Syntax: Em :: Elem
+ Slab.Syntax: JsonPath :: FilePath -> Expr
+ Slab.Syntax: addScript :: Text -> [Block] -> [Block]
+ Slab.Syntax: data DefinitionUse
+ Slab.Syntax: idNamesFromAttrs' :: [Attr] -> Maybe Text
+ Slab.Syntax: instance GHC.Classes.Eq Slab.Syntax.DefinitionUse
+ Slab.Syntax: instance GHC.Show.Show Slab.Syntax.DefinitionUse
+ Slab.Syntax: splitAttrsAndArgs :: [Attr] -> ([Attr], [Expr])
- Slab.Build: buildDir :: FilePath -> RenderMode -> FilePath -> IO ()
+ Slab.Build: buildDir :: FilePath -> RenderMode -> RunMode -> FilePath -> IO ()
- Slab.Build: buildDirInMemory :: FilePath -> RenderMode -> StmStore -> IO ()
+ Slab.Build: buildDirInMemory :: FilePath -> RenderMode -> RunMode -> StmStore -> IO ()
- Slab.Build: buildFile :: FilePath -> RenderMode -> FilePath -> FilePath -> IO ()
+ Slab.Build: buildFile :: FilePath -> RenderMode -> RunMode -> FilePath -> FilePath -> IO ()
- Slab.Build: buildFileInMemory :: FilePath -> RenderMode -> StmStore -> FilePath -> IO ()
+ Slab.Build: buildFileInMemory :: FilePath -> RenderMode -> RunMode -> StmStore -> FilePath -> IO ()
- Slab.Command: Build :: FilePath -> RenderMode -> FilePath -> Command
+ Slab.Command: Build :: FilePath -> RenderMode -> RunMode -> FilePath -> Command
- Slab.Command: Execute :: CommandWithPath
+ Slab.Command: Execute :: Bool -> RunMode -> CommandWithPath
- Slab.Command: Render :: RenderMode -> CommandWithPath
+ Slab.Command: Render :: RenderMode -> RunMode -> CommandWithPath
- Slab.Command: Watch :: FilePath -> RenderMode -> FilePath -> Command
+ Slab.Command: Watch :: FilePath -> RenderMode -> RunMode -> FilePath -> Command
- Slab.Execute: execute :: FilePath -> [Block] -> ExceptT Error IO [Block]
+ Slab.Execute: execute :: Context -> [Block] -> ExceptT Error IO [Block]
- Slab.Execute: executeFile :: FilePath -> IO (Either Error [Block])
+ Slab.Execute: executeFile :: Context -> IO (Either Error [Block])
- Slab.Syntax: Attr :: Text -> Maybe Expr -> Attr
+ Slab.Syntax: Attr :: Text -> Expr -> Attr
- Slab.Syntax: BlockFragmentDef :: Text -> [Text] -> [Block] -> Block
+ Slab.Syntax: BlockFragmentDef :: DefinitionUse -> Text -> [Text] -> [Block] -> Block
- Slab.Syntax: BlockRun :: Text -> Maybe [Block] -> Block
+ Slab.Syntax: BlockRun :: Text -> Maybe Text -> Maybe [Block] -> Block

Files

slab.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.2 name:               slab-version:            0.0.2.0+version:            0.0.3.0 license:            BSD-2-Clause license-file:       LICENSE copyright:          2024 Võ Minh Thu, Hypered SRL@@ -68,6 +68,7 @@     , servant     , servant-blaze     , servant-server+    , servant-websockets     , stm     , text     , transformers@@ -75,6 +76,7 @@     , wai     , wai-app-static     , warp+    , websockets   exposed-modules:     Slab.Build     Slab.Command
src/Slab/Build.hs view
@@ -34,19 +34,20 @@ import System.FilePath.Glob qualified as Glob  ---------------------------------------------------------------------------------buildDir :: FilePath -> Command.RenderMode -> FilePath -> IO ()-buildDir srcDir mode distDir = do+buildDir :: FilePath -> Command.RenderMode -> Command.RunMode -> FilePath -> IO ()+buildDir srcDir mode passthrough distDir = do   templates <- listTemplates srcDir-  mapM_ (buildFile srcDir mode distDir) templates+  mapM_ (buildFile srcDir mode passthrough distDir) templates -buildFile :: FilePath -> Command.RenderMode -> FilePath -> FilePath -> IO ()-buildFile srcDir mode distDir path = do+buildFile :: FilePath -> Command.RenderMode -> Command.RunMode -> FilePath -> FilePath -> IO ()+buildFile srcDir mode passthrough distDir path = do   let path' = distDir </> replaceExtension (makeRelative srcDir path) ".html"       dir' = takeDirectory path'+      ctx = Execute.Context path passthrough   putStrLn $ "Building " <> path' <> "..."   createDirectoryIfMissing True dir' -  nodes <- Execute.executeFile path >>= Error.unwrap+  nodes <- Execute.executeFile ctx >>= Error.unwrap   if Evaluate.simplify nodes == []     then putStrLn $ "No generated content for " <> path     else case mode of@@ -57,36 +58,39 @@  -------------------------------------------------------------------------------- -type Store = M.Map FilePath [Syntax.Block]+type Store = M.Map FilePath (Either Error.Error [Syntax.Block])  type StmStore = STM.TVar Store  -- | A version of `buildDir` that doesn't write files to disk, but instead -- record the generated `Syntax.Block`s in STM.-buildDirInMemory :: FilePath -> Command.RenderMode -> StmStore -> IO ()-buildDirInMemory srcDir mode store = do+buildDirInMemory :: FilePath -> Command.RenderMode -> Command.RunMode -> StmStore -> IO ()+buildDirInMemory srcDir mode passthrough store = do   templates <- listTemplates srcDir-  mapM_ (buildFileInMemory srcDir mode store) templates+  mapM_ (buildFileInMemory srcDir mode passthrough store) templates -buildFileInMemory :: FilePath -> Command.RenderMode -> StmStore -> FilePath -> IO ()-buildFileInMemory srcDir mode store path = do+buildFileInMemory :: FilePath -> Command.RenderMode -> Command.RunMode -> StmStore -> FilePath -> IO ()+buildFileInMemory srcDir mode passthrough store path = do   let path' = replaceExtension (makeRelative srcDir path) ".html"+      ctx = Execute.Context path passthrough   putStrLn $ "Building " <> path' <> "..." -  mnodes <- Execute.executeFile path+  mnodes <- Execute.executeFile ctx   case mnodes of     Right nodes ->       if Evaluate.simplify nodes == []         then putStrLn $ "No generated content for " <> path         else case mode of           Command.RenderNormal ->-            atomically $ STM.modifyTVar store (writeStore path' nodes)+            atomically $ STM.modifyTVar store (writeStore path' $ Right nodes)           Command.RenderPretty ->-            atomically $ STM.modifyTVar store (writeStore path' nodes)-    Left err -> Error.display err+            atomically $ STM.modifyTVar store (writeStore path' $ Right nodes)+    Left err -> do+      Error.display err+      atomically $ STM.modifyTVar store (writeStore path' $ Left err) -writeStore :: FilePath -> [Syntax.Block] -> Store -> Store-writeStore path blocks = M.insert path blocks+writeStore :: FilePath -> Either Error.Error [Syntax.Block] -> Store -> Store+writeStore path mblocks = M.insert path mblocks  -------------------------------------------------------------------------------- listTemplates :: FilePath -> IO [FilePath]
src/Slab/Command.hs view
@@ -15,6 +15,7 @@   , CommandWithPath (..)   , RenderMode (..)   , ParseMode (..)+  , RunMode (..)   , parserInfo   ) where @@ -24,19 +25,21 @@  -------------------------------------------------------------------------------- data Command-  = Build FilePath RenderMode FilePath-  | Watch FilePath RenderMode FilePath+  = Build FilePath RenderMode RunMode FilePath+  | Watch FilePath RenderMode RunMode FilePath   | Serve FilePath FilePath-  | Report FilePath+  | ReportPages FilePath+  | ReportHeadings FilePath   | -- | Generate code. Only Haskell for now.     Generate FilePath   | CommandWithPath FilePath ParseMode CommandWithPath  -- | Commands operating on a path. data CommandWithPath-  = Render RenderMode-  | Execute+  = Render RenderMode RunMode   | -- | If True, simplify the evaluated AST.+    Execute Bool RunMode+  | -- | If True, simplify the evaluated AST.     Evaluate Bool   | Parse   | -- | List the classes used in a template. TODO Later, we want to list (or create a tree) of extends/includes.@@ -52,6 +55,13 @@   | -- | Process the include statements, creating a complete template.     ParseDeep +data RunMode+  = -- | A failing external command fails the template.+    RunNormal+  | -- | A failing external command doesn't fail the template and its output is+    -- rendered in the template.+    RunPassthrough+ -------------------------------------------------------------------------------- parserInfo :: A.ParserInfo Command parserInfo =@@ -81,7 +91,7 @@           "serve"           ( A.info (parserServe <**> A.helper) $               A.progDesc-                "Watch and serve a library of Slab templates to HTML"+                "Watch and serve a library of Slab templates"           )         <> A.command           "report"@@ -146,6 +156,7 @@       RenderPretty       ( A.long "pretty" <> A.help "Use pretty-printing"       )+  passthrough <- parserPassthroughFlag   distDir <-     A.strOption       ( A.long "dist"@@ -154,7 +165,7 @@           <> A.help             "A destination directory for the generated HTML files."       )-  pure $ Build srcDir mode distDir+  pure $ Build srcDir mode passthrough distDir  parserServe :: A.Parser Command parserServe = do@@ -173,13 +184,35 @@   pure $ Serve srcDir distDir  parserReport :: A.Parser Command-parserReport = do+parserReport =+  A.subparser+    ( A.command+        "pages"+        ( A.info (parserReportPages <**> A.helper) $+            A.progDesc+              "Report pages found in a directory"+        )+        <> A.command+          "headings"+          ( A.info (parserReportHeadings <**> A.helper) $+              A.progDesc+                "Report the headings of a page"+          )+    )++parserReportPages :: A.Parser Command+parserReportPages = do   srcDir <-     A.argument       A.str       (A.metavar "DIR" <> A.action "file" <> A.help "Directory of Slab templates to analyse.")-  pure $ Report srcDir+  pure $ ReportPages srcDir +parserReportHeadings :: A.Parser Command+parserReportHeadings = do+  path <- parserTemplatePath+  pure $ ReportHeadings path+ parserWatch :: A.Parser Command parserWatch = do   srcDir <-@@ -192,6 +225,7 @@       RenderPretty       ( A.long "pretty" <> A.help "Use pretty-printing"       )+  passthrough <- parserPassthroughFlag   distDir <-     A.strOption       ( A.long "dist"@@ -200,12 +234,17 @@           <> A.help             "A destination directory for the generated HTML files."       )-  pure $ Watch srcDir mode distDir+  pure $ Watch srcDir mode passthrough distDir  parserExectue :: A.Parser Command parserExectue = do   pathAndmode <- parserWithPath-  pure $ uncurry CommandWithPath pathAndmode $ Execute+  simpl <-+    A.switch+      ( A.long "simplify" <> A.help "Simplify the AST"+      )+  passthrough <- parserPassthroughFlag+  pure $ uncurry CommandWithPath pathAndmode $ Execute simpl passthrough  parserRender :: A.Parser Command parserRender = do@@ -216,7 +255,8 @@       ( A.long "pretty" <> A.help "Use pretty-printing"       )   pathAndmode <- parserWithPath-  pure $ uncurry CommandWithPath pathAndmode $ Render mode+  passthrough <- parserPassthroughFlag+  pure $ uncurry CommandWithPath pathAndmode $ Render mode passthrough  parserEvaluate :: A.Parser Command parserEvaluate = do@@ -268,4 +308,13 @@     ParseDeep     ParseShallow     ( A.long "shallow" <> A.help "Don't parse recursively the included Slab files"+    )++--------------------------------------------------------------------------------+parserPassthroughFlag :: A.Parser RunMode+parserPassthroughFlag =+  A.flag+    RunNormal+    RunPassthrough+    ( A.long "passthrough" <> A.help "Allow external command failures"     )
src/Slab/Evaluate.hs view
@@ -24,13 +24,10 @@  import Control.Monad (forM) import Control.Monad.Trans.Except (ExceptT, runExceptT, throwE)-import Data.Aeson qualified as Aeson-import Data.Aeson.Key qualified as Aeson.Key-import Data.Aeson.KeyMap qualified as Aeson.KeyMap+import Data.List ((\\)) import Data.Maybe (isJust) import Data.Text (Text) import Data.Text qualified as T-import Data.Vector qualified as V import Slab.Error qualified as Error import Slab.PreProcess qualified as PreProcess import Slab.Syntax@@ -73,6 +70,7 @@     , mkElem "i" I     , mkElem "pre" Pre     , mkElem "p" P+    , mkElem "em" Em     , mkElem "ul" Ul     , mkElem "li" Li     , mkElem "title" Title@@ -135,8 +133,9 @@ eval env stack bl = case bl of   node@BlockDoctype -> pure node   BlockElem name mdot attrs nodes -> do+    attrs' <- evalAttrs env stack attrs     nodes' <- evaluate env stack nodes-    pure $ BlockElem name mdot attrs nodes'+    pure $ BlockElem name mdot attrs' nodes'   BlockText syn template -> do     template' <- evalTemplate env template     pure $ BlockText syn template'@@ -147,11 +146,12 @@         pure $ BlockInclude mname path (Just nodes')       Nothing ->         pure $ BlockInclude mname path Nothing-  node@(BlockFragmentDef _ _ _) -> pure node+  node@(BlockFragmentDef _ _ _ _) -> pure node   BlockFragmentCall name mdot attrs values args -> do+    attrs' <- evalAttrs env stack attrs     body <- call env stack name values args-    let body' = setAttrs attrs body-    pure $ BlockFragmentCall name mdot attrs values body'+    let body' = setAttrs attrs' body+    pure $ BlockFragmentCall name mdot attrs' values body'   BlockFor name mindex values nodes -> do     -- Re-use BlockFor to construct a single node to return.     let zero :: Int@@ -184,9 +184,8 @@   BlockImport path _ args -> do     body <- call env stack (T.pack path) [] args     pure $ BlockImport path (Just body) args-  node@(BlockRun _ _) -> pure node-  node@(BlockReadJson _ _ _) -> pure node-  node@(BlockAssignVar _ _) -> pure node+  node@(BlockRun _ _ _) -> pure node+  node@(BlockAssignVars _) -> pure node   BlockIf cond as bs -> do     cond' <- evalExpr env cond     case cond' of@@ -223,12 +222,29 @@ evalFrag :: Monad m => Env -> [Text] -> Text -> [Expr] -> [Block] -> Expr -> ExceptT Error.Error m [Block] evalFrag env stack name values args (Frag names capturedEnv body) = do   env' <- extractVariables' env args-  let env'' = augmentVariables capturedEnv env'+  case map fst env' \\ names of+    [] -> pure ()+    ["content"] -> pure ()+    ns -> throwE . Error.EvaluateError $+      "Unnecessary arguments to " <> name <> ": " <> T.pack (show ns)+  let env'' = augmentVariables (removeFormalParams names capturedEnv) env'       arguments = zip names (map (thunk env) values)       env''' = augmentVariables env'' arguments   body' <- evaluate env''' ("frag " <> name : stack) body   pure body' +removeFormalParams names Env {..} = Env { envVariables = vars' }+ where+  vars' = filter (not . (`elem` names) . fst) envVariables++evalAttrs :: Monad m => Env -> [Text] -> [Attr] -> ExceptT Error.Error m [Attr]+evalAttrs env stack attrs = mapM f attrs+ where+  f (Attr a b) = do+    b' <- evalExpr env b+    pure $ Attr a b'+  f attr = pure attr+ evalExpr :: Monad m => Env -> Expr -> ExceptT Error.Error m Expr evalExpr env = \case   Variable name ->@@ -361,9 +377,10 @@   let named = extractVariables env' nodes       unnamed = concatMap unnamedBlock nodes       content = if null unnamed then [] else [("content", Frag [] env' unnamed)]-      vars = named <> content       env' = augmentVariables env named -- Note we don't add the implicit "content" entry.-  if isJust (lookup "content" named) && not (null unnamed)+      args = extractArguments env' nodes+      vars = args <> content+  if isJust (lookup "content" args) && not (null unnamed)     then       throwE $         Error.EvaluateError $@@ -372,7 +389,7 @@  unnamedBlock :: Block -> [Block] unnamedBlock (BlockImport path _ args) = [BlockFragmentCall (T.pack path) NoSym [] [] args]-unnamedBlock (BlockFragmentDef _ _ _) = []+unnamedBlock (BlockFragmentDef DefinitionArg _ _ _) = [] unnamedBlock node = [node]  -- Extract both fragments and assignments.@@ -389,7 +406,10 @@   (BlockText _ _) -> []   (BlockInclude _ _ children) -> maybe [] (extractVariables env) children   (BlockFor _ _ _ _) -> []-  (BlockFragmentDef name names children) -> [(name, Frag names env children)]+  (BlockFragmentDef DefinitionNormal name names children) ->+    [(name, Frag names env children)]+  (BlockFragmentDef DefinitionArg name names children) ->+    []   (BlockFragmentCall _ _ _ _ _) -> []   (BlockComment _ _) -> []   (BlockFilter _ _) -> []@@ -397,24 +417,39 @@   (BlockDefault _ _) -> []   (BlockImport path (Just body) _) -> [(T.pack path, Frag [] env body)]   (BlockImport _ _ _) -> []-  (BlockRun _ _) -> []-  (BlockReadJson name _ (Just val)) -> [(name, jsonToExpr val)]-  (BlockReadJson _ _ Nothing) -> []-  (BlockAssignVar name val) -> [(name, val)]+  (BlockRun _ _ _) -> []+  (BlockAssignVars pairs) -> pairs   (BlockIf _ _ _) -> []   (BlockList _) -> []   (BlockCode _) -> [] -jsonToExpr :: Aeson.Value -> Expr-jsonToExpr = \case-  Aeson.String s -> SingleQuoteString s-  Aeson.Array xs ->-    List $ map jsonToExpr (V.toList xs)-  Aeson.Object kvs ->-    let f (k, v) = (SingleQuoteString $ Aeson.Key.toText k, jsonToExpr v)-     in Object $ map f (Aeson.KeyMap.toList kvs)-  x -> error $ "jsonToExpr: " <> show x+-- Extract fragments used as arguments of fragment calls.+extractArguments :: Env -> [Block] -> [(Text, Expr)]+extractArguments env = concatMap (extractArgument env) +extractArgument :: Env -> Block -> [(Text, Expr)]+extractArgument env = \case+  BlockDoctype -> []+  (BlockElem _ _ _ _) -> []+  (BlockText _ _) -> []+  (BlockInclude _ _ _) -> []+  (BlockFor _ _ _ _) -> []+  (BlockFragmentDef DefinitionNormal _ _ _) ->+    []+  (BlockFragmentDef DefinitionArg name names children) ->+    [(name, Frag names env children)]+  (BlockFragmentCall _ _ _ _ _) -> []+  (BlockComment _ _) -> []+  (BlockFilter _ _) -> []+  (BlockRawElem _ _) -> []+  (BlockDefault _ _) -> []+  (BlockImport _ _ _) -> []+  (BlockRun _ _ _) -> []+  (BlockAssignVars _) -> []+  (BlockIf _ _ _) -> []+  (BlockList _) -> []+  (BlockCode _) -> []+ -------------------------------------------------------------------------------- simplify :: [Block] -> [Block] simplify = concatMap simplify'@@ -424,8 +459,8 @@   node@BlockDoctype -> [node]   BlockElem name mdot attrs nodes -> [BlockElem name mdot attrs $ simplify nodes]   node@(BlockText _ _) -> [node]-  BlockInclude _ _ mnodes -> maybe [] simplify mnodes-  BlockFragmentDef _ _ _ -> []+  BlockInclude mfilter path mnodes -> [BlockInclude mfilter path $ simplify <$> mnodes]+  BlockFragmentDef _ _ _ _ -> []   BlockFragmentCall _ _ _ _ args -> simplify args   BlockFor _ _ _ nodes -> simplify nodes   node@(BlockComment _ _) -> [node]@@ -433,9 +468,8 @@   node@(BlockRawElem _ _) -> [node]   BlockDefault _ nodes -> simplify nodes   BlockImport _ mbody _ -> maybe [] simplify mbody-  BlockRun _ mbody -> maybe [] simplify mbody-  BlockReadJson _ _ _ -> []-  BlockAssignVar _ _ -> []+  BlockRun _ _ mbody -> maybe [] simplify mbody+  BlockAssignVars _ -> []   BlockIf _ [] bs -> simplify bs   BlockIf _ as _ -> simplify as   BlockList nodes -> simplify nodes
src/Slab/Execute.hs view
@@ -12,38 +12,45 @@ -- After execution, the resulting blocks can be rendered to HTML by -- "Slab.Render". module Slab.Execute-  ( executeFile+  ( Context (..)+  , executeFile   , execute   ) where  import Control.Monad.IO.Class (liftIO)-import Control.Monad.Trans.Except (ExceptT, runExceptT)+import Control.Monad.Trans.Except (ExceptT, runExceptT, throwE) import Data.Text qualified as T+import Slab.Command qualified as Command import Slab.Error qualified as Error import Slab.Evaluate qualified as Evaluate import Slab.PreProcess qualified as PreProcess import Slab.Syntax qualified as Syntax-import System.Process (cwd, readCreateProcess, shell)+import System.Exit (ExitCode (..))+import System.Process (readCreateProcessWithExitCode, shell)  --------------------------------------------------------------------------------+data Context = Context+  { ctxPath :: FilePath+  , ctxRunMode :: Command.RunMode+  }  -- | Similar to `evaluateFile` but run external commands.-executeFile :: FilePath -> IO (Either Error.Error [Syntax.Block])-executeFile path =+executeFile :: Context -> IO (Either Error.Error [Syntax.Block])+executeFile ctx@(Context {..}) =   runExceptT $-    PreProcess.preprocessFileE path+    PreProcess.preprocessFileE ctxPath       >>= Evaluate.evaluate Evaluate.defaultEnv ["toplevel"]-      >>= execute path+      >>= execute ctx  -------------------------------------------------------------------------------- execute-  :: FilePath+  :: Context   -> [Syntax.Block]   -> ExceptT Error.Error IO [Syntax.Block] execute ctx = mapM (exec ctx) -exec :: FilePath -> Syntax.Block -> ExceptT Error.Error IO Syntax.Block-exec ctx = \case+exec :: Context -> Syntax.Block -> ExceptT Error.Error IO Syntax.Block+exec ctx@(Context {..}) = \case   node@Syntax.BlockDoctype -> pure node   Syntax.BlockElem name mdot attrs nodes -> do     nodes' <- execute ctx nodes@@ -52,9 +59,9 @@   Syntax.BlockInclude mname path mbody -> do     mbody' <- traverse (execute ctx) mbody     pure $ Syntax.BlockInclude mname path mbody'-  Syntax.BlockFragmentDef name params nodes -> do+  Syntax.BlockFragmentDef usage name params nodes -> do     nodes' <- execute ctx nodes-    pure $ Syntax.BlockFragmentDef name params nodes'+    pure $ Syntax.BlockFragmentDef usage name params nodes'   Syntax.BlockFragmentCall name mdot attrs values nodes -> do     nodes' <- execute ctx nodes     pure $ Syntax.BlockFragmentCall name mdot attrs values nodes'@@ -68,16 +75,37 @@   Syntax.BlockImport path mbody args -> do     mbody' <- traverse (execute ctx) mbody     pure $ Syntax.BlockImport path mbody' args-  node@(Syntax.BlockRun _ (Just _)) -> pure node-  Syntax.BlockRun cmd Nothing -> do-    out <-+  node@(Syntax.BlockRun _ _ (Just _)) -> pure node+  Syntax.BlockRun cmd minput Nothing -> do+    (code, out, err) <-       liftIO $-        readCreateProcess ((shell $ T.unpack cmd) {cwd = Just "/tmp/"}) ""-    pure $-      Syntax.BlockRun cmd $-        Just [Syntax.BlockText Syntax.RunOutput [Syntax.Lit $ T.pack out]]-  node@(Syntax.BlockReadJson _ _ _) -> pure node-  node@(Syntax.BlockAssignVar _ _) -> pure node+        readCreateProcessWithExitCode (shell $ T.unpack cmd) $ maybe "" T.unpack minput+    case code of+      ExitSuccess ->+        pure $+          Syntax.BlockRun cmd minput $+            Just [Syntax.BlockText Syntax.RunOutput [Syntax.Lit $ T.pack out]]+      ExitFailure _ -> case ctxRunMode of+        Command.RunNormal ->+          throwE $+            Error.ExecuteError $+              T.pack err <> T.pack out+        Command.RunPassthrough ->+          pure $+            Syntax.BlockRun cmd minput $+              Just $+                [ Syntax.BlockElem+                    Syntax.Pre+                    Syntax.NoSym+                    []+                    [ Syntax.BlockElem+                        Syntax.Code+                        Syntax.HasDot+                        []+                        [Syntax.BlockText Syntax.RunOutput [Syntax.Lit $ T.pack (err <> out)]]+                    ]+                ]+  node@(Syntax.BlockAssignVars _) -> pure node   Syntax.BlockIf cond as bs -> do     as' <- execute ctx as     bs' <- execute ctx bs
src/Slab/Generate/Haskell.hs view
@@ -102,7 +102,7 @@ prettyAttr (Syntax.Id t) = pretty $ "A.id (H.toValue (\"" <> t <> "\" :: Text))" prettyAttr (Syntax.Class t) = pretty $ "A.class_ (H.toValue (\"" <> t <> "\" :: Text))" prettyAttr (Syntax.Attr a b) =-  "H.customAttribute" <+> pretty a <+> maybe (pretty a) prettyExpr b+  "H.customAttribute" <+> pretty a <+> prettyExpr b  prettyExpr :: Syntax.Expr -> Doc ann prettyExpr _ = "TODO"
src/Slab/Parse.hs view
@@ -24,13 +24,13 @@   , parseInlines   ) where -import Control.Monad (void)+import Control.Monad (guard, void) import Control.Monad.Combinators.Expr import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans.Except (ExceptT, except, runExceptT, withExceptT) import Data.Char (isSpace) import Data.Functor (($>))-import Data.List (intercalate)+import Data.List (intercalate, isSuffixOf) import Data.Maybe (isJust) import Data.Text (Text) import Data.Text qualified as T@@ -63,7 +63,7 @@ --     Parse.parseExpr "1 + 2 * a" -- @ parseExpr :: Text -> Either (ParseErrorBundle Text Void) Expr-parseExpr = runParser (sc *> parserExpr <* eof) ""+parseExpr = runParser (sc *> (getSourcePos >>= parserExprInd) <* eof) ""  -------------------------------------------------------------------------------- type Parser = Parsec Void Text@@ -79,6 +79,7 @@         , parserPipe         , parserExpr'         , parserFragmentDef+        , parserFragmentArg         , parserComment         , parserFilter         , parserRawElement@@ -122,10 +123,8 @@       template <- parseInlines       case template of         [] -> do-          scn-          items <- textBlock ref parserText -- TODO Use parseInlines-          let items' = realign items-          pure $ L.IndentNone $ header [BlockText Dot [Lit $ T.intercalate "\n" items']]+          s <-  parserDotText ref+          pure $ L.IndentNone $ header [BlockText Dot [Lit s]]         _ -> pure $ L.IndentNone $ header [BlockText Dot template]     HasEqual -> do       mcontent <- optional parserExpr@@ -141,6 +140,14 @@         [] -> pure $ L.IndentMany Nothing (pure . header) parserBlock         _ -> pure $ L.IndentNone $ header [BlockText Normal template] +-- | Parse indented text following a dot, or a run block.+parserDotText :: Pos -> Parser Text+parserDotText ref = do+  scn+  items <- textBlock ref parserText -- TODO Use parseInlines+  let items' = realign items+  pure $ T.intercalate "\n" items'+ -- | Parse lines of text, indented more than `ref`. -- E.g.: --     a@@ -215,6 +222,7 @@   content <- parserExpr   pure $ L.IndentNone $ BlockCode content +-- | Parse an expression on a single line. parserExpr :: Parser Expr parserExpr = makeExprParser pApp operatorTable  where@@ -233,6 +241,28 @@       <|> parens parserExpr   parens = between (lexeme $ char '(') (lexeme $ char ')') +-- | Same as "parserExpr" but allow expressions to be written on multiple lines.+-- Subsequent lines must be indented compared to the first line.+parserExprInd :: SourcePos -> Parser Expr+parserExprInd initialIndent = makeExprParser pApp (operatorTable' initialIndent)+ where+  pApp = do+    a <- pTerm+    mb <- optional $ pTerm+    case mb of+      Nothing -> pure a+      Just b -> pure $ Application a b+  pTerm =+    lx (Int <$> parserNumber)+      <|> lx (SingleQuoteString <$> parserSingleQuoteString)+      <|> lx (SingleQuoteString <$> parserDoubleQuoteString) -- TODO Double+      <|> lx parserVariable'+      <|> lx (Object <$> parserObject)+      <|> lx (JsonPath <$> try parserPath) -- TODO Force the .json extension.+      <|> parens (parserExprInd initialIndent)+  parens = between (lx $ char '(') (lx $ char ')')+  lx = lexeme' initialIndent+ parserVariable :: Parser Text parserVariable = parserName @@ -247,6 +277,18 @@     [InfixR (symbol "|" $> Cons)]   ] +operatorTable' :: SourcePos -> [[Operator Parser Expr]]+operatorTable' initialIndent =+  [ [InfixL (sym "*" $> Times), InfixL (sym "/" $> Divide)]+  , [InfixL (sym "+" $> Add), InfixL (sym "-" $> Sub)]+  , [InfixL (sym ">" $> GreaterThan), InfixL (sym "<" $> LesserThan)]+  , [InfixL (sym "==" $> Equal)]+  , -- I'd like to use : instead, but it is already used for objects...+    [InfixR (sym "|" $> Cons)]+  ]+ where+  sym = symbol' initialIndent+ parserVariable' :: Parser Expr parserVariable' = do   name <- parserName@@ -339,29 +381,30 @@     <$> (char '.' *> some (alphaNumChar <|> oneOf ("-_" :: String)))     <?> "class name" +-- | Parse both attributes and arguments. -- E.g. (), (class='a') parserAttrList :: Parser [Attr] parserAttrList = (<?> "attribute") $ do   _ <- string "("-  pairs <- many parserPair+  pairs <- sepBy+    (uncurry Attr <$> try parserPair <|> Arg <$> parserExpr)+    (lexeme $ string ",")   _ <- string ")"-  pure $ map (uncurry Attr) pairs+  pure pairs -parserPair :: Parser (Text, Maybe Expr)+parserPair :: Parser (Text, Expr) parserPair = do   a <- T.pack <$> (some (noneOf (",()= \n" :: String))) <?> "key"-  mb <- optional $ do-    _ <- string "="-    b <- lexeme parserValue-    pure b-  _ <- optional (lexeme $ string ",")-  pure (a, mb)+  _ <- string "="+  b <- lexeme parserValue+  pure (a, b)  parserValue :: Parser Expr parserValue =   SingleQuoteString <$> parserSingleQuoteString     <|> SingleQuoteString <$> parserDoubleQuoteString     <|> Int <$> parserNumber+    <|> Variable <$> parserVariable  parserSingleQuoteString :: Parser Text parserSingleQuoteString = do@@ -385,7 +428,7 @@ parserText = T.pack <$> lexeme (some (noneOf ['\n'])) <?> "text content"  parserIdentifier :: Parser Text-parserIdentifier = T.pack <$> (some (noneOf (" .=#(){}\n" :: String))) <?> "identifier"+parserIdentifier = T.pack <$> (some (noneOf (" ,.=#(){}\n" :: String))) <?> "identifier"  -------------------------------------------------------------------------------- parserInclude :: Parser (L.IndentOpt Parser Block Block)@@ -399,20 +442,49 @@   pure $ L.IndentNone $ BlockInclude mname path Nothing  parserPath :: Parser FilePath-parserPath = lexeme (some (noneOf ("'\"\n" :: String))) <?> "path"+parserPath = do+  path <- lexeme (some (noneOf ("'\"\n" :: String))) <?> "path"+  -- TODO Simply require a slash.+  guard+    ( ".slab"+        `isSuffixOf` path+        || ".json"+        `isSuffixOf` path+        || ".html"+        `isSuffixOf` path+        || ".txt"+        `isSuffixOf` path+        || ".svg"+        `isSuffixOf` path+    )+  pure path  -------------------------------------------------------------------------------- parserFragmentDef :: Parser (L.IndentOpt Parser Block Block) parserFragmentDef = do   _ <- lexeme (string "fragment" <|> string "frag")+  parserFragmentDef' DefinitionNormal++parserFragmentArg :: Parser (L.IndentOpt Parser Block Block)+parserFragmentArg = do+  _ <- lexeme (string "with")+  parserFragmentDef' DefinitionArg++parserFragmentDef' :: DefinitionUse -> Parser (L.IndentOpt Parser Block Block)+parserFragmentDef' use = do   name <- lexeme parserIdentifier   params <- maybe [] id <$> optional parserParameters-  pure $ L.IndentMany Nothing (pure . BlockFragmentDef name params) parserBlock+  kwparams <- maybe [] id <$> optional parserKwParameters+  pure $ L.IndentMany Nothing (pure . BlockFragmentDef use name (params <> kwparams)) parserBlock  -- E.g. {}, {a, b} parserParameters :: Parser [Text]-parserParameters = parserList' "{" "}" (lexeme parserIdentifier) <?> "arguments"+parserParameters = parserList' "(" ")" (lexeme parserIdentifier) <?> "parameters" +-- E.g. {}, {a, b}+parserKwParameters :: Parser [Text]+parserKwParameters = parserList' "{" "}" (lexeme parserIdentifier) <?> "named parameters"+ -------------------------------------------------------------------------------- parserFragmentCall :: Parser (L.IndentOpt Parser Block Block) parserFragmentCall = do@@ -425,9 +497,10 @@   -- TODO Use parserNameWithAttrs.   (name, attrs, trailing, args) <- lexeme $ do     name <- parserIdentifier-    attrs <- concat <$> many parserAttrs'+    attrs_ <- concat <$> many parserAttrs'+    let (attrs, args) = splitAttrsAndArgs attrs_     trailing <- parserTrailingSym-    args <- maybe [] id <$> optional parserArguments+    kwargs <- maybe [] id <$> optional parserArguments     pure (name, attrs, trailing, args)   pure $ BlockFragmentCall name trailing attrs args @@ -564,41 +637,39 @@ -------------------------------------------------------------------------------- parserRun :: Parser (L.IndentOpt Parser Block Block) parserRun = do+  ref <- L.indentLevel   _ <- lexeme (string "run")   cmd <- parserText-  pure $ L.IndentNone $ BlockRun cmd Nothing+  s <- parserDotText ref+  pure . L.IndentNone $ BlockRun cmd (Just s) Nothing  -------------------------------------------------------------------------------- parserLet :: Parser (L.IndentOpt Parser Block Block) parserLet = do   _ <- lexeme (string "let")-  name <- lexeme parserName-  _ <- lexeme (string "=")-  choice-    [ parserAssignVar name-    , parserReadJson name-    ]--parserAssignVar :: Text -> Parser (L.IndentOpt Parser Block Block)-parserAssignVar name = do-  val <- parserExpr-  pure $ L.IndentNone $ BlockAssignVar name val--parserReadJson :: Text -> Parser (L.IndentOpt Parser Block Block)-parserReadJson name = do-  path <- parserPath-  pure $ L.IndentNone $ BlockReadJson name path Nothing+  scn+  blockIndent <- getSourcePos+  pairs <- some $ do+    initialIndent <- getSourcePos+    guard+      ( sourceColumn blockIndent == sourceColumn initialIndent+      )+    name <- lexeme' initialIndent parserName+    _ <- lexeme' initialIndent (string "=")+    val <- parserExprInd initialIndent+    pure (name, val)+  pure $ L.IndentNone $ BlockAssignVars pairs  ---------------------------------------------------------------------------------scn :: Parser ()-scn = L.space space1 empty empty- -- Similar to space, but counts newlines space' :: Parser Int space' = do   s <- takeWhileP (Just "white space") isSpace   pure . length $ filter (== '\n') $ T.unpack s +scn :: Parser ()+scn = L.space space1 empty empty+ sc :: Parser () sc = L.space (void $ some (char ' ' <|> char '\t')) empty empty @@ -607,6 +678,27 @@  symbol :: Text -> Parser Text symbol = L.symbol sc++--------------------------------------------------------------------------------+-- Custom lexeme that checks against the passed indentation.+lexeme' :: SourcePos -> Parser a -> Parser a+lexeme' initialIndent p = do+  currentIndent <- getSourcePos+  guard+    ( sourceLine currentIndent == sourceLine initialIndent+        || sourceColumn currentIndent > sourceColumn initialIndent+    )+  L.lexeme scn p++-- Custom symbol that checks against the passed indentation.+symbol' :: SourcePos -> Text -> Parser Text+symbol' initialIndent s = do+  currentIndent <- getSourcePos+  guard+    ( sourceLine currentIndent == sourceLine initialIndent+        || sourceColumn currentIndent > sourceColumn initialIndent+    )+  L.symbol scn s  -------------------------------------------------------------------------------- 
src/Slab/PreProcess.hs view
@@ -17,9 +17,12 @@ import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans.Except (ExceptT, runExceptT, throwE) import Data.Aeson qualified as Aeson+import Data.Aeson.Key qualified as Aeson.Key+import Data.Aeson.KeyMap qualified as Aeson.KeyMap import Data.ByteString.Lazy qualified as BL import Data.Text qualified as T import Data.Text.IO qualified as T+import Data.Vector qualified as V import Slab.Error qualified as Error import Slab.Parse qualified as Parse import Slab.Syntax@@ -76,9 +79,9 @@             pure $ BlockInclude mname path (Just nodes')         | otherwise ->             throwE $ Error.PreProcessError $ "File " <> T.pack includedPath <> " doesn't exist"-  BlockFragmentDef name params nodes -> do+  BlockFragmentDef usage name params nodes -> do     nodes' <- preprocess ctx nodes-    pure $ BlockFragmentDef name params nodes'+    pure $ BlockFragmentDef usage name params nodes'   BlockFragmentCall name mdot attrs values nodes -> do     nodes' <- preprocess ctx nodes     pure $ BlockFragmentCall name mdot attrs values nodes'@@ -105,16 +108,19 @@             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+  node@(BlockRun _ _ _) -> pure node+  BlockAssignVars pairs -> do+    let f (name, JsonPath path) = do+          let path' = takeDirectory ctxStartPath </> path+          content <- liftIO $ BL.readFile path'+          case Aeson.eitherDecode content of+            Right val ->+              pure (name, jsonToExpr val)+            Left err ->+              throwE $ Error.PreProcessError $ "Can't decode JSON: " <> T.pack err+        f pair = pure pair+    pairs' <- mapM f pairs+    pure $ BlockAssignVars pairs'   BlockIf cond as bs -> do     -- File inclusion is done right away, without checking the condition.     as' <- preprocess ctx as@@ -124,3 +130,13 @@     nodes' <- preprocess ctx nodes     pure $ BlockList nodes'   node@(BlockCode _) -> pure node++jsonToExpr :: Aeson.Value -> Expr+jsonToExpr = \case+  Aeson.String s -> SingleQuoteString s+  Aeson.Array xs ->+    List $ map jsonToExpr (V.toList xs)+  Aeson.Object kvs ->+    let f (k, v) = (SingleQuoteString $ Aeson.Key.toText k, jsonToExpr v)+     in Object $ map f (Aeson.KeyMap.toList kvs)+  x -> error $ "jsonToExpr: " <> show x
src/Slab/Render.hs view
@@ -9,6 +9,7 @@   , renderHtmls   , renderHtmlsUtf8   , renderBlocks+  , extractTexts   ) where  import Data.ByteString.Lazy qualified as BSL@@ -54,12 +55,10 @@  where   mAddId :: H.Html -> H.Html   mAddId e =-    if idNames == []-      then e-      else e ! A.id (H.toValue idNames')-  idNames = Syntax.idNamesFromAttrs attrs-  idNames' :: Text-  idNames' = T.intercalate "-" idNames -- TODO Refuse multiple Ids in some kind of validation step after parsing ?+    case idNames of+      Nothing -> e+      Just names -> e ! A.id (H.toValue names)+  idNames = Syntax.idNamesFromAttrs' attrs   mAddClass :: H.Html -> H.Html   mAddClass e =     if classNames == []@@ -84,7 +83,7 @@   escapeTexts nodes renderBlock (Syntax.BlockInclude _ _ (Just nodes)) = mapM_ renderBlock nodes renderBlock (Syntax.BlockInclude _ path Nothing) = H.stringComment $ "include " <> path-renderBlock (Syntax.BlockFragmentDef _ _ _) = mempty+renderBlock (Syntax.BlockFragmentDef _ _ _ _) = mempty renderBlock (Syntax.BlockFragmentCall _ _ _ _ nodes) = mapM_ renderBlock nodes renderBlock (Syntax.BlockFor _ _ _ nodes) = mapM_ renderBlock nodes renderBlock (Syntax.BlockComment b content) =@@ -99,11 +98,10 @@   mapM_ renderBlock children renderBlock (Syntax.BlockDefault _ nodes) = mapM_ renderBlock nodes renderBlock (Syntax.BlockImport _ (Just nodes) _) = mapM_ renderBlock nodes-renderBlock (Syntax.BlockRun _ (Just nodes)) = mapM_ renderBlock nodes-renderBlock (Syntax.BlockRun cmd _) = H.textComment $ "run " <> cmd+renderBlock (Syntax.BlockRun _ _ (Just nodes)) = mapM_ renderBlock nodes+renderBlock (Syntax.BlockRun cmd _ _) = H.textComment $ "run " <> cmd renderBlock (Syntax.BlockImport path Nothing _) = H.stringComment $ "extends " <> path-renderBlock (Syntax.BlockReadJson _ _ _) = mempty-renderBlock (Syntax.BlockAssignVar _ _) = mempty+renderBlock (Syntax.BlockAssignVars _) = mempty renderBlock (Syntax.BlockIf _ as bs) = do   -- The evaluation code transforms a BlockIf into a BlockList, so this should   -- not be called.@@ -124,15 +122,14 @@ renderBlock (Syntax.BlockCode c) = error $ "renderBlock called on BlockCode " <> show c  renderTexts :: [Syntax.Block] -> H.Html-renderTexts xs = H.preEscapedText xs'- where-  xs' = T.intercalate "\n" $ map extractText xs+renderTexts = H.preEscapedText . extractTexts  escapeTexts :: [Syntax.Block] -> H.Html-escapeTexts xs = H.text xs'- where-  xs' = T.intercalate "\n" $ map extractText xs+escapeTexts = H.text . extractTexts +extractTexts :: [Syntax.Block] -> Text+extractTexts = T.intercalate "\n" . map extractText+ extractText :: Syntax.Block -> Text extractText = f  where@@ -141,7 +138,7 @@   f (Syntax.BlockText _ [Syntax.Lit s]) = s   f (Syntax.BlockText _ _) = error "extractTexts called on unevaluated BlockText"   f (Syntax.BlockInclude _ _ _) = error "extractTexts called on a BlockInclude"-  f (Syntax.BlockFragmentDef _ _ _) = error "extractTexts called on a BlockFragmentDef"+  f (Syntax.BlockFragmentDef _ _ _ _) = error "extractTexts called on a BlockFragmentDef"   f (Syntax.BlockFragmentCall _ _ _ _ _) = error "extractTexts called on a BlockFragmentCall"   f (Syntax.BlockFor _ _ _ _) = error "extractTexts called on a BlockFor"   f (Syntax.BlockComment _ _) = error "extractTexts called on a BlockComment"@@ -149,9 +146,8 @@   f (Syntax.BlockRawElem _ _) = error "extractTexts called on a BlockRawElem"   f (Syntax.BlockDefault _ _) = error "extractTexts called on a BlockDefault"   f (Syntax.BlockImport _ _ _) = error "extractTexts called on a BlockImport"-  f (Syntax.BlockRun _ _) = error "extractTexts called on a BlockRun"-  f (Syntax.BlockReadJson _ _ _) = error "extractTexts called on a BlockReadJson"-  f (Syntax.BlockAssignVar _ _) = error "extractTexts called on a BlockAssignVar"+  f (Syntax.BlockRun _ _ _) = error "extractTexts called on a BlockRun"+  f (Syntax.BlockAssignVars _) = error "extractTexts called on a BlockAssignVars"   f (Syntax.BlockIf _ _ _) = error "extractTexts called on a BlockIf"   f (Syntax.BlockList _) = error "extractTexts called on a BlockList"   f (Syntax.BlockCode _) = error "extractTexts called on a BlockCode"@@ -194,6 +190,7 @@   Syntax.Link -> const H.link   Syntax.A -> H.a   Syntax.P -> H.p+  Syntax.Em -> H.em   Syntax.Ul -> H.ul   Syntax.Li -> H.li   Syntax.Title -> H.title
src/Slab/Report.hs view
@@ -5,17 +5,22 @@ -- This module serves as a way to explore new Slab features, e.g. creating a -- module system, or analyzing a growing HTML code base to help refactor it. module Slab.Report-  ( run+  ( reportPages+  , reportHeadings   ) where +import Data.Text (Text)+import Data.Text qualified as T+import Data.Tree (Tree (..), drawForest) import Slab.Build qualified as Build import Slab.Error qualified as Error import Slab.Evaluate qualified as Evaluate+import Slab.Render qualified as Render import Slab.Syntax qualified as Syntax  ---------------------------------------------------------------------------------run :: FilePath -> IO ()-run srcDir = do+reportPages :: FilePath -> IO ()+reportPages srcDir = do   modules <- buildDir srcDir   putStrLn $ "Read " <> show (length modules) <> " modules."   let pages = filter isPage modules@@ -33,6 +38,14 @@ isPage _ = False  --------------------------------------------------------------------------------+reportHeadings :: FilePath -> IO ()+reportHeadings path = do+  modl <- buildFile path+  let headings = extractHeadings . Evaluate.simplify $ moduleNodes modl+      f (Heading level _ t) = show level <> " " <> T.unpack t+  putStrLn . drawForest . map (fmap f) $ buildTrees headings++-------------------------------------------------------------------------------- -- Similar to Build.buildDir and buildFile, but don't render HTML to disk. -- TODO Move this code to (and combine it with) with @Slab.Build@. @@ -50,3 +63,52 @@       { modulePath = path       , moduleNodes = nodes       }++--------------------------------------------------------------------------------++buildTrees :: [Heading] -> [Tree Heading]+buildTrees [] = []+buildTrees (h : hs) =+  let (children, rest) = span ((> headingLevel h) . headingLevel) hs+      childTrees = buildTrees children+      tree = Node h childTrees+   in tree : buildTrees rest++data Heading = Heading+  { headingLevel :: Int+  , headingId :: Maybe Text+  , headingText :: Text+  }+  deriving (Show, Eq)++extractHeadings :: [Syntax.Block] -> [Heading]+extractHeadings = concatMap f+ where+  f Syntax.BlockDoctype = []+  f (Syntax.BlockElem el _ attrs children) =+    let i = Syntax.idNamesFromAttrs' attrs+        t = Render.extractTexts children+     in case el of+          Syntax.H1 -> [Heading 1 i t]+          Syntax.H2 -> [Heading 2 i t]+          Syntax.H3 -> [Heading 3 i t]+          Syntax.H4 -> [Heading 4 i t]+          Syntax.H5 -> [Heading 5 i t]+          Syntax.H6 -> [Heading 6 i t]+          _ -> extractHeadings children+  f (Syntax.BlockText _ _) = []+  f (Syntax.BlockInclude _ _ children) = maybe [] extractHeadings children+  f (Syntax.BlockFragmentDef _ _ _ _) = []+  f (Syntax.BlockFragmentCall _ _ _ _ children) =+    extractHeadings children+  f (Syntax.BlockFor _ _ _ children) = extractHeadings children+  f (Syntax.BlockComment _ _) = []+  f (Syntax.BlockFilter _ _) = []+  f (Syntax.BlockRawElem _ _) = []+  f (Syntax.BlockDefault _ children) = extractHeadings children+  f (Syntax.BlockImport _ children args) = maybe [] extractHeadings children <> extractHeadings args+  f (Syntax.BlockRun _ _ _) = []+  f (Syntax.BlockAssignVars _) = []+  f (Syntax.BlockIf _ as bs) = extractHeadings as <> extractHeadings bs+  f (Syntax.BlockList children) = extractHeadings children+  f (Syntax.BlockCode _) = []
src/Slab/Run.hs view
@@ -12,6 +12,7 @@   , calc   ) where +import Control.Monad (when) import Control.Monad.Trans.Except (ExceptT, except, runExceptT, withExceptT) import Data.Text (Text) import Data.Text qualified as T@@ -30,25 +31,32 @@ import Slab.Serve qualified as Serve import Slab.Syntax qualified as Syntax import Slab.Watch qualified as Watch+import System.FilePath (takeExtension) import Text.Pretty.Simple (pPrintNoColor, pShowNoColor)  -------------------------------------------------------------------------------- run :: Command.Command -> IO ()-run (Command.Build srcDir renderMode distDir) = Build.buildDir srcDir renderMode distDir-run (Command.Watch srcDir renderMode distDir) =-  Watch.run srcDir (Build.buildFile srcDir renderMode distDir)+run (Command.Build srcDir renderMode passthrough distDir) =+  Build.buildDir srcDir renderMode passthrough distDir+run (Command.Watch srcDir renderMode passthrough distDir) =+  Watch.run srcDir $ \path -> do+    when (takeExtension path == ".slab") $+      Build.buildFile srcDir renderMode passthrough distDir path run (Command.Serve srcDir distDir) = Serve.run srcDir distDir-run (Command.Report srcDir) = Report.run srcDir+run (Command.ReportPages srcDir) = Report.reportPages srcDir+run (Command.ReportHeadings path) = Report.reportHeadings path run (Command.Generate path) = Generate.renderHs path-run (Command.CommandWithPath path pmode (Command.Render Command.RenderNormal)) = do-  nodes <- executeWithMode path pmode >>= Error.unwrap+run (Command.CommandWithPath path pmode (Command.Render Command.RenderNormal passthrough)) = do+  nodes <- executeWithMode path pmode passthrough >>= Error.unwrap   TL.putStrLn . Render.renderHtmls $ Render.renderBlocks nodes-run (Command.CommandWithPath path pmode (Command.Render Command.RenderPretty)) = do-  nodes <- executeWithMode path pmode >>= Error.unwrap+run (Command.CommandWithPath path pmode (Command.Render Command.RenderPretty passthrough)) = do+  nodes <- executeWithMode path pmode passthrough >>= Error.unwrap   T.putStr . Render.prettyHtmls $ Render.renderBlocks nodes-run (Command.CommandWithPath path pmode Command.Execute) = do-  nodes <- executeWithMode path pmode >>= Error.unwrap-  TL.putStrLn $ pShowNoColor nodes+run (Command.CommandWithPath path pmode (Command.Execute simpl passthrough)) = do+  nodes <- executeWithMode path pmode passthrough >>= Error.unwrap+  if simpl+    then TL.putStrLn $ pShowNoColor $ Evaluate.simplify nodes+    else TL.putStrLn $ pShowNoColor nodes run (Command.CommandWithPath path pmode (Command.Evaluate simpl)) = do   nodes <- evaluateWithMode path pmode >>= Error.unwrap   if simpl@@ -85,8 +93,9 @@ executeWithMode   :: FilePath   -> Command.ParseMode+  -> Command.RunMode   -> IO (Either Error.Error [Syntax.Block])-executeWithMode path pmode = runExceptT $ executeWithModeE path pmode+executeWithMode path pmode passthrough = runExceptT $ executeWithModeE path pmode passthrough  -------------------------------------------------------------------------------- parseWithModeE@@ -109,9 +118,10 @@ executeWithModeE   :: FilePath   -> Command.ParseMode+  -> Command.RunMode   -> ExceptT Error.Error IO [Syntax.Block]-executeWithModeE path pmode =-  evaluateWithModeE path pmode >>= Execute.execute path+executeWithModeE path pmode passthrough =+  evaluateWithModeE path pmode >>= Execute.execute (Execute.Context path passthrough)  -------------------------------------------------------------------------------- -- Play with the whole language.
src/Slab/Serve.hs view
@@ -7,26 +7,40 @@ -- Description : Run a development server to preview Slab templates -- -- @Slab.Serve@ watches a set of Slab templates, continuously rebuilding them--- as they change, and runs a web server to serve them.+-- as they change, and runs a web server to serve them. Pages containing a+-- @head@ element are modified to include a bit of JavaScript. It serves at+-- auto-reloading the page when it is rebuilt (by connecting to a websocket). module Slab.Serve   ( run   ) where +import Control.Concurrent.Chan qualified as Chan import Control.Concurrent.STM qualified as STM import Data.Map qualified as M import Data.Text qualified as T+import Data.Text.Lazy.Encoding qualified as TLE import Network.HTTP.Types (status200) import Network.Wai qualified as Wai import Network.Wai.Handler.Warp qualified as Warp+import Network.WebSockets.Connection+  ( Connection+  , sendTextData+  , withPingThread+  ) import Protolude hiding (Handler) import Servant hiding (serve)+import Servant.API.WebSocket (WebSocket) import Servant.HTML.Blaze qualified as B import Servant.Server qualified as Server import Slab.Build qualified as Build import Slab.Command qualified as Command+import Slab.Evaluate qualified as Evaluate import Slab.Render qualified as Render+import Slab.Syntax qualified as Syntax import Slab.Watch qualified as Watch+import System.FilePath (takeExtension) import Text.Blaze.Html5 (Html)+import Text.Pretty.Simple (pShowNoColor) import WaiAppStatic.Storage.Filesystem   ( defaultWebAppSettings   )@@ -35,21 +49,29 @@ run :: FilePath -> FilePath -> IO () run srcDir distDir = do   store <- atomically $ STM.newTVar M.empty-  -- Initial build to populate the store.-  Build.buildDirInMemory srcDir Command.RenderNormal store-  -- Then rebuild one file upon change.+  chan <- Chan.newChan+  -- Initial build to populate the store...+  Build.buildDirInMemory srcDir Command.RenderNormal Command.RunPassthrough store+  -- ...then rebuild individual files upon change, and notify the channel.   _ <-     forkIO $-      Watch.run srcDir (Build.buildFileInMemory srcDir Command.RenderNormal store)-  Warp.run 9000 $ serve distDir store+      Watch.run srcDir $ \path -> do+        when (takeExtension path == ".slab") $+          Build.buildFileInMemory srcDir Command.RenderNormal Command.RunPassthrough store path+        when (takeExtension path /= ".slab") $+          -- Rebuild everything. TODO create a dependency graph and rebuild+          -- only what is needed.+          Build.buildDirInMemory srcDir Command.RenderNormal Command.RunPassthrough store+        Chan.writeChan chan path+  Warp.run 9000 $ serve distDir store chan  -- | Turn our `serverT` implementation into a Wai application, suitable for -- Warp.run.-serve :: FilePath -> Build.StmStore -> Wai.Application-serve root store =+serve :: FilePath -> Build.StmStore -> Chan FilePath -> Wai.Application+serve root store chan =   Servant.serveWithContext appProxy Server.EmptyContext $     Server.hoistServerWithContext appProxy settingsProxy identity $-      serverT root store+      serverT root store chan  ------------------------------------------------------------------------------ type ServerSettings = '[]@@ -60,15 +82,17 @@ ------------------------------------------------------------------------------ type App =   "hello" :> Get '[B.HTML] Html+    :<|> WebSocketApi     :<|> Servant.Raw -- Fallback handler for the static files.  appProxy :: Proxy App appProxy = Proxy  -------------------------------------------------------------------------------serverT :: FilePath -> Build.StmStore -> ServerT App Handler-serverT root store =+serverT :: FilePath -> Build.StmStore -> Chan FilePath -> ServerT App Handler+serverT root store chan =   showHelloPage+    :<|> websocket chan     :<|> app root store  ------------------------------------------------------------------------------@@ -89,12 +113,21 @@       path' = if T.null path then "index.html" else path   -- TODO Check requestMethod is GET.   case M.lookup (T.unpack path') templates of-    Just blocks ->-      sendRes $-        Wai.responseLBS-          status200-          [("Content-Type", "text/html")]-          (Render.renderHtmlsUtf8 $ Render.renderBlocks blocks)+    Just mblocks -> do+      case mblocks of+        Right blocks -> do+          let blocks' = Syntax.addScript autoreloadScript $ Evaluate.simplify blocks+          sendRes $+            Wai.responseLBS+              status200+              [("Content-Type", "text/html")]+              (Render.renderHtmlsUtf8 $ Render.renderBlocks blocks')+        Left err -> do+          sendRes $+            Wai.responseLBS+              status200+              [("Content-Type", "text/plain")]+              (TLE.encodeUtf8 $ pShowNoColor err)     Nothing -> do       let Tagged staticApp = serveStatic root       staticApp req sendRes@@ -104,3 +137,60 @@ serveStatic root = Servant.serveDirectoryWith settings  where   settings = defaultWebAppSettings root++------------------------------------------------------------------------------+-- Accept websocket connections, and keep them alive.+type WebSocketApi = "ws" :> WebSocket++-- | Sends messages to the browser whenever a message is written to the channel.+websocket :: Chan FilePath -> Connection -> Handler ()+websocket chan con =+  liftIO $ do+    chan' <- dupChan chan+    withPingThread con 30 (pure ()) $+      liftIO . forever $ do+        path <- Chan.readChan chan'+        sendTextData con (T.pack $ "updated: " <> path)++-- | The auto-reload script. It connects to a websocket and refreshes the+-- current page when it receives a message from the server. Such a message is+-- sent whenever a @.slab@ file is rebuilt.+autoreloadScript :: Text+autoreloadScript =+  "function connect(isInitialConnection) {\n\+  \  // Create WebSocket connection.\n\+  \  var ws = new WebSocket('ws://' + location.host + '/ws');\n\+  \\n\+  \  // Connection opened\n\+  \  ws.onopen = function() {\n\+  \    ws.send('Hello server.');\n\+  \    if (isInitialConnection) {\n\+  \      console.log('autoreload: Initial connection.');\n\+  \    } else {\n\+  \      console.log('autoreload: Reconnected.');\n\+  \      location.reload();\n\+  \    }\n\+  \  };\n\+  \\n\+  \  // Listen for messages.\n\+  \  ws.onmessage = function(ev) {\n\+  \    console.log('autoreload: Message from server:', ev.data);\n\+  \    if (ev.data.startsWith('updated:')) {\n\+  \      location.reload();\n\+  \    }\n\+  \  };\n\+  \\n\+  \  // Trying to reconnect when the socket is closed.\n\+  \  ws.onclose = function(ev) {\n\+  \    console.log('autoreload: Socket closed. Trying to reconnect in 0.5 second.');\n\+  \    setTimeout(function() { connect(false); }, 500);\n\+  \  };\n\+  \\n\+  \  // Close the socker upon error.\n\+  \  ws.onerror = function(err) {\n\+  \    console.log('autoreload: Socket errored. Closing socket.');\n\+  \    ws.close();\n\+  \  };\n\+  \}\n\+  \\n\+  \connect(true);\n"
src/Slab/Syntax.hs view
@@ -12,10 +12,13 @@   , pasteBlocks   , setAttrs   , setContent+  , addScript   , CommentType (..)   , Elem (..)+  , DefinitionUse (..)   , TrailingSym (..)   , Attr (..)+  , splitAttrsAndArgs   , TextSyntax (..)   , Expr (..)   , Inline (..)@@ -29,13 +32,13 @@   , extractFragments   , findFragment   , idNamesFromAttrs+  , idNamesFromAttrs'   , classNamesFromAttrs   , namesFromAttrs   , groupAttrs   ) where -import Data.Aeson qualified as Aeson-import Data.List (nub, sort)+import Data.List (nub, partition, sort) import Data.Text (Text) import Data.Text qualified as T @@ -49,9 +52,7 @@     -- preprocessing (i.e. actually running the include statement).     -- The filter name follows the same behavior as BlockFilter.     BlockInclude (Maybe Text) FilePath (Maybe [Block])-  | -- | This doesn't exist in Pug. This is like a mixin than receive block arguments.-    -- Or like a parent template that can be @extended@ by a child template.-    BlockFragmentDef Text [Text] [Block]+  | BlockFragmentDef DefinitionUse Text [Text] [Block]   | BlockFragmentCall Text TrailingSym [Attr] [Expr] [Block]   | BlockFor Text (Maybe Text) Expr [Block]   | -- TODO Should we allow string interpolation here ?@@ -64,11 +65,9 @@   | -- | Similar to an anonymous fragment call, where the fragment body is the     -- content of the referenced file.     BlockImport FilePath (Maybe [Block]) [Block]-  | BlockRun Text (Maybe [Block])-  | -- | Allow to assign the content of a JSON file to a variable. The syntax-    -- is specific to how Struct has a @require@ function in scope.-    BlockReadJson Text FilePath (Maybe Aeson.Value)-  | BlockAssignVar Text Expr+  | -- | Run an external command, with maybe some stdin input.+    BlockRun Text (Maybe Text) (Maybe [Block])+  | BlockAssignVars [(Text, Expr)]   | BlockIf Expr [Block] [Block]   | BlockList [Block]   | BlockCode Expr@@ -103,6 +102,17 @@   BlockElem name mdot attrs nodes setContent _ b = b +-- | Find the head element and add a script element at its end.  TODO This+-- doesn't go through all children to find the head. It's best to use+-- "Evaluate.simplify" before using this function.+addScript :: Text -> [Block] -> [Block]+addScript t = map f+ where+  f (BlockElem Head mdot attrs children) = BlockElem Head mdot attrs (children <> [s])+  f (BlockElem name mdot attrs children) = BlockElem name mdot attrs (map f children)+  f block = block+  s = BlockElem Script NoSym [] [BlockText Dot [Lit t]]+ -- | A "passthrough" comment will be included in the generated HTML. data CommentType = NormalComment | PassthroughComment   deriving (Show, Eq)@@ -127,6 +137,7 @@   | Link   | A   | P+  | Em   | Ul   | Li   | Title@@ -163,13 +174,32 @@     Elem Text   deriving (Show, Eq) +-- | Specifies if a fragment definition is a normal definition, or one meant to+-- be an argument of a fragment call.+data DefinitionUse = DefinitionNormal | DefinitionArg+  deriving (Show, Eq)+ data TrailingSym = HasDot | HasEqual | NoSym   deriving (Show, Eq) +-- | Represent an attribute or an argument of an element. Attributes can be+-- IDs, classes, or arbitrary keys. Arguments are expressions with no key. -- The Code must already be evaluated.-data Attr = Id Text | Class Text | Attr Text (Maybe Expr)+data Attr = Id Text | Class Text | Attr Text Expr | Arg Expr   deriving (Show, Eq) +splitAttrsAndArgs :: [Attr] -> ([Attr], [Expr])+splitAttrsAndArgs = g . partition f+ where+  f = \case+    Id _ -> True+    Class _ -> True+    Attr _ _ -> True+    Arg _ -> False+  g (a, b) = (a, map h b)+  h (Arg e) = e+  h _ = error "Can't happen"+ -- Tracks the syntax used to enter the text. data TextSyntax   = -- | The text follows an element on the same line.@@ -212,6 +242,8 @@     Frag [Text] Env [Block]   | -- Same for Expr instead of Block.     Thunk Env Expr+  | -- | Allow to assign the content of a JSON file to a variable.+    JsonPath FilePath   | BuiltIn Text   deriving (Show, Eq) @@ -275,7 +307,7 @@   f (BlockElem _ _ attrs children) = concatMap g attrs <> extractClasses children   f (BlockText _ _) = []   f (BlockInclude _ _ children) = maybe [] extractClasses children-  f (BlockFragmentDef _ _ _) = [] -- We extract them in BlockFragmentCall instead.+  f (BlockFragmentDef _ _ _ _) = [] -- We extract them in BlockFragmentCall instead.   f (BlockFragmentCall _ _ attrs _ children) = concatMap g attrs <> extractClasses children   f (BlockFor _ _ _ children) = extractClasses children   f (BlockComment _ _) = []@@ -284,9 +316,8 @@   f (BlockRawElem _ _) = []   f (BlockDefault _ children) = extractClasses children   f (BlockImport _ children blocks) = maybe [] extractClasses children <> extractClasses blocks-  f (BlockRun _ _) = []-  f (BlockReadJson _ _ _) = []-  f (BlockAssignVar _ _) = []+  f (BlockRun _ _ _) = []+  f (BlockAssignVars _) = []   f (BlockIf _ as bs) = extractClasses as <> extractClasses bs   f (BlockList children) = extractClasses children   f (BlockCode _) = []@@ -294,7 +325,7 @@   g (Id _) = []   g (Class c) = [c]   g (Attr a b) = h a b-  h "class" (Just (SingleQuoteString c)) = [c]+  h "class" (SingleQuoteString c) = [c]   h "class" _ = error "The class is not a string"   h _ _ = [] @@ -311,7 +342,8 @@   f (BlockElem _ _ _ children) = extractFragments children   f (BlockText _ _) = []   f (BlockInclude _ _ children) = maybe [] extractFragments children-  f (BlockFragmentDef name _ children) = [BlockFragmentDef' name children]+  f (BlockFragmentDef DefinitionNormal name _ children) = [BlockFragmentDef' name children]+  f (BlockFragmentDef DefinitionArg _ _ _) = []   f (BlockFragmentCall name _ _ _ children) =     [BlockFragmentCall' name] <> extractFragments children   f (BlockFor _ _ _ children) = extractFragments children@@ -320,9 +352,8 @@   f (BlockRawElem _ _) = []   f (BlockDefault _ children) = extractFragments children   f (BlockImport _ children args) = maybe [] extractFragments children <> extractFragments args-  f (BlockRun _ _) = []-  f (BlockReadJson _ _ _) = []-  f (BlockAssignVar _ _) = []+  f (BlockRun _ _ _) = []+  f (BlockAssignVars _) = []   f (BlockIf _ as bs) = extractFragments as <> extractFragments bs   f (BlockList children) = extractFragments children   f (BlockCode _) = []@@ -345,10 +376,20 @@         Attr a b -> f a b     )  where-  f "id" (Just (SingleQuoteString x)) = [x]-  f "id" (Just _) = error "The id is not a string"+  f "id" (SingleQuoteString x) = [x]+  f "id" _ = error "The id is not a string"   f _ _ = [] +idNamesFromAttrs' :: [Attr] -> Maybe Text+idNamesFromAttrs' attrs =+  if idNames == []+    then Nothing+    else Just idNames'+ where+  idNames = idNamesFromAttrs attrs+  -- TODO Refuse multiple Ids in some kind of validation step after parsing ?+  idNames' = T.intercalate "-" idNames+ classNamesFromAttrs :: [Attr] -> [Text] classNamesFromAttrs =   concatMap@@ -358,8 +399,8 @@         Attr a b -> f a b     )  where-  f "class" (Just (SingleQuoteString x)) = [x]-  f "class" (Just _) = error "The class is not a string"+  f "class" (SingleQuoteString x) = [x]+  f "class" _ = error "The class is not a string"   f _ _ = []  namesFromAttrs :: [Attr] -> [(Text, Text)]@@ -373,10 +414,12 @@  where   f "id" _ = []   f "class" _ = []-  f a (Just (SingleQuoteString b)) = [(a, b)]-  f a (Just (Int b)) = [(a, T.pack $ show b)]-  f _ (Just _) = error "The attribute is not a string"-  f a Nothing = [(a, a)]+  f a (SingleQuoteString b) = [(a, b)]+  f a (Int b) = [(a, T.pack $ show b)]+  f a (Bool True) = [(a, a)]+  f a (Bool False) = []+  f a (Variable _) = error "The attribute is not evaluated"+  f _ _ = error "The attribute is not a string"  -- | Group multiple classes or IDs in a single class or ID, and transform the -- other attributes in 'SingleQuoteString's.@@ -400,4 +443,4 @@       else [Class classNames']    attrs' = namesFromAttrs attrs-  elemAttrs = map (\(a, b) -> Attr a (Just $ SingleQuoteString b)) attrs'+  elemAttrs = map (\(a, b) -> Attr a (SingleQuoteString b)) attrs'
src/Slab/Watch.hs view
@@ -29,7 +29,7 @@         srcDir         ( \e -> do             case e of-              Modified path _ _ | takeExtension path == ".slab" -> True+              Modified path _ _ | takeExtension path `elem` [".slab", ".pikchr"] -> True               _ -> False         )         ( \e -> do
tests/Slab/Runner.hs view
@@ -5,6 +5,7 @@  import Data.List (sort) import Data.Text (Text)+import Slab.Command qualified as Command import Slab.Execute qualified as Execute import Slab.Render qualified as Render import System.FilePath@@ -52,7 +53,8 @@  where   action :: IO Text   action = do-    evaluated <- Execute.executeFile path+    let ctx = Execute.Context path Command.RunNormal+    evaluated <- Execute.executeFile ctx     case evaluated of       Left _ -> pure "TODO"       Right nodes -> do