packages feed

screp-0.2.0.0: src/Scrappy/Grep/Output.hs

module Scrappy.Grep.Output
  ( formatResult
  , formatResults
  , formatResultsWithOpts
  , formatResultsGrouped
  , OutputFormat(..)
  , OutputOptions(..)
  , defaultOutputOptions
  , ColorMode(..)
  ) where

import Scrappy.Grep.DSL (MatchResult(..), MatchContext(..))
import Data.List (intercalate, groupBy)
import Data.Function (on)

data OutputFormat
  = FormatGrep         -- file:line:col:match
  | FormatVerbose      -- file:line:col: match (with context)
  | FormatJSON         -- JSON output
  | FormatCount        -- Just count of matches
  | FormatFilesOnly    -- -l: just filenames with matches
  | FormatFilesWithout -- -L: just filenames without matches
  deriving (Show, Eq)

data ColorMode = ColorNever | ColorAlways | ColorAuto
  deriving (Show, Eq)

data OutputOptions = OutputOptions
  { ooColor      :: ColorMode
  , ooNoFilename :: Bool      -- -h: suppress filename
  } deriving (Show, Eq)

defaultOutputOptions :: OutputOptions
defaultOutputOptions = OutputOptions
  { ooColor = ColorNever
  , ooNoFilename = False
  }

-- ANSI color codes
colorRed, colorGreen, colorCyan, colorMagenta, colorReset :: String
colorRed     = "\ESC[1;31m"
colorGreen   = "\ESC[1;32m"
colorCyan    = "\ESC[1;36m"
colorMagenta = "\ESC[1;35m"
colorReset   = "\ESC[0m"

-- | Format a single match result
formatResult :: OutputFormat -> MatchResult -> String
formatResult = formatResultWithOpts defaultOutputOptions

-- | Format a single match result with options
formatResultWithOpts :: OutputOptions -> OutputFormat -> MatchResult -> String
formatResultWithOpts opts fmt mr = case fmt of
  FormatGrep ->
    let useColor = ooColor opts == ColorAlways
        file = if ooNoFilename opts then "" else colorIf useColor colorMagenta (mrFilePath mr) ++ ":"
        lineNum = colorIf useColor colorGreen (show (mrLine mr)) ++ ":"
        colNum = show (mrCol mr) ++ ":"
        match = colorIf useColor colorRed (escapeNewlines (mrMatchText mr))
    in file ++ lineNum ++ colNum ++ match

  FormatVerbose ->
    let useColor = ooColor opts == ColorAlways
        file = if ooNoFilename opts then "" else colorIf useColor colorMagenta (mrFilePath mr) ++ ":"
        lineNum = colorIf useColor colorGreen (show (mrLine mr)) ++ ":"
        colNum = show (mrCol mr) ++ ": "
        match = colorIf useColor colorRed (mrMatchText mr)
        ctx = formatContext useColor mr
    in ctx ++ file ++ lineNum ++ colNum ++ match

  FormatJSON ->
    "{\"file\":\"" ++ escapeJSON (mrFilePath mr) ++
    "\",\"line\":" ++ show (mrLine mr) ++
    ",\"col\":" ++ show (mrCol mr) ++
    ",\"match\":\"" ++ escapeJSON (mrMatchText mr) ++ "\"}"

  FormatCount -> ""
  FormatFilesOnly -> mrFilePath mr
  FormatFilesWithout -> mrFilePath mr

-- | Conditionally apply color
colorIf :: Bool -> String -> String -> String
colorIf False _ s = s
colorIf True color s = color ++ s ++ colorReset

-- | Format context lines
formatContext :: Bool -> MatchResult -> String
formatContext useColor mr = case mrContext mr of
  Nothing -> ""
  Just ctx ->
    let beforeStr = unlines $ map (\l -> colorIf useColor colorCyan "-" ++ l) (mcBefore ctx)
    in beforeStr

-- | Format multiple results
formatResults :: OutputFormat -> [MatchResult] -> String
formatResults = formatResultsWithOpts defaultOutputOptions

-- | Format multiple results with options
formatResultsWithOpts :: OutputOptions -> OutputFormat -> [MatchResult] -> String
formatResultsWithOpts _ FormatCount results =
  show (length results) ++ " match" ++ (if length results == 1 then "" else "es") ++ "\n"
formatResultsWithOpts _ FormatJSON results =
  "[" ++ intercalate "," (map (formatResult FormatJSON) results) ++ "]\n"
formatResultsWithOpts _ FormatFilesOnly results =
  unlines $ unique $ map mrFilePath results
formatResultsWithOpts _ FormatFilesWithout _ = ""  -- Handled specially in Main
formatResultsWithOpts opts fmt results =
  unlines $ map (formatResultWithOpts opts fmt) results

-- | Format results grouped by file with context (for -A/-B/-C)
formatResultsGrouped :: OutputOptions -> OutputFormat -> [MatchResult] -> String
formatResultsGrouped opts fmt results =
  let grouped = groupBy ((==) `on` mrFilePath) results
      useColor = ooColor opts == ColorAlways
      separator = if useColor then colorCyan ++ "--" ++ colorReset else "--"
  in intercalate (separator ++ "\n") $ map (formatGroup opts fmt) grouped

formatGroup :: OutputOptions -> OutputFormat -> [MatchResult] -> String
formatGroup opts fmt mrs =
  let useColor = ooColor opts == ColorAlways
  in concatMap (formatWithContext opts useColor fmt) mrs

formatWithContext :: OutputOptions -> Bool -> OutputFormat -> MatchResult -> String
formatWithContext opts useColor fmt mr =
  let beforeCtx = case mrContext mr of
        Nothing -> ""
        Just ctx -> unlines $ zipWith (formatCtxLine opts useColor (mrFilePath mr) (mrLine mr))
                              [mrLine mr - length (mcBefore ctx)..]
                              (mcBefore ctx)
      mainLine = formatResultWithOpts opts fmt mr ++ "\n"
      afterCtx = case mrContext mr of
        Nothing -> ""
        Just ctx -> unlines $ zipWith (formatCtxLine opts useColor (mrFilePath mr) (mrLine mr))
                              [mrLine mr + 1..]
                              (mcAfter ctx)
  in beforeCtx ++ mainLine ++ afterCtx

formatCtxLine :: OutputOptions -> Bool -> FilePath -> Int -> Int -> String -> String
formatCtxLine opts useColor fp _ lineNum content =
  let file = if ooNoFilename opts then "" else colorIf useColor colorMagenta fp ++ "-"
      num = colorIf useColor colorGreen (show lineNum) ++ "-"
  in file ++ num ++ content

-- | Get unique elements preserving order
unique :: Eq a => [a] -> [a]
unique = foldr (\x acc -> if x `elem` acc then acc else x : acc) []

-- | Escape newlines for single-line output
escapeNewlines :: String -> String
escapeNewlines = concatMap escape
  where
    escape '\n' = "\\n"
    escape '\t' = "\\t"
    escape '\r' = "\\r"
    escape c = [c]

-- | Escape for JSON strings
escapeJSON :: String -> String
escapeJSON = concatMap escape
  where
    escape '"' = "\\\""
    escape '\\' = "\\\\"
    escape '\n' = "\\n"
    escape '\t' = "\\t"
    escape '\r' = "\\r"
    escape c = [c]