git.iko.soy/typeable/dom-parser
on master help sign in
Upd: refactor and split Content.hs
bee2747Aleksey Uimanov
dom-parser.cabal+1
@@ -57,6 +57,7 @@ library
57 , xml-conduit
57 , xml-conduit
58 , xml-lens
58 , xml-lens
59 exposed-modules: Text.XML.DOM.Parser
59 exposed-modules: Text.XML.DOM.Parser
60+ , Text.XML.DOM.Parser.Attributes
60 , Text.XML.DOM.Parser.Buildable
61 , Text.XML.DOM.Parser.Buildable
61 , Text.XML.DOM.Parser.Combinators
62 , Text.XML.DOM.Parser.Combinators
62 , Text.XML.DOM.Parser.Content
63 , Text.XML.DOM.Parser.Content
src/Text/XML/DOM/Parser.hs+3−1
@@ -1,5 +1,6 @@
1module Text.XML.DOM.Parser
1module Text.XML.DOM.Parser
2− ( module Text.XML.DOM.Parser.Buildable
2+ ( module Text.XML.DOM.Parser.Attributes
3+ , module Text.XML.DOM.Parser.Buildable
3 , module Text.XML.DOM.Parser.Combinators
4 , module Text.XML.DOM.Parser.Combinators
4 , module Text.XML.DOM.Parser.Content
5 , module Text.XML.DOM.Parser.Content
5 , module Text.XML.DOM.Parser.FromAttribute
6 , module Text.XML.DOM.Parser.FromAttribute
@@ -7,6 +8,7 @@ module Text.XML.DOM.Parser
7 , module Text.XML.DOM.Parser.Types
8 , module Text.XML.DOM.Parser.Types
8 ) where
9 ) where
9
10
11+import Text.XML.DOM.Parser.Attributes
10import Text.XML.DOM.Parser.Buildable
12import Text.XML.DOM.Parser.Buildable
11import Text.XML.DOM.Parser.Combinators
13import Text.XML.DOM.Parser.Combinators
12import Text.XML.DOM.Parser.Content
14import Text.XML.DOM.Parser.Content
src/Text/XML/DOM/Parser/Attributes.hsnew file+64
@@ -0,0 +1,64 @@
1+module Text.XML.DOM.Parser.Attributes
2+ ( parseAttribute
3+ , parseAttributeMaybe
4+ , getCurrentAttributes
5+ , getCurrentAttribute
6+ ) where
7+
8+import Control.Lens
9+import Data.Map.Strict as M
10+import Data.Text as T
11+import Text.XML.DOM.Parser.Types
12+import Text.XML.Lens
13+
14+-- | Parses attribute with given name, throws error if attribute is not found.
15+--
16+-- @since 1.0.0
17+parseAttribute
18+ :: (Monad m)
19+ => NameMatcher
20+ -- ^ Attribute name
21+ -> (Text -> Either Text a)
22+ -- ^ Attribute content parser
23+ -> DomParserT Identity m a
24+parseAttribute attrName parser =
25+ parseAttributeMaybe attrName parser >>= \case
26+ Nothing -> throwParserError $ PEAttributeNotFound attrName
27+ Just a -> return a
28+
29+-- | Parses attribute with given name. Returns Nothing if attribute is
30+-- not found.
31+--
32+-- @since 1.0.0
33+parseAttributeMaybe
34+ :: (Monad m)
35+ => NameMatcher
36+ -- ^ Attribute name
37+ -> (Text -> Either Text a)
38+ -- ^ Attribute content parser
39+ -> DomParserT Identity m (Maybe a)
40+parseAttributeMaybe attrName parser =
41+ getCurrentAttribute attrName >>= \case
42+ Nothing -> return Nothing
43+ Just aval -> case parser aval of
44+ Left err -> throwParserError $ PEAttributeWrongFormat attrName err
45+ Right a -> return $ Just a
46+
47+-- | Retuns map of attributes of current element
48+--
49+-- @since 1.0.0
50+getCurrentAttributes
51+ :: (Monad m)
52+ => DomParserT Identity m (M.Map Name Text)
53+getCurrentAttributes = view $ pdElements . to runIdentity . attrs
54+
55+-- | Returns element with given name or 'Nothing'
56+--
57+-- @since 1.0.0
58+getCurrentAttribute
59+ :: (Monad m)
60+ => NameMatcher
61+ -> DomParserT Identity m (Maybe Text)
62+getCurrentAttribute attrName
63+ = preview $ pdElements . to runIdentity . attrs . to M.toList
64+ . traversed . filtered (views _1 (attrName ^. nmMatch)) . _2
src/Text/XML/DOM/Parser/Combinators.hs+13−4
@@ -68,9 +68,13 @@ inFilteredTrav deeper = traverseElems trav
68 Nothing -> throwParserError $ PENotFound . (<> path)
68 Nothing -> throwParserError $ PENotFound . (<> path)
69 Just tr -> return $ fmap (path,) tr
69 Just tr -> return $ fmap (path,) tr
70
70
71+-- | Runs parser arbitrary times, depending on 'Buildable' instance of
72+-- 'f'. For example if 'f' becomes 'NonEmpty' then 'inElemTrav' finds
73+-- @one or more@ elements matched by given 'ElemMatcher' and run
74+-- parser in each found element, then returns @NonEmpty a@ of results.
71inElemTrav
75inElemTrav
72 :: (Monad m, Foldable g, Buildable f)
76 :: (Monad m, Foldable g, Buildable f)
73− => ElemMatcher -- ^ Name of tag to traverse in
77+ => ElemMatcher -- ^ Tag(s) matcher to traverse in
74 -> DomParserT Identity m a
78 -> DomParserT Identity m a
75 -> DomParserT g m (f a)
79 -> DomParserT g m (f a)
76inElemTrav n = inFilteredTrav deeper
80inElemTrav n = inFilteredTrav deeper
@@ -78,7 +82,7 @@ inElemTrav n = inFilteredTrav deeper
78 elemsFold = folded . nodes . folded . _Element . elMatch n
82 elemsFold = folded . nodes . folded . _Element . elMatch n
79 deeper = (DomPath [_emShow n],) . toListOf elemsFold
83 deeper = (DomPath [_emShow n],) . toListOf elemsFold
80
84
81−-- | Runs parser inside first children element with given name
85+-- | Runs parser inside first children element matched by macher
82inElem
86inElem
83 :: (Monad m, Foldable g)
87 :: (Monad m, Foldable g)
84 => ElemMatcher
88 => ElemMatcher
@@ -108,10 +112,10 @@ inElemNe
108inElemNe = inElemTrav
112inElemNe = inElemTrav
109
113
110{- | Dive given parser's current tags set into the given path. The @divePath
114{- | Dive given parser's current tags set into the given path. The @divePath
111−["a", "b"]@ differs from @inElem "a" . inElem "b"@. Namely the first variant
115+["a", "b"]@ differs from @inElem "a" $ inElem "b"@. Namely the first variant
112will not fail if occured tag "a" which does not contains tag "b". This
116will not fail if occured tag "a" which does not contains tag "b". This
113behaviour is desireable when you dont want to parse whole XML and just want
117behaviour is desireable when you dont want to parse whole XML and just want
114−to pull tags in some path. The other difference is in traversing inner
118+to pull tags located in some path. The other difference is in traversing inner
115elements. Consider this code
119elements. Consider this code
116
120
117@
121@
@@ -129,6 +133,11 @@ which translates like: @a > b > c > fromDom@
129As you can see, inElem always takes first element and runs inner parser in this
133As you can see, inElem always takes first element and runs inner parser in this
130single element, unlike 'divePath' which runs inner parser @in all@ descendants
134single element, unlike 'divePath' which runs inner parser @in all@ descendants
131in given path.
135in given path.
136+
137+Note also that 'divePath' takes parser parameterized by @[]@ not by
138+'Identity'. This because when you dive using some path you will get a
139+list of found elements and all these elements will be @current@ for
140+parser.
132-}
141-}
133
142
134divePath
143divePath
src/Text/XML/DOM/Parser/Content.hs+62−118
@@ -1,28 +1,22 @@
1module Text.XML.DOM.Parser.Content
1module Text.XML.DOM.Parser.Content
2− ( -- * Content readers
2+ (
3− readBool
3+ -- * Parsing element's content
4− , readChar
4+ parseContent
5− -- ** Reader constructors
6− , maybeReadContent
7− , readContent
8 -- * Getting current element's properties
5 -- * Getting current element's properties
9 , getCurrentName
6 , getCurrentName
10 , getCurrentContent
7 , getCurrentContent
11− , getCurrentAttributes
12− , getCurrentAttribute
13 , checkCurrentName
8 , checkCurrentName
14− -- * Parsing element's content
9+ -- * Internal
15− , parseContent
10+ , maybeReadContent
16− -- * Parsing attributes
11+ , readContent
17− , parseAttribute
12+ , readBool
18− , parseAttributeMaybe
13+ , readChar
19 ) where
14 ) where
20
15
21import Control.Lens
16import Control.Lens
22import Control.Monad
17import Control.Monad
23import Control.Monad.Except
18import Control.Monad.Except
24import Data.List as L
19import Data.List as L
25−import Data.Map.Strict as M
26import Data.Monoid
20import Data.Monoid
27import Data.Text as T
21import Data.Text as T
28import Data.Typeable
22import Data.Typeable
@@ -31,54 +25,19 @@ import Text.XML.DOM.Parser.Types
31import Text.XML.Lens
25import Text.XML.Lens
32
26
33
27
34−-- | @since 1.0.0
28+-- | Parses content inside current tag. It expects current element set
35−readBool :: Text -> Either Text Bool
29+-- consists of exactly ONE element.
36−readBool t =
30+parseContent
37− let
31+ :: (Monad m)
38− lowt = T.toLower $ T.strip t
39− tvals = ["y", "yes", "t", "true", "1"]
40− fvals = ["n", "no", "f", "false", "0"]
41− in if
42− | lowt `elem` tvals -> Right True
43− | lowt `elem` fvals -> Right False
44− | otherwise ->
45− Left $ "Could not read " <> t <> " as Bool"
46−
47−-- | Expects text to be single character
48−--
49−-- @since 1.0.0
50−readChar :: Text -> Either Text Char
51−readChar t = case T.unpack $ T.strip t of
52− [c] -> Right c
53− _ -> Left "Should have exactly one non-blank character"
54−
55−-- | If reader returns 'Nothing' then resulting function returns 'Left
56−-- "error message"'. 'Typeable' is used for generating usefull error
57−-- message.
58−--
59−-- @since 1.0.0
60−maybeReadContent
61− :: forall a
62− . (Typeable a)
63− => (Text -> Maybe a)
32+ => (Text -> Either Text a)
64− -- ^ Content or attribute reader
33+ -- ^ Content parser, return error msg if value is not parsed
65− -> Text
34+ -> DomParserT Identity m a
66− -- ^ Content or attribute value
35+parseContent parse = getCurrentContent >>= \case
67− -> Either Text a
36+ Nothing -> throwParserError PEContentNotFound
68−maybeReadContent f t = maybe (Left msg) Right $ f t
37+ Just c -> case parse c of
69− where
38+ Left e -> throwParserError $ PEWrongFormat e
70− msg = "Not readable " <> n <> ": " <> t
39+ Right a -> return a
71− n = T.pack $ show $ typeRep (Proxy :: Proxy a)
72−
73
40
74−-- | Tries to read given text to value using 'Read'. Useful to use
75−-- with 'parseContent' and 'parseAttribute'. Content is stripped
76−-- before reading.
77−readContent
78− :: (Read a, Typeable a)
79− => Text
80− -> Either Text a
81−readContent = maybeReadContent $ readMaybe . T.unpack . T.strip
82
41
83-- | Returns name of current element.
42-- | Returns name of current element.
84--
43--
@@ -118,67 +77,52 @@ getCurrentContent = do
118 | L.null conts -> Nothing
77 | L.null conts -> Nothing
119 | otherwise -> Just $ mconcat conts
78 | otherwise -> Just $ mconcat conts
120
79
121−-- | Parses content inside current tag. It expects current element set
80+-- | If reader returns 'Nothing' then resulting function returns 'Left
122−-- consists of exactly ONE element.
81+-- "error message"'. 'Typeable' is used for generating usefull error
123−parseContent
82+-- message.
124− :: (Monad m)
125− => (Text -> Either Text a)
126− -- ^ Content parser, return error msg if value is not parsed
127− -> DomParserT Identity m a
128−parseContent parse = getCurrentContent >>= \case
129− Nothing -> throwParserError PEContentNotFound
130− Just c -> case parse c of
131− Left e -> throwParserError $ PEWrongFormat e
132− Right a -> return a
133−
134−-- | Retuns map of attributes of current element
135--
83--
136-- @since 1.0.0
84-- @since 1.0.0
137−getCurrentAttributes
85+maybeReadContent
138− :: (Monad m)
86+ :: forall a
139− => DomParserT Identity m (M.Map Name Text)
87+ . (Typeable a)
140−getCurrentAttributes = view $ pdElements . to runIdentity . attrs
88+ => (Text -> Maybe a)
89+ -- ^ Content or attribute reader
90+ -> Text
91+ -- ^ Content or attribute value
92+ -> Either Text a
93+maybeReadContent f t = maybe (Left msg) Right $ f t
94+ where
95+ msg = "Not readable " <> n <> ": " <> t
96+ n = T.pack $ show $ typeRep (Proxy :: Proxy a)
141
97
142−-- | Returns element with given name or 'Nothing'
143−--
144−-- @since 1.0.0
145−getCurrentAttribute
146− :: (Monad m)
147− => NameMatcher
148− -> DomParserT Identity m (Maybe Text)
149−getCurrentAttribute attrName
150− = preview $ pdElements . to runIdentity . attrs . to M.toList
151− . traversed . filtered (views _1 (attrName ^. nmMatch)) . _2
152
98
153−-- | Parses attribute with given name, throws error if attribute is not found.
99+-- | Tries to read given text to value using 'Read'. Useful to use
154−--
100+-- with 'parseContent' and 'parseAttribute'. Content is stripped
155−-- @since 1.0.0
101+-- before reading.
156−parseAttribute
102+readContent
157− :: (Monad m)
103+ :: (Read a, Typeable a)
158− => NameMatcher
104+ => Text
159− -- ^ Attribute name
160− -> (Text -> Either Text a)
105+ -> Either Text a
161− -- ^ Attribute content parser
106+readContent = maybeReadContent $ readMaybe . T.unpack . T.strip
162− -> DomParserT Identity m a
107+
163−parseAttribute attrName parser =
164− parseAttributeMaybe attrName parser >>= \case
165− Nothing -> throwParserError $ PEAttributeNotFound attrName
166− Just a -> return a
167
108
168−-- | Parses attribute with given name. Returns Nothing if attribute is
109+-- | @since 1.0.0
169−-- not found.
110+readBool :: Text -> Either Text Bool
111+readBool t =
112+ let
113+ lowt = T.toLower $ T.strip t
114+ tvals = ["y", "yes", "t", "true", "1"]
115+ fvals = ["n", "no", "f", "false", "0"]
116+ in if
117+ | lowt `elem` tvals -> Right True
118+ | lowt `elem` fvals -> Right False
119+ | otherwise ->
120+ Left $ "Could not read " <> t <> " as Bool"
121+
122+-- | Expects text to be single character
170--
123--
171-- @since 1.0.0
124-- @since 1.0.0
172−parseAttributeMaybe
173− :: (Monad m)
174− => NameMatcher
175− -- ^ Attribute name
176− -> (Text -> Either Text a)
125+readChar :: Text -> Either Text Char
177− -- ^ Attribute content parser
126+readChar t = case T.unpack $ T.strip t of
178− -> DomParserT Identity m (Maybe a)
127+ [c] -> Right c
179−parseAttributeMaybe attrName parser =
128+ _ -> Left "Should have exactly one non-blank character"
180− getCurrentAttribute attrName >>= \case
181− Nothing -> return Nothing
182− Just aval -> case parser aval of
183− Left err -> throwParserError $ PEAttributeWrongFormat attrName err
184− Right a -> return $ Just a
src/Text/XML/DOM/Parser/Types.hs+2−2
@@ -46,6 +46,7 @@ import Text.XML.Lens
46data ElemMatcher = ElemMatcher
46data ElemMatcher = ElemMatcher
47 { _emMatch :: Element -> Bool
47 { _emMatch :: Element -> Bool
48 , _emShow :: Text
48 , _emShow :: Text
49+ -- ^ Field for 'Show' instance and bulding usefull errors
49 }
50 }
50
51
51makeLenses ''ElemMatcher
52makeLenses ''ElemMatcher
@@ -74,8 +75,7 @@ data NameMatcher = NameMatcher
74 -- function takin in account only local name or other components
75 -- function takin in account only local name or other components
75 -- of 'Name'
76 -- of 'Name'
76 , _nmShow :: Text
77 , _nmShow :: Text
77− -- ^ How to show the name matcher, since there is no 'Read'
78+ -- ^ Field for 'Show' instance and bulding usefull errors
78− -- instance we can do it any way we want
79 }
79 }
80
80
81makeLenses ''NameMatcher
81makeLenses ''NameMatcher