#!/usr/bin/python
#
# (cc) 2004 Marc-Antoine Parent. Some rights reserved.
# Redistribution information: Creative Commons - Attribution - ShareAlike
# http://creativecommons.org/licenses/by-sa/2.0/
# 

import libxml2, libxslt
import sys, os, re, time, datetime, sets, logging, urllib2

log = logging.getLogger("Tinderbox")
log.addHandler(logging.StreamHandler())


class _styleRun(object):
    """Utility class that encapsulates a run of styled text, defined by position"""
    __slots__ = ("start", "font", "bold", "italic", "underline", "size", "height", "ascent", "color")

    def __init__(self, start, font=None, bold=None, italic=None, underline=None, size=None, height=None, ascent=None, color=None, base=None):
        self.start = start
        if base:
            self.assign(base.font, base.bold, base.italic, base.underline, base.size, base.height, base.ascent, base.color)
        else:
            self.assign(font, bold, italic, underline, size, height, ascent, color)

    def assign(self, font=None, bold=None, italic=None, underline=None, size=None, height=None, ascent=None, color=None):
        if font is not None:
            self.font = font
        if color is not None:
            self.color = color
        if bold is not None:
            self.bold = bold
        if italic is not None:
            self.italic = italic
        if underline is not None:
            self.underline = underline
        if size is not None:
            self.size = size
        if height is not None:
            self.height = height
        if ascent is not None:
            self.ascent = ascent

    def asNode(self):
        node = libxml2.newNode('tstyle')
        node.newProp('start',str(self.start))
        node.newProp('font',str(self.font))
        node.newProp('bold',str(self.bold))
        node.newProp('italic',str(self.italic))
        node.newProp('underline',str(self.underline))
        node.newProp('size',str(self.size))
        node.newProp('height',str(self.height))
        node.newProp('ascent',str(self.ascent))
        node.newProp('color',self.color)
        return node

class _styleRuns(object):
    """Utility class that encapsulates a collection of styleRun, referring to a single text"""
    __slots__ = ("runs")
    def __init__(self, font, bold, italic, underline, size, height, ascent, color):
        self.runs = []
        args = (font or "Geneva", bold or 0, italic or 0, underline or 0, size or 10,
            height or 13, ascent or 10, color or "#000000")
        newRun = _styleRun(0, *args)
        self.runs.append(newRun)

    def assign(self, position, font=None, bold=None, italic=None, underline=None, size=None, height=None, ascent=None, color=None):
        last = self.runs[-1]
        if last.start > position:
            raise StandardError, "assign styles in order"
        elif last.start < position:
            last = _styleRun(position, base=last)
            self.runs.append(last)
        last.assign(font, bold, italic, underline, size, height, ascent, color)

    def asNode(self):
        styles = libxml2.newNode('styles')
        for node in self.runs:
            styles.addChild(node.asNode())
        return styles


#****************************************

