packages feed

dataframe-core-2.0.0.0: src-internal/DataFrame/Internal/Pretty.hs

{- | A minimal Wadler/Leijen-style document combinator and width-aware renderer.
A 'Doc' describes a layout abstractly; 'render' chooses where soft breaks become
newlines to fit a target width. 'Group' lays a region flat when it fits.
-}
module DataFrame.Internal.Pretty (
    Doc,
    text,
    line,
    hardline,
    nest,
    group,
    (<+>),
    hcat,
    punctuate,
    parens,
    parensWhenBroken,
    defaultWidth,
    render,
) where

data Doc
    = Empty
    | Text String
    | Line
    | Cat Doc Doc
    | Nest Int Doc
    | Group Doc
    | Hard
    | Alt Doc Doc

instance Semigroup Doc where
    (<>) = Cat

instance Monoid Doc where
    mempty = Empty

-- | A literal chunk of text. Must not contain newlines (use 'line'/'hardline').
text :: String -> Doc
text = Text

{- | A soft break: a single space when its enclosing 'group' fits the width,
otherwise a newline + current indentation.
-}
line :: Doc
line = Line

-- | A hard break that never flattens; any enclosing 'group' is forced to break.
hardline :: Doc
hardline = Hard

-- | Add @k@ spaces to the indentation applied at line breaks inside @d@.
nest :: Int -> Doc -> Doc
nest = Nest

-- | Lay the document out flat if it fits the remaining width, broken otherwise.
group :: Doc -> Doc
group = Group

-- | Concatenate two documents separated by a single space.
(<+>) :: Doc -> Doc -> Doc
x <+> y = x <> Text " " <> y

infixr 6 <+>

hcat :: [Doc] -> Doc
hcat = mconcat

-- | Append @sep@ after every element but the last.
punctuate :: Doc -> [Doc] -> [Doc]
punctuate _ [] = []
punctuate _ [d] = [d]
punctuate sep (d : ds) = (d <> sep) : punctuate sep ds

parens :: Doc -> Doc
parens d = Text "(" <> d <> Text ")"

{- | Render @d@ bare when it fits flat on the current line, wrapped in parens when
it must break across lines. Keeps operator grouping unambiguous once a
sub-expression wraps, without parenthesis noise on one-line expressions.
-}
parensWhenBroken :: Doc -> Doc
parensWhenBroken d = Group (Alt d (parens d))

defaultWidth :: Int
defaultWidth = 80

data Mode = Flat | Break

-- | Render a document, breaking soft lines so output fits @width@ columns.
render :: Int -> Doc -> String
render width doc = layout 0 [(0, Break, doc)]
  where
    layout :: Int -> [(Int, Mode, Doc)] -> String
    layout _ [] = ""
    layout col ((i, m, d) : rest) = case d of
        Empty -> layout col rest
        Text s -> s ++ layout (col + length s) rest
        Cat x y -> layout col ((i, m, x) : (i, m, y) : rest)
        Nest j x -> layout col ((i + j, m, x) : rest)
        Line -> case m of
            Flat -> ' ' : layout (col + 1) rest
            Break -> '\n' : replicate i ' ' ++ layout i rest
        Hard -> '\n' : replicate i ' ' ++ layout i rest
        Group x ->
            if fits (width - col) ((i, Flat, x) : rest)
                then layout col ((i, Flat, x) : rest)
                else layout col ((i, Break, x) : rest)
        Alt flat broken -> case m of
            Flat -> layout col ((i, Flat, flat) : rest)
            Break -> layout col ((i, Break, broken) : rest)

    fits :: Int -> [(Int, Mode, Doc)] -> Bool
    fits w _ | w < 0 = False
    fits _ [] = True
    fits w ((i, m, d) : rest) = case d of
        Empty -> fits w rest
        Text s -> fits (w - length s) rest
        Cat x y -> fits w ((i, m, x) : (i, m, y) : rest)
        Nest j x -> fits w ((i + j, m, x) : rest)
        Line -> case m of
            Flat -> fits (w - 1) rest
            Break -> True
        Hard -> case m of
            Flat -> False
            Break -> True
        Group x -> fits w ((i, Flat, x) : rest)
        Alt flat broken -> case m of
            Flat -> fits w ((i, Flat, flat) : rest)
            Break -> fits w ((i, Break, broken) : rest)