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 # Old implementations. 268 269 def multi_field_name(context, multivalue_name): 270 #print "multi_field_name" 271 multivalue_name = unicode(multivalue_name, "utf-8") 272 r = path_to_context(context, 1, multivalue_name) 273 return r.encode("utf-8") 274 275 def other_multi_field_names(context, multivalue_name, nodes): 276 #print "other_multi_field_names" 277 multivalue_name = unicode(multivalue_name, "utf-8") 278 names = [] 279 for node in nodes: 280 name = path_to_node(libxml2dom.Node(node), 1, multivalue_name, 1) 281 if name not in names: 282 names.append(name) 283 r = ",".join(names) 284 return r.encode("utf-8") 285 286 # Utility functions. 287 288 def url_encode(context, nodes, charset="utf-8"): 289 290 """ 291 Exposed as {template:url-encode(nodes)}. 292 Provides a "URL encoded" string created from the merged textual contents of 293 the given 'nodes', with the encoded character values representing characters 294 in the optional 'charset' (UTF-8 if not specified). 295 296 template:url-encode(./text(), 'iso-8859-1') 297 """ 298 299 l = [] 300 for node in nodes: 301 s = libxml2dom.Node(node).nodeValue 302 l.append(urllib.quote(s.encode("utf-8")).replace("/", "%2F")) 303 output = "".join(l) 304 return output 305 306 def element_path(context, field_names): 307 308 """ 309 Convert the given 'field_names' back to XPath references. 310 For example: 311 /configuration$1/details$1/base-system$$value -> /*[position() = 1]/*[position() = 1]/base-system 312 If more than one field name is given - ie. 'field_names' contains a 313 comma-separated list of names - then only the first name is used. 314 """ 315 316 field_name = field_names.split(",")[0] 317 318 # Get the main part of the name (where a multivalue reference was given). 319 320 field_name = get_field_name(field_name) 321 322 # Build the XPath expression. 323 324 parts = field_name.split(Constants.path_separator) 325 new_parts = [] 326 for part in parts: 327 path_parts = part.split(Constants.pair_separator) 328 if len(path_parts) == 2: 329 new_parts.append("*[position() = " + path_parts[1] + "]") 330 else: 331 new_parts.append(path_parts[0]) 332 return "/".join(new_parts) 333 334 # New functions. 335 336 libxsltmod.xsltRegisterExtModuleFunction("list-attribute", "http://www.boddie.org.uk/ns/xmltools/template", list_attribute) 337 libxsltmod.xsltRegisterExtModuleFunction("other-list-attributes", "http://www.boddie.org.uk/ns/xmltools/template", other_list_attributes) 338 libxsltmod.xsltRegisterExtModuleFunction("other-attributes", "http://www.boddie.org.uk/ns/xmltools/template", other_attributes) 339 libxsltmod.xsltRegisterExtModuleFunction("child-element", "http://www.boddie.org.uk/ns/xmltools/template", child_element) 340 libxsltmod.xsltRegisterExtModuleFunction("child-attribute", "http://www.boddie.org.uk/ns/xmltools/template", child_attribute) 341 342 # New names. 343 344 libxsltmod.xsltRegisterExtModuleFunction("this-element", "http://www.boddie.org.uk/ns/xmltools/template", this_element) 345 libxsltmod.xsltRegisterExtModuleFunction("this-attribute", "http://www.boddie.org.uk/ns/xmltools/template", this_attribute) 346 libxsltmod.xsltRegisterExtModuleFunction("new-attribute", "http://www.boddie.org.uk/ns/xmltools/template", new_attribute) 347 libxsltmod.xsltRegisterExtModuleFunction("other-elements", "http://www.boddie.org.uk/ns/xmltools/template", other_elements) 348 349 # Old names. 350 351 libxsltmod.xsltRegisterExtModuleFunction("this-position", "http://www.boddie.org.uk/ns/xmltools/template", this_element) 352 libxsltmod.xsltRegisterExtModuleFunction("field-name", "http://www.boddie.org.uk/ns/xmltools/template", this_attribute) 353 libxsltmod.xsltRegisterExtModuleFunction("new-field", "http://www.boddie.org.uk/ns/xmltools/template", new_attribute) 354 libxsltmod.xsltRegisterExtModuleFunction("other-field-names", "http://www.boddie.org.uk/ns/xmltools/template", other_elements) 355 356 # Old functions. 357 358 libxsltmod.xsltRegisterExtModuleFunction("multi-field-name", "http://www.boddie.org.uk/ns/xmltools/template", multi_field_name) 359 libxsltmod.xsltRegisterExtModuleFunction("other-multi-field-names", "http://www.boddie.org.uk/ns/xmltools/template", other_multi_field_names) 360 361 # Utility functions. 362 363 libxsltmod.xsltRegisterExtModuleFunction("url-encode", "http://www.boddie.org.uk/ns/xmltools/template", url_encode) 364 libxsltmod.xsltRegisterExtModuleFunction("element-path", "http://www.boddie.org.uk/ns/xmltools/template", element_path) 365 366 def get_field_name(field_or_multi_name): 367 return field_or_multi_name.split(Constants.multi_separator)[0] 368 369 # vim: tabstop=4 expandtab shiftwidth=4