content: {-# LANGUAGE CPP                        #-}
  {-# LANGUAGE DeriveDataTypeable         #-}
  {-# LANGUAGE GeneralizedNewtypeDeriving #-}
  {-# LANGUAGE OverloadedStrings          #-}
  
  module Turtle.Line
    ( Line
    , lineToText
    , textToLines
    , linesToText
    , textToLine
    , unsafeTextToLine
    , NewlineForbidden(..)
    ) where
  
  import Data.Text (Text)
  import qualified Data.Text as Text
  #if __GLASGOW_HASKELL__ >= 708
  import Data.Coerce
  #endif
  import Data.List.NonEmpty (NonEmpty(..))
  import Data.String
  #if __GLASGOW_HASKELL__ >= 710
  #else
  import Data.Monoid
  #endif
  import Data.Maybe
  import Data.Typeable
  import Control.Exception
  
  import qualified Data.List.NonEmpty
  
  -- | The `NewlineForbidden` exception is thrown when you construct a `Line`
  -- using an overloaded string literal or by calling `fromString` explicitly
  -- and the supplied string contains newlines. This is a programming error to
  -- do so: if you aren't sure that the input string is newline-free, do not
  -- rely on the @`IsString` `Line`@ instance.
  --
  -- When debugging, it might be useful to look for implicit invocations of
  -- `fromString` for `Line`:
  --
  -- > >>> sh (do { line <- "Hello\nWorld"; echo line })
  -- > *** Exception: NewlineForbidden
  --
  -- In the above example, `echo` expects its argument to be a `Line`, thus
  -- @line :: `Line`@. Since we bind @line@ in `Shell`, the string literal
  -- @\"Hello\\nWorld\"@ has type @`Shell` `Line`@. The
  -- @`IsString` (`Shell` `Line`)@ instance delegates the construction of a
  -- `Line` to the @`IsString` `Line`@ instance, where the exception is thrown.
  --
  -- To fix the problem, use `textToLines`:
  --
  -- > >>> sh (do { line <- select (textToLines "Hello\nWorld"); echo line })
  -- > Hello
  -- > World
  data NewlineForbidden = NewlineForbidden
    deriving (Show, Typeable)
  
  instance Exception NewlineForbidden
  
  -- | A line of text (does not contain newlines).
  newtype Line = Line Text
    deriving (Eq, Ord, Show, Monoid)
  
  #if __GLASGOW_HASKELL__ >= 804
  instance Semigroup Line where
    (<>) = mappend
  #endif
  
  instance IsString Line where
    fromString = fromMaybe (throw NewlineForbidden) . textToLine . fromString
  
  -- | Convert a line to a text value.
  lineToText :: Line -> Text
  lineToText (Line t) = t
  
  -- | Split text into lines. The inverse of `linesToText`.
  textToLines :: Text -> NonEmpty Line
  textToLines =
  #if __GLASGOW_HASKELL__ >= 708
    Data.List.NonEmpty.fromList . coerce (Text.splitOn "\n")
  #else
    Data.List.NonEmpty.fromList . map unsafeTextToLine . Text.splitOn "\n"
  #endif
  
  -- | Merge lines into a single text value.
  linesToText :: [Line] -> Text
  linesToText =
  #if __GLASGOW_HASKELL__ >= 708
    coerce Text.unlines
  #else
    Text.unlines . map lineToText
  #endif
  
  -- | Try to convert a text value into a line.
  -- Precondition (checked): the argument does not contain newlines.
  textToLine :: Text -> Maybe Line
  textToLine = fromSingleton . textToLines
    where
      fromSingleton (a :| []) = Just a
      fromSingleton  _        = Nothing
  
  -- | Convert a text value into a line.
  -- Precondition (unchecked): the argument does not contain newlines.
  unsafeTextToLine :: Text -> Line
  unsafeTextToLine = Line
  