mirror of https://github.com/python/cpython
984 lines
36 KiB
Python
984 lines
36 KiB
Python
"""Facility to use the Expat parser to load a minidom instance
|
|
from a string or file.
|
|
|
|
This avoids all the overhead of SAX and pulldom to gain performance.
|
|
"""
|
|
|
|
# Warning!
|
|
#
|
|
# This module is tightly bound to the implementation details of the
|
|
# minidom DOM and can't be used with other DOM implementations. This
|
|
# is due, in part, to a lack of appropriate methods in the DOM (there is
|
|
# no way to create Entity and Notation nodes via the DOM Level 2
|
|
# interface), and for performance. The later is the cause of some fairly
|
|
# cryptic code.
|
|
#
|
|
# Performance hacks:
|
|
#
|
|
# - .character_data_handler() has an extra case in which continuing
|
|
# data is appended to an existing Text node; this can be a
|
|
# speedup since pyexpat can break up character data into multiple
|
|
# callbacks even though we set the buffer_text attribute on the
|
|
# parser. This also gives us the advantage that we don't need a
|
|
# separate normalization pass.
|
|
#
|
|
# - Determining that a node exists is done using an identity comparison
|
|
# with None rather than a truth test; this avoids searching for and
|
|
# calling any methods on the node object if it exists. (A rather
|
|
# nice speedup is achieved this way as well!)
|
|
|
|
from xml.dom import xmlbuilder, minidom, Node
|
|
from xml.dom import EMPTY_NAMESPACE, EMPTY_PREFIX, XMLNS_NAMESPACE
|
|
from xml.parsers import expat
|
|
from xml.dom.minidom import _append_child, _set_attribute_node
|
|
from xml.dom.NodeFilter import NodeFilter
|
|
|
|
from xml.dom.minicompat import *
|
|
|
|
TEXT_NODE = Node.TEXT_NODE
|
|
CDATA_SECTION_NODE = Node.CDATA_SECTION_NODE
|
|
DOCUMENT_NODE = Node.DOCUMENT_NODE
|
|
|
|
FILTER_ACCEPT = xmlbuilder.DOMBuilderFilter.FILTER_ACCEPT
|
|
FILTER_REJECT = xmlbuilder.DOMBuilderFilter.FILTER_REJECT
|
|
FILTER_SKIP = xmlbuilder.DOMBuilderFilter.FILTER_SKIP
|
|
FILTER_INTERRUPT = xmlbuilder.DOMBuilderFilter.FILTER_INTERRUPT
|
|
|
|
theDOMImplementation = minidom.getDOMImplementation()
|
|
|
|
# Expat typename -> TypeInfo
|
|
_typeinfo_map = {
|
|
"CDATA": minidom.TypeInfo(None, "cdata"),
|
|
"ENUM": minidom.TypeInfo(None, "enumeration"),
|
|
"ENTITY": minidom.TypeInfo(None, "entity"),
|
|
"ENTITIES": minidom.TypeInfo(None, "entities"),
|
|
"ID": minidom.TypeInfo(None, "id"),
|
|
"IDREF": minidom.TypeInfo(None, "idref"),
|
|
"IDREFS": minidom.TypeInfo(None, "idrefs"),
|
|
"NMTOKEN": minidom.TypeInfo(None, "nmtoken"),
|
|
"NMTOKENS": minidom.TypeInfo(None, "nmtokens"),
|
|
}
|
|
|
|
class ElementInfo(object):
|
|
__slots__ = '_attr_info', '_model', 'tagName'
|
|
|
|
def __init__(self, tagName, model=None):
|
|
self.tagName = tagName
|
|
self._attr_info = []
|
|
self._model = model
|
|
|
|
def __getstate__(self):
|
|
return self._attr_info, self._model, self.tagName
|
|
|
|
def __setstate__(self, state):
|
|
self._attr_info, self._model, self.tagName = state
|
|
|
|
def getAttributeType(self, aname):
|
|
for info in self._attr_info:
|
|
if info[1] == aname:
|
|
t = info[-2]
|
|
if t[0] == "(":
|
|
return _typeinfo_map["ENUM"]
|
|
else:
|
|
return _typeinfo_map[info[-2]]
|
|
return minidom._no_type
|
|
|
|
def getAttributeTypeNS(self, namespaceURI, localName):
|
|
return minidom._no_type
|
|
|
|
def isElementContent(self):
|
|
if self._model:
|
|
type = self._model[0]
|
|
return type not in (expat.model.XML_CTYPE_ANY,
|
|
expat.model.XML_CTYPE_MIXED)
|
|
else:
|
|
return False
|
|
|
|
def isEmpty(self):
|
|
if self._model:
|
|
return self._model[0] == expat.model.XML_CTYPE_EMPTY
|
|
else:
|
|
return False
|
|
|
|
def isId(self, aname):
|
|
for info in self._attr_info:
|
|
if info[1] == aname:
|
|
return info[-2] == "ID"
|
|
return False
|
|
|
|
def isIdNS(self, euri, ename, auri, aname):
|
|
# not sure this is meaningful
|
|
return self.isId((auri, aname))
|
|
|
|
def _intern(builder, s):
|
|
return builder._intern_setdefault(s, s)
|
|
|
|
def _parse_ns_name(builder, name):
|
|
assert ' ' in name
|
|
parts = name.split(' ')
|
|
intern = builder._intern_setdefault
|
|
if len(parts) == 3:
|
|
uri, localname, prefix = parts
|
|
prefix = intern(prefix, prefix)
|
|
qname = "%s:%s" % (prefix, localname)
|
|
qname = intern(qname, qname)
|
|
localname = intern(localname, localname)
|
|
else:
|
|
uri, localname = parts
|
|
prefix = EMPTY_PREFIX
|
|
qname = localname = intern(localname, localname)
|
|
return intern(uri, uri), localname, prefix, qname
|
|
|
|
|
|
class ExpatBuilder:
|
|
"""Document builder that uses Expat to build a ParsedXML.DOM document
|
|
instance."""
|
|
|
|
def __init__(self, options=None):
|
|
if options is None:
|
|
options = xmlbuilder.Options()
|
|
self._options = options
|
|
if self._options.filter is not None:
|
|
self._filter = FilterVisibilityController(self._options.filter)
|
|
else:
|
|
self._filter = None
|
|
# This *really* doesn't do anything in this case, so
|
|
# override it with something fast & minimal.
|
|
self._finish_start_element = id
|
|
self._parser = None
|
|
self.reset()
|
|
|
|
def createParser(self):
|
|
"""Create a new parser object."""
|
|
return expat.ParserCreate()
|
|
|
|
def getParser(self):
|
|
"""Return the parser object, creating a new one if needed."""
|
|
if not self._parser:
|
|
self._parser = self.createParser()
|
|
self._intern_setdefault = self._parser.intern.setdefault
|
|
self._parser.buffer_text = True
|
|
self._parser.ordered_attributes = True
|
|
self._parser.specified_attributes = True
|
|
self.install(self._parser)
|
|
return self._parser
|
|
|
|
def reset(self):
|
|
"""Free all data structures used during DOM construction."""
|
|
self.document = theDOMImplementation.createDocument(
|
|
EMPTY_NAMESPACE, None, None)
|
|
self.curNode = self.document
|
|
self._elem_info = self.document._elem_info
|
|
self._cdata = False
|
|
|
|
def install(self, parser):
|
|
"""Install the callbacks needed to build the DOM into the parser."""
|
|
# This creates circular references!
|
|
parser.StartDoctypeDeclHandler = self.start_doctype_decl_handler
|
|
parser.StartElementHandler = self.first_element_handler
|
|
parser.EndElementHandler = self.end_element_handler
|
|
parser.ProcessingInstructionHandler = self.pi_handler
|
|
if self._options.entities:
|
|
parser.EntityDeclHandler = self.entity_decl_handler
|
|
parser.NotationDeclHandler = self.notation_decl_handler
|
|
if self._options.comments:
|
|
parser.CommentHandler = self.comment_handler
|
|
if self._options.cdata_sections:
|
|
parser.StartCdataSectionHandler = self.start_cdata_section_handler
|
|
parser.EndCdataSectionHandler = self.end_cdata_section_handler
|
|
parser.CharacterDataHandler = self.character_data_handler_cdata
|
|
else:
|
|
parser.CharacterDataHandler = self.character_data_handler
|
|
parser.ExternalEntityRefHandler = self.external_entity_ref_handler
|
|
parser.XmlDeclHandler = self.xml_decl_handler
|
|
parser.ElementDeclHandler = self.element_decl_handler
|
|
parser.AttlistDeclHandler = self.attlist_decl_handler
|
|
|
|
def parseFile(self, file):
|
|
"""Parse a document from a file object, returning the document
|
|
node."""
|
|
parser = self.getParser()
|
|
first_buffer = True
|
|
try:
|
|
while 1:
|
|
buffer = file.read(16*1024)
|
|
if not buffer:
|
|
break
|
|
parser.Parse(buffer, 0)
|
|
if first_buffer and self.document.documentElement:
|
|
self._setup_subset(buffer)
|
|
first_buffer = False
|
|
parser.Parse("", True)
|
|
except ParseEscape:
|
|
pass
|
|
doc = self.document
|
|
self.reset()
|
|
self._parser = None
|
|
return doc
|
|
|
|
def parseString(self, string):
|
|
"""Parse a document from a string, returning the document node."""
|
|
parser = self.getParser()
|
|
try:
|
|
parser.Parse(string, True)
|
|
self._setup_subset(string)
|
|
except ParseEscape:
|
|
pass
|
|
doc = self.document
|
|
self.reset()
|
|
self._parser = None
|
|
return doc
|
|
|
|
def _setup_subset(self, buffer):
|
|
"""Load the internal subset if there might be one."""
|
|
if self.document.doctype:
|
|
extractor = InternalSubsetExtractor()
|
|
extractor.parseString(buffer)
|
|
subset = extractor.getSubset()
|
|
self.document.doctype.internalSubset = subset
|
|
|
|
def start_doctype_decl_handler(self, doctypeName, systemId, publicId,
|
|
has_internal_subset):
|
|
doctype = self.document.implementation.createDocumentType(
|
|
doctypeName, publicId, systemId)
|
|
doctype.ownerDocument = self.document
|
|
self.document.childNodes.append(doctype)
|
|
self.document.doctype = doctype
|
|
if self._filter and self._filter.acceptNode(doctype) == FILTER_REJECT:
|
|
self.document.doctype = None
|
|
del self.document.childNodes[-1]
|
|
doctype = None
|
|
self._parser.EntityDeclHandler = None
|
|
self._parser.NotationDeclHandler = None
|
|
if has_internal_subset:
|
|
if doctype is not None:
|
|
doctype.entities._seq = []
|
|
doctype.notations._seq = []
|
|
self._parser.CommentHandler = None
|
|
self._parser.ProcessingInstructionHandler = None
|
|
self._parser.EndDoctypeDeclHandler = self.end_doctype_decl_handler
|
|
|
|
def end_doctype_decl_handler(self):
|
|
if self._options.comments:
|
|
self._parser.CommentHandler = self.comment_handler
|
|
self._parser.ProcessingInstructionHandler = self.pi_handler
|
|
if not (self._elem_info or self._filter):
|
|
self._finish_end_element = id
|
|
|
|
def pi_handler(self, target, data):
|
|
node = self.document.createProcessingInstruction(target, data)
|
|
_append_child(self.curNode, node)
|
|
if self._filter and self._filter.acceptNode(node) == FILTER_REJECT:
|
|
self.curNode.removeChild(node)
|
|
|
|
def character_data_handler_cdata(self, data):
|
|
childNodes = self.curNode.childNodes
|
|
if self._cdata:
|
|
if ( self._cdata_continue
|
|
and childNodes[-1].nodeType == CDATA_SECTION_NODE):
|
|
childNodes[-1].appendData(data)
|
|
return
|
|
node = self.document.createCDATASection(data)
|
|
self._cdata_continue = True
|
|
elif childNodes and childNodes[-1].nodeType == TEXT_NODE:
|
|
node = childNodes[-1]
|
|
value = node.data + data
|
|
d = node.__dict__
|
|
d['data'] = d['nodeValue'] = value
|
|
return
|
|
else:
|
|
node = minidom.Text()
|
|
d = node.__dict__
|
|
d['data'] = d['nodeValue'] = data
|
|
d['ownerDocument'] = self.document
|
|
_append_child(self.curNode, node)
|
|
|
|
def character_data_handler(self, data):
|
|
childNodes = self.curNode.childNodes
|
|
if childNodes and childNodes[-1].nodeType == TEXT_NODE:
|
|
node = childNodes[-1]
|
|
d = node.__dict__
|
|
d['data'] = d['nodeValue'] = node.data + data
|
|
return
|
|
node = minidom.Text()
|
|
d = node.__dict__
|
|
d['data'] = d['nodeValue'] = node.data + data
|
|
d['ownerDocument'] = self.document
|
|
_append_child(self.curNode, node)
|
|
|
|
def entity_decl_handler(self, entityName, is_parameter_entity, value,
|
|
base, systemId, publicId, notationName):
|
|
if is_parameter_entity:
|
|
# we don't care about parameter entities for the DOM
|
|
return
|
|
if not self._options.entities:
|
|
return
|
|
node = self.document._create_entity(entityName, publicId,
|
|
systemId, notationName)
|
|
if value is not None:
|
|
# internal entity
|
|
# node *should* be readonly, but we'll cheat
|
|
child = self.document.createTextNode(value)
|
|
node.childNodes.append(child)
|
|
self.document.doctype.entities._seq.append(node)
|
|
if self._filter and self._filter.acceptNode(node) == FILTER_REJECT:
|
|
del self.document.doctype.entities._seq[-1]
|
|
|
|
def notation_decl_handler(self, notationName, base, systemId, publicId):
|
|
node = self.document._create_notation(notationName, publicId, systemId)
|
|
self.document.doctype.notations._seq.append(node)
|
|
if self._filter and self._filter.acceptNode(node) == FILTER_ACCEPT:
|
|
del self.document.doctype.notations._seq[-1]
|
|
|
|
def comment_handler(self, data):
|
|
node = self.document.createComment(data)
|
|
_append_child(self.curNode, node)
|
|
if self._filter and self._filter.acceptNode(node) == FILTER_REJECT:
|
|
self.curNode.removeChild(node)
|
|
|
|
def start_cdata_section_handler(self):
|
|
self._cdata = True
|
|
self._cdata_continue = False
|
|
|
|
def end_cdata_section_handler(self):
|
|
self._cdata = False
|
|
self._cdata_continue = False
|
|
|
|
def external_entity_ref_handler(self, context, base, systemId, publicId):
|
|
return 1
|
|
|
|
def first_element_handler(self, name, attributes):
|
|
if self._filter is None and not self._elem_info:
|
|
self._finish_end_element = id
|
|
self.getParser().StartElementHandler = self.start_element_handler
|
|
self.start_element_handler(name, attributes)
|
|
|
|
def start_element_handler(self, name, attributes):
|
|
node = self.document.createElement(name)
|
|
_append_child(self.curNode, node)
|
|
self.curNode = node
|
|
|
|
if attributes:
|
|
for i in range(0, len(attributes), 2):
|
|
a = minidom.Attr(attributes[i], EMPTY_NAMESPACE,
|
|
None, EMPTY_PREFIX)
|
|
value = attributes[i+1]
|
|
d = a.childNodes[0].__dict__
|
|
d['data'] = d['nodeValue'] = value
|
|
d = a.__dict__
|
|
d['value'] = d['nodeValue'] = value
|
|
d['ownerDocument'] = self.document
|
|
_set_attribute_node(node, a)
|
|
|
|
if node is not self.document.documentElement:
|
|
self._finish_start_element(node)
|
|
|
|
def _finish_start_element(self, node):
|
|
if self._filter:
|
|
# To be general, we'd have to call isSameNode(), but this
|
|
# is sufficient for minidom:
|
|
if node is self.document.documentElement:
|
|
return
|
|
filt = self._filter.startContainer(node)
|
|
if filt == FILTER_REJECT:
|
|
# ignore this node & all descendents
|
|
Rejecter(self)
|
|
elif filt == FILTER_SKIP:
|
|
# ignore this node, but make it's children become
|
|
# children of the parent node
|
|
Skipper(self)
|
|
else:
|
|
return
|
|
self.curNode = node.parentNode
|
|
node.parentNode.removeChild(node)
|
|
node.unlink()
|
|
|
|
# If this ever changes, Namespaces.end_element_handler() needs to
|
|
# be changed to match.
|
|
#
|
|
def end_element_handler(self, name):
|
|
curNode = self.curNode
|
|
self.curNode = curNode.parentNode
|
|
self._finish_end_element(curNode)
|
|
|
|
def _finish_end_element(self, curNode):
|
|
info = self._elem_info.get(curNode.tagName)
|
|
if info:
|
|
self._handle_white_text_nodes(curNode, info)
|
|
if self._filter:
|
|
if curNode is self.document.documentElement:
|
|
return
|
|
if self._filter.acceptNode(curNode) == FILTER_REJECT:
|
|
self.curNode.removeChild(curNode)
|
|
curNode.unlink()
|
|
|
|
def _handle_white_text_nodes(self, node, info):
|
|
if (self._options.whitespace_in_element_content
|
|
or not info.isElementContent()):
|
|
return
|
|
|
|
# We have element type information and should remove ignorable
|
|
# whitespace; identify for text nodes which contain only
|
|
# whitespace.
|
|
L = []
|
|
for child in node.childNodes:
|
|
if child.nodeType == TEXT_NODE and not child.data.strip():
|
|
L.append(child)
|
|
|
|
# Remove ignorable whitespace from the tree.
|
|
for child in L:
|
|
node.removeChild(child)
|
|
|
|
def element_decl_handler(self, name, model):
|
|
info = self._elem_info.get(name)
|
|
if info is None:
|
|
self._elem_info[name] = ElementInfo(name, model)
|
|
else:
|
|
assert info._model is None
|
|
info._model = model
|
|
|
|
def attlist_decl_handler(self, elem, name, type, default, required):
|
|
info = self._elem_info.get(elem)
|
|
if info is None:
|
|
info = ElementInfo(elem)
|
|
self._elem_info[elem] = info
|
|
info._attr_info.append(
|
|
[None, name, None, None, default, 0, type, required])
|
|
|
|
def xml_decl_handler(self, version, encoding, standalone):
|
|
self.document.version = version
|
|
self.document.encoding = encoding
|
|
# This is still a little ugly, thanks to the pyexpat API. ;-(
|
|
if standalone >= 0:
|
|
if standalone:
|
|
self.document.standalone = True
|
|
else:
|
|
self.document.standalone = False
|
|
|
|
|
|
# Don't include FILTER_INTERRUPT, since that's checked separately
|
|
# where allowed.
|
|
_ALLOWED_FILTER_RETURNS = (FILTER_ACCEPT, FILTER_REJECT, FILTER_SKIP)
|
|
|
|
class FilterVisibilityController(object):
|
|
"""Wrapper around a DOMBuilderFilter which implements the checks
|
|
to make the whatToShow filter attribute work."""
|
|
|
|
__slots__ = 'filter',
|
|
|
|
def __init__(self, filter):
|
|
self.filter = filter
|
|
|
|
def startContainer(self, node):
|
|
mask = self._nodetype_mask[node.nodeType]
|
|
if self.filter.whatToShow & mask:
|
|
val = self.filter.startContainer(node)
|
|
if val == FILTER_INTERRUPT:
|
|
raise ParseEscape
|
|
if val not in _ALLOWED_FILTER_RETURNS:
|
|
raise ValueError(
|
|
"startContainer() returned illegal value: " + repr(val))
|
|
return val
|
|
else:
|
|
return FILTER_ACCEPT
|
|
|
|
def acceptNode(self, node):
|
|
mask = self._nodetype_mask[node.nodeType]
|
|
if self.filter.whatToShow & mask:
|
|
val = self.filter.acceptNode(node)
|
|
if val == FILTER_INTERRUPT:
|
|
raise ParseEscape
|
|
if val == FILTER_SKIP:
|
|
# move all child nodes to the parent, and remove this node
|
|
parent = node.parentNode
|
|
for child in node.childNodes[:]:
|
|
parent.appendChild(child)
|
|
# node is handled by the caller
|
|
return FILTER_REJECT
|
|
if val not in _ALLOWED_FILTER_RETURNS:
|
|
raise ValueError(
|
|
"acceptNode() returned illegal value: " + repr(val))
|
|
return val
|
|
else:
|
|
return FILTER_ACCEPT
|
|
|
|
_nodetype_mask = {
|
|
Node.ELEMENT_NODE: NodeFilter.SHOW_ELEMENT,
|
|
Node.ATTRIBUTE_NODE: NodeFilter.SHOW_ATTRIBUTE,
|
|
Node.TEXT_NODE: NodeFilter.SHOW_TEXT,
|
|
Node.CDATA_SECTION_NODE: NodeFilter.SHOW_CDATA_SECTION,
|
|
Node.ENTITY_REFERENCE_NODE: NodeFilter.SHOW_ENTITY_REFERENCE,
|
|
Node.ENTITY_NODE: NodeFilter.SHOW_ENTITY,
|
|
Node.PROCESSING_INSTRUCTION_NODE: NodeFilter.SHOW_PROCESSING_INSTRUCTION,
|
|
Node.COMMENT_NODE: NodeFilter.SHOW_COMMENT,
|
|
Node.DOCUMENT_NODE: NodeFilter.SHOW_DOCUMENT,
|
|
Node.DOCUMENT_TYPE_NODE: NodeFilter.SHOW_DOCUMENT_TYPE,
|
|
Node.DOCUMENT_FRAGMENT_NODE: NodeFilter.SHOW_DOCUMENT_FRAGMENT,
|
|
Node.NOTATION_NODE: NodeFilter.SHOW_NOTATION,
|
|
}
|
|
|
|
|
|
class FilterCrutch(object):
|
|
__slots__ = '_builder', '_level', '_old_start', '_old_end'
|
|
|
|
def __init__(self, builder):
|
|
self._level = 0
|
|
self._builder = builder
|
|
parser = builder._parser
|
|
self._old_start = parser.StartElementHandler
|
|
self._old_end = parser.EndElementHandler
|
|
parser.StartElementHandler = self.start_element_handler
|
|
parser.EndElementHandler = self.end_element_handler
|
|
|
|
class Rejecter(FilterCrutch):
|
|
__slots__ = ()
|
|
|
|
def __init__(self, builder):
|
|
FilterCrutch.__init__(self, builder)
|
|
parser = builder._parser
|
|
for name in ("ProcessingInstructionHandler",
|
|
"CommentHandler",
|
|
"CharacterDataHandler",
|
|
"StartCdataSectionHandler",
|
|
"EndCdataSectionHandler",
|
|
"ExternalEntityRefHandler",
|
|
):
|
|
setattr(parser, name, None)
|
|
|
|
def start_element_handler(self, *args):
|
|
self._level = self._level + 1
|
|
|
|
def end_element_handler(self, *args):
|
|
if self._level == 0:
|
|
# restore the old handlers
|
|
parser = self._builder._parser
|
|
self._builder.install(parser)
|
|
parser.StartElementHandler = self._old_start
|
|
parser.EndElementHandler = self._old_end
|
|
else:
|
|
self._level = self._level - 1
|
|
|
|
class Skipper(FilterCrutch):
|
|
__slots__ = ()
|
|
|
|
def start_element_handler(self, *args):
|
|
node = self._builder.curNode
|
|
self._old_start(*args)
|
|
if self._builder.curNode is not node:
|
|
self._level = self._level + 1
|
|
|
|
def end_element_handler(self, *args):
|
|
if self._level == 0:
|
|
# We're popping back out of the node we're skipping, so we
|
|
# shouldn't need to do anything but reset the handlers.
|
|
self._builder._parser.StartElementHandler = self._old_start
|
|
self._builder._parser.EndElementHandler = self._old_end
|
|
self._builder = None
|
|
else:
|
|
self._level = self._level - 1
|
|
self._old_end(*args)
|
|
|
|
|
|
# framework document used by the fragment builder.
|
|
# Takes a string for the doctype, subset string, and namespace attrs string.
|
|
|
|
_FRAGMENT_BUILDER_INTERNAL_SYSTEM_ID = \
|
|
"http://xml.python.org/entities/fragment-builder/internal"
|
|
|
|
_FRAGMENT_BUILDER_TEMPLATE = (
|
|
'''\
|
|
<!DOCTYPE wrapper
|
|
%%s [
|
|
<!ENTITY fragment-builder-internal
|
|
SYSTEM "%s">
|
|
%%s
|
|
]>
|
|
<wrapper %%s
|
|
>&fragment-builder-internal;</wrapper>'''
|
|
% _FRAGMENT_BUILDER_INTERNAL_SYSTEM_ID)
|
|
|
|
|
|
class FragmentBuilder(ExpatBuilder):
|
|
"""Builder which constructs document fragments given XML source
|
|
text and a context node.
|
|
|
|
The context node is expected to provide information about the
|
|
namespace declarations which are in scope at the start of the
|
|
fragment.
|
|
"""
|
|
|
|
def __init__(self, context, options=None):
|
|
if context.nodeType == DOCUMENT_NODE:
|
|
self.originalDocument = context
|
|
self.context = context
|
|
else:
|
|
self.originalDocument = context.ownerDocument
|
|
self.context = context
|
|
ExpatBuilder.__init__(self, options)
|
|
|
|
def reset(self):
|
|
ExpatBuilder.reset(self)
|
|
self.fragment = None
|
|
|
|
def parseFile(self, file):
|
|
"""Parse a document fragment from a file object, returning the
|
|
fragment node."""
|
|
return self.parseString(file.read())
|
|
|
|
def parseString(self, string):
|
|
"""Parse a document fragment from a string, returning the
|
|
fragment node."""
|
|
self._source = string
|
|
parser = self.getParser()
|
|
doctype = self.originalDocument.doctype
|
|
ident = ""
|
|
if doctype:
|
|
subset = doctype.internalSubset or self._getDeclarations()
|
|
if doctype.publicId:
|
|
ident = ('PUBLIC "%s" "%s"'
|
|
% (doctype.publicId, doctype.systemId))
|
|
elif doctype.systemId:
|
|
ident = 'SYSTEM "%s"' % doctype.systemId
|
|
else:
|
|
subset = ""
|
|
nsattrs = self._getNSattrs() # get ns decls from node's ancestors
|
|
document = _FRAGMENT_BUILDER_TEMPLATE % (ident, subset, nsattrs)
|
|
try:
|
|
parser.Parse(document, 1)
|
|
except:
|
|
self.reset()
|
|
raise
|
|
fragment = self.fragment
|
|
self.reset()
|
|
## self._parser = None
|
|
return fragment
|
|
|
|
def _getDeclarations(self):
|
|
"""Re-create the internal subset from the DocumentType node.
|
|
|
|
This is only needed if we don't already have the
|
|
internalSubset as a string.
|
|
"""
|
|
doctype = self.context.ownerDocument.doctype
|
|
s = ""
|
|
if doctype:
|
|
for i in range(doctype.notations.length):
|
|
notation = doctype.notations.item(i)
|
|
if s:
|
|
s = s + "\n "
|
|
s = "%s<!NOTATION %s" % (s, notation.nodeName)
|
|
if notation.publicId:
|
|
s = '%s PUBLIC "%s"\n "%s">' \
|
|
% (s, notation.publicId, notation.systemId)
|
|
else:
|
|
s = '%s SYSTEM "%s">' % (s, notation.systemId)
|
|
for i in range(doctype.entities.length):
|
|
entity = doctype.entities.item(i)
|
|
if s:
|
|
s = s + "\n "
|
|
s = "%s<!ENTITY %s" % (s, entity.nodeName)
|
|
if entity.publicId:
|
|
s = '%s PUBLIC "%s"\n "%s"' \
|
|
% (s, entity.publicId, entity.systemId)
|
|
elif entity.systemId:
|
|
s = '%s SYSTEM "%s"' % (s, entity.systemId)
|
|
else:
|
|
s = '%s "%s"' % (s, entity.firstChild.data)
|
|
if entity.notationName:
|
|
s = "%s NOTATION %s" % (s, entity.notationName)
|
|
s = s + ">"
|
|
return s
|
|
|
|
def _getNSattrs(self):
|
|
return ""
|
|
|
|
def external_entity_ref_handler(self, context, base, systemId, publicId):
|
|
if systemId == _FRAGMENT_BUILDER_INTERNAL_SYSTEM_ID:
|
|
# this entref is the one that we made to put the subtree
|
|
# in; all of our given input is parsed in here.
|
|
old_document = self.document
|
|
old_cur_node = self.curNode
|
|
parser = self._parser.ExternalEntityParserCreate(context)
|
|
# put the real document back, parse into the fragment to return
|
|
self.document = self.originalDocument
|
|
self.fragment = self.document.createDocumentFragment()
|
|
self.curNode = self.fragment
|
|
try:
|
|
parser.Parse(self._source, 1)
|
|
finally:
|
|
self.curNode = old_cur_node
|
|
self.document = old_document
|
|
self._source = None
|
|
return -1
|
|
else:
|
|
return ExpatBuilder.external_entity_ref_handler(
|
|
self, context, base, systemId, publicId)
|
|
|
|
|
|
class Namespaces:
|
|
"""Mix-in class for builders; adds support for namespaces."""
|
|
|
|
def _initNamespaces(self):
|
|
# list of (prefix, uri) ns declarations. Namespace attrs are
|
|
# constructed from this and added to the element's attrs.
|
|
self._ns_ordered_prefixes = []
|
|
|
|
def createParser(self):
|
|
"""Create a new namespace-handling parser."""
|
|
parser = expat.ParserCreate(namespace_separator=" ")
|
|
parser.namespace_prefixes = True
|
|
return parser
|
|
|
|
def install(self, parser):
|
|
"""Insert the namespace-handlers onto the parser."""
|
|
ExpatBuilder.install(self, parser)
|
|
if self._options.namespace_declarations:
|
|
parser.StartNamespaceDeclHandler = (
|
|
self.start_namespace_decl_handler)
|
|
|
|
def start_namespace_decl_handler(self, prefix, uri):
|
|
"""Push this namespace declaration on our storage."""
|
|
self._ns_ordered_prefixes.append((prefix, uri))
|
|
|
|
def start_element_handler(self, name, attributes):
|
|
if ' ' in name:
|
|
uri, localname, prefix, qname = _parse_ns_name(self, name)
|
|
else:
|
|
uri = EMPTY_NAMESPACE
|
|
qname = name
|
|
localname = None
|
|
prefix = EMPTY_PREFIX
|
|
node = minidom.Element(qname, uri, prefix, localname)
|
|
node.ownerDocument = self.document
|
|
_append_child(self.curNode, node)
|
|
self.curNode = node
|
|
|
|
if self._ns_ordered_prefixes:
|
|
for prefix, uri in self._ns_ordered_prefixes:
|
|
if prefix:
|
|
a = minidom.Attr(_intern(self, 'xmlns:' + prefix),
|
|
XMLNS_NAMESPACE, prefix, "xmlns")
|
|
else:
|
|
a = minidom.Attr("xmlns", XMLNS_NAMESPACE,
|
|
"xmlns", EMPTY_PREFIX)
|
|
d = a.childNodes[0].__dict__
|
|
d['data'] = d['nodeValue'] = uri
|
|
d = a.__dict__
|
|
d['value'] = d['nodeValue'] = uri
|
|
d['ownerDocument'] = self.document
|
|
_set_attribute_node(node, a)
|
|
del self._ns_ordered_prefixes[:]
|
|
|
|
if attributes:
|
|
_attrs = node._attrs
|
|
_attrsNS = node._attrsNS
|
|
for i in range(0, len(attributes), 2):
|
|
aname = attributes[i]
|
|
value = attributes[i+1]
|
|
if ' ' in aname:
|
|
uri, localname, prefix, qname = _parse_ns_name(self, aname)
|
|
a = minidom.Attr(qname, uri, localname, prefix)
|
|
_attrs[qname] = a
|
|
_attrsNS[(uri, localname)] = a
|
|
else:
|
|
a = minidom.Attr(aname, EMPTY_NAMESPACE,
|
|
aname, EMPTY_PREFIX)
|
|
_attrs[aname] = a
|
|
_attrsNS[(EMPTY_NAMESPACE, aname)] = a
|
|
d = a.childNodes[0].__dict__
|
|
d['data'] = d['nodeValue'] = value
|
|
d = a.__dict__
|
|
d['ownerDocument'] = self.document
|
|
d['value'] = d['nodeValue'] = value
|
|
d['ownerElement'] = node
|
|
|
|
if __debug__:
|
|
# This only adds some asserts to the original
|
|
# end_element_handler(), so we only define this when -O is not
|
|
# used. If changing one, be sure to check the other to see if
|
|
# it needs to be changed as well.
|
|
#
|
|
def end_element_handler(self, name):
|
|
curNode = self.curNode
|
|
if ' ' in name:
|
|
uri, localname, prefix, qname = _parse_ns_name(self, name)
|
|
assert (curNode.namespaceURI == uri
|
|
and curNode.localName == localname
|
|
and curNode.prefix == prefix), \
|
|
"element stack messed up! (namespace)"
|
|
else:
|
|
assert curNode.nodeName == name, \
|
|
"element stack messed up - bad nodeName"
|
|
assert curNode.namespaceURI == EMPTY_NAMESPACE, \
|
|
"element stack messed up - bad namespaceURI"
|
|
self.curNode = curNode.parentNode
|
|
self._finish_end_element(curNode)
|
|
|
|
|
|
class ExpatBuilderNS(Namespaces, ExpatBuilder):
|
|
"""Document builder that supports namespaces."""
|
|
|
|
def reset(self):
|
|
ExpatBuilder.reset(self)
|
|
self._initNamespaces()
|
|
|
|
|
|
class FragmentBuilderNS(Namespaces, FragmentBuilder):
|
|
"""Fragment builder that supports namespaces."""
|
|
|
|
def reset(self):
|
|
FragmentBuilder.reset(self)
|
|
self._initNamespaces()
|
|
|
|
def _getNSattrs(self):
|
|
"""Return string of namespace attributes from this element and
|
|
ancestors."""
|
|
# XXX This needs to be re-written to walk the ancestors of the
|
|
# context to build up the namespace information from
|
|
# declarations, elements, and attributes found in context.
|
|
# Otherwise we have to store a bunch more data on the DOM
|
|
# (though that *might* be more reliable -- not clear).
|
|
attrs = ""
|
|
context = self.context
|
|
L = []
|
|
while context:
|
|
if hasattr(context, '_ns_prefix_uri'):
|
|
for prefix, uri in context._ns_prefix_uri.items():
|
|
# add every new NS decl from context to L and attrs string
|
|
if prefix in L:
|
|
continue
|
|
L.append(prefix)
|
|
if prefix:
|
|
declname = "xmlns:" + prefix
|
|
else:
|
|
declname = "xmlns"
|
|
if attrs:
|
|
attrs = "%s\n %s='%s'" % (attrs, declname, uri)
|
|
else:
|
|
attrs = " %s='%s'" % (declname, uri)
|
|
context = context.parentNode
|
|
return attrs
|
|
|
|
|
|
class ParseEscape(Exception):
|
|
"""Exception raised to short-circuit parsing in InternalSubsetExtractor."""
|
|
pass
|
|
|
|
class InternalSubsetExtractor(ExpatBuilder):
|
|
"""XML processor which can rip out the internal document type subset."""
|
|
|
|
subset = None
|
|
|
|
def getSubset(self):
|
|
"""Return the internal subset as a string."""
|
|
return self.subset
|
|
|
|
def parseFile(self, file):
|
|
try:
|
|
ExpatBuilder.parseFile(self, file)
|
|
except ParseEscape:
|
|
pass
|
|
|
|
def parseString(self, string):
|
|
try:
|
|
ExpatBuilder.parseString(self, string)
|
|
except ParseEscape:
|
|
pass
|
|
|
|
def install(self, parser):
|
|
parser.StartDoctypeDeclHandler = self.start_doctype_decl_handler
|
|
parser.StartElementHandler = self.start_element_handler
|
|
|
|
def start_doctype_decl_handler(self, name, publicId, systemId,
|
|
has_internal_subset):
|
|
if has_internal_subset:
|
|
parser = self.getParser()
|
|
self.subset = []
|
|
parser.DefaultHandler = self.subset.append
|
|
parser.EndDoctypeDeclHandler = self.end_doctype_decl_handler
|
|
else:
|
|
raise ParseEscape()
|
|
|
|
def end_doctype_decl_handler(self):
|
|
s = ''.join(self.subset).replace('\r\n', '\n').replace('\r', '\n')
|
|
self.subset = s
|
|
raise ParseEscape()
|
|
|
|
def start_element_handler(self, name, attrs):
|
|
raise ParseEscape()
|
|
|
|
|
|
def parse(file, namespaces=True):
|
|
"""Parse a document, returning the resulting Document node.
|
|
|
|
'file' may be either a file name or an open file object.
|
|
"""
|
|
if namespaces:
|
|
builder = ExpatBuilderNS()
|
|
else:
|
|
builder = ExpatBuilder()
|
|
|
|
if isinstance(file, str):
|
|
fp = open(file, 'rb')
|
|
try:
|
|
result = builder.parseFile(fp)
|
|
finally:
|
|
fp.close()
|
|
else:
|
|
result = builder.parseFile(file)
|
|
return result
|
|
|
|
|
|
def parseString(string, namespaces=True):
|
|
"""Parse a document from a string, returning the resulting
|
|
Document node.
|
|
"""
|
|
if namespaces:
|
|
builder = ExpatBuilderNS()
|
|
else:
|
|
builder = ExpatBuilder()
|
|
return builder.parseString(string)
|
|
|
|
|
|
def parseFragment(file, context, namespaces=True):
|
|
"""Parse a fragment of a document, given the context from which it
|
|
was originally extracted. context should be the parent of the
|
|
node(s) which are in the fragment.
|
|
|
|
'file' may be either a file name or an open file object.
|
|
"""
|
|
if namespaces:
|
|
builder = FragmentBuilderNS(context)
|
|
else:
|
|
builder = FragmentBuilder(context)
|
|
|
|
if isinstance(file, str):
|
|
fp = open(file, 'rb')
|
|
try:
|
|
result = builder.parseFile(fp)
|
|
finally:
|
|
fp.close()
|
|
else:
|
|
result = builder.parseFile(file)
|
|
return result
|
|
|
|
|
|
def parseFragmentString(string, context, namespaces=True):
|
|
"""Parse a fragment of a document from a string, given the context
|
|
from which it was originally extracted. context should be the
|
|
parent of the node(s) which are in the fragment.
|
|
"""
|
|
if namespaces:
|
|
builder = FragmentBuilderNS(context)
|
|
else:
|
|
builder = FragmentBuilder(context)
|
|
return builder.parseString(string)
|
|
|
|
|
|
def makeBuilder(options):
|
|
"""Create a builder based on an Options object."""
|
|
if options.namespaces:
|
|
return ExpatBuilderNS(options)
|
|
else:
|
|
return ExpatBuilder(options)
|