@@ -2,7 +2,7 @@ module Text.XML.DOM.Parser.Combinators
2 ( -- * Generic combinators to traverse descendants
2 ( -- * Generic combinators to traverse descendants
3 traverseElems
3 traverseElems
4 , inFilteredTrav
4 , inFilteredTrav
5− -- * Using 'Buildable'
5+ -- * Using 'DomTraversable'
6 , inElemTrav
6 , inElemTrav
8 , inElemAll
8 , inElemAll
@@ -15,30 +15,47 @@ module Text.XML.DOM.Parser.Combinators
15 , ignoreElem
15 , ignoreElem
16 , ignoreEmpty
16 , ignoreEmpty
17 , ignoreBlank
17 , ignoreBlank
18+ -- * Getting current element's properties
21+ , getCurrentAttributes
22+ , getCurrentAttribute
23+ -- * Current element's checks
25+ -- * Parsing element's content
29+ -- * Parsing attributes
31+ , parseAttributeMaybe
20−import Control.Lens
34+import Control.Lens
35+import Control.Monad.Except
21−import Control.Monad.Reader
36+import Control.Monad.Reader
22−import Data.Foldable as F
37+import Data.List.NonEmpty (NonEmpty(..))
23−import Data.List as L
38+import qualified Data.Map.Strict as M
24−import Data.List.NonEmpty (NonEmpty(..))
39+import Data.Monoid
40+import Data.Text (Text)
25−import Data.Text as T
41+import qualified Data.Text as T
26−import Data.Traversable
42+import Data.Traversable
27−import Text.XML
43+import Data.Typeable
28−import Text.XML.DOM.Parser.Buildable
45+import Text.XML
29−import Text.XML.DOM.Parser.Types
46+import Text.XML.DOM.Parser.Types
30−import Text.XML.Lens
47+import Text.XML.Lens
33−-- | Generic function to traverse arbitrary inner elements.
50+-- | Generic function to traverse arbitrary inner cursors.
34traverseElems
51traverseElems
35 :: (Monad m, Foldable g, Traversable f)
52 :: (Monad m, Foldable g, Traversable f)
36 => ([Element] -> DomParserT g m (f (DomPath, Element)))
53 => ([Element] -> DomParserT g m (f (DomPath, Element)))
37 -- ^ Takes list of current elements and returns container with
54 -- ^ Takes list of current elements and returns container with
38− -- pairs of subpath (relatively to current elements) and element
55+ -- pairs of subpath (relatively to current element) and element
39 -- to run parser in
56 -- to run parser in
40 -> DomParserT Identity m a
57 -> DomParserT Identity m a
41− -- ^ Parser to run for each element found in traversable 'f'
58+ -- ^ Parser will be runned for each element found in traversable
42 -> DomParserT g m (f a)
59 -> DomParserT g m (f a)
43traverseElems trav parser = do
60traverseElems trav parser = do
@@ -48,73 +65,68 @@ traverseElems trav parser = do
48 newpd = ParserData
65 newpd = ParserData
49 { _pdElements = Identity e
66 { _pdElements = Identity e
50 , _pdPath = pd ^. pdPath <> subpath }
67 , _pdPath = pd ^. pdPath <> subpath }
51− lift $ runReaderT parser newpd
68+ -- type of reader has been changed, local won't work here
69+ magnify (to $ const newpd) parser
53−-- | Traverses elements located in same path using filtering function
71+-- | Takes function filtering
54inFilteredTrav
72inFilteredTrav
55− :: (Monad m, Foldable g, Buildable f)
73+ :: (Monad m, Foldable g, DomTraversable f)
56 => ([Element] -> (DomPath, [Element]))
74 => ([Element] -> (DomPath, [Element]))
57− -- ^ Takes list of current elements and returns some descendants
75+ -- ^ Function returning some filtered elements with path suffixes which will
58− -- subset and path this descendants located at. Path is should be
76+ -- be appended to parser's state
59− -- same for all descendants and required for error message
60 -> DomParserT Identity m a
77 -> DomParserT Identity m a
61 -> DomParserT g m (f a)
78 -> DomParserT g m (f a)
62inFilteredTrav deeper = traverseElems trav
79inFilteredTrav deeper = traverseElems trav
64 trav e = do
81 trav e = do
65 let (path, elems) = deeper e
82 let (path, elems) = deeper e
66− case build elems of
83+ case buildDomTraversable elems of
67 Nothing -> throwParserError $ PENotFound . (<> path)
84 Nothing -> throwParserError $ PENotFound . (<> path)
68 Just tr -> return $ fmap (path,) tr
85 Just tr -> return $ fmap (path,) tr
70−-- | Runs parser arbitrary times, depending on 'Buildable' instance of
71−-- 'f'. For example if 'f' becomes 'NonEmpty' then 'inElemTrav' finds
72−-- @one or more@ elements matched by given 'ElemMatcher' and run
73−-- parser in each found element, then returns @NonEmpty a@ of results.
75− :: (Monad m, Foldable g, Buildable f)
88+ :: (Monad m, Foldable g, DomTraversable f)
76− => ElemMatcher -- ^ Tag(s) matcher to traverse in
89+ => Text
77 -> DomParserT Identity m a
90 -> DomParserT Identity m a
78 -> DomParserT g m (f a)
91 -> DomParserT g m (f a)
79inElemTrav n = inFilteredTrav deeper
92inElemTrav n = inFilteredTrav deeper
81− elemsFold = folded . nodes . folded . _Element . elMatch n
94+ deeper = ([n],) . toListOf (folded . nodes . folded . _Element . ell n)
82− deeper = (DomPath [_emShow n],) . toListOf elemsFold
84−-- | Runs parser inside first children element matched by macher
96+-- | Runs parser inside first children element with given name
86 :: (Monad m, Foldable g)
98 :: (Monad m, Foldable g)
87− => ElemMatcher
99+ => Text
88 -> DomParserT Identity m a
100 -> DomParserT Identity m a
89 -> DomParserT g m a
101 -> DomParserT g m a
90inElem n = fmap runIdentity . inElemTrav n
102inElem n = fmap runIdentity . inElemTrav n
93 :: (Monad m, Foldable g)
105 :: (Monad m, Foldable g)
94− => ElemMatcher
106+ => Text
95 -> DomParserT Identity m a
107 -> DomParserT Identity m a
96 -> DomParserT g m [a]
108 -> DomParserT g m [a]
97inElemAll = inElemTrav
109inElemAll = inElemTrav
100 :: (Monad m, Foldable g)
112 :: (Monad m, Foldable g)
101− => ElemMatcher
113+ => Text
102 -> DomParserT Identity m a
114 -> DomParserT Identity m a
103 -> DomParserT g m (Maybe a)
115 -> DomParserT g m (Maybe a)
104inElemMay = inElemTrav
116inElemMay = inElemTrav
107 :: (Monad m, Foldable g)
119 :: (Monad m, Foldable g)
108− => ElemMatcher
120+ => Text
109 -> DomParserT Identity m a
121 -> DomParserT Identity m a
110 -> DomParserT g m (NonEmpty a)
122 -> DomParserT g m (NonEmpty a)
111inElemNe = inElemTrav
123inElemNe = inElemTrav
113{- | Dive given parser's current tags set into the given path. The @divePath
125{- | Dive given parser's current tags set into the given path. The @divePath
114−["a", "b"]@ differs from @inElem "a" $ inElem "b"@. Namely the first variant
126+["a", "b"]@ differs from @inElem "a" . inElem "b"@. Namely the first variant
115will not fail if occured tag "a" which does not contains tag "b". This
127will not fail if occured tag "a" which does not contains tag "b". This
116behaviour is desireable when you dont want to parse whole XML and just want
128behaviour is desireable when you dont want to parse whole XML and just want
117−to pull tags located in some path. The other difference is in traversing inner
129+to pull tags in some path. The other difference is in traversing inner
118elements. Consider this code
130elements. Consider this code
@@ -132,31 +144,26 @@ which translates like: @a > b > c > fromDom@
132As you can see, inElem always takes first element and runs inner parser in this
144As you can see, inElem always takes first element and runs inner parser in this
133single element, unlike 'divePath' which runs inner parser @in all@ descendants
145single element, unlike 'divePath' which runs inner parser @in all@ descendants
134in given path.
146in given path.
136−Note also that 'divePath' takes parser parameterized by @[]@ not by
137−'Identity'. This because when you dive using some path you will get a
138−list of found elements and all these elements will be @current@ for
143 :: forall m g a
150 :: forall m g a
144 . (Monad m, Foldable g)
151 . (Monad m, Foldable g)
145− => [ElemMatcher]
152+ => [Text]
146 -> DomParserT [] m a
153 -> DomParserT [] m a
147 -> DomParserT g m a
154 -> DomParserT g m a
148divePath path = magnify $ to modElems
155divePath path = magnify $ to modElems
151 = over pdElements (toListOf $ folded . diver)
158 = over pdElements (toListOf $ folded . diver)
152− . over pdPath (<> DomPath (L.map _emShow path))
159+ . over pdPath (<> path)
153 diver :: Fold Element Element
160 diver :: Fold Element Element
154− diver = F.foldr (.) id $ L.map toDive path
161+ diver = foldr (.) id $ map toDive path
155− toDive n = nodes . folded . _Element . elMatch n
162+ toDive n = nodes . folded . _Element . ell n
158 :: (Monad m, Foldable g)
165 :: (Monad m, Foldable g)
159− => ElemMatcher
166+ => Text
160 -> DomParserT [] m a
167 -> DomParserT [] m a
161 -> DomParserT g m a
168 -> DomParserT g m a
162diveElem p = divePath [p]
169diveElem p = divePath [p]
@@ -183,7 +190,7 @@ ignoreEmpty
183 -> DomParserT Identity m (Maybe a)
190 -> DomParserT Identity m (Maybe a)
184ignoreEmpty = ignoreElem test
191ignoreEmpty = ignoreElem test
186− test e = L.null $ e ^. nodes
193+ test e = null $ e ^. nodes
188-- | If all current elements contains blank content, or contains nothing at all
195-- | If all current elements contains blank content, or contains nothing at all
189-- , then returns Nothing, else runs parser.
196-- , then returns Nothing, else runs parser.
@@ -197,6 +204,130 @@ ignoreBlank = ignoreElem test
198 elems = e ^.. nodes . folded . _Element
205 elems = e ^.. nodes . folded . _Element
199 cont = mconcat $ e ^.. nodes . folded . _Content
206 cont = mconcat $ e ^.. nodes . folded . _Content
200− in if | not $ L.null elems -> False
207+ in if | not $ null elems -> False
201 | T.null $ T.strip cont -> True
208 | T.null $ T.strip cont -> True
202 | otherwise -> False
209 | otherwise -> False
211+-- | Returns name of current element.
214+getCurrentName :: (Monad m) => DomParserT Identity m Text
215+getCurrentName = view $ pdElements . to runIdentity . localName
217+-- | If name of current tag differs from first argument throws 'PENotFound' with
218+-- tag name replaced in last path's segment. Useful for checking root
219+-- document's element name.
223+ -> DomParserT Identity m ()
224+checkCurrentName n = do
225+ cn <- getCurrentName
226+ unless (cn == n) $ do
228+ let pinit = if null p then [] else init p
229+ throwError $ ParserErrors [PENotFound $ pinit ++ [n]]
232+-- | Get current content. If current element contains no content or
233+-- have inner elements then Nothing returned
236+getCurrentContent :: (Monad m) => DomParserT Identity m (Maybe Text)
237+getCurrentContent = do
238+ nds <- view $ pdElements . to runIdentity . nodes
241+ els = nds ^.. folded . _Element
243+ conts = nds ^.. folded . _Content
245+ | not $ null els -> Nothing
246+ | null conts -> Nothing
247+ | otherwise -> Just $ mconcat conts
249+-- | Parses content inside current tag. It expects current element set
250+-- consists of exactly ONE element. If current element does not
251+-- contains content or have other elements as childs then throws error
254+ => (Text -> Either Text a)
255+ -- ^ Content parser, return error msg if value is not parsed
256+ -> DomParserT Identity m a
257+parseContent parse = getCurrentContent >>= \case
258+ Nothing -> throwParserError PEContentNotFound
259+ Just c -> case parse c of
260+ Left e -> throwParserError $ PEWrongFormat e
263+-- | If reader returns 'Nothing' then resulting function returns 'Left
270+ => (Text -> Maybe a)
271+ -- ^ Content or attribute reader
273+ -- ^ Content or attribute value
275+maybeReadContent f t = maybe (Left msg) Right $ f t
277+ msg = "Not readable " <> n <> ": " <> t
278+ n = T.pack $ show $ typeRep (Proxy :: Proxy a)
280+-- | Tries to read given text to value using 'Read'. Useful to use
281+-- with 'parseContent' and 'parseAttribute'
283+ :: (Read a, Typeable a)
286+readContent = maybeReadContent $ readMaybe . T.unpack . T.strip
288+-- | Retuns map of attributes of current element
291+getCurrentAttributes :: (Monad m) => DomParserT Identity m (M.Map Name Text)
292+getCurrentAttributes = view $ pdElements . to runIdentity . attrs
294+-- | Returns element with given name or 'Nothing'
297+getCurrentAttribute :: (Monad m) => Text -> DomParserT Identity m (Maybe Text)
298+getCurrentAttribute attrName'
299+ = preview $ pdElements . to runIdentity . attr attrName
301+ attrName = Name attrName' Nothing Nothing
303+-- | Parses attribute with given name, throws error if attribute is not found.
310+ -> (Text -> Either Text a)
311+ -- ^ Attribute content parser
312+ -> DomParserT Identity m a
313+parseAttribute attrName parser =
314+ parseAttributeMaybe attrName parser >>= \case
315+ Nothing -> throwParserError $ PEAttributeNotFound attrName
318+-- | Parses attribute with given name.
325+ -> (Text -> Either Text a)
326+ -- ^ Attribute content parser
327+ -> DomParserT Identity m (Maybe a)
328+parseAttributeMaybe attrName parser =
329+ getCurrentAttribute attrName >>= \case
330+ Nothing -> return Nothing
331+ Just aval -> case parser aval of
332+ Left err -> throwParserError $ PEAttributeWrongFormat attrName err
333+ Right a -> return $ Just a