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