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 Provides a reference to the current element in the form data structure. 127 """ 128 129 #print "this_element" 130 r = path_to_context(context, 0) 131 return r.encode("utf-8") 132 133 def this_attribute(context): 134 135 """ 136 Exposed as {template:this-attribute()}. 137 Provides a reference to the current attribute in the form data structure. 138 """ 139 140 #print "this_attribute" 141 r = path_to_context(context, 1) 142 return r.encode("utf-8") 143 144 def new_attribute(context, name): 145 146 """ 147 Exposed as {template:new-attribute(name)}. 148 Provides a reference to a new attribute of the given 'name' on the current 149 element in the form data structure. 150 """ 151 152 #print "new_attribute" 153 name = unicode(name, "utf-8") 154 r = path_to_context(context, 0) + "/" + name 155 return r.encode("utf-8") 156 157 def other_elements(context, nodes): 158 159 """ 160 Exposed as {template:other-elements(nodes)}. 161 Provides a reference to other elements in the form data structure according 162 to the specified 'nodes' parameter (an XPath expression in the template). 163 """ 164 165 #print "other_elements" 166 names = [] 167 for node in nodes: 168 name = path_to_node(libxml2dom.Node(node), 0, None, 0) 169 if name not in names: 170 names.append(name) 171 r = ",".join(names) 172 return r.encode("utf-8") 173 174 def list_attribute(context, element_name, attribute_name): 175 176 """ 177 Exposed as {template:list-attribute(element_name, attribute_name)}. 178 Provides a reference to one or many elements of the given 'element_name' 179 found under the current element in the form data structure having 180 attributes with the given 'attribute_name'. 181 """ 182 183 #print "list_attribute" 184 element_name = unicode(element_name, "utf-8") 185 attribute_name = unicode(attribute_name, "utf-8") 186 r = path_to_context(context, 0, (element_name, attribute_name)) 187 return r.encode("utf-8") 188 189 def other_list_attributes(context, element_name, attribute_name, nodes): 190 191 """ 192 Exposed as {template:other-list-attributes(element_name, attribute_name, nodes)}. 193 Provides a reference to other elements in the form data structure, found 194 under the specified 'nodes' (described using an XPath expression in the 195 template) having the given 'element_name' and bearing attributes of the 196 given 'attribute_name'. 197 """ 198 199 #print "other_list_attributes" 200 element_name = unicode(element_name, "utf-8") 201 attribute_name = unicode(attribute_name, "utf-8") 202 names = [] 203 for node in nodes: 204 name = path_to_node(libxml2dom.Node(node), 0, (element_name, attribute_name), 1) 205 if name not in names: 206 names.append(name) 207 r = ",".join(names) 208 return r.encode("utf-8") 209 210 def other_attributes(context, attribute_name, nodes): 211 212 """ 213 Exposed as {template:other-attributes(name, nodes)}. 214 Provides a reference to attributes in the form data structure of the given 215 'attribute_name' residing on the specified 'nodes' (described using an XPath 216 expression in the template). 217 """ 218 219 #print "other_attributes" 220 attribute_name = unicode(attribute_name, "utf-8") 221 # NOTE: Cannot directly reference attributes in the nodes list because 222 # NOTE: libxml2dom does not yet support parent element discovery on 223 # NOTE: attributes. 224 names = [] 225 for node in nodes: 226 name = path_to_node(libxml2dom.Node(node), 1, attribute_name, 0) 227 if name not in names: 228 names.append(name) 229 r = ",".join(names) 230 return r.encode("utf-8") 231 232 def child_element(context, element_name, position, node_paths): 233 234 """ 235 Exposed as {template:child-element(element_name, position, node_paths)}. 236 Provides relative paths to the specifed 'element_name', having the given 237 'position' (1-based) under each element specified in 'node_paths' (provided 238 by calls to other extension functions in the template). For example: 239 240 template:child-element('comment', 1, template:this-element()) -> '.../comment$1' 241 """ 242 243 element_name = unicode(element_name, "utf-8") 244 l = [] 245 for node_path in node_paths.split(","): 246 l.append(node_path + Constants.path_separator + element_name 247 + Constants.pair_separator + str(int(position))) 248 return ",".join(l).encode("utf-8") 249 250 def child_attribute(context, attribute_name, node_paths): 251 252 """ 253 Exposed as {template:child-attribute(attribute_name, node_paths)}. 254 Provides a relative path to the specifed 'attribute_name' for each element 255 specified in 'node_paths' (provided by calls to other extension functions in 256 the template). For example: 257 258 template:child-attribute('value', template:this-element()) -> '.../value' 259 """ 260 261 attribute_name = unicode(attribute_name, "utf-8") 262 l = [] 263 for node_path in node_paths.split(","): 264 l.append(node_path + Constants.path_separator + attribute_name) 265 return ",".join(l).encode("utf-8") 266 267 def selector_name(context, field_name, nodes): 268 269 """ 270 Exposed as {template:selector-name(field_name, nodes)}. 271 Provides a selector field name defined using 'field_name' and referring to 272 the given 'nodes'. For example: 273 274 template:selector-name('add-platform', package/platforms) -> 'add-platform=/package$1/platforms$1' 275 276 NOTE: The 'nodes' must be element references. 277 """ 278 279 #print "selector_name" 280 names = [] 281 for node in nodes: 282 name = path_to_node(libxml2dom.Node(node), 0, None, 0) 283 if name not in names: 284 names.append(field_name + "=" + name) 285 r = ",".join(names) 286 return r.encode("utf-8") 287 288 # Old implementations. 289 290 def multi_field_name(context, multivalue_name): 291 #print "multi_field_name" 292 multivalue_name = unicode(multivalue_name, "utf-8") 293 r = path_to_context(context, 1, multivalue_name) 294 return r.encode("utf-8") 295 296 def other_multi_field_names(context, multivalue_name, nodes): 297 #print "other_multi_field_names" 298 multivalue_name = unicode(multivalue_name, "utf-8") 299 names = [] 300 for node in nodes: 301 name = path_to_node(libxml2dom.Node(node), 1, multivalue_name, 1) 302 if name not in names: 303 names.append(name) 304 r = ",".join(names) 305 return r.encode("utf-8") 306 307 # Utility functions. 308 309 def url_encode(context, nodes, charset="utf-8"): 310 311 """ 312 Exposed as {template:url-encode(nodes)}. 313 Provides a "URL encoded" string created from the merged textual contents of 314 the given 'nodes', with the encoded character values representing characters 315 in the optional 'charset' (UTF-8 if not specified). 316 317 template:url-encode(./text(), 'iso-8859-1') 318 """ 319 320 l = [] 321 for node in nodes: 322 s = libxml2dom.Node(node).nodeValue 323 l.append(urllib.quote(s.encode("utf-8")).replace("/", "%2F")) 324 output = "".join(l) 325 return output 326 327 def element_path(context, field_names): 328 329 """ 330 Convert the given 'field_names' back to XPath references. 331 For example: 332 /configuration$1/details$1/base-system$$value -> /*[position() = 1]/*[position() = 1]/base-system 333 If more than one field name is given - ie. 'field_names' contains a 334 comma-separated list of names - then only the first name is used. 335 """ 336 337 field_name = field_names.split(",")[0] 338 339 # Get the main part of the name (where a multivalue reference was given). 340 341 field_name = get_field_name(field_name) 342 343 # Build the XPath expression. 344 345 parts = field_name.split(Constants.path_separator) 346 new_parts = [] 347 for part in parts: 348 path_parts = part.split(Constants.pair_separator) 349 if len(path_parts) == 2: 350 new_parts.append("*[position() = " + path_parts[1] + "]") 351 else: 352 new_parts.append(path_parts[0]) 353 return "/".join(new_parts) 354 355 # New functions. 356 357 libxsltmod.xsltRegisterExtModuleFunction("list-attribute", "http://www.boddie.org.uk/ns/xmltools/template", list_attribute) 358 libxsltmod.xsltRegisterExtModuleFunction("other-list-attributes", "http://www.boddie.org.uk/ns/xmltools/template", other_list_attributes) 359 libxsltmod.xsltRegisterExtModuleFunction("other-attributes", "http://www.boddie.org.uk/ns/xmltools/template", other_attributes) 360 libxsltmod.xsltRegisterExtModuleFunction("child-element", "http://www.boddie.org.uk/ns/xmltools/template", child_element) 361 libxsltmod.xsltRegisterExtModuleFunction("child-attribute", "http://www.boddie.org.uk/ns/xmltools/template", child_attribute) 362 libxsltmod.xsltRegisterExtModuleFunction("selector-name", "http://www.boddie.org.uk/ns/xmltools/template", selector_name) 363 364 # New names. 365 366 libxsltmod.xsltRegisterExtModuleFunction("this-element", "http://www.boddie.org.uk/ns/xmltools/template", this_element) 367 libxsltmod.xsltRegisterExtModuleFunction("this-attribute", "http://www.boddie.org.uk/ns/xmltools/template", this_attribute) 368 libxsltmod.xsltRegisterExtModuleFunction("new-attribute", "http://www.boddie.org.uk/ns/xmltools/template", new_attribute) 369 libxsltmod.xsltRegisterExtModuleFunction("other-elements", "http://www.boddie.org.uk/ns/xmltools/template", other_elements) 370 371 # Old names. 372 373 libxsltmod.xsltRegisterExtModuleFunction("this-position", "http://www.boddie.org.uk/ns/xmltools/template", this_element) 374 libxsltmod.xsltRegisterExtModuleFunction("field-name", "http://www.boddie.org.uk/ns/xmltools/template", this_attribute) 375 libxsltmod.xsltRegisterExtModuleFunction("new-field", "http://www.boddie.org.uk/ns/xmltools/template", new_attribute) 376 libxsltmod.xsltRegisterExtModuleFunction("other-field-names", "http://www.boddie.org.uk/ns/xmltools/template", other_elements) 377 378 # Old functions. 379 380 libxsltmod.xsltRegisterExtModuleFunction("multi-field-name", "http://www.boddie.org.uk/ns/xmltools/template", multi_field_name) 381 libxsltmod.xsltRegisterExtModuleFunction("other-multi-field-names", "http://www.boddie.org.uk/ns/xmltools/template", other_multi_field_names) 382 383 # Utility functions. 384 385 libxsltmod.xsltRegisterExtModuleFunction("url-encode", "http://www.boddie.org.uk/ns/xmltools/template", url_encode) 386 libxsltmod.xsltRegisterExtModuleFunction("element-path", "http://www.boddie.org.uk/ns/xmltools/template", element_path) 387 388 def get_field_name(field_or_multi_name): 389 return field_or_multi_name.split(Constants.multi_separator)[0] 390 391 # vim: tabstop=4 expandtab shiftwidth=4