class TinderboxDoc(libxslt.extensionModule):
    """This class encapsulates knowledge about a Tinderbox document, as a parsed
    libxml2 object. It contains methods to query (get...) and alter the content
    of the document, in a way that aims to preserve the Tinderbox file format.
    The altering methods generally expect a target, which is a (list of) nodes,
    and a value, which may be another (list of) notes.
    When the target and value are both sets of notes, the XPath determining the
    value will be recalculated relative to each target, in turn.
    Some altering methods that create a note or link also expect the name of the
    note or link as a further argument. 
    In the case of links, prefixing the name with "-" allows to designate links
    in reverse direction.
    The method transform will look for encoded versions of those altering methods
    in the Tinderbox file itself, and alter the document accordingly.
    These methods are also exposed as xpath extensions functions and xslt extension
    elements. (The names are altered to fit xsl conventions)"""

    ns_uri = "http://eastgate.com/Tinderbox"

    def __init__(self, document):
        """ Initialize the Tinderbox instance. """
        self.document = document                        # The libxml2.xmlDoc instance
        self.root = document.getRootElement()           # The root node (libxml2.xmlNode)
        self.documentVersion = (int(self.root.prop('version')) , int(self.root.prop('revision')))
        if self.documentVersion < (2 , 0) or self.documentVersion >= (3 , 0):
            raise RuntimeException("I do not know this document format")
        self.xpathContext = document.xpathNewContext()  # this context will be used for xpathEvals, 
                                                        # as it will have the registered 
        self.xpathContext2 = document.xpathNewContext() # This one is for quicker calculations
        self.contextNode = self.root                    # The context node for XPath evaluations
        self._registerXPathFunctions()
        self.ids = {}                                   # ids(long)->note(libxml2.xmlNode)
        self._lastNewId=0                               # the last new Id assigned
        self.prototypes = {}                            # ids of nodes(long)->prototype note(libxml2.xmlNode)
        self.styleRuns = None                           # Allows to calculate styles elements from within a stylesheet
        self.attribDefaults = {}
        self._prepareKeys()                             # Pre-calculate the content of ids, prototypes, links, rootItem
        self.results = {}
        libxslt.registerExtensionClass(TinderboxDoc.ns_uri, self)

    def _registerXPathFunctions(self):
        """Register XPath extension functions with the xpathContext"""
        self.xpathContext.xpathRegisterNs("tb", TinderboxDoc.ns_uri)
        for mName in dir(self):
            if mName.startswith("xpath"):
                boundMethod = getattr(self,mName)
                mName = TinderboxDoc.convertCamelToHyphens(mName[5:])
                self.xpathContext.registerXPathFunction(mName, TinderboxDoc.ns_uri, boundMethod)

    def childrenIterator(node):
        """Iterate over the children of a node"""
        c=node.children
        while c:
            yield c
            c = c.next
    childrenIterator=staticmethod(childrenIterator)

    def _prepareKeys(self):
        self._calculateIds(self.root)
        for c in TinderboxDoc.childrenIterator(self.root):
            if c.name == "item":
                self.rootItem=c     # the root item element, under the root (tinderbox) element
            elif c.name == "attrib":
                self._cacheAttribDefaults(c)
            elif c.name == "links":
                self.linksNode = c  # the links item element, under the root (tinderbox) element
                self._calculateProto(c)
                break

    def _cacheAttribDefaults(self, node):
        name = node.prop('Name')
        self.attribDefaults[name] = node.prop('default')
        if name == "User" and node.prop("parent") == "anything":
            self.userAttributes = node
        subAttribs = TinderboxDoc.childrenIterator(node)
        subAttribs = filter(lambda x: x.name == "attrib",subAttribs)
        for attrib in subAttribs:
            self._cacheAttribDefaults(attrib)

    def _calculateIds(self, node):
        self.xpathContext2.setContextNode(node)
        items = self.xpathContext2.xpathEval("//item")
        for c in items:
                id = long(c.prop('ID'))
                self.ids[id] = c

    def _declareNewNoteId(self, node):
        id = long(node.prop('ID'))
        self.ids[id] = node

    def _calculateProto(self, links):
        for link in TinderboxDoc.childrenIterator(links):
            if link.isText():
                continue
            if not link.name == 'link': raise TypeError, "Non-link under links"+link.name
            if link.prop('name') == 'prototype':
                noteId = long(link.prop('destid'))
                protoId = long(link.prop('sourceid'))
                if noteId == protoId: raise StandardError, "Self-prototype???"
                proto = self.getNoteById(protoId)
                self.prototypes[noteId]=proto
        self.prototypes['done']=True # To indicate that the calculation was done, even if no prototype exists


    def getNoteById(self, id):
        """ From an ID, obtain the node. Will raise an exception if the node does not exist."""
        return self.ids[long(id)]

    def getNoteByIdAllowNone(self, id):
        """ From an ID, obtain a node if the node exists."""
        if id not in self.ids: return None
        return self.ids[long(id)]

    def getNotesByIds(self,*ids):
        """ returns a list of nodes from a list of Ids """
        return map(self.getNoteById,ids)

    def getPrototypeById(self, id):
        """ get the prototype of a node from the node's ID"""
        if id in self.prototypes:
            return self.prototypes[id]
        return None

    def getPrototypeByNode(self, node):
        """ get the prototype of a node from the node"""
        id = long(node.prop('ID'))
        return self.getPrototypeById(id)

    def getPrototypeNameByNode(self, node):
        """ get the name of the prototype of a node from the node"""
        proto = self.getPrototypeByNode(node)
        if proto:
            return self.getProperty(proto, "Name")
        return None

    def getPrototypeByName(self, name):
        """ get a prototype node from its name, if it exists"""
        xpath="//item[attribute[@name='IsPrototype' and text() = 'true'] and attribute[@name='Name' and text() = '%s']]"%name
        self.xpathContext.setContextNode(self.root)
        r= self.xpathContext.xpathEval(xpath)
        if r:
            return r[0]
        return None

    def getLocalProperty(self, node, propName):
        """get the value of a Tinderbox property of a (list of) node, if defined at the node level."""
        if isinstance(node,list):
            return map(lambda x: self.getLocalProperty(x, propName),node)
        if (propName == 'ID'): return node.prop('ID')
        if (propName == 'Creator'): return node.prop('Creator')
        if (propName == 'Prototype'): return self.getPrototypeNameByNode(node)
        self.xpathContext2.setContextNode(node)
        text = self.xpathContext2.xpathEval("attribute[@name='%s']/text()"%propName)
        if text:
            return text[0].content
        return None

    def getProperty(self, node, propName):
        """get the value of a Tinderbox property of a (list of) node.
        If needed, will obtain the value from the prototype or property default"""
        if isinstance(node,list):
            return map(lambda x: self.getProperty(x, propName),node)
        prop = self.getLocalProperty(node, propName)
        if prop is not None:
            return prop
        # Should we resolve aliases? I think that we do not want aliases per se...
        id = long(node.prop('ID'))
        proto = self.getPrototypeById(id)
        if proto:
            return self.getProperty(proto, propName) # recursion allows for chains of prototypes
        return self.attribDefaults[propName]

    def getNewId(self):
        """This computes a new ID for a new node.
        Note: You have to associate it with the node in self.ids using _declareNewNoteId"""
        id = max(self._lastNewId+1,long(time.time())+2082826800) # difference between unix and MacOS epochs.
        while self.getNoteByIdAllowNone(id):
            id+=1
        self._lastNewId=id
        return id

    def getNoteFromAlias(self,alias):
        """This retrieves the original node from a (list of) alias nodes"""
        if isinstance(alias,list):
            return map(lambda x: self.getNoteFromAlias(x),alias)
        id = self.getLocalProperty(alias, "Alias")
        if not id:
            return alias
        id = long(id)
        if id < 0:
            id = id + (1L << 32)
        return self.getNoteById(id)

    def getLinks(self, nodes, linkType=None, fromTarget=True):
        """
        This returns a list of all notes linked from (to) the given notes
        the direction is given by the fromTarget argument
        optionally, restrict to a link type
        """
        val = self.getLinksIds(nodes, linkType,fromTarget)
        return map(lambda x: self.getNoteById(x), val)

    def getLinksIds(self, nodes, linkType=None, fromTarget=True):
        """
        This returns a list of the ids of all notes linked from (to) the given notes
        the direction is given by the fromTarget argument
        optionally, restrict to a link type
        """
        if isinstance(nodes,list):
            ids = sets.Set()
            for node in nodes:
                ids.union_update(self.getLinksIds(node, linkType, forwards))
            return list(ids)
        if not isinstance(nodes, libxml2.xmlCore): raise TypeError, "not a node"
        #if nodes.get_name() != "item": raise TypeError, "Not a tinderbox item: %s" % nodes
        id = nodes.prop('ID')
        typeRest=""
        if linkType:
            typeRest=" and @name='%s'"%linkType
        if fromTarget:
            dfrom = 'sourceid'
            dto = 'destid'
        else:
            dfrom = 'destid'
            dto = 'sourceid'
        xpath = "/tinderbox/links/link[@%s='%s'%s]/@%s" % (dfrom, id, typeRest, dto)
        val = self.evalXPath(xpath)
        val = map(lambda x: long(x.content), val)
        return val

    def getUrl(self, url):
        """ Obtain the content of a url as text.
        Uses the python liburl2, with a handler for XmlRpc calls"""
        urlFile = urllib2.urlopen(url)
        data = urlFile.read()
        if hasattr(urlFile,"info"):
            encoding = urlFile.info().getencoding() # usually wrong
            if encoding and encoding != '7bit':
                try:
                    data = data.decode(encoding)
                except:
                    pass
        return data

    _camelCapsRe=re.compile('([a-z])([A-Z])')

    def convertCamelToHyphens(name):
        """Convert a camel caps (python-style) method name to a
        hyphenated (xsl-style) function or element name"""
        return TinderboxDoc._camelCapsRe.sub(r'\1-\2',name).lower()
    convertCamelToHyphens = staticmethod(convertCamelToHyphens)

    def convertIdToAliasRef(id):
        """Old TB versions: Convert a Tinderbox ID as used by nodes and links
        (unsigned int) into an Alias'es IDREF (signed int)"""
        id = long(id)
        if id > sys.maxint:
            id = id - (1L << 32)
        return str(id)
    convertIdToAliasRef = staticmethod(convertIdToAliasRef)

    _tbDateFormat = "%d %b %Y %H:%M:%S"
    _isoDateFormat = "%Y-%m-%dT%H:%M:%S"

    def convertTbDateToDatetime(date):
        """ Convert a Tinderbox date (as used in date type attributes) into a Python datetime"""
        ts = time.strptime(date,TinderboxDoc._tbDateFormat)
        return datetime.datetime(*ts[0:6])
    convertTbDateToDatetime = staticmethod(convertTbDateToDatetime)
    
    def convertTbDateToIso(date):
        """ Convert a Tinderbox date (as used in date type attributes) into
        a ISO format datetime: 2004-01-31T13:30:00 """
        ts=time.strptime(date,TinderboxDoc._tbDateFormat)
        return time.strftime(TinderboxDoc._isoDateFormat,ts)
    convertTbDateToIso = staticmethod(convertTbDateToIso)
    
    def convertIsoDateToTb(date):
        """ Convert a ISO format datetime: 2004-01-31T13:30:00 into
        a Tinderbox date (as used in date type attributes) """
        format = TinderboxDoc._isoDateFormat
        if ':' not in date:
            format = format[:8]
        elif '-' not in date:
            format = format[9:]
        ts=time.strptime(date,format)
        result = time.strftime(TinderboxDoc._tbDateFormat,ts)
        if result[0]=='0': result = result[1:]
        return result
    convertIsoDateToTb = staticmethod(convertIsoDateToTb)

    def declareStyleChange(self, position=0, font=None, bold=None, italic=None, underline=None, size=None, height=None, ascent=None, color=None):
        """Declare a style change at the given text position in a text.
        There must be a declareStyleChange provided at position 0 of the text.
        (This only works well for brand new nodes, as inside a XSLT stylesheet.)
        Each element is optional; this method can be called many times at a given text
        position and the specified text attributes will combine.
        The resulting styles tag can be retrieved through implementStyleChange"""
        runs = self.styleRuns
        if runs:
            runs.assign(position, font, bold, italic, underline, size, height, ascent, color)
        else:
            runs = _styleRuns(font, bold, italic, underline, size, height, ascent, color)
            self.styleRuns = runs

    def implementStyleChange(self, insertNode):
        """Inserts the <styles> tag with tstyles provided by the previous
        declareStyleChange tags on the given node."""
        runs = self.styleRuns
        if not runs:
            raise StandardError, "No style was declared"
        insertNode.addChild(runs.asNode())
        self.styleRuns = None

    def createLink(self, targetNotes, valueNotesXPath, name, fromTargetToValue=True, startPosition=-1, length=0, newLinkSignature=None):
        """ create new links from each of a list of target Notes to a set of value notes.
        The value notes are given by XPath, recomputed relative to each target note.
        if fromTargetToValue is False, the links will go from the value to the target.
        Text start position and length can be given for nodes anchored in text.
        (this only makes sense for a single target.)
        TODO: if newLinkSignature (a XPath) is given, a signature will be computed using that XPath
        for the created link, and it will not be inserted if another link matches the same signature
        """
        if isinstance(targetNotes,list):
            return map(lambda x: self.createLink(x, valueNotesXPath, name, fromTargetToValue, startPosition, length, newLinkSignature), targetNotes)
        self.setTargetNote(targetNotes)
        values = self.evalXPath(valueNotesXPath)
        if fromTargetToValue:
            for v in values:
                self._createSingleLink(targetNotes, v, name, None, startPosition, length, newLinkSignature)
        else:
            for v in values:
                self._createSingleLink(v, targetNotes, name, None, startPosition, length, newLinkSignature)
        return targetNotes

    def _createSingleLink(self, sourceNote, destNote, name, url=None, startPosition=-1, length=0, newLinkSignature=None):
        if newLinkSignature:
            pass #TODO
        if url:
            destNote = self.rootItem
        node = libxml2.newNode('link')
        node.newProp('name',name)
        node.newProp('sourceid',sourceNote.prop('ID'))
        node.newProp('destid',destNote.prop('ID'))
        node.newProp('sourcecreator',sourceNote.prop('Creator'))
        node.newProp('destcreator',destNote.prop('Creator'))
        node.newProp('sstart', str(startPosition))
        node.newProp('slen',str(length))
        if url:
            node.newProp('URL',url)
        self.linksNode.addChild(node)

    def createUrlLink(self, targetNotes, valueUrl, name, startPosition=-1, length=0, newLinkSignature=None, deferred=False):
        """ create new links from each of a list of target Notes to a URL.
        Text start position and length can be given for nodes anchored in text.
        (this only makes sense for a single target.)
        TODO: if newLinkSignature (a XPath) is given, a signature will be computed using that XPath
        for the created link, and it will not be inserted if another link matches the same signature
        """
        if isinstance(targetNotes,list):
            return map(lambda x: self.createUrlLink(x, valueUrl, name, startPosition, length, newLinkSignature),targetNotes)
        self.setTargetNote(targetNotes)
        self._createSingleLink(targetNotes, self.rootItem, name, valueUrl, startPosition, length, newLinkSignature)
        return targetNotes

    def createAlias(self, targetNote, valueNotes):
        """Create aliases of each of the valueNotes under the targetNote.
        This acts as a traditional Tinderbox agent.
        If an alias to the same note already exists, it is not duplicated,
        but existing aliases are not cleared. """
        if isinstance(valueNotes,list):
            return map(lambda x: self.createAlias(targetNote, x),valueNotes)
        valueid = long(valueNotes.prop("ID"))
        if (self.documentVersion < (2 , 3)):
            valueid = TinderboxDoc.convertIdToAliasRef(valueid)
        self.xpathContext.setContextNode(targetNote)
        existingAlias = self.evalXPath(
            "item/attribute[@name='Alias' and text() = '%s']"%(str(valueid)))
        if existingAlias:
            return None
        alias = libxml2.newNode("item")
        alias.newProp("Creator",valueNotes.prop("Creator"))
        aliasid = self._makeNoteNew(alias, False)
        self.setProperty(alias, "Alias", str(valueid))
        targetNote.addChild(alias)
        return alias

    def setProperty(self, targetNotes, propName, value,evalXPath = False):
        """set the property 'propName' to the value 'value' on each of the (list of) target notes.
        If evalXPath is true, the value is considered to be a XPath, computed at each target note.
        TODO: Make sure the attribute type is declared in the Tinderbox attribute declarations
        """
        value = str(value)
        if isinstance(targetNotes,list):
            return map(lambda x: self.setProperty(x, propName, value, evalXPath),targetNotes)
        if evalXPath:
            self.setTargetNote(targetNotes)
            value = self.evalXPath(value)
            if isinstance(value,list):
                if value:
                    value = value[0]
                else:
                    return self.clearProperty(targetNotes,propName)
            value=str(value)
        if (propName == 'ID' or propName == 'Creator'):
            targetNotes.setProp(propName, value)
            return targetNotes
        if (propName == 'Prototype'):
            self.setPrototype(targetNotes,prototypeName=value)
        for c in TinderboxDoc.childrenIterator(targetNotes):
            if not c.isText():
                if c.name == "attribute":
                    if c.prop('name') == propName:
                        c.setContent(value)
                        return targetNotes
                else:
                    break
        prop = libxml2.newNode("attribute")
        prop.newProp("name",propName)
        prop.setContent(value)
        if targetNotes.children:
            targetNotes.children.addPrevSibling(prop)
        else:
            targetNotes.addChild(prop)
        return targetNotes

    def applyStylesheet(self, targetNote, valueXmlText, stylesheetXmlText, stylesheetParams, newItemSignature=None):
        """Apply the given stylesheet, with the given stylesheetParams (a dict)
        to the XML contained in valueUrl.
        Expects to obtain a (list of) items (and/or adornments) from the
        stylesheet and adds them to the targetNote.
        Note that the result of applying the stylesheet does not need to be a
        rooted xml document, but can be a series of XML fragments.
        If newItemSignature (a XPath) is given, a signature will be computed
        using that XPath for the newly created node, and the node will not be
        created if the target node already has a subnode with the same signature."""
        try:
            xmlData = libxml2.readDoc(valueXmlText,None,None,0)
        except (libxml2.treeError, libxml2.parserError):
            try:
                xmlData = libxml2.readDoc(valueXmlText,None,"iso-8859-1",0)
            except (libxml2.treeError, libxml2.parserError):
                try:
                    xmlData = libxml2.htmlReadDoc(valueXmlText,None,None,0)
                except (libxml2.treeError, libxml2.parserError):
                    try:
                        xmlData = libxml2.htmlReadDoc(valueXmlText,None,"iso-8859-1",0)
                    except (libxml2.treeError, libxml2.parserError):
                        raise StandardError, "Cannot parse"
        styleDoc = libxml2.parseDoc(stylesheetXmlText)
        style = libxslt.parseStylesheetDoc(styleDoc)
        result = style.applyStylesheet(xmlData, stylesheetParams)
        node = result.getRootElement()
        nodes=[]
        sigs={}
        if newItemSignature:
            for c in filter(lambda x: x.type == "element" and (x.name == "item" or x.name == "adornment"),
                TinderboxDoc.childrenIterator(targetNote)):
                self.xpathContext.setContextNode(c)
                sig=self.xpathContext.xpathEval(newItemSignature)
                sigs[sig]=c

        while node:
            nextNode = node.next
            if node.isText():
                node = nextNode
                continue
            if newItemSignature:
                self.xpathContext.setContextNode(node)
                sig=self.xpathContext.xpathEval(newItemSignature)
                if sig in sigs:
                    node = nextNode
                    continue
            self._declareNewNoteId(node)
            nodes.append(node)
            node.unlinkNode()
            targetNote.addChild(node)
            node = nextNode
        return nodes

    def moveNote(self, targetNote, valueNotes):
        """Move a (list of) value notes to a target Note"""
        for note in valueNotes:
            note.unlinkNode()
            targetNote.addChild(note)
        return valueNotes

    def copyNote(self, targetNote, valueNotes, recursive=True):
        """Move a (list of) value notes to a target Note"""
        copies = []
        for note in valueNotes:
            rec = 0
            if recursive: rec=1
            copy = note.copyNode(rec)
            copies.append(copy)
            targetNote.addChild(copy)
            if not recursive:
                # copy attributes and text at least...
                for c in self.childrenIterator(note):
                    if c.name is not "item":
                        copy.addChild(c.copyNode(1))
            self._makeNoteNew(copy,True,True)
        #TODO: Renew nodes (ID, created...)
        return copies

    def createNote(self, targetNotes, noteName, newItemSignature=None):
        """ Create a new note under each target note. """
        if isinstance(targetNotes,list):
            return map(lambda x: self.createNote(x, noteName, newItemSignature),targetNotes)
        newNote = libxml2.newNode("item")
        newNote.newProp("Creator",targetNotes.prop("Creator"))
        id = self._makeNoteNew(newNote)
        self.setProperty(newNote,"Name",noteName)
        targetNotes.addChild(newNote)
        return newNote

    def _makeNoteNew(self, newNote,setDate=True,recursive=False):
        """Ensure a newly created note has a new Id and correct creation date."""
        id = self.getNewId()
        newNote.setProp("ID", str(id))
        self._declareNewNoteId(newNote)
        if setDate:
            self.setProperty(newNote,"Created",
                                             datetime.datetime.now().strftime(TinderboxDoc._tbDateFormat))
        if recursive:
            for c in self.childrenIterator(newNote):
                if c.name == "item":
                    self._makeNoteNew(c,setDate,True)
        return id

    def deleteNote(self, targetNotes):
        """Removes the target nodes from the document """
        for note in targetNotes:
            note.unlinkNode()
        # I do not ajust self.ids because I want the result to be accurate...
        # Or should I instead return an empty result?
        return targetNotes

    def clearText(self, targetNotes):
        """Clears the text of the target nodes """
        if isinstance(targetNotes,list):
            return map(lambda x: self.clearText(x),targetNotes)
        for c in TinderboxDoc.childrenIterator(targetNotes):
            if c.type == "element":
                if c.name == "text" or c.name == "styles":
                    c.unlinkNode()
        return targetNotes

    def clearProperty(self, targetNotes, propName):
        """ Clears the named property of the target nodes"""
        if isinstance(targetNotes,list):
            return map(lambda x: self.clearProperty(x,propName),targetNotes)
        for c in TinderboxDoc.childrenIterator(targetNotes):
            if c.type == "element":
                if c.name == "attribute" and c.prop("name") == propName:
                    c.unlinkNode()
                    return targetNotes
        return None

    def setText(self, targetNotes, value, evalXPath=True):
        """ set the text of each target node to the value.
        If evalXPath is true, the value is a XPath, evaluated at each target."""
        if isinstance(targetNotes,list):
            return map(lambda x: self.setText(x, valueTextXPath),targetNotes)
        self.setTargetNote(targetNotes)
        if evalXPath:
            value = self.evalXPath(value)
            if isinstance(value,list):
                if value:
                    value = value[0]
                else:
                    return self.clearText(targetNotes)
        value=str(value)
        textFound = False
        lastAttrib = None
        for c in TinderboxDoc.childrenIterator(targetNotes):
            log.debug("%s %s %s",c,c.type,c.name)
            if c.type == "element":
                if c.name == "attribute":
                    lastAttrib=c
                if c.name == "text":
                    c.setContent(value)
                    textFound = True
                elif c.name == "styles":
                    c.unlinkNode()
        if not textFound:
            newNode = libxml2.newNode("text")
            newNode.setContent(value)
            if lastAttrib:
                lastAttrib.addNextSibling(newNode)
            else:
                targetNotes.children.addPrevSibling(newNode)
        return targetNotes

    def appendText(self, targetNotes, valueTextXPath):
        """ for each target node, evaluate the given XPath and append the resulting text to the target node's text"""
        if isinstance(targetNotes,list):
            return map(lambda x: self.appendText(x, valueTextXPath),targetNotes)
        self.setTargetNote(targetNotes)
        value = self.evalXPath(valueTextXPath)
        if isinstance(value,list):
            if value:
                value = value[0]
            else:
                return self.clearText(targetNotes)
        value=str(value)
        if not value:
            return None
        textFound = False
        for c in TinderboxDoc.childrenIterator(targetNotes):
            if c.type == "element" and c.name == "text":
                c.addContent(value)
                return targetNotes
        return self.setText(targetNotes,value,False)

    def setPrototype(self, targetNotes, prototypeNoteValue=None, prototypeName=None):
        """Set each target node's prototype to the given prototype. Give either the prototype's node or name."""
        if prototypeNoteValue == None:
            if prototypeName == None:
                raise StandardError, "Prototype node or name needed"
            prototypeNoteValue = self.getPrototypeByName(prototypeName)
        if isinstance(targetNotes,list):
            return map(lambda x: self.setPrototype(x, prototypeNoteValue),targetNotes)
        existingLinkXPath = "link[@destid='%s' and @name='prototype']"%targetNotes.prop("ID")
        existingLinks = self.linksNode.xpathEval(existingLinkXPath)
        if existingLinks:
            link = existingLinks[0]
        else:
            link = libxml2.newNode('link')
            link.newProp('name','prototype')
            link.newProp('destid',targetNotes.prop('ID'))
            link.newProp('destcreator',targetNotes.prop('Creator'))
            link.newProp('sstart', '0')
            link.newProp('slen','-1')
            self.linksNode.addChild(link)
        link.setProp("sourceid", prototypeNoteValue.prop("ID"))
        link.setProp("sourcecreator", prototypeNoteValue.prop("Creator"))
        return targetNotes

    def deleteLink(self, targetNotes, valueNotesXPath, name=None,fromTargetToValue=True):
        """ Not completed """
        pass #TODO. Trickier than it seems.

    def getIsInside(self, outsideNode, insideNode):
        """See whether 2nd node is inside first"""
        x1 = float(self.getProperty(outsideNode, "Xpos"))
        y1 = float(self.getProperty(outsideNode, "Ypos"))
        w1 = float(self.getProperty(outsideNode, "Width"))
        h1 = float(self.getProperty(outsideNode, "Height"))
        x2 = float(self.getProperty(insideNode, "Xpos"))
        y2 = float(self.getProperty(insideNode, "Ypos"))
        w2 = float(self.getProperty(insideNode, "Width"))
        h2 = float(self.getProperty(insideNode, "Height"))
        return bool(x1 <= x2 and x1+w1 >= x2+w2 and y1 <= y2 and y1+h1 >= y2+h2)

    def getOverlaps(self, node1, node2):
        """See whether two nodes overlap"""
        x1 = float(self.getProperty(node1, "Xpos"))
        y1 = float(self.getProperty(node1, "Ypos"))
        w1 = float(self.getProperty(node1, "Width"))
        h1 = float(self.getProperty(node1, "Height"))
        x2 = float(self.getProperty(node2, "Xpos"))
        y2 = float(self.getProperty(node2, "Ypos"))
        w2 = float(self.getProperty(node2, "Width"))
        h2 = float(self.getProperty(node2, "Height"))
        left = max(x1,x2)
        right = min(x1+w1,x2+w2)
        top = max(y1,y2)
        bottom = min(y1+h1,y2+h2)
        return bool(left < right and top < bottom)


    def executeScriptForCommandNote(self, declaringNode):
        """ Execute the Python function given in the declaring node """
        text=None
        for c in TinderboxDoc.childrenIterator(declaringNode):
            if c.type == "element" and c.name == "text":
                text = c.content
        if not text:
            raise StandardError, "function missing!"
        #TODO: Get Args from the same args as XSLStylesheet params?
        self.evalScript(text,self.xpathContext)
        #Cannot use the return value: Nodes expected.
        return [declaringNode]

    def evalScript(self, text, ctx, *args):
        """ Execute the given python function with the given arguments, and return the result"""
        m = re.search(r"\bdef\s+(\w+)\s*\(",text)
        if not m:
            raise StandardError, "Could not find a function definition"
        try:
            funcName = m.group(1)
            exec text
            func = locals()[funcName]
            if not func:
                raise StandardError, "function missing!"
            r = func(self, *args)
            return r
        except:
            log.exception( "Unexpected while executing \n%s%s\n:",text,args)
            return None

    def wrapXPathRetVal(self, returnValue):
        """ wrap a return value in a way suitable for XPath functions"""
        if not returnValue:
            return []
        elif isinstance(returnValue, libxml2.xmlNode):
            return [returnValue]
        else:
            return returnValue

    def wrapXPathArgument(self, node, context=None):
        """ On input to a XPath function, the current node may not be given,
        or may be given as a CPython object. Obtain a libxml2.xmlNode from the context."""
        wrap = lambda x: libxml2.xmlNode(_obj=x)
        if node:
            if isinstance(node, list):
                node = map(wrap,node)
                if len(node) == 1:
                    node = node[0]
            else:
                node = wrap(node)
        elif context:
            parserContext = libxml2.xpathParserContext(_obj= context)
            xpathContext = parserContext.context()
            try:
                node = xpathContext.contextNode()
            except:
                return None
        return node

    def xpathNodeById(self, ctx, *ids):
        """Backwards-compatibility for inconsistent naming"""
        return self.xpathNoteById(ctx, *ids)

    def xpathNoteById(self, ctx, *ids):
        """Return a (list of) Tinderbox note items given a (list of) numeric ID."""
        if not ids: return []
        ids = [self.wrapXPathArgument(id, ctx) for id in ids]
        def wrap(id):
            if isinstance(id, libxml2.xmlNode):
                id = id.content
            return int(id)
        ids = [wrap(id) for id in ids]
        return self.wrapXPathRetVal(self.getNotesByIds(*ids))

    def xpathError(self, ctx, msg=None):
        """ Generate an error message... Used in "xmlResult" to signal an error."""
        raise Exception, msg

    def xpathProperty(self, ctx, propName, node=None):
        """ Evaluate the value of a named Tinderbox property on the current note.
        If the value is not defined for that note, will go up the prototype chain to
        look for the value, and if it is not found, it will return the default for that property."""
        node = self.wrapXPathArgument(node, ctx)
        if isinstance(propName,list): propName = propName[0]
        if type(propName).__name__ == 'PyCObject': propName = libxml2.nodeWrap(propName).content
        if not node:return []
        return self.getProperty(node, propName)

    def xpathLocalProperty(self, ctx, propName, node=None):
        """Evaluate the value of a named Tinderbox property on the current note.
        Will return a value only if the note defines this property locally. Compare xpathProperty(). """
        node = self.wrapXPathArgument(node, ctx)
        if not node:return []
        return self.wrapXPathRetVal(self.getLocalProperty(node, propName))

    def xpathPrototype(self, ctx, node=None):
        """ From a (list of) note, retrieve the name of its prototype.
        (I use the name to conform to the way Tinderbox handles Prototype, somewhat as a property.)"""
        node = self.wrapXPathArgument(node, ctx)
        if not node:return []
        return self.wrapXPathRetVal(self.getPrototypeNameByNode(node))

    def xpathNewId(self, ctx):
        """ Return an ID for a new Tinderbox node. This ID does not exist in the document, and will not be used again."""
        return str(self.getNewId())

    def xpathNoteFromAlias(self, ctx, node=None):
        """ given an alias note, return the underlying note."""
        node = self.wrapXPathArgument(node, ctx)
        if not node:return []
        return self.wrapXPathRetVal(self.getNoteFromAlias(node))

    def xpathTargetNote(self, ctx):
        """ Returns the note which is the target of the current command. Allows to anchor XPaths for search."""
        return [self.targetNote]

    def xpathCommandNote(self, ctx):
        """ Returns the note containing the command"""
        return [self.commandNote]

    def xpathRootNote(self, ctx):
        """ Returns the root note of the Tinderbox document."""
        return [self.rootItem]

    def xpathLinks(self, ctx, nodes,linkType="",forwards=True):
        """ Given a set of nodes, give all the notes which are linked from them
        (or to them if direction  evaluates to false.)
        If linkName is specified, only the links of this type will be considered."""
        nodes = self.wrapXPathArgument(nodes, ctx)
        if not nodes:return []
        return self.wrapXPathRetVal(self.getLinks(nodes,linkType, forwards))

    def xpathPrototypeByName(self, ctx, name):
        """Returns the prototype note that has a given name, if any. """
        node = self.getPrototypeByName(name)
        if node:
            return [node]
        return []

    def xpathCurrentTextPosition(self, ctx):
        """ For XSL stylesheets:
        Retrieves the number of text characters already in the note being constructed.
        Useful for expressing link anchors in terms of text positions."""
        parserContext = libxslt.xpathParserContext(_obj= ctx)
        xpathContext = parserContext.context()
        transformContext = xpathContext.transformContext()
        currentNode = transformContext.insertNode()
        return str(len(currentNode.content))

    def xpathNoteInConstruction(self, ctx):
        """ For use in XSL stylesheets:
        Returns the note being constructed. Useful for declaring a link on an existing note."""
        parserContext = libxslt.xpathParserContext(_obj= ctx)
        xpathContext = parserContext.context()
        transformContext = xpathContext.transformContext()
        currentNode = transformContext.insertNode()
        while currentNode:
            if currentNode.type == "element" and currentNode.name=="item":
                return [currentNode]
            currentNode=currentNode.parent
        return []

    def xpathIsInside(self, ctx, outsideNode, insideNode):
        """ For use in XSL: Is second node inside first node? """
        outsideNode = self.wrapXPathArgument(outsideNode, ctx)
        insideNode = self.wrapXPathArgument(insideNode, ctx)
        answer = self.getIsInside(outsideNode, insideNode)
        if answer: return [insideNode]
        else: return []

    def xpathOverlaps(self, ctx, n1, n2):
        """ For use in XSL: Is second node inside first node? """
        n1 = self.wrapXPathArgument(n1, ctx)
        n2 = self.wrapXPathArgument(n2, ctx)
        answer = self.getOverlaps(n1,n2)
        if answer: return [n1]
        else: return []

    def xpathEvalScript(self, ctx, text, *args):
        """ evaluate an arbitrary python function, given in the text.
        That function will recieve the TinderboxDoc instance as first arguments,
        and then any other argument passed to the eval-script."""
        parserContext = libxslt.xpathParserContext(_obj= ctx)
        xpathContext = parserContext.context()
        text = self.wrapXPathArgument(text, ctx)
        if isinstance(text,libxml2.xmlNode): text = text.content
        args = map(lambda x: self.wrapXPathArgument(x, ctx),args)
        returnVal = self.evalScript(text, xpathContext, *args)
        return self.wrapXPathRetVal(returnVal)

    def xpathGetUrl(self, ctx, url):
        """ Obtain a url from the web as text."""
        return self.getUrl(url)

    def xpathTbdateToIsodate(self, ctx, date):
        """ Convert a Tinderbox date (as used in date type attributes) into
        a ISO format datetime: 2004-01-31T13:30:00 """
        return self.convertTbDateToIso(date)

    def xpathResultOf(self, ctx, node):
        """ """
        node = self.wrapXPathArgument(node, ctx)
        if not node:return []
        id = long(node.prop("ID"))
        result = self.results[id]
        if isinstance(result, Exception):
            raise result
        return self.wrapXPathRetVal(result)

    _activationPropName="xmlActivation"
    _commandPropName="xmlCommand"
    _namePropName="xmlName"
    _targetPropName="xmlTarget"
    _uniqueItemSignaturePropName="xmlUniqueItemSignature"
    _resultPropName="xmlResult"
    _stylesheetPropName="xmlStylesheet"
    _stylesheetParamsPropName="xmlStylesheetParams"
    _valuePropName="xmlValue"

    _createLinkCommandName="CreateLink"
    _createUrlLinkCommandName="CreateUrlLink"
    _createAliasCommandName="CreateAlias"
    _moveNoteCommandName="MoveNote"
    _createNoteCommandName="CreateNote"
    _deleteNoteCommandName="DeleteNote"
    _deleteLinkCommandName="DeleteLink"
    _setPropertyCommandName="SetProperty"
    _setPrototypeCommandName="SetPrototype"
    _clearPropertyCommandName="ClearProperty"
    _executeScriptCommandName="ExecuteScript"
    _applyStylesheetCommandName="ApplyStylesheet"
    _appendTextCommandName="AppendText"
    _setTextCommandName="SetText"
    _clearTextCommandName="ClearText"
    _doOnceCommandName="DoOnce"
    _copyNoteCommandName="CopyNote"
    _evalCommandName="Eval"
    _forEachCommandName="ForEach"

    def resultToXPath(self, result):
        """ Given a an arbitrary value, attempt to generate a XPath that would evaluate back
        to the same result. It is a way to retrieve results from previous operations."""
        if not result: return ""
        if isinstance(result, list):
            result = filter(lambda x: isinstance(x,libxml2.xmlNode) and x.name == "item",result)
            result = map(lambda x: x.prop('ID'),result)
            return "tb:note-by-id(%s)"%(','.join(result))
        elif isinstance(result,libxml2.xmlNode) and result.name == "item":
            return "tb:note-by-id(%s)"%result.prop("ID")
        elif isinstance(result,(int,long,float)):
            return float(result)
        else:
            return "'%s'"%str(result)

    def _evalCommandArgument(self, commandNote, argName, allowMultiple = True, allowNone=False, evalXPath = True):
        prop = self.getProperty(commandNote, argName)
        if not prop:
            if allowNone:
                if allowMultiple:
                    return []
                else:
                    return None
            else:
                raise AttributeError, "Missing %s for note %s"%(argName, commandNote.prop('ID'))
        if evalXPath:
            xpath=prop
            prop = self.evalXPathDisallowNone(xpath)
            if not prop and not allowNone:
                raise AttributeError, "evaluating %s for note %s yields nothing"%(xpath, commandNote.prop('ID'))
        if not allowMultiple and isinstance(prop,list):
            if prop:
                prop = prop[0]
            else:
                prop = None
        return prop

    def evalXPathDisallowNone(self,xpath):
        """ Evaluate a given XPath. use self.contextNode as the current node.
        Will raise an libxml2.xpathError if the result is empty."""
        self.xpathContext.setContextNode(self.contextNode)
        return self.xpathContext.xpathEval(xpath)

    def evalXPath(self,xpath):
        """ Evaluate a given XPath. use self.contextNode as the current node. The result may be empty."""
        self.xpathContext.setContextNode(self.contextNode)
        try:
            return self.xpathContext.xpathEval(xpath)
        except libxml2.xpathError:
            return []

    def setCommandNote(self, node):
        """ sets the current command note"""
        self.commandNote = node
        self.contextNode = node

    def setTargetNote(self, node):
        """ sets the current target note"""
        self.targetNote = node
        self.contextNode = node

    def transform(self, item=None):
        """ Executes the transformation on a Tinderbox document, starting at the given node,
        according to the commands in that document.
        A Tinderbox note is a command by virtue of containing a valid command in the xmlCommand attribute.
        The commands are enabled or disabled according to the xmlActivation boolean attribute.
        The commands are executed in document order.
        All other command arguments are given as XPaths. Note that literals must be 'single-quoted'.
        The command arguments are extracted from the command note attribute, and passed to the
        corresponding method.
        The result of the command will be a XPath which evaluates to the set of notes affected;
        this XPath will be put in the xmlResult attribute.
        """
        if not item:
            item = self.root
        recurse = True
        command = self.getLocalProperty(item,TinderboxDoc._commandPropName)
        if command:
            recurse = self._doOneCommand(item)
        if recurse:
            for c in TinderboxDoc.childrenIterator(item):
                if c.name == "item":
                    self.transform(c)

    def _doOneCommand(self,commandNote):
        result = []
        uniqueSig = None
        recurse = True
        try:
            activation = self.getProperty(commandNote, TinderboxDoc._activationPropName)
            if activation == 'false':
                return False
            self.setCommandNote(commandNote)
            command = self.getLocalProperty(commandNote, TinderboxDoc._commandPropName)
            commandNoteId = long(commandNote.prop("ID"))
            if command == TinderboxDoc._doOnceCommandName:
                self.setProperty(commandNote, TinderboxDoc._activationPropName, 'false')
                return True
            elif command == TinderboxDoc._evalCommandName:
                result = self._evalCommandArgument(commandNote, TinderboxDoc._targetPropName,allowNone=True)
            elif command == TinderboxDoc._forEachCommandName:
                recurse = False # handled locally
                targets = self._evalCommandArgument(commandNote, TinderboxDoc._targetPropName,allowNone=True)
                for target in targets:
                    self.results[commandNoteId]=target
                    for c in TinderboxDoc.childrenIterator(commandNote):
                        if c.name == "item":
                            self.transform(c)
                result=targets
            elif command == TinderboxDoc._createLinkCommandName:
                fromTargetToValue=True
                targets = self._evalCommandArgument(commandNote, TinderboxDoc._targetPropName)
                valueXPath = self.getProperty(commandNote, TinderboxDoc._valuePropName)
                name = self._evalCommandArgument(commandNote, TinderboxDoc._namePropName, False, False)
                if name[0] == '-':
                    name = name[1:]
                    fromTargetToValue=False
                uniqueSig = self.getProperty(commandNote, TinderboxDoc._uniqueItemSignaturePropName)
                result = self.createLink(targets,valueXPath,name, fromTargetToValue,newLinkSignature=uniqueSig)
            elif command == TinderboxDoc._createUrlLinkCommandName:
                fromTargetToValue=True
                targets = self._evalCommandArgument(commandNote, TinderboxDoc._targetPropName)
                valueUrl = self._evalCommandArgument(commandNote, TinderboxDoc._valuePropName,allowNone=False)
                name = self._evalCommandArgument(commandNote, TinderboxDoc._namePropName, False, False)
                if name[0] == '-':
                    name = name[1:]
                    fromTargetToValue=False
                uniqueSig = self.getProperty(commandNote, TinderboxDoc._uniqueItemSignaturePropName)
                result = self.createUrlLink(targets, valueUrl,name, fromTargetToValue,newLinkSignature=uniqueSig)
            elif command == TinderboxDoc._deleteLinkCommandName:
                fromTargetToValue=True
                targets = self._evalCommandArgument(commandNote, TinderboxDoc._targetPropName)
                valueXPath = self.getProperty(commandNote, TinderboxDoc._valuePropName)
                name = self._evalCommandArgument(commandNote, TinderboxDoc._namePropName, False, False)
                if name[0] == '-':
                    name = name[1:]
                    fromTargetToValue=False
                result = self.createUrlLinks(targets, valueXPath,name, fromTargetToValue)
            elif command == TinderboxDoc._createAliasCommandName:
                target = self._evalCommandArgument(commandNote, TinderboxDoc._targetPropName, False)
                self.setTargetNote(target)
                valueNotes = self._evalCommandArgument(commandNote, TinderboxDoc._valuePropName, allowNone=True)
                result = self.createAlias(target,valueNotes)
            elif command == TinderboxDoc._moveNoteCommandName:
                target = self._evalCommandArgument(commandNote, TinderboxDoc._targetPropName, False)
                self.setTargetNote(target)
                valueNotes = self._evalCommandArgument(commandNote, TinderboxDoc._valuePropName, allowNone=True)
                result = self.moveNote(target,valueNotes)
            elif command == TinderboxDoc._copyNoteCommandName:
                target = self._evalCommandArgument(commandNote, TinderboxDoc._targetPropName, False)
                self.setTargetNote(target)
                valueNotes = self._evalCommandArgument(commandNote, TinderboxDoc._valuePropName, allowNone=True)
                result = self.copyNote(target,valueNotes)
            elif command == TinderboxDoc._createNoteCommandName:
                target = self._evalCommandArgument(commandNote, TinderboxDoc._targetPropName, allowNone=True)
                name = self._evalCommandArgument(commandNote, TinderboxDoc._namePropName, False, False)
                uniqueSig = self.getProperty(commandNote, TinderboxDoc._uniqueItemSignaturePropName)
                #valueNotes = self._evalCommandArgument(commandNote, TinderboxDoc._valuePropName, allowNone=True)
                result = self.createNote(target,name,uniqueSig)
            elif command == TinderboxDoc._deleteNoteCommandName:
                target = self._evalCommandArgument(commandNote, TinderboxDoc._targetPropName, allowNone=True)
                if target:
                    result = self.deleteNote(target)
            elif command == TinderboxDoc._setPropertyCommandName:
                target = self._evalCommandArgument(commandNote, TinderboxDoc._targetPropName, allowNone=True)
                if target:
                    name = self._evalCommandArgument(commandNote, TinderboxDoc._namePropName, False, False)
                    valueXPath = self.getProperty(commandNote, TinderboxDoc._valuePropName)
                    result = self.setProperty(target,name,valueXPath,True)
            elif command == TinderboxDoc._setPrototypeCommandName:
                target = self._evalCommandArgument(commandNote, TinderboxDoc._targetPropName, allowNone=True)
                if target:
                    prototypeName = self._evalCommandArgument(commandNote, TinderboxDoc._namePropName, False,True)
                    prototype = self._evalCommandArgument(commandNote, TinderboxDoc._valuePropName, False,True)
                    if prototype:
                        if isinstance(prototype, str):
                            prototypeName = str
                            prototype = None
                    else:
                        prototype = None
                    result = self.setPrototype(target,prototype,prototypeName)
            elif command == TinderboxDoc._clearPropertyCommandName:
                target = self._evalCommandArgument(commandNote, TinderboxDoc._targetPropName, allowNone=True)
                if target:
                    name = self._evalCommandArgument(commandNote, TinderboxDoc._namePropName, False, False)
                    result = self.clearProperty(target,name)
            elif command == TinderboxDoc._appendTextCommandName:
                target = self._evalCommandArgument(commandNote, TinderboxDoc._targetPropName, allowNone=True)
                if target:
                    valueXPath = self.getProperty(commandNote, TinderboxDoc._valuePropName)
                    result = self.appendText(target,valueXPath)
            elif command == TinderboxDoc._setTextCommandName:
                target = self._evalCommandArgument(commandNote, TinderboxDoc._targetPropName, allowNone=True)
                if target:
                    valueXPath = self.getProperty(commandNote, TinderboxDoc._valuePropName)
                    result = self.setText(target,valueXPath)
            elif command == TinderboxDoc._clearTextCommandName:
                target = self._evalCommandArgument(commandNote, TinderboxDoc._targetPropName, allowNone=True)
                if target:
                    result = self.clearText(target)
            elif command == TinderboxDoc._executeScriptCommandName:
                result = self.executeScriptForCommandNote(commandNote)
            elif command == TinderboxDoc._applyStylesheetCommandName:
                target = self._evalCommandArgument(commandNote, TinderboxDoc._targetPropName, False)
                self.setTargetNote(target)
                valueText = self._evalCommandArgument(commandNote, TinderboxDoc._valuePropName,False,False)
                if isinstance(valueText, libxml2.xmlCore):
                    valueText = valueText.content
                stylesheetText = self._evalCommandArgument(commandNote, TinderboxDoc._stylesheetPropName, False, False)
                if isinstance(stylesheetText, libxml2.xmlCore):
                    stylesheetText = stylesheetText.content
                stylesheetParams = self.getProperty(commandNote, TinderboxDoc._stylesheetParamsPropName)
                params = {}
                if stylesheetParams:
                    for p in stylesheetParams.split(";"):
                        (k,v) = p.split("=")
                        v=self.evalXPath(v)
                        if v:
                            v=v[0]
                        else:
                            v=""
                        params[k]=str(v)
                uniqueSig = self.getProperty(commandNote, TinderboxDoc._uniqueItemSignaturePropName)
                result = self.applyStylesheet(target, valueText, stylesheetText, params,uniqueSig)
            else:
                raise NotImplementedError, "Unknown command: "+command
            self.results[commandNoteId] = result
            self.setProperty(commandNote, TinderboxDoc._resultPropName, self.resultToXPath(result))
            return recurse
        except Exception, inst:
            log.exception("Trying to execute %s on node %s", command, str(commandNoteId))
            self.results[commandNoteId] = inst
            try:
                self.setProperty(commandNote, TinderboxDoc._resultPropName, "tb:error('%s')"%str(inst))
            except:
                log.exception("Error setting the error message")
            return False

    def compileExtension(self, style, inst, func):
        """ Empty method to keep stylesheets happy..."""
        pass

    def styleInit(self, style, uri):
        """ from the libxslt.extensionModule protocol. Called on a new stylesheet to register
        the XPath and XSLT extension elements."""
        for mName in dir(self):
            if mName.startswith("xpath"):
                boundMethod = getattr(self,mName)
                mName = TinderboxDoc.convertCamelToHyphens(mName[5:])
                libxslt.registerExtModuleFunction(mName, uri,boundMethod)
            if mName.startswith("xslt"):
                boundMethod = getattr(self,mName)
                mName = TinderboxDoc.convertCamelToHyphens(mName[4:])
                libxslt.registerExtModuleElement(mName, uri,self.compileExtension,boundMethod)

    def xsltDeclareNewLink(self, tctxt, sourceNode, instruction, compiledInst):
        """<tb:declare-new-link source="." dest="tb:root-note()"
            name="'href'" url="'http://www.example.com/'" start="0" len="-1"/>
        This declares a new link between two nodes, which will be created later.
        For each node, you can either provide the node itself, or its Id.
        Node creators are deduced unless provided. (for new nodes, we have to assume creator is 'system'.)
        If you give a URL, the destId is optional. If the start is not given, but startId
        is given, assume current text position in the output text. 
        Must then be matched by a tb:endNewLink."""
        tctxt = libxslt.transformCtxt(_obj=tctxt)
        sourceNode = libxml2.nodeWrap(sourceNode)
        instruction = libxml2.nodeWrap(instruction)
        xpathContext = tctxt.context()
        args = map(lambda x: instruction.prop(x),("source","dest","name", "url","start","len"))
        def f(x):
            if x is not None:
                xpathContext.setContextNode(sourceNode)
                x=xpathContext.xpathEval(x)
                if isinstance(x,list):
                    if x:
                        x=x[0]
                    else:
                        x=None
                if isinstance(x,libxml2.xmlAttr):
                    x=x.content
            return x
        args = map(f,args)
        log.debug(str(args))
        if not args[4]: args[4]="-1" #start
        if not args[5]: args[5]="0" #slen
        if not args[1]:
            args[1]=self.rootItem
        self._createSingleLink(*args)
        return []

    def xsltDeclareStyleChange(self, tctxt, sourceNode, instruction, compiledInst):
        """<tb:declare-style-change font="Courier" bold="1" italic="1" underline="1"
            size="10" height="12" ascent="12" color="\x2300000" />
        Declare a style change at the current text position in a text run.
        There must be a tb:declareStyleChange provided at 
        the beginning of the text run. Each element optional; can be called
        many times at a given text position and the specified
        text attributes will combine."""
        instruction = libxml2.nodeWrap(instruction)
        args = map(lambda x: instruction.prop(x),("font", "bold", "italic", "underline", "size", "height", "ascent", "color"))
        tctxt = libxslt.transformCtxt(_obj=tctxt)
        insertNode = tctxt.insertNode()
        contentL = len(insertNode.content)
        runs = self.styleRuns
        if runs:
            runs.assign(contentL, *args)
        else:
            runs = _styleRuns(*args)
            self.styleRuns = runs

    def xsltImplementStyleChange(self, tctxt, sourceNode, instruction, compiledInst):
        """<tb:implement-style-change/>
        Inserts (at the current position) the <style> tag with tstyles provided by
        the previous <tb:declareStyleChange> tags."""
        tctxt = libxslt.transformCtxt(_obj=tctxt)
        insertNode = tctxt.insertNode()
        runs = self.styleRuns
        if not runs:
            raise StandardError, "No style was declared"
        insertNode.addChild(runs.asNode())
        self.styleRuns = None

    def xsltApplyStylesheet(self, tctxt, sourceNode, instruction, compiledInst):
        """
        <tb:apply-stylesheet stylesheet="..." data="...a"/>
        Apply the stylesheet (whose text is given by a XPath in 'stylesheet',
        possibly using get-url) to the XML data (text given by a XPath in 'data')
        and insert the result at the current note.
        Stylesheet parameters: not implemented yet.
        """
        instruction = libxml2.nodeWrap(instruction)
        tctxt = libxslt.transformCtxt(_obj=tctxt)
        insertNode = tctxt.insertNode()
        sourceNode = libxml2.nodeWrap(sourceNode)
        self.contextNode=sourceNode
        xmlData = instruction.prop("data")
        xmlData = self.evalXPath(xmlData)
        if isinstance(xmlData, list):
            xmlData = xmlData[0]
        if isinstance(xmlData, libxml2.xmlCore):
            xmlData = xmlData.content
        stylesheetData = instruction.prop("stylesheet")
        stylesheetData = self.evalXPath(stylesheetData)
        if isinstance(stylesheetData, list):
            stylesheetData = stylesheetData[0]
        if isinstance(stylesheetData, libxml2.xmlCore):
            stylesheetData = stylesheetData.content
        #instruction.prop("signature")
        self.applyStylesheet(insertNode, xmlData, stylesheetData,{},None)

