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 27 """ 28 import libxml2 29 30 def quiet(context, s): 31 pass 32 33 libxml2.registerErrorHandler(quiet, None) 34 """ 35 36 def path_to_node(node, attribute_ref, name, multivalue=0): 37 38 """ 39 Generate an XSLForms path to the given 'node', producing an attribute 40 reference if 'attribute_ref' is true; for example: 41 42 /package$1/discriminators$5/discriminator$1/category 43 44 Otherwise an element reference is produced; for example: 45 46 /package$1/discriminators$5/discriminator$1 47 48 Use the given 'name' to complete the path if an attribute reference is 49 required (and if a genuine attribute is found at the context node - 50 otherwise 'name' will be None and the context node will be treated like an 51 attribute). 52 53 If 'multivalue' is true and 'attribute_ref' is set, produce an attribute 54 reference using the given 'name': 55 56 /package$1/categories$1/category$$name 57 58 If 'multivalue' is true and 'attribute_ref' is not set, produce an attribute 59 reference using the given 'name' of form (element, attribute): 60 61 /package$1/categories$1/element$$attribute 62 """ 63 64 l = [] 65 # Skip attribute reference. 66 if node.nodeType == node.ATTRIBUTE_NODE: 67 node = node.parentNode 68 # Manually insert the attribute name if defined. 69 if attribute_ref: 70 # A real attribute is referenced. 71 if name is not None: 72 l.insert(0, name) 73 if multivalue: 74 l.insert(0, Constants.multi_separator) 75 l.insert(0, node.nodeName) 76 node = node.parentNode 77 l.insert(0, Constants.path_separator) 78 # Otherwise, treat the element name as an attribute name. 79 # NOTE: Not sure how useful this is. 80 else: 81 l.insert(0, node.nodeName) 82 l.insert(0, Constants.path_separator) 83 node = node.parentNode 84 # Otherwise insert any multivalue references (eg. list-attribute). 85 elif multivalue: 86 element_name, attribute_name = name 87 l.insert(0, attribute_name) 88 l.insert(0, Constants.multi_separator) 89 l.insert(0, element_name) 90 l.insert(0, Constants.path_separator) 91 92 # Element references. 93 while node is not None and node.nodeType != node.DOCUMENT_NODE: 94 l.insert(0, str(int(node.xpath("count(preceding-sibling::*) + 1")))) 95 l.insert(0, Constants.pair_separator) 96 l.insert(0, node.nodeName) 97 l.insert(0, Constants.path_separator) 98 node = node.parentNode 99 return "".join(l) 100 101 def path_to_context(context, attribute_ref, multivalue_name=None): 102 103 """ 104 As a libxslt extension function, return a string containing the XSLForms 105 path to the 'context' node, using the special "this-name" variable to 106 complete the path if an attribute reference is required (as indicated by 107 'attribute_ref' being set to true). If 'multivalue_name' is set, produce a 108 reference to a multivalued field using the given string as the attribute 109 name. 110 """ 111 112 context = libxml2mod.xmlXPathParserGetContext(context) 113 transform_context = libxsltmod.xsltXPathGetTransformContext(context) 114 name_var = libxsltmod.xsltVariableLookup(transform_context, "this-name", None) 115 if multivalue_name is not None: 116 name = multivalue_name 117 multivalue = 1 118 elif name_var is not None: 119 name = libxml2mod.xmlNodeGetContent(name_var[0]) 120 multivalue = 0 121 else: 122 name = None 123 multivalue = 0 124 node = libxml2dom.Node(libxml2mod.xmlXPathGetContextNode(context)) 125 return path_to_node(node, attribute_ref, name, multivalue) 126 127 # Exposed extension functions. 128 129 def this_element(context): 130 131 """ 132 Exposed as {template:this-element()}. 133 Provides a reference to the current element in the form data structure. 134 """ 135 136 #print "this_element" 137 r = path_to_context(context, 0) 138 return r.encode("utf-8") 139 140 def this_attribute(context): 141 142 """ 143 Exposed as {template:this-attribute()}. 144 Provides a reference to the current attribute in the form data structure. 145 """ 146 147 #print "this_attribute" 148 r = path_to_context(context, 1) 149 return r.encode("utf-8") 150 151 def new_attribute(context, name): 152 153 """ 154 Exposed as {template:new-attribute(name)}. 155 Provides a reference to a new attribute of the given 'name' on the current 156 element in the form data structure. 157 """ 158 159 #print "new_attribute" 160 r = path_to_context(context, 0) + "/" + name 161 return r.encode("utf-8") 162 163 def other_elements(context, nodes): 164 165 """ 166 Exposed as {template:other-elements(nodes)}. 167 Provides a reference to other elements in the form data structure according 168 to the specified 'nodes' parameter (an XPath expression in the template). 169 """ 170 171 #print "other_elements" 172 names = [] 173 for node in nodes: 174 name = path_to_node(libxml2dom.Node(node), 0, None, 0) 175 if name not in names: 176 names.append(name) 177 r = ",".join(names) 178 return r.encode("utf-8") 179 180 def list_attribute(context, element_name, attribute_name): 181 182 """ 183 Exposed as {template:list-attribute(element_name, attribute_name)}. 184 Provides a reference to one or many elements of the given 'element_name' 185 found under the current element in the form data structure having 186 attributes with the given 'attribute_name'. 187 """ 188 189 #print "list_attribute" 190 r = path_to_context(context, 0, (element_name, attribute_name)) 191 return r.encode("utf-8") 192 193 def other_list_attributes(context, element_name, attribute_name, nodes): 194 195 """ 196 Exposed as {template:other-list-attributes(element_name, attribute_name, nodes)}. 197 Provides a reference to other elements in the form data structure, found 198 under the specified 'nodes' (described using an XPath expression in the 199 template) having the given 'element_name' and bearing attributes of the 200 given 'attribute_name'. 201 """ 202 203 #print "other_list_attributes" 204 names = [] 205 for node in nodes: 206 name = path_to_node(libxml2dom.Node(node), 0, (element_name, attribute_name), 1) 207 if name not in names: 208 names.append(name) 209 r = ",".join(names) 210 return r.encode("utf-8") 211 212 def other_attributes(context, attribute_name, nodes): 213 214 """ 215 Exposed as {template:other-attributes(name, nodes)}. 216 Provides a reference to attributes in the form data structure of the given 217 'attribute_name' residing on the specified 'nodes' (described using an XPath 218 expression in the template). 219 """ 220 221 #print "other_attributes" 222 # NOTE: Cannot directly reference attributes in the nodes list because 223 # NOTE: libxml2dom does not yet support parent element discovery on 224 # NOTE: attributes. 225 names = [] 226 for node in nodes: 227 name = path_to_node(libxml2dom.Node(node), 1, attribute_name, 0) 228 if name not in names: 229 names.append(name) 230 r = ",".join(names) 231 return r.encode("utf-8") 232 233 def child_element(context, element_name, position, node_paths): 234 235 """ 236 Exposed as {template:child-element(element_name, position, node_paths)}. 237 Provides relative paths to the specifed 'element_name', having the given 238 'position' (1-based) under each element specified in 'node_paths' (provided 239 by calls to other extension functions in the template). For example: 240 241 template:child-element('comment', 1, template:this-element()) -> '.../comment$1' 242 """ 243 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 l = [] 262 for node_path in node_paths.split(","): 263 l.append(node_path + Constants.path_separator + attribute_name) 264 return ",".join(l).encode("utf-8") 265 266 # Old implementations. 267 268 def multi_field_name(context, multivalue_name): 269 #print "multi_field_name" 270 r = path_to_context(context, 1, multivalue_name) 271 return r.encode("utf-8") 272 273 def other_multi_field_names(context, multivalue_name, nodes): 274 #print "other_multi_field_names" 275 names = [] 276 for node in nodes: 277 name = path_to_node(libxml2dom.Node(node), 1, multivalue_name, 1) 278 if name not in names: 279 names.append(name) 280 r = ",".join(names) 281 return r.encode("utf-8") 282 283 # New functions. 284 285 libxsltmod.xsltRegisterExtModuleFunction("list-attribute", "http://www.boddie.org.uk/ns/xmltools/template", list_attribute) 286 libxsltmod.xsltRegisterExtModuleFunction("other-list-attributes", "http://www.boddie.org.uk/ns/xmltools/template", other_list_attributes) 287 libxsltmod.xsltRegisterExtModuleFunction("other-attributes", "http://www.boddie.org.uk/ns/xmltools/template", other_attributes) 288 libxsltmod.xsltRegisterExtModuleFunction("child-element", "http://www.boddie.org.uk/ns/xmltools/template", child_element) 289 libxsltmod.xsltRegisterExtModuleFunction("child-attribute", "http://www.boddie.org.uk/ns/xmltools/template", child_attribute) 290 291 # New names. 292 293 libxsltmod.xsltRegisterExtModuleFunction("this-element", "http://www.boddie.org.uk/ns/xmltools/template", this_element) 294 libxsltmod.xsltRegisterExtModuleFunction("this-attribute", "http://www.boddie.org.uk/ns/xmltools/template", this_attribute) 295 libxsltmod.xsltRegisterExtModuleFunction("new-attribute", "http://www.boddie.org.uk/ns/xmltools/template", new_attribute) 296 libxsltmod.xsltRegisterExtModuleFunction("other-elements", "http://www.boddie.org.uk/ns/xmltools/template", other_elements) 297 298 # Old names. 299 300 libxsltmod.xsltRegisterExtModuleFunction("this-position", "http://www.boddie.org.uk/ns/xmltools/template", this_element) 301 libxsltmod.xsltRegisterExtModuleFunction("field-name", "http://www.boddie.org.uk/ns/xmltools/template", this_attribute) 302 libxsltmod.xsltRegisterExtModuleFunction("new-field", "http://www.boddie.org.uk/ns/xmltools/template", new_attribute) 303 libxsltmod.xsltRegisterExtModuleFunction("other-field-names", "http://www.boddie.org.uk/ns/xmltools/template", other_elements) 304 305 # Old functions. 306 307 libxsltmod.xsltRegisterExtModuleFunction("multi-field-name", "http://www.boddie.org.uk/ns/xmltools/template", multi_field_name) 308 libxsltmod.xsltRegisterExtModuleFunction("other-multi-field-names", "http://www.boddie.org.uk/ns/xmltools/template", other_multi_field_names) 309 310 def get_field_name(field_or_multi_name): 311 return field_or_multi_name.split(Constants.multi_separator)[0] 312 313 def get_element_path(field_or_multi_name): 314 315 """ 316 Convert the given 'field_or_multi_name' back to an XPath reference. 317 For example: 318 /configuration$1/details$1/base-system$$value -> /*[position() = 1]/*[position() = 1]/base-system 319 """ 320 321 field_name = get_field_name(field_or_multi_name) 322 parts = field_name.split(Constants.path_separator) 323 new_parts = [] 324 for part in parts: 325 path_parts = part.split(Constants.pair_separator) 326 if len(path_parts) == 2: 327 new_parts.append("*[position() = " + path_parts[1] + "]") 328 else: 329 new_parts.append(path_parts[0]) 330 return "/".join(new_parts) 331 332 # vim: tabstop=4 expandtab shiftwidth=4