{-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE FlexibleContexts #-} -- | Backtracking combinators for consuming XML productions (elements, attributes). module Fadno.Xml.XParser {-# DEPRECATED "in favor of XParse" #-} ( -- * XParser monad XParser ,parseX -- * Stack manipulation ,peek,push,pop,checkStack -- * Element operations ,atEl,findChild,findChildren,anyChildren,oneChild,allChildren,manyOrdered -- * Attribute/Text operations ,attr,textContent -- * QNames ,name,xsName -- * Utility ,readXml ) where import qualified Text.XML.Light as X import Control.Exception import Control.Monad.State.Strict hiding (sequence) import Control.Monad.Except hiding (sequence) import Data.Either import Control.Applicative import Prelude hiding (sequence) import Control.Lens -- Element lenses lAttrs :: Lens' X.Element [X.Attr] lAttrs f s = fmap (\a -> s { X.elAttribs = a }) (f $ X.elAttribs s) lContent :: Lens' X.Element [X.Content] lContent f s = fmap (\a -> s { X.elContent = a }) (f $ X.elContent s) _Elem :: Prism' X.Content X.Element _Elem = prism X.Elem $ \c -> case c of X.Elem e -> Right e; _ -> Left c -- | Stack entry tracking identified elements. -- | XParser constraint kind. Stack state + alternative + errors. type XParser m = (Alternative m, MonadState [X.Element] m, MonadError String m) -- | run XParser on an element. parseX :: (Monad m) => StateT [X.Element] (ExceptT String m) b -> X.Element -> m (Either String b) parseX sel e = runExceptT (evalStateT sel [e]) -- | Stack peek. peek :: XParser m => m X.Element peek = head <$> checkStack -- | Verify populated stack. checkStack :: XParser m => m [X.Element] checkStack = get >>= \s -> if null s then throwError "Invalid stack" else return s -- | Stack push. push :: XParser m => X.Element -> m () push e = modify (e:) -- | Stack pop. pop :: XParser m => m () pop = checkStack >>= \(_:rest) -> put rest -- | Expect/consume a particular attribute. attr :: XParser m => X.QName -> m String attr n = do as <- view lAttrs <$> peek let (as',found) = foldl test ([],Nothing) as test (rs,f@(Just _)) a = (a:rs,f) test (rs,_) a | X.attrKey a == n = (rs,Just (X.attrVal a)) | otherwise = (a:rs,Nothing) case found of Nothing -> throwError $ "Attribute not found: " ++ show n Just t -> do _head.lAttrs .= as' return t -- | Get text content, returning empty string if none, per 'strContent'. textContent :: XParser m => m String textContent = X.strContent <$> peek -- | Verify and "consume" current element. atEl :: XParser m => X.QName -> m () atEl n = do e <- X.elName <$> peek when (n /= e) $ throwError ("Wrong element name: " ++ show e) -- | Find child element and act on it. findChild :: XParser m => X.QName -> m a -> m a findChild n act = do c <- onChildren ((==n) . X.elName) True True act case c of [] -> throwError $ "No such child " ++ show n [e] -> return e _ -> throwError $ "findChild: multiple results: " ++ show n -- | Expect to find one child only, and run action on it. oneChild :: XParser m => m a -> m a oneChild act = do cs <- onChildren (const True) True True act case cs of [c] -> return c _ -> throwError $ "oneChild: found " ++ show (length cs) -- | Find zero or many children and act on them. findChildren :: XParser m => X.QName -> m a -> m [a] findChildren n = onChildren ((==n) . X.elName) False False -- | Act on all children. allChildren :: XParser m => m a -> m [a] allChildren = onChildren (const True) False False -- | Act on, consume children. -- Accepts filter, optional flag, "just1" flag. onChildren :: XParser m => (X.Element -> Bool) -> Bool -> Bool -> m a -> m [a] onChildren filt opt just1 act = do h <- peek let exec rs c@(X.Elem e) | filt e = if just1 && not (null (view _2 rs)) then return $ over _1 (c:) rs else do push e r <- catchError (Right <$> act) (return . Left) pop case r of Left err | opt -> return $ over _1 (c:) rs | otherwise -> return $ over _3 (err:) rs Right v -> return $ over _2 (v:) rs | otherwise = return $ over _1 (c:) rs exec rs c = return $ over _1 (c:) rs (cs',rs,fs) <- foldM exec ([],[],[]) (view lContent h) unless (null fs) $ throwError $ "Failure: " ++ show fs _head.lContent .= reverse cs' return (reverse rs) -- | Flailing attempt to restore "order" by faking a single-child element one at a time. manyOrdered :: XParser m => m a -> m [a] manyOrdered act = do cs <- view lContent <$> peek let fake c = X.Element (name "fake") [] [c] Nothing exec rs c = do push (fake c) r <- catchError (Right <$> act) (return . Left) pop case r of Left _ -> return (over _2 (c:) rs) Right a -> return (over _1 (a:) rs) (as,cs') <- foldM exec ([],[]) cs _head.lContent .= reverse cs' return $ reverse as -- | Run optional action on all children. anyChildren :: XParser m => m a -> m [a] anyChildren = onChildren (const True) True False -- | Special support for XSD QNames. xsName :: String -> X.QName xsName n = X.QName n (Just "http://www.w3.org/2001/XMLSchema") (Just "xs") -- | Local-only QName. name :: String -> X.QName name n = X.QName n Nothing Nothing -- | Convenience to read in top element from file. readXml :: FilePath -> IO X.Element readXml f = maybe (throwIO $ userError "parse failed") return =<< X.parseXMLDoc <$> readFile f