XSLTools

XSLForms/Output.py

545:983ae5601924
2007-01-18 paulb [project @ 2007-01-18 22:56:53 by paulb] Fixed translation selection for unrecognised locales.
     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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA    21 """    22     23 import Constants    24     25 # Try the conventional import first.    26     27 try:    28     import libxsltmod, libxml2mod    29 except ImportError:    30     from libxmlmods import libxml2mod    31     from libxmlmods import libxsltmod    32     33 import libxml2dom    34 import urllib    35     36 libxml2_encoding = "utf-8"    37     38 def path_to_node(node, attribute_ref, name, multivalue=0):    39     40     """    41     Generate an XSLForms path to the given 'node', producing an attribute    42     reference if 'attribute_ref' is true; for example:    43     44     /package$1/discriminators$5/discriminator$1/category    45     46     Otherwise an element reference is produced; for example:    47     48     /package$1/discriminators$5/discriminator$1    49     50     Use the given 'name' to complete the path if an attribute reference is    51     required (and if a genuine attribute is found at the context node -    52     otherwise 'name' will be None and the context node will be treated like an    53     attribute).    54     55     If 'multivalue' is true and 'attribute_ref' is set, produce an attribute    56     reference using the given 'name':    57     58     /package$1/categories$1/category$$name    59     60     If 'multivalue' is true and 'attribute_ref' is not set, produce an attribute    61     reference using the given 'name' of form (element, attribute):    62     63     /package$1/categories$1/element$$attribute    64     """    65     66     l = []    67     # Skip attribute reference.    68     if node.nodeType == node.ATTRIBUTE_NODE:    69         node = node.parentNode    70     # Manually insert the attribute name if defined.    71     if attribute_ref:    72         # A real attribute is referenced.    73         if name is not None:    74             l.insert(0, name)    75             if multivalue:    76                 l.insert(0, Constants.multi_separator)    77                 l.insert(0, node.nodeName)    78                 node = node.parentNode    79             l.insert(0, Constants.path_separator)    80         # Otherwise, treat the element name as an attribute name.    81         # NOTE: Not sure how useful this is.    82         else:    83             l.insert(0, node.nodeName)    84             l.insert(0, Constants.path_separator)    85             node = node.parentNode    86     # Otherwise insert any multivalue references (eg. list-attribute).    87     elif multivalue:    88         element_name, attribute_name = name    89         l.insert(0, attribute_name)    90         l.insert(0, Constants.multi_separator)    91         l.insert(0, element_name)    92         l.insert(0, Constants.path_separator)    93     94     # Element references.    95     while node is not None and node.nodeType != node.DOCUMENT_NODE:    96         l.insert(0, str(int(node.xpath("count(preceding-sibling::*) + 1"))))    97         l.insert(0, Constants.pair_separator)    98         l.insert(0, node.nodeName)    99         l.insert(0, Constants.path_separator)   100         node = node.parentNode   101     return "".join(l)   102    103 def path_to_context(context, attribute_ref, multivalue_name=None):   104    105     """   106     As a libxslt extension function, return a string containing the XSLForms   107     path to the 'context' node, using the special "this-name" variable to   108     complete the path if an attribute reference is required (as indicated by   109     'attribute_ref' being set to true). If 'multivalue_name' is set, produce a   110     reference to a multivalued field using the given string as the attribute   111     name.   112     """   113    114     context = libxml2mod.xmlXPathParserGetContext(context)   115     transform_context = libxsltmod.xsltXPathGetTransformContext(context)   116     name_var = libxsltmod.xsltVariableLookup(transform_context, "this-name", None)   117     if multivalue_name is not None:   118         name = multivalue_name   119         multivalue = 1   120     elif name_var is not None:   121         name = libxml2mod.xmlNodeGetContent(name_var[0])   122         name = unicode(name, libxml2_encoding)   123         multivalue = 0   124     else:   125         name = None   126         multivalue = 0   127     node = libxml2dom.Node(libxml2mod.xmlXPathGetContextNode(context))   128     return path_to_node(node, attribute_ref, name, multivalue)   129    130 # Exposed extension functions.   131    132 def this_element(context):   133    134     """   135     Exposed as {template:this-element()}.   136    137     Provides a reference to the current element in the form data structure.   138     """   139    140     #print "this_element"   141     r = path_to_context(context, 0)   142     return r.encode(libxml2_encoding)   143    144 def this_attribute(context):   145    146     """   147     Exposed as {template:this-attribute()}.   148    149     Provides a reference to the current attribute in the form data structure.   150     """   151    152     #print "this_attribute"   153     r = path_to_context(context, 1)   154     return r.encode(libxml2_encoding)   155    156 def new_attribute(context, name):   157    158     """   159     Exposed as {template:new-attribute(name)}.   160    161     Provides a reference to a new attribute of the given 'name' on the current   162     element in the form data structure.   163     """   164    165     #print "new_attribute"   166     name = unicode(name, libxml2_encoding)   167     r = path_to_context(context, 0) + "/" + name   168     return r.encode(libxml2_encoding)   169    170 def other_elements(context, nodes):   171    172     """   173     Exposed as {template:other-elements(nodes)}.   174    175     Provides a reference to other elements in the form data structure according   176     to the specified 'nodes' parameter (an XPath expression in the template).   177     """   178    179     #print "other_elements"   180     names = []   181     for node in nodes:   182         name = path_to_node(libxml2dom.Node(node), 0, None, 0)   183         if name not in names:   184             names.append(name)   185     r = ",".join(names)   186     return r.encode(libxml2_encoding)   187    188 def list_attribute(context, element_name, attribute_name):   189    190     """   191     Exposed as {template:list-attribute(element_name, attribute_name)}.   192    193     Provides a reference to one or many elements of the given 'element_name'   194     found under the current element in the form data structure having   195     attributes with the given 'attribute_name'.   196     """   197    198     #print "list_attribute"   199     element_name = unicode(element_name, libxml2_encoding)   200     attribute_name = unicode(attribute_name, libxml2_encoding)   201     r = path_to_context(context, 0, (element_name, attribute_name))   202     return r.encode(libxml2_encoding)   203    204 def other_list_attributes(context, element_name, attribute_name, nodes):   205    206     """   207     Exposed as {template:other-list-attributes(element_name, attribute_name, nodes)}.   208    209     Provides a reference to other elements in the form data structure, found   210     under the specified 'nodes' (described using an XPath expression in the   211     template) having the given 'element_name' and bearing attributes of the   212     given 'attribute_name'.   213     """   214    215     #print "other_list_attributes"   216     element_name = unicode(element_name, libxml2_encoding)   217     attribute_name = unicode(attribute_name, libxml2_encoding)   218     names = []   219     for node in nodes:   220         name = path_to_node(libxml2dom.Node(node), 0, (element_name, attribute_name), 1)   221         if name not in names:   222             names.append(name)   223     r = ",".join(names)   224     return r.encode(libxml2_encoding)   225    226 def other_attributes(context, attribute_name, nodes):   227    228     """   229     Exposed as {template:other-attributes(name, nodes)}.   230    231     Provides a reference to attributes in the form data structure of the given   232     'attribute_name' residing on the specified 'nodes' (described using an XPath   233     expression in the template).   234     """   235    236     #print "other_attributes"   237     attribute_name = unicode(attribute_name, libxml2_encoding)   238     # NOTE: Cannot directly reference attributes in the nodes list because   239     # NOTE: libxml2dom does not yet support parent element discovery on   240     # NOTE: attributes.   241     names = []   242     for node in nodes:   243         name = path_to_node(libxml2dom.Node(node), 1, attribute_name, 0)   244         if name not in names:   245             names.append(name)   246     r = ",".join(names)   247     return r.encode(libxml2_encoding)   248    249 def child_element(context, element_name, position, node_paths):   250    251     """   252     Exposed as {template:child-element(element_name, position, node_paths)}.   253    254     Provides relative paths to the specifed 'element_name', having the given   255     'position' (1-based) under each element specified in 'node_paths' (provided   256     by calls to other extension functions in the template). For example:   257    258     template:child-element('comment', 1, template:this-element()) -> '.../comment$1'   259     """   260    261     element_name = unicode(element_name, libxml2_encoding)   262     l = []   263     for node_path in node_paths.split(","):   264         l.append(node_path + Constants.path_separator + element_name   265             + Constants.pair_separator + str(int(position)))   266     return ",".join(l).encode(libxml2_encoding)   267    268 def child_attribute(context, attribute_name, node_paths):   269    270     """   271     Exposed as {template:child-attribute(attribute_name, node_paths)}.   272    273     Provides a relative path to the specifed 'attribute_name' for each element   274     specified in 'node_paths' (provided by calls to other extension functions in   275     the template). For example:   276    277     template:child-attribute('value', template:this-element()) -> '.../value'   278     """   279    280     attribute_name = unicode(attribute_name, libxml2_encoding)   281     l = []   282     for node_path in node_paths.split(","):   283         l.append(node_path + Constants.path_separator + attribute_name)   284     return ",".join(l).encode(libxml2_encoding)   285    286 def selector_name(context, field_name, nodes):   287    288     """   289     Exposed as {template:selector-name(field_name, nodes)}.   290    291     Provides a selector field name defined using 'field_name' and referring to   292     the given 'nodes'. For example:   293    294     template:selector-name('add-platform', package/platforms) -> 'add-platform=/package$1/platforms$1'   295    296     NOTE: The 'nodes' must be element references.   297     """   298    299     #print "selector_name"   300     names = []   301     for node in nodes:   302         name = path_to_node(libxml2dom.Node(node), 0, None, 0)   303         if name not in names:   304             names.append(field_name + "=" + name)   305     r = ",".join(names)   306     return r.encode(libxml2_encoding)   307    308 # Old implementations.   309    310 def multi_field_name(context, multivalue_name):   311     #print "multi_field_name"   312     multivalue_name = unicode(multivalue_name, libxml2_encoding)   313     r = path_to_context(context, 1, multivalue_name)   314     return r.encode(libxml2_encoding)   315    316 def other_multi_field_names(context, multivalue_name, nodes):   317     #print "other_multi_field_names"   318     multivalue_name = unicode(multivalue_name, libxml2_encoding)   319     names = []   320     for node in nodes:   321         name = path_to_node(libxml2dom.Node(node), 1, multivalue_name, 1)   322         if name not in names:   323             names.append(name)   324     r = ",".join(names)   325     return r.encode(libxml2_encoding)   326    327 # Utility functions.   328    329 def xslforms_range(context, range_spec):   330    331     """   332     Exposed as {template:range(range_spec)}.   333    334     The 'range_spec' is split up into 'start', 'finish' and 'step' according to   335     the following format:   336    337     start...finish...step   338    339     Provides the Python range function by producing a list of numbers, starting   340     at 'start', ending one step before 'finish', and employing the optional   341     'step' to indicate the magnitude of the difference between successive   342     elements in the list as well as the "direction" of the sequence. By default,   343     'step' is set to 1.   344    345     NOTE: This uses a single string because template:element and other   346     NOTE: annotations use commas to separate fields, thus making the usage of   347     NOTE: this function impossible if each range parameter is exposed as a   348     NOTE: function parameter.   349     NOTE: The returning of values from this function is not fully verified, and   350     NOTE: it is probably better to use other extension functions instead of this   351     NOTE: one to achieve simple results (such as str:split from EXSLT).   352     """   353    354     parts = range_spec.split("...")   355     start, finish = parts[:2]   356     if len(parts) > 2:   357         step = parts[2]   358     else:   359         step = None   360    361     start = int(start)   362     finish = int(finish)   363     if step is not None:   364         step = int(step)   365     else:   366         step = 1   367    368     # Create a list of elements.   369     # NOTE: libxslt complains: "Got a CObject"   370    371     range_elements = libxml2mod.xmlXPathNewNodeSet(None)   372     for i in range(start, finish, step):   373         range_elements.append(libxml2mod.xmlNewText(str(i)))   374     return range_elements   375    376 def i18n(context, value):   377    378     """   379     Exposed as {template:i18n(value)}.   380    381     Provides a translation of the given 'value' using the 'translations' and   382     'locale' variables defined in the output stylesheet. The 'value' may be a   383     string or a collection of nodes, each having a textual value, where such   384     values are then concatenated to produce a single string value.   385     """   386    387     if isinstance(value, str):   388         value = unicode(value, libxml2_encoding)   389     else:   390         l = []   391         for node in value:   392             s = libxml2dom.Node(node).nodeValue   393             l.append(s)   394         value = "".join(l)   395    396     context = libxml2mod.xmlXPathParserGetContext(context)   397     transform_context = libxsltmod.xsltXPathGetTransformContext(context)   398     translations_var = libxsltmod.xsltVariableLookup(transform_context, "translations", None)   399     locale_var = libxsltmod.xsltVariableLookup(transform_context, "locale", None)   400     if translations_var is not None and translations_var and locale_var is not None:   401         translations = libxml2dom.Node(translations_var[0])   402         results = translations.xpath("/translations/locale[code/@value='%s']/translation[@value='%s']/text()" % (locale_var, value))   403         if not results:   404             results = translations.xpath("/translations/locale[1]/translation[@value='%s']/text()" % value)   405         if results:   406             return results[0].nodeValue.encode(libxml2_encoding)   407     return value.encode(libxml2_encoding)   408    409 def choice(context, value, true_string, false_string=None):   410    411     """   412     Exposed as {template:choice(value, true_string, false_string)}.   413    414     Using the given boolean 'value', which may itself be an expression evaluated   415     by the XSLT processor, return the 'true_string' if 'value' is true or the   416     'false_string' if 'value' is false. If 'false_string' is omitted and if   417     'value' evaluates to a false value, an empty string is returned.   418     """   419    420     if value:   421         return true_string   422     else:   423         return false_string or ""   424    425 def url_encode(context, nodes, charset=libxml2_encoding):   426    427     """   428     Exposed as {template:url-encode(nodes)}.   429    430     Provides a "URL encoded" string created from the merged textual contents of   431     the given 'nodes', with the encoded character values representing characters   432     in the optional 'charset' (UTF-8 if not specified). Note that / and #   433     characters are replaced with their "URL encoded" character values.   434    435     If a string value is supplied for 'nodes', this will be translated instead.   436    437     template:url-encode(./text(), 'iso-8859-1')   438     """   439    440     l = []   441     if isinstance(nodes, str):   442         return urllib.quote(nodes.encode(libxml2_encoding)).replace("/", "%2F").replace("#", "%23")   443    444     for node in nodes:   445         s = libxml2dom.Node(node).nodeValue   446         l.append(urllib.quote(s.encode(libxml2_encoding)).replace("/", "%2F").replace("#", "%23"))   447     output = "".join(l)   448     return output   449    450 def element_path(context, field_names):   451    452     """   453     Exposed as {template:element-path(field_names)}.   454    455     Convert the given 'field_names' back to XPath references.   456     For example:   457    458     /configuration$1/details$1/base-system$$value -> /*[position() = 1]/*[position() = 1]/base-system   459    460     If more than one field name is given - ie. 'field_names' contains a   461     comma-separated list of names - then only the first name is used.   462    463     To use this function effectively, use the result of another function as the   464     argument. For example:   465    466     template:element-path(template:this-element())   467     template:element-path(template:other-elements(matches))   468     template:element-path(template:other-elements(..))   469     """   470    471     field_name = field_names.split(",")[0]   472    473     # Get the main part of the name (where a multivalue reference was given).   474    475     field_name = get_field_name(field_name)   476    477     # Build the XPath expression.   478    479     parts = field_name.split(Constants.path_separator)   480     new_parts = []   481     for part in parts:   482         path_parts = part.split(Constants.pair_separator)   483         if len(path_parts) == 2:   484             new_parts.append("*[position() = " + path_parts[1] + "]")   485         else:   486             new_parts.append(path_parts[0])   487     return "/".join(new_parts)   488    489 # New functions.   490    491 libxsltmod.xsltRegisterExtModuleFunction("list-attribute", "http://www.boddie.org.uk/ns/xmltools/template", list_attribute)   492 libxsltmod.xsltRegisterExtModuleFunction("other-list-attributes", "http://www.boddie.org.uk/ns/xmltools/template", other_list_attributes)   493 libxsltmod.xsltRegisterExtModuleFunction("other-attributes", "http://www.boddie.org.uk/ns/xmltools/template", other_attributes)   494 libxsltmod.xsltRegisterExtModuleFunction("child-element", "http://www.boddie.org.uk/ns/xmltools/template", child_element)   495 libxsltmod.xsltRegisterExtModuleFunction("child-attribute", "http://www.boddie.org.uk/ns/xmltools/template", child_attribute)   496 libxsltmod.xsltRegisterExtModuleFunction("selector-name", "http://www.boddie.org.uk/ns/xmltools/template", selector_name)   497    498 # New names.   499    500 libxsltmod.xsltRegisterExtModuleFunction("this-element", "http://www.boddie.org.uk/ns/xmltools/template", this_element)   501 libxsltmod.xsltRegisterExtModuleFunction("this-attribute", "http://www.boddie.org.uk/ns/xmltools/template", this_attribute)   502 libxsltmod.xsltRegisterExtModuleFunction("new-attribute", "http://www.boddie.org.uk/ns/xmltools/template", new_attribute)   503 libxsltmod.xsltRegisterExtModuleFunction("other-elements", "http://www.boddie.org.uk/ns/xmltools/template", other_elements)   504    505 # Old names.   506    507 libxsltmod.xsltRegisterExtModuleFunction("this-position", "http://www.boddie.org.uk/ns/xmltools/template", this_element)   508 libxsltmod.xsltRegisterExtModuleFunction("field-name", "http://www.boddie.org.uk/ns/xmltools/template", this_attribute)   509 libxsltmod.xsltRegisterExtModuleFunction("new-field", "http://www.boddie.org.uk/ns/xmltools/template", new_attribute)   510 libxsltmod.xsltRegisterExtModuleFunction("other-field-names", "http://www.boddie.org.uk/ns/xmltools/template", other_elements)   511    512 # Old functions.   513    514 libxsltmod.xsltRegisterExtModuleFunction("multi-field-name", "http://www.boddie.org.uk/ns/xmltools/template", multi_field_name)   515 libxsltmod.xsltRegisterExtModuleFunction("other-multi-field-names", "http://www.boddie.org.uk/ns/xmltools/template", other_multi_field_names)   516    517 # Utility functions.   518    519 libxsltmod.xsltRegisterExtModuleFunction("range", "http://www.boddie.org.uk/ns/xmltools/template", xslforms_range)   520 libxsltmod.xsltRegisterExtModuleFunction("i18n", "http://www.boddie.org.uk/ns/xmltools/template", i18n)   521 libxsltmod.xsltRegisterExtModuleFunction("choice", "http://www.boddie.org.uk/ns/xmltools/template", choice)   522 libxsltmod.xsltRegisterExtModuleFunction("url-encode", "http://www.boddie.org.uk/ns/xmltools/template", url_encode)   523 libxsltmod.xsltRegisterExtModuleFunction("element-path", "http://www.boddie.org.uk/ns/xmltools/template", element_path)   524    525 def get_field_name(field_or_multi_name):   526     return field_or_multi_name.split(Constants.multi_separator)[0]   527    528 # vim: tabstop=4 expandtab shiftwidth=4