XSLTools

XSLForms/Output.py

233:15159e66b39a
2005-09-30 paulb [project @ 2005-09-30 22:53:43 by paulb] Added special "macro" attribute support: simpler attributes for use in templates. Also added experimental stylesheets for schema creation and for the production of generic processing stylesheets.
     1 #!/usr/bin/env python     2      3 """     4 XSL-based form templating.     5      6 Copyright (C) 2005 Paul Boddie <paul@boddie.org.uk>     7      8 This library is free software; you can redistribute it and/or     9 modify it under the terms of the GNU Lesser General Public    10 License as published by the Free Software Foundation; either    11 version 2.1 of the License, or (at your option) any later version.    12     13 This library is distributed in the hope that it will be useful,    14 but WITHOUT ANY WARRANTY; without even the implied warranty of    15 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU    16 Lesser General Public License for more details.    17     18 You should have received a copy of the GNU Lesser General Public    19 License along with this library; if not, write to the Free Software    20 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA    21 """    22     23 import Constants    24 import libxsltmod, libxml2mod    25 import libxml2dom    26 import urllib    27     28 """    29 import libxml2    30     31 def quiet(context, s):    32     pass    33     34 libxml2.registerErrorHandler(quiet, None)    35 """    36     37 def path_to_node(node, attribute_ref, name, multivalue=0):    38     39     """    40     Generate an XSLForms path to the given 'node', producing an attribute    41     reference if 'attribute_ref' is true; for example:    42     43     /package$1/discriminators$5/discriminator$1/category    44     45     Otherwise an element reference is produced; for example:    46     47     /package$1/discriminators$5/discriminator$1    48     49     Use the given 'name' to complete the path if an attribute reference is    50     required (and if a genuine attribute is found at the context node -    51     otherwise 'name' will be None and the context node will be treated like an    52     attribute).    53     54     If 'multivalue' is true and 'attribute_ref' is set, produce an attribute    55     reference using the given 'name':    56     57     /package$1/categories$1/category$$name    58     59     If 'multivalue' is true and 'attribute_ref' is not set, produce an attribute    60     reference using the given 'name' of form (element, attribute):    61     62     /package$1/categories$1/element$$attribute    63     """    64     65     l = []    66     # Skip attribute reference.    67     if node.nodeType == node.ATTRIBUTE_NODE:    68         node = node.parentNode    69     # Manually insert the attribute name if defined.    70     if attribute_ref:    71         # A real attribute is referenced.    72         if name is not None:    73             l.insert(0, name)    74             if multivalue:    75                 l.insert(0, Constants.multi_separator)    76                 l.insert(0, node.nodeName)    77                 node = node.parentNode    78             l.insert(0, Constants.path_separator)    79         # Otherwise, treat the element name as an attribute name.    80         # NOTE: Not sure how useful this is.    81         else:    82             l.insert(0, node.nodeName)    83             l.insert(0, Constants.path_separator)    84             node = node.parentNode    85     # Otherwise insert any multivalue references (eg. list-attribute).    86     elif multivalue:    87         element_name, attribute_name = name    88         l.insert(0, attribute_name)    89         l.insert(0, Constants.multi_separator)    90         l.insert(0, element_name)    91         l.insert(0, Constants.path_separator)    92     93     # Element references.    94     while node is not None and node.nodeType != node.DOCUMENT_NODE:    95         l.insert(0, str(int(node.xpath("count(preceding-sibling::*) + 1"))))    96         l.insert(0, Constants.pair_separator)    97         l.insert(0, node.nodeName)    98         l.insert(0, Constants.path_separator)    99         node = node.parentNode   100     return "".join(l)   101    102 def path_to_context(context, attribute_ref, multivalue_name=None):   103    104     """   105     As a libxslt extension function, return a string containing the XSLForms   106     path to the 'context' node, using the special "this-name" variable to   107     complete the path if an attribute reference is required (as indicated by   108     'attribute_ref' being set to true). If 'multivalue_name' is set, produce a   109     reference to a multivalued field using the given string as the attribute   110     name.   111     """   112    113     context = libxml2mod.xmlXPathParserGetContext(context)   114     transform_context = libxsltmod.xsltXPathGetTransformContext(context)   115     name_var = libxsltmod.xsltVariableLookup(transform_context, "this-name", None)   116     if multivalue_name is not None:   117         name = multivalue_name   118         multivalue = 1   119     elif name_var is not None:   120         name = libxml2mod.xmlNodeGetContent(name_var[0])   121         name = unicode(name, "utf-8")   122         multivalue = 0   123     else:   124         name = None   125         multivalue = 0   126     node = libxml2dom.Node(libxml2mod.xmlXPathGetContextNode(context))   127     return path_to_node(node, attribute_ref, name, multivalue)   128    129 # Exposed extension functions.   130    131 def this_element(context):   132    133     """   134     Exposed as {template:this-element()}.   135     Provides a reference to the current element in the form data structure.   136     """   137    138     #print "this_element"   139     r = path_to_context(context, 0)   140     return r.encode("utf-8")   141    142 def this_attribute(context):   143    144     """   145     Exposed as {template:this-attribute()}.   146     Provides a reference to the current attribute in the form data structure.   147     """   148    149     #print "this_attribute"   150     r = path_to_context(context, 1)   151     return r.encode("utf-8")   152    153 def new_attribute(context, name):   154    155     """   156     Exposed as {template:new-attribute(name)}.   157     Provides a reference to a new attribute of the given 'name' on the current   158     element in the form data structure.   159     """   160    161     #print "new_attribute"   162     name = unicode(name, "utf-8")   163     r = path_to_context(context, 0) + "/" + name   164     return r.encode("utf-8")   165    166 def other_elements(context, nodes):   167    168     """   169     Exposed as {template:other-elements(nodes)}.   170     Provides a reference to other elements in the form data structure according   171     to the specified 'nodes' parameter (an XPath expression in the template).   172     """   173    174     #print "other_elements"   175     names = []   176     for node in nodes:   177         name = path_to_node(libxml2dom.Node(node), 0, None, 0)   178         if name not in names:   179             names.append(name)   180     r = ",".join(names)   181     return r.encode("utf-8")   182    183 def list_attribute(context, element_name, attribute_name):   184    185     """   186     Exposed as {template:list-attribute(element_name, attribute_name)}.   187     Provides a reference to one or many elements of the given 'element_name'   188     found under the current element in the form data structure having   189     attributes with the given 'attribute_name'.   190     """   191    192     #print "list_attribute"   193     element_name = unicode(element_name, "utf-8")   194     attribute_name = unicode(attribute_name, "utf-8")   195     r = path_to_context(context, 0, (element_name, attribute_name))   196     return r.encode("utf-8")   197    198 def other_list_attributes(context, element_name, attribute_name, nodes):   199    200     """   201     Exposed as {template:other-list-attributes(element_name, attribute_name, nodes)}.   202     Provides a reference to other elements in the form data structure, found   203     under the specified 'nodes' (described using an XPath expression in the   204     template) having the given 'element_name' and bearing attributes of the   205     given 'attribute_name'.   206     """   207    208     #print "other_list_attributes"   209     element_name = unicode(element_name, "utf-8")   210     attribute_name = unicode(attribute_name, "utf-8")   211     names = []   212     for node in nodes:   213         name = path_to_node(libxml2dom.Node(node), 0, (element_name, attribute_name), 1)   214         if name not in names:   215             names.append(name)   216     r = ",".join(names)   217     return r.encode("utf-8")   218    219 def other_attributes(context, attribute_name, nodes):   220    221     """   222     Exposed as {template:other-attributes(name, nodes)}.   223     Provides a reference to attributes in the form data structure of the given   224     'attribute_name' residing on the specified 'nodes' (described using an XPath   225     expression in the template).   226     """   227    228     #print "other_attributes"   229     attribute_name = unicode(attribute_name, "utf-8")   230     # NOTE: Cannot directly reference attributes in the nodes list because   231     # NOTE: libxml2dom does not yet support parent element discovery on   232     # NOTE: attributes.   233     names = []   234     for node in nodes:   235         name = path_to_node(libxml2dom.Node(node), 1, attribute_name, 0)   236         if name not in names:   237             names.append(name)   238     r = ",".join(names)   239     return r.encode("utf-8")   240    241 def child_element(context, element_name, position, node_paths):   242    243     """   244     Exposed as {template:child-element(element_name, position, node_paths)}.   245     Provides relative paths to the specifed 'element_name', having the given   246     'position' (1-based) under each element specified in 'node_paths' (provided   247     by calls to other extension functions in the template). For example:   248    249     template:child-element('comment', 1, template:this-element()) -> '.../comment$1'   250     """   251    252     element_name = unicode(element_name, "utf-8")   253     l = []   254     for node_path in node_paths.split(","):   255         l.append(node_path + Constants.path_separator + element_name   256             + Constants.pair_separator + str(int(position)))   257     return ",".join(l).encode("utf-8")   258    259 def child_attribute(context, attribute_name, node_paths):   260    261     """   262     Exposed as {template:child-attribute(attribute_name, node_paths)}.   263     Provides a relative path to the specifed 'attribute_name' for each element   264     specified in 'node_paths' (provided by calls to other extension functions in   265     the template). For example:   266    267     template:child-attribute('value', template:this-element()) -> '.../value'   268     """   269    270     attribute_name = unicode(attribute_name, "utf-8")   271     l = []   272     for node_path in node_paths.split(","):   273         l.append(node_path + Constants.path_separator + attribute_name)   274     return ",".join(l).encode("utf-8")   275    276 # Old implementations.   277    278 def multi_field_name(context, multivalue_name):   279     #print "multi_field_name"   280     multivalue_name = unicode(multivalue_name, "utf-8")   281     r = path_to_context(context, 1, multivalue_name)   282     return r.encode("utf-8")   283    284 def other_multi_field_names(context, multivalue_name, nodes):   285     #print "other_multi_field_names"   286     multivalue_name = unicode(multivalue_name, "utf-8")   287     names = []   288     for node in nodes:   289         name = path_to_node(libxml2dom.Node(node), 1, multivalue_name, 1)   290         if name not in names:   291             names.append(name)   292     r = ",".join(names)   293     return r.encode("utf-8")   294    295 # Utility functions.   296    297 def url_encode(context, nodes, charset="utf-8"):   298    299     """   300     Exposed as {template:url-encode(nodes)}.   301     Provides a "URL encoded" string created from the merged textual contents of   302     the given 'nodes', with the encoded character values representing characters   303     in the optional 'charset' (UTF-8 if not specified).   304    305     template:url-encode(./text(), 'iso-8859-1')   306     """   307    308     l = []   309     for node in nodes:   310         s = libxml2dom.Node(node).nodeValue   311         l.append(urllib.quote(s.encode("utf-8")).replace("/", "%2F"))   312     output = "".join(l)   313     return output   314    315 def element_path(context, field_names):   316    317     """   318     Convert the given 'field_names' back to XPath references.   319     For example:   320     /configuration$1/details$1/base-system$$value -> /*[position() = 1]/*[position() = 1]/base-system   321     If more than one field name is given - ie. 'field_names' contains a   322     comma-separated list of names - then only the first name is used.   323     """   324    325     field_name = field_names.split(",")[0]   326    327     # Get the main part of the name (where a multivalue reference was given).   328    329     field_name = get_field_name(field_name)   330    331     # Build the XPath expression.   332    333     parts = field_name.split(Constants.path_separator)   334     new_parts = []   335     for part in parts:   336         path_parts = part.split(Constants.pair_separator)   337         if len(path_parts) == 2:   338             new_parts.append("*[position() = " + path_parts[1] + "]")   339         else:   340             new_parts.append(path_parts[0])   341     return "/".join(new_parts)   342    343 # New functions.   344    345 libxsltmod.xsltRegisterExtModuleFunction("list-attribute", "http://www.boddie.org.uk/ns/xmltools/template", list_attribute)   346 libxsltmod.xsltRegisterExtModuleFunction("other-list-attributes", "http://www.boddie.org.uk/ns/xmltools/template", other_list_attributes)   347 libxsltmod.xsltRegisterExtModuleFunction("other-attributes", "http://www.boddie.org.uk/ns/xmltools/template", other_attributes)   348 libxsltmod.xsltRegisterExtModuleFunction("child-element", "http://www.boddie.org.uk/ns/xmltools/template", child_element)   349 libxsltmod.xsltRegisterExtModuleFunction("child-attribute", "http://www.boddie.org.uk/ns/xmltools/template", child_attribute)   350    351 # New names.   352    353 libxsltmod.xsltRegisterExtModuleFunction("this-element", "http://www.boddie.org.uk/ns/xmltools/template", this_element)   354 libxsltmod.xsltRegisterExtModuleFunction("this-attribute", "http://www.boddie.org.uk/ns/xmltools/template", this_attribute)   355 libxsltmod.xsltRegisterExtModuleFunction("new-attribute", "http://www.boddie.org.uk/ns/xmltools/template", new_attribute)   356 libxsltmod.xsltRegisterExtModuleFunction("other-elements", "http://www.boddie.org.uk/ns/xmltools/template", other_elements)   357    358 # Old names.   359    360 libxsltmod.xsltRegisterExtModuleFunction("this-position", "http://www.boddie.org.uk/ns/xmltools/template", this_element)   361 libxsltmod.xsltRegisterExtModuleFunction("field-name", "http://www.boddie.org.uk/ns/xmltools/template", this_attribute)   362 libxsltmod.xsltRegisterExtModuleFunction("new-field", "http://www.boddie.org.uk/ns/xmltools/template", new_attribute)   363 libxsltmod.xsltRegisterExtModuleFunction("other-field-names", "http://www.boddie.org.uk/ns/xmltools/template", other_elements)   364    365 # Old functions.   366    367 libxsltmod.xsltRegisterExtModuleFunction("multi-field-name", "http://www.boddie.org.uk/ns/xmltools/template", multi_field_name)   368 libxsltmod.xsltRegisterExtModuleFunction("other-multi-field-names", "http://www.boddie.org.uk/ns/xmltools/template", other_multi_field_names)   369    370 # Utility functions.   371    372 libxsltmod.xsltRegisterExtModuleFunction("url-encode", "http://www.boddie.org.uk/ns/xmltools/template", url_encode)   373 libxsltmod.xsltRegisterExtModuleFunction("element-path", "http://www.boddie.org.uk/ns/xmltools/template", element_path)   374    375 def get_field_name(field_or_multi_name):   376     return field_or_multi_name.split(Constants.multi_separator)[0]   377    378 # vim: tabstop=4 expandtab shiftwidth=4