packages feed

exon-1.7.3.0: lib/Exon/Quote.hs

{-# options_haddock prune #-}

-- | Description: Internal
module Exon.Quote where

import qualified Language.Haskell.TH as TH
import Language.Haskell.TH (Exp (AppE, InfixE, ListE), Q, extsEnabled, runQ)
import Language.Haskell.TH.Quote (QuasiQuoter (QuasiQuoter))
import Language.Haskell.TH.Syntax (Quasi)

import Exon.Class.Exon (exonProcess, exonProcessWith)
import Exon.Class.ToSegment (toSegment)
import Exon.Data.RawSegment (RawSegment (AutoExpSegment, ExpSegment, StringSegment, WsSegment))
import qualified Exon.Data.Segment as Segment
import Exon.Haskell.Parse (parseExpWithExts)
import Exon.Parse (parse, parseWs)

exonError ::
  ToString e =>
  MonadFail m =>
  e ->
  m a
exonError err =
  fail ("Exon: " <> toString err)

segmentsQ ::
  QOrIO m =>
  Bool ->
  String ->
  m (NonEmpty RawSegment)
segmentsQ _ "" =
  exonError ("empty quasiquote" :: String)
segmentsQ whitespace s =
  (if whitespace then parseWs else parse) s & fmap nonEmpty & \case
    Right (Just segs) -> pure segs
    Right Nothing -> pure (pure (StringSegment ""))
    Left err -> exonError err

class Quasi m => QOrIO (m :: Type -> Type) where
  fileExtensions :: m [TH.Extension]

instance QOrIO IO where
  fileExtensions =
    pure []

instance QOrIO Q where
  fileExtensions =
    extsEnabled

reifyExp ::
  QOrIO m =>
  String ->
  m Exp
reifyExp "" = exonError ("Empty interpolation" :: String)
reifyExp s = do
  exts <- fileExtensions
  case parseExpWithExts exts s of
    Left (_, _, err) -> exonError err
    Right e -> pure e

reifySegments ::
  QOrIO m =>
  Bool ->
  NonEmpty RawSegment ->
  m (NonEmpty Exp)
reifySegments unsafe segs = do
  expCon <- runQ [e|Segment.Expression|]
  expToSegment <- runQ [e|toSegment|]
  for segs \case
    StringSegment s ->
      runQ [e|Segment.String s|]
    ExpSegment s | unsafe -> do
      e <- reifyExp s
      pure (AppE expCon (AppE expToSegment e))
    ExpSegment s -> do
      e <- reifyExp s
      pure (AppE expCon e)
    AutoExpSegment s -> do
      e <- reifyExp s
      pure (AppE expCon (AppE expToSegment e))
    WsSegment s ->
     runQ [e|Segment.Whitespace s|]

quoteExpWith ::
  QOrIO m =>
  Maybe (Q TH.Exp, Q TH.Exp) ->
  Bool ->
  Bool ->
  String ->
  m Exp
quoteExpWith wrapper whitespace unsafe code = do
  raw <- segmentsQ whitespace code
  hseg :| segs <- reifySegments unsafe raw
  conc <- runQ $ maybe [e|exonProcess|] wrapped wrapper
  consE <- runQ [e|(:|)|]
  pure (AppE conc (InfixE (Just hseg) consE (Just (ListE segs))))
  where
    wrapped (wrap, unwrap) = do
      [e|exonProcessWith ($wrap) $(unwrap)|]

quoteExp ::
  QOrIO m =>
  Bool ->
  Bool ->
  String ->
  m Exp
quoteExp =
  quoteExpWith Nothing

quoteSegments ::
  QOrIO m =>
  String ->
  m Exp
quoteSegments code = do
  raw <- segmentsQ True code
  hseg :| segs <- reifySegments False raw
  consE <- runQ [e|(:|)|]
  pure (InfixE (Just hseg) consE (Just (ListE segs)))

-- | Constructor for a quasiquoter that wraps all segments with the first expression and unwraps the result with the
-- second.
--
-- This can be used to define quoters with custom logic by providing instances of any of the classes in
-- "Exon.Class.Exon" with the result type argument set to the wrapper type:
--
-- >>> import Exon.Class.Exon (ExonString (..))
-- >>> import Exon.Data.Segment (Segment(String))
-- >>> import qualified Data.Text.Lazy.Builder as Text
-- >>> newtype Nl = Nl Text deriving (Generic)
-- >>> getNl (Nl t) = t
-- >>> instance ExonString Nl Text.Builder where exonWhitespace _ = exonString @Nl "\n"
-- >>> exonnl = exonWith (Just ([e|Nl|], [e|getNl|])) True False
-- >>> [exonnl|one   two     three|]
-- "one\ntwo\nthree"
--
-- @since 0.2.0.0
exonWith ::
  Maybe (Q TH.Exp, Q TH.Exp) ->
  Bool ->
  Bool ->
  QuasiQuoter
exonWith wrapper whitespace unsafe =
  QuasiQuoter (quoteExpWith wrapper whitespace unsafe) (err "pattern") (err "type") (err "decl")
  where
    err :: String -> String -> Q a
    err tpe _ =
      exonError ("Cannot quote " <> tpe)

-- | A quasiquoter that allows interpolation, concatenating the resulting segments with '(<>)' or a an arbitrary
-- user-defined implementation.
-- See the [introduction]("Exon") for details.
--
-- >>> [exon|write #{show (5 :: Int)} lines of ##{"code" :: ByteString}|] :: Text
-- "write 5 lines of code"
exon :: QuasiQuoter
exon =
  exonWith Nothing False False

-- | Unsafe version of 'exon', allowing automatic conversion with the same splice brackets as matching types.
--
-- @since 1.0.0.0
exun :: QuasiQuoter
exun =
  exonWith Nothing False True

-- | A variant of 'exon' that creates segments for each sequence of whitespace characters that can be processed
-- differently by 'Exon.ExonAppend', 'Exon.ExonSegment' or 'Exon.ExonString'.
--
-- @since 1.0.0.0
exonws :: QuasiQuoter
exonws =
  exonWith Nothing True False

-- | Internal debugging quoter that produces the raw segments.
--
-- @since 1.0.0.0
exonSegments :: QuasiQuoter
exonSegments =
  QuasiQuoter quoteSegments (err "pattern") (err "type") (err "decl")
  where
    err :: String -> String -> Q a
    err tpe _ =
      exonError ("Cannot quote " <> tpe)