1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236
|
-- | The Parser monad.
module Data.GI.GIR.Parser
( Parser
, ParseContext(..)
, ParseError
, parseError
, runParser
, parseName
, parseDeprecation
, parseDocumentation
, parseIntegral
, parseBool
, parseChildrenWithLocalName
, parseAllChildrenWithLocalName
, parseChildrenWithNSName
, getAttr
, getAttrWithNamespace
, queryAttr
, queryAttrWithNamespace
, optionalAttr
, currentNamespace
, qualifyName
, resolveQualifiedTypeName
-- Reexported for convenience
, Name(..)
, Element
, GIRXMLNamespace(..)
, DeprecationInfo
, Documentation
) where
import Control.Monad.Except
import Control.Monad.Reader
#if !MIN_VERSION_base(4,11,0)
import Data.Monoid ((<>))
#endif
import qualified Data.Map as M
import qualified Data.Text as T
import qualified Data.Text.Read as TR
import Data.Text (Text)
import qualified Text.XML as XML
import Text.XML (Element(elementAttributes))
import Text.Show.Pretty (ppShow)
import Data.GI.GIR.BasicTypes (Name(..), Alias(..), Type(TInterface))
import Data.GI.GIR.Deprecation (DeprecationInfo, queryDeprecated)
import Data.GI.GIR.Documentation (Documentation, queryDocumentation)
import Data.GI.GIR.XMLUtils (localName, GIRXMLNamespace(..),
childElemsWithLocalName, childElemsWithNSName,
lookupAttr, lookupAttrWithNamespace)
-- | Info to carry around when parsing.
data ParseContext = ParseContext {
ctxNamespace :: Text,
-- Location in the XML tree of the node being parsed (for
-- debugging purposes).
treePosition :: [Text],
-- Current element being parsed (to be set by withElement)
currentElement :: Element,
knownAliases :: M.Map Alias Type
} deriving Show
-- | A message describing a parsing error in human readable form.
type ParseError = Text
-- | Monad where parsers live: we carry a context around, and can
-- throw errors that abort the parsing.
type Parser a = ReaderT ParseContext (Except ParseError) a
-- | Throw a parse error.
parseError :: ParseError -> Parser a
parseError msg = do
ctx <- ask
let position = (T.intercalate " / " . reverse . treePosition) ctx
throwError $ "Error when parsing \"" <> position <> "\": " <> msg <> "\n"
<> (T.pack . ppShow . currentElement) ctx
-- | Build a textual description (for debug purposes) of a given element.
elementDescription :: Element -> Text
elementDescription element =
case M.lookup "name" (elementAttributes element) of
Nothing -> localName element
Just n -> localName element <> " [" <> n <> "]"
-- | Build a name in the current namespace.
nameInCurrentNS :: Text -> Parser Name
nameInCurrentNS n = do
ctx <- ask
return $ Name (ctxNamespace ctx) n
-- | Return the current namespace.
currentNamespace :: Parser Text
currentNamespace = ctxNamespace <$> ask
-- | Check whether there is an alias for the given name, and return
-- the corresponding type in case it exists, and otherwise a TInterface.
resolveQualifiedTypeName :: Name -> Parser Type
resolveQualifiedTypeName name = do
ctx <- ask
case M.lookup (Alias name) (knownAliases ctx) of
-- The resolved type may be an alias itself, like for
-- Gtk.Allocation -> Gdk.Rectangle -> cairo.RectangleInt
Just (TInterface n) -> resolveQualifiedTypeName n
Just t -> return t
Nothing -> return $ TInterface name
-- | Return the value of an attribute for the given element. If the
-- attribute is not present this throws an error.
getAttr :: XML.Name -> Parser Text
getAttr attr = do
ctx <- ask
case lookupAttr attr (currentElement ctx) of
Just val -> return val
Nothing -> parseError $ "Expected attribute \"" <>
(T.pack . show) attr <> "\" not present."
-- | Like 'getAttr', but allow for specifying the namespace.
getAttrWithNamespace :: GIRXMLNamespace -> XML.Name -> Parser Text
getAttrWithNamespace ns attr = do
ctx <- ask
case lookupAttrWithNamespace ns attr (currentElement ctx) of
Just val -> return val
Nothing -> parseError $ "Expected attribute \"" <>
(T.pack . show) attr <> "\" in namespace \"" <>
(T.pack . show) ns <> "\" not present."
-- | Return the value of an attribute if it is present, and Nothing otherwise.
queryAttr :: XML.Name -> Parser (Maybe Text)
queryAttr attr = do
ctx <- ask
return $ lookupAttr attr (currentElement ctx)
-- | Like `queryAttr`, but allow for specifying the namespace.
queryAttrWithNamespace :: GIRXMLNamespace -> XML.Name -> Parser (Maybe Text)
queryAttrWithNamespace ns attr = do
ctx <- ask
return $ lookupAttrWithNamespace ns attr (currentElement ctx)
-- | Ask for an optional attribute, applying the given parser to
-- it. If the argument does not exist return the default value provided.
optionalAttr :: XML.Name -> a -> (Text -> Parser a) -> Parser a
optionalAttr attr def parser =
queryAttr attr >>= \case
Just a -> parser a
Nothing -> return def
-- | Build a 'Name' out of the (possibly qualified) supplied name. If
-- the supplied name is unqualified we qualify with the current
-- namespace, and otherwise we simply parse it.
qualifyName :: Text -> Parser Name
qualifyName n = case T.split (== '.') n of
[ns, name] -> return $ Name ns name
[name] -> nameInCurrentNS name
_ -> parseError "Could not understand name"
-- | Get the qualified name for the current element.
parseName :: Parser Name
parseName = getAttr "name" >>= qualifyName
-- | Parse the deprecation text, if present.
parseDeprecation :: Parser (Maybe DeprecationInfo)
parseDeprecation = do
ctx <- ask
return $ queryDeprecated (currentElement ctx)
-- | Parse the documentation info for the current node.
parseDocumentation :: Parser Documentation
parseDocumentation = do
ctx <- ask
return $ queryDocumentation (currentElement ctx)
-- | Parse a signed integral number.
parseIntegral :: Integral a => Text -> Parser a
parseIntegral str =
case TR.signed TR.decimal str of
Right (n, r) | T.null r -> return n
_ -> parseError $ "Could not parse integral value: \"" <> str <> "\"."
-- | A boolean value given by a numerical constant.
parseBool :: Text -> Parser Bool
parseBool "0" = return False
parseBool "1" = return True
parseBool other = parseError $ "Unsupported boolean value: " <> T.pack (show other)
-- | Parse all the introspectable subelements with the given local name.
parseChildrenWithLocalName :: Text -> Parser a -> Parser [a]
parseChildrenWithLocalName n parser = do
ctx <- ask
let introspectableChildren = filter introspectable
(childElemsWithLocalName n (currentElement ctx))
mapM (withElement parser) introspectableChildren
where introspectable :: Element -> Bool
introspectable e = lookupAttr "introspectable" e /= Just "0" &&
lookupAttr "shadowed-by" e == Nothing
-- | Parse all subelements with the given local name.
parseAllChildrenWithLocalName :: Text -> Parser a -> Parser [a]
parseAllChildrenWithLocalName n parser = do
ctx <- ask
mapM (withElement parser) (childElemsWithLocalName n (currentElement ctx))
-- | Parse all introspectable children with the given namespace and
-- local name.
parseChildrenWithNSName :: GIRXMLNamespace -> Text -> Parser a -> Parser [a]
parseChildrenWithNSName ns n parser = do
ctx <- ask
let introspectableChildren = filter introspectable
(childElemsWithNSName ns n (currentElement ctx))
mapM (withElement parser) introspectableChildren
where introspectable :: Element -> Bool
introspectable e = lookupAttr "introspectable" e /= Just "0"
-- | Run the given parser for a given subelement in the XML tree.
withElement :: Parser a -> Element -> Parser a
withElement parser element = local modifyParsePosition parser
where modifyParsePosition ctx =
ctx { treePosition = elementDescription element : treePosition ctx
, currentElement = element}
-- | Run the given parser, returning either success or an error.
runParser :: Text -> M.Map Alias Type -> Element -> Parser a ->
Either ParseError a
runParser ns aliases element parser =
runExcept (runReaderT parser ctx)
where ctx = ParseContext {
ctxNamespace = ns
, treePosition = [elementDescription element]
, currentElement = element
, knownAliases = aliases
}
|