-- | -- Module : Presentation.Yeamer -- Copyright : (c) Justus Sagemüller 2017 -- License : GPL v3 -- -- Maintainer : (@) jsagemue $ uni-koeln.de -- Stability : experimental -- Portability : portable -- {-# LANGUAGE CPP #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE UnicodeSyntax #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE ViewPatterns #-} module Presentation.Yeamer ( Presentation -- * Running a presentation , yeamer -- * Primitives , staticContent, tweakContent, serverSide -- ** Maths , ($<>), maths -- ** Media content , imageFromFile, mediaFromFile, imageFromFileSupplier -- ** Arbitrary file serving , useFile, useFileSupplier -- ** Code / plaintext , verbatim, plaintext, verbatimWithin -- * Structure / composition , addHeading, (======), discardResult -- * CSS , divClass, spanClass, (#%), styling -- * Server configuration , yeamer' , YeamerServerConfig -- | Default port is 14910 , yeamerTcpPort ) where import Yesod hiding (get) import Yesod.Form.Jquery import qualified Data.Text as Txt import qualified Data.Text.Lazy as Txt (toStrict) import qualified Data.Text.Encoding as Txt import Data.Text (Text) import Data.String (IsString (..)) import Data.ByteString (ByteString) import qualified Data.ByteString.Lazy as BSL import qualified Data.ByteString.Char8 as BC8 import Data.Flat (Flat, flat, unflat) import qualified Data.Aeson as JSON import qualified Text.Blaze.Html5 as HTM import qualified Text.Blaze.Html5.Attributes as HTM import qualified Text.Blaze.Html.Renderer.Text as HTMText import Presentation.Yeamer.Internal.Progress import Presentation.Yeamer.Internal.PrPathStepCompression import Data.Map (Map) import qualified Data.Map as Map import qualified Data.Vector as Arr import Presentation.Yeamer.Internal.Grid import Text.Cassius (Css) import Text.Julius (rawJS) import Yesod.Static (Static, static, base64md5) import Yesod.EmbeddedStatic import qualified Language.Javascript.JQuery as JQuery import Language.Haskell.TH.Syntax ( Exp(LitE, AppE, VarE, ConE) , Lit(StringL), Name, runIO ) import Language.Haskell.TH.Quote import qualified CAS.Dumb.Symbols as TMM import qualified CAS.Dumb.Tree as TMM import qualified Math.LaTeX.Prelude as TMM import Text.LaTeX (LaTeX) import qualified Text.LaTeX as LaTeX import qualified Text.TeXMath as MathML import qualified Text.XML.Light as XML import Data.List (intercalate) import Data.Foldable (fold) import Data.Traversable.Redundancy (rmRedundancy) import Control.Monad.Trans.Writer.JSONable import Control.Monad.Trans.List import Control.Monad.Trans.State import Control.Monad.Trans.Reader import Data.These import Data.These.Lens import qualified Data.Semigroup as SG import Data.Semigroup.Numbered import Data.Monoid import Data.Maybe import Data.Functor.Identity import Control.Monad import Control.Arrow (first, second, (&&&)) import Control.Applicative import Data.Function ((&)) import Data.Tuple (swap) import Data.Default.Class import System.FilePath ( takeFileName, takeExtension, takeBaseName, dropExtension , (<.>), () ) import System.Directory ( doesPathExist, makeAbsolute , createDirectoryIfMissing, renameFile #if MIN_VERSION_directory(1,3,1) , createFileLink, pathIsSymbolicLink, getSymbolicLinkTarget #endif ) #if !MIN_VERSION_directory(1,3,1) import System.Posix.Files (createSymbolicLink, readSymbolicLink) #endif import System.IO.Temp import GHC.Generics import Lens.Micro import Lens.Micro.TH import Data.Bifunctor (bimap) #if !MIN_VERSION_directory(1,3,1) createFileLink = createSymbolicLink getSymbolicLinkTarget = readSymbolicLink pathIsSymbolicLink _ = pure True #endif #if !MIN_VERSION_flat(0,4,0) instance (Flat a) => Flat (Identity a) #endif data Container t where WithHeading :: Html -> Container Identity ManualCSSClasses :: Container (WriterT HTMChunkK []) GriddedBlocks :: Container Gridded CustomEncapsulation :: (t Html -> Html) -> Container t data HTMChunkK = HTMDiv {_hchunkCSSClass::Text} | HTMSpan {_hchunkCSSClass::Text} deriving (Generic, Eq, Ord) instance JSON.FromJSON HTMChunkK instance JSON.ToJSON HTMChunkK instance Flat HTMChunkK makeLenses ''HTMChunkK type Sessionable = Flat data IPresentation m r where StaticContent :: Html -> IPresentation m () Resultless :: IPresentation m r -> IPresentation m () Styling :: [Css] -> IPresentation m r -> IPresentation m r Encaps :: (Traversable t, Sessionable r, Sessionable (t ())) => Container t -> t (IPresentation m r) -> IPresentation m (t r) Pure :: r -> IPresentation m r Deterministic :: (r -> s) -> IPresentation m r -> IPresentation m s Interactive :: Sessionable r => IPresentation m () -> m r -> IPresentation m r Dependent :: Sessionable x => IPresentation m x -> (x -> IPresentation m r) -> IPresentation m r instance (r ~ ()) => IsString (IPresentation m r) where fromString = StaticContent . fromString type Presentation = IPresentation IO () data PresentationServer = PresentationServer { presentationToShow :: Presentation , getStatic :: EmbeddedStatic , getPseudostatic :: Static } mkEmbeddedStatic False "myStatic" . pure . embedFileAt "jquery.js" =<< runIO JQuery.file pStatDir :: FilePath pStatDir = ".pseudo-static-content" mkYesod "PresentationServer" [parseRoutes| / HomeR GET /p/#PresProgress ExactPositionR GET /changeposition/#PresProgress/#PositionChange ChPosR GET /reset ResetR GET /static StaticR EmbeddedStatic getStatic /pseudostatic PStaticR Static getPseudostatic |] instance Yesod PresentationServer where addStaticContent = embedStaticContent getStatic StaticR Right approot = ApprootRelative instance YesodJquery PresentationServer preprocPres :: IPresentation m r -> IPresentation m r preprocPres (StaticContent c) = StaticContent c preprocPres (Resultless p) = Resultless $ preprocPres p preprocPres (Styling s p) = Styling s $ preprocPres p preprocPres (Encaps (WithHeading h) p) = Encaps (WithHeading h) $ preprocPres<$>p preprocPres (Encaps ManualCSSClasses p) = Encaps ManualCSSClasses $ preprocPres<$>p preprocPres (Encaps (CustomEncapsulation f) p) = Encaps (CustomEncapsulation f) $ preprocPres<$>p preprocPres (Encaps GriddedBlocks p) = Styling grids . divClass gridClass . fmap (backonstruct . map (first (read . Txt.unpack . _hchunkCSSClass) . swap) . runWriterT) . Encaps ManualCSSClasses $ preprocPres <$> layouted where (GridLayout w h prelayed, backonstruct) = layoutGridP p layouted = WriterT $ swap . first (HTMDiv . ("autogrid-range_"<>) . idc) . snd <$> prelayed gridRep :: [[Text]] gridRep = foldr fill (replicate h $ replicate w ".") prelayed where fill (GridRange xb xe yb ye, (i, _)) field = yPre ++ [ xPre ++ (idc i<$xRel) ++ xPost | xAll <- yRel , let (xPre, (xRel, xPost)) = splitAt xb xAll & second (splitAt $ xe-xb) ] ++ yPost where (yPre, (yRel, yPost)) = splitAt yb field & second (splitAt $ ye-yb) idc i | c <- toEnum $ fromEnum 'a' + i , c <= 'z' = Txt.singleton c gridClass = "autogrid_"<>Txt.intercalate "-" (Txt.concat<$>gridRep) grids = [lucius| div.#{gridClass} { display: grid; grid-template-areas: #{areas} } |]() : [ [lucius| div .#{divid} { grid-area: #{ist} } |]() | (_, (i, _)) <- prelayed , let ist = idc i divid = "autogrid-range_" <> ist ] where areas = fold ["\""<>Txt.intercalate " " line<>"\" " | line <- gridRep ] preprocPres (Pure x) = Pure x preprocPres (Deterministic f p) = Deterministic f $ preprocPres p preprocPres (Interactive p a) = Interactive (preprocPres p) a preprocPres (Dependent d o) = Dependent (preprocPres d) (preprocPres<$>o) isInline :: IPresentation m a -> Bool isInline (StaticContent _) = True isInline (Encaps ManualCSSClasses (WriterT qs)) = all (\(_,i) -> case i of HTMSpan _ -> True HTMDiv _ -> False ) qs isInline (Encaps _ _) = False isInline (Styling _ q) = isInline q isInline (Interactive q _) = isInline q isInline (Resultless q) = isInline q isInline (Dependent q _) = isInline q isInline (Deterministic _ q) = isInline q isInline (Pure _) = True getHomeR :: Handler Html getHomeR = redirect . ExactPositionR $ assemblePresProgress mempty getExactPositionR :: PresProgress -> Handler Html getExactPositionR pPosition = do PresentationServer presentation _ _ <- getYesod defaultLayout $ do addScript $ StaticR jquery_js slideChoice <- (`runReaderT`pPosition) $ chooseSlide "" defaultChoiceName "" Nothing Nothing presentation (`here`slideChoice) $ \slide -> do let contents = go 0 slide toWidget contents return () where chooseSlide :: PrPath -> (Text->PrPath) -> Text -> Maybe PrPath -> Maybe PrPath -> IPresentation IO r -> ReaderT PresProgress (WidgetT PresentationServer IO) (These Presentation r) chooseSlide _ _ "" Nothing Nothing (StaticContent conts) = pure $ These (StaticContent conts) () chooseSlide path choiceName "" Nothing Nothing (Styling sty conts) = mapM_ toWidget sty >> chooseSlide path choiceName "" Nothing Nothing conts chooseSlide path choiceName "" Nothing Nothing (Encaps f conts) = postGather <$> cellwise f where postGather sq = case sequence sq of That p's -> That p's This _ -> This . discardResult $ Encaps f $ fmap (maybe mempty id . (^?here)) sq These _ p's -> (`These`p's) . discardResult $ Encaps f $ fmap (maybe mempty id . (^?here)) sq cellwise ManualCSSClasses | WriterT contsL <- conts = WriterT <$> (`traverse`contsL) `id` \(cell,i) -> (,i) <$> chooseSlide (path<>case i of HTMDiv c -> " div."<>c HTMSpan c -> " span."<>c ) choiceName "" Nothing Nothing cell cellwise _ = traverse (chooseSlide path choiceName "" Nothing Nothing) conts chooseSlide path choiceName "" Nothing Nothing (Interactive conts followAction) = do purity <- chooseSlide path choiceName "" Nothing Nothing conts case purity ^? here of Just pres -> pure . This $ discardResult pres Nothing -> That <$> liftIO followAction chooseSlide path choiceName "" Nothing Nothing (Resultless conts) = do purity <- chooseSlide path choiceName "" Nothing Nothing conts case purity ^? here of Just pres -> pure . (`These`()) $ discardResult pres Nothing -> pure $ That () chooseSlide path choiceName "" Nothing Nothing (Deterministic f conts) = do purity <- chooseSlide path choiceName "" Nothing Nothing conts pure $ bimap discardResult f purity chooseSlide path choiceName pdiv bwd fwd (Dependent def opt) = do let progPath = path<>" span."<>choiceName pdiv positionCh <- lookupProgress progPath case positionCh of Nothing -> do liftIO . putStrLn $ "Not enter '"++Txt.unpack progPath++"'" purity <- chooseSlide path choiceName (pdiv<>"0") bwd (Just progPath) def case preferThis purity of Left pres -> pure . This $ discardResult pres Right x -> do pPosition' <- setProgress progPath x `execStateT` pPosition redirect $ ExactPositionR pPosition' Just x -> chooseSlide path choiceName (pdiv<>"1") (Just progPath) fwd $ opt x chooseSlide path choiceName pdiv bwd fwd pres | isJust bwd || isJust fwd = do let thisChoice = choiceName pdiv newPath = (path<>" span."<>thisChoice) [revertPossible, progressPossible] = maybe "false" (const "true") <$> [bwd,fwd] :: [Text] [previous,next] = maybe "null" id <$> [bwd, fwd] toWidget [julius| $("#{rawJS newPath}").click(function(e){ if (e.ctrlKey && #{rawJS revertPossible}) { isRevert = true; pChanger = "@{ChPosR pPosition (PositionChange previous True)}"; } else if (!(e.ctrlKey) && #{rawJS progressPossible}) { isRevert = false; pChanger = "@{ChPosR pPosition (PositionChange next False)}"; } else { return; } e.stopPropagation(); hasErrored = false; $.ajax({ contentType: "application/json", processData: false, url: pChanger, type: "GET", dataType: "text", success: function(newURL, textStatus, jqXHR) { if (isRevert) { window.location.replace(newURL); } else { window.location.href = newURL; } }, error: function(jqXHR, textStatus, errorThrown) { $("body").css("cursor","not-allowed"); hasErrored = true; setTimeout(function() { $("body").css("cursor","auto")}, 500); } }); setTimeout(function() { if (!hasErrored) {$("body").css("cursor","wait")} }, 150); }) |] (here %~ spanClass thisChoice) <$> chooseSlide newPath (disambiguateChoiceName choiceName) "" Nothing Nothing pres chooseSlide _ _ _ _ _ (Pure x) = pure $ That x chooseSlide _ _ _ _ _ pres = error $ "Cannot display "++outerConstructorName pres go :: Int -> IPresentation m r -> Html go _ (StaticContent conts) = conts go _ (Pure _) = error $ "Error: impossible to render a slide of an empty presentation." go _ (Dependent _ _) = error $ "Internal error: un-selected Dependent option while rendering to HTML." go lvl (Deterministic _ conts) = go lvl conts go lvl (Resultless conts) = go lvl conts go lvl (Interactive conts _) = go lvl conts go lvl (Styling sty conts) = go lvl conts go lvl (Encaps (WithHeading h) conts) = let lvl' = min 6 $ lvl + 1 hh = [HTM.h1, HTM.h2, HTM.h3, HTM.h4, HTM.h5, HTM.h6]!!lvl in go lvl' $ Encaps (CustomEncapsulation $ \(Identity contsr) -> HTM.div HTM.! HTM.class_ "headed-container" $ hh h <> contsr ) conts go lvl (Encaps ManualCSSClasses conts) = go lvl $ Encaps (CustomEncapsulation $ \(WriterT contsrs) -> foldMap (\(q,i) -> case i of HTMDiv c -> [hamlet|
#{q} |]() HTMSpan c -> [hamlet| #{q} |]()) $ contsrs ) conts where withSupclass c | Just _ <- Txt.stripPrefix "autogrid_" c = "autogrid "<>c | otherwise = c go lvl (Encaps (CustomEncapsulation f) conts) = f $ go lvl <$> conts go _ p = error $ outerConstructorName p <> " cannot be rendered." preferThis :: These a b -> Either a b preferThis (This a) = Left a preferThis (That b) = Right b preferThis (These a _) = Left a hchunkFor :: Text -> IPresentation m r -> HTMChunkK hchunkFor t p | isInline p = HTMSpan t | otherwise = HTMDiv t instance (Monoid r, Sessionable r) => SG.Semigroup (IPresentation m r) where StaticContent c <> StaticContent d = StaticContent $ c<>d Encaps ManualCSSClasses (WriterT elems₀) <> Encaps ManualCSSClasses (WriterT elems₁) = Encaps ManualCSSClasses . WriterT . disambiguate $ elems₀ ++ elems₁ where disambiguate = go 0 Map.empty where go _ _ [] = [] go i occupied ((q,c):qs) | Txt.null (_hchunkCSSClass c) || c`Map.member`occupied = let c' = c & hchunkCSSClass .~ Txt.pack ("anonymousCell-"++show i) in go (i+1) occupied (( fmap (fst . head . runWriterT) . Encaps ManualCSSClasses $ WriterT [(q,c)] , c' ):qs) | otherwise = (q,c) : go (i+1) (Map.insert c () occupied) qs Resultless (Encaps ManualCSSClasses ps) <> Resultless (Encaps ManualCSSClasses qs) = Resultless $ Encaps ManualCSSClasses (discardResult<$>ps) SG.<> Encaps ManualCSSClasses (discardResult<$>qs) Resultless p@(Encaps ManualCSSClasses _) <> c = Resultless p <> Resultless (Encaps ManualCSSClasses $ WriterT [(c,hchunkFor""c)]) c <> Resultless p@(Encaps ManualCSSClasses _) = Resultless (Encaps ManualCSSClasses $ WriterT [(c,hchunkFor""c)]) <> Resultless p p <> q = fmap fold . Encaps ManualCSSClasses $ WriterT [(p, hchunkFor"anonymousCell-0"p), (q, hchunkFor"anonymousCell-1"q)] instance ∀ m . Monoid (IPresentation m ()) where mappend = (SG.<>) mempty = Resultless $ Encaps ManualCSSClasses (WriterT [] :: WriterT HTMChunkK [] (IPresentation m ())) instance ∀ m . SemigroupNo 0 (IPresentation m ()) where sappendN _ (Resultless (Encaps GriddedBlocks l)) (Resultless (Encaps GriddedBlocks r)) = Resultless . Encaps GriddedBlocks $ (discardResult<$>l) │ (discardResult<$>r) sappendN _ l@(Resultless (Encaps GriddedBlocks _)) r = l │ Resultless (Encaps GriddedBlocks $ pure r) sappendN _ l r = Resultless (Encaps GriddedBlocks $ pure l) │ r instance ∀ m . SemigroupNo 1 (IPresentation m ()) where sappendN _ (Resultless (Encaps GriddedBlocks t)) (Resultless (Encaps GriddedBlocks b)) = Resultless . Encaps GriddedBlocks $ (discardResult<$>t) ── (discardResult<$>b) sappendN _ t@(Resultless (Encaps GriddedBlocks _)) b = t ── Resultless (Encaps GriddedBlocks $ pure b) sappendN _ t b = Resultless (Encaps GriddedBlocks $ pure t) ── b outerConstructorName :: IPresentation m r -> String outerConstructorName (StaticContent _) = "StaticContent" outerConstructorName (Resultless _) = "Resultless" outerConstructorName (Styling _ _) = "Styling" outerConstructorName (Encaps (WithHeading _) _) = "Encaps WithHeading" outerConstructorName (Encaps (CustomEncapsulation _) _) = "Encaps CustomEncapsulation" outerConstructorName (Encaps ManualCSSClasses _) = "Encaps ManualCSSClasses" outerConstructorName (Encaps GriddedBlocks _) = "Encaps GriddedBlocks" outerConstructorName (Pure _) = "Pure" outerConstructorName (Deterministic _ _) = "Deterministic" outerConstructorName (Interactive _ _) = "Interactive" outerConstructorName (Dependent _ _) = "Dependent" discardResult :: IPresentation m r -> IPresentation m () discardResult (StaticContent c) = StaticContent c discardResult (Resultless p) = Resultless p discardResult p = Resultless p serverSide :: Sessionable a => m a -> IPresentation m a serverSide = Interactive (pure ()) instance Functor (IPresentation m) where fmap f (Deterministic g q) = Deterministic (f . g) q fmap f (Pure x) = Pure $ f x fmap f q = Deterministic f q instance Applicative (IPresentation m) where pure = Pure Pure f <*> x = fmap f x f <*> Pure x = fmap ($ x) f fs<*>xs = ap fs xs instance Monad (IPresentation m) where return = pure StaticContent c >>= f = Dependent (StaticContent c) f Resultless p >>= f = Dependent (Resultless p) f Styling _ (Pure x) >>= f = f x Styling s (StaticContent c) >>= f = Dependent (Styling s (StaticContent c)) f Styling s (Resultless c) >>= f = Dependent (Styling s (Resultless c)) f Styling s (Styling s' x) >>= f = Styling (s++s') x >>= f Styling s (Encaps (WithHeading h) x) >>= f = Dependent (Styling s (Encaps (WithHeading h) x)) f Styling s (Encaps ManualCSSClasses x) >>= f = Dependent (Styling s (Encaps ManualCSSClasses x)) f Styling s (Deterministic g x) >>= f = Styling s x >>= f . g Styling s (Interactive p o) >>= f = Dependent (Interactive (Styling s p) o) f Styling s (Dependent p g) >>= f = Dependent (Styling s p) $ Styling s . g >=> f Encaps (WithHeading h) p >>= f = Dependent (Encaps (WithHeading h) p) f Encaps ManualCSSClasses ps >>= f = Dependent (Encaps ManualCSSClasses ps) f Pure x >>= f = f x Deterministic g p >>= f = p >>= f . g Interactive p q >>= f = Dependent (Interactive p q) f Dependent p g >>= f = Dependent p $ g >=> f o >> Interactive (Pure _) q = Interactive (discardResult o) q o >> Pure x = fmap (const x) o o >> n = o >>= const n infixr 6 ====== -- | Infix synonym of 'addHeading'. Intended to be used -- in @do@ blocks, for headings of presentation slides. (======) :: Sessionable r => Html -> IPresentation m r -> IPresentation m r (======) = addHeading addHeading :: Sessionable r => Html -> IPresentation m r -> IPresentation m r addHeading h = fmap runIdentity . Encaps (WithHeading h) . Identity divClass :: Sessionable r => Text -> IPresentation m r -> IPresentation m r divClass cn = fmap (fst . head . runWriterT) . Encaps ManualCSSClasses . WriterT . pure . (,HTMDiv cn) spanClass :: Sessionable r => Text -> IPresentation m r -> IPresentation m r spanClass cn = fmap (fst . head . runWriterT) . Encaps ManualCSSClasses . WriterT . pure . (,HTMSpan cn) infix 8 #% -- | Assign this content a CSS class attribute. If the content is inline, this will -- be a @@, else a @
@. (#%) :: Sessionable r => Text -> IPresentation m r -> IPresentation m r c#%q | isInline q = spanClass c q | otherwise = divClass c q styling :: Css -> IPresentation m r -> IPresentation m r styling s (Styling s' a) = Styling (s:s') a styling s a = Styling [s] a staticContent :: Monoid r => Html -> IPresentation m r staticContent = fmap (const mempty) . StaticContent tweakContent :: Sessionable r => (Html -> Html) -> IPresentation m r -> IPresentation m r tweakContent f = fmap runIdentity . Encaps (CustomEncapsulation $ f . runIdentity) . Identity infixr 6 $<> ($<>) :: (r ~ (), TMM.SymbolClass σ, TMM.SCConstraint σ LaTeX) => TMM.CAS (TMM.Infix LaTeX) (TMM.Encapsulation LaTeX) (TMM.SymbolD σ LaTeX) -> IPresentation m r -> IPresentation m r ($<>) = (<>) . renderTeXMaths MathML.DisplayInline . TMM.toMathLaTeX maths :: (r ~ (), TMM.SymbolClass σ, TMM.SCConstraint σ LaTeX) => [[TMM.CAS (TMM.Infix LaTeX) (TMM.Encapsulation LaTeX) (TMM.SymbolD σ LaTeX)]] -> String -> IPresentation m r maths eqns = renderTeXMaths MathML.DisplayBlock . TMM.maths eqns renderTeXMaths :: MathML.DisplayType -> LaTeX -> IPresentation m () renderTeXMaths dispSty tex = case MathML.readTeX . Txt.unpack $ LaTeX.render tex of Right exps -> StaticContent . HTM.preEscapedText . Txt.pack . XML.showElement $ MathML.writeMathML dispSty exps Left err -> error $ "Failed to re-parse generated LaTeX. "++err -- | Include a piece of plaintext, preserving all formatting. To be used in an -- oxford bracket. -- -- In practice, you probably want to use this for monospace plaintext, which should -- appear in a @
@ or @