{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE Safe #-}
module Text.LambdaOptions.Formatter (
FormatConfig(..),
defaultFormatConfig,
format,
formatKeywords,
) where
import Control.Monad
( forM_, when, unless, void )
import qualified Control.Monad.State as State
import Control.Monad.State
( State )
import Data.Function
( on )
import Data.List
( sortBy )
import Text.LambdaOptions.Keyword
( Keyword(..) )
format :: FormatConfig -> String -> String
format config str = runFormatter config $ do
emitString str
void flushWord
formatKeywords :: FormatConfig -> [Keyword] -> String
formatKeywords config = runFormatter config . mapM_ formatKeyword
data FormatConfig
= FormatConfig
{ fmtMaxWidth :: Int
}
deriving (Show, Read, Eq, Ord)
defaultFormatConfig :: FormatConfig
defaultFormatConfig = FormatConfig
{ fmtMaxWidth = 80
}
data FormatterState
= FormatterState
{ fmtConfig :: FormatConfig
, fmtEmittedChars :: [Char]
, fmtWord :: [Char]
, fmtWidth :: Int
, fmtIndentation :: Int
}
type Formatter = State FormatterState
runFormatter :: FormatConfig -> Formatter () -> String
runFormatter config m = reverse $ fmtEmittedChars $ State.execState m FormatterState
{ fmtConfig = config
, fmtEmittedChars = []
, fmtWord = []
, fmtWidth = 0
, fmtIndentation = 0
}
formatKeyword :: Keyword -> Formatter ()
formatKeyword kwd = do
State.modify $ \st -> st
{ fmtWidth = 0
}
changeIndentation 0
newLine True
formatKeywordNames kwd
formatKeywordArgText kwd
formatKeywordText kwd
void flushWord
isShort :: String -> Bool
isShort name
| nameLen <= 1
= True
| nameLen /= 2
= False
| otherwise
= c == '-' || c == '/'
where
nameLen = length name
c = head name
formatKeywordNames :: Keyword -> Formatter ()
formatKeywordNames kwd = do
let names = sortBy cmp $ kwNames kwd
(mShortName, otherNames) = case names of
name : rest -> case isShort name of
True -> (Just name, rest)
False -> (Nothing, names)
[] -> (Nothing, [])
otherIdxs = [maybe 0 (const 1) mShortName ..] :: [Int]
case mShortName of
Nothing -> pure ()
Just shortName -> do
changeIndentation 1
emitString shortName
forM_ (zip otherIdxs otherNames) $ \(idx, name) -> do
when (idx > 0) $ do
emitChar ','
changeIndentation 5
emitString name
where
cmp n1 n2 = case (compare `on` length) n1 n2 of
LT -> LT
GT -> GT
EQ -> compare n1 n2
formatKeywordArgText :: Keyword -> Formatter ()
formatKeywordArgText kwd = case kwArgText kwd of
"" -> pure ()
argTxt -> do
void flushWord
changeIndentation . succ =<< State.gets fmtWidth
emitString argTxt
formatKeywordText :: Keyword -> Formatter()
formatKeywordText kwd = do
void flushWord
case kwText kwd of
"" -> pure ()
txt -> do
changeIndentation . succ =<< State.gets fmtWidth
changeIndentation 29
emitString txt
flushWord :: Formatter Bool
flushWord = do
st <- State.get
case fmtWord st of
[] -> pure False
word -> do
let indentation = fmtIndentation st
width = fmtWidth st
wordLen = length word
maxWidth = fmtMaxWidth $ fmtConfig st
unless (width == indentation || wordLen + width <= maxWidth) $ do
newLine False
State.modify $ \s -> s
{ fmtEmittedChars = word ++ fmtEmittedChars s
, fmtWidth = fmtWidth s + wordLen
, fmtWord = []
}
pure True
changeIndentation :: Int -> Formatter ()
changeIndentation newAmount = do
void flushWord
State.modify $ \st -> st
{ fmtIndentation = newAmount
}
indent True
indent :: Bool -> Formatter ()
indent doFlushWord = do
when doFlushWord $ do
void flushWord
st <- State.get
let indentation = fmtIndentation st
width = fmtWidth st
amount = indentation - width
case width > indentation of
True -> newLine True
False -> State.modify $ \s -> s
{ fmtEmittedChars = replicate amount ' ' ++ fmtEmittedChars s
, fmtWidth = indentation
}
newLine :: Bool -> Formatter ()
newLine doFlushWord = do
emittedChars <- State.gets fmtEmittedChars
unless (null emittedChars) $ do
State.modify $ \st -> st
{ fmtEmittedChars = '\n' : fmtEmittedChars st
}
State.modify $ \st -> st
{ fmtWidth = 0
}
indent doFlushWord
emitSpace :: Formatter ()
emitSpace = flushWord >>= \case
False -> pure ()
True -> do
st <- State.get
let width = fmtWidth st
maxWidth = fmtMaxWidth $ fmtConfig st
case width < maxWidth of
True -> State.modify $ \s -> s
{ fmtEmittedChars = ' ' : fmtEmittedChars st
, fmtWidth = width + 1
}
False -> newLine True
emitChar :: Char -> Formatter ()
emitChar = \case
' ' -> emitSpace
'\n' -> do
void flushWord
newLine False
c -> State.modify $ \st -> st
{ fmtWord = c : fmtWord st
}
emitString :: String -> Formatter ()
emitString = mapM_ emitChar