{-# LANGUAGE DeriveFoldable #-} {-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE DeriveTraversable #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} module Patat.Presentation.Syntax ( Block (..) , Inline (..) , dftBlocks , dftInlines , fromPandocBlocks , fromPandocInlines , isHorizontalRule , isComment , Var (..) , variables , RevealID (..) , blocksRevealSteps , blocksRevealStep , blocksRevealLastStep , blocksRevealOrder , blocksReveal , RevealState , revealToBlocks , RevealWrapper (..) , revealWrapper , RevealSequence (..) ) where import Control.Monad.Identity (runIdentity) import Control.Monad.State (State, execState, modify) import Control.Monad.Writer (Writer, execWriter, tell) import Data.CaseInsensitive (CI) import qualified Data.CaseInsensitive as CI import Data.Hashable (Hashable) import qualified Data.HashSet as HS import Data.List (foldl') import qualified Data.Map as M import Data.Maybe (fromMaybe) import qualified Data.Set as S import qualified Data.Text as T import qualified Data.Text.Encoding as T import Data.Traversable (for) import qualified Data.Yaml as Yaml import Patat.Presentation.Settings (PresentationSettings, parseSlideSettings) import Patat.Unique import qualified Text.Pandoc as Pandoc import qualified Text.Pandoc.Writers.Shared as Pandoc -- | This is similar to 'Pandoc.Block'. Having our own datatype has some -- advantages: -- -- * We can extend it with slide-specific data (eval, reveals) -- * We can remove stuff we don't care about -- * We can parse attributes and move them to haskell datatypes -- * This conversion can happen in a single parsing phase -- * We can catch backwards-incompatible pandoc changes in this module -- -- We try to follow the naming conventions from Pandoc as much as possible. data Block = Plain ![Inline] | Para ![Inline] | LineBlock ![[Inline]] | CodeBlock ![CI T.Text] !T.Text | RawBlock !Pandoc.Format !T.Text | BlockQuote ![Block] | OrderedList !Pandoc.ListAttributes ![[Block]] | BulletList ![[Block]] | DefinitionList ![([Inline], [[Block]])] | Header Int !Pandoc.Attr ![Inline] | HorizontalRule | Table ![Inline] ![Pandoc.Alignment] ![[Block]] ![[[Block]]] | Figure !Pandoc.Attr ![Block] | Div !Pandoc.Attr ![Block] -- Our own extensions: | Reveal !RevealWrapper !(RevealSequence [Block]) | VarBlock !Var | SpeakerNote !T.Text | Config !(Either String PresentationSettings) deriving (Eq, Show) -- | See comment on 'Block'. data Inline = Str !T.Text | Emph ![Inline] | Underline ![Inline] | Strong ![Inline] | Strikeout ![Inline] | Superscript ![Inline] | Subscript ![Inline] | SmallCaps ![Inline] | Quoted !Pandoc.QuoteType ![Inline] | Cite ![Pandoc.Citation] ![Inline] | Code !Pandoc.Attr !T.Text | Space | SoftBreak | LineBreak | Math !Pandoc.MathType !T.Text | RawInline !Pandoc.Format !T.Text | Link !Pandoc.Attr ![Inline] !Pandoc.Target | Image !Pandoc.Attr ![Inline] !Pandoc.Target | Note ![Block] | Span !Pandoc.Attr ![Inline] deriving (Eq, Show) -- | Depth-First Traversal of blocks (and inlines). dftBlocks :: forall m. Monad m => (Block -> m [Block]) -> (Inline -> m [Inline]) -> [Block] -> m [Block] dftBlocks fb fi = blocks where blocks :: [Block] -> m [Block] blocks = fmap concat . traverse block inlines :: [Inline] -> m [Inline] inlines = dftInlines fb fi block :: Block -> m [Block] block = (>>= fb) . \case Plain xs -> Plain <$> inlines xs Para xs -> Para <$> inlines xs LineBlock xss -> LineBlock <$> traverse inlines xss b@(CodeBlock _attr _txt) -> pure b b@(RawBlock _fmt _txt) -> pure b BlockQuote xs -> BlockQuote <$> blocks xs OrderedList attr xss -> OrderedList attr <$> traverse blocks xss BulletList xss ->BulletList <$> traverse blocks xss DefinitionList xss -> DefinitionList <$> for xss (\(term, definition) -> (,) <$> inlines term <*> traverse blocks definition) Header lvl attr xs -> Header lvl attr <$> inlines xs b@HorizontalRule -> pure b Table cptn aligns thead trows -> Table <$> inlines cptn <*> pure aligns <*> traverse blocks thead <*> traverse (traverse blocks) trows Figure attr xs -> Figure attr <$> blocks xs Div attr xs -> Div attr <$> blocks xs Reveal w revealer-> Reveal w <$> traverse blocks revealer b@(VarBlock _var) -> pure b b@(SpeakerNote _txt) -> pure b b@(Config _cfg) -> pure b -- | Depth-First Traversal of inlines (and blocks). dftInlines :: forall m. Monad m => (Block -> m [Block]) -> (Inline -> m [Inline]) -> [Inline] -> m [Inline] dftInlines fb fi = inlines where inlines :: [Inline] -> m [Inline] inlines = fmap concat . traverse inline inline :: Inline -> m [Inline] inline = (>>= fi) . \case i@(Str _txt) -> pure i Emph xs -> Emph <$> inlines xs Underline xs -> Underline <$> inlines xs Strong xs -> Strong <$> inlines xs Strikeout xs -> Strikeout <$> inlines xs Superscript xs -> Superscript <$> inlines xs Subscript xs -> Subscript <$> inlines xs SmallCaps xs -> SmallCaps <$> inlines xs Quoted ty xs -> Quoted ty <$> inlines xs Cite c xs -> Cite c <$> inlines xs i@(Code _attr _txt) -> pure i i@Space -> pure i i@SoftBreak -> pure i i@LineBreak -> pure i i@(Math _ty _txt) -> pure i i@(RawInline _fmt _txt) -> pure i Link attr xs tgt -> Link attr <$> inlines xs <*> pure tgt Image attr xs tgt -> Image attr <$> inlines xs <*> pure tgt Note blocks -> Note <$> dftBlocks fb fi blocks Span attr xs -> Span attr . concat <$> traverse inline xs fromPandocBlocks :: [Pandoc.Block] -> [Block] fromPandocBlocks = concatMap fromPandocBlock fromPandocBlock :: Pandoc.Block -> [Block] fromPandocBlock (Pandoc.Plain xs) = [Plain (fromPandocInlines xs)] fromPandocBlock (Pandoc.Para xs) = [Para (fromPandocInlines xs)] fromPandocBlock (Pandoc.LineBlock xs) = [LineBlock (map fromPandocInlines xs)] fromPandocBlock (Pandoc.CodeBlock (_, classes, _) body) = [CodeBlock (map CI.mk classes) body] fromPandocBlock (Pandoc.RawBlock fmt body) -- Parse config blocks. | fmt == "html" , Just t1 <- T.stripPrefix "" t1 = pure $ Config $ case Yaml.decodeEither' (T.encodeUtf8 t2) of Left err -> Left (show err) Right obj -> parseSlideSettings obj -- Parse other comments. | Just t1 <- T.stripPrefix "" t1 = pure $ SpeakerNote $ T.strip t2 -- Other raw blocks, leave as-is. | otherwise = [RawBlock fmt body] fromPandocBlock (Pandoc.BlockQuote blocks) = [BlockQuote $ fromPandocBlocks blocks] fromPandocBlock (Pandoc.OrderedList attrs items) = [OrderedList attrs $ map fromPandocBlocks items] fromPandocBlock (Pandoc.BulletList items) = [BulletList $ map fromPandocBlocks items] fromPandocBlock (Pandoc.DefinitionList items) = pure $ DefinitionList $ do (inlines, blockss) <- items pure (fromPandocInlines inlines, map (fromPandocBlocks) blockss) fromPandocBlock (Pandoc.Header lvl attrs inlines) = [Header lvl attrs (fromPandocInlines inlines)] fromPandocBlock Pandoc.HorizontalRule = [HorizontalRule] fromPandocBlock (Pandoc.Table _ cptn specs thead tbodies tfoot) = pure $ Table (fromPandocInlines cptn') aligns (map (fromPandocBlocks) headers) (map (map fromPandocBlocks) rows) where (cptn', aligns, _, headers, rows) = Pandoc.toLegacyTable cptn specs thead tbodies tfoot fromPandocBlock (Pandoc.Figure attrs _caption blocks) = [Figure attrs $ fromPandocBlocks blocks] fromPandocBlock (Pandoc.Div attrs blocks) = [Div attrs $ fromPandocBlocks blocks] fromPandocInlines :: [Pandoc.Inline] -> [Inline] fromPandocInlines = concatMap fromPandocInline fromPandocInline :: Pandoc.Inline -> [Inline] fromPandocInline inline = case inline of Pandoc.Str txt -> pure $ Str txt Pandoc.Emph xs -> pure $ Emph (fromPandocInlines xs) Pandoc.Underline xs -> pure $ Underline (fromPandocInlines xs) Pandoc.Strong xs -> pure $ Strong (fromPandocInlines xs) Pandoc.Strikeout xs -> pure $ Strikeout (fromPandocInlines xs) Pandoc.Superscript xs -> pure $ Superscript (fromPandocInlines xs) Pandoc.Subscript xs -> pure $ Subscript (fromPandocInlines xs) Pandoc.SmallCaps xs -> pure $ SmallCaps (fromPandocInlines xs) Pandoc.Quoted ty xs -> pure $ Quoted ty (fromPandocInlines xs) Pandoc.Cite c xs -> pure $ Cite c (fromPandocInlines xs) Pandoc.Code attr txt -> pure $ Code attr txt Pandoc.Space -> pure $ Space Pandoc.SoftBreak -> pure $ SoftBreak Pandoc.LineBreak -> pure $ LineBreak Pandoc.Math ty txt -> pure $ Math ty txt Pandoc.RawInline fmt txt -> pure $ RawInline fmt txt Pandoc.Link attr xs tgt -> pure $ Link attr (fromPandocInlines xs) tgt Pandoc.Image attr xs tgt -> pure $ Image attr (fromPandocInlines xs) tgt Pandoc.Note xs -> pure $ Note (fromPandocBlocks xs) Pandoc.Span attr xs -> pure $ Span attr (fromPandocInlines xs) isHorizontalRule :: Block -> Bool isHorizontalRule HorizontalRule = True isHorizontalRule _ = False isComment :: Block -> Bool isComment (SpeakerNote _) = True isComment (Config _) = True isComment _ = False -- | 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 Unique deriving (Hashable, Eq, Ord, Show) -- | Finds all variables that appear in some content. variables :: [Block] -> HS.HashSet Var variables = execWriter . dftBlocks visit (pure . pure) where visit :: Block -> Writer (HS.HashSet Var) [Block] visit b = do case b of VarBlock var -> tell $ HS.singleton var _ -> pure () pure [b] -- | A counter is used to change state in a slide. As counters increment, -- content may deterministically show or hide. newtype RevealID = RevealID Unique deriving (Eq, Ord, Show) -- | A reveal sequence stores content which can be hidden or shown depending on -- a counter state. -- -- The easiest example to think about is a bullet list which appears -- incrmentally on a slide. Initially, the counter state is 0. As it is -- incremented (the user goes to the next fragment in the slide), more list -- items become visible. data RevealSequence a = RevealSequence { -- The ID used for this sequence. rsID :: RevealID , -- These reveals should be advanced in this order. -- Reveal IDs will be included multiple times if needed. -- -- This should (only) contain the ID of this counter, and IDs of counters -- nested inside the children fields. rsOrder :: [RevealID] , -- For each piece of content in this sequence, we store a set of ints. -- When the current counter state is included in this set, the item is -- visible. rsVisible :: [(S.Set Int, a)] } deriving (Foldable, Functor, Eq, Show, Traversable) -- | This determines how we construct content based on the visible items. -- This could also be represented as `[[Block]] -> [Block]` but then we lose -- the convenient Eq and Show instances. data RevealWrapper = ConcatWrapper | BulletListWrapper | OrderedListWrapper Pandoc.ListAttributes deriving (Eq, Show) revealWrapper :: RevealWrapper -> [[Block]] -> [Block] revealWrapper ConcatWrapper = concat revealWrapper BulletListWrapper = pure . BulletList revealWrapper (OrderedListWrapper attr) = pure . OrderedList attr -- | Number of reveal steps in some blocks. blocksRevealSteps :: [Block] -> Int blocksRevealSteps = succ . length . blocksRevealOrder -- | Construct the reveal state for a specific step. blocksRevealStep :: Int -> [Block] -> RevealState blocksRevealStep fidx = makeRevealState . take fidx . blocksRevealOrder -- | Construct the final reveal state. blocksRevealLastStep :: [Block] -> RevealState blocksRevealLastStep = makeRevealState . blocksRevealOrder -- | This does a deep traversal of some blocks, and returns all reveals that -- should be advanced in-order. blocksRevealOrder :: [Block] -> [RevealID] blocksRevealOrder blocks = concat $ execState (dftBlocks visit (pure . pure) blocks) [] where -- We store a [[RevealID]] state, where each list represents the triggers -- necessary for a single reveal block. visit :: Block -> State [[RevealID]] [Block] visit (Reveal w rs) = do modify $ merge rs pure [Reveal w rs] visit block = pure [block] -- When we encounter a new reveal, we want to merge this into our -- [[RevealID]] state. However, we need to ensure to remove any children -- of that reveal block that were already in this list. merge :: RevealSequence [Block] -> [[RevealID]] -> [[RevealID]] merge (RevealSequence fid triggers _) known | any (fid `elem`) known = known | otherwise = filter (not . any (`elem` triggers)) known ++ [triggers] -- | Stores the state of several counters. type RevealState = M.Map RevealID Int -- | Convert a list of counters that need to be triggered to the final state. makeRevealState :: [RevealID] -> RevealState makeRevealState = foldl' (\acc x -> M.insertWith (+) x 1 acc) M.empty -- | Render a reveal by applying its constructor to what is visible. revealToBlocks :: RevealState -> RevealWrapper -> RevealSequence [Block] -> [Block] revealToBlocks revealState rw (RevealSequence cid _ sections) = revealWrapper rw [s | (activation, s) <- sections, counter `S.member` activation] where counter = fromMaybe 0 $ M.lookup cid revealState -- | Apply `revealToBlocks` recursively at each position, removing reveals -- in favor of their currently visible content. blocksReveal :: RevealState -> [Block] -> [Block] blocksReveal revealState = runIdentity . dftBlocks visit (pure . pure) where visit (Reveal w rs) = pure $ revealToBlocks revealState w rs visit block = pure [block]