packages feed

lentil-1.0.9.0: src/Lentil/Parse/Source.hs

-----------------------------------------------------------------------------
-- |
-- Module      :  Lentil.Parse.Source
-- Copyright   :  © 2015 Francesco Ariis
-- License     :  GPLv3 (see the LICENSE file)
--
-- Comments from source files
-----------------------------------------------------------------------------


module Lentil.Parse.Source where

import Lentil.Types
import Lentil.Helpers

import Text.Parsec hiding (Line)

import qualified Data.Char  as C
import qualified Data.Maybe as M
import qualified Data.List as L

import Control.Applicative hiding (many, (<|>))

import Prelude -- 7.8 hack

-----------
-- TYPES --
-----------

-- from source to comment strings
type ParSource a = Parsec String () a

data ParSyntax = -- 'standard' language (with single line and/or multiline
                 -- markers, etc.
                 StdSyntax { psLineComms   :: [String],
                             psBlockComms  :: [(String, String)],
                             psStringStyle :: EscapeStyle,
                             psStringLits  :: [Char],
                             psCharStyle   :: CharStyle,
                             psCharLits    :: [Char] }
               | RstSyntax -- reStructuredText/sphinx parsing
                 deriving (Show)

data EscapeStyle = ClangLike | SQLLike
                 deriving (Show, Eq)

data CharStyle = CommonChr | ErlangChr
               deriving (Show, Eq)

--------------
-- COMMENTS --
--------------

data CommentString = SingleLine Row String  -- sans newline
                   | MultiLine Row String
                   deriving (Show, Eq)

-- parses stuff like "-- comment"
lineComment :: String -> ParSource CommentString
lineComment s = SingleLine                 <$>
                (initId s *> getRow)       <*>
                manyTill anyChar newline
                <?> "line comment"
    where
          -- for 1-char line comments (#, ;, etc.), erases repeating
          initId :: String -> ParSource ()
          initId [a] = () <$ many1 (char a)
          initId as  = () <$ string as

blockComment :: (String, String) -> ParSource CommentString
blockComment (i, e) = MultiLine                         <$>
                      (string i *> getRow)              <*>
                      manyTill anyChar (try $ string e)
                      <?> "block comment"

getRow :: ParSource Row
getRow = fmap sourceLine getPosition


-------------
-- STRINGS --
-------------

litString :: EscapeStyle -> Char -> ParSource String
litString ClangLike ic = cString ic
litString SQLLike   ic = sqlString ic

-- string ancillaries --

-- quoted strings, escaped by \, by mauke^
cString :: Char -> ParSource String
cString ic = q *> many ((char '\\' *> anyChar) <|> noneOf no) <* q
             <?> "codestring"
    where q  = char ic
          no = C.showLitChar ic "\\"

-- sqllike string (no escape with \, '' to escape ')
sqlString :: Char -> ParSource String
sqlString ic = char ic *> manyTill anyChar (char ic)
    -- we treat 'cdscsad  cdscdsa '' csdcs' as two strings
    -- because the content is meaningless to our program


-----------
-- CHARS --
-----------

litChar :: CharStyle -> Char -> ParSource Char
litChar CommonChr ic = commonChar ic
litChar ErlangChr ic = erlangChar ic

-- quoted single character
commonChar :: Char -> ParSource Char
commonChar ic = q *> ((char '\\' *> anyChar) <|> anyChar) <* q
             <?> "char string sign"
    where q = char ic

-- $a for 'a' (where ic = '$')
erlangChar :: Char -> ParSource Char
erlangChar ic = char ic *> anyChar


------------------
-- OTHER BLOCKS --
------------------

-- a program is instructions to the computer. Ends when you meet
-- a well formed element from above (linecomm, blockcom, stringlit,
-- charLit)
program :: ParSyntax -> ParSource String
program ps = manyTill1 anyChar (endp <|> ("" <$ eof))
             <?> "program"
    where
          -- endp :: [ParSource String]
          endp  = lookAhead . choice . map try $ map string posts ++ [lchars]

          -- every "post" symbol (init comments, listring char)
          posts  = psLineComms ps              ++ -- line
                   map fst (psBlockComms ps)   ++ -- block
                   map (:[]) (psStringLits ps)    -- lit string

          -- ambiguous symbols (pall'aaa'foo is valid haskell identifier,
          -- so it should stay here
          lchars = "" <$ choice (map (litChar (psCharStyle ps))
                                     (psCharLits ps))


-----------------------
-- RST+SPHINX SYNTAX --
-----------------------

rstDocumentPart :: ParSource (Maybe CommentString)
rstDocumentPart = choice . map try $ [Just <$> rstTodo, Nothing <$ rstOther]

-- `.. todo::` directive
rstTodo :: ParSource CommentString
rstTodo = MultiLine <$> getRow <*>
            (rstTodoIncipit *> fmap modText todoBody)
          <?> "rst todo directive"
  where
        startPara :: ParSource ()
        startPara = newline *> (() <$ satisfy (not . C.isSpace) <|> eof)

        todoBody :: ParSource String
        todoBody = manyTill1 anyChar (lookAhead . try $ startPara <|> eof)

        -- modText does 2 things
        --   1. injects a dummy todo keyword, having discarded `.. todo::`
        --   2. replaces '\n' with "\n ", so that issue is not broken
        --      by newline
        modText :: String -> String
        modText cs = "TODO " ++ addSpace cs
            -- todo modText is extremely ducttape [duct]

        addSpace :: String -> String
        addSpace [] = []
        addSpace ('\n':cs) = '\n' : ' ' : addSpace cs
        addSpace (c:cs) = c : addSpace cs

-- everything else
rstOther :: ParSource String
rstOther = manyTill1 anyChar (endp <|> eof)
           <?> "rst other text"
    where
          endp = lookAhead . try $ rstTodoIncipit

rstTodoIncipit :: ParSource ()
rstTodoIncipit = fmap sourceColumn getPosition >>= \c ->
                 -- todo probabilmente getPosition non dovrebbe essere usato
                 --      che solo per gli errori [duct]
                 if c == 1
                   then () <$ string ".. todo::"
                   else parserFail "rst todo incipit: not at begin-of-line"


------------
-- SOURCE --
------------

-- given a set of lineparsers / blockparsers
sourcePart :: ParSyntax -> ParSource (Maybe CommentString)
sourcePart RstSyntax                        = rstDocumentPart
sourcePart ps@(StdSyntax lc bc es sl cs cl) =
            choice [plc, pbc, psl, pcl, ppr]
            <?> "source file part"
    where
          ct = choice . map try

          plc = Just <$> (ct . map lineComment  $ lc)  -- line comm
          pbc = Just <$> (ct . map blockComment $ bc)  -- block comm
          psl = Nothing <$ (choice . map (try . litString es) $ sl)
                                                           -- str lit
          pcl = Nothing <$ (ct . map (litChar cs) $ cl)    -- char lit
          ppr = Nothing <$ program ps                      -- program

-- ps: syntax to sever comment from the rest
source :: ParSyntax -> ParSource [CommentString]
source ps = fmap M.catMaybes (many1 (sourcePart ps)) <|> ([] <$ eof)
         <?> "source file"


-------------
-- CONVERT --
-------------

type Comment = (Row, String)


comms2Tuple :: [CommentString] -> [Comment]
comms2Tuple [] = []
comms2Tuple (c:cs) | isSl c    = let (a,z) = span isSl (c:cs)
                                 in groupLineComms a ++ comms2Tuple z
                   | otherwise = comment2Tuple c : comms2Tuple cs
    where
          isSl (SingleLine _ _) = True
          isSl _                = False

-- use it on Line comments **only**!
groupLineComms :: [CommentString] -> [Comment]
groupLineComms cs = let cs'         = map comment2Tuple cs
                        f (r, s) i  = (r, s, r-i)

                        zipped      = zipWith f cs' (enumFrom 1)
                        grouped     = L.groupBy threeEq zipped

                    in map (flatComm . map back) grouped

    where
          threeEq (_, _, x) (_, _, y) = x == y

          back (a, b, _) = (a, b)
          flatComm cs'   = (fst . head $ cs',
                            unlines . map snd $ cs')

comment2Tuple :: CommentString -> Comment
comment2Tuple (SingleLine r t) = (r, t)
comment2Tuple (MultiLine  r t) = (r, t)