1 {-# LANGUAGE OverloadedStrings #-}
2 {-# LANGUAGE FlexibleInstances #-}
3 {-# LANGUAGE TypeFamilies #-}
4 {-# OPTIONS_GHC -fno-warn-orphans #-}
5 module Text.Blaze.Utils where
7 import Blaze.ByteString.Builder (Builder)
8 import Control.Applicative (Applicative(..))
9 import Control.Monad (Monad(..))
11 import Data.Char (Char)
12 import Data.Foldable (Foldable(..))
13 import Data.Function ((.), ($))
14 import Data.Functor ((<$>))
15 import Data.Functor.Compose (Compose(..))
17 import Data.Maybe (Maybe(..), maybe)
18 import Data.Monoid (Monoid(..))
19 import Data.Semigroup (Semigroup(..))
20 import Data.String (IsString(..))
21 import Data.Text (Text)
22 import GHC.Exts (IsList(..))
23 import Prelude (Num(..), undefined)
25 import Text.Blaze as B
26 import Text.Blaze.Internal as B hiding (null)
27 import Text.Show (Show(..))
28 import qualified Blaze.ByteString.Builder as BS
29 import qualified Blaze.ByteString.Builder.Html.Utf8 as BS
30 import qualified Control.Monad.Trans.State as S
31 import qualified Data.ByteString as BS
32 import qualified Data.ByteString.Lazy as BSL
33 import qualified Data.List as List
34 import qualified Data.Text as Text
35 import qualified Data.Text.Lazy as TL
36 import qualified Data.Text.Encoding as BS
37 import qualified Text.Blaze.Html5 as H
38 import qualified Text.Blaze.Renderer.Utf8 as BS
40 -- | 'Attribute' in 'Maybe'.
42 (!??) :: Attributable h => h -> Maybe Attribute -> h
43 (!??) h = maybe h (h !)
45 whenMarkup :: MarkupM a -> MarkupM () -> MarkupM ()
46 whenMarkup Empty{} _b = return ()
49 whenJust :: Applicative m => Maybe a -> (a -> m ()) -> m ()
50 whenJust Nothing _f = pure ()
51 whenJust (Just a) f = f a
53 whenSome :: (Applicative m, Foldable f) => f a -> (f a -> m ()) -> m ()
54 whenSome x _f | null x = pure ()
57 whenText :: Applicative m => Text -> (Text -> m ()) -> m ()
58 whenText "" _f = pure ()
62 instance Semigroup H.AttributeValue where
65 instance IsList H.AttributeValue where
66 type Item AttributeValue = AttributeValue
67 fromList = mconcat . List.intersperse " "
72 attrify :: a -> H.AttributeValue
73 instance Attrify Char where
74 attrify = fromString . pure
75 instance Attrify Text where
76 attrify = fromString . Text.unpack
77 instance Attrify TL.Text where
78 attrify = fromString . TL.unpack
79 instance Attrify Int where
80 attrify = fromString . show
81 instance Attrify [Char] where
86 mayAttr :: (AttributeValue -> Attribute) -> a -> Maybe Attribute
87 instance MayAttr a => MayAttr (Maybe a) where
88 mayAttr a t = t >>= mayAttr a
89 instance MayAttr Text where
90 mayAttr _ "" = Nothing
91 mayAttr a t = Just (a $ fromString $ Text.unpack t)
92 instance MayAttr TL.Text where
93 mayAttr _ "" = Nothing
94 mayAttr a t = Just (a $ fromString $ TL.unpack t)
95 instance MayAttr Int where
96 mayAttr a t = Just (a $ fromString $ show t)
97 instance MayAttr [Char] where
98 mayAttr _ "" = Nothing
99 mayAttr a t = Just (a $ fromString t)
100 instance MayAttr AttributeValue where
103 -- * Type 'StateMarkup'
104 -- | Composing state and markups.
105 type StateMarkup st = Compose (S.State st) B.MarkupM
106 instance Semigroup (StateMarkup st a) where
108 instance Monoid (StateMarkup st ()) where
111 instance Monad (StateMarkup st) where
113 Compose sma >>= a2csmb =
114 Compose $ sma >>= \ma ->
115 case ma >>= B.Empty . a2csmb of
116 B.Append _ma (B.Empty csmb) ->
117 B.Append ma <$> getCompose csmb
118 _ -> undefined -- NOTE: impossible case, by definition of (>>=) on 'B.MarkupM'.
119 {- NOTE: the 'st' may need to use the 'String', so no such instance.
120 instance IsString (StateMarkup st ()) where
121 fromString = Compose . return . fromString
124 -- | Lift a 'B.MarkupM' constructor to a 'StateMarkup' one.
125 ($$) :: (B.MarkupM a -> B.MarkupM a) -> StateMarkup st a -> StateMarkup st a
126 ($$) f m = Compose $ f <$> getCompose m
129 liftStateMarkup :: S.State st a -> StateMarkup st a
130 liftStateMarkup = Compose . (return <$>)
132 runStateMarkup :: st -> StateMarkup st a -> (B.MarkupM a, st)
133 runStateMarkup st = (`S.runState` st) . getCompose
135 evalStateMarkup :: st -> StateMarkup st a -> B.MarkupM a
136 evalStateMarkup st = (`S.evalState` st) . getCompose
138 -- | Render some 'Markup' to a 'Builder'.
140 -- An 'IndentTag' is queried on each tag
141 -- to indent tags differently according to their names.
142 prettyMarkupBuilder :: (Text -> Bool) -> Markup -> Builder
143 prettyMarkupBuilder isInlinedElement ms = go (noIndent "" ms) "\n" mempty ms
147 noIndent :: Text -> MarkupM b -> Bool
148 noIndent e children =
149 isInlinedElement e ||
150 allInlined children && not (allComment children)
152 allInlined :: MarkupM b -> Bool
154 Append x y -> allInlined x && allInlined y
155 CustomParent tag _m -> isInlinedElement $ t_ChoiceString tag
156 CustomLeaf tag _close _ -> isInlinedElement $ t_ChoiceString tag
157 Parent tag _open _close _m -> isInlinedElement $ getText tag
158 Leaf tag _begin _end _ -> isInlinedElement $ getText tag
159 AddAttribute _ _key _value m -> allInlined m
160 AddCustomAttribute _key _value m -> allInlined m
164 allComment :: MarkupM b -> Bool
166 Append x y -> allComment x && allComment y
167 AddAttribute _ _key _value m -> allComment m
168 AddCustomAttribute _key _value m -> allComment m
172 goAttrs :: Bool -> Builder -> Text -> [Builder] -> Builder
173 goAttrs noInd ind t_tag attrs =
178 let ind_key = BS.fromString $ List.replicate (Text.length t_tag + 1) ' ' in
179 let ind_attr = if noInd then mempty else ind<>ind_key in
180 a0 <> foldMap (ind_attr <>) as
181 go :: Bool -> Builder -> [Builder] -> MarkupM b -> Builder
182 go noInd ind attrs = \case
183 Parent tag open close content ->
184 let noInd' = noIndent (getText tag) content in
185 (if noInd then mempty else ind)
186 <> BS.copyByteString (getUtf8ByteString open)
187 <> goAttrs noInd ind (getText tag) attrs
189 <> go noInd' (if noInd then ind else ind<>inc) mempty content
190 <> (if noInd' then mempty else ind)
191 <> BS.copyByteString (getUtf8ByteString close)
192 CustomParent tag content ->
193 let t_tag = t_ChoiceString tag in
194 let noInd' = noIndent t_tag content in
195 (if noInd then mempty else ind)
198 <> goAttrs noInd ind t_tag attrs
200 <> go noInd' (if noInd' then ind else ind<>inc) mempty content
201 <> (if noInd' then mempty else ind)
202 <> BS.fromByteString "</"
203 <> bs_ChoiceString tag
205 Leaf tag begin end _ ->
206 (if noInd then mempty else ind)
207 <> BS.copyByteString (getUtf8ByteString begin)
208 <> goAttrs noInd ind (getText tag) attrs
209 <> BS.copyByteString (getUtf8ByteString end)
210 CustomLeaf tag close _ ->
211 let t_tag = t_ChoiceString tag in
212 (if noInd then mempty else ind)
215 <> goAttrs noInd ind t_tag attrs
216 <> (if close then BS.fromByteString "/>" else BS.fromChar '>')
217 AddAttribute _ key value m ->
219 ( BS.copyByteString (getUtf8ByteString key)
220 <> bs_ChoiceString value
223 AddCustomAttribute key value m ->
226 <> bs_ChoiceString key
227 <> BS.fromByteString "=\""
228 <> bs_ChoiceString value
231 Content c _ -> bs_ChoiceString c
233 (if noInd then mempty else ind)
234 <> BS.fromByteString "<!--"
235 <> indentChoiceString (ind <> " ") comment
236 <> (if noInd then mempty else ind)
237 <> BS.fromByteString "-->"
239 go noInd ind attrs m1 <>
240 go noInd ind attrs m2
243 -- | Render 'Markup' to a lazy UTF-8 encoded 'BSL.ByteString'.
244 prettyMarkup :: (Text -> Bool) -> Markup -> BSL.ByteString
245 prettyMarkup ind = BS.toLazyByteString . prettyMarkupBuilder ind
247 prettyMarkupIO :: (Text -> Bool) -> (BS.ByteString -> IO ()) -> Markup -> IO ()
248 prettyMarkupIO ind io = BS.toByteStringIO io . prettyMarkupBuilder ind
250 bs_ChoiceString :: ChoiceString -> Builder
251 bs_ChoiceString cs = BS.renderMarkupBuilder (Content cs ())
253 t_ChoiceString :: ChoiceString -> Text
254 t_ChoiceString = BS.decodeUtf8 . BS.toByteString . bs_ChoiceString
256 -- | @indentText ind txt@ indent 'txt' with 'ind' at newlines.
257 indentText :: Builder -> Text -> Builder
260 List.intersperse ind .
261 (BS.fromHtmlEscapedText <$>) .
264 -- | Render an indented 'ChoiceString'.
265 indentChoiceString :: Builder -> ChoiceString -> Builder
266 indentChoiceString ind (Static s) = indentText ind $ getText s
267 indentChoiceString ind (String s) = indentText ind $ Text.pack s
268 indentChoiceString ind (Text s) = indentText ind s
269 indentChoiceString ind (ByteString s) = indentText ind $ BS.decodeUtf8 s
270 indentChoiceString ind (PreEscaped x) = case x of
271 String s -> indentText ind $ Text.pack s
272 Text s -> indentText ind s
273 s -> indentChoiceString ind s
274 indentChoiceString ind (External x) = case x of
275 -- Check that the sequence "</" is *not* in the external data.
276 String s -> if "</" `List.isInfixOf` s then mempty else BS.fromString s
277 Text s -> if "</" `Text.isInfixOf` s then mempty else BS.fromText s
278 ByteString s -> if "</" `BS.isInfixOf` s then mempty else BS.fromByteString s
279 s -> indentChoiceString ind s
280 indentChoiceString ind (AppendChoiceString x y) =
281 indentChoiceString ind x <>
282 indentChoiceString ind y
283 indentChoiceString ind EmptyChoiceString = indentText ind mempty
284 {-# INLINE indentChoiceString #-}