{-# LANGUAGE PatternSynonyms, TemplateHaskell #-} module BuilderQQ (b, Builder, toStrictText, toLazyText, fromText, fromString, ifJust, escId, escAttr, CanBuild (..)) where import Data.Char (isSpace, isAlphaNum) import Language.Haskell.TH import Language.Haskell.TH.Quote import Data.List (dropWhileEnd) import Data.Maybe (mapMaybe, fromMaybe) import Data.Text.Lazy.Builder (Builder, fromText, fromLazyText, fromString, singleton, toLazyText) import qualified Data.Text as Strict import qualified Data.Text.Lazy as Lazy import Data.Text.Lazy (toStrict) import Data.List.NonEmpty (NonEmpty, toList) data Chunk = Lit String | Var String -- | -- * use @$var@ to insert a variable (instance of 'CanBuild') -- * use @$&@ to insert nothing like @\&@ in a string (e.g. to add whitespace -- at the start or end, or to have a variable followed by a letter -- * use @$$@ for a literal @$@ parseB :: String -> ExpQ parseB = toExpQ . reverse . go "" [] . trim where trim = dropWhileEnd isSpace . dropWhile isSpace go acc cs [] = addLit acc cs go acc cs ('$':'&':rest) = go acc cs rest -- $&: expands to nothing go acc cs ('$':'$':rest) = go ('$' : acc) cs rest -- $$: expands to one $ go acc cs ('$':rest) = go "" (Var var : addLit acc cs) rest' -- $var where (var, rest') = span isIdChar rest go acc cs (c:rest) = go (c : acc) cs rest addLit l cs = if null l then cs else Lit (reverse l) : cs isIdChar c = isAlphaNum c || c `elem` ("_'" :: String) toExpQ cs = [|mconcat $(listE $ mapMaybe chunk1 cs) :: Builder|] chunk1 (Lit lit) = Just $ stringE lit chunk1 (Var name) = Just $ [|build $(varE $ mkName name)|] toStrictText :: Builder -> Strict.Text toStrictText = toStrict . toLazyText b :: QuasiQuoter b = QuasiQuoter parseB undefined undefined undefined ifJust :: Monoid b => Maybe a -> (a -> b) -> b ifJust x f = maybe mempty f x escId :: Strict.Text -> Builder escId = foldMap esc1 . Strict.unpack where esc1 c | isSpace c = "" | latin1Special c = "_" | otherwise = singleton c latin1Special c = c <= 'ΓΏ' && not (isAlphaNum c) && c /= '-' escAttr :: Strict.Text -> Builder escAttr = foldMap esc1 . Strict.unpack where esc1 c = fromMaybe (singleton c) $ lookup c [('<', "<"), ('>', ">"), ('"', """), ('\'', "'"), ('&', "&")] class CanBuild a where build :: a -> Builder instance CanBuild Builder where build = id instance CanBuild Strict.Text where build = fromText instance CanBuild Lazy.Text where build = fromLazyText instance CanBuild Char where build = singleton instance CanBuild String where build = fromString newtype ShowBuild a = ShowBuild a deriving newtype Show instance Show a => CanBuild (ShowBuild a) where build = build . show deriving via ShowBuild Int instance CanBuild Int deriving via ShowBuild Integer instance CanBuild Integer instance {-# OVERLAPPABLE #-} CanBuild a => CanBuild [a] where build = foldMap \x -> build x <> "\n" instance CanBuild a => CanBuild (NonEmpty a) where build = build . toList