packages feed

patat 0.12.0.1 → 0.13.0.0

raw patch · 16 files changed

+362/−136 lines, 16 filesdep +goldplatedep +hashabledep ~basePVP ok

version bump matches the API change (PVP)

Dependencies added: goldplate, hashable

Dependency ranges changed: base

API changes (from Hackage documentation)

- Patat.Eval: eval :: Presentation -> IO Presentation
- Patat.Eval: instance GHC.Show.Show Patat.Eval.EvalResult
+ Patat.Eval: evalActiveVars :: (Var -> [Block] -> IO ()) -> Presentation -> IO Presentation
+ Patat.Eval: evalAllVars :: Presentation -> IO Presentation
+ Patat.Eval: evalVar :: Var -> ([Block] -> IO ()) -> Presentation -> IO Presentation
+ Patat.Eval: parseEvalBlocks :: Presentation -> Presentation
+ Patat.Eval.Internal: EvalBlock :: !EvalSettings -> !Attr -> !Text -> !Maybe (Async ()) -> EvalBlock
+ Patat.Eval.Internal: [ebAsync] :: EvalBlock -> !Maybe (Async ())
+ Patat.Eval.Internal: [ebAttr] :: EvalBlock -> !Attr
+ Patat.Eval.Internal: [ebInput] :: EvalBlock -> !Text
+ Patat.Eval.Internal: [ebSettings] :: EvalBlock -> !EvalSettings
+ Patat.Eval.Internal: data EvalBlock
+ Patat.Eval.Internal: renderEvalBlock :: EvalBlock -> Text -> [Block]
+ Patat.Eval.Internal: type EvalBlocks = HashMap Var EvalBlock
+ Patat.Presentation: UpdateVar :: Var -> [Block] -> PresentationCommand
+ Patat.Presentation: [pEvalBlocks] :: Presentation -> !EvalBlocks
+ Patat.Presentation: [pVarGen] :: Presentation -> !VarGen
+ Patat.Presentation: [pVars] :: Presentation -> !HashMap Var [Block]
+ Patat.Presentation: activeVars :: Presentation -> HashSet Var
+ Patat.Presentation: data Var
+ Patat.Presentation: data VarGen
+ Patat.Presentation: zeroVarGen :: VarGen
+ Patat.Presentation.Instruction: AppendVar :: Var -> Instruction a
+ Patat.Presentation.Instruction: beforePause :: Int -> Instructions a -> Instructions a
+ Patat.Presentation.Instruction: data Var
+ Patat.Presentation.Instruction: data VarGen
+ Patat.Presentation.Instruction: freshVar :: VarGen -> (Var, VarGen)
+ Patat.Presentation.Instruction: instance Data.Hashable.Class.Hashable Patat.Presentation.Instruction.Var
+ Patat.Presentation.Instruction: instance GHC.Classes.Eq Patat.Presentation.Instruction.Var
+ Patat.Presentation.Instruction: instance GHC.Classes.Ord Patat.Presentation.Instruction.Var
+ Patat.Presentation.Instruction: instance GHC.Show.Show Patat.Presentation.Instruction.Var
+ Patat.Presentation.Instruction: instance GHC.Show.Show Patat.Presentation.Instruction.VarGen
+ Patat.Presentation.Instruction: variables :: Instructions a -> HashSet Var
+ Patat.Presentation.Instruction: zeroVarGen :: VarGen
+ Patat.Presentation.Interactive: UpdateVar :: Var -> [Block] -> PresentationCommand
+ Patat.Presentation.Internal: [evalStderr] :: EvalSettings -> !Bool
+ Patat.Presentation.Internal: [pEvalBlocks] :: Presentation -> !EvalBlocks
+ Patat.Presentation.Internal: [pVarGen] :: Presentation -> !VarGen
+ Patat.Presentation.Internal: [pVars] :: Presentation -> !HashMap Var [Block]
+ Patat.Presentation.Internal: activeVars :: Presentation -> HashSet Var
+ Patat.Presentation.Internal: updateVar :: Var -> [Block] -> Presentation -> Presentation
+ Patat.Presentation.Settings: [evalStderr] :: EvalSettings -> !Bool
- Patat.Presentation: Presentation :: !FilePath -> !EncodingFallback -> ![Inline] -> ![Inline] -> !PresentationSettings -> !Seq Slide -> !Seq Breadcrumbs -> !Seq PresentationSettings -> !Seq (Maybe TransitionGen) -> !Index -> !SyntaxMap -> Presentation
+ Patat.Presentation: Presentation :: !FilePath -> !EncodingFallback -> ![Inline] -> ![Inline] -> !PresentationSettings -> !Seq Slide -> !Seq Breadcrumbs -> !Seq PresentationSettings -> !Seq (Maybe TransitionGen) -> !Index -> !SyntaxMap -> !EvalBlocks -> !VarGen -> !HashMap Var [Block] -> Presentation
- Patat.Presentation: readPresentation :: FilePath -> IO (Either String Presentation)
+ Patat.Presentation: readPresentation :: VarGen -> FilePath -> IO (Either String Presentation)
- Patat.Presentation.Instruction: renderFragment :: Int -> Instructions Block -> Fragment
+ Patat.Presentation.Instruction: renderFragment :: (Var -> [Block]) -> Instructions Block -> Fragment
- Patat.Presentation.Internal: EvalSettings :: !Text -> !Bool -> !Bool -> !EvalSettingsContainer -> EvalSettings
+ Patat.Presentation.Internal: EvalSettings :: !Text -> !Bool -> !Bool -> !EvalSettingsContainer -> !Bool -> EvalSettings
- Patat.Presentation.Internal: Presentation :: !FilePath -> !EncodingFallback -> ![Inline] -> ![Inline] -> !PresentationSettings -> !Seq Slide -> !Seq Breadcrumbs -> !Seq PresentationSettings -> !Seq (Maybe TransitionGen) -> !Index -> !SyntaxMap -> Presentation
+ Patat.Presentation.Internal: Presentation :: !FilePath -> !EncodingFallback -> ![Inline] -> ![Inline] -> !PresentationSettings -> !Seq Slide -> !Seq Breadcrumbs -> !Seq PresentationSettings -> !Seq (Maybe TransitionGen) -> !Index -> !SyntaxMap -> !EvalBlocks -> !VarGen -> !HashMap Var [Block] -> Presentation
- Patat.Presentation.Read: readPresentation :: FilePath -> IO (Either String Presentation)
+ Patat.Presentation.Read: readPresentation :: VarGen -> FilePath -> IO (Either String Presentation)
- Patat.Presentation.Settings: EvalSettings :: !Text -> !Bool -> !Bool -> !EvalSettingsContainer -> EvalSettings
+ Patat.Presentation.Settings: EvalSettings :: !Text -> !Bool -> !Bool -> !EvalSettingsContainer -> !Bool -> EvalSettings

