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 xslforms_range(context, range_spec): 322 323 """ 324 Exposed as {template:range(range_spec)}. 325 326 The 'range_spec' is split up into 'start', 'finish' and 'step' according to 327 the following format: 328 329 start...finish...step 330 331 Provides the Python range function by producing a list of numbers, starting 332 at 'start', ending one step before 'finish', and employing the optional 333 'step' to indicate the magnitude of the difference between successive 334 elements in the list as well as the "direction" of the sequence. By default, 335 'step' is set to 1. 336 337 NOTE: This uses a single string because template:element and other 338 NOTE: annotations use commas to separate fields, thus making the usage of 339 NOTE: this function impossible if each range parameter is exposed as a 340 NOTE: function parameter. 341 NOTE: The returning of values from this function is not fully verified, and 342 NOTE: it is probably better to use other extension functions instead of this 343 NOTE: one to achieve simple results (such as str:split from EXSLT). 344 """ 345 346 parts = range_spec.split("...") 347 start, finish = parts[:2] 348 if len(parts) > 2: 349 step = parts[2] 350 else: 351 step = None 352 353 start = int(start) 354 finish = int(finish) 355 if step is not None: 356 step = int(step) 357 else: 358 step = 1 359 360 # Create a list of elements. 361 # NOTE: libxslt complains: "Got a CObject" 362 363 range_elements = libxml2mod.xmlXPathNewNodeSet(None) 364 for i in range(start, finish, step): 365 range_elements.append(libxml2mod.xmlNewText(str(i))) 366 return range_elements 367 368 def i18n(context, value): 369 370 """ 371 Exposed as {template:i18n(value)}. 372 373 Provides a translation of the given 'value' using the 'translations' and 374 'locale' variables defined in the output stylesheet. The 'value' may be a 375 string or a collection of nodes, each having a textual value, where such 376 values are then concatenated to produce a single string value. 377 """ 378 379 if isinstance(value, str): 380 value = unicode(value, libxml2_encoding) 381 else: 382 l = [] 383 for node in value: 384 s = libxml2dom.Node(node).nodeValue 385 l.append(s) 386 value = "".join(l) 387 388 context = libxml2mod.xmlXPathParserGetContext(context) 389 transform_context = libxsltmod.xsltXPathGetTransformContext(context) 390 translations_var = libxsltmod.xsltVariableLookup(transform_context, "translations", None) 391 locale_var = libxsltmod.xsltVariableLookup(transform_context, "locale", None) 392 if translations_var is not None and locale_var is not None: 393 translations = libxml2dom.Node(translations_var[0]) 394 results = translations.xpath("/translations/locale[code/@value='%s']/translation[@value='%s']/text()" % (locale_var, value)) 395 if len(results) > 0: 396 return results[0].nodeValue.encode(libxml2_encoding) 397 return value.encode(libxml2_encoding) 398 399 def choice(context, value, true_string, false_string=None): 400 401 """ 402 Exposed as {template:choice(value, true_string, false_string)}. 403 404 Using the given boolean 'value', which may itself be an expression evaluated 405 by the XSLT processor, return the 'true_string' if 'value' is true or the 406 'false_string' if 'value' is false. If 'false_string' is omitted and if 407 'value' evaluates to a false value, an empty string is returned. 408 """ 409 410 if value: 411 return true_string 412 else: 413 return false_string or "" 414 415 def url_encode(context, nodes, charset=libxml2_encoding): 416 417 """ 418 Exposed as {template:url-encode(nodes)}. 419 420 Provides a "URL encoded" string created from the merged textual contents of 421 the given 'nodes', with the encoded character values representing characters 422 in the optional 'charset' (UTF-8 if not specified). Note that / and # 423 characters are replaced with their "URL encoded" character values. 424 425 If a string value is supplied for 'nodes', this will be translated instead. 426 427 template:url-encode(./text(), 'iso-8859-1') 428 """ 429 430 l = [] 431 if isinstance(nodes, str): 432 return urllib.quote(nodes.encode(libxml2_encoding)).replace("/", "%2F").replace("#", "%23") 433 434 for node in nodes: 435 s = libxml2dom.Node(node).nodeValue 436 l.append(urllib.quote(s.encode(libxml2_encoding)).replace("/", "%2F").replace("#", "%23")) 437 output = "".join(l) 438 return output 439 440 def element_path(context, field_names): 441 442 """ 443 Exposed as {template:element-path(field_names)}. 444 445 Convert the given 'field_names' back to XPath references. 446 For example: 447 448 /configuration$1/details$1/base-system$$value -> /*[position() = 1]/*[position() = 1]/base-system 449 450 If more than one field name is given - ie. 'field_names' contains a 451 comma-separated list of names - then only the first name is used. 452 453 To use this function effectively, use the result of another function as the 454 argument. For example: 455 456 template:element-path(template:this-element()) 457 template:element-path(template:other-elements(matches)) 458 template:element-path(template:other-elements(..)) 459 """ 460 461 field_name = field_names.split(",")[0] 462 463 # Get the main part of the name (where a multivalue reference was given). 464 465 field_name = get_field_name(field_name) 466 467 # Build the XPath expression. 468 469 parts = field_name.split(Constants.path_separator) 470 new_parts = [] 471 for part in parts: 472 path_parts = part.split(Constants.pair_separator) 473 if len(path_parts) == 2: 474 new_parts.append("*[position() = " + path_parts[1] + "]") 475 else: 476 new_parts.append(path_parts[0]) 477 return "/".join(new_parts) 478 479 # New functions. 480 481 libxsltmod.xsltRegisterExtModuleFunction("list-attribute", "http://www.boddie.org.uk/ns/xmltools/template", list_attribute) 482 libxsltmod.xsltRegisterExtModuleFunction("other-list-attributes", "http://www.boddie.org.uk/ns/xmltools/template", other_list_attributes) 483 libxsltmod.xsltRegisterExtModuleFunction("other-attributes", "http://www.boddie.org.uk/ns/xmltools/template", other_attributes) 484 libxsltmod.xsltRegisterExtModuleFunction("child-element", "http://www.boddie.org.uk/ns/xmltools/template", child_element) 485 libxsltmod.xsltRegisterExtModuleFunction("child-attribute", "http://www.boddie.org.uk/ns/xmltools/template", child_attribute) 486 libxsltmod.xsltRegisterExtModuleFunction("selector-name", "http://www.boddie.org.uk/ns/xmltools/template", selector_name) 487 488 # New names. 489 490 libxsltmod.xsltRegisterExtModuleFunction("this-element", "http://www.boddie.org.uk/ns/xmltools/template", this_element) 491 libxsltmod.xsltRegisterExtModuleFunction("this-attribute", "http://www.boddie.org.uk/ns/xmltools/template", this_attribute) 492 libxsltmod.xsltRegisterExtModuleFunction("new-attribute", "http://www.boddie.org.uk/ns/xmltools/template", new_attribute) 493 libxsltmod.xsltRegisterExtModuleFunction("other-elements", "http://www.boddie.org.uk/ns/xmltools/template", other_elements) 494 495 # Old names. 496 497 libxsltmod.xsltRegisterExtModuleFunction("this-position", "http://www.boddie.org.uk/ns/xmltools/template", this_element) 498 libxsltmod.xsltRegisterExtModuleFunction("field-name", "http://www.boddie.org.uk/ns/xmltools/template", this_attribute) 499 libxsltmod.xsltRegisterExtModuleFunction("new-field", "http://www.boddie.org.uk/ns/xmltools/template", new_attribute) 500 libxsltmod.xsltRegisterExtModuleFunction("other-field-names", "http://www.boddie.org.uk/ns/xmltools/template", other_elements) 501 502 # Old functions. 503 504 libxsltmod.xsltRegisterExtModuleFunction("multi-field-name", "http://www.boddie.org.uk/ns/xmltools/template", multi_field_name) 505 libxsltmod.xsltRegisterExtModuleFunction("other-multi-field-names", "http://www.boddie.org.uk/ns/xmltools/template", other_multi_field_names) 506 507 # Utility functions. 508 509 libxsltmod.xsltRegisterExtModuleFunction("range", "http://www.boddie.org.uk/ns/xmltools/template", xslforms_range) 510 libxsltmod.xsltRegisterExtModuleFunction("i18n", "http://www.boddie.org.uk/ns/xmltools/template", i18n) 511 libxsltmod.xsltRegisterExtModuleFunction("choice", "http://www.boddie.org.uk/ns/xmltools/template", choice) 512 libxsltmod.xsltRegisterExtModuleFunction("url-encode", "http://www.boddie.org.uk/ns/xmltools/template", url_encode) 513 libxsltmod.xsltRegisterExtModuleFunction("element-path", "http://www.boddie.org.uk/ns/xmltools/template", element_path) 514 515 def get_field_name(field_or_multi_name): 516 return field_or_multi_name.split(Constants.multi_separator)[0] 517 518 # vim: tabstop=4 expandtab shiftwidth=4