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