# HG changeset patch
# User paulb
# Date 1103502639 0
# Node ID 8a3b53c14b8a382a824cfc65a195e3d3d377b229
# Parent 15f403f72a2b1e49265b5fff04fa390d24f62d77
[project @ 2004-12-20 00:30:39 by paulb]
Initial revision
diff -r 15f403f72a2b -r 8a3b53c14b8a XSLForms/Fields.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/XSLForms/Fields.py Mon Dec 20 00:30:39 2004 +0000
@@ -0,0 +1,332 @@
+#!/usr/bin/env python
+
+"""
+Classes which process field collections, producing instance
+documents. Each field entry consists of a field name mapped
+to a string value, where the field name may have the following
+formats:
+
+ /name1#n1/name2
+ /name1#n1/name2#n2/name3
+ /name1#n1/name2#n2/name3#n3/name4
+ ...
+
+The indexes n1, n2, n3, ... indicate the position of elements
+(starting from 1) in the entire element list, whose elements
+may have different names. For example:
+
+ /zoo#1/name
+ /zoo#1/cage#1:name
+ /zoo#1/cage#2:name
+ /zoo#1/funding#3/contributor#1/name
+
+Some fields may contain the "=" string. This string is
+reserved and all text following it is meant to specify a path
+into a particular document. For example:
+
+ _action_add_animal=/zoo#1/cage#2
+"""
+
+import libxml2dom
+from xml.dom import EMPTY_NAMESPACE
+
+class FieldsError(Exception):
+ pass
+
+class Fields:
+
+ """
+ A class which converts fields in the documented form to XML
+ instance documents.
+ """
+
+ _path_separator = "/"
+ _pair_separator = "#"
+ _selector_indicator = "="
+
+ def __init__(self, encoding="utf-8"):
+
+ """
+ Initialise the fields processor with the given 'encoding',
+ which is optional and which only applies to field data in
+ Python string form (and not Unicode objects).
+ """
+
+ self.encoding = encoding
+
+ def complete_documents(self, documents, fields):
+
+ """
+ Complete the given 'documents' using the 'fields' items list.
+ """
+
+ for field, value in fields:
+
+ # Ignore selectors.
+
+ if field.find(self._selector_indicator) != -1:
+ continue
+
+ model_name, components = self._get_model_name_and_components(field)
+ if model_name is None:
+ continue
+
+ # Convert the value to Unicode if necessary.
+
+ if type(value) == type(""):
+ value = unicode(value, encoding=self.encoding)
+
+ # Get a new instance document if none has been made for the
+ # model.
+
+ if not documents.has_key(model_name):
+ documents[model_name] = self._new_instance(model_name)
+ node = documents[model_name]
+
+ # Traverse the components within the instance.
+
+ for component in components:
+ t = component.split(self._pair_separator)
+ if len(t) == 1:
+ node.setAttributeNS(EMPTY_NAMESPACE, t[0], value)
+ break
+
+ elif len(t) == 2:
+
+ # Convert from one-based indexing (the position() function)
+ # to zero-based indexing.
+
+ name, index = t[0], int(t[1]) - 1
+ if index < 0:
+ break
+ node = self._enter_element(node, name, index)
+
+ def complete_selectors(self, selectors, fields, documents):
+
+ """
+ Fill in the given 'selectors' dictionary using the given
+ 'fields' so that it contains mappings from selector names to
+ parts of the specified 'documents'.
+ """
+
+ for field, value in fields:
+
+ # Process selectors only.
+
+ selector_components = field.split(self._selector_indicator)
+ if len(selector_components) < 2:
+ continue
+
+ # Get the selector name and path.
+ # Note that the joining of the components uses the separator,
+ # but the separator really should not exist in the path.
+
+ selector_name = selector_components[0]
+ path = self._selector_indicator.join(selector_components[1:])
+
+ model_name, components = self._get_model_name_and_components(path)
+ if model_name is None:
+ continue
+
+ # Go to the instance element.
+
+ if not documents.has_key(model_name) or documents[model_name] is None:
+ continue
+
+ node = documents[model_name]
+
+ # Traverse the path to find the part of the document to be
+ # selected.
+
+ for component in components:
+ t = component.split(self._pair_separator)
+ if len(t) == 1:
+
+ # Select attribute.
+
+ node = node.getAttributeNodeNS(EMPTY_NAMESPACE, t[0])
+ break
+
+ elif len(t) == 2:
+
+ # Convert from one-based indexing (the position() function)
+ # to zero-based indexing.
+
+ name, index = t[0], int(t[1]) - 1
+ if index < 0:
+ break
+
+ # NOTE: Controversial creation of potentially non-existent
+ # NOTE: nodes.
+
+ node = self._enter_element(node, name, index)
+
+ if not selectors.has_key(selector_name):
+ selectors[selector_name] = []
+ selectors[selector_name].append(node)
+
+ def _enter_element(self, node, name, index):
+
+ """
+ From 'node' enter the element with the given 'name' at the
+ given 'index' position amongst the child elements. Create
+ missing child elements if necessary.
+ """
+
+ self._ensure_elements(node, index)
+
+ elements = node.xpath("*")
+ if elements[index].localName == "placeholder":
+ new_node = node.ownerDocument.createElementNS(EMPTY_NAMESPACE, name)
+ node.replaceChild(new_node, elements[index])
+ else:
+ new_node = elements[index]
+ if new_node.localName != name:
+ raise FieldsError, (new_node.localName, name, elements, index)
+
+ # Enter the newly-created element.
+
+ return new_node
+
+ def _get_model_name_and_components(self, field):
+
+ """
+ From 'field', return the model name and components which
+ describe the path within the instance document associated
+ with that model.
+ """
+
+ # Get the components of the field name.
+ # Example: /name1#n1/name2#n2/name3
+ # Expected: ['', 'name1#n1', 'name2#n2', 'name3']
+
+ components = field.split(self._path_separator)
+ if len(components) < 2:
+ return None, None
+
+ # Extract the model name from the top-level element
+ # specification.
+ # Expected: ['name1', 'n1']
+
+ model_name_and_index = components[1].split(self._pair_separator)
+ if len(model_name_and_index) != 2:
+ return None, None
+
+ # Expected: 'name1', ['', 'name1#n1', 'name2#n2', 'name3']
+
+ return model_name_and_index[0], components[1:]
+
+ def _ensure_elements(self, document, index):
+
+ """
+ In the given 'document', extend the child elements list
+ so that a node can be stored at the given 'index'.
+ """
+
+ elements = document.xpath("*")
+ i = len(elements)
+ while i <= index:
+ new_node = document.ownerDocument.createElementNS(EMPTY_NAMESPACE, "placeholder")
+ document.appendChild(new_node)
+ i += 1
+
+ def make_documents(self, fields):
+
+ """
+ Make a dictionary mapping model names to new documents prepared
+ from the given 'fields' dictionary.
+ """
+
+ documents = {}
+ self.complete_documents(documents, fields)
+
+ # Fix the dictionary to return the actual document root.
+
+ for model_name, instance_root in documents.items():
+ documents[model_name] = instance_root
+ return documents
+
+ def get_selectors(self, fields, documents):
+
+ """
+ Get a dictionary containing a mapping of selector names to
+ selected parts of the given 'documents'.
+ """
+
+ selectors = {}
+ self.complete_selectors(selectors, fields, documents)
+ return selectors
+
+ def _new_instance(self, name):
+
+ "Return an instance root of the given 'name' in a new document."
+
+ return libxml2dom.createDocument(EMPTY_NAMESPACE, name, None)
+
+if __name__ == "__main__":
+
+ d = [
+ ("_action_update", "Some value"),
+ ("_action_delete=/zoo#1/cage#2", "Some value"),
+ ("/actions#1/update#1/selected", "Some value"), # Not actually used in output documents or input.
+ ("/zoo#1/name", "The Zoo זרו"),
+ ("/zoo#1/cage#1/name", "reptiles"),
+ ("/zoo#1/cage#1/capacity", "5"),
+ ("/zoo#1/cage#1/animal#1/name", "Monty"),
+ ("/zoo#1/cage#1/animal#1/species#1/name", "Python"),
+ ("/zoo#1/cage#1/animal#1/property#2/name", "texture"),
+ ("/zoo#1/cage#1/animal#1/property#2/value", "scaled"),
+ ("/zoo#1/cage#1/animal#1/property#3/name", "length"),
+ ("/zoo#1/cage#1/animal#1/property#3/value", "5m"),
+ ("/zoo#1/cage#1/animal#2/name", "Vincent"),
+ ("/zoo#1/cage#1/animal#2/species#1/name", "Lizard"),
+ ("/zoo#1/cage#1/animal#2/property#2/name", "colour"),
+ ("/zoo#1/cage#1/animal#2/property#2/value", "variable"),
+ ("/zoo#1/cage#1/animal#2/property#3/name", "length"),
+ ("/zoo#1/cage#1/animal#2/property#3/value", "1m"),
+ ("/zoo#1/cage#2/name", "mammals"),
+ ("/zoo#1/cage#2/capacity", "25"),
+ ("/zoo#1/cage#2/animal#1/name", "Simon"),
+ ("/zoo#1/cage#2/animal#1/species#1/name", "Giraffe"),
+ ("/zoo#1/cage#2/animal#2/name", "Leonard"),
+ ("/zoo#1/cage#2/animal#2/species#1/name", "Lion"),
+ ("/zoo#1/cage#2/animal#2/property#2/name", "danger"),
+ ("/zoo#1/cage#2/animal#2/property#2/value", "high"),
+ ("/zoo#1/funding#3/type", "private"),
+ ("/zoo#1/funding#3/contributor#1/name", "Animal Corporation"),
+ ("/zoo#1/funding#3/contributor#1/amount", "543210.987")
+ ]
+
+ import time
+ import sys, cmdsyntax
+
+ # Find the documents.
+
+ syntax = cmdsyntax.Syntax("""
+ --plain-output=OUTPUT_FILE
+ --instance-name=NAME
+ """)
+
+ syntax_matches = syntax.get_args(sys.argv[1:])
+
+ try:
+ args = syntax_matches[0]
+ except IndexError:
+ print syntax.syntax
+ sys.exit(1)
+
+ # Create an object to interpret the test data.
+
+ fields = Fields("iso-8859-1")
+
+ t = time.time()
+ documents = fields.make_documents(d)
+ print "Building time", time.time() - t
+
+ t = time.time()
+ libxml2dom.toStream(documents[args["instance-name"]], stream=open(args["plain-output"], "wb"), encoding="utf-8")
+ print "Prettyprinting time", time.time() - t
+
+ print "Selectors", repr(fields.get_selectors(d, documents))
+
+# vim: tabstop=4 expandtab shiftwidth=4
diff -r 15f403f72a2b -r 8a3b53c14b8a XSLForms/Output.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/XSLForms/Output.py Mon Dec 20 00:30:39 2004 +0000
@@ -0,0 +1,50 @@
+#!/usr/bin/env python
+
+"""
+XSL-based form templating.
+"""
+
+import libxslt
+
+"""
+import libxml2
+
+def quiet(context, s):
+ pass
+
+libxml2.registerErrorHandler(quiet, None)
+"""
+
+def path_to_node(node):
+
+ "Generate an XSLForms path to the given 'node'."
+
+ l = []
+ # Attribute reference.
+ l.insert(0, node.name)
+ l.insert(0, "/")
+ node = node.parent
+ # Element references.
+ while node is not None and node.type != "document_xml":
+ l.insert(0, str(int(node.xpathEval("count(preceding-sibling::*) + 1"))))
+ l.insert(0, "#")
+ l.insert(0, node.name)
+ l.insert(0, "/")
+ node = node.parent
+ return "".join(l)
+
+def this_position(context):
+
+ """
+ As a libxslt extension function, return a string containing the XSLForms
+ path to the 'context' node.
+ """
+
+ pctxt = libxslt.xpathParserContext(_obj=context)
+ context = pctxt.context()
+ node = context.contextNode()
+ return path_to_node(node)
+
+libxslt.registerExtModuleFunction("this-position", "http://www.boddie.org.uk/ns/xmltools/template", this_position)
+
+# vim: tabstop=4 expandtab shiftwidth=4
diff -r 15f403f72a2b -r 8a3b53c14b8a XSLForms/Prepare.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/XSLForms/Prepare.py Mon Dec 20 00:30:39 2004 +0000
@@ -0,0 +1,19 @@
+#!/usr/bin/env python
+
+"""
+Preparation of templating stylesheets.
+"""
+
+import XSLOutput
+import libxml2dom
+import os
+
+resource_dir = os.path.join(os.path.split(__file__)[0], "XSL")
+
+def make_stylesheet(template_name, output_name, stylesheet_name="Prepare.xsl", encoding="utf-8"):
+ global resource_dir
+ proc = XSLOutput.Processor([os.path.join(resource_dir, stylesheet_name)])
+ template = libxml2dom.parse(template_name)
+ proc.send_output(open(output_name, "wb"), encoding, template)
+
+# vim: tabstop=4 expandtab shiftwidth=4
diff -r 15f403f72a2b -r 8a3b53c14b8a XSLForms/XSL/Prepare.xsl
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/XSLForms/XSL/Prepare.xsl Mon Dec 20 00:30:39 2004 +0000
@@ -0,0 +1,141 @@
+
+