pollock-0.1.0.0: src/Pollock/ModuleInfo/ModuleHeader.hs
{- |
Module: Pollock.ModuleInfo.ModuleHeader
Copyright: (c) Trevis Elser 2023
License: MIT
Maintainer: trevis@flipstone.com
Stability: experimental
Portability: not-portable
-}
module Pollock.ModuleInfo.ModuleHeader
( ModuleHeader (..)
, processModuleHeader
) where
import qualified Control.Applicative as App
import qualified Data.Char as Char
import qualified Data.Maybe as Maybe
import qualified Pollock.CompatGHC as CompatGHC
-- FIXME Consider safety, language and extensions if they are manually present? Does anyone
-- actually do that? Unclear, but could be useful.
data ModuleHeader = ModuleHeader
{ description :: !(Maybe String)
-- ^ The description field of the Haddock module header.
, copyright :: !(Maybe String)
-- ^ The copyright field of the Haddock module header.
, license :: !(Maybe String)
-- ^ The license field of the Haddock module header.
, maintainer :: !(Maybe String)
-- ^ The maintainer field of the Haddock module header.
, stability :: !(Maybe String)
-- ^ The stability field of the Haddock module header.
, portability :: !(Maybe String)
-- ^ The portability field of the Haddock module header.
}
emptyHaddockModInfo :: ModuleHeader
emptyHaddockModInfo =
ModuleHeader
{ description = Nothing
, copyright = Nothing
, license = Nothing
, maintainer = Nothing
, stability = Nothing
, portability = Nothing
}
processModuleHeader ::
Maybe CompatGHC.HsDocString
-> ModuleHeader
processModuleHeader mayStr =
case mayStr of
Nothing -> emptyHaddockModInfo
Just hds ->
parseModuleHeader $ CompatGHC.renderHsDocString hds
parseModuleHeader ::
String -> ModuleHeader
parseModuleHeader str0 =
let
kvs :: [(String, String)]
kvs = Maybe.fromMaybe mempty $ runP fields str0
-- trim whitespaces
trim :: String -> String
trim = dropWhile Char.isSpace . reverse . dropWhile Char.isSpace . reverse
getKey :: String -> Maybe String
getKey key = fmap trim (lookup key kvs)
descriptionOpt = getKey "Description"
copyrightOpt = getKey "Copyright"
licenseOpt = getKey "License"
licenceOpt = getKey "Licence"
spdxLicenceOpt = getKey "SPDX-License-Identifier"
maintainerOpt = getKey "Maintainer"
stabilityOpt = getKey "Stability"
portabilityOpt = getKey "Portability"
in
ModuleHeader
{ description = descriptionOpt
, copyright = copyrightOpt
, license = spdxLicenceOpt App.<|> licenseOpt App.<|> licenceOpt
, maintainer = maintainerOpt
, stability = stabilityOpt
, portability = portabilityOpt
}
-------------------------------------------------------------------------------
-- Small parser to parse module header.
-------------------------------------------------------------------------------
{- | The below is a small parser framework how we read keys.
all fields in the header are optional and have the form
[spaces1][field name][spaces] ":"
[text]"\n" ([spaces2][space][text]"\n" | [spaces]"\n")*
where each [spaces2] should have [spaces1] as a prefix.
Thus for the key "Description",
> Description : this is a
> rather long
>
> description
>
> The module comment starts here
the value will be "this is a .. description" and the rest will begin
at "The module comment".
-}
{- | 'C' is a 'Char' carrying its column.
This let us make an indentation-aware parser, as we know current indentation.
by looking at the next character in the stream ('curInd').
Thus we can munch all spaces but only not-spaces which are indented.
-}
data C = C {-# UNPACK #-} !Int Char
newtype P a = P {unP :: [C] -> Maybe ([C], a)}
instance Functor P where
fmap f (P pn) =
P ((fmap . fmap) f . pn)
instance Applicative P where
pure x = P $ \s -> Just (s, x)
(<*>) m1 m2 =
P $ \t0 ->
case unP m1 t0 of
Nothing -> Nothing
Just (t1, z) ->
(fmap . fmap) z (unP m2 t1)
instance Monad P where
m >>= k =
P $ \s0 ->
case unP m s0 of
Nothing -> Nothing
Just (s1, x) -> unP (k x) s1
return = pure
instance App.Alternative P where
empty = P $ const Nothing
a <|> b = P $ \s -> unP a s App.<|> unP b s
runP :: P a -> String -> Maybe a
runP p input = fmap snd (unP p input')
where
input' =
concat
[ zipWith C [0 ..] l <> [C (length l) '\n']
| l <- lines input
]
-------------------------------------------------------------------------------
--
-------------------------------------------------------------------------------
curInd :: P Int
curInd = P $ \s -> Just . (,) s $ case s of
[] -> 0
C i _ : _ -> i
munch :: (Int -> Char -> Bool) -> P String
munch p = P $ \cs ->
let (xs, ys) = takeWhileMaybe p' cs in Just (ys, xs)
where
p' (C i c)
| p i c = Just c
| otherwise = Nothing
munch1 :: (Int -> Char -> Bool) -> P String
munch1 p = P $ \s -> case s of
[] -> Nothing
(c : cs)
| Just c' <- p' c -> let (xs, ys) = takeWhileMaybe p' cs in Just (ys, c' : xs)
| otherwise -> Nothing
where
p' (C i c)
| p i c = Just c
| otherwise = Nothing
char :: Char -> P Char
char c = P $ \s -> case s of
[] -> Nothing
(C _ c' : cs)
| c == c' -> Just (cs, c)
| otherwise -> Nothing
skipSpaces :: P ()
skipSpaces = P $ \cs -> Just (dropWhile (\(C _ c) -> Char.isSpace c) cs, ())
takeWhileMaybe :: (a -> Maybe b) -> [a] -> ([b], [a])
takeWhileMaybe f = go
where
go xs0@[] = ([], xs0)
go xs0@(x : xs) = case f x of
Just y -> let (ys, zs) = go xs in (y : ys, zs)
Nothing -> ([], xs0)
-------------------------------------------------------------------------------
-- Fields
-------------------------------------------------------------------------------
field :: Int -> P (String, String)
field i = do
fn <- munch1 $ \_ c -> Char.isAlpha c || c == '-'
skipSpaces
_ <- char ':'
skipSpaces
val <- munch $ \j c -> Char.isSpace c || j > i
pure (fn, val)
fields :: P [(String, String)]
fields = do
skipSpaces
i <- curInd
App.many (field i)