packages feed

literatex-0.1.0.0: src/LiterateX.hs

------------------------------------------------------------------------------
-- |
-- Module      : LiterateX
-- Description : API
-- Copyright   : Copyright (c) 2021 Travis Cardwell
-- License     : MIT
--
-- This module provides high-level as well as low-level API functions for
-- transforming literate source code.
------------------------------------------------------------------------------

{-# LANGUAGE FlexibleInstances #-}

module LiterateX
  ( -- * Constants
    version
    -- * API
    -- ** High-Level
    -- $HighLevelAPI
  , transformTextToText
  , transformTextToHandle
  , transformTextToFile
  , transformHandleToText
  , transformHandleToHandle
  , transformHandleToFile
  , transformFileToText
  , transformFileToHandle
  , transformFileToFile
    -- ** Low-Level
    -- $LowLevelAPI
  , runPure
  , runIO
  , runResource
    -- *** Producers
  , sourceString
  , sourceText
  , sourceLazyText
  , sourceByteString
  , sourceLazyByteString
  , sourceHandle
  , sourceFile
    -- *** Consumers
  , sinkString
  , sinkText
  , sinkLazyText
  , sinkByteString
  , sinkLazyByteString
  , sinkHandle
  , sinkFile
    -- *** Transformers
  , transform
  ) where

-- https://hackage.haskell.org/package/base
import Control.Monad.IO.Class (MonadIO)
import Data.Version (showVersion)
import System.IO (Handle)

-- https://hackage.haskell.org/package/bytestring
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BSL

-- https://hackage.haskell.org/package/conduit
import qualified Conduit as C
import Data.Conduit ((.|))
import qualified Data.Conduit.Combinators as CC
import qualified Data.Conduit.List as CL

-- https://hackage.haskell.org/package/text
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import qualified Data.Text.Encoding.Error as TEE
import qualified Data.Text.Lazy as TL

-- https://hackage.haskell.org/package/unliftio
import UnliftIO (MonadUnliftIO)

-- (literatex)
import qualified LiterateX.Parser as Parser
import qualified LiterateX.Renderer as Renderer
import LiterateX.Types (SourceFormat)

-- (literatex:cabal)
import qualified Paths_literatex as Project

------------------------------------------------------------------------------
-- $Constants

-- | LiterateX version string (\"@literatex-haskell X.X.X@\")
version :: String
version = "literatex-haskell " ++ showVersion Project.version

------------------------------------------------------------------------------
-- $HighLevelAPI
--
-- This high-level API provides functions for transforming literate source
-- code.  These functions provide support for transforming from/to lazy
-- 'TL.Text', 'Handle's, and files.
--
-- Example usage:
--
-- @
-- {-\# LANGUAGE OverloadedStrings \#-}
--
-- module Main (main) where
--
-- -- https://hackage.haskell.org/package/literatex
-- import qualified LiterateX
-- import qualified LiterateX.Renderer as Renderer
-- import qualified LiterateX.Types.SourceFormat as SourceFormat
--
-- main :: IO ()
-- main = LiterateX.transformFileToFile
--     SourceFormat.LiterateHaskell
--     (Renderer.defaultOptionsFor "haskell")
--     "demo.lhs"
--     "demo.md"
-- @

-- | Transform from lazy 'TL.Text' to lazy 'TL.Text'
--
-- @since 0.0.1.0
transformTextToText
  :: SourceFormat
  -> Renderer.Options
  -> TL.Text
  -> TL.Text
transformTextToText sourceFormat rendererOpts source =
    runPure sourceFormat rendererOpts (sourceLazyText source) sinkLazyText

-- | Transform from lazy 'TL.Text' to a 'Handle'
--
-- @since 0.0.1.0
transformTextToHandle
  :: MonadIO m
  => SourceFormat
  -> Renderer.Options
  -> TL.Text
  -> Handle
  -> m ()
transformTextToHandle sourceFormat rendererOpts source target =
    runIO sourceFormat rendererOpts
      (sourceLazyText source)
      (sinkHandle target)

-- | Transform from lazy 'TL.Text' to a file
--
-- @since 0.0.1.0
transformTextToFile
  :: MonadUnliftIO m
  => SourceFormat
  -> Renderer.Options
  -> TL.Text
  -> FilePath
  -> m ()
transformTextToFile sourceFormat rendererOpts source target =
    runResource sourceFormat rendererOpts
      (sourceLazyText source)
      (sinkFile target)

-- | Transform from a 'Handle' to lazy 'TL.Text'
--
-- @since 0.0.1.0
transformHandleToText
  :: MonadIO m
  => SourceFormat
  -> Renderer.Options
  -> Handle
  -> m TL.Text
transformHandleToText sourceFormat rendererOpts source =
    runIO sourceFormat rendererOpts (sourceHandle source) sinkLazyText

-- | Transform from a 'Handle' to a 'Handle'
--
-- @since 0.0.1.0
transformHandleToHandle
  :: MonadIO m
  => SourceFormat
  -> Renderer.Options
  -> Handle
  -> Handle
  -> m ()
transformHandleToHandle sourceFormat rendererOpts source target =
    runIO sourceFormat rendererOpts (sourceHandle source) (sinkHandle target)

-- | Transform from a 'Handle' to a file
--
-- @since 0.0.1.0
transformHandleToFile
  :: MonadUnliftIO m
  => SourceFormat
  -> Renderer.Options
  -> Handle
  -> FilePath
  -> m ()
transformHandleToFile sourceFormat rendererOpts source target =
    runResource sourceFormat rendererOpts
      (sourceHandle source)
      (sinkFile target)

-- | Transform from a file to lazy 'TL.Text'
--
-- @since 0.0.1.0
transformFileToText
  :: MonadUnliftIO m
  => SourceFormat
  -> Renderer.Options
  -> FilePath
  -> m TL.Text
transformFileToText sourceFormat rendererOpts source =
    runResource sourceFormat rendererOpts (sourceFile source) sinkLazyText

-- | Transform from a file to a 'Handle'
--
-- @since 0.0.1.0
transformFileToHandle
  :: MonadUnliftIO m
  => SourceFormat
  -> Renderer.Options
  -> FilePath
  -> Handle
  -> m ()
transformFileToHandle sourceFormat rendererOpts source target =
    runResource sourceFormat rendererOpts
      (sourceFile source)
      (sinkHandle target)

-- | Transform from a file to a file
--
-- @since 0.0.1.0
transformFileToFile
  :: MonadUnliftIO m
  => SourceFormat
  -> Renderer.Options
  -> FilePath
  -> FilePath
  -> m ()
transformFileToFile sourceFormat rendererOpts source target =
    runResource sourceFormat rendererOpts
      (sourceFile source)
      (sinkFile target)

------------------------------------------------------------------------------
-- $LowLevelAPI
--
-- This low-level API provides more control over transforming literate source
-- code, using "Conduit".  The 'transform' transformer implements the
-- transformation, transforming lines of input 'T.Text' to lines of output
-- 'T.Text'.  Various producers are provided to produce lines of input
-- 'T.Text' from common sources, and various consumers are provided to consume
-- lines of output 'T.Text' to common sinks.  These can be used separately if
-- necessary, but some run functions are provided for common usage.
--
-- Example usage:
--
-- @
-- {-\# LANGUAGE OverloadedStrings \#-}
--
-- module Main (main) where
--
-- -- https://hackage.haskell.org/package/base
-- import System.IO (stdout)
--
-- -- https://hackage.haskell.org/package/literatex
-- import qualified LiterateX
-- import qualified LiterateX.Renderer as Renderer
-- import qualified LiterateX.Types.SourceFormat as SourceFormat
--
-- main :: IO ()
-- main = do
--     let demoBS = "..."
--     LiterateX.runIO
--       SourceFormat.LiterateHaskell
--       (Renderer.defaultOptionsFor "haskell")
--       (LiterateX.sourceByteString demoBS)
--       (LiterateX.sinkHandle stdout)
-- @

-- | Run a pure LiterateX transformation
--
-- This function works with the following input line producers:
--
-- * 'sourceString'
-- * 'sourceText'
-- * 'sourceLazyText'
-- * 'sourceByteString'
-- * 'sourceLazyByteString'
--
-- This function works with the following output line consumers:
--
-- * 'sinkString'
-- * 'sinkText'
-- * 'sinkLazyText'
-- * 'sinkByteString'
-- * 'sinkLazyByteString'
--
-- @since 0.0.1.0
runPure
  :: SourceFormat
  -> Renderer.Options
  -> C.ConduitT () T.Text C.Identity ()     -- ^ input line producer
  -> C.ConduitT T.Text C.Void C.Identity r  -- ^ output line consumer
  -> r
runPure sourceFormat rendererOpts source sink = C.runConduitPure $
    source .| transform sourceFormat rendererOpts .| sink

-- | Run a LiterateX transformation using IO
--
-- This function works with the following input line producers:
--
-- * 'sourceString'
-- * 'sourceText'
-- * 'sourceLazyText'
-- * 'sourceByteString'
-- * 'sourceLazyByteString'
-- * 'sourceHandle'
--
-- This function works with the following output line consumers:
--
-- * 'sinkString'
-- * 'sinkText'
-- * 'sinkLazyText'
-- * 'sinkByteString'
-- * 'sinkLazyByteString'
-- * 'sinkHandle'
--
-- @since 0.0.1.0
runIO
  :: MonadIO m
  => SourceFormat
  -> Renderer.Options
  -> C.ConduitT () T.Text m ()     -- ^ input line producer
  -> C.ConduitT T.Text C.Void m r  -- ^ output line consumer
  -> m r
runIO sourceFormat rendererOpts source sink = C.runConduit $
    source .| transform sourceFormat rendererOpts .| sink

-- | Run a LiterateX transformation using resource management
--
-- This function works with the following input line producers:
--
-- * 'sourceString'
-- * 'sourceText'
-- * 'sourceLazyText'
-- * 'sourceByteString'
-- * 'sourceLazyByteString'
-- * 'sourceHandle'
-- * 'sourceFile'
--
-- This function works with the following output line consumers:
--
-- * 'sinkString'
-- * 'sinkText'
-- * 'sinkLazyText'
-- * 'sinkByteString'
-- * 'sinkLazyByteString'
-- * 'sinkHandle'
-- * 'sinkFile'
--
-- @since 0.0.1.0
runResource
  :: MonadUnliftIO m
  => SourceFormat
  -> Renderer.Options
  -> C.ConduitT () T.Text (C.ResourceT m) ()     -- ^ input line producer
  -> C.ConduitT T.Text C.Void (C.ResourceT m) r  -- ^ output line consumer
  -> m r
runResource sourceFormat rendererOpts source sink = C.runConduitRes $
    source .| transform sourceFormat rendererOpts .| sink

------------------------------------------------------------------------------
-- $Producers

-- | Produce input lines from a 'String' source
--
-- @since 0.0.1.0
sourceString
  :: Monad m
  => String
  -> C.ConduitT i T.Text m ()
sourceString = CC.yieldMany . map T.pack . lines

-- | Produce input lines from a 'T.Text' source
--
-- @since 0.0.1.0
sourceText
  :: Monad m
  => T.Text
  -> C.ConduitT i T.Text m ()
sourceText source = C.yield source .| CC.linesUnbounded

-- | Produce input lines from a lazy 'TL.Text' source
--
-- @since 0.0.1.0
sourceLazyText
  :: Monad m
  => TL.Text
  -> C.ConduitT i T.Text m ()
sourceLazyText source = CC.sourceLazy source .| CC.linesUnbounded

-- | Produce input lines from a 'BS.ByteString' source
--
-- @since 0.0.1.0
sourceByteString
  :: Monad m
  => BS.ByteString
  -> C.ConduitT i T.Text m ()
sourceByteString source =
    C.yield source .| CC.linesUnboundedAscii .| decodeUtf8LinesLenient

-- | Produce input lines from a lazy 'BS.ByteString' source
--
-- @since 0.0.1.0
sourceLazyByteString
  :: Monad m
  => BSL.ByteString
  -> C.ConduitT i T.Text m ()
sourceLazyByteString source =
    CC.sourceLazy source .| CC.linesUnboundedAscii .| decodeUtf8LinesLenient

-- | Produce input lines from a 'Handle' source
--
-- @since 0.0.1.0
sourceHandle
  :: MonadIO m
  => Handle
  -> C.ConduitT i T.Text m ()
sourceHandle source =
    CC.sourceHandle source .| CC.linesUnboundedAscii .| decodeUtf8LinesLenient

-- | Produce input lines from a file source
--
-- @since 0.0.1.0
sourceFile
  :: C.MonadResource m
  => FilePath
  -> C.ConduitT i T.Text m ()
sourceFile source =
    CC.sourceFile source .| CC.linesUnboundedAscii .| decodeUtf8LinesLenient

------------------------------------------------------------------------------
-- $Consumers

-- | Consume output lines, returning a 'String'
--
-- @since 0.0.1.0
sinkString
  :: Monad m
  => C.ConduitT T.Text o m String
sinkString = fmap TL.unpack $ CC.unlines .| CC.sinkLazy

-- | Consume output lines, returning 'T.Text'
--
-- @since 0.0.1.0
sinkText
  :: Monad m
  => C.ConduitT T.Text o m T.Text
sinkText = fmap TL.toStrict $ CC.unlines .| CC.sinkLazy

-- | Consume output lines, returning lazy 'T.Text'
--
-- @since 0.0.1.0
sinkLazyText
  :: Monad m
  => C.ConduitT T.Text o m TL.Text
sinkLazyText = CC.unlines .| CC.sinkLazy

-- | Consume output lines, returning a 'BS.ByteString'
--
-- @since 0.0.1.0
sinkByteString
  :: Monad m
  => C.ConduitT T.Text o m BS.ByteString
sinkByteString =
    fmap BSL.toStrict $ CC.unlines .| CC.encodeUtf8 .| CC.sinkLazy

-- | Consume output lines, returning a lazy 'BS.ByteString'
--
-- @since 0.0.1.0
sinkLazyByteString
  :: Monad m
  => C.ConduitT T.Text o m BSL.ByteString
sinkLazyByteString = CC.unlines .| CC.encodeUtf8 .| CC.sinkLazy

-- | Consume output lines, writing to a 'Handle'
--
-- @since 0.0.1.0
sinkHandle
  :: MonadIO m
  => Handle
  -> C.ConduitT T.Text o m ()
sinkHandle target = CC.encodeUtf8 .| CC.unlinesAscii .| CC.sinkHandle target

-- | Consume output lines, writing to a file
--
-- @since 0.0.1.0
sinkFile
  :: C.MonadResource m
  => FilePath
  -> C.ConduitT T.Text o m ()
sinkFile target = CC.encodeUtf8 .| CC.unlinesAscii .| CC.sinkFile target

------------------------------------------------------------------------------
-- $Transformers

-- | Transform input lines to output lines
--
-- @since 0.0.1.0
transform
  :: Monad m
  => SourceFormat
  -> Renderer.Options
  -> C.ConduitT T.Text T.Text m ()
transform sourceFormat rendererOpts
    = Parser.parse sourceFormat
    .| CL.concatMapAccum (\x n -> (n + 1, [(n, x)])) 1
    .| Renderer.render rendererOpts

------------------------------------------------------------------------------
-- $Internal

-- | Decode UTF-8 'BS.ByteString' lines to 'T.Text'
--
-- Note that 'CC.decodeUtf8Lenient' decodes UTF-8 streams and therefore
-- discards empty strings.  This version decodes UTF-8 lines and does not
-- discard empty lines.
decodeUtf8LinesLenient
  :: Monad m
  => C.ConduitT BS.ByteString T.Text m ()
decodeUtf8LinesLenient =
    C.awaitForever $ C.yield . TE.decodeUtf8With TEE.lenientDecode