packages feed

patat-0.13.0.0: lib/Patat/Presentation/Instruction.hs

--------------------------------------------------------------------------------
-- | The Pandoc AST is not extensible, so we need to use another way to model
-- different parts of slides that we want to appear bit by bit.
--
-- 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           Data.Hashable (Hashable)
import qualified Data.HashSet as HS
import           Data.List     (foldl')
import qualified Text.Pandoc   as Pandoc

newtype Instructions a = Instructions {unInstructions :: [Instruction a]}
    deriving (Show)

-- A smart constructor that guarantees some invariants:
--
--  *  No consecutive pauses.
--  *  All pauses moved to the top level.
--  *  No pauses at the end.
fromList :: [Instruction a] -> Instructions a
fromList = Instructions . go
  where
    go instrs = case break (not . isPause) instrs of
        (_, [])             -> []
        (_ : _, remainder)  -> Pause : go remainder
        ([], x : remainder) -> x : go remainder

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.
    | ModifyLast (Instruction a)
    deriving (Show)

isPause :: Instruction a -> Bool
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
    :: (Var -> [Pandoc.Block]) -> Instructions Pandoc.Block -> Fragment
renderFragment resolve = \instrs -> Fragment $ foldl'
    (\acc instr -> goBlocks resolve instr acc) [] (unInstructions instrs)

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 resolve f) xs

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.OrderedList attr xs -> Pandoc.OrderedList attr $ xs ++ [ys]
    _                          -> 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 $ 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 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 resolve f) xs
    Pandoc.OrderedList attr xs -> Pandoc.OrderedList attr $
        modifyLast (goBlocks resolve f) xs
    _ -> block

modifyLast :: (a -> a) -> [a] -> [a]
modifyLast f (x : y : zs) = x : modifyLast f (y : zs)
modifyLast f (x : [])     = [f x]
modifyLast _ []           = []

sinit :: [a] -> [a]
sinit xs = if null xs then [] else init xs