Mercurial > genshi > genshi-test
changeset 1:821114ec4f69
Initial import.
author | cmlenz |
---|---|
date | Sat, 03 Jun 2006 07:16:01 +0000 |
parents | 20f3417d4171 |
children | e22ec9e4f213 |
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 < 2"/> + >>> print Element('span', title='"baz"') + <span title=""baz""/> + + 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 < 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 (&, ', >, < and "). + """ + 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 '&%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('&', '&') \ + .replace('<', '<') \ + .replace('>', '>') + if quotes: + text = text.replace('"', '"') + 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('"', '"') \ + .replace('>', '>') \ + .replace('<', '<') \ + .replace('&', '&') + + 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('<b>"&"</b>', markup) + + def test_escape_noquotes(self): + markup = escape('<b>"&"</b>', quotes=False) + assert isinstance(markup, Markup) + self.assertEquals('<b>"&"</b>', 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><br/>', 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>&</b>', markup) + + def test_mod_multi(self): + markup = Markup('<b>%s</b> %s') % ('&', 'boo') + assert isinstance(markup, Markup) + self.assertEquals('<b>&</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 /><bar /><br /><baz />', markup) + + def test_stripentities_all(self): + markup = Markup('& j').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&</a>') + self.assertEquals('<a href="#">fo&</a>', str(markup.sanitize())) + markup = Markup('<a href="#"><foo></a>') + self.assertEquals('<a href="#"><foo></a>', str(markup.sanitize())) + + def test_sanitize_entityref_text(self): + markup = Markup('<a href="#">foö</a>') + self.assertEquals(u'<a href="#">foƶ</a>', unicode(markup.sanitize())) + + def test_sanitize_escape_attr(self): + markup = Markup('<div title="<foo>"></div>') + self.assertEquals('<div title="<foo>"/>', 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('&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(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=\'javascri' + 'pt: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=\'java' + 'script' + ':alert("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=\'javascri' + 'pt: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	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 +)