packages feed

hpack-0.39.6: src/Hpack/Render/Hints.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE RecordWildCards #-}
module Hpack.Render.Hints (
  FormattingHints (..)
, sniffFormattingHints
, RenderSettings (..)
, defaultRenderSettings
, formattingHintsRenderSettings
#ifdef TEST
, extractFieldOrder
, extractSectionsFieldOrder
, sanitize
, unindent
, sniffAlignment
, splitField
, sniffIndentation
, sniffCommaStyle
#endif
) where

import           Imports

import           Data.Char
import           Data.Maybe

import           Hpack.Render.Dsl (Alignment(..), CommaStyle(..))
import qualified Hpack.Render.Dsl as Dsl
import           Hpack.Util

data FormattingHints = FormattingHints {
  formattingHintsFieldOrder :: [String]
, formattingHintsSectionsFieldOrder :: [(String, [String])]
, formattingHintsAlignment :: Maybe Alignment
, formattingHintsIndentation :: Maybe Int
, formattingHintsCommaStyle :: Maybe CommaStyle
} deriving (Eq, Show)

sniffFormattingHints :: [String] -> FormattingHints
sniffFormattingHints (sanitize -> input) = FormattingHints {
  formattingHintsFieldOrder = extractFieldOrder input
, formattingHintsSectionsFieldOrder = extractSectionsFieldOrder input
, formattingHintsAlignment = sniffAlignment input
, formattingHintsIndentation = sniffIndentation input
, formattingHintsCommaStyle = sniffCommaStyle input
}

sanitize :: [String] -> [String]
sanitize = filter (not . isPrefixOf "cabal-version:") . filter (not . null) . map stripEnd

stripEnd :: String -> String
stripEnd = reverse . dropWhile isSpace . reverse

extractFieldOrder :: [String] -> [String]
extractFieldOrder = map fst . catMaybes . map splitField

extractSectionsFieldOrder :: [String] -> [(String, [String])]
extractSectionsFieldOrder = map (fmap extractFieldOrder) . splitSections
  where
    splitSections input = case break startsWithSpace input of
      ([], []) -> []
      (xs, ys) -> case span startsWithSpace ys of
        (fields, zs) -> case reverse xs of
          name : _ -> (name, unindent fields) : splitSections zs
          _ -> splitSections zs

    startsWithSpace :: String -> Bool
    startsWithSpace xs = case xs of
      y : _ -> isSpace y
      _ -> False

unindent :: [String] -> [String]
unindent input = map (drop indentation) input
  where
    indentation = minimum $ map (length . takeWhile isSpace) input

data Indentation = Indentation {
  indentationFieldNameLength :: Int
, indentationPadding :: Int
}

indentationTotal :: Indentation -> Int
indentationTotal (Indentation fieldName padding) = fieldName + padding

sniffAlignment :: [String] -> Maybe Alignment
sniffAlignment input = case indentations of
  [] -> Nothing
  _ | all (indentationPadding >>> (== 1)) indentations -> Just 0
  _ -> case nub (map indentationTotal indentations) of
    [n] -> Just (Alignment n)
    _ -> Nothing
  where
    indentations :: [Indentation]
    indentations = catMaybes . map (splitField >=> indentation) $ input

    indentation :: (String, String) -> Maybe Indentation
    indentation (name, value) = case span isSpace value of
      (_, "") -> Nothing
      (padding, _) -> Just Indentation {
        indentationFieldNameLength = succ $ length name
      , indentationPadding = length padding
      }

splitField :: String -> Maybe (String, String)
splitField field = case span isNameChar field of
  (xs, ':':ys) -> Just (xs, ys)
  _ -> Nothing
  where
    isNameChar = (`elem` nameChars)
    nameChars = ['a'..'z'] ++ ['A'..'Z'] ++ "-"

sniffIndentation :: [String] -> Maybe Int
sniffIndentation input = sniffFrom "library" <|> sniffFrom "executable"
  where
    sniffFrom :: String -> Maybe Int
    sniffFrom section = case findSection . removeEmptyLines $ input of
      _ : x : _ -> Just . length $ takeWhile isSpace x
      _ -> Nothing
      where
        findSection = dropWhile (not . isPrefixOf section)

    removeEmptyLines :: [String] -> [String]
    removeEmptyLines = filter $ any (not . isSpace)

sniffCommaStyle :: [String] -> Maybe CommaStyle
sniffCommaStyle input
  | any startsWithComma input = Just LeadingCommas
  | any (startsWithComma . reverse) input = Just TrailingCommas
  | otherwise = Nothing
  where
    startsWithComma = isPrefixOf "," . dropWhile isSpace

data RenderSettings = RenderSettings {
  renderSettingsIndentation :: Int
, renderSettingsFieldAlignment :: Alignment
, renderSettingsCommaStyle :: CommaStyle
} deriving (Eq, Show)

defaultRenderSettings :: RenderSettings
defaultRenderSettings = let Dsl.RenderSettings{..} = Dsl.defaultRenderSettings in RenderSettings{..}

formattingHintsRenderSettings :: FormattingHints -> RenderSettings
formattingHintsRenderSettings FormattingHints{..} = defaultRenderSettings {
  renderSettingsIndentation = indentation
, renderSettingsCommaStyle = commaStyle
} where
    indentation = max def $ fromMaybe def formattingHintsIndentation
      where def = renderSettingsIndentation defaultRenderSettings
    commaStyle = fromMaybe (renderSettingsCommaStyle defaultRenderSettings) formattingHintsCommaStyle