1 {-# LANGUAGE BangPatterns #-}
2 {-# LANGUAGE OverloadedStrings #-}
3 {-# LANGUAGE StrictData #-}
4 module Language.Symantic.XML.Write where
6 import Control.Applicative (Applicative(..), liftA2)
7 import Control.Monad (Monad(..))
9 import Data.Default.Class (Default(..))
10 import Data.Eq (Eq(..))
11 import Data.Foldable (Foldable(..), all)
12 import Data.Function (($), (.), const)
13 import Data.Maybe (Maybe(..))
14 import Data.Monoid (Monoid(..))
15 import Data.Semigroup (Semigroup(..))
16 import Data.String (String, IsString(..))
17 import Data.Traversable (Traversable(..))
18 import System.IO (IO, FilePath)
19 import Text.Show (Show(..))
20 import qualified Control.Monad.Trans.Reader as R
21 import qualified Control.Monad.Trans.State as S
22 import qualified Data.ByteString.Lazy as BSL
23 import qualified Data.Char as Char
24 import qualified Data.HashMap.Strict as HM
25 import qualified Data.HashSet as HS
26 import qualified Data.Sequence as Seq
27 import qualified Data.Text.Lazy as TL
28 import qualified Data.Text.Lazy.Builder as TLB
29 import qualified Data.Text.Lazy.Encoding as TL
31 import Language.Symantic.XML.Document as XML
33 writeXML :: XMLs -> TL.Text
34 writeXML xs = TLB.toLazyText $ write xs `R.runReader` def
36 writeXMLIndented :: TL.Text -> XMLs -> TL.Text
37 writeXMLIndented ind xs =
39 write xs `R.runReader` def
40 { reader_indent = if TL.null ind then mempty else "\n"
41 , reader_indent_delta = ind
44 writeFile :: FilePath -> TL.Text -> IO ()
45 writeFile fp t = BSL.writeFile fp $ TL.encodeUtf8 t
48 type Write = R.Reader Reader TLB.Builder
49 instance Semigroup Write where
51 instance Monoid Write where
54 instance IsString Write where
55 fromString = return . fromString
59 { reader_ns_scope :: Namespaces NCName
60 , reader_indent :: TLB.Builder
61 , reader_indent_delta :: TL.Text
62 , reader_no_text :: Bool
64 instance Default Reader where
66 { reader_ns_scope = def
68 , reader_indent_delta = ""
69 , reader_no_text = False
72 -- * Class 'Buildable'
73 class Buildable a where
74 build :: a -> TLB.Builder
75 instance Buildable Char.Char where
77 instance Buildable String where
78 build = TLB.fromString
79 instance Buildable TL.Text where
80 build = TLB.fromLazyText
81 instance Buildable NCName where
82 build = build . unNCName
83 instance Buildable Name where
84 build = build . unName
85 instance Buildable PName where
88 Nothing -> build pNameLocal
89 Just p -> build p<>":"<> build pNameLocal
90 instance Buildable Namespace where
91 build = build . unNamespace
92 instance Buildable EntityRef where
93 build EntityRef{..} = "&"<>build entityRef_name<>";"
94 instance Buildable CharRef where
95 build (CharRef c) = "&#"<>build (show (Char.ord c))<>";"
96 instance Buildable EscapedText where
97 build = foldMap $ \case
98 EscapedPlain t -> build t
99 EscapedEntityRef r -> build r
100 EscapedCharRef r -> build r
102 -- * Class 'Writable'
103 class Writeable a where
105 instance Writeable NCName where
106 write = return . TLB.fromLazyText . unNCName
107 instance Writeable XMLs where
110 if TL.null (reader_indent_delta ro)
111 then foldMap write xs
113 R.local (const ro{reader_no_text}) $
115 where reader_no_text =
117 Tree (Sourced _ (NodeText txt)) _ts ->
119 EscapedPlain t -> TL.all Char.isSpace t
122 instance Writeable XML where
123 write (Tree (Sourced _src nod) xs) = do
127 | [Tree (Sourced _ (NodeText av)) _] <- toList xs -> do
128 return $ " "<>buildAttr (prefixifyQName (reader_ns_scope ro) an) av
129 | otherwise -> mempty
133 "<[CDATA[["<>build t<>"]]>"
137 "<!--"<>build t<>"-->"
138 NodeElem elemQName -> do
139 let (elemAttrs, elemChilds) =
140 (`Seq.spanl` xs) $ \case
141 Tree (Sourced _ NodeAttr{}) _ -> True
143 let (usedNS, declNS) ::
144 ( HS.HashSet Namespace
147 foldl' go (initUsedNS, initDeclNS) elemAttrs
150 | qNameSpace elemQName == xmlns_empty = mempty
151 | otherwise = HS.singleton $ qNameSpace elemQName
152 initDeclNS = def{namespaces_default = namespaces_default $ reader_ns_scope ro}
153 go (!uNS, !dNS) = \case
154 Tree (Sourced _ (NodeAttr QName{..})) vs
155 -- xmlns:prefix="namespace"
156 | qNameSpace == xmlns_xmlns
157 , [Tree (Sourced _ (NodeText t)) _] <- toList vs ->
158 let n = unescapeText t in
160 { namespaces_prefixes =
163 -- NOTE: empty namespace means removal of the prefix from scope.
164 else (`HM.insert` qNameLocal))
166 (namespaces_prefixes dNS)
169 | qNameSpace == xmlns_empty
170 , qNameLocal == NCName "xmlns"
171 , [Tree (Sourced _ (NodeText t)) _] <- toList vs ->
173 dNS{namespaces_default = Namespace $ unescapeText t}
175 | qNameSpace == xmlns_empty -> (uNS, dNS)
176 -- {namespace}name="value"
177 | otherwise -> (HS.insert qNameSpace uNS, dNS)
180 -- NOTE: the inherited namespaces,
181 -- including those declared at this element.
183 (namespaces_prefixes declNS)
184 (namespaces_prefixes (reader_ns_scope ro))
186 -- NOTE: the namespaces used but not declared nor default,
187 -- with fresh prefixes.
188 HM.delete (namespaces_default declNS) $
189 (`S.evalState` HS.empty) $
191 (\() -> S.gets freshNCName)
192 (HS.toMap usedNS `HM.difference` inhNS)
194 -- NOTE: XMLify autoNS
196 (\acc (Namespace v) p ->
198 Tree (notSourced $ NodeAttr QName{qNameSpace=xmlns_xmlns, qNameLocal=p}) $
199 pure $ tree0 $ notSourced $ NodeText $ pure $ EscapedPlain v
201 let scopeNS = declNS { namespaces_prefixes = autoNS <> inhNS }
203 let build_elemPName = build $ prefixifyQName scopeNS elemQName in
204 let build_elemAttrs =
205 (`foldMap` (autoAttrs <> elemAttrs)) $ \case
206 Tree (Sourced _ (NodeAttr an)) vs
207 | [Tree (Sourced _ (NodeText av)) _] <- toList vs ->
208 " "<>buildAttr (prefixifyQName scopeNS{namespaces_default=""} an) av
211 <> "<"<>build_elemPName
212 <> build_elemAttrs <>
213 let build_elemChilds = write elemChilds
215 { reader_ns_scope = scopeNS
216 , reader_indent = reader_indent ro <> build (reader_indent_delta ro)
223 if TL.null (reader_indent_delta ro)
224 || noIndent elemChilds
226 else reader_indent ro
228 <> "</"<>build_elemPName<>">"
232 Tree (Sourced _ (NodeText _txt)) _ts -> True
238 "<?"<>build pn<>s<>write_xs<>"?>"
242 "<?"<>build pn<>s<>build pv<>"?>"
243 where s | TL.null pv = ""
251 buildAttr :: PName -> EscapedText -> TLB.Builder
252 buildAttr n v = build n<>"=\""<>buildAttrValue v<>"\""
254 buildAttrValue :: EscapedText -> TLB.Builder
255 buildAttrValue = foldMap $ \case
256 EscapedPlain p -> build p
257 EscapedEntityRef EntityRef{..} ->
258 build $ TL.replace "\"" """ entityRef_value
259 EscapedCharRef (CharRef c)
260 | c == '\"' -> """
261 | otherwise -> build c
263 removeSpaces :: XMLs -> XMLs
265 if (`all` xs) $ \case
266 Tree (Sourced _ (NodeText txt)) _ts ->
268 EscapedPlain t -> TL.all Char.isSpace t
271 then (`Seq.filter` xs) $ \case
272 Tree (Sourced _ NodeText{}) _ts -> False