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 i18n(context, value): 322 323 """ 324 Exposed as {template:i18n(value)}. 325 326 Provides a translation of the given 'value' using the 'translations' and 327 'locale' variables defined in the output stylesheet. The 'value' may be a 328 string or a collection of nodes, each having a textual value, where such 329 values are then concatenated to produce a single string value. 330 """ 331 332 if isinstance(value, str): 333 value = unicode(value, libxml2_encoding) 334 else: 335 l = [] 336 for node in value: 337 s = libxml2dom.Node(node).nodeValue 338 l.append(s) 339 value = "".join(l) 340 341 context = libxml2mod.xmlXPathParserGetContext(context) 342 transform_context = libxsltmod.xsltXPathGetTransformContext(context) 343 translations_var = libxsltmod.xsltVariableLookup(transform_context, "translations", None) 344 locale_var = libxsltmod.xsltVariableLookup(transform_context, "locale", None) 345 if translations_var is not None and locale_var is not None: 346 translations = libxml2dom.Node(translations_var[0]) 347 results = translations.xpath("/translations/locale[code/@value='%s']/translation[@value='%s']/text()" % (locale_var, value)) 348 if len(results) > 0: 349 return results[0].nodeValue.encode(libxml2_encoding) 350 return value.encode(libxml2_encoding) 351 352 def choice(context, value, true_string, false_string=None): 353 354 """ 355 Exposed as {template:choice(value, true_string, false_string)}. 356 357 Using the given boolean 'value', which may itself be an expression evaluated 358 by the XSLT processor, return the 'true_string' if 'value' is true or the 359 'false_string' if 'value' is false. If 'false_string' is omitted and if 360 'value' evaluates to a false value, an empty string is returned. 361 """ 362 363 if value: 364 return true_string 365 else: 366 return false_string or "" 367 368 def url_encode(context, nodes, charset=libxml2_encoding): 369 370 """ 371 Exposed as {template:url-encode(nodes)}. 372 373 Provides a "URL encoded" string created from the merged textual contents of 374 the given 'nodes', with the encoded character values representing characters 375 in the optional 'charset' (UTF-8 if not specified). Note that / and # 376 characters are replaced with their "URL encoded" character values. 377 378 If a string value is supplied for 'nodes', this will be translated instead. 379 380 template:url-encode(./text(), 'iso-8859-1') 381 """ 382 383 l = [] 384 if isinstance(nodes, str): 385 return urllib.quote(nodes.encode(libxml2_encoding)).replace("/", "%2F").replace("#", "%23") 386 387 for node in nodes: 388 s = libxml2dom.Node(node).nodeValue 389 l.append(urllib.quote(s.encode(libxml2_encoding)).replace("/", "%2F").replace("#", "%23")) 390 output = "".join(l) 391 return output 392 393 def element_path(context, field_names): 394 395 """ 396 Exposed as {template:element-path(field_names)}. 397 398 Convert the given 'field_names' back to XPath references. 399 For example: 400 401 /configuration$1/details$1/base-system$$value -> /*[position() = 1]/*[position() = 1]/base-system 402 403 If more than one field name is given - ie. 'field_names' contains a 404 comma-separated list of names - then only the first name is used. 405 406 To use this function effectively, use the result of another function as the 407 argument. For example: 408 409 template:element-path(template:this-element()) 410 template:element-path(template:other-elements(matches)) 411 template:element-path(template:other-elements(..)) 412 """ 413 414 field_name = field_names.split(",")[0] 415 416 # Get the main part of the name (where a multivalue reference was given). 417 418 field_name = get_field_name(field_name) 419 420 # Build the XPath expression. 421 422 parts = field_name.split(Constants.path_separator) 423 new_parts = [] 424 for part in parts: 425 path_parts = part.split(Constants.pair_separator) 426 if len(path_parts) == 2: 427 new_parts.append("*[position() = " + path_parts[1] + "]") 428 else: 429 new_parts.append(path_parts[0]) 430 return "/".join(new_parts) 431 432 # New functions. 433 434 libxsltmod.xsltRegisterExtModuleFunction("list-attribute", "http://www.boddie.org.uk/ns/xmltools/template", list_attribute) 435 libxsltmod.xsltRegisterExtModuleFunction("other-list-attributes", "http://www.boddie.org.uk/ns/xmltools/template", other_list_attributes) 436 libxsltmod.xsltRegisterExtModuleFunction("other-attributes", "http://www.boddie.org.uk/ns/xmltools/template", other_attributes) 437 libxsltmod.xsltRegisterExtModuleFunction("child-element", "http://www.boddie.org.uk/ns/xmltools/template", child_element) 438 libxsltmod.xsltRegisterExtModuleFunction("child-attribute", "http://www.boddie.org.uk/ns/xmltools/template", child_attribute) 439 libxsltmod.xsltRegisterExtModuleFunction("selector-name", "http://www.boddie.org.uk/ns/xmltools/template", selector_name) 440 441 # New names. 442 443 libxsltmod.xsltRegisterExtModuleFunction("this-element", "http://www.boddie.org.uk/ns/xmltools/template", this_element) 444 libxsltmod.xsltRegisterExtModuleFunction("this-attribute", "http://www.boddie.org.uk/ns/xmltools/template", this_attribute) 445 libxsltmod.xsltRegisterExtModuleFunction("new-attribute", "http://www.boddie.org.uk/ns/xmltools/template", new_attribute) 446 libxsltmod.xsltRegisterExtModuleFunction("other-elements", "http://www.boddie.org.uk/ns/xmltools/template", other_elements) 447 448 # Old names. 449 450 libxsltmod.xsltRegisterExtModuleFunction("this-position", "http://www.boddie.org.uk/ns/xmltools/template", this_element) 451 libxsltmod.xsltRegisterExtModuleFunction("field-name", "http://www.boddie.org.uk/ns/xmltools/template", this_attribute) 452 libxsltmod.xsltRegisterExtModuleFunction("new-field", "http://www.boddie.org.uk/ns/xmltools/template", new_attribute) 453 libxsltmod.xsltRegisterExtModuleFunction("other-field-names", "http://www.boddie.org.uk/ns/xmltools/template", other_elements) 454 455 # Old functions. 456 457 libxsltmod.xsltRegisterExtModuleFunction("multi-field-name", "http://www.boddie.org.uk/ns/xmltools/template", multi_field_name) 458 libxsltmod.xsltRegisterExtModuleFunction("other-multi-field-names", "http://www.boddie.org.uk/ns/xmltools/template", other_multi_field_names) 459 460 # Utility functions. 461 462 libxsltmod.xsltRegisterExtModuleFunction("i18n", "http://www.boddie.org.uk/ns/xmltools/template", i18n) 463 libxsltmod.xsltRegisterExtModuleFunction("choice", "http://www.boddie.org.uk/ns/xmltools/template", choice) 464 libxsltmod.xsltRegisterExtModuleFunction("url-encode", "http://www.boddie.org.uk/ns/xmltools/template", url_encode) 465 libxsltmod.xsltRegisterExtModuleFunction("element-path", "http://www.boddie.org.uk/ns/xmltools/template", element_path) 466 467 def get_field_name(field_or_multi_name): 468 return field_or_multi_name.split(Constants.multi_separator)[0] 469 470 # vim: tabstop=4 expandtab shiftwidth=4