Files

CHANGELOG.md view
@@ -1,5 +1,25 @@ # Changelog +## 0.13.0.0 (2024-10-30)++ *  Incrementally display output of `eval` commands (#132)++    Rather than waiting for the process to complete and then displaying its+    output, `patat` now fetches the `stdout` and `stderr` as it becomes+    available and refreshes the display.++    This means that by default, **stderr is now displayed as well**.+    To disable displaying `stderr`, you can add `stderr: false` to the eval+    configuration, e.g.:++    ```yaml+    patat:+      eval:+        bash:+          command: bash+          stderr: false+    ```+ ## 0.12.0.1 (2024-09-28)   *  Fix width of code blocks when using wide characters (#171)
README.md view
@@ -81,31 +81,11 @@  ### From source -Installation from source is very easy.  You can build from source using `stack-install` or `cabal install`.  `patat` is also available from [Hackage].--[Hackage]: https://hackage.haskell.org/package/patat--For people unfamiliar with the Haskell ecosystem, this means you can do either-of the following:--#### Using stack--1. Install [stack] for your platform.-2. Clone this repository.-3. Run `stack setup` (if you're running stack for the first time) and-   `stack install`.-4. Make sure `$HOME/.local/bin` is in your `$PATH`.--[stack]: https://docs.haskellstack.org/en/stable/README/--#### Using cabal--1. Install [cabal] for your platform.-2. Run `cabal install patat`.-3. Make sure `$HOME/.cabal/bin` is in your `$PATH`.+`patat` is also available from [Hackage].  You can install it using [cabal] by+running `cabal install patat`.  [cabal]: https://www.haskell.org/cabal/+[Hackage]: https://hackage.haskell.org/package/patat  Running -------@@ -740,7 +720,7 @@ code of presentations downloaded from the internet before running them if they contain `eval` settings. -Aside from the command, there are three more options:+Aside from the command, there are four more options:   -  `fragment`: Introduce a pause (see [fragments](#fragmented-slides)) in     between showing the original code block and the output.  Defaults to `true`.@@ -752,6 +732,7 @@      *  `code`: the default setting.      *  `none`: no formatting applied.      *  `inline`: no formatting applied and no trailing newline.+ -  `stderr`: Include output from standard error.  Defaults to `true`.  -  `wrap`: this is a deprecated name for `container`, used in version 0.11 and     earlier. 
lib/Patat/Eval.hs view
@@ -2,34 +2,48 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards   #-} module Patat.Eval-    ( eval+    ( parseEvalBlocks++    , evalVar+    , evalActiveVars+    , evalAllVars     ) where   -------------------------------------------------------------------------------- import qualified Control.Concurrent.Async       as Async-import           Control.Exception              (finally)+import           Control.Exception              (IOException, catch, finally)+import           Control.Monad                  (foldM, when)+import           Control.Monad.State            (StateT, runStateT, state)+import           Control.Monad.Writer           (Writer, runWriter, tell)+import           Data.Foldable                  (for_) import qualified Data.HashMap.Strict            as HMS+import qualified Data.IORef                     as IORef+import           Data.List                      (foldl') import           Data.Maybe                     (maybeToList) import qualified Data.Text                      as T import qualified Data.Text.IO                   as T+import           Patat.Eval.Internal import           Patat.Presentation.Instruction import           Patat.Presentation.Internal-import           Patat.Presentation.Settings import           System.Exit                    (ExitCode (..)) import qualified System.IO                      as IO-import           System.IO.Unsafe               (unsafeInterleaveIO) import qualified System.Process                 as Process import qualified Text.Pandoc.Definition         as Pandoc   ---------------------------------------------------------------------------------eval :: Presentation -> IO Presentation-eval presentation = case psEval (pSettings presentation) of-    Nothing -> pure presentation-    Just settings -> do-        slides <- traverse (evalSlide settings) (pSlides presentation)-        pure presentation {pSlides = slides}+parseEvalBlocks :: Presentation -> Presentation+parseEvalBlocks presentation =+    let ((pres, varGen), evalBlocks) = runWriter $+            runStateT work (pVarGen presentation) in+    pres {pEvalBlocks = evalBlocks, pVarGen = varGen}+  where+    work = case psEval (pSettings presentation) of+        Nothing -> pure presentation+        Just settings -> do+            slides <- traverse (evalSlide settings) (pSlides presentation)+            pure presentation {pSlides = slides}   --------------------------------------------------------------------------------@@ -40,7 +54,13 @@   ---------------------------------------------------------------------------------evalSlide :: EvalSettingsMap -> Slide -> IO Slide+-- | Monad used for identifying and extracting the evaluation blocks from a+-- presentation.+type ExtractEvalM a = StateT VarGen (Writer (HMS.HashMap Var EvalBlock)) a+++--------------------------------------------------------------------------------+evalSlide :: EvalSettingsMap -> Slide -> ExtractEvalM Slide evalSlide settings slide = case slideContent slide of     TitleSlide _ _ -> pure slide     ContentSlide instrs0 -> do@@ -51,40 +71,35 @@ -------------------------------------------------------------------------------- evalInstruction     :: EvalSettingsMap -> Instruction Pandoc.Block-    -> IO [Instruction Pandoc.Block]+    -> ExtractEvalM [Instruction Pandoc.Block] evalInstruction settings instr = case instr of     Pause         -> pure [Pause]     ModifyLast i  -> map ModifyLast <$> evalInstruction settings i     Append []     -> pure [Append []]     Append blocks -> concat <$> traverse (evalBlock settings) blocks+    AppendVar v   ->+        -- Should not happen since we don't do recursive evaluation.+        pure [AppendVar v]     Delete        -> pure [Delete]   ---------------------------------------------------------------------------------evalBlock :: EvalSettingsMap -> Pandoc.Block -> IO [Instruction Pandoc.Block]+evalBlock+    :: EvalSettingsMap -> Pandoc.Block+    -> ExtractEvalM [Instruction Pandoc.Block] evalBlock settings orig@(Pandoc.CodeBlock attr@(_, classes, _) txt)     | [s@EvalSettings {..}] <- lookupSettings classes settings = do-        out <- unsafeInterleaveIO $ do-            EvalResult {..} <-  evalCode s txt-            pure $ case erExitCode of-                ExitSuccess -> erStdout-                ExitFailure i ->-                    evalCommand <> ": exit code " <> T.pack (show i) <> "\n" <>-                    erStderr-        let fmt = "eval"-            blocks = case evalContainer of-                EvalContainerCode   -> [Pandoc.CodeBlock attr out]-                EvalContainerNone   -> [Pandoc.RawBlock fmt out]-                EvalContainerInline -> [Pandoc.Plain [Pandoc.RawInline fmt out]]+        var <- state freshVar+        tell $ HMS.singleton var $ EvalBlock s attr txt Nothing         pure $ case (evalFragment, evalReplace) of-            (False, True) -> [Append blocks]-            (False, False) -> [Append (orig : blocks)]+            (False, True) -> [AppendVar var]+            (False, False) -> [Append [orig], AppendVar var]             (True, True) ->                 [ Append [orig], Pause-                , Delete, Append blocks+                , Delete, AppendVar var                 ]             (True, False) ->-                [Append [orig], Pause, Append blocks]+                [Append [orig], Pause, AppendVar var]     | _ : _ : _ <- lookupSettings classes settings =         let msg = "patat eval matched multiple settings for " <>                 T.intercalate "," classes in@@ -94,30 +109,78 @@   ---------------------------------------------------------------------------------data EvalResult = EvalResult-    { erExitCode :: !ExitCode-    , erStdout   :: !T.Text-    , erStderr   :: !T.Text-    } deriving (Show)+newAccum :: Monoid m => (m -> IO ()) -> IO (m -> IO ())+newAccum f = do+    ref <- IORef.newIORef mempty+    pure $ \x ->+        IORef.atomicModifyIORef' ref (\y -> let z = y <> x in (z, z)) >>= f   ---------------------------------------------------------------------------------evalCode :: EvalSettings -> T.Text -> IO EvalResult-evalCode EvalSettings {..} input = do-    let proc = (Process.shell $ T.unpack evalCommand)-            { Process.std_in  = Process.CreatePipe-            , Process.std_out = Process.CreatePipe-            , Process.std_err = Process.CreatePipe+evalVar :: Var -> ([Pandoc.Block] -> IO ()) -> Presentation -> IO Presentation+evalVar var writeOutput presentation = case HMS.lookup var evalBlocks of+    Nothing -> pure presentation+    Just EvalBlock {..} | Just _ <- ebAsync -> pure presentation+    Just eb@EvalBlock {..} -> do+        let EvalSettings {..} = ebSettings++        writeChunk <- newAccum (writeOutput . renderEvalBlock eb)+        let drainLines copy h = do+                c <- catch (T.hGetChunk h) ((\_ -> pure "") :: IOException -> IO T.Text)+                when (c /= "") $ do+                    when copy $ writeChunk c+                    drainLines copy h++        let proc = (Process.shell $ T.unpack evalCommand)+                { Process.std_in  = Process.CreatePipe+                , Process.std_out = Process.CreatePipe+                , Process.std_err = Process.CreatePipe+                }+        (Just hIn, Just hOut, Just hErr, hProc) <- Process.createProcess proc+        async <- Async.async $+            Async.withAsync (T.hPutStr hIn ebInput `finally` IO.hClose hIn) $ \_ ->+            Async.withAsync (drainLines True hOut) $ \outAsync ->+            Async.withAsync (drainLines evalStderr hErr) $ \errAsync ->+            Async.withAsync (Process.waitForProcess hProc) $ \exitCodeAsync -> do+            erExitCode <- Async.wait exitCodeAsync+            _ <- Async.wait outAsync+            _ <- Async.wait errAsync+            case erExitCode of+                ExitSuccess -> pure ()+                ExitFailure i -> writeChunk $+                    evalCommand <> ": exit code " <> T.pack (show i) <> "\n"+        pure presentation+            { pEvalBlocks = HMS.insert var eb {ebAsync = Just async} evalBlocks             }+  where+    evalBlocks = pEvalBlocks presentation -    (Just hIn, Just hOut, Just hErr, hProc) <- Process.createProcess proc -    Async.withAsync (T.hPutStr hIn input `finally` IO.hClose hIn) $ \_ ->-        Async.withAsync (T.hGetContents hOut) $ \outAsync ->-        Async.withAsync (T.hGetContents hErr) $ \errAsync ->-        Async.withAsync (Process.waitForProcess hProc) $ \exitCodeAsync -> do -        erExitCode <- Async.wait exitCodeAsync-        erStdout <- Async.wait outAsync-        erStderr <- Async.wait errAsync-        pure $ EvalResult {..}+--------------------------------------------------------------------------------+evalActiveVars+    :: (Var -> [Pandoc.Block] -> IO ()) -> Presentation -> IO Presentation+evalActiveVars update presentation = foldM+    (\p var -> evalVar var (update var) p)+    presentation+    (activeVars presentation)+++--------------------------------------------------------------------------------+evalAllVars :: Presentation -> IO Presentation+evalAllVars pres = do+    updates <- IORef.newIORef []++    let forceEvalVar pres0 var = do+            pres1 <- evalVar+                var+                (\u -> IORef.atomicModifyIORef' updates (\l -> (l ++ [u], ())))+                pres0+            case HMS.lookup var (pEvalBlocks pres1) of+                Nothing -> pure pres1+                Just eb -> do+                    for_ (ebAsync eb) Async.wait+                    IORef.atomicModifyIORef' updates $ \l ->+                        ([], foldl' (\p u -> updateVar var u p) pres1 l)++    foldM forceEvalVar pres (HMS.keys (pEvalBlocks pres))
+ lib/Patat/Eval/Internal.hs view
@@ -0,0 +1,40 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards   #-}+module Patat.Eval.Internal+    ( EvalBlocks+    , EvalBlock (..)+    , renderEvalBlock+    ) where+++--------------------------------------------------------------------------------+import qualified Control.Concurrent.Async       as Async+import qualified Data.HashMap.Strict            as HMS+import qualified Data.Text                      as T+import           Patat.Presentation.Instruction+import           Patat.Presentation.Settings+import qualified Text.Pandoc                    as Pandoc+++--------------------------------------------------------------------------------+type EvalBlocks = HMS.HashMap Var EvalBlock+++--------------------------------------------------------------------------------+-- | Block that needs to be evaluated.+data EvalBlock = EvalBlock+    { ebSettings :: !EvalSettings+    , ebAttr     :: !Pandoc.Attr+    , ebInput    :: !T.Text+    , ebAsync    :: !(Maybe (Async.Async ()))+    }+++--------------------------------------------------------------------------------+renderEvalBlock :: EvalBlock -> T.Text -> [Pandoc.Block]+renderEvalBlock EvalBlock {..} out = case evalContainer ebSettings of+    EvalContainerCode   -> [Pandoc.CodeBlock ebAttr out]+    EvalContainerNone   -> [Pandoc.RawBlock fmt out]+    EvalContainerInline -> [Pandoc.Plain [Pandoc.RawInline fmt out]]+  where+    fmt = "eval"
lib/Patat/Main.hs view
@@ -24,6 +24,7 @@ import qualified Options.Applicative.Help.Pretty  as OA.PP import           Patat.AutoAdvance import qualified Patat.EncodingFallback           as EncodingFallback+import qualified Patat.Eval                       as Eval import qualified Patat.Images                     as Images import           Patat.Presentation import qualified Patat.Presentation.Comments      as Comments@@ -160,15 +161,15 @@             OA.parserFailure parserPrefs parserInfo             (OA.ShowHelpText Nothing) mempty -    errOrPres <- readPresentation filePath+    errOrPres <- readPresentation zeroVarGen filePath     pres      <- either (errorAndExit . return) return errOrPres     let settings = pSettings pres      unless (oForce options) assertAnsiFeatures      if oDump options then-        EncodingFallback.withHandle IO.stdout (pEncodingFallback pres) $-        dumpPresentation pres+        EncodingFallback.withHandle IO.stdout (pEncodingFallback pres) $ do+        Eval.evalAllVars pres >>= dumpPresentation     else         -- (Maybe) initialize images backend.         withMaybeHandle Images.withHandle (psImages settings) $ \images ->@@ -209,15 +210,20 @@         (pEncodingFallback aPresentation)         (activeSpeakerNotes aPresentation) -    size <- getPresentationSize aPresentation+    -- Start necessary eval blocks+    presentation <- Eval.evalActiveVars+        (\v -> Chan.writeChan aCommandChan . PresentationCommand . UpdateVar v)+        aPresentation++    size <- getPresentationSize presentation     Ansi.clearScreen     Ansi.setCursorPosition 0 0     cleanup <- case aView of-        PresentationView -> case displayPresentation size aPresentation of+        PresentationView -> case displayPresentation size presentation of             DisplayDoc doc    -> drawDoc doc             DisplayImage path -> drawImg size path         ErrorView err -> drawDoc $-                displayPresentationError size aPresentation err+                displayPresentationError size presentation err         TransitionView tr -> do             drawMatrix (tiSize tr) . fst . NonEmpty.head $ tiFrames tr             pure mempty@@ -234,11 +240,11 @@                     loop app {aView = TransitionView tr1}                 Nothing -> loop app {aView = PresentationView}         PresentationCommand c -> do-            update <- updatePresentation c aPresentation+            update <- updatePresentation c presentation             case update of                 ExitedPresentation       -> return ()                 UpdatedPresentation pres-                    | Just tgen <- mbTransition c size aPresentation pres -> do+                    | Just tgen <- mbTransition c size presentation pres -> do                         tr <- tgen                         scheduleTransitionTick tr                         loop app@@ -251,7 +257,7 @@     drawDoc doc = EncodingFallback.withHandle         IO.stdout (pEncodingFallback aPresentation) $         PP.putDoc doc $> mempty-    drawImg size path =case aImages of+    drawImg size path = case aImages of         Nothing -> drawDoc $ displayPresentationError             size aPresentation "image backend not initialized"         Just img -> do
lib/Patat/Presentation.hs view
@@ -2,10 +2,15 @@     ( PresentationSettings (..)     , defaultPresentationSettings +    , VarGen+    , Var+    , zeroVarGen+     , Presentation (..)     , readPresentation      , activeSpeakerNotes+    , activeVars      , Size     , getPresentationSize@@ -22,6 +27,7 @@     ) where  import           Patat.Presentation.Display+import           Patat.Presentation.Instruction import           Patat.Presentation.Interactive import           Patat.Presentation.Internal import           Patat.Presentation.Read
lib/Patat/Presentation/Display.hs view
@@ -161,7 +161,7 @@     dumpFragment :: Index -> [PP.Doc]     dumpFragment idx =         case displayPresentation (getSize idx) pres {pActiveFragment = idx} of-            DisplayDoc doc        -> [doc]+            DisplayDoc   doc      -> [doc]             DisplayImage filepath -> [PP.string $ "{image: " ++ filepath ++ "}"]      getSize :: Index -> Size@@ -187,7 +187,7 @@         mconcat (replicate top PP.hardline) <> doc0       where         top = case mTop of-            Auto -> let (r, _) = PP.dimensions doc0 in (rows - r) `div` 2+            Auto      -> let (r, _) = PP.dimensions doc0 in (rows - r) `div` 2             NotAuto x -> x      horizontal = horizontalIndent . horizontalWrap
lib/Patat/Presentation/Fragment.hs view
@@ -30,6 +30,7 @@     fragmentInstruction Pause = [Pause]     fragmentInstruction (Append []) = [Append []]     fragmentInstruction (Append xs) = fragmentBlocks fs xs+    fragmentInstruction (AppendVar v) = [AppendVar v]     fragmentInstruction Delete = [Delete]     fragmentInstruction (ModifyLast f) = map ModifyLast $ fragmentInstruction f 
lib/Patat/Presentation/Instruction.hs view
@@ -4,21 +4,33 @@ -- -- We do this by modelling a slide as a list of instructions, that manipulate -- the contents on a slide in a (for now) very basic way.+{-# LANGUAGE GeneralizedNewtypeDeriving #-} module Patat.Presentation.Instruction     ( Instructions     , fromList     , toList +    , Var+    , VarGen+    , zeroVarGen+    , freshVar+     , Instruction (..)+    , beforePause     , numFragments+    , variables      , Fragment (..)     , renderFragment     ) where -import qualified Text.Pandoc as Pandoc+import           Data.Hashable (Hashable)+import qualified Data.HashSet as HS+import           Data.List     (foldl')+import qualified Text.Pandoc   as Pandoc -newtype Instructions a = Instructions [Instruction a] deriving (Show)+newtype Instructions a = Instructions {unInstructions :: [Instruction a]}+    deriving (Show)  -- A smart constructor that guarantees some invariants: --@@ -36,11 +48,26 @@ toList :: Instructions a -> [Instruction a] toList (Instructions xs) = xs +-- | A variable is like a placeholder in the instructions, something we don't+-- know yet, dynamic content.  Currently this is only used for code evaluation.+newtype Var = Var Int deriving (Hashable, Eq, Ord, Show)++-- | Used to generate fresh variables.+newtype VarGen = VarGen Int deriving (Show)++zeroVarGen :: VarGen+zeroVarGen = VarGen 0++freshVar :: VarGen -> (Var, VarGen)+freshVar (VarGen x) = (Var x, VarGen (x + 1))+ data Instruction a     -- Pause.     = Pause     -- Append items.     | Append [a]+    -- Append the content of a variable.+    | AppendVar Var     -- Remove the last item.     | Delete     -- Modify the last block with the provided instruction.@@ -48,52 +75,75 @@     deriving (Show)  isPause :: Instruction a -> Bool-isPause Pause = True-isPause (Append _) = False-isPause Delete = False+isPause Pause          = True+isPause (Append _)     = False+isPause (AppendVar _)  = False+isPause Delete         = False isPause (ModifyLast i) = isPause i  numPauses :: Instructions a -> Int numPauses (Instructions xs) = length $ filter isPause xs +beforePause :: Int -> Instructions a -> Instructions a+beforePause n = Instructions . go 0 . unInstructions+  where+    go _ []          = []+    go i (Pause : t) = if i >= n then [] else go (i + 1) t+    go i (h     : t) = h : go i t++variables :: Instructions a -> HS.HashSet Var+variables (Instructions []               )  = mempty+variables (Instructions (AppendVar v : t))  = HS.insert v (variables (Instructions t))+variables (Instructions (ModifyLast i : t)) = variables (Instructions t) <> variables (Instructions [i])+variables (Instructions (_           : t))  = variables (Instructions t)+ numFragments :: Instructions a -> Int numFragments = succ . numPauses  newtype Fragment = Fragment [Pandoc.Block] deriving (Show) -renderFragment :: Int -> Instructions Pandoc.Block -> Fragment-renderFragment = \n (Instructions instrs) -> Fragment $ go [] n instrs-  where-    go acc _ []         = acc-    go acc n (Pause : instrs) = if n <= 0 then acc else go acc (n - 1) instrs-    go acc n (instr : instrs) = go (goBlocks instr acc) n instrs+renderFragment+    :: (Var -> [Pandoc.Block]) -> Instructions Pandoc.Block -> Fragment+renderFragment resolve = \instrs -> Fragment $ foldl'+    (\acc instr -> goBlocks resolve instr acc) [] (unInstructions instrs) -goBlocks :: Instruction Pandoc.Block -> [Pandoc.Block] -> [Pandoc.Block]-goBlocks Pause xs = xs-goBlocks (Append ys) xs = xs ++ ys-goBlocks Delete xs = sinit xs-goBlocks (ModifyLast f) xs+goBlocks+    :: (Var -> [Pandoc.Block]) -> Instruction Pandoc.Block -> [Pandoc.Block]+    -> [Pandoc.Block]+goBlocks _ Pause xs = xs+goBlocks _ (Append ys) xs = xs ++ ys+goBlocks resolve (AppendVar v) xs = xs ++ resolve v+goBlocks _ Delete xs = sinit xs+goBlocks resolve (ModifyLast f) xs     | null xs   = xs  -- Shouldn't happen unless instructions are malformed.-    | otherwise = modifyLast (goBlock f) xs+    | otherwise = modifyLast (goBlock resolve f) xs -goBlock :: Instruction Pandoc.Block -> Pandoc.Block -> Pandoc.Block-goBlock Pause x = x-goBlock (Append ys) block = case block of+goBlock+    :: (Var -> [Pandoc.Block]) -> Instruction Pandoc.Block -> Pandoc.Block+    -> Pandoc.Block+goBlock _ Pause x = x+goBlock _ (Append ys) block = case block of     -- We can only append to a few specific block types for now.-    Pandoc.BulletList xs -> Pandoc.BulletList $ xs ++ [ys]+    Pandoc.BulletList xs       -> Pandoc.BulletList $ xs ++ [ys]     Pandoc.OrderedList attr xs -> Pandoc.OrderedList attr $ xs ++ [ys]-    _ -> block-goBlock Delete block = case block of+    _                          -> block+goBlock resolve (AppendVar v) block = case block of     -- We can only append to a few specific block types for now.-    Pandoc.BulletList xs -> Pandoc.BulletList $ sinit xs+    Pandoc.BulletList xs       -> Pandoc.BulletList $ xs ++ [resolve v]+    Pandoc.OrderedList attr xs -> Pandoc.OrderedList attr $ xs ++ [resolve v]+    _                          -> block+goBlock _ Delete block = case block of+    -- We can only delete from a few specific block types for now.+    Pandoc.BulletList xs       -> Pandoc.BulletList $ sinit xs     Pandoc.OrderedList attr xs -> Pandoc.OrderedList attr $ sinit xs-    _ -> block-goBlock (ModifyLast f) block = case block of+    _                          -> block+goBlock resolve (ModifyLast f) block = case block of     -- We can only modify the last content of a few specific block types for     -- now.-    Pandoc.BulletList xs -> Pandoc.BulletList $ modifyLast (goBlocks f) xs-    Pandoc.OrderedList attr xs ->-        Pandoc.OrderedList attr $ modifyLast (goBlocks f) xs+    Pandoc.BulletList xs -> Pandoc.BulletList $+        modifyLast (goBlocks resolve f) xs+    Pandoc.OrderedList attr xs -> Pandoc.OrderedList attr $+        modifyLast (goBlocks resolve f) xs     _ -> block  modifyLast :: (a -> a) -> [a] -> [a]
lib/Patat/Presentation/Interactive.hs view
@@ -13,11 +13,13 @@   ---------------------------------------------------------------------------------import           Data.Char                   (isDigit)+import           Data.Char                      (isDigit)+import           Patat.Presentation.Instruction (Var) import           Patat.Presentation.Internal import           Patat.Presentation.Read-import qualified System.IO                   as IO-import           Text.Read                   (readMaybe)+import qualified System.IO                      as IO+import qualified Text.Pandoc                    as Pandoc+import           Text.Read                      (readMaybe)   --------------------------------------------------------------------------------@@ -31,6 +33,7 @@     | Last     | Reload     | Seek Int+    | UpdateVar Var [Pandoc.Block]     | UnknownCommand String     deriving (Eq, Show) @@ -96,16 +99,17 @@     :: PresentationCommand -> Presentation -> IO UpdatedPresentation  updatePresentation cmd presentation = case cmd of-    Exit             -> return ExitedPresentation-    Forward          -> return $ goToSlide $ \(s, f) -> (s, f + 1)-    Backward         -> return $ goToSlide $ \(s, f) -> (s, f - 1)-    SkipForward      -> return $ goToSlide $ \(s, _) -> (s + 10, 0)-    SkipBackward     -> return $ goToSlide $ \(s, _) -> (s - 10, 0)-    First            -> return $ goToSlide $ \_ -> (0, 0)-    Last             -> return $ goToSlide $ \_ -> (numSlides presentation, 0)-    Seek n           -> return $ goToSlide $ \_ -> (n - 1, 0)+    Exit             -> pure ExitedPresentation+    Forward          -> pure $ goToSlide $ \(s, f) -> (s, f + 1)+    Backward         -> pure $ goToSlide $ \(s, f) -> (s, f - 1)+    SkipForward      -> pure $ goToSlide $ \(s, _) -> (s + 10, 0)+    SkipBackward     -> pure $ goToSlide $ \(s, _) -> (s - 10, 0)+    First            -> pure $ goToSlide $ \_ -> (0, 0)+    Last             -> pure $ goToSlide $ \_ -> (numSlides presentation, 0)+    Seek n           -> pure $ goToSlide $ \_ -> (n - 1, 0)     Reload           -> reloadPresentation-    UnknownCommand _ -> return (UpdatedPresentation presentation)+    UnknownCommand _ -> pure $ UpdatedPresentation presentation+    UpdateVar v b    -> pure $ UpdatedPresentation $ updateVar v b presentation   where     numSlides :: Presentation -> Int     numSlides pres = length (pSlides pres)@@ -133,7 +137,7 @@         }      reloadPresentation = do-        errOrPres <- readPresentation (pFilePath presentation)+        errOrPres <- readPresentation (pVarGen presentation) (pFilePath presentation)         return $ case errOrPres of             Left  err  -> ErroredPresentation err             Right pres -> UpdatedPresentation $ pres
lib/Patat/Presentation/Internal.hs view
@@ -31,21 +31,27 @@     , ActiveFragment (..)     , activeFragment     , activeSpeakerNotes+    , activeVars      , getSettings     , activeSettings      , Size     , getPresentationSize++    , updateVar     ) where   -------------------------------------------------------------------------------- import qualified Data.Aeson.Extended            as A+import qualified Data.HashMap.Strict            as HMS+import qualified Data.HashSet                   as HS import           Data.Maybe                     (fromMaybe) import           Data.Sequence.Extended         (Seq) import qualified Data.Sequence.Extended         as Seq import           Patat.EncodingFallback         (EncodingFallback)+import qualified Patat.Eval.Internal            as Eval import qualified Patat.Presentation.Comments    as Comments import qualified Patat.Presentation.Instruction as Instruction import           Patat.Presentation.Settings@@ -73,6 +79,9 @@     , pTransitionGens   :: !(Seq (Maybe TransitionGen))  -- One for each slide.     , pActiveFragment   :: !Index     , pSyntaxMap        :: !Skylighting.SyntaxMap+    , pEvalBlocks       :: !Eval.EvalBlocks+    , pVarGen           :: !Instruction.VarGen+    , pVars             :: !(HMS.HashMap Instruction.Var [Pandoc.Block])     }  @@ -144,7 +153,10 @@         TitleSlide lvl is -> ActiveTitle $             Pandoc.Header lvl Pandoc.nullAttr is         ContentSlide instrs -> ActiveContent $-            Instruction.renderFragment fidx instrs+            Instruction.renderFragment resolve $+            Instruction.beforePause fidx instrs+  where+    resolve var = fromMaybe [] $ HMS.lookup var (pVars presentation)   --------------------------------------------------------------------------------@@ -156,6 +168,17 @@   --------------------------------------------------------------------------------+activeVars :: Presentation -> HS.HashSet Instruction.Var+activeVars presentation = fromMaybe HS.empty $ do+    let (sidx, fidx) = pActiveFragment presentation+    slide <- getSlide sidx presentation+    case slideContent slide of+        TitleSlide _ _ -> Nothing+        ContentSlide instrs -> pure $ Instruction.variables $+            Instruction.beforePause fidx instrs+++-------------------------------------------------------------------------------- getSettings :: Int -> Presentation -> PresentationSettings getSettings sidx pres =     fromMaybe mempty (Seq.safeIndex (pSlideSettings pres) sidx) <>@@ -177,3 +200,8 @@     pure $ Size {sRows = rows, sCols = cols}   where     settings = activeSettings pres+++--------------------------------------------------------------------------------+updateVar :: Instruction.Var -> [Pandoc.Block] -> Presentation -> Presentation+updateVar var blocks pres = pres {pVars = HMS.insert var blocks $ pVars pres}
lib/Patat/Presentation/Read.hs view
@@ -28,10 +28,11 @@ import qualified Data.Yaml                      as Yaml import           Patat.EncodingFallback         (EncodingFallback) import qualified Patat.EncodingFallback         as EncodingFallback-import           Patat.Eval                     (eval)+import qualified Patat.Eval                     as Eval import qualified Patat.Presentation.Comments    as Comments import           Patat.Presentation.Fragment import qualified Patat.Presentation.Instruction as Instruction+import           Patat.Presentation.Instruction (VarGen) import           Patat.Presentation.Internal import           Patat.Transition               (parseTransitionSettings) import           Prelude@@ -47,8 +48,8 @@   ---------------------------------------------------------------------------------readPresentation :: FilePath -> IO (Either String Presentation)-readPresentation filePath = runExceptT $ do+readPresentation :: VarGen -> FilePath -> IO (Either String Presentation)+readPresentation varGen filePath = runExceptT $ do     -- We need to read the settings first.     (enc, src)   <- liftIO $ EncodingFallback.readFile filePath     homeSettings <- ExceptT readHomeSettings@@ -71,8 +72,8 @@         Right x -> return x      pres <- ExceptT $ pure $-        pandocToPresentation filePath enc settings syntaxMap doc-    liftIO $ eval pres+        pandocToPresentation varGen filePath enc settings syntaxMap doc+    pure $ Eval.parseEvalBlocks pres   where     ext = takeExtension filePath @@ -122,9 +123,9 @@  -------------------------------------------------------------------------------- pandocToPresentation-    :: FilePath -> EncodingFallback -> PresentationSettings+    :: VarGen -> FilePath -> EncodingFallback -> PresentationSettings     -> Skylighting.SyntaxMap -> Pandoc.Pandoc -> Either String Presentation-pandocToPresentation pFilePath pEncodingFallback pSettings pSyntaxMap+pandocToPresentation pVarGen pFilePath pEncodingFallback pSettings pSyntaxMap         pandoc@(Pandoc.Pandoc meta _) = do     let !pTitle          = case Pandoc.docTitle meta of             []    -> [Pandoc.Str . T.pack . snd $ splitFileName pFilePath]@@ -133,6 +134,8 @@         !pBreadcrumbs    = collectBreadcrumbs pSlides         !pActiveFragment = (0, 0)         !pAuthor         = concat (Pandoc.docAuthors meta)+        !pEvalBlocks     = mempty+        !pVars           = mempty     pSlideSettings <- Seq.traverseWithIndex         (\i ->             first (\err -> "on slide " ++ show (i + 1) ++ ": " ++ err) .
lib/Patat/Presentation/Settings.hs view
@@ -248,6 +248,7 @@     , evalReplace   :: !Bool     , evalFragment  :: !Bool     , evalContainer :: !EvalSettingsContainer+    , evalStderr    :: !Bool     } deriving (Show)  @@ -258,6 +259,7 @@         <*> o A..:? "replace"  A..!= False         <*> o A..:? "fragment" A..!= True         <*> deprecated "wrap" "container" EvalContainerCode o+        <*> o A..:? "stderr" A..!= True       where         deprecated old new def obj = do             mo <- obj A..:? old
lib/Patat/Transition/Dissolve.hs view
@@ -39,7 +39,7 @@     -- Select the initial or final value depending on the noise.     frame :: Double -> Matrix     frame t = V.zipWith3-        (\treshold l r -> if t < treshold then l else r)+        (\threshold l r -> if t < threshold then l else r)         noise         initial         final
patat.cabal view
@@ -1,5 +1,5 @@ Name:          patat-Version:       0.12.0.1+Version:       0.13.0.0 Synopsis:      Terminal-based presentations using Pandoc Description:   Terminal-based presentations using Pandoc. License:       GPL-2@@ -43,6 +43,7 @@     containers           >= 0.5  && < 0.7,     directory            >= 1.2  && < 1.4,     filepath             >= 1.4  && < 1.6,+    hashable             >= 1.4  && < 1.5,     mtl                  >= 2.2  && < 2.4,     optparse-applicative >= 0.16 && < 0.19,     pandoc               >= 3.1  && < 3.3,@@ -71,6 +72,7 @@     Patat.Cleanup     Patat.EncodingFallback     Patat.Eval+    Patat.Eval.Internal     Patat.Images     Patat.Images.Internal     Patat.Images.ITerm2@@ -160,3 +162,15 @@     tasty-quickcheck >= 0.10 && < 0.11,     text             >= 1.2  && < 2.2,     QuickCheck       >= 2.8  && < 2.15++Test-suite patat-goldplate+  Main-is:            Main.hs+  Ghc-options:        -Wall+  Hs-source-dirs:     tests/golden+  Type:               exitcode-stdio-1.0+  Default-language:   Haskell2010+  Build-tool-depends: patat:patat++  Build-depends:+    base      >= 4.8     && < 5,+    goldplate >= 0.2.2.1 && < 0.3
+ tests/golden/Main.hs view
@@ -0,0 +1,8 @@+import Goldplate   (Options (..), defaultOptions, mainWith)+import System.Exit (exitWith)++main :: IO ()+main = mainWith options >>= exitWith+  where+    options = defaultOptions+        { oPrettyDiff = True, oPaths = ["tests/golden"] }