packages feed

pollock-0.1.0.0: src/Pollock/Documentation/Parser.hs

{- |
Module: Pollock.Documentation.Parser
Copyright: (c) Trevis Elser 2023
License: MIT
Maintainer: trevis@flipstone.com
Stability: experimental
Portability: non-portable
-}
module Pollock.Documentation.Parser
  ( processDocStringParas
  , processDocStrings
  , parseText
  )
where

import qualified Control.Applicative as App
import qualified Control.Monad as M
import qualified Data.Attoparsec.Text as AttoText
import qualified Data.Char as Char
import qualified Data.Text as T

import qualified Pollock.CompatGHC as CompatGHC
import Pollock.Documentation.Doc
  ( Doc
      ( DocCodeBlock
      , DocEmpty
      , DocParagraph
      , DocProperty
      , DocString
      )
  )
import Pollock.Documentation.Metadata (Metadata (Metadata, version))
import Pollock.Documentation.MetadataAndDoc
  ( MetaAndDoc (MetaAndDoc, doc, meta)
  , metaAndDocConcat
  , withEmptyMetadata
  )

parseText :: T.Text -> Doc
parseText =
  either error id
    . AttoText.parseOnly (fmap docStringFromText AttoText.takeText)
    . T.filter (/= '\r')

processDocStringParas ::
  CompatGHC.HsDocString -> MetaAndDoc
processDocStringParas =
  either error id
    . AttoText.parseOnly parseParas
    . T.pack
    . filter (/= '\r')
    . CompatGHC.renderHsDocString

processDocStrings ::
  [CompatGHC.HsDocString]
  -> Maybe MetaAndDoc
processDocStrings strs =
  case metaAndDocConcat $ fmap processDocStringParas strs of
    -- We check that we don't have any version info to render instead
    -- of just checking if there is no comment: there may not be a
    -- comment but we still want to pass through any meta data.
    MetaAndDoc{meta = Metadata Nothing, doc = DocEmpty} -> Nothing
    x -> Just x

since :: AttoText.Parser MetaAndDoc
since = do
  skipHorizontalSpace
  _ <- AttoText.string (T.pack "@since ")
  s <- AttoText.sepBy1 AttoText.decimal (AttoText.string $ T.pack ".")
  skipHorizontalSpace
  let
    metadata =
      Metadata
        { version = Just s
        }
  pure $ MetaAndDoc metadata DocEmpty

skipHorizontalSpace :: AttoText.Parser ()
skipHorizontalSpace =
  AttoText.skipWhile AttoText.isHorizontalSpace

takeLine :: AttoText.Parser T.Text
takeLine = takeToEndOfLine

takeNonEmptyLine :: AttoText.Parser T.Text
takeNonEmptyLine =
  M.mfilter (T.any (not . Char.isSpace)) takeLine

birdtracks :: AttoText.Parser MetaAndDoc
birdtracks =
  let line = do
        skipHorizontalSpace
        _ <- AttoText.string (T.pack ">")
        takeLine
   in fmap (withEmptyMetadata . DocCodeBlock . docStringFromText . T.intercalate (T.pack "\n")) $
        AttoText.many1 line

paragraph :: AttoText.Parser MetaAndDoc
paragraph =
  AttoText.choice
    [ since
    , birdtracks
    , fmap withEmptyMetadata codeblock
    , fmap withEmptyMetadata property
    , fmap (withEmptyMetadata . docStringFromText) takeLine
    , fmap (withEmptyMetadata . DocParagraph) textParagraph
    ]

docStringFromText :: T.Text -> Doc
docStringFromText = DocString . T.unpack

textParagraph :: AttoText.Parser Doc
textParagraph = do
  lines' <- AttoText.many1 takeNonEmptyLine
  App.pure $ (docStringFromText . T.intercalate (T.pack "\n")) lines'

parseParas :: AttoText.Parser MetaAndDoc
parseParas =
  fmap metaAndDocConcat . AttoText.many' $ do
    p <- paragraph
    consumeEmptyLines
    App.pure p

{- | Property parser.

>>> snd <$> parseOnly property "prop> hello world"
Right (DocProperty "hello world")
-}
property :: AttoText.Parser Doc
property =
  fmap (DocProperty . T.unpack . T.strip) $ do
    _ <- AttoText.string (T.pack "prop>")
    takeToEndOfLine

{- |
Paragraph level codeblock. Anything between the two delimiting \@ is parsed
for markup.
-}
codeblock :: AttoText.Parser Doc
codeblock = do
  let
    atText = T.pack "@"
  _ <- AttoText.string atText
  skipHorizontalSpace
  AttoText.endOfLine
  blockDoc <- textParagraph
  _ <- AttoText.string atText
  App.pure $ DocCodeBlock blockDoc

takeToEndOfLine :: AttoText.Parser T.Text
takeToEndOfLine = AttoText.takeWhile1 (not . AttoText.isEndOfLine)

consumeEmptyLines :: AttoText.Parser ()
consumeEmptyLines =
  M.void . AttoText.many' $ do
    skipHorizontalSpace
    AttoText.endOfLine