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