CurryInfo: currydoc-5.0.0 / CurryDoc.Info.Header

classes: Info
 
documentation: Info
 
name: Info
 CurryDoc.Info.Header
operations: Info
 getCategoryWithDefault getFieldWithDefault readModuleHeader
sourcecode: Info
 
{- |
     Author  : Kai-Oliver Prott
     Version : May 2025

     Operations to parse the module comments into a usable format.
-}
module CurryDoc.Info.Header
  ( ModuleHeader(..), HeaderField(..), readModuleHeader
  , getCategoryWithDefault, getFieldWithDefault )
  where

import CurryDoc.Info.Goodies
import CurryDoc.Info.Comments

import Data.Char  ( isSpace )
import Data.Maybe ( fromMaybe )
import Data.List  ( isPrefixOf, sort, intercalate)

-- | The header of a Curry module.
data ModuleHeader = ModuleHeader [(HeaderField, String)] String
  deriving (Show, Read)

-- | The types of header fields.
data HeaderField = Description
                 | Category
                 | Author
                 | Version
  deriving (Eq, Show, Read, Enum, Ord)

-- | Reads the module header from a list of comments.
readModuleHeader :: [Comment] -> ModuleHeader
readModuleHeader = orderFields . readFullDesc . removeBorder . toStrings

-- | Reads the full description of a module header.
--   
--   This implementation allows for an arbitrary structure of header fields
--   and descriptions, with no requirements regarding the order.
-- 
--   We have to take care of the indentation of a paragraph,
--   because if a line in the paragraph is indented even further,
--   then it might be markdown. This is why trimming space would be wrong, 
--   as markdown would be broken. Additionally, the comments should be concatenated with
--   something like a unlines, thus the newlines in between.
readFullDesc :: [String] -> ModuleHeader
readFullDesc strs = readFullDesc' (ModuleHeader [] "") (getIndentation strs) strs
 where
  readFullDesc' h _    []    = h
  readFullDesc' h intd (s:ss)
     -- Skip empty lines: 
     | all isSpace s      = readFullDesc' (addComment h "\n") (getIndentation ss) ss
     -- Read indented paragraph:
     | spaceAmount > intd = readFullDesc' (addComment h $ drop intd s ++ "\n") intd ss
     -- Read either a field or a comment:
     | otherwise = 
        case readCommentLine intd s ss of
          (HeaderFieldLine f v, rest) 
            -> readFullDesc' (addField h f v) intd rest
          (Line t, rest)
            -> readFullDesc' (addComment h $ t ++ "\n") spaceAmount rest
   where 
    spaceAmount = countIndent s

-----------------------------------------------------------
-- Header field parsing and handling

data HeaderComment = Line String
                   | HeaderFieldLine HeaderField String
  deriving (Show, Read) 

-- | Converts a string to a header field.
stringToHeaderField :: String -> Maybe HeaderField
stringToHeaderField str = case toLowerString str of
  "description" -> Just Description
  "category"    -> Just Category
  "author"      -> Just Author
  "version"     -> Just Version
  _             -> Nothing

-- | Reads a header comment line. Parses the line as either a field-value
--   pair or a normal comment. The indentation is used to determine the
--   end of the field-value pair.
--
--   The function returns the parsed header comment and the remaining lines.
readCommentLine :: Int -> String -> [String] -> (HeaderComment, [String])
readCommentLine i s ss 
  | "@" `isPrefixOf` s = 
      let f  = tail $ head $ words s
          s' = dropTokens 1 s
          v  = unlines (s' : ssV)
      in makeField f v
  | otherwise =
      let (field, v) = break (== ':') s
      in case v of
        []     -> -- no field, treat as comment
          (Line $ dropSpaces s, ss) 
        (_:v') -> -- drop the colon and trim spaces
          makeField (trimSpace field) (unlines (trimSpace v' : ssV))
  where
    (ssV, rest) = splitWhileIndented i ss

    -- Tries to create a field-value pair from the given string.
    -- If the field is not recognized, the line is treated as a comment.
    makeField :: String -> String -> (HeaderComment, [String])
    makeField f v = case stringToHeaderField f of
      Just field -> (HeaderFieldLine field v, rest)
      Nothing    -> (Line $ dropSpaces s,       ss)

-----------------------------------------------------------
-- Helper functions for indentation and comments

-- | Splits a list of strings into two lists, where the first list
--   contains the longest prefix of the input list, where each element
--   is indented by at least the given amount of spaces.
--   The second list contains the rest of the input list.
splitWhileIndented :: Int -> [String] -> ([String], [String])
splitWhileIndented _    []     = ([], [])
splitWhileIndented intd (s:ss) =
  if length sp > intd
    then let (ssV', rest) = splitWhileIndented intd ss
         in  (text : ssV', rest)
    else ([], s:ss)
  where (sp, text) = span isSpace s

-- | Returns the indentation of a list of strings.
-- 
--   The indentation is the number of leading spaces of the first 
--   non-empty line, or 0, if no such line exists.
getIndentation :: [String] -> Int
getIndentation []        = 0
getIndentation (s:ss)
   | not (all isSpace s) = length $ takeWhile isSpace s
   | otherwise           = getIndentation ss

-- | Converts a list of comments to a list of strings.
toStrings :: [Comment] -> [String]
toStrings = map commentString . concatMap splitNestedComment

-- | Removes the border of a list of strings.
--   The border is a sequence of lines that contain only '-' characters.
removeBorder :: [String] -> [String]
removeBorder = reverse . removeEmpty . reverse . removeEmpty

-- | Removes the first string iff it contains only '-' characters.
removeEmpty :: [String] -> [String]
removeEmpty []     = []
removeEmpty (s:ss) = if all (== '-') s then ss else s:ss

-----------------------------------------------------------
-- Auxiliary functions for header handling

-- | Adds a comment to the header.
addComment :: ModuleHeader -> String -> ModuleHeader
addComment (ModuleHeader fs cs) c = ModuleHeader fs (cs ++ c)

-- | Adds a field to the header. If the field already exists,
--   it is overwritten.
addField :: ModuleHeader -> HeaderField -> String -> ModuleHeader
addField (ModuleHeader fs cs) f v 
  = ModuleHeader ((f, v) : filter ((/=f) . fst) fs) cs

-- | Returns the category of the module, if existing.
--   Otherwise, the default value is returned.
getCategoryWithDefault :: String -> [(HeaderField, String)] -> String
getCategoryWithDefault def = getFieldWithDefault' def Category

-- | Gets the value of a specific header field, or returns a default value.
getFieldWithDefault' :: String -> HeaderField -> [(HeaderField, String)] -> String
getFieldWithDefault' def f = fromMaybe def . getField f

-- | Gets the value of a specific header field.
getField :: HeaderField -> [(HeaderField, String)] -> Maybe String
getField = lookup

-- | Gets the value of a specific header field, or returns a default value.
getFieldWithDefault :: String -> HeaderField -> ModuleHeader -> String
getFieldWithDefault def f (ModuleHeader fs _) 
  = getFieldWithDefault' def f fs

-- | Orders the fields of a module header.
orderFields :: ModuleHeader -> ModuleHeader
orderFields (ModuleHeader fs cs) = ModuleHeader (sort fs) cs
types: Info
 HeaderField ModuleHeader
unsafe: Info
 safe