From 86d76055d9eaa67f2e5b77832c684e8c76070a54 Mon Sep 17 00:00:00 2001 From: Henrik Tramberend <henrik.tramberend@beuth-hochschule.de> Date: Tue, 9 Jan 2018 14:54:01 +0100 Subject: [PATCH] Starting on the decker monaad --- app/decker.hs | 2 +- decker.cabal | 2 + resource/template/help-page.md | 2 +- src/Common.hs | 38 ++++++++ src/Filter.hs | 80 +++++------------ src/Macro.hs | 159 +++++++++++++++++++++++++++++++++ src/Utilities.hs | 18 ++-- 7 files changed, 229 insertions(+), 72 deletions(-) create mode 100644 src/Macro.hs diff --git a/app/decker.hs b/app/decker.hs index fba4791..06cc441 100644 --- a/app/decker.hs +++ b/app/decker.hs @@ -70,7 +70,7 @@ main = do everythingA <++> indexA >>= need need ["support"] -- - -- phony "pdf" $ pagesPdfA <++> handoutsPdfA <++> indexA >>= need + phony "pdf" $ pagesPdfA <++> handoutsPdfA <++> indexA >>= need -- -- phony "pdf-decks" $ decksPdfA <++> indexA >>= need -- diff --git a/decker.cabal b/decker.cabal index 3b64996..a28ead1 100644 --- a/decker.cabal +++ b/decker.cabal @@ -25,6 +25,7 @@ executable decker other-modules: Action , Cache , CRC32 + , Macro , Meta , Watch , Embed @@ -61,6 +62,7 @@ executable decker , http-conduit , http-types , monad-loops + , mtl , multimap , mustache , network-uri diff --git a/resource/template/help-page.md b/resource/template/help-page.md index 88654e1..261f5a2 100644 --- a/resource/template/help-page.md +++ b/resource/template/help-page.md @@ -82,7 +82,7 @@ decker [options] [target] using `pandoc`: ``` {.sh} - decker help | pandoc -s -t html > ~/tmp/decker-help.html + decker help | pandoc -s -t html > decker-help.html ``` `decker check` diff --git a/src/Common.hs b/src/Common.hs index 7365422..25cc01f 100644 --- a/src/Common.hs +++ b/src/Common.hs @@ -1,12 +1,20 @@ {-- Author: Henrik Tramberend <henrik@tramberend.de> --} module Common ( DeckerException(..) + , DeckerState(..) + , Layout(..) + , OutputFormat(..) + , Disposition(..) + , MediaType(..) + , Decker , deckerVersion ) where import Control.Exception +import Control.Monad.State import Data.Typeable import Data.Version (showVersion) +import Network.URI as U import Paths_decker (version) -- | The version from the cabal file @@ -38,3 +46,33 @@ instance Show DeckerException where show (DecktapeException e) = "decktape.sh failed for reason: " ++ e show RsyncUrlException = "attributes 'destinationRsyncHost' or 'destinationRsyncPath' not defined in meta data" + +data DeckerState = DeckerState + { disposition :: Disposition + , slideCount :: Int + , externalReferences :: [U.URI] + } + +type Decker = StateT DeckerState IO + +data Layout + = Deck + | Page + | Handout + deriving (Eq, Show) + +data OutputFormat + = Html + | Pdf + deriving (Eq, Show) + +data Disposition = Disposition + { layout :: Layout + , format :: OutputFormat + } deriving (Eq, Show) + +data MediaType + = ImageMedia + | AudioMedia + | VideoMedia + | IframeMedia diff --git a/src/Filter.hs b/src/Filter.hs index a22074c..350dce3 100644 --- a/src/Filter.hs +++ b/src/Filter.hs @@ -2,9 +2,11 @@ {-# LANGUAGE OverloadedStrings, TupleSections #-} module Filter - ( Disposition(..) + ( Layout(..) + , OutputFormat(..) + , Disposition(..) + , processPandoc , hasAttrib - , hasLayout , blockClasses , expandMacros , makeSlides @@ -25,13 +27,16 @@ module Filter import Common import Control.Exception import Control.Monad +import Control.Monad.State import qualified Data.ByteString.Lazy.Char8 as L8 import Data.Default () import Data.List import Data.List.Split +import Data.Maybe import qualified Data.Map as Map (Map, fromList, lookup) import Data.Maybe import Debug.Trace +import Macro import Network.HTTP.Conduit import Network.HTTP.Simple import qualified Network.URI as U @@ -52,7 +57,17 @@ import Text.Pandoc.Walk import Text.Printf import Text.Read -type MacroFunc = [String] -> Attr -> Target -> Format -> Meta -> Inline +processPandoc :: Disposition -> Pandoc -> IO Pandoc +processPandoc disposition pandoc = + evalStateT (processPandoc_ pandoc) (DeckerState disposition 0 []) + +processPandoc_ :: Pandoc -> Decker Pandoc +processPandoc_ pandoc@(Pandoc meta blocks) = do + disp <- gets disposition + case disp of + (Disposition Deck _) -> walkM (expandDeckerMacros meta) pandoc + (Disposition Page _) -> walkM (expandDeckerMacros meta) pandoc + (Disposition Handout _) -> walkM (expandDeckerMacros meta) pandoc -- iframe resizing, see: -- https://css-tricks.com/NetMag/FluidWidthVideo/Article-FluidWidthVideo.php @@ -190,12 +205,6 @@ hasAnyClass which = isJust . firstClass which firstClass :: [String] -> Block -> Maybe String firstClass which block = listToMaybe $ filter ((flip hasClass) block) which --- | Slide layouts -data Layout = Layout - { name :: String - , areas :: [String] - } deriving (Show) - -- | Slide layouts are rows of one ore more columns. data RowLayout = RowLayout { lname :: String @@ -278,15 +287,6 @@ layoutSlides slide@(header, body) = in (header, renderLayout areas layout) Nothing -> slide -masters :: [(String, Layout)] -masters = - [ ("default", Layout "default" []) - , ("columns", Layout "columns" ["left", "right"]) - , ("top2columns", Layout "top2columns" ["top", "left", "right"]) - , ("bottom2columns", Layout "bottom2columns" ["left", "right", "bottom"]) - , ("centeredoverlay", Layout "centeredoverlay" ["back", "front"]) - ] - hasAttrib :: String -> Block -> Maybe String hasAttrib which (Div (_, _, keyvals) _) = lookup which keyvals hasAttrib which (Header 1 (_, _, keyvals) _) = lookup which keyvals @@ -294,9 +294,6 @@ hasAttrib which (CodeBlock (_, _, keyvals) _) = lookup which keyvals hasAttrib which (Para [Image (_, _, keyvals) _ _]) = lookup which keyvals hasAttrib _ _ = Nothing -hasLayout :: Block -> Maybe Layout -hasLayout block = hasAttrib "layout" block >>= (flip lookup) masters - blockClasses :: Block -> [String] blockClasses (Div (_, classes, _) _) = classes blockClasses (Header 1 (_, classes, _) _) = classes @@ -311,31 +308,6 @@ blockAttribs (CodeBlock attribs _) = attribs blockAttribs (Para [Image attribs _ _]) = attribs blockAttribs _ = ("", [], []) --- | Fit slide to layout -fitLayout :: [Block] -> [Block] -fitLayout slide@(header:body) = - case hasLayout header of - Just layout -> - let wrapArea blocks@(first:_) = - case whichArea first of - Just area -> - let (_, _, attribs) = blockAttribs first - in [Div ("", [(name layout), area], attribs) blocks] - Nothing -> blocks - wrapArea block = block - whichArea block = - listToMaybe $ intersect (blockClasses block) (areas layout) - slideAreas = - split (keepDelimsL $ whenElt (hasAnyClass (areas layout))) body - orderedAreas = - catMaybes $ - map - (\a -> find (maybe False (hasClass a) . listToMaybe) slideAreas) - (areas layout) - in header : concatMap wrapArea orderedAreas - Nothing -> slide -fitLayout [] = [] - -- | Split join columns with CSS3. Must be performed after `wrapBoxes`. splitJoinColumns :: Slide -> Slide splitJoinColumns (header, body) = (header, concatMap wrapRow rows) @@ -446,7 +418,7 @@ mapSlides func (Pandoc meta blocks) = extractHeader (rule@(HorizontalRule):blocks) = (rule, blocks) extractHeader slide = throw $ - PandocException $ "Error extracting slide header: \n" ++ show slide -- never happens + PandocException $ "Error extracting slide header: \n" ++ show slide prependHeader (header, blocks) = header : blocks makeSlides :: Format -> Pandoc -> Pandoc @@ -528,18 +500,6 @@ audioExtensions = [".m4a", ".mp3", ".ogg", ".wav"] iframeExtensions :: [String] iframeExtensions = [".html", ".html", ".pdf"] -data Disposition - = Deck - | Page - | Handout - deriving (Eq) - -data MediaType - = ImageMedia - | AudioMedia - | VideoMedia - | IframeMedia - uriPathExtension :: String -> String uriPathExtension path = case U.parseRelativeReference path of @@ -581,7 +541,7 @@ renderImageAudioVideoTag disposition (Image (ident, cls, values) inlines (url, t then element else element ! attr (toValue value) srcAttr = - if disposition == Deck + if disposition == Disposition Deck Html then "data-src" else "src" transformedValues = (lazyLoad . transformImageSize) values diff --git a/src/Macro.hs b/src/Macro.hs new file mode 100644 index 0000000..1ac9abf --- /dev/null +++ b/src/Macro.hs @@ -0,0 +1,159 @@ +{-- Author: Henrik Tramberend <henrik@tramberend.de> --} +{-# LANGUAGE OverloadedStrings, TupleSections #-} + +module Macro + ( expandDeckerMacros + , MacroFunc + ) where + +import Common +import Control.Monad.State +import Data.List.Split +import qualified Data.Map as Map (Map, fromList, lookup) +import System.FilePath +import Text.Blaze (customAttribute) +import Text.Blaze.Html.Renderer.String +import Text.Blaze.Html5 as H + ((!), audio, div, figure, iframe, iframe, img, p, stringTag, + toValue, video) +import Text.Blaze.Html5.Attributes as A + (alt, class_, height, id, src, style, title, width) +import Text.Pandoc +import Text.Pandoc.Definition () +import Text.Pandoc.Shared +import Text.Pandoc.Walk +import Text.Printf +import Text.Read + +expandDeckerMacros :: Meta -> Inline -> Decker Inline +expandDeckerMacros meta inline = return inline + +type MacroFunc = [String] -> Attr -> Target -> Format -> Meta -> Inline + +type MacroAction = [String] -> Attr -> Target -> Meta -> Decker Inline + +-- iframe resizing, see: +-- https://css-tricks.com/NetMag/FluidWidthVideo/Article-FluidWidthVideo.php +-- YouTube links: iv_load_policy=3 disables annotations, rel=0 disables related +-- videos. See: +-- https://developers.google.com/youtube/player_parameters?hl=de#IFrame_Player_API +embedYoutubeHtml :: [String] -> Attr -> Target -> Inline +embedYoutubeHtml args attr (vid, _) = + RawInline (Format "html") (renderHtml html) + where + url = + printf + "https://www.youtube.com/embed/%s?iv_load_policy=3&disablekb=1&rel=0&modestbranding=1&autohide=1" + vid :: String + vidWidthStr = macroArg 0 args "560" + vidHeightStr = macroArg 1 args "315" + vidWidth = readDefault 560.0 vidWidthStr :: Float + vidHeight = readDefault 315.0 vidHeightStr :: Float + wrapperStyle = + printf + "position:relative;padding-top:25px;padding-bottom:%f%%;height:0;" + (vidHeight / vidWidth * 100.0) :: String + iframeStyle = + "position:absolute;top:0;left:0;width:100%;height:100%;" :: String + figureStyle (_, _, kv) = + foldl (\s (k, v) -> s ++ printf "%s:%s;" k v :: String) "" kv + figureClass (_, cls, _) = unwords cls + html = + H.figure ! class_ (toValue (figureClass attr)) ! + style (toValue (figureStyle attr)) $ + H.div ! style (toValue wrapperStyle) $ + iframe ! style (toValue iframeStyle) ! width (toValue vidWidthStr) ! + height (toValue vidHeightStr) ! + src (toValue url) ! + customAttribute "frameborder" "0" ! + customAttribute "allowfullscreen" "" $ + H.p "" + +embedYoutubePdf :: [String] -> Attr -> Target -> Inline +embedYoutubePdf args attr (vid, _) = + Link nullAttr [Image attr [Str text] (imageUrl, "")] (videoUrl, "") + where + videoUrl = + printf + "https://www.youtube.com/embed/%s?iv_load_policy=3&disablekb=0&rel=0&modestbranding=1&autohide=1" + vid :: String + imageUrl = + printf "http://img.youtube.com/vi/%s/maxresdefault.jpg" vid :: String + text = printf "YouTube: %s" vid :: String + +youtube :: MacroFunc +youtube args attr target meta = do + disp <- gets disposition + case disp of + Disposition _ Html -> return $ embedYoutubeHtml args attr target + Disposition _ Pdf -> return $ embedYoutubePdf args attr target + +fontAwesome :: MacroFunc +fontAwesome _ _ (iconName, _) _ = do + disp <- gets disposition + case disp of + Disposition _ Html -> + return $ + RawInline (Format "html") $ "<i class=\"fa fa-" ++ iconName ++ "\"></i>" + Disposition _ Pdf -> return $ Str $ "[" ++ iconName ++ "]" + +metaValue :: MacroFunc +metaValue _ _ (key, _) meta = + case splitOn "." key of + [] -> return $ Str key + k:ks -> return $ lookup' ks (lookupMeta k meta) + where + lookup' :: [String] -> Maybe MetaValue -> Inline + lookup' [] (Just (MetaString s)) = Str s + lookup' [] (Just (MetaInlines i)) = Span nullAttr i + lookup' (k:ks) (Just (MetaMap metaMap)) = lookup' ks (Map.lookup k metaMap) + lookup' _ _ = Strikeout [Str key] + +type MacroMap = Map.Map String MacroFunc + +macroMap :: MacroMap +macroMap = + Map.fromList [("meta", metaValue), ("youtube", youtube), ("fa", fontAwesome)] + +readDefault :: Read a => a -> String -> a +readDefault default_ string = fromMaybe default_ (readMaybe string) + +macroArg :: Int -> [String] -> String -> String +macroArg n args default_ = + if length args > n + then args !! n + else default_ + +parseMacro :: String -> Maybe [String] +parseMacro (pre:invocation) + | pre == ':' = Just (words invocation) +parseMacro _ = Nothing + +isMacro :: String -> Bool +isMacro (pre:_) = pre == ':' +isMacro _ = False + +onlyStrings :: [Inline] -> [String] +onlyStrings = reverse . foldl only [] + where + only ss (Str s) = s : ss + only ss _ = ss + +expand :: Inline -> Format -> Meta -> Maybe Inline +expand (Link attr text target) format meta = + expand_ attr text target format meta +expand x _ _ = Just x + +expand_ :: Attr -> [Inline] -> Target -> Format -> Meta -> Maybe Inline +expand_ attr text target format meta = do + name:args <- parseMacro $ stringify text + func <- Map.lookup name macroMap + return (func args attr target format meta) + +expandInlineMacros :: Format -> Meta -> Inline -> Inline +expandInlineMacros format meta inline = + fromMaybe inline (expand inline format meta) + +expandMacros :: Format -> Pandoc -> Pandoc +expandMacros format doc@(Pandoc meta _) = + walk (expandInlineMacros format meta) doc diff --git a/src/Utilities.hs b/src/Utilities.hs index b00ccf7..4aa486e 100644 --- a/src/Utilities.hs +++ b/src/Utilities.hs @@ -180,7 +180,7 @@ markdownToHtmlDeck markdownFile out = do ] , writerCiteMethod = Citeproc } - pandoc <- readAndPreprocessMarkdown markdownFile Deck + pandoc <- readAndPreprocessMarkdown markdownFile (Disposition Deck Html) processed <- processPandocDeck "revealjs" pandoc writePandocString "revealjs" options out processed @@ -303,7 +303,7 @@ markdownToHtmlPage markdownFile out = do , writerVariables = [("decker-support-dir", supportDir)] , writerCiteMethod = Citeproc } - pandoc <- readAndPreprocessMarkdown markdownFile Page + pandoc <- readAndPreprocessMarkdown markdownFile (Disposition Page Html) processed <- processPandocPage "html5" pandoc writePandocString "html5" options out processed @@ -315,12 +315,10 @@ markdownToPdfPage markdownFile out = do let options = pandocWriterOpts { writerTemplate = Just template - -- , writerStandalone = True , writerHighlight = True - -- , writerHighlightStyle = pygments , writerCiteMethod = Citeproc } - pandoc <- readAndPreprocessMarkdown markdownFile Page + pandoc <- readAndPreprocessMarkdown markdownFile (Disposition Page Pdf) processed <- processPandocPage "latex" pandoc putNormal $ "# pandoc (for " ++ out ++ ")" pandocMakePdf options processed out @@ -336,7 +334,7 @@ pandocMakePdf options processed out = do markdownToHtmlHandout :: FilePath -> FilePath -> Action () markdownToHtmlHandout markdownFile out = do putCurrentDocument out - pandoc <- readAndPreprocessMarkdown markdownFile Handout + pandoc <- readAndPreprocessMarkdown markdownFile (Disposition Handout Html) processed <- processPandocHandout "html" pandoc supportDir <- getRelativeSupportDir out template <- getTemplate "handout.html" @@ -357,7 +355,7 @@ markdownToHtmlHandout markdownFile out = do markdownToPdfHandout :: FilePath -> FilePath -> Action () markdownToPdfHandout markdownFile out = do putCurrentDocument out - pandoc <- readAndPreprocessMarkdown markdownFile Handout + pandoc <- readAndPreprocessMarkdown markdownFile (Disposition Handout Pdf) processed <- processPandocHandout "latex" pandoc template <- getTemplate "handout.tex" let options = @@ -561,20 +559,20 @@ processCitesWithDefault pandoc@(Pandoc meta blocks) = do processPandocPage :: String -> Pandoc -> Action Pandoc processPandocPage format pandoc = do cited <- processCitesWithDefault pandoc - return $ (renderMediaTags Page . expandMacros (Format format)) cited + return $ (renderMediaTags (Disposition Page Html) . expandMacros (Format format)) cited processPandocDeck :: String -> Pandoc -> Action Pandoc processPandocDeck format pandoc = do cited <- processCitesWithDefault pandoc return $ - (renderMediaTags Page . + (renderMediaTags (Disposition Deck Html) . makeSlides (Format format) . expandMacros (Format format)) cited processPandocHandout :: String -> Pandoc -> Action Pandoc processPandocHandout format pandoc = do cited <- processCitesWithDefault pandoc - return $ (renderMediaTags Page . expandMacros (Format format)) cited + return $ (renderMediaTags (Disposition Handout Html) . expandMacros (Format format)) cited type StringWriter = WriterOptions -> Pandoc -> String -- GitLab