Viewing file: StylesheetHandler.py (29.06 KB) -rw-r--r-- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
######################################################################## # $Header: /var/local/cvsroot/4Suite/Ft/Xml/Xslt/StylesheetHandler.py,v 1.58 2005/04/07 20:05:57 jkloth Exp $ """ Stylesheet tree generator
Copyright 2004 Fourthought, Inc. (USA). Detailed license and copyright information: http://4suite.org/COPYRIGHT Project home, documentation, distributions: http://4suite.org/ """
from Ft.Lib import Truncate, UriException from Ft.Xml import XML_NAMESPACE, EMPTY_NAMESPACE, Domlette from Ft.Xml.Lib.XmlString import IsXmlSpace from Ft.Xml.Xslt import XSL_NAMESPACE, MessageSource from Ft.Xml.Xslt import XsltException, XsltParserException, Error from Ft.Xml.Xslt import CategoryTypes, BuiltInExtElements from Ft.Xml.Xslt import Exslt
from LiteralElement import LiteralElement from UndefinedElements import UndefinedXsltElement, UndefinedExtensionElement
import StylesheetTree, ContentInfo, AttributeInfo
# Table for load-on-demand of the XSLT elements _ELEMENT_MAPPING = { 'apply-templates' : 'ApplyTemplatesElement.ApplyTemplatesElement', 'apply-imports' : 'ApplyImportsElement.ApplyImportsElement', 'attribute' : 'AttributeElement.AttributeElement', 'attribute-set' : 'AttributeSetElement.AttributeSetElement', 'call-template' : 'CallTemplateElement.CallTemplateElement', 'choose' : 'ChooseElement.ChooseElement', 'when' : 'ChooseElement.WhenElement', 'otherwise' : 'ChooseElement.OtherwiseElement', 'copy' : 'CopyElement.CopyElement', 'copy-of' : 'CopyOfElement.CopyOfElement', 'comment' : 'CommentElement.CommentElement', 'element' : 'ElementElement.ElementElement', 'for-each' : 'ForEachElement.ForEachElement', 'if' : 'IfElement.IfElement', 'message' : 'MessageElement.MessageElement', 'number' : 'NumberElement.NumberElement', 'param' : 'ParamElement.ParamElement', 'processing-instruction' : 'ProcessingInstructionElement.ProcessingInstructionElement', 'sort' : 'SortElement.SortElement', 'stylesheet' : 'Stylesheet.StylesheetElement', 'transform' : 'Stylesheet.StylesheetElement', 'template' : 'TemplateElement.TemplateElement', 'text' : 'TextElement.TextElement', 'variable' : 'VariableElement.VariableElement', 'value-of' : 'ValueOfElement.ValueOfElement', 'with-param' : 'WithParamElement.WithParamElement', 'import' : 'OtherXslElement.ImportElement', 'include' : 'OtherXslElement.IncludeElement', 'decimal-format' : 'OtherXslElement.DecimalFormatElement', 'key' : 'OtherXslElement.KeyElement', 'namespace-alias' : 'OtherXslElement.NamespaceAliasElement', 'output' : 'OtherXslElement.OutputElement', 'fallback' : 'OtherXslElement.FallbackElement', 'preserve-space' : 'WhitespaceElements.PreserveSpaceElement', 'strip-space' : 'WhitespaceElements.StripSpaceElement', }
# The XSL attributes allowed on literal elements _RESULT_ELEMENT_XSL_ATTRS = { 'exclude-result-prefixes' : AttributeInfo.Prefixes(), 'extension-element-prefixes' : AttributeInfo.Prefixes(), 'use-attribute-sets' : AttributeInfo.QNames(), 'version' : AttributeInfo.Number(), } _RESULT_ELEMENT_ATTR_INFO = AttributeInfo.AnyAvt()
_ELEMENT_CLASSES = {} _LEGAL_ATTRS = {}
class ParseState: """ Stores the current state of the parser.
Constructor arguments/instance variables: validation - validation state for the current containing node.
localVariables - set of in-scope variable bindings to determine variable shadowing.
forwardsCompatible - flag indicating whether or not forwards-compatible processing is enabled.
currentNamespaces - set of in-scope namespaces for the current node.
extensionNamespaces - set of namespaces defining extension elements
outputNamespaces - set of in-scope namespaces for literal result elements """ def __init__(self, node, validation, localVariables, forwardsCompatible, currentNamespaces, extensionNamespaces, outputNamespaces): self.node = node self.validation = validation self.localVariables = localVariables self.forwardsCompatible = forwardsCompatible self.currentNamespaces = currentNamespaces self.extensionNamespaces = extensionNamespaces self.outputNamespaces = outputNamespaces return
class StylesheetHandler: """ Handles SAX events coming from the stylesheet parser, in order to build the stylesheet tree. """
def __init__(self, importIndex=0, globalVars=None, extElements=None, visitedStyUris=None, altBaseUris=None, ownerDocument=None): self._import_index = importIndex if globalVars is None: # We need to make sure that the same dictionary is used # through the entire processing (even if empty) self._global_vars = {} else: self._global_vars = globalVars if extElements is None: self._extElements = d = {} d.update(Exslt.ExtElements) d.update(BuiltInExtElements.ExtElements) else: self._extElements = extElements
self._visited_stylesheet_uris = visitedStyUris or {} self._alt_base_uris = altBaseUris or [] self._ownerDoc = ownerDocument return
def reset(self): self._global_vars = {} self._import_index = 0 self._visited_stylesheet_uris = {} self._ownerDoc = None return
def clone(self): return self.__class__(self._import_index, self._global_vars, self._extElements, self._visited_stylesheet_uris, self._alt_base_uris, self._ownerDoc)
def getResult(self): return self._ownerDoc
def addExtensionElementMapping(self, elementMapping): """ Add a mapping of extension element names to classes to the existing mapping of extension elements.
This should only be used for standalone uses of this class. The only known standalone use for this class is for creating compiled stylesheets. The benefits of compiled stylesheets are now so minor that this use case may also disappear and then so will this function. You have been warned. """ self._extElements = self._extElements.copy() self._extElements.update(elementMapping) return
# -- ContentHandler interface --------------------------------------
def setDocumentLocator(self, locator): self._locator = locator return
def startDocument(self): """ ownerDoc is supplied when processing an XSLT import or include. """ # Our root is always a document # We use a document for this because of error checking and # because we explicitly pass ownerDocument to the nodes as # they are created document_uri = self._locator.getSystemId() root = StylesheetTree.XsltRoot(document_uri)
if not self._ownerDoc: self._ownerDoc = root
# the stylesheet element instance self._stylesheet = None
self._state_stack = [ ParseState(node=root, validation=root.validator.getValidation(), localVariables={}, forwardsCompatible=False, currentNamespaces={'xml' : XML_NAMESPACE, None : None}, extensionNamespaces={}, outputNamespaces={}) ]
# for recursive include checks for xsl:include/xsl:import self._visited_stylesheet_uris[document_uri] = True
# namespaces added for the next element self._new_namespaces = {} return
def endDocument(self): self._import_index += 1 self._locator = None return
def startPrefixMapping(self, prefix, uri): self._new_namespaces[prefix] = uri return
def startElementNS(self, expandedName, qualifiedName, attribs): state = ParseState(**self._state_stack[-1].__dict__)
# ---------------------------------------------------------- # update in-scope namespaces if self._new_namespaces: d = state.currentNamespaces = state.currentNamespaces.copy() d.update(self._new_namespaces)
d = state.outputNamespaces = state.outputNamespaces.copy() for prefix, uri in self._new_namespaces.items(): if uri not in (XML_NAMESPACE, XSL_NAMESPACE): d[prefix] = uri
# reset for next element self._new_namespaces = {} # ---------------------------------------------------------- # get the class defining this element namespace, local = expandedName xsl_class = ext_class = None category = CategoryTypes.RESULT_ELEMENT if namespace == XSL_NAMESPACE: try: xsl_class = _ELEMENT_CLASSES[local] except KeyError: # We need to try to import (and cache) it try: module = _ELEMENT_MAPPING[local] except KeyError: if not state.forwardsCompatible: raise XsltParserException(Error.XSLT_ILLEGAL_ELEMENT, self._locator, local) xsl_class = UndefinedXsltElement else: parts = module.split('.') path = '.'.join(['Ft.Xml.Xslt'] + parts[:-1]) module = __import__(path, {}, {}, parts[-1:]) try: xsl_class = module.__dict__[parts[-1]] except KeyError: raise ImportError('.'.join(parts)) _ELEMENT_CLASSES[local] = xsl_class _LEGAL_ATTRS[xsl_class] = xsl_class.legalAttrs.items() xsl_class.validator = ContentInfo.Validator(xsl_class.content) category = xsl_class.category elif namespace in state.extensionNamespaces: try: ext_class = self._extElements[(namespace, local)] except KeyError: ext_class = UndefinedExtensionElement else: if ext_class not in _LEGAL_ATTRS: ext_class.validator = \ ContentInfo.Validator(ext_class.content) legal_attrs = ext_class.legalAttrs if legal_attrs is not None: _LEGAL_ATTRS[ext_class] = legal_attrs.items()
# ---------------------------------------------------------- # verify that this element can be declared here validation_else = ContentInfo.ELSE if category is not None: next = state.validation.get(category) if next is None and validation_else in state.validation: next = state.validation[validation_else].get(category) else: next = None if next is None: next = state.validation.get(expandedName) if next is None and validation_else in state.validation: next = state.validation[ContentInfo.ELSE].get(expandedName) if next is None: #self._debug_validation(expandedName) parent = state.node if parent is self._stylesheet: if (XSL_NAMESPACE, 'import') == expandedName: raise XsltParserException(Error.ILLEGAL_IMPORT, self._locator) elif parent.expandedName == (XSL_NAMESPACE, 'choose'): if (XSL_NAMESPACE, 'otherwise') == expandedName: raise XsltParserException(Error.ILLEGAL_CHOOSE_CHILD, self._locator) # ignore whatever elements are defined within an undefined # element as an exception will occur when/if this element # is actually instantiated if not isinstance(parent, UndefinedExtensionElement): raise XsltParserException(Error.ILLEGAL_ELEMENT_CHILD, self._locator, qualifiedName, parent.nodeName) else: # save this state for next go round self._state_stack[-1].validation = next
# ---------------------------------------------------------- # create the instance defining this element klass = (xsl_class or ext_class or LiteralElement) instance = klass(self._ownerDoc, namespace, local, self._locator.getSystemId()) instance.lineNumber = self._locator.getLineNumber() instance.columnNumber = self._locator.getColumnNumber() instance.importIndex = self._import_index instance.namespaces = state.currentNamespaces instance.nodeName = qualifiedName # -- XSLT element -------------------------------------- if xsl_class: # Handle attributes in the null-namespace inst_dict = instance.__dict__ for attr_name, attr_info in _LEGAL_ATTRS[xsl_class]: attr_expanded = (None, attr_name) if attr_expanded in attribs: value = attribs[attr_expanded] del attribs[attr_expanded] elif attr_info.required: raise XsltParserException( Error.MISSING_REQUIRED_ATTRIBUTE, self._locator, qualifiedName, attr_name) else: value = None try: value = attr_info.prepare(instance, value) except XsltException, e: raise self._mutate_exception(e)
if local in ('stylesheet', 'transform'): self._stylesheet = instance self._handle_standard_attr(state, instance, attr_name, value) else: if '-' in attr_name: attr_name = attr_name.replace('-', '_') inst_dict['_' + attr_name] = value
if attribs: # Process attributes with a namespace-uri and check for # any illegal attributes in the null-namespace for expanded in attribs: attr_ns, attr_name = expanded if attr_ns is None: if not state.forwardsCompatible: raise XsltParserException( Error.ILLEGAL_NULL_NAMESPACE_ATTR, self._locator, attr_name, qualifiedName) else: instance.attributes[expanded] = attribs[expanded]
# XSLT Spec 2.6 - Combining Stylesheets if local in ('import', 'include'): self._combine_stylesheet(instance._href, (local == 'import'))
# -- extension element --------------------------------- elif ext_class: validate_attributes = (ext_class in _LEGAL_ATTRS) if validate_attributes: # Handle attributes in the null-namespace inst_dict = instance.__dict__ for attr_name, attr_info in _LEGAL_ATTRS[ext_class]: attr_expanded = (None, attr_name) if attr_expanded in attribs: value = attribs[attr_expanded] del attribs[attr_expanded] elif attr_info.required: raise XsltParserException( Error.MISSING_REQUIRED_ATTRIBUTE, self._locator, qualifiedName, attr_name) else: value = None try: value = attr_info.prepare(instance, value) except XsltException, e: raise self_mutate_exception(e) if '-' in attr_name: attr_name = attr_name.replace('-', '_') inst_dict['_' + attr_name] = value
# Process attributes with a namespace-uri and check for # any illegal attributes in the null-namespace if attribs: for expanded in attribs: attr_ns, attr_name = expanded value = attribs[expanded] if validate_attributes and attr_ns is None: raise XsltParserException( Error.ILLEGAL_NULL_NAMESPACE_ATTR, self._locator, attr_name, qualifiedName) elif attr_ns == XSL_NAMESPACE: self._handle_result_element_attr(state, instance, qualifiedName, attr_name, value) else: instance.attributes[expanded] = value
# -- literal result element ---------------------------- else: output_attrs = [] for expanded in attribs: attr_ns, attr_local = expanded value = attribs[expanded] if attr_ns == XSL_NAMESPACE: self._handle_result_element_attr(state, instance, qualifiedName, attr_local, value) else: instance.attributes[expanded] = value # prepare attributes for literal output value = _RESULT_ELEMENT_ATTR_INFO.prepare(instance, value) attr_qname = attribs.getQNameByName(expanded) output_attrs.append((attr_qname, attr_ns, value))
# save information for literal output instance._output_namespace = namespace instance._output_nss = state.outputNamespaces instance._output_attrs = output_attrs
# Check for top-level result-element in null namespace parent = state.node if parent is self._stylesheet and \ not namespace and not state.forwardsCompatible: raise XsltParserException(Error.ILLEGAL_ELEMENT_CHILD, self._locator, qualifiedName, parent.nodeName)
# ---------------------------------------------------------- # update depth information state.node = instance state.validation = instance.validator.getValidation() self._state_stack.append(state)
if instance.doesPrime: self._ownerDoc.primeInstructions.append(instance) if instance.doesIdle: self._ownerDoc.idleInstructions.append(instance) return
def endElementNS(self, expandedName, qualifiedName): state = self._state_stack.pop() element = state.node if len(self._state_stack) == 1 and isinstance(element, LiteralElement): # a literal result element as stylesheet try: version = element._version except AttributeError: raise XsltParserException(Error.LITERAL_RESULT_MISSING_VERSION, self._locator)
# FIXME: use the prefix from the document for the XSL namespace stylesheet = (XSL_NAMESPACE, u'stylesheet') self.startElementNS(stylesheet, u'xsl:stylesheet', {(None, u'version') : version})
template = (XSL_NAMESPACE, u'template') self.startElementNS(template, u'xsl:template', {(None, u'match') : u'/'})
# make this element the template's content self._state_stack[-1].node.appendChild(element)
self.endElementNS(template, u'xsl:template') self.endElementNS(stylesheet, u'xsl:stylesheet') else: self._state_stack[-1].node.appendChild(element)
if expandedName in ((XSL_NAMESPACE, u'variable'), (XSL_NAMESPACE, u'param')): name = element._name # one for the root and one for the stylesheet or # a literal result element as stylesheet if len(self._state_stack) > 2 or \ isinstance(self._state_stack[-1].node, LiteralElement): # local variables # it is safe to ignore import precedence here local_vars = self._state_stack[-1].localVariables if name in local_vars: raise XsltParserException(Error.ILLEGAL_SHADOWING, self._locator, name) # Copy on use if local_vars is self._state_stack[-2].localVariables: local_vars = local_vars.copy() self._state_stack[-1].localVariables = local_vars local_vars[name] = True else: # global variables existing = self._global_vars.get(name, -1) if self._import_index > existing: self._global_vars[name] = self._import_index elif self._import_index == existing: raise XsltParserException(Error.DUPLICATE_TOP_LEVEL_VAR, self._locator, name) return
def characters(self, data): state = self._state_stack[-1] # verify that the current element can have text children validation = state.validation token = ContentInfo.TEXT_NODE next = validation.get(token) if next is None and ContentInfo.ELSE in validation: next = validation[ContentInfo.ELSE].get(token) if next is None: # If the parent can have element children, but not text nodes, # ignore pure whitespace nodes. This clarification is from # XSLT 2.0 [3.4] Whitespace Stripping. # e.g. xsl:stylesheet, xsl:apply-templates, xsl:choose if not (ContentInfo.EMPTY in validation and IsXmlSpace(data)): raise XsltParserException(Error.ILLEGAL_TEXT_CHILD_PARSE, self._locator, repr(Truncate(data, 10)), state.node.nodeName) #self._debug_validation(expandedName) else: # update validation state.validation = next
node = StylesheetTree.XsltText(self._ownerDoc, self._locator.getSystemId(), data) state.node.appendChild(node) return
# -- utility functions --------------------------------------------- def _combine_stylesheet(self, href, is_import): hint = is_import and 'STYLESHEET IMPORT' or 'STYLESHEET INCLUDE' try: new_source = self._input_source.resolve(href, pubid=None, hint=hint) except (OSError, UriException): for uri in self._alt_base_uris: try: new_href = self._input_source.getUriResolver().normalize(href, uri) #Do we need to figure out a way to pass the hint here? new_source = self._input_source.factory.fromUri(new_href) break except (OSError, UriException): pass else: raise XsltParserException(Error.INCLUDE_NOT_FOUND, self._locator, href, self._locator.getSystemId())
# XSLT Spec 2.6.1, Detect circular references in stylesheets # Note, it is NOT an error to include/import the same stylesheet # multiple times, rather that it may lead to duplicate definitions # which are handled regardless (variables, params, templates, ...) if new_source.uri in self._visited_stylesheet_uris: raise XsltParserException(Error.CIRCULAR_INCLUDE, self._locator, new_source.uri) else: self._visited_stylesheet_uris[new_source.uri] = True
# Create a new reader to handle the inclusion include = self.clone().fromSrc(new_source)
# Make sure we detect circular imports/includes, but not other duplicates del self._visited_stylesheet_uris[new_source.uri]
# The stylesheet containing the import will always be one higher # than the imported stylesheet. # # For example: # stylesheet A imports stylesheets B and C in that order, # stylesheet B imports stylesheet D, # stylesheet C imports stylesheet E. # The resulting import precedences are: # A=4, C=3, E=2, B=1, D=0
# Always update the precedence from the included stylesheet # because it may have contained imports thus increasing its # import precedence. import_index = include.importIndex + is_import self._stylesheet.importIndex = self._import_index = import_index
# merge the top-level elements self._stylesheet.children.extend(include.children) for child in include.children: child.parent = self._stylesheet return
def _handle_standard_attr(self, state, instance, name, value): if name == 'extension-element-prefixes': # a whitespace separated list of prefixes ext = state.extensionNamespaces = state.extensionNamespaces.copy() out = state.outputNamespaces = state.outputNamespaces.copy() for prefix in value: # add the namespace URI to the set of extension namespaces try: uri = instance.namespaces[prefix] except KeyError: raise XsltParserException(Error.UNDEFINED_PREFIX, self._locator, prefix or '#default') ext[uri] = True
# remove all matching namespace URIs for output_prefix, output_uri in out.items(): if output_uri == uri: del out[output_prefix] elif name == 'exclude-result-prefixes': # a whitespace separated list of prefixes out = state.outputNamespaces = state.outputNamespaces.copy() for prefix in value: try: uri = instance.namespaces[prefix] except KeyError: raise XsltParserException(Error.UNDEFINED_PREFIX, self._locator, prefix or '#default') # remove all matching namespace URIs for output_prefix, output_uri in out.items(): if output_uri == uri: del out[output_prefix] elif name == 'version': # XSLT Spec 2.5 - Forwards-Compatible Processing state.forwardsCompatible = (value != 1.0) instance._version = value else: if '-' in name: name = name.replace('-', '_') instance.__dict__['_' + name] = value return
def _handle_result_element_attr(self, state, instance, elementName, attributeName, value): try: attr_info = _RESULT_ELEMENT_XSL_ATTRS[attributeName] except KeyError: raise XsltParserException(Error.ILLEGAL_XSL_NAMESPACE_ATTR, self._locator, attributeName, elementName) value = attr_info.prepare(instance, value) self._handle_standard_attr(state, instance, attributeName, value) return
def _mutate_exception(self, exception): msg = MessageSource.POSITION_INFO % (self._locator.getSystemId(), self._locator.getLineNumber(), self._locator.getColumnNumber(), exception.message) exception.message = msg return exception
# -- debugging routines -------------------------------------------- def _debug_validation(self, token=None, next=None): from pprint import pprint state = self._state_stack[-1] parent = state.node print '='*60 print 'parent =',parent print 'parent class =',parent.__class__ print 'content expression =', parent.validator print 'initial validation' pprint(parent.validator.getValidation()) print 'current validation' pprint(state.validation) if token: print 'token', token if next: print 'next validation' pprint(next) print '='*60 return
|