Compare commits
No commits in common. "master" and "wiki" have entirely different histories.
39
CallingFromPython.md
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
# Getting Started #
|
||||||
|
|
||||||
|
The easiest way to call oohembed from python is by using [python-oembed](http://code.google.com/p/python-oembed/)
|
||||||
|
|
||||||
|
[Download](http://code.google.com/p/python-oembed/downloads/list) and install python-oembed to get started.
|
||||||
|
|
||||||
|
# Using python-oembed with oohembed #
|
||||||
|
|
||||||
|
below sample shows how to use oohembed for various services:
|
||||||
|
|
||||||
|
```
|
||||||
|
import oembed
|
||||||
|
import pprint
|
||||||
|
|
||||||
|
consumer = oembed.OEmbedConsumer()
|
||||||
|
endpoint = oembed.OEmbedEndpoint('http://oohembed.com/oohembed','*')
|
||||||
|
consumer.addEndpoint(endpoint)
|
||||||
|
|
||||||
|
#now this consumer can be used with several oEmbed providers.
|
||||||
|
response = consumer.embed('http://www.flickr.com/photos/wizardbt/2584979382/')
|
||||||
|
pprint.pprint(response.getData())
|
||||||
|
|
||||||
|
response = consumer.embed('http://www.youtube.com/watch?v=vk1HvP7NO5w')
|
||||||
|
pprint.pprint(response.getData())
|
||||||
|
|
||||||
|
response = consumer.embed('http://www.metacafe.com/watch/1350976/funny_call/')
|
||||||
|
pprint.pprint(response.getData())
|
||||||
|
|
||||||
|
response = consumer.embed('http://twitter.com/mai_co_jp/statuses/822499364')
|
||||||
|
pprint.pprint(response.getData())
|
||||||
|
|
||||||
|
response = consumer.embed('http://en.wikipedia.org/wiki/Life_on_Mars_(TV_series)')
|
||||||
|
pprint.pprint(response.getData())
|
||||||
|
|
||||||
|
#Other elements can be accessed like;
|
||||||
|
print response['url']
|
||||||
|
print response['html']
|
||||||
|
|
||||||
|
```
|
51
LICENSE
|
@ -1,51 +0,0 @@
|
||||||
oohEmbed
|
|
||||||
========
|
|
||||||
Copyright (c) 2008, Deepak Sarda
|
|
||||||
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are
|
|
||||||
met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
* Redistributions in binary form must reproduce the above
|
|
||||||
copyright notice, this list of conditions and the following
|
|
||||||
disclaimer in the documentation and/or other materials provided
|
|
||||||
with the distribution.
|
|
||||||
|
|
||||||
* Neither the name of the oohEmbed project nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived
|
|
||||||
from this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
|
||||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
||||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
||||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
||||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
||||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
|
|
||||||
Jinja
|
|
||||||
=====
|
|
||||||
Jinja is a template language that oohEmbed uses for generating the
|
|
||||||
homepage at oohembed.com. Jinja is licensed under a BSD License.
|
|
||||||
See http://jinja.pocoo.org/ for more details
|
|
||||||
|
|
||||||
Python-Markdown
|
|
||||||
===============
|
|
||||||
Python-Markdown is an implementation of Markdown markup syntax
|
|
||||||
in Python. It is under a GPL2 license or BSD license.
|
|
||||||
See http://www.freewisdom.org/projects/python-markdown/
|
|
||||||
|
|
||||||
Beautiful Soup
|
|
||||||
==============
|
|
||||||
Beautiful Soup is a HTML/XML parser designed for quick turnaround
|
|
||||||
projects like screen-scraping. It is licensed under a BSD style license.
|
|
||||||
See http://www.crummy.com/software/BeautifulSoup/ for more.
|
|
49
ProjectHome.md
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
This project hosts the source code that powers oohEmbed.com
|
||||||
|
|
||||||
|
## No Longer Supported ##
|
||||||
|
|
||||||
|
oohEmbed.com was [acquired by Embedly](http://blog.embed.ly/oohembed) and since then, the site runs on Embedly's infrastructure and not using the code derived from this project.
|
||||||
|
|
||||||
|
This project is basically in archive mode now. Feel free to fork it and use it to host your own instances on App Engine. If you prefer not to use App Engine, then a few changes should get the code up & running in any Python WSGI environment.
|
||||||
|
|
||||||
|
## What is oohEmbed? ##
|
||||||
|
|
||||||
|
In a nutshell: _oohEmbed is an oEmbed compatible provider of HTML embed codes for various web sites._
|
||||||
|
|
||||||
|
What is this oEmbed? From [oembed.com](http://www.oembed.com/):
|
||||||
|
|
||||||
|
> oEmbed is a format for allowing an embedded representation of a URL on third party sites. The simple API allows a website to display embedded content (such as photos or videos) when a user posts a link to that resource, without having to parse the resource directly.
|
||||||
|
|
||||||
|
Still don't get it? Perhaps an example will make things clear. If you make a URL request like this:
|
||||||
|
|
||||||
|
> `http://oohembed.com/oohembed/?url=http%3A//www.amazon.com/Myths-Innovation-Scott-Berkun/dp/0596527055/`
|
||||||
|
|
||||||
|
You will get this as the response:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"asin": "0596527055",
|
||||||
|
"title": "The Myths of Innovation",
|
||||||
|
"url": "http://ecx.images-amazon.com/images/I/31%2BfVjL2nqL.jpg",
|
||||||
|
"thumbnail_width": "48",
|
||||||
|
"height": "500",
|
||||||
|
"width": "317",
|
||||||
|
"version": "1.0",
|
||||||
|
"author_name": "Scott Berkun",
|
||||||
|
"provider_name": "Amazon Product Image",
|
||||||
|
"thumbnail_url": "http://ecx.images-amazon.com/images/I/31%2BfVjL2nqL._SL75_.jpg",
|
||||||
|
"type": "photo",
|
||||||
|
"thumbnail_height": "75",
|
||||||
|
"author_url": "http://www.amazon.com/gp/redirect.html%3FASIN=0596527055%26location=/Myths-Innovation-Scott-Berkun/dp/0596527055"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
That should make everything clear. No? Then head over to http://oohEmbed.com/ for more details!
|
||||||
|
|
||||||
|
## Brief Overview ##
|
||||||
|
|
||||||
|
oohEmbed is running on Google App Engine and so what you see in the [source repository](http://code.google.com/p/oohembed/source/browse/) here is a Python app meant to run on App Engine. Thus, some familiarity with App Engine would be a good thing to have before jumping into the code.
|
||||||
|
|
||||||
|
Although oohEmbed is a Google App Engine app, it contains very little App Engine specific code. Apart from the use of `urlfetch`, everything else is standard Python and should run anywhere else that Python runs.
|
||||||
|
|
||||||
|
The codebase is so small that hopefully, it is self-explanatory :-) If you have any questions, please feel free to ask!
|
55
README
|
@ -1,55 +0,0 @@
|
||||||
# Welcome to oohEmbed!
|
|
||||||
|
|
||||||
## oohEmbed What?
|
|
||||||
|
|
||||||
In a nutshell: __oohEmbed is an oEmbed compatible provider of HTML embed codes for various web sites.__
|
|
||||||
|
|
||||||
What is this oEmbed? From [oembed.com](http://www.oembed.com/):
|
|
||||||
|
|
||||||
> oEmbed is a format for allowing an embedded representation of a URL on third party sites.
|
|
||||||
> The simple API allows a website to display embedded content (such as photos or videos) when
|
|
||||||
> a user posts a link to that resource, without having to parse the resource directly.
|
|
||||||
|
|
||||||
Still don't get it? Perhaps an example will make things clear. If you make a URL request like this:
|
|
||||||
|
|
||||||
http://oohembed.com/oohembed/?url=http%3A//www.amazon.com/Myths-Innovation-Scott-Berkun/dp/0596527055/
|
|
||||||
|
|
||||||
You will get this as the response:
|
|
||||||
|
|
||||||
{
|
|
||||||
"asin": "0596527055",
|
|
||||||
"title": "The Myths of Innovation",
|
|
||||||
"url": "http://ecx.images-amazon.com/images/I/31%2BfVjL2nqL.jpg",
|
|
||||||
"thumbnail_width": "48",
|
|
||||||
"height": "500",
|
|
||||||
"width": "317",
|
|
||||||
"version": "1.0",
|
|
||||||
"author_name": "Scott Berkun",
|
|
||||||
"provider_name": "Amazon Product Image",
|
|
||||||
"thumbnail_url": "http://ecx.images-amazon.com/images/I/31%2BfVjL2nqL._SL75_.jpg",
|
|
||||||
"type": "photo",
|
|
||||||
"thumbnail_height": "75",
|
|
||||||
"author_url": "http://www.amazon.com/gp/redirect.html%3FASIN=0596527055%26location=/Myths-Innovation-Scott-Berkun/dp/0596527055"
|
|
||||||
}
|
|
||||||
|
|
||||||
That should make everything clear. No? Then head over to http://oohEmbed.com/ for more details!
|
|
||||||
|
|
||||||
## What's in the source?
|
|
||||||
|
|
||||||
If you are reading this file, you've chanced upon the source code that powers oohEmbed.com
|
|
||||||
|
|
||||||
Yes, the code running behind oohEmbed.com is open source (BSD License) and is available at
|
|
||||||
http://code.google.com/p/oohembed/
|
|
||||||
|
|
||||||
## Brief Overview
|
|
||||||
|
|
||||||
oohEmbed is running on Google App Engine and so what you see in the source files is a Python
|
|
||||||
app meant to run on App Engine. Thus, some familiarity with App Engine would be a good thing
|
|
||||||
to have before jumping into the code.
|
|
||||||
|
|
||||||
Although oohEmbed is a Google App Engine app, there is very little App Engine specific in the
|
|
||||||
code itself. Apart from the use of `urlfetch`, everything else is standard Python and should
|
|
||||||
run anywhere that Python runs.
|
|
||||||
|
|
||||||
The codebase is so small that hopefully, it is self-explanatory! If you have any questions,
|
|
||||||
please feel free to ask!
|
|
1931
app/BeautifulSoup.py
41
app/app.yaml
|
@ -1,41 +0,0 @@
|
||||||
application: oohembed
|
|
||||||
version: vanilla
|
|
||||||
runtime: python
|
|
||||||
api_version: 1
|
|
||||||
|
|
||||||
default_expiration: "15d"
|
|
||||||
|
|
||||||
handlers:
|
|
||||||
- url: /dumble/
|
|
||||||
static_files: dumble/index.html
|
|
||||||
upload: dumble/index.html
|
|
||||||
|
|
||||||
- url: /favicon.ico
|
|
||||||
static_files: static/favicon.ico
|
|
||||||
upload: static/favicon.ico
|
|
||||||
|
|
||||||
- url: /robots.txt
|
|
||||||
static_files: static/robots.txt
|
|
||||||
upload: static/robots.txt
|
|
||||||
|
|
||||||
- url: /crossdomain.xml
|
|
||||||
static_files: static/crossdomain.xml
|
|
||||||
upload: static/crossdomain.xml
|
|
||||||
|
|
||||||
- url: /endpoints.json
|
|
||||||
static_files: static/endpoints.json
|
|
||||||
upload: static/endpoints.json
|
|
||||||
mime_type: application/json
|
|
||||||
|
|
||||||
- url: /dumble
|
|
||||||
static_dir: dumble
|
|
||||||
|
|
||||||
- url: /static
|
|
||||||
static_dir: static
|
|
||||||
|
|
||||||
- url: /admin/.*
|
|
||||||
script: main.py
|
|
||||||
login: admin
|
|
||||||
|
|
||||||
- url: /.*
|
|
||||||
script: main.py
|
|
|
@ -1,376 +0,0 @@
|
||||||
String.prototype.supplant = function (o) {
|
|
||||||
/* http://javascript.crockford.com/remedial.html */
|
|
||||||
return this.replace(/{([^{}]*)}/g,
|
|
||||||
function (a, b) {
|
|
||||||
var r = o[b];
|
|
||||||
return typeof r === 'string' || typeof r === 'number' ? r : a;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
String.prototype.trim = function() {
|
|
||||||
return this.replace(/^\s+|\s+$/g, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
var DUMBLE_DEBUG = false;
|
|
||||||
if (location.hostname.toLowerCase() == 'localhost') {
|
|
||||||
DUMBLE_DEBUG = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
var firelog = function(str) {
|
|
||||||
if(DUMBLE_DEBUG) {
|
|
||||||
if (window.console && window.console.firebug) {
|
|
||||||
console.log(str);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var getUrlParam = function(param) {
|
|
||||||
var regex = '[?&]' + param + '=([^&#]*)';
|
|
||||||
var results = (new RegExp(regex)).exec(window.location.search);
|
|
||||||
if(results) return results[1];
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$.ajaxSetup({
|
|
||||||
cache: true
|
|
||||||
});
|
|
||||||
|
|
||||||
OohembedProvider = function(url, caption, notes) {
|
|
||||||
var url = url
|
|
||||||
var caption = caption
|
|
||||||
var notes = notes.trim()
|
|
||||||
|
|
||||||
var ratings = /(\d{1,2}) ?\/ ?(\d{1,2})/i .exec(notes)
|
|
||||||
var stars = ''
|
|
||||||
|
|
||||||
if (ratings && (parseInt(ratings[1]) <= parseInt(ratings[2]))) {
|
|
||||||
stars = ' <img src="images/' + Math.round(5*ratings[1]/ratings[2]) +
|
|
||||||
'_stars.png" height=16 class="stars" />'
|
|
||||||
notes = notes.replace(ratings[0], '')
|
|
||||||
} else {
|
|
||||||
// the note starts with *, ends with * or is entirely *
|
|
||||||
ratings = /^\*{2,5}\s|\s\*{2,5}$|^\*{2,5}$/i .exec(notes)
|
|
||||||
if (ratings) {
|
|
||||||
stars = ' <img src="images/' + ratings[0].trim().length
|
|
||||||
+ '_stars.png" height=16 class="stars" /> '
|
|
||||||
notes = notes.replace(ratings[0], '').trim()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stars) {
|
|
||||||
firelog('stars are: ' + stars)
|
|
||||||
notes = notes ? notes + '<br/>' + stars : stars
|
|
||||||
}
|
|
||||||
|
|
||||||
this.template = '<div class="link"><h3><a href="{url}" target="_blank">{caption}</a></h3>{notes}</div>'
|
|
||||||
|
|
||||||
var elem = $(this.template.supplant({url: url, caption: caption, notes: notes}));
|
|
||||||
|
|
||||||
this.video_template = '<div class="video">{embed}<span class="caption">{caption}</span>{notes}</div>'
|
|
||||||
this.photo_template = '<div class="photo"><a href="{url}" target="_blank"><img src="{img_url}" alt="{caption}" title="{caption}" width="{width}" height="{height}" /></a><span class="caption">{caption}</span>{notes}</div>{html}'
|
|
||||||
this.quote_template = '<div class="quote">“{text}” ' +
|
|
||||||
'<span class="source"><a href="{url}" target="_blank">{source}</a></span></div><div style="margin: 1em;">{notes}</div>{html}';
|
|
||||||
|
|
||||||
$.getJSON('http://'+location.host+'/oohembed/?url=' + escape(url) + '&format=json&maxwidth=480&callback=?',
|
|
||||||
function(data) {
|
|
||||||
firelog('received data: ' + data.provider_name);
|
|
||||||
if (data.type == 'video') {
|
|
||||||
var snip = video_template.supplant({
|
|
||||||
embed: data.html,
|
|
||||||
caption: caption,
|
|
||||||
notes: notes?notes:''});
|
|
||||||
firelog('replacing elem with: ' + snip);
|
|
||||||
elem.html(snip);
|
|
||||||
} else if (data.type == 'photo') {
|
|
||||||
var credit = ''
|
|
||||||
if (data.title && data.author_name) {
|
|
||||||
credit = '<em>'+data.title+' by '+data.author_name+'</em>'
|
|
||||||
}
|
|
||||||
notes = notes ? (notes + (credit ? '<br/>'+credit : '')) : credit
|
|
||||||
var snip = photo_template.supplant({
|
|
||||||
url: url,
|
|
||||||
img_url: data.url,
|
|
||||||
width: data.width,
|
|
||||||
height: data.height,
|
|
||||||
caption: caption,
|
|
||||||
notes: notes,
|
|
||||||
html: data.html?'<div><blockquote>'+data.html+'</blockquote></div>':''});
|
|
||||||
firelog('replacing elem with: ' + snip);
|
|
||||||
elem.html(snip);
|
|
||||||
} else if (data.type == 'link') {
|
|
||||||
if (data.title) {
|
|
||||||
var source = data.author_name ? data.author_name : 'source'
|
|
||||||
var snip = quote_template.supplant({
|
|
||||||
url: url,
|
|
||||||
text: data.title,
|
|
||||||
html: data.html?'<div><blockquote>'+data.html+'</blockquote></div>':'',
|
|
||||||
source: source,
|
|
||||||
notes: notes?notes:''});
|
|
||||||
firelog('replacing elem with: ' + snip);
|
|
||||||
elem.html(snip);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return elem;
|
|
||||||
}
|
|
||||||
|
|
||||||
GenericImageProvider = function(url, caption, notes) {
|
|
||||||
this.re = /.*(jpeg|jpg|png|bmp|gif)$/i
|
|
||||||
this.template = '<div class="photo"><img src="{url}" alt="{caption}" title="{caption}" /><span class="caption">{caption}</span>{notes}</div>'
|
|
||||||
|
|
||||||
var matches = this.re.exec(url);
|
|
||||||
if (!matches) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var elem = this.template.supplant({url: url, caption: caption, notes: notes});
|
|
||||||
return $(elem);
|
|
||||||
}
|
|
||||||
|
|
||||||
var Providers = new Array(
|
|
||||||
GenericImageProvider,
|
|
||||||
OohembedProvider
|
|
||||||
);
|
|
||||||
|
|
||||||
var Analytics = Analytics ? Analytics : {
|
|
||||||
pageTracker: null,
|
|
||||||
init: function(page) {
|
|
||||||
if (DUMBLE_DEBUG) {return}
|
|
||||||
$.getScript("http://www.google-analytics.com/ga.js", function(page) {
|
|
||||||
Analytics.pageTracker = _gat._getTracker("UA-1736551-3");
|
|
||||||
Analytics.pageTracker._initData();
|
|
||||||
if(page) { Analytics.trackPage(page); }
|
|
||||||
});
|
|
||||||
},
|
|
||||||
trackPage: function(page) {
|
|
||||||
if (DUMBLE_DEBUG) {return}
|
|
||||||
if (!this.pageTracker) {
|
|
||||||
this.init(page);
|
|
||||||
} else {
|
|
||||||
this.pageTracker._trackPageview(page);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var Dumble = Dumble ? Dumble : {
|
|
||||||
currentUser: 'antrix',
|
|
||||||
currentTag: 'linker',
|
|
||||||
currentData: [],
|
|
||||||
currentURL: function() {
|
|
||||||
return this.urlFor(this.currentUser, this.currentTag);
|
|
||||||
},
|
|
||||||
urlFor: function(user, tag) {
|
|
||||||
return 'http://feeds.delicious.com/v2/json/' + user + ( tag ? '/' + tag : '');
|
|
||||||
},
|
|
||||||
friendsURLFor: function(user) {
|
|
||||||
return 'http://feeds.delicious.com/v2/json/networkmembers/' + user;
|
|
||||||
},
|
|
||||||
tagsURLFor: function(user) {
|
|
||||||
return 'http://feeds.delicious.com/v2/json/tags/' + user;
|
|
||||||
},
|
|
||||||
permalink: function(user, tag) {
|
|
||||||
if(!user) {user = this.currentUser}
|
|
||||||
if(!tag && user == this.currentUser) {tag = this.currentTag}
|
|
||||||
return location.protocol + '//' + location.host + location.pathname + '?u=' + user
|
|
||||||
+ (tag ? '&t=' + tag : '');
|
|
||||||
},
|
|
||||||
writeCookie: function() {
|
|
||||||
$.cookie('dumble010608', 'u='+this.currentUser+';t='+this.currentTag, {expires: 365});
|
|
||||||
/* Google Analytics */
|
|
||||||
Analytics.trackPage("/dumble/"+this.currentUser+"/"+this.currentTag);
|
|
||||||
},
|
|
||||||
readCookie: function() {
|
|
||||||
var prefs = $.cookie('dumble010608');
|
|
||||||
if (!prefs) return;
|
|
||||||
var data = prefs.split(';');
|
|
||||||
for (i=0; i<data.length; i++) {
|
|
||||||
if (data[i].charAt(0)=='u') {
|
|
||||||
this.currentUser = data[i].substring(2, data[i].length);
|
|
||||||
}
|
|
||||||
if (data[i].charAt(0)=='t') {
|
|
||||||
this.currentTag = data[i].substring(2, data[i].length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
updatePageFor: function(user, tag) {
|
|
||||||
if (typeof _lastUser == 'undefined' || _lastUser != user) {
|
|
||||||
_lastUser = user;
|
|
||||||
this.currentUser = user;
|
|
||||||
this.updateFriends();
|
|
||||||
this.updateTags();
|
|
||||||
}
|
|
||||||
this.currentTag = tag ? tag : '';
|
|
||||||
this.currentData = [];
|
|
||||||
$('#sourceUser').val(this.currentUser);
|
|
||||||
$('#sourceTag').val(this.currentTag);
|
|
||||||
$('#permalink').attr('href', this.permalink());
|
|
||||||
$('#rss-feed-body').attr('href', 'http://feeds.delicious.com/v2/rss/'+this.currentUser+'/'+this.currentTag);
|
|
||||||
$('#header h2').html('auto tumbling <em>'+this.currentUser+'’s</em> delicious links'
|
|
||||||
+ (tag ? ' tagged <em>' + tag + '</em>' : ''));
|
|
||||||
this.writeCookie();
|
|
||||||
this.updateHistory();
|
|
||||||
this.updatePage();
|
|
||||||
},
|
|
||||||
|
|
||||||
insertItems: function() {
|
|
||||||
var count = 0;
|
|
||||||
while (this.currentData.length > 0) {
|
|
||||||
var item = this.currentData.shift();
|
|
||||||
|
|
||||||
$.each(Providers, function() {
|
|
||||||
var v = this(item.u, item.d, item.n ? item.n : '');
|
|
||||||
if (v) {
|
|
||||||
$('#dynposts').append(
|
|
||||||
$('<div class="post"></div>\n').hide().prepend(v));
|
|
||||||
count += 1;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (count >= (DUMBLE_DEBUG ? 5 : 20)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.currentData.length > 0) {
|
|
||||||
$('#previous-next').fadeIn(1000);
|
|
||||||
}
|
|
||||||
$('.post').fadeIn(3000);
|
|
||||||
},
|
|
||||||
|
|
||||||
updateHistory: function() {
|
|
||||||
var string = this.currentUser + (this.currentTag ? '/' + this.currentTag : '');
|
|
||||||
var e = $('<li><a href="{l}" onClick="javascript:Dumble.updatePageFor(\'{n}\', \'{t}\');return false;">'.supplant({n: this.currentUser, t: this.currentTag, l: this.permalink()})
|
|
||||||
+string+ '</a></li>');
|
|
||||||
$('#history ul').prepend(e);
|
|
||||||
|
|
||||||
if ($('#history ul li').length == 2) {
|
|
||||||
$('#history ul').slideDown('fast');
|
|
||||||
$('#history-clear').show();
|
|
||||||
$('#history').fadeIn('slow');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
updateTags: function() {
|
|
||||||
$('#tags-list').fadeOut(1000);
|
|
||||||
$('#tags h3').text("{name}'s top tags".supplant({name: this.currentUser}));
|
|
||||||
|
|
||||||
$.getJSON(this.tagsURLFor(this.currentUser) + '?count=20&sort=count&callback=?',
|
|
||||||
function(tags) {
|
|
||||||
var tgt = $('#tags-list');
|
|
||||||
tgt.empty();
|
|
||||||
if(tags) { /* Delicious returns tags as {tag1: count1, 'foo': 20, 'bar': 30} */
|
|
||||||
$.each(tags, function(tag, count) {
|
|
||||||
var e = $('<li><a href="' +Dumble.permalink(Dumble.currentUser, tag)+ '" onClick="javascript:Dumble.updatePageFor(\'{name}\', \'{tag}\');return false;">{tag}</a></li>'.supplant({name: Dumble.currentUser, tag: tag}));
|
|
||||||
tgt.append(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if(tgt.html() == '') {
|
|
||||||
tgt.text(Dumble.currentUser + " hasn't tagged any links!");
|
|
||||||
}
|
|
||||||
$('#tags-list').fadeIn(1000);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
updateFriends: function() {
|
|
||||||
$('#friends-list').fadeOut(1000);
|
|
||||||
$('#friends h3').text("Explore {name}'s network".supplant({name: this.currentUser}));
|
|
||||||
$('#networklink a').attr('href', 'http://delicious.com/network?add=' + this.currentUser)
|
|
||||||
.text("Add {name} to your delicious network".supplant({name: this.currentUser}));
|
|
||||||
|
|
||||||
$.getJSON(this.friendsURLFor(this.currentUser) + '?callback=?',
|
|
||||||
function(names) {
|
|
||||||
var tgt = $('#friends-list');
|
|
||||||
tgt.empty();
|
|
||||||
$.each(names, function() {
|
|
||||||
var name = this.user;
|
|
||||||
var e = $('<li><a href="' +Dumble.permalink(name)+ '" onClick="javascript:Dumble.updatePageFor(\'{name}\');return false;">{name}</a></li>'.supplant({name: name}));
|
|
||||||
tgt.append(e);
|
|
||||||
});
|
|
||||||
if (!tgt.text()) {
|
|
||||||
tgt.text(Dumble.currentUser + "'s network is empty! Is this an anti-social person? ;-)");
|
|
||||||
}
|
|
||||||
$('#friends-list').fadeIn(1000);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
updatePage: function(URL) {
|
|
||||||
$('body').css({ cursor: 'wait' });
|
|
||||||
$('#previous-next').fadeOut(2000);
|
|
||||||
|
|
||||||
if (this.currentData.length <= 0) {
|
|
||||||
$('#dynposts').fadeOut(1000).empty().fadeIn(1);
|
|
||||||
$.getJSON((URL ? URL : this.currentURL())+'?count=100&callback=?',
|
|
||||||
function(data) {
|
|
||||||
if (data.length > 0) {
|
|
||||||
Dumble.currentData = data;
|
|
||||||
Dumble.insertItems();
|
|
||||||
} else {
|
|
||||||
$('#dynposts').append(
|
|
||||||
$('<div class="post"> \
|
|
||||||
<h3 style="text-align: center;">\
|
|
||||||
No items found :-(</h3></div>\n').hide());
|
|
||||||
$('.post').fadeIn(3000);
|
|
||||||
}
|
|
||||||
$('#about').slideUp('fast');
|
|
||||||
$('body').css({ cursor: 'default' });
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.insertItems();
|
|
||||||
$('body').css({ cursor: 'default' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} /* End Dumble namespace */
|
|
||||||
|
|
||||||
$(document).ready(function() {
|
|
||||||
|
|
||||||
/* Some page setup first */
|
|
||||||
$('#about').hide();
|
|
||||||
$('#previous-next').hide();
|
|
||||||
$('#history').hide();
|
|
||||||
|
|
||||||
/* Is our location URL the base Dumble app url or does it have u=? & t=? */
|
|
||||||
var isBaseURL = true;
|
|
||||||
|
|
||||||
var m = getUrlParam('u');
|
|
||||||
if (m) {
|
|
||||||
Dumble.currentUser = m;
|
|
||||||
Dumble.currentTag = '';
|
|
||||||
isBaseURL = false;
|
|
||||||
}
|
|
||||||
m = getUrlParam('t');
|
|
||||||
if (m) {
|
|
||||||
Dumble.currentTag = m;
|
|
||||||
isBaseURL = false;
|
|
||||||
}
|
|
||||||
m = unescape(getUrlParam('title'));
|
|
||||||
if (m) {
|
|
||||||
$('#header h1 a').text(m);
|
|
||||||
window.document.title = m;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isBaseURL) {
|
|
||||||
/* The initial page loaded via the root Dumble app url */
|
|
||||||
Dumble.readCookie();
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#aboutHeader,#updateSource').hover(
|
|
||||||
function() {
|
|
||||||
$(this).css({ cursor: 'pointer' });
|
|
||||||
},
|
|
||||||
function() {
|
|
||||||
$(this).css({ cursor: 'default' });
|
|
||||||
})
|
|
||||||
$('#aboutHeader').click(
|
|
||||||
function() {
|
|
||||||
$('#about').slideToggle('fast');
|
|
||||||
Analytics.trackPage("/dumble/--about-header--/");
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#sourceForm').submit(function() {
|
|
||||||
Dumble.updatePageFor($('#sourceUser').val(), $('#sourceTag').val());
|
|
||||||
$('#sourceUser').blur(); $('#sourceTag').blur();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
Dumble.updatePageFor(Dumble.currentUser, Dumble.currentTag);
|
|
||||||
|
|
||||||
}); /* End $(document).ready() block */
|
|
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 689 B |
|
@ -1,445 +0,0 @@
|
||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
|
|
||||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
|
||||||
|
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
|
||||||
<head>
|
|
||||||
<title>Dumble : auto tumble your delicious links</title>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
|
||||||
<meta name="description" content="Dumble is an experiment in creating a tumblelog from a user's delicious links."/>
|
|
||||||
|
|
||||||
<!--<link rel="shortcut icon" href="{Favicon}">
|
|
||||||
<link rel="alternate" type="application/rss+xml" title="RSS" id="rss-feed-head" href=""/>
|
|
||||||
-->
|
|
||||||
|
|
||||||
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.2/jquery.min.js"></script>
|
|
||||||
<script type="text/javascript" src="jquery.cookie.js"></script>
|
|
||||||
<script type="text/javascript" src="dumble.js"></script>
|
|
||||||
|
|
||||||
<style type="text/css" media="screen">
|
|
||||||
/* CSS Reset by Eric Meyer
|
|
||||||
* http://meyerweb.com/eric/thoughts/2007/05/01/reset-reloaded/
|
|
||||||
*/
|
|
||||||
html, body, div, span, applet, object, iframe,
|
|
||||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
|
||||||
a, abbr, acronym, address, big, cite, code,
|
|
||||||
del, dfn, em, font, img, ins, kbd, q, s, samp,
|
|
||||||
small, strike, strong, sub, sup, tt, var,
|
|
||||||
dl, dt, dd, ol, ul, li,
|
|
||||||
fieldset, form, label, legend,
|
|
||||||
table, caption, tbody, tfoot, thead, tr, th, td {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
outline: 0;
|
|
||||||
font-weight: inherit;
|
|
||||||
font-style: inherit;
|
|
||||||
font-size: 100%;
|
|
||||||
font-family: inherit;
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
||||||
/* remember to define focus styles! */
|
|
||||||
:focus {
|
|
||||||
outline: 0;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
line-height: 1;
|
|
||||||
color: black;
|
|
||||||
background: white;
|
|
||||||
}
|
|
||||||
ol, ul {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
/* tables still need 'cellspacing="0"' in the markup */
|
|
||||||
table {
|
|
||||||
border-collapse: separate;
|
|
||||||
border-spacing: 0;
|
|
||||||
}
|
|
||||||
caption, th, td {
|
|
||||||
text-align: left;
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
blockquote:before, blockquote:after,
|
|
||||||
q:before, q:after {
|
|
||||||
content: "";
|
|
||||||
}
|
|
||||||
blockquote, q {
|
|
||||||
quotes: "" "";
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background:#262523;
|
|
||||||
color: #ddc;
|
|
||||||
font: normal 14px "Trebuchet MS", Helvetica, Arial, sans-serif;
|
|
||||||
line-height: 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
b, strong {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
abbr,acronym {
|
|
||||||
border-bottom:1px dotted #CC9933;
|
|
||||||
cursor:help;
|
|
||||||
}
|
|
||||||
|
|
||||||
em, i {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 2.5em;
|
|
||||||
font-weight: bold;
|
|
||||||
line-height: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
color: #ccb;
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: normal;
|
|
||||||
padding-left: 3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: 0 0 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
a, a:visited, a:active {
|
|
||||||
color: #94a970;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
color: #262523;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
abbr {
|
|
||||||
border-bottom-width: 1px;
|
|
||||||
border-bottom-style: dotted;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote {
|
|
||||||
background-color: #4f534f;
|
|
||||||
border: 1px solid #333;
|
|
||||||
font-style: italic;
|
|
||||||
margin: 1em 2em 1.5em 1em;
|
|
||||||
padding: .25em 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul, ol {
|
|
||||||
margin: 1.5em 3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
list-style: disc;
|
|
||||||
}
|
|
||||||
|
|
||||||
ol {
|
|
||||||
list-style: decimal;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
color: #ddc;
|
|
||||||
background-color: #363b39;
|
|
||||||
font-size: 0.9em;
|
|
||||||
padding: 0.3em;
|
|
||||||
border: 1px solid #ddc;
|
|
||||||
margin-right: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
color: #ddc;
|
|
||||||
background-color: #334B42;
|
|
||||||
font-size: 0.9em;
|
|
||||||
padding: 0.3em;
|
|
||||||
border: 1px solid #ddc;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header */
|
|
||||||
#header {
|
|
||||||
margin: 1em auto;
|
|
||||||
width: 40em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#header a, #header a:visited, #header a:active {
|
|
||||||
color: #eef;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#header a:hover {
|
|
||||||
color: #ccb;
|
|
||||||
}
|
|
||||||
|
|
||||||
#friends {
|
|
||||||
border-top: 1px solid #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wordlist {
|
|
||||||
margin: 1em;
|
|
||||||
padding: 1em 0em 0em 0em;
|
|
||||||
}
|
|
||||||
.wordlist br {clear: both}
|
|
||||||
.wordlist ul { list-style: none;}
|
|
||||||
.wordlist li {
|
|
||||||
margin-bottom: 0.5em; padding: 0; display: block; float: left; width: 25%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Posts */
|
|
||||||
#posts {
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 42em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Individual Post */
|
|
||||||
.post {
|
|
||||||
background-color: #363b39;
|
|
||||||
border: 1px solid #494949;
|
|
||||||
margin: 1.5em 0;
|
|
||||||
padding: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post .date {
|
|
||||||
display: block;
|
|
||||||
font-size: 1.5em;
|
|
||||||
font-weight: normal;
|
|
||||||
margin-bottom: .5em;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post .date a, .post .date a:visited, .post .date a:active {
|
|
||||||
color: #94a970;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post .date a:hover {
|
|
||||||
color: #262523;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post .date a .diff {
|
|
||||||
font-size: 1.6em;
|
|
||||||
line-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Titles */
|
|
||||||
.regular h3, .conversation h3, .link h3 {
|
|
||||||
color: #94a970;
|
|
||||||
font-size: 1.5em;
|
|
||||||
line-height: 1em;
|
|
||||||
margin-bottom: .5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* Quote Posts */
|
|
||||||
.quote {
|
|
||||||
font-size: 1.5em;
|
|
||||||
line-height: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quote .source {
|
|
||||||
font-size: smaller; font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quote .source a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Link Posts */
|
|
||||||
.link h3 a, .link h3 a:visited, .link h3 a:active {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.link h3 a:hover {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Photo/Video/Audio */
|
|
||||||
.photo, .video, .audio {
|
|
||||||
text-align: center;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.photo img {
|
|
||||||
background-color: #404040;
|
|
||||||
border: 1px solid #333;
|
|
||||||
margin: 1em 0 .5em;
|
|
||||||
padding: 5px;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.photo .stars {
|
|
||||||
background-color: transparent;
|
|
||||||
border: 0pt none;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
vertical-align: middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
.photo .caption, .video .caption, .audio .caption {
|
|
||||||
display: block;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 1.5em;
|
|
||||||
font-weight: normal;
|
|
||||||
margin-top: .5em;
|
|
||||||
margin-bottom: .5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Conversations */
|
|
||||||
.conversation ul {
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversation ul li {
|
|
||||||
border-bottom: 1px solid #565b59;
|
|
||||||
padding: .25em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversation ul li .person {
|
|
||||||
color: #565b59;
|
|
||||||
display: block;
|
|
||||||
float: left;
|
|
||||||
width: 9em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.conversation ul li .line {
|
|
||||||
display: block;
|
|
||||||
margin-left: 10em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Previous and Next Links */
|
|
||||||
#previous-next {
|
|
||||||
font-size: 1.25em;
|
|
||||||
margin: 3em 0 2em;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#previous-next .pagecount {
|
|
||||||
margin: 0 1.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#previous-next a, #previous-next a:visited, #previous-next a:active {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#previous-next a:hover {
|
|
||||||
color: #ddc;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Footer */
|
|
||||||
#footer {
|
|
||||||
border-top: 1px solid #555;
|
|
||||||
color: #555;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 40em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#footer ul {
|
|
||||||
list-style: none;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#footer ul li {
|
|
||||||
display: inline;
|
|
||||||
margin: 0 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#footer ul li a, #footer ul li a:visited, #footer ul li a:active {
|
|
||||||
color: #94a970;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#footer ul li a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
#history {
|
|
||||||
text-align: left;
|
|
||||||
position: absolute;
|
|
||||||
width: 15em;
|
|
||||||
background-color: #363b39;
|
|
||||||
border: 1px solid #494949;
|
|
||||||
margin-left: 44em;
|
|
||||||
padding: 0em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#history h2 {text-align: center; padding: 0.2em; border-bottom: 1px solid #494949;
|
|
||||||
font-size: 1.2em;}
|
|
||||||
#history ul {list-style: none; margin: 1em 1em 0.5em 1em;}
|
|
||||||
#history-clear a, #history-clear a:visited,
|
|
||||||
#history-clear a:active, #history-clear a:hover {text-decoration: none; color: #555;}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!--[if IE]>
|
|
||||||
<style type="text/css" media="screen">
|
|
||||||
.conversation ul li {
|
|
||||||
width: 98%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<![endif]-->
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="header">
|
|
||||||
<h1><a href=".">Dumble: delicious tumblelog</a></h1>
|
|
||||||
<h2>auto tumble your delicious links</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="posts">
|
|
||||||
|
|
||||||
<div id="history"><h2>Session History</h2><ul></ul>
|
|
||||||
<h2 id="history-clear">
|
|
||||||
<a href="" onClick="javascript:$('#history-clear').hide();$('#history ul').slideUp('slow').empty();Dumble.updateHistory();return false;">Clear?</a>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="post" style="background-color: #334B42;">
|
|
||||||
<div class="regular">
|
|
||||||
<h3 id="aboutHeader" style="color: #eef; text-align: center;">What is Dumble?</h3>
|
|
||||||
<div id="about"><strong>Dumble</strong> is an <a href="http://oohembed.com/" target="_blank">oohEmbed</a> powered <a href="http://antrix.net/journal/techtalk/dumble.html" target="_blank">experiment</a> in creating a <a href="http://en.wikipedia.org/wiki/Tumblelog" target="_blank">tumblelog</a> from a user's <a href="http://delicious.com/" target="_blank">delicious.com</a> links.<br/><br/>To create a tumblelog for a different delicious user, just modify the details below.
|
|
||||||
<div style="margin: 1em 1em 0em 1em; padding: 1em 1em 0em 1em;">
|
|
||||||
<form id="sourceForm" style="margin-bottom: 2em;">
|
|
||||||
Username: <input id="sourceUser" type="text" size="10"/>
|
|
||||||
A tag (optional): <input id="sourceTag" type="text" size="10"/>
|
|
||||||
<button type="submit">Update</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div id="tags" class="wordlist">
|
|
||||||
<h3 style="text-align: center; color: #ddc;"></h3>
|
|
||||||
<ul id="tags-list" class="wordlist"></ul><br/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="border-top: 1px solid #555; padding: 1em; margin: 1em;">
|
|
||||||
Need a link to this page? <a id='permalink' href="">Here you go!</a>
|
|
||||||
Or get the feed! <a id='rss-feed-body' style="border: none;" href=""><img style="vertical-align: middle" src="images/feed-icon.png" alt="RSS Feed icon"/></a><br/><br/>
|
|
||||||
There's also a <a href="javascript:{var b='http://oohembed.com/dumble/?u='; var u=document.location.href;m = /del\.icio\.us\/([\w\.]+)\/?/i.exec(u);if(m && m[1] != 'tag' && m[1] != 'network' && m[1] != 'subscriptions' && m[1] != 'url' && m[1] != 'for') {var go = b+m[1]; n = /del\.icio\.us\/\w+\/(\w+)\/?/i.exec(u); if (n) {go = go + '&t=' + n[1];} document.location.href=go;} else {alert('Could not figure out delicious user name');}}">View in Dumble</a> bookmarklet and a similar <a href="http://antrix.net/stuff/gm/dumble.oohembed.user.js">greasemonkey script</a> to integrate Dumble with delicious.
|
|
||||||
</div>
|
|
||||||
<div id="friends" class="wordlist">
|
|
||||||
<h3 style="text-align: center; color: #ddc;"></h3>
|
|
||||||
<ul id="friends-list" class="wordlist"></ul><br/>
|
|
||||||
<div id="networklink" style="text-align: center; margin-top: 1em;">
|
|
||||||
<a target="_blank"></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div> <!-- end regular post -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="dynposts">
|
|
||||||
<!-- dynamic posts inserted here -->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="previous-next">
|
|
||||||
<a href="" onClick="javascript:Analytics.trackPage('/dumble/--more--/');Dumble.updatePage();return false;">More?</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div> <!-- end posts-->
|
|
||||||
|
|
||||||
<div id="footer">
|
|
||||||
<ul>
|
|
||||||
<li>Engine <a href="http://jquery.com/" target="_blank">jQuery</a></li>
|
|
||||||
<li>Designer <a href="http://cubicle17.com/" target="_blank">Bill Israel</a></li>
|
|
||||||
<li>Code Monkey <a href="http://antrix.net/" target="_blank">Deepak Sarda</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,51 +0,0 @@
|
||||||
/**
|
|
||||||
* Cookie plugin
|
|
||||||
*
|
|
||||||
* Copyright (c) 2006 Klaus Hartl (stilbuero.de)
|
|
||||||
* Dual licensed under the MIT and GPL licenses:
|
|
||||||
* http://www.opensource.org/licenses/mit-license.php
|
|
||||||
* http://www.gnu.org/licenses/gpl.html
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
jQuery.cookie = function(name, value, options) {
|
|
||||||
if (typeof value != 'undefined') { // name and value given, set cookie
|
|
||||||
options = options || {};
|
|
||||||
if (value === null) {
|
|
||||||
value = '';
|
|
||||||
options.expires = -1;
|
|
||||||
}
|
|
||||||
var expires = '';
|
|
||||||
if (options.expires && (typeof options.expires == 'number' || options.expires.toUTCString)) {
|
|
||||||
var date;
|
|
||||||
if (typeof options.expires == 'number') {
|
|
||||||
date = new Date();
|
|
||||||
date.setTime(date.getTime() + (options.expires * 24 * 60 * 60 * 1000));
|
|
||||||
} else {
|
|
||||||
date = options.expires;
|
|
||||||
}
|
|
||||||
expires = '; expires=' + date.toUTCString(); // use expires attribute, max-age is not supported by IE
|
|
||||||
}
|
|
||||||
// CAUTION: Needed to parenthesize options.path and options.domain
|
|
||||||
// in the following expressions, otherwise they evaluate to undefined
|
|
||||||
// in the packed version for some reason...
|
|
||||||
var path = options.path ? '; path=' + (options.path) : '';
|
|
||||||
var domain = options.domain ? '; domain=' + (options.domain) : '';
|
|
||||||
var secure = options.secure ? '; secure' : '';
|
|
||||||
document.cookie = [name, '=', encodeURIComponent(value), expires, path, domain, secure].join('');
|
|
||||||
} else { // only name given, get cookie
|
|
||||||
var cookieValue = null;
|
|
||||||
if (document.cookie && document.cookie != '') {
|
|
||||||
var cookies = document.cookie.split(';');
|
|
||||||
for (var i = 0; i < cookies.length; i++) {
|
|
||||||
var cookie = jQuery.trim(cookies[i]);
|
|
||||||
// Does this cookie string begin with the name we want?
|
|
||||||
if (cookie.substring(0, name.length + 1) == (name + '=')) {
|
|
||||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cookieValue;
|
|
||||||
}
|
|
||||||
};
|
|
2858
app/feedparser.py
267
app/index.jinja
|
@ -1,267 +0,0 @@
|
||||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
|
|
||||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
|
||||||
|
|
||||||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
|
||||||
<head>
|
|
||||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
|
||||||
<meta name="description" content="oohEmbed is an oEmbed compatible provider of HTML embed codes for various web sites"/>
|
|
||||||
<title>oohEmbed.com - your one-stop oEmbed provider</title>
|
|
||||||
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.5.2/build/reset/reset-min.css" />
|
|
||||||
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.5.2/build/base/base-min.css" />
|
|
||||||
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/2.5.2/build/fonts/fonts-min.css" />
|
|
||||||
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.2/jquery.min.js"></script>
|
|
||||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
|
|
||||||
<style type="text/css">
|
|
||||||
html, body {
|
|
||||||
height: 100%;
|
|
||||||
background-color: #EFEFEF;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
margin: 1em;
|
|
||||||
color: black;
|
|
||||||
/*font-family: Consolas, 'Lucida Console', 'DejaVu Sans Mono', Monaco, 'Courier New', monospace; */
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
pre {
|
|
||||||
background-color: #e0e0e0;
|
|
||||||
border: 1px solid #999;
|
|
||||||
padding: 8px;
|
|
||||||
margin: 0px 20px 10px 0px;
|
|
||||||
}
|
|
||||||
code {
|
|
||||||
background-color: #e0e0e0;
|
|
||||||
border: 1px solid #999;
|
|
||||||
padding: 0px 2px;
|
|
||||||
}
|
|
||||||
.naked {
|
|
||||||
margin: 0em;
|
|
||||||
}
|
|
||||||
.naked li {
|
|
||||||
list-style-type: none;
|
|
||||||
margin-left: 0;
|
|
||||||
padding: 0.5em;
|
|
||||||
text-indent: -0.5em;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
{% filter markdown %}
|
|
||||||
# Welcome to oohEmbed!
|
|
||||||
## oohEmbed What?
|
|
||||||
|
|
||||||
In a nutshell: __oohEmbed is an oEmbed compatible provider of HTML embed codes for various web sites.__
|
|
||||||
|
|
||||||
What is this oEmbed? From [oembed.com](http://www.oembed.com/):
|
|
||||||
|
|
||||||
> oEmbed is a format for allowing an embedded representation of a URL on third party sites.
|
|
||||||
> The simple API allows a website to display embedded content (such as photos or videos) when
|
|
||||||
> a user posts a link to that resource, without having to parse the resource directly.
|
|
||||||
|
|
||||||
Don't get it? Perhaps an example will make things clear. If you make a URL request like this:
|
|
||||||
{% endfilter %}
|
|
||||||
|
|
||||||
<pre>http://oohembed.com/oohembed/?url=http%3A//www.amazon.com/Myths-Innovation-Scott-Berkun/dp/0596527055/</pre>
|
|
||||||
|
|
||||||
{% filter markdown %}
|
|
||||||
You will get this as the response:
|
|
||||||
{% endfilter %}
|
|
||||||
|
|
||||||
<pre>{
|
|
||||||
"type": "photo",
|
|
||||||
"version": "1.0",
|
|
||||||
"provider_name": "Amazon Product Image",
|
|
||||||
"url": "http://ecx.images-amazon.com/images/I/31%2BfVjL2nqL.jpg",
|
|
||||||
"height": "500",
|
|
||||||
"width": "317",
|
|
||||||
"thumbnail_url": "http://ecx.images-amazon.com/images/I/31%2BfVjL2nqL._SL75_.jpg",
|
|
||||||
"thumbnail_height": "75",
|
|
||||||
"thumbnail_width": "48",
|
|
||||||
"asin": "0596527055",
|
|
||||||
"title": "The Myths of Innovation",
|
|
||||||
"author_name": "Scott Berkun",
|
|
||||||
"author_url": "http://www.amazon.com/gp/redirect.html%3FASIN=0596527055%26location=/Myths-Innovation-Scott-Berkun/dp/0596527055"
|
|
||||||
}</pre>
|
|
||||||
|
|
||||||
{% filter markdown %}
|
|
||||||
There, that should make everything clear. Still no? Then perhaps you should go and read this excellent article introducing [oEmbed & oohEmbed](http://ciaranmcnulty.com/blog/2009/01/embedding-third-party-content-in-your-site-using-oembed) by Ciaran McNulty. Once you are done reading that, come back here for our Q&A!
|
|
||||||
|
|
||||||
## Q & A
|
|
||||||
|
|
||||||
1. How is this useful?
|
|
||||||
I built oohEmbed to further development of [Dumble](/dumble/?u=antrix&t=dumbletest), my delicious.com tumblelog.
|
|
||||||
But I imagine oohEmbed could be useful in several other places. For example, someone could build a
|
|
||||||
blog engine plugin backed by the oohEmbed web service that makes embedding content into blog posts
|
|
||||||
easier. (If you do build such a plugin, let me know!)
|
|
||||||
|
|
||||||
Scroll down to see a list of [consumer sites and apps](#consumers) that use oohEmbed.
|
|
||||||
|
|
||||||
2. So what sites can oohEmbed provide information for?
|
|
||||||
Quite a few to start with - scroll down to see [the list](#configuration). I'll be adding more sites as I find time!
|
|
||||||
|
|
||||||
3. Is oohEmbed really oembed.com spec compliant?
|
|
||||||
Almost. oohEmbed differs in some ways from the spec.
|
|
||||||
* It usually ignores the maxwidth/maxheight request parameters.
|
|
||||||
* Only JSON responses are provided. I don't have any plans of supporting XML.
|
|
||||||
* You can specify a JSON callback.
|
|
||||||
|
|
||||||
4. JSON Callback?
|
|
||||||
To overcome the [same origin policy](http://en.wikipedia.org/wiki/Same_origin_policy) of web browsers, you can specify a `callback` parameter as part of your request.
|
|
||||||
e.g., if you specify `callback=myCallback` as part of the request, then the JSON response object will be wrapped as a parameter to the `myCallback` function.
|
|
||||||
|
|
||||||
5. Will this callback expose my site to cross-site attacks?
|
|
||||||
Possibly. Although you have to trust that I won't be doing anything funny or malicious intentionally, oohEmbed itself is just a proxy to the target website.
|
|
||||||
So if the target website sends back something bad, all bets are off!
|
|
||||||
|
|
||||||
6. So I can't sue you if anything goes wrong?
|
|
||||||
No! By using this service, you agree that I will not be held liable for anything that goes wrong. I do appreciate [bug reports](http://code.google.com/p/oohembed/issues/list) though.
|
|
||||||
|
|
||||||
7. Is it free to use?
|
|
||||||
Yes. At the moment, all this service costs me is $10/year for the domain name and several hours of my free time.
|
|
||||||
Google App Engine kindly hosts the app for free (thanks!) so that's taken care of.
|
|
||||||
|
|
||||||
8. But is it _really free_?
|
|
||||||
Oh yes, it is _really free_ too! You can get access to the code that powers oohEmbed.com from the [oohEmbed project on Google Code](http://oohembed.googlecode.com).
|
|
||||||
The code is available under a liberal BSD license so feel free to use it as you wish. Of course, I'd appreciate any code contributions too!
|
|
||||||
|
|
||||||
8. So I can use the site as much as I want?
|
|
||||||
Yes, but to a limit. If I detect abuse, I reserve the right to throttle your use or worse, ban you from accessing the service.
|
|
||||||
|
|
||||||
9. Great, how do I sign-up?!
|
|
||||||
Umm.. there is no sign-up. Here's your service, run with it: `http://oohembed.com/oohembed/?url=`
|
|
||||||
|
|
||||||
10. I'm all fired up. Where's the oEmbed provider configuration for the oohEmbed service to get me going?
|
|
||||||
Just scroll down this page for the [API endpoint and supported URL schemes](#configuration).
|
|
||||||
|
|
||||||
11. I notice that you also support websites which already have an oEmbed compliant provider. Why?
|
|
||||||
Two reasons:
|
|
||||||
* They don't support the 'callback' parameter for JSON requests
|
|
||||||
* As a developer, wouldn't it be nice if there was just one API endpoint for all sites
|
|
||||||
instead of having to support one endpoint for each?
|
|
||||||
|
|
||||||
12. I represent one of the sites for which oohEmbed provides responses and I don't like it.
|
|
||||||
Fair enough. Drop me an <a href="mailto:deepak@antrix.deletethis.net">email</a> and I'll stop supporting your site.
|
|
||||||
|
|
||||||
13. I would rather not bounce my requests through oohEmbed. Can I just get your list of oEmbed compliant providers instead?
|
|
||||||
You are in luck! The list of all oEmbed compliant endpoints that oohEmbed knows about can be [downloaded as a JSON file](/static/endpoints.json).
|
|
||||||
|
|
||||||
14. How do I report bugs? Do you take feature requests?
|
|
||||||
Sure. Please report bugs & enhancement requests using the [issue tracker](http://code.google.com/p/oohembed/issues/) on Google Code.
|
|
||||||
|
|
||||||
15. Anything else?
|
|
||||||
Nothing, except that oohEmbed wouldn't be possible without the folks behind the oEmbed.com spec so kudos to them.
|
|
||||||
And thanks again to Google for creating App Engine!
|
|
||||||
|
|
||||||
16. Who are you?
|
|
||||||
I am Deepak Sarda and you can find me <a href="http://antrix.net/" rel="me">here</a>.
|
|
||||||
|
|
||||||
## <a name='configuration'>Configuration</a>
|
|
||||||
|
|
||||||
There's just one API endpoint: <code>http://oohembed.com/oohembed/</code>. The supported URL schemes are listed below. If you call the API endpoint with a URL that doesn't confirm to one of the schemes listed below, you will get a 404 response. As mentioned earlier, you can use a `callback` parameter when calling the endpoint.
|
|
||||||
|
|
||||||
Click on any item in the list below to see configuration details for that item.
|
|
||||||
{% endfilter %}
|
|
||||||
|
|
||||||
<ol>
|
|
||||||
{% for p in providers %}
|
|
||||||
<li style="margin-bottom: 1.5em;"><strong class="provider-title">{{ p.title }}</strong>
|
|
||||||
<ul class="naked provider-body"><li>{{ p.description }}</li>
|
|
||||||
<li><em>URL scheme:</em> <code>{{ p.url|escape }}</code></li>
|
|
||||||
<li><em>Example:</em> <code>
|
|
||||||
{{ '<a target="_blank" href="http://'+ hostname + '/oohembed/?url='+(p.example_url|urlencode)+'">http://'+hostname+'/oohembed/?url='+(p.example_url|urlencode)+'</a>' }}</code></li></ul>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
{% filter markdown %}
|
|
||||||
## <a name='consumers'>Consumers</a>
|
|
||||||
|
|
||||||
These consumers currently use the oohEmbed service.
|
|
||||||
|
|
||||||
* [Dumble](/dumble/?u=antrix&t=linker) of course, being the first consumer and my reason for creating oohEmbed.
|
|
||||||
* [280 Slides](http://blog.280north.com/2008/06/25/280-slides-gets-smarter-about-links/), a snazzy web-app to create presentations.
|
|
||||||
* [Buckybase](http://buckybase.blogspot.com/2008/06/oembed-support.html), a social web database with bidirectional hyperlinks.
|
|
||||||
* [Indy.com](http://www.indy.com/), a local entertainment site by The Indianapolis Star.
|
|
||||||
* [Ars Technica](http://arstechnica.com/), a technology news and analysis site.
|
|
||||||
* [DropAVideo](http://dropavideo.com/), a Brazilian video micro-blogging service.
|
|
||||||
* [Zenbe Shareflow](http://www.zenbe.com/shareflow), a team collaboration service.
|
|
||||||
* [Hostelz.com](http://www.hostelz.com/), a hostel review site.
|
|
||||||
* [StatusNet](http://status.net/), a popular open-source microblogging platform.
|
|
||||||
|
|
||||||
Moreover, oohEmbed support is already baked into these libraries:
|
|
||||||
|
|
||||||
* [django-oembed](http://code.google.com/p/django-oembed/)
|
|
||||||
* [ruby-oembed](http://github.com/judofyr/ruby-oembed/tree/master)
|
|
||||||
* [oembed_links gem](http://github.com/netshade/oembed_links/tree/master)
|
|
||||||
* [jquery-oembed](http://code.google.com/p/jquery-oembed/)
|
|
||||||
* [drupal-oembed](http://github.com/voxpelli/drupal-oembed/tree/master)
|
|
||||||
* [Typo3 Extension](http://typo3.org/extensions/repository/view/oembed/current/)
|
|
||||||
|
|
||||||
If you are using oohEmbed, please leave a comment below and I'll add you to this list!
|
|
||||||
|
|
||||||
Consumers may also be interested in [downloading the list of oEmbed compliant endpoints](/static/endpoints.json) that oohEmbed knows about and use this information to make direct requests to the respective endpoints.
|
|
||||||
|
|
||||||
## Changes
|
|
||||||
|
|
||||||
* _19th November, 2010:_ Added downloadable list of oEmbed compliant providers. New support for Clikthrough, Photobucket, Kinomap, dotsub & YFrog.
|
|
||||||
* _17th April, 2010:_ New dotsub.com support.
|
|
||||||
* _25th February, 2010:_ Fix Amazon support. Switch Youtube to upstream oembed endpoint.
|
|
||||||
* _2nd September, 2009:_ New yfrog.com support. New xkcd support.
|
|
||||||
* _31st August, 2009:_ Chocochip release with bunch of newly supported sites and lots of bug fixes. [Release Notes](http://code.google.com/p/oohembed/wiki/chocochip).
|
|
||||||
* _6th June, 2009:_ Added missing width, height elements to Youtube response. Added title, author and thumbnail elements to Metacafe response.
|
|
||||||
* _10th May, 2009:_ New LiveJournal UserPic provider. More data returned in Youtube provider. Removed Pownce provider. Updated Consumers section.
|
|
||||||
* _31st March, 2009:_ Updates to Consumers section.
|
|
||||||
* _2nd March, 2009:_ New twitpic.com support.
|
|
||||||
* _3rd February, 2009:_ New 5min.com, nfb.ca & thedailyshow.com support.
|
|
||||||
* _14th November, 2008:_ Released oohEmbed source. Fixed mime-type of responses. Moved Dumble to new delicious.com API.
|
|
||||||
* _23rd July, 2008:_ New Vimeo support. Some text encoding related fixes.
|
|
||||||
* _30th June, 2008:_ New SlideShare provider. Cleaned up homepage (added consumers). Several minor fixes.
|
|
||||||
* _12th June, 2008:_ New IMDb.com movie photo/link provider.
|
|
||||||
* _11th June, 2008:_ New Wordpress.com blogs support. Several fixes in [Dumble](/dumble/).
|
|
||||||
* _3rd June, 2008:_ New hulu.com & revision3.com support
|
|
||||||
* _1st June, 2008:_ Launch
|
|
||||||
{% endfilter %}
|
|
||||||
|
|
||||||
<hr/>
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
$(document).ready(function() {
|
|
||||||
$('.provider-body').hide();
|
|
||||||
|
|
||||||
$('.provider-title').hover(
|
|
||||||
function() {
|
|
||||||
$(this).css({ cursor: 'pointer' });
|
|
||||||
},
|
|
||||||
function() {
|
|
||||||
$(this).css({ cursor: 'default' });
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
$('.provider-title').click(
|
|
||||||
function() {
|
|
||||||
$(this).next('.provider-body').slideToggle('fast');
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{% if production %}
|
|
||||||
<a name='comments'></a>
|
|
||||||
<div style="margin-top: 1em;" id="disqus_thread"></div><script type="text/javascript" src="http://disqus.com/forums/oohembed/embed.js"></script><noscript><a href="http://oohembed.disqus.com/?url=ref">View the forum thread.</a></noscript><a href="http://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
|
|
||||||
document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
|
|
||||||
</script>
|
|
||||||
<script type="text/javascript">
|
|
||||||
var pageTracker = _gat._getTracker("UA-1736551-3");
|
|
||||||
pageTracker._initData();
|
|
||||||
pageTracker._trackPageview();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
|
@ -1,72 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja
|
|
||||||
~~~~~
|
|
||||||
|
|
||||||
Jinja is a `sandboxed`_ template engine written in pure Python. It
|
|
||||||
provides a `Django`_ like non-XML syntax and compiles templates into
|
|
||||||
executable python code. It's basically a combination of Django templates
|
|
||||||
and python code.
|
|
||||||
|
|
||||||
Nutshell
|
|
||||||
--------
|
|
||||||
|
|
||||||
Here a small example of a Jinja template::
|
|
||||||
|
|
||||||
{% extends 'base.html' %}
|
|
||||||
{% block title %}Memberlist{% endblock %}
|
|
||||||
{% block content %}
|
|
||||||
<ul>
|
|
||||||
{% for user in users %}
|
|
||||||
<li><a href="{{ user.url|e }}">{{ user.username|e }}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
Philosophy
|
|
||||||
----------
|
|
||||||
|
|
||||||
Application logic is for the controller but don't try to make the life
|
|
||||||
for the template designer too hard by giving him too few functionality.
|
|
||||||
|
|
||||||
For more informations visit the new `jinja webpage`_ and `documentation`_.
|
|
||||||
|
|
||||||
Note
|
|
||||||
----
|
|
||||||
|
|
||||||
This is the Jinja 1.0 release which is completely incompatible with the
|
|
||||||
old "pre 1.0" branch. The old branch will still receive security updates
|
|
||||||
and bugfixes but the 1.0 branch will be the only version that receives
|
|
||||||
support.
|
|
||||||
|
|
||||||
If you have an application that uses Jinja 0.9 and won't be updated in
|
|
||||||
the near future the best idea is to ship a Jinja 0.9 checkout together
|
|
||||||
with the application.
|
|
||||||
|
|
||||||
The `Jinja tip`_ is installable via `easy_install` with ``easy_install
|
|
||||||
Jinja==dev``.
|
|
||||||
|
|
||||||
.. _sandboxed: http://en.wikipedia.org/wiki/Sandbox_(computer_security)
|
|
||||||
.. _Django: http://www.djangoproject.com/
|
|
||||||
.. _jinja webpage: http://jinja.pocoo.org/
|
|
||||||
.. _documentation: http://jinja.pocoo.org/documentation/index.html
|
|
||||||
.. _Jinja tip: http://dev.pocoo.org/hg/jinja-main/archive/tip.tar.gz#egg=Jinja-dev
|
|
||||||
|
|
||||||
|
|
||||||
:copyright: 2007 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from jinja.environment import Environment
|
|
||||||
from jinja.datastructure import Markup
|
|
||||||
from jinja.plugin import jinja_plugin_factory as template_plugin_factory
|
|
||||||
from jinja.loaders import FileSystemLoader, PackageLoader, DictLoader, \
|
|
||||||
ChoiceLoader, FunctionLoader, MemcachedFileSystemLoader
|
|
||||||
from jinja.utils import from_string
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['Environment', 'Markup', 'FileSystemLoader', 'PackageLoader',
|
|
||||||
'DictLoader', 'ChoiceLoader', 'FunctionLoader',
|
|
||||||
'MemcachedFileSystemLoader', 'from_string']
|
|
||||||
|
|
||||||
__version__ = '1.2'
|
|
|
@ -1,85 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja._native
|
|
||||||
~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
This module implements the native base classes in case of not
|
|
||||||
having a jinja with the _speedups module compiled.
|
|
||||||
|
|
||||||
Note that if you change semantics here you have to edit the
|
|
||||||
_speedups.c file to in order to support those changes for jinja
|
|
||||||
setups with enabled speedup module.
|
|
||||||
|
|
||||||
:copyright: 2007 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
from jinja.datastructure import Deferred
|
|
||||||
from jinja.utils import deque
|
|
||||||
|
|
||||||
|
|
||||||
class BaseContext(object):
|
|
||||||
|
|
||||||
def __init__(self, undefined_singleton, globals, initial):
|
|
||||||
self._undefined_singleton = undefined_singleton
|
|
||||||
self.current = current = {}
|
|
||||||
self._stack = deque([current, initial, globals])
|
|
||||||
self.globals = globals
|
|
||||||
self.initial = initial
|
|
||||||
|
|
||||||
self._push = self._stack.appendleft
|
|
||||||
self._pop = self._stack.popleft
|
|
||||||
|
|
||||||
def stack(self):
|
|
||||||
return list(self._stack)[::-1]
|
|
||||||
stack = property(stack)
|
|
||||||
|
|
||||||
def pop(self):
|
|
||||||
"""Pop the last layer from the stack and return it."""
|
|
||||||
rv = self._pop()
|
|
||||||
self.current = self._stack[0]
|
|
||||||
return rv
|
|
||||||
|
|
||||||
def push(self, data=None):
|
|
||||||
"""
|
|
||||||
Push one layer to the stack and return it. Layer must be
|
|
||||||
a dict or omitted.
|
|
||||||
"""
|
|
||||||
data = data or {}
|
|
||||||
self._push(data)
|
|
||||||
self.current = self._stack[0]
|
|
||||||
return data
|
|
||||||
|
|
||||||
def __getitem__(self, name):
|
|
||||||
"""
|
|
||||||
Resolve one item. Restrict the access to internal variables
|
|
||||||
such as ``'::cycle1'``. Resolve deferreds.
|
|
||||||
"""
|
|
||||||
if not name.startswith('::'):
|
|
||||||
for d in self._stack:
|
|
||||||
if name in d:
|
|
||||||
rv = d[name]
|
|
||||||
if rv.__class__ is Deferred:
|
|
||||||
rv = rv(self, name)
|
|
||||||
# never touch the globals!
|
|
||||||
if d is self.globals:
|
|
||||||
self.initial[name] = rv
|
|
||||||
else:
|
|
||||||
d[name] = rv
|
|
||||||
return rv
|
|
||||||
return self._undefined_singleton
|
|
||||||
|
|
||||||
def __setitem__(self, name, value):
|
|
||||||
"""Set a variable in the outermost layer."""
|
|
||||||
self.current[name] = value
|
|
||||||
|
|
||||||
def __delitem__(self, name):
|
|
||||||
"""Delete a variable in the outermost layer."""
|
|
||||||
if name in self.current:
|
|
||||||
del self.current[name]
|
|
||||||
|
|
||||||
def __contains__(self, name):
|
|
||||||
""" Check if the context contains a given variable."""
|
|
||||||
for layer in self._stack:
|
|
||||||
if name in layer:
|
|
||||||
return True
|
|
||||||
return False
|
|
|
@ -1,32 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja.constants
|
|
||||||
~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Various constants.
|
|
||||||
|
|
||||||
:copyright: 2007 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
#: list of lorem ipsum words used by the lipsum() helper function
|
|
||||||
LOREM_IPSUM_WORDS = u'''\
|
|
||||||
a ac accumsan ad adipiscing aenean aliquam aliquet amet ante aptent arcu at
|
|
||||||
auctor augue bibendum blandit class commodo condimentum congue consectetuer
|
|
||||||
consequat conubia convallis cras cubilia cum curabitur curae cursus dapibus
|
|
||||||
diam dictum dictumst dignissim dis dolor donec dui duis egestas eget eleifend
|
|
||||||
elementum elit enim erat eros est et etiam eu euismod facilisi facilisis fames
|
|
||||||
faucibus felis fermentum feugiat fringilla fusce gravida habitant habitasse hac
|
|
||||||
hendrerit hymenaeos iaculis id imperdiet in inceptos integer interdum ipsum
|
|
||||||
justo lacinia lacus laoreet lectus leo libero ligula litora lobortis lorem
|
|
||||||
luctus maecenas magna magnis malesuada massa mattis mauris metus mi molestie
|
|
||||||
mollis montes morbi mus nam nascetur natoque nec neque netus nibh nisi nisl non
|
|
||||||
nonummy nostra nulla nullam nunc odio orci ornare parturient pede pellentesque
|
|
||||||
penatibus per pharetra phasellus placerat platea porta porttitor posuere
|
|
||||||
potenti praesent pretium primis proin pulvinar purus quam quis quisque rhoncus
|
|
||||||
ridiculus risus rutrum sagittis sapien scelerisque sed sem semper senectus sit
|
|
||||||
sociis sociosqu sodales sollicitudin suscipit suspendisse taciti tellus tempor
|
|
||||||
tempus tincidunt torquent tortor tristique turpis ullamcorper ultrices
|
|
||||||
ultricies urna ut varius vehicula vel velit venenatis vestibulum vitae vivamus
|
|
||||||
viverra volutpat vulputate'''
|
|
|
@ -1,708 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja.datastructure
|
|
||||||
~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Module that helds several data types used in the template engine.
|
|
||||||
|
|
||||||
:copyright: 2007 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
from jinja.exceptions import TemplateSyntaxError, TemplateRuntimeError
|
|
||||||
|
|
||||||
_missing = object()
|
|
||||||
|
|
||||||
|
|
||||||
def contextcallable(f):
|
|
||||||
"""
|
|
||||||
Mark a function context callable.
|
|
||||||
"""
|
|
||||||
f.jinja_context_callable = True
|
|
||||||
return f
|
|
||||||
|
|
||||||
|
|
||||||
def unsafe(f):
|
|
||||||
"""
|
|
||||||
Mark function as unsafe.
|
|
||||||
"""
|
|
||||||
f.jinja_unsafe_call = True
|
|
||||||
return f
|
|
||||||
|
|
||||||
|
|
||||||
def make_undefined(implementation):
|
|
||||||
"""
|
|
||||||
Creates an undefined singleton based on a given implementation.
|
|
||||||
It performs some tests that make sure the undefined type implements
|
|
||||||
everything it should.
|
|
||||||
"""
|
|
||||||
self = object.__new__(implementation)
|
|
||||||
self.__reduce__()
|
|
||||||
return self
|
|
||||||
|
|
||||||
|
|
||||||
class AbstractUndefinedType(object):
|
|
||||||
"""
|
|
||||||
Base class for any undefined type.
|
|
||||||
"""
|
|
||||||
__slots__ = ()
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
raise TypeError('cannot create %r instances' %
|
|
||||||
self.__class__.__name__)
|
|
||||||
|
|
||||||
def __setattr__(self, name, value):
|
|
||||||
raise AttributeError('%r object has no attribute %r' % (
|
|
||||||
self.__class__.__name__,
|
|
||||||
name
|
|
||||||
))
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return self is other
|
|
||||||
|
|
||||||
def __ne__(self, other):
|
|
||||||
return self is not other
|
|
||||||
|
|
||||||
def __copy__(self):
|
|
||||||
return self
|
|
||||||
__deepcopy__ = __copy__
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'Undefined'
|
|
||||||
|
|
||||||
def __reduce__(self):
|
|
||||||
raise TypeError('undefined objects have to provide a __reduce__')
|
|
||||||
|
|
||||||
|
|
||||||
class SilentUndefinedType(AbstractUndefinedType):
|
|
||||||
"""
|
|
||||||
An object that does not exist.
|
|
||||||
"""
|
|
||||||
__slots__ = ()
|
|
||||||
|
|
||||||
def __add__(self, other):
|
|
||||||
"""Any operator returns the operand."""
|
|
||||||
return other
|
|
||||||
__sub__ = __mul__ = __div__ = __rsub__ = __rmul__ = __div__ = __mod__ =\
|
|
||||||
__radd__ = __rmod__ = __add__
|
|
||||||
|
|
||||||
def __getitem__(self, arg):
|
|
||||||
"""Getting any item returns `Undefined`"""
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
"""Iterating over `Undefined` returns an empty iterator."""
|
|
||||||
if False:
|
|
||||||
yield None
|
|
||||||
|
|
||||||
def __getattr__(self, arg):
|
|
||||||
"""Getting any attribute returns `Undefined`"""
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __nonzero__(self):
|
|
||||||
"""`Undefined` is considered boolean `False`"""
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
"""`Undefined` is an empty sequence"""
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
"""The string representation is empty."""
|
|
||||||
return ''
|
|
||||||
|
|
||||||
def __unicode__(self):
|
|
||||||
"""The unicode representation is empty."""
|
|
||||||
return u''
|
|
||||||
|
|
||||||
def __int__(self):
|
|
||||||
"""Converting `Undefined` to an integer ends up in ``0``"""
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def __float__(self):
|
|
||||||
"""Converting `Undefined` to an float ends up in ``0.0``"""
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
|
||||||
"""Calling `Undefined` returns `Undefined`"""
|
|
||||||
return self
|
|
||||||
|
|
||||||
def __reduce__(self):
|
|
||||||
"""Helper for pickle."""
|
|
||||||
return 'SilentUndefined'
|
|
||||||
|
|
||||||
|
|
||||||
class ComplainingUndefinedType(AbstractUndefinedType):
|
|
||||||
"""
|
|
||||||
An object that does not exist.
|
|
||||||
"""
|
|
||||||
__slots__ = ()
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
"""Getting the length raises error."""
|
|
||||||
raise TemplateRuntimeError('Operated on undefined object')
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
"""Iterating over `Undefined` raises an error."""
|
|
||||||
raise TemplateRuntimeError('Iterated over undefined object')
|
|
||||||
|
|
||||||
def __nonzero__(self):
|
|
||||||
"""`Undefined` is considered boolean `False`"""
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
"""The string representation raises an error."""
|
|
||||||
raise TemplateRuntimeError('Undefined object rendered')
|
|
||||||
|
|
||||||
def __unicode__(self):
|
|
||||||
"""The unicode representation raises an error."""
|
|
||||||
self.__str__()
|
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
|
||||||
"""Calling `Undefined` returns `Undefined`"""
|
|
||||||
raise TemplateRuntimeError('Undefined object called')
|
|
||||||
|
|
||||||
def __reduce__(self):
|
|
||||||
"""Helper for pickle."""
|
|
||||||
return 'ComplainingUndefined'
|
|
||||||
|
|
||||||
|
|
||||||
#: the singleton instances for the undefined objects
|
|
||||||
SilentUndefined = make_undefined(SilentUndefinedType)
|
|
||||||
ComplainingUndefined = make_undefined(ComplainingUndefinedType)
|
|
||||||
|
|
||||||
#: jinja 1.0 compatibility
|
|
||||||
Undefined = SilentUndefined
|
|
||||||
UndefinedType = SilentUndefinedType
|
|
||||||
|
|
||||||
|
|
||||||
class FakeTranslator(object):
|
|
||||||
"""
|
|
||||||
Default null translator.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def gettext(self, s):
|
|
||||||
"""
|
|
||||||
Translate a singular string.
|
|
||||||
"""
|
|
||||||
return s
|
|
||||||
|
|
||||||
def ngettext(self, s, p, n):
|
|
||||||
"""
|
|
||||||
Translate a plural string.
|
|
||||||
"""
|
|
||||||
if n == 1:
|
|
||||||
return s
|
|
||||||
return p
|
|
||||||
|
|
||||||
|
|
||||||
class Deferred(object):
|
|
||||||
"""
|
|
||||||
Object marking an deferred value. Deferred objects are
|
|
||||||
objects that are called first access in the context.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, factory):
|
|
||||||
self.factory = factory
|
|
||||||
|
|
||||||
def __call__(self, context, name):
|
|
||||||
return self.factory(context.environment, context, name)
|
|
||||||
|
|
||||||
|
|
||||||
class Markup(unicode):
|
|
||||||
"""
|
|
||||||
Compatibility for Pylons and probably some other frameworks.
|
|
||||||
|
|
||||||
It's only used in Jinja environments with `auto_escape` set
|
|
||||||
to true.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __html__(self):
|
|
||||||
return unicode(self)
|
|
||||||
|
|
||||||
|
|
||||||
class TemplateData(Markup):
|
|
||||||
"""
|
|
||||||
Subclass of unicode to mark objects that are coming from the
|
|
||||||
template. The autoescape filter can use that.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
# import these here because those modules import Deferred and Undefined
|
|
||||||
# from this module.
|
|
||||||
try:
|
|
||||||
# try to use the c implementation of the base context if available
|
|
||||||
from jinja._speedups import BaseContext
|
|
||||||
except ImportError:
|
|
||||||
# if there is no c implementation we go with a native python one
|
|
||||||
from jinja._native import BaseContext
|
|
||||||
|
|
||||||
|
|
||||||
class Context(BaseContext):
|
|
||||||
"""
|
|
||||||
Dict like object containing the variables for the template.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
environment = args[0]
|
|
||||||
if not kwargs and len(args) == 2 and isinstance(args[1], dict):
|
|
||||||
base = args[1]
|
|
||||||
else:
|
|
||||||
base = dict(*args[1:], **kwargs)
|
|
||||||
super(Context, self).__init__(environment.undefined_singleton,
|
|
||||||
environment.globals, base)
|
|
||||||
self._translate_func = None
|
|
||||||
self.cache = {}
|
|
||||||
self.environment = environment
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
"""
|
|
||||||
Convert the context into a dict. This skips the globals.
|
|
||||||
"""
|
|
||||||
result = {}
|
|
||||||
for layer in self.stack[1:]:
|
|
||||||
for key, value in layer.iteritems():
|
|
||||||
if key.startswith('::'):
|
|
||||||
continue
|
|
||||||
result[key] = value
|
|
||||||
return result
|
|
||||||
|
|
||||||
def set_nonlocal(self, name, value):
|
|
||||||
"""
|
|
||||||
Set a value in an outer scope.
|
|
||||||
"""
|
|
||||||
for layer in self.stack[:0:-1]:
|
|
||||||
if name in layer:
|
|
||||||
layer[name] = value
|
|
||||||
return
|
|
||||||
self.initial[name] = value
|
|
||||||
|
|
||||||
def translate_func(self):
|
|
||||||
"""
|
|
||||||
The translation function for this context. It takes
|
|
||||||
4 parameters. The singular string, the optional plural one,
|
|
||||||
The name of the variable in the replacements dict and the
|
|
||||||
replacements dict. This is only used by the i18n system
|
|
||||||
internally the simplified version (just one argument) is
|
|
||||||
available in the template for the user too.
|
|
||||||
"""
|
|
||||||
if self._translate_func is not None:
|
|
||||||
return self._translate_func
|
|
||||||
translator = self.environment.get_translator(self)
|
|
||||||
gettext = translator.gettext
|
|
||||||
ngettext = translator.ngettext
|
|
||||||
def translate(s, p=None, n=None, r=None):
|
|
||||||
if p is None:
|
|
||||||
s = gettext(s)
|
|
||||||
else:
|
|
||||||
s = ngettext(s, p, r[n])
|
|
||||||
# apply replacement substitution only if replacements
|
|
||||||
# are given. This is the case for {% trans %}...{% endtrans %}
|
|
||||||
# but for the "_()" syntax and a trans tag without a body.
|
|
||||||
if r is not None:
|
|
||||||
return s % r
|
|
||||||
return s
|
|
||||||
translate.__doc__ = Context.translate_func.__doc__
|
|
||||||
self._translate_func = translate
|
|
||||||
return translate
|
|
||||||
translate_func = property(translate_func, doc=translate_func.__doc__)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
"""
|
|
||||||
String representation of the context.
|
|
||||||
"""
|
|
||||||
return 'Context(%r)' % self.to_dict()
|
|
||||||
|
|
||||||
def __pretty__(self, p, cycle):
|
|
||||||
if cycle:
|
|
||||||
return p.text('Context({...})')
|
|
||||||
p.begin_group(9, 'Context({')
|
|
||||||
for idx, (key, value) in enumerate(self.to_dict().iteritems()):
|
|
||||||
if idx:
|
|
||||||
p.text(',')
|
|
||||||
p.breakable()
|
|
||||||
p.pretty(key)
|
|
||||||
p.text(': ')
|
|
||||||
p.pretty(value)
|
|
||||||
p.end_group(9, '})')
|
|
||||||
|
|
||||||
|
|
||||||
class LoopContext(object):
|
|
||||||
"""
|
|
||||||
Simple class that provides special loop variables.
|
|
||||||
Used by `Environment.iterate`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
jinja_allowed_attributes = ['index', 'index0', 'length', 'parent',
|
|
||||||
'even', 'odd', 'revindex0', 'revindex',
|
|
||||||
'first', 'last']
|
|
||||||
|
|
||||||
def __init__(self, seq, parent, loop_function):
|
|
||||||
self.loop_function = loop_function
|
|
||||||
self.parent = parent
|
|
||||||
self._stack = []
|
|
||||||
if loop_function is None:
|
|
||||||
self.push(seq)
|
|
||||||
|
|
||||||
def push(self, seq):
|
|
||||||
"""
|
|
||||||
Push a sequence to the loop stack. This is used by the
|
|
||||||
recursive for loop.
|
|
||||||
"""
|
|
||||||
# iteration over None is catched, but we don't catch iteration
|
|
||||||
# over undefined because that behavior is handled in the
|
|
||||||
# undefined singleton
|
|
||||||
if seq is None:
|
|
||||||
seq = ()
|
|
||||||
length = 0
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
length = len(seq)
|
|
||||||
except (AttributeError, TypeError):
|
|
||||||
seq = list(seq)
|
|
||||||
length = len(seq)
|
|
||||||
self._stack.append({
|
|
||||||
'index': -1,
|
|
||||||
'seq': seq,
|
|
||||||
'length': length
|
|
||||||
})
|
|
||||||
return self
|
|
||||||
|
|
||||||
def pop(self):
|
|
||||||
"""Remove the last layer from the loop stack."""
|
|
||||||
return self._stack.pop()
|
|
||||||
|
|
||||||
iterated = property(lambda s: s._stack[-1]['index'] > -1)
|
|
||||||
index0 = property(lambda s: s._stack[-1]['index'])
|
|
||||||
index = property(lambda s: s._stack[-1]['index'] + 1)
|
|
||||||
revindex0 = property(lambda s: s._stack[-1]['length'] -
|
|
||||||
s._stack[-1]['index'] - 1)
|
|
||||||
revindex = property(lambda s: s._stack[-1]['length'] -
|
|
||||||
s._stack[-1]['index'])
|
|
||||||
length = property(lambda s: s._stack[-1]['length'])
|
|
||||||
even = property(lambda s: s._stack[-1]['index'] % 2 == 1)
|
|
||||||
odd = property(lambda s: s._stack[-1]['index'] % 2 == 0)
|
|
||||||
first = property(lambda s: s._stack[-1]['index'] == 0)
|
|
||||||
last = property(lambda s: s._stack[-1]['index'] ==
|
|
||||||
s._stack[-1]['length'] - 1)
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
s = self._stack[-1]
|
|
||||||
for idx, item in enumerate(s['seq']):
|
|
||||||
s['index'] = idx
|
|
||||||
yield item
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return self._stack[-1]['length']
|
|
||||||
|
|
||||||
def __call__(self, seq):
|
|
||||||
if self.loop_function is not None:
|
|
||||||
return self.loop_function(seq)
|
|
||||||
raise TemplateRuntimeError('In order to make loops callable you have '
|
|
||||||
'to define them with the "recursive" '
|
|
||||||
'modifier.')
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
if self._stack:
|
|
||||||
return '<LoopContext %d/%d%s>' % (
|
|
||||||
self.index,
|
|
||||||
self.length,
|
|
||||||
self.loop_function is not None and ' recursive' or ''
|
|
||||||
)
|
|
||||||
return '<LoopContext (empty)>'
|
|
||||||
|
|
||||||
|
|
||||||
class CycleContext(object):
|
|
||||||
"""
|
|
||||||
Helper class used for cycling.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, seq=None):
|
|
||||||
self.pos = -1
|
|
||||||
# bind the correct helper function based on the constructor signature
|
|
||||||
if seq is not None:
|
|
||||||
self.seq = seq
|
|
||||||
self.length = len(seq)
|
|
||||||
self.cycle = self.cycle_static
|
|
||||||
else:
|
|
||||||
self.cycle = self.cycle_dynamic
|
|
||||||
|
|
||||||
def cycle_static(self):
|
|
||||||
"""Helper function for static cycling."""
|
|
||||||
self.pos = (self.pos + 1) % self.length
|
|
||||||
return self.seq[self.pos]
|
|
||||||
|
|
||||||
def cycle_dynamic(self, seq):
|
|
||||||
"""Helper function for dynamic cycling."""
|
|
||||||
self.pos = pos = (self.pos + 1) % len(seq)
|
|
||||||
return seq[pos]
|
|
||||||
|
|
||||||
|
|
||||||
class SuperBlock(object):
|
|
||||||
"""
|
|
||||||
Helper class for ``{{ super() }}``.
|
|
||||||
"""
|
|
||||||
jinja_allowed_attributes = ['name']
|
|
||||||
|
|
||||||
def __init__(self, name, blocks, level, context):
|
|
||||||
self.name = name
|
|
||||||
self.context = context
|
|
||||||
if name in blocks:
|
|
||||||
self.stack = blocks[name]
|
|
||||||
self.level = level
|
|
||||||
else:
|
|
||||||
self.stack = None
|
|
||||||
|
|
||||||
def __call__(self, offset=1):
|
|
||||||
if self.stack is not None:
|
|
||||||
level = self.level + (offset - 1)
|
|
||||||
if level < len(self.stack):
|
|
||||||
return self.stack[level](self.context)
|
|
||||||
raise TemplateRuntimeError('no super block for %r' % self.name)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<SuperBlock %r>' % self.name
|
|
||||||
|
|
||||||
|
|
||||||
class StateTest(object):
|
|
||||||
"""
|
|
||||||
Wrapper class for basic lambdas in order to simplify
|
|
||||||
debugging in the parser. It also provides static helper
|
|
||||||
functions that replace some lambda expressions
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, func, msg):
|
|
||||||
self.func = func
|
|
||||||
self.msg = msg
|
|
||||||
|
|
||||||
def __call__(self, token):
|
|
||||||
return self.func(token)
|
|
||||||
|
|
||||||
def expect_token(*types, **kw):
|
|
||||||
"""Scans until one of the given tokens is found."""
|
|
||||||
msg = kw.pop('msg', None)
|
|
||||||
if kw:
|
|
||||||
raise TypeError('unexpected keyword argument %r' % iter(kw).next())
|
|
||||||
if len(types) == 1:
|
|
||||||
if msg is None:
|
|
||||||
msg = "expected '%s'" % types[0]
|
|
||||||
return StateTest(lambda t: t.type == types[0], msg)
|
|
||||||
if msg is None:
|
|
||||||
msg = 'expected one of %s' % ', '.join(["'%s'" % type
|
|
||||||
for type in types])
|
|
||||||
return StateTest(lambda t: t.type in types, msg)
|
|
||||||
expect_token = staticmethod(expect_token)
|
|
||||||
|
|
||||||
|
|
||||||
class Token(object):
|
|
||||||
"""
|
|
||||||
Token class.
|
|
||||||
"""
|
|
||||||
__slots__ = ('lineno', 'type', 'value')
|
|
||||||
|
|
||||||
def __init__(self, lineno, type, value):
|
|
||||||
self.lineno = lineno
|
|
||||||
self.type = intern(str(type))
|
|
||||||
self.value = value
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
from jinja.lexer import keywords, reverse_operators
|
|
||||||
if self.type in keywords:
|
|
||||||
return self.type
|
|
||||||
elif self.type in reverse_operators:
|
|
||||||
return reverse_operators[self.type]
|
|
||||||
return self.value
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'Token(%r, %r, %r)' % (
|
|
||||||
self.lineno,
|
|
||||||
self.type,
|
|
||||||
self.value
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TokenStreamIterator(object):
|
|
||||||
"""
|
|
||||||
The iterator for tokenstreams. Iterate over the stream
|
|
||||||
until the eof token is reached.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, stream):
|
|
||||||
self._stream = stream
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def next(self):
|
|
||||||
token = self._stream.current
|
|
||||||
if token.type == 'eof':
|
|
||||||
self._stream.close()
|
|
||||||
raise StopIteration()
|
|
||||||
self._stream.next()
|
|
||||||
return token
|
|
||||||
|
|
||||||
|
|
||||||
class TokenStream(object):
|
|
||||||
"""
|
|
||||||
A token stream wraps a generator and supports pushing tokens back.
|
|
||||||
It also provides some functions to expect tokens and similar stuff.
|
|
||||||
|
|
||||||
Important note: Do never push more than one token back to the
|
|
||||||
stream. Although the stream object won't stop you
|
|
||||||
from doing so, the behavior is undefined. Multiple
|
|
||||||
pushed tokens are only used internally!
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, generator, filename):
|
|
||||||
self._next = generator.next
|
|
||||||
self._pushed = []
|
|
||||||
self.current = Token(1, 'initial', '')
|
|
||||||
self.filename = filename
|
|
||||||
self.next()
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return TokenStreamIterator(self)
|
|
||||||
|
|
||||||
def bound(self):
|
|
||||||
"""Return True if the token stream is bound to a parser."""
|
|
||||||
return self.parser is not None
|
|
||||||
bound = property(bound, doc=bound.__doc__)
|
|
||||||
|
|
||||||
def lineno(self):
|
|
||||||
"""The current line number."""
|
|
||||||
return self.current.lineno
|
|
||||||
lineno = property(lineno, doc=lineno.__doc__)
|
|
||||||
|
|
||||||
def __nonzero__(self):
|
|
||||||
"""Are we at the end of the tokenstream?"""
|
|
||||||
return bool(self._pushed) or self.current.type != 'eof'
|
|
||||||
|
|
||||||
eos = property(lambda x: not x.__nonzero__(), doc=__nonzero__.__doc__)
|
|
||||||
|
|
||||||
def look(self):
|
|
||||||
"""See what's the next token."""
|
|
||||||
if self._pushed:
|
|
||||||
return self._pushed[-1]
|
|
||||||
old_token = self.current
|
|
||||||
self.next()
|
|
||||||
new_token = self.current
|
|
||||||
self.current = old_token
|
|
||||||
self.push(new_token)
|
|
||||||
return new_token
|
|
||||||
|
|
||||||
def push(self, token):
|
|
||||||
"""Push a token back to the stream."""
|
|
||||||
self._pushed.append(token)
|
|
||||||
|
|
||||||
def skip(self, n):
|
|
||||||
"""Got n tokens ahead."""
|
|
||||||
for x in xrange(n):
|
|
||||||
self.next()
|
|
||||||
|
|
||||||
def shift(self, token):
|
|
||||||
"""
|
|
||||||
Push one token into the stream.
|
|
||||||
"""
|
|
||||||
old_current = self.current
|
|
||||||
self.next()
|
|
||||||
self.push(self.current)
|
|
||||||
self.push(old_current)
|
|
||||||
self.push(token)
|
|
||||||
self.next()
|
|
||||||
|
|
||||||
def next(self):
|
|
||||||
"""Go one token ahead."""
|
|
||||||
if self._pushed:
|
|
||||||
self.current = self._pushed.pop()
|
|
||||||
elif self.current.type != 'eof':
|
|
||||||
try:
|
|
||||||
self.current = self._next()
|
|
||||||
except StopIteration:
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
def read_whitespace(self):
|
|
||||||
"""Read all the whitespace, up to the next tag."""
|
|
||||||
lineno = self.current.lineno
|
|
||||||
buf = []
|
|
||||||
while self.current.type == 'data' and not \
|
|
||||||
self.current.value.strip():
|
|
||||||
buf.append(self.current.value)
|
|
||||||
self.next()
|
|
||||||
if buf:
|
|
||||||
return Token(lineno, 'data', u''.join(buf))
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
"""Close the stream."""
|
|
||||||
self.current = Token(self.current.lineno, 'eof', '')
|
|
||||||
self._next = None
|
|
||||||
|
|
||||||
def expect(self, token_type, token_value=_missing):
|
|
||||||
"""Expect a given token type and return it"""
|
|
||||||
if self.current.type != token_type:
|
|
||||||
raise TemplateSyntaxError("expected token %r, got %r" %
|
|
||||||
(token_type, self.current.type),
|
|
||||||
self.current.lineno,
|
|
||||||
self.filename)
|
|
||||||
elif token_value is not _missing and \
|
|
||||||
self.current.value != token_value:
|
|
||||||
raise TemplateSyntaxError("expected %r, got %r" %
|
|
||||||
(token_value, self.current.value),
|
|
||||||
self.current.lineno,
|
|
||||||
self.filename)
|
|
||||||
try:
|
|
||||||
return self.current
|
|
||||||
finally:
|
|
||||||
self.next()
|
|
||||||
|
|
||||||
|
|
||||||
class TemplateStream(object):
|
|
||||||
"""
|
|
||||||
Wraps a genererator for outputing template streams.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, gen):
|
|
||||||
self._gen = gen
|
|
||||||
self._next = gen.next
|
|
||||||
self.buffered = False
|
|
||||||
|
|
||||||
def disable_buffering(self):
|
|
||||||
"""
|
|
||||||
Disable the output buffering.
|
|
||||||
"""
|
|
||||||
self._next = self._gen.next
|
|
||||||
self.buffered = False
|
|
||||||
|
|
||||||
def enable_buffering(self, size=5):
|
|
||||||
"""
|
|
||||||
Enable buffering. Buffer `size` items before
|
|
||||||
yielding them.
|
|
||||||
"""
|
|
||||||
if size <= 1:
|
|
||||||
raise ValueError('buffer size too small')
|
|
||||||
self.buffered = True
|
|
||||||
|
|
||||||
def buffering_next():
|
|
||||||
buf = []
|
|
||||||
c_size = 0
|
|
||||||
push = buf.append
|
|
||||||
next = self._gen.next
|
|
||||||
|
|
||||||
try:
|
|
||||||
while True:
|
|
||||||
item = next()
|
|
||||||
if item:
|
|
||||||
push(item)
|
|
||||||
c_size += 1
|
|
||||||
if c_size >= size:
|
|
||||||
raise StopIteration()
|
|
||||||
except StopIteration:
|
|
||||||
if not c_size:
|
|
||||||
raise
|
|
||||||
return u''.join(buf)
|
|
||||||
|
|
||||||
self._next = buffering_next
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def next(self):
|
|
||||||
return self._next()
|
|
|
@ -1,213 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja.debugger
|
|
||||||
~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
This module implements helper function Jinja uses to give the users a
|
|
||||||
possibility to develop Jinja templates like they would debug python code.
|
|
||||||
It seamlessly integreates into the python traceback system, in fact it
|
|
||||||
just modifies the trackback stack so that the line numbers are correct
|
|
||||||
and the frame information are bound to the context and not the frame of
|
|
||||||
the template evaluation loop.
|
|
||||||
|
|
||||||
To achive this it raises the exception it cought before in an isolated
|
|
||||||
namespace at a given line. The locals namespace is set to the current
|
|
||||||
template context.
|
|
||||||
|
|
||||||
The traceback generated by raising that exception is then either returned
|
|
||||||
or linked with the former traceback if the `jinja._debugger` module is
|
|
||||||
available. Because it's not possible to modify traceback objects from the
|
|
||||||
python space this module is needed for this process.
|
|
||||||
|
|
||||||
If it's not available it just ignores the other frames. Because this can
|
|
||||||
lead to actually harder to debug code there is a setting on the jinja
|
|
||||||
environment to disable the debugging system.
|
|
||||||
|
|
||||||
The isolated namespace which is used to raise the exception also contains
|
|
||||||
a `__loader__` name that helds a reference to a PEP 302 compatible loader.
|
|
||||||
Because there are currently some traceback systems (such as the paste
|
|
||||||
evalexception debugger) that do not provide the frame globals when
|
|
||||||
retrieving the source from the linecache module, Jinja injects the source
|
|
||||||
to the linecache module itself and changes the filename to a URL style
|
|
||||||
"virtual filename" so that Jinja doesn't acidentally override other files
|
|
||||||
in the linecache.
|
|
||||||
|
|
||||||
:copyright: 2007 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
from random import randrange
|
|
||||||
|
|
||||||
# if we have extended debugger support we should really use it
|
|
||||||
try:
|
|
||||||
from jinja._debugger import *
|
|
||||||
has_extended_debugger = True
|
|
||||||
except ImportError:
|
|
||||||
has_extended_debugger = False
|
|
||||||
|
|
||||||
# we need the RUNTIME_EXCEPTION_OFFSET to skip the not used frames
|
|
||||||
from jinja.utils import reversed, RUNTIME_EXCEPTION_OFFSET
|
|
||||||
|
|
||||||
|
|
||||||
def fake_template_exception(exc_type, exc_value, tb, filename, lineno,
|
|
||||||
source, context_or_env, tb_back=None):
|
|
||||||
"""
|
|
||||||
Raise an exception "in a template". Return a traceback
|
|
||||||
object. This is used for runtime debugging, not compile time.
|
|
||||||
"""
|
|
||||||
# some traceback systems allow to skip frames
|
|
||||||
__traceback_hide__ = True
|
|
||||||
|
|
||||||
# create the namespace which will be the local namespace
|
|
||||||
# of the new frame then. Some debuggers show local variables
|
|
||||||
# so we better inject the context and not the evaluation loop context.
|
|
||||||
from jinja.datastructure import Context
|
|
||||||
if isinstance(context_or_env, Context):
|
|
||||||
env = context_or_env.environment
|
|
||||||
namespace = context_or_env.to_dict()
|
|
||||||
else:
|
|
||||||
env = context_or_env
|
|
||||||
namespace = {}
|
|
||||||
|
|
||||||
# no unicode for filenames
|
|
||||||
if isinstance(filename, unicode):
|
|
||||||
filename = filename.encode('utf-8')
|
|
||||||
|
|
||||||
# generate an jinja unique filename used so that linecache
|
|
||||||
# gets data that doesn't interfere with other modules
|
|
||||||
if filename is None:
|
|
||||||
vfilename = 'jinja://~%d' % randrange(0, 10000)
|
|
||||||
filename = '<string>'
|
|
||||||
else:
|
|
||||||
vfilename = 'jinja://%s' % filename
|
|
||||||
|
|
||||||
# now create the used loaded and update the linecache
|
|
||||||
loader = TracebackLoader(env, source, filename)
|
|
||||||
loader.update_linecache(vfilename)
|
|
||||||
globals = {
|
|
||||||
'__name__': vfilename,
|
|
||||||
'__file__': vfilename,
|
|
||||||
'__loader__': loader
|
|
||||||
}
|
|
||||||
|
|
||||||
# use the simple debugger to reraise the exception in the
|
|
||||||
# line where the error originally occoured
|
|
||||||
globals['__exception_to_raise__'] = (exc_type, exc_value)
|
|
||||||
offset = '\n' * (lineno - 1)
|
|
||||||
code = compile(offset + 'raise __exception_to_raise__[0], '
|
|
||||||
'__exception_to_raise__[1]',
|
|
||||||
vfilename or '<template>', 'exec')
|
|
||||||
try:
|
|
||||||
exec code in globals, namespace
|
|
||||||
except:
|
|
||||||
exc_info = sys.exc_info()
|
|
||||||
|
|
||||||
# if we have an extended debugger we set the tb_next flag so that
|
|
||||||
# we don't loose the higher stack items.
|
|
||||||
if has_extended_debugger:
|
|
||||||
if tb_back is not None:
|
|
||||||
tb_set_next(tb_back, exc_info[2])
|
|
||||||
if tb is not None:
|
|
||||||
tb_set_next(exc_info[2].tb_next, tb.tb_next)
|
|
||||||
|
|
||||||
# otherwise just return the exc_info from the simple debugger
|
|
||||||
return exc_info
|
|
||||||
|
|
||||||
|
|
||||||
def translate_exception(template, context, exc_type, exc_value, tb):
|
|
||||||
"""
|
|
||||||
Translate an exception and return the new traceback.
|
|
||||||
"""
|
|
||||||
# depending on the python version we have to skip some frames to
|
|
||||||
# step to get the frame of the current template. The frames before
|
|
||||||
# are the toolchain used to render that thing.
|
|
||||||
for x in xrange(RUNTIME_EXCEPTION_OFFSET):
|
|
||||||
tb = tb.tb_next
|
|
||||||
|
|
||||||
result_tb = prev_tb = None
|
|
||||||
initial_tb = tb
|
|
||||||
|
|
||||||
# translate all the jinja frames in this traceback
|
|
||||||
while tb is not None:
|
|
||||||
if tb.tb_frame.f_globals.get('__jinja_template__'):
|
|
||||||
debug_info = tb.tb_frame.f_globals['debug_info']
|
|
||||||
|
|
||||||
# the next thing we do is matching the current error line against the
|
|
||||||
# debugging table to get the correct source line. If we can't find the
|
|
||||||
# filename and line number we return the traceback object unaltered.
|
|
||||||
error_line = tb.tb_lineno
|
|
||||||
for code_line, tmpl_filename, tmpl_line in reversed(debug_info):
|
|
||||||
if code_line <= error_line:
|
|
||||||
source = tb.tb_frame.f_globals['template_source']
|
|
||||||
tb = fake_template_exception(exc_type, exc_value, tb,
|
|
||||||
tmpl_filename, tmpl_line,
|
|
||||||
source, context, prev_tb)[-1]
|
|
||||||
break
|
|
||||||
if result_tb is None:
|
|
||||||
result_tb = tb
|
|
||||||
prev_tb = tb
|
|
||||||
tb = tb.tb_next
|
|
||||||
|
|
||||||
# under some conditions we cannot translate any frame. in that
|
|
||||||
# situation just return the original traceback.
|
|
||||||
return (exc_type, exc_value, result_tb or intial_tb)
|
|
||||||
|
|
||||||
|
|
||||||
def raise_syntax_error(exception, env, source=None):
|
|
||||||
"""
|
|
||||||
This method raises an exception that includes more debugging
|
|
||||||
informations so that debugging works better. Unlike
|
|
||||||
`translate_exception` this method raises the exception with
|
|
||||||
the traceback.
|
|
||||||
"""
|
|
||||||
exc_info = fake_template_exception(exception, None, None,
|
|
||||||
exception.filename,
|
|
||||||
exception.lineno, source, env)
|
|
||||||
raise exc_info[0], exc_info[1], exc_info[2]
|
|
||||||
|
|
||||||
|
|
||||||
class TracebackLoader(object):
|
|
||||||
"""
|
|
||||||
Fake importer that just returns the source of a template. It's just used
|
|
||||||
by Jinja interally and you shouldn't use it on your own.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, environment, source, filename):
|
|
||||||
self.loader = environment.loader
|
|
||||||
self.source = source
|
|
||||||
self.filename = filename
|
|
||||||
|
|
||||||
def update_linecache(self, virtual_filename):
|
|
||||||
"""
|
|
||||||
Hacky way to let traceback systems know about the
|
|
||||||
Jinja template sourcecode. Very hackish indeed.
|
|
||||||
"""
|
|
||||||
# check for linecache, not every implementation of python
|
|
||||||
# might have such an module (this check is pretty senseless
|
|
||||||
# because we depend on cpython anway)
|
|
||||||
try:
|
|
||||||
from linecache import cache
|
|
||||||
except ImportError:
|
|
||||||
return
|
|
||||||
data = self.get_source(None)
|
|
||||||
cache[virtual_filename] = (
|
|
||||||
len(data),
|
|
||||||
None,
|
|
||||||
data.splitlines(True),
|
|
||||||
virtual_filename
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_source(self, impname):
|
|
||||||
"""Return the source as bytestring."""
|
|
||||||
source = ''
|
|
||||||
if self.source is not None:
|
|
||||||
source = self.source
|
|
||||||
elif self.loader is not None:
|
|
||||||
try:
|
|
||||||
source = self.loader.get_source(self.filename)
|
|
||||||
except TemplateNotFound:
|
|
||||||
pass
|
|
||||||
if isinstance(source, unicode):
|
|
||||||
source = source.encode('utf-8')
|
|
||||||
return source
|
|
|
@ -1,16 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja.defaults
|
|
||||||
~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Jinja default filters and tags.
|
|
||||||
|
|
||||||
:copyright: 2007 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
from jinja.filters import FILTERS as DEFAULT_FILTERS
|
|
||||||
from jinja.tests import TESTS as DEFAULT_TESTS
|
|
||||||
from jinja.utils import NAMESPACE as DEFAULT_NAMESPACE
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['DEFAULT_FILTERS', 'DEFAULT_TESTS', 'DEFAULT_NAMESPACE']
|
|
|
@ -1,395 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja.environment
|
|
||||||
~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Provides a class that holds runtime and parsing time options.
|
|
||||||
|
|
||||||
:copyright: 2007 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
from jinja.lexer import Lexer
|
|
||||||
from jinja.parser import Parser
|
|
||||||
from jinja.loaders import LoaderWrapper
|
|
||||||
from jinja.datastructure import SilentUndefined, Markup, Context, FakeTranslator
|
|
||||||
from jinja.utils import collect_translations, get_attribute
|
|
||||||
from jinja.exceptions import FilterNotFound, TestNotFound, \
|
|
||||||
SecurityException, TemplateSyntaxError
|
|
||||||
from jinja.defaults import DEFAULT_FILTERS, DEFAULT_TESTS, DEFAULT_NAMESPACE
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['Environment']
|
|
||||||
|
|
||||||
|
|
||||||
#: minor speedup
|
|
||||||
_getattr = getattr
|
|
||||||
|
|
||||||
|
|
||||||
class Environment(object):
|
|
||||||
"""
|
|
||||||
The Jinja environment.
|
|
||||||
|
|
||||||
The core component of Jinja is the `Environment`. It contains
|
|
||||||
important shared variables like configuration, filters, tests,
|
|
||||||
globals and others.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self,
|
|
||||||
block_start_string='{%',
|
|
||||||
block_end_string='%}',
|
|
||||||
variable_start_string='{{',
|
|
||||||
variable_end_string='}}',
|
|
||||||
comment_start_string='{#',
|
|
||||||
comment_end_string='#}',
|
|
||||||
trim_blocks=False,
|
|
||||||
auto_escape=False,
|
|
||||||
default_filters=None,
|
|
||||||
template_charset='utf-8',
|
|
||||||
charset='utf-8',
|
|
||||||
namespace=None,
|
|
||||||
loader=None,
|
|
||||||
filters=None,
|
|
||||||
tests=None,
|
|
||||||
context_class=Context,
|
|
||||||
undefined_singleton=SilentUndefined,
|
|
||||||
disable_regexps=False,
|
|
||||||
friendly_traceback=True,
|
|
||||||
translator_factory=None):
|
|
||||||
"""
|
|
||||||
Here the possible initialization parameters:
|
|
||||||
|
|
||||||
========================= ============================================
|
|
||||||
`block_start_string` * the string marking the begin of a block.
|
|
||||||
this defaults to ``'{%'``.
|
|
||||||
`block_end_string` * the string marking the end of a block.
|
|
||||||
defaults to ``'%}'``.
|
|
||||||
`variable_start_string` * the string marking the begin of a print
|
|
||||||
statement. defaults to ``'{{'``.
|
|
||||||
`comment_start_string` * the string marking the begin of a
|
|
||||||
comment. defaults to ``'{#'``.
|
|
||||||
`comment_end_string` * the string marking the end of a comment.
|
|
||||||
defaults to ``'#}'``.
|
|
||||||
`trim_blocks` * If this is set to ``True`` the first newline
|
|
||||||
after a block is removed (block, not
|
|
||||||
variable tag!). Defaults to ``False``.
|
|
||||||
`auto_escape` If this is set to ``True`` Jinja will
|
|
||||||
automatically escape all variables using xml
|
|
||||||
escaping methods. If you don't want to
|
|
||||||
escape a string you have to wrap it in a
|
|
||||||
``Markup`` object from the
|
|
||||||
``jinja.datastructure`` module. If
|
|
||||||
`auto_escape` is ``True`` there will be also
|
|
||||||
a ``Markup`` object in the template
|
|
||||||
namespace to define partial html fragments.
|
|
||||||
Note that we do not recommend this feature.
|
|
||||||
`default_filters` list of tuples in the form (``filter_name``,
|
|
||||||
``arguments``) where ``filter_name`` is the
|
|
||||||
name of a registered filter and
|
|
||||||
``arguments`` a tuple with the filter
|
|
||||||
arguments. The filters specified here will
|
|
||||||
always be applied when printing data to the
|
|
||||||
template. *new in Jinja 1.1*
|
|
||||||
`template_charset` The charset of the templates. Defaults
|
|
||||||
to ``'utf-8'``.
|
|
||||||
`charset` Charset of all string input data. Defaults
|
|
||||||
to ``'utf-8'``.
|
|
||||||
`namespace` Global namespace for all templates.
|
|
||||||
`loader` Specify a template loader.
|
|
||||||
`filters` dict of filters or the default filters if
|
|
||||||
not defined.
|
|
||||||
`tests` dict of tests of the default tests if not
|
|
||||||
defined.
|
|
||||||
`context_class` the context class this template should use.
|
|
||||||
See the `Context` documentation for more
|
|
||||||
details.
|
|
||||||
`undefined_singleton` The singleton value that is used for missing
|
|
||||||
variables. *new in Jinja 1.1*
|
|
||||||
`disable_regexps` Disable support for regular expresssions.
|
|
||||||
`friendly_traceback` Set this to `False` to disable the developer
|
|
||||||
friendly traceback rewriting. Whenever an
|
|
||||||
runtime or syntax error occours jinja will
|
|
||||||
try to make a developer friendly traceback
|
|
||||||
that shows the error in the template line.
|
|
||||||
This however can be annoying when debugging
|
|
||||||
broken functions that are called from the
|
|
||||||
template. *new in Jinja 1.1*
|
|
||||||
`translator_factory` A callback function that is called with
|
|
||||||
the context as first argument to get the
|
|
||||||
translator for the current instance.
|
|
||||||
*new in Jinja 1.2*
|
|
||||||
========================= ============================================
|
|
||||||
|
|
||||||
All of these variables except those marked with a star (*) are
|
|
||||||
modifiable after environment initialization.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# lexer / parser information
|
|
||||||
self.block_start_string = block_start_string
|
|
||||||
self.block_end_string = block_end_string
|
|
||||||
self.variable_start_string = variable_start_string
|
|
||||||
self.variable_end_string = variable_end_string
|
|
||||||
self.comment_start_string = comment_start_string
|
|
||||||
self.comment_end_string = comment_end_string
|
|
||||||
self.trim_blocks = trim_blocks
|
|
||||||
|
|
||||||
# other stuff
|
|
||||||
self.template_charset = template_charset
|
|
||||||
self.charset = charset
|
|
||||||
self.loader = loader
|
|
||||||
if filters is None:
|
|
||||||
filters = DEFAULT_FILTERS.copy()
|
|
||||||
self.filters = filters
|
|
||||||
if tests is None:
|
|
||||||
tests = DEFAULT_TESTS.copy()
|
|
||||||
self.tests = tests
|
|
||||||
self.default_filters = default_filters or []
|
|
||||||
self.context_class = context_class
|
|
||||||
self.undefined_singleton = undefined_singleton
|
|
||||||
self.disable_regexps = disable_regexps
|
|
||||||
self.friendly_traceback = friendly_traceback
|
|
||||||
|
|
||||||
# global namespace
|
|
||||||
if namespace is None:
|
|
||||||
namespace = DEFAULT_NAMESPACE.copy()
|
|
||||||
self.globals = namespace
|
|
||||||
|
|
||||||
# jinja 1.0 compatibility
|
|
||||||
if auto_escape:
|
|
||||||
self.default_filters.append(('escape', (True,)))
|
|
||||||
self.globals['Markup'] = Markup
|
|
||||||
|
|
||||||
# and here the translator factory
|
|
||||||
self.translator_factory = translator_factory
|
|
||||||
|
|
||||||
# create lexer
|
|
||||||
self.lexer = Lexer(self)
|
|
||||||
|
|
||||||
def loader(self, value):
|
|
||||||
"""
|
|
||||||
Get or set the template loader.
|
|
||||||
"""
|
|
||||||
self._loader = LoaderWrapper(self, value)
|
|
||||||
loader = property(lambda s: s._loader, loader, doc=loader.__doc__)
|
|
||||||
|
|
||||||
def parse(self, source, filename=None):
|
|
||||||
"""
|
|
||||||
Parse the sourcecode and return the abstract syntax tree. This tree
|
|
||||||
of nodes is used by the `translators`_ to convert the template into
|
|
||||||
executable source- or bytecode.
|
|
||||||
|
|
||||||
.. _translators: translators.txt
|
|
||||||
"""
|
|
||||||
parser = Parser(self, source, filename)
|
|
||||||
return parser.parse()
|
|
||||||
|
|
||||||
def lex(self, source, filename=None):
|
|
||||||
"""
|
|
||||||
Lex the given sourcecode and return a generator that yields tokens.
|
|
||||||
The stream returned is not usable for Jinja but can be used if
|
|
||||||
Jinja templates should be processed by other tools (for example
|
|
||||||
syntax highlighting etc)
|
|
||||||
|
|
||||||
The tuples are returned in the form ``(lineno, token, value)``.
|
|
||||||
"""
|
|
||||||
return self.lexer.tokeniter(source, filename)
|
|
||||||
|
|
||||||
def from_string(self, source):
|
|
||||||
"""
|
|
||||||
Load and parse a template source and translate it into eval-able
|
|
||||||
Python code. This code is wrapped within a `Template` class that
|
|
||||||
allows you to render it.
|
|
||||||
"""
|
|
||||||
from jinja.translators.python import PythonTranslator
|
|
||||||
try:
|
|
||||||
rv = PythonTranslator.process(self, Parser(self, source).parse(),
|
|
||||||
source)
|
|
||||||
except TemplateSyntaxError, e:
|
|
||||||
# on syntax errors rewrite the traceback if wanted
|
|
||||||
if not self.friendly_traceback:
|
|
||||||
raise
|
|
||||||
from jinja.debugger import raise_syntax_error
|
|
||||||
if __debug__:
|
|
||||||
__traceback_hide__ = True
|
|
||||||
raise_syntax_error(e, self, source)
|
|
||||||
else:
|
|
||||||
return rv
|
|
||||||
|
|
||||||
def get_template(self, filename):
|
|
||||||
"""
|
|
||||||
Load a template from a loader. If the template does not exist, you
|
|
||||||
will get a `TemplateNotFound` exception.
|
|
||||||
"""
|
|
||||||
return self._loader.load(filename)
|
|
||||||
|
|
||||||
def to_unicode(self, value):
|
|
||||||
"""
|
|
||||||
Convert a value to unicode with the rules defined on the environment.
|
|
||||||
"""
|
|
||||||
# undefined and None expand to ""
|
|
||||||
if value in (None, self.undefined_singleton):
|
|
||||||
return u''
|
|
||||||
# things that are already unicode can pass. As long as nobody
|
|
||||||
# does ugly things with the class it works for jinja too
|
|
||||||
elif isinstance(value, unicode):
|
|
||||||
return value
|
|
||||||
# otherwise try to use __unicode__ or decode __str__
|
|
||||||
try:
|
|
||||||
return unicode(value)
|
|
||||||
except UnicodeError:
|
|
||||||
return str(value).decode(self.charset, 'ignore')
|
|
||||||
|
|
||||||
def get_translator(self, context):
|
|
||||||
"""
|
|
||||||
Return the translator for i18n.
|
|
||||||
|
|
||||||
A translator is an object that provides the two functions
|
|
||||||
``gettext(string)`` and ``ngettext(singular, plural, n)``. Note
|
|
||||||
that both of them have to return unicode!
|
|
||||||
"""
|
|
||||||
if self.translator_factory is not None:
|
|
||||||
return self.translator_factory(context)
|
|
||||||
return FakeTranslator()
|
|
||||||
|
|
||||||
def get_translations(self, name):
|
|
||||||
"""
|
|
||||||
Load template `name` and return all translatable strings (note that
|
|
||||||
that it really just returns the strings form this template, not from
|
|
||||||
the parent or any included templates!)
|
|
||||||
"""
|
|
||||||
return collect_translations(self.loader.parse(name))
|
|
||||||
|
|
||||||
def get_translations_for_string(self, string):
|
|
||||||
"""
|
|
||||||
Like `get_translations`, but the translations are loaded from a
|
|
||||||
normal string that represents the template.
|
|
||||||
"""
|
|
||||||
return collect_translations(self.parse(string))
|
|
||||||
|
|
||||||
def apply_filters(self, value, context, filters):
|
|
||||||
"""
|
|
||||||
Apply a list of filters on the variable.
|
|
||||||
"""
|
|
||||||
# some traceback systems allow to skip frames. but allow
|
|
||||||
# disabling that via -O to not make things slow
|
|
||||||
if __debug__:
|
|
||||||
__traceback_hide__ = True
|
|
||||||
|
|
||||||
cache = context.cache
|
|
||||||
for key in filters:
|
|
||||||
if key in cache:
|
|
||||||
func = cache[key]
|
|
||||||
else:
|
|
||||||
filtername, args = key
|
|
||||||
if filtername not in self.filters:
|
|
||||||
raise FilterNotFound(filtername)
|
|
||||||
cache[key] = func = self.filters[filtername](*args)
|
|
||||||
value = func(self, context, value)
|
|
||||||
return value
|
|
||||||
|
|
||||||
def perform_test(self, context, testname, args, value):
|
|
||||||
"""
|
|
||||||
Perform a test on a variable.
|
|
||||||
"""
|
|
||||||
# some traceback systems allow to skip frames. but allow
|
|
||||||
# disabling that via -O to not make things slow
|
|
||||||
if __debug__:
|
|
||||||
__traceback_hide__ = True
|
|
||||||
|
|
||||||
key = (testname, args)
|
|
||||||
if key in context.cache:
|
|
||||||
func = context.cache[key]
|
|
||||||
else:
|
|
||||||
if testname not in self.tests:
|
|
||||||
raise TestNotFound(testname)
|
|
||||||
context.cache[key] = func = self.tests[testname](*args)
|
|
||||||
return not not func(self, context, value)
|
|
||||||
|
|
||||||
def get_attribute(self, obj, name):
|
|
||||||
"""
|
|
||||||
Get one attribute from an object.
|
|
||||||
"""
|
|
||||||
# some traceback systems allow to skip frames. but allow
|
|
||||||
# disabling that via -O to not make things slow
|
|
||||||
if __debug__:
|
|
||||||
__traceback_hide__ = True
|
|
||||||
|
|
||||||
try:
|
|
||||||
return obj[name]
|
|
||||||
except (TypeError, KeyError, IndexError, AttributeError):
|
|
||||||
try:
|
|
||||||
return get_attribute(obj, name)
|
|
||||||
except (AttributeError, SecurityException):
|
|
||||||
pass
|
|
||||||
if obj is self.undefined_singleton:
|
|
||||||
return _getattr(obj, name)
|
|
||||||
return self.undefined_singleton
|
|
||||||
|
|
||||||
def get_attributes(self, obj, attributes):
|
|
||||||
"""
|
|
||||||
Get some attributes from an object. If attributes is an
|
|
||||||
empty sequence the object is returned as it.
|
|
||||||
"""
|
|
||||||
get = self.get_attribute
|
|
||||||
for name in attributes:
|
|
||||||
obj = get(obj, name)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def call_function(self, f, context, args, kwargs, dyn_args, dyn_kwargs):
|
|
||||||
"""
|
|
||||||
Function call helper. Called for all functions that are passed
|
|
||||||
any arguments.
|
|
||||||
"""
|
|
||||||
# some traceback systems allow to skip frames. but allow
|
|
||||||
# disabling that via -O to not make things slow
|
|
||||||
if __debug__:
|
|
||||||
__traceback_hide__ = True
|
|
||||||
|
|
||||||
if dyn_args is not None:
|
|
||||||
args += tuple(dyn_args)
|
|
||||||
if dyn_kwargs is not None:
|
|
||||||
kwargs.update(dyn_kwargs)
|
|
||||||
if _getattr(f, 'jinja_unsafe_call', False) or \
|
|
||||||
_getattr(f, 'alters_data', False):
|
|
||||||
return self.undefined_singleton
|
|
||||||
if _getattr(f, 'jinja_context_callable', False):
|
|
||||||
args = (self, context) + args
|
|
||||||
return f(*args, **kwargs)
|
|
||||||
|
|
||||||
def call_function_simple(self, f, context):
|
|
||||||
"""
|
|
||||||
Function call without arguments. Because of the smaller signature and
|
|
||||||
fewer logic here we have a bit of redundant code.
|
|
||||||
"""
|
|
||||||
# some traceback systems allow to skip frames. but allow
|
|
||||||
# disabling that via -O to not make things slow
|
|
||||||
if __debug__:
|
|
||||||
__traceback_hide__ = True
|
|
||||||
|
|
||||||
if _getattr(f, 'jinja_unsafe_call', False) or \
|
|
||||||
_getattr(f, 'alters_data', False):
|
|
||||||
return self.undefined_singleton
|
|
||||||
if _getattr(f, 'jinja_context_callable', False):
|
|
||||||
return f(self, context)
|
|
||||||
return f()
|
|
||||||
|
|
||||||
def finish_var(self, value, ctx):
|
|
||||||
"""
|
|
||||||
As long as no write_var function is passed to the template
|
|
||||||
evaluator the source generated by the python translator will
|
|
||||||
call this function for all variables.
|
|
||||||
"""
|
|
||||||
# some traceback systems allow to skip frames. but allow
|
|
||||||
# disabling that via -O to not make things slow
|
|
||||||
if __debug__:
|
|
||||||
__traceback_hide__ = True
|
|
||||||
|
|
||||||
if value is None:
|
|
||||||
return u''
|
|
||||||
elif value is self.undefined_singleton:
|
|
||||||
return unicode(value)
|
|
||||||
elif _getattr(value, 'jinja_no_finalization', False):
|
|
||||||
return value
|
|
||||||
val = self.to_unicode(value)
|
|
||||||
if self.default_filters:
|
|
||||||
val = self.apply_filters(val, ctx, self.default_filters)
|
|
||||||
return val
|
|
|
@ -1,91 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja.exceptions
|
|
||||||
~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Jinja exceptions.
|
|
||||||
|
|
||||||
:copyright: 2007 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class TemplateError(RuntimeError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SecurityException(TemplateError):
|
|
||||||
"""
|
|
||||||
Raise if the template designer tried to do something dangerous.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class FilterNotFound(KeyError, TemplateError):
|
|
||||||
"""
|
|
||||||
Raised if a filter does not exist.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, message):
|
|
||||||
KeyError.__init__(self, message)
|
|
||||||
|
|
||||||
|
|
||||||
class FilterArgumentError(TypeError, TemplateError):
|
|
||||||
"""
|
|
||||||
An argument passed to the filter was invalid.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, message):
|
|
||||||
TypeError.__init__(self, message)
|
|
||||||
|
|
||||||
|
|
||||||
class TestNotFound(KeyError, TemplateError):
|
|
||||||
"""
|
|
||||||
Raised if a test does not exist.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, message):
|
|
||||||
KeyError.__init__(self, message)
|
|
||||||
|
|
||||||
|
|
||||||
class TestArgumentError(TypeError, TemplateError):
|
|
||||||
"""
|
|
||||||
An argument passed to a test function was invalid.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, message):
|
|
||||||
TypeError.__init__(self, message)
|
|
||||||
|
|
||||||
|
|
||||||
class TemplateNotFound(IOError, TemplateError):
|
|
||||||
"""
|
|
||||||
Raised if a template does not exist.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, name):
|
|
||||||
IOError.__init__(self, name)
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
|
|
||||||
class TemplateSyntaxError(SyntaxError, TemplateError):
|
|
||||||
"""
|
|
||||||
Raised to tell the user that there is a problem with the template.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, message, lineno, filename):
|
|
||||||
SyntaxError.__init__(self, message)
|
|
||||||
self.lineno = lineno
|
|
||||||
self.filename = filename
|
|
||||||
|
|
||||||
|
|
||||||
class TemplateRuntimeError(TemplateError):
|
|
||||||
"""
|
|
||||||
Raised by the template engine if a tag encountered an error when
|
|
||||||
rendering.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class TemplateIncludeError(TemplateError):
|
|
||||||
"""
|
|
||||||
Raised by the `ControlledLoader` if recursive includes where
|
|
||||||
detected.
|
|
||||||
"""
|
|
|
@ -1,986 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja.filters
|
|
||||||
~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Bundled jinja filters.
|
|
||||||
|
|
||||||
:copyright: 2007 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
import re
|
|
||||||
from random import choice
|
|
||||||
from operator import itemgetter
|
|
||||||
from urllib import urlencode, quote
|
|
||||||
from jinja.utils import urlize, escape, reversed, sorted, groupby, \
|
|
||||||
get_attribute, pformat
|
|
||||||
from jinja.datastructure import TemplateData
|
|
||||||
from jinja.exceptions import FilterArgumentError, SecurityException
|
|
||||||
|
|
||||||
|
|
||||||
_striptags_re = re.compile(r'(<!--.*?-->|<[^>]*>)')
|
|
||||||
|
|
||||||
|
|
||||||
def stringfilter(f):
|
|
||||||
"""
|
|
||||||
Decorator for filters that just work on unicode objects.
|
|
||||||
"""
|
|
||||||
def decorator(*args):
|
|
||||||
def wrapped(env, context, value):
|
|
||||||
nargs = list(args)
|
|
||||||
for idx, var in enumerate(nargs):
|
|
||||||
if isinstance(var, str):
|
|
||||||
nargs[idx] = env.to_unicode(var)
|
|
||||||
return f(env.to_unicode(value), *nargs)
|
|
||||||
return wrapped
|
|
||||||
try:
|
|
||||||
decorator.__doc__ = f.__doc__
|
|
||||||
decorator.__name__ = f.__name__
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
def simplefilter(f):
|
|
||||||
"""
|
|
||||||
Decorator for simplifying filters. Filter arguments are passed
|
|
||||||
to the decorated function without environment and context. The
|
|
||||||
source value is the first argument. (like stringfilter but
|
|
||||||
without unicode conversion)
|
|
||||||
"""
|
|
||||||
def decorator(*args):
|
|
||||||
def wrapped(env, context, value):
|
|
||||||
return f(value, *args)
|
|
||||||
return wrapped
|
|
||||||
try:
|
|
||||||
decorator.__doc__ = f.__doc__
|
|
||||||
decorator.__name__ = f.__name__
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
def do_replace(s, old, new, count=None):
|
|
||||||
"""
|
|
||||||
Return a copy of the value with all occurrences of a substring
|
|
||||||
replaced with a new one. The first argument is the substring
|
|
||||||
that should be replaced, the second is the replacement string.
|
|
||||||
If the optional third argument ``count`` is given, only the first
|
|
||||||
``count`` occurrences are replaced:
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{{ "Hello World"|replace("Hello", "Goodbye") }}
|
|
||||||
-> Goodbye World
|
|
||||||
|
|
||||||
{{ "aaaaargh"|replace("a", "d'oh, ", 2) }}
|
|
||||||
-> d'oh, d'oh, aaargh
|
|
||||||
"""
|
|
||||||
if not isinstance(old, basestring) or \
|
|
||||||
not isinstance(new, basestring):
|
|
||||||
raise FilterArgumentError('the replace filter requires '
|
|
||||||
'string replacement arguments')
|
|
||||||
if count is None:
|
|
||||||
return s.replace(old, new)
|
|
||||||
if not isinstance(count, (int, long)):
|
|
||||||
raise FilterArgumentError('the count parameter of the '
|
|
||||||
'replace filter requires '
|
|
||||||
'an integer')
|
|
||||||
return s.replace(old, new, count)
|
|
||||||
do_replace = stringfilter(do_replace)
|
|
||||||
|
|
||||||
|
|
||||||
def do_upper(s):
|
|
||||||
"""
|
|
||||||
Convert a value to uppercase.
|
|
||||||
"""
|
|
||||||
return s.upper()
|
|
||||||
do_upper = stringfilter(do_upper)
|
|
||||||
|
|
||||||
|
|
||||||
def do_lower(s):
|
|
||||||
"""
|
|
||||||
Convert a value to lowercase.
|
|
||||||
"""
|
|
||||||
return s.lower()
|
|
||||||
do_lower = stringfilter(do_lower)
|
|
||||||
|
|
||||||
|
|
||||||
def do_escape(attribute=False):
|
|
||||||
"""
|
|
||||||
XML escape ``&``, ``<``, and ``>`` in a string of data. If the
|
|
||||||
optional parameter is `true` this filter will also convert
|
|
||||||
``"`` to ``"``. This filter is just used if the environment
|
|
||||||
was configured with disabled `auto_escape`.
|
|
||||||
|
|
||||||
This method will have no effect it the value is already escaped.
|
|
||||||
"""
|
|
||||||
#: because filters are cached we can make a local alias to
|
|
||||||
#: speed things up a bit
|
|
||||||
e = escape
|
|
||||||
def wrapped(env, context, s):
|
|
||||||
if isinstance(s, TemplateData):
|
|
||||||
return s
|
|
||||||
elif hasattr(s, '__html__'):
|
|
||||||
return s.__html__()
|
|
||||||
#: small speedup, do not convert to unicode if we already
|
|
||||||
#: have an unicode object.
|
|
||||||
if s.__class__ is not unicode:
|
|
||||||
s = env.to_unicode(s)
|
|
||||||
return e(s, attribute)
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def do_xmlattr(autospace=False):
|
|
||||||
"""
|
|
||||||
Create an SGML/XML attribute string based on the items in a dict.
|
|
||||||
All values that are neither `none` nor `undefined` are automatically
|
|
||||||
escaped:
|
|
||||||
|
|
||||||
.. sourcecode:: html+jinja
|
|
||||||
|
|
||||||
<ul{{ {'class': 'my_list', 'missing': None,
|
|
||||||
'id': 'list-%d'|format(variable)}|xmlattr }}>
|
|
||||||
...
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
Results in something like this:
|
|
||||||
|
|
||||||
.. sourcecode:: html
|
|
||||||
|
|
||||||
<ul class="my_list" id="list-42">
|
|
||||||
...
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
As you can see it automatically prepends a space in front of the item
|
|
||||||
if the filter returned something. You can disable this by passing
|
|
||||||
`false` as only argument to the filter.
|
|
||||||
|
|
||||||
*New in Jinja 1.1*
|
|
||||||
"""
|
|
||||||
e = escape
|
|
||||||
def wrapped(env, context, d):
|
|
||||||
if not hasattr(d, 'iteritems'):
|
|
||||||
raise TypeError('a dict is required')
|
|
||||||
result = []
|
|
||||||
for key, value in d.iteritems():
|
|
||||||
if value not in (None, env.undefined_singleton):
|
|
||||||
result.append(u'%s="%s"' % (
|
|
||||||
e(env.to_unicode(key)),
|
|
||||||
e(env.to_unicode(value), True)
|
|
||||||
))
|
|
||||||
rv = u' '.join(result)
|
|
||||||
if autospace:
|
|
||||||
rv = ' ' + rv
|
|
||||||
return rv
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def do_capitalize(s):
|
|
||||||
"""
|
|
||||||
Capitalize a value. The first character will be uppercase, all others
|
|
||||||
lowercase.
|
|
||||||
"""
|
|
||||||
return s.capitalize()
|
|
||||||
do_capitalize = stringfilter(do_capitalize)
|
|
||||||
|
|
||||||
|
|
||||||
def do_title(s):
|
|
||||||
"""
|
|
||||||
Return a titlecased version of the value. I.e. words will start with
|
|
||||||
uppercase letters, all remaining characters are lowercase.
|
|
||||||
"""
|
|
||||||
return s.title()
|
|
||||||
do_title = stringfilter(do_title)
|
|
||||||
|
|
||||||
|
|
||||||
def do_dictsort(case_sensitive=False, by='key'):
|
|
||||||
"""
|
|
||||||
Sort a dict and yield (key, value) pairs. Because python dicts are
|
|
||||||
unsorted you may want to use this function to order them by either
|
|
||||||
key or value:
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{% for item in mydict|dictsort %}
|
|
||||||
sort the dict by key, case insensitive
|
|
||||||
|
|
||||||
{% for item in mydict|dicsort(true) %}
|
|
||||||
sort the dict by key, case sensitive
|
|
||||||
|
|
||||||
{% for item in mydict|dictsort(false, 'value') %}
|
|
||||||
sort the dict by key, case insensitive, sorted
|
|
||||||
normally and ordered by value.
|
|
||||||
"""
|
|
||||||
if by == 'key':
|
|
||||||
pos = 0
|
|
||||||
elif by == 'value':
|
|
||||||
pos = 1
|
|
||||||
else:
|
|
||||||
raise FilterArgumentError('You can only sort by either '
|
|
||||||
'"key" or "value"')
|
|
||||||
def sort_func(value, env):
|
|
||||||
if isinstance(value, basestring):
|
|
||||||
value = env.to_unicode(value)
|
|
||||||
if not case_sensitive:
|
|
||||||
value = value.lower()
|
|
||||||
return value
|
|
||||||
|
|
||||||
def wrapped(env, context, value):
|
|
||||||
items = value.items()
|
|
||||||
items.sort(lambda a, b: cmp(sort_func(a[pos], env),
|
|
||||||
sort_func(b[pos], env)))
|
|
||||||
return items
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def do_default(default_value=u'', boolean=False):
|
|
||||||
"""
|
|
||||||
If the value is undefined it will return the passed default value,
|
|
||||||
otherwise the value of the variable:
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{{ my_variable|default('my_variable is not defined') }}
|
|
||||||
|
|
||||||
This will output the value of ``my_variable`` if the variable was
|
|
||||||
defined, otherwise ``'my_variable is not defined'``. If you want
|
|
||||||
to use default with variables that evaluate to false you have to
|
|
||||||
set the second parameter to `true`:
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{{ ''|default('the string was empty', true) }}
|
|
||||||
"""
|
|
||||||
def wrapped(env, context, value):
|
|
||||||
if (boolean and not value) or value in (env.undefined_singleton, None):
|
|
||||||
return default_value
|
|
||||||
return value
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def do_join(d=u''):
|
|
||||||
"""
|
|
||||||
Return a string which is the concatenation of the strings in the
|
|
||||||
sequence. The separator between elements is an empty string per
|
|
||||||
default, you can define ith with the optional parameter:
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{{ [1, 2, 3]|join('|') }}
|
|
||||||
-> 1|2|3
|
|
||||||
|
|
||||||
{{ [1, 2, 3]|join }}
|
|
||||||
-> 123
|
|
||||||
"""
|
|
||||||
def wrapped(env, context, value):
|
|
||||||
return env.to_unicode(d).join([env.to_unicode(x) for x in value])
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def do_count():
|
|
||||||
"""
|
|
||||||
Return the length of the value. In case if getting an integer or float
|
|
||||||
it will convert it into a string an return the length of the new
|
|
||||||
string. If the object has no length it will of corse return 0.
|
|
||||||
"""
|
|
||||||
def wrapped(env, context, value):
|
|
||||||
try:
|
|
||||||
if type(value) in (int, float, long):
|
|
||||||
return len(str(value))
|
|
||||||
return len(value)
|
|
||||||
except TypeError:
|
|
||||||
return 0
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def do_reverse():
|
|
||||||
"""
|
|
||||||
Return a reversed list of the sequence filtered. You can use this
|
|
||||||
for example for reverse iteration:
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{% for item in seq|reverse %}
|
|
||||||
{{ item|e }}
|
|
||||||
{% endfor %}
|
|
||||||
"""
|
|
||||||
def wrapped(env, context, value):
|
|
||||||
try:
|
|
||||||
return value[::-1]
|
|
||||||
except:
|
|
||||||
l = list(value)
|
|
||||||
l.reverse()
|
|
||||||
return l
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def do_center(value, width=80):
|
|
||||||
"""
|
|
||||||
Centers the value in a field of a given width.
|
|
||||||
"""
|
|
||||||
return value.center(width)
|
|
||||||
do_center = stringfilter(do_center)
|
|
||||||
|
|
||||||
|
|
||||||
def do_first():
|
|
||||||
"""
|
|
||||||
Return the frist item of a sequence.
|
|
||||||
"""
|
|
||||||
def wrapped(env, context, seq):
|
|
||||||
try:
|
|
||||||
return iter(seq).next()
|
|
||||||
except StopIteration:
|
|
||||||
return env.undefined_singleton
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def do_last():
|
|
||||||
"""
|
|
||||||
Return the last item of a sequence.
|
|
||||||
"""
|
|
||||||
def wrapped(env, context, seq):
|
|
||||||
try:
|
|
||||||
return iter(reversed(seq)).next()
|
|
||||||
except StopIteration:
|
|
||||||
return env.undefined_singleton
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def do_random():
|
|
||||||
"""
|
|
||||||
Return a random item from the sequence.
|
|
||||||
"""
|
|
||||||
def wrapped(env, context, seq):
|
|
||||||
try:
|
|
||||||
return choice(seq)
|
|
||||||
except IndexError:
|
|
||||||
return env.undefined_singleton
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def do_urlencode():
|
|
||||||
"""
|
|
||||||
urlencode a string or directory.
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{{ {'foo': 'bar', 'blub': 'blah'}|urlencode }}
|
|
||||||
-> foo=bar&blub=blah
|
|
||||||
|
|
||||||
{{ 'Hello World' }}
|
|
||||||
-> Hello%20World
|
|
||||||
"""
|
|
||||||
def wrapped(env, context, value):
|
|
||||||
if isinstance(value, dict):
|
|
||||||
tmp = {}
|
|
||||||
for key, value in value.iteritems():
|
|
||||||
key = env.to_unicode(key).encode(env.charset)
|
|
||||||
value = env.to_unicode(value).encode(env.charset)
|
|
||||||
tmp[key] = value
|
|
||||||
return urlencode(tmp)
|
|
||||||
else:
|
|
||||||
return quote(env.to_unicode(value).encode(env.charset))
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def do_jsonencode():
|
|
||||||
"""
|
|
||||||
JSON dump a variable. just works if simplejson is installed.
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{{ 'Hello World'|jsonencode }}
|
|
||||||
-> "Hello World"
|
|
||||||
"""
|
|
||||||
global simplejson
|
|
||||||
try:
|
|
||||||
simplejson
|
|
||||||
except NameError:
|
|
||||||
import simplejson
|
|
||||||
return lambda e, c, v: simplejson.dumps(v)
|
|
||||||
|
|
||||||
|
|
||||||
def do_filesizeformat():
|
|
||||||
"""
|
|
||||||
Format the value like a 'human-readable' file size (i.e. 13 KB, 4.1 MB, 102
|
|
||||||
bytes, etc).
|
|
||||||
"""
|
|
||||||
def wrapped(env, context, value):
|
|
||||||
# fail silently
|
|
||||||
try:
|
|
||||||
bytes = float(value)
|
|
||||||
except TypeError:
|
|
||||||
bytes = 0
|
|
||||||
|
|
||||||
if bytes < 1024:
|
|
||||||
return "%d Byte%s" % (bytes, bytes != 1 and 's' or '')
|
|
||||||
elif bytes < 1024 * 1024:
|
|
||||||
return "%.1f KB" % (bytes / 1024)
|
|
||||||
elif bytes < 1024 * 1024 * 1024:
|
|
||||||
return "%.1f MB" % (bytes / (1024 * 1024))
|
|
||||||
return "%.1f GB" % (bytes / (1024 * 1024 * 1024))
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def do_pprint(verbose=False):
|
|
||||||
"""
|
|
||||||
Pretty print a variable. Useful for debugging.
|
|
||||||
|
|
||||||
With Jinja 1.2 onwards you can pass it a parameter. If this parameter
|
|
||||||
is truthy the output will be more verbose (this requires `pretty`)
|
|
||||||
"""
|
|
||||||
def wrapped(env, context, value):
|
|
||||||
return pformat(value, verbose=verbose)
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def do_urlize(value, trim_url_limit=None, nofollow=False):
|
|
||||||
"""
|
|
||||||
Converts URLs in plain text into clickable links.
|
|
||||||
|
|
||||||
If you pass the filter an additional integer it will shorten the urls
|
|
||||||
to that number. Also a third argument exists that makes the urls
|
|
||||||
"nofollow":
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{{ mytext|urlize(40, True) }}
|
|
||||||
links are shortened to 40 chars and defined with rel="nofollow"
|
|
||||||
"""
|
|
||||||
return urlize(value, trim_url_limit, nofollow)
|
|
||||||
do_urlize = stringfilter(do_urlize)
|
|
||||||
|
|
||||||
|
|
||||||
def do_indent(s, width=4, indentfirst=False):
|
|
||||||
"""
|
|
||||||
{{ s|indent[ width[ indentfirst[ usetab]]] }}
|
|
||||||
|
|
||||||
Return a copy of the passed string, each line indented by
|
|
||||||
4 spaces. The first line is not indented. If you want to
|
|
||||||
change the number of spaces or indent the first line too
|
|
||||||
you can pass additional parameters to the filter:
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{{ mytext|indent(2, True) }}
|
|
||||||
indent by two spaces and indent the first line too.
|
|
||||||
"""
|
|
||||||
indention = ' ' * width
|
|
||||||
if indentfirst:
|
|
||||||
return u'\n'.join([indention + line for line in s.splitlines()])
|
|
||||||
return s.replace('\n', '\n' + indention)
|
|
||||||
do_indent = stringfilter(do_indent)
|
|
||||||
|
|
||||||
|
|
||||||
def do_truncate(s, length=255, killwords=False, end='...'):
|
|
||||||
"""
|
|
||||||
Return a truncated copy of the string. The length is specified
|
|
||||||
with the first parameter which defaults to ``255``. If the second
|
|
||||||
parameter is ``true`` the filter will cut the text at length. Otherwise
|
|
||||||
it will try to save the last word. If the text was in fact
|
|
||||||
truncated it will append an ellipsis sign (``"..."``). If you want a
|
|
||||||
different ellipsis sign than ``"..."`` you can specify it using the
|
|
||||||
third parameter.
|
|
||||||
|
|
||||||
.. sourcecode jinja::
|
|
||||||
|
|
||||||
{{ mytext|truncate(300, false, '»') }}
|
|
||||||
truncate mytext to 300 chars, don't split up words, use a
|
|
||||||
right pointing double arrow as ellipsis sign.
|
|
||||||
"""
|
|
||||||
if len(s) <= length:
|
|
||||||
return s
|
|
||||||
elif killwords:
|
|
||||||
return s[:length] + end
|
|
||||||
words = s.split(' ')
|
|
||||||
result = []
|
|
||||||
m = 0
|
|
||||||
for word in words:
|
|
||||||
m += len(word) + 1
|
|
||||||
if m > length:
|
|
||||||
break
|
|
||||||
result.append(word)
|
|
||||||
result.append(end)
|
|
||||||
return u' '.join(result)
|
|
||||||
do_truncate = stringfilter(do_truncate)
|
|
||||||
|
|
||||||
|
|
||||||
def do_wordwrap(s, pos=79, hard=False):
|
|
||||||
"""
|
|
||||||
Return a copy of the string passed to the filter wrapped after
|
|
||||||
``79`` characters. You can override this default using the first
|
|
||||||
parameter. If you set the second parameter to `true` Jinja will
|
|
||||||
also split words apart (usually a bad idea because it makes
|
|
||||||
reading hard).
|
|
||||||
"""
|
|
||||||
if len(s) < pos:
|
|
||||||
return s
|
|
||||||
if hard:
|
|
||||||
return u'\n'.join([s[idx:idx + pos] for idx in
|
|
||||||
xrange(0, len(s), pos)])
|
|
||||||
# code from http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/148061
|
|
||||||
return reduce(lambda line, word, pos=pos: u'%s%s%s' %
|
|
||||||
(line, u' \n'[(len(line)-line.rfind('\n') - 1 +
|
|
||||||
len(word.split('\n', 1)[0]) >= pos)],
|
|
||||||
word), s.split(' '))
|
|
||||||
do_wordwrap = stringfilter(do_wordwrap)
|
|
||||||
|
|
||||||
|
|
||||||
def do_wordcount(s):
|
|
||||||
"""
|
|
||||||
Count the words in that string.
|
|
||||||
"""
|
|
||||||
return len([x for x in s.split() if x])
|
|
||||||
do_wordcount = stringfilter(do_wordcount)
|
|
||||||
|
|
||||||
|
|
||||||
def do_textile(s):
|
|
||||||
"""
|
|
||||||
Prase the string using textile.
|
|
||||||
|
|
||||||
requires the `PyTextile`_ library.
|
|
||||||
|
|
||||||
.. _PyTextile: http://dealmeida.net/projects/textile/
|
|
||||||
"""
|
|
||||||
from textile import textile
|
|
||||||
return textile(s.encode('utf-8')).decode('utf-8')
|
|
||||||
do_textile = stringfilter(do_textile)
|
|
||||||
|
|
||||||
|
|
||||||
def do_markdown(s):
|
|
||||||
"""
|
|
||||||
Parse the string using markdown.
|
|
||||||
|
|
||||||
requires the `Python-markdown`_ library.
|
|
||||||
|
|
||||||
.. _Python-markdown: http://www.freewisdom.org/projects/python-markdown/
|
|
||||||
"""
|
|
||||||
from markdown import markdown
|
|
||||||
return markdown(s.encode('utf-8')).decode('utf-8')
|
|
||||||
do_markdown = stringfilter(do_markdown)
|
|
||||||
|
|
||||||
|
|
||||||
def do_rst(s):
|
|
||||||
"""
|
|
||||||
Parse the string using the reStructuredText parser from the
|
|
||||||
docutils package.
|
|
||||||
|
|
||||||
requires `docutils`_.
|
|
||||||
|
|
||||||
.. _docutils: http://docutils.sourceforge.net/
|
|
||||||
"""
|
|
||||||
from docutils.core import publish_parts
|
|
||||||
parts = publish_parts(source=s, writer_name='html4css1')
|
|
||||||
return parts['fragment']
|
|
||||||
do_rst = stringfilter(do_rst)
|
|
||||||
|
|
||||||
|
|
||||||
def do_int(default=0):
|
|
||||||
"""
|
|
||||||
Convert the value into an integer. If the
|
|
||||||
conversion doesn't work it will return ``0``. You can
|
|
||||||
override this default using the first parameter.
|
|
||||||
"""
|
|
||||||
def wrapped(env, context, value):
|
|
||||||
try:
|
|
||||||
return int(value)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
try:
|
|
||||||
return int(float(value))
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return default
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def do_float(default=0.0):
|
|
||||||
"""
|
|
||||||
Convert the value into a floating point number. If the
|
|
||||||
conversion doesn't work it will return ``0.0``. You can
|
|
||||||
override this default using the first parameter.
|
|
||||||
"""
|
|
||||||
def wrapped(env, context, value):
|
|
||||||
try:
|
|
||||||
return float(value)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
return default
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def do_string():
|
|
||||||
"""
|
|
||||||
Convert the value into an string.
|
|
||||||
"""
|
|
||||||
return lambda e, c, v: e.to_unicode(v)
|
|
||||||
|
|
||||||
|
|
||||||
def do_format(*args):
|
|
||||||
"""
|
|
||||||
Apply python string formatting on an object:
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{{ "%s - %s"|format("Hello?", "Foo!") }}
|
|
||||||
-> Hello? - Foo!
|
|
||||||
|
|
||||||
Note that you cannot use the mapping syntax (``%(name)s``)
|
|
||||||
like in python. Use `|dformat` for that.
|
|
||||||
"""
|
|
||||||
def wrapped(env, context, value):
|
|
||||||
return env.to_unicode(value) % args
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def do_dformat(d):
|
|
||||||
"""
|
|
||||||
Apply python mapping string formatting on an object:
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{{ "Hello %(username)s!"|dformat({'username': 'John Doe'}) }}
|
|
||||||
-> Hello John Doe!
|
|
||||||
|
|
||||||
This is useful when adding variables to translateable
|
|
||||||
string expressions.
|
|
||||||
|
|
||||||
*New in Jinja 1.1*
|
|
||||||
"""
|
|
||||||
if not isinstance(d, dict):
|
|
||||||
raise FilterArgumentError('dict required')
|
|
||||||
def wrapped(env, context, value):
|
|
||||||
return env.to_unicode(value) % d
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def do_trim(value):
|
|
||||||
"""
|
|
||||||
Strip leading and trailing whitespace.
|
|
||||||
"""
|
|
||||||
return value.strip()
|
|
||||||
do_trim = stringfilter(do_trim)
|
|
||||||
|
|
||||||
|
|
||||||
def do_capture(name='captured', clean=False):
|
|
||||||
"""
|
|
||||||
Store the value in a variable called ``captured`` or a variable
|
|
||||||
with the name provided. Useful for filter blocks:
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{% filter capture('foo') %}
|
|
||||||
...
|
|
||||||
{% endfilter %}
|
|
||||||
{{ foo }}
|
|
||||||
|
|
||||||
This will output "..." two times. One time from the filter block
|
|
||||||
and one time from the variable. If you don't want the filter to
|
|
||||||
output something you can use it in `clean` mode:
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{% filter capture('foo', True) %}
|
|
||||||
...
|
|
||||||
{% endfilter %}
|
|
||||||
{{ foo }}
|
|
||||||
"""
|
|
||||||
if not isinstance(name, basestring):
|
|
||||||
raise FilterArgumentError('You can only capture into variables')
|
|
||||||
def wrapped(env, context, value):
|
|
||||||
context[name] = value
|
|
||||||
if clean:
|
|
||||||
return TemplateData()
|
|
||||||
return value
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def do_striptags(value):
|
|
||||||
"""
|
|
||||||
Strip SGML/XML tags and replace adjacent whitespace by one space.
|
|
||||||
|
|
||||||
*new in Jinja 1.1*
|
|
||||||
"""
|
|
||||||
return ' '.join(_striptags_re.sub('', value).split())
|
|
||||||
do_striptags = stringfilter(do_striptags)
|
|
||||||
|
|
||||||
|
|
||||||
def do_slice(slices, fill_with=None):
|
|
||||||
"""
|
|
||||||
Slice an iterator and return a list of lists containing
|
|
||||||
those items. Useful if you want to create a div containing
|
|
||||||
three div tags that represent columns:
|
|
||||||
|
|
||||||
.. sourcecode:: html+jinja
|
|
||||||
|
|
||||||
<div class="columwrapper">
|
|
||||||
{%- for column in items|slice(3) %}
|
|
||||||
<ul class="column-{{ loop.index }}">
|
|
||||||
{%- for item in column %}
|
|
||||||
<li>{{ item }}</li>
|
|
||||||
{%- endfor %}
|
|
||||||
</ul>
|
|
||||||
{%- endfor %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
If you pass it a second argument it's used to fill missing
|
|
||||||
values on the last iteration.
|
|
||||||
|
|
||||||
*new in Jinja 1.1*
|
|
||||||
"""
|
|
||||||
def wrapped(env, context, value):
|
|
||||||
result = []
|
|
||||||
seq = list(value)
|
|
||||||
length = len(seq)
|
|
||||||
items_per_slice = length // slices
|
|
||||||
slices_with_extra = length % slices
|
|
||||||
offset = 0
|
|
||||||
for slice_number in xrange(slices):
|
|
||||||
start = offset + slice_number * items_per_slice
|
|
||||||
if slice_number < slices_with_extra:
|
|
||||||
offset += 1
|
|
||||||
end = offset + (slice_number + 1) * items_per_slice
|
|
||||||
tmp = seq[start:end]
|
|
||||||
if fill_with is not None and slice_number >= slices_with_extra:
|
|
||||||
tmp.append(fill_with)
|
|
||||||
result.append(tmp)
|
|
||||||
return result
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def do_batch(linecount, fill_with=None):
|
|
||||||
"""
|
|
||||||
A filter that batches items. It works pretty much like `slice`
|
|
||||||
just the other way round. It returns a list of lists with the
|
|
||||||
given number of items. If you provide a second parameter this
|
|
||||||
is used to fill missing items. See this example:
|
|
||||||
|
|
||||||
.. sourcecode:: html+jinja
|
|
||||||
|
|
||||||
<table>
|
|
||||||
{%- for row in items|batch(3, ' ') %}
|
|
||||||
<tr>
|
|
||||||
{%- for column in row %}
|
|
||||||
<tr>{{ column }}</td>
|
|
||||||
{%- endfor %}
|
|
||||||
</tr>
|
|
||||||
{%- endfor %}
|
|
||||||
</table>
|
|
||||||
|
|
||||||
*new in Jinja 1.1*
|
|
||||||
"""
|
|
||||||
def wrapped(env, context, value):
|
|
||||||
result = []
|
|
||||||
tmp = []
|
|
||||||
for item in value:
|
|
||||||
if len(tmp) == linecount:
|
|
||||||
result.append(tmp)
|
|
||||||
tmp = []
|
|
||||||
tmp.append(item)
|
|
||||||
if tmp:
|
|
||||||
if fill_with is not None and len(tmp) < linecount:
|
|
||||||
tmp += [fill_with] * (linecount - len(tmp))
|
|
||||||
result.append(tmp)
|
|
||||||
return result
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def do_sum():
|
|
||||||
"""
|
|
||||||
Sum up the given sequence of numbers.
|
|
||||||
|
|
||||||
*new in Jinja 1.1*
|
|
||||||
"""
|
|
||||||
def wrapped(env, context, value):
|
|
||||||
return sum(value)
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def do_abs():
|
|
||||||
"""
|
|
||||||
Return the absolute value of a number.
|
|
||||||
|
|
||||||
*new in Jinja 1.1*
|
|
||||||
"""
|
|
||||||
def wrapped(env, context, value):
|
|
||||||
return abs(value)
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def do_round(precision=0, method='common'):
|
|
||||||
"""
|
|
||||||
Round the number to a given precision. The first
|
|
||||||
parameter specifies the precision (default is ``0``), the
|
|
||||||
second the rounding method:
|
|
||||||
|
|
||||||
- ``'common'`` rounds either up or down
|
|
||||||
- ``'ceil'`` always rounds up
|
|
||||||
- ``'floor'`` always rounds down
|
|
||||||
|
|
||||||
If you don't specify a method ``'common'`` is used.
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{{ 42.55|round }}
|
|
||||||
-> 43
|
|
||||||
{{ 42.55|round(1, 'floor') }}
|
|
||||||
-> 42.5
|
|
||||||
|
|
||||||
*new in Jinja 1.1*
|
|
||||||
"""
|
|
||||||
if not method in ('common', 'ceil', 'floor'):
|
|
||||||
raise FilterArgumentError('method must be common, ceil or floor')
|
|
||||||
if precision < 0:
|
|
||||||
raise FilterArgumentError('precision must be a postive integer '
|
|
||||||
'or zero.')
|
|
||||||
def wrapped(env, context, value):
|
|
||||||
if method == 'common':
|
|
||||||
return round(value, precision)
|
|
||||||
import math
|
|
||||||
func = getattr(math, method)
|
|
||||||
if precision:
|
|
||||||
return func(value * 10 * precision) / (10 * precision)
|
|
||||||
else:
|
|
||||||
return func(value)
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def do_sort(reverse=False):
|
|
||||||
"""
|
|
||||||
Sort a sequence. Per default it sorts ascending, if you pass it
|
|
||||||
`True` as first argument it will reverse the sorting.
|
|
||||||
|
|
||||||
*new in Jinja 1.1*
|
|
||||||
"""
|
|
||||||
def wrapped(env, context, value):
|
|
||||||
return sorted(value, reverse=reverse)
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def do_groupby(attribute):
|
|
||||||
"""
|
|
||||||
Group a sequence of objects by a common attribute.
|
|
||||||
|
|
||||||
If you for example have a list of dicts or objects that represent persons
|
|
||||||
with `gender`, `first_name` and `last_name` attributes and you want to
|
|
||||||
group all users by genders you can do something like the following
|
|
||||||
snippet:
|
|
||||||
|
|
||||||
.. sourcecode:: html+jinja
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
{% for group in persons|groupby('gender') %}
|
|
||||||
<li>{{ group.grouper }}<ul>
|
|
||||||
{% for person in group.list %}
|
|
||||||
<li>{{ person.first_name }} {{ person.last_name }}</li>
|
|
||||||
{% endfor %}</ul></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
As you can see the item we're grouping by is stored in the `grouper`
|
|
||||||
attribute and the `list` contains all the objects that have this grouper
|
|
||||||
in common.
|
|
||||||
|
|
||||||
*New in Jinja 1.2*
|
|
||||||
"""
|
|
||||||
def wrapped(env, context, value):
|
|
||||||
expr = lambda x: env.get_attribute(x, attribute)
|
|
||||||
return sorted([{
|
|
||||||
'grouper': a,
|
|
||||||
'list': list(b)
|
|
||||||
} for a, b in groupby(sorted(value, key=expr), expr)],
|
|
||||||
key=itemgetter('grouper'))
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def do_getattribute(attribute):
|
|
||||||
"""
|
|
||||||
Get one attribute from an object. Normally you don't have to use this
|
|
||||||
filter because the attribute and subscript expressions try to either
|
|
||||||
get an attribute of an object or an item. In some situations it could
|
|
||||||
be that there is an item *and* an attribute with the same name. In that
|
|
||||||
situation only the item is returned, never the attribute.
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{{ foo.bar }} -> {{ foo|getattribute('bar') }}
|
|
||||||
|
|
||||||
*New in Jinja 1.2*
|
|
||||||
"""
|
|
||||||
def wrapped(env, context, value):
|
|
||||||
try:
|
|
||||||
return get_attribute(value, attribute)
|
|
||||||
except (SecurityException, AttributeError):
|
|
||||||
return env.undefined_singleton
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def do_getitem(key):
|
|
||||||
"""
|
|
||||||
This filter basically works like the normal subscript expression but
|
|
||||||
it doesn't fall back to attribute lookup. If an item does not exist for
|
|
||||||
an object undefined is returned.
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{{ foo.bar }} -> {{ foo|getitem('bar') }}
|
|
||||||
|
|
||||||
*New in Jinja 1.2*
|
|
||||||
"""
|
|
||||||
def wrapped(env, context, value):
|
|
||||||
try:
|
|
||||||
return value[key]
|
|
||||||
except (TypeError, KeyError, IndexError, AttributeError):
|
|
||||||
return env.undefined_singleton
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
FILTERS = {
|
|
||||||
'replace': do_replace,
|
|
||||||
'upper': do_upper,
|
|
||||||
'lower': do_lower,
|
|
||||||
'escape': do_escape,
|
|
||||||
'e': do_escape,
|
|
||||||
'xmlattr': do_xmlattr,
|
|
||||||
'capitalize': do_capitalize,
|
|
||||||
'title': do_title,
|
|
||||||
'default': do_default,
|
|
||||||
'join': do_join,
|
|
||||||
'count': do_count,
|
|
||||||
'dictsort': do_dictsort,
|
|
||||||
'length': do_count,
|
|
||||||
'reverse': do_reverse,
|
|
||||||
'center': do_center,
|
|
||||||
'title': do_title,
|
|
||||||
'capitalize': do_capitalize,
|
|
||||||
'first': do_first,
|
|
||||||
'last': do_last,
|
|
||||||
'random': do_random,
|
|
||||||
'urlencode': do_urlencode,
|
|
||||||
'jsonencode': do_jsonencode,
|
|
||||||
'filesizeformat': do_filesizeformat,
|
|
||||||
'pprint': do_pprint,
|
|
||||||
'indent': do_indent,
|
|
||||||
'truncate': do_truncate,
|
|
||||||
'wordwrap': do_wordwrap,
|
|
||||||
'wordcount': do_wordcount,
|
|
||||||
'textile': do_textile,
|
|
||||||
'markdown': do_markdown,
|
|
||||||
'rst': do_rst,
|
|
||||||
'int': do_int,
|
|
||||||
'float': do_float,
|
|
||||||
'string': do_string,
|
|
||||||
'urlize': do_urlize,
|
|
||||||
'format': do_format,
|
|
||||||
'dformat': do_dformat,
|
|
||||||
'capture': do_capture,
|
|
||||||
'trim': do_trim,
|
|
||||||
'striptags': do_striptags,
|
|
||||||
'slice': do_slice,
|
|
||||||
'batch': do_batch,
|
|
||||||
'sum': do_sum,
|
|
||||||
'abs': do_abs,
|
|
||||||
'round': do_round,
|
|
||||||
'sort': do_sort,
|
|
||||||
'groupby': do_groupby,
|
|
||||||
'getattribute': do_getattribute,
|
|
||||||
'getitem': do_getitem
|
|
||||||
}
|
|
|
@ -1,513 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja.lexer
|
|
||||||
~~~~~~~~~~~
|
|
||||||
|
|
||||||
This module implements a Jinja / Python combination lexer. The
|
|
||||||
`Lexer` class provided by this module is used to do some preprocessing
|
|
||||||
for Jinja.
|
|
||||||
|
|
||||||
On the one hand it filters out invalid operators like the bitshift
|
|
||||||
operators we don't allow in templates. On the other hand it separates
|
|
||||||
template code and python code in expressions.
|
|
||||||
|
|
||||||
Because of some limitations in the compiler package which are just
|
|
||||||
natural but annoying for Jinja, the lexer also "escapes" non names that
|
|
||||||
are not keywords. The Jinja parser then removes those escaping marks
|
|
||||||
again.
|
|
||||||
|
|
||||||
This is required in order to make "class" and some other python keywords
|
|
||||||
we don't use valid identifiers.
|
|
||||||
|
|
||||||
:copyright: 2007 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
import re
|
|
||||||
import unicodedata
|
|
||||||
from jinja.datastructure import TokenStream, Token
|
|
||||||
from jinja.exceptions import TemplateSyntaxError
|
|
||||||
from jinja.utils import set, sorted
|
|
||||||
from weakref import WeakValueDictionary
|
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['Lexer', 'Failure', 'keywords']
|
|
||||||
|
|
||||||
|
|
||||||
# cache for the lexers. Exists in order to be able to have multiple
|
|
||||||
# environments with the same lexer
|
|
||||||
_lexer_cache = WeakValueDictionary()
|
|
||||||
|
|
||||||
|
|
||||||
# static regular expressions
|
|
||||||
whitespace_re = re.compile(r'\s+(?um)')
|
|
||||||
name_re = re.compile(r'[a-zA-Z_][a-zA-Z0-9_]*')
|
|
||||||
string_re = re.compile(r"('([^'\\]*(?:\\.[^'\\]*)*)'"
|
|
||||||
r'|"([^"\\]*(?:\\.[^"\\]*)*)")(?ms)')
|
|
||||||
integer_re = re.compile(r'\d+')
|
|
||||||
float_re = re.compile(r'\d+\.\d+')
|
|
||||||
regex_re = re.compile(r'@/([^/\\]*(?:\\.[^/\\]*)*)*/[a-z]*(?ms)')
|
|
||||||
|
|
||||||
|
|
||||||
# set of used keywords
|
|
||||||
keywords = set(['and', 'block', 'cycle', 'elif', 'else', 'endblock',
|
|
||||||
'endfilter', 'endfor', 'endif', 'endmacro', 'endraw',
|
|
||||||
'endtrans', 'extends', 'filter', 'for', 'if', 'in',
|
|
||||||
'include', 'is', 'macro', 'not', 'or', 'pluralize', 'raw',
|
|
||||||
'recursive', 'set', 'trans', 'print', 'call', 'endcall'])
|
|
||||||
|
|
||||||
# bind operators to token types
|
|
||||||
operators = {
|
|
||||||
'+': 'add',
|
|
||||||
'-': 'sub',
|
|
||||||
'/': 'div',
|
|
||||||
'//': 'floordiv',
|
|
||||||
'*': 'mul',
|
|
||||||
'%': 'mod',
|
|
||||||
'**': 'pow',
|
|
||||||
'~': 'tilde',
|
|
||||||
'!': 'bang',
|
|
||||||
'@': 'at',
|
|
||||||
'[': 'lbracket',
|
|
||||||
']': 'rbracket',
|
|
||||||
'(': 'lparen',
|
|
||||||
')': 'rparen',
|
|
||||||
'{': 'lbrace',
|
|
||||||
'}': 'rbrace',
|
|
||||||
'==': 'eq',
|
|
||||||
'!=': 'ne',
|
|
||||||
'>': 'gt',
|
|
||||||
'>=': 'gteq',
|
|
||||||
'<': 'lt',
|
|
||||||
'<=': 'lteq',
|
|
||||||
'=': 'assign',
|
|
||||||
'.': 'dot',
|
|
||||||
':': 'colon',
|
|
||||||
'|': 'pipe',
|
|
||||||
',': 'comma'
|
|
||||||
}
|
|
||||||
|
|
||||||
reverse_operators = dict([(v, k) for k, v in operators.iteritems()])
|
|
||||||
assert len(operators) == len(reverse_operators), 'operators dropped'
|
|
||||||
operator_re = re.compile('(%s)' % '|'.join([re.escape(x) for x in
|
|
||||||
sorted(operators, key=lambda x: -len(x))]))
|
|
||||||
|
|
||||||
|
|
||||||
def unescape_string(lineno, filename, s):
|
|
||||||
r"""
|
|
||||||
Unescape a string. Supported escapes:
|
|
||||||
\a, \n, \r\, \f, \v, \\, \", \', \0
|
|
||||||
|
|
||||||
\x00, \u0000, \U00000000, \N{...}
|
|
||||||
|
|
||||||
Not supported are \101 because imho redundant.
|
|
||||||
"""
|
|
||||||
result = []
|
|
||||||
write = result.append
|
|
||||||
simple_escapes = {
|
|
||||||
'a': '\a',
|
|
||||||
'n': '\n',
|
|
||||||
'r': '\r',
|
|
||||||
'f': '\f',
|
|
||||||
't': '\t',
|
|
||||||
'v': '\v',
|
|
||||||
'\\': '\\',
|
|
||||||
'"': '"',
|
|
||||||
"'": "'",
|
|
||||||
'0': '\x00'
|
|
||||||
}
|
|
||||||
unicode_escapes = {
|
|
||||||
'x': 2,
|
|
||||||
'u': 4,
|
|
||||||
'U': 8
|
|
||||||
}
|
|
||||||
chariter = iter(s)
|
|
||||||
next_char = chariter.next
|
|
||||||
|
|
||||||
try:
|
|
||||||
for char in chariter:
|
|
||||||
if char == '\\':
|
|
||||||
char = next_char()
|
|
||||||
if char in simple_escapes:
|
|
||||||
write(simple_escapes[char])
|
|
||||||
elif char in unicode_escapes:
|
|
||||||
seq = [next_char() for x in xrange(unicode_escapes[char])]
|
|
||||||
try:
|
|
||||||
write(unichr(int(''.join(seq), 16)))
|
|
||||||
except ValueError:
|
|
||||||
raise TemplateSyntaxError('invalid unicode codepoint',
|
|
||||||
lineno, filename)
|
|
||||||
elif char == 'N':
|
|
||||||
if next_char() != '{':
|
|
||||||
raise TemplateSyntaxError('no name for codepoint',
|
|
||||||
lineno, filename)
|
|
||||||
seq = []
|
|
||||||
while True:
|
|
||||||
char = next_char()
|
|
||||||
if char == '}':
|
|
||||||
break
|
|
||||||
seq.append(char)
|
|
||||||
try:
|
|
||||||
write(unicodedata.lookup(u''.join(seq)))
|
|
||||||
except KeyError:
|
|
||||||
raise TemplateSyntaxError('unknown character name',
|
|
||||||
lineno, filename)
|
|
||||||
else:
|
|
||||||
write('\\' + char)
|
|
||||||
else:
|
|
||||||
write(char)
|
|
||||||
except StopIteration:
|
|
||||||
raise TemplateSyntaxError('invalid string escape', lineno, filename)
|
|
||||||
return u''.join(result)
|
|
||||||
|
|
||||||
|
|
||||||
def unescape_regex(s):
|
|
||||||
"""
|
|
||||||
Unescape rules for regular expressions.
|
|
||||||
"""
|
|
||||||
buffer = []
|
|
||||||
write = buffer.append
|
|
||||||
in_escape = False
|
|
||||||
for char in s:
|
|
||||||
if in_escape:
|
|
||||||
in_escape = False
|
|
||||||
if char not in safe_chars:
|
|
||||||
write('\\' + char)
|
|
||||||
continue
|
|
||||||
write(char)
|
|
||||||
return u''.join(buffer)
|
|
||||||
|
|
||||||
|
|
||||||
class Failure(object):
|
|
||||||
"""
|
|
||||||
Class that raises a `TemplateSyntaxError` if called.
|
|
||||||
Used by the `Lexer` to specify known errors.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, message, cls=TemplateSyntaxError):
|
|
||||||
self.message = message
|
|
||||||
self.error_class = cls
|
|
||||||
|
|
||||||
def __call__(self, lineno, filename):
|
|
||||||
raise self.error_class(self.message, lineno, filename)
|
|
||||||
|
|
||||||
|
|
||||||
class LexerMeta(type):
|
|
||||||
"""
|
|
||||||
Metaclass for the lexer that caches instances for
|
|
||||||
the same configuration in a weak value dictionary.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __call__(cls, environment):
|
|
||||||
key = hash((environment.block_start_string,
|
|
||||||
environment.block_end_string,
|
|
||||||
environment.variable_start_string,
|
|
||||||
environment.variable_end_string,
|
|
||||||
environment.comment_start_string,
|
|
||||||
environment.comment_end_string,
|
|
||||||
environment.trim_blocks))
|
|
||||||
|
|
||||||
# use the cached lexer if possible
|
|
||||||
if key in _lexer_cache:
|
|
||||||
return _lexer_cache[key]
|
|
||||||
|
|
||||||
# create a new lexer and cache it
|
|
||||||
lexer = type.__call__(cls, environment)
|
|
||||||
_lexer_cache[key] = lexer
|
|
||||||
return lexer
|
|
||||||
|
|
||||||
|
|
||||||
class Lexer(object):
|
|
||||||
"""
|
|
||||||
Class that implements a lexer for a given environment. Automatically
|
|
||||||
created by the environment class, usually you don't have to do that.
|
|
||||||
|
|
||||||
Note that the lexer is not automatically bound to an environment.
|
|
||||||
Multiple environments can share the same lexer.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__metaclass__ = LexerMeta
|
|
||||||
|
|
||||||
def __init__(self, environment):
|
|
||||||
# shortcuts
|
|
||||||
c = lambda x: re.compile(x, re.M | re.S)
|
|
||||||
e = re.escape
|
|
||||||
|
|
||||||
# lexing rules for tags
|
|
||||||
tag_rules = [
|
|
||||||
(whitespace_re, None, None),
|
|
||||||
(float_re, 'float', None),
|
|
||||||
(integer_re, 'integer', None),
|
|
||||||
(name_re, 'name', None),
|
|
||||||
(string_re, 'string', None),
|
|
||||||
(regex_re, 'regex', None),
|
|
||||||
(operator_re, 'operator', None)
|
|
||||||
]
|
|
||||||
|
|
||||||
#: if variables and blocks have the same delimiters we won't
|
|
||||||
#: receive any variable blocks in the parser. This variable is `True`
|
|
||||||
#: if we need that.
|
|
||||||
self.no_variable_block = (
|
|
||||||
(environment.variable_start_string is
|
|
||||||
environment.variable_end_string is None) or
|
|
||||||
(environment.variable_start_string ==
|
|
||||||
environment.block_start_string and
|
|
||||||
environment.variable_end_string ==
|
|
||||||
environment.block_end_string)
|
|
||||||
)
|
|
||||||
|
|
||||||
# assamble the root lexing rule. because "|" is ungreedy
|
|
||||||
# we have to sort by length so that the lexer continues working
|
|
||||||
# as expected when we have parsing rules like <% for block and
|
|
||||||
# <%= for variables. (if someone wants asp like syntax)
|
|
||||||
# variables are just part of the rules if variable processing
|
|
||||||
# is required.
|
|
||||||
root_tag_rules = [
|
|
||||||
('comment', environment.comment_start_string),
|
|
||||||
('block', environment.block_start_string)
|
|
||||||
]
|
|
||||||
if not self.no_variable_block:
|
|
||||||
root_tag_rules.append(('variable',
|
|
||||||
environment.variable_start_string))
|
|
||||||
root_tag_rules.sort(lambda a, b: cmp(len(b[1]), len(a[1])))
|
|
||||||
|
|
||||||
# block suffix if trimming is enabled
|
|
||||||
block_suffix_re = environment.trim_blocks and '\\n?' or ''
|
|
||||||
|
|
||||||
# global lexing rules
|
|
||||||
self.rules = {
|
|
||||||
'root': [
|
|
||||||
# directives
|
|
||||||
(c('(.*?)(?:%s)' % '|'.join(
|
|
||||||
['(?P<raw_begin>(?:\s*%s\-|%s)\s*raw\s*%s)' % (
|
|
||||||
e(environment.block_start_string),
|
|
||||||
e(environment.block_start_string),
|
|
||||||
e(environment.block_end_string)
|
|
||||||
)] + [
|
|
||||||
'(?P<%s_begin>\s*%s\-|%s)' % (n, e(r), e(r))
|
|
||||||
for n, r in root_tag_rules
|
|
||||||
])), ('data', '#bygroup'), '#bygroup'),
|
|
||||||
# data
|
|
||||||
(c('.+'), 'data', None)
|
|
||||||
],
|
|
||||||
# comments
|
|
||||||
'comment_begin': [
|
|
||||||
(c(r'(.*?)((?:\-%s\s*|%s)%s)' % (
|
|
||||||
e(environment.comment_end_string),
|
|
||||||
e(environment.comment_end_string),
|
|
||||||
block_suffix_re
|
|
||||||
)), ('comment', 'comment_end'), '#pop'),
|
|
||||||
(c('(.)'), (Failure('Missing end of comment tag'),), None)
|
|
||||||
],
|
|
||||||
# blocks
|
|
||||||
'block_begin': [
|
|
||||||
(c('(?:\-%s\s*|%s)%s' % (
|
|
||||||
e(environment.block_end_string),
|
|
||||||
e(environment.block_end_string),
|
|
||||||
block_suffix_re
|
|
||||||
)), 'block_end', '#pop'),
|
|
||||||
] + tag_rules,
|
|
||||||
# raw block
|
|
||||||
'raw_begin': [
|
|
||||||
(c('(.*?)((?:\s*%s\-|%s)\s*endraw\s*(?:\-%s\s*|%s%s))' % (
|
|
||||||
e(environment.block_start_string),
|
|
||||||
e(environment.block_start_string),
|
|
||||||
e(environment.block_end_string),
|
|
||||||
e(environment.block_end_string),
|
|
||||||
block_suffix_re
|
|
||||||
)), ('data', 'raw_end'), '#pop'),
|
|
||||||
(c('(.)'), (Failure('Missing end of raw directive'),), None)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
# only add the variable rules to the list if we process variables
|
|
||||||
# the variable_end_string variable could be None and break things.
|
|
||||||
if not self.no_variable_block:
|
|
||||||
self.rules['variable_begin'] = [
|
|
||||||
(c('\-%s\s*|%s' % (
|
|
||||||
e(environment.variable_end_string),
|
|
||||||
e(environment.variable_end_string)
|
|
||||||
)), 'variable_end', '#pop')
|
|
||||||
] + tag_rules
|
|
||||||
|
|
||||||
def tokenize(self, source, filename=None):
|
|
||||||
"""
|
|
||||||
Works like `tokeniter` but returns a tokenstream of tokens and not a
|
|
||||||
generator or token tuples. Additionally all token values are already
|
|
||||||
converted into types and postprocessed. For example keywords are
|
|
||||||
already keyword tokens, not named tokens, comments are removed,
|
|
||||||
integers and floats converted, strings unescaped etc.
|
|
||||||
"""
|
|
||||||
def generate():
|
|
||||||
for lineno, token, value in self.tokeniter(source, filename):
|
|
||||||
if token in ('comment_begin', 'comment', 'comment_end'):
|
|
||||||
continue
|
|
||||||
elif token == 'data':
|
|
||||||
try:
|
|
||||||
value = str(value)
|
|
||||||
except UnicodeError:
|
|
||||||
pass
|
|
||||||
elif token == 'name':
|
|
||||||
value = str(value)
|
|
||||||
if value in keywords:
|
|
||||||
token = value
|
|
||||||
value = ''
|
|
||||||
elif token == 'string':
|
|
||||||
value = unescape_string(lineno, filename, value[1:-1])
|
|
||||||
try:
|
|
||||||
value = str(value)
|
|
||||||
except UnicodeError:
|
|
||||||
pass
|
|
||||||
elif token == 'regex':
|
|
||||||
args = value[value.rfind('/') + 1:]
|
|
||||||
value = unescape_regex(value[2:-(len(args) + 1)])
|
|
||||||
if args:
|
|
||||||
value = '(?%s)%s' % (args, value)
|
|
||||||
elif token == 'integer':
|
|
||||||
value = int(value)
|
|
||||||
elif token == 'float':
|
|
||||||
value = float(value)
|
|
||||||
elif token == 'operator':
|
|
||||||
token = operators[value]
|
|
||||||
value = ''
|
|
||||||
yield Token(lineno, token, value)
|
|
||||||
return TokenStream(generate(), filename)
|
|
||||||
|
|
||||||
def tokeniter(self, source, filename=None):
|
|
||||||
"""
|
|
||||||
This method tokenizes the text and returns the tokens in a generator.
|
|
||||||
Use this method if you just want to tokenize a template. The output
|
|
||||||
you get is not compatible with the input the jinja parser wants. The
|
|
||||||
parser uses the `tokenize` function with returns a `TokenStream` and
|
|
||||||
keywords instead of just names.
|
|
||||||
"""
|
|
||||||
source = '\n'.join(source.splitlines())
|
|
||||||
pos = 0
|
|
||||||
lineno = 1
|
|
||||||
stack = ['root']
|
|
||||||
statetokens = self.rules['root']
|
|
||||||
source_length = len(source)
|
|
||||||
|
|
||||||
balancing_stack = []
|
|
||||||
|
|
||||||
while True:
|
|
||||||
# tokenizer loop
|
|
||||||
for regex, tokens, new_state in statetokens:
|
|
||||||
m = regex.match(source, pos)
|
|
||||||
# if no match we try again with the next rule
|
|
||||||
if not m:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# we only match blocks and variables if brances / parentheses
|
|
||||||
# are balanced. continue parsing with the lower rule which
|
|
||||||
# is the operator rule. do this only if the end tags look
|
|
||||||
# like operators
|
|
||||||
if balancing_stack and \
|
|
||||||
tokens in ('variable_end', 'block_end'):
|
|
||||||
continue
|
|
||||||
|
|
||||||
# tuples support more options
|
|
||||||
if isinstance(tokens, tuple):
|
|
||||||
for idx, token in enumerate(tokens):
|
|
||||||
# hidden group
|
|
||||||
if token is None:
|
|
||||||
g = m.group(idx)
|
|
||||||
if g:
|
|
||||||
lineno += g.count('\n')
|
|
||||||
continue
|
|
||||||
# failure group
|
|
||||||
elif token.__class__ is Failure:
|
|
||||||
raise token(lineno, filename)
|
|
||||||
# bygroup is a bit more complex, in that case we
|
|
||||||
# yield for the current token the first named
|
|
||||||
# group that matched
|
|
||||||
elif token == '#bygroup':
|
|
||||||
for key, value in m.groupdict().iteritems():
|
|
||||||
if value is not None:
|
|
||||||
yield lineno, key, value
|
|
||||||
lineno += value.count('\n')
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise RuntimeError('%r wanted to resolve '
|
|
||||||
'the token dynamically'
|
|
||||||
' but no group matched'
|
|
||||||
% regex)
|
|
||||||
# normal group
|
|
||||||
else:
|
|
||||||
data = m.group(idx + 1)
|
|
||||||
if data:
|
|
||||||
yield lineno, token, data
|
|
||||||
lineno += data.count('\n')
|
|
||||||
|
|
||||||
# strings as token just are yielded as it, but just
|
|
||||||
# if the data is not empty
|
|
||||||
else:
|
|
||||||
data = m.group()
|
|
||||||
# update brace/parentheses balance
|
|
||||||
if tokens == 'operator':
|
|
||||||
if data == '{':
|
|
||||||
balancing_stack.append('}')
|
|
||||||
elif data == '(':
|
|
||||||
balancing_stack.append(')')
|
|
||||||
elif data == '[':
|
|
||||||
balancing_stack.append(']')
|
|
||||||
elif data in ('}', ')', ']'):
|
|
||||||
if not balancing_stack:
|
|
||||||
raise TemplateSyntaxError('unexpected "%s"' %
|
|
||||||
data, lineno,
|
|
||||||
filename)
|
|
||||||
expected_op = balancing_stack.pop()
|
|
||||||
if expected_op != data:
|
|
||||||
raise TemplateSyntaxError('unexpected "%s", '
|
|
||||||
'expected "%s"' %
|
|
||||||
(data, expected_op),
|
|
||||||
lineno, filename)
|
|
||||||
# yield items
|
|
||||||
if tokens is not None:
|
|
||||||
if data:
|
|
||||||
yield lineno, tokens, data
|
|
||||||
lineno += data.count('\n')
|
|
||||||
|
|
||||||
# fetch new position into new variable so that we can check
|
|
||||||
# if there is a internal parsing error which would result
|
|
||||||
# in an infinite loop
|
|
||||||
pos2 = m.end()
|
|
||||||
|
|
||||||
# handle state changes
|
|
||||||
if new_state is not None:
|
|
||||||
# remove the uppermost state
|
|
||||||
if new_state == '#pop':
|
|
||||||
stack.pop()
|
|
||||||
# resolve the new state by group checking
|
|
||||||
elif new_state == '#bygroup':
|
|
||||||
for key, value in m.groupdict().iteritems():
|
|
||||||
if value is not None:
|
|
||||||
stack.append(key)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise RuntimeError('%r wanted to resolve the '
|
|
||||||
'new state dynamically but'
|
|
||||||
' no group matched' %
|
|
||||||
regex)
|
|
||||||
# direct state name given
|
|
||||||
else:
|
|
||||||
stack.append(new_state)
|
|
||||||
statetokens = self.rules[stack[-1]]
|
|
||||||
# we are still at the same position and no stack change.
|
|
||||||
# this means a loop without break condition, avoid that and
|
|
||||||
# raise error
|
|
||||||
elif pos2 == pos:
|
|
||||||
raise RuntimeError('%r yielded empty string without '
|
|
||||||
'stack change' % regex)
|
|
||||||
# publish new function and start again
|
|
||||||
pos = pos2
|
|
||||||
break
|
|
||||||
# if loop terminated without break we havn't found a single match
|
|
||||||
# either we are at the end of the file or we have a problem
|
|
||||||
else:
|
|
||||||
# end of text
|
|
||||||
if pos >= source_length:
|
|
||||||
return
|
|
||||||
# something went wrong
|
|
||||||
raise TemplateSyntaxError('unexpected char %r at %d' %
|
|
||||||
(source[pos], pos), lineno,
|
|
||||||
filename)
|
|
|
@ -1,822 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja.loaders
|
|
||||||
~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Jinja loader classes.
|
|
||||||
|
|
||||||
:copyright: 2007 by Armin Ronacher, Bryan McLemore.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import codecs
|
|
||||||
try:
|
|
||||||
from hashlib import sha1
|
|
||||||
except ImportError:
|
|
||||||
from sha import new as sha1
|
|
||||||
import time
|
|
||||||
from os import path
|
|
||||||
from threading import Lock
|
|
||||||
from jinja.parser import Parser
|
|
||||||
from jinja.translators.python import PythonTranslator, Template
|
|
||||||
from jinja.exceptions import TemplateNotFound, TemplateSyntaxError, \
|
|
||||||
TemplateIncludeError
|
|
||||||
from jinja.utils import CacheDict
|
|
||||||
|
|
||||||
|
|
||||||
#: when updating this, update the listing in the jinja package too
|
|
||||||
__all__ = ['FileSystemLoader', 'PackageLoader', 'DictLoader', 'ChoiceLoader',
|
|
||||||
'FunctionLoader', 'MemcachedFileSystemLoader']
|
|
||||||
|
|
||||||
|
|
||||||
def get_template_filename(searchpath, name):
|
|
||||||
"""
|
|
||||||
Return the filesystem filename wanted.
|
|
||||||
"""
|
|
||||||
return path.join(searchpath, *[p for p in name.split('/')
|
|
||||||
if p and p[0] != '.'])
|
|
||||||
|
|
||||||
|
|
||||||
def get_cachename(cachepath, name, salt=None):
|
|
||||||
"""
|
|
||||||
Return the filename for a cached file.
|
|
||||||
"""
|
|
||||||
return path.join(cachepath, 'jinja_%s.cache' %
|
|
||||||
sha1('jinja(%s|%s)tmpl' %
|
|
||||||
(name, salt or '')).hexdigest())
|
|
||||||
|
|
||||||
|
|
||||||
def _loader_missing(*args, **kwargs):
|
|
||||||
"""Helper function for `LoaderWrapper`."""
|
|
||||||
raise RuntimeError('no loader defined')
|
|
||||||
|
|
||||||
|
|
||||||
class LoaderWrapper(object):
|
|
||||||
"""
|
|
||||||
Wraps a loader so that it's bound to an environment.
|
|
||||||
Also handles template syntax errors.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, environment, loader):
|
|
||||||
self.environment = environment
|
|
||||||
self.loader = loader
|
|
||||||
if self.loader is None:
|
|
||||||
self.get_source = self.parse = self.load = _loader_missing
|
|
||||||
self.available = False
|
|
||||||
else:
|
|
||||||
self.available = True
|
|
||||||
|
|
||||||
def __getattr__(self, name):
|
|
||||||
"""
|
|
||||||
Not found attributes are redirected to the loader
|
|
||||||
"""
|
|
||||||
return getattr(self.loader, name)
|
|
||||||
|
|
||||||
def get_source(self, name, parent=None):
|
|
||||||
"""Retrieve the sourcecode of a template."""
|
|
||||||
# just ascii chars are allowed as template names
|
|
||||||
name = str(name)
|
|
||||||
return self.loader.get_source(self.environment, name, parent)
|
|
||||||
|
|
||||||
def parse(self, name, parent=None):
|
|
||||||
"""Retreive a template and parse it."""
|
|
||||||
# just ascii chars are allowed as template names
|
|
||||||
name = str(name)
|
|
||||||
return self.loader.parse(self.environment, name, parent)
|
|
||||||
|
|
||||||
def load(self, name, translator=PythonTranslator):
|
|
||||||
"""
|
|
||||||
Translate a template and return it. This must not necesarily
|
|
||||||
be a template class. The javascript translator for example
|
|
||||||
will just output a string with the translated code.
|
|
||||||
"""
|
|
||||||
# just ascii chars are allowed as template names
|
|
||||||
name = str(name)
|
|
||||||
try:
|
|
||||||
return self.loader.load(self.environment, name, translator)
|
|
||||||
except TemplateSyntaxError, e:
|
|
||||||
if not self.environment.friendly_traceback:
|
|
||||||
raise
|
|
||||||
__traceback_hide__ = True
|
|
||||||
from jinja.debugger import raise_syntax_error
|
|
||||||
raise_syntax_error(e, self.environment)
|
|
||||||
|
|
||||||
def get_controlled_loader(self):
|
|
||||||
"""
|
|
||||||
Return a loader that runs in a controlled environment. (Keeps
|
|
||||||
track of templates that it loads and is not thread safe).
|
|
||||||
"""
|
|
||||||
return ControlledLoader(self.environment, self.loader)
|
|
||||||
|
|
||||||
def _loader_missing(self, *args, **kwargs):
|
|
||||||
"""Helper method that overrides all other methods if no
|
|
||||||
loader is defined."""
|
|
||||||
raise RuntimeError('no loader defined')
|
|
||||||
|
|
||||||
def __nonzero__(self):
|
|
||||||
return self.available
|
|
||||||
|
|
||||||
|
|
||||||
class ControlledLoader(LoaderWrapper):
|
|
||||||
"""
|
|
||||||
Used for template extending and including.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, environment, loader):
|
|
||||||
LoaderWrapper.__init__(self, environment, loader)
|
|
||||||
self._stack = []
|
|
||||||
|
|
||||||
def get_controlled_loader(self):
|
|
||||||
raise TypeError('Cannot get new controlled loader from an already '
|
|
||||||
'controlled loader.')
|
|
||||||
|
|
||||||
def mark_as_processed(self):
|
|
||||||
"""Mark the last parsed/sourced/included template as processed."""
|
|
||||||
if not self._stack:
|
|
||||||
raise RuntimeError('No template for marking found')
|
|
||||||
self._stack.pop()
|
|
||||||
|
|
||||||
def _controlled(method):
|
|
||||||
def new_method(self, name, *args, **kw):
|
|
||||||
if name in self._stack:
|
|
||||||
raise TemplateIncludeError('Circular imports/extends '
|
|
||||||
'detected. %r appeared twice.' %
|
|
||||||
name)
|
|
||||||
self._stack.append(name)
|
|
||||||
return method(self, name, *args, **kw)
|
|
||||||
try:
|
|
||||||
new_method.__name__ = method.__name__
|
|
||||||
new_method.__doc__ = method.__doc__
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
return new_method
|
|
||||||
|
|
||||||
get_source = _controlled(LoaderWrapper.get_source)
|
|
||||||
parse = _controlled(LoaderWrapper.parse)
|
|
||||||
load = _controlled(LoaderWrapper.load)
|
|
||||||
del _controlled
|
|
||||||
|
|
||||||
|
|
||||||
class BaseLoader(object):
|
|
||||||
"""
|
|
||||||
Use this class to implement loaders.
|
|
||||||
|
|
||||||
Just inherit from this class and implement a method called
|
|
||||||
`get_source` with the signature (`environment`, `name`, `parent`)
|
|
||||||
that returns sourcecode for the template.
|
|
||||||
|
|
||||||
For more complex loaders you probably want to override `load` to
|
|
||||||
or not use the `BaseLoader` at all.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def parse(self, environment, name, parent):
|
|
||||||
"""
|
|
||||||
Load and parse a template
|
|
||||||
"""
|
|
||||||
source = self.get_source(environment, name, parent)
|
|
||||||
return Parser(environment, source, name).parse()
|
|
||||||
|
|
||||||
def load(self, environment, name, translator):
|
|
||||||
"""
|
|
||||||
Load and translate a template
|
|
||||||
"""
|
|
||||||
ast = self.parse(environment, name, None)
|
|
||||||
return translator.process(environment, ast)
|
|
||||||
|
|
||||||
def get_source(self, environment, name, parent):
|
|
||||||
"""
|
|
||||||
Override this method to get the source for a template.
|
|
||||||
"""
|
|
||||||
raise TemplateNotFound(name)
|
|
||||||
|
|
||||||
|
|
||||||
class CachedLoaderMixin(object):
|
|
||||||
"""
|
|
||||||
Mixin this class to implement simple memory and disk caching. The
|
|
||||||
memcaching just uses a dict in the loader so if you have a global
|
|
||||||
environment or at least a global loader this can speed things up.
|
|
||||||
|
|
||||||
If the memcaching is enabled you can use (with Jinja 1.1 onwards)
|
|
||||||
the `clear_memcache` function to clear the cache.
|
|
||||||
|
|
||||||
For memcached support check the `MemcachedLoaderMixin`.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, use_memcache, cache_size, cache_folder, auto_reload,
|
|
||||||
cache_salt=None):
|
|
||||||
if use_memcache:
|
|
||||||
self.__memcache = CacheDict(cache_size)
|
|
||||||
else:
|
|
||||||
self.__memcache = None
|
|
||||||
self.__cache_folder = cache_folder
|
|
||||||
if not hasattr(self, 'check_source_changed'):
|
|
||||||
self.__auto_reload = False
|
|
||||||
else:
|
|
||||||
self.__auto_reload = auto_reload
|
|
||||||
self.__salt = cache_salt
|
|
||||||
self.__times = {}
|
|
||||||
self.__lock = Lock()
|
|
||||||
|
|
||||||
def clear_memcache(self):
|
|
||||||
"""
|
|
||||||
Clears the memcache.
|
|
||||||
"""
|
|
||||||
if self.__memcache is not None:
|
|
||||||
self.__memcache.clear()
|
|
||||||
|
|
||||||
def load(self, environment, name, translator):
|
|
||||||
"""
|
|
||||||
Load and translate a template. First we check if there is a
|
|
||||||
cached version of this template in the memory cache. If this is
|
|
||||||
not the cache check for a compiled template in the disk cache
|
|
||||||
folder. And if none of this is the case we translate the temlate,
|
|
||||||
cache and return it.
|
|
||||||
"""
|
|
||||||
self.__lock.acquire()
|
|
||||||
try:
|
|
||||||
# caching is only possible for the python translator. skip
|
|
||||||
# all other translators
|
|
||||||
if translator is not PythonTranslator:
|
|
||||||
return super(CachedLoaderMixin, self).load(
|
|
||||||
environment, name, translator)
|
|
||||||
|
|
||||||
tmpl = None
|
|
||||||
save_to_disk = False
|
|
||||||
push_to_memory = False
|
|
||||||
|
|
||||||
# auto reload enabled? check for the last change of
|
|
||||||
# the template
|
|
||||||
if self.__auto_reload:
|
|
||||||
last_change = self.check_source_changed(environment, name)
|
|
||||||
else:
|
|
||||||
last_change = None
|
|
||||||
|
|
||||||
# check if we have something in the memory cache and the
|
|
||||||
# memory cache is enabled.
|
|
||||||
if self.__memcache is not None:
|
|
||||||
if name in self.__memcache:
|
|
||||||
tmpl = self.__memcache[name]
|
|
||||||
# if auto reload is enabled check if the template changed
|
|
||||||
if last_change and last_change > self.__times[name]:
|
|
||||||
tmpl = None
|
|
||||||
push_to_memory = True
|
|
||||||
else:
|
|
||||||
push_to_memory = True
|
|
||||||
|
|
||||||
# mem cache disabled or not cached by now
|
|
||||||
# try to load if from the disk cache
|
|
||||||
if tmpl is None and self.__cache_folder is not None:
|
|
||||||
cache_fn = get_cachename(self.__cache_folder, name, self.__salt)
|
|
||||||
if last_change is not None:
|
|
||||||
try:
|
|
||||||
cache_time = path.getmtime(cache_fn)
|
|
||||||
except OSError:
|
|
||||||
cache_time = 0
|
|
||||||
if last_change is None or (cache_time and
|
|
||||||
last_change <= cache_time):
|
|
||||||
try:
|
|
||||||
f = file(cache_fn, 'rb')
|
|
||||||
except IOError:
|
|
||||||
tmpl = None
|
|
||||||
save_to_disk = True
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
tmpl = Template.load(environment, f)
|
|
||||||
finally:
|
|
||||||
f.close()
|
|
||||||
else:
|
|
||||||
save_to_disk = True
|
|
||||||
|
|
||||||
# if we still have no template we load, parse and translate it.
|
|
||||||
if tmpl is None:
|
|
||||||
tmpl = super(CachedLoaderMixin, self).load(
|
|
||||||
environment, name, translator)
|
|
||||||
|
|
||||||
# save the compiled template on the disk if enabled
|
|
||||||
if save_to_disk:
|
|
||||||
f = file(cache_fn, 'wb')
|
|
||||||
try:
|
|
||||||
tmpl.dump(f)
|
|
||||||
finally:
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
# if memcaching is enabled and the template not loaded
|
|
||||||
# we add that there.
|
|
||||||
if push_to_memory:
|
|
||||||
self.__times[name] = time.time()
|
|
||||||
self.__memcache[name] = tmpl
|
|
||||||
return tmpl
|
|
||||||
finally:
|
|
||||||
self.__lock.release()
|
|
||||||
|
|
||||||
|
|
||||||
class MemcachedLoaderMixin(object):
|
|
||||||
"""
|
|
||||||
Uses a memcached server to cache the templates.
|
|
||||||
|
|
||||||
Requires the memcache library from `tummy`_ or the cmemcache library
|
|
||||||
from `Gijsbert de Haan`_.
|
|
||||||
|
|
||||||
With Jinja 1.2 onwards you can also provide a `client` keyword argument
|
|
||||||
that takes an already instanciated memcache client or memcache client
|
|
||||||
like object.
|
|
||||||
|
|
||||||
.. _tummy: http://www.tummy.com/Community/software/python-memcached/
|
|
||||||
.. _Gisjsbert de Haan: http://gijsbert.org/cmemcache/
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, use_memcache, memcache_time=60 * 60 * 24 * 7,
|
|
||||||
memcache_host=None, item_prefix='template/', client=None):
|
|
||||||
if memcache_host is None:
|
|
||||||
memcache_host = ['127.0.0.1:11211']
|
|
||||||
if use_memcache:
|
|
||||||
if client is None:
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
from cmemcache import Client
|
|
||||||
except ImportError:
|
|
||||||
from memcache import Client
|
|
||||||
except ImportError:
|
|
||||||
raise RuntimeError('the %r loader requires an installed '
|
|
||||||
'memcache module' %
|
|
||||||
self.__class__.__name__)
|
|
||||||
client = Client(list(memcache_host))
|
|
||||||
self.__memcache = client
|
|
||||||
self.__memcache_time = memcache_time
|
|
||||||
else:
|
|
||||||
self.__memcache = None
|
|
||||||
self.__item_prefix = item_prefix
|
|
||||||
self.__lock = Lock()
|
|
||||||
|
|
||||||
def load(self, environment, name, translator):
|
|
||||||
"""
|
|
||||||
Load and translate a template. First we check if there is a
|
|
||||||
cached version of this template in the memory cache. If this is
|
|
||||||
not the cache check for a compiled template in the disk cache
|
|
||||||
folder. And if none of this is the case we translate the template,
|
|
||||||
cache and return it.
|
|
||||||
"""
|
|
||||||
self.__lock.acquire()
|
|
||||||
try:
|
|
||||||
# caching is only possible for the python translator. skip
|
|
||||||
# all other translators
|
|
||||||
if translator is not PythonTranslator:
|
|
||||||
return super(MemcachedLoaderMixin, self).load(
|
|
||||||
environment, name, translator)
|
|
||||||
tmpl = None
|
|
||||||
push_to_memory = False
|
|
||||||
|
|
||||||
# check if we have something in the memory cache and the
|
|
||||||
# memory cache is enabled.
|
|
||||||
if self.__memcache is not None:
|
|
||||||
bytecode = self.__memcache.get(self.__item_prefix + name)
|
|
||||||
if bytecode:
|
|
||||||
tmpl = Template.load(environment, bytecode)
|
|
||||||
else:
|
|
||||||
push_to_memory = True
|
|
||||||
|
|
||||||
# if we still have no template we load, parse and translate it.
|
|
||||||
if tmpl is None:
|
|
||||||
tmpl = super(MemcachedLoaderMixin, self).load(
|
|
||||||
environment, name, translator)
|
|
||||||
|
|
||||||
# if memcaching is enabled and the template not loaded
|
|
||||||
# we add that there.
|
|
||||||
if push_to_memory:
|
|
||||||
self.__memcache.set(self.__item_prefix + name, tmpl.dump(),
|
|
||||||
self.__memcache_time)
|
|
||||||
return tmpl
|
|
||||||
finally:
|
|
||||||
self.__lock.release()
|
|
||||||
|
|
||||||
|
|
||||||
class BaseFileSystemLoader(BaseLoader):
|
|
||||||
"""
|
|
||||||
Baseclass for the file system loader that does not do any caching.
|
|
||||||
It exists to avoid redundant code, just don't use it without subclassing.
|
|
||||||
|
|
||||||
How subclassing can work:
|
|
||||||
|
|
||||||
.. sourcecode:: python
|
|
||||||
|
|
||||||
from jinja.loaders import BaseFileSystemLoader
|
|
||||||
|
|
||||||
class MyFileSystemLoader(BaseFileSystemLoader):
|
|
||||||
def __init__(self):
|
|
||||||
BaseFileSystemLoader.__init__(self, '/path/to/templates')
|
|
||||||
|
|
||||||
The base file system loader only takes one parameter beside self which
|
|
||||||
is the path to the templates.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, searchpath):
|
|
||||||
self.searchpath = path.abspath(searchpath)
|
|
||||||
|
|
||||||
def get_source(self, environment, name, parent):
|
|
||||||
filename = get_template_filename(self.searchpath, name)
|
|
||||||
if path.exists(filename):
|
|
||||||
f = codecs.open(filename, 'r', environment.template_charset)
|
|
||||||
try:
|
|
||||||
return f.read()
|
|
||||||
finally:
|
|
||||||
f.close()
|
|
||||||
else:
|
|
||||||
raise TemplateNotFound(name)
|
|
||||||
|
|
||||||
|
|
||||||
class FileSystemLoader(CachedLoaderMixin, BaseFileSystemLoader):
|
|
||||||
"""
|
|
||||||
Loads templates from the filesystem:
|
|
||||||
|
|
||||||
.. sourcecode:: python
|
|
||||||
|
|
||||||
from jinja import Environment, FileSystemLoader
|
|
||||||
e = Environment(loader=FileSystemLoader('templates/'))
|
|
||||||
|
|
||||||
You can pass the following keyword arguments to the loader on
|
|
||||||
initialization:
|
|
||||||
|
|
||||||
=================== =================================================
|
|
||||||
``searchpath`` String with the path to the templates on the
|
|
||||||
filesystem.
|
|
||||||
``use_memcache`` Set this to ``True`` to enable memory caching.
|
|
||||||
This is usually a good idea in production mode,
|
|
||||||
but disable it during development since it won't
|
|
||||||
reload template changes automatically.
|
|
||||||
This only works in persistent environments like
|
|
||||||
FastCGI.
|
|
||||||
``memcache_size`` Number of template instance you want to cache.
|
|
||||||
Defaults to ``40``.
|
|
||||||
``cache_folder`` Set this to an existing directory to enable
|
|
||||||
caching of templates on the file system. Note
|
|
||||||
that this only affects templates transformed
|
|
||||||
into python code. Default is ``None`` which means
|
|
||||||
that caching is disabled.
|
|
||||||
``auto_reload`` Set this to `False` for a slightly better
|
|
||||||
performance. In that case Jinja won't check for
|
|
||||||
template changes on the filesystem.
|
|
||||||
``cache_salt`` Optional unique number to not confuse the
|
|
||||||
caching system when caching more than one
|
|
||||||
template loader in the same folder. Defaults
|
|
||||||
to the searchpath. *New in Jinja 1.1*
|
|
||||||
=================== =================================================
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, searchpath, use_memcache=False, memcache_size=40,
|
|
||||||
cache_folder=None, auto_reload=True, cache_salt=None):
|
|
||||||
BaseFileSystemLoader.__init__(self, searchpath)
|
|
||||||
|
|
||||||
if cache_salt is None:
|
|
||||||
cache_salt = self.searchpath
|
|
||||||
CachedLoaderMixin.__init__(self, use_memcache, memcache_size,
|
|
||||||
cache_folder, auto_reload, cache_salt)
|
|
||||||
|
|
||||||
def check_source_changed(self, environment, name):
|
|
||||||
filename = get_template_filename(self.searchpath, name)
|
|
||||||
if path.exists(filename):
|
|
||||||
return path.getmtime(filename)
|
|
||||||
return -1
|
|
||||||
|
|
||||||
|
|
||||||
class MemcachedFileSystemLoader(MemcachedLoaderMixin, BaseFileSystemLoader):
|
|
||||||
"""
|
|
||||||
Loads templates from the filesystem and caches them on a memcached
|
|
||||||
server.
|
|
||||||
|
|
||||||
.. sourcecode:: python
|
|
||||||
|
|
||||||
from jinja import Environment, MemcachedFileSystemLoader
|
|
||||||
e = Environment(loader=MemcachedFileSystemLoader('templates/',
|
|
||||||
memcache_host=['192.168.2.250:11211']
|
|
||||||
))
|
|
||||||
|
|
||||||
You can pass the following keyword arguments to the loader on
|
|
||||||
initialization:
|
|
||||||
|
|
||||||
=================== =================================================
|
|
||||||
``searchpath`` String with the path to the templates on the
|
|
||||||
filesystem.
|
|
||||||
``use_memcache`` Set this to ``True`` to enable memcached caching.
|
|
||||||
In that case it behaves like a normal
|
|
||||||
`FileSystemLoader` with disabled caching.
|
|
||||||
``memcache_time`` The expire time of a template in the cache.
|
|
||||||
``memcache_host`` a list of memcached servers.
|
|
||||||
``item_prefix`` The prefix for the items on the server. Defaults
|
|
||||||
to ``'template/'``.
|
|
||||||
=================== =================================================
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, searchpath, use_memcache=True,
|
|
||||||
memcache_time=60 * 60 * 24 * 7, memcache_host=None,
|
|
||||||
item_prefix='template/'):
|
|
||||||
BaseFileSystemLoader.__init__(self, searchpath)
|
|
||||||
MemcachedLoaderMixin.__init__(self, use_memcache, memcache_time,
|
|
||||||
memcache_host, item_prefix)
|
|
||||||
|
|
||||||
|
|
||||||
class BasePackageLoader(BaseLoader):
|
|
||||||
"""
|
|
||||||
Baseclass for the package loader that does not do any caching.
|
|
||||||
|
|
||||||
It accepts two parameters: The name of the package and the path relative
|
|
||||||
to the package:
|
|
||||||
|
|
||||||
.. sourcecode:: python
|
|
||||||
|
|
||||||
from jinja.loaders import BasePackageLoader
|
|
||||||
|
|
||||||
class MyPackageLoader(BasePackageLoader):
|
|
||||||
def __init__(self):
|
|
||||||
BasePackageLoader.__init__(self, 'my_package', 'shared/templates')
|
|
||||||
|
|
||||||
The relative path must use slashes as path delimiters, even on Mac OS
|
|
||||||
and Microsoft Windows.
|
|
||||||
|
|
||||||
It uses the `pkg_resources` libraries distributed with setuptools for
|
|
||||||
retrieving the data from the packages. This works for eggs too so you
|
|
||||||
don't have to mark your egg as non zip safe. If pkg_resources is not
|
|
||||||
available it just falls back to path joining relative to the package.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, package_name, package_path, force_native=False):
|
|
||||||
try:
|
|
||||||
import pkg_resources
|
|
||||||
except ImportError:
|
|
||||||
raise RuntimeError('setuptools not installed')
|
|
||||||
self.package_name = package_name
|
|
||||||
self.package_path = package_path
|
|
||||||
self.force_native = force_native
|
|
||||||
|
|
||||||
def _get_load_func(self):
|
|
||||||
if hasattr(self, '_load_func'):
|
|
||||||
return self._load_func
|
|
||||||
try:
|
|
||||||
from pkg_resources import resource_exists, resource_string
|
|
||||||
if self.force_native:
|
|
||||||
raise ImportError()
|
|
||||||
except ImportError:
|
|
||||||
basepath = path.dirname(__import__(self.package_name, None, None,
|
|
||||||
['__file__']).__file__)
|
|
||||||
def load_func(name):
|
|
||||||
filename = path.join(basepath, *(
|
|
||||||
self.package_path.split('/') +
|
|
||||||
[p for p in name.split('/') if p != '..'])
|
|
||||||
)
|
|
||||||
if path.exists(filename):
|
|
||||||
f = file(filename)
|
|
||||||
try:
|
|
||||||
return f.read()
|
|
||||||
finally:
|
|
||||||
f.close()
|
|
||||||
else:
|
|
||||||
def load_func(name):
|
|
||||||
path = '/'.join([self.package_path] + [p for p in name.split('/')
|
|
||||||
if p != '..'])
|
|
||||||
if resource_exists(self.package_name, path):
|
|
||||||
return resource_string(self.package_name, path)
|
|
||||||
self._load_func = load_func
|
|
||||||
return load_func
|
|
||||||
|
|
||||||
def get_source(self, environment, name, parent):
|
|
||||||
load_func = self._get_load_func()
|
|
||||||
contents = load_func(name)
|
|
||||||
if contents is None:
|
|
||||||
raise TemplateNotFound(name)
|
|
||||||
return contents.decode(environment.template_charset)
|
|
||||||
|
|
||||||
|
|
||||||
class PackageLoader(CachedLoaderMixin, BasePackageLoader):
|
|
||||||
"""
|
|
||||||
Loads templates from python packages using setuptools.
|
|
||||||
|
|
||||||
.. sourcecode:: python
|
|
||||||
|
|
||||||
from jinja import Environment, PackageLoader
|
|
||||||
e = Environment(loader=PackageLoader('yourapp', 'template/path'))
|
|
||||||
|
|
||||||
You can pass the following keyword arguments to the loader on
|
|
||||||
initialization:
|
|
||||||
|
|
||||||
=================== =================================================
|
|
||||||
``package_name`` Name of the package containing the templates.
|
|
||||||
``package_path`` Path of the templates inside the package.
|
|
||||||
``use_memcache`` Set this to ``True`` to enable memory caching.
|
|
||||||
This is usually a good idea in production mode,
|
|
||||||
but disable it during development since it won't
|
|
||||||
reload template changes automatically.
|
|
||||||
This only works in persistent environments like
|
|
||||||
FastCGI.
|
|
||||||
``memcache_size`` Number of template instance you want to cache.
|
|
||||||
Defaults to ``40``.
|
|
||||||
``cache_folder`` Set this to an existing directory to enable
|
|
||||||
caching of templates on the file system. Note
|
|
||||||
that this only affects templates transformed
|
|
||||||
into python code. Default is ``None`` which means
|
|
||||||
that caching is disabled.
|
|
||||||
``auto_reload`` Set this to `False` for a slightly better
|
|
||||||
performance. In that case Jinja won't check for
|
|
||||||
template changes on the filesystem. If the
|
|
||||||
templates are inside of an egg file this won't
|
|
||||||
have an effect.
|
|
||||||
``cache_salt`` Optional unique number to not confuse the
|
|
||||||
caching system when caching more than one
|
|
||||||
template loader in the same folder. Defaults
|
|
||||||
to ``package_name + '/' + package_path``.
|
|
||||||
*New in Jinja 1.1*
|
|
||||||
=================== =================================================
|
|
||||||
|
|
||||||
Important note: If you're using an application that is inside of an
|
|
||||||
egg never set `auto_reload` to `True`. The egg resource manager will
|
|
||||||
automatically export files to the file system and touch them so that
|
|
||||||
you not only end up with additional temporary files but also an automatic
|
|
||||||
reload each time you load a template.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, package_name, package_path, use_memcache=False,
|
|
||||||
memcache_size=40, cache_folder=None, auto_reload=True,
|
|
||||||
cache_salt=None):
|
|
||||||
BasePackageLoader.__init__(self, package_name, package_path)
|
|
||||||
|
|
||||||
if cache_salt is None:
|
|
||||||
cache_salt = package_name + '/' + package_path
|
|
||||||
CachedLoaderMixin.__init__(self, use_memcache, memcache_size,
|
|
||||||
cache_folder, auto_reload, cache_salt)
|
|
||||||
|
|
||||||
def check_source_changed(self, environment, name):
|
|
||||||
from pkg_resources import resource_exists, resource_filename
|
|
||||||
fn = resource_filename(self.package_name, '/'.join([self.package_path] +
|
|
||||||
[p for p in name.split('/') if p and p[0] != '.']))
|
|
||||||
if resource_exists(self.package_name, fn):
|
|
||||||
return path.getmtime(fn)
|
|
||||||
return -1
|
|
||||||
|
|
||||||
|
|
||||||
class BaseFunctionLoader(BaseLoader):
|
|
||||||
"""
|
|
||||||
Baseclass for the function loader that doesn't do any caching.
|
|
||||||
|
|
||||||
It just accepts one parameter which is the function which is called
|
|
||||||
with the name of the requested template. If the return value is `None`
|
|
||||||
the loader will raise a `TemplateNotFound` error.
|
|
||||||
|
|
||||||
.. sourcecode:: python
|
|
||||||
|
|
||||||
from jinja.loaders import BaseFunctionLoader
|
|
||||||
|
|
||||||
templates = {...}
|
|
||||||
|
|
||||||
class MyFunctionLoader(BaseFunctionLoader):
|
|
||||||
def __init__(self):
|
|
||||||
BaseFunctionLoader(templates.get)
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, loader_func):
|
|
||||||
self.loader_func = loader_func
|
|
||||||
|
|
||||||
def get_source(self, environment, name, parent):
|
|
||||||
rv = self.loader_func(name)
|
|
||||||
if rv is None:
|
|
||||||
raise TemplateNotFound(name)
|
|
||||||
if isinstance(rv, str):
|
|
||||||
return rv.decode(environment.template_charset)
|
|
||||||
return rv
|
|
||||||
|
|
||||||
|
|
||||||
class FunctionLoader(CachedLoaderMixin, BaseFunctionLoader):
|
|
||||||
"""
|
|
||||||
Loads templates by calling a function which has to return a string
|
|
||||||
or `None` if an error occoured.
|
|
||||||
|
|
||||||
.. sourcecode:: python
|
|
||||||
|
|
||||||
from jinja import Environment, FunctionLoader
|
|
||||||
|
|
||||||
def my_load_func(template_name):
|
|
||||||
if template_name == 'foo':
|
|
||||||
return '...'
|
|
||||||
|
|
||||||
e = Environment(loader=FunctionLoader(my_load_func))
|
|
||||||
|
|
||||||
Because the interface is limited there is no way to cache such
|
|
||||||
templates. Usually you should try to use a loader with a more
|
|
||||||
solid backend.
|
|
||||||
|
|
||||||
You can pass the following keyword arguments to the loader on
|
|
||||||
initialization:
|
|
||||||
|
|
||||||
=================== =================================================
|
|
||||||
``loader_func`` Function that takes the name of the template to
|
|
||||||
load. If it returns a string or unicode object
|
|
||||||
it's used to load a template. If the return
|
|
||||||
value is None it's considered missing.
|
|
||||||
``getmtime_func`` Function used to check if templates requires
|
|
||||||
reloading. Has to return the UNIX timestamp of
|
|
||||||
the last template change or ``-1`` if this template
|
|
||||||
does not exist or requires updates at any cost.
|
|
||||||
``use_memcache`` Set this to ``True`` to enable memory caching.
|
|
||||||
This is usually a good idea in production mode,
|
|
||||||
but disable it during development since it won't
|
|
||||||
reload template changes automatically.
|
|
||||||
This only works in persistent environments like
|
|
||||||
FastCGI.
|
|
||||||
``memcache_size`` Number of template instance you want to cache.
|
|
||||||
Defaults to ``40``.
|
|
||||||
``cache_folder`` Set this to an existing directory to enable
|
|
||||||
caching of templates on the file system. Note
|
|
||||||
that this only affects templates transformed
|
|
||||||
into python code. Default is ``None`` which means
|
|
||||||
that caching is disabled.
|
|
||||||
``auto_reload`` Set this to `False` for a slightly better
|
|
||||||
performance. In that case of `getmtime_func`
|
|
||||||
not being provided this won't have an effect.
|
|
||||||
``cache_salt`` Optional unique number to not confuse the
|
|
||||||
caching system when caching more than one
|
|
||||||
template loader in the same folder.
|
|
||||||
=================== =================================================
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, loader_func, getmtime_func=None, use_memcache=False,
|
|
||||||
memcache_size=40, cache_folder=None, auto_reload=True,
|
|
||||||
cache_salt=None):
|
|
||||||
BaseFunctionLoader.__init__(self, loader_func)
|
|
||||||
# when changing the signature also check the jinja.plugin function
|
|
||||||
# loader instantiation.
|
|
||||||
self.getmtime_func = getmtime_func
|
|
||||||
if auto_reload and getmtime_func is None:
|
|
||||||
auto_reload = False
|
|
||||||
CachedLoaderMixin.__init__(self, use_memcache, memcache_size,
|
|
||||||
cache_folder, auto_reload, cache_salt)
|
|
||||||
|
|
||||||
def check_source_changed(self, environment, name):
|
|
||||||
return self.getmtime_func(name)
|
|
||||||
|
|
||||||
|
|
||||||
class DictLoader(BaseLoader):
|
|
||||||
"""
|
|
||||||
Load templates from a given dict:
|
|
||||||
|
|
||||||
.. sourcecode:: python
|
|
||||||
|
|
||||||
from jinja import Environment, DictLoader
|
|
||||||
e = Environment(loader=DictLoader(dict(
|
|
||||||
layout='...',
|
|
||||||
index='{% extends 'layout' %}...'
|
|
||||||
)))
|
|
||||||
|
|
||||||
This loader does not have any caching capabilities.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, templates):
|
|
||||||
self.templates = templates
|
|
||||||
|
|
||||||
def get_source(self, environment, name, parent):
|
|
||||||
if name in self.templates:
|
|
||||||
return self.templates[name]
|
|
||||||
raise TemplateNotFound(name)
|
|
||||||
|
|
||||||
|
|
||||||
class ChoiceLoader(object):
|
|
||||||
"""
|
|
||||||
A loader that tries multiple loaders in the order they are given to
|
|
||||||
the `ChoiceLoader`:
|
|
||||||
|
|
||||||
.. sourcecode:: python
|
|
||||||
|
|
||||||
from jinja import ChoiceLoader, FileSystemLoader
|
|
||||||
loader1 = FileSystemLoader("templates1")
|
|
||||||
loader2 = FileSystemLoader("templates2")
|
|
||||||
loader = ChoiceLoader([loader1, loader2])
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, loaders):
|
|
||||||
self.loaders = list(loaders)
|
|
||||||
|
|
||||||
def get_source(self, environment, name, parent):
|
|
||||||
for loader in self.loaders:
|
|
||||||
try:
|
|
||||||
return loader.get_source(environment, name, parent)
|
|
||||||
except TemplateNotFound, e:
|
|
||||||
if e.name != name:
|
|
||||||
raise
|
|
||||||
continue
|
|
||||||
raise TemplateNotFound(name)
|
|
||||||
|
|
||||||
def parse(self, environment, name, parent):
|
|
||||||
for loader in self.loaders:
|
|
||||||
try:
|
|
||||||
return loader.parse(environment, name, parent)
|
|
||||||
except TemplateNotFound, e:
|
|
||||||
if e.name != name:
|
|
||||||
raise
|
|
||||||
continue
|
|
||||||
raise TemplateNotFound(name)
|
|
||||||
|
|
||||||
def load(self, environment, name, translator):
|
|
||||||
for loader in self.loaders:
|
|
||||||
try:
|
|
||||||
return loader.load(environment, name, translator)
|
|
||||||
except TemplateNotFound, e:
|
|
||||||
if e.name != name:
|
|
||||||
raise
|
|
||||||
continue
|
|
||||||
raise TemplateNotFound(name)
|
|
|
@ -1,792 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja.nodes
|
|
||||||
~~~~~~~~~~~
|
|
||||||
|
|
||||||
This module implements additional nodes derived from the ast base node.
|
|
||||||
|
|
||||||
It also provides some node tree helper functions like `in_lineno` and
|
|
||||||
`get_nodes` used by the parser and translator in order to normalize
|
|
||||||
python and jinja nodes.
|
|
||||||
|
|
||||||
:copyright: 2007 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
from itertools import chain
|
|
||||||
from copy import copy
|
|
||||||
|
|
||||||
|
|
||||||
def get_nodes(nodetype, tree, exclude_root=True):
|
|
||||||
"""
|
|
||||||
Get all nodes from nodetype in the tree excluding the
|
|
||||||
node passed if `exclude_root` is `True` (default).
|
|
||||||
"""
|
|
||||||
if exclude_root:
|
|
||||||
todo = tree.get_child_nodes()
|
|
||||||
else:
|
|
||||||
todo = [tree]
|
|
||||||
while todo:
|
|
||||||
node = todo.pop()
|
|
||||||
if node.__class__ is nodetype:
|
|
||||||
yield node
|
|
||||||
todo.extend(node.get_child_nodes())
|
|
||||||
|
|
||||||
|
|
||||||
class NotPossible(NotImplementedError):
|
|
||||||
"""
|
|
||||||
If a given node cannot do something.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class Node(object):
|
|
||||||
"""
|
|
||||||
Jinja node.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, lineno=None, filename=None):
|
|
||||||
self.lineno = lineno
|
|
||||||
self.filename = filename
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_child_nodes(self):
|
|
||||||
return [x for x in self.get_items() if isinstance(x, Node)]
|
|
||||||
|
|
||||||
def allows_assignments(self):
|
|
||||||
return False
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'Node()'
|
|
||||||
|
|
||||||
|
|
||||||
class Text(Node):
|
|
||||||
"""
|
|
||||||
Node that represents normal text.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, text, variables, lineno=None, filename=None):
|
|
||||||
Node.__init__(self, lineno, filename)
|
|
||||||
self.text = text
|
|
||||||
self.variables = variables
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return [self.text] + list(self.variables)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'Text(%r, %r)' % (
|
|
||||||
self.text,
|
|
||||||
self.variables
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class NodeList(list, Node):
|
|
||||||
"""
|
|
||||||
A node that stores multiple childnodes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, data, lineno=None, filename=None):
|
|
||||||
Node.__init__(self, lineno, filename)
|
|
||||||
list.__init__(self, data)
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return list(self)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'NodeList(%s)' % list.__repr__(self)
|
|
||||||
|
|
||||||
|
|
||||||
class Template(Node):
|
|
||||||
"""
|
|
||||||
Node that represents a template.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, extends, body, lineno=None, filename=None):
|
|
||||||
Node.__init__(self, lineno, filename)
|
|
||||||
self.extends = extends
|
|
||||||
self.body = body
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return [self.extends, self.body]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'Template(%r, %r)' % (
|
|
||||||
self.extends,
|
|
||||||
self.body
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ForLoop(Node):
|
|
||||||
"""
|
|
||||||
A node that represents a for loop
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, item, seq, body, else_, recursive, lineno=None,
|
|
||||||
filename=None):
|
|
||||||
Node.__init__(self, lineno, filename)
|
|
||||||
self.item = item
|
|
||||||
self.seq = seq
|
|
||||||
self.body = body
|
|
||||||
self.else_ = else_
|
|
||||||
self.recursive = recursive
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return [self.item, self.seq, self.body, self.else_, self.recursive]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'ForLoop(%r, %r, %r, %r, %r)' % (
|
|
||||||
self.item,
|
|
||||||
self.seq,
|
|
||||||
self.body,
|
|
||||||
self.else_,
|
|
||||||
self.recursive
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class IfCondition(Node):
|
|
||||||
"""
|
|
||||||
A node that represents an if condition.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, tests, else_, lineno=None, filename=None):
|
|
||||||
Node.__init__(self, lineno, filename)
|
|
||||||
self.tests = tests
|
|
||||||
self.else_ = else_
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
result = []
|
|
||||||
for test in self.tests:
|
|
||||||
result.extend(test)
|
|
||||||
result.append(self.else_)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'IfCondition(%r, %r)' % (
|
|
||||||
self.tests,
|
|
||||||
self.else_
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Cycle(Node):
|
|
||||||
"""
|
|
||||||
A node that represents the cycle statement.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, seq, lineno=None, filename=None):
|
|
||||||
Node.__init__(self, lineno, filename)
|
|
||||||
self.seq = seq
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return [self.seq]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'Cycle(%r)' % (self.seq,)
|
|
||||||
|
|
||||||
|
|
||||||
class Print(Node):
|
|
||||||
"""
|
|
||||||
A node that represents variable tags and print calls.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, expr, lineno=None, filename=None):
|
|
||||||
Node.__init__(self, lineno, filename)
|
|
||||||
self.expr = expr
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return [self.expr]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'Print(%r)' % (self.expr,)
|
|
||||||
|
|
||||||
|
|
||||||
class Macro(Node):
|
|
||||||
"""
|
|
||||||
A node that represents a macro.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, name, arguments, body, lineno=None, filename=None):
|
|
||||||
Node.__init__(self, lineno, filename)
|
|
||||||
self.name = name
|
|
||||||
self.arguments = arguments
|
|
||||||
self.body = body
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return [self.name] + list(chain(*self.arguments)) + [self.body]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'Macro(%r, %r, %r)' % (
|
|
||||||
self.name,
|
|
||||||
self.arguments,
|
|
||||||
self.body
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Call(Node):
|
|
||||||
"""
|
|
||||||
A node that represents am extended macro call.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, expr, body, lineno=None, filename=None):
|
|
||||||
Node.__init__(self, lineno, filename)
|
|
||||||
self.expr = expr
|
|
||||||
self.body = body
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return [self.expr, self.body]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'Call(%r, %r)' % (
|
|
||||||
self.expr,
|
|
||||||
self.body
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Set(Node):
|
|
||||||
"""
|
|
||||||
Allows defining own variables.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, name, expr, scope_local, lineno=None, filename=None):
|
|
||||||
Node.__init__(self, lineno, filename)
|
|
||||||
self.name = name
|
|
||||||
self.expr = expr
|
|
||||||
self.scope_local = scope_local
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return [self.name, self.expr, self.scope_local]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'Set(%r, %r, %r)' % (
|
|
||||||
self.name,
|
|
||||||
self.expr,
|
|
||||||
self.scope_local
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Filter(Node):
|
|
||||||
"""
|
|
||||||
Node for filter sections.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, body, filters, lineno=None, filename=None):
|
|
||||||
Node.__init__(self, lineno, filename)
|
|
||||||
self.body = body
|
|
||||||
self.filters = filters
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return [self.body] + list(self.filters)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'Filter(%r, %r)' % (
|
|
||||||
self.body,
|
|
||||||
self.filters
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Block(Node):
|
|
||||||
"""
|
|
||||||
A node that represents a block.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, name, body, lineno=None, filename=None):
|
|
||||||
Node.__init__(self, lineno, filename)
|
|
||||||
self.name = name
|
|
||||||
self.body = body
|
|
||||||
|
|
||||||
def replace(self, node):
|
|
||||||
"""
|
|
||||||
Replace the current data with the copied data of another block
|
|
||||||
node.
|
|
||||||
"""
|
|
||||||
assert node.__class__ is Block
|
|
||||||
self.lineno = node.lineno
|
|
||||||
self.filename = node.filename
|
|
||||||
self.name = node.name
|
|
||||||
self.body = copy(node.body)
|
|
||||||
|
|
||||||
def clone(self):
|
|
||||||
"""
|
|
||||||
Create an independent clone of this node.
|
|
||||||
"""
|
|
||||||
return copy(self)
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return [self.name, self.body]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'Block(%r, %r)' % (
|
|
||||||
self.name,
|
|
||||||
self.body
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Include(Node):
|
|
||||||
"""
|
|
||||||
A node that represents the include tag.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, template, lineno=None, filename=None):
|
|
||||||
Node.__init__(self, lineno, filename)
|
|
||||||
self.template = template
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return [self.template]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'Include(%r)' % (
|
|
||||||
self.template
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Trans(Node):
|
|
||||||
"""
|
|
||||||
A node for translatable sections.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, singular, plural, indicator, replacements,
|
|
||||||
lineno=None, filename=None):
|
|
||||||
Node.__init__(self, lineno, filename)
|
|
||||||
self.singular = singular
|
|
||||||
self.plural = plural
|
|
||||||
self.indicator = indicator
|
|
||||||
self.replacements = replacements
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
rv = [self.singular, self.plural, self.indicator]
|
|
||||||
if self.replacements:
|
|
||||||
rv.extend(self.replacements.values())
|
|
||||||
rv.extend(self.replacements.keys())
|
|
||||||
return rv
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'Trans(%r, %r, %r, %r)' % (
|
|
||||||
self.singular,
|
|
||||||
self.plural,
|
|
||||||
self.indicator,
|
|
||||||
self.replacements
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Expression(Node):
|
|
||||||
"""
|
|
||||||
Baseclass for all expressions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class BinaryExpression(Expression):
|
|
||||||
"""
|
|
||||||
Baseclass for all binary expressions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, left, right, lineno=None, filename=None):
|
|
||||||
Expression.__init__(self, lineno, filename)
|
|
||||||
self.left = left
|
|
||||||
self.right = right
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return [self.left, self.right]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '%s(%r, %r)' % (
|
|
||||||
self.__class__.__name__,
|
|
||||||
self.left,
|
|
||||||
self.right
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class UnaryExpression(Expression):
|
|
||||||
"""
|
|
||||||
Baseclass for all unary expressions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, node, lineno=None, filename=None):
|
|
||||||
Expression.__init__(self, lineno, filename)
|
|
||||||
self.node = node
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return [self.node]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '%s(%r)' % (
|
|
||||||
self.__class__.__name__,
|
|
||||||
self.node
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class ConstantExpression(Expression):
|
|
||||||
"""
|
|
||||||
any constat such as {{ "foo" }}
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, value, lineno=None, filename=None):
|
|
||||||
Expression.__init__(self, lineno, filename)
|
|
||||||
self.value = value
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return [self.value]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'ConstantExpression(%r)' % (self.value,)
|
|
||||||
|
|
||||||
|
|
||||||
class UndefinedExpression(Expression):
|
|
||||||
"""
|
|
||||||
represents the special 'undefined' value.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'UndefinedExpression()'
|
|
||||||
|
|
||||||
|
|
||||||
class RegexExpression(Expression):
|
|
||||||
"""
|
|
||||||
represents the regular expression literal.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, value, lineno=None, filename=None):
|
|
||||||
Expression.__init__(self, lineno, filename)
|
|
||||||
self.value = value
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return [self.value]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'RegexExpression(%r)' % (self.value,)
|
|
||||||
|
|
||||||
|
|
||||||
class NameExpression(Expression):
|
|
||||||
"""
|
|
||||||
any name such as {{ foo }}
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, name, lineno=None, filename=None):
|
|
||||||
Expression.__init__(self, lineno, filename)
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return [self.name]
|
|
||||||
|
|
||||||
def allows_assignments(self):
|
|
||||||
return self.name != '_'
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'NameExpression(%r)' % self.name
|
|
||||||
|
|
||||||
|
|
||||||
class ListExpression(Expression):
|
|
||||||
"""
|
|
||||||
any list literal such as {{ [1, 2, 3] }}
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, items, lineno=None, filename=None):
|
|
||||||
Expression.__init__(self, lineno, filename)
|
|
||||||
self.items = items
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return list(self.items)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'ListExpression(%r)' % (self.items,)
|
|
||||||
|
|
||||||
|
|
||||||
class DictExpression(Expression):
|
|
||||||
"""
|
|
||||||
any dict literal such as {{ {1: 2, 3: 4} }}
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, items, lineno=None, filename=None):
|
|
||||||
Expression.__init__(self, lineno, filename)
|
|
||||||
self.items = items
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return list(chain(*self.items))
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'DictExpression(%r)' % (self.items,)
|
|
||||||
|
|
||||||
|
|
||||||
class SetExpression(Expression):
|
|
||||||
"""
|
|
||||||
any set literal such as {{ @(1, 2, 3) }}
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, items, lineno=None, filename=None):
|
|
||||||
Expression.__init__(self, lineno, filename)
|
|
||||||
self.items = items
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return self.items[:]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'SetExpression(%r)' % (self.items,)
|
|
||||||
|
|
||||||
|
|
||||||
class ConditionalExpression(Expression):
|
|
||||||
"""
|
|
||||||
{{ foo if bar else baz }}
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, test, expr1, expr2, lineno=None, filename=None):
|
|
||||||
Expression.__init__(self, lineno, filename)
|
|
||||||
self.test = test
|
|
||||||
self.expr1 = expr1
|
|
||||||
self.expr2 = expr2
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return [self.test, self.expr1, self.expr2]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'ConstantExpression(%r, %r, %r)' % (
|
|
||||||
self.test,
|
|
||||||
self.expr1,
|
|
||||||
self.expr2
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class FilterExpression(Expression):
|
|
||||||
"""
|
|
||||||
{{ foo|bar|baz }}
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, node, filters, lineno=None, filename=None):
|
|
||||||
Expression.__init__(self, lineno, filename)
|
|
||||||
self.node = node
|
|
||||||
self.filters = filters
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
result = [self.node]
|
|
||||||
for filter, args in self.filters:
|
|
||||||
result.append(filter)
|
|
||||||
result.extend(args)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'FilterExpression(%r, %r)' % (
|
|
||||||
self.node,
|
|
||||||
self.filters
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TestExpression(Expression):
|
|
||||||
"""
|
|
||||||
{{ foo is lower }}
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, node, name, args, lineno=None, filename=None):
|
|
||||||
Expression.__init__(self, lineno, filename)
|
|
||||||
self.node = node
|
|
||||||
self.name = name
|
|
||||||
self.args = args
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return [self.node, self.name] + list(self.args)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'TestExpression(%r, %r, %r)' % (
|
|
||||||
self.node,
|
|
||||||
self.name,
|
|
||||||
self.args
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CallExpression(Expression):
|
|
||||||
"""
|
|
||||||
{{ foo(bar) }}
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, node, args, kwargs, dyn_args, dyn_kwargs,
|
|
||||||
lineno=None, filename=None):
|
|
||||||
Expression.__init__(self, lineno, filename)
|
|
||||||
self.node = node
|
|
||||||
self.args = args
|
|
||||||
self.kwargs = kwargs
|
|
||||||
self.dyn_args = dyn_args
|
|
||||||
self.dyn_kwargs = dyn_kwargs
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return [self.node, self.args, self.kwargs, self.dyn_args,
|
|
||||||
self.dyn_kwargs]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'CallExpression(%r, %r, %r, %r, %r)' % (
|
|
||||||
self.node,
|
|
||||||
self.args,
|
|
||||||
self.kwargs,
|
|
||||||
self.dyn_args,
|
|
||||||
self.dyn_kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SubscriptExpression(Expression):
|
|
||||||
"""
|
|
||||||
{{ foo.bar }} and {{ foo['bar'] }} etc.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, node, arg, lineno=None, filename=None):
|
|
||||||
Expression.__init__(self, lineno, filename)
|
|
||||||
self.node = node
|
|
||||||
self.arg = arg
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return [self.node, self.arg]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'SubscriptExpression(%r, %r)' % (
|
|
||||||
self.node,
|
|
||||||
self.arg
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class SliceExpression(Expression):
|
|
||||||
"""
|
|
||||||
1:2:3 etc.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, start, stop, step, lineno=None, filename=None):
|
|
||||||
Expression.__init__(self, lineno, filename)
|
|
||||||
self.start = start
|
|
||||||
self.stop = stop
|
|
||||||
self.step = step
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return [self.start, self.stop, self.step]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'SliceExpression(%r, %r, %r)' % (
|
|
||||||
self.start,
|
|
||||||
self.stop,
|
|
||||||
self.step
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class TupleExpression(Expression):
|
|
||||||
"""
|
|
||||||
For loop unpacking and some other things like multiple arguments
|
|
||||||
for subscripts.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, items, lineno=None, filename=None):
|
|
||||||
Expression.__init__(self, lineno, filename)
|
|
||||||
self.items = items
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return list(self.items)
|
|
||||||
|
|
||||||
def allows_assignments(self):
|
|
||||||
for item in self.items:
|
|
||||||
if not item.allows_assignments():
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'TupleExpression(%r)' % (self.items,)
|
|
||||||
|
|
||||||
|
|
||||||
class ConcatExpression(Expression):
|
|
||||||
"""
|
|
||||||
For {{ foo ~ bar }}. Because of various reasons (especially because
|
|
||||||
unicode conversion takes place for the left and right expression and
|
|
||||||
is better optimized that way)
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, args, lineno=None, filename=None):
|
|
||||||
Expression.__init__(self, lineno, filename)
|
|
||||||
self.args = args
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return list(self.args)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'ConcatExpression(%r)' % (self.items,)
|
|
||||||
|
|
||||||
|
|
||||||
class CompareExpression(Expression):
|
|
||||||
"""
|
|
||||||
{{ foo == bar }}, {{ foo >= bar }} etc.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, expr, ops, lineno=None, filename=None):
|
|
||||||
Expression.__init__(self, lineno, filename)
|
|
||||||
self.expr = expr
|
|
||||||
self.ops = ops
|
|
||||||
|
|
||||||
def get_items(self):
|
|
||||||
return [self.expr] + list(chain(*self.ops))
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'CompareExpression(%r, %r)' % (
|
|
||||||
self.expr,
|
|
||||||
self.ops
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class MulExpression(BinaryExpression):
|
|
||||||
"""
|
|
||||||
{{ foo * bar }}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class DivExpression(BinaryExpression):
|
|
||||||
"""
|
|
||||||
{{ foo / bar }}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class FloorDivExpression(BinaryExpression):
|
|
||||||
"""
|
|
||||||
{{ foo // bar }}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class AddExpression(BinaryExpression):
|
|
||||||
"""
|
|
||||||
{{ foo + bar }}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class SubExpression(BinaryExpression):
|
|
||||||
"""
|
|
||||||
{{ foo - bar }}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class ModExpression(BinaryExpression):
|
|
||||||
"""
|
|
||||||
{{ foo % bar }}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class PowExpression(BinaryExpression):
|
|
||||||
"""
|
|
||||||
{{ foo ** bar }}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class AndExpression(BinaryExpression):
|
|
||||||
"""
|
|
||||||
{{ foo and bar }}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class OrExpression(BinaryExpression):
|
|
||||||
"""
|
|
||||||
{{ foo or bar }}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class NotExpression(UnaryExpression):
|
|
||||||
"""
|
|
||||||
{{ not foo }}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class NegExpression(UnaryExpression):
|
|
||||||
"""
|
|
||||||
{{ -foo }}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class PosExpression(UnaryExpression):
|
|
||||||
"""
|
|
||||||
{{ +foo }}
|
|
||||||
"""
|
|
1187
app/jinja/parser.py
|
@ -1,166 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja.plugin
|
|
||||||
~~~~~~~~~~~~
|
|
||||||
|
|
||||||
Support for the `GeneralTemplateInterface`__ and the Buffet interface.
|
|
||||||
|
|
||||||
Do not use this module on your own. We don't recommend those interfaces!
|
|
||||||
If you are able to, you should really use Jinja without those abstraction
|
|
||||||
layers.
|
|
||||||
|
|
||||||
__ http://trac.pocoo.org/wiki/GeneralTemplateInterface
|
|
||||||
|
|
||||||
:copyright: 2007 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
from warnings import warn
|
|
||||||
from jinja.environment import Environment
|
|
||||||
from jinja.loaders import FunctionLoader, FileSystemLoader, PackageLoader
|
|
||||||
from jinja.exceptions import TemplateNotFound
|
|
||||||
|
|
||||||
|
|
||||||
class BuffetPlugin(object):
|
|
||||||
"""
|
|
||||||
Implements the Jinja buffet plugin. Well. It works for pylons and should
|
|
||||||
work for TurboGears too if their plugin system would work.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, extra_vars_func=None, options=None):
|
|
||||||
if 'jinja.environment' in options:
|
|
||||||
self.env = options['jinja.environment']
|
|
||||||
else:
|
|
||||||
opt = {}
|
|
||||||
for key, value in options.iteritems():
|
|
||||||
if key.startswith('jinja.') and key != 'jinja.extension':
|
|
||||||
opt[key[6:]] = value
|
|
||||||
loader_func = opt.pop('loader_func', None)
|
|
||||||
getmtime_func = opt.pop('getmtime_func', None)
|
|
||||||
use_memcache = opt.pop('use_memcache', False)
|
|
||||||
memcache_size = opt.pop('memcache_size', 40)
|
|
||||||
cache_folder = opt.pop('cache_folder', None)
|
|
||||||
auto_reload = opt.pop('auto_reload', True)
|
|
||||||
if 'searchpath' in opt:
|
|
||||||
opt['loader'] = FileSystemLoader(opt.pop('searchpath'),
|
|
||||||
use_memcache, memcache_size,
|
|
||||||
cache_folder, auto_reload)
|
|
||||||
elif 'package' in opt:
|
|
||||||
opt['loader'] = PackageLoader(opt.pop('package'),
|
|
||||||
opt.pop('package_path', ''),
|
|
||||||
use_memcache, memcache_size,
|
|
||||||
cache_folder, auto_reload)
|
|
||||||
elif loader_func is not None:
|
|
||||||
opt['loader'] = FunctionLoader(loader_func, getmtime_func,
|
|
||||||
use_memcache, memcache_size,
|
|
||||||
cache_folder, auto_reload)
|
|
||||||
self.env = Environment(**opt)
|
|
||||||
|
|
||||||
self.extra_vars_func = extra_vars_func
|
|
||||||
self.extension = options.pop('jinja.extension', 'html')
|
|
||||||
|
|
||||||
def load_template(self, templatename, template_string=None):
|
|
||||||
if template_string is not None:
|
|
||||||
return self.env.from_string(template_string)
|
|
||||||
if templatename.startswith('!'):
|
|
||||||
jinja_name = templatename[1:]
|
|
||||||
else:
|
|
||||||
jinja_name = templatename.replace('.', '/') + '.' + self.extension
|
|
||||||
return self.env.get_template(jinja_name)
|
|
||||||
|
|
||||||
def render(self, info, format='html', fragment=False, template=None):
|
|
||||||
if isinstance(template, basestring):
|
|
||||||
template = self.load_template(template)
|
|
||||||
if self.extra_vars_func:
|
|
||||||
info.update(self.extra_vars_func())
|
|
||||||
return template.render(info)
|
|
||||||
|
|
||||||
|
|
||||||
def jinja_plugin_factory(options):
|
|
||||||
"""
|
|
||||||
Basic implementation of the `GeneralTemplateInterface`.
|
|
||||||
|
|
||||||
Supports ``loader_func`` and ``getmtime_func``, as well as
|
|
||||||
string and file loading but ignores ``mode`` since it's a
|
|
||||||
text based template engine.
|
|
||||||
|
|
||||||
All options passed to this function are forwarded to the
|
|
||||||
jinja environment. Exceptions are the following keys:
|
|
||||||
|
|
||||||
=================== =================================================
|
|
||||||
``environment`` If this is provided it must be the only
|
|
||||||
configuration value and it's used as jinja
|
|
||||||
environment.
|
|
||||||
``searchpath`` If provided a new file system loader with this
|
|
||||||
search path is instanciated.
|
|
||||||
``package`` Name of the python package containing the
|
|
||||||
templates. If this and ``package_path`` is
|
|
||||||
defined a `PackageLoader` is used.
|
|
||||||
``package_path`` Path to the templates inside of a package.
|
|
||||||
``loader_func`` Function that takes the name of the template to
|
|
||||||
load. If it returns a string or unicode object
|
|
||||||
it's used to load a template. If the return
|
|
||||||
value is None it's considered missing.
|
|
||||||
``getmtime_func`` Function used to check if templates requires
|
|
||||||
reloading. Has to return the UNIX timestamp of
|
|
||||||
the last template change or 0 if this template
|
|
||||||
does not exist or requires updates at any cost.
|
|
||||||
``use_memcache`` Set this to ``True`` to enable memory caching.
|
|
||||||
This is usually a good idea in production mode,
|
|
||||||
but disable it during development since it won't
|
|
||||||
reload template changes automatically.
|
|
||||||
This only works in persistent environments like
|
|
||||||
FastCGI.
|
|
||||||
``memcache_size`` Number of template instance you want to cache.
|
|
||||||
Defaults to ``40``.
|
|
||||||
``cache_folder`` Set this to an existing directory to enable
|
|
||||||
caching of templates on the file system. Note
|
|
||||||
that this only affects templates transformed
|
|
||||||
into python code. Default is ``None`` which means
|
|
||||||
that caching is disabled.
|
|
||||||
``auto_reload`` Set this to `False` for a slightly better
|
|
||||||
performance. In that case of `getmtime_func`
|
|
||||||
not being provided this won't have an effect.
|
|
||||||
=================== =================================================
|
|
||||||
"""
|
|
||||||
warn(DeprecationWarning('general plugin interface implementation '
|
|
||||||
'deprecated because not an accepted '
|
|
||||||
'standard.'))
|
|
||||||
|
|
||||||
if 'environment' in options:
|
|
||||||
env = options['environment']
|
|
||||||
if not len(options) == 1:
|
|
||||||
raise TypeError('if environment provided no other '
|
|
||||||
'arguments are allowed')
|
|
||||||
else:
|
|
||||||
loader_func = options.pop('loader_func', None)
|
|
||||||
getmtime_func = options.pop('getmtime_func', None)
|
|
||||||
use_memcache = options.pop('use_memcache', False)
|
|
||||||
memcache_size = options.pop('memcache_size', 40)
|
|
||||||
cache_folder = options.pop('cache_folder', None)
|
|
||||||
auto_reload = options.pop('auto_reload', True)
|
|
||||||
if 'searchpath' in options:
|
|
||||||
options['loader'] = FileSystemLoader(options.pop('searchpath'),
|
|
||||||
use_memcache, memcache_size,
|
|
||||||
cache_folder, auto_reload)
|
|
||||||
elif 'package' in options:
|
|
||||||
options['loader'] = PackageLoader(options.pop('package'),
|
|
||||||
options.pop('package_path', ''),
|
|
||||||
use_memcache, memcache_size,
|
|
||||||
cache_folder, auto_reload)
|
|
||||||
elif loader_func is not None:
|
|
||||||
options['loader'] = FunctionLoader(loader_func, getmtime_func,
|
|
||||||
use_memcache, memcache_size,
|
|
||||||
cache_folder, auto_reload)
|
|
||||||
env = Environment(**options)
|
|
||||||
|
|
||||||
def render_function(template, values, options):
|
|
||||||
if options.get('is_string'):
|
|
||||||
tmpl = env.from_string(template)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
tmpl = env.get_template(template)
|
|
||||||
except TemplateNotFound:
|
|
||||||
return
|
|
||||||
return tmpl.render(**values)
|
|
||||||
|
|
||||||
return render_function
|
|
|
@ -1,143 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja.tests
|
|
||||||
~~~~~~~~~~~
|
|
||||||
|
|
||||||
Jinja test functions. Used with the "is" operator.
|
|
||||||
|
|
||||||
:copyright: 2007 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
number_re = re.compile(r'^-?\d+(\.\d+)?$')
|
|
||||||
regex_type = type(number_re)
|
|
||||||
|
|
||||||
|
|
||||||
def test_odd():
|
|
||||||
"""
|
|
||||||
Return true if the variable is odd.
|
|
||||||
"""
|
|
||||||
return lambda e, c, v: v % 2 == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_even():
|
|
||||||
"""
|
|
||||||
Return true of the variable is even.
|
|
||||||
"""
|
|
||||||
return lambda e, c, v: v % 2 == 0
|
|
||||||
|
|
||||||
|
|
||||||
def test_defined():
|
|
||||||
"""
|
|
||||||
Return true if the variable is defined:
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{% if variable is defined %}
|
|
||||||
value of variable: {{ variable }}
|
|
||||||
{% else %}
|
|
||||||
variable is not defined
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
See also the ``default`` filter.
|
|
||||||
"""
|
|
||||||
return lambda e, c, v: v is not e.undefined_singleton
|
|
||||||
|
|
||||||
|
|
||||||
def test_lower():
|
|
||||||
"""
|
|
||||||
Return true if the variable is lowercase.
|
|
||||||
"""
|
|
||||||
return lambda e, c, v: isinstance(v, basestring) and v.islower()
|
|
||||||
|
|
||||||
|
|
||||||
def test_upper():
|
|
||||||
"""
|
|
||||||
Return true if the variable is uppercase.
|
|
||||||
"""
|
|
||||||
return lambda e, c, v: isinstance(v, basestring) and v.isupper()
|
|
||||||
|
|
||||||
|
|
||||||
def test_numeric():
|
|
||||||
"""
|
|
||||||
Return true if the variable is numeric.
|
|
||||||
"""
|
|
||||||
return lambda e, c, v: isinstance(v, (int, long, float)) or (
|
|
||||||
isinstance(v, basestring) and
|
|
||||||
number_re.match(v) is not None)
|
|
||||||
|
|
||||||
|
|
||||||
def test_sequence():
|
|
||||||
"""
|
|
||||||
Return true if the variable is a sequence. Sequences are variables
|
|
||||||
that are iterable.
|
|
||||||
"""
|
|
||||||
def wrapped(environment, context, value):
|
|
||||||
try:
|
|
||||||
len(value)
|
|
||||||
value.__getitem__
|
|
||||||
except:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def test_matching(regex):
|
|
||||||
r"""
|
|
||||||
Test if the variable matches the regular expression given. Note that
|
|
||||||
you have to escape special chars using *two* backslashes, these are
|
|
||||||
*not* raw strings.
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{% if var is matching @/^\d+$/ %}
|
|
||||||
var looks like a number
|
|
||||||
{% else %}
|
|
||||||
var doesn't really look like a number
|
|
||||||
{% endif %}
|
|
||||||
"""
|
|
||||||
def wrapped(environment, context, value):
|
|
||||||
if type(regex) is regex_type:
|
|
||||||
regex_ = regex
|
|
||||||
else:
|
|
||||||
if environment.disable_regexps:
|
|
||||||
raise RuntimeError('regular expressions disabled.')
|
|
||||||
if isinstance(regex, unicode):
|
|
||||||
regex_ = re.compile(regex, re.U)
|
|
||||||
elif isinstance(regex, str):
|
|
||||||
regex_ = re.compile(regex)
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
return regex_.search(value) is not None
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def test_sameas(other):
|
|
||||||
"""
|
|
||||||
Check if an object points to the same memory address than another
|
|
||||||
object:
|
|
||||||
|
|
||||||
.. sourcecode:: jinja
|
|
||||||
|
|
||||||
{% if foo.attribute is sameas(false) %}
|
|
||||||
the foo attribute really is the `False` singleton
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
*New in Jinja 1.2*
|
|
||||||
"""
|
|
||||||
return lambda e, c, v: v is other
|
|
||||||
|
|
||||||
|
|
||||||
TESTS = {
|
|
||||||
'odd': test_odd,
|
|
||||||
'even': test_even,
|
|
||||||
'defined': test_defined,
|
|
||||||
'lower': test_lower,
|
|
||||||
'upper': test_upper,
|
|
||||||
'numeric': test_numeric,
|
|
||||||
'sequence': test_sequence,
|
|
||||||
'matching': test_matching,
|
|
||||||
'sameas': test_sameas
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja.translators
|
|
||||||
~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
The submodules of this module provide translators for the jinja ast.
|
|
||||||
|
|
||||||
:copyright: 2007 by Armin Ronacher.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class Translator(object):
|
|
||||||
"""
|
|
||||||
Base class of all translators.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def process(environment, tree, source=None):
|
|
||||||
"""
|
|
||||||
Process the given ast with the rules defined in
|
|
||||||
environment and return a translated version of it.
|
|
||||||
The translated object can be anything. The python
|
|
||||||
translator for example outputs Template instances,
|
|
||||||
a javascript translator would probably output strings.
|
|
||||||
|
|
||||||
This is a static function.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
process = staticmethod(process)
|
|
|
@ -1,644 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
"""
|
|
||||||
jinja.utils
|
|
||||||
~~~~~~~~~~~
|
|
||||||
|
|
||||||
Utility functions.
|
|
||||||
|
|
||||||
**license information**: some of the regular expressions and
|
|
||||||
the ``urlize`` function were taken from the django framework.
|
|
||||||
|
|
||||||
:copyright: 2007 by Armin Ronacher, Lawrence Journal-World.
|
|
||||||
:license: BSD, see LICENSE for more details.
|
|
||||||
"""
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import string
|
|
||||||
from types import MethodType, FunctionType
|
|
||||||
from jinja import nodes
|
|
||||||
from jinja.exceptions import SecurityException, TemplateNotFound
|
|
||||||
from jinja.datastructure import TemplateData
|
|
||||||
|
|
||||||
# the python2.4 version of deque is missing the remove method
|
|
||||||
# because a for loop with a lookup for the missing value written
|
|
||||||
# in python is slower we just use deque if we have python2.5 or higher
|
|
||||||
try:
|
|
||||||
from collections import deque
|
|
||||||
deque.remove
|
|
||||||
except (ImportError, AttributeError):
|
|
||||||
class deque(list):
|
|
||||||
"""
|
|
||||||
Minimal subclass of list that provides the deque
|
|
||||||
interface used by the native `BaseContext` and the
|
|
||||||
`CacheDict`
|
|
||||||
"""
|
|
||||||
def appendleft(self, item):
|
|
||||||
list.insert(self, 0, item)
|
|
||||||
def popleft(self):
|
|
||||||
return list.pop(self, 0)
|
|
||||||
def clear(self):
|
|
||||||
del self[:]
|
|
||||||
|
|
||||||
# support for a working reversed() in 2.3
|
|
||||||
try:
|
|
||||||
reversed = reversed
|
|
||||||
except NameError:
|
|
||||||
def reversed(iterable):
|
|
||||||
if hasattr(iterable, '__reversed__'):
|
|
||||||
return iterable.__reversed__()
|
|
||||||
try:
|
|
||||||
return iter(iterable[::-1])
|
|
||||||
except TypeError:
|
|
||||||
return iter(tuple(iterable)[::-1])
|
|
||||||
|
|
||||||
# set support for python 2.3
|
|
||||||
try:
|
|
||||||
set = set
|
|
||||||
except NameError:
|
|
||||||
from sets import Set as set
|
|
||||||
|
|
||||||
# sorted support (just a simplified version)
|
|
||||||
try:
|
|
||||||
sorted = sorted
|
|
||||||
except NameError:
|
|
||||||
_cmp = cmp
|
|
||||||
def sorted(seq, cmp=None, key=None, reverse=False):
|
|
||||||
rv = list(seq)
|
|
||||||
if key is not None:
|
|
||||||
cmp = lambda a, b: _cmp(key(a), key(b))
|
|
||||||
rv.sort(cmp)
|
|
||||||
if reverse:
|
|
||||||
rv.reverse()
|
|
||||||
return rv
|
|
||||||
|
|
||||||
# group by support
|
|
||||||
try:
|
|
||||||
from itertools import groupby
|
|
||||||
except ImportError:
|
|
||||||
class groupby(object):
|
|
||||||
|
|
||||||
def __init__(self, iterable, key=lambda x: x):
|
|
||||||
self.keyfunc = key
|
|
||||||
self.it = iter(iterable)
|
|
||||||
self.tgtkey = self.currkey = self.currvalue = xrange(0)
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def next(self):
|
|
||||||
while self.currkey == self.tgtkey:
|
|
||||||
self.currvalue = self.it.next()
|
|
||||||
self.currkey = self.keyfunc(self.currvalue)
|
|
||||||
self.tgtkey = self.currkey
|
|
||||||
return (self.currkey, self._grouper(self.tgtkey))
|
|
||||||
|
|
||||||
def _grouper(self, tgtkey):
|
|
||||||
while self.currkey == tgtkey:
|
|
||||||
yield self.currvalue
|
|
||||||
self.currvalue = self.it.next()
|
|
||||||
self.currkey = self.keyfunc(self.currvalue)
|
|
||||||
|
|
||||||
#: function types
|
|
||||||
callable_types = (FunctionType, MethodType)
|
|
||||||
|
|
||||||
#: number of maximal range items
|
|
||||||
MAX_RANGE = 1000000
|
|
||||||
|
|
||||||
_word_split_re = re.compile(r'(\s+)')
|
|
||||||
|
|
||||||
_punctuation_re = re.compile(
|
|
||||||
'^(?P<lead>(?:%s)*)(?P<middle>.*?)(?P<trail>(?:%s)*)$' % (
|
|
||||||
'|'.join([re.escape(p) for p in ('(', '<', '<')]),
|
|
||||||
'|'.join([re.escape(p) for p in ('.', ',', ')', '>', '\n', '>')])
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
_simple_email_re = re.compile(r'^\S+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+$')
|
|
||||||
|
|
||||||
#: used by from_string as cache
|
|
||||||
_from_string_env = None
|
|
||||||
|
|
||||||
|
|
||||||
def escape(s, quote=None):
|
|
||||||
"""
|
|
||||||
SGML/XML escape an unicode object.
|
|
||||||
"""
|
|
||||||
s = s.replace("&", "&").replace("<", "<").replace(">", ">")
|
|
||||||
if not quote:
|
|
||||||
return s
|
|
||||||
return s.replace('"', """)
|
|
||||||
|
|
||||||
|
|
||||||
def urlize(text, trim_url_limit=None, nofollow=False):
|
|
||||||
"""
|
|
||||||
Converts any URLs in text into clickable links. Works on http://,
|
|
||||||
https:// and www. links. Links can have trailing punctuation (periods,
|
|
||||||
commas, close-parens) and leading punctuation (opening parens) and
|
|
||||||
it'll still do the right thing.
|
|
||||||
|
|
||||||
If trim_url_limit is not None, the URLs in link text will be limited
|
|
||||||
to trim_url_limit characters.
|
|
||||||
|
|
||||||
If nofollow is True, the URLs in link text will get a rel="nofollow"
|
|
||||||
attribute.
|
|
||||||
"""
|
|
||||||
trim_url = lambda x, limit=trim_url_limit: limit is not None \
|
|
||||||
and (x[:limit] + (len(x) >=limit and '...'
|
|
||||||
or '')) or x
|
|
||||||
words = _word_split_re.split(text)
|
|
||||||
nofollow_attr = nofollow and ' rel="nofollow"' or ''
|
|
||||||
for i, word in enumerate(words):
|
|
||||||
match = _punctuation_re.match(word)
|
|
||||||
if match:
|
|
||||||
lead, middle, trail = match.groups()
|
|
||||||
if middle.startswith('www.') or (
|
|
||||||
'@' not in middle and
|
|
||||||
not middle.startswith('http://') and
|
|
||||||
len(middle) > 0 and
|
|
||||||
middle[0] in string.letters + string.digits and (
|
|
||||||
middle.endswith('.org') or
|
|
||||||
middle.endswith('.net') or
|
|
||||||
middle.endswith('.com')
|
|
||||||
)):
|
|
||||||
middle = '<a href="http://%s"%s>%s</a>' % (middle,
|
|
||||||
nofollow_attr, trim_url(middle))
|
|
||||||
if middle.startswith('http://') or \
|
|
||||||
middle.startswith('https://'):
|
|
||||||
middle = '<a href="%s"%s>%s</a>' % (middle,
|
|
||||||
nofollow_attr, trim_url(middle))
|
|
||||||
if '@' in middle and not middle.startswith('www.') and \
|
|
||||||
not ':' in middle and _simple_email_re.match(middle):
|
|
||||||
middle = '<a href="mailto:%s">%s</a>' % (middle, middle)
|
|
||||||
if lead + middle + trail != word:
|
|
||||||
words[i] = lead + middle + trail
|
|
||||||
return u''.join(words)
|
|
||||||
|
|
||||||
|
|
||||||
def from_string(source):
|
|
||||||
"""
|
|
||||||
Create a template from the template source.
|
|
||||||
"""
|
|
||||||
global _from_string_env
|
|
||||||
if _from_string_env is None:
|
|
||||||
from jinja.environment import Environment
|
|
||||||
_from_string_env = Environment()
|
|
||||||
return _from_string_env.from_string(source)
|
|
||||||
|
|
||||||
|
|
||||||
#: minor speedup
|
|
||||||
_getattr = getattr
|
|
||||||
|
|
||||||
def get_attribute(obj, name):
|
|
||||||
"""
|
|
||||||
Return the attribute from name. Raise either `AttributeError`
|
|
||||||
or `SecurityException` if something goes wrong.
|
|
||||||
"""
|
|
||||||
if not isinstance(name, basestring):
|
|
||||||
raise AttributeError(name)
|
|
||||||
if name[:2] == name[-2:] == '__':
|
|
||||||
raise SecurityException('not allowed to access internal attributes')
|
|
||||||
if getattr(obj, '__class__', None) in callable_types and \
|
|
||||||
name.startswith('func_') or name.startswith('im_'):
|
|
||||||
raise SecurityException('not allowed to access function attributes')
|
|
||||||
r = _getattr(obj, 'jinja_allowed_attributes', None)
|
|
||||||
if r is not None and name not in r:
|
|
||||||
raise SecurityException('disallowed attribute accessed')
|
|
||||||
|
|
||||||
# attribute lookups convert unicode strings to ascii bytestrings.
|
|
||||||
# this process could raise an UnicodeEncodeError.
|
|
||||||
try:
|
|
||||||
return _getattr(obj, name)
|
|
||||||
except UnicodeError:
|
|
||||||
raise AttributeError(name)
|
|
||||||
|
|
||||||
|
|
||||||
def safe_range(start, stop=None, step=None):
|
|
||||||
"""
|
|
||||||
"Safe" form of range that does not generate too large lists.
|
|
||||||
"""
|
|
||||||
if step is None:
|
|
||||||
step = 1
|
|
||||||
if stop is None:
|
|
||||||
r = xrange(0, start, step)
|
|
||||||
else:
|
|
||||||
r = xrange(start, stop, step)
|
|
||||||
if len(r) > MAX_RANGE:
|
|
||||||
def limit():
|
|
||||||
i = 0
|
|
||||||
for item in r:
|
|
||||||
i += 1
|
|
||||||
yield item
|
|
||||||
if i >= MAX_RANGE:
|
|
||||||
break
|
|
||||||
return list(limit())
|
|
||||||
return list(r)
|
|
||||||
|
|
||||||
|
|
||||||
def generate_lorem_ipsum(n=5, html=True, min=20, max=100):
|
|
||||||
"""
|
|
||||||
Generate some lorem impsum for the template.
|
|
||||||
"""
|
|
||||||
from jinja.constants import LOREM_IPSUM_WORDS
|
|
||||||
from random import choice, random, randrange
|
|
||||||
words = LOREM_IPSUM_WORDS.split()
|
|
||||||
result = []
|
|
||||||
|
|
||||||
for _ in xrange(n):
|
|
||||||
next_capitalized = True
|
|
||||||
last_comma = last_fullstop = 0
|
|
||||||
word = None
|
|
||||||
last = None
|
|
||||||
p = []
|
|
||||||
|
|
||||||
# each paragraph contains out of 20 to 100 words.
|
|
||||||
for idx, _ in enumerate(xrange(randrange(min, max))):
|
|
||||||
while True:
|
|
||||||
word = choice(words)
|
|
||||||
if word != last:
|
|
||||||
last = word
|
|
||||||
break
|
|
||||||
if next_capitalized:
|
|
||||||
word = word.capitalize()
|
|
||||||
next_capitalized = False
|
|
||||||
# add commas
|
|
||||||
if idx - randrange(3, 8) > last_comma:
|
|
||||||
last_comma = idx
|
|
||||||
last_fullstop += 2
|
|
||||||
word += ','
|
|
||||||
# add end of sentences
|
|
||||||
if idx - randrange(10, 20) > last_fullstop:
|
|
||||||
last_comma = last_fullstop = idx
|
|
||||||
word += '.'
|
|
||||||
next_capitalized = True
|
|
||||||
p.append(word)
|
|
||||||
|
|
||||||
# ensure that the paragraph ends with a dot.
|
|
||||||
p = u' '.join(p)
|
|
||||||
if p.endswith(','):
|
|
||||||
p = p[:-1] + '.'
|
|
||||||
elif not p.endswith('.'):
|
|
||||||
p += '.'
|
|
||||||
result.append(p)
|
|
||||||
|
|
||||||
if not html:
|
|
||||||
return u'\n\n'.join(result)
|
|
||||||
return u'\n'.join([u'<p>%s</p>' % escape(x) for x in result])
|
|
||||||
|
|
||||||
|
|
||||||
def watch_changes(env, context, iterable, *attributes):
|
|
||||||
"""
|
|
||||||
Wise replacement for ``{% ifchanged %}``.
|
|
||||||
"""
|
|
||||||
# find the attributes to watch
|
|
||||||
if attributes:
|
|
||||||
tests = []
|
|
||||||
tmp = []
|
|
||||||
for attribute in attributes:
|
|
||||||
if isinstance(attribute, (str, unicode, int, long, bool)):
|
|
||||||
tmp.append(attribute)
|
|
||||||
else:
|
|
||||||
tests.append(tuple(attribute))
|
|
||||||
if tmp:
|
|
||||||
tests.append(tuple(attribute))
|
|
||||||
last = tuple([object() for x in tests])
|
|
||||||
# or no attributes if we watch the object itself
|
|
||||||
else:
|
|
||||||
tests = None
|
|
||||||
last = object()
|
|
||||||
|
|
||||||
# iterate trough it and keep check the attributes or values
|
|
||||||
for item in iterable:
|
|
||||||
if tests is None:
|
|
||||||
cur = item
|
|
||||||
else:
|
|
||||||
cur = tuple([env.get_attributes(item, x) for x in tests])
|
|
||||||
if cur != last:
|
|
||||||
changed = True
|
|
||||||
last = cur
|
|
||||||
else:
|
|
||||||
changed = False
|
|
||||||
yield changed, item
|
|
||||||
watch_changes.jinja_context_callable = True
|
|
||||||
|
|
||||||
|
|
||||||
def render_included(env, context, template_name):
|
|
||||||
"""
|
|
||||||
Works like djangos {% include %} tag. It doesn't include the
|
|
||||||
template but load it independently and renders it to a string.
|
|
||||||
"""
|
|
||||||
#XXX: ignores parent completely!
|
|
||||||
tmpl = env.get_template(template_name)
|
|
||||||
return tmpl.render(context.to_dict())
|
|
||||||
render_included.jinja_context_callable = True
|
|
||||||
|
|
||||||
|
|
||||||
# python2.4 and lower has a bug regarding joining of broken generators.
|
|
||||||
# because of the runtime debugging system we have to keep track of the
|
|
||||||
# number of frames to skip. that's what RUNTIME_EXCEPTION_OFFSET is for.
|
|
||||||
try:
|
|
||||||
_test_singleton = object()
|
|
||||||
def _test_gen_bug():
|
|
||||||
raise TypeError(_test_singleton)
|
|
||||||
yield None
|
|
||||||
''.join(_test_gen_bug())
|
|
||||||
except TypeError, e:
|
|
||||||
if e.args and e.args[0] is _test_singleton:
|
|
||||||
capture_generator = u''.join
|
|
||||||
RUNTIME_EXCEPTION_OFFSET = 1
|
|
||||||
else:
|
|
||||||
capture_generator = lambda gen: u''.join(tuple(gen))
|
|
||||||
RUNTIME_EXCEPTION_OFFSET = 2
|
|
||||||
del _test_singleton, _test_gen_bug
|
|
||||||
|
|
||||||
|
|
||||||
def pformat(obj, verbose=False):
|
|
||||||
"""
|
|
||||||
Prettyprint an object. Either use the `pretty` library or the
|
|
||||||
builtin `pprint`.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
from pretty import pretty
|
|
||||||
return pretty(obj, verbose=verbose)
|
|
||||||
except ImportError:
|
|
||||||
from pprint import pformat
|
|
||||||
return pformat(obj)
|
|
||||||
|
|
||||||
|
|
||||||
def buffereater(f, template_data=False):
|
|
||||||
"""
|
|
||||||
Used by the python translator to capture output of substreams.
|
|
||||||
(macros, filter sections etc)
|
|
||||||
"""
|
|
||||||
def wrapped(*a, **kw):
|
|
||||||
__traceback_hide__ = True
|
|
||||||
rv = capture_generator(f(*a, **kw))
|
|
||||||
if template_data:
|
|
||||||
rv = TemplateData(rv)
|
|
||||||
return rv
|
|
||||||
return wrapped
|
|
||||||
|
|
||||||
|
|
||||||
def empty_block(context):
|
|
||||||
"""
|
|
||||||
An empty callable that just returns an empty decorator.
|
|
||||||
Used to represent empty blocks.
|
|
||||||
"""
|
|
||||||
if 0: yield None
|
|
||||||
|
|
||||||
|
|
||||||
def collect_translations(ast):
|
|
||||||
"""
|
|
||||||
Collect all translatable strings for the given ast. The
|
|
||||||
return value is a list of tuples in the form ``(lineno, singular,
|
|
||||||
plural)``. If a translation doesn't require a plural form the
|
|
||||||
third item is `None`.
|
|
||||||
"""
|
|
||||||
todo = [ast]
|
|
||||||
result = []
|
|
||||||
while todo:
|
|
||||||
node = todo.pop()
|
|
||||||
if node.__class__ is nodes.Trans:
|
|
||||||
result.append((node.lineno, node.singular, node.plural))
|
|
||||||
elif node.__class__ is nodes.CallExpression and \
|
|
||||||
node.node.__class__ is nodes.NameExpression and \
|
|
||||||
node.node.name == '_':
|
|
||||||
if len(node.args) == 1 and not node.kwargs and not node.dyn_args \
|
|
||||||
and not node.dyn_kwargs and \
|
|
||||||
node.args[0].__class__ is nodes.ConstantExpression:
|
|
||||||
result.append((node.lineno, node.args[0].value, None))
|
|
||||||
todo.extend(node.get_child_nodes())
|
|
||||||
result.sort(lambda a, b: cmp(a[0], b[0]))
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
class DebugHelper(object):
|
|
||||||
"""
|
|
||||||
Debugging Helper. Available in the template as "debug".
|
|
||||||
"""
|
|
||||||
jinja_context_callable = True
|
|
||||||
jinja_allowed_attributes = ['filters']
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
raise TypeError('cannot create %r instances' %
|
|
||||||
self.__class__.__name__)
|
|
||||||
|
|
||||||
def __call__(self, env, context):
|
|
||||||
"""Print a nice representation of the context."""
|
|
||||||
return pformat(context.to_dict(), verbose=True)
|
|
||||||
|
|
||||||
def filters(self, env, context, builtins=True):
|
|
||||||
"""List the filters."""
|
|
||||||
from inspect import getdoc
|
|
||||||
strip = set()
|
|
||||||
if not builtins:
|
|
||||||
from jinja.defaults import DEFAULT_FILTERS
|
|
||||||
strip = set(DEFAULT_FILTERS.values())
|
|
||||||
filters = env.filters.items()
|
|
||||||
filters.sort(lambda a, b: cmp(a[0].lower(), b[0].lower()))
|
|
||||||
result = []
|
|
||||||
for name, f in filters:
|
|
||||||
if f in strip:
|
|
||||||
continue
|
|
||||||
doc = '\n'.join([' ' + x for x in (getdoc(f) or '').splitlines()])
|
|
||||||
result.append('`%s`\n\n%s' % (name, doc))
|
|
||||||
return '\n\n'.join(result)
|
|
||||||
filters.jinja_context_callable = True
|
|
||||||
|
|
||||||
def tests(self, env, context, builtins=True):
|
|
||||||
"""List the tests."""
|
|
||||||
from inspect import getdoc
|
|
||||||
strip = set()
|
|
||||||
if not builtins:
|
|
||||||
from jinja.defaults import DEFAULT_TESTS
|
|
||||||
strip = set(DEFAULT_TESTS.values())
|
|
||||||
tests = env.tests.items()
|
|
||||||
tests.sort(lambda a, b: cmp(a[0].lower(), b[0].lower()))
|
|
||||||
result = []
|
|
||||||
for name, f in tests:
|
|
||||||
if f in strip:
|
|
||||||
continue
|
|
||||||
doc = '\n'.join([' ' + x for x in (getdoc(f) or '').splitlines()])
|
|
||||||
result.append('`%s`\n\n%s' % (name, doc))
|
|
||||||
return '\n\n'.join(result)
|
|
||||||
tests.jinja_context_callable = True
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
print 'use debug() for debugging the context'
|
|
||||||
|
|
||||||
|
|
||||||
#: the singleton instance of `DebugHelper`
|
|
||||||
debug_helper = object.__new__(DebugHelper)
|
|
||||||
|
|
||||||
|
|
||||||
class CacheDict(object):
|
|
||||||
"""
|
|
||||||
A dict like object that stores a limited number of items and forgets
|
|
||||||
about the least recently used items::
|
|
||||||
|
|
||||||
>>> cache = CacheDict(3)
|
|
||||||
>>> cache['A'] = 0
|
|
||||||
>>> cache['B'] = 1
|
|
||||||
>>> cache['C'] = 2
|
|
||||||
>>> len(cache)
|
|
||||||
3
|
|
||||||
|
|
||||||
If we now access 'A' again it has a higher priority than B::
|
|
||||||
|
|
||||||
>>> cache['A']
|
|
||||||
0
|
|
||||||
|
|
||||||
If we add a new item 'D' now 'B' will disappear::
|
|
||||||
|
|
||||||
>>> cache['D'] = 3
|
|
||||||
>>> len(cache)
|
|
||||||
3
|
|
||||||
>>> 'B' in cache
|
|
||||||
False
|
|
||||||
|
|
||||||
If you iterate over the object the most recently used item will be
|
|
||||||
yielded First::
|
|
||||||
|
|
||||||
>>> for item in cache:
|
|
||||||
... print item
|
|
||||||
D
|
|
||||||
A
|
|
||||||
C
|
|
||||||
|
|
||||||
If you want to iterate the other way round use ``reverse(cache)``.
|
|
||||||
|
|
||||||
Implementation note: This is not a nice way to solve that problem but
|
|
||||||
for smaller capacities it's faster than a linked list.
|
|
||||||
Perfect for template environments where you don't expect too many
|
|
||||||
different keys.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, capacity):
|
|
||||||
self.capacity = capacity
|
|
||||||
self._mapping = {}
|
|
||||||
self._queue = deque()
|
|
||||||
|
|
||||||
# alias all queue methods for faster lookup
|
|
||||||
self._popleft = self._queue.popleft
|
|
||||||
self._pop = self._queue.pop
|
|
||||||
self._remove = self._queue.remove
|
|
||||||
self._append = self._queue.append
|
|
||||||
|
|
||||||
def copy(self):
|
|
||||||
"""
|
|
||||||
Return an shallow copy of the instance.
|
|
||||||
"""
|
|
||||||
rv = CacheDict(self.capacity)
|
|
||||||
rv._mapping.update(self._mapping)
|
|
||||||
rv._queue = self._queue[:]
|
|
||||||
return rv
|
|
||||||
|
|
||||||
def get(self, key, default=None):
|
|
||||||
"""
|
|
||||||
Return an item from the cache dict or `default`
|
|
||||||
"""
|
|
||||||
if key in self:
|
|
||||||
return self[key]
|
|
||||||
return default
|
|
||||||
|
|
||||||
def setdefault(self, key, default=None):
|
|
||||||
"""
|
|
||||||
Set `default` if the key is not in the cache otherwise
|
|
||||||
leave unchanged. Return the value of this key.
|
|
||||||
"""
|
|
||||||
if key in self:
|
|
||||||
return self[key]
|
|
||||||
self[key] = default
|
|
||||||
return default
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
"""
|
|
||||||
Clear the cache dict.
|
|
||||||
"""
|
|
||||||
self._mapping.clear()
|
|
||||||
self._queue.clear()
|
|
||||||
|
|
||||||
def __contains__(self, key):
|
|
||||||
"""
|
|
||||||
Check if a key exists in this cache dict.
|
|
||||||
"""
|
|
||||||
return key in self._mapping
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
"""
|
|
||||||
Return the current size of the cache dict.
|
|
||||||
"""
|
|
||||||
return len(self._mapping)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<%s %r>' % (
|
|
||||||
self.__class__.__name__,
|
|
||||||
self._mapping
|
|
||||||
)
|
|
||||||
|
|
||||||
def __getitem__(self, key):
|
|
||||||
"""
|
|
||||||
Get an item from the cache dict. Moves the item up so that
|
|
||||||
it has the highest priority then.
|
|
||||||
|
|
||||||
Raise an `KeyError` if it does not exist.
|
|
||||||
"""
|
|
||||||
rv = self._mapping[key]
|
|
||||||
if self._queue[-1] != key:
|
|
||||||
self._remove(key)
|
|
||||||
self._append(key)
|
|
||||||
return rv
|
|
||||||
|
|
||||||
def __setitem__(self, key, value):
|
|
||||||
"""
|
|
||||||
Sets the value for an item. Moves the item up so that it
|
|
||||||
has the highest priority then.
|
|
||||||
"""
|
|
||||||
if key in self._mapping:
|
|
||||||
self._remove(key)
|
|
||||||
elif len(self._mapping) == self.capacity:
|
|
||||||
del self._mapping[self._popleft()]
|
|
||||||
self._append(key)
|
|
||||||
self._mapping[key] = value
|
|
||||||
|
|
||||||
def __delitem__(self, key):
|
|
||||||
"""
|
|
||||||
Remove an item from the cache dict.
|
|
||||||
Raise an `KeyError` if it does not exist.
|
|
||||||
"""
|
|
||||||
del self._mapping[key]
|
|
||||||
self._remove(key)
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
"""
|
|
||||||
Iterate over all values in the cache dict, ordered by
|
|
||||||
the most recent usage.
|
|
||||||
"""
|
|
||||||
return reversed(self._queue)
|
|
||||||
|
|
||||||
def __reversed__(self):
|
|
||||||
"""
|
|
||||||
Iterate over the values in the cache dict, oldest items
|
|
||||||
coming first.
|
|
||||||
"""
|
|
||||||
return iter(self._queue)
|
|
||||||
|
|
||||||
__copy__ = copy
|
|
||||||
|
|
||||||
def __deepcopy__(self):
|
|
||||||
"""
|
|
||||||
Return a deep copy of the cache dict.
|
|
||||||
"""
|
|
||||||
from copy import deepcopy
|
|
||||||
rv = CacheDict(self.capacity)
|
|
||||||
rv._mapping = deepcopy(self._mapping)
|
|
||||||
rv._queue = deepcopy(self._queue)
|
|
||||||
return rv
|
|
||||||
|
|
||||||
|
|
||||||
NAMESPACE = {
|
|
||||||
'range': safe_range,
|
|
||||||
'debug': debug_helper,
|
|
||||||
'lipsum': generate_lorem_ipsum,
|
|
||||||
'watchchanges': watch_changes,
|
|
||||||
'rendertemplate': render_included
|
|
||||||
}
|
|
216
app/main.py
|
@ -1,216 +0,0 @@
|
||||||
"""oohEmbed
|
|
||||||
Your one-stop oEmbed provider
|
|
||||||
|
|
||||||
See http://code.google.com/p/oohembed/
|
|
||||||
and http://oohembed.com/
|
|
||||||
|
|
||||||
Copyright (c) 2008-2009, Deepak Sarda
|
|
||||||
|
|
||||||
All rights reserved.
|
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
|
||||||
modification, are permitted provided that the following conditions are
|
|
||||||
met:
|
|
||||||
|
|
||||||
* Redistributions of source code must retain the above copyright
|
|
||||||
notice, this list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
* Redistributions in binary form must reproduce the above
|
|
||||||
copyright notice, this list of conditions and the following
|
|
||||||
disclaimer in the documentation and/or other materials provided
|
|
||||||
with the distribution.
|
|
||||||
|
|
||||||
* Neither the name of the oohEmbed project nor the names of its
|
|
||||||
contributors may be used to endorse or promote products derived
|
|
||||||
from this software without specific prior written permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
||||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
|
||||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
||||||
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
||||||
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
||||||
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
|
||||||
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
|
||||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
||||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import wsgiref.handlers
|
|
||||||
import datetime
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import urllib
|
|
||||||
import email.utils
|
|
||||||
import time
|
|
||||||
|
|
||||||
from google.appengine.ext import webapp
|
|
||||||
from google.appengine.api import memcache
|
|
||||||
|
|
||||||
from jinja import Environment, FileSystemLoader
|
|
||||||
|
|
||||||
from provider import *
|
|
||||||
|
|
||||||
HTTP_DATE_FMT = "%a, %d %b %Y %H:%M:%S GMT"
|
|
||||||
CACHE_SEP = "=+=+="
|
|
||||||
CACHE_TIME = 2*24*60*60
|
|
||||||
|
|
||||||
class EndPoint(webapp.RequestHandler):
|
|
||||||
providers = Provider.get_providers()
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
|
|
||||||
query_url = urllib.unquote(self.request.get('url').encode('utf-8')).strip().rstrip("#")
|
|
||||||
|
|
||||||
resp_format = self.request.get('format', default_value='json')
|
|
||||||
|
|
||||||
logging.debug('Incoming request for %s' % query_url)
|
|
||||||
|
|
||||||
if 'Development' in os.environ['SERVER_SOFTWARE']:
|
|
||||||
self.response.headers['Content-Type'] = 'text/plain; charset=utf-8'
|
|
||||||
else:
|
|
||||||
self.response.headers['Content-Type'] = 'application/json'
|
|
||||||
|
|
||||||
if not query_url:
|
|
||||||
self.error(400)
|
|
||||||
self.response.out.write('Missing url parameter in request')
|
|
||||||
return
|
|
||||||
|
|
||||||
if not resp_format in ['json', 'jsonp']:
|
|
||||||
self.error(501)
|
|
||||||
self.response.out.write('Only json format is currently supported')
|
|
||||||
return
|
|
||||||
|
|
||||||
callback = self.request.get('callback').encode('utf-8')
|
|
||||||
# TODO: Make sure callback name here is a valid Javascript
|
|
||||||
# variable name using a whitelist. Otherwise raise 400
|
|
||||||
|
|
||||||
extra_params = {}
|
|
||||||
if self.request.get('maxwidth'):
|
|
||||||
extra_params['maxwidth'] = self.request.get('maxwidth').encode('utf-8')
|
|
||||||
if self.request.get('maxheight'):
|
|
||||||
extra_params['maxheight'] = self.request.get('maxheight').encode('utf-8')
|
|
||||||
|
|
||||||
# Check memcache
|
|
||||||
cached_item = memcache.get(make_key(query_url, extra_params))
|
|
||||||
if cached_item:
|
|
||||||
logging.debug('Cache hit for url %s' % query_url)
|
|
||||||
resp, timestamp = cached_item.split(CACHE_SEP)
|
|
||||||
self.send_response(resp, last_modified=timestamp, callback=callback)
|
|
||||||
return
|
|
||||||
|
|
||||||
resp = None
|
|
||||||
for p in self.providers:
|
|
||||||
try:
|
|
||||||
resp = p.provide(query_url, extra_params)
|
|
||||||
if resp:
|
|
||||||
# Save to memcache first
|
|
||||||
timestamp = datetime.datetime.now().strftime(HTTP_DATE_FMT)
|
|
||||||
if not memcache.set(make_key(query_url, extra_params), resp + CACHE_SEP + timestamp, time=CACHE_TIME):
|
|
||||||
logging.error('Failed saving cache for url %s' % query_url)
|
|
||||||
else:
|
|
||||||
logging.debug('Saved url response to cache for url %s' % query_url)
|
|
||||||
|
|
||||||
self.send_response(resp, last_modified=timestamp, callback=callback)
|
|
||||||
return
|
|
||||||
except UnsupportedUrlError, e:
|
|
||||||
pass
|
|
||||||
except HTTPError, e:
|
|
||||||
self.error(e.code)
|
|
||||||
if e.content:
|
|
||||||
self.response.out.write(e.content)
|
|
||||||
else:
|
|
||||||
self.response.out.write("Encountered HTTP Error %s fetching " +
|
|
||||||
"url from the remote host" % e.code)
|
|
||||||
return
|
|
||||||
except OohEmbedError, e:
|
|
||||||
logging.error("Throwing OohEmbed error", exc_info=True)
|
|
||||||
self.error(500)
|
|
||||||
return self.response.out.write(e.reason)
|
|
||||||
except Exception, e:
|
|
||||||
logging.error("Throwing 500 error", exc_info=True)
|
|
||||||
self.error(500)
|
|
||||||
return self.response.out.write("Unrecoverable error. Please try again." +
|
|
||||||
" If the error persists, please file a bug at http://oohembed.googlecode.com/")
|
|
||||||
|
|
||||||
self.error(404)
|
|
||||||
self.response.out.write('Could not determine suitable ' +
|
|
||||||
'representation for queried URL')
|
|
||||||
return
|
|
||||||
|
|
||||||
def send_response(self, resp, last_modified=None, callback=None):
|
|
||||||
|
|
||||||
if 'Development' not in os.environ['SERVER_SOFTWARE']:
|
|
||||||
self.response.headers['Expires'] = email.utils.formatdate(time.time() + CACHE_TIME, usegmt=True)
|
|
||||||
self.response.headers['Cache-Control'] = 'max-age=%d' % int(CACHE_TIME)
|
|
||||||
|
|
||||||
if last_modified:
|
|
||||||
if last_modified == self.request.headers.get('If-Modified-Since', '-1'):
|
|
||||||
self.error(304)
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
self.response.headers['Last-Modified'] = last_modified.encode('utf-8')
|
|
||||||
|
|
||||||
if callback:
|
|
||||||
self.response.headers['Content-Type'] = 'text/javascript'
|
|
||||||
resp = '%s(%s);' % (callback, resp)
|
|
||||||
|
|
||||||
self.response.out.write(resp)
|
|
||||||
|
|
||||||
class AdminEndPoint(webapp.RequestHandler):
|
|
||||||
def get(self):
|
|
||||||
if self.request.get('flushcache'):
|
|
||||||
result = memcache.flush_all()
|
|
||||||
response = 'memcache flush ' + (result and 'succeeded' or 'failed')
|
|
||||||
return self.response.out.write(response)
|
|
||||||
elif self.request.get('infocache'):
|
|
||||||
result = memcache.get_stats()
|
|
||||||
if result:
|
|
||||||
body = '<br/>'.join(('<b>%s</b>: %s\n' % t for t in result.iteritems()))
|
|
||||||
else:
|
|
||||||
body = 'failed to retrieve memcache stats'
|
|
||||||
response = '<html><title>Memcache Stats</title><body>' + body
|
|
||||||
return self.response.out.write(response)
|
|
||||||
else:
|
|
||||||
return self.response.out.write('What function did you want? "infocache" or "flushcache"?')
|
|
||||||
|
|
||||||
class MainPage(webapp.RequestHandler):
|
|
||||||
providers = Provider.get_providers()
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
|
|
||||||
providers = [{'title': p.title, 'url': p.url, \
|
|
||||||
'example_url': p.example_url, \
|
|
||||||
'description': p.__doc__} \
|
|
||||||
for p in self.providers]
|
|
||||||
providers.sort(lambda x, y: cmp(x['title'].lower(), y['title'].lower()))
|
|
||||||
|
|
||||||
if 'Development' in os.environ['SERVER_SOFTWARE']:
|
|
||||||
production = False
|
|
||||||
else:
|
|
||||||
production = True
|
|
||||||
|
|
||||||
env = Environment(loader=FileSystemLoader(os.path.dirname(__file__)))
|
|
||||||
tmpl = env.get_template('index.jinja')
|
|
||||||
|
|
||||||
hostname = os.environ['HTTP_HOST'].lower()
|
|
||||||
self.response.out.write(tmpl.render(providers=providers,
|
|
||||||
production=production, hostname=hostname))
|
|
||||||
|
|
||||||
urls = [('/', MainPage),
|
|
||||||
('/oohembed\/?', EndPoint),
|
|
||||||
('/admin/', AdminEndPoint)
|
|
||||||
]
|
|
||||||
|
|
||||||
def main():
|
|
||||||
if 'Development' in os.environ['SERVER_SOFTWARE']:
|
|
||||||
logging.getLogger().setLevel(logging.DEBUG)
|
|
||||||
else:
|
|
||||||
logging.getLogger().setLevel(logging.INFO)
|
|
||||||
|
|
||||||
application = webapp.WSGIApplication(urls, debug=True)
|
|
||||||
wsgiref.handlers.CGIHandler().run(application)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
1929
app/markdown.py
|
@ -1,8 +0,0 @@
|
||||||
from base import Provider
|
|
||||||
from utils import OohEmbedError, UnsupportedUrlError, HTTPError, make_key
|
|
||||||
import photoprovider
|
|
||||||
import videoprovider
|
|
||||||
import linkprovider
|
|
||||||
import oembedprovider
|
|
||||||
|
|
||||||
__all__ = ["Provider", "OohEmbedError", "UnsupportedUrlError", "HTTPError", "make_key"]
|
|
|
@ -1,35 +0,0 @@
|
||||||
import re
|
|
||||||
from google.appengine.api import urlfetch
|
|
||||||
|
|
||||||
"""Plugin infrastructure based on Marty Alchin's post at
|
|
||||||
http://gulopine.gamemusic.org/2008/jan/10/simple-plugin-framework/
|
|
||||||
"""
|
|
||||||
class ProviderMount(type):
|
|
||||||
def __init__(cls, name, bases, attrs):
|
|
||||||
if not hasattr(cls, 'plugins'):
|
|
||||||
cls.plugins = []
|
|
||||||
else:
|
|
||||||
cls.plugins.append(cls)
|
|
||||||
|
|
||||||
def get_providers(self, *args, **kwargs):
|
|
||||||
return [p(*args, **kwargs) for p in self.plugins]
|
|
||||||
|
|
||||||
class Provider(object):
|
|
||||||
"""
|
|
||||||
Mount point for plugins which refer to actions that can be performed.
|
|
||||||
Plugins implementing this reference should provide the following attributes:
|
|
||||||
======== ========================================================
|
|
||||||
title the site/api for which this provider works
|
|
||||||
url friendly url description - ombed.com's configuration URL scheme
|
|
||||||
url_re the regex pattern for the URLs which the provider works for
|
|
||||||
example_url An exemplary URL that this provider should be able to work with
|
|
||||||
======== ========================================================
|
|
||||||
|
|
||||||
With the provided url_re, this class' constructor will create a
|
|
||||||
class attribute named `url_regex`.
|
|
||||||
"""
|
|
||||||
__metaclass__ = ProviderMount
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.__class__.url_regex = re.compile(self.__class__.url_re, re.I|re.UNICODE)
|
|
||||||
|
|
|
@ -1,114 +0,0 @@
|
||||||
[
|
|
||||||
{
|
|
||||||
"url": "http://*.blip.tv/*",
|
|
||||||
"url_re": "blip\\.tv/.+",
|
|
||||||
"example_url": "http://pycon.blip.tv/file/2058801/",
|
|
||||||
"endpoint_url": "http://blip.tv/oembed/",
|
|
||||||
"title": "blip.tv"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "http://*.dailymotion.com/*",
|
|
||||||
"url_re": "dailymotion\\.com/.+",
|
|
||||||
"example_url": "http://www.dailymotion.com/video/x5ioet_phoenix-mars-lander_tech",
|
|
||||||
"endpoint_url": "http://www.dailymotion.com/api/oembed/",
|
|
||||||
"title": "Dailymotion"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "http://*.flickr.com/photos/*",
|
|
||||||
"url_re": "flickr\\.com/photos/[-.\\w@]+/\\d+/?",
|
|
||||||
"example_url": "http://www.flickr.com/photos/fuffer2005/2435339994/",
|
|
||||||
"endpoint_url": "http://www.flickr.com/services/oembed/",
|
|
||||||
"title": "Flickr Photos"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "http://www.hulu.com/watch/*",
|
|
||||||
"url_re": "hulu\\.com/watch/.*",
|
|
||||||
"example_url": "http://www.hulu.com/watch/20807/late-night-with-conan",
|
|
||||||
"endpoint_url": "http://www.hulu.com/api/oembed.json",
|
|
||||||
"title": "Hulu"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "http://*.nfb.ca/film/*",
|
|
||||||
"url_re": "nfb\\.ca/film/[-\\w]+/?",
|
|
||||||
"example_url": "http://www.nfb.ca/film/blackfly/",
|
|
||||||
"endpoint_url": "http://www.nfb.ca/remote/services/oembed/",
|
|
||||||
"title": "National Film Board of Canada"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "http://qik.com/*",
|
|
||||||
"url_re": "qik\\.com/\\w+",
|
|
||||||
"example_url": "http://qik.com/video/86776",
|
|
||||||
"endpoint_url": "http://qik.com/api/oembed.json",
|
|
||||||
"title": "Qik Video"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "http://*.revision3.com/*",
|
|
||||||
"url_re": "revision3\\.com/.+",
|
|
||||||
"example_url": "http://revision3.com/diggnation/2008-04-17xsanned/",
|
|
||||||
"endpoint_url": "http://revision3.com/api/oembed/",
|
|
||||||
"title": "Revision3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "http://*.scribd.com/*",
|
|
||||||
"url_re": "scribd\\.com/.+",
|
|
||||||
"example_url": "http://www.scribd.com/doc/17896323/Indian-Automobile-industryPEST",
|
|
||||||
"endpoint_url": "http://www.scribd.com/services/oembed",
|
|
||||||
"title": "Scribd"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "http://*.viddler.com/explore/*",
|
|
||||||
"url_re": "viddler\\.com/explore/.*/videos/\\w+/?",
|
|
||||||
"example_url": "http://www.viddler.com/explore/engadget/videos/14/",
|
|
||||||
"endpoint_url": "http://lab.viddler.com/services/oembed/",
|
|
||||||
"title": "Viddler Video"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "http://www.vimeo.com/* and http://www.vimeo.com/groups/*/videos/*",
|
|
||||||
"url_re": "vimeo\\.com/.*",
|
|
||||||
"example_url": "http://www.vimeo.com/1211060",
|
|
||||||
"endpoint_url": "http://www.vimeo.com/api/oembed.json",
|
|
||||||
"title": "Vimeo"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "http://*.youtube.com/watch*",
|
|
||||||
"url_re": "youtube\\.com/watch.+v=[\\w-]+&?",
|
|
||||||
"example_url": "http://www.youtube.com/watch?v=vk1HvP7NO5w",
|
|
||||||
"endpoint_url": "http://www.youtube.com/oembed",
|
|
||||||
"title": "YouTube"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "http://dotsub.com/view/*",
|
|
||||||
"url_re": "dotsub\\.com/view/[-\\da-zA-Z]+$",
|
|
||||||
"example_url": "http://dotsub.com/view/10e3cb5e-96c7-4cfb-bcea-8ab11e04e090",
|
|
||||||
"endpoint_url": "http://dotsub.com/services/oembed",
|
|
||||||
"title": "dotSUB.com"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "http://yfrog.(com|ru|com.tr|it|fr|co.il|co.uk|com.pl|pl|eu|us)/*",
|
|
||||||
"url_re": "yfrog\\.(com|ru|com\\.tr|it|fr|co\\.il|co\\.uk|com\\.pl|pl|eu|us)/[a-zA-Z0-9]+$",
|
|
||||||
"example_url": "http://yfrog.com/0wgvcpj",
|
|
||||||
"endpoint_url": "http://www.yfrog.com/api/oembed",
|
|
||||||
"title": "YFrog"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "http://*.clikthrough.com/theater/video/*",
|
|
||||||
"url_re": "clikthrough\\.com/theater/video/\\d+$",
|
|
||||||
"example_url": "http://www.clikthrough.com/theater/video/55",
|
|
||||||
"endpoint_url": "http://clikthrough.com/services/oembed",
|
|
||||||
"title": "Clikthrough"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "http://*.kinomap.com/*",
|
|
||||||
"url_re": "kinomap\\.com/.+",
|
|
||||||
"example_url": "http://www.kinomap.com/kms-vzkpc7",
|
|
||||||
"endpoint_url": "http://www.kinomap.com/oembed",
|
|
||||||
"title": "Kinomap"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "http://*.photobucket.com/albums/*|http://*.photobucket.com/groups/*",
|
|
||||||
"url_re": "photobucket\\.com/(albums|groups)/.+$",
|
|
||||||
"example_url": "http://img.photobucket.com/albums/v211/JAV123/Michael%20Holland%20Candle%20Burning/_MG_5661.jpg",
|
|
||||||
"endpoint_url": "http://photobucket.com/oembed",
|
|
||||||
"title": "Photobucket"
|
|
||||||
}
|
|
||||||
]
|
|
|
@ -1,143 +0,0 @@
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
import urllib
|
|
||||||
|
|
||||||
from django.utils import simplejson as json
|
|
||||||
from BeautifulSoup import BeautifulSoup, NavigableString
|
|
||||||
|
|
||||||
from base import Provider
|
|
||||||
from utils import *
|
|
||||||
|
|
||||||
class TwitterStatusProvider(Provider):
|
|
||||||
"""Provides info on a particular tweet as a link type oEmbed response"""
|
|
||||||
title = 'Twitter Status'
|
|
||||||
url = 'http://twitter.com/*/statuses/*'
|
|
||||||
url_re = r'twitter\.com/(?P<user>\w+)/statuses/(?P<status>\d+)'
|
|
||||||
example_url = 'http://twitter.com/mai_co_jp/statuses/822499364'
|
|
||||||
|
|
||||||
def provide(self, query_url, extra_params=None):
|
|
||||||
matches = self.url_regex.search(query_url)
|
|
||||||
if not matches:
|
|
||||||
raise UnsupportedUrlError()
|
|
||||||
|
|
||||||
fetch_url = 'http://twitter.com/statuses/show/' + matches.group('status') + '.json'
|
|
||||||
|
|
||||||
result = get_url(fetch_url)
|
|
||||||
|
|
||||||
try:
|
|
||||||
parsed = json.loads(result)
|
|
||||||
except:
|
|
||||||
logging.error("error decoding as json. String was\n%s" % result, exc_info=True)
|
|
||||||
raise OohEmbedError("Error decoding response")
|
|
||||||
|
|
||||||
response = {'type': u'link', 'version': u'1.0', 'provider_name': self.title}
|
|
||||||
|
|
||||||
if not 'text' in parsed:
|
|
||||||
raise OohEmbedError("Error decoding response")
|
|
||||||
else:
|
|
||||||
response['title'] = parsed['text']
|
|
||||||
|
|
||||||
if 'user' in parsed:
|
|
||||||
u = parsed['user']
|
|
||||||
if 'name' in u:
|
|
||||||
response['author_name'] = u['name']
|
|
||||||
elif 'screen_name' in u:
|
|
||||||
response['author_name'] = u['screen_name']
|
|
||||||
|
|
||||||
if 'url' in u:
|
|
||||||
response['author_url'] = u['url']
|
|
||||||
|
|
||||||
if 'profile_image_url' in u:
|
|
||||||
response['thumbnail_url'] = u['profile_image_url']
|
|
||||||
response['thumbnail_width'] = 48
|
|
||||||
response['thumbnail_height'] = 48
|
|
||||||
|
|
||||||
json_response = json.dumps(response, ensure_ascii=False, indent=0)
|
|
||||||
return json_response
|
|
||||||
|
|
||||||
|
|
||||||
class WikipediaProvider(Provider):
|
|
||||||
"""Returns lead content from a Wikipedia page as 'html' attribute of link type oEmbed response"""
|
|
||||||
title = 'Wikipedia'
|
|
||||||
url = 'http://*.wikipedia.org/wiki/*'
|
|
||||||
#url_re = r'wikipedia\.org/wiki/(?P<title>[-\w\.\(\)]+)'
|
|
||||||
url_re = r'wikipedia\.org/wiki/(?P<title>[^&=]+)'
|
|
||||||
example_url = 'http://en.wikipedia.org/wiki/Life_on_Mars_(TV_series)'
|
|
||||||
|
|
||||||
def provide(self, query_url, extra_params=None):
|
|
||||||
matches = self.url_regex.search(query_url)
|
|
||||||
if not matches:
|
|
||||||
raise UnsupportedUrlError()
|
|
||||||
|
|
||||||
fetch_url = query_url + '?action=render'
|
|
||||||
|
|
||||||
result = get_url(fetch_url)
|
|
||||||
|
|
||||||
soup = BeautifulSoup(result)
|
|
||||||
|
|
||||||
page = u''
|
|
||||||
count = 0
|
|
||||||
|
|
||||||
for para in soup('p', recursive=False):
|
|
||||||
page += unicode(para)
|
|
||||||
count += 1
|
|
||||||
if count >= 3: break
|
|
||||||
|
|
||||||
response = {'type': u'link', 'version': u'1.0', 'provider_name': self.title}
|
|
||||||
|
|
||||||
page_title = unicode(matches.group('title'), 'utf-8')
|
|
||||||
page_title = urllib.unquote(page_title).replace('_', ' ')
|
|
||||||
|
|
||||||
response['title'] = page_title
|
|
||||||
response['html'] = page
|
|
||||||
|
|
||||||
json_response = json.dumps(response, ensure_ascii=True, indent=0)
|
|
||||||
return json_response
|
|
||||||
|
|
||||||
class WordpressProvider(Provider):
|
|
||||||
"""Returns lead content from a Wordpress.com blog post page as 'html' attribute of link type oEmbed response"""
|
|
||||||
title = 'Wordpress.com'
|
|
||||||
url = 'http://*.wordpress.com/yyyy/mm/dd/*'
|
|
||||||
url_re = r'wordpress\.com/\d{4}/\d{2}/\d{2}/(?P<slug>[-\w\.]+)'
|
|
||||||
example_url = 'http://martinpitt.wordpress.com/2008/05/07/my-computer-discovered-playing-games/'
|
|
||||||
|
|
||||||
def provide(self, query_url, extra_params=None):
|
|
||||||
matches = self.url_regex.search(query_url)
|
|
||||||
if not matches:
|
|
||||||
raise UnsupportedUrlError()
|
|
||||||
|
|
||||||
fetch_url = query_url
|
|
||||||
|
|
||||||
result = get_url(fetch_url)
|
|
||||||
|
|
||||||
soup = BeautifulSoup(result)
|
|
||||||
|
|
||||||
response = {'type': u'link', 'version': u'1.0', 'provider_name': self.title}
|
|
||||||
|
|
||||||
response['title'] = unicode(soup.title.string)
|
|
||||||
|
|
||||||
content = soup.find('div', 'snap_preview')
|
|
||||||
if not content:
|
|
||||||
logging.error("Didn't find any snap_preview node on this page: %s" % query_url)
|
|
||||||
raise OohEmbedError("Could not parse the Wordpress page")
|
|
||||||
|
|
||||||
page = u''
|
|
||||||
count = 1000
|
|
||||||
para = content.first()
|
|
||||||
if not para:
|
|
||||||
logging.error("Didn't find any first paragraph on this page: %s" % query_url)
|
|
||||||
raise OohEmbedError("Could not parse the Wordpress page")
|
|
||||||
|
|
||||||
while len(page) <= count:
|
|
||||||
page += unicode(para)
|
|
||||||
para = para.nextSibling
|
|
||||||
if not para:
|
|
||||||
break
|
|
||||||
|
|
||||||
if len(page) > count:
|
|
||||||
page = page[:count] + ' ...'
|
|
||||||
|
|
||||||
response['html'] = unicode(BeautifulSoup(page))
|
|
||||||
|
|
||||||
json_response = json.dumps(response, ensure_ascii=False, indent=0)
|
|
||||||
return json_response
|
|
|
@ -1,41 +0,0 @@
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import urllib
|
|
||||||
|
|
||||||
from django.utils import simplejson as json
|
|
||||||
|
|
||||||
from base import ProviderMount, Provider
|
|
||||||
from utils import *
|
|
||||||
|
|
||||||
class Proxy():
|
|
||||||
def provide(self, query_url, extra_params=None):
|
|
||||||
matches = self.url_regex.search(query_url)
|
|
||||||
if not matches:
|
|
||||||
raise UnsupportedUrlError()
|
|
||||||
|
|
||||||
params = {'url': query_url, 'format': 'json'}
|
|
||||||
if extra_params:
|
|
||||||
params.update(extra_params)
|
|
||||||
|
|
||||||
fetch_url = self.endpoint_url + urllib.urlencode(params)
|
|
||||||
result = get_url(fetch_url)
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def load_providers():
|
|
||||||
"""Loads OEmbed compliant providers from a json source"""
|
|
||||||
|
|
||||||
fp = open(os.path.join(os.path.split(__file__)[0], "endpoints.json"))
|
|
||||||
|
|
||||||
providers = json.load(fp)
|
|
||||||
|
|
||||||
for provider in providers:
|
|
||||||
provider["endpoint_url"] = provider["endpoint_url"] + "?" # For ease in Proxy.provide()
|
|
||||||
provider["__doc__"] = "Just a proxy for the original oEmbed compliant service"
|
|
||||||
|
|
||||||
clazz_name = ''.join(provider["title"].strip().split()) # remove whitespace
|
|
||||||
clazz_name = str(clazz_name) # coerce to string
|
|
||||||
|
|
||||||
clazz = ProviderMount(clazz_name, (Provider, Proxy), provider)
|
|
||||||
|
|
||||||
load_providers()
|
|
|
@ -1,245 +0,0 @@
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
import urllib
|
|
||||||
import xml.etree.cElementTree as ET
|
|
||||||
import base64
|
|
||||||
import hashlib
|
|
||||||
import hmac
|
|
||||||
import time
|
|
||||||
|
|
||||||
from django.utils import simplejson as json
|
|
||||||
from BeautifulSoup import BeautifulSoup, NavigableString
|
|
||||||
|
|
||||||
from base import Provider
|
|
||||||
from utils import *
|
|
||||||
from secrets import *
|
|
||||||
|
|
||||||
class ImdbProvider(object):
|
|
||||||
"""Photo and some metadata for IMDb movie urls. Check sample response to see what metadata beyond that
|
|
||||||
specified by the oEmbed spec is returned. Note that sometimes, a photo can't be found in which case
|
|
||||||
you will get a link type response."""
|
|
||||||
title = 'IMDb'
|
|
||||||
url = r'http://*.imdb.com/title/tt*/'
|
|
||||||
url_re = r'imdb.com/title/(?P<resource>tt\d{7,7})'
|
|
||||||
example_url = 'http://www.imdb.com/title/tt0468569/'
|
|
||||||
|
|
||||||
IMDB_NS = '{http://webservice.imdb.com/doc/2006-12-15/}'
|
|
||||||
|
|
||||||
def set_value(self, elem, tag, d, key):
|
|
||||||
"""Check `tag` with Element `elem`. If exists, set `text` of tag
|
|
||||||
as value of `key` in dictionary `d`. NOTE: `d` is modified for caller."""
|
|
||||||
|
|
||||||
e = elem.find('.//' + self.IMDB_NS + tag)
|
|
||||||
if e is not None and e.text:
|
|
||||||
d[key] = e.text
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def provide(self, query_url, extra_params=None):
|
|
||||||
matches = self.url_regex.search(query_url)
|
|
||||||
if not matches:
|
|
||||||
raise UnsupportedUrlError()
|
|
||||||
|
|
||||||
resource_id = matches.group('resource')
|
|
||||||
params = urllib.urlencode({'ResourceId': resource_id})
|
|
||||||
|
|
||||||
fetch_url = 'http://cc00.clearspring.com/imdb/LookupTitle?' + params
|
|
||||||
result = get_url(fetch_url)
|
|
||||||
|
|
||||||
response = {'type': u'photo', 'version': u'1.0', 'provider_name': self.title}
|
|
||||||
|
|
||||||
tree = ET.fromstring(result)
|
|
||||||
if not self.set_value(tree, 'Source', response, 'url'):
|
|
||||||
response['type'] = 'link'
|
|
||||||
else:
|
|
||||||
self.set_value(tree, 'Width', response, 'width')
|
|
||||||
self.set_value(tree, 'Height', response, 'height')
|
|
||||||
|
|
||||||
self.set_value(tree, 'Title', response, 'title')
|
|
||||||
self.set_value(tree, 'Year', response, 'year')
|
|
||||||
|
|
||||||
e = tree.find('.//'+self.IMDB_NS+'Director')
|
|
||||||
if e:
|
|
||||||
self.set_value(e, 'Name', response, 'author_name')
|
|
||||||
self.set_value(e, 'NameId', response, 'author_url')
|
|
||||||
|
|
||||||
if self.set_value(tree, 'PlotSummary', response, 'html'):
|
|
||||||
response['html'] = u'<p>' + response['html'] + u'</p>'
|
|
||||||
|
|
||||||
self.set_value(tree, 'Average', response, 'rating')
|
|
||||||
|
|
||||||
json_response = json.dumps(response, ensure_ascii=False, indent=1)
|
|
||||||
return json_response
|
|
||||||
|
|
||||||
|
|
||||||
class AmazonProvider(Provider):
|
|
||||||
"""Product images (and author_name for books) for Amazon products. Will soon honour maxwidth/maxheight"""
|
|
||||||
title = 'Amazon Product Image'
|
|
||||||
url_re = r'amazon\.(?:com|co\.uk|de|ca|jp)/.*/?(?:gp/product|o/ASIN|obidos/ASIN|dp)/(?P<asin>\w{8,11})[/\?]?'
|
|
||||||
url = 'http://*.amazon.(com|co.uk|de|ca|jp)/*/(gp/product|o/ASIN|obidos/ASIN|dp)/*'
|
|
||||||
example_url = 'http://www.amazon.com/Myths-Innovation-Scott-Berkun/dp/0596527055'
|
|
||||||
|
|
||||||
def provide(self, query_url, extra_params=None):
|
|
||||||
matches = self.url_regex.search(query_url)
|
|
||||||
if not matches:
|
|
||||||
raise UnsupportedUrlError()
|
|
||||||
|
|
||||||
params = {'Service': 'AWSECommerceService',
|
|
||||||
'AWSAccessKeyId': AWS_ACCESS_KEY_ID, # Please don't abuse!
|
|
||||||
'AssociateTag': 'antrixnet-20',
|
|
||||||
'Operation': 'ItemLookup',
|
|
||||||
'ResponseGroup': 'Images,ItemAttributes',
|
|
||||||
'Style': 'http://oohembed.com/static/amazon_json.xsl',
|
|
||||||
'ContentType': 'text/javascript',
|
|
||||||
'IdType': 'ASIN',
|
|
||||||
'Timestamp': time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime()), #ISO 8601
|
|
||||||
'ItemId': matches.group('asin')}
|
|
||||||
|
|
||||||
str_to_sign = "GET" + "\n" + "xml-us.amznxslt.com" + "\n" + "/onca/xml" + "\n"
|
|
||||||
str_to_sign = str_to_sign + urllib.urlencode(sorted(params.items())) # All query params sorted
|
|
||||||
|
|
||||||
signature = hmac.new(key=AWS_SECRET_ACCESS_KEY, msg=str_to_sign,
|
|
||||||
digestmod=hashlib.sha256).digest()
|
|
||||||
|
|
||||||
signature = base64.encodestring(signature).strip("\n") # base64.urlsafe_b64encode(signature)
|
|
||||||
|
|
||||||
params['Signature'] = signature # Add the Signature to the query params
|
|
||||||
|
|
||||||
fetch_url = 'http://xml-us.amznxslt.com/onca/xml?' + urllib.urlencode(params)
|
|
||||||
|
|
||||||
result = get_url(fetch_url)
|
|
||||||
|
|
||||||
try:
|
|
||||||
parsed = json.loads(result)
|
|
||||||
except:
|
|
||||||
logging.error("error decoding as json. String was\n%s" % result, exc_info=True)
|
|
||||||
raise OohEmbedError("Error decoding response from Amazon.")
|
|
||||||
|
|
||||||
item = parsed['Item']
|
|
||||||
|
|
||||||
# The returned item contains small, medium and large image details
|
|
||||||
# Each size is in nested dict in `item` with keyname `img_<size>`.
|
|
||||||
# We pick the one we want and move it up to the item dict.
|
|
||||||
|
|
||||||
item.update(item['img_large'])
|
|
||||||
|
|
||||||
# Now we create a response by selecting all needed key/value pairs from `item`.
|
|
||||||
# This mostly means removing `img_*` keys since the size we want is already
|
|
||||||
# in top-level of `item`.
|
|
||||||
# However, sometimes we don't get image details so 'url', 'thumbnail_url', etc.,
|
|
||||||
# attribute values will be empty strings. So we also prune those now.
|
|
||||||
|
|
||||||
selected = dict((k, v) for k, v in item.iteritems()
|
|
||||||
if not k.startswith('img_') and v)
|
|
||||||
|
|
||||||
if not 'url' in selected:
|
|
||||||
# Return a standard Amazon.com logo
|
|
||||||
selected['url'] = \
|
|
||||||
'http://images.amazon.com/images/G/01/x-locale/browse/upf/amzn-logo-5.gif'
|
|
||||||
selected['width'] = 140
|
|
||||||
selected['height'] = 66
|
|
||||||
|
|
||||||
response = {'type': u'photo', 'version': u'1.0', 'provider_name': self.title}
|
|
||||||
response.update(selected)
|
|
||||||
|
|
||||||
# The returned url includes Subscription ID, etc. Replace it.
|
|
||||||
response['author_url'] = query_url
|
|
||||||
|
|
||||||
json_response = json.dumps(response, ensure_ascii=False, indent=1)
|
|
||||||
return json_response
|
|
||||||
|
|
||||||
class TwitPicProvider(Provider):
|
|
||||||
"""Photo and thumbnail for TwitPic.com photos."""
|
|
||||||
title = 'TwitPic'
|
|
||||||
url = r'http://*.twitpic.com/*'
|
|
||||||
url_re = r'twitpic.com/(?P<id>\w+)'
|
|
||||||
example_url = 'http://www.twitpic.com/1pz6z'
|
|
||||||
|
|
||||||
def provide(self, query_url, extra_params=None):
|
|
||||||
matches = self.url_regex.search(query_url)
|
|
||||||
if not matches:
|
|
||||||
raise UnsupportedUrlError()
|
|
||||||
|
|
||||||
photo_url = 'http://twitpic.com/show/full/' + matches.group('id')
|
|
||||||
thumb_url = 'http://twitpic.com/show/thumb/' + matches.group('id')
|
|
||||||
|
|
||||||
response = {'type': u'photo', 'version': u'1.0', 'provider_name': self.title,
|
|
||||||
'thumbnail_url': thumb_url, 'thumbnail_width': 150, 'thumbnail_height': 150,
|
|
||||||
'url': photo_url}
|
|
||||||
|
|
||||||
json_response = json.dumps(response, ensure_ascii=False, indent=1)
|
|
||||||
return json_response
|
|
||||||
|
|
||||||
class PhodroidProvider(Provider):
|
|
||||||
"""Provider for phodroid.com photos."""
|
|
||||||
title = 'Phodroid Photos'
|
|
||||||
url = r'http://*.phodroid.com/*/*/*'
|
|
||||||
url_re = r'phodroid.com/(?P<id>\d\d/\d\d/\w+)/?'
|
|
||||||
example_url = 'http://phodroid.com/09/06/k3q6bd'
|
|
||||||
|
|
||||||
def provide(self, query_url, extra_params=None):
|
|
||||||
matches = self.url_regex.search(query_url)
|
|
||||||
if not matches:
|
|
||||||
raise UnsupportedUrlError()
|
|
||||||
|
|
||||||
photo_url = 'http://s.phodroid.com/' + matches.group('id') + '.jpg'
|
|
||||||
|
|
||||||
response = {'type': u'photo', 'version': u'1.0', 'provider_name': self.title,
|
|
||||||
'url': photo_url}
|
|
||||||
|
|
||||||
json_response = json.dumps(response, ensure_ascii=False, indent=1)
|
|
||||||
return json_response
|
|
||||||
|
|
||||||
class LJAvatarProvider(Provider):
|
|
||||||
"""Avatar image for LiveJournal user. Uses http://ljpic.seacrow.com/"""
|
|
||||||
title = 'LiveJournal UserPic'
|
|
||||||
url = r'http://*.livejournal.com/'
|
|
||||||
url_re = r'(?P<id>\w+).livejournal.com/?$'
|
|
||||||
example_url = 'http://jace.livejournal.com'
|
|
||||||
|
|
||||||
def provide(self, query_url, extra_params=None):
|
|
||||||
matches = self.url_regex.search(query_url)
|
|
||||||
if not matches:
|
|
||||||
raise UnsupportedUrlError()
|
|
||||||
|
|
||||||
fetch_url = 'http://ljpic.seacrow.com/json/' + matches.group('id')
|
|
||||||
result = get_url(fetch_url)
|
|
||||||
|
|
||||||
try:
|
|
||||||
parsed = json.loads(result)
|
|
||||||
except:
|
|
||||||
logging.error("error decoding as json. String was\n%s" % result, exc_info=True)
|
|
||||||
raise OohEmbedError("Error decoding response from LJPic.")
|
|
||||||
|
|
||||||
response = {'type': u'photo', 'version': u'1.0', 'provider_name': self.title,
|
|
||||||
'url': parsed['image'], 'author_name': parsed['name']}
|
|
||||||
|
|
||||||
json_response = json.dumps(response, ensure_ascii=False, indent=1)
|
|
||||||
return json_response
|
|
||||||
|
|
||||||
class XKCDProvider(Provider):
|
|
||||||
"""Provides the comic image link for an xkcd.com comic page"""
|
|
||||||
|
|
||||||
title = 'XKCD Comic'
|
|
||||||
url = r'http://*.xkcd.com/*/'
|
|
||||||
url_re = r'xkcd\.com/\d+/?$'
|
|
||||||
example_url = 'http://xkcd.com/310/'
|
|
||||||
|
|
||||||
def provide(self, query_url, extra_params=None):
|
|
||||||
matches = self.url_regex.search(query_url)
|
|
||||||
if not matches:
|
|
||||||
raise UnsupportedUrlError()
|
|
||||||
|
|
||||||
result = get_url(query_url)
|
|
||||||
|
|
||||||
soup = BeautifulSoup(result)
|
|
||||||
|
|
||||||
photo = soup.find('div', id='contentContainer').find('img')
|
|
||||||
|
|
||||||
response = {'type': u'photo', 'version': u'1.0', 'provider_name': self.title,
|
|
||||||
'url': photo['src'], 'title': photo['alt'], 'author_name': 'Randall Munroe',
|
|
||||||
'author_url': 'http://xkcd.com/'}
|
|
||||||
|
|
||||||
json_response = json.dumps(response, ensure_ascii=False, indent=1)
|
|
||||||
return json_response
|
|
|
@ -1,110 +0,0 @@
|
||||||
import logging
|
|
||||||
import xml.etree.cElementTree as ET
|
|
||||||
from google.appengine.api import urlfetch
|
|
||||||
|
|
||||||
# xml to dict stuff from
|
|
||||||
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/410469
|
|
||||||
class XmlListConfig(list):
|
|
||||||
def __init__(self, aList):
|
|
||||||
for element in aList:
|
|
||||||
if element:
|
|
||||||
# treat like dict
|
|
||||||
if len(element) == 1 or element[0].tag != element[1].tag:
|
|
||||||
self.append(XmlDictConfig(element))
|
|
||||||
# treat like list
|
|
||||||
elif element[0].tag == element[1].tag:
|
|
||||||
self.append(XmlListConfig(element))
|
|
||||||
elif element.text:
|
|
||||||
text = element.text.strip()
|
|
||||||
if text:
|
|
||||||
self.append(text)
|
|
||||||
|
|
||||||
|
|
||||||
class XmlDictConfig(dict):
|
|
||||||
'''
|
|
||||||
Example usage:
|
|
||||||
|
|
||||||
>>> tree = ElementTree.parse('your_file.xml')
|
|
||||||
>>> root = tree.getroot()
|
|
||||||
>>> xmldict = XmlDictConfig(root)
|
|
||||||
|
|
||||||
Or, if you want to use an XML string:
|
|
||||||
|
|
||||||
>>> root = ElementTree.XML(xml_string)
|
|
||||||
>>> xmldict = XmlDictConfig(root)
|
|
||||||
|
|
||||||
And then use xmldict for what it is... a dict.
|
|
||||||
'''
|
|
||||||
def __init__(self, parent_element):
|
|
||||||
if parent_element.items():
|
|
||||||
self.update(dict(parent_element.items()))
|
|
||||||
for element in parent_element:
|
|
||||||
if element:
|
|
||||||
# treat like dict - we assume that if the first two tags
|
|
||||||
# in a series are different, then they are all different.
|
|
||||||
if len(element) == 1 or element[0].tag != element[1].tag:
|
|
||||||
aDict = XmlDictConfig(element)
|
|
||||||
# treat like list - we assume that if the first two tags
|
|
||||||
# in a series are the same, then the rest are the same.
|
|
||||||
else:
|
|
||||||
# here, we put the list in dictionary; the key is the
|
|
||||||
# tag name the list elements all share in common, and
|
|
||||||
# the value is the list itself
|
|
||||||
aDict = {element[0].tag: XmlListConfig(element)}
|
|
||||||
# if the tag has attributes, add those to the dict
|
|
||||||
if element.items():
|
|
||||||
aDict.update(dict(element.items()))
|
|
||||||
self.update({element.tag: aDict})
|
|
||||||
# this assumes that if you've got an attribute in a tag,
|
|
||||||
# you won't be having any text. This may or may not be a
|
|
||||||
# good idea -- time will tell. It works for the way we are
|
|
||||||
# currently doing XML configuration files...
|
|
||||||
elif element.items():
|
|
||||||
self.update({element.tag: dict(element.items())})
|
|
||||||
# finally, if there are no child tags and no attributes, extract
|
|
||||||
# the text
|
|
||||||
else:
|
|
||||||
self.update({element.tag: element.text})
|
|
||||||
|
|
||||||
def xml2dict(xml_string):
|
|
||||||
"""Returns a dictionary representation of xml in string `xml_string`"""
|
|
||||||
|
|
||||||
root = ET.fromstring(xml_string)
|
|
||||||
return XmlDictConfig(root)
|
|
||||||
|
|
||||||
class OohEmbedError(Exception):
|
|
||||||
def __init__(self, value):
|
|
||||||
self.reason = value
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return repr(self.reason)
|
|
||||||
|
|
||||||
class UnsupportedUrlError(OohEmbedError):
|
|
||||||
def __init__(self):
|
|
||||||
super(UnsupportedUrlError, self).__init__("This provider does not support this URL")
|
|
||||||
|
|
||||||
class HTTPError(Exception):
|
|
||||||
def __init__(self, url, code, content=""):
|
|
||||||
self.url = url
|
|
||||||
self.code = code
|
|
||||||
self.content = content
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "HTTPError %s on url %s" % (self.url, self.code)
|
|
||||||
|
|
||||||
def get_url(url):
|
|
||||||
try:
|
|
||||||
result = urlfetch.fetch(url, headers={'User-Agent': 'oohEmbed.com'})
|
|
||||||
if result.status_code != 200:
|
|
||||||
logging.debug('Error code %s while fetching url: %s' % (result.status_code, url))
|
|
||||||
raise HTTPError(url, result.status_code, result.content)
|
|
||||||
else:
|
|
||||||
return result.content
|
|
||||||
except urlfetch.Error, e:
|
|
||||||
logging.warn("Error fetching url %s" % url, exc_info=True)
|
|
||||||
raise OohEmbedError("Error fetching url %s" % query_url)
|
|
||||||
|
|
||||||
|
|
||||||
def make_key(query_url, extra_params):
|
|
||||||
keys = sorted(extra_params.keys())
|
|
||||||
return query_url + "|".join(["%s:%s" % (key, extra_params[key]) for key in keys])
|
|
|
@ -1,308 +0,0 @@
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
import urllib
|
|
||||||
import xml.etree.cElementTree as ET
|
|
||||||
|
|
||||||
from django.utils import simplejson as json
|
|
||||||
import feedparser
|
|
||||||
|
|
||||||
from base import Provider
|
|
||||||
from utils import *
|
|
||||||
from secrets import *
|
|
||||||
|
|
||||||
class YoutubeProvider(object):
|
|
||||||
"""Provides the flash video embed code
|
|
||||||
__NOTE:__ This is deprecated now. Youtube is handled via the upstream
|
|
||||||
oembed provider. See oembedprovider.py """
|
|
||||||
|
|
||||||
title = 'Youtube'
|
|
||||||
url = 'http://*.youtube.com/watch*'
|
|
||||||
url_re = r'youtube\.com/watch.+v=(?P<videoid>[\w-]+)&?'
|
|
||||||
example_url = 'http://www.youtube.com/watch?v=vk1HvP7NO5w'
|
|
||||||
|
|
||||||
def provide(self, query_url, extra_params=None):
|
|
||||||
matches = self.url_regex.search(query_url)
|
|
||||||
if not matches:
|
|
||||||
raise UnsupportedUrlError()
|
|
||||||
|
|
||||||
fetch_url = 'http://gdata.youtube.com/feeds/api/videos/' + matches.group('videoid') + '?alt=json'
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = urlfetch.fetch(fetch_url)
|
|
||||||
if result.status_code != 200:
|
|
||||||
logging.error('Youtube returned error (code %s): "%s" for url: %s' % (result.status_code, result.content, query_url))
|
|
||||||
return None
|
|
||||||
except urlfetch.Error, e:
|
|
||||||
logging.error("error fetching url %s" % query_url, exc_info=True)
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
parsed = json.loads(result.content)
|
|
||||||
entry = parsed['entry']
|
|
||||||
except:
|
|
||||||
logging.error("error decoding as json. String was\n%s" % result.content, exc_info=True)
|
|
||||||
return None
|
|
||||||
|
|
||||||
author_name = entry['author'][0]['name']['$t']
|
|
||||||
author_url = 'http://www.youtube.com/user/' + author_name
|
|
||||||
title = entry['title']['$t']
|
|
||||||
response = {'type': u'video', 'version': u'1.0', 'provider_name': self.title,
|
|
||||||
'title': title, 'author_name': author_name, 'author_url': author_url,
|
|
||||||
'width': 425, 'height': 344}
|
|
||||||
|
|
||||||
thumbnails = entry['media$group']['media$thumbnail']
|
|
||||||
for thumb in thumbnails:
|
|
||||||
if thumb['url'].endswith('1.jpg'):
|
|
||||||
response['thumbnail_url'] = thumb['url']
|
|
||||||
response['thumbnail_width'] = thumb['width']
|
|
||||||
response['thumbnail_height'] = thumb['height']
|
|
||||||
break
|
|
||||||
|
|
||||||
html = "<embed src='http://www.youtube.com/v/%s&fs=1' allowfullscreen='true' " \
|
|
||||||
"type='application/x-shockwave-flash' wmode='transparent' width='425' " \
|
|
||||||
"height='344'></embed>" % matches.group('videoid')
|
|
||||||
|
|
||||||
response['html'] = html
|
|
||||||
|
|
||||||
json_response = json.dumps(response, ensure_ascii=False, indent=1)
|
|
||||||
return json_response
|
|
||||||
|
|
||||||
class MetacafeProvider(Provider):
|
|
||||||
"""Provides the flash video embed code"""
|
|
||||||
title = 'Metacafe'
|
|
||||||
url = 'http://*.metacafe.com/watch/*'
|
|
||||||
url_re = r'metacafe\.com/watch/(?P<videoid>[-\w]+)/.+'
|
|
||||||
example_url = 'http://www.metacafe.com/watch/1350976/funny_call/'
|
|
||||||
|
|
||||||
def provide(self, query_url, extra_params=None):
|
|
||||||
matches = self.url_regex.search(query_url)
|
|
||||||
if not matches:
|
|
||||||
raise UnsupportedUrlError()
|
|
||||||
|
|
||||||
fetch_url = 'http://www.metacafe.com/api/item/' + matches.group('videoid') + '/'
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = urlfetch.fetch(fetch_url)
|
|
||||||
if result.status_code != 200:
|
|
||||||
logging.error('Metacafe returned error (code %s): "%s" for url: %s' % (result.status_code, result.content, query_url))
|
|
||||||
return None
|
|
||||||
except urlfetch.Error, e:
|
|
||||||
logging.error("error fetching url %s" % query_url, exc_info=True)
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
parsed = feedparser.parse(result.content)
|
|
||||||
entry = parsed['entries'][0]
|
|
||||||
except:
|
|
||||||
logging.error("error decoding feed. String was\n%s" % result.content, exc_info=True)
|
|
||||||
return None
|
|
||||||
|
|
||||||
title = entry['title']
|
|
||||||
author_name = entry['author']
|
|
||||||
response = {'type': u'video', 'version': u'1.0', 'provider_name': self.title,
|
|
||||||
'title': title, 'author_name': author_name, 'width': 425, 'height': 344,
|
|
||||||
'thumbnail_url': 'http://www.metacafe.com/thumb/' + matches.group('videoid') + '.jpg',
|
|
||||||
'thumbnail_width': 136, 'thumbnail_height': 81
|
|
||||||
}
|
|
||||||
response['html'] = "<embed src='http://www.metacafe.com/fplayer/%s/movie.swf' style='width:400px; height:345px;' width='400' height='345' wmode='transparent' type='application/x-shockwave-flash'></embed>" % matches.group('videoid')
|
|
||||||
|
|
||||||
json_response = json.dumps(response, ensure_ascii=False, indent=1)
|
|
||||||
return json_response
|
|
||||||
|
|
||||||
class GoogleVideoProvider(Provider):
|
|
||||||
"""Provides the flash video embed code"""
|
|
||||||
title = 'Google Video'
|
|
||||||
url = 'http://video.google.com/videoplay?*'
|
|
||||||
url_re = r'video\.google\.com/videoplay.+docid=(?P<videoid>[\d-]+)&?'
|
|
||||||
example_url = 'http://video.google.com/videoplay?docid=8372603330420559198'
|
|
||||||
|
|
||||||
json_template = u"""{
|
|
||||||
"version": "1.0",
|
|
||||||
"type": "video",
|
|
||||||
"provider_name": "Google Video",
|
|
||||||
"width": 400,
|
|
||||||
"height": 326,
|
|
||||||
"html": "<embed style='width:400px; height:326px;' type='application/x-shockwave-flash' src='http://video.google.com/googleplayer.swf?docId=%s&hl=en' width='400' height='326'></embed>"
|
|
||||||
}"""
|
|
||||||
|
|
||||||
def provide(self, query_url, extra_params=None):
|
|
||||||
matches = self.url_regex.search(query_url)
|
|
||||||
if not matches:
|
|
||||||
raise UnsupportedUrlError()
|
|
||||||
|
|
||||||
return self.json_template % matches.group('videoid')
|
|
||||||
|
|
||||||
class CollegeHumorVideoProvider(Provider):
|
|
||||||
"""Provides the flash video embed code"""
|
|
||||||
title = 'CollegeHumor Video'
|
|
||||||
url = 'http://*.collegehumor.com/video:*'
|
|
||||||
url_re = r'collegehumor\.com/video:(?P<videoid>[\d]+)'
|
|
||||||
example_url = 'http://www.collegehumor.com/video:1772239'
|
|
||||||
|
|
||||||
json_template = u"""{
|
|
||||||
"version": "1.0",
|
|
||||||
"type": "video",
|
|
||||||
"provider_name": "CollegeHumor Video",
|
|
||||||
"width": 480,
|
|
||||||
"height": 360,
|
|
||||||
"html": "<embed style='width:480px; height:360px;' width='480' height='360' type='application/x-shockwave-flash' src='http://www.collegehumor.com/moogaloop/moogaloop.swf?clip_id=%s&fullscreen=1' ></embed>"
|
|
||||||
}"""
|
|
||||||
|
|
||||||
def provide(self, query_url, extra_params=None):
|
|
||||||
matches = self.url_regex.search(query_url)
|
|
||||||
if not matches:
|
|
||||||
raise UnsupportedUrlError()
|
|
||||||
|
|
||||||
return self.json_template % matches.group('videoid')
|
|
||||||
|
|
||||||
class FunnyOrDieProvider(Provider):
|
|
||||||
"""Provides the flash video embed code"""
|
|
||||||
title = 'Funny or Die Video'
|
|
||||||
url = 'http://*.funnyordie.com/videos/*'
|
|
||||||
url_re = r'funnyordie\.com/videos/(?P<videoid>\w+)'
|
|
||||||
example_url = 'http://www.funnyordie.com/videos/eae26bb96d'
|
|
||||||
|
|
||||||
json_template = u"""{
|
|
||||||
"version": "1.0",
|
|
||||||
"type": "video",
|
|
||||||
"provider_name": "Funny Or Die Video",
|
|
||||||
"width": 464,
|
|
||||||
"height": 388,
|
|
||||||
"html": "<embed width='464' height='388' flashvars='key=%s' allowfullscreen='true' quality='high' src='http://www2.funnyordie.com/public/flash/fodplayer.swf?7228' type='application/x-shockwave-flash'></embed>"
|
|
||||||
}"""
|
|
||||||
|
|
||||||
def provide(self, query_url, extra_params=None):
|
|
||||||
matches = self.url_regex.search(query_url)
|
|
||||||
if not matches:
|
|
||||||
raise UnsupportedUrlError()
|
|
||||||
|
|
||||||
return self.json_template % matches.group('videoid')
|
|
||||||
|
|
||||||
class FiveMinVideoProvider(Provider):
|
|
||||||
"""Provides the flash video embed code"""
|
|
||||||
title = '5min.com'
|
|
||||||
url = 'http://*.5min.com/Video/*'
|
|
||||||
url_re = r'5min\.com/Video/.*-(?P<videoid>[\d]+)$'
|
|
||||||
example_url = 'http://www.5min.com/Video/Chocolate-Marquise-Recipe-89007978'
|
|
||||||
|
|
||||||
json_template = u"""{
|
|
||||||
"version": "1.0",
|
|
||||||
"type": "video",
|
|
||||||
"provider_name": "5min.com Video",
|
|
||||||
"width": 480,
|
|
||||||
"height": 401,
|
|
||||||
"html": "<embed style='width:480px; height:401px;' width='480' height='401' type='application/x-shockwave-flash' src='http://www.5min.com/Embeded/%s/' allowFullscreen='true'></embed>"
|
|
||||||
}"""
|
|
||||||
|
|
||||||
def provide(self, query_url, extra_params=None):
|
|
||||||
matches = self.url_regex.search(query_url)
|
|
||||||
if not matches:
|
|
||||||
raise UnsupportedUrlError()
|
|
||||||
|
|
||||||
return self.json_template % matches.group('videoid')
|
|
||||||
|
|
||||||
class DailyShowVideoProvider(Provider):
|
|
||||||
"""Provides the flash video embed code"""
|
|
||||||
title = 'Daily Show with Jon Stewart'
|
|
||||||
url = 'http://*.thedailyshow.com/video/*'
|
|
||||||
url_re = r'thedailyshow\.com/video/index\.jhtml.*videoId=(?P<videoid>[\d]+)'
|
|
||||||
example_url = 'http://www.thedailyshow.com/video/index.jhtml?videoId=210855&title=CNN%27s-Magic-Wall-Conspiracy-Thriller'
|
|
||||||
|
|
||||||
json_template = u"""{
|
|
||||||
"version": "1.0",
|
|
||||||
"type": "video",
|
|
||||||
"provider_name": "Daily Show with Jon Stewart",
|
|
||||||
"width": 360,
|
|
||||||
"height": 301,
|
|
||||||
"html": "<embed style='width:360px; height:301px;' width='360' height='301' type='application/x-shockwave-flash' src='http://media.mtvnservices.com/mgid:cms:item:comedycentral.com:%s' allowFullscreen='true'></embed>"
|
|
||||||
}"""
|
|
||||||
|
|
||||||
def provide(self, query_url, extra_params=None):
|
|
||||||
matches = self.url_regex.search(query_url)
|
|
||||||
if not matches:
|
|
||||||
raise UnsupportedUrlError()
|
|
||||||
|
|
||||||
return self.json_template % matches.group('videoid')
|
|
||||||
|
|
||||||
class SlideShareProvider(Provider):
|
|
||||||
"""Provides the embed code for slideshow"""
|
|
||||||
title = 'SlideShare'
|
|
||||||
url = 'http://*.slideshare.net/*'
|
|
||||||
url_re = r'slideshare\.net/.+'
|
|
||||||
example_url = 'http://www.slideshare.net/igniteportland/' \
|
|
||||||
'how-to-run-a-startup-without-losing-your-mind'
|
|
||||||
|
|
||||||
_api_key = SLIDESHARE_KEY
|
|
||||||
_api_secret = SLIDESHARE_SECRET
|
|
||||||
_api_url = 'http://www.slideshare.net/api/2/get_slideshow?'
|
|
||||||
|
|
||||||
def fetch_info(self, query_url):
|
|
||||||
from hashlib import sha1
|
|
||||||
import time
|
|
||||||
|
|
||||||
ts = int(time.time()) # unix timestamp
|
|
||||||
|
|
||||||
params = {'api_key': self._api_key,
|
|
||||||
'ts': ts,
|
|
||||||
'hash': sha1(self._api_secret + str(ts)).hexdigest(),
|
|
||||||
'slideshow_url': query_url}
|
|
||||||
|
|
||||||
fetch_url = self._api_url + urllib.urlencode(params)
|
|
||||||
result = get_url(fetch_url)
|
|
||||||
return result
|
|
||||||
|
|
||||||
def provide(self, query_url, extra_params=None):
|
|
||||||
matches = self.url_regex.search(query_url)
|
|
||||||
if not matches:
|
|
||||||
raise UnsupportedUrlError()
|
|
||||||
|
|
||||||
result = self.fetch_info(query_url)
|
|
||||||
|
|
||||||
if not result:
|
|
||||||
raise OohEmbedError("Did not get response from SlideShare")
|
|
||||||
|
|
||||||
if "SlideShareServiceError" in result:
|
|
||||||
error_msg = ET.fromstring(result)
|
|
||||||
error_msg = error_msg.find("Message")
|
|
||||||
if error_msg is not None:
|
|
||||||
raise OohEmbedError("SlideShare returned error: %s" % error_msg.text)
|
|
||||||
else:
|
|
||||||
logging.error("SlideShare error response: %s" % result)
|
|
||||||
raise OohEmbedError("SlideShare returned error: %s" % result)
|
|
||||||
|
|
||||||
result = xml2dict(result)
|
|
||||||
|
|
||||||
response = {'version' : '1.0',
|
|
||||||
'type': 'rich',
|
|
||||||
'provider_name': self.title
|
|
||||||
}
|
|
||||||
|
|
||||||
response['title'] = result['Title']
|
|
||||||
|
|
||||||
# Slideshare's embed code is wrapped in an extra
|
|
||||||
# left-aligned div. Strip that div out
|
|
||||||
m = re.match(r'<div.*?>(?P<code>.+)</div>', result['Embed'], re.I)
|
|
||||||
if not m:
|
|
||||||
raise OohEmbedError("Could not parse response from SlideShare")
|
|
||||||
|
|
||||||
response['html'] = m.group('code')
|
|
||||||
|
|
||||||
m = re.search(r'width="(?P<width>\d+)"', response['html'], re.I)
|
|
||||||
if m:
|
|
||||||
response['width'] = m.group('width')
|
|
||||||
|
|
||||||
m = re.search(r'height="(?P<height>\d+)"', response['html'], re.I)
|
|
||||||
if m:
|
|
||||||
response['height'] = m.group('height')
|
|
||||||
|
|
||||||
try:
|
|
||||||
response['author_name'] = result['Username']
|
|
||||||
response['author_url'] = 'http://www.slideshare.net/'+result['Username']
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
response['thumbnail_url'] = result['ThumbnailURL']
|
|
||||||
|
|
||||||
json_response = json.dumps(response, ensure_ascii=False, indent=1)
|
|
||||||
return json_response
|
|
||||||
|
|
|
@ -1,116 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:aws="http://webservices.amazon.com/AWSECommerceService/2005-10-05" exclude-result-prefixes="aws">
|
|
||||||
|
|
||||||
<xsl:output method="text"/>
|
|
||||||
|
|
||||||
<!-- +- -->
|
|
||||||
<!-- | Base Template Match, General JSON Format -->
|
|
||||||
<!-- +- -->
|
|
||||||
<xsl:template match="/">
|
|
||||||
<xsl:value-of select="aws:ItemLookupResponse/aws:OperationRequest/aws:Arguments/aws:Argument[@Name = 'CallBack']/@Value" /><xsl:text> { "Item" : </xsl:text><xsl:apply-templates/><xsl:text> } </xsl:text>
|
|
||||||
</xsl:template>
|
|
||||||
|
|
||||||
<xsl:template match="aws:RequestId"></xsl:template>
|
|
||||||
<xsl:template match="aws:RequestProcessingTime"></xsl:template>
|
|
||||||
<xsl:template match="aws:Items">
|
|
||||||
<xsl:apply-templates select="aws:Item"/>
|
|
||||||
</xsl:template>
|
|
||||||
|
|
||||||
<!-- +- -->
|
|
||||||
<!-- | Fetch ASIN, URL, Title, Price, Description and return as oembed.com photo type json object -->
|
|
||||||
<!-- +- -->
|
|
||||||
<xsl:template match="aws:Item">
|
|
||||||
<xsl:text> {</xsl:text>
|
|
||||||
<xsl:text>"asin":"</xsl:text><xsl:value-of select="aws:ASIN"/><xsl:text>",</xsl:text>
|
|
||||||
<xsl:text>"author_url":"</xsl:text><xsl:value-of select="aws:DetailPageURL"/><xsl:text>",</xsl:text>
|
|
||||||
<xsl:text>"author_name":"</xsl:text><xsl:value-of select="aws:ItemAttributes/aws:Author"/><xsl:text>",</xsl:text>
|
|
||||||
<xsl:text>"title":"</xsl:text><xsl:apply-templates select="aws:ItemAttributes/aws:Title"/><xsl:text>",</xsl:text>
|
|
||||||
<xsl:text>"thumbnail_url":"</xsl:text><xsl:value-of select="aws:SmallImage/aws:URL"/><xsl:text>",</xsl:text>
|
|
||||||
<xsl:text>"thumbnail_height":"</xsl:text><xsl:value-of select="aws:SmallImage/aws:Height"/><xsl:text>",</xsl:text>
|
|
||||||
<xsl:text>"thumbnail_width":"</xsl:text><xsl:value-of select="aws:SmallImage/aws:Width"/><xsl:text>",</xsl:text>
|
|
||||||
<xsl:text>"img_large": {</xsl:text>
|
|
||||||
<xsl:text>"height":"</xsl:text><xsl:value-of select="aws:LargeImage/aws:Height"/><xsl:text>",</xsl:text>
|
|
||||||
<xsl:text>"width":"</xsl:text><xsl:value-of select="aws:LargeImage/aws:Width"/><xsl:text>",</xsl:text>
|
|
||||||
<xsl:text>"url":"</xsl:text><xsl:value-of select="aws:LargeImage/aws:URL"/><xsl:text>"</xsl:text>
|
|
||||||
<xsl:text>},</xsl:text>
|
|
||||||
<xsl:text>"img_medium": {</xsl:text>
|
|
||||||
<xsl:text>"height":"</xsl:text><xsl:value-of select="aws:MediumImage/aws:Height"/><xsl:text>",</xsl:text>
|
|
||||||
<xsl:text>"width":"</xsl:text><xsl:value-of select="aws:MediumImage/aws:Width"/><xsl:text>",</xsl:text>
|
|
||||||
<xsl:text>"url":"</xsl:text><xsl:value-of select="aws:MediumImage/aws:URL"/><xsl:text>"</xsl:text>
|
|
||||||
<xsl:text>},</xsl:text>
|
|
||||||
<xsl:text>"img_small": {</xsl:text>
|
|
||||||
<xsl:text>"height":"</xsl:text><xsl:value-of select="aws:SmallImage/aws:Height"/><xsl:text>",</xsl:text>
|
|
||||||
<xsl:text>"width":"</xsl:text><xsl:value-of select="aws:SmallImage/aws:Width"/><xsl:text>",</xsl:text>
|
|
||||||
<xsl:text>"url":"</xsl:text><xsl:value-of select="aws:SmallImage/aws:URL"/><xsl:text>"</xsl:text>
|
|
||||||
<xsl:text>}</xsl:text>
|
|
||||||
<xsl:text>} </xsl:text>
|
|
||||||
</xsl:template>
|
|
||||||
|
|
||||||
<!-- +- -->
|
|
||||||
<!-- | Title Template, used to strip out quotation marks (which would break the javascript) -->
|
|
||||||
<!-- +- -->
|
|
||||||
<xsl:template match="aws:Title">
|
|
||||||
<xsl:call-template name="find-and-replace">
|
|
||||||
<xsl:with-param name="str" select="."/>
|
|
||||||
<xsl:with-param name="target">"</xsl:with-param>
|
|
||||||
<xsl:with-param name="replacement" select="''"/>
|
|
||||||
</xsl:call-template>
|
|
||||||
</xsl:template>
|
|
||||||
|
|
||||||
<!-- +- -->
|
|
||||||
<!-- | Description Template, used to strip out quotation marks, newlines (which would break the javascript) -->
|
|
||||||
<!-- +- -->
|
|
||||||
<xsl:template match="aws:Content">
|
|
||||||
<xsl:variable name="x">
|
|
||||||
<xsl:call-template name="find-and-replace">
|
|
||||||
<xsl:with-param name="str" select="."/>
|
|
||||||
<xsl:with-param name="target">"</xsl:with-param>
|
|
||||||
<xsl:with-param name="replacement" select="''"/>
|
|
||||||
</xsl:call-template>
|
|
||||||
</xsl:variable>
|
|
||||||
<xsl:variable name="y">
|
|
||||||
<xsl:call-template name="find-and-replace">
|
|
||||||
<xsl:with-param name="str" select="string($x)"/>
|
|
||||||
<xsl:with-param name="target" select="' '"/>
|
|
||||||
<xsl:with-param name="replacement" select="''"/>
|
|
||||||
</xsl:call-template>
|
|
||||||
</xsl:variable>
|
|
||||||
<xsl:variable name="z">
|
|
||||||
<xsl:call-template name="find-and-replace">
|
|
||||||
<xsl:with-param name="str" select="string($y)"/>
|
|
||||||
<xsl:with-param name="target" select="' '"/>
|
|
||||||
<xsl:with-param name="replacement" select="''"/>
|
|
||||||
</xsl:call-template>
|
|
||||||
</xsl:variable>
|
|
||||||
<xsl:value-of select="$z"/>
|
|
||||||
</xsl:template>
|
|
||||||
|
|
||||||
<!-- +- -->
|
|
||||||
<!-- | Search-and-Replace Template, swaps one string (target) with another (replacement) -->
|
|
||||||
<!-- +- -->
|
|
||||||
<xsl:template name="find-and-replace">
|
|
||||||
<xsl:param name="str"/>
|
|
||||||
<xsl:param name="target"/>
|
|
||||||
<xsl:param name="replacement"/>
|
|
||||||
<xsl:choose>
|
|
||||||
<xsl:when test="$target and contains($str, $target)">
|
|
||||||
<xsl:value-of select="substring-before($str, $target)" disable-output-escaping="yes"/>
|
|
||||||
<xsl:value-of select="$replacement" disable-output-escaping="yes"/>
|
|
||||||
<xsl:call-template name="find-and-replace">
|
|
||||||
<xsl:with-param name="str" select="substring-after($str, $target)"/>
|
|
||||||
<xsl:with-param name="target" select="$target"/>
|
|
||||||
<xsl:with-param name="replacement" select="$replacement"/>
|
|
||||||
</xsl:call-template>
|
|
||||||
</xsl:when>
|
|
||||||
<xsl:otherwise>
|
|
||||||
<xsl:value-of select="$str" disable-output-escaping="yes"/>
|
|
||||||
</xsl:otherwise>
|
|
||||||
</xsl:choose>
|
|
||||||
</xsl:template>
|
|
||||||
|
|
||||||
</xsl:stylesheet>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0"?>
|
|
||||||
|
|
||||||
<!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd">
|
|
||||||
<cross-domain-policy>
|
|
||||||
<allow-access-from domain="*" />
|
|
||||||
</cross-domain-policy>
|
|
|
@ -1 +0,0 @@
|
||||||
../provider/endpoints.json
|
|
Before Width: | Height: | Size: 1.1 KiB |
|
@ -1,2 +0,0 @@
|
||||||
User-Agent: *
|
|
||||||
Disallow: /oohembed/
|
|
15
chocochip.md
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# Chocochip #
|
||||||
|
|
||||||
|
Features/Fixes I intend to address in the _chocochip_ release.
|
||||||
|
|
||||||
|
* ~~Propagate error messages from upstream providers back to client~~
|
||||||
|
* ~~memcache responses~~
|
||||||
|
* ~~Set proper http expires headers in response~~
|
||||||
|
* ~~update slideshare provider to v2 api~~
|
||||||
|
* add new providers where feasible: ~~dailymotion~~, ~~blip.tv~~, revver, laconica, ~~phodroid.com~~, vzaar.com, ~~scribd~~
|
||||||
|
* hopefully bring back imdb provider
|
||||||
|
* ~~add crossdomain.xml~~
|
||||||
|
|
||||||
|
Striked out items are done!
|
||||||
|
|
||||||
|
Please test the chocochip branch at this staging url: http://chocochip.latest.oohembed.appspot.com/
|