packages feed

slab 0.0.1.0 → 0.0.2.0

raw patch · 16 files changed

+715/−251 lines, 16 filesdep +http-typesdep +stmPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: http-types, stm

API changes (from Hackage documentation)

+ Slab.Build: buildDirInMemory :: FilePath -> RenderMode -> StmStore -> IO ()
+ Slab.Build: buildFileInMemory :: FilePath -> RenderMode -> StmStore -> FilePath -> IO ()
+ Slab.Build: type StmStore = TVar Store
+ Slab.Error: display :: Error -> IO ()
+ Slab.Evaluate: evalExpr :: Monad m => Env -> Expr -> ExceptT Error m Expr
+ Slab.Render: renderHtmlsUtf8 :: [Html] -> ByteString
+ Slab.Run: calc :: Text -> IO ()
+ Slab.Run: eval :: Text -> IO ()
+ Slab.Run: parse :: Text -> IO ()
+ Slab.Run: render :: Text -> IO ()
+ Slab.Syntax: Application :: Expr -> Expr -> Expr
+ Slab.Syntax: Block :: Block -> Expr
+ Slab.Syntax: Bool :: Bool -> Expr
+ Slab.Syntax: BuiltIn :: Text -> Expr
+ Slab.Syntax: Cons :: Expr -> Expr -> Expr
+ Slab.Syntax: Elem :: Text -> Elem
+ Slab.Syntax: Equal :: Expr -> Expr -> Expr
+ Slab.Syntax: GreaterThan :: Expr -> Expr -> Expr
+ Slab.Syntax: LesserThan :: Expr -> Expr -> Expr
+ Slab.Syntax: displayEnv :: Env -> Text
+ Slab.Syntax: pasteBlocks :: Block -> Block -> Block
+ Slab.Syntax: setAttrs :: [Attr] -> [Block] -> [Block]
+ Slab.Syntax: setContent :: [Block] -> Block -> Block
- Slab.Command: Serve :: FilePath -> Command
+ Slab.Command: Serve :: FilePath -> FilePath -> Command
- Slab.Serve: run :: FilePath -> IO ()
+ Slab.Serve: run :: FilePath -> FilePath -> IO ()
- Slab.Syntax: BlockFragmentCall :: Text -> [Expr] -> [Block] -> Block
+ Slab.Syntax: BlockFragmentCall :: Text -> TrailingSym -> [Attr] -> [Expr] -> [Block] -> Block

Files

