packages feed

toodles-0.1.1: app/Parse.hs

{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Parse where

import           Types

import           Data.List                  (find)
import           Data.Maybe                 (fromJust, fromMaybe, isNothing)
import           Data.Text                  (Text)
import qualified Data.Text                  as T
import           Data.Void                  (Void)
import           Text.Megaparsec
import           Text.Megaparsec.Char
import qualified Text.Megaparsec.Char.Lexer as L
import           Text.Read

type Parser = Parsec Void Text

symbol :: Text -> Parser Text
symbol = L.symbol space

parseComment :: Text -> Parser TodoEntry
parseComment extension = do
    _ <- manyTill anyChar (symbol $ getCommentForFileType extension)
    b <- many anyChar
    return . TodoBodyLine $ T.pack b

getCommentForFileType :: Text -> Text
getCommentForFileType extension =
    fromMaybe unkownMarker $ lookup adjustedExtension fileTypeToComment
    where
    adjustedExtension =
        if T.isPrefixOf "." extension
            then extension
            else "." <> extension

integer :: Parser Integer
integer = lexeme $ L.signed space L.decimal

    where
    lexeme :: Parser a -> Parser a
    lexeme = L.lexeme space

parseAssignee :: Parser String
parseAssignee = many (noneOf [')', '|', '='])

-- TODO(avi|p=3|#cleanup) - fix and type this better
parseDetails :: Text -> (Maybe Text, Maybe Text, [(Text, Text)], [Text])
parseDetails toParse =
  let dataTokens = T.splitOn "|" toParse
      assigneeTo =
        find
          (\t ->
             not (T.null t) &&
             not (T.isInfixOf "=" t) && not (T.isPrefixOf "#" t))
          dataTokens
      allDetails =
        map (\[a, b] -> (a, b)) $ filter (\t -> length t == 2) $
        map (T.splitOn "=") dataTokens
      priorityVal = snd <$> find (\t -> T.strip (fst t) == "p") allDetails
      filteredDetails = filter (\t -> T.strip (fst t) /= "p") allDetails
      entryTags = filter (T.isPrefixOf "#") dataTokens
  in (assigneeTo, priorityVal, filteredDetails, entryTags)

-- | parse "hard-coded" flags, and user-defined flags if any
parseFlag :: [UserFlag] -> Parser Flag
parseFlag us = foldr (\a b -> b <|> foo a) (try parseFlagHardcoded) us
  where
    foo :: UserFlag -> Parser Flag
    foo (UserFlag x) = try (symbol x *> pure (UF $ UserFlag x))

-- | parse flags TODO, FIXME, XXX
parseFlagHardcoded :: Parser Flag
parseFlagHardcoded =
      try (symbol "TODO"  *> pure TODO )
  <|> try (symbol "FIXME" *> pure FIXME)
  <|>     (symbol "XXX"   *> pure XXX  )


fileTypeToComment :: [(Text, Text)]
fileTypeToComment =
  [ (".c", "//")
  , (".cc", "//")
  , (".clj", ";;")
  , (".cpp", "//")
  , (".cxx", "//")
  , (".c++", "//")
  , (".cs", "//")
  , (".ex", "#")
  , (".erl", "%")
  , (".go", "//")
  , (".h", "//")
  , (".hh", "//")
  , (".hpp", "//")
  , (".hs", "--")
  , (".hxx", "//")
  , (".h++", "//")
  , (".java", "//")
  , (".js", "//")
  , (".kt", "//")
  , (".kts", "//")
  , (".lua", "--")
  , (".m", "//")
  , (".php", "//")
  , (".proto", "//")
  , (".py", "#")
  , (".rb", "#")
  , (".rs", "//")
  , (".scala", "//")
  , (".sh", "#")
  , (".swift", "///")
  , (".ts", "//")
  , (".tsx", "//")
  , (".txt", "")
  , (".vue", "//")
  , (".yaml", "#")
  ]

runTodoParser :: [UserFlag] -> SourceFile -> [TodoEntry]
runTodoParser us (SourceFile path ls) =
  let parsedTodoLines =
        map
          (\(lineNum, lineText) -> parseMaybe (parseTodo us path lineNum) lineText)
          (zip [1 ..] ls)
      groupedTodos = foldl foldTodoHelper ([], False) parsedTodoLines
  in fst groupedTodos

  where
    -- fold fn to concatenate todos that a multiple, single line comments
    foldTodoHelper :: ([TodoEntry], Bool) -> Maybe TodoEntry -> ([TodoEntry], Bool)
    foldTodoHelper (todoEntries, currentlyBuildingTodoLines) maybeTodo
        -- We're not on a todo line, keep going
        | isNothing maybeTodo = (todoEntries, False)
        -- We see the start of a new todo
        | isEntryHead $ fromJust maybeTodo = (todoEntries ++ [fromJust maybeTodo], True)
        -- We a body line of a todo to concatenate to the current one
        | isBodyLine (fromJust maybeTodo) && currentlyBuildingTodoLines =
            (init todoEntries ++ [combineTodo (last todoEntries) (fromJust maybeTodo)], True)
        | otherwise = (todoEntries, False)

          where
            isEntryHead :: TodoEntry -> Bool
            isEntryHead TodoEntryHead {} = True
            isEntryHead _                = False

            isBodyLine :: TodoEntry -> Bool
            isBodyLine (TodoBodyLine _) = True
            isBodyLine _                = False

            combineTodo :: TodoEntry -> TodoEntry -> TodoEntry
            combineTodo (TodoEntryHead i b a p n entryPriority f attrs entryTags entryLeadingText) (TodoBodyLine l) =
                TodoEntryHead i (b ++ [l]) a p n entryPriority f attrs entryTags entryLeadingText
            combineTodo _ _ = error "Can't combine todoEntry of these types"

getExtension :: FilePath -> Text
getExtension path = last $ T.splitOn "." (T.pack path)

stringToMaybe :: Text -> Maybe Text
stringToMaybe t =
  if T.null t
    then Nothing
    else Just t

fst4 :: (a, b, c, d) -> a
fst4 (x, _, _, _) = x

snd4 :: (a, b, c, d) -> b
snd4 (_, x, _, _) = x

thd4 :: (a, b, c, d) -> c
thd4 (_, _, x, _) = x

fth4 :: (a, b, c, d) -> d
fth4 (_, _, _, x) = x

unkownMarker :: Text
unkownMarker = "UNKNOWN-DELIMETER-UNKNOWN-DELIMETER-UNKNOWN-DELIMETER"

parseTodo :: [UserFlag] -> FilePath -> LineNumber -> Parser TodoEntry
parseTodo us path lineNum = try (parseTodoEntryHead us)
                            <|> parseComment (getExtension path)
  where
    parseTodoEntryHead :: [UserFlag] -> Parser TodoEntry
    parseTodoEntryHead uf = do
        entryLeadingText <- manyTill anyChar (prefixParserForFileType $ getExtension path)
        f <- parseFlag uf
        entryDetails <- optional $ try (inParens $ many (noneOf [')', '(']))
        let parsedDetails = parseDetails . T.pack <$> entryDetails
            entryPriority = (readMaybe . T.unpack) =<< (snd4 =<< parsedDetails)
            otherDetails = maybe [] thd4 parsedDetails
            entryTags = maybe [] fth4 parsedDetails
        _ <- optional $ symbol "-"
        _ <- optional $ symbol ":"
        b <- many anyChar
        return $
            TodoEntryHead
            0
            [T.pack b]
            (stringToMaybe . T.strip $ fromMaybe "" (fst4 =<< parsedDetails))
            path
            lineNum
            entryPriority
            f
            otherDetails
            entryTags
            (T.pack entryLeadingText)

        where
        inParens :: Parser a -> Parser a
        inParens = between (symbol "(") (symbol ")")

        prefixParserForFileType :: Text -> Parser Text
        prefixParserForFileType "org" = try (symbol "****")
                                    <|> try (symbol "***")
                                    <|> try (symbol "**")
                                    <|> try (symbol "*")
                                    <|> symbol "-"
        prefixParserForFileType extension = symbol . getCommentForFileType $ extension