packages feed

literatex-0.1.0.0: src/LiterateX/Parser.hs

------------------------------------------------------------------------------
-- |
-- Module      : LiterateX.Parser
-- Description : source parser
-- Copyright   : Copyright (c) 2021 Travis Cardwell
-- License     : MIT
--
-- This module implements the source parser.
------------------------------------------------------------------------------

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-}

module LiterateX.Parser
  ( -- * API
    parse
  ) where

-- https://hackage.haskell.org/package/base
import Control.Monad (guard)
import Data.Maybe (fromMaybe)

-- 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 (SourceFormat, SourceLine)
import qualified LiterateX.Types.SourceFormat as SourceFormat
import qualified LiterateX.Types.SourceLine as SourceLine

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

-- | Create a "Conduit" transformer that parses the specified source format
--
-- The transformer consumes lines of the input and produces a 'SourceLine' for
-- each line of input.
--
-- @since 0.0.1.0
parse
  :: Monad m
  => SourceFormat
  -> ConduitT Text SourceLine m ()
parse = parseSourceLines . parserFunctionsFor

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

-- | Parser functions that determine how input is parsed
data ParserFunctions
  = ParserFunctions
    { isCodeBlank :: !(Text -> Bool)
    , isDocBlank  :: !(Text -> Bool)
    , isRule      :: !(Text -> Bool)
    , getDoc      :: !(Text -> Maybe Text)
    , getCode     :: !(Text -> Text)
    }

------------------------------------------------------------------------------

-- | Get the parser functions for the specified source format
parserFunctionsFor :: SourceFormat -> ParserFunctions
parserFunctionsFor = \case
    SourceFormat.DoubleDash      -> lineCommentParserFunctions '-' 2
    SourceFormat.DoubleSlash     -> lineCommentParserFunctions '/' 2
    SourceFormat.Hash            -> lineCommentParserFunctions '#' 1
    SourceFormat.LiterateHaskell -> literateHaskellParserFunctions
    SourceFormat.Percent         -> lineCommentParserFunctions '%' 1
    SourceFormat.LispSemicolons  -> lispCommentParserFunctions

------------------------------------------------------------------------------

-- | Get parser functions for source with line-based comments
lineCommentParserFunctions
  :: Char  -- ^ comment character
  -> Int   -- ^ number of comment characters to create line comment
  -> ParserFunctions
lineCommentParserFunctions char count = ParserFunctions{..}
  where
    docBlank :: Text
    docBlank = T.pack $ replicate count char

    prefixLen :: Int
    prefixLen = count + 1

    prefix :: Text
    prefix = T.pack $ replicate count char ++ " "

    isCodeBlank :: Text -> Bool
    isCodeBlank = T.null

    isDocBlank :: Text -> Bool
    isDocBlank = (== docBlank)

    isRule :: Text -> Bool
    isRule line = T.length line > count && T.all (== char) line

    getDoc :: Text -> Maybe Text
    getDoc line = do
      let (linePrefix, lineSuffix) = T.splitAt prefixLen line
      guard $ linePrefix == prefix
      pure lineSuffix

    getCode :: Text -> Text
    getCode = id

------------------------------------------------------------------------------

-- | Get parser functions for source with Lisp-style comments
--
-- Lisp-style comments begin with one or more semicolons.
lispCommentParserFunctions :: ParserFunctions
lispCommentParserFunctions = ParserFunctions{..}
  where
    isCodeBlank :: Text -> Bool
    isCodeBlank = T.null

    isDocBlank :: Text -> Bool
    isDocBlank line =
      let len = T.length line
      in  len >= 1 && len <= 4 && T.all (== ';') line

    isRule :: Text -> Bool
    isRule line = T.length line > 4 && T.all (== ';') line

    getDoc :: Text -> Maybe Text
    getDoc line = do
      let (linePrefix, (sep, lineSuffix)) = T.splitAt 1 <$> T.breakOn " " line
      guard $ not (T.null linePrefix) && T.all (== ';') linePrefix
      guard $ sep == " "
      pure lineSuffix

    getCode :: Text -> Text
    getCode = id

------------------------------------------------------------------------------

-- | Get parser functions for parsing literate Haskell
literateHaskellParserFunctions :: ParserFunctions
literateHaskellParserFunctions = ParserFunctions{..}
  where
    isCodeBlank :: Text -> Bool
    isCodeBlank = (== ">")

    isDocBlank :: Text -> Bool
    isDocBlank = T.null

    isRule :: Text -> Bool
    isRule = const False

    getDoc :: Text -> Maybe Text
    getDoc line
      | "> " `T.isPrefixOf` line = Nothing
      | otherwise                = Just line

    getCode :: Text -> Text
    getCode line = fromMaybe line $ T.stripPrefix "> " line

------------------------------------------------------------------------------

-- | Create a "Conduit" transformer for the specified parser functions
--
-- This function produces a 'SourceLine' for each line of input.  A
-- 'SourceLine.Shebang' can only be produced on the first line.  Note that the
-- order that the parser functions are used is significant; the parser
-- functions are written for this order.
parseSourceLines
  :: Monad m
  => ParserFunctions
  -> ConduitT Text SourceLine m ()
parseSourceLines ParserFunctions{..} = do
    mLine <- C.await
    case mLine of
      Just line -> do
        C.yield $ if "#!" `T.isPrefixOf` line
          then SourceLine.Shebang line
          else parse' line
        C.awaitForever $ C.yield . parse'
      Nothing -> return ()
  where
    parse' :: Text -> SourceLine
    parse' line
      | isCodeBlank line = SourceLine.CodeBlank
      | isDocBlank line  = SourceLine.DocBlank
      | isRule line      = SourceLine.Rule
      | otherwise        = case (getDoc line, getCode line) of
          (Just doc, _code) -> SourceLine.Doc doc
          (Nothing,  code)  -> SourceLine.Code code