slab.cabal view
@@ -1,6 +1,6 @@ cabal-version:      2.2 name:               slab-version:            0.0.1.0+version:            0.0.2.0 license:            BSD-2-Clause license-file:       LICENSE copyright:          2024 Võ Minh Thu, Hypered SRL@@ -11,7 +11,7 @@ description:     Slab is an alternative syntax for writing HTML, plus some programming     language features (often found in templating languages, like conditionals-    and loops).+    and loops). You can visit the project homepage at <https://slab-lang.org>. category:           text  source-repository head@@ -59,6 +59,7 @@     , filepath     , fsnotify     , Glob+    , http-types     , megaparsec     , parser-combinators     , prettyprinter@@ -67,6 +68,7 @@     , servant     , servant-blaze     , servant-server+    , stm     , text     , transformers     , vector
src/Slab/Build.hs view
@@ -1,10 +1,26 @@+-- |+-- Module      : Slab.Build+-- Description : Build Slab templates to HTML+--+-- @Slab.Build@ provides types and functions to easily build Slab templates.+-- There are mostly two ways to build templates: by writing the resulting HTML+-- to files, or by writing them to an @STM@-based store.+--+-- Writing to disk is used by the @slab watch@ command. Writing to the @STM@+-- store is used by the @slab serve@ command. module Slab.Build   ( buildDir   , buildFile+  , StmStore+  , buildDirInMemory+  , buildFileInMemory   , listTemplates   ) where +import Control.Concurrent.STM (atomically)+import Control.Concurrent.STM qualified as STM import Data.List (sort)+import Data.Map qualified as M import Data.Text.IO qualified as T import Data.Text.Lazy.IO qualified as TL import Slab.Command qualified as Command@@ -12,6 +28,7 @@ import Slab.Evaluate qualified as Evaluate import Slab.Execute qualified as Execute import Slab.Render qualified as Render+import Slab.Syntax qualified as Syntax import System.Directory (createDirectoryIfMissing) import System.FilePath (makeRelative, replaceExtension, takeDirectory, (</>)) import System.FilePath.Glob qualified as Glob@@ -37,6 +54,39 @@         TL.writeFile path' . Render.renderHtmls $ Render.renderBlocks nodes       Command.RenderPretty ->         T.writeFile path' . Render.prettyHtmls $ Render.renderBlocks nodes++--------------------------------------------------------------------------------++type Store = M.Map FilePath [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+  templates <- listTemplates srcDir+  mapM_ (buildFileInMemory srcDir mode store) templates++buildFileInMemory :: FilePath -> Command.RenderMode -> StmStore -> FilePath -> IO ()+buildFileInMemory srcDir mode store path = do+  let path' = replaceExtension (makeRelative srcDir path) ".html"+  putStrLn $ "Building " <> path' <> "..."++  mnodes <- Execute.executeFile path+  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)+          Command.RenderPretty ->+            atomically $ STM.modifyTVar store (writeStore path' nodes)+    Left err -> Error.display err++writeStore :: FilePath -> [Syntax.Block] -> Store -> Store+writeStore path blocks = M.insert path blocks  -------------------------------------------------------------------------------- listTemplates :: FilePath -> IO [FilePath]
src/Slab/Command.hs view
@@ -26,7 +26,7 @@ data Command   = Build FilePath RenderMode FilePath   | Watch FilePath RenderMode FilePath-  | Serve FilePath+  | Serve FilePath FilePath   | Report FilePath   | -- | Generate code. Only Haskell for now.     Generate FilePath@@ -158,15 +158,19 @@  parserServe :: A.Parser Command parserServe = do+  srcDir <-+    A.argument+      A.str+      (A.metavar "DIR" <> A.action "file" <> A.help "Directory of Slab templates to build.")   distDir <-     A.strOption       ( A.long "dist"           <> A.value "./_site"           <> A.metavar "DIR"           <> A.help-            "A destination directory for the generated HTML files."+            "A directory with existing static files."       )-  pure $ Serve distDir+  pure $ Serve srcDir distDir  parserReport :: A.Parser Command parserReport = do
src/Slab/Error.hs view
@@ -1,6 +1,14 @@+-- |+-- Module      : Slab.Error+-- Description : Errors that the different Slab stages can produce+--+-- @Slab.Error@ provides a data type to represent all the errors emitted by+-- Slab. It also provides helper functions to report errors in a human-readable+-- way. module Slab.Error   ( Error (..)   , unwrap+  , display   ) where  import Data.List.NonEmpty qualified as NE (toList)@@ -30,15 +38,21 @@ -- | Extract a Right value, or die, emitting an error message. unwrap :: Either Error a -> IO a unwrap = \case-  Left (ParseError err) -> do+  Left err -> do+    display err+    exitFailure+  Right a -> pure a++display :: Error -> IO ()+display = \case+  ParseError err ->     -- Our custom function seems actually worse than errorBundlePretty.     -- T.putStrLn . parseErrorPretty $ err     T.putStrLn . T.pack $ errorBundlePretty err-    exitFailure-  Left err -> do+  EvaluateError err ->+    T.putStrLn $ "Error during evaluation: " <> err+  err ->     TL.putStrLn $ pShowNoColor err-    exitFailure-  Right a -> pure a  -------------------------------------------------------------------------------- -- Convert parse errors to a user-friendly message.
src/Slab/Evaluate.hs view
@@ -1,8 +1,23 @@ {-# LANGUAGE RecordWildCards #-} +-- |+-- Module      : Slab.Evaluate+-- Description : Evaluate an AST (to a non-reducible AST)+--+-- @Slab.Evaluate@ implements the evaluation stage of Slab, following both the+-- parsing and pre-processing stages. This is responsible of reducing for+-- instance @1 + 2@ to @3@, or transforming a loop construct to an actual list+-- of HTML blocks.+--+-- Evaluation works on an abstract syntax tree (defined in "Slab.Syntax") and+-- currently reuses the sames types for its result.+--+-- The stage following evaluation is "Slab.Execute", responsible of running+-- external commands. module Slab.Evaluate   ( evaluateFile   , evaluate+  , evalExpr   , defaultEnv   , simplify   ) where@@ -28,10 +43,75 @@  evaluateFileE :: FilePath -> ExceptT Error.Error IO [Block] evaluateFileE path =-  PreProcess.preprocessFileE path >>= evaluate defaultEnv ["toplevel"]+  PreProcess.preprocessFileE path >>= evaluate defaultEnv [T.pack path]  --------------------------------------------------------------------------------+defaultEnv :: Env+defaultEnv =+  Env+    [ ("true", Bool True)+    , ("false", Bool False)+    , ("show", BuiltIn "show")+    , ("null", BuiltIn "null")+    , mkElem "div" Div+    , mkElem "html" Html+    , mkElem "body" Body+    , mkElem "span" Span+    , mkElem "h1" H1+    , mkElem "h2" H2+    , mkElem "h3" H3+    , mkElem "h4" H4+    , mkElem "h5" H5+    , mkElem "h6" H6+    , mkElem "header" Header+    , mkElem "head" Head+    , mkElem "main" Main+    , mkElem "audio" Audio+    , mkElem "a" A+    , mkElem "code" Code+    , mkElem "iframe" IFrame+    , mkElem "i" I+    , mkElem "pre" Pre+    , mkElem "p" P+    , mkElem "ul" Ul+    , mkElem "li" Li+    , mkElem "title" Title+    , mkElem "table" Table+    , mkElem "thead" Thead+    , mkElem "tbody" Tbody+    , mkElem "tr" Tr+    , mkElem "td" Td+    , mkElem "dl" Dl+    , mkElem "dt" Dt+    , mkElem "dd" Dd+    , mkElem "footer" Footer+    , mkElem "figure" Figure+    , mkElem "form" Form+    , mkElem "label" Label+    , mkElem "blockquote" Blockquote+    , mkElem "button" Button+    , mkElem "figcaption" Figcaption+    , mkElem "script" Script+    , mkElem "style" Style+    , mkElem "small" Small+    , mkElem "svg" Svg+    , mkElem "textarea" Textarea+    , mkElem "canvas" Canvas+    , -- Elements with no content.+      ("br", Block (BlockElem Br NoSym [] []))+    , ("hr", Block (BlockElem Hr NoSym [] []))+    , ("meta", Block (BlockElem Meta NoSym [] []))+    , ("link", Block (BlockElem Link NoSym [] []))+    , ("source", Block (BlockElem Source NoSym [] []))+    , ("img", Block (BlockElem Img NoSym [] []))+    , ("input", Block (BlockElem Input NoSym [] []))+    ]+ where+  mkElem name el =+    (name, Frag ["content"] emptyEnv [BlockElem el NoSym [] [BlockDefault "content" []]]) +--------------------------------------------------------------------------------+ -- Process mixin calls. This should be done after processing the include statement -- since mixins may be defined in included files. evaluate :: Monad m => Env -> [Text] -> [Block] -> ExceptT Error.Error m [Block]@@ -43,7 +123,16 @@   mapM (eval env' stack) nodes  eval :: Monad m => Env -> [Text] -> Block -> ExceptT Error.Error m Block-eval env stack = \case+eval env stack b+  | length stack > 100 =+      throwE $+        Error.EvaluateError $+          "Stack overflow. Is there an infinite loop?"+            <> " "+            <> T.pack (show $ reverse stack)+            <> " "+            <> displayEnv env+eval env stack bl = case bl of   node@BlockDoctype -> pure node   BlockElem name mdot attrs nodes -> do     nodes' <- evaluate env stack nodes@@ -59,9 +148,10 @@       Nothing ->         pure $ BlockInclude mname path Nothing   node@(BlockFragmentDef _ _ _) -> pure node-  BlockFragmentCall name values args -> do+  BlockFragmentCall name mdot attrs values args -> do     body <- call env stack name values args-    pure $ BlockFragmentCall name 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@@ -88,7 +178,7 @@         nodes' <- evaluate env ("?block" : stack) nodes         pure $ BlockDefault name nodes'       Just (Frag _ capturedEnv nodes') -> do-        nodes'' <- evaluate capturedEnv ("+block" : stack) nodes'+        nodes'' <- evaluate capturedEnv ("default block " <> name : stack) nodes'         pure $ BlockDefault name nodes''       Just _ -> throwE $ Error.EvaluateError $ "Calling something that is not a fragment \"" <> name <> "\" in " <> T.pack (show stack)   BlockImport path _ args -> do@@ -100,17 +190,15 @@   BlockIf cond as bs -> do     cond' <- evalExpr env cond     case cond' of-      SingleQuoteString s-        | not (T.null s) -> do-            as' <- evaluate env ("then" : stack) as-            pure $ BlockIf cond as' []-      Int n-        | n /= 0 -> do-            as' <- evaluate env ("then" : stack) as-            pure $ BlockIf cond as' []-      _ -> do+      Bool True -> do+        as' <- evaluate env ("then" : stack) as+        pure $ BlockIf cond as' []+      Bool False -> do         bs' <- evaluate env ("else" : stack) bs         pure $ BlockIf cond [] bs'+      _ ->+        throwE . Error.EvaluateError $+          "Conditional is not a boolean: " <> T.pack (show cond')   BlockList nodes -> do     nodes' <- evaluate env stack nodes     pure $ BlockList nodes'@@ -121,18 +209,10 @@ call :: Monad m => Env -> [Text] -> Text -> [Expr] -> [Block] -> ExceptT Error.Error m [Block] call env stack name values args =   case lookupVariable name env of-    Just (Frag names capturedEnv body) -> do-      env' <- map (\(a, (as, b)) -> (a, Frag as env b)) <$> namedBlocks args-      let env'' = augmentVariables capturedEnv env'-          arguments = zip names (map (thunk env) values)-          env''' = augmentVariables env'' arguments-      body' <- evaluate env''' ("frag" : stack) body-      pure body'+    Just frag@(Frag _ _ _) -> evalFrag env stack name values args frag+    Just (Block x) -> pure [x]     Just _ -> throwE $ Error.EvaluateError $ "Calling something that is not a fragment \"" <> name <> "\" in " <> T.pack (show stack)-    Nothing -> throwE $ Error.EvaluateError $ "Can't find fragment \"" <> name <> "\""--defaultEnv :: Env-defaultEnv = Env [("true", Int 1), ("false", Int 0)]+    Nothing -> throwE $ Error.EvaluateError $ "Can't find fragment \"" <> name <> "\" while evaluating " <> T.pack (show $ reverse stack) <> " with environment " <> displayEnv env  lookupVariable :: Text -> Env -> Maybe Expr lookupVariable name Env {..} = lookup name envVariables@@ -140,25 +220,14 @@ augmentVariables :: Env -> [(Text, Expr)] -> Env augmentVariables Env {..} xs = Env {envVariables = xs <> envVariables} -namedBlocks :: Monad m => [Block] -> ExceptT Error.Error m [(Text, ([Text], [Block]))]-namedBlocks nodes = do-  named <- concat <$> mapM namedBlock nodes-  unnamed <- concat <$> mapM unnamedBlock nodes-  let content = if null unnamed then [] else [("content", ([], unnamed))]-  if isJust (lookup "content" named) && not (null unnamed)-    then throwE $ Error.EvaluateError $ "A block of content and a content argument are provided"-    else pure $ named <> content--namedBlock :: Monad m => Block -> ExceptT Error.Error m [(Text, ([Text], [Block]))]-namedBlock (BlockImport path (Just body) _) = pure [(T.pack path, ([], body))]-namedBlock (BlockFragmentDef name names content) = pure [(name, (names, content))]-namedBlock _ = pure []--unnamedBlock :: Monad m => Block -> ExceptT Error.Error m [Block]-unnamedBlock (BlockImport path _ args) =-  pure [BlockFragmentCall (T.pack path) [] args]-unnamedBlock (BlockFragmentDef _ _ _) = pure []-unnamedBlock node = pure [node]+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'+      arguments = zip names (map (thunk env) values)+      env''' = augmentVariables env'' arguments+  body' <- evaluate env''' ("frag " <> name : stack) body+  pure body'  evalExpr :: Monad m => Env -> Expr -> ExceptT Error.Error m Expr evalExpr env = \case@@ -172,8 +241,7 @@         -- key' <- evalExpr env key         case lookup key obj of           Just val -> evalExpr env val-          Nothing ->-            pure $ Variable "false"+          Nothing -> pure $ Bool False -- TODO Either crash, or we have to implement on option type.       Just _ -> throwE $ Error.EvaluateError $ "Variable \"" <> name <> "\" is not an object"       Nothing -> throwE $ Error.EvaluateError $ "Can't find variable \"" <> name <> "\""   Add a b -> do@@ -181,75 +249,162 @@     b' <- evalExpr env b     case (a', b') of       (Int i, Int j) -> pure . Int $ i + j-      (Int i, SingleQuoteString s) -> pure . SingleQuoteString $ T.pack (show i) <> s+      (SingleQuoteString s, SingleQuoteString t) ->+        pure . SingleQuoteString $ s <> t+      (Block a, Block b) ->+        pure . Block $ pasteBlocks a b       _ -> throwE $ Error.EvaluateError $ "Unimplemented (add): " <> T.pack (show (Add a' b'))   Sub a b -> do     a' <- evalExpr env a     b' <- evalExpr env b     case (a', b') of       (Int i, Int j) -> pure . Int $ i - j-      _ -> throwE $ Error.EvaluateError $ "Unimplemented (sub): " <> T.pack (show (Add a' b'))+      _ -> throwE $ Error.EvaluateError $ "Unimplemented (sub): " <> T.pack (show (Sub a' b'))   Times a b -> do     a' <- evalExpr env a     b' <- evalExpr env b     case (a', b') of       (Int i, Int j) -> pure . Int $ i * j-      _ -> throwE $ Error.EvaluateError $ "Unimplemented (times): " <> T.pack (show (Add a' b'))+      _ -> throwE $ Error.EvaluateError $ "Unimplemented (times): " <> T.pack (show (Times a' b'))   Divide a b -> do     a' <- evalExpr env a     b' <- evalExpr env b     case (a', b') of       (Int i, Int j) -> pure . Int $ i `div` j-      _ -> throwE $ Error.EvaluateError $ "Unimplemented (divide): " <> T.pack (show (Add a' b'))+      _ -> throwE $ Error.EvaluateError $ "Unimplemented (divide): " <> T.pack (show (Divide a' b'))+  GreaterThan a b -> do+    a' <- evalExpr env a+    b' <- evalExpr env b+    case (a', b') of+      (Int i, Int j) -> pure . Bool $ i > j+      _ -> throwE $ Error.EvaluateError $ "Unimplemented (greater-than): " <> T.pack (show (GreaterThan a' b'))+  LesserThan a b -> do+    a' <- evalExpr env a+    b' <- evalExpr env b+    case (a', b') of+      (Int i, Int j) -> pure . Bool $ i < j+      _ -> throwE $ Error.EvaluateError $ "Unimplemented (lesser-than): " <> T.pack (show (LesserThan a' b'))+  Equal a b -> do+    a' <- evalExpr env a+    b' <- evalExpr env b+    case (a', b') of+      (Bool i, Bool j) -> pure . Bool $ i == j+      (Int i, Int j) -> pure . Bool $ i == j+      (SingleQuoteString s, SingleQuoteString t) -> pure . Bool $ s == t+      _ -> throwE $ Error.EvaluateError $ "Unimplemented (equal): " <> T.pack (show (Equal a' b'))+  Cons a b -> do+    a' <- evalExpr env a+    b' <- evalExpr env b+    case (a', b') of+      (Block bl, Block c) ->+        pure . Block $ setContent [c] bl+      (Block bl, SingleQuoteString s) ->+        pure . Block $ setContent [BlockText Normal [Lit s]] bl+      _ -> throwE $ Error.EvaluateError $ "Unimplemented (cons): " <> T.pack (show (Cons a' b'))+  Application a b -> do+    a' <- evalExpr env a+    b' <- evalExpr env b+    evalApplication env a' b'   Thunk capturedEnv code ->     evalExpr capturedEnv code+  frag@(Frag _ _ _) -> do+    blocks <- evalFrag env ["frag"] "-" [] [] frag+    case blocks of+      [bl] -> pure $ Block bl+      _ -> pure . Block $ BlockList blocks+  Block b -> do+    b' <- eval env ["block"] b+    pure $ Block b'   code -> pure code --- After evaluation, the template should be either empty or contain a single literal.+evalApplication :: Monad m => Env -> Expr -> Expr -> ExceptT Error.Error m Expr+evalApplication env a b =+  case a of+    BuiltIn "show" -> case b of+      Int i -> pure . SingleQuoteString . T.pack $ show i+      _ -> throwE $ Error.EvaluateError $ "Cannot apply show to: " <> T.pack (show b)+    BuiltIn "null" -> case b of+      SingleQuoteString s -> pure . Bool $ T.null s+      -- TODO Lookup returns False when the key is not present,+      -- but I have this code around:+      --   if null entry['journal']+      -- We need something like:+      --   if 'journal' in entry+      --   if elem 'journal' (keys entry)+      --   ...+      Bool False -> pure . Bool $ True+      _ -> throwE $ Error.EvaluateError $ "Cannot apply null to: " <> T.pack (show b)+    _ -> throwE $ Error.EvaluateError $ "Cannot apply: " <> T.pack (show a)+ evalTemplate :: Monad m => Env -> [Inline] -> ExceptT Error.Error m [Inline]-evalTemplate env inlines = do-  t <- T.concat <$> traverse (evalInline env) inlines-  pure [Lit t]+evalTemplate env inlines =+  traverse (evalInline env) inlines -evalInline :: Monad m => Env -> Inline -> ExceptT Error.Error m Text+evalInline :: Monad m => Env -> Inline -> ExceptT Error.Error m Inline evalInline env = \case-  Lit s -> pure s+  Lit s -> pure $ Lit s   Place code -> do     code' <- evalExpr env code     case code' of-      SingleQuoteString s -> pure s-      Int x -> pure . T.pack $ show x+      SingleQuoteString _ -> pure $ Place code'+      Bool _ -> pure $ Place code'+      Int _ -> pure $ Place code'+      Block _ -> pure $ Place code'       -- Variable x -> context x -- Should not happen after evalExpr       x -> error $ "evalInline: unhandled value: " <> show x +-- | Same as `extractVariables` plus an implicit @content@ block.+-- Note that unlike `extractVariables`, this version takes also care of+-- passing the environment being constructed to each definition.+extractVariables' :: Monad m => Env -> [Block] -> ExceptT Error.Error m [(Text, Expr)]+extractVariables' env nodes = do+  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)+    then+      throwE $+        Error.EvaluateError $+          "A block of content and a content argument are provided"+    else pure vars++unnamedBlock :: Block -> [Block]+unnamedBlock (BlockImport path _ args) = [BlockFragmentCall (T.pack path) NoSym [] [] args]+unnamedBlock (BlockFragmentDef _ _ _) = []+unnamedBlock node = [node]+ -- Extract both fragments and assignments.--- TODO This should be merged with namedBlocks.+-- TODO This should be merged with extractVariables'. -- TODO We could filter the env, keeping only the free variables that appear -- in the bodies. extractVariables :: Env -> [Block] -> [(Text, Expr)]-extractVariables env = concatMap f- where-  f BlockDoctype = []-  f (BlockElem _ _ _ _) = []-  f (BlockText _ _) = []-  f (BlockInclude _ _ children) = maybe [] (extractVariables env) children-  f (BlockFor _ _ _ _) = []-  f (BlockFragmentDef name names children) = [(name, Frag names env children)]-  f (BlockFragmentCall _ _ _) = []-  f (BlockComment _ _) = []-  f (BlockFilter _ _) = []-  f (BlockRawElem _ _) = []-  f (BlockDefault _ _) = []-  f (BlockImport path (Just body) _) = [(T.pack path, Frag [] env body)]-  f (BlockImport _ _ _) = []-  f (BlockRun _ _) = []-  f (BlockReadJson name _ (Just val)) = [(name, jsonToExpr val)]-  f (BlockReadJson _ _ Nothing) = []-  f (BlockAssignVar name val) = [(name, val)]-  f (BlockIf _ _ _) = []-  f (BlockList _) = []-  f (BlockCode _) = []+extractVariables env = concatMap (extractVariable env) +extractVariable :: Env -> Block -> [(Text, Expr)]+extractVariable env = \case+  BlockDoctype -> []+  (BlockElem _ _ _ _) -> []+  (BlockText _ _) -> []+  (BlockInclude _ _ children) -> maybe [] (extractVariables env) children+  (BlockFor _ _ _ _) -> []+  (BlockFragmentDef name names children) -> [(name, Frag names env children)]+  (BlockFragmentCall _ _ _ _ _) -> []+  (BlockComment _ _) -> []+  (BlockFilter _ _) -> []+  (BlockRawElem _ _) -> []+  (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)]+  (BlockIf _ _ _) -> []+  (BlockList _) -> []+  (BlockCode _) -> []+ jsonToExpr :: Aeson.Value -> Expr jsonToExpr = \case   Aeson.String s -> SingleQuoteString s@@ -271,7 +426,7 @@   node@(BlockText _ _) -> [node]   BlockInclude _ _ mnodes -> maybe [] simplify mnodes   BlockFragmentDef _ _ _ -> []-  BlockFragmentCall _ _ args -> simplify args+  BlockFragmentCall _ _ _ _ args -> simplify args   BlockFor _ _ _ nodes -> simplify nodes   node@(BlockComment _ _) -> [node]   node@(BlockFilter _ _) -> [node]
src/Slab/Execute.hs view
@@ -1,6 +1,16 @@ {-# LANGUAGE MultiWayIf #-} {-# LANGUAGE RecordWildCards #-} +-- |+-- Module      : Slab.Execute+-- Description : Run external commands referenced by an AST+--+-- @Slab.Execute@ implements the execution stage of Slab, i.e. running external+-- commands (for instance referenced by the @run@ syntax). This is done after+-- the evaluation stage (implemented in "Slab.Evaluate").+--+-- After execution, the resulting blocks can be rendered to HTML by+-- "Slab.Render". module Slab.Execute   ( executeFile   , execute@@ -45,9 +55,9 @@   Syntax.BlockFragmentDef name params nodes -> do     nodes' <- execute ctx nodes     pure $ Syntax.BlockFragmentDef name params nodes'-  Syntax.BlockFragmentCall name values nodes -> do+  Syntax.BlockFragmentCall name mdot attrs values nodes -> do     nodes' <- execute ctx nodes-    pure $ Syntax.BlockFragmentCall name values nodes'+    pure $ Syntax.BlockFragmentCall name mdot attrs values nodes'   node@(Syntax.BlockFor _ _ _ _) -> pure node   node@(Syntax.BlockComment _ _) -> pure node   node@(Syntax.BlockFilter _ _) -> pure node
src/Slab/Generate/Haskell.hs view
@@ -1,3 +1,11 @@+-- |+-- Module      : Slab.Generate.Haskell+-- Description : Translate from the Slab syntax to Haskell+--+-- @Slab.Generate.Haskell@ is an attempt to translate Slab to Haskell. This+-- could make it possible to use Slab within Haskell projects without needing+-- to interpret Slab templates at runtime. If this proves useful, other+-- languages could also be supported. module Slab.Generate.Haskell   ( renderHs   ) where
src/Slab/Parse.hs view
@@ -1,5 +1,13 @@ {-# LANGUAGE RecordWildCards #-} +-- |+-- Module      : Slab.Parse+-- Description : Parse the concrete Slab syntax to an AST+--+-- @Slab.Parse@ provides parsers to read text and construct an abstract syntax+-- tree, as represented by "Slab.Syntax".+--+-- Parsers are written using the @megaparsec@ library. module Slab.Parse   ( parseFile   , parseFileE@@ -47,12 +55,12 @@ --------------------------------------------------------------------------------  parse :: FilePath -> Text -> Either (ParseErrorBundle Text Void) [Block]-parse fn = runParser (many parserNode <* eof) fn+parse fn = runParser (many parserBlock <* eof) fn  -- | We expose the expression parser for development: -- -- @---     print $ Parse.parseExpr "1 + 2 * a"+--     Parse.parseExpr "1 + 2 * a" -- @ parseExpr :: Text -> Either (ParseErrorBundle Text Void) Expr parseExpr = runParser (sc *> parserExpr <* eof) ""@@ -60,8 +68,8 @@ -------------------------------------------------------------------------------- type Parser = Parsec Void Text -parserNode :: Parser Block-parserNode = do+parserBlock :: Parser Block+parserBlock = do   node <-     L.indentBlock scn $       choice@@ -79,7 +87,7 @@         , parserRun         , parserLet         , try parserEach-        , parserIf+        , try parserIf         , parserFragmentCall         ]   case node of@@ -90,19 +98,25 @@  parserIf :: Parser (L.IndentOpt Parser Block Block) parserIf = do-  _ <- lexeme $ string "if"+  _ <- string "if"+  _ <- some (char ' ' <|> char '\t')   cond <- parserExpr-  pure $ L.IndentMany Nothing (pure . (\as -> BlockIf cond as [])) parserNode+  pure $ L.IndentMany Nothing (pure . (\as -> BlockIf cond as [])) parserBlock  parserElse :: Parser (L.IndentOpt Parser [Block] Block) parserElse = do   _ <- lexeme $ string "else"-  pure $ L.IndentMany Nothing pure parserNode+  pure $ L.IndentMany Nothing pure parserBlock  parserElement :: Parser (L.IndentOpt Parser Block Block) parserElement = do   ref <- L.indentLevel   header <- parserDiv+  parserElemBody ref header++-- | Parse the indented content of an HTML element or a fragment call.+parserElemBody :: Pos -> ([Block] -> Block) -> Parser (L.IndentOpt Parser Block Block)+parserElemBody ref header =   case trailingSym $ header [] of     HasDot -> do       template <- parseInlines@@ -124,7 +138,7 @@     NoSym -> do       template <- parseInlines       case template of-        [] -> pure $ L.IndentMany Nothing (pure . header) parserNode+        [] -> pure $ L.IndentMany Nothing (pure . header) parserBlock         _ -> pure $ L.IndentNone $ header [BlockText Normal template]  -- | Parse lines of text, indented more than `ref`.@@ -149,7 +163,7 @@             l <- p             ls <- go             let prefix = T.replicate (unPos pos - unPos ref) " "-                n' = replicate (n - 1) prefix+                n' = replicate (n - 1) ""                 l' = prefix <> l             pure $ n' <> (l' : ls) @@ -159,7 +173,7 @@ realign :: [Text] -> [Text] realign xs = map (T.drop n) xs  where-  n = minimum $ map (T.length . T.takeWhile (== ' ')) xs+  n = minimum $ map (T.length . T.takeWhile (== ' ')) $ filter (not . T.null) xs  -- | Parse multiple lines starting each with a pipe prefix. Most of our parsers -- parse a single line (and optional indented child lines) and most nodes map@@ -202,28 +216,35 @@   pure $ L.IndentNone $ BlockCode content  parserExpr :: Parser Expr-parserExpr =-  parserExpression-    <|> (Object <$> parserObject)--parserVariable :: Parser Text-parserVariable = parserName--parserExpression :: Parser Expr-parserExpression = makeExprParser pTerm operatorTable+parserExpr = makeExprParser pApp operatorTable  where+  pApp = do+    a <- pTerm+    mb <- optional $ pTerm+    case mb of+      Nothing -> pure a+      Just b -> pure $ Application a b   pTerm =     lexeme (Int <$> parserNumber)       <|> lexeme (SingleQuoteString <$> parserSingleQuoteString)+      <|> lexeme (SingleQuoteString <$> parserDoubleQuoteString) -- TODO Double       <|> lexeme parserVariable'-      <|> parens parserExpression-  parens = between (char '(') (char ')')+      <|> lexeme (Object <$> parserObject)+      <|> parens parserExpr+  parens = between (lexeme $ char '(') (lexeme $ char ')') +parserVariable :: Parser Text+parserVariable = parserName+ -- An operator table to define precedence and associativity operatorTable :: [[Operator Parser Expr]] operatorTable =   [ [InfixL (symbol "*" $> Times), InfixL (symbol "/" $> Divide)]   , [InfixL (symbol "+" $> Add), InfixL (symbol "-" $> Sub)]+  , [InfixL (symbol ">" $> GreaterThan), InfixL (symbol "<" $> LesserThan)]+  , [InfixL (symbol "==" $> Equal)]+  , -- I'd like to use : instead, but it is already used for objects...+    [InfixR (symbol "|" $> Cons)]   ]  parserVariable' :: Parser Expr@@ -254,89 +275,23 @@ -- E.g. div, div.a, div() parserElemWithAttrs :: Parser ([Block] -> Block) parserElemWithAttrs = do-  (name, attrs, mdot) <--    lexeme-      ( do-          a <- parserElem-          -- `try` because we want to backtrack if there is a dot-          -- not followed by a class name, for mdot to succeed.-          b <- many parserAttrs'-          mtrailing <--            optional $-              choice-                [ string "." >> pure HasDot-                , string "=" >> pure HasEqual-                ]-          pure (a, concat b, maybe NoSym id mtrailing)-      )-      <?> "div tag"+  (name, attrs, mdot) <- parserNameWithAttrs   pure $ BlockElem name mdot attrs +parserNameWithAttrs :: Parser (Elem, [Attr], TrailingSym)+parserNameWithAttrs =+  lexeme+    ( do+        el <- parserElem+        attrs <- concat <$> many parserAttrs'+        trailing <- parserTrailingSym+        pure (el, attrs, trailing)+    )+    <?> "element"+ parserElem :: Parser Elem parserElem =-  ( try $ do-      name <--        T.pack-          <$> ( do-                  a <- letterChar-                  as <- many (alphaNumChar <|> oneOf ("-_" :: String))-                  pure (a : as)-              )-          <?> "identifier"-      case name of-        "html" -> pure Html-        "body" -> pure Body-        "div" -> pure Div-        "span" -> pure Span-        "br" -> pure Br-        "hr" -> pure Hr-        "h1" -> pure H1-        "h2" -> pure H2-        "h3" -> pure H3-        "h4" -> pure H4-        "h5" -> pure H5-        "h6" -> pure H6-        "header" -> pure Header-        "head" -> pure Head-        "meta" -> pure Meta-        "main" -> pure Main-        "audio" -> pure Audio-        "a" -> pure A-        "code" -> pure Code-        "img" -> pure Img-        "iframe" -> pure IFrame-        "input" -> pure Input-        "i" -> pure I-        "pre" -> pure Pre-        "p" -> pure P-        "ul" -> pure Ul-        "link" -> pure Link-        "li" -> pure Li-        "title" -> pure Title-        "table" -> pure Table-        "thead" -> pure Thead-        "tbody" -> pure Tbody-        "tr" -> pure Tr-        "td" -> pure Td-        "dl" -> pure Dl-        "dt" -> pure Dt-        "dd" -> pure Dd-        "footer" -> pure Footer-        "figure" -> pure Figure-        "form" -> pure Form-        "label" -> pure Label-        "blockquote" -> pure Blockquote-        "button" -> pure Button-        "figcaption" -> pure Figcaption-        "script" -> pure Script-        "style" -> pure Style-        "small" -> pure Small-        "source" -> pure Source-        "svg" -> pure Svg-        "textarea" -> pure Textarea-        "canvas" -> pure Canvas-        _ -> fail "invalid element name"-  )+  (lexeme (string "el") *> (Elem <$> lexeme parserName))     <?> "element name"  -- E.g. .a, ()@@ -345,9 +300,9 @@   (attrs, mdot) <-     lexeme       ( do-          attrs <- some parserAttrs'+          attrs <- concat <$> some parserAttrs'           mdot <- optional (string ".")-          pure (concat attrs, maybe NoSym (const HasDot) mdot)+          pure (attrs, maybe NoSym (const HasDot) mdot)       )       <?> "attributes"   pure $ BlockElem Div mdot attrs@@ -358,6 +313,16 @@   -- not followed by a class name, for mdot to succeed.   ((: []) <$> parserId) <|> try ((: []) <$> parserClass) <|> parserAttrList +parserTrailingSym :: Parser TrailingSym+parserTrailingSym = do+  ms <-+    optional $+      choice+        [ string "." >> pure HasDot+        , string "=" >> pure HasEqual+        ]+  pure $ maybe NoSym id ms+ -- E.g. #a parserId :: Parser Attr parserId =@@ -401,14 +366,14 @@ parserSingleQuoteString :: Parser Text parserSingleQuoteString = do   _ <- string "'"-  s <- T.pack <$> (some (noneOf ("'\n" :: String))) <?> "string"+  s <- T.pack <$> (many (noneOf ("'\n" :: String))) <?> "string"   _ <- string "'"   pure s  parserDoubleQuoteString :: Parser Text parserDoubleQuoteString = do   _ <- string "\""-  s <- T.pack <$> (some (noneOf ("\"\n" :: String))) <?> "string"+  s <- T.pack <$> (many (noneOf ("\"\n" :: String))) <?> "string"   _ <- string "\""   pure s @@ -420,7 +385,7 @@ parserText = T.pack <$> lexeme (some (noneOf ['\n'])) <?> "text content"  parserIdentifier :: Parser Text-parserIdentifier = T.pack <$> lexeme (some (noneOf (" {}\n" :: String))) <?> "identifier"+parserIdentifier = T.pack <$> (some (noneOf (" .=#(){}\n" :: String))) <?> "identifier"  -------------------------------------------------------------------------------- parserInclude :: Parser (L.IndentOpt Parser Block Block)@@ -434,27 +399,38 @@   pure $ L.IndentNone $ BlockInclude mname path Nothing  parserPath :: Parser FilePath-parserPath = lexeme (some (noneOf ['\n'])) <?> "path"+parserPath = lexeme (some (noneOf ("'\"\n" :: String))) <?> "path"  -------------------------------------------------------------------------------- parserFragmentDef :: Parser (L.IndentOpt Parser Block Block) parserFragmentDef = do   _ <- lexeme (string "fragment" <|> string "frag")-  name <- parserIdentifier+  name <- lexeme parserIdentifier   params <- maybe [] id <$> optional parserParameters-  pure $ L.IndentMany Nothing (pure . BlockFragmentDef name params) parserNode+  pure $ L.IndentMany Nothing (pure . BlockFragmentDef name params) parserBlock  -- E.g. {}, {a, b} parserParameters :: Parser [Text]-parserParameters = parserList' "{" "}" parserIdentifier <?> "arguments"+parserParameters = parserList' "{" "}" (lexeme parserIdentifier) <?> "arguments"  -------------------------------------------------------------------------------- parserFragmentCall :: Parser (L.IndentOpt Parser Block Block) parserFragmentCall = do-  name <- parserIdentifier-  args <- maybe [] id <$> optional parserArguments-  pure $ L.IndentMany Nothing (pure . BlockFragmentCall name args) parserNode+  ref <- L.indentLevel+  header <- parserCall+  parserElemBody ref header +parserCall :: Parser ([Block] -> Block)+parserCall = do+  -- TODO Use parserNameWithAttrs.+  (name, attrs, trailing, args) <- lexeme $ do+    name <- parserIdentifier+    attrs <- concat <$> many parserAttrs'+    trailing <- parserTrailingSym+    args <- maybe [] id <$> optional parserArguments+    pure (name, attrs, trailing, args)+  pure $ BlockFragmentCall name trailing attrs args+ -- E.g. {}, {1, 'a'} parserArguments :: Parser [Expr] parserArguments = parserList' "{" "}" parserExpr <?> "arguments"@@ -462,7 +438,8 @@ -------------------------------------------------------------------------------- parserEach :: Parser (L.IndentOpt Parser Block Block) parserEach = do-  _ <- lexeme (string "for")+  _ <- string "for"+  _ <- some (char ' ' <|> char '\t')   name <- lexeme parserName   mindex <- optional $ do     _ <- lexeme $ string ","@@ -470,7 +447,7 @@   _ <- lexeme (string "in")   collection <-     (List <$> parserList) <|> (Object <$> parserObject) <|> (Variable <$> parserVariable)-  pure $ L.IndentMany Nothing (pure . BlockFor name mindex collection) parserNode+  pure $ L.IndentMany Nothing (pure . BlockFor name mindex collection) parserBlock  parserList :: Parser [Expr] parserList = parserList' "[" "]" parserExpr@@ -562,7 +539,7 @@ parserRawElement :: Parser (L.IndentOpt Parser Block Block) parserRawElement = do   header <- parserAngleBracket-  pure $ L.IndentMany Nothing (pure . header) parserNode+  pure $ L.IndentMany Nothing (pure . header) parserBlock  parserAngleBracket :: Parser ([Block] -> Block) parserAngleBracket = do@@ -575,14 +552,14 @@ parserDefault = do   _ <- lexeme (string "default")   name <- parserText-  pure $ L.IndentMany Nothing (pure . BlockDefault name) parserNode+  pure $ L.IndentMany Nothing (pure . BlockDefault name) parserBlock  -------------------------------------------------------------------------------- parserImport :: Parser (L.IndentOpt Parser Block Block) parserImport = do   _ <- lexeme (string "import")   path <- parserPath-  pure $ L.IndentMany Nothing (pure . BlockImport path Nothing) parserNode+  pure $ L.IndentMany Nothing (pure . BlockImport path Nothing) parserBlock  -------------------------------------------------------------------------------- parserRun :: Parser (L.IndentOpt Parser Block Block)@@ -604,7 +581,7 @@  parserAssignVar :: Text -> Parser (L.IndentOpt Parser Block Block) parserAssignVar name = do-  val <- lexeme parserValue+  val <- parserExpr   pure $ L.IndentNone $ BlockAssignVar name val  parserReadJson :: Text -> Parser (L.IndentOpt Parser Block Block)@@ -637,12 +614,15 @@  -- Text interpolation modeled on the @template@ library by Johan Tibell. -- - This uses Megaparsec instead of an internal State monad parser.--- - This uses @#@ instead of @$@.+-- - This uses @#@ instead of @$@, and both @()@ and @{}@.+--   - @()@ uses the expression syntax.+--   - @{}@ uses the block syntax (although limited). -- - Only the safe parsers are provided. -- - Only the applicative interface is provided.--- - We don't support #name, but only #{name}, because # can appear+-- - We don't support #name, but only #(name), because # can appear --   in character entities, URLs, ... -- TODO Mention the BSD-3 license and Johan.+-- TODO Actually support #name.  -------------------------------------------------------------------------------- @@ -684,31 +664,61 @@ -------------------------------------------------------------------------------- -- Template parser +-- | Record whether we are parsing a template within the normal block syntax,+-- or within an inline block syntax (introduced by #{...}). This is allow a+-- closing curly bracket in the normal case without requiring to escape it, and+-- disallowing it in the inline case (since it is used to end the inline case).+data InlineContext = NormalBlock | InlineBlock+ parseInlines :: Parser [Inline]-parseInlines = combineLits <$> M.many parseInline+parseInlines = parseInlines' NormalBlock -parseInline :: Parser Inline-parseInline =+parseInlines' :: InlineContext -> Parser [Inline]+parseInlines' ctx = combineLits <$> M.many (parseInline ctx)++parseInline :: InlineContext -> Parser Inline+parseInline ctx =   M.choice-    [ parseLit-    , parsePlace+    [ parseLit ctx+    , parsePlaceExpr+    , parsePlaceBlock     , parseEscape     , parseSharpLit     ]  -- TODO The \n condition could be optional if we want this module to be useful -- outside Slab.-parseLit :: Parser Inline-parseLit = do-  s <- M.takeWhile1P (Just "literal") (\c -> c /= '#' && c /= '\n')+parseLit :: InlineContext -> Parser Inline+parseLit ctx = do+  s <- case ctx of+    NormalBlock -> M.takeWhile1P (Just "literal") (\c -> c /= '#' && c /= '\n')+    InlineBlock -> M.takeWhile1P (Just "literal") (\c -> c /= '#' && c /= '\n' && c /= '}')   pure $ Lit s -parsePlace :: Parser Inline-parsePlace = do-  _ <- string $ T.pack "#{"+parsePlaceExpr :: Parser Inline+parsePlaceExpr = do+  _ <- string $ T.pack "#("   e <- parserExpr+  _ <- string $ T.pack ")"+  pure $ Place e++parsePlaceBlock :: Parser Inline+parsePlaceBlock = do+  _ <- string $ T.pack "#{"+  e <- Block <$> parseInlineBlock   _ <- string $ T.pack "}"   pure $ Place e++-- | Equivalent to `parserBlock` but in an inline context.+parseInlineBlock :: Parser Block+parseInlineBlock = do+  header <- parserDiv <|> parserCall+  template <- parseInlines' InlineBlock+  -- Don't return anything of the template is empty. to avoid a newline when+  -- rendering.+  if null template+    then pure $ header []+    else pure $ header [BlockText Dot template]  parseEscape :: Parser Inline parseEscape = do
src/Slab/PreProcess.hs view
@@ -1,6 +1,13 @@ {-# LANGUAGE MultiWayIf #-} {-# LANGUAGE RecordWildCards #-} +-- |+-- Module      : Slab.PreProcess+-- Description : Parse and process included and imported files+--+-- @Slab.PreProcess@ recursively parses files, following includes and imports.+-- This is also responsible of reading JSON files referenced in the expression+-- language. module Slab.PreProcess   ( Context (..)   , preprocessFile@@ -72,9 +79,9 @@   BlockFragmentDef name params nodes -> do     nodes' <- preprocess ctx nodes     pure $ BlockFragmentDef name params nodes'-  BlockFragmentCall name values nodes -> do+  BlockFragmentCall name mdot attrs values nodes -> do     nodes' <- preprocess ctx nodes-    pure $ BlockFragmentCall name values nodes'+    pure $ BlockFragmentCall name mdot attrs values nodes'   node@(BlockFor _ _ _ _) -> pure node   node@(BlockComment _ _) -> pure node   node@(BlockFilter _ _) -> pure node
src/Slab/Render.hs view
@@ -1,9 +1,17 @@+-- |+-- Module      : Slab.Render+-- Description : Render an AST to HTML+--+-- @Slab.Render@ transforms an evaluated syntax tree to HTML. In practice this+-- can be @blaze-html@'s @Html@, or @Text@ and lazy @ByteString@. module Slab.Render   ( prettyHtmls   , renderHtmls+  , renderHtmlsUtf8   , renderBlocks   ) where +import Data.ByteString.Lazy qualified as BSL import Data.String (fromString) import Data.Text (Text) import Data.Text qualified as T@@ -11,9 +19,11 @@ import Slab.Syntax qualified as Syntax import Text.Blaze.Html.Renderer.Pretty qualified as Pretty (renderHtml) import Text.Blaze.Html.Renderer.Text (renderHtml)+import Text.Blaze.Html.Renderer.Utf8 qualified as Utf8 (renderHtml) import Text.Blaze.Html5 (Html, (!)) import Text.Blaze.Html5 qualified as H import Text.Blaze.Html5.Attributes qualified as A+import Text.Blaze.Internal qualified as I import Text.Blaze.Svg11 qualified as S  --------------------------------------------------------------------------------@@ -23,6 +33,9 @@ renderHtmls :: [Html] -> TL.Text renderHtmls = TL.concat . map renderHtml +renderHtmlsUtf8 :: [Html] -> BSL.ByteString+renderHtmlsUtf8 = BSL.concat . map Utf8.renderHtml+ -------------------------------------------------------------------------------- renderBlocks :: [Syntax.Block] -> [H.Html] renderBlocks = map renderBlock@@ -62,16 +75,17 @@   attrs' = Syntax.namesFromAttrs attrs renderBlock (Syntax.BlockText _ []) =   H.preEscapedText "\n" -- This allows to force some whitespace.-renderBlock (Syntax.BlockText _ [Syntax.Lit s])-  | s == T.empty = H.preEscapedText "\n" -- This allows to force some whitespace.-  | otherwise = H.preEscapedText s -- TODO-renderBlock (Syntax.BlockText _ _) = error "Template is not rendered."+renderBlock (Syntax.BlockText _ t) =+  let s = renderTemplate t+   in if s == T.empty+        then H.preEscapedText "\n" -- This allows to force some whitespace.+        else H.preEscapedText s -- TODO renderBlock (Syntax.BlockInclude (Just "escape-html") _ (Just nodes)) =   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.BlockFragmentCall _ _ nodes) = mapM_ renderBlock nodes+renderBlock (Syntax.BlockFragmentCall _ _ _ _ nodes) = mapM_ renderBlock nodes renderBlock (Syntax.BlockFor _ _ _ nodes) = mapM_ renderBlock nodes renderBlock (Syntax.BlockComment b content) =   case b of@@ -106,6 +120,7 @@   H.string $ show i renderBlock (Syntax.BlockCode (Syntax.Object _)) =   H.text "<Object>"+renderBlock (Syntax.BlockCode (Syntax.Block x)) = renderBlock x renderBlock (Syntax.BlockCode c) = error $ "renderBlock called on BlockCode " <> show c  renderTexts :: [Syntax.Block] -> H.Html@@ -127,7 +142,7 @@   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.BlockFragmentCall _ _ _) = error "extractTexts called on a BlockFragmentCall"+  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"   f (Syntax.BlockFilter _ _) = error "extractTexts called on a BlockFilter"@@ -141,6 +156,23 @@   f (Syntax.BlockList _) = error "extractTexts called on a BlockList"   f (Syntax.BlockCode _) = error "extractTexts called on a BlockCode" +-- After evaluation, we should have only reduced values (e.g. no variables).+renderTemplate :: [Syntax.Inline] -> Text+renderTemplate inlines =+  let t = map renderInline inlines+   in T.concat t++renderInline :: Syntax.Inline -> Text+renderInline = \case+  Syntax.Lit s -> s+  Syntax.Place code -> do+    case code of+      Syntax.SingleQuoteString s -> s+      Syntax.Bool x -> T.pack $ show x+      Syntax.Int x -> T.pack $ show x+      Syntax.Block b -> TL.toStrict . renderHtml $ renderBlock b+      x -> error $ "renderInline: unhandled value: " <> show x+ renderElem :: Syntax.Elem -> Html -> Html renderElem = \case   Syntax.Html -> H.html@@ -194,3 +226,4 @@   Syntax.Svg -> S.svg   Syntax.Textarea -> H.textarea   Syntax.Canvas -> H.canvas+  Syntax.Elem name -> I.customParent (H.textTag name)
src/Slab/Report.hs view
@@ -1,3 +1,9 @@+-- |+-- Module      : Slab.Report+-- Description : Report information about Slab templates (mostly empty for now)+--+-- 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   ) where@@ -28,6 +34,7 @@  -------------------------------------------------------------------------------- -- 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@.  buildDir :: FilePath -> IO [Module] buildDir srcDir = do
src/Slab/Run.hs view
@@ -6,9 +6,15 @@ -- runs them. module Slab.Run   ( run+  , parse+  , eval+  , render+  , calc   ) where -import Control.Monad.Trans.Except (ExceptT, runExceptT)+import Control.Monad.Trans.Except (ExceptT, except, runExceptT, withExceptT)+import Data.Text (Text)+import Data.Text qualified as T import Data.Text.IO qualified as T import Data.Text.Lazy.IO qualified as TL import Slab.Build qualified as Build@@ -24,14 +30,14 @@ import Slab.Serve qualified as Serve import Slab.Syntax qualified as Syntax import Slab.Watch qualified as Watch-import Text.Pretty.Simple (pShowNoColor)+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.Serve distDir) = Serve.run distDir+run (Command.Serve srcDir distDir) = Serve.run srcDir distDir run (Command.Report srcDir) = Report.run srcDir run (Command.Generate path) = Generate.renderHs path run (Command.CommandWithPath path pmode (Command.Render Command.RenderNormal)) = do@@ -98,7 +104,7 @@   -> ExceptT Error.Error IO [Syntax.Block] evaluateWithModeE path pmode = do   parsed <- parseWithModeE path pmode-  Evaluate.evaluate Evaluate.defaultEnv ["toplevel"] parsed+  Evaluate.evaluate Evaluate.defaultEnv [T.pack path] parsed  executeWithModeE   :: FilePath@@ -106,3 +112,52 @@   -> ExceptT Error.Error IO [Syntax.Block] executeWithModeE path pmode =   evaluateWithModeE path pmode >>= Execute.execute path++--------------------------------------------------------------------------------+-- Play with the whole language.++parse :: Text -> IO ()+parse s = do+  blocks <- runExceptT $ withExceptT Error.ParseError . except $ Parse.parse "-" s+  pPrintNoColor blocks++-- | "eval" parses a string as a "Syntax.Syntax", and evaluates it. This doesn't+-- run the proprocessing stage.+--+-- @+--     Run.eval "p= 1 + 2 * 3"+-- @+eval :: Text -> IO ()+eval s = do+  x <- runExceptT $ parseAndEvaluateBlocks s+  pPrintNoColor x++-- | Run "eval" and render the result.+render :: Text -> IO ()+render s = do+  x <- runExceptT (parseAndEvaluateBlocks s) >>= Error.unwrap+  T.putStr . Render.prettyHtmls $ Render.renderBlocks x++parseAndEvaluateBlocks :: Text -> ExceptT Error.Error IO [Syntax.Block]+parseAndEvaluateBlocks s = do+  blocks <- withExceptT Error.ParseError . except $ Parse.parse "-" s+  Evaluate.evaluate Evaluate.defaultEnv [] blocks++--------------------------------------------------------------------------------+-- Play with the expression language.++-- | "calc" parses a string as a "Syntax.Expr", and evaluates it. I.e. it+-- doens't use the fragment syntax, or imports and includes.+--+-- @+--     Run.calc "1 + 2 * 3"+-- @+calc :: Text -> IO ()+calc s = do+  x <- runExceptT $ parseAndEvaluateExpr s+  pPrintNoColor x++parseAndEvaluateExpr :: Text -> ExceptT Error.Error IO Syntax.Expr+parseAndEvaluateExpr s = do+  expr <- withExceptT Error.ParseError . except $ Parse.parseExpr s+  Evaluate.evalExpr Evaluate.defaultEnv expr
src/Slab/Serve.hs view
@@ -1,33 +1,55 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE NoImplicitPrelude #-} +-- |+-- Module      : Slab.Serve+-- 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. module Slab.Serve   ( run   ) where +import Control.Concurrent.STM qualified as STM+import Data.Map qualified as M+import Data.Text qualified as T+import Network.HTTP.Types (status200) import Network.Wai qualified as Wai import Network.Wai.Handler.Warp qualified as Warp import Protolude hiding (Handler) import Servant hiding (serve) 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.Render qualified as Render+import Slab.Watch qualified as Watch import Text.Blaze.Html5 (Html) import WaiAppStatic.Storage.Filesystem   ( defaultWebAppSettings   )  -------------------------------------------------------------------------------run :: FilePath -> IO ()-run distDir =-  Warp.run 9000 $ serve distDir+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.+  _ <-+    forkIO $+      Watch.run srcDir (Build.buildFileInMemory srcDir Command.RenderNormal store)+  Warp.run 9000 $ serve distDir store  -- | Turn our `serverT` implementation into a Wai application, suitable for -- Warp.run.-serve :: FilePath -> Wai.Application-serve root =+serve :: FilePath -> Build.StmStore -> Wai.Application+serve root store =   Servant.serveWithContext appProxy Server.EmptyContext $     Server.hoistServerWithContext appProxy settingsProxy identity $-      serverT root+      serverT root store  ------------------------------------------------------------------------------ type ServerSettings = '[]@@ -38,20 +60,44 @@ ------------------------------------------------------------------------------ type App =   "hello" :> Get '[B.HTML] Html-    :<|> Servant.Raw -- Fallback handler for the static files, in particular the+    :<|> Servant.Raw -- Fallback handler for the static files.  appProxy :: Proxy App appProxy = Proxy  -------------------------------------------------------------------------------serverT :: FilePath -> ServerT App Handler-serverT root =+serverT :: FilePath -> Build.StmStore -> ServerT App Handler+serverT root store =   showHelloPage-    :<|> serveStatic root+    :<|> app root store  ------------------------------------------------------------------------------ showHelloPage :: Handler Html showHelloPage = pure "Hello."++------------------------------------------------------------------------------++-- | Try to serve a built page, and fallback to static files if the page+-- doesn't exist.+app :: FilePath -> Build.StmStore -> Server.Tagged Handler Server.Application+app root store = Tagged $ \req sendRes -> app' root store req sendRes++app' :: FilePath -> Build.StmStore -> Application+app' root store req sendRes = do+  templates <- liftIO . atomically $ STM.readTVar store+  let path = T.intercalate "/" $ Wai.pathInfo req+      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)+    Nothing -> do+      let Tagged staticApp = serveStatic root+      staticApp req sendRes  ------------------------------------------------------------------------------ serveStatic :: FilePath -> Server.Tagged Handler Server.Application
src/Slab/Syntax.hs view
@@ -1,8 +1,17 @@ {-# LANGUAGE RecordWildCards #-} +-- |+-- Module      : Slab.Syntax+-- Description : The abstract syntax used by Slab+--+-- @Slab.Syntax@ provides data types to represent the syntax used by the Slab+-- language. It also provides small helpers functions to operate on the syntax. module Slab.Syntax   ( Block (..)   , isDoctype+  , pasteBlocks+  , setAttrs+  , setContent   , CommentType (..)   , Elem (..)   , TrailingSym (..)@@ -12,6 +21,7 @@   , Inline (..)   , Env (..)   , emptyEnv+  , displayEnv   , trailingSym   , freeVariables   , thunk@@ -42,7 +52,7 @@   | -- | 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]-  | BlockFragmentCall Text [Expr] [Block]+  | BlockFragmentCall Text TrailingSym [Attr] [Expr] [Block]   | BlockFor Text (Maybe Text) Expr [Block]   | -- TODO Should we allow string interpolation here ?     BlockComment CommentType Text@@ -70,8 +80,29 @@  trailingSym :: Block -> TrailingSym trailingSym (BlockElem _ sym _ _) = sym+trailingSym (BlockFragmentCall _ sym _ _ _) = sym trailingSym _ = NoSym +-- | Takes two blocks and returns a BlockList containing both, but peel the+-- outer list of a and b if they are themselves BlockList.+pasteBlocks :: Block -> Block -> Block+pasteBlocks a b = BlockList $ peel a <> peel b+ where+  peel (BlockList xs) = xs+  peel x = [x]++-- | Set attrs on a the first block, if it is a BlockElem.+setAttrs :: [Attr] -> [Block] -> [Block]+setAttrs attrs (BlockElem name mdot attrs' nodes : bs) =+  BlockElem name mdot (attrs' <> attrs) nodes : bs+setAttrs _ bs = bs++-- | Set the content on a block, if it is a BlockElem.+setContent :: [Block] -> Block -> Block+setContent nodes (BlockElem name mdot attrs _) =+  BlockElem name mdot attrs nodes+setContent _ b = b+ -- | A "passthrough" comment will be included in the generated HTML. data CommentType = NormalComment | PassthroughComment   deriving (Show, Eq)@@ -128,6 +159,8 @@   | Svg   | Textarea   | Canvas+  | -- | Arbitrary element name, using the @el@ keyword.+    Elem Text   deriving (Show, Eq)  data TrailingSym = HasDot | HasEqual | NoSym@@ -155,21 +188,31 @@ -- | Simple expression language. data Expr   = Variable Text+  | Bool Bool   | Int Int   | SingleQuoteString Text   | List [Expr]   | Object [(Expr, Expr)]   | -- The object[key] lookup. This is quite restrive as a start.     Lookup Text Expr+  | Application Expr Expr   | Add Expr Expr   | Sub Expr Expr   | Times Expr Expr   | Divide Expr Expr+  | GreaterThan Expr Expr+  | LesserThan Expr Expr+  | Equal Expr Expr+  | -- Not really a cons for lists, but instead to add content to an element.+    -- E.g. p : "Hello."+    Cons Expr Expr+  | Block Block   | -- Expr can be a fragment, so we can manipulate them with code later.     -- We also capture the current environment.     Frag [Text] Env [Block]   | -- Same for Expr instead of Block.     Thunk Env Expr+  | BuiltIn Text   deriving (Show, Eq)  -- | A representation of a 'Data.Text' template is a list of Inline, supporting@@ -187,11 +230,22 @@ emptyEnv :: Env emptyEnv = Env [] +-- Similar to `show`, but makes the environment capture by "Frag" and "Thunk"+-- empty to avoid an infinite data structure.+displayEnv :: Env -> Text+displayEnv = T.pack . show . map (\(a, b) -> (a, f b)) . envVariables+ where+  f = \case+    Frag names _ children -> Frag names emptyEnv children+    Thunk _ expr -> Thunk emptyEnv expr+    expr -> expr+ -------------------------------------------------------------------------------- freeVariables :: Expr -> [Text] freeVariables =   nub . \case     Variable a -> [a]+    Bool _ -> []     Int _ -> []     SingleQuoteString _ -> []     List as -> concatMap freeVariables as@@ -222,7 +276,7 @@   f (BlockText _ _) = []   f (BlockInclude _ _ children) = maybe [] extractClasses children   f (BlockFragmentDef _ _ _) = [] -- We extract them in BlockFragmentCall instead.-  f (BlockFragmentCall _ _ children) = extractClasses children+  f (BlockFragmentCall _ _ attrs _ children) = concatMap g attrs <> extractClasses children   f (BlockFor _ _ _ children) = extractClasses children   f (BlockComment _ _) = []   f (BlockFilter _ _) = []@@ -258,7 +312,8 @@   f (BlockText _ _) = []   f (BlockInclude _ _ children) = maybe [] extractFragments children   f (BlockFragmentDef name _ children) = [BlockFragmentDef' name children]-  f (BlockFragmentCall name _ children) = [BlockFragmentCall' name] <> extractFragments children+  f (BlockFragmentCall name _ _ _ children) =+    [BlockFragmentCall' name] <> extractFragments children   f (BlockFor _ _ _ children) = extractFragments children   f (BlockComment _ _) = []   f (BlockFilter _ _) = []
src/Slab/Watch.hs view
@@ -1,3 +1,9 @@+-- |+-- Module      : Slab.Watch+-- Description : Continuously build a set of Slab templates+--+-- @Slab.Watch@ watches a set of Slab templates, continuously rebuilding them+-- as they change. module Slab.Watch   ( run   ) where
tests/Slab/GHCi.hs view
@@ -8,9 +8,11 @@   (   ) where +import Data.Text (Text) import Data.Text qualified as T import Slab.Build qualified as Build import Slab.Command qualified as Command+import Slab.Error qualified as Error import Slab.Evaluate qualified as Evaluate import Slab.Execute qualified as Execute import Slab.Generate.Haskell qualified as Generate