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 """ 29 import libxml2 30 31 def quiet(context, s): 32 pass 33 34 libxml2.registerErrorHandler(quiet, None) 35 """ 36 37 def path_to_node(node, attribute_ref, name, multivalue=0): 38 39 """ 40 Generate an XSLForms path to the given 'node', producing an attribute 41 reference if 'attribute_ref' is true; for example: 42 43 /package$1/discriminators$5/discriminator$1/category 44 45 Otherwise an element reference is produced; for example: 46 47 /package$1/discriminators$5/discriminator$1 48 49 Use the given 'name' to complete the path if an attribute reference is 50 required (and if a genuine attribute is found at the context node - 51 otherwise 'name' will be None and the context node will be treated like an 52 attribute). 53 54 If 'multivalue' is true and 'attribute_ref' is set, produce an attribute 55 reference using the given 'name': 56 57 /package$1/categories$1/category$$name 58 59 If 'multivalue' is true and 'attribute_ref' is not set, produce an attribute 60 reference using the given 'name' of form (element, attribute): 61 62 /package$1/categories$1/element$$attribute 63 """ 64 65 l = [] 66 # Skip attribute reference. 67 if node.nodeType == node.ATTRIBUTE_NODE: 68 node = node.parentNode 69 # Manually insert the attribute name if defined. 70 if attribute_ref: 71 # A real attribute is referenced. 72 if name is not None: 73 l.insert(0, name) 74 if multivalue: 75 l.insert(0, Constants.multi_separator) 76 l.insert(0, node.nodeName) 77 node = node.parentNode 78 l.insert(0, Constants.path_separator) 79 # Otherwise, treat the element name as an attribute name. 80 # NOTE: Not sure how useful this is. 81 else: 82 l.insert(0, node.nodeName) 83 l.insert(0, Constants.path_separator) 84 node = node.parentNode 85 # Otherwise insert any multivalue references (eg. list-attribute). 86 elif multivalue: 87 element_name, attribute_name = name 88 l.insert(0, attribute_name) 89 l.insert(0, Constants.multi_separator) 90 l.insert(0, element_name) 91 l.insert(0, Constants.path_separator) 92 93 # Element references. 94 while node is not None and node.nodeType != node.DOCUMENT_NODE: 95 l.insert(0, str(int(node.xpath("count(preceding-sibling::*) + 1")))) 96 l.insert(0, Constants.pair_separator) 97 l.insert(0, node.nodeName) 98 l.insert(0, Constants.path_separator) 99 node = node.parentNode 100 return "".join(l) 101 102 def path_to_context(context, attribute_ref, multivalue_name=None): 103 104 """ 105 As a libxslt extension function, return a string containing the XSLForms 106 path to the 'context' node, using the special "this-name" variable to 107 complete the path if an attribute reference is required (as indicated by 108 'attribute_ref' being set to true). If 'multivalue_name' is set, produce a 109 reference to a multivalued field using the given string as the attribute 110 name. 111 """ 112 113 context = libxml2mod.xmlXPathParserGetContext(context) 114 transform_context = libxsltmod.xsltXPathGetTransformContext(context) 115 name_var = libxsltmod.xsltVariableLookup(transform_context, "this-name", None) 116 if multivalue_name is not None: 117 name = multivalue_name 118 multivalue = 1 119 elif name_var is not None: 120 name = libxml2mod.xmlNodeGetContent(name_var[0]) 121 name = unicode(name, "utf-8") 122 multivalue = 0 123 else: 124 name = None 125 multivalue = 0 126 node = libxml2dom.Node(libxml2mod.xmlXPathGetContextNode(context)) 127 return path_to_node(node, attribute_ref, name, multivalue) 128 129 # Exposed extension functions. 130 131 def this_element(context): 132 133 """ 134 Exposed as {template:this-element()}. 135 Provides a reference to the current element in the form data structure. 136 """ 137 138 #print "this_element" 139 r = path_to_context(context, 0) 140 return r.encode("utf-8") 141 142 def this_attribute(context): 143 144 """ 145 Exposed as {template:this-attribute()}. 146 Provides a reference to the current attribute in the form data structure. 147 """ 148 149 #print "this_attribute" 150 r = path_to_context(context, 1) 151 return r.encode("utf-8") 152 153 def new_attribute(context, name): 154 155 """ 156 Exposed as {template:new-attribute(name)}. 157 Provides a reference to a new attribute of the given 'name' on the current 158 element in the form data structure. 159 """ 160 161 #print "new_attribute" 162 name = unicode(name, "utf-8") 163 r = path_to_context(context, 0) + "/" + name 164 return r.encode("utf-8") 165 166 def other_elements(context, nodes): 167 168 """ 169 Exposed as {template:other-elements(nodes)}. 170 Provides a reference to other elements in the form data structure according 171 to the specified 'nodes' parameter (an XPath expression in the template). 172 """ 173 174 #print "other_elements" 175 names = [] 176 for node in nodes: 177 name = path_to_node(libxml2dom.Node(node), 0, None, 0) 178 if name not in names: 179 names.append(name) 180 r = ",".join(names) 181 return r.encode("utf-8") 182 183 def list_attribute(context, element_name, attribute_name): 184 185 """ 186 Exposed as {template:list-attribute(element_name, attribute_name)}. 187 Provides a reference to one or many elements of the given 'element_name' 188 found under the current element in the form data structure having 189 attributes with the given 'attribute_name'. 190 """ 191 192 #print "list_attribute" 193 element_name = unicode(element_name, "utf-8") 194 attribute_name = unicode(attribute_name, "utf-8") 195 r = path_to_context(context, 0, (element_name, attribute_name)) 196 return r.encode("utf-8") 197 198 def other_list_attributes(context, element_name, attribute_name, nodes): 199 200 """ 201 Exposed as {template:other-list-attributes(element_name, attribute_name, nodes)}. 202 Provides a reference to other elements in the form data structure, found 203 under the specified 'nodes' (described using an XPath expression in the 204 template) having the given 'element_name' and bearing attributes of the 205 given 'attribute_name'. 206 """ 207 208 #print "other_list_attributes" 209 element_name = unicode(element_name, "utf-8") 210 attribute_name = unicode(attribute_name, "utf-8") 211 names = [] 212 for node in nodes: 213 name = path_to_node(libxml2dom.Node(node), 0, (element_name, attribute_name), 1) 214 if name not in names: 215 names.append(name) 216 r = ",".join(names) 217 return r.encode("utf-8") 218 219 def other_attributes(context, attribute_name, nodes): 220 221 """ 222 Exposed as {template:other-attributes(name, nodes)}. 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, "utf-8") 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("utf-8") 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 Provides relative paths to the specifed 'element_name', having the given 246 'position' (1-based) under each element specified in 'node_paths' (provided 247 by calls to other extension functions in the template). For example: 248 249 template:child-element('comment', 1, template:this-element()) -> '.../comment$1' 250 """ 251 252 element_name = unicode(element_name, "utf-8") 253 l = [] 254 for node_path in node_paths.split(","): 255 l.append(node_path + Constants.path_separator + element_name 256 + Constants.pair_separator + str(int(position))) 257 return ",".join(l).encode("utf-8") 258 259 def child_attribute(context, attribute_name, node_paths): 260 261 """ 262 Exposed as {template:child-attribute(attribute_name, node_paths)}. 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 # Old implementations. 277 278 def multi_field_name(context, multivalue_name): 279 #print "multi_field_name" 280 multivalue_name = unicode(multivalue_name, "utf-8") 281 r = path_to_context(context, 1, multivalue_name) 282 return r.encode("utf-8") 283 284 def other_multi_field_names(context, multivalue_name, nodes): 285 #print "other_multi_field_names" 286 multivalue_name = unicode(multivalue_name, "utf-8") 287 names = [] 288 for node in nodes: 289 name = path_to_node(libxml2dom.Node(node), 1, multivalue_name, 1) 290 if name not in names: 291 names.append(name) 292 r = ",".join(names) 293 return r.encode("utf-8") 294 295 # Utility functions. 296 297 def url_encode(context, nodes, charset="utf-8"): 298 299 """ 300 Exposed as {template:url-encode(nodes)}. 301 Provides a "URL encoded" string created from the merged textual contents of 302 the given 'nodes', with the encoded character values representing characters 303 in the optional 'charset' (UTF-8 if not specified). 304 305 template:url-encode(./text(), 'iso-8859-1') 306 """ 307 308 l = [] 309 for node in nodes: 310 s = libxml2dom.Node(node).nodeValue 311 l.append(urllib.quote(s.encode("utf-8")).replace("/", "%2F")) 312 output = "".join(l) 313 return output 314 315 def element_path(context, field_names): 316 317 """ 318 Convert the given 'field_names' back to XPath references. 319 For example: 320 /configuration$1/details$1/base-system$$value -> /*[position() = 1]/*[position() = 1]/base-system 321 If more than one field name is given - ie. 'field_names' contains a 322 comma-separated list of names - then only the first name is used. 323 """ 324 325 field_name = field_names.split(",")[0] 326 327 # Get the main part of the name (where a multivalue reference was given). 328 329 field_name = get_field_name(field_name) 330 331 # Build the XPath expression. 332 333 parts = field_name.split(Constants.path_separator) 334 new_parts = [] 335 for part in parts: 336 path_parts = part.split(Constants.pair_separator) 337 if len(path_parts) == 2: 338 new_parts.append("*[position() = " + path_parts[1] + "]") 339 else: 340 new_parts.append(path_parts[0]) 341 return "/".join(new_parts) 342 343 # New functions. 344 345 libxsltmod.xsltRegisterExtModuleFunction("list-attribute", "http://www.boddie.org.uk/ns/xmltools/template", list_attribute) 346 libxsltmod.xsltRegisterExtModuleFunction("other-list-attributes", "http://www.boddie.org.uk/ns/xmltools/template", other_list_attributes) 347 libxsltmod.xsltRegisterExtModuleFunction("other-attributes", "http://www.boddie.org.uk/ns/xmltools/template", other_attributes) 348 libxsltmod.xsltRegisterExtModuleFunction("child-element", "http://www.boddie.org.uk/ns/xmltools/template", child_element) 349 libxsltmod.xsltRegisterExtModuleFunction("child-attribute", "http://www.boddie.org.uk/ns/xmltools/template", child_attribute) 350 351 # New names. 352 353 libxsltmod.xsltRegisterExtModuleFunction("this-element", "http://www.boddie.org.uk/ns/xmltools/template", this_element) 354 libxsltmod.xsltRegisterExtModuleFunction("this-attribute", "http://www.boddie.org.uk/ns/xmltools/template", this_attribute) 355 libxsltmod.xsltRegisterExtModuleFunction("new-attribute", "http://www.boddie.org.uk/ns/xmltools/template", new_attribute) 356 libxsltmod.xsltRegisterExtModuleFunction("other-elements", "http://www.boddie.org.uk/ns/xmltools/template", other_elements) 357 358 # Old names. 359 360 libxsltmod.xsltRegisterExtModuleFunction("this-position", "http://www.boddie.org.uk/ns/xmltools/template", this_element) 361 libxsltmod.xsltRegisterExtModuleFunction("field-name", "http://www.boddie.org.uk/ns/xmltools/template", this_attribute) 362 libxsltmod.xsltRegisterExtModuleFunction("new-field", "http://www.boddie.org.uk/ns/xmltools/template", new_attribute) 363 libxsltmod.xsltRegisterExtModuleFunction("other-field-names", "http://www.boddie.org.uk/ns/xmltools/template", other_elements) 364 365 # Old functions. 366 367 libxsltmod.xsltRegisterExtModuleFunction("multi-field-name", "http://www.boddie.org.uk/ns/xmltools/template", multi_field_name) 368 libxsltmod.xsltRegisterExtModuleFunction("other-multi-field-names", "http://www.boddie.org.uk/ns/xmltools/template", other_multi_field_names) 369 370 # Utility functions. 371 372 libxsltmod.xsltRegisterExtModuleFunction("url-encode", "http://www.boddie.org.uk/ns/xmltools/template", url_encode) 373 libxsltmod.xsltRegisterExtModuleFunction("element-path", "http://www.boddie.org.uk/ns/xmltools/template", element_path) 374 375 def get_field_name(field_or_multi_name): 376 return field_or_multi_name.split(Constants.multi_separator)[0] 377 378 # vim: tabstop=4 expandtab shiftwidth=4