bricks-0.0.0.1: src/Bricks/IndentedString.hs
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
module Bricks.IndentedString
(
-- * Indented string
InStr (..)
, inStr'join
, inStr'level
, inStr'dedent
, inStr'trim
, inStr'toList
-- * Single line of an indented string
, InStr'1 (..)
, inStr'1'nonEmpty
, inStr'1'empty
, inStr'1'modifyLevel
) where
-- Bricks
import Bricks.Expression
-- Bricks internal
import Bricks.Internal.Prelude
import Bricks.Internal.Seq (Seq, (<|))
import qualified Bricks.Internal.Seq as Seq
import qualified Bricks.Internal.Text as Text
{- | An "indented string literal," delimited by two single-quotes @''@.
This type of literal is called "indented" because the parser automatically
removes leading whitespace from the string ('inStr'dedent'), which makes it
convenient to use these literals for multi-line strings within an indented
expression without the whitespace from indentation ending up as part of the
string. -}
newtype InStr = InStr { inStr'toSeq :: Seq InStr'1 }
deriving (Monoid, Semigroup)
instance Show InStr
where
show = show . inStr'toList
inStr'toList :: InStr -> [InStr'1]
inStr'toList =
Seq.toList . inStr'toSeq
-- | One line of an 'InStr'.
data InStr'1 =
InStr'1
{ inStr'1'level :: Natural
-- ^ The number of leading space characters. We store this separately
-- for easier implementation of 'inStr'dedent'.
, inStr'1'str :: Str'Dynamic
-- ^ The rest of the line after any leading spaces.
}
instance Show InStr'1
where
show (InStr'1 n s) = "indent-" <> show n <> " " <> show s
{- | Join 'InStr's with newlines interspersed. -}
inStr'join :: InStr -> Str'Dynamic
inStr'join xs =
Str'Dynamic . Seq.concat $
Seq.intersperse
(Seq.singleton (Str'1'Literal "\n"))
(f <$> inStr'toSeq xs)
where
f :: InStr'1 -> Seq Str'1
f (InStr'1 n parts) = Str'1'Literal (Text.replicate (fromIntegral n) " ")
<| strDynamic'toSeq parts
{- | Determines whether an 'InStr'1' contains any non-space
characters. The opposite of 'inStr'1'nonEmpty'.
This is used to determine whether this line should be considered when
calculating the number of space characters to strip in 'inStr'dedent'. -}
inStr'1'nonEmpty :: InStr'1 -> Bool
inStr'1'nonEmpty =
not . inStr'1'empty
-- | The opposite of 'inStr'1'nonEmpty'.
inStr'1'empty :: InStr'1 -> Bool
inStr'1'empty (InStr'1{ inStr'1'str = Str'Dynamic x }) =
Seq.null x
{- | Determine how many characters of whitespace to strip from an indented
string. -}
inStr'level :: InStr -> Natural
inStr'level =
maybe 0 id
. Seq.minimum
. Seq.map inStr'1'level
. Seq.filter inStr'1'nonEmpty
. inStr'toSeq
-- | Modify an 'InStr' by applying a function to its number of leading spaces.
inStr'1'modifyLevel :: (Natural -> Natural) -> (InStr'1 -> InStr'1)
inStr'1'modifyLevel f x@InStr'1{inStr'1'level = a} =
x{ inStr'1'level = f a }
{- | Determine the minimum indentation of any nonempty line, and remove that
many space characters from the front of every line. -}
inStr'dedent :: InStr -> InStr
inStr'dedent xs =
let
b = inStr'level xs
f a = if a >= b then a - b else 0
in
InStr $ inStr'1'modifyLevel f <$> inStr'toSeq xs
-- | Remove any empty lines from the beginning or end of an indented string.
inStr'trim :: InStr -> InStr
inStr'trim =
InStr . trimWhile inStr'1'empty . inStr'toSeq
where
trimWhile f = Seq.dropWhileL f . Seq.dropWhileR f