Howto ===== XIST is an extensible HTML/XML generator written in Python. It was developed as a replacement for an HTML preprocessor named HSC and borrows some features and ideas from it. It also borrows the basic ideas (XML/HTML elements as Python objects) from HTMLgen or HyperText. (If you're impatient, there's also a list of examples that shows what can be done with XIST.) Overview ======== XIST can be used as a compiler that reads an input XML file and generates a transformed output file, or it could be used for generating XML dynamically inside a web server (but note that handling object trees is slower than simply sending string fragments). In either case generating the final HTML or XML output requires the following three steps: * Generating a source XML tree: This can be done either by parsing an XML file, or by directly constructing the tree -- as HTMLgen and HyperText do -- as a tree of Python objects. XIST provides a very natural and pythonic API for that. * Converting the source tree into a target tree: This target tree can be a HTML tree or a SVG tree or XSL-FO tree or any other XML tree you like. Every node class provides a convert method for performing this conversion. For your own XML element types you have to define your own element classes and implement an appropriate convert method. This is possible for processing instructions and entity references too. * Publishing the target tree: For generating the final output a Publisher object is used that generates the encoded byte string fragments that can be written to an output stream (or yielded from a WSGI application, etc.). Constructing XML trees ====================== Like any other XML tree API, XIST provides the usual classes: * Element for XML elements; * Attr for attributes; * Attrs for attribute mappings; * Text for text data; * Frag for document fragments, (a Frag object is simply a list of nodes); * Comment for XML comments (e.g. ); * ProcInst for processing instructions (e.g. ); * Entity for entity references (e.g. &parrot;) and * DocType for document type declarations (e.g. ). XML trees as Python objects --------------------------- XIST works somewhat different from a normal DOM API. Instead of only one element class, XIST has one class for every element type. All the elements from different XML vocabularies known to XIST are defined in modules in the ll.xist.ns subpackage. (Of course it's possible to define additional namespaces for your own XML vocabulary). The definition of HTML can be found in ll.xist.ns.html for example. Every element class has a constructor of the form: __init__(self, *content, **attrs) Positional arguments (i.e. items in content) will be the child nodes of the element node. Keyword arguments will be attributes. You can pass most of Python's builtin types to such a constructor. Strings (str and unicode) and integers will be automatically converted to Text objects. Constructing an HTML element works like this: from ll.xist.ns import html node = html.div( "Hello ", html.a("Python", href="http://www.python.org/"), " world!" ) For attribute names that collide with Python keywords or are not legal identifiers (most notably class in HTML) the attribute name must be slightly modified, so that it's a legal Python identifier (for class an underscore is appended): node = html.div( "Hello world!", class_="greeting" ) (Don't worry: This modified attribute name will be mapped to the real official attribute name once the output is generated.) You can pass attributes as a dictionary too: node = html.div( "Hello world!", { "class_": "greeting", "id": 42, "title": "Greet the world" } ) Generating XML trees from XML files ----------------------------------- XML trees can also be generated by parsing XML files. For this the module ll.xist.parsers provides several functions: def parseString(text, base=None, sysid=None, **parserargs) def parseURL(url, base=None, sysid=None, **parserargs) def parseFile(filename, base=None, sysid=None, **parserargs) def parse(stream, base=None, sysid=None, **parserargs) parseString is for parsing strings (str and unicode) and parseURL is for parsing resources from URLs. With parseFile you can parse local files and with parse you can directly parse from a file-like object. All four functions create a parser internally, parse the supplied source document and return the resulting object tree. For example, parsing a string can be done like this: from ll.xist import parsers from ll.xist.ns import html node = parsers.parseString( "

Hello Python world!

" ) For further info about the rest of the arguments to the parsing functions, see the documentation for ll.xist.parsers.Parser. Defining new elements and converting XML trees ============================================== To be able to parse an XML file, you have to provide an element class for every element type that appears in the file. These classes either come from modules provided by XIST or you can define your own. Defining your own element class for an element named cool works like this: class cool(xsc.Element): def convert(self, converter): node = html.b(self.content, u" is cool!") return node.convert(converter) You have to derive your new class from xsc.Element. The name of the class will be the element name. For element type names that are invalid Python identifiers, you can use the class attribute xmlname in the element class to overwrite the element name. To be able to convert an element of this type to a new XML tree (probably HTML in most cases), you have to implement the convert method. In this method you can build a new XML tree from the content and attributes of the object. Using this new element is simple >>> node = cool("Python") >>> print node.conv().asBytes() Python is cool! conv simply calls convert with a default converter argument. We'll come to converters in a minute. asBytes is a method that converts the node to a byte string. This method will be explained when we discuss the publishing interface. Note that it is vital for your own convert methods that you recursively call convert on your own content, because otherwise some unconverted nodes might remain in the tree. Let's define a new element: class python(xsc.Element): def convert(self, converter): return html.a(u"Python", href=u"http://www.python.org/") Now we can do the following: >>> node = cool(python()) >>> print node.conv().asBytes() Python is cool! But if we forget to call convert for our own content, i.e. if the element cool was written like this: class cool(xsc.Element): def convert(self, converter): return html.b(self.content, " is cool!") we would get: >>> node = cool(python()) >>> print node.conv().asBytes() is cool! Furthermore convert should never modify self, because convert might be called multiple times for the same node. Converters ---------- conv is a convenience method that creates a default converter for you and calls convert. This converter is created once and is passed to all convert calls. It is used to store parameters for the conversion process and it allows elements to pass information to other nodes. You can also call convert yourself, which would look like this: from ll.xist import converters from ll.xist.ns import html node = cool(python()) node = node.convert(converters.Converter()) You can pass the following arguments to the Converter constructor: root root (which defaults to None) is the root URL for the conversion process. When you want to resolve a link in some of your own convert methods, the URL must be interpreted relative to this root URL (You can use URLAttr.forInput for that). mode mode (which defaults to None) works the same way as modes in XSLT. You can use this for implementing different conversion modes. stage stage (which defaults to "deliver") allows you to implement multi stage conversion: Suppose that you want to deliver a dynamically constructed web page with XIST that contains results from a database query and the current time. The data in the database changes infrequently, so it doesn't make sense to do the query on every request. The query is done every few minutes and the resulting HTML tree is stored in the servlet (using any of the available Python servlet technologies). For this conversion the stage would be "cache" and your database XML element would do the query when stage=="cache". Your time display element would do the conversion when stage=="deliver" and simply returns itself when stage=="cache", so it would still be part of the cached XML tree and would be converted to HTML on every request. target target (which defaults to ll.xist.ns.html) specifies what the output should be. Values must be namespace subclasses (see below for an explanation of namespaces). lang lang (which defaults to None) is the language in which the result tree should be. This can be used in the convert method to implement different conversions for different languages, e.g.: class note(xsc.Element): def convert(self, converter): if converter.lang==u"de": title = u"Anmerkung" elif converter.lang==u"en": title = u"Note" else: title = u"???" node = xsc.Frag( html.h1(title), html.div(self.content) ) return node.convert(converter) Additional arguments are passed when a converter is created in the context of a make script. Attributes ---------- Setting and accessing the attributes of an element works via the dictionary interface: >>> node = html.a(u"Python", href=u"http://www.python.org/") >>> print node[u"href"].asBytes() http://www.python.org/ >>> del node[u"href"] >>> print node[u"href"].asBytes() >>> node[u"href"] = u"http://www.python.org" >>> print node[u"href"].asBytes() http://www.python.org/ All attribute values are instances of subclasses of the class Attr. Available subclasses are: * TextAttr, for normal text attributes; * URLAttr, for attributes that are URLs; * BoolAttr, for boolean attributes (for such an attribute only its presence is important, it's value will always be the same as the attribute name when publishing); * IntAttr, for integer attributes; * ColorAttr, for color attributes (e.g. #ffffff). IntAttr and ColorAttr mostly serve as documentation of the attributes purpose. Both classes have no added functionality. Attr itself is derived from Frag so it is possible to use all the sequence methods on an attribute. Unset attributes will be treated like empty ones so the following is possible: del node["spam"] node["spam"].append("ham") This also means that after del node["spam"][:] the attribute will be empty again and will be considered to be unset. Such attributes will be skipped when publishing. The main purpose of this is to allow you to construct values conditionally and then use those values as attribute values: import random if random.random() < 0.5: class_ = None else: class_ = u"foo" node = html.div(u"foo", class_=class_) In 50% of the cases the generated div element will not have a class attribute. Defining attributes ------------------- When you define a new element you have to specify the attributes allowed for this element. For this use the class attribute Attrs (which must be a class derived from xsc.Element.Attrs) and define the attributes by deriving them from one of the existing attribute classes. We could extend our example element in the following way: class cool(xsc.Element): class Attrs(xsc.Element.Attrs): class adj(xsc.TextAttr): pass def convert(self, converter): node = xsc.Frag(self.content, u" is") if u"adj" in self.attrs: node.append(u" ", html.em(self[u"adj"])) node.append(u" cool!") return node.convert(converter) and use it like this: >>> node = cool(python(), adj=u"totally") >>> node.conv().asBytes() Python is totally cool! Default attributes ------------------ It is possible to define default values for attributes via the class attribute default: class cool(xsc.Element): class Attrs(xsc.Element.Attrs): class adj(xsc.TextAttr): default = u"absolutely" def convert(self, converter): node = xsc.Frag(self.content, u" is") if u"adj" in self.attrs: node.append(u" ", html.em(self[u"adj"])) node.append(u" cool!") return node.convert(converter) Now if we instantiate the class without specifying adj we'll get the default: >>> node = cool(python()) >>> print node.conv().asBytes() Python is absolutely cool! If we want a cool instance without an adj attribute, we can pass None as the attribute value: >>> node = cool(python(), adj=None) >>> print node.conv().asBytes() Python is cool! Attribute value sets -------------------- It's possible to specify that an attribute has a fixed set of allowed values. This can be done with the class attribute values. We could extend our example to look like this: class cool(xsc.Element): class Attrs(xsc.Element.Attrs): class adj(xsc.TextAttr): default = "absolutely" values = (u"absolutely", u"totally", u"very") def convert(self, converter): node = xsc.Frag(self.content, u" is") if u"adj" in self.attrs: node.append(" ", html.em(self[u"adj"])) node.append(u" cool!") return node.convert(converter) These values won't be checked when we create our cool instance. Only when this node is parsed from a file will the warning be issued: >>> s = '' >>> node = parsers.parseString(s) /home/walter/pythonroot/ll/xist/xsc.py:1665: IllegalAttrValueWarning: Attribute value 'pretty' not allowed for __main__:cool.Attrs.adj. warnings.warn(errors.IllegalAttrValueWarning(self)) The warning will also be issued if we publish such a node, but note that for warnings Python's warning framework is used, so the warning will be printed only once (but of course you can change that with warnings.filterwarnings): >>> node = cool(python(), adj=u"pretty") >>> print node.asBytes() /home/walter/pythonroot/ll/xist/xsc.py:1665: IllegalAttrValueWarning: Attribute value 'pretty' not allowed for __main__:cool.Attrs.adj. warnings.warn(errors.IllegalAttrValueWarning(self)) Required attributes ------------------- Finally it's possible to specify that an attribute is required. This again will only be checked when parsing or publishing. To specify that an attribute is required simply add the class attribute required with the value True. The attribute alt of the class ll.xist.ns.html.img is such an attribute, so we'll get: >>> from ll.xist.ns import html >>> node = html.img(src="eggs.png") >>> print node.asBytes() /home/walter/pythonroot/ll/xist/xsc.py:2046: RequiredAttrMissingWarning: Required attribute 'alt' missing in ll.xist.ns.html:img.Attrs. warnings.warn(errors.RequiredAttrMissingWarning(self, attrs.keys())) Namespaces ---------- Now that you've defined your own elements, you have to tell the parser about them, so they can be instantiated when a file is parsed. This is done with namespace classes. Namespace classes can be thought of as object oriented versions of XML namespaces. Two class attributes can be used to configure the namespace: xmlname specifies the default namespace prefix to use for the namespace and xmlurl is the namespace name. All element classes nested inside the namespace class belong to the namespace. It's also possible to define your namespace class without any nested element classes and later add those classes to the namespace by attribute assignment or with the class method update, which expects a dictionary as an argument. All objects found in the values of the dictionary will be added to the namespace as attributes. So you can put the namespace class at the end of your Python module after all the element classes are defined and add all the objects from the local scope to the namespace. Your complete namespace might looks like this: class python(xsc.Element): def convert(self, converter): return html.a( u"Python", href=u"http://www.python.org/" ) class cool(xsc.Element): def convert(self, converter): node = html.b(self.content, u" is cool!") return node.convert(converter) class __ns__(xsc.Namespace): xmlname = "foo" xmlurl = "http://www.example.com/foo" __ns__.update(vars()) All defined namespace classes will be registered with the parser automatically, so all elements belonging to the namespace will be used when parsing files. Namespaces as modules --------------------- It is convenient to define all classes that belong to one namespace in one Python module. However this means that the resulting module will only contain one "interesting" object: the namespace class. To make using this class more convenient, it's possible to turn the namespace class into a module by using the class method makemod instead of update: class python(xsc.Element): def convert(self, converter): return html.a( u"Python", href=u"http://www.python.org/" ) class cool(xsc.Element): def convert(self, converter): node = html.b(self.content, u" is cool!") return node.convert(converter) class __ns__(xsc.Namespace): xmlname = "foo" xmlurl = "http://www.example.com/foo" __ns__.makemod(vars()) Suppose that the above code is in the file foo.py. Doing an import foo will then give you the namespace class instead of the module: >>> import foo >>> foo >>> foo.python Global attributes ----------------- You can define global attributes belonging to a certain namespace in the same way as defining local attributes belonging to a certain element type: Define a nested Attrs class inside the namespace class (derived from ll.xist.xsc.Namespace.Attrs): from ll.xist import xsc class __ns__(xsc.Namespace): xmlname = "foo" xmlurl = "http://www.example.com/foo" class Attrs(xsc.Namespace.Attrs): class foo(xsc.TextAttr): pass __ns__.makemod(vars()) Setting and accessing such an attribute can be done like this: >>> from ll.xist.ns import html >>> import foo >>> node = html.div(u"foo", {(foo, u"foo"): u"bar") >>> str(node[foo, u"foo"]) 'bar' An alternate way of specifying a global attribute in a constructor looks like this: >>> from ll.xist.ns import html >>> import foo >>> node = html.div(u"foo", foo.Attrs(foo=u"baz")) >>> str(node[foo, u"foo"]) 'baz' Subclassing namespaces ---------------------- Each element class that belongs to a namespace can access its namespace via the class attribute __ns__. When you're subclassing namespace classes, the elements in the base namespace will be automatically subclassed too. Of course you can explicitly subclass an element class too. The following example shows the usefulness of this feature. Define your base namespace like this and put it into navns.py: from ll.xist import xsc from ll.xist.ns import html languages = [ (u"Python", u"http://www.python.org/"), (u"Perl", u"http://www.perl.org/"), (u"PHP", u"http://www.php.net/"), (u"Java", u"http://java.sun.com/") ] class navigation(xsc.Element): def convert(self, converter): node = self.__ns__.links() for (name, url) in languages: node.append(self.__ns__.link(name, href=url)) return node.convert(converter) class links(xsc.Element): def convert(self, converter): node = self.content return node.convert(converter) class link(xsc.Element): class Attrs(xsc.Element.Attrs): class href(xsc.URLAttr): pass def convert(self, converter): node = html.div(html.a(self.content, href=self[u"href"])) return node.convert(converter) class __ns__(xsc.Namespace): xmlname = "nav" xmlurl = "http://www.example.com/nav" __ns__.makemod(vars()) This namespace defines a navigation element that generates divs with links to various homepages for programming languages. We can use it like this: >>> import navns >>> print navns.navigation().conv().asBytes()
Python
Perl
PHP
Java
(Of course the output will all be on one line.) Now we can define a derived namespace (in the file nav2ns.py) that overwrites the element classes links and link to change how the navigation looks: from ll.xist import xsc from ll.xist.ns import html import navns class __ns__(navns): class links(navns.links): def convert(self, converter): node = html.table( self.content, border=0, cellpadding=0, cellspacing=0, class_=u"navigation", ) return node.convert(converter) class link(navns.link): def convert(self, converter): node = html.tr( html.td( html.a( self.content, href=self[u"href"], ) ) ) return node.convert(converter) __ns__.makemod(vars()) When we use the navigation element from the derived namespace we'll get the following output: >>> import nav2ns >>> print nav2ns.navigation().conv().asBytes() (again all on one line.) Notice that we automatically got an element class nav2ns.navigation, that this element class inherited the convert method from its base class and that the call to convert on the derived class did instantiate the link classes from the derived namespace. Namespaces as conversion targets -------------------------------- The converter argument passed to the convert method has an attribute target which is a namespace class and specifies the target namespace to which self should be converted. You can check which conversion is wanted with issubclass. Once this is determined you can use element classes from the target to create the required XML object tree. This makes it possible to customize the conversion by passing a derived namespace to the convert method. To demonstrate this, we change our example namespace to use the conversion target like this: import navns class __ns__(navns): class links(nav.links): def convert(self, converter): node = converter.target.table( self.content, border=0, cellpadding=0, cellspacing=0, class_=u"navigation", ) return node.convert(converter) class link(nav.link): def convert(self, converter): target = converter.target node = target.tr( target.td( target.a( self.content, href=self[u"href"], ) ) ) return node.convert(converter) What we might want to do is have all links (i.e. all ll.xist.ns.html.a elements) generated with an attribute target="_top". For this we derive a new namespace from ll.xist.ns.html and overwrite the a element: from ll.xist.ns import html class __ns__(html): class a(html.a): def convert(self, converter): node = html.a(self.content, self.attrs, target=u"_top") return node.convert(converter) Now we can pass this namespace as the conversion target and all links will have a target="_top". Validation and content models ----------------------------- When generating HTML you might want to make sure that your generated code doesn't contain any illegal tag nesting (i.e. something bad like

Foo

). The module ll.xist.ns.html does this automatically: >>> from ll.xist.ns import html >>> node = html.p(html.p(u"foo")) >>> print node.asBytes() /home/walter/pythonroot/ll/xist/sims.py:238: WrongElementWarning: element may not contain element warnings.warn(WrongElementWarning(node, child, self.elements))

foo

For your own elements you can specify the content model too. This is done by setting the class attribute model inside the element class. model must be an object that provides a checkvalid method. This method will be called during parsing or publishing with the element as an argument. When a validation violation is detected, the Python warning framework should be used to issue a warning. The module ll.xist.sims contains several classes that provide simple validation methods: Empty can be used to ensure that the element doesn't have any content (like br and img in HTML). Any does allow any content. NoElements will warn about elements from the same namespace (elements from other namespaces will be OK). NoElementsOrText will warn about elements from the same namespace and non-whitespace text content. Elements will only allow the elements specified in the constructor. ElementsOrText will only allow the elements specified in the constructor and text. None of these classes will check the number of child elements or their order. For more info see the sims module. Entities -------- In the same way as defining new element types, you can define new entities. But to be able to use the new entities in an XML file you have to use a parser that supports reporting undefined entities to the application via skippedEntity (SGMLOPParser and ExpatParser in the module ll.xist.parsers do that). The following example is from the module ll.xist.ns.abbr: from ll.xist import xsc from ll.xist.ns import html class html(xsc.Entity): def convert(self, converter): return html.abbr( u"HTML", title=u"Hypertext Markup Language", lang=u"en" ) You can use this entity in your XML files like this: &html; Processing instructions ----------------------- Defining processing instructions works just like elements and entities. Derive a new class from ll.xist.xsc.ProcInst and implement convert. The following example implements a processing instruction that returns an uppercase version of its content as a text node. class upper(xsc.ProcInst): def convert(self, converter): return xsc.Text(self.content.upper()) It can be used in an XML file like this: There are namespaces containing processing instruction classes that don't provide a convert method. These processing instruction objects will then be published as XML processing instructions. One example is the namespace ll.xist.ns.php. Other namespaces (like ll.xist.ns.jsp) contain processing instruction classes, but they will be published in a different (not XML compatible) format. For example ll.xist.ns.jsp.expression("foo") will be published as <%= foo>. Publishing XML trees ==================== After creating the XML tree and converting the tree into its final output form, you have to write the resulting tree to a file. This can be done with the publishing API. Three methods that use the publishing API are bytes, asBytes and write. bytes is a generator that will yield the complete 8-bit XML string in fragments. asBytes returns the complete 8-bit XML string. You can specify the encoding with the parameter encoding (with "utf-8" being the default). Unencodable characters will be escaped with character references when possible (i.e. inside text nodes, for comments or processing instructions you'll get an exception): >>> from ll.xist import xsc >>> from ll.xist.ns import html >>> s = u"A\e4\u03a9\u8a9e" >>> node = html.div(s) >>> print node.asBytes(encoding="ascii")
AäΩ語> >>> print node.asBytes(encoding="iso-8859-1")
AäΩ語> >>> print xsc.Comment(s).asBytes() Traceback (most recent call last): File "", line 1, in ? File "/home/walter/pythonroot/ll/xist/xsc.py", line 600, in asBytes publisher.publish(stream, self, base) File "/home/walter/pythonroot/ll/xist/publishers.py", line 205, in publish self.node.publish(self) File "/home/walter/pythonroot/ll/xist/xsc.py", line 1305, in publish publisher.write(self.content) File "/usr/local/lib/python2.3/codecs.py", line 178, in write data, consumed = self.encode(object, self.errors) UnicodeEncodeError: 'ascii' codec can't encode characters in position 1-4: ordinal not in range(128) When you include an XML header or an HTML meta header, XIST will automatically insert the correct encoding when publishing: >>> from ll.xist import xsc >>> from ll.xist.ns import xml, meta >>> e = xsc.Frag(xml.XML10(), u"\n", meta.contenttype()) >>> print e.conv().asBytes(encoding="iso-8859-15") Another useful parameter is xhtml, it specifies whether you want pure HTML or XHTML as output: xhtml==0 This will give you pure HTML, i.e. no final / for elements with an empty content model, so you'll get e.g.
in the output. Elements that don't have an empty content model, but are empty will be published with a start and end tag (i.e.
). xhtml==1 This gives HTML compatible XHTML. Elements with an empty content model will be published like this:
(This is the default). xhtml==2 This gives full XML output. Every empty element will be published with an empty tag (without an additional space):
or
. Writing a node to a file can be done with the method write: >>> from ll.xist.ns import html >>> node = html.div(u"äöü", html.br(), u"ÄÖÜ") >>> node.write(open("foo.html", "wb"), encoding="ascii") All these methods use the method publish internally. publish gets passed an instance of ll.xist.publisher.Publisher. Searching trees =============== There are two methods available for iterating through an XML tree and finding nodes in the tree: The walk method and XFind expressions. The walk method --------------- The method walk is a generator. You pass a callable object to walk which is used for determining which part of the tree should be searched and which nodes should be returned. ll.xist.xsc provides several useful predefined classes for specifying what should be returned from walk: FindType will search only the first level of the tree and will return any node that is an instance of one of the classes passed to the constructor. So if you have an instance of ll.xist.ns.html.ul named node you could search for all ll.xist.ns.html.li elements inside with the following code: for cursor in node.content.walk(xsc.FindType(html.li)): print unicode(cursor.node) FindTypeAll can be used when you want to search the complete tree. The following example extracts all the links on the Python home page: from ll.xist import xsc, parsers from ll.xist.ns import html node = parsers.parseURL("http://www.python.org/", tidy=True) for cursor in node.walk(xsc.FindTypeAll(html.a)): print cursor.node[u"href"] This gives the output: http://www.python.org/ http://www.python.org/search/ http://www.python.org/download/ http://www.python.org/doc/ http://www.python.org/Help.html http://www.python.org/dev/ ... The following example will find all external links on the Python home page: from ll.xist import xsc, parsers from ll.xist.ns import html node = parsers.parseURL("http://www.python.org/", tidy=True) def isextlink(cursor): if isinstance(cursor.node, html.a) and not unicode(cursor.node[u"href"]).startswith(u"http://www.python.org"): return (True, xsc.entercontent) return (xsc.entercontent,) for cursor in node.walk(isextlink): print cursor.node[u"href"] This gives the output: http://www.jython.org/ http://sourceforge.net/tracker/?atid=105470&group%5fid=5470 http://sourceforge.net/tracker/?atid=305470&group%5fid=5470 http://sourceforge.net/cvs/?group%5fid=5470 http://www.python-in-business.org/ http://www.europython.org/ mailto:webmaster@python.org ... The callable (isextlink in the example) will be called for each node visited. The cursor argument has an attribute node that is the node in question. For the other attributes see the Cursor class. The callable must return a sequence with the following entries: ll.xist.xsc.entercontent enter the content of this element and continue searching; ll.xist.xsc.enterattrs enter the attributes of this element and continue searching; boolean value If true, the node will be part of the result. The sequence will be "executed" in the order you specify. To change the top down traversal from our example to a bottom up traversal we could change isextlink to the following (note the swapped tuple entries): def isextlink(node): if isinstance(node, html.a) and not unicode(node[u"href"]).startswith(u"http://www.python.org"): return (xsc.entercontent, True) return (xsc.entercontent,) Note that the cursor yielded from walk will be reused by subsequent next calls, so you should not modify the cursor and you can't rely on attributes of the cursor after reentry to walk. XFind expressions ----------------- A second method exists for iterating through a tree: XFind expressions. An XFind expression looks somewhat like an XPath expression, but is implemented as a pure Python expression (overloading the division operators). Our example from above that searched for lis inside uls can be rewritten as follows: for child in node/html.li: print unicode(child) A XFind expression returns an iterator for certain parts of the XML tree. In an XFind expression a/b, a must be either a node or an iterator producing nodes (note that an XFind expression itself is such an iterator, so a itself might be a XFind expression). b must be an XFind operator. Every subclass of ll.xist.xsc.Node is a XFind operator. If b is such a subclass, a/b will produce any child nodes of the nodes from a that is an instance of b. If b is an attribute class, you will get attribute nodes instead of child nodes. Other XFind operators can be found in the module ll.xist.xfind. The all operator will produce every node in the tree (except for attributes): from ll.xist import xfind from ll.xist.ns import html node = html.div( html.div( html.div(id=3), html.div(id=4), id=2, ), html.div( html.div(id=6), html.div(id=7), id=5, ), id=1 ) for child in node/xfind.all: print child["id"] The output of this is: 1 2 3 4 5 6 7 The following example demonstrates how to find all links on the Python homepage via an XFind expression: from ll.xist import xfind, parsers from ll.xist.ns import html node = parsers.parseURL("http://www.python.org/", tidy=True) for link in node/xfind.all/html.a: print link["href"] An all operator in the middle of an XFind expression can be abbreviated. The XFind expression from the last example (node/xfind.all/html.a) can be rewritten like this: node//html.a. Another XFind operator is contains. It acts as a filter, i.e. the nodes produced by a/xfind.contains(b) are a subset of the nodes produced by a, those that contain child nodes of type b. Searching for all links on the Python home page that contain images can be done like this: >>> from ll.xist import xfind, parsers >>> from ll.xist.ns import html >>> node = parsers.parseURL("http://www.python.org/", tidy=True) >>> for link in node//html.a/xfind.contains(html.img): ... print link["href"] ... http://www.python.org/ http://www.python.org/psf/donations.html http://www.opensource.org/ Note that using the all operator twice in an XFind expression currently won't give you the expected result, as nodes might be produced twice. Calling __getitem__ on an XFind operator gives you an item operator. Such an item operator only returns a specific item (or slice) of those nodes returned by the base iterator. An example: >>> from ll.xist.ns import html >>> e = html.table(html.tr(html.td(j) for j in xrange(i, i+3)) for i in xrange(1, 10, 3)) >>> print e.pretty().asBytes()
1 2 3
4 5 6
7 8 9
>>> # Every cell >>> for td in e/html.tr/html.td: ... print td ... 1 2 3 4 5 6 7 8 9 >>> # Every first cell in each row >>> for td in e/html.tr/html.td[0]: ... print td 1 4 7 >>> # Every cell in the first row >>> for td in e/html.tr[0]/html.td: ... print td 1 2 3 >>> # The first of all cells >>> for td in e/(html.tr/html.td)[0]: ... print td 1 Manipulating trees ================== XIST provides many methods for manipulating an XML tree. The method withsep can be used to put a seperator node between the child nodes of an Element or a Frag: >>> from ll.xist import xsc >>> from ll.xist.ns import html >>> node = html.div(*xrange(10)) >>> print node.withsep(", ").asBytes()
0, 1, 2, 3, 4, 5, 6, 7, 8, 9
The method shuffled returns a shuffled version of the Element or Frag: >>> from ll.xist import xsc >>> from ll.xist.ns import html >>> node = html.div(*xrange(10)) >>> print node.shuffled().withsep(", ").asBytes()
8, 1, 3, 6, 7, 5, 2, 9, 4, 0
There are methods named reversed and sorted that return a reversed or sorted version of an element or fragment: >>> from ll.xist import xsc >>> from ll.xist.ns import html >>> def key(n): ... return unicode(n) >>> node = html.div(8,4,2,1,9,6,3,0,7,5) >>> print node.sorted(key=key).reversed().withsep(",").asBytes()
9,8,7,6,5,4,3,2,1,0
The method mapped recursively walks the tree and generates a new tree, where all the nodes are mapped through a function. An example: To replace Python with Parrot in every text node on the Python page, do the following: from ll.xist import xsc, parsers def p2p(node, converter): if isinstance(node, xsc.Text): node = node.replace(u"Python", u"Parrot") node = node.replace(u"python", u"parrot") return node node = parsers.parseURL("http://www.python.org/", tidy=True) node = node.mapped(p2p) node.write(open("parrot_index.html", "wb")) The function must either return a new node, in which case this new node will be used instead of the old one, or return the old node to tell mapped that it should recursively continue with the content of the node. URLs ==== For URL handling XIST uses the module ll.url. Refer to its documentation for the basic functionality (especially regarding the methods __div__ and relative). When XIST parses an XML resource it uses a so called "base" URL. This base URL can be passed to all parsing functions. If it isn't specified it defaults to the URL of the resource being parsed. This base URL will be prepended to all URLs that are read during parsing: >>> from ll.xist import parsers >>> from ll.xist.ns import html >>> node = parsers.parseString('', base="root:spam/index.html") >>> print node.asBytes() For publishing a base URL can be specified too. URLs will be published relative to this base URL with the exception of relative URLs in the tree. This means: * When you have a relative URL (e.g. #top) generated by a convert call, this URL will stay the same when publishing. * Base URLs for parsing should never be relative: Relative base URLs will be prepended to all relative URLs in the file, but this will not be reverted for publishing. In most cases the base URL should be a root URL when you parse local files. * When you parse remote web pages you can either omit the base argument, so it will default to the URL being parsing, so that links, images, etc. on the page will still point back to their original location, or you might want to use the empty URL URL() as the base, so you'll get all URLs in the page as they are. * When XIST is used as a compiler for static pages, you're going to read source XML files, do a conversion and write the result to a new target file. In this case you should probably use the URL of the target file for both parsing and publishing. Let's assume we have an URL #top in the source file. When we use the "real" file names for parsing and publishing like this: node = parsers.parseFile("spam.htmlxsc", base="root:spam.htmlxsc") node = node.conv() node.write(open("spam.html", "wb"), base="root:spam.html") the following will happen: The URL #top will be parsed as root:spam.htmlxsc#top. After conversion this will be written to spam.html relative to the URL root:spam.html, which results in spam.html#top, which works, but is not what you want. When you use root:spam.html both for parsing and publishing, #top will be written to the target file as expected. Pretty printing XML =================== The method pretty can be used for pretty printing XML. It returns a new version of the node, with additional white space between the elements: from ll.xist.ns import html node = html.html( html.head( html.title(u"foo"), ), html.body( html.div( html.h1(u"The ", html.em(u"foo"), u" page!"), html.p(u"Welcome to the ", html.em(u"foo"), u" page."), ), ), ) print node.pretty().asBytes() This will print: foo

The foo page!

Welcome to the foo page.

Element content will only be modified if it doesn't contain Text nodes, so mixed content will not be touched. Automatic generation of image size attributes ============================================= The module ll.xist.ns.htmlspecials contains an element autoimg that extends ll.xist.ns.html.img. When converted to HTML via the convert method the size of the image will be determined and the height and width attributes will be set accordingly (if those attributes are not set already). Embedding Python code ===================== It's possible to embed Python code into XIST XML files. For this XIST supports two new processing instructions: pyexec and pyeval (in the module ll.xist.ns.code). The content of pyexec will be executed when the processing instruction node is converted. The result of a call to convert for a pyeval processing instruction is whatever the Python code in the content returns. The processing instruction content is treated as the body of a function, so you can put multiple return statements there. The converter is available as the parameter converter inside the processing instruction. For example, consider the following XML file: Parsing this file and calling convert results in the following: 5050