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