{-# LANGUAGE OverloadedStrings #-}
module Main where
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import System.Environment (getArgs)
import Control.Applicative
import Data.List (transpose)
import Data.Monoid
import Options.Applicative
data Options = Options {
matchMode :: MatchMode
, matchStrings :: [Text]
} deriving (Show)
data MatchMode = Series | Alternatives deriving Show
parseOpts :: Parser Options
parseOpts = Options
<$> flag Series Alternatives
(short 'a' <> long "alternatives"
<> help "Treat match strings as alternatives for alignment, like regex /(a|b|c)/.")
<*> ((T.words . T.pack) <$>
(argument str
(metavar "MATCH STRINGS"
<> help "The strings to align on, between a pair of single quotes")))
opts = info (helper <*> parseOpts)
(fullDesc <> progDesc "Align code text from STDIN on operators."
<> header "align"
<> footer "See https://github.com/danchoi/align for more info.")
main = do
Options mode alignStrings <- execParser opts
input <- (T.lines . T.pack) <$> getContents
let result :: [Text]
result =
case mode of
Series -> foldl (\lines sep -> align lines sep) input alignStrings
Alternatives -> alignOnAlteratives alignStrings input
T.putStr . T.unlines $ result
-- Aligning in standard Series mode
align :: [Text] -> Text -> [Text]
align lines alignString =
let lines' :: [[Text]]
lines' = map (splitOn alignString) lines
firstCol:rest = transpose lines'
firstCol' = adjustWidth alignString firstCol
lines'' = transpose (firstCol':rest)
in map T.concat lines''
-- split a line into two segments if it contains the alignstring,
-- and one if it doesn't
splitOn :: Text -> Text -> [Text]
splitOn alignString input =
let (x,y) = T.breakOn alignString input
in [z | z <- [x, trim alignString y], z /= mempty]
-- | Makes column cells in a column the same width
-- Used for the standard Series mode.
adjustWidth :: Text -> [Text] -> [Text]
adjustWidth alignStr xs =
map maybeAdjust xs
where maxWidth = maximum $ map (T.length . T.stripEnd) $ xs
maybeAdjust :: Text -> Text
maybeAdjust cell = T.justifyLeft maxWidth ' ' . T.stripEnd $ cell
alignOnAlteratives :: [Text] -> [Text] -> [Text]
alignOnAlteratives alts lines =
-- each row cell contains (Maybe alternative) that was split on
let lines' :: [[Text]]
lines' = map (splitOnAny alts) lines
firstCol:rest = transpose lines'
firstCol' = adjustWidth' firstCol
lines'' = transpose (firstCol':rest)
in map T.concat lines''
-- | adjust width of a column of cells
adjustWidth' :: [Text] -> [Text]
adjustWidth' xs =
map adj xs
where maxWidth = maximum $ map (T.length . T.stripEnd) xs
adj cell = T.justifyLeft maxWidth ' ' . T.stripEnd $ cell
-- | Attempts to split line on any of the alternatives
-- and returns the segments
splitOnAny :: [Text] -> Text -> [Text]
splitOnAny alts line =
let alts' = filter (`T.isInfixOf` line) alts
in if null alts'
then [line]
else let alt = head alts'
in splitOn alt line
-- | strips whitespace around text but makes sure that it's left-padded with one space
trim :: Text -> Text -> Text
trim alignString s =
-- alignString will be on 1st elem of tuple:
let (x,y) = T.breakOnEnd alignString s
in T.cons ' '
$ mconcat [T.strip x, " ", T.strip y]