@@ -0,0 +1,1064 @@
1+{-# LANGUAGE Arrows #-}
2+{-# LANGUAGE TupleSections #-}
3+{-# LANGUAGE GADTs #-}
4+{-# LANGUAGE PatternGuards #-}
5+{-# LANGUAGE RecordWildCards #-}
6+
7+{-
8+Copyright (C) 2015 Martin Linnemann <theCodingMarlin@googlemail.com>
9+
10+This program is free software; you can redistribute it and/or modify
11+it under the terms of the GNU General Public License as published by
12+the Free Software Foundation; either version 2 of the License, or
13+(at your option) any later version.
14+
15+This program is distributed in the hope that it will be useful,
16+but WITHOUT ANY WARRANTY; without even the implied warranty of
17+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+GNU General Public License for more details.
19+
20+You should have received a copy of the GNU General Public License
21+along with this program; if not, write to the Free Software
22+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
23+-}
24+
25+{- |
26+ Module : Text.Pandoc.Readers.Odt.Generic.XMLConverter
27+ Copyright : Copyright (C) 2015 Martin Linnemann
28+ License : GNU GPL, version 2 or above
29+
30+ Maintainer : Martin Linnemann <theCodingMarlin@googlemail.com>
31+ Stability : alpha
32+ Portability : portable
33+
34+A generalized XML parser based on stateful arrows.
35+It might be sufficient to define this reader as a comonad, but there is
36+not a lot of use in trying.
37+-}
38+
39+module Text.Pandoc.Readers.Odt.Generic.XMLConverter
40+( ElementName
41+, XMLConverterState
42+, XMLConverter
43+, FallibleXMLConverter
44+, swapPosition
45+, runConverter
46+, runConverter''
47+, runConverter'
48+, runConverterF'
49+, runConverterF
50+, getCurrentElement
51+, getExtraState
52+, setExtraState
53+, modifyExtraState
54+, convertingExtraState
55+, producingExtraState
56+, lookupNSiri
57+, lookupNSprefix
58+, readNSattributes
59+, elemName
60+, elemNameIs
61+, strContent
62+, elContent
63+, currentElem
64+, currentElemIs
65+, expectElement
66+, elChildren
67+, findChildren
68+, filterChildren
69+, filterChildrenName
70+, findChild'
71+, findChild
72+, filterChild'
73+, filterChild
74+, filterChildName'
75+, filterChildName
76+, isSet
77+, isSet'
78+, isSetWithDefault
79+, hasAttrValueOf'
80+, failIfNotAttrValueOf
81+, isThatTheAttrValue
82+, searchAttrIn
83+, searchAttrWith
84+, searchAttr
85+, lookupAttr
86+, lookupAttr'
87+, lookupAttrWithDefault
88+, lookupDefaultingAttr
89+, findAttr'
90+, findAttr
91+, findAttrWithDefault
92+, readAttr
93+, readAttr'
94+, readAttrWithDefault
95+, getAttr
96+-- , (>/<)
97+-- , (?>/<)
98+, executeIn
99+, collectEvery
100+, withEveryL
101+, withEvery
102+, tryAll
103+, tryAll'
104+, IdXMLConverter
105+, MaybeEConverter
106+, ElementMatchConverter
107+, MaybeCConverter
108+, ContentMatchConverter
109+, makeMatcherE
110+, makeMatcherC
111+, prepareMatchersE
112+, prepareMatchersC
113+, matchChildren
114+, matchContent''
115+, matchContent'
116+, matchContent
117+) where
118+
119+import Control.Applicative hiding ( liftA, liftA2 )
120+import Control.Monad ( MonadPlus )
121+import Control.Arrow
122+
123+import qualified Data.Map as M
124+import qualified Data.Foldable as F
125+import Data.Default
126+import Data.Monoid ( Monoid )
127+import Data.Maybe
128+
129+import qualified Text.XML.Light as XML
130+
131+import Text.Pandoc.Readers.Odt.Arrows.State
132+import Text.Pandoc.Readers.Odt.Arrows.Utils
133+
134+import Text.Pandoc.Readers.Odt.Generic.Namespaces
135+import Text.Pandoc.Readers.Odt.Generic.Utils
136+import Text.Pandoc.Readers.Odt.Generic.Fallible
137+
138+--------------------------------------------------------------------------------
139+-- Basis types for readability
140+--------------------------------------------------------------------------------
141+
142+--
143+type ElementName = String
144+type AttributeName = String
145+type AttributeValue = String
146+
147+--
148+type NameSpacePrefix = String
149+
150+--
151+type NameSpacePrefixes nsID = M.Map nsID NameSpacePrefix
152+
153+--------------------------------------------------------------------------------
154+-- Main converter state
155+--------------------------------------------------------------------------------
156+
157+-- GADT so some of the NameSpaceID restrictions can be deduced
158+data XMLConverterState nsID extraState where
159+ XMLConverterState :: NameSpaceID nsID
160+ => { -- | A stack of parent elements. The top element is the current one.
161+ -- Arguably, a real Zipper would be better. But that is an
162+ -- optimization that can be made at a later time, e.g. when
163+ -- replacing Text.XML.Light.
164+ parentElements :: [XML.Element]
165+ -- | A map from internal namespace IDs to the namespace prefixes
166+ -- used in XML elements
167+ , namespacePrefixes :: NameSpacePrefixes nsID
168+ -- | A map from internal namespace IDs to namespace IRIs
169+ -- (Only necessary for matching namespace IDs and prefixes)
170+ , namespaceIRIs :: NameSpaceIRIs nsID
171+ -- | A place to put "something else". This feature is used heavily
172+ -- to keep the main code cleaner. More specifically, the main reader
173+ -- is divided into different stages. Each stage lifts something up
174+ -- here, which the next stage can then use. This could of course be
175+ -- generalized to a state-tree or used for the namespace IRIs. The
176+ -- border between states and values is an imaginary one, after all.
177+ -- But the separation as it is seems to be enough for now.
178+ , moreState :: extraState
179+ }
180+ -> XMLConverterState nsID extraState
181+
182+--
183+createStartState :: (NameSpaceID nsID)
184+ => XML.Element
185+ -> extraState
186+ -> XMLConverterState nsID extraState
187+createStartState element extraState =
188+ XMLConverterState
189+ { parentElements = [element]
190+ , namespacePrefixes = M.empty
191+ , namespaceIRIs = getInitialIRImap
192+ , moreState = extraState
193+ }
194+
195+-- | Functor over extra state
196+instance Functor (XMLConverterState nsID) where
197+ fmap f ( XMLConverterState parents prefixes iRIs extraState )
198+ = XMLConverterState parents prefixes iRIs (f extraState)
199+
200+--
201+replaceExtraState :: extraState
202+ -> XMLConverterState nsID _x
203+ -> XMLConverterState nsID extraState
204+replaceExtraState x s
205+ = fmap (const x) s
206+
207+--
208+currentElement :: XMLConverterState nsID extraState
209+ -> XML.Element
210+currentElement state = head (parentElements state)
211+
212+-- | Replace the current position by another, modifying the extra state
213+-- in the process
214+swapPosition :: (extraState -> extraState')
215+ -> [XML.Element]
216+ -> XMLConverterState nsID extraState
217+ -> XMLConverterState nsID extraState'
218+swapPosition f stack state
219+ = state { parentElements = stack
220+ , moreState = f (moreState state)
221+ }
222+
223+-- | Replace the current position by another, modifying the extra state
224+-- in the process
225+swapStack' :: XMLConverterState nsID extraState
226+ -> [XML.Element]
227+ -> ( XMLConverterState nsID extraState , [XML.Element] )
228+swapStack' state stack
229+ = ( state { parentElements = stack }
230+ , parentElements state
231+ )
232+
233+--
234+pushElement :: XML.Element
235+ -> XMLConverterState nsID extraState
236+ -> XMLConverterState nsID extraState
237+pushElement e state = state { parentElements = e:(parentElements state) }
238+
239+-- | Pop the top element from the call stack, unless it is the last one.
240+popElement :: XMLConverterState nsID extraState
241+ -> Maybe (XMLConverterState nsID extraState)
242+popElement state
243+ | _:es@(_:_) <- parentElements state = Just $ state { parentElements = es }
244+ | otherwise = Nothing
245+
246+--------------------------------------------------------------------------------
247+-- Main type
248+--------------------------------------------------------------------------------
249+
250+-- It might be a good idea to pack the converters in a GADT
251+-- Downside: data instead of type
252+-- Upside: 'Failure' could be made a parameter as well.
253+
254+--
255+type XMLConverter nsID extraState input output
256+ = ArrowState (XMLConverterState nsID extraState ) input output
257+
258+type FallibleXMLConverter nsID extraState input output
259+ = XMLConverter nsID extraState input (Fallible output)
260+
261+--
262+runConverter :: XMLConverter nsID extraState input output
263+ -> XMLConverterState nsID extraState
264+ -> input
265+ -> output
266+runConverter converter state input = snd $ runArrowState converter (state,input)
267+
268+--
269+runConverter'' :: (NameSpaceID nsID)
270+ => XMLConverter nsID extraState (Fallible ()) output
271+ -> extraState
272+ -> XML.Element
273+ -> output
274+runConverter'' converter extraState element = runConverter (readNSattributes >>> converter) (createStartState element extraState) ()
275+
276+runConverter' :: (NameSpaceID nsID)
277+ => FallibleXMLConverter nsID extraState () success
278+ -> extraState
279+ -> XML.Element
280+ -> Fallible success
281+runConverter' converter extraState element = runConverter (readNSattributes >>? converter) (createStartState element extraState) ()
282+
283+--
284+runConverterF' :: FallibleXMLConverter nsID extraState x y
285+ -> XMLConverterState nsID extraState
286+ -> Fallible x -> Fallible y
287+runConverterF' a s e = runConverter (returnV e >>? a) s e
288+
289+--
290+runConverterF :: (NameSpaceID nsID)
291+ => FallibleXMLConverter nsID extraState XML.Element x
292+ -> extraState
293+ -> Fallible XML.Element -> Fallible x
294+runConverterF a s = either failWith
295+ (\e -> runConverter a (createStartState e s) e)
296+
297+--
298+getCurrentElement :: XMLConverter nsID extraState x XML.Element
299+getCurrentElement = extractFromState currentElement
300+
301+--
302+getExtraState :: XMLConverter nsID extraState x extraState
303+getExtraState = extractFromState moreState
304+
305+--
306+setExtraState :: XMLConverter nsID extraState extraState extraState
307+setExtraState = withState $ \state extra
308+ -> (replaceExtraState extra state , extra)
309+
310+
311+-- | Lifts a function to the extra state.
312+modifyExtraState :: (extraState -> extraState)
313+ -> XMLConverter nsID extraState x x
314+modifyExtraState = modifyState.fmap
315+
316+
317+-- | First sets the extra state to the new value. Then modifies the original
318+-- extra state with a converter that uses the new state. Finally, the
319+-- intermediate state is dropped and the extra state is lifted into the
320+-- state as it was at the beginning of the function.
321+-- As a result, exactly the extra state and nothing else is changed.
322+-- The resulting converter even behaves like an identity converter on the
323+-- value level.
324+--
325+-- (The -ing form is meant to be mnemonic in a sequence of arrows as in
326+-- convertingExtraState () converter >>> doOtherStuff)
327+--
328+convertingExtraState :: extraState'
329+ -> FallibleXMLConverter nsID extraState' extraState extraState
330+ -> FallibleXMLConverter nsID extraState x x
331+convertingExtraState v a = withSubStateF setVAsExtraState modifyWithA
332+ where
333+ setVAsExtraState = liftAsSuccess $ extractFromState id >>^ replaceExtraState v
334+ modifyWithA = keepingTheValue (moreState ^>> a)
335+ >>^ spreadChoice >>?§ flip replaceExtraState
336+
337+-- | First sets the extra state to the new value. Then produces a new
338+-- extra state with a converter that uses the new state. Finally, the
339+-- intermediate state is dropped and the extra state is lifted into the
340+-- state as it was at the beginning of the function.
341+-- As a result, exactly the extra state and nothing else is changed.
342+-- The resulting converter even behaves like an identity converter on the
343+-- value level.
344+--
345+-- Aequivalent to
346+--
347+-- > \v x a -> convertingExtraState v (returnV x >>> a)
348+--
349+-- (The -ing form is meant to be mnemonic in a sequence of arrows as in
350+-- producingExtraState () () producer >>> doOtherStuff)
351+--
352+producingExtraState :: extraState'
353+ -> a
354+ -> FallibleXMLConverter nsID extraState' a extraState
355+ -> FallibleXMLConverter nsID extraState x x
356+producingExtraState v x a = convertingExtraState v (returnV x >>> a)
357+
358+
359+--------------------------------------------------------------------------------
360+-- Work in namespaces
361+--------------------------------------------------------------------------------
362+
363+-- | Arrow version of 'getIRI'
364+lookupNSiri :: (NameSpaceID nsID)
365+ => nsID
366+ -> XMLConverter nsID extraState x (Maybe NameSpaceIRI)
367+lookupNSiri nsID = extractFromState
368+ $ \state -> getIRI nsID $ namespaceIRIs state
369+
370+--
371+lookupNSprefix :: (NameSpaceID nsID)
372+ => nsID
373+ -> XMLConverter nsID extraState x (Maybe NameSpacePrefix)
374+lookupNSprefix nsID = extractFromState
375+ $ \state -> M.lookup nsID $ namespacePrefixes state
376+
377+-- | Extracts namespace attributes from the current element and tries to
378+-- update the current mapping accordingly
379+readNSattributes :: (NameSpaceID nsID)
380+ => FallibleXMLConverter nsID extraState x ()
381+readNSattributes = fromState $ \state -> maybe (state, failEmpty )
382+ ( , succeedWith ())
383+ (extractNSAttrs state )
384+ where
385+ extractNSAttrs :: (NameSpaceID nsID)
386+ => XMLConverterState nsID extraState
387+ -> Maybe (XMLConverterState nsID extraState)
388+ extractNSAttrs startState
389+ = foldl (\state d -> state >>= addNS d)
390+ (Just startState)
391+ nsAttribs
392+ where nsAttribs = mapMaybe readNSattr (XML.elAttribs element)
393+ element = currentElement startState
394+ readNSattr (XML.Attr (XML.QName name _ (Just "xmlns")) iri)
395+ = Just (name, iri)
396+ readNSattr _ = Nothing
397+ addNS (prefix, iri) state = fmap updateState
398+ $ getNamespaceID iri
399+ $ namespaceIRIs state
400+ where updateState (iris,nsID)
401+ = state { namespaceIRIs = iris
402+ , namespacePrefixes = M.insert nsID prefix
403+ $ namespacePrefixes state
404+ }
405+
406+--------------------------------------------------------------------------------
407+-- Common namespace accessors
408+--------------------------------------------------------------------------------
409+
410+-- | Given a namespace id and an element name, creates a 'XML.QName' for
411+-- internal use
412+elemName :: (NameSpaceID nsID)
413+ => nsID -> ElementName
414+ -> XMLConverter nsID extraState x XML.QName
415+elemName nsID name = lookupNSiri nsID
416+ &&& lookupNSprefix nsID
417+ >>§ XML.QName name
418+
419+-- | Checks if a given element matches both a specified namespace id
420+-- and a specified element name
421+elemNameIs :: (NameSpaceID nsID)
422+ => nsID -> ElementName
423+ -> XMLConverter nsID extraState XML.Element Bool
424+elemNameIs nsID name = keepingTheValue (lookupNSiri nsID) >>§ hasThatName
425+ where hasThatName e iri = let elName = XML.elName e
426+ in XML.qName elName == name
427+ && XML.qURI elName == iri
428+
429+--------------------------------------------------------------------------------
430+-- General content
431+--------------------------------------------------------------------------------
432+
433+--
434+strContent :: XMLConverter nsID extraState x String
435+strContent = getCurrentElement
436+ >>^ XML.strContent
437+
438+--
439+elContent :: XMLConverter nsID extraState x [XML.Content]
440+elContent = getCurrentElement
441+ >>^ XML.elContent
442+
443+--------------------------------------------------------------------------------
444+-- Current element
445+--------------------------------------------------------------------------------
446+
447+--
448+currentElem :: XMLConverter nsID extraState x (XML.QName)
449+currentElem = getCurrentElement
450+ >>^ XML.elName
451+
452+currentElemIs :: (NameSpaceID nsID)
453+ => nsID -> ElementName
454+ -> XMLConverter nsID extraState x Bool
455+currentElemIs nsID name = getCurrentElement
456+ >>> elemNameIs nsID name
457+
458+
459+
460+{-
461+currentElemIs'' nsID name = ( (getCurrentElement >>^ XML.elName >>>
462+ (XML.qName >>^ (&&).(== name) )
463+ ^&&&^
464+ (XML.qIRI >>^ (==) )
465+ ) >>§ (.)
466+ ) &&& lookupNSiri nsID >>§ ($)
467+-}
468+
469+--
470+expectElement :: (NameSpaceID nsID)
471+ => nsID -> ElementName
472+ -> FallibleXMLConverter nsID extraState x ()
473+expectElement nsID name = currentElemIs nsID name
474+ >>^ boolToChoice
475+
476+--------------------------------------------------------------------------------
477+-- Chilren
478+--------------------------------------------------------------------------------
479+
480+--
481+elChildren :: XMLConverter nsID extraState x [XML.Element]
482+elChildren = getCurrentElement
483+ >>^ XML.elChildren
484+
485+--
486+findChildren :: (NameSpaceID nsID)
487+ => nsID -> ElementName
488+ -> XMLConverter nsID extraState x [XML.Element]
489+findChildren nsID name = elemName nsID name
490+ &&& getCurrentElement
491+ >>§ XML.findChildren
492+
493+--
494+filterChildren :: (XML.Element -> Bool)
495+ -> XMLConverter nsID extraState x [XML.Element]
496+filterChildren p = getCurrentElement
497+ >>^ XML.filterChildren p
498+
499+--
500+filterChildrenName :: (XML.QName -> Bool)
501+ -> XMLConverter nsID extraState x [XML.Element]
502+filterChildrenName p = getCurrentElement
503+ >>^ XML.filterChildrenName p
504+
505+--
506+findChild' :: (NameSpaceID nsID)
507+ => nsID
508+ -> ElementName
509+ -> XMLConverter nsID extraState x (Maybe XML.Element)
510+findChild' nsID name = elemName nsID name
511+ &&& getCurrentElement
512+ >>§ XML.findChild
513+
514+--
515+findChild :: (NameSpaceID nsID)
516+ => nsID -> ElementName
517+ -> FallibleXMLConverter nsID extraState x XML.Element
518+findChild nsID name = findChild' nsID name
519+ >>> maybeToChoice
520+
521+--
522+filterChild' :: (XML.Element -> Bool)
523+ -> XMLConverter nsID extraState x (Maybe XML.Element)
524+filterChild' p = getCurrentElement
525+ >>^ XML.filterChild p
526+
527+--
528+filterChild :: (XML.Element -> Bool)
529+ -> FallibleXMLConverter nsID extraState x XML.Element
530+filterChild p = filterChild' p
531+ >>> maybeToChoice
532+
533+--
534+filterChildName' :: (XML.QName -> Bool)
535+ -> XMLConverter nsID extraState x (Maybe XML.Element)
536+filterChildName' p = getCurrentElement
537+ >>^ XML.filterChildName p
538+
539+--
540+filterChildName :: (XML.QName -> Bool)
541+ -> FallibleXMLConverter nsID extraState x XML.Element
542+filterChildName p = filterChildName' p
543+ >>> maybeToChoice
544+
545+
546+--------------------------------------------------------------------------------
547+-- Attributes
548+--------------------------------------------------------------------------------
549+
550+--
551+isSet :: (NameSpaceID nsID)
552+ => nsID -> AttributeName
553+ -> (Either Failure Bool)
554+ -> FallibleXMLConverter nsID extraState x Bool
555+isSet nsID attrName deflt
556+ = findAttr' nsID attrName
557+ >>^ maybe deflt stringToBool
558+
559+--
560+isSet' :: (NameSpaceID nsID)
561+ => nsID -> AttributeName
562+ -> XMLConverter nsID extraState x (Maybe Bool)
563+isSet' nsID attrName = findAttr' nsID attrName
564+ >>^ (>>= stringToBool')
565+
566+isSetWithDefault :: (NameSpaceID nsID)
567+ => nsID -> AttributeName
568+ -> Bool
569+ -> XMLConverter nsID extraState x Bool
570+isSetWithDefault nsID attrName def'
571+ = isSet' nsID attrName
572+ >>^ fromMaybe def'
573+
574+--
575+hasAttrValueOf' :: (NameSpaceID nsID)
576+ => nsID -> AttributeName
577+ -> AttributeValue
578+ -> XMLConverter nsID extraState x Bool
579+hasAttrValueOf' nsID attrName attrValue
580+ = findAttr nsID attrName
581+ >>> ( const False ^|||^ (==attrValue))
582+
583+--
584+failIfNotAttrValueOf :: (NameSpaceID nsID)
585+ => nsID -> AttributeName
586+ -> AttributeValue
587+ -> FallibleXMLConverter nsID extraState x ()
588+failIfNotAttrValueOf nsID attrName attrValue
589+ = hasAttrValueOf' nsID attrName attrValue
590+ >>^ boolToChoice
591+
592+-- | Is the value that is currently transported in the arrow the value of
593+-- the specified attribute?
594+isThatTheAttrValue :: (NameSpaceID nsID)
595+ => nsID -> AttributeName
596+ -> FallibleXMLConverter nsID extraState AttributeValue Bool
597+isThatTheAttrValue nsID attrName
598+ = keepingTheValue
599+ (findAttr nsID attrName)
600+ >>§ right.(==)
601+
602+-- | Lookup value in a dictionary, fail if no attribute found or value
603+-- not in dictionary
604+searchAttrIn :: (NameSpaceID nsID)
605+ => nsID -> AttributeName
606+ -> [(AttributeValue,a)]
607+ -> FallibleXMLConverter nsID extraState x a
608+searchAttrIn nsID attrName dict
609+ = findAttr nsID attrName
610+ >>?^? maybeToChoice.(`lookup` dict )
611+
612+
613+-- | Lookup value in a dictionary. Fail if no attribute found. If value not in
614+-- dictionary, return default value
615+searchAttrWith :: (NameSpaceID nsID)
616+ => nsID -> AttributeName
617+ -> a
618+ -> [(AttributeValue,a)]
619+ -> FallibleXMLConverter nsID extraState x a
620+searchAttrWith nsID attrName defV dict
621+ = findAttr nsID attrName
622+ >>?^ (fromMaybe defV).(`lookup` dict )
623+
624+-- | Lookup value in a dictionary. If attribute or value not found,
625+-- return default value
626+searchAttr :: (NameSpaceID nsID)
627+ => nsID -> AttributeName
628+ -> a
629+ -> [(AttributeValue,a)]
630+ -> XMLConverter nsID extraState x a
631+searchAttr nsID attrName defV dict
632+ = searchAttrIn nsID attrName dict
633+ >>> const defV ^|||^ id
634+
635+-- | Read a 'Lookupable' attribute. Fail if no match.
636+lookupAttr :: (NameSpaceID nsID, Lookupable a)
637+ => nsID -> AttributeName
638+ -> FallibleXMLConverter nsID extraState x a
639+lookupAttr nsID attrName = lookupAttr' nsID attrName
640+ >>^ maybeToChoice
641+
642+
643+-- | Read a 'Lookupable' attribute. Return the result as a 'Maybe'.
644+lookupAttr' :: (NameSpaceID nsID, Lookupable a)
645+ => nsID -> AttributeName
646+ -> XMLConverter nsID extraState x (Maybe a)
647+lookupAttr' nsID attrName
648+ = findAttr' nsID attrName
649+ >>^ (>>= readLookupable)
650+
651+-- | Read a 'Lookupable' attribute with explicit default
652+lookupAttrWithDefault :: (NameSpaceID nsID, Lookupable a)
653+ => nsID -> AttributeName
654+ -> a
655+ -> XMLConverter nsID extraState x a
656+lookupAttrWithDefault nsID attrName deflt
657+ = lookupAttr' nsID attrName
658+ >>^ fromMaybe deflt
659+
660+-- | Read a 'Lookupable' attribute with implicit default
661+lookupDefaultingAttr :: (NameSpaceID nsID, Lookupable a, Default a)
662+ => nsID -> AttributeName
663+ -> XMLConverter nsID extraState x a
664+lookupDefaultingAttr nsID attrName
665+ = lookupAttrWithDefault nsID attrName def
666+
667+-- | Return value as a (Maybe String)
668+findAttr' :: (NameSpaceID nsID)
669+ => nsID -> AttributeName
670+ -> XMLConverter nsID extraState x (Maybe AttributeValue)
671+findAttr' nsID attrName = elemName nsID attrName
672+ &&& getCurrentElement
673+ >>§ XML.findAttr
674+
675+-- | Return value as string or fail
676+findAttr :: (NameSpaceID nsID)
677+ => nsID -> AttributeName
678+ -> FallibleXMLConverter nsID extraState x AttributeValue
679+findAttr nsID attrName = findAttr' nsID attrName
680+ >>> maybeToChoice
681+
682+-- | Return value as string or return provided default value
683+findAttrWithDefault :: (NameSpaceID nsID)
684+ => nsID -> AttributeName
685+ -> AttributeValue
686+ -> XMLConverter nsID extraState x AttributeValue
687+findAttrWithDefault nsID attrName deflt
688+ = findAttr' nsID attrName
689+ >>^ fromMaybe deflt
690+
691+-- | Read and return value or fail
692+readAttr :: (NameSpaceID nsID, Read attrValue)
693+ => nsID -> AttributeName
694+ -> FallibleXMLConverter nsID extraState x attrValue
695+readAttr nsID attrName = readAttr' nsID attrName
696+ >>> maybeToChoice
697+
698+-- | Read and return value or return Nothing
699+readAttr' :: (NameSpaceID nsID, Read attrValue)
700+ => nsID -> AttributeName
701+ -> XMLConverter nsID extraState x (Maybe attrValue)
702+readAttr' nsID attrName = findAttr' nsID attrName
703+ >>^ (>>= tryToRead)
704+
705+-- | Read and return value or return provided default value
706+readAttrWithDefault :: (NameSpaceID nsID, Read attrValue)
707+ => nsID -> AttributeName
708+ -> attrValue
709+ -> XMLConverter nsID extraState x attrValue
710+readAttrWithDefault nsID attrName deflt
711+ = findAttr' nsID attrName
712+ >>^ (>>= tryToRead)
713+ >>^ fromMaybe deflt
714+
715+-- | Read and return value or return default value from 'Default' instance
716+getAttr :: (NameSpaceID nsID, Read attrValue, Default attrValue)
717+ => nsID -> AttributeName
718+ -> XMLConverter nsID extraState x attrValue
719+getAttr nsID attrName = readAttrWithDefault nsID attrName def
720+
721+--------------------------------------------------------------------------------
722+-- Movements
723+--------------------------------------------------------------------------------
724+
725+--
726+jumpThere :: XMLConverter nsID extraState XML.Element XML.Element
727+jumpThere = withState (\state element
728+ -> ( pushElement element state , element )
729+ )
730+
731+--
732+swapStack :: XMLConverter nsID extraState [XML.Element] [XML.Element]
733+swapStack = withState swapStack'
734+
735+--
736+jumpBack :: FallibleXMLConverter nsID extraState _x _x
737+jumpBack = tryModifyState (popElement >>> maybeToChoice)
738+
739+-- | Support function for "procedural" converters: jump to an element, execute
740+-- a converter, jump back.
741+-- This version is safer than 'executeThere', because it does not rely on the
742+-- internal stack. As a result, the converter can not move around in arbitrary
743+-- ways. The downside is of course that some of the environment is not
744+-- accessible to the converter.
745+switchingTheStack :: XMLConverter nsID moreState a b
746+ -> XMLConverter nsID moreState (a, XML.Element) b
747+switchingTheStack a = second ( (:[]) ^>> swapStack )
748+ >>> first a
749+ >>> second swapStack
750+ >>^ fst
751+
752+-- | Support function for "procedural" converters: jumps to an element, executes
753+-- a converter, jumps back.
754+-- Make sure that the converter is well-behaved; that is it should
755+-- return to the exact position it started from in /every possible path/ of
756+-- execution, even if it "fails". If it does not, you may encounter
757+-- strange bugs. If you are not sure about the behaviour or want to use
758+-- shortcuts, you can often use 'switchingTheStack' instead.
759+executeThere :: FallibleXMLConverter nsID moreState a b
760+ -> FallibleXMLConverter nsID moreState (a, XML.Element) b
761+executeThere a = second jumpThere
762+ >>> fst
763+ ^>> a
764+ >>> jumpBack -- >>? jumpBack would not ensure the jump.
765+ >>^ collapseEither
766+
767+-- | Do something in a sub-element, tnen come back
768+executeIn :: (NameSpaceID nsID)
769+ => nsID -> ElementName
770+ -> FallibleXMLConverter nsID extraState f s
771+ -> FallibleXMLConverter nsID extraState f s
772+executeIn nsID name a = keepingTheValue
773+ (findChild nsID name)
774+ >>> ignoringState liftFailure
775+ >>? switchingTheStack a
776+ where liftFailure (_, (Left f)) = Left f
777+ liftFailure (x, (Right e)) = Right (x, e)
778+
779+--------------------------------------------------------------------------------
780+-- Iterating over children
781+--------------------------------------------------------------------------------
782+
783+-- Helper converter to prepare different types of iterations.
784+-- It lifts the children (of a certain type) of the current element
785+-- into the value level and pairs each one with the current input value.
786+prepareIteration :: (NameSpaceID nsID)
787+ => nsID -> ElementName
788+ -> XMLConverter nsID extraState b [(b, XML.Element)]
789+prepareIteration nsID name = keepingTheValue
790+ (findChildren nsID name)
791+ >>§ distributeValue
792+
793+-- | Applies a converter to every child element of a specific type.
794+-- Collects results in a 'Monoid'.
795+-- Fails completely if any conversion fails.
796+collectEvery :: (NameSpaceID nsID, Monoid m)
797+ => nsID -> ElementName
798+ -> FallibleXMLConverter nsID extraState a m
799+ -> FallibleXMLConverter nsID extraState a m
800+collectEvery nsID name a = prepareIteration nsID name
801+ >>> foldS' (switchingTheStack a)
802+
803+--
804+withEveryL :: (NameSpaceID nsID)
805+ => nsID -> ElementName
806+ -> FallibleXMLConverter nsID extraState a b
807+ -> FallibleXMLConverter nsID extraState a [b]
808+withEveryL = withEvery
809+
810+-- | Applies a converter to every child element of a specific type.
811+-- Collects results in a 'MonadPlus'.
812+-- Fails completely if any conversion fails.
813+withEvery :: (NameSpaceID nsID, MonadPlus m)
814+ => nsID -> ElementName
815+ -> FallibleXMLConverter nsID extraState a b
816+ -> FallibleXMLConverter nsID extraState a (m b)
817+withEvery nsID name a = prepareIteration nsID name
818+ >>> iterateS' (switchingTheStack a)
819+
820+-- | Applies a converter to every child element of a specific type.
821+-- Collects all successful results in a list.
822+tryAll :: (NameSpaceID nsID)
823+ => nsID -> ElementName
824+ -> FallibleXMLConverter nsID extraState b a
825+ -> XMLConverter nsID extraState b [a]
826+tryAll nsID name a = prepareIteration nsID name
827+ >>> iterateS (switchingTheStack a)
828+ >>^ collectRights
829+
830+-- | Applies a converter to every child element of a specific type.
831+-- Collects all successful results.
832+tryAll' :: (NameSpaceID nsID, F.Foldable c, MonadPlus c)
833+ => nsID -> ElementName
834+ -> FallibleXMLConverter nsID extraState b a
835+ -> XMLConverter nsID extraState b (c a)
836+tryAll' nsID name a = prepareIteration nsID name
837+ >>> iterateS (switchingTheStack a)
838+ >>^ collectRightsF
839+
840+--------------------------------------------------------------------------------
841+-- Matching children
842+--------------------------------------------------------------------------------
843+
844+type IdXMLConverter nsID moreState x
845+ = XMLConverter nsID moreState x x
846+
847+type MaybeEConverter nsID moreState x
848+ = Maybe (IdXMLConverter nsID moreState (x, XML.Element))
849+
850+-- Chainable converter that helps deciding which converter to actually use.
851+type ElementMatchConverter nsID extraState x
852+ = IdXMLConverter nsID
853+ extraState
854+ (MaybeEConverter nsID extraState x, XML.Element)
855+
856+type MaybeCConverter nsID moreState x
857+ = Maybe (IdXMLConverter nsID moreState (x, XML.Content))
858+
859+-- Chainable converter that helps deciding which converter to actually use.
860+type ContentMatchConverter nsID extraState x
861+ = IdXMLConverter nsID
862+ extraState
863+ (MaybeCConverter nsID extraState x, XML.Content)
864+
865+-- Helper function: The @c@ is actually a converter that is to be selected by
866+-- matching XML elements to the first two parameters.
867+-- The fold used to match elements however is very simple, so to use it,
868+-- this function wraps the converter in another converter that unifies
869+-- the accumulator. Think of a lot of converters with the resulting type
870+-- chained together. The accumulator not only transports the element
871+-- unchanged to the next matcher, it also does the actual selecting by
872+-- combining the intermediate results with '(<|>)'.
873+makeMatcherE :: (NameSpaceID nsID)
874+ => nsID -> ElementName
875+ -> FallibleXMLConverter nsID extraState a a
876+ -> ElementMatchConverter nsID extraState a
877+makeMatcherE nsID name c = ( second (
878+ elemNameIs nsID name
879+ >>^ bool Nothing (Just tryC)
880+ )
881+ >>§ (<|>)
882+ ) &&&^ snd
883+ where tryC = (fst ^&&& executeThere c >>§ recover) &&&^ snd
884+
885+-- Helper function: The @c@ is actually a converter that is to be selected by
886+-- matching XML content to the first two parameters.
887+-- The fold used to match elements however is very simple, so to use it,
888+-- this function wraps the converter in another converter that unifies
889+-- the accumulator. Think of a lot of converters with the resulting type
890+-- chained together. The accumulator not only transports the element
891+-- unchanged to the next matcher, it also does the actual selecting by
892+-- combining the intermediate results with '(<|>)'.
893+makeMatcherC :: (NameSpaceID nsID)
894+ => nsID -> ElementName
895+ -> FallibleXMLConverter nsID extraState a a
896+ -> ContentMatchConverter nsID extraState a
897+makeMatcherC nsID name c = ( second ( contentToElem
898+ >>> returnV Nothing
899+ ||| ( elemNameIs nsID name
900+ >>^ bool Nothing (Just cWithJump)
901+ )
902+ )
903+ >>§ (<|>)
904+ ) &&&^ snd
905+ where cWithJump = ( fst
906+ ^&&& ( second contentToElem
907+ >>> spreadChoice
908+ ^>>? executeThere c
909+ )
910+ >>§ recover)
911+ &&&^ snd
912+ contentToElem :: FallibleXMLConverter nsID extraState XML.Content XML.Element
913+ contentToElem = arr $ \e -> case e of
914+ XML.Elem e' -> succeedWith e'
915+ _ -> failEmpty
916+
917+-- Creates and chains a bunch of matchers
918+prepareMatchersE :: (NameSpaceID nsID)
919+ => [(nsID, ElementName, FallibleXMLConverter nsID extraState x x)]
920+ -> ElementMatchConverter nsID extraState x
921+--prepareMatchersE = foldSs . (map $ uncurry3 makeMatcherE)
922+prepareMatchersE = reverseComposition . (map $ uncurry3 makeMatcherE)
923+
924+-- Creates and chains a bunch of matchers
925+prepareMatchersC :: (NameSpaceID nsID)
926+ => [(nsID, ElementName, FallibleXMLConverter nsID extraState x x)]
927+ -> ContentMatchConverter nsID extraState x
928+--prepareMatchersC = foldSs . (map $ uncurry3 makeMatcherC)
929+prepareMatchersC = reverseComposition . (map $ uncurry3 makeMatcherC)
930+
931+-- | Takes a list of element-data - converter groups and
932+-- * Finds all children of the current element
933+-- * Matches each group to each child in order (at most one group per child)
934+-- * Filters non-matched children
935+-- * Chains all found converters in child-order
936+-- * Applies the chain to the input element
937+matchChildren :: (NameSpaceID nsID)
938+ => [(nsID, ElementName, FallibleXMLConverter nsID extraState a a)]
939+ -> XMLConverter nsID extraState a a
940+matchChildren lookups = let matcher = prepareMatchersE lookups
941+ in keepingTheValue (
942+ elChildren
943+ >>> map (Nothing,)
944+ ^>> iterateSL matcher
945+ >>^ catMaybes.map (\(m,e) -> fmap (swallowElem e) m)
946+ -- >>> foldSs
947+ >>> reverseComposition
948+ )
949+ >>> swap
950+ ^>> app
951+ where
952+ -- let the converter swallow the element and drop the element
953+ -- in the return value
954+ swallowElem element converter = (,element) ^>> converter >>^ fst
955+
956+--
957+matchContent'' :: (NameSpaceID nsID)
958+ => [(nsID, ElementName, FallibleXMLConverter nsID extraState a a)]
959+ -> XMLConverter nsID extraState a a
960+matchContent'' lookups = let matcher = prepareMatchersC lookups
961+ in keepingTheValue (
962+ elContent
963+ >>> map (Nothing,)
964+ ^>> iterateSL matcher
965+ >>^ catMaybes.map (\(m,c) -> fmap (swallowContent c) m)
966+ -- >>> foldSs
967+ >>> reverseComposition
968+ )
969+ >>> swap
970+ ^>> app
971+ where
972+ -- let the converter swallow the content and drop the content
973+ -- in the return value
974+ swallowContent content converter = (,content) ^>> converter >>^ fst
975+
976+
977+-- | Takes a list of element-data - converter groups and
978+-- * Finds all content of the current element
979+-- * Matches each group to each piece of content in order
980+-- (at most one group per piece of content)
981+-- * Filters non-matched content
982+-- * Chains all found converters in content-order
983+-- * Applies the chain to the input element
984+matchContent' :: (NameSpaceID nsID)
985+ => [(nsID, ElementName, FallibleXMLConverter nsID extraState a a)]
986+ -> XMLConverter nsID extraState a a
987+matchContent' lookups = matchContent lookups (arr fst)
988+
989+-- | Takes a list of element-data - converter groups and
990+-- * Finds all content of the current element
991+-- * Matches each group to each piece of content in order
992+-- (at most one group per piece of content)
993+-- * Adds a default converter for all non-matched content
994+-- * Chains all found converters in content-order
995+-- * Applies the chain to the input element
996+matchContent :: (NameSpaceID nsID)
997+ => [(nsID, ElementName, FallibleXMLConverter nsID extraState a a)]
998+ -> XMLConverter nsID extraState (a,XML.Content) a
999+ -> XMLConverter nsID extraState a a
1000+matchContent lookups fallback
1001+ = let matcher = prepareMatchersC lookups
1002+ in keepingTheValue (
1003+ elContent
1004+ >>> map (Nothing,)
1005+ ^>> iterateSL matcher
1006+ >>^ map swallowOrFallback
1007+ -- >>> foldSs
1008+ >>> reverseComposition
1009+ )
1010+ >>> swap
1011+ ^>> app
1012+ where
1013+ -- let the converter swallow the content and drop the content
1014+ -- in the return value
1015+ swallowOrFallback (Just converter,content) = (,content) ^>> converter >>^ fst
1016+ swallowOrFallback (Nothing ,content) = (,content) ^>> fallback
1017+
1018+--------------------------------------------------------------------------------
1019+-- Internals
1020+--------------------------------------------------------------------------------
1021+
1022+stringToBool :: (Monoid failure) => String -> Either failure Bool
1023+stringToBool val -- stringToBool' val >>> maybeToChoice
1024+ | val `elem` trueValues = succeedWith True
1025+ | val `elem` falseValues = succeedWith False
1026+ | otherwise = failEmpty
1027+ where trueValues = ["true" ,"on" ,"1"]
1028+ falseValues = ["false","off","0"]
1029+
1030+stringToBool' :: String -> Maybe Bool
1031+stringToBool' val | val `elem` trueValues = Just True
1032+ | val `elem` falseValues = Just False
1033+ | otherwise = Nothing
1034+ where trueValues = ["true" ,"on" ,"1"]
1035+ falseValues = ["false","off","0"]
1036+
1037+
1038+distributeValue :: a -> [b] -> [(a,b)]
1039+distributeValue = map.(,)
1040+
1041+--------------------------------------------------------------------------------
1042+
1043+{-
1044+NOTES
1045+It might be a good idea to refactor the namespace stuff.
1046+E.g.: if a namespace constructor took a string as a parameter, things like
1047+> a ?>/< (NsText,"body")
1048+would be nicer.
1049+Together with a rename and some trickery, something like
1050+> |< NsText "body" >< NsText "p" ?> a </> </>|
1051+might even be possible.
1052+
1053+Some day, XML.Light should be replaced by something better.
1054+While doing that, it might be useful to replace String as the type of element
1055+names with something else, too. (Of course with OverloadedStrings).
1056+While doing that, maybe the types can be created in a way that something like
1057+> NsText:"body"
1058+could be used. Overloading (:) does not sounds like the best idea, but if the
1059+element name type was a list, this might be possible.
1060+Of course that would be a bit hackish, so the "right" way would probably be
1061+something like
1062+> InNS NsText "body"
1063+but isn't that a bit boring? ;)
1064+-}