{- | Module : Data.Graph.Analysis.Reporting.Pandoc Description : Graphalyze Types and Classes Copyright : (c) Ivan Lazar Miljenovic 2009 License : 2-Clause BSD Maintainer : Ivan.Miljenovic@gmail.com This module uses /Pandoc/ to generate documents: Note that Pandoc is released under GPL-2 or later, however according to the Free Software Foundation, I am indeed allowed to use it: since the 2-Clause BSD license that I'm using is GPL-compatible: (search for /FreeBSD License/, which is another name for it). -} module Data.Graph.Analysis.Reporting.Pandoc ( PandocDocument, pandocHtml, pandocLaTeX, pandocRtf, pandocMarkdown, alsoSaveDot ) where -- TODO : the ability to create multiple files. import Data.Graph.Analysis.Reporting import Data.GraphViz.Commands(GraphvizOutput(Png, Svg)) import Text.Pandoc import Data.List(intersperse) import Data.Maybe(isNothing, fromJust) import Control.Arrow((***)) import Control.Exception.Extensible(SomeException, try) import System.Directory(removeDirectoryRecursive) import System.FilePath((), (<.>)) -- ----------------------------------------------------------------------------- {- $writers The actual exported writers. -} pandocHtml :: PandocDocument pandocHtml = pd { writer = writeHtmlString , extension = "html" , templateName = "html" , extGraphProps = Just VProps { size = DefaultSize , format = Svg } } pandocLaTeX :: PandocDocument pandocLaTeX = pd { writer = writeLaTeX , extension = "tex" , templateName = "latex" -- 4.5" should be less than \textwidth in LaTeX. , graphProps = defaultProps { size = createSize 4.5 } } pandocRtf :: PandocDocument pandocRtf = pd { writer = writeRTF , extension = "rtf" , templateName = "rtf" } pandocMarkdown :: PandocDocument pandocMarkdown = pd { writer = writeMarkdown , extension = "text" , templateName = "markdown" } -- ----------------------------------------------------------------------------- {- $defs Pandoc definition. -} -- | Definition of a Pandoc Document. Size measurements are in inches, -- and a 6:4 ratio is used for width:length. data PandocDocument = PD { -- | The Pandoc document style writer :: WriterOptions -> Pandoc -> String -- | The file extension used , extension :: FilePath -- | Which template to get. , templateName :: String -- | Size of graphs to be produced. , graphProps :: VisProperties -- | Optional size of external linked graphs. , extGraphProps :: Maybe VisProperties -- | Should the Dot source code be saved as well? , keepDot :: Bool } -- | Some default sizes. Note that all other fields of 'PandocDocument' -- still need to be defined. pd :: PandocDocument pd = PD { writer = undefined , extension = undefined , templateName = undefined , graphProps = defaultProps , extGraphProps = Nothing , keepDot = False } -- | Also save the generated Dot code to file when creating visualisations. alsoSaveDot :: PandocDocument -> PandocDocument alsoSaveDot p = p { keepDot = True } defaultWidth :: Double defaultWidth = 10 defaultProps :: VisProperties defaultProps = VProps { size = createSize defaultWidth , format = Png } instance DocumentGenerator PandocDocument where createDocument = createPandoc docExtension = extension -- | Define the 'WriterOptions' used. writerOptions :: WriterOptions writerOptions = defaultWriterOptions { writerStandalone = True , writerTableOfContents = True , writerNumberSections = True } -- | Used when traversing the document structure. data PandocProcess = PP { secLevel :: Int , visParams :: VisParams } deriving (Eq, Ord, Show, Read) -- | Start with a level 1 heading. defaultProcess :: PandocProcess defaultProcess = PP { secLevel = 1 , visParams = undefined } -- | Create the document. createPandoc :: PandocDocument -> Document -> IO (Maybe FilePath) createPandoc p d = do Right template <- getDefaultTemplate Nothing (templateName p) created <- tryCreateDirectory dir -- If the first one fails, so will this one. _ <- tryCreateDirectory $ dir gdir if not created then failDoc else do d' <- addLegend dir gdir (graphProps p) d elems <- multiElems pp $ content d' case elems of Just es -> do let es' = htmlAuthDt : es pnd = Pandoc meta es' doc = convert template pnd wr <- tryWrite doc case wr of (Right _) -> success (Left _) -> failDoc Nothing -> failDoc where dir = rootDirectory d gdir = graphDirectory d auth = author d dt = date d meta = makeMeta (title d) auth dt -- Html output doesn't show date and auth anywhere by default. htmlAuthDt = htmlInfo auth dt pp = defaultProcess { visParams = vp } vp = VParams { rootDir = dir , graphDir = gdir , defaultImage = graphProps p , largeImage = extGraphProps p , saveDot = keepDot p } convert t = writer p (writerOptions {writerTemplate = t}) file = dir fileFront d <.> extension p tryWrite :: String -> IO (Either SomeException ()) tryWrite = try . writeFile file success = return (Just file) failDoc = removeDirectoryRecursive dir >> return Nothing -- ----------------------------------------------------------------------------- {- $conv Converting individual elements to their corresponding Pandoc types. -} -- | The meta information makeMeta :: DocInline -> String -> String -> Meta makeMeta tle a t = Meta (inlines tle) [[Str a]] [Str t] -- | Html output doesn't show the author and date; use this to print it. htmlInfo :: String -> String -> Block htmlInfo auth dt = RawBlock "html" html where heading = "

Document Information

" html = unlines [heading, htmlize auth, htmlize dt] htmlize str = "

" ++ str ++ "

" -- | Link conversion loc2target :: Location -> Target loc2target (Web url) = (url,"") loc2target (File file) = (file,"") -- | Conversion of simple inline elements. inlines :: DocInline -> [Inline] inlines (Text str) = [Str str] inlines BlankSpace = [Space] inlines (Grouping grp) = concat . intersperse [Space] $ map inlines grp inlines (Bold inl) = [Strong (inlines inl)] inlines (Emphasis inl) = [Emph (inlines inl)] inlines (DocLink inl loc) = [Link (inlines inl) (loc2target loc)] inlines (DocImage inl loc) = [Image (inlines inl) (loc2target loc)] {- | Conversion of complex elements. The only reason it's in the IO monad is for GraphImage, as it requires IO to create the image. If any one of the conversions fail (i.e. returns 'Nothing'), the entire process fails. In future, may extend this to creating multiple files for top-level sections, in which case the IO monad will be required for that as well. -} elements :: PandocProcess -> DocElement -> IO (Maybe [Block]) elements p (Section lbl elems) = do let n = secLevel p p' = p { secLevel = n + 1} sec = Header n (inlines lbl) elems' <- multiElems p' elems return (fmap (sec:) elems') elements _ (Paragraph inls) = return $ Just [Para (concatMap inlines inls)] elements p (Enumeration elems) = do elems' <- multiElems' p elems let attrs = (1,DefaultStyle,DefaultDelim) list = fmap (OrderedList attrs) elems' return (fmap return list) elements p (Itemized elems) = do elems' <- multiElems' p elems return (fmap (return . BulletList) elems') elements _ (Definitions defs) = return . Just . return . DefinitionList $ map (inlines *** ((:[]) . (:[]) . Plain . inlines)) defs elements p (GraphImage dg) = elements p =<< createGraph (visParams p) dg -- | Concatenate the result of multiple calls to 'elements'. multiElems :: PandocProcess -> [DocElement] -> IO (Maybe [Block]) multiElems p elems = do elems' <- mapM (elements p) elems if any isNothing elems' then return Nothing else return (Just $ concatMap fromJust elems') -- | As for 'multiElems', but don't @concat@ the resulting 'Block's. multiElems' :: PandocProcess -> [DocElement] -> IO (Maybe [[Block]]) multiElems' p elems = do elems' <- mapM (elements p) elems if any isNothing elems' then return Nothing else return (Just $ map fromJust elems')