changeset 1:5479aae32f5a trunk

Initial import.
author cmlenz
date Sat, 03 Jun 2006 07:16:01 +0000
parents 5f9862282a9d
children edbbe45da6e2
files doc/2000ft.graffle doc/2000ft.png markup/__init__.py markup/builder.py markup/core.py markup/eval.py markup/filters.py markup/input.py markup/output.py markup/path.py markup/template.py markup/tests/__init__.py markup/tests/builder.py markup/tests/core.py markup/tests/eval.py markup/tests/input.py markup/tests/output.py markup/tests/path.py markup/tests/template.py setup.cfg setup.py
diffstat 21 files changed, 4187 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
new file mode 100755
--- /dev/null
+++ b/doc/2000ft.graffle
@@ -0,0 +1,1094 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>ActiveLayerIndex</key>
+	<integer>0</integer>
+	<key>AutoAdjust</key>
+	<false/>
+	<key>CanvasColor</key>
+	<dict>
+		<key>w</key>
+		<string>1</string>
+	</dict>
+	<key>CanvasOrigin</key>
+	<string>{0, 0}</string>
+	<key>CanvasScale</key>
+	<real>1</real>
+	<key>ColumnAlign</key>
+	<integer>1</integer>
+	<key>ColumnSpacing</key>
+	<real>36</real>
+	<key>CreationDate</key>
+	<string>2006-05-08 14:33:49 +0200</string>
+	<key>Creator</key>
+	<string>chris</string>
+	<key>DisplayScale</key>
+	<string>1 cm = 1 cm</string>
+	<key>GraphDocumentVersion</key>
+	<integer>5</integer>
+	<key>GraphicsList</key>
+	<array>
+		<dict>
+			<key>Bounds</key>
+			<string>{{241.464, 258}, {94.5364, 31}}</string>
+			<key>Class</key>
+			<string>ShapedGraphic</string>
+			<key>FontInfo</key>
+			<dict>
+				<key>Font</key>
+				<string>Helvetica</string>
+				<key>Size</key>
+				<real>12</real>
+			</dict>
+			<key>ID</key>
+			<integer>50</integer>
+			<key>Shape</key>
+			<string>Rectangle</string>
+			<key>Style</key>
+			<dict>
+				<key>shadow</key>
+				<dict>
+					<key>Draws</key>
+					<string>NO</string>
+				</dict>
+			</dict>
+			<key>Text</key>
+			<dict>
+				<key>Text</key>
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf330
+{\fonttbl\f0\fswiss\fcharset77 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
+
+\f0\fs22 \cf0 ...}</string>
+			</dict>
+		</dict>
+		<dict>
+			<key>Class</key>
+			<string>LineGraphic</string>
+			<key>Head</key>
+			<dict>
+				<key>ID</key>
+				<integer>29</integer>
+			</dict>
+			<key>ID</key>
+			<integer>48</integer>
+			<key>OrthogonalBarAutomatic</key>
+			<true/>
+			<key>OrthogonalBarPosition</key>
+			<real>-1</real>
+			<key>Points</key>
+			<array>
+				<string>{372, 149.5}</string>
+				<string>{317.768, 149.5}</string>
+			</array>
+			<key>Style</key>
+			<dict>
+				<key>stroke</key>
+				<dict>
+					<key>HeadArrow</key>
+					<string>FilledArrow</string>
+					<key>LineType</key>
+					<integer>2</integer>
+					<key>TailArrow</key>
+					<string>0</string>
+				</dict>
+			</dict>
+			<key>Tail</key>
+			<dict>
+				<key>ID</key>
+				<integer>46</integer>
+			</dict>
+		</dict>
+		<dict>
+			<key>Bounds</key>
+			<string>{{387.348, 138}, {94.5364, 31}}</string>
+			<key>Class</key>
+			<string>ShapedGraphic</string>
+			<key>FontInfo</key>
+			<dict>
+				<key>Font</key>
+				<string>Helvetica</string>
+				<key>Size</key>
+				<real>12</real>
+			</dict>
+			<key>ID</key>
+			<integer>47</integer>
+			<key>Shape</key>
+			<string>Rectangle</string>
+			<key>Style</key>
+			<dict>
+				<key>shadow</key>
+				<dict>
+					<key>Draws</key>
+					<string>NO</string>
+				</dict>
+			</dict>
+			<key>Text</key>
+			<dict>
+				<key>Text</key>
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf330
+{\fonttbl\f0\fswiss\fcharset77 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
+
+\f0\fs22 \cf0 Template}</string>
+			</dict>
+		</dict>
+		<dict>
+			<key>Bounds</key>
+			<string>{{372, 119}, {125.232, 61}}</string>
+			<key>Class</key>
+			<string>ShapedGraphic</string>
+			<key>FontInfo</key>
+			<dict>
+				<key>Color</key>
+				<dict>
+					<key>b</key>
+					<string>1</string>
+					<key>g</key>
+					<string>1</string>
+					<key>r</key>
+					<string>1</string>
+				</dict>
+				<key>Font</key>
+				<string>Helvetica-Bold</string>
+				<key>Size</key>
+				<real>12</real>
+			</dict>
+			<key>ID</key>
+			<integer>46</integer>
+			<key>Shape</key>
+			<string>Rectangle</string>
+			<key>Style</key>
+			<dict>
+				<key>fill</key>
+				<dict>
+					<key>Color</key>
+					<dict>
+						<key>b</key>
+						<string>0.835052</string>
+						<key>g</key>
+						<string>0.669573</string>
+						<key>r</key>
+						<string>0.544872</string>
+					</dict>
+				</dict>
+				<key>shadow</key>
+				<dict>
+					<key>Draws</key>
+					<string>NO</string>
+				</dict>
+				<key>stroke</key>
+				<dict>
+					<key>CornerRadius</key>
+					<real>5</real>
+					<key>Draws</key>
+					<string>NO</string>
+				</dict>
+			</dict>
+			<key>Text</key>
+			<dict>
+				<key>Text</key>
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf330
+{\fonttbl\f0\fswiss\fcharset77 Helvetica-BoldOblique;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
+
+\f0\i\b\fs24 \cf1 templating}</string>
+				<key>VerticalPad</key>
+				<integer>2</integer>
+			</dict>
+			<key>TextPlacement</key>
+			<integer>0</integer>
+		</dict>
+		<dict>
+			<key>Class</key>
+			<string>LineGraphic</string>
+			<key>Head</key>
+			<dict>
+				<key>ID</key>
+				<integer>29</integer>
+			</dict>
+			<key>ID</key>
+			<integer>45</integer>
+			<key>OrthogonalBarAutomatic</key>
+			<true/>
+			<key>OrthogonalBarPosition</key>
+			<real>-1</real>
+			<key>Points</key>
+			<array>
+				<string>{168, 149.5}</string>
+				<string>{222.232, 149.5}</string>
+			</array>
+			<key>Style</key>
+			<dict>
+				<key>stroke</key>
+				<dict>
+					<key>HeadArrow</key>
+					<string>FilledArrow</string>
+					<key>LineType</key>
+					<integer>2</integer>
+					<key>TailArrow</key>
+					<string>0</string>
+				</dict>
+			</dict>
+			<key>Tail</key>
+			<dict>
+				<key>ID</key>
+				<integer>44</integer>
+			</dict>
+		</dict>
+		<dict>
+			<key>Bounds</key>
+			<string>{{58.1159, 138}, {94.5364, 31}}</string>
+			<key>Class</key>
+			<string>ShapedGraphic</string>
+			<key>FontInfo</key>
+			<dict>
+				<key>Font</key>
+				<string>Helvetica</string>
+				<key>Size</key>
+				<real>12</real>
+			</dict>
+			<key>ID</key>
+			<integer>43</integer>
+			<key>Shape</key>
+			<string>Rectangle</string>
+			<key>Style</key>
+			<dict>
+				<key>shadow</key>
+				<dict>
+					<key>Draws</key>
+					<string>NO</string>
+				</dict>
+			</dict>
+			<key>Text</key>
+			<dict>
+				<key>Text</key>
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf330
+{\fonttbl\f0\fswiss\fcharset77 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
+
+\f0\fs22 \cf0 Fragment}</string>
+			</dict>
+		</dict>
+		<dict>
+			<key>Class</key>
+			<string>LineGraphic</string>
+			<key>Head</key>
+			<dict>
+				<key>ID</key>
+				<integer>23</integer>
+			</dict>
+			<key>ID</key>
+			<integer>41</integer>
+			<key>OrthogonalBarAutomatic</key>
+			<true/>
+			<key>OrthogonalBarPosition</key>
+			<real>-1</real>
+			<key>Points</key>
+			<array>
+				<string>{270, 299}</string>
+				<string>{270, 328}</string>
+			</array>
+			<key>Style</key>
+			<dict>
+				<key>stroke</key>
+				<dict>
+					<key>HeadArrow</key>
+					<string>FilledArrow</string>
+					<key>LineType</key>
+					<integer>2</integer>
+					<key>TailArrow</key>
+					<string>0</string>
+				</dict>
+			</dict>
+			<key>Tail</key>
+			<dict>
+				<key>ID</key>
+				<integer>34</integer>
+			</dict>
+		</dict>
+		<dict>
+			<key>Class</key>
+			<string>LineGraphic</string>
+			<key>Head</key>
+			<dict>
+				<key>ID</key>
+				<integer>29</integer>
+			</dict>
+			<key>ID</key>
+			<integer>40</integer>
+			<key>OrthogonalBarAutomatic</key>
+			<true/>
+			<key>OrthogonalBarPosition</key>
+			<real>-1</real>
+			<key>Points</key>
+			<array>
+				<string>{270, 83}</string>
+				<string>{270, 133.5}</string>
+			</array>
+			<key>Style</key>
+			<dict>
+				<key>stroke</key>
+				<dict>
+					<key>HeadArrow</key>
+					<string>FilledArrow</string>
+					<key>LineType</key>
+					<integer>2</integer>
+					<key>TailArrow</key>
+					<string>0</string>
+				</dict>
+			</dict>
+			<key>Tail</key>
+			<dict>
+				<key>ID</key>
+				<integer>26</integer>
+			</dict>
+		</dict>
+		<dict>
+			<key>Class</key>
+			<string>LineGraphic</string>
+			<key>Head</key>
+			<dict>
+				<key>ID</key>
+				<integer>34</integer>
+			</dict>
+			<key>ID</key>
+			<integer>39</integer>
+			<key>OrthogonalBarAutomatic</key>
+			<true/>
+			<key>OrthogonalBarPosition</key>
+			<real>-1</real>
+			<key>Points</key>
+			<array>
+				<string>{270, 165.5}</string>
+				<string>{270, 210}</string>
+			</array>
+			<key>Style</key>
+			<dict>
+				<key>stroke</key>
+				<dict>
+					<key>HeadArrow</key>
+					<string>FilledArrow</string>
+					<key>LineType</key>
+					<integer>2</integer>
+					<key>TailArrow</key>
+					<string>0</string>
+				</dict>
+			</dict>
+			<key>Tail</key>
+			<dict>
+				<key>ID</key>
+				<integer>29</integer>
+			</dict>
+		</dict>
+		<dict>
+			<key>Bounds</key>
+			<string>{{241.464, 220}, {94.5364, 31}}</string>
+			<key>Class</key>
+			<string>ShapedGraphic</string>
+			<key>FontInfo</key>
+			<dict>
+				<key>Font</key>
+				<string>Helvetica</string>
+				<key>Size</key>
+				<real>12</real>
+			</dict>
+			<key>ID</key>
+			<integer>37</integer>
+			<key>Shape</key>
+			<string>Rectangle</string>
+			<key>Style</key>
+			<dict>
+				<key>shadow</key>
+				<dict>
+					<key>Draws</key>
+					<string>NO</string>
+				</dict>
+			</dict>
+			<key>Text</key>
+			<dict>
+				<key>Text</key>
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf330
+{\fonttbl\f0\fswiss\fcharset77 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
+
+\f0\fs22 \cf0 Whitespace Filter}</string>
+			</dict>
+		</dict>
+		<dict>
+			<key>Bounds</key>
+			<string>{{126, 220}, {94.5364, 31}}</string>
+			<key>Class</key>
+			<string>ShapedGraphic</string>
+			<key>FontInfo</key>
+			<dict>
+				<key>Font</key>
+				<string>Helvetica</string>
+				<key>Size</key>
+				<real>12</real>
+			</dict>
+			<key>ID</key>
+			<integer>36</integer>
+			<key>Shape</key>
+			<string>Rectangle</string>
+			<key>Style</key>
+			<dict>
+				<key>shadow</key>
+				<dict>
+					<key>Draws</key>
+					<string>NO</string>
+				</dict>
+			</dict>
+			<key>Text</key>
+			<dict>
+				<key>Text</key>
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf330
+{\fonttbl\f0\fswiss\fcharset77 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
+
+\f0\fs22 \cf0 XInclude Filter}</string>
+			</dict>
+		</dict>
+		<dict>
+			<key>Bounds</key>
+			<string>{{126, 258}, {94.5364, 31}}</string>
+			<key>Class</key>
+			<string>ShapedGraphic</string>
+			<key>FontInfo</key>
+			<dict>
+				<key>Font</key>
+				<string>Helvetica</string>
+				<key>Size</key>
+				<real>12</real>
+			</dict>
+			<key>ID</key>
+			<integer>35</integer>
+			<key>Shape</key>
+			<string>Rectangle</string>
+			<key>Style</key>
+			<dict>
+				<key>shadow</key>
+				<dict>
+					<key>Draws</key>
+					<string>NO</string>
+				</dict>
+			</dict>
+			<key>Text</key>
+			<dict>
+				<key>Text</key>
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf330
+{\fonttbl\f0\fswiss\fcharset77 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
+
+\f0\fs22 \cf0 HTML Sanitizer}</string>
+			</dict>
+		</dict>
+		<dict>
+			<key>Bounds</key>
+			<string>{{116, 210}, {308, 89}}</string>
+			<key>Class</key>
+			<string>ShapedGraphic</string>
+			<key>FontInfo</key>
+			<dict>
+				<key>Color</key>
+				<dict>
+					<key>b</key>
+					<string>1</string>
+					<key>g</key>
+					<string>1</string>
+					<key>r</key>
+					<string>1</string>
+				</dict>
+				<key>Font</key>
+				<string>Helvetica-Bold</string>
+				<key>Size</key>
+				<real>12</real>
+			</dict>
+			<key>ID</key>
+			<integer>34</integer>
+			<key>Shape</key>
+			<string>Rectangle</string>
+			<key>Style</key>
+			<dict>
+				<key>fill</key>
+				<dict>
+					<key>Color</key>
+					<dict>
+						<key>b</key>
+						<string>0.835052</string>
+						<key>g</key>
+						<string>0.669573</string>
+						<key>r</key>
+						<string>0.544872</string>
+					</dict>
+				</dict>
+				<key>shadow</key>
+				<dict>
+					<key>Draws</key>
+					<string>NO</string>
+				</dict>
+				<key>stroke</key>
+				<dict>
+					<key>CornerRadius</key>
+					<real>5</real>
+					<key>Draws</key>
+					<string>NO</string>
+				</dict>
+			</dict>
+			<key>Text</key>
+			<dict>
+				<key>Align</key>
+				<integer>2</integer>
+				<key>Text</key>
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf330
+{\fonttbl\f0\fswiss\fcharset77 Helvetica-BoldOblique;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qr\pardirnatural
+
+\f0\i\b\fs24 \cf1 filtering}</string>
+			</dict>
+		</dict>
+		<dict>
+			<key>Bounds</key>
+			<string>{{222.732, 134}, {94.5364, 31}}</string>
+			<key>Class</key>
+			<string>ShapedGraphic</string>
+			<key>FontInfo</key>
+			<dict>
+				<key>Font</key>
+				<string>Helvetica</string>
+				<key>Size</key>
+				<real>11</real>
+			</dict>
+			<key>ID</key>
+			<integer>29</integer>
+			<key>Shape</key>
+			<string>Rectangle</string>
+			<key>Style</key>
+			<dict>
+				<key>shadow</key>
+				<dict>
+					<key>Draws</key>
+					<string>NO</string>
+				</dict>
+			</dict>
+			<key>Text</key>
+			<dict>
+				<key>Text</key>
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf330
+{\fonttbl\f0\fswiss\fcharset77 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
+
+\f0\fs22 \cf0 Stream}</string>
+			</dict>
+		</dict>
+		<dict>
+			<key>Bounds</key>
+			<string>{{241.464, 42}, {94.5364, 31}}</string>
+			<key>Class</key>
+			<string>ShapedGraphic</string>
+			<key>FontInfo</key>
+			<dict>
+				<key>Font</key>
+				<string>Helvetica</string>
+				<key>Size</key>
+				<real>12</real>
+			</dict>
+			<key>ID</key>
+			<integer>28</integer>
+			<key>Shape</key>
+			<string>Rectangle</string>
+			<key>Style</key>
+			<dict>
+				<key>shadow</key>
+				<dict>
+					<key>Draws</key>
+					<string>NO</string>
+				</dict>
+			</dict>
+			<key>Text</key>
+			<dict>
+				<key>Text</key>
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf330
+{\fonttbl\f0\fswiss\fcharset77 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
+
+\f0\fs22 \cf0 XML Parser}</string>
+			</dict>
+		</dict>
+		<dict>
+			<key>Bounds</key>
+			<string>{{126, 42}, {94.5364, 31}}</string>
+			<key>Class</key>
+			<string>ShapedGraphic</string>
+			<key>FontInfo</key>
+			<dict>
+				<key>Font</key>
+				<string>Helvetica</string>
+				<key>Size</key>
+				<real>12</real>
+			</dict>
+			<key>ID</key>
+			<integer>27</integer>
+			<key>Shape</key>
+			<string>Rectangle</string>
+			<key>Style</key>
+			<dict>
+				<key>shadow</key>
+				<dict>
+					<key>Draws</key>
+					<string>NO</string>
+				</dict>
+			</dict>
+			<key>Text</key>
+			<dict>
+				<key>Text</key>
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf330
+{\fonttbl\f0\fswiss\fcharset77 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
+
+\f0\fs22 \cf0 HTML Parser}</string>
+			</dict>
+		</dict>
+		<dict>
+			<key>Bounds</key>
+			<string>{{116, 32}, {308, 51}}</string>
+			<key>Class</key>
+			<string>ShapedGraphic</string>
+			<key>FontInfo</key>
+			<dict>
+				<key>Color</key>
+				<dict>
+					<key>b</key>
+					<string>1</string>
+					<key>g</key>
+					<string>1</string>
+					<key>r</key>
+					<string>1</string>
+				</dict>
+				<key>Font</key>
+				<string>Helvetica-Bold</string>
+				<key>Size</key>
+				<real>12</real>
+			</dict>
+			<key>ID</key>
+			<integer>26</integer>
+			<key>Shape</key>
+			<string>Rectangle</string>
+			<key>Style</key>
+			<dict>
+				<key>fill</key>
+				<dict>
+					<key>Color</key>
+					<dict>
+						<key>b</key>
+						<string>0.835052</string>
+						<key>g</key>
+						<string>0.669573</string>
+						<key>r</key>
+						<string>0.544872</string>
+					</dict>
+				</dict>
+				<key>shadow</key>
+				<dict>
+					<key>Draws</key>
+					<string>NO</string>
+				</dict>
+				<key>stroke</key>
+				<dict>
+					<key>CornerRadius</key>
+					<real>5</real>
+					<key>Draws</key>
+					<string>NO</string>
+				</dict>
+			</dict>
+			<key>Text</key>
+			<dict>
+				<key>Align</key>
+				<integer>2</integer>
+				<key>Text</key>
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf330
+{\fonttbl\f0\fswiss\fcharset77 Helvetica-BoldOblique;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qr\pardirnatural
+
+\f0\i\b\fs24 \cf1 parsing}</string>
+			</dict>
+		</dict>
+		<dict>
+			<key>Bounds</key>
+			<string>{{241.464, 338}, {94.5364, 31}}</string>
+			<key>Class</key>
+			<string>ShapedGraphic</string>
+			<key>FontInfo</key>
+			<dict>
+				<key>Font</key>
+				<string>Helvetica</string>
+				<key>Size</key>
+				<real>12</real>
+			</dict>
+			<key>ID</key>
+			<integer>25</integer>
+			<key>Shape</key>
+			<string>Rectangle</string>
+			<key>Style</key>
+			<dict>
+				<key>shadow</key>
+				<dict>
+					<key>Draws</key>
+					<string>NO</string>
+				</dict>
+			</dict>
+			<key>Text</key>
+			<dict>
+				<key>Text</key>
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf330
+{\fonttbl\f0\fswiss\fcharset77 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
+
+\f0\fs22 \cf0 XML Serializer}</string>
+			</dict>
+		</dict>
+		<dict>
+			<key>Bounds</key>
+			<string>{{126, 338}, {94.5364, 31}}</string>
+			<key>Class</key>
+			<string>ShapedGraphic</string>
+			<key>FontInfo</key>
+			<dict>
+				<key>Font</key>
+				<string>Helvetica</string>
+				<key>Size</key>
+				<real>12</real>
+			</dict>
+			<key>ID</key>
+			<integer>24</integer>
+			<key>Shape</key>
+			<string>Rectangle</string>
+			<key>Style</key>
+			<dict>
+				<key>shadow</key>
+				<dict>
+					<key>Draws</key>
+					<string>NO</string>
+				</dict>
+			</dict>
+			<key>Text</key>
+			<dict>
+				<key>Text</key>
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf330
+{\fonttbl\f0\fswiss\fcharset77 Helvetica;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
+
+\f0\fs22 \cf0 HTML Serializer}</string>
+			</dict>
+		</dict>
+		<dict>
+			<key>Bounds</key>
+			<string>{{116, 328}, {308, 51}}</string>
+			<key>Class</key>
+			<string>ShapedGraphic</string>
+			<key>FontInfo</key>
+			<dict>
+				<key>Color</key>
+				<dict>
+					<key>b</key>
+					<string>1</string>
+					<key>g</key>
+					<string>1</string>
+					<key>r</key>
+					<string>1</string>
+				</dict>
+				<key>Font</key>
+				<string>Helvetica-Bold</string>
+				<key>Size</key>
+				<real>12</real>
+			</dict>
+			<key>ID</key>
+			<integer>23</integer>
+			<key>Shape</key>
+			<string>Rectangle</string>
+			<key>Style</key>
+			<dict>
+				<key>fill</key>
+				<dict>
+					<key>Color</key>
+					<dict>
+						<key>b</key>
+						<string>0.835052</string>
+						<key>g</key>
+						<string>0.669573</string>
+						<key>r</key>
+						<string>0.544872</string>
+					</dict>
+				</dict>
+				<key>shadow</key>
+				<dict>
+					<key>Draws</key>
+					<string>NO</string>
+				</dict>
+				<key>stroke</key>
+				<dict>
+					<key>CornerRadius</key>
+					<real>5</real>
+					<key>Draws</key>
+					<string>NO</string>
+				</dict>
+			</dict>
+			<key>Text</key>
+			<dict>
+				<key>Align</key>
+				<integer>2</integer>
+				<key>Text</key>
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf330
+{\fonttbl\f0\fswiss\fcharset77 Helvetica-BoldOblique;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qr\pardirnatural
+
+\f0\i\b\fs24 \cf1 serialization}</string>
+			</dict>
+		</dict>
+		<dict>
+			<key>Bounds</key>
+			<string>{{42.7682, 119}, {125.232, 61}}</string>
+			<key>Class</key>
+			<string>ShapedGraphic</string>
+			<key>FontInfo</key>
+			<dict>
+				<key>Color</key>
+				<dict>
+					<key>b</key>
+					<string>1</string>
+					<key>g</key>
+					<string>1</string>
+					<key>r</key>
+					<string>1</string>
+				</dict>
+				<key>Font</key>
+				<string>Helvetica-Bold</string>
+				<key>Size</key>
+				<real>12</real>
+			</dict>
+			<key>ID</key>
+			<integer>44</integer>
+			<key>Shape</key>
+			<string>Rectangle</string>
+			<key>Style</key>
+			<dict>
+				<key>fill</key>
+				<dict>
+					<key>Color</key>
+					<dict>
+						<key>b</key>
+						<string>0.835052</string>
+						<key>g</key>
+						<string>0.669573</string>
+						<key>r</key>
+						<string>0.544872</string>
+					</dict>
+				</dict>
+				<key>shadow</key>
+				<dict>
+					<key>Draws</key>
+					<string>NO</string>
+				</dict>
+				<key>stroke</key>
+				<dict>
+					<key>CornerRadius</key>
+					<real>5</real>
+					<key>Draws</key>
+					<string>NO</string>
+				</dict>
+			</dict>
+			<key>Text</key>
+			<dict>
+				<key>Text</key>
+				<string>{\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf330
+{\fonttbl\f0\fswiss\fcharset77 Helvetica-BoldOblique;}
+{\colortbl;\red255\green255\blue255;}
+\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural
+
+\f0\i\b\fs24 \cf1 building}</string>
+				<key>VerticalPad</key>
+				<integer>2</integer>
+			</dict>
+			<key>TextPlacement</key>
+			<integer>0</integer>
+		</dict>
+	</array>
+	<key>GridInfo</key>
+	<dict/>
+	<key>GuidesLocked</key>
+	<string>NO</string>
+	<key>GuidesVisible</key>
+	<string>YES</string>
+	<key>HPages</key>
+	<integer>1</integer>
+	<key>ImageCounter</key>
+	<integer>1</integer>
+	<key>IsPalette</key>
+	<string>NO</string>
+	<key>KeepToScale</key>
+	<false/>
+	<key>Layers</key>
+	<array>
+		<dict>
+			<key>Lock</key>
+			<string>NO</string>
+			<key>Name</key>
+			<string>Ebene 1</string>
+			<key>Print</key>
+			<string>YES</string>
+			<key>View</key>
+			<string>YES</string>
+		</dict>
+	</array>
+	<key>LayoutInfo</key>
+	<dict>
+		<key>LayoutTarget</key>
+		<integer>3</integer>
+	</dict>
+	<key>LinksVisible</key>
+	<string>NO</string>
+	<key>MagnetsVisible</key>
+	<string>NO</string>
+	<key>MasterSheets</key>
+	<array>
+		<dict>
+			<key>ActiveLayerIndex</key>
+			<integer>0</integer>
+			<key>AutoAdjust</key>
+			<false/>
+			<key>CanvasColor</key>
+			<dict>
+				<key>w</key>
+				<string>1</string>
+			</dict>
+			<key>CanvasOrigin</key>
+			<string>{0, 0}</string>
+			<key>CanvasScale</key>
+			<real>1</real>
+			<key>ColumnAlign</key>
+			<integer>1</integer>
+			<key>ColumnSpacing</key>
+			<real>36</real>
+			<key>DisplayScale</key>
+			<string>1 cm = 1 cm</string>
+			<key>GraphicsList</key>
+			<array/>
+			<key>GridInfo</key>
+			<dict/>
+			<key>HPages</key>
+			<integer>1</integer>
+			<key>IsPalette</key>
+			<string>NO</string>
+			<key>KeepToScale</key>
+			<false/>
+			<key>Layers</key>
+			<array>
+				<dict>
+					<key>Lock</key>
+					<string>NO</string>
+					<key>Name</key>
+					<string>Ebene 1</string>
+					<key>Print</key>
+					<string>YES</string>
+					<key>View</key>
+					<string>YES</string>
+				</dict>
+			</array>
+			<key>LayoutInfo</key>
+			<dict>
+				<key>LayoutTarget</key>
+				<integer>3</integer>
+			</dict>
+			<key>Orientation</key>
+			<integer>2</integer>
+			<key>RowAlign</key>
+			<integer>1</integer>
+			<key>RowSpacing</key>
+			<real>36</real>
+			<key>SheetTitle</key>
+			<string>Master 1</string>
+			<key>UniqueID</key>
+			<integer>1</integer>
+			<key>VPages</key>
+			<integer>1</integer>
+		</dict>
+	</array>
+	<key>ModificationDate</key>
+	<string>2006-05-09 01:09:46 +0200</string>
+	<key>Modifier</key>
+	<string>chris</string>
+	<key>NotesVisible</key>
+	<string>NO</string>
+	<key>Orientation</key>
+	<integer>2</integer>
+	<key>OriginVisible</key>
+	<string>NO</string>
+	<key>PageBreaks</key>
+	<string>YES</string>
+	<key>PrintInfo</key>
+	<dict>
+		<key>NSPaperSize</key>
+		<array>
+			<string>size</string>
+			<string>{595.276, 841.89}</string>
+		</array>
+	</dict>
+	<key>ReadOnly</key>
+	<string>NO</string>
+	<key>RowAlign</key>
+	<integer>1</integer>
+	<key>RowSpacing</key>
+	<real>36</real>
+	<key>SheetTitle</key>
+	<string>ArbeitsflƤche 1</string>
+	<key>SmartAlignmentGuidesActive</key>
+	<string>YES</string>
+	<key>SmartDistanceGuidesActive</key>
+	<string>NO</string>
+	<key>UniqueID</key>
+	<integer>1</integer>
+	<key>UseEntirePage</key>
+	<false/>
+	<key>VPages</key>
+	<integer>1</integer>
+	<key>WindowInfo</key>
+	<dict>
+		<key>CurrentSheet</key>
+		<string>0</string>
+		<key>DrawerOpen</key>
+		<false/>
+		<key>DrawerTab</key>
+		<string>Outline</string>
+		<key>DrawerWidth</key>
+		<real>209</real>
+		<key>FitInWindow</key>
+		<false/>
+		<key>Frame</key>
+		<string>{{28, 214}, {757, 512}}</string>
+		<key>ShowRuler</key>
+		<false/>
+		<key>ShowStatusBar</key>
+		<true/>
+		<key>VisibleRegion</key>
+		<string>{{-109, 0}, {742, 422}}</string>
+		<key>Zoom</key>
+		<string>1</string>
+	</dict>
+</dict>
+</plist>
new file mode 100755
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..11d29cedacb43203e5ea6f8dfe1278c7ac208a72
GIT binary patch
literal 22649
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
new file mode 100644
--- /dev/null
+++ b/markup/__init__.py
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Christopher Lenz
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+
+"""This package provides various means for generating and processing web markup
+(XML or HTML).
+
+The design is centered around the concept of streams of markup events (similar
+in concept to SAX parsing events) which can be processed in a uniform manner
+independently of where or how they are produced.
+
+
+Generating content
+------------------
+
+Literal XML and HTML text can be used to easily produce markup streams
+via helper functions in the `markup.input` module:
+
+>>> from markup.input import XML
+>>> doc = XML('<html lang="en"><head><title>My document</title></head></html>')
+
+This results in a `Stream` object that can be used in a number of way.
+
+>>> doc.render(method='html', encoding='utf-8')
+'<html lang="en"><head><title>My document</title></head></html>'
+
+>>> from markup.input import HTML
+>>> doc = HTML('<HTML lang=en><HEAD><TITLE>My document</HTML>')
+>>> doc.render(method='html', encoding='utf-8')
+'<html lang="en"><head><title>My document</title></head></html>'
+
+>>> title = doc.select('head/title')
+>>> title.render(method='html', encoding='utf-8')
+'<title>My document</title>'
+
+
+Markup streams can also be generated programmatically using the
+`markup.builder` module:
+
+>>> from markup.builder import tag
+>>> doc = tag.DOC(tag.TITLE('My document'), lang='en')
+>>> doc.generate().render(method='html')
+'<doc lang="en"><title>My document</title></doc>'
+
+"""
+
+from markup.core import *
+from markup.input import XML, HTML
new file mode 100644
--- /dev/null
+++ b/markup/builder.py
@@ -0,0 +1,178 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Christopher Lenz
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+
+from markup.core import Attributes, QName, Stream
+
+__all__ = ['Fragment', 'Element', 'tag']
+
+
+class Fragment(object):
+    __slots__ = ['children']
+
+    def __init__(self):
+        self.children = []
+
+    def append(self, node):
+        """Append an element or string as child node."""
+        if isinstance(node, (Element, basestring, int, float, long)):
+            # For objects of a known/primitive type, we avoid the check for
+            # whether it is iterable for better performance
+            self.children.append(node)
+        elif isinstance(node, Fragment):
+            self.children += node.children
+        elif node is not None:
+            try:
+                children = iter(node)
+            except TypeError:
+                self.children.append(node)
+            else:
+                for child in node:
+                    self.append(children)
+
+    def __add__(self, other):
+        return Fragment()(self, other)
+
+    def __call__(self, *args):
+        for arg in args:
+            self.append(arg)
+        return self
+
+    def generate(self):
+        """Generator that yield tags and text nodes as strings."""
+        def _generate():
+            for child in self.children:
+                if isinstance(child, Fragment):
+                    for event in child.generate():
+                        yield event
+                else:
+                    yield Stream.TEXT, child, (-1, -1)
+        return Stream(_generate())
+
+    def __iter__(self):
+        return iter(self.generate())
+
+    def __str__(self):
+        return str(self.generate())
+
+    def __unicode__(self):
+        return unicode(self.generate())
+
+
+class Element(Fragment):
+    """Simple XML output generator based on the builder pattern.
+
+    Construct XML elements by passing the tag name to the constructor:
+
+    >>> print Element('strong')
+    <strong/>
+
+    Attributes can be specified using keyword arguments. The values of the
+    arguments will be converted to strings and any special XML characters
+    escaped:
+
+    >>> print Element('textarea', rows=10, cols=60)
+    <textarea rows="10" cols="60"/>
+    >>> print Element('span', title='1 < 2')
+    <span title="1 &lt; 2"/>
+    >>> print Element('span', title='"baz"')
+    <span title="&#34;baz&#34;"/>
+
+    The " character is escaped using a numerical entity.
+    The order in which attributes are rendered is undefined.
+
+    If an attribute value evaluates to `None`, that attribute is not included
+    in the output:
+
+    >>> print Element('a', name=None)
+    <a/>
+
+    Attribute names that conflict with Python keywords can be specified by
+    appending an underscore:
+
+    >>> print Element('div', class_='warning')
+    <div class="warning"/>
+
+    Nested elements can be added to an element using item access notation.
+    The call notation can also be used for this and for adding attributes
+    using keyword arguments, as one would do in the constructor.
+
+    >>> print Element('ul')(Element('li'), Element('li'))
+    <ul><li/><li/></ul>
+    >>> print Element('a')('Label')
+    <a>Label</a>
+    >>> print Element('a')('Label', href="target")
+    <a href="target">Label</a>
+
+    Text nodes can be nested in an element by adding strings instead of
+    elements. Any special characters in the strings are escaped automatically:
+
+    >>> print Element('em')('Hello world')
+    <em>Hello world</em>
+    >>> print Element('em')(42)
+    <em>42</em>
+    >>> print Element('em')('1 < 2')
+    <em>1 &lt; 2</em>
+
+    This technique also allows mixed content:
+
+    >>> print Element('p')('Hello ', Element('b')('world'))
+    <p>Hello <b>world</b></p>
+
+    Elements can also be combined with other elements or strings using the
+    addition operator, which results in a `Fragment` object that contains the
+    operands:
+    
+    >>> print Element('br') + 'some text' + Element('br')
+    <br/>some text<br/>
+    
+    Elements with a namespace can be generated using the `Namespace` and/or
+    `QName` classes:
+    
+    >>> from markup.core import Namespace
+    >>> xhtml = Namespace('http://www.w3.org/1999/xhtml')
+    >>> print Element(xhtml.html, lang='en')
+    <html lang="en" xmlns="http://www.w3.org/1999/xhtml"/>
+    """
+    __slots__ = ['tag', 'attrib']
+
+    def __init__(self, tag_, **attrib):
+        Fragment.__init__(self)
+        self.tag = QName(tag_)
+        self.attrib = Attributes()
+        self(**attrib)
+
+    def __call__(self, *args, **kwargs):
+        for attr, value in kwargs.items():
+            if value is None:
+                continue
+            attr = attr.rstrip('_').replace('_', '-')
+            self.attrib.set(attr, value)
+        return Fragment.__call__(self, *args)
+
+    def generate(self):
+        """Generator that yield tags and text nodes as strings."""
+        def _generate():
+            yield Stream.START, (self.tag, self.attrib), (-1, -1)
+            for kind, data, pos in Fragment.generate(self):
+                yield kind, data, pos
+            yield Stream.END, self.tag, (-1, -1)
+        return Stream(_generate())
+
+
+class ElementFactory(object):
+
+    def __getattribute__(self, name):
+        return Element(name.lower())
+
+
+tag = ElementFactory()
new file mode 100644
--- /dev/null
+++ b/markup/core.py
@@ -0,0 +1,309 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Christopher Lenz
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+
+"""Core classes for markup processing."""
+
+import htmlentitydefs
+import re
+from StringIO import StringIO
+
+__all__ = ['Stream', 'Markup', 'escape', 'unescape', 'Namespace', 'QName']
+
+
+class StreamEventKind(object):
+    """A kind of event on an XML stream."""
+
+    __slots__ = ['name']
+
+    def __init__(self, name):
+        self.name = name
+
+    def __repr__(self):
+        return self.name
+
+
+class Stream(object):
+    """Represents a stream of markup events.
+    
+    This class is basically an iterator over the events.
+    
+    Also provided are ways to serialize the stream to text. The `serialize()`
+    method will return an iterator over generated strings, while `render()`
+    returns the complete generated text at once. Both accept various parameters
+    that impact the way the stream is serialized.
+    
+    Stream events are tuples of the form:
+
+      (kind, data, position)
+
+    where `kind` is the event kind (such as `START`, `END`, `TEXT`, etc), `data`
+    depends on the kind of event, and `position` is a `(line, offset)` tuple
+    that contains the location of the original element or text in the input.
+    """
+    __slots__ = ['events']
+
+    START = StreamEventKind('start') # a start tag
+    END = StreamEventKind('end') # an end tag
+    TEXT = StreamEventKind('text') # literal text
+    EXPR = StreamEventKind('expr') # an expression
+    SUB = StreamEventKind('sub') # a "subprogram"
+    PROLOG = StreamEventKind('prolog') # XML prolog
+    DOCTYPE = StreamEventKind('doctype') # doctype declaration
+    START_NS = StreamEventKind('start-ns') # start namespace mapping
+    END_NS = StreamEventKind('end-ns') # end namespace mapping
+    PI = StreamEventKind('pi') # processing instruction
+    COMMENT = StreamEventKind('comment') # comment
+
+    def __init__(self, events):
+        """Initialize the stream with a sequence of markup events.
+        
+        @oaram events: a sequence or iterable providing the events
+        """
+        self.events = events
+
+    def __iter__(self):
+        return iter(self.events)
+
+    def render(self, method='xml', encoding='utf-8', **kwargs):
+        """Return a string representation of the stream.
+        
+        @param method: determines how the stream is serialized; can be either
+                       'xml' or 'html', or a custom `Serializer` subclass
+        @param encoding: how the output string should be encoded; if set to
+                         `None`, this method returns a `unicode` object
+
+        Any additional keyword arguments are passed to the serializer, and thus
+        depend on the `method` parameter value.
+        """
+        retval = u''.join(self.serialize(method=method, **kwargs))
+        if encoding is not None:
+            return retval.encode('utf-8')
+        return retval
+
+    def select(self, path):
+        """Return a new stream that contains the events matching the given
+        XPath expression.
+        
+        @param path: a string containing the XPath expression
+        """
+        from markup.path import Path
+        path = Path(path)
+        return path.select(self)
+
+    def serialize(self, method='xml', **kwargs):
+        """Generate strings corresponding to a specific serialization of the
+        stream.
+        
+        Unlike the `render()` method, this method is a generator this returns
+        the serialized output incrementally, as opposed to returning a single
+        string.
+        
+        @param method: determines how the stream is serialized; can be either
+                       'xml' or 'html', or a custom `Serializer` subclass
+        """
+        from markup import output
+        cls = method
+        if isinstance(method, basestring):
+            cls = {'xml': output.XMLSerializer,
+                   'html': output.HTMLSerializer}[method]
+        else:
+            assert issubclass(cls, serializers.Serializer)
+        serializer = cls(**kwargs)
+        return serializer.serialize(self)
+
+    def __str__(self):
+        return self.render()
+
+    def __unicode__(self):
+        return self.render(encoding=None)
+
+
+class Attributes(list):
+
+    def __init__(self, attrib=None):
+        list.__init__(self, map(lambda (k, v): (QName(k), v), attrib or []))
+
+    def __contains__(self, name):
+        return name in [attr for attr, value in self]
+
+    def get(self, name, default=None):
+        for attr, value in self:
+            if attr == name:
+                return value
+        return default
+
+    def set(self, name, value):
+        for idx, (attr, _) in enumerate(self):
+            if attr == name:
+                self[idx] = (attr, value)
+                break
+        else:
+            self.append((QName(name), value))
+
+
+class Markup(unicode):
+    """Marks a string as being safe for inclusion in HTML/XML output without
+    needing to be escaped.
+    """
+    def __new__(self, text='', *args):
+        if args:
+            text %= tuple([escape(arg) for arg in args])
+        return unicode.__new__(self, text)
+
+    def __add__(self, other):
+        return Markup(unicode(self) + Markup.escape(other))
+
+    def __mod__(self, args):
+        if not isinstance(args, (list, tuple)):
+            args = [args]
+        return Markup(unicode.__mod__(self,
+                                      tuple([escape(arg) for arg in args])))
+
+    def __mul__(self, num):
+        return Markup(unicode(self) * num)
+
+    def join(self, seq):
+        return Markup(unicode(self).join([Markup.escape(item) for item in seq]))
+
+    def stripentities(self, keepxmlentities=False):
+        """Return a copy of the text with any character or numeric entities
+        replaced by the equivalent UTF-8 characters.
+        
+        If the `keepxmlentities` parameter is provided and evaluates to `True`,
+        the core XML entities (&amp;, &apos;, &gt;, &lt; and &quot;).
+        """
+        def _replace_entity(match):
+            if match.group(1): # numeric entity
+                ref = match.group(1)
+                if ref.startswith('x'):
+                    ref = int(ref[1:], 16)
+                else:
+                    ref = int(ref, 10)
+                return unichr(ref)
+            else: # character entity
+                ref = match.group(2)
+                if keepxmlentities and ref in ('amp', 'apos', 'gt', 'lt', 'quot'):
+                    return '&%s;' % ref
+                try:
+                    codepoint = htmlentitydefs.name2codepoint[ref]
+                    return unichr(codepoint)
+                except KeyError:
+                    if keepxmlentities:
+                        return '&amp;%s;' % ref
+                    else:
+                        return ref
+        return Markup(re.sub(r'&(?:#((?:\d+)|(?:[xX][0-9a-fA-F]+));?|(\w+);)',
+                             _replace_entity, self))
+
+    def striptags(self):
+        """Return a copy of the text with all XML/HTML tags removed."""
+        return Markup(re.sub(r'<[^>]*?>', '', self))
+
+    def escape(cls, text, quotes=True):
+        """Create a Markup instance from a string and escape special characters
+        it may contain (<, >, & and \").
+        
+        If the `quotes` parameter is set to `False`, the \" character is left
+        as is. Escaping quotes is generally only required for strings that are
+        to be used in attribute values.
+        """
+        if isinstance(text, cls):
+            return text
+        text = unicode(text)
+        if not text:
+            return cls()
+        text = text.replace('&', '&amp;') \
+                   .replace('<', '&lt;') \
+                   .replace('>', '&gt;')
+        if quotes:
+            text = text.replace('"', '&#34;')
+        return cls(text)
+    escape = classmethod(escape)
+
+    def unescape(self):
+        """Reverse-escapes &, <, > and \" and returns a `unicode` object."""
+        if not self:
+            return ''
+        return unicode(self).replace('&#34;', '"') \
+                            .replace('&gt;', '>') \
+                            .replace('&lt;', '<') \
+                            .replace('&amp;', '&')
+
+    def plaintext(self, keeplinebreaks=True):
+        """Returns the text as a `unicode`with all entities and tags removed."""
+        text = unicode(self.striptags().stripentities())
+        if not keeplinebreaks:
+            text = text.replace('\n', ' ')
+        return text
+
+    def sanitize(self):
+        from markup.filters import HTMLSanitizer
+        from markup.input import HTMLParser
+        sanitize = HTMLSanitizer()
+        text = self.stripentities(keepxmlentities=True)
+        return Stream(sanitize(HTMLParser(StringIO(text)), None))
+
+
+escape = Markup.escape
+
+def unescape(text):
+    """Reverse-escapes &, <, > and \" and returns a `unicode` object."""
+    if not isinstance(text, Markup):
+        return text
+    return text.unescape()
+
+
+class Namespace(object):
+
+    def __init__(self, uri):
+        self.uri = uri
+
+    def __getitem__(self, name):
+        return QName(self.uri + '}' + name)
+
+    __getattr__ = __getitem__
+
+    def __repr__(self):
+        return '<Namespace "%s">' % self.uri
+
+    def __str__(self):
+        return self.uri
+
+    def __unicode__(self):
+        return unicode(self.uri)
+
+
+class QName(unicode):
+    """A qualified element or attribute name.
+    
+    The unicode value of instances of this class contains the qualified name of
+    the element or attribute, in the form `{namespace}localname`. The namespace
+    URI can be obtained through the additional `namespace` attribute, while the
+    local name can be accessed through the `localname` attribute.
+    """
+    __slots__ = ['namespace', 'localname']
+
+    def __new__(cls, qname):
+        if isinstance(qname, QName):
+            return qname
+
+        parts = qname.split('}', 1)
+        if qname.find('}') > 0:
+            self = unicode.__new__(cls, '{' + qname)
+            self.namespace = parts[0]
+            self.localname = parts[1]
+        else:
+            self = unicode.__new__(cls, qname)
+            self.namespace = None
+            self.localname = qname
+        return self
new file mode 100644
--- /dev/null
+++ b/markup/eval.py
@@ -0,0 +1,232 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Christopher Lenz
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+
+import __builtin__
+import compiler
+import operator
+
+from markup.core import Stream
+
+__all__ = ['Expression']
+
+
+class Expression(object):
+    """Evaluates Python expressions used in templates.
+
+    >>> data = dict(test='Foo', items=[1, 2, 3], dict={'some': 'thing'})
+    >>> Expression('test').evaluate(data)
+    'Foo'
+    >>> Expression('items[0]').evaluate(data)
+    1
+    >>> Expression('items[-1]').evaluate(data)
+    3
+    >>> Expression('dict["some"]').evaluate(data)
+    'thing'
+    
+    Similar to e.g. Javascript, expressions in templates can use the dot
+    notation for attribute access to access items in mappings:
+    
+    >>> Expression('dict.some').evaluate(data)
+    'thing'
+    
+    This also works the other way around: item access can be used to access
+    any object attribute (meaning there's no use for `getattr()` in templates):
+    
+    >>> class MyClass(object):
+    ...     myattr = 'Bar'
+    >>> data = dict(mine=MyClass(), key='myattr')
+    >>> Expression('mine.myattr').evaluate(data)
+    'Bar'
+    >>> Expression('mine["myattr"]').evaluate(data)
+    'Bar'
+    >>> Expression('mine[key]').evaluate(data)
+    'Bar'
+    
+    Most of the standard Python operators are also available to template
+    expressions. Bitwise operators (including inversion and shifting) are not
+    supported.
+    
+    >>> Expression('1 + 1').evaluate(data)
+    2
+    >>> Expression('3 - 1').evaluate(data)
+    2
+    >>> Expression('1 * 2').evaluate(data)
+    2
+    >>> Expression('4 / 2').evaluate(data)
+    2
+    >>> Expression('4 // 3').evaluate(data)
+    1
+    >>> Expression('4 % 3').evaluate(data)
+    1
+    >>> Expression('2 ** 3').evaluate(data)
+    8
+    >>> Expression('not True').evaluate(data)
+    False
+    >>> Expression('True and False').evaluate(data)
+    False
+    >>> Expression('True or False').evaluate(data)
+    True
+    >>> Expression('1 == 3').evaluate(data)
+    False
+    >>> Expression('1 != 3 == 3').evaluate(data)
+    True
+    
+    Built-in functions such as `len()` are also available in template
+    expressions:
+    
+    >>> data = dict(items=[1, 2, 3])
+    >>> Expression('len(items)').evaluate(data)
+    3
+    """
+    __slots__ = ['source', 'ast']
+    __visitors = {}
+
+    def __init__(self, source):
+        self.source = source
+        self.ast = None
+
+    def evaluate(self, data, default=None):
+        if not self.ast:
+            self.ast = compiler.parse(self.source, 'eval')
+        retval = self._visit(self.ast.node, data)
+        if retval is not None:
+            return retval
+        return default
+
+    def __repr__(self):
+        return '<Expression "%s">' % self.source
+
+    # AST traversal
+
+    def _visit(self, node, data):
+        v = self.__visitors.get(node.__class__)
+        if not v:
+            v = getattr(self, '_visit_%s' % node.__class__.__name__.lower())
+            self.__visitors[node.__class__] = v
+        return v(node, data)
+
+    def _visit_expression(self, node, data):
+        for child in node.getChildNodes():
+            return self._visit(child, data)
+
+    # Functions & Accessors
+
+    def _visit_callfunc(self, node, data):
+        func = self._visit(node.node, data)
+        if func is None:
+            return None
+        args = [self._visit(arg, data) for arg in node.args
+                if not isinstance(arg, compiler.ast.Keyword)]
+        kwargs = dict([(arg.name, self._visit(arg.expr, data)) for arg
+                       in node.args if isinstance(arg, compiler.ast.Keyword)])
+        return func(*args, **kwargs)
+
+    def _visit_getattr(self, node, data):
+        obj = self._visit(node.expr, data)
+        try:
+            return getattr(obj, node.attrname)
+        except AttributeError, e:
+            try:
+                return obj[node.attrname]
+            except (KeyError, TypeError):
+                return None
+
+    def _visit_slice(self, node, data):
+        obj = self._visit(node.expr, data)
+        lower = node.lower and self._visit(node.lower, data) or None
+        upper = node.upper and self._visit(node.upper, data) or None
+        return obj[lower:upper]
+
+    def _visit_subscript(self, node, data):
+        obj = self._visit(node.expr, data)
+        subs = map(lambda sub: self._visit(sub, data), node.subs)
+        if len(subs) == 1:
+            subs = subs[0]
+        try:
+            return obj[subs]
+        except (KeyError, IndexError, TypeError):
+            try:
+                return getattr(obj, subs)
+            except (AttributeError, TypeError):
+                return None
+
+    # Operators
+
+    def _visit_and(self, node, data):
+        return reduce(operator.and_, [self._visit(n, data) for n in node.nodes])
+
+    def _visit_or(self, node, data):
+        return reduce(operator.or_, [self._visit(n, data) for n in node.nodes])
+
+    _OP_MAP = {'==': operator.eq, '!=': operator.ne,
+               '<':  operator.lt, '<=': operator.le,
+               '>':  operator.gt, '>=': operator.ge,
+               'in': operator.contains}
+    def _visit_compare(self, node, data):
+        result = self._visit(node.expr, data)
+        ops = node.ops[:]
+        ops.reverse()
+        for op, rval in ops:
+            result = self._OP_MAP[op](result, self._visit(rval, data))
+        return result
+
+    def _visit_add(self, node, data):
+        return self._visit(node.left, data) + self._visit(node.right, data)
+
+    def _visit_div(self, node, data):
+        return self._visit(node.left, data) / self._visit(node.right, data)
+
+    def _visit_floordiv(self, node, data):
+        return self._visit(node.left, data) // self._visit(node.right, data)
+
+    def _visit_mod(self, node, data):
+        return self._visit(node.left, data) % self._visit(node.right, data)
+
+    def _visit_mul(self, node, data):
+        return self._visit(node.left, data) * self._visit(node.right, data)
+
+    def _visit_power(self, node, data):
+        return self._visit(node.left, data) ** self._visit(node.right, data)
+
+    def _visit_sub(self, node, data):
+        return self._visit(node.left, data) - self._visit(node.right, data)
+
+    def _visit_not(self, node, data):
+        return not self._visit(node.expr, data)
+
+    def _visit_unaryadd(self, node, data):
+        return +self._visit(node.expr, data)
+
+    def _visit_unarysub(self, node, data):
+        return -self._visit(node.expr, data)
+
+    # Identifiers & Literals
+
+    def _visit_name(self, node, data):
+        val = data.get(node.name)
+        if val is None:
+            val = getattr(__builtin__, node.name, None)
+        return val
+
+    def _visit_const(self, node, data):
+        return node.value
+
+    def _visit_dict(self, node, data):
+        return dict([(self._visit(k, data), self._visit(v, data))
+                     for k, v in node.items])
+
+    def _visit_tuple(self, node, data):
+        return tuple([self._visit(n, data) for n in node.nodes])
+
+    def _visit_list(self, node, data):
+        return [self._visit(n, data) for n in node.nodes]
new file mode 100644
--- /dev/null
+++ b/markup/filters.py
@@ -0,0 +1,319 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Christopher Lenz
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+
+"""Implementation of a number of stream filters."""
+
+try:
+    frozenset
+except NameError:
+    from sets import ImmutableSet as frozenset
+import re
+
+from markup.core import Attributes, Markup, Stream
+from markup.path import Path
+
+__all__ = ['EvalFilter', 'IncludeFilter', 'MatchFilter', 'WhitespaceFilter',
+           'HTMLSanitizer']
+
+
+class EvalFilter(object):
+    """Responsible for evaluating expressions in a template."""
+
+    def __call__(self, stream, ctxt=None):
+        for kind, data, pos in stream:
+
+            if kind is Stream.START:
+                # Attributes may still contain expressions in start tags at
+                # this point, so do some evaluation
+                tag, attrib = data
+                new_attrib = []
+                for name, substream in attrib:
+                    if isinstance(substream, basestring):
+                        value = substream
+                    else:
+                        values = []
+                        for subkind, subdata, subpos in substream:
+                            if subkind is Stream.EXPR:
+                                values.append(subdata.evaluate(ctxt))
+                            else:
+                                values.append(subdata)
+                        value = filter(lambda x: x is not None, values)
+                        if not value:
+                            continue
+                    new_attrib.append((name, ''.join(value)))
+                yield kind, (tag, Attributes(new_attrib)), pos
+
+            elif kind is Stream.EXPR:
+                result = data.evaluate(ctxt)
+                if result is None:
+                    continue
+
+                # First check for a string, otherwise the iterable
+                # test below succeeds, and the string will be
+                # chopped up into characters
+                if isinstance(result, basestring):
+                    yield Stream.TEXT, result, pos
+                else:
+                    # Test if the expression evaluated to an
+                    # iterable, in which case we yield the
+                    # individual items
+                    try:
+                        yield Stream.SUB, ([], iter(result)), pos
+                    except TypeError:
+                        # Neither a string nor an iterable, so just
+                        # pass it through
+                        yield Stream.TEXT, unicode(result), pos
+
+            else:
+                yield kind, data, pos
+
+
+class IncludeFilter(object):
+    """Template filter providing (very) basic XInclude support
+    (see http://www.w3.org/TR/xinclude/) in templates.
+    """
+
+    _NAMESPACE = 'http://www.w3.org/2001/XInclude'
+
+    def __init__(self, loader):
+        """Initialize the filter.
+        
+        @param loader: the `TemplateLoader` to use for resolving references to
+            external template files
+        """
+        self.loader = loader
+
+    def __call__(self, stream, ctxt=None):
+        """Filter the stream, processing any XInclude directives it may
+        contain.
+        
+        @param ctxt: the template context
+        @param stream: the markup event stream to filter
+        """
+        from markup.template import TemplateError, TemplateNotFound
+
+        in_fallback = False
+        include_href, fallback_stream = None, None
+        indent = 0
+
+        for kind, data, pos in stream:
+
+            if kind is Stream.START and data[0].namespace == self._NAMESPACE \
+                    and not in_fallback:
+                tag, attrib = data
+                if tag.localname == 'include':
+                    include_href = attrib.get('href')
+                    indent = pos[1]
+                elif tag.localname == 'fallback':
+                    in_fallback = True
+                    fallback_stream = []
+
+            elif kind is Stream.END and data.namespace == self._NAMESPACE:
+                if data.localname == 'include':
+                    try:
+                        if not include_href:
+                            raise TemplateError('Include misses required '
+                                                'attribute "href"')
+                        template = self.loader.load(include_href)
+                        for ikind, idata, ipos in template.generate(ctxt):
+                            # Fixup indentation of included markup
+                            if ikind is Stream.TEXT:
+                                idata = idata.replace('\n', '\n' + ' ' * indent)
+                            yield ikind, idata, ipos
+
+                        # If the included template defines any filters added at
+                        # runtime (such as py:match templates), those need to be
+                        # applied to the including template, too.
+                        for filter_ in template.filters:
+                            stream = filter_(stream, ctxt)
+
+                    except TemplateNotFound:
+                        if fallback_stream is None:
+                            raise
+                        for event in fallback_stream:
+                            yield event
+
+                    include_href = None
+                    fallback_stream = None
+                    indent = 0
+                    break
+                elif data.localname == 'fallback':
+                    in_fallback = False
+
+            elif in_fallback:
+                fallback_stream.append((kind, data, pos))
+
+            elif kind is Stream.START_NS and data[1] == self._NAMESPACE:
+                continue
+
+            else:
+                yield kind, data, pos
+        else:
+            # The loop exited normally, so there shouldn't be further events to
+            # process
+            return
+
+        for event in self(stream, ctxt):
+            yield event
+
+
+class MatchFilter(object):
+    """A filter that delegates to a given handler function when the input stream
+    matches some path expression.
+    """
+
+    def __init__(self, path, handler):
+        self.path = Path(path)
+        self.handler = handler
+
+    def __call__(self, stream, ctxt=None):
+        test = self.path.test()
+        for kind, data, pos in stream:
+            result = test(kind, data, pos)
+            if result is True:
+                content = [(kind, data, pos)]
+                depth = 1
+                while depth > 0:
+                    ev = stream.next()
+                    if ev[0] is Stream.START:
+                        depth += 1
+                    elif ev[0] is Stream.END:
+                        depth -= 1
+                    content.append(ev)
+                    test(*ev)
+
+                yield (Stream.SUB,
+                       ([lambda stream, ctxt: self.handler(content, ctxt)], []),
+                       pos)
+            else:
+                yield kind, data, pos
+
+
+class WhitespaceFilter(object):
+    """A filter that removes extraneous white space from the stream.
+
+    Todo:
+     * Support for xml:space
+    """
+
+    _TRAILING_SPACE = re.compile('[ \t]+(?=\n)')
+    _LINE_COLLAPSE = re.compile('\n{2,}')
+
+    def __call__(self, stream, ctxt=None):
+        textbuf = []
+        prev_kind = None
+        for kind, data, pos in stream:
+            if kind is Stream.TEXT:
+                textbuf.append(data)
+            elif prev_kind is Stream.TEXT:
+                text = ''.join(textbuf)
+                text = self._TRAILING_SPACE.sub('', text)
+                text = self._LINE_COLLAPSE.sub('\n', text)
+                yield Stream.TEXT, text, pos
+                del textbuf[:]
+            prev_kind = kind
+            if kind is not Stream.TEXT:
+                yield kind, data, pos
+
+        if textbuf:
+            text = self._LINE_COLLAPSE.sub('\n', ''.join(textbuf))
+            yield Stream.TEXT, text, pos
+
+
+class HTMLSanitizer(object):
+    """A filter that removes potentially dangerous HTML tags and attributes
+    from the stream.
+    """
+
+    _SAFE_TAGS = frozenset(['a', 'abbr', 'acronym', 'address', 'area', 'b',
+        'big', 'blockquote', 'br', 'button', 'caption', 'center', 'cite',
+        'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'dir', 'div', 'dl', 'dt',
+        'em', 'fieldset', 'font', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
+        'hr', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'map',
+        'menu', 'ol', 'optgroup', 'option', 'p', 'pre', 'q', 's', 'samp',
+        'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'table',
+        'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'tr', 'tt', 'u',
+        'ul', 'var'])
+
+    _SAFE_ATTRS = frozenset(['abbr', 'accept', 'accept-charset', 'accesskey',
+        'action', 'align', 'alt', 'axis', 'border', 'cellpadding',
+        'cellspacing', 'char', 'charoff', 'charset', 'checked', 'cite', 'class',
+        'clear', 'cols', 'colspan', 'color', 'compact', 'coords', 'datetime',
+        'dir', 'disabled', 'enctype', 'for', 'frame', 'headers', 'height',
+        'href', 'hreflang', 'hspace', 'id', 'ismap', 'label', 'lang',
+        'longdesc', 'maxlength', 'media', 'method', 'multiple', 'name',
+        'nohref', 'noshade', 'nowrap', 'prompt', 'readonly', 'rel', 'rev',
+        'rows', 'rowspan', 'rules', 'scope', 'selected', 'shape', 'size',
+        'span', 'src', 'start', 'style', 'summary', 'tabindex', 'target',
+        'title', 'type', 'usemap', 'valign', 'value', 'vspace', 'width'])
+    _URI_ATTRS = frozenset(['action', 'background', 'dynsrc', 'href', 'lowsrc',
+        'src'])
+    _SAFE_SCHEMES = frozenset(['file', 'ftp', 'http', 'https', 'mailto', None])
+
+    def __call__(self, stream, ctxt=None):
+        waiting_for = None
+
+        for kind, data, pos in stream:
+            if kind is Stream.START:
+                if waiting_for:
+                    continue
+                tag, attrib = data
+                if tag not in self._SAFE_TAGS:
+                    waiting_for = tag
+                    continue
+
+                new_attrib = []
+                for attr, value in attrib:
+                    if attr not in self._SAFE_ATTRS:
+                        continue
+                    elif attr in self._URI_ATTRS:
+                        # Don't allow URI schemes such as "javascript:"
+                        if self._get_scheme(value) not in self._SAFE_SCHEMES:
+                            continue
+                    elif attr == 'style':
+                        # Remove dangerous CSS declarations from inline styles
+                        decls = []
+                        for decl in filter(None, value.split(';')):
+                            is_evil = False
+                            if 'expression' in decl:
+                                is_evil = True
+                            for m in re.finditer(r'url\s*\(([^)]+)', decl):
+                                if self._get_scheme(m.group(1)) not in self._SAFE_SCHEMES:
+                                    is_evil = True
+                                    break
+                            if not is_evil:
+                                decls.append(decl.strip())
+                        if not decls:
+                            continue
+                        value = '; '.join(decls)
+                    new_attrib.append((attr, value))
+
+                yield kind, (tag, new_attrib), pos
+
+            elif kind is Stream.END:
+                tag = data
+                if waiting_for:
+                    if waiting_for == tag:
+                        waiting_for = None
+                else:
+                    yield kind, data, pos
+
+            else:
+                if not waiting_for:
+                    yield kind, data, pos
+
+    def _get_scheme(self, text):
+        if ':' not in text:
+            return None
+        chars = [char for char in text.split(':', 1)[0] if char.isalnum()]
+        return ''.join(chars).lower()
new file mode 100644
--- /dev/null
+++ b/markup/input.py
@@ -0,0 +1,202 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Christopher Lenz
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+
+from xml.parsers import expat
+try:
+    frozenset
+except NameError:
+    from sets import ImmutableSet as frozenset
+import HTMLParser as html
+import htmlentitydefs
+import re
+from StringIO import StringIO
+
+from markup.core import Attributes, Markup, QName, Stream
+
+
+class XMLParser(object):
+    """Generator-based XML parser based on roughly equivalent code in
+    Kid/ElementTree."""
+
+    def __init__(self, source):
+        self.source = source
+
+        # Setup the Expat parser
+        parser = expat.ParserCreate('utf-8', '}')
+        parser.buffer_text = True
+        parser.returns_unicode = True
+        parser.StartElementHandler = self._handle_start
+        parser.EndElementHandler = self._handle_end
+        parser.CharacterDataHandler = self._handle_data
+        parser.XmlDeclHandler = self._handle_prolog
+        parser.StartDoctypeDeclHandler = self._handle_doctype
+        parser.StartNamespaceDeclHandler = self._handle_start_ns
+        parser.EndNamespaceDeclHandler = self._handle_end_ns
+        parser.ProcessingInstructionHandler = self._handle_pi
+        parser.CommentHandler = self._handle_comment
+        parser.DefaultHandler = self._handle_other
+
+        # Location reporting is only support in Python >= 2.4
+        if not hasattr(parser, 'CurrentLineNumber'):
+            self.getpos = self._getpos_unknown
+
+        self.expat = parser
+        self.queue = []
+
+    def __iter__(self):
+        bufsize = 4 * 1024 # 4K
+        done = False
+        while True:
+            while not done and len(self.queue) == 0:
+                data = self.source.read(bufsize)
+                if data == '': # end of data
+                    if hasattr(self, 'expat'):
+                        self.expat.Parse('', True)
+                        del self.expat # get rid of circular references
+                    done = True
+                else:
+                    self.expat.Parse(data, False)
+            for event in self.queue:
+                yield event
+            self.queue = []
+            if done:
+                break
+
+    def _getpos_unknown(self):
+        return (-1, -1)
+
+    def getpos(self):
+        return self.expat.CurrentLineNumber, self.expat.CurrentColumnNumber
+
+    def _handle_start(self, tag, attrib):
+        self.queue.append((Stream.START, (QName(tag), Attributes(attrib.items())),
+                           self.getpos()))
+
+    def _handle_end(self, tag):
+        self.queue.append((Stream.END, QName(tag), self.getpos()))
+
+    def _handle_data(self, text):
+        self.queue.append((Stream.TEXT, text, self.getpos()))
+
+    def _handle_prolog(self, version, encoding, standalone):
+        self.queue.append((Stream.PROLOG, (version, encoding, standalone),
+                           self.getpos()))
+
+    def _handle_doctype(self, name, sysid, pubid, has_internal_subset):
+        self.queue.append((Stream.DOCTYPE, (name, pubid, sysid), self.getpos()))
+
+    def _handle_start_ns(self, prefix, uri):
+        self.queue.append((Stream.START_NS, (prefix or '', uri), self.getpos()))
+
+    def _handle_end_ns(self, prefix):
+        self.queue.append((Stream.END_NS, prefix or '', self.getpos()))
+
+    def _handle_pi(self, target, data):
+        self.queue.append((Stream.PI, (target, data), self.getpos()))
+
+    def _handle_comment(self, text):
+        self.queue.append((Stream.COMMENT, text, self.getpos()))
+
+    def _handle_other(self, text):
+        if text.startswith('&'):
+            # deal with undefined entities
+            try:
+                text = unichr(htmlentitydefs.name2codepoint[text[1:-1]])
+                self.queue.append((Stream.TEXT, text, self.getpos()))
+            except KeyError:
+                lineno, offset = self.getpos()
+                raise expat.error("undefined entity %s: line %d, column %d" %
+                                  (text, lineno, offset))
+
+
+def XML(text):
+    return Stream(list(XMLParser(StringIO(text))))
+
+
+class HTMLParser(html.HTMLParser):
+    """Parser for HTML input based on the Python `HTMLParser` module.
+    
+    This class provides the same interface for generating stream events as
+    `XMLParser`, and attempts to automatically balance tags.
+    """
+
+    _EMPTY_ELEMS = frozenset(['area', 'base', 'basefont', 'br', 'col', 'frame',
+                              'hr', 'img', 'input', 'isindex', 'link', 'meta',
+                              'param'])
+
+    def __init__(self, source):
+        html.HTMLParser.__init__(self)
+        self.source = source
+        self.queue = []
+        self._open_tags = []
+
+    def __iter__(self):
+        bufsize = 4 * 1024 # 4K
+        done = False
+        while True:
+            while not done and len(self.queue) == 0:
+                data = self.source.read(bufsize)
+                if data == '': # end of data
+                    self.close()
+                    done = True
+                else:
+                    self.feed(data)
+            for kind, data, pos in self.queue:
+                yield kind, data, pos
+            self.queue = []
+            if done:
+                open_tags = self._open_tags
+                open_tags.reverse()
+                for tag in open_tags:
+                    yield Stream.END, QName(tag), pos
+                break
+
+    def handle_starttag(self, tag, attrib):
+        pos = self.getpos()
+        self.queue.append((Stream.START, (QName(tag), Attributes(attrib)), pos))
+        if tag in self._EMPTY_ELEMS:
+            self.queue.append((Stream.END, QName(tag), pos))
+        else:
+            self._open_tags.append(tag)
+
+    def handle_endtag(self, tag):
+        if tag not in self._EMPTY_ELEMS:
+            pos = self.getpos()
+            while self._open_tags:
+                open_tag = self._open_tags.pop()
+                if open_tag.lower() == tag.lower():
+                    break
+                self.queue.append((Stream.END, QName(open_tag), pos))
+            self.queue.append((Stream.END, QName(tag), pos))
+
+    def handle_data(self, text):
+        self.queue.append((Stream.TEXT, text, self.getpos()))
+
+    def handle_charref(self, name):
+        self.queue.append((Stream.TEXT, Markup('&#%s;' % name), self.getpos()))
+
+    def handle_entityref(self, name):
+        self.queue.append((Stream.TEXT, Markup('&%s;' % name), self.getpos()))
+
+    def handle_pi(self, data):
+        target, data = data.split(maxsplit=1)
+        data = data.rstrip('?')
+        self.queue.append((Stream.PI, (target.strip(), data.strip()),
+                           self.getpos()))
+
+    def handle_comment(self, text):
+        self.queue.append((Stream.COMMENT, text, self.getpos()))
+
+
+def HTML(text):
+    return Stream(list(HTMLParser(StringIO(text))))
new file mode 100644
--- /dev/null
+++ b/markup/output.py
@@ -0,0 +1,199 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Christopher Lenz
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+
+"""This module provides different kinds of serialization methods for XML event
+streams.
+"""
+
+try:
+    frozenset
+except NameError:
+    from sets import ImmutableSet as frozenset
+
+from markup.core import Markup, QName, Stream
+from markup.filters import WhitespaceFilter
+
+__all__ = ['Serializer', 'XMLSerializer', 'HTMLSerializer']
+
+
+class Serializer(object):
+    """Base class for serializers."""
+
+    def serialize(self, stream):
+        raise NotImplementedError
+
+
+class XMLSerializer(Serializer):
+    """Produces XML text from an event stream.
+    
+    >>> from markup.builder import tag
+    >>> elem = tag.DIV(tag.A(href='foo'), tag.BR, tag.HR(noshade=True))
+    >>> print ''.join(XMLSerializer().serialize(elem.generate()))
+    <div><a href="foo"/><br/><hr noshade="True"/></div>
+    """
+
+    def serialize(self, stream):
+        ns_attrib = []
+        ns_mapping = {}
+
+        stream = PushbackIterator(stream)
+        for kind, data, pos in stream:
+
+            if kind is Stream.DOCTYPE:
+                # FIXME: what if there's no system or public ID in the input?
+                yield Markup('<!DOCTYPE %s "%s" "%s">\n' % data)
+
+            elif kind is Stream.START_NS:
+                prefix, uri = data
+                if uri not in ns_mapping:
+                    ns_mapping[uri] = prefix
+                    if not prefix:
+                        ns_attrib.append((QName('xmlns'), uri))
+                    else:
+                        ns_attrib.append((QName('xmlns:%s' % prefix), uri))
+
+            elif kind is Stream.START:
+                tag, attrib = data
+
+                tagname = tag.localname
+                if tag.namespace:
+                    try:
+                        prefix = ns_mapping[tag.namespace]
+                        if prefix:
+                            tagname = prefix + ':' + tag.localname
+                    except KeyError:
+                        ns_attrib.append((QName('xmlns'), tag.namespace))
+                buf = ['<', tagname]
+
+                if ns_attrib:
+                    attrib.extend(ns_attrib)
+                    ns_attrib = []
+                for attr, value in attrib:
+                    attrname = attr.localname
+                    if attr.namespace:
+                        try:
+                            prefix = ns_mapping[attr.namespace]
+                        except KeyError:
+                            # FIXME: synthesize a prefix for the attribute?
+                            prefix = ''
+                        if prefix:
+                            attrname = prefix + ':' + attrname
+                    buf.append(' %s="%s"' % (attrname, Markup.escape(value)))
+
+                kind, data, pos = stream.next()
+                if kind is Stream.END:
+                    buf.append('/>')
+                else:
+                    buf.append('>')
+                    stream.pushback((kind, data, pos))
+
+                yield Markup(''.join(buf))
+
+            elif kind is Stream.END:
+                tag = data
+                tagname = tag.localname
+                if tag.namespace:
+                    prefix = ns_mapping[tag.namespace]
+                    if prefix:
+                        tagname = prefix + ':' + tag.localname
+                yield Markup('</%s>' % tagname)
+
+            elif kind is Stream.TEXT:
+                yield Markup.escape(data, quotes=False)
+
+
+class HTMLSerializer(Serializer):
+    """Produces HTML text from an event stream.
+    
+    >>> from markup.builder import tag
+    >>> elem = tag.DIV(tag.A(href='foo'), tag.BR, tag.HR(noshade=True))
+    >>> print ''.join(HTMLSerializer().serialize(elem.generate()))
+    <div><a href="foo"></a><br><hr noshade></div>
+    """
+
+    NAMESPACE = 'http://www.w3.org/1999/xhtml'
+
+    _EMPTY_ELEMS = frozenset(['area', 'base', 'basefont', 'br', 'col', 'frame',
+                              'hr', 'img', 'input', 'isindex', 'link', 'meta',
+                              'param'])
+    _BOOLEAN_ATTRS = frozenset(['selected', 'checked', 'compact', 'declare',
+                                'defer', 'disabled', 'ismap', 'multiple',
+                                'nohref', 'noresize', 'noshade', 'nowrap'])
+
+    def serialize(self, stream):
+        ns_mapping = {}
+
+        stream = PushbackIterator(stream)
+        for kind, data, pos in stream:
+
+            if kind is Stream.DOCTYPE:
+                yield Markup('<!DOCTYPE %s "%s" "%s">\n' % data)
+
+            elif kind is Stream.START_NS:
+                prefix, uri = data
+                if uri not in ns_mapping:
+                    ns_mapping[uri] = prefix
+
+            elif kind is Stream.START:
+                tag, attrib = data
+                if tag.namespace and tag.namespace != self.NAMESPACE:
+                    continue # not in the HTML namespace, so don't emit
+                buf = ['<', tag.localname]
+                for attr, value in attrib:
+                    if attr.namespace and attr.namespace != self.NAMESPACE:
+                        continue # not in the HTML namespace, so don't emit
+                    if attr.localname in self._BOOLEAN_ATTRS:
+                        if value:
+                            buf.append(' %s' % attr.localname)
+                    else:
+                        buf.append(' %s="%s"' % (attr.localname,
+                                                 Markup.escape(value)))
+
+                if tag.localname in self._EMPTY_ELEMS:
+                    kind, data, pos = stream.next()
+                    if kind is not Stream.END:
+                        stream.pushback((kind, data, pos))
+
+                yield Markup(''.join(buf + ['>']))
+
+            elif kind is Stream.END:
+                tag = data
+                if tag.namespace and tag.namespace != self.NAMESPACE:
+                    continue # not in the HTML namespace, so don't emit
+                yield Markup('</%s>' % tag.localname)
+
+            elif kind is Stream.TEXT:
+                yield Markup.escape(data, quotes=False)
+
+
+class PushbackIterator(object):
+    """A simple wrapper for iterators that allows pushing items back on the
+    queue via the `pushback()` method.
+    
+    That can effectively be used to peek at the next item."""
+    __slots__ = ['iterable', 'buf']
+
+    def __init__(self, iterable):
+        self.iterable = iter(iterable)
+        self.buf = []
+
+    def __iter__(self):
+        return self
+
+    def next(self):
+        if self.buf:
+            return self.buf.pop(0)
+        return self.iterable.next()
+
+    def pushback(self, item):
+        self.buf.append(item)
new file mode 100644
--- /dev/null
+++ b/markup/path.py
@@ -0,0 +1,308 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+
+"""Basic support for evaluating XPath expressions against streams."""
+
+import re
+
+from markup.core import QName, Stream
+
+__all__ = ['Path']
+
+_QUOTES = (("'", "'"), ('"', '"'))
+
+class Path(object):
+    """Basic XPath support on markup event streams.
+    
+    >>> from markup.input import XML
+    
+    Selecting specific tags:
+    
+    >>> Path('root').select(XML('<root/>')).render()
+    '<root/>'
+    >>> Path('//root').select(XML('<root/>')).render()
+    '<root/>'
+    
+    Using wildcards for tag names:
+    
+    >>> Path('*').select(XML('<root/>')).render()
+    '<root/>'
+    >>> Path('//*').select(XML('<root/>')).render()
+    '<root/>'
+    
+    Selecting attribute values:
+    
+    >>> Path('@foo').select(XML('<root/>')).render()
+    ''
+    >>> Path('@foo').select(XML('<root foo="bar"/>')).render()
+    'bar'
+    
+    Selecting descendants:
+    
+    >>> Path("root/*").select(XML('<root><foo/><bar/></root>')).render()
+    '<foo/><bar/>'
+    >>> Path("root/bar").select(XML('<root><foo/><bar/></root>')).render()
+    '<bar/>'
+    >>> Path("root/baz").select(XML('<root><foo/><bar/></root>')).render()
+    ''
+    >>> Path("root/foo/*").select(XML('<root><foo><bar/></foo></root>')).render()
+    '<bar/>'
+    
+    Selecting text nodes:
+    >>> Path("item/text()").select(XML('<root><item>Foo</item></root>')).render()
+    'Foo'
+    >>> Path("item/text()").select(XML('<root><item>Foo</item><item>Bar</item></root>')).render()
+    'FooBar'
+    
+    Skipping ancestors:
+    
+    >>> Path("foo/bar").select(XML('<root><foo><bar/></foo></root>')).render()
+    '<bar/>'
+    >>> Path("foo/*").select(XML('<root><foo><bar/></foo></root>')).render()
+    '<bar/>'
+    >>> Path("root/bar").select(XML('<root><foo><bar/></foo></root>')).render()
+    ''
+    >>> Path("root/bar").select(XML('<root><foo><bar id="1"/></foo><bar id="2"/></root>')).render()
+    '<bar id="2"/>'
+    >>> Path("root/*/bar").select(XML('<root><foo><bar/></foo></root>')).render()
+    '<bar/>'
+    >>> Path("root//bar").select(XML('<root><foo><bar id="1"/></foo><bar id="2"/></root>')).render()
+    '<bar id="1"/><bar id="2"/>'
+    >>> Path("root//bar").select(XML('<root><foo><bar id="1"/></foo><bar id="2"/></root>')).render()
+    '<bar id="1"/><bar id="2"/>'
+    
+    Using simple attribute predicates:
+    >>> Path("root/item[@important]").select(XML('<root><item/><item important="very"/></root>')).render()
+    '<item important="very"/>'
+    >>> Path('root/item[@important="very"]').select(XML('<root><item/><item important="very"/></root>')).render()
+    '<item important="very"/>'
+    >>> Path("root/item[@important='very']").select(XML('<root><item/><item important="notso"/></root>')).render()
+    ''
+    >>> Path("root/item[@important!='very']").select(
+    ...     XML('<root><item/><item important="notso"/></root>')).render()
+    '<item/><item important="notso"/>'
+    """
+
+    _TOKEN_RE = re.compile('(::|\.\.|\(\)|[/.:\[\]\(\)@=!])|'
+                           '([^/:\[\]\(\)@=!\s]+)|'
+                           '\s+')
+
+    def __init__(self, text):
+        self.source = text
+
+        steps = []
+        cur_op = ''
+        cur_tag = ''
+        in_predicate = False
+        for op, tag in self._TOKEN_RE.findall(text):
+            if op:
+                if op == '[':
+                    in_predicate = True
+                elif op == ']':
+                    in_predicate = False
+                elif op.startswith('('):
+                    if cur_tag == 'text':
+                        steps[-1] = (False, self.fn_text(), [])
+                    else:
+                        raise NotImplementedError('XPath function "%s" not '
+                                                  'supported' % cur_tag)
+                else:
+                    cur_op += op
+                cur_tag = ''
+            else:
+                closure = cur_op in ('', '//')
+                if cur_op == '@':
+                    if tag == '*':
+                        node_test = self.any_attribute()
+                    else:
+                        node_test = self.attribute_by_name(tag)
+                else:
+                    if tag == '*':
+                        node_test = self.any_element()
+                    elif in_predicate:
+                        if len(tag) > 1 and (tag[0], tag[-1]) in _QUOTES:
+                            node_test = self.literal_string(tag[1:-1])
+                        if cur_op == '=':
+                            node_test = self.op_eq(steps[-1][2][-1], node_test)
+                            steps[-1][2].pop()
+                        elif cur_op == '!=':
+                            node_test = self.op_neq(steps[-1][2][-1], node_test)
+                            steps[-1][2].pop()
+                    else:
+                        node_test = self.element_by_name(tag)
+                if in_predicate:
+                    steps[-1][2].append(node_test)
+                else:
+                    steps.append([closure, node_test, []])
+                cur_op = ''
+                cur_tag = tag
+        self.steps = steps
+
+    def __repr__(self):
+        return '<%s "%s">' % (self.__class__.__name__, self.source)
+
+    def select(self, stream):
+        stream = iter(stream)
+        def _generate(tests):
+            test = self.test()
+            for kind, data, pos in stream:
+                result = test(kind, data, pos)
+                if result is True:
+                    yield kind, data, pos
+                    depth = 1
+                    while depth > 0:
+                        ev = stream.next()
+                        if ev[0] is Stream.START:
+                            depth += 1
+                        elif ev[0] is Stream.END:
+                            depth -= 1
+                        yield ev
+                        test(*ev)
+                elif result:
+                    yield result
+        return Stream(_generate(self.steps))
+
+    def test(self):
+        stack = [0] # stack of cursors into the location path
+
+        def _test(kind, data, pos):
+            #print '\nTracker %r test [%s] %r' % (self, kind, data)
+
+            if not stack:
+                return False
+
+            if kind is Stream.END:
+                stack.pop()
+                return None
+
+            if kind is Stream.START:
+                stack.append(stack[-1])
+
+            matched = False
+            closure, node_test, predicates = self.steps[stack[-1]]
+
+            #print '  Testing against %r' % node_test
+            matched = node_test(kind, data, pos)
+            if matched and predicates:
+                for predicate in predicates:
+                    if not predicate(kind, data, pos):
+                        matched = None
+                        break
+
+            if matched:
+                if stack[-1] == len(self.steps) - 1:
+                    #print '  Last step %r... returned %r' % (node_test, matched)
+                    return matched
+
+                #print '  Matched intermediate step %r... proceed to next step %r' % (node_test, self.steps[stack[-1] + 1])
+                stack[-1] += 1
+
+            elif kind is Stream.START and not closure:
+                # FIXME: If this step is not a closure, it cannot be matched
+                #        until the current element is closed... so we need to
+                #        move the cursor back to the last closure and retest
+                #        that against the current element
+                closures = [step for step in self.steps[:stack[-1]] if step[0]]
+                closures.reverse()
+                for closure, node_test, predicates in closures:
+                    stack[-1] -= 1
+                    if closure:
+                        matched = node_test(kind, data, pos)
+                        if matched:
+                            stack[-1] += 1
+                        break
+
+            return None
+
+        return _test
+
+    class any_element(object):
+        def __call__(self, kind, data, pos):
+            if kind is Stream.START:
+                return True
+            return None
+        def __repr__(self):
+            return '<%s>' % self.__class__.__name__
+
+    class element_by_name(object):
+        def __init__(self, name):
+            self.name = QName(name)
+        def __call__(self, kind, data, pos):
+            if kind is Stream.START:
+                return data[0].localname == self.name
+            return None
+        def __repr__(self):
+            return '<%s "%s">' % (self.__class__.__name__, self.name)
+
+    class any_attribute(object):
+        def __call__(self, kind, data, pos):
+            if kind is Stream.START:
+                text = ''.join([val for name, val in data[1]])
+                if text:
+                    return Stream.TEXT, text, pos
+                return None
+            return None
+        def __repr__(self):
+            return '<%s>' % (self.__class__.__name__)
+
+    class attribute_by_name(object):
+        def __init__(self, name):
+            self.name = QName(name)
+        def __call__(self, kind, data, pos):
+            if kind is Stream.START:
+                if self.name in data[1]:
+                    return Stream.TEXT, data[1].get(self.name), pos
+                return None
+            return None
+        def __repr__(self):
+            return '<%s "%s">' % (self.__class__.__name__, self.name)
+
+    class fn_text(object):
+        def __call__(self, kind, data, pos):
+            if kind is Stream.TEXT:
+                return kind, data, pos
+            return None
+        def __repr__(self):
+            return '<%s>' % (self.__class__.__name__)
+
+    class literal_string(object):
+        def __init__(self, value):
+            self.value = value
+        def __call__(self, kind, data, pos):
+            return Stream.TEXT, self.value, (-1, -1)
+        def __repr__(self):
+            return '<%s>' % (self.__class__.__name__)
+
+    class op_eq(object):
+        def __init__(self, lval, rval):
+            self.lval = lval
+            self.rval = rval
+        def __call__(self, kind, data, pos):
+            lval = self.lval(kind, data, pos)
+            rval = self.rval(kind, data, pos)
+            return (lval and lval[1]) == (rval and rval[1])
+        def __repr__(self):
+            return '<%s %r = %r>' % (self.__class__.__name__, self.lval,
+                                     self.rval)
+
+    class op_neq(object):
+        def __init__(self, lval, rval):
+            self.lval = lval
+            self.rval = rval
+        def __call__(self, kind, data, pos):
+            lval = self.lval(kind, data, pos)
+            rval = self.rval(kind, data, pos)
+            return (lval and lval[1]) != (rval and rval[1])
+        def __repr__(self):
+            return '<%s %r != %r>' % (self.__class__.__name__, self.lval,
+                                      self.rval)
new file mode 100644
--- /dev/null
+++ b/markup/template.py
@@ -0,0 +1,780 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Christopher Lenz
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+
+"""Template engine that is compatible with Kid (http://kid.lesscode.org) to a
+certain extent.
+
+Differences include:
+ * No generation of Python code for a template; the template is "interpreted"
+ * No support for <?python ?> processing instructions
+ * Expressions are evaluated in a more flexible manner, meaning you can use e.g.
+   attribute access notation to access items in a dictionary, etc
+ * Use of XInclude and match templates instead of Kid's py:extends/py:layout
+   directives
+ * Real (thread-safe) search path support
+ * No dependency on ElementTree (due to the lack of pos info)
+ * The original pos of parse events is kept throughout the processing
+   pipeline, so that errors can be tracked back to a specific line/column in
+   the template file
+ * py:match directives use (basic) XPath expressions to match against input
+   nodes, making match templates more powerful while keeping the syntax simple
+
+Todo items:
+ * XPath support needs a real implementation
+ * Improved error reporting
+ * Support for using directives as elements and not just as attributes, reducing
+   the need for wrapper elements with py:strip=""
+ * Support for py:choose/py:when/py:otherwise (similar to XSLT)
+ * Support for list comprehensions and generator expressions in expressions
+
+Random thoughts:
+ * Is there any need to support py:extends and/or py:layout?
+ * Could we generate byte code from expressions?
+"""
+
+import compiler
+from itertools import chain
+import os
+import re
+from StringIO import StringIO
+
+from markup.core import Attributes, Stream
+from markup.eval import Expression
+from markup.filters import EvalFilter, IncludeFilter, MatchFilter, \
+                           WhitespaceFilter
+from markup.input import HTML, XMLParser, XML
+
+__all__ = ['Context', 'BadDirectiveError', 'TemplateError',
+           'TemplateSyntaxError', 'TemplateNotFound', 'Template',
+           'TemplateLoader']
+
+
+class TemplateError(Exception):
+    """Base exception class for errors related to template processing."""
+
+
+class TemplateSyntaxError(TemplateError):
+    """Exception raised when an expression in a template causes a Python syntax
+    error."""
+
+    def __init__(self, message, filename='<string>', lineno=-1, offset=-1):
+        if isinstance(message, SyntaxError) and message.lineno is not None:
+            message = str(message).replace(' (line %d)' % message.lineno, '')
+        TemplateError.__init__(self, message)
+        self.filename = filename
+        self.lineno = lineno
+        self.offset = offset
+
+
+class BadDirectiveError(TemplateSyntaxError):
+    """Exception raised when an unknown directive is encountered when parsing
+    a template.
+    
+    An unknown directive is any attribute using the namespace for directives,
+    with a local name that doesn't match any registered directive.
+    """
+
+    def __init__(self, name, filename='<string>', lineno=-1):
+        TemplateSyntaxError.__init__(self, 'Bad directive "%s"' % name.localname,
+                                     filename, lineno)
+
+
+class TemplateNotFound(TemplateError):
+    """Exception raised when a specific template file could not be found."""
+
+    def __init__(self, name, search_path):
+        TemplateError.__init__(self, 'Template "%s" not found' % name)
+        self.search_path = search_path
+
+
+class Context(object):
+    """A container for template input data.
+    
+    A context provides a stack of scopes. Template directives such as loops can
+    push a new scope on the stack with data that should only be available
+    inside the loop. When the loop terminates, that scope can get popped off
+    the stack again.
+    
+    >>> ctxt = Context(one='foo', other=1)
+    >>> ctxt.get('one')
+    'foo'
+    >>> ctxt.get('other')
+    1
+    >>> ctxt.push(one='frost')
+    >>> ctxt.get('one')
+    'frost'
+    >>> ctxt.get('other')
+    1
+    >>> ctxt.pop()
+    >>> ctxt.get('one')
+    'foo'
+    """
+
+    def __init__(self, **data):
+        self.stack = [data]
+
+    def __getitem__(self, key):
+        """Get a variable's value, starting at the current context and going
+        upward.
+        """
+        return self.get(key)
+
+    def __repr__(self):
+        return repr(self.stack)
+
+    def __setitem__(self, key, value):
+        """Set a variable in the current context."""
+        self.stack[0][key] = value
+
+    def get(self, key):
+        for frame in self.stack:
+            if key in frame:
+                return frame[key]
+
+    def push(self, **data):
+        self.stack.insert(0, data)
+
+    def pop(self):
+        assert self.stack, 'Pop from empty context stack'
+        self.stack.pop(0)
+
+
+class Directive(object):
+    """Abstract base class for template directives.
+    
+    A directive is basically a callable that takes two parameters: `ctxt` is
+    the template data context, and `stream` is an iterable over the events that
+    the directive applies to.
+    
+    Directives can be "anonymous" or "registered". Registered directives can be
+    applied by the template author using an XML attribute with the
+    corresponding name in the template. Such directives should be subclasses of
+    this base class that can  be instantiated with two parameters: `template`
+    is the `Template` instance, and `value` is the value of the directive
+    attribute.
+    
+    Anonymous directives are simply functions conforming to the protocol
+    described above, and can only be applied programmatically (for example by
+    template filters).
+    """
+    __slots__ = ['expr']
+
+    def __init__(self, template, value, pos):
+        self.expr = value and Expression(value) or None
+
+    def __call__(self, stream, ctxt):
+        raise NotImplementedError
+
+    def __repr__(self):
+        expr = ''
+        if self.expr is not None:
+            expr = ' "%s"' % self.expr.source
+        return '<%s%s>' % (self.__class__.__name__, expr)
+
+
+class AttrsDirective(Directive):
+    """Implementation of the `py:attrs` template directive.
+    
+    The value of the `py:attrs` attribute should be a dictionary. The keys and
+    values of that dictionary will be added as attributes to the element:
+    
+    >>> ctxt = Context(foo={'class': 'collapse'})
+    >>> tmpl = Template('''<ul xmlns:py="http://purl.org/kid/ns#">
+    ...   <li py:attrs="foo">Bar</li>
+    ... </ul>''')
+    >>> print tmpl.generate(ctxt)
+    <ul>
+      <li class="collapse">Bar</li>
+    </ul>
+    
+    If the value evaluates to `None` (or any other non-truth value), no
+    attributes are added:
+    
+    >>> ctxt = Context(foo=None)
+    >>> print tmpl.generate(ctxt)
+    <ul>
+      <li>Bar</li>
+    </ul>
+    """
+    def __call__(self, stream, ctxt):
+        kind, (tag, attrib), pos  = stream.next()
+        attrs = self.expr.evaluate(ctxt)
+        if attrs:
+            attrib = attrib[:]
+            for name, value in attrs.items():
+                if value is not None:
+                    value = unicode(value).strip()
+                    attrib.append((name, value))
+        yield kind, (tag, Attributes(attrib)), pos
+        for event in stream:
+            yield event
+
+
+class ContentDirective(Directive):
+    """Implementation of the `py:content` template directive.
+    
+    This directive replaces the content of the element with the result of
+    evaluating the value of the `py:content` attribute:
+    
+    >>> ctxt = Context(bar='Bye')
+    >>> tmpl = Template('''<ul xmlns:py="http://purl.org/kid/ns#">
+    ...   <li py:content="bar">Hello</li>
+    ... </ul>''')
+    >>> print tmpl.generate(ctxt)
+    <ul>
+      <li>Bye</li>
+    </ul>
+    """
+    def __call__(self, stream, ctxt):
+        kind, data, pos = stream.next()
+        if kind is Stream.START:
+            yield kind, data, pos # emit start tag
+        yield Stream.EXPR, self.expr, pos
+        previous = None
+        try:
+            while True:
+                previous = stream.next()
+        except StopIteration:
+            if previous is not None:
+                yield previous
+
+
+class DefDirective(Directive):
+    """Implementation of the `py:def` template directive.
+    
+    This directive can be used to create "Named Template Functions", which
+    are template snippets that are not actually output during normal
+    processing, but rather can be expanded from expressions in other places
+    in the template.
+    
+    A named template function can be used just like a normal Python function
+    from template expressions:
+    
+    >>> ctxt = Context(bar='Bye')
+    >>> tmpl = Template('''<div xmlns:py="http://purl.org/kid/ns#">
+    ...   <p py:def="echo(greeting, name='world')" class="message">
+    ...     ${greeting}, ${name}!
+    ...   </p>
+    ...   ${echo('hi', name='you')}
+    ... </div>''')
+    >>> print tmpl.generate(ctxt)
+    <div>
+      <p class="message">
+        hi, you!
+      </p>
+    </div>
+    
+    >>> ctxt = Context(bar='Bye')
+    >>> tmpl = Template('''<div xmlns:py="http://purl.org/kid/ns#">
+    ...   <p py:def="echo(greeting, name='world')" class="message">
+    ...     ${greeting}, ${name}!
+    ...   </p>
+    ...   <div py:replace="echo('hello')"></div>
+    ... </div>''')
+    >>> print tmpl.generate(ctxt)
+    <div>
+      <p class="message">
+        hello, world!
+      </p>
+    </div>
+    """
+    __slots__ = ['name', 'args', 'defaults', 'stream']
+
+    def __init__(self, template, args, pos):
+        Directive.__init__(self, template, None, pos)
+        ast = compiler.parse(args, 'eval').node
+        self.args = []
+        self.defaults = {}
+        if isinstance(ast, compiler.ast.CallFunc):
+            self.name = ast.node.name
+            for arg in ast.args:
+                if isinstance(arg, compiler.ast.Keyword):
+                    self.args.append(arg.name)
+                    self.defaults[arg.name] = arg.expr.value
+                else:
+                    self.args.append(arg.name)
+        else:
+            self.name = ast.name
+        self.stream = []
+
+    def __call__(self, stream, ctxt):
+        self.stream = list(stream)
+        ctxt[self.name] = lambda *args, **kwargs: self._exec(ctxt, *args,
+                                                             **kwargs)
+        return []
+
+    def _exec(self, ctxt, *args, **kwargs):
+        scope = {}
+        args = list(args) # make mutable
+        for name in self.args:
+            if args:
+                scope[name] = args.pop(0)
+            else:
+                scope[name] = kwargs.pop(name, self.defaults.get(name))
+        ctxt.push(**scope)
+        for event in self.stream:
+            yield event
+        ctxt.pop()
+
+
+class ForDirective(Directive):
+    """Implementation of the `py:for` template directive.
+    
+    >>> ctxt = Context(items=[1, 2, 3])
+    >>> tmpl = Template('''<ul xmlns:py="http://purl.org/kid/ns#">
+    ...   <li py:for="item in items">${item}</li>
+    ... </ul>''')
+    >>> print tmpl.generate(ctxt)
+    <ul>
+      <li>1</li><li>2</li><li>3</li>
+    </ul>
+    """
+    __slots__ = ['targets']
+
+    def __init__(self, template, value, pos):
+        targets, expr_source = value.split(' in ', 1)
+        self.targets = [str(name.strip()) for name in targets.split(',')]
+        Directive.__init__(self, template, expr_source, pos)
+
+    def __call__(self, stream, ctxt):
+        iterable = self.expr.evaluate(ctxt, [])
+        if iterable is not None:
+            stream = list(stream)
+            for item in iter(iterable):
+                if len(self.targets) == 1:
+                    item = [item]
+                scope = {}
+                for idx, name in enumerate(self.targets):
+                    scope[name] = item[idx]
+                ctxt.push(**scope)
+                for event in stream:
+                    yield event
+                ctxt.pop()
+
+    def __repr__(self):
+        return '<%s "%s in %s">' % (self.__class__.__name__,
+                                    ', '.join(self.targets), self.expr.source)
+
+
+class IfDirective(Directive):
+    """Implementation of the `py:if` template directive.
+    
+    >>> ctxt = Context(foo=True, bar='Hello')
+    >>> tmpl = Template('''<div xmlns:py="http://purl.org/kid/ns#">
+    ...   <b py:if="foo">${bar}</b>
+    ... </div>''')
+    >>> print tmpl.generate(ctxt)
+    <div>
+      <b>Hello</b>
+    </div>
+    """
+    def __call__(self, stream, ctxt):
+        if self.expr.evaluate(ctxt):
+            return stream
+        return []
+
+
+class MatchDirective(Directive):
+    """Implementation of the `py:match` template directive.
+    
+    >>> ctxt = Context()
+    >>> tmpl = Template('''<div xmlns:py="http://purl.org/kid/ns#">
+    ...   <span py:match="div/greeting">
+    ...     Hello ${select('@name')}
+    ...   </span>
+    ...   <greeting name="Dude" />
+    ... </div>''')
+    >>> print tmpl.generate(ctxt)
+    <div>
+      <span>
+        Hello Dude
+      </span>
+    </div>
+    """
+    __slots__ = ['path', 'stream']
+
+    def __init__(self, template, value, pos):
+        Directive.__init__(self, template, None, pos)
+        template.filters.append(MatchFilter(value, self._handle_match))
+        self.path = value
+        self.stream = []
+
+    def __call__(self, stream, ctxt):
+        self.stream = list(stream)
+        return []
+
+    def __repr__(self):
+        return '<%s "%s">' % (self.__class__.__name__, self.path)
+
+    def _handle_match(self, orig_stream, ctxt):
+        ctxt.push(select=lambda path: Stream(orig_stream).select(path))
+        for event in self.stream:
+            yield event
+        ctxt.pop()
+
+
+class ReplaceDirective(Directive):
+    """Implementation of the `py:replace` template directive.
+    
+    >>> ctxt = Context(bar='Bye')
+    >>> tmpl = Template('''<div xmlns:py="http://purl.org/kid/ns#">
+    ...   <span py:replace="bar">Hello</span>
+    ... </div>''')
+    >>> print tmpl.generate(ctxt)
+    <div>
+      Bye
+    </div>
+    
+    This directive is equivalent to `py:content` combined with `py:strip`,
+    providing a less verbose way to achieve the same effect:
+    
+    >>> ctxt = Context(bar='Bye')
+    >>> tmpl = Template('''<div xmlns:py="http://purl.org/kid/ns#">
+    ...   <span py:content="bar" py:strip="">Hello</span>
+    ... </div>''')
+    >>> print tmpl.generate(ctxt)
+    <div>
+      Bye
+    </div>
+    """
+    def __call__(self, stream, ctxt):
+        kind, data, pos = stream.next()
+        yield Stream.EXPR, self.expr, pos
+
+
+class StripDirective(Directive):
+    """Implementation of the `py:strip` template directive.
+    
+    When the value of the `py:strip` attribute evaluates to `True`, the element
+    is stripped from the output
+    
+    >>> ctxt = Context()
+    >>> tmpl = Template('''<div xmlns:py="http://purl.org/kid/ns#">
+    ...   <div py:strip="True"><b>foo</b></div>
+    ... </div>''')
+    >>> print tmpl.generate(ctxt)
+    <div>
+      <b>foo</b>
+    </div>
+    
+    On the other hand, when the attribute evaluates to `False`, the element is
+    not stripped:
+    
+    >>> ctxt = Context()
+    >>> tmpl = Template('''<div xmlns:py="http://purl.org/kid/ns#">
+    ...   <div py:strip="False"><b>foo</b></div>
+    ... </div>''')
+    >>> print tmpl.generate(ctxt)
+    <div>
+      <div><b>foo</b></div>
+    </div>
+    
+    Leaving the attribute value empty is equivalent to a truth value:
+    
+    >>> ctxt = Context()
+    >>> tmpl = Template('''<div xmlns:py="http://purl.org/kid/ns#">
+    ...   <div py:strip=""><b>foo</b></div>
+    ... </div>''')
+    >>> print tmpl.generate(ctxt)
+    <div>
+      <b>foo</b>
+    </div>
+    
+    This directive is particulary interesting for named template functions or
+    match templates that do not generate a top-level element:
+    
+    >>> ctxt = Context()
+    >>> tmpl = Template('''<div xmlns:py="http://purl.org/kid/ns#">
+    ...   <div py:def="echo(what)" py:strip="">
+    ...     <b>${what}</b>
+    ...   </div>
+    ...   ${echo('foo')}
+    ... </div>''')
+    >>> print tmpl.generate(ctxt)
+    <div>
+        <b>foo</b>
+    </div>
+    """
+    def __call__(self, stream, ctxt):
+        if self.expr:
+            strip = self.expr.evaluate(ctxt)
+        else:
+            strip = True
+        if strip:
+            stream.next() # skip start tag
+            # can ignore StopIteration since it will just break from this
+            # generator
+            previous = stream.next()
+            for event in stream:
+                yield previous
+                previous = event
+        else:
+            for event in stream:
+                yield event
+
+
+class Template(object):
+    """Can parse a template and transform it into the corresponding output
+    based on context data.
+    """
+    NAMESPACE = 'http://purl.org/kid/ns#'
+
+    directives = [('def', DefDirective),
+                  ('match', MatchDirective),
+                  ('for', ForDirective),
+                  ('if', IfDirective),
+                  ('replace', ReplaceDirective),
+                  ('content', ContentDirective),
+                  ('attrs', AttrsDirective),
+                  ('strip', StripDirective)]
+    _dir_by_name = dict(directives)
+    _dir_order = [directive[1] for directive in directives]
+
+    def __init__(self, source, filename=None):
+        """Initialize a template from either a string or a file-like object."""
+        if isinstance(source, basestring):
+            self.source = StringIO(source)
+        else:
+            self.source = source
+        self.filename = filename or '<string>'
+
+        self.pre_filters = [EvalFilter()]
+        self.filters = []
+        self.post_filters = [WhitespaceFilter()]
+        self.parse()
+
+    def __repr__(self):
+        return '<%s "%s">' % (self.__class__.__name__,
+                              os.path.basename(self.filename))
+
+    def parse(self):
+        """Parse the template.
+        
+        The parsing stage parses the XML template and constructs a list of
+        directives that will be executed in the render stage. The input is
+        split up into literal output (markup that does not depend on the
+        context data) and actual directives (commands or variable
+        substitution).
+        """
+        stream = [] # list of events of the "compiled" template
+        dirmap = {} # temporary mapping of directives to elements
+        ns_prefix = {}
+        depth = 0
+
+        for kind, data, pos in XMLParser(self.source):
+
+            if kind is Stream.START_NS:
+                # Strip out the namespace declaration for template directives
+                prefix, uri = data
+                if uri == self.NAMESPACE:
+                    ns_prefix[prefix] = uri
+                else:
+                    stream.append((kind, data, pos))
+
+            elif kind is Stream.END_NS:
+                if data in ns_prefix:
+                    del ns_prefix[data]
+                else:
+                    stream.append((kind, data, pos))
+
+            elif kind is Stream.START:
+                # Record any directive attributes in start tags
+                tag, attrib = data
+                directives = []
+                new_attrib = []
+                for name, value in attrib:
+                    if name.namespace == self.NAMESPACE:
+                        cls = self._dir_by_name.get(name.localname)
+                        if cls is None:
+                            raise BadDirectiveError(name, self.filename, pos[0])
+                        else:
+                            directives.append(cls(self, value, pos))
+                    else:
+                        value = list(self._interpolate(value, *pos))
+                        new_attrib.append((name, value))
+                if directives:
+                    directives.sort(lambda a, b: cmp(self._dir_order.index(a.__class__),
+                                                     self._dir_order.index(b.__class__)))
+                    dirmap[(depth, tag)] = (directives, len(stream))
+
+                stream.append((kind, (tag, Attributes(new_attrib)), pos))
+                depth += 1
+
+            elif kind is Stream.END:
+                depth -= 1
+                stream.append((kind, data, pos))
+
+                # If there have have directive attributes with the corresponding
+                # start tag, move the events inbetween into a "subprogram"
+                if (depth, data) in dirmap:
+                    directives, start_offset = dirmap.pop((depth, data))
+                    substream = stream[start_offset:]
+                    stream[start_offset:] = [(Stream.SUB,
+                                              (directives, substream), pos)]
+
+            elif kind is Stream.TEXT:
+                for kind, data, pos in self._interpolate(data, *pos):
+                    stream.append((kind, data, pos))
+
+            else:
+                stream.append((kind, data, pos))
+
+        self.stream = stream
+
+    def generate(self, ctxt):
+        """Transform the template based on the given context data."""
+
+        def _transform(stream):
+            # Apply pre and runtime filters
+            for filter_ in chain(self.pre_filters, self.filters):
+                stream = filter_(iter(stream), ctxt)
+
+            try:
+                for kind, data, pos in stream:
+
+                    if kind is Stream.SUB:
+                        # This event is a list of directives and a list of
+                        # nested events to which those directives should be
+                        # applied
+                        directives, substream = data
+                        directives.reverse()
+                        for directive in directives:
+                            substream = directive(iter(substream), ctxt)
+                        for event in _transform(iter(substream)):
+                            yield event
+
+                    else:
+                        yield kind, data, pos
+            except SyntaxError, err:
+                raise TemplateSyntaxError(err, self.filename, pos[0],
+                                          pos[1] + (err.offset or 0))
+
+        stream = _transform(self.stream)
+
+        # Apply post-filters
+        for filter_ in self.post_filters:
+            stream = filter_(iter(stream), ctxt)
+
+        return Stream(stream)
+
+    _FULL_EXPR_RE = re.compile(r'(?<!\$)\$\{(.+?)\}')
+    _SHORT_EXPR_RE = re.compile(r'(?<!\$)\$([a-zA-Z][a-zA-Z0-9_\.]*)')
+
+    def _interpolate(cls, text, lineno=-1, offset=-1):
+        """Parse the given string and extract expressions.
+        
+        This method returns a list containing both literal text and `Expression`
+        objects.
+
+        @param text: the text to parse
+        @param lineno: the line number at which the text was found (optional)
+        @param offset: the column number at which the text starts in the source
+            (optional)
+        """
+        patterns = [cls._FULL_EXPR_RE, cls._SHORT_EXPR_RE]
+        def _interpolate(text):
+            for idx, group in enumerate(patterns.pop(0).split(text)):
+                if idx % 2:
+                    yield Stream.EXPR, Expression(group), (lineno, offset)
+                elif group:
+                    if patterns:
+                        for result in _interpolate(group):
+                            yield result
+                    else:
+                        yield Stream.TEXT, group.replace('$$', '$'), \
+                              (lineno, offset)
+        return _interpolate(text)
+    _interpolate = classmethod(_interpolate)
+
+
+class TemplateLoader(object):
+    """Responsible for loading templates from files on the specified search
+    path.
+    
+    >>> import tempfile
+    >>> fd, path = tempfile.mkstemp(suffix='.html', prefix='template')
+    >>> os.write(fd, '<p>$var</p>')
+    11
+    >>> os.close(fd)
+    
+    The template loader accepts a list of directory paths that are then used
+    when searching for template files, in the given order:
+    
+    >>> loader = TemplateLoader([os.path.dirname(path)])
+    
+    The `load()` method first checks the template cache whether the requested
+    template has already been loaded. If not, it attempts to locate the
+    template file, and returns the corresponding `Template` object:
+    
+    >>> template = loader.load(os.path.basename(path))
+    >>> isinstance(template, Template)
+    True
+    
+    Template instances are cached: requesting a template with the same name
+    results in the same instance being returned:
+    
+    >>> loader.load(os.path.basename(path)) is template
+    True
+    """
+    def __init__(self, search_path=None, auto_reload=False):
+        """Create the template laoder.
+        
+        @param search_path: a list of absolute path names that should be
+            searched for template files
+        @param auto_reload: whether to check the last modification time of
+            template files, and reload them if they have changed
+        """
+        self.search_path = search_path
+        if self.search_path is None:
+            self.search_path = []
+        self.auto_reload = auto_reload
+        self._cache = {}
+        self._mtime = {}
+
+    def load(self, filename):
+        """Load the template with the given name.
+        
+        This method searches the search path trying to locate a template
+        matching the given name. If no such template is found, a
+        `TemplateNotFound` exception is raised. Otherwise, a `Template` object
+        representing the requested template is returned.
+        
+        Template searches are cached to avoid having to parse the same template
+        file more than once. Thus, subsequent calls of this method with the
+        same template file name will return the same `Template` object.
+        
+        @param filename: the relative path of the template file to load
+        """
+        filename = os.path.normpath(filename)
+        try:
+            tmpl = self._cache[filename]
+            if not self.auto_reload or \
+                    os.path.getmtime(tmpl.filename) == self._mtime[filename]:
+                return tmpl
+        except KeyError:
+            pass
+        for dirname in self.search_path:
+            filepath = os.path.join(dirname, filename)
+            try:
+                fileobj = file(filepath, 'rt')
+                try:
+                    tmpl = Template(fileobj, filename=filepath)
+                    tmpl.pre_filters.append(IncludeFilter(self))
+                finally:
+                    fileobj.close()
+                self._cache[filename] = tmpl
+                self._mtime[filename] = os.path.getmtime(filepath)
+                return tmpl
+            except IOError:
+                continue
+        raise TemplateNotFound(filename, self.search_path)
new file mode 100644
--- /dev/null
+++ b/markup/tests/__init__.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Christopher Lenz
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+
+import doctest
+import unittest
+
+def suite():
+    import markup
+    from markup.tests import builder, core, eval, input, output, path, template
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(markup))
+    suite.addTest(builder.suite())
+    suite.addTest(core.suite())
+    suite.addTest(eval.suite())
+    suite.addTest(input.suite())
+    suite.addTest(output.suite())
+    suite.addTest(path.suite())
+    suite.addTest(template.suite())
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/markup/tests/builder.py
@@ -0,0 +1,40 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Christopher Lenz
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+
+import doctest
+from HTMLParser import HTMLParseError
+import unittest
+
+from markup.builder import Element, tag
+from markup.core import Stream
+
+
+class ElementFactoryTestCase(unittest.TestCase):
+
+    def test_link(self):
+        link = tag.A(href='#', title='Foo', accesskey=None)('Bar')
+        bits = iter(link.generate())
+        self.assertEqual((Stream.START, ('a', [('href', "#"), ('title', "Foo")]),
+                          (-1, -1)), bits.next())
+        self.assertEqual((Stream.TEXT, u'Bar', (-1, -1)), bits.next())
+        self.assertEqual((Stream.END, 'a', (-1, -1)), bits.next())
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(Element.__module__))
+    suite.addTest(unittest.makeSuite(ElementFactoryTestCase, 'test'))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/markup/tests/core.py
@@ -0,0 +1,189 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Christopher Lenz
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+
+import doctest
+from HTMLParser import HTMLParseError
+import unittest
+
+from markup.core import Markup, escape, unescape
+
+
+class MarkupTestCase(unittest.TestCase):
+
+    def test_escape(self):
+        markup = escape('<b>"&"</b>')
+        assert isinstance(markup, Markup)
+        self.assertEquals('&lt;b&gt;&#34;&amp;&#34;&lt;/b&gt;', markup)
+
+    def test_escape_noquotes(self):
+        markup = escape('<b>"&"</b>', quotes=False)
+        assert isinstance(markup, Markup)
+        self.assertEquals('&lt;b&gt;"&amp;"&lt;/b&gt;', markup)
+
+    def test_unescape_markup(self):
+        string = '<b>"&"</b>'
+        markup = Markup.escape(string)
+        assert isinstance(markup, Markup)
+        self.assertEquals(string, unescape(markup))
+
+    def test_add_str(self):
+        markup = Markup('<b>foo</b>') + '<br/>'
+        assert isinstance(markup, Markup)
+        self.assertEquals('<b>foo</b>&lt;br/&gt;', markup)
+
+    def test_add_markup(self):
+        markup = Markup('<b>foo</b>') + Markup('<br/>')
+        assert isinstance(markup, Markup)
+        self.assertEquals('<b>foo</b><br/>', markup)
+
+    def test_add_reverse(self):
+        markup = 'foo' + Markup('<b>bar</b>')
+        assert isinstance(markup, unicode)
+        self.assertEquals('foo<b>bar</b>', markup)
+
+    def test_mod(self):
+        markup = Markup('<b>%s</b>') % '&'
+        assert isinstance(markup, Markup)
+        self.assertEquals('<b>&amp;</b>', markup)
+
+    def test_mod_multi(self):
+        markup = Markup('<b>%s</b> %s') % ('&', 'boo')
+        assert isinstance(markup, Markup)
+        self.assertEquals('<b>&amp;</b> boo', markup)
+
+    def test_mul(self):
+        markup = Markup('<b>foo</b>') * 2
+        assert isinstance(markup, Markup)
+        self.assertEquals('<b>foo</b><b>foo</b>', markup)
+
+    def test_join(self):
+        markup = Markup('<br />').join(['foo', '<bar />', Markup('<baz />')])
+        assert isinstance(markup, Markup)
+        self.assertEquals('foo<br />&lt;bar /&gt;<br /><baz />', markup)
+
+    def test_stripentities_all(self):
+        markup = Markup('&amp; &#106;').stripentities()
+        assert isinstance(markup, Markup)
+        self.assertEquals('& j', markup)
+
+    def test_stripentities_keepxml(self):
+        markup = Markup('<a href="#">fo<br />o</a>').striptags()
+        assert isinstance(markup, Markup)
+        self.assertEquals('foo', markup)
+
+    def test_striptags_empty(self):
+        markup = Markup('<br />').striptags()
+        assert isinstance(markup, Markup)
+        self.assertEquals('', markup)
+
+    def test_striptags_mid(self):
+        markup = Markup('<a href="#">fo<br />o</a>').striptags()
+        assert isinstance(markup, Markup)
+        self.assertEquals('foo', markup)
+
+    def test_sanitize_unchanged(self):
+        markup = Markup('<a href="#">fo<br />o</a>')
+        self.assertEquals('<a href="#">fo<br/>o</a>', str(markup.sanitize()))
+
+    def test_sanitize_escape_text(self):
+        markup = Markup('<a href="#">fo&amp;</a>')
+        self.assertEquals('<a href="#">fo&amp;</a>', str(markup.sanitize()))
+        markup = Markup('<a href="#">&lt;foo&gt;</a>')
+        self.assertEquals('<a href="#">&lt;foo&gt;</a>', str(markup.sanitize()))
+
+    def test_sanitize_entityref_text(self):
+        markup = Markup('<a href="#">fo&ouml;</a>')
+        self.assertEquals(u'<a href="#">foƶ</a>', unicode(markup.sanitize()))
+
+    def test_sanitize_escape_attr(self):
+        markup = Markup('<div title="&lt;foo&gt;"></div>')
+        self.assertEquals('<div title="&lt;foo&gt;"/>', str(markup.sanitize()))
+
+    def test_sanitize_close_empty_tag(self):
+        markup = Markup('<a href="#">fo<br>o</a>')
+        self.assertEquals('<a href="#">fo<br/>o</a>', str(markup.sanitize()))
+
+    def test_sanitize_invalid_entity(self):
+        markup = Markup('&junk;')
+        self.assertEquals('&amp;junk;', str(markup.sanitize()))
+
+    def test_sanitize_remove_script_elem(self):
+        markup = Markup('<script>alert("Foo")</script>')
+        self.assertEquals('', str(markup.sanitize()))
+        markup = Markup('<SCRIPT SRC="http://example.com/"></SCRIPT>')
+        self.assertEquals('', str(markup.sanitize()))
+        markup = Markup('<SCR\0IPT>alert("foo")</SCR\0IPT>')
+        self.assertRaises(HTMLParseError, markup.sanitize().render)
+        markup = Markup('<SCRIPT&XYZ SRC="http://example.com/"></SCRIPT>')
+        self.assertRaises(HTMLParseError, markup.sanitize().render)
+
+    def test_sanitize_remove_onclick_attr(self):
+        markup = Markup('<div onclick=\'alert("foo")\' />')
+        self.assertEquals('<div/>', str(markup.sanitize()))
+
+    def test_sanitize_remove_style_scripts(self):
+        # Inline style with url() using javascript: scheme
+        markup = Markup('<DIV STYLE=\'background: url(javascript:alert("foo"))\'>')
+        self.assertEquals('<div/>', str(markup.sanitize()))
+        # Inline style with url() using javascript: scheme, using control char
+        markup = Markup('<DIV STYLE=\'background: url(&#1;javascript:alert("foo"))\'>')
+        self.assertEquals('<div/>', str(markup.sanitize()))
+        # Inline style with url() using javascript: scheme, in quotes
+        markup = Markup('<DIV STYLE=\'background: url("javascript:alert(foo)")\'>')
+        self.assertEquals('<div/>', str(markup.sanitize()))
+        # IE expressions in CSS not allowed
+        markup = Markup('<DIV STYLE=\'width: expression(alert("foo"));\'>')
+        self.assertEquals('<div/>', str(markup.sanitize()))
+        markup = Markup('<DIV STYLE=\'background: url(javascript:alert("foo"));'
+                                     'color: #fff\'>')
+        self.assertEquals('<div style="color: #fff"/>', str(markup.sanitize()))
+
+    def test_sanitize_remove_src_javascript(self):
+        markup = Markup('<img src=\'javascript:alert("foo")\'>')
+        self.assertEquals('<img/>', str(markup.sanitize()))
+        # Case-insensitive protocol matching
+        markup = Markup('<IMG SRC=\'JaVaScRiPt:alert("foo")\'>')
+        self.assertEquals('<img/>', str(markup.sanitize()))
+        # Grave accents (not parsed)
+        markup = Markup('<IMG SRC=`javascript:alert("RSnake says, \'foo\'")`>')
+        self.assertRaises(HTMLParseError, markup.sanitize().render)
+        # Protocol encoded using UTF-8 numeric entities
+        markup = Markup('<IMG SRC=\'&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;'
+                        '&#112;&#116;&#58;alert("foo")\'>')
+        self.assertEquals('<img/>', str(markup.sanitize()))
+        # Protocol encoded using UTF-8 numeric entities without a semicolon
+        # (which is allowed because the max number of digits is used)
+        markup = Markup('<IMG SRC=\'&#0000106&#0000097&#0000118&#0000097'
+                        '&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116'
+                        '&#0000058alert("foo")\'>')
+        self.assertEquals('<img/>', str(markup.sanitize()))
+        # Protocol encoded using UTF-8 numeric hex entities without a semicolon
+        # (which is allowed because the max number of digits is used)
+        markup = Markup('<IMG SRC=\'&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69'
+                        '&#x70&#x74&#x3A;alert("foo")\'>')
+        self.assertEquals('<img/>', str(markup.sanitize()))
+        # Embedded tab character in protocol
+        markup = Markup('<IMG SRC=\'jav\tascript:alert("foo");\'>')
+        self.assertEquals('<img/>', str(markup.sanitize()))
+        # Embedded tab character in protocol, but encoded this time
+        markup = Markup('<IMG SRC=\'jav&#x09;ascript:alert("foo");\'>')
+        self.assertEquals('<img/>', str(markup.sanitize()))
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(MarkupTestCase, 'test'))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/markup/tests/eval.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Christopher Lenz
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+
+import doctest
+import unittest
+
+from markup import eval
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(eval))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/markup/tests/input.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+
+import unittest
+
+from markup.core import Stream
+from markup.input import XMLParser
+
+
+class XMLParserTestCase(unittest.TestCase):
+    pass
+
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(unittest.makeSuite(XMLParserTestCase, 'test'))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/markup/tests/output.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+
+import doctest
+import unittest
+import sys
+
+from markup import output
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(output))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/markup/tests/path.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Christopher Lenz
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+
+import doctest
+import unittest
+
+from markup import path
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(path))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/markup/tests/template.py
@@ -0,0 +1,111 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Edgewall Software
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+
+import doctest
+import unittest
+import sys
+
+from markup.core import Stream
+from markup.template import BadDirectiveError, Context, Template, \
+                            TemplateSyntaxError
+
+
+class TemplateTestCase(unittest.TestCase):
+
+    def test_interpolate_string(self):
+        parts = list(Template._interpolate('bla'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(Stream.TEXT, parts[0][0])
+        self.assertEqual('bla', parts[0][1])
+
+    def test_interpolate_simple(self):
+        parts = list(Template._interpolate('${bla}'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(Stream.EXPR, parts[0][0])
+        self.assertEqual('bla', parts[0][1].source)
+
+    def test_interpolate_escaped(self):
+        parts = list(Template._interpolate('$${bla}'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(Stream.TEXT, parts[0][0])
+        self.assertEqual('${bla}', parts[0][1])
+
+    def test_interpolate_short(self):
+        parts = list(Template._interpolate('$bla'))
+        self.assertEqual(1, len(parts))
+        self.assertEqual(Stream.EXPR, parts[0][0])
+        self.assertEqual('bla', parts[0][1].source)
+
+    def test_interpolate_mixed1(self):
+        parts = list(Template._interpolate('$foo bar $baz'))
+        self.assertEqual(3, len(parts))
+        self.assertEqual(Stream.EXPR, parts[0][0])
+        self.assertEqual('foo', parts[0][1].source)
+        self.assertEqual(Stream.TEXT, parts[1][0])
+        self.assertEqual(' bar ', parts[1][1])
+        self.assertEqual(Stream.EXPR, parts[2][0])
+        self.assertEqual('baz', parts[2][1].source)
+
+    def test_interpolate_mixed2(self):
+        parts = list(Template._interpolate('foo $bar baz'))
+        self.assertEqual(3, len(parts))
+        self.assertEqual(Stream.TEXT, parts[0][0])
+        self.assertEqual('foo ', parts[0][1])
+        self.assertEqual(Stream.EXPR, parts[1][0])
+        self.assertEqual('bar', parts[1][1].source)
+        self.assertEqual(Stream.TEXT, parts[2][0])
+        self.assertEqual(' baz', parts[2][1])
+
+    def test_bad_directive_error(self):
+        xml = '<p xmlns:py="http://purl.org/kid/ns#" py:do="nothing" />'
+        try:
+            tmpl = Template(xml, 'test.html')
+        except BadDirectiveError, e:
+            self.assertEqual('test.html', e.filename)
+            if sys.version_info[:2] >= (2, 4):
+                self.assertEqual(1, e.lineno)
+
+    def test_directive_value_syntax_error(self):
+        xml = '<p xmlns:py="http://purl.org/kid/ns#" py:if="bar\'" />'
+        tmpl = Template(xml, 'test.html')
+        try:
+            list(tmpl.generate(Context()))
+            self.fail('Expected SyntaxError')
+        except TemplateSyntaxError, e:
+            self.assertEqual('test.html', e.filename)
+            if sys.version_info[:2] >= (2, 4):
+                self.assertEqual(1, e.lineno)
+                # We don't really care about the offset here, do we?
+
+    def test_expression_syntax_error(self):
+        xml = '<p>\n  Foo <em>${bar"}</em>\n</p>'
+        tmpl = Template(xml, filename='test.html')
+        ctxt = Context(bar='baz')
+        try:
+            list(tmpl.generate(ctxt))
+            self.fail('Expected SyntaxError')
+        except TemplateSyntaxError, e:
+            self.assertEqual('test.html', e.filename)
+            if sys.version_info[:2] >= (2, 4):
+                self.assertEqual(2, e.lineno)
+                self.assertEqual(10, e.offset)
+
+
+def suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocTestSuite(Template.__module__))
+    suite.addTest(unittest.makeSuite(TemplateTestCase, 'test'))
+    return suite
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='suite')
new file mode 100644
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,3 @@
+[egg_info]
+tag_build = dev
+tag_svn_revision = true
new file mode 100755
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2006 Christopher Lenz
+# All rights reserved.
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at http://trac.edgewall.com/license.html.
+#
+# This software consists of voluntary contributions made by many
+# individuals. For the exact contribution history, see the revision
+# history and logs, available at http://projects.edgewall.com/trac/.
+
+from setuptools import setup, find_packages
+
+setup(
+    name='Markup', version='0.1',
+    description='Toolkit for stream-based generation of markup for the web',
+    author='Christopher Lenz', author_email='cmlenz@gmx.net',
+    license='BSD', url='http://markup.cmlenz.net/',
+    packages=find_packages(exclude=['*.tests*']),
+    test_suite = 'markup.tests.suite',
+    zip_safe = True
+)
Copyright (C) 2012-2017 Edgewall Software