rib-0.5.0.0: src/Rib/Markup/Pandoc.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
-- Suppressing orphans warning for `Markup Pandoc` instance
-- | Helpers for working with Pandoc documents
module Rib.Markup.Pandoc
( -- * Manual rendering
renderPandoc,
renderPandocInlines,
-- * Extracting information
getH1,
getFirstImg,
-- * Re-exports
Pandoc,
)
where
import Control.Monad.Except
import Data.Aeson
import Lucid (Html, toHtmlRaw)
import Named
import Path
import Relude.Extra.Map ((!?))
import Rib.Markup
import Text.Pandoc
import Text.Pandoc.Filter.IncludeCode (includeCode)
import Text.Pandoc.Walk (query, walkM)
import qualified Text.Show
data RibPandocError
= RibPandocError_PandocError PandocError
| RibPandocError_UnknownFormat UnknownExtension
instance Show RibPandocError where
show = \case
RibPandocError_PandocError e ->
show e
RibPandocError_UnknownFormat s ->
"Unsupported extension: " <> show s
instance Markup Pandoc where
parseDoc k s = first show $ runExcept $ do
r <-
withExcept RibPandocError_UnknownFormat $
detectReader k
withExcept RibPandocError_PandocError
$ runPure'
$ r readerSettings s
readDoc (Arg k) (Arg f) = fmap (first show) $ runExceptT $ do
content <- readFileText (toFilePath f)
r <-
withExceptT RibPandocError_UnknownFormat $
detectReader k
withExceptT RibPandocError_PandocError $ do
v' <- runIO' $ r readerSettings content
liftIO $ walkM includeSources v'
where
includeSources = includeCode $ Just $ Format "html5"
extractMeta (Pandoc meta _) = flattenMeta meta
renderDoc doc = first show $ runExcept $ do
withExcept RibPandocError_PandocError
$ runPure'
$ fmap toHtmlRaw
$ writeHtml5String writerSettings doc
runPure' :: MonadError PandocError m => PandocPure a -> m a
runPure' = liftEither . runPure
runIO' :: (MonadError PandocError m, MonadIO m) => PandocIO a -> m a
runIO' = liftEither <=< liftIO . runIO
-- | Parse and render the markup directly to HTML
renderPandoc :: Path Rel File -> Text -> Html ()
renderPandoc f s = either (error . show) id $ runExcept $ do
doc <- liftEither $ parseDoc @Pandoc f s
liftEither $ renderDoc doc
-- | Render a list of Pandoc `Text.Pandoc.Inline` values as Lucid HTML
--
-- Useful when working with `Text.Pandoc.Meta` values from the document metadata.
renderPandocInlines :: [Inline] -> Html ()
renderPandocInlines =
either (error . show) toHtmlRaw
. renderDoc
. Pandoc mempty
. pure
. Plain
-- | Get the top-level heading as Lucid HTML
getH1 :: Pandoc -> Maybe (Html ())
getH1 (Pandoc _ bs) = fmap renderPandocInlines $ flip query bs $ \case
Header 1 _ xs -> Just xs
_ -> Nothing
-- | Get the first image in the document if one exists
getFirstImg ::
Pandoc ->
-- | Relative URL path to the image
Maybe Text
getFirstImg (Pandoc _ bs) = listToMaybe $ flip query bs $ \case
Image _ _ (url, _) -> [toText url]
_ -> []
exts :: Extensions
exts =
mconcat
[ extensionsFromList
[ Ext_yaml_metadata_block,
Ext_fenced_code_attributes,
Ext_auto_identifiers,
Ext_smart
],
githubMarkdownExtensions
]
readerSettings :: ReaderOptions
readerSettings = def {readerExtensions = exts}
writerSettings :: WriterOptions
writerSettings = def {writerExtensions = exts}
-- Internal code
data UnknownExtension
= UnknownExtension String
deriving (Show, Eq)
-- | Detect the Pandoc reader to use based on file extension
detectReader ::
forall m m1.
(MonadError UnknownExtension m, PandocMonad m1) =>
Path Rel File ->
m (ReaderOptions -> Text -> m1 Pandoc)
detectReader f = do
ext <-
liftEither . first (UnknownExtension . show) $
fileExtension f
liftEither . maybeToRight (UnknownExtension ext) $
formats !? ext
where
formats :: Map String (ReaderOptions -> Text -> m1 Pandoc)
formats =
fromList
[ (".md", readMarkdown),
(".rst", readRST),
(".org", readOrg),
(".tex", readLaTeX),
(".ipynb", readIpynb)
]
-- | Flatten a Pandoc 'Meta' into a well-structured JSON object.
--
-- Renders Pandoc text objects into plain strings along the way.
flattenMeta :: Meta -> Maybe (Either Text Value)
flattenMeta (Meta meta) = fmap toJSON . traverse go <$> guarded null meta
where
go :: MetaValue -> Either Text Value
go (MetaMap m) = toJSON <$> traverse go m
go (MetaList m) = toJSONList <$> traverse go m
go (MetaBool m) = pure $ toJSON m
go (MetaString m) = pure $ toJSON m
go (MetaInlines m) =
bimap show toJSON
$ runPure . plainWriter
$ Pandoc mempty [Plain m]
go (MetaBlocks m) =
bimap show toJSON
$ runPure . plainWriter
$ Pandoc mempty m
plainWriter = writePlain def