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