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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 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 url_encode(context, nodes, charset="utf-8"): 320 321 """ 322 Exposed as {template:url-encode(nodes)}. 323 324 Provides a "URL encoded" string created from the merged textual contents of 325 the given 'nodes', with the encoded character values representing characters 326 in the optional 'charset' (UTF-8 if not specified). 327 328 template:url-encode(./text(), 'iso-8859-1') 329 """ 330 331 l = [] 332 for node in nodes: 333 s = libxml2dom.Node(node).nodeValue 334 l.append(urllib.quote(s.encode("utf-8")).replace("/", "%2F")) 335 output = "".join(l) 336 return output 337 338 def element_path(context, field_names): 339 340 """ 341 Exposed as {template:element-path(field_names)}. 342 343 Convert the given 'field_names' back to XPath references. 344 For example: 345 346 /configuration$1/details$1/base-system$$value -> /*[position() = 1]/*[position() = 1]/base-system 347 348 If more than one field name is given - ie. 'field_names' contains a 349 comma-separated list of names - then only the first name is used. 350 351 To use this function effectively, use the result of another function as the 352 argument. For example: 353 354 template:element-path(template:this-element()) 355 template:element-path(template:other-elements(matches)) 356 template:element-path(template:other-elements(..)) 357 """ 358 359 field_name = field_names.split(",")[0] 360 361 # Get the main part of the name (where a multivalue reference was given). 362 363 field_name = get_field_name(field_name) 364 365 # Build the XPath expression. 366 367 parts = field_name.split(Constants.path_separator) 368 new_parts = [] 369 for part in parts: 370 path_parts = part.split(Constants.pair_separator) 371 if len(path_parts) == 2: 372 new_parts.append("*[position() = " + path_parts[1] + "]") 373 else: 374 new_parts.append(path_parts[0]) 375 return "/".join(new_parts) 376 377 # New functions. 378 379 libxsltmod.xsltRegisterExtModuleFunction("list-attribute", "http://www.boddie.org.uk/ns/xmltools/template", list_attribute) 380 libxsltmod.xsltRegisterExtModuleFunction("other-list-attributes", "http://www.boddie.org.uk/ns/xmltools/template", other_list_attributes) 381 libxsltmod.xsltRegisterExtModuleFunction("other-attributes", "http://www.boddie.org.uk/ns/xmltools/template", other_attributes) 382 libxsltmod.xsltRegisterExtModuleFunction("child-element", "http://www.boddie.org.uk/ns/xmltools/template", child_element) 383 libxsltmod.xsltRegisterExtModuleFunction("child-attribute", "http://www.boddie.org.uk/ns/xmltools/template", child_attribute) 384 libxsltmod.xsltRegisterExtModuleFunction("selector-name", "http://www.boddie.org.uk/ns/xmltools/template", selector_name) 385 386 # New names. 387 388 libxsltmod.xsltRegisterExtModuleFunction("this-element", "http://www.boddie.org.uk/ns/xmltools/template", this_element) 389 libxsltmod.xsltRegisterExtModuleFunction("this-attribute", "http://www.boddie.org.uk/ns/xmltools/template", this_attribute) 390 libxsltmod.xsltRegisterExtModuleFunction("new-attribute", "http://www.boddie.org.uk/ns/xmltools/template", new_attribute) 391 libxsltmod.xsltRegisterExtModuleFunction("other-elements", "http://www.boddie.org.uk/ns/xmltools/template", other_elements) 392 393 # Old names. 394 395 libxsltmod.xsltRegisterExtModuleFunction("this-position", "http://www.boddie.org.uk/ns/xmltools/template", this_element) 396 libxsltmod.xsltRegisterExtModuleFunction("field-name", "http://www.boddie.org.uk/ns/xmltools/template", this_attribute) 397 libxsltmod.xsltRegisterExtModuleFunction("new-field", "http://www.boddie.org.uk/ns/xmltools/template", new_attribute) 398 libxsltmod.xsltRegisterExtModuleFunction("other-field-names", "http://www.boddie.org.uk/ns/xmltools/template", other_elements) 399 400 # Old functions. 401 402 libxsltmod.xsltRegisterExtModuleFunction("multi-field-name", "http://www.boddie.org.uk/ns/xmltools/template", multi_field_name) 403 libxsltmod.xsltRegisterExtModuleFunction("other-multi-field-names", "http://www.boddie.org.uk/ns/xmltools/template", other_multi_field_names) 404 405 # Utility functions. 406 407 libxsltmod.xsltRegisterExtModuleFunction("url-encode", "http://www.boddie.org.uk/ns/xmltools/template", url_encode) 408 libxsltmod.xsltRegisterExtModuleFunction("element-path", "http://www.boddie.org.uk/ns/xmltools/template", element_path) 409 410 def get_field_name(field_or_multi_name): 411 return field_or_multi_name.split(Constants.multi_separator)[0] 412 413 # vim: tabstop=4 expandtab shiftwidth=4