aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMichael Williamson <mike@zwobble.org>2013-12-01 00:42:19 +0000
committerMichael Williamson <mike@zwobble.org>2013-12-01 00:42:19 +0000
commit2c07fef611dd1d5cd7e83d59d02d5ab52db74e63 (patch)
tree91e69988641f0e890cef9e70c4a2fb39d5d499eb
parent73a80869afb8f2e4c7727150a0ea211b93ccc475 (diff)
Add xmlparser
-rw-r--r--mammoth/xmlparser.py69
-rw-r--r--tests/xmlparser_tests.py63
2 files changed, 132 insertions, 0 deletions
diff --git a/mammoth/xmlparser.py b/mammoth/xmlparser.py
new file mode 100644
index 0000000..bd344a0
--- /dev/null
+++ b/mammoth/xmlparser.py
@@ -0,0 +1,69 @@
+import collections
+import xml.sax
+
+
+XmlElement = collections.namedtuple("XmlElement", ["name", "attributes", "children"])
+XmlText = collections.namedtuple("XmlText", ["value"])
+
+
+element = XmlElement
+text = XmlText
+
+
+def parse_xml(fileobj, namespace_mapping=None):
+ if namespace_mapping is None:
+ namespace_prefixes = {}
+ else:
+ namespace_prefixes = dict((uri, prefix) for prefix, uri in namespace_mapping)
+
+ handler = Handler(namespace_prefixes)
+ parser = xml.sax.make_parser()
+ parser.setFeature(xml.sax.handler.feature_namespaces, True)
+ parser.setContentHandler(handler)
+ parser.parse(fileobj)
+ return handler.root()
+
+
+class Handler(xml.sax.handler.ContentHandler):
+ def __init__(self, namespace_prefixes):
+ self._namespace_prefixes = namespace_prefixes
+ self._element_stack = [RootElement()]
+ self._character_buffer = []
+
+ def root(self):
+ return self._element_stack[0].children[0]
+
+ def startElementNS(self, name, qname, attrs):
+ self._flush_character_buffer()
+ attributes = dict((self._read_name(key), value) for key, value in attrs.items())
+ element = XmlElement(self._read_name(name), attributes, [])
+ self._element_stack[-1].children.append(element)
+ self._element_stack.append(element)
+
+ def endElementNS(self, name, qname):
+ self._flush_character_buffer()
+ self._element_stack.pop()
+
+ def characters(self, content):
+ self._character_buffer.append(content)
+
+ def _flush_character_buffer(self):
+ if self._character_buffer:
+ text = "".join(self._character_buffer)
+ self._element_stack[-1].children.append(XmlText(text))
+ self._character_buffer = []
+
+ def _read_name(self, name):
+ uri, local_name = name
+ if uri is None:
+ return local_name
+ else:
+ prefix = self._namespace_prefixes.get(uri)
+ if prefix is None:
+ return "{%s}%s" % (uri, local_name)
+ else:
+ return "%s:%s" % (prefix, local_name)
+
+class RootElement(object):
+ def __init__(self):
+ self.children = []
diff --git a/tests/xmlparser_tests.py b/tests/xmlparser_tests.py
new file mode 100644
index 0000000..90d8d9b
--- /dev/null
+++ b/tests/xmlparser_tests.py
@@ -0,0 +1,63 @@
+import io
+
+from nose.tools import istest, assert_equal
+
+from mammoth.xmlparser import parse_xml, element as xml_element, text as xml_text
+
+
+@istest
+def can_parse_self_closing_element():
+ xml = _parse_xml_string(b"<body/>")
+ assert_equal(xml_element("body", {}, []), xml)
+
+
+@istest
+def can_parse_empty_element_with_separate_closing_tag():
+ xml = _parse_xml_string(b"<body></body>")
+ assert_equal(xml_element("body", {}, []), xml)
+
+
+@istest
+def can_parse_attributes_of_tag():
+ xml = _parse_xml_string(b"<body name='bob'></body>")
+ assert_equal(xml_element("body", {"name": "bob"}, []), xml)
+
+
+@istest
+def can_parse_text_element():
+ xml = _parse_xml_string(b"<body>Hello!</body>")
+ assert_equal(xml_element("body", {}, [xml_text("Hello!")]), xml)
+
+
+@istest
+def can_parse_text_element_before_new_tag():
+ xml = _parse_xml_string(b"<body>Hello!<br/></body>")
+ assert_equal(xml_element("body", {}, [xml_text("Hello!"), xml_element("br", {}, [])]), xml)
+
+
+@istest
+def can_parse_element_with_children():
+ xml = _parse_xml_string(b"<body><a/><b/></body>")
+ assert_equal([xml_element("a", {}, []), xml_element("b", {}, [])], xml.children)
+
+
+@istest
+def unmapped_namespaces_uris_are_included_in_braces_as_prefix():
+ xml = _parse_xml_string(b'<w:body xmlns:w="word"/>')
+ assert_equal("{word}body", xml.name)
+
+
+@istest
+def mapped_namespaces_uris_are_translated_using_namespace_map():
+ xml = _parse_xml_string(b'<w:body xmlns:w="word"/>', [("x", "word")])
+ assert_equal("x:body", xml.name)
+
+
+@istest
+def namespace_of_attributes_is_mapped_to_prefix():
+ xml = _parse_xml_string(b'<w:body xmlns:w="word" w:val="Hello!"/>', [("x", "word")])
+ assert_equal("Hello!", xml.attributes["x:val"])
+
+
+def _parse_xml_string(string, namespace_mapping=None):
+ return parse_xml(io.BytesIO(string), namespace_mapping)