#!/usr/bin/python # Copyright (C) 2005 by Tapsell-Ferrier Limited # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2, or (at your option) # any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; see the file COPYING. If not, write to the # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, # Boston, MA 02110-1301 USA """Module to make libxslt as easy as possible. """ import os.path import StringIO import libxml2 import libxslt import logging import types import sys # Global stylesheet cache STYLESHEETS = {} class xsltError(Exception): """Something went wrong with the xslt process""" def __init__(self, value): self.value = value def __str__(self): return str(self.value) # Standard XSLT wrapper def xslt(stylesheet_xml, out, src_doc): """Push the doc or the filename through the stylesheet into out.""" logger = logging.getLogger("xslt") # Setup the error handler ## FIXME we should accept an error handler passed into this func. def error_handler(ctx, str): logger.error(str) return None libxslt.registerErrorHandler(error_handler, "") global STYLESHEETS script = None try: script = STYLESHEETS[stylesheet_xml] # Make sure that the stylesheet has not been updated recently try: filename = script.filename if os.path.getmtime(filename) > script.filename_cached_date: script = libxslt.parseStylesheetFile(filename) script.filename_cached_date = os.path.getmtime(filename) except AttributeError: pass except KeyError: try: if os.path.exists(stylesheet_xml): logger.info("stylesheet file is: %s" % (stylesheet_xml)) # FIXME: should record the file against the XSLT object here script = libxslt.parseStylesheetFile(stylesheet_xml) script.filename = stylesheet_xml script.filename_cached_date = os.path.getmtime(stylesheet_xml) # Try and load functions attached to the stylesheet register_modules(stylesheet_xml) else: xsl_doc = libxml2.readMemory(stylesheet_xml, len(stylesheet_xml), "file:///-", "UTF-8", 0) script = libxslt.parseStylesheetDoc(xsl_doc) except: raise xsltError("something went wrong: %s" % (stylesheet_xml)) # Record the stylesheet in the cache STYLESHEETS[stylesheet_xml] = script # Get a doc for the input if isinstance(src_doc, file): doc = libxml2.readFd(src_doc.fileno(), "file:///", "utf-8", 0) elif isinstance(src_doc, libxml2.xmlDoc): doc = src_doc elif isinstance(src_doc, str): doc = libxml2.readMemory(src_doc, len(src_doc), "file:///", "utf-8", 0) elif isinstance(src_doc, types.FunctionType): doc = __function_output_to_dom__(src_doc) else: raise xsltError(src_doc) # Transform it. result = script.applyStylesheet(doc, {}) # Handle the result if isinstance(out, libxml2.xmlNode): root = result.getRootElement() out.addChild(root) root.setTreeDoc(out.get_doc()) elif isinstance(out, file): script.saveResultToFile(out, result) else: str_val = script.saveResultToString(result) print >>out, str_val return None def __function_output_to_dom__(func): """Call the function with a stream to collect XML which will then be parsed.""" try: buffer = StringIO.StringIO() func(buffer) str_src = buffer.getvalue() buffer.close() dom = libxml2.readMemory(str_src, len(str_src), "file:///-", "UTF-8", 0) return dom except: raise xsltError(func) return None ## XSL type mapping def py_xslfn_typemap(value, node = None): """Map the Python value to some libxml2 value. Adds the value to the supplied node if it's not 'None'. """ if isinstance(value, dict): if node == None: node = libxml2.newDoc("1.0") # Render the mappings as XML mappings = node.newChild(None, "mappings", None) for key, data in value.iteritems(): pair = mappings.newChild(None, "mapping", None) py_xslfn_typemap(key, pair.newChild(None, "key", None)) py_xslfn_typemap(data, pair.newChild(None, "value", None)) return mappings elif hasattr(value, "__iter__") \ or isinstance(value, tuple) \ or isinstance(value, list): if node == None: node = libxml2.newDoc("1.0") # Now we need a list of items items = node.newChild(None, "items", None) for val in value: py_xslfn_typemap(val, items.newChild(None, "item", None)) return items elif isinstance(value, bool) \ or isinstance(value, int): ### how do you test for all numeric types? # If we have a node then add the data there if node != None: node.addContent(str(value)) # otherwise simply return it. else: return str(value) # Base case is that everythings a string else: if node != None: node.addContent(value) else: return str(value) return None # Stores allocations per context PER_CONTEXTS_ALLOC = {} def py_xslfn_glue(fname, ctx, *args): """Glue function to make ordinary python code accessible to XSLT It has to turn ordinary values into Xpath objects Importantly it has to turn objects like iterators, lists, tuples and dictionaries into elements in a DOM """ # First generate the result by calling the user's function result = fname(*args) # This is how to call the function... xml_value = py_xslfn_typemap(result, None) # Now check if we need to store a ptr for freeing the object later... ## FIXME: should py_xsl_typemap do this? global PER_CONTEXTS_ALLOC if isinstance(xml_value.doc, libxml2.xmlNode): # Can't return DOMs - only elements xml_value = xml_value.doc.getRootElement() # And they have to be unlinked xml_value.unlinkNode() # Save it for later try: lst = PER_CONTEXTS_ALLOC[ctx] except KeyError: lst = [] PER_CONTEXTS_ALLOC[ctx] = lst if xml_value not in lst: lst.append(xml_value) # Always return a list return [xml_value] # 'Dynamic' function loading import imp import re def register_modules(xslt_filename): """load functions associated with the stylesheet""" toload = re.match("(.*/)*([^./]+)\.xslt", xslt_filename) if len(toload.groups()) < 2: raise xsltError("unknown xslt filename '%s'" % (xslt_filename)) try: m = imp.find_module("_xsltpagefuncs_" + toload.groups()[1]) lm = imp.load_module("_xsltpagefuncs_" + toload.groups()[1], *m) for name, value in lm.__dict__.iteritems(): if re.match("[^_].*", name): # Need to take a copy to fix python's stupid scoping rules xslt_func = value libxslt.registerExtModuleFunction(name, "http://www.tapsellferrier.co.uk", lambda ctx, *str: py_xslfn_glue(xslt_func, ctx, *str)) except: pass return None # Simple xsltproc application if __name__ == "__main__": import pdb logging.basicConfig() if sys.argv[1]: fname = sys.argv[1] if os.path.exists(fname) and os.path.isfile(fname): src = "-" if len(sys.argv) > 2: if sys.argv[2]: src = sys.argv[2] if src == "-": xslt(fname, sys.stdout, sys.stdin) else: xslt(fname, sys.stdout, open(src)) # End