diff options
| -rw-r--r-- | mammoth/xmlparser.py | 69 | ||||
| -rw-r--r-- | tests/xmlparser_tests.py | 63 |
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) |
