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