diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2021 Nikita Volkov
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/library/XmlParser.hs b/library/XmlParser.hs
new file mode 100644
--- /dev/null
+++ b/library/XmlParser.hs
@@ -0,0 +1,72 @@
+module XmlParser
+  ( -- * Execution
+    parseByteString,
+    parseLazyByteString,
+    parseFile,
+    parseElementAst,
+
+    -- * Parsers by context
+
+    -- ** Element
+    AstParser.Element,
+    AstParser.elementName,
+    AstParser.elementNameIs,
+    AstParser.children,
+    AstParser.childrenByName,
+    AstParser.attributesByName,
+    AstParser.astElement,
+
+    -- ** Nodes
+    AstParser.Nodes,
+    AstParser.elementNode,
+    AstParser.contentNode,
+
+    -- ** ByName
+    AstParser.ByName,
+    AstParser.byName,
+
+    -- ** Content
+    AstParser.Content,
+    AstParser.textContent,
+    AstParser.narrowedContent,
+    AstParser.refinedContent,
+    AstParser.enumContent,
+    AstParser.attoparsedContent,
+    AstParser.qNameContent,
+  )
+where
+
+import qualified Data.ByteString.Lazy as Lbs
+import qualified Text.XML as XmlConduit
+import qualified XmlParser.AstParser as AstParser
+import XmlParser.Prelude
+import qualified XmlParser.XmlConduitWrapper as XmlConduitWrapper
+
+-- |
+-- Parse XML bytestring.
+parseByteString :: AstParser.Element a -> ByteString -> Either Text a
+parseByteString astParser input =
+  XmlConduitWrapper.parseByteString input >>= parseDocumentAst astParser
+
+-- |
+-- Parse XML lazy bytestring.
+parseLazyByteString :: AstParser.Element a -> Lbs.ByteString -> Either Text a
+parseLazyByteString astParser input =
+  XmlConduitWrapper.parseLazyByteString input >>= parseDocumentAst astParser
+
+-- |
+-- Parse XML file.
+parseFile :: AstParser.Element a -> FilePath -> IO (Either Text a)
+parseFile astParser path =
+  fmap (>>= parseDocumentAst astParser) $
+    XmlConduitWrapper.parseFile path
+
+parseDocumentAst :: AstParser.Element a -> XmlConduit.Document -> Either Text a
+parseDocumentAst astParser =
+  parseElementAst astParser . XmlConduit.documentRoot
+
+-- |
+-- Parse an \"xml-conduit\" element AST.
+parseElementAst :: AstParser.Element a -> XmlConduit.Element -> Either Text a
+parseElementAst astParser =
+  first AstParser.renderElementError . AstParser.parseElement astParser
diff --git a/library/XmlParser/AstParser.hs b/library/XmlParser/AstParser.hs
new file mode 100644
--- /dev/null
+++ b/library/XmlParser/AstParser.hs
@@ -0,0 +1,495 @@
+module XmlParser.AstParser
+  ( -- * Execution
+    parseElement,
+    renderElementError,
+    ElementError (..),
+    NodeType (..),
+
+    -- * Parsers by context
+
+    -- ** Element
+    Element,
+    elementName,
+    elementNameIs,
+    children,
+    childrenByName,
+    attributesByName,
+    astElement,
+
+    -- ** Nodes
+    Nodes,
+    elementNode,
+    contentNode,
+
+    -- ** ByName
+    ByName,
+    byName,
+
+    -- ** Content
+    Content,
+    textContent,
+    narrowedContent,
+    refinedContent,
+    enumContent,
+    attoparsedContent,
+    qNameContent,
+  )
+where
+
+import qualified Data.Attoparsec.Text as Attoparsec
+import qualified Data.HashMap.Strict as HashMap
+import qualified Data.List as List
+import qualified Text.Builder as Tb
+import qualified Text.XML as Xml
+import qualified XmlParser.ElementDestructionState as ElementDestructionState
+import qualified XmlParser.NameMap as NameMap
+import qualified XmlParser.NamespaceRegistry as NamespaceRegistry
+import qualified XmlParser.NodeConsumerState as NodeConsumerState
+import XmlParser.Prelude
+import qualified XmlParser.XmlSchemaAttoparsec as XmlSchemaAttoparsec
+
+-- |
+-- Parse an \"xml-conduit\" element AST.
+parseElement :: Element a -> Xml.Element -> Either ElementError a
+parseElement (Element run) element =
+  run
+    (NamespaceRegistry.interpretAttributes (Xml.elementAttributes element) NamespaceRegistry.new)
+    element
+    ElementDestructionState.new
+    & fmap fst
+
+renderElementError :: ElementError -> Text
+renderElementError =
+  Tb.run . (\(a, b) -> "/" <> Tb.intercalate "/" (reverse a) <> ": " <> b) . simplifyElementError
+
+simplifyElementError :: ElementError -> ([Tb.Builder], Tb.Builder)
+simplifyElementError =
+  elementError []
+  where
+    sortedList renderer =
+      mappend "[" . flip mappend "]" . Tb.intercalate ", " . fmap renderer . sort
+    name a b =
+      case a of
+        Just _ -> Tb.text b
+        Nothing -> Tb.text b
+    elementError collectedPath = \case
+      NoneOfChildrenFoundByNameElementError a b ->
+        ( collectedPath,
+          "None of following child element names found: "
+            <> sortedList (uncurry name) a
+            <> ". Names available: "
+            <> sortedList (uncurry name) b
+        )
+      ChildByNameElementError a b c ->
+        elementError (name a b : collectedPath) c
+      ChildAtOffsetElementError a b ->
+        nodeError (Tb.decimal a : collectedPath) b
+      AttributeByNameElementError a b c ->
+        (("@" <> name a b) : collectedPath, maybeContentError c)
+      NoneOfAttributesFoundByNameElementError a b ->
+        ( collectedPath,
+          "Found none of the following attributes: " <> sortedList (uncurry name) a
+            <> ". The following are available: "
+            <> sortedList (uncurry name) b
+        )
+      NameElementError a ->
+        (collectedPath, Tb.text a)
+      UserElementError a ->
+        (collectedPath, Tb.text a)
+    nodeError collectedPath = \case
+      UnexpectedNodeTypeNodeError a b ->
+        ( collectedPath,
+          "Unexpected node type. Got " <> nodeType b <> ", but expected " <> nodeType a
+        )
+      NotAvailableNodeError ->
+        (collectedPath, "No nodes left")
+      ElementNodeError a ->
+        elementError collectedPath a
+      TextNodeError a ->
+        (collectedPath, maybeContentError a)
+    nodeType = \case
+      ElementNodeType -> "element"
+      InstructionNodeType -> "instruction"
+      ContentNodeType -> "content"
+      CommentNodeType -> "comment"
+    maybeContentError = maybe "Empty alternative" contentError
+    contentError = \case
+      UserContentError a ->
+        Tb.text a
+      ParsingContentError a ->
+        Tb.text a
+      NamespaceNotFoundContentError a ->
+        "Namespace not found: " <> Tb.text a
+      UnexpectedValueContentError a ->
+        "Unexpected value: " <> Tb.text a
+      EnumContentError a b ->
+        "Unexpected value: " <> Tb.text b <> ". Expecting one of the following: " <> sortedList Tb.text a
+
+-- |
+-- Error in the context of an element.
+--
+-- It has a tree structure specifying the context of containing operations.
+data ElementError
+  = AttributeByNameElementError
+      (Maybe Text)
+      Text
+      (Maybe ContentError)
+  | NoneOfAttributesFoundByNameElementError
+      [(Maybe Text, Text)]
+      -- ^ Not found.
+      [(Maybe Text, Text)]
+      -- ^ Out of.
+  | NoneOfChildrenFoundByNameElementError
+      [(Maybe Text, Text)]
+      -- ^ Not found.
+      [(Maybe Text, Text)]
+      -- ^ Out of.
+  | ChildByNameElementError
+      (Maybe Text)
+      -- ^ Namespace.
+      Text
+      -- ^ Name.
+      ElementError
+      -- ^ Reason. Not 'NodeError' because only element nodes can be looked up by name.
+  | ChildAtOffsetElementError
+      Int
+      -- ^ Offset.
+      NodeError
+      -- ^ Reason.
+  | NameElementError Text
+  | -- | Error raised by the user of this library.
+    UserElementError Text
+
+data NodeError
+  = UnexpectedNodeTypeNodeError
+      NodeType
+      -- ^ Expected.
+      NodeType
+      -- ^ Actual.
+  | NotAvailableNodeError
+  | ElementNodeError ElementError
+  | TextNodeError (Maybe ContentError)
+
+data ContentError
+  = ParsingContentError Text
+  | NamespaceNotFoundContentError Text
+  | UnexpectedValueContentError Text
+  | EnumContentError
+      [Text]
+      -- ^ List of expected values.
+      Text
+      -- ^ Actual value
+  | UserContentError Text
+
+data NodeType
+  = ElementNodeType
+  | InstructionNodeType
+  | ContentNodeType
+  | CommentNodeType
+
+-- |
+-- Parse in the context of an element node.
+newtype Element a
+  = Element
+      ( NamespaceRegistry.NamespaceRegistry ->
+        Xml.Element ->
+        ElementDestructionState.ElementDestructionState ->
+        Either ElementError (a, ElementDestructionState.ElementDestructionState)
+      )
+  deriving
+    (Functor, Applicative, Monad)
+    via ( ReaderT
+            (NamespaceRegistry.NamespaceRegistry)
+            ( ReaderT
+                Xml.Element
+                ( StateT
+                    ElementDestructionState.ElementDestructionState
+                    (Except ElementError)
+                )
+            )
+        )
+
+instance MonadFail Element where
+  fail = fromString >>> UserElementError >>> Left >>> const >>> const >>> const >>> Element
+
+-- |
+-- Parse namespace and name with the given function.
+elementName :: (Maybe Text -> Text -> Either Text a) -> Element a
+elementName parse =
+  Element $ \nreg (Xml.Element name _ _) state ->
+    fmap (,state) $ case NamespaceRegistry.resolveElementName name nreg of
+      Nothing -> Left (NameElementError ("Unresolvable name: " <> fromString (show name)))
+      Just (ns, name) -> parse ns name & first NameElementError
+
+-- |
+-- Fail if the namespace and name don't match the provided.
+elementNameIs :: Maybe Text -> Text -> Element ()
+elementNameIs ns name =
+  elementName $ \actualNs actualName ->
+    if actualNs == ns
+      then
+        if actualName == name
+          then Right ()
+          else Left ("Unexpected name: \"" <> actualName <> "\". Expecting: \"" <> name <> "\"")
+      else Left ("Unexpected namespace: \"" <> (fromString . show) actualNs <> "\". Expecting: \"" <> (fromString . show) ns <> "\"")
+
+-- |
+-- Look up elements by name and parse them.
+childrenByName :: ByName Element a -> Element a
+childrenByName (ByName runByName) =
+  Element $ \nreg element@(Xml.Element _ attributes _) state ->
+    case ElementDestructionState.resolveChildNames (ElementDestructionState.ElementDestructionContext nreg element) state of
+      (nameMap, state) ->
+        case runByName nameMap (\element (Element run) -> fmap fst (run deeperNreg element ElementDestructionState.new)) of
+          OkByNameResult _ res -> Right (res, state)
+          NotFoundByNameResult unfoundNames ->
+            let availNames = NameMap.extractNames nameMap
+             in Left (NoneOfChildrenFoundByNameElementError unfoundNames availNames)
+          FailedDeeperByNameResult ns name err ->
+            Left (ChildByNameElementError ns name err)
+        where
+          deeperNreg = NamespaceRegistry.interpretAttributes attributes nreg
+
+-- |
+-- Look up the last attribute by name and parse it.
+attributesByName :: ByName Content a -> Element a
+attributesByName (ByName runByName) =
+  Element $ \nreg element state ->
+    case ElementDestructionState.resolveAttributeNames (ElementDestructionState.ElementDestructionContext nreg element) state of
+      (nameMap, state) -> case runByName nameMap (\content (Content parseContent) -> parseContent (\ns -> NamespaceRegistry.lookup ns nreg) content) of
+        OkByNameResult _ res -> Right (res, state)
+        NotFoundByNameResult unfoundNames ->
+          let availNames = NameMap.extractNames nameMap
+           in Left (NoneOfAttributesFoundByNameElementError unfoundNames availNames)
+        FailedDeeperByNameResult ns name err ->
+          Left (AttributeByNameElementError ns name err)
+
+-- |
+-- Children sequence by order.
+children :: Nodes a -> Element a
+children (Nodes runNodes) =
+  Element $ \nreg (Xml.Element _ _ nodes) state ->
+    runNodes (NodeConsumerState.new nodes nreg)
+      & fmap fst
+      & fmap (,state)
+
+-- |
+-- Expose the element's AST.
+astElement :: Element Xml.Element
+astElement =
+  Element $ \_ element state -> Right (element, state)
+
+-- |
+-- Parser in the context of a sequence of nodes.
+newtype Nodes a
+  = Nodes (NodeConsumerState.NodeConsumerState -> Either ElementError (a, NodeConsumerState.NodeConsumerState))
+  deriving
+    (Functor, Applicative, Monad)
+    via (StateT NodeConsumerState.NodeConsumerState (Either ElementError))
+
+-- |
+-- Consume the next node expecting it to be element and parse its contents.
+elementNode :: Element a -> Nodes a
+elementNode (Element runElement) =
+  Nodes $ \x ->
+    case NodeConsumerState.fetchNode x of
+      Just (node, x) -> case node of
+        Xml.NodeElement element ->
+          bimap
+            (ChildAtOffsetElementError (NodeConsumerState.getOffset x) . ElementNodeError)
+            (,NodeConsumerState.bumpOffset x)
+            (fmap fst (runElement (NodeConsumerState.getNamespaceRegistry x) element ElementDestructionState.new))
+        Xml.NodeInstruction _ -> failWithUnexpectedNodeType InstructionNodeType
+        Xml.NodeContent _ -> failWithUnexpectedNodeType ContentNodeType
+        Xml.NodeComment _ -> failWithUnexpectedNodeType CommentNodeType
+        where
+          failWithUnexpectedNodeType actualType =
+            Left
+              ( ChildAtOffsetElementError
+                  (NodeConsumerState.getOffset x)
+                  (UnexpectedNodeTypeNodeError ElementNodeType actualType)
+              )
+      _ -> Left (ChildAtOffsetElementError (NodeConsumerState.getOffset x) NotAvailableNodeError)
+
+-- |
+-- Consume the next node expecting it to be textual and parse its contents.
+contentNode :: Content content -> Nodes content
+contentNode (Content parseContent) =
+  Nodes $ \x ->
+    case NodeConsumerState.fetchNode x of
+      Just (node, x) -> case node of
+        Xml.NodeContent content ->
+          case parseContent (\ns -> NodeConsumerState.lookupNamespace ns x) content of
+            Right parsedContent ->
+              Right (parsedContent, NodeConsumerState.bumpOffset x)
+            Left contentError ->
+              Left
+                ( ChildAtOffsetElementError
+                    (NodeConsumerState.getOffset x)
+                    (TextNodeError contentError)
+                )
+        Xml.NodeElement _ -> failWithUnexpectedNodeType ElementNodeType
+        Xml.NodeInstruction _ -> failWithUnexpectedNodeType InstructionNodeType
+        Xml.NodeComment _ -> failWithUnexpectedNodeType CommentNodeType
+        where
+          failWithUnexpectedNodeType actualType =
+            Left
+              ( ChildAtOffsetElementError
+                  (NodeConsumerState.getOffset x)
+                  (UnexpectedNodeTypeNodeError ContentNodeType actualType)
+              )
+      _ -> Left (ChildAtOffsetElementError (NodeConsumerState.getOffset x) NotAvailableNodeError)
+
+-- * Content
+
+-- |
+-- Parser in the context of decoded textual content,
+-- which can be the value of an attribute or a textual node.
+newtype Content content
+  = -- | Parser in the context of an xml namespace URI by alias lookup function.
+    Content ((Text -> Maybe Text) -> Text -> Either (Maybe ContentError) content)
+  deriving
+    (Functor, Applicative, Monad, Alternative, MonadPlus)
+    via (ReaderT (Text -> Maybe Text) (ExceptT (Last ContentError) ((->) Text)))
+
+instance MonadFail Content where
+  fail = fromString >>> UserContentError >>> Just >>> Left >>> const >>> const >>> Content
+
+-- |
+-- Return the content as it is.
+textContent :: Content Text
+textContent =
+  Content (const pure)
+
+-- |
+-- Map the content to a type if it's valid.
+narrowedContent :: (Text -> Maybe a) -> Content a
+narrowedContent mapper =
+  Content (const (\x -> maybe (Left (Just (UnexpectedValueContentError x))) Right (mapper x)))
+
+-- |
+-- Parse the content with a possibly failing function.
+refinedContent :: (Text -> Either Text a) -> Content a
+refinedContent refine =
+  Content (const (first (Just . ParsingContentError) . refine))
+
+-- |
+-- Map the content using a dictionary.
+enumContent :: [(Text, a)] -> Content a
+enumContent mappingList =
+  let !expectedKeysList =
+        fmap fst mappingList
+      mappingListLength =
+        length mappingList
+      !narrow =
+        if mappingListLength > 512
+          then
+            let !hashMap = HashMap.fromList mappingList
+             in flip HashMap.lookup hashMap
+          else flip List.lookup mappingList
+      extract a =
+        case narrow a of
+          Just b -> Right b
+          _ -> Left (Just (EnumContentError expectedKeysList a))
+   in Content (const extract)
+
+-- |
+-- Parse the content using the \"attoparsec\" parser.
+attoparsedContent :: Attoparsec.Parser a -> Content a
+attoparsedContent parser =
+  Content (const (first (Just . ParsingContentError . fromString) . Attoparsec.parseOnly parser))
+
+-- |
+-- Parse the content as XML Schema QName,
+-- automatically resolving the namespace as URI and failing,
+-- if none is associated.
+--
+-- Produces a URI associated with the namespace and name.
+-- If the content does not contain colon, produces an unnamespaced name.
+--
+-- Refs:
+--
+-- - https://www.w3.org/2001/tag/doc/qnameids.html#sec-qnames-xml
+-- - https://en.wikipedia.org/wiki/QName
+qNameContent :: Content (Maybe Text, Text)
+qNameContent =
+  Content $ \lookup content -> case Attoparsec.parseOnly XmlSchemaAttoparsec.qName content of
+    Right (ns, name) -> case ns of
+      Just ns -> case lookup ns of
+        Just uri -> Right (Just uri, name)
+        Nothing -> Left (Just (NamespaceNotFoundContentError ns))
+      Nothing -> Right (Nothing, name)
+    Left err -> Left (Just (ParsingContentError (fromString err)))
+
+-- * ByName
+
+data ByNameResult deeperError content a
+  = NotFoundByNameResult [(Maybe Text, Text)]
+  | FailedDeeperByNameResult (Maybe Text) Text deeperError
+  | OkByNameResult (NameMap.NameMap content) a
+  deriving (Functor)
+
+-- |
+-- Composable extension to a parser, which looks up its input by name.
+--
+-- Useful for searching elements and attributes by name.
+--
+-- Alternative and MonadPlus alternate only on lookup errors.
+-- When lookup is successful, but the deeper parser fails,
+-- the error propagates.
+--
+-- Monad and Applicative sequentially fetch contents by matching names.
+newtype ByName parser a
+  = ByName
+      ( forall content deeperError.
+        NameMap.NameMap content ->
+        (content -> forall x. parser x -> Either deeperError x) ->
+        ByNameResult deeperError content a
+      )
+
+instance Functor (ByName parser) where
+  fmap fn (ByName run) =
+    ByName $ \map parse -> fmap fn (run map parse)
+
+instance Applicative (ByName parser) where
+  pure x =
+    ByName $ \map _ -> OkByNameResult map x
+  ByName runL <*> ByName runR =
+    ByName $ \map parse -> case runL map parse of
+      OkByNameResult map lRes -> runR map parse & fmap lRes
+      NotFoundByNameResult unfoundNames -> NotFoundByNameResult unfoundNames
+      FailedDeeperByNameResult ns name err -> FailedDeeperByNameResult ns name err
+
+instance Monad (ByName parser) where
+  return = pure
+  ByName runL >>= k =
+    ByName $ \map parse -> case runL map parse of
+      OkByNameResult map lRes -> case k lRes of ByName runR -> runR map parse
+      NotFoundByNameResult unfoundNames -> NotFoundByNameResult unfoundNames
+      FailedDeeperByNameResult ns name err -> FailedDeeperByNameResult ns name err
+
+instance Alternative (ByName parser) where
+  empty =
+    ByName $ \_ _ -> NotFoundByNameResult []
+  ByName runL <|> ByName runR =
+    ByName $ \map parse -> case runL map parse of
+      OkByNameResult map lRes -> OkByNameResult map lRes
+      NotFoundByNameResult unfoundNamesL -> case runR map parse of
+        NotFoundByNameResult unfoundNamesR -> NotFoundByNameResult (unfoundNamesL <> unfoundNamesR)
+        resR -> resR
+      FailedDeeperByNameResult ns name err -> FailedDeeperByNameResult ns name err
+
+instance MonadPlus (ByName parser) where
+  mzero = empty
+  mplus = (<|>)
+
+-- |
+-- Execute a parser on the result of looking up a content by namespace and name.
+byName :: Maybe Text -> Text -> parser a -> ByName parser a
+byName ns name parser =
+  ByName $ \map parse ->
+    case NameMap.fetch ns name map of
+      Just (content, map) -> case parse content parser of
+        Right a -> OkByNameResult map a
+        Left err -> FailedDeeperByNameResult ns name err
+      Nothing -> NotFoundByNameResult [(ns, name)]
diff --git a/library/XmlParser/ElementDestructionState.hs b/library/XmlParser/ElementDestructionState.hs
new file mode 100644
--- /dev/null
+++ b/library/XmlParser/ElementDestructionState.hs
@@ -0,0 +1,59 @@
+module XmlParser.ElementDestructionState
+  ( ElementDestructionContext (..),
+    ElementDestructionState,
+    new,
+    resolveAttributeNames,
+    resolveChildNames,
+  )
+where
+
+import qualified Text.XML as Xml
+import qualified XmlParser.NameMap as NameMap
+import qualified XmlParser.NamespaceRegistry as NamespaceRegistry
+import XmlParser.Prelude
+
+-- |
+-- Context needed for the reading by functions of this component.
+-- They however do not change these values.
+--
+-- You can use this as a parameter to a reader monad.
+data ElementDestructionContext
+  = ElementDestructionContext
+      NamespaceRegistry.NamespaceRegistry
+      -- ^ Namespace registry as seen from the context of this node.
+      Xml.Element
+      -- ^ The node that we're in the context of.
+
+-- |
+-- Used for the state of a parser in the context of specifically element node.
+--
+-- It is basically the state of the context of the parser.
+--
+-- You can use this as a parameter to the state monad.
+data ElementDestructionState
+  = ElementDestructionState
+      (Maybe (NameMap.NameMap Text))
+      -- ^ Cached attribute by name lookup map.
+      (Maybe (NameMap.NameMap Xml.Element))
+      -- ^ Cached child element by name lookup map.
+
+new :: ElementDestructionState
+new = ElementDestructionState Nothing Nothing
+
+-- |
+-- Cache attribute names once and return them.
+resolveAttributeNames :: ElementDestructionContext -> ElementDestructionState -> (NameMap.NameMap Text, ElementDestructionState)
+resolveAttributeNames
+  (ElementDestructionContext nreg (Xml.Element _ attributes _))
+  (ElementDestructionState attributeByNameMap childByNameMap) =
+    let resolvedAttributeByNameMap = fromMaybe (NameMap.fromAttributes nreg attributes) attributeByNameMap
+     in (resolvedAttributeByNameMap, ElementDestructionState (Just resolvedAttributeByNameMap) childByNameMap)
+
+-- |
+-- Cache child names once and return them.
+resolveChildNames :: ElementDestructionContext -> ElementDestructionState -> (NameMap.NameMap Xml.Element, ElementDestructionState)
+resolveChildNames
+  (ElementDestructionContext nreg (Xml.Element _ _ nodes))
+  (ElementDestructionState attributeByNameMap childByNameMap) =
+    let resolvedChildByNameMap = fromMaybe (NameMap.fromNodes nreg nodes) childByNameMap
+     in (resolvedChildByNameMap, ElementDestructionState attributeByNameMap (Just resolvedChildByNameMap))
diff --git a/library/XmlParser/NameMap.hs b/library/XmlParser/NameMap.hs
new file mode 100644
--- /dev/null
+++ b/library/XmlParser/NameMap.hs
@@ -0,0 +1,106 @@
+module XmlParser.NameMap
+  ( NameMap,
+    fromNodes,
+    fromAttributes,
+    empty,
+    insert,
+    fetch,
+    extractNames,
+  )
+where
+
+import qualified Data.HashMap.Strict as HashMap
+import qualified Data.Map.Strict as Map
+import qualified Text.XML as Xml
+import qualified XmlParser.NamespaceRegistry as NamespaceRegistry
+import XmlParser.Prelude hiding (empty, fromList, insert, toList)
+import qualified XmlParser.TupleHashMap as TupleHashMap
+
+data NameMap a
+  = NameMap
+      (TupleHashMap.TupleHashMap Text Text [a])
+      -- ^ Namespaced
+      (HashMap Text [a])
+      -- ^ Unnamespaced
+
+fromNodes :: NamespaceRegistry.NamespaceRegistry -> [Xml.Node] -> NameMap Xml.Element
+fromNodes nreg =
+  fromReverseList (flip NamespaceRegistry.resolveElementName nreg) . foldl' appendIfElement []
+  where
+    appendIfElement list = \case
+      Xml.NodeElement element -> (Xml.elementName element, element) : list
+      _ -> list
+
+fromAttributes :: NamespaceRegistry.NamespaceRegistry -> Map Xml.Name Text -> NameMap Text
+fromAttributes nreg =
+  fromList (flip NamespaceRegistry.resolveAttributeName nreg) . Map.toList
+
+fromList :: (Xml.Name -> Maybe (Maybe Text, Text)) -> [(Xml.Name, a)] -> NameMap a
+fromList resolve =
+  fromReverseList resolve . reverse
+
+fromReverseList :: (Xml.Name -> Maybe (Maybe Text, Text)) -> [(Xml.Name, a)] -> NameMap a
+fromReverseList resolve list =
+  foldr step NameMap list TupleHashMap.empty HashMap.empty
+  where
+    step (name, contents) next !map1 !map2 =
+      case resolve name of
+        Nothing -> next map1 map2
+        Just (ns, name) ->
+          case ns of
+            Just ns -> next (TupleHashMap.insertSemigroup ns name [contents] map1) map2
+            Nothing -> next map1 (HashMap.insertWith (++) name [contents] map2)
+
+empty :: NameMap a
+empty =
+  NameMap TupleHashMap.empty HashMap.empty
+
+insert :: Maybe Text -> Text -> a -> NameMap a -> NameMap a
+insert ns name contents (NameMap map1 map2) =
+  case ns of
+    Just ns ->
+      NameMap (TupleHashMap.insertSemigroup ns name [contents] map1) map2
+    Nothing ->
+      NameMap map1 (HashMap.insertWith (++) name [contents] map2)
+
+fetch :: Maybe Text -> Text -> NameMap a -> Maybe (a, NameMap a)
+fetch ns name (NameMap map1 map2) =
+  case ns of
+    Just ns ->
+      TupleHashMap.alterF
+        ( \case
+            Just list ->
+              case list of
+                head : tail -> case tail of
+                  [] -> Compose (Just (head, Nothing))
+                  _ -> Compose (Just (head, Just tail))
+                _ -> Compose Nothing
+            Nothing ->
+              Compose Nothing
+        )
+        ns
+        name
+        map1
+        & getCompose
+        & fmap (fmap (flip NameMap map2))
+    Nothing ->
+      HashMap.alterF
+        ( \case
+            Just list ->
+              case list of
+                head : tail -> case tail of
+                  [] -> Compose (Just (head, Nothing))
+                  _ -> Compose (Just (head, Just tail))
+                _ -> Compose Nothing
+            Nothing ->
+              Compose Nothing
+        )
+        name
+        map2
+        & getCompose
+        & fmap (fmap (NameMap map1))
+
+extractNames :: NameMap a -> [(Maybe Text, Text)]
+extractNames (NameMap map1 map2) =
+  fmap (\(a, b, _) -> (Just a, b)) (TupleHashMap.toList map1)
+    <> fmap ((Nothing,) . fst) (HashMap.toList map2)
diff --git a/library/XmlParser/NamespaceRegistry.hs b/library/XmlParser/NamespaceRegistry.hs
new file mode 100644
--- /dev/null
+++ b/library/XmlParser/NamespaceRegistry.hs
@@ -0,0 +1,82 @@
+module XmlParser.NamespaceRegistry
+  ( NamespaceRegistry,
+    new,
+    lookup,
+    resolveElementName,
+    resolveAttributeName,
+    interpretAttribute,
+    interpretAttributes,
+  )
+where
+
+import qualified Data.Attoparsec.Text as Attoparsec
+import qualified Data.HashMap.Strict as HashMap
+import qualified Data.Map.Strict as Map
+import qualified Text.XML as Xml
+import XmlParser.Prelude hiding (insert, lookup)
+import qualified XmlParser.XmlSchemaAttoparsec as XmlSchemaAttoparsec
+
+data NamespaceRegistry
+  = NamespaceRegistry
+      (HashMap Text Text)
+      (Maybe Text)
+
+new :: NamespaceRegistry
+new = NamespaceRegistry HashMap.empty Nothing
+
+lookup :: Text -> NamespaceRegistry -> Maybe Text
+lookup ns (NamespaceRegistry nsMap _) =
+  HashMap.lookup ns nsMap
+
+resolveElementName :: Xml.Name -> NamespaceRegistry -> Maybe (Maybe Text, Text)
+resolveElementName = resolveName True
+
+resolveAttributeName :: Xml.Name -> NamespaceRegistry -> Maybe (Maybe Text, Text)
+resolveAttributeName = resolveName False
+
+resolveName :: Bool -> Xml.Name -> NamespaceRegistry -> Maybe (Maybe Text, Text)
+resolveName useDef (Xml.Name localName uri ns) (NamespaceRegistry map def) =
+  case uri of
+    Just uri -> Just (Just uri, localName)
+    Nothing -> case ns of
+      Just ns -> case HashMap.lookup ns map of
+        Just uri -> Just (Just uri, localName)
+        Nothing -> Nothing
+      Nothing ->
+        case Attoparsec.parseOnly XmlSchemaAttoparsec.qName localName of
+          Right (ns, localName) -> case ns of
+            Just ns -> case HashMap.lookup ns map of
+              Just uri -> Just (Just uri, localName)
+              Nothing -> Nothing
+            Nothing ->
+              Just (if useDef then def else Nothing, localName)
+          _ -> Nothing
+
+insert :: Text -> Text -> NamespaceRegistry -> NamespaceRegistry
+insert alias uri (NamespaceRegistry map def) =
+  NamespaceRegistry (HashMap.insert alias uri map) def
+
+setDefault :: Text -> NamespaceRegistry -> NamespaceRegistry
+setDefault uri (NamespaceRegistry map _) =
+  NamespaceRegistry map (Just uri)
+
+-- |
+-- Extend the registry by reading in the value if this is an \"xmlns\" attribute.
+interpretAttribute :: Xml.Name -> Text -> NamespaceRegistry -> NamespaceRegistry
+interpretAttribute (Xml.Name localName namespace prefix) uri =
+  case namespace of
+    Nothing -> case prefix of
+      Nothing -> case Attoparsec.parseOnly XmlSchemaAttoparsec.qName localName of
+        Right (Just "xmlns", name) -> insert name uri
+        Right (Nothing, "xmlns") -> setDefault uri
+        _ -> id
+      _ -> id
+    _ -> id
+
+-- |
+-- Extend the registry by reading in the \"xmlns\" attributes of an element.
+--
+-- Useful when diving into an element
+interpretAttributes :: Map Xml.Name Text -> NamespaceRegistry -> NamespaceRegistry
+interpretAttributes attributes x =
+  Map.foldlWithKey' (\x name value -> interpretAttribute name value x) x attributes
diff --git a/library/XmlParser/NodeConsumerState.hs b/library/XmlParser/NodeConsumerState.hs
new file mode 100644
--- /dev/null
+++ b/library/XmlParser/NodeConsumerState.hs
@@ -0,0 +1,45 @@
+module XmlParser.NodeConsumerState
+  ( NodeConsumerState,
+    new,
+    bumpOffset,
+    getOffset,
+    fetchNode,
+    lookupNamespace,
+    getNamespaceRegistry,
+  )
+where
+
+import qualified Text.XML as Xml
+import qualified XmlParser.NamespaceRegistry as NamespaceRegistry
+import XmlParser.Prelude
+
+data NodeConsumerState
+  = NodeConsumerState
+      [Xml.Node]
+      -- ^ Nodes.
+      Int
+      -- ^ Offset.
+      NamespaceRegistry.NamespaceRegistry
+      -- ^ Namespace registry.
+
+new :: [Xml.Node] -> NamespaceRegistry.NamespaceRegistry -> NodeConsumerState
+new nodes nsReg = NodeConsumerState nodes 0 nsReg
+
+bumpOffset :: NodeConsumerState -> NodeConsumerState
+bumpOffset (NodeConsumerState a b c) = NodeConsumerState a (succ b) c
+
+getOffset :: NodeConsumerState -> Int
+getOffset (NodeConsumerState _ x _) = x
+
+fetchNode :: NodeConsumerState -> Maybe (Xml.Node, NodeConsumerState)
+fetchNode (NodeConsumerState nodes offset nsReg) =
+  case nodes of
+    nodesHead : nodesTail -> Just (nodesHead, NodeConsumerState nodesTail offset nsReg)
+    _ -> Nothing
+
+lookupNamespace :: Text -> NodeConsumerState -> Maybe Text
+lookupNamespace ns (NodeConsumerState _ _ nsReg) =
+  NamespaceRegistry.lookup ns nsReg
+
+getNamespaceRegistry :: NodeConsumerState -> NamespaceRegistry.NamespaceRegistry
+getNamespaceRegistry (NodeConsumerState _ _ x) = x
diff --git a/library/XmlParser/Prelude.hs b/library/XmlParser/Prelude.hs
new file mode 100644
--- /dev/null
+++ b/library/XmlParser/Prelude.hs
@@ -0,0 +1,90 @@
+module XmlParser.Prelude
+  ( module Exports,
+    tryMapping,
+  )
+where
+
+import Control.Applicative as Exports hiding (WrappedArrow (..))
+import Control.Arrow as Exports hiding (first, second)
+import Control.Category as Exports
+import Control.Concurrent as Exports
+import Control.Exception as Exports
+import Control.Monad as Exports hiding (fail, forM, forM_, mapM, mapM_, msum, sequence, sequence_)
+import Control.Monad.Fail as Exports
+import Control.Monad.Fix as Exports hiding (fix)
+import Control.Monad.IO.Class as Exports
+import Control.Monad.ST as Exports
+import Control.Monad.Trans.Class as Exports
+import Control.Monad.Trans.Cont as Exports hiding (callCC, shift)
+import Control.Monad.Trans.Except as Exports (Except, ExceptT (ExceptT), catchE, except, mapExcept, mapExceptT, runExcept, runExceptT, throwE, withExcept, withExceptT)
+import Control.Monad.Trans.Maybe as Exports
+import Control.Monad.Trans.Reader as Exports (Reader, ReaderT (ReaderT), ask, mapReader, mapReaderT, runReader, runReaderT, withReader, withReaderT)
+import Control.Monad.Trans.State.Strict as Exports (State, StateT (StateT), evalState, evalStateT, execState, execStateT, mapState, mapStateT, runState, runStateT, withState, withStateT)
+import Control.Monad.Trans.Writer.Strict as Exports (Writer, WriterT (..), execWriter, execWriterT, mapWriter, mapWriterT, runWriter)
+import Data.Bifunctor as Exports
+import Data.Bits as Exports
+import Data.Bool as Exports
+import Data.ByteString as Exports (ByteString)
+import Data.Char as Exports
+import Data.Coerce as Exports
+import Data.Complex as Exports
+import Data.Data as Exports
+import Data.Dynamic as Exports
+import Data.Either as Exports
+import Data.Fixed as Exports
+import Data.Foldable as Exports hiding (toList)
+import Data.Function as Exports hiding (id, (.))
+import Data.Functor as Exports
+import Data.Functor.Compose as Exports
+import Data.Functor.Identity as Exports
+import Data.HashMap.Strict as Exports (HashMap)
+import Data.Hashable as Exports (Hashable)
+import Data.IORef as Exports
+import Data.Int as Exports
+import Data.Ix as Exports
+import Data.List as Exports hiding (all, and, any, concat, concatMap, elem, find, foldl, foldl', foldl1, foldr, foldr1, isSubsequenceOf, mapAccumL, mapAccumR, maximum, maximumBy, minimum, minimumBy, notElem, or, product, sortOn, sum, uncons)
+import Data.List.NonEmpty as Exports (NonEmpty (..))
+import Data.Map.Strict as Exports (Map)
+import Data.Maybe as Exports
+import Data.Monoid as Exports hiding (Alt)
+import Data.Ord as Exports
+import Data.Proxy as Exports
+import Data.Ratio as Exports
+import Data.STRef as Exports
+import Data.Semigroup as Exports hiding (First (..), Last (..))
+import Data.String as Exports
+import Data.Text as Exports (Text)
+import Data.Traversable as Exports
+import Data.Tuple as Exports
+import Data.Unique as Exports
+import Data.Version as Exports
+import Data.Void as Exports
+import Data.Word as Exports
+import Debug.Trace as Exports
+import Foreign.ForeignPtr as Exports
+import Foreign.Ptr as Exports
+import Foreign.StablePtr as Exports
+import Foreign.Storable as Exports
+import GHC.Conc as Exports hiding (orElse, threadWaitRead, threadWaitReadSTM, threadWaitWrite, threadWaitWriteSTM, withMVar)
+import GHC.Exts as Exports (IsList (..), groupWith, inline, lazy, sortWith)
+import GHC.Generics as Exports (Generic)
+import GHC.IO.Exception as Exports
+import GHC.OverloadedLabels as Exports
+import Numeric as Exports
+import System.Environment as Exports
+import System.Exit as Exports
+import System.IO as Exports (Handle, hClose)
+import System.IO.Error as Exports
+import System.IO.Unsafe as Exports
+import System.Mem as Exports
+import System.Mem.StableName as Exports
+import System.Timeout as Exports
+import Text.ParserCombinators.ReadP as Exports (ReadP, readP_to_S, readS_to_P)
+import Text.ParserCombinators.ReadPrec as Exports (ReadPrec, readP_to_Prec, readPrec_to_P, readPrec_to_S, readS_to_Prec)
+import Text.Printf as Exports (hPrintf, printf)
+import Text.Read as Exports (Read (..), readEither, readMaybe)
+import Unsafe.Coerce as Exports
+import Prelude as Exports hiding (all, and, any, concat, concatMap, elem, fail, foldl, foldl1, foldr, foldr1, id, mapM, mapM_, maximum, minimum, notElem, or, product, sequence, sequence_, sum, (.))
+
+tryMapping :: Exception e => (e -> e') -> IO a -> IO (Either e' a)
+tryMapping f action = catch (fmap Right action) (pure . Left . f)
diff --git a/library/XmlParser/TupleHashMap.hs b/library/XmlParser/TupleHashMap.hs
new file mode 100644
--- /dev/null
+++ b/library/XmlParser/TupleHashMap.hs
@@ -0,0 +1,55 @@
+module XmlParser.TupleHashMap
+  ( TupleHashMap,
+    KeyConstraints,
+    empty,
+    insertSemigroup,
+    alterF,
+    toList,
+  )
+where
+
+import qualified Data.HashMap.Strict as HashMap
+import XmlParser.Prelude hiding (empty, fromList, toList)
+
+newtype TupleHashMap k1 k2 v = TupleHashMap (HashMap k1 (HashMap k2 v))
+
+-- |
+-- Serves to reduce noise in signatures.
+type KeyConstraints k1 k2 = (Eq k1, Hashable k1, Eq k2, Hashable k2)
+
+empty :: TupleHashMap k1 k2 v
+empty =
+  TupleHashMap HashMap.empty
+
+insertSemigroup :: (Semigroup v, KeyConstraints k1 k2) => k1 -> k2 -> v -> TupleHashMap k1 k2 v -> TupleHashMap k1 k2 v
+insertSemigroup k1 k2 v (TupleHashMap map1) =
+  HashMap.alter
+    ( maybe
+        (Just (HashMap.singleton k2 v))
+        (Just . HashMap.insertWith (<>) k2 v)
+    )
+    k1
+    map1
+    & TupleHashMap
+
+alterF :: (Functor f, KeyConstraints k1 k2) => (Maybe v -> f (Maybe v)) -> k1 -> k2 -> TupleHashMap k1 k2 v -> f (TupleHashMap k1 k2 v)
+alterF fn k1 k2 (TupleHashMap map1) =
+  HashMap.alterF
+    ( \case
+        Just map2 ->
+          HashMap.alterF fn k2 map2
+            & fmap (\map2 -> if HashMap.null map2 then Nothing else Just map2)
+        Nothing ->
+          fn Nothing
+            & (fmap . fmap) (HashMap.singleton k2)
+    )
+    k1
+    map1
+    & fmap TupleHashMap
+
+toList :: TupleHashMap k1 k2 b -> [(k1, k2, b)]
+toList (TupleHashMap map1) =
+  do
+    (k1, map2) <- HashMap.toList map1
+    (k2, v) <- HashMap.toList map2
+    return (k1, k2, v)
diff --git a/library/XmlParser/XmlConduitWrapper.hs b/library/XmlParser/XmlConduitWrapper.hs
new file mode 100644
--- /dev/null
+++ b/library/XmlParser/XmlConduitWrapper.hs
@@ -0,0 +1,44 @@
+-- |
+-- A minimal wrapper over xml-conduit parsing API bringing it to our standards.
+module XmlParser.XmlConduitWrapper
+  ( parseByteString,
+    parseLazyByteString,
+    parseFile,
+  )
+where
+
+import qualified Data.ByteString.Lazy as LazyByteString
+import qualified Data.Text as Text
+import qualified Text.XML as XmlConduit
+import qualified Text.XML.Unresolved as XmlConduit (InvalidEventStream (..))
+import XmlParser.Prelude
+
+parseByteString :: ByteString -> Either Text XmlConduit.Document
+parseByteString =
+  parseLazyByteString . LazyByteString.fromStrict
+
+parseLazyByteString :: LazyByteString.ByteString -> Either Text XmlConduit.Document
+parseLazyByteString input =
+  first renderError (XmlConduit.parseLBS settings input)
+
+parseFile :: FilePath -> IO (Either Text XmlConduit.Document)
+parseFile path =
+  tryMapping renderError (XmlConduit.readFile settings path)
+
+settings :: XmlConduit.ParseSettings
+settings =
+  XmlConduit.def
+    { XmlConduit.psRetainNamespaces = True
+    }
+
+renderError :: SomeException -> Text
+renderError e
+  | Just e <- fromException @XmlConduit.XMLException e =
+    fromString (show e)
+  | Just e <- fromException @XmlConduit.InvalidEventStream e =
+    fromString (show e)
+  | Just (XmlConduit.UnresolvedEntityException e) <- fromException @XmlConduit.UnresolvedEntityException e =
+    "Unresolved entities: " <> Text.intercalate "," (toList e)
+  | otherwise =
+    -- FIXME: Find other cases and do something more user-friendly about them
+    fromString (show e)
diff --git a/library/XmlParser/XmlSchemaAttoparsec.hs b/library/XmlParser/XmlSchemaAttoparsec.hs
new file mode 100644
--- /dev/null
+++ b/library/XmlParser/XmlSchemaAttoparsec.hs
@@ -0,0 +1,74 @@
+module XmlParser.XmlSchemaAttoparsec where
+
+import Data.Attoparsec.Text
+import qualified Data.Text as Text
+import XmlParser.Prelude hiding (takeWhile)
+
+qName :: Parser (Maybe Text, Text)
+qName =
+  {-
+  Ref (from https://en.wikipedia.org/wiki/QName):
+
+      QName            ::=   PrefixedName | UnprefixedName
+      PrefixedName     ::=   Prefix ':' LocalPart
+      UnprefixedName   ::=   LocalPart
+      Prefix           ::=   NCName
+      LocalPart        ::=   NCName
+  -}
+  do
+    a <- ncName
+    asum
+      [ do
+          char ':'
+          b <- ncName
+          return (Just a, b),
+        return (Nothing, a)
+      ]
+
+{-# NOINLINE ncName #-}
+ncName :: Parser Text
+ncName =
+  {-
+  Ref (from https://en.wikipedia.org/wiki/QName):
+
+      NCName           ::=   Name - (Char* ':' Char*)  (* An XML Name, minus the ":" *)
+      Name             ::=   NameStartChar (NameChar)*
+      NameStartChar    ::=   ":" | [A-Z] | "_" | [a-z] | [#xC0-#xD6] | [#xD8-#xF6]
+                             | [#xF8-#x2FF] | [#x370-#x37D] | [#x37F-#x1FFF]
+                             | [#x200C-#x200D] | [#x2070-#x218F] | [#x2C00-#x2FEF]
+                             | [#x3001-#xD7FF] | [#xF900-#xFDCF] | [#xFDF0-#xFFFD]
+                             | [#x10000-#xEFFFF]
+      NameChar         ::=   NameStartChar | "-" | "." | [0-9]
+                             | #xB7 | [#x0300-#x036F] | [#x203F-#x2040]
+      Char             ::=   (* any Unicode char, excluding surrogate blocks FFFE and FFFF. *)
+                             #x9 | #xA | #xD | [#x20-#xD7FF]
+                             | [#xE000-#xFFFD] | [#x10000-#x10FFFF]
+  -}
+  do
+    a <- satisfy nameStartCharPredicate
+    b <- takeWhile nameCharPredicate
+    return (Text.cons a b)
+  where
+    nameStartCharPredicate x =
+      x >= 'A' && x <= 'Z'
+        || x == '_'
+        || x >= 'a' && x <= 'z'
+        || x >= '\xC0' && x <= '\xD6'
+        || x >= '\xD8' && x <= '\xF6'
+        || x >= '\xF8' && x <= '\x2FF'
+        || x >= '\x370' && x <= '\x37D'
+        || x >= '\x37F' && x <= '\x1FFF'
+        || x >= '\x200C' && x <= '\x200D'
+        || x >= '\x2070' && x <= '\x218F'
+        || x >= '\x2C00' && x <= '\x2FEF'
+        || x >= '\x3001' && x <= '\xD7FF'
+        || x >= '\xF900' && x <= '\xFDCF'
+        || x >= '\xFDF0' && x <= '\xFFFD'
+        || x >= '\x10000' && x <= '\xEFFFF'
+    nameCharPredicate x =
+      x == '-' || x == '.'
+        || x >= '0' && x <= '9'
+        || x == '\xB7'
+        || x >= '\x0300' && x <= '\x036F'
+        || x >= '\x203F' && x <= '\x2040'
+        || nameStartCharPredicate x
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -0,0 +1,58 @@
+{-# LANGUAGE OverloadedLists #-}
+
+module Main where
+
+import qualified Data.ByteString.Lazy as LazyByteString
+import Test.QuickCheck.Instances ()
+import Test.Tasty
+import Test.Tasty.HUnit
+import Test.Tasty.QuickCheck
+import qualified Text.XML as Xc
+import qualified XmlParser as Xp
+import Prelude hiding (assert)
+
+main =
+  defaultMain $
+    testGroup "All tests" $
+      [ testProperty "ByName/many" $ do
+          bContents <- fmap (fromString . show) <$> listOf (chooseInt (0, 99))
+
+          xml <-
+            let childByName name =
+                  Xc.Element name [] . pure . Xc.NodeContent
+                root =
+                  Xc.Element "a" [] . fmap (Xc.NodeElement)
+             in fmap (documentByteString . elementDocument . root . join) $
+                  forM bContents $ \bContent -> do
+                    prefix <-
+                      oneof
+                        [ pure [],
+                          listOf (childByName "c" <$> arbitrary)
+                        ]
+                    return (prefix <> [childByName "b" bContent])
+
+          let parser =
+                Xp.childrenByName $ many $ Xp.byName Nothing "b" $ Xp.children $ Xp.contentNode $ Xp.textContent
+              result = Xp.parseByteString parser xml
+
+          return (result === Right bContents),
+        testCase "Namespaces" $ do
+          let parser = Xp.childrenByName $ Xp.byName (Just "B") "c" $ Xp.children $ Xp.contentNode $ Xp.textContent
+              result = Xp.parseByteString parser "<a xmlns:b=\"B\"><b:c>d</b:c></a>"
+          assertEqual "" (Right "d") result,
+        testCase "Error" $ do
+          let parser = Xp.childrenByName $ Xp.byName Nothing "c" $ Xp.children $ Xp.contentNode $ Xp.textContent
+              result = Xp.parseByteString parser "<a><b>c</b><d/></a>"
+          assertEqual "" (Left "/: None of following child element names found: [c]. Names available: [b, d]") result,
+        testCase "QName content" $ do
+          let input = "<root xmlns:abc='abc-uri'>abc:d</root>"
+              parser = Xp.children $ Xp.contentNode $ Xp.qNameContent
+           in assertEqual "" (Right (Just "abc-uri", "d")) (Xp.parseByteString parser input)
+      ]
+
+documentByteString :: Xc.Document -> ByteString
+documentByteString = LazyByteString.toStrict . Xc.renderLBS Xc.def
+
+elementDocument :: Xc.Element -> Xc.Document
+elementDocument x =
+  Xc.Document (Xc.Prologue [] Nothing []) x []
diff --git a/xml-parser.cabal b/xml-parser.cabal
new file mode 100644
--- /dev/null
+++ b/xml-parser.cabal
@@ -0,0 +1,64 @@
+cabal-version: 3.0
+name: xml-parser
+version: 0.1
+synopsis: XML parser with informative error-reporting and simple API
+homepage: https://github.com/nikita-volkov/xml-parser
+bug-reports: https://github.com/nikita-volkov/xml-parser/issues
+author: Nikita Volkov <nikita.y.volkov@mail.ru>
+maintainer: Nikita Volkov <nikita.y.volkov@mail.ru>
+copyright: (c) 2021 Nikita Volkov
+license: MIT
+license-file: LICENSE
+build-type: Simple
+
+source-repository head
+  type: git
+  location: git://github.com/nikita-volkov/xml-parser.git
+
+library
+  hs-source-dirs: library
+  default-extensions: BangPatterns, BinaryLiterals, BlockArguments, ConstraintKinds, DataKinds, DefaultSignatures, DeriveDataTypeable, DeriveFoldable, DeriveFunctor, DeriveGeneric, DeriveTraversable, DerivingVia, EmptyDataDecls, FlexibleContexts, FlexibleInstances, FunctionalDependencies, GADTs, GeneralizedNewtypeDeriving, InstanceSigs, LambdaCase, LiberalTypeSynonyms, MagicHash, MultiParamTypeClasses, MultiWayIf, NoImplicitPrelude, NoMonomorphismRestriction, OverloadedStrings, PatternGuards, ParallelListComp, QuasiQuotes, RankNTypes, RecordWildCards, ScopedTypeVariables, StandaloneDeriving, StrictData, TemplateHaskell, TupleSections, TypeApplications, TypeFamilies, TypeOperators, UnboxedSums, UnboxedTuples, ViewPatterns
+  default-language: Haskell2010
+  ghc-options:
+    -funbox-strict-fields
+  exposed-modules:
+    XmlParser
+  other-modules:
+    XmlParser.AstParser
+    XmlParser.ElementDestructionState
+    XmlParser.NameMap
+    XmlParser.NamespaceRegistry
+    XmlParser.NodeConsumerState
+    XmlParser.TupleHashMap
+    XmlParser.Prelude
+    XmlParser.XmlConduitWrapper
+    XmlParser.XmlSchemaAttoparsec
+  build-depends:
+    attoparsec >=0.13 && <0.15,
+    base >=4.11 && <5,
+    bytestring >=0.10 && <0.12,
+    containers ^>=0.6,
+    hashable >=1.2 && <2,
+    text >=1.2.4 && <2,
+    text-builder >=0.6.6.2 && <0.7,
+    transformers ^>=0.5,
+    unordered-containers ^>=0.2.14,
+    xml-conduit ^>=1.9.1.1,
+
+test-suite test
+  type: exitcode-stdio-1.0
+  hs-source-dirs: test
+  default-extensions: BangPatterns, BinaryLiterals, BlockArguments, ConstraintKinds, DataKinds, DefaultSignatures, DeriveDataTypeable, DeriveFoldable, DeriveFunctor, DeriveGeneric, DeriveTraversable, DerivingVia, EmptyDataDecls, FlexibleContexts, FlexibleInstances, FunctionalDependencies, GADTs, GeneralizedNewtypeDeriving, InstanceSigs, LambdaCase, LiberalTypeSynonyms, MagicHash, MultiParamTypeClasses, MultiWayIf, NoImplicitPrelude, NoMonomorphismRestriction, OverloadedStrings, PatternGuards, ParallelListComp, QuasiQuotes, RankNTypes, RecordWildCards, ScopedTypeVariables, StandaloneDeriving, StrictData, TemplateHaskell, TupleSections, TypeApplications, TypeFamilies, TypeOperators, UnboxedSums, UnboxedTuples, ViewPatterns
+  default-language: Haskell2010
+  main-is: Main.hs
+  other-modules:
+  build-depends:
+    attoparsec >=0.13 && <0.15,
+    QuickCheck >=2.8.1 && <3,
+    quickcheck-instances >=0.3.11 && <0.4,
+    rerebase >=1.6.1 && <2,
+    tasty >=0.12 && <2,
+    tasty-hunit >=0.9 && <0.11,
+    tasty-quickcheck >=0.9 && <0.11,
+    xml-conduit,
+    xml-parser,
