packages feed

literatex-0.2.0.2: src/LiterateX/Renderer.hs

------------------------------------------------------------------------------
-- |
-- Module      : LiterateX.Renderer
-- Description : target renderer
-- Copyright   : Copyright (c) 2021-2022 Travis Cardwell
-- License     : MIT
--
-- This module implements the target renderer.
------------------------------------------------------------------------------

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}

module LiterateX.Renderer
  ( -- * Types
    Options(..)
    -- * API
  , defaultOptions
  , defaultOptionsFor
  , render
  ) where

-- https://hackage.haskell.org/package/base
import Control.Monad (replicateM_)

-- https://hackage.haskell.org/package/conduit
import qualified Data.Conduit as C
import Data.Conduit (ConduitT)

-- https://hackage.haskell.org/package/text
import qualified Data.Text as T
import Data.Text (Text)

-- (literatex)
import LiterateX.Types (CodeLanguage, SourceLine, TargetFormat)
import qualified LiterateX.Types.SourceLine as SourceLine
import qualified LiterateX.Types.TargetFormat as TargetFormat

------------------------------------------------------------------------------
-- $Types

-- | Renderer options determine how output is rendered
--
-- @since 0.0.1.0
data Options
  = Options
    { targetFormat    :: !TargetFormat
    , codeLanguage    :: !(Maybe CodeLanguage)
    , ignoreShebang   :: !Bool  -- ^ 'True' to ignore shebangs
    , renderCode      :: !Bool  -- ^ 'True' to render code
    , numberCodeLines :: !Bool  -- ^ 'True' to number code lines
    }
  deriving Show

------------------------------------------------------------------------------
-- $API

-- | Default options
--
-- * @targetFormat@: 'TargetFormat.PandocMarkdown'
-- * @codeLanguage@: 'Nothing'
-- * @ignoreShebang@: 'True'
-- * @renderCode@: 'True'
-- * @numberCodeLines@: 'True'
--
-- @since 0.0.1.0
defaultOptions :: Options
defaultOptions = Options
    { targetFormat    = TargetFormat.PandocMarkdown
    , codeLanguage    = Nothing
    , ignoreShebang   = True
    , renderCode      = True
    , numberCodeLines = True
    }

-- | Default options for the specified code language
--
-- @since 0.0.1.0
defaultOptionsFor :: CodeLanguage -> Options
defaultOptionsFor lang = defaultOptions
    { codeLanguage = Just lang
    }

-- | Create a "Conduit" transformer that renders using the specified options
--
-- The transformer consumes 'SourceLine' values annotated with the line number
-- and produces lines of output.
--
-- @since 0.0.1.0
render
  :: forall m. Monad m
  => Options
  -> ConduitT (Int, SourceLine) Text m ()
render Options{..} = C.await >>= sStart
  where
    -- Start state
    sStart
      :: Maybe (Int, SourceLine)
      -> ConduitT (Int, SourceLine) Text m ()
    sStart (Just (lineNum, sourceLine)) = case sourceLine of
      -- render shebang if enabled, otherwise skip
      SourceLine.Shebang line
        | renderCode && not ignoreShebang -> do
            C.yield $ startShebang lineNum
            C.yield line
            C.yield endCode
            C.await >>= sBlank
        | otherwise -> C.await >>= sStart
      -- start rendering documentation
      SourceLine.Doc line -> do
        C.yield line
        C.await >>= sDoc
      -- start rendering code if enabled, otherwise skip
      SourceLine.Code line
        | renderCode -> do
            C.yield $ startCode lineNum
            C.yield line
            C.await >>= sCode 0
        | otherwise -> C.await >>= sStart
      -- skip blank lines and rules
      _BlankOrRule -> C.await >>= sStart
    sStart Nothing = return ()

    -- Code state
    sCode
      :: Int  -- ^ number of pending (code) blank lines
      -> Maybe (Int, SourceLine)
      -> ConduitT (Int, SourceLine) Text m ()
    sCode !numBlanks (Just (_lineNum, sourceLine)) = case sourceLine of
      -- render any pending blank lines and code
      SourceLine.Code line -> do
        replicateM_ numBlanks $ C.yield T.empty
        C.yield line
        C.await >>= sCode 0
      -- increment number of pending blank lines
      SourceLine.CodeBlank -> C.await >>= sCode (numBlanks + 1)
      -- end code, add blank line, start rendering documentation
      SourceLine.Doc line -> do
        C.yield endCode
        C.yield T.empty
        C.yield line
        C.await >>= sDoc
      -- end code, enter blank state
      SourceLine.DocBlank -> do
        C.yield endCode
        C.await >>= sBlank
      -- end code, enter blank state
      SourceLine.Rule -> do
        C.yield endCode
        C.await >>= sBlank
      -- shebang only possible on first line (seen in start state)
      SourceLine.Shebang _line -> error "impossible: Shebang in sCode"
    sCode _numBlanks Nothing = C.yield endCode

    -- Documentation state
    sDoc
      :: Maybe (Int, SourceLine)
      -> ConduitT (Int, SourceLine) Text m ()
    sDoc (Just (lineNum, sourceLine)) = case sourceLine of
      -- render documentation
      SourceLine.Doc line -> do
        C.yield line
        C.await >>= sDoc
      -- start rendering code if enabled, otherwise enter blank state
      SourceLine.Code line
        | renderCode -> do
            C.yield T.empty
            C.yield $ startCode lineNum
            C.yield line
            C.await >>= sCode 0
        | otherwise -> C.await >>= sBlank
      -- shebang only possible on first line (seen in start state)
      SourceLine.Shebang _line -> error "impossible: Shebang in sDoc"
      -- blank lines and rules transition to blank state
      _BlankOrRule -> C.await >>= sBlank
    sDoc Nothing = return ()

    -- Blank state
    sBlank
      :: Maybe (Int, SourceLine)
      -> ConduitT (Int, SourceLine) Text m ()
    sBlank (Just (lineNum, sourceLine)) = case sourceLine of
      -- start rendering code if enabled, otherwise skip
      SourceLine.Code line
        | renderCode -> do
            C.yield T.empty
            C.yield $ startCode lineNum
            C.yield line
            C.await >>= sCode 0
        | otherwise -> C.await >>= sBlank
      -- render blank line, start rendering documentation
      SourceLine.Doc line -> do
        C.yield T.empty
        C.yield line
        C.await >>= sDoc
      -- shebang only possible on first line (seen in start state)
      SourceLine.Shebang _line  -> error "impossible: Shebang in sBlank"
      -- skip additional blank lines and rules
      _BlankOrRule -> C.await >>= sBlank
    sBlank Nothing = return ()

    -- start code line for code starting at given line number
    startCode :: Int -> Text
    startCode =
      TargetFormat.mkBeginCode targetFormat codeLanguage numberCodeLines

    -- shebang start code line does not use syntax highlighting
    startShebang :: Int -> Text
    startShebang =
      TargetFormat.mkBeginCode targetFormat Nothing numberCodeLines

    -- end code line
    endCode :: Text
    endCode = TargetFormat.mkEndCode targetFormat
{-# ANN render ("HLint: ignore Reduce duplication" :: String) #-}