packages feed

diagrams-pandoc-0.4: src/Text/Pandoc/Diagrams.hs

{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts          #-}
{-# LANGUAGE FlexibleInstances         #-}
{-# LANGUAGE OverloadedStrings         #-}
{-# LANGUAGE RecordWildCards           #-}

-- | Convert appropriately annotated Code blocks to an image, with or
-- without display of the code.  Interpret the Code blocks as Haskell
-- code using the Diagrams libraries.

module Text.Pandoc.Diagrams where

import           Data.Hashable                   (Hashable)
import           Data.List                       (delete)
import           Data.Text                       (Text)
import qualified Data.Text                       as T
import           Data.Typeable                   (Typeable)
import qualified Diagrams.Backend.Cairo.Internal as BCairo
import qualified Diagrams.Backend.SVG            as BSvg
import qualified Diagrams.Builder                as DB
import qualified Diagrams.Core                   as DC
import           Diagrams.Prelude                (centerXY, pad, (&), (.~))
import           Diagrams.Size                   (dims)
import qualified Graphics.Svg                    as Svg
import           Linear                          (V2 (..), zero)
import           System.Directory                (createDirectoryIfMissing)
import           System.FilePath                 (pathSeparator, (<.>), (</>))
import           System.IO                       (hFlush, hPutStr, hPutStrLn, stderr)
import           Text.Pandoc.Definition          (Block(..), Caption, Attr)
import qualified Text.Pandoc.Builder as PB
import qualified Text.Pandoc as Pandoc
import qualified Text.Pandoc.Shared as Pandoc
import           Text.Pandoc.Options             (def, readerExtensions, pandocExtensions)
import           Data.Maybe                      (fromMaybe)
import           Data.Foldable                   (fold)

backendExt :: Opts -> String
backendExt Opts {_backend = SVG } = "svg"
backendExt Opts {_backend = Cairo, ..} = case _outFormat of
  "beamer" -> "pdf"
  "latex"  -> "pdf"
  _        -> "png"

-- Return output type for a string
findCairoOutputType :: String -> BCairo.OutputType
findCairoOutputType "beamer" = BCairo.PDF
findCairoOutputType "latex"  = BCairo.PDF
findCairoOutputType _        = BCairo.PNG

data Opts = Opts {
    _outFormat    :: String,
    _outDir       :: FilePath,
    _expression   :: String,
    _absolutePath :: Bool,
    _backend      :: Backend
    }

data Backend = Cairo | SVG deriving (Read)

data Echo = Above | Below

insertDiagrams :: Opts -> Block -> IO [Block]
insertDiagrams opts@Opts{..} (CodeBlock (ident, classes, attrs) code)
    | "diagram-haskell" `elem` classes = do
      i <- PB.toList <$> img
      return $ case echo of
        Above -> bl' : i
        Below -> i <> [bl']
    | "diagram" `elem` classes = PB.toList <$> img
  where
    img = do
      d <- compileDiagram opts attrs code
      case d of
        Left _err     -> pure mempty  -- TODO log an error here
        Right imgName -> case captionRaw of
          Just captionRaw' -> do
            captionBlocks <- parseCaption captionRaw'
            let caption = blocksToCaption captionBlocks
                alt = fromMaybe (blocksToAlt captionBlocks) altAttr
            pure $ figureWithCaption
              -- transfer identifier from code block to figure, so that
              -- it can be referenced by `pandoc-crossref` and the like.
              (ident, [], [])
              caption
              (if _absolutePath then T.cons pathSeparator imgName else imgName)
              ""
              alt
          Nothing ->
            pure $ PB.plain $ PB.imageWith
              (ident, [], [])
              (if _absolutePath then T.cons pathSeparator imgName else imgName)
              ""
              (fold altAttr)
    bl' = CodeBlock (ident, "haskell":delete "diagram-haskell" classes, attrs) code
    echo = readEcho attrs
    captionRaw = lookup "caption" attrs
    altAttr = PB.str <$> lookup "alt" attrs
insertDiagrams _ block = return [block]

figureWithCaption :: Attr -> Caption -> Text -> Text -> PB.Inlines -> PB.Blocks
figureWithCaption attr caption url title alt =
  PB.figure caption . PB.plain $ PB.imageWith attr url title alt

blocksToCaption :: [Block] -> Caption
blocksToCaption = PB.simpleCaption . PB.fromList

blocksToAlt :: [Block] -> PB.Inlines
blocksToAlt = Pandoc.blocksToInlines'

parseCaption :: Text -> IO [Block]
parseCaption s = Pandoc.runIOorExplode $ do
  (Pandoc.Pandoc _ blks) <-
    Pandoc.readMarkdown (def { readerExtensions = pandocExtensions }) s
  pure blks

-- Copied from https://github.com/diagrams/diagrams-doc/blob/master/doc/Xml2Html.hs
-- With the CPP removed, thereby requiring Cairo
-- TODO clean this up, move it into -builder somehow
-- | Compile the literate source code of a diagram to a .png/.pdf file with
--   a file name given by a hash of the source code contents
compileDiagram :: Opts -> [(Text,Text)] -> Text -> IO (Either String Text)
compileDiagram opts attrs src = do
  ensureDir $ _outDir opts
  case mkBuildOpts opts attrs src of
    SomeBuildOpts bo -> do
      res <- DB.buildDiagram bo
      case res of
        DB.ParseErr err    -> do
          hPutStrLn stderr ("\nError while parsing\n" ++ T.unpack src)
          hPutStrLn stderr err
          return $ Left "Error while parsing"

        DB.InterpErr ierr  -> do
          hPutStrLn stderr ("\nError while interpreting\n" ++ T.unpack src)
          hPutStrLn stderr (DB.ppInterpError ierr)
          return $ Left "Error while interpreting"

        DB.Skipped hash    -> do
          hPutStr stderr "."
          hFlush stderr
          return $ Right (T.pack $ mkFile opts (DB.hashToHexStr hash))

        DB.OK hash out -> do
          hPutStr stderr "O"
          hFlush stderr
          let path = mkFile opts (DB.hashToHexStr hash)
          handleResult path $ SomeResult out
          return $ Right (T.pack path)
  where
    ensureDir = createDirectoryIfMissing True
    handleResult path (SomeResult a) = mkImage path a

mkFile :: Opts -> FilePath -> FilePath
mkFile opts base = _outDir opts </> base <.> backendExt opts

data SomeResult = forall r. (MkImage r) => SomeResult r

data SomeBuildOpts v n =
  forall a. (Typeable a, DC.Backend a v n, Hashable (DC.Options a v n), MkImage (DC.Result a v n))
  => SomeBuildOpts (DB.BuildOpts a v n)

class MkImage a where
  mkImage :: FilePath -> a -> IO ()

instance MkImage (IO (), r) where
  mkImage _ = fst

instance MkImage Svg.Element where
  mkImage path e = writeFile path $ show e

mkBuildOpts :: Opts -> [(Text, Text)] -> Text -> SomeBuildOpts V2 Double
mkBuildOpts opts attrs src = case _backend opts of
  Cairo -> SomeBuildOpts $ DB.mkBuildOpts BCairo.Cairo zero
    ( BCairo.CairoOptions "default.png"
      (dims $ V2 (widthAttribute attrs) (heightAttribute attrs))
      (findCairoOutputType $ _outFormat opts)
      False
    )
    & DB.snippets .~ [T.unpack src]
    & DB.imports  .~
      [ "Diagrams.TwoD.Types" -- WHY IS THIS NECESSARY =(
      , "Diagrams.Core.Points" -- GHC 7.2 bug?  need  V (Point R2) = R2  (see #65)
      , "Diagrams.Backend.Cairo"
      , "Diagrams.Backend.Cairo.Internal"
      , "Graphics.SVGFonts"
      , "Data.Typeable"
      ]
    & DB.pragmas .~ ["DeriveDataTypeable"]
    & DB.diaExpr .~ _expression opts
    & DB.postProcess .~ postProcess
    & DB.decideRegen .~
      DB.hashedRegenerate
        (\hash opts' -> opts' { BCairo._cairoFileName = mkFile opts hash })
        (_outDir opts)
  SVG -> SomeBuildOpts $ DB.mkBuildOpts BSvg.SVG zero
    (BSvg.SVGOptions (dims $ V2 (widthAttribute attrs) (heightAttribute attrs)) Nothing "" [] True)
    & DB.snippets .~ [T.unpack src]
    & DB.imports  .~
      [ "Diagrams.TwoD.Types"
      , "Diagrams.Core.Points"
      , "Diagrams.Backend.SVG"
      , "Graphics.SVGFonts"
      , "Data.Typeable"
      ]
    & DB.pragmas .~ ["DeriveDataTypeable"]
    & DB.diaExpr .~ _expression opts
    & DB.postProcess .~ postProcess
  where
    postProcess = pad 1.1 . centerXY

widthAttribute :: [(Text,Text)] -> Double
widthAttribute attrs =
    case lookup "width" attrs of
        Nothing -> 500
        Just v  -> read (T.unpack v) :: Double

heightAttribute :: [(Text,Text)] -> Double
heightAttribute attrs =
    case lookup "height" attrs of
        Nothing -> 200
        Just v  -> read (T.unpack v) :: Double

readEcho :: [(Text, Text)] -> Echo
readEcho attrs = case lookup "echo" attrs of
  Nothing -> Below
  Just v -> case T.toLower v of
    "above" -> Above
    _       -> Below