module Language.Docker.Normalize ( normalizeEscapedLines ) where import Data.List (dropWhileEnd, mapAccumL) import Data.Maybe (catMaybes) data NormalizedLine = Continue | Joined !String !Int trimLines :: [String] -> [String] trimLines = map strip where strip = lstrip . rstrip lstrip = dropWhile (`elem` " \t\r") rstrip = reverse . lstrip . reverse -- Finds all lines ending with \ and joins them with the next line using -- a single space. If the next line is a comment, then the comment line is -- deleted. It finally adds the same amount of new lines for each of the -- lines it joined, in order to preserve the line count in the document. normalize :: [String] -> [String] normalize allLines = let (lastState, res) -- mapAccumL is the idea of a for loop with a variable holding -- some state and another variable where we append the final result -- of the looping operation. For each line in lines, apply the transform -- function. This function always returns a new state, and another element -- to append to the final result. The ending result of mapAccumL is the final -- state variale and the resulting list of values. We initialize the loop with -- the 'Continue' state, which means "no special action to do next" = mapAccumL transform Continue allLines in case lastState of Continue -- The last line of the document is a normal line, cleanup and return -> catMaybes res Joined l times -- The last line contains a \, so we need to add the buffered -- line back to the result, pad with newlines and cleanup -> catMaybes res ++ [l ++ padNewlines times] where normalizeLast = dropWhileEnd (== '\\') -- | Checks the result of the previous operation in the loop (first argument) -- -- If the previous result is a 'Joined' operation, then we merge the previous -- and the current line in a single line and return it. -- -- If the current line ends with a \, then we produce a 'Joined' state as result -- of this looping operation. -- -- If the previous 2 conditions are true at the same time, then we produce a new -- 'Joined' state holding the concatenation of the Joined buffer and the previous line -- and we return 'Nothing' as an indication that this line does not form part of the -- final result. transform :: NormalizedLine -> String -> (NormalizedLine, Maybe String) -- If we are buffering lines, and the next line is a comment, -- we simply ignore the comment and remember to add a newline transform (Joined prev times) ('#':_) = (Joined prev (times + 1), Nothing) -- We do the same if we are buffering lines and the next one is empty transform (Joined prev times) "" = (Joined prev (times + 1), Nothing) -- If we are buffering lines, then we check whether the current line end with \, -- if it does, then we merged it into the buffered state, otherwise we just yield -- the concatanation of the buffer and the current line as result, after padding with -- newlines transform (Joined prev times) l = if endsWithEscape l then (Joined (prev ++ ' ' : normalizeLast l) (times + 1), Nothing) else (Continue, Just (prev ++ ' ' : l ++ padNewlines times)) -- When not buffering lines, then we just check if we need to start doing it by checking -- whether or not the current line ends with \. If it does not, then we just yield the -- current line as part of the result transform Continue l = if endsWithEscape l then (Joined (normalizeLast l) 1, Nothing) else (Continue, Just l) -- endsWithEscape "" = False endsWithEscape s = last s == '\\' -- padNewlines times = replicate times '\n' -- | Remove new line escapes and join escaped lines together on one line -- to simplify parsing later on. Escapes are replaced with line breaks -- to not alter the line numbers. normalizeEscapedLines :: String -> String normalizeEscapedLines = unlines . normalize . trimLines . lines