diff --git a/README.rst b/README.rst
new file mode 100644
index 0000000..e09b259
--- /dev/null
+++ b/README.rst
@@ -0,0 +1,9 @@
+
+Testing
+-------
+
+::
+
+ export DJANGO_SETTINGS_MODULE='fest.tests.settings'
+ django-admin.py test
+
diff --git a/example/example/__init__.py b/example/example/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/example/example/app/__init__.py b/example/example/app/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/example/example/app/models.py b/example/example/app/models.py
new file mode 100644
index 0000000..71a8362
--- /dev/null
+++ b/example/example/app/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/example/example/app/templates/app/index.xml b/example/example/app/templates/app/index.xml
new file mode 100644
index 0000000..380519c
--- /dev/null
+++ b/example/example/app/templates/app/index.xml
@@ -0,0 +1,10 @@
+
+
+
+ example.app
+
+
+ App Index
+
+
+
diff --git a/example/example/app/tests.py b/example/example/app/tests.py
new file mode 100644
index 0000000..501deb7
--- /dev/null
+++ b/example/example/app/tests.py
@@ -0,0 +1,16 @@
+"""
+This file demonstrates writing tests using the unittest module. These will pass
+when you run "manage.py test".
+
+Replace this with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+
+class SimpleTest(TestCase):
+ def test_basic_addition(self):
+ """
+ Tests that 1 + 1 always equals 2.
+ """
+ self.assertEqual(1 + 1, 2)
diff --git a/example/example/app/urls.py b/example/example/app/urls.py
new file mode 100644
index 0000000..ec45dd5
--- /dev/null
+++ b/example/example/app/urls.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+from django.conf.urls import patterns, url
+
+from . import views
+
+
+urlpatterns = patterns('',
+ url(r'^$', views.index, name='index'),
+)
diff --git a/example/example/app/views.py b/example/example/app/views.py
new file mode 100644
index 0000000..815f82a
--- /dev/null
+++ b/example/example/app/views.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+from django.views.generic import TemplateView
+
+
+class IndexView(TemplateView):
+ template_name = 'app/index.xml'
+
+ def get_context_data(self, **kwargs):
+ context = super(IndexView, self).get_context_data(**kwargs)
+
+ return context
+
+
+index = IndexView.as_view()
diff --git a/example/example/settings.py b/example/example/settings.py
new file mode 100644
index 0000000..d94b15c
--- /dev/null
+++ b/example/example/settings.py
@@ -0,0 +1,159 @@
+# Django settings for example project.
+import os
+
+PROJECT_ROOT = os.path.dirname(__file__)
+
+DEBUG = False
+TEMPLATE_DEBUG = DEBUG
+
+ADMINS = (
+ # ('Your Name', 'your_email@example.com'),
+)
+
+MANAGERS = ADMINS
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
+ 'NAME': ':memory', # Or path to database file if using sqlite3.
+ 'USER': '', # Not used with sqlite3.
+ 'PASSWORD': '', # Not used with sqlite3.
+ 'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
+ 'PORT': '', # Set to empty string for default. Not used with sqlite3.
+ }
+}
+
+# Local time zone for this installation. Choices can be found here:
+# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
+# although not all choices may be available on all operating systems.
+# On Unix systems, a value of None will cause Django to use the same
+# timezone as the operating system.
+# If running in a Windows environment this must be set to the same as your
+# system time zone.
+TIME_ZONE = 'America/Chicago'
+
+# Language code for this installation. All choices can be found here:
+# http://www.i18nguy.com/unicode/language-identifiers.html
+LANGUAGE_CODE = 'en-us'
+
+SITE_ID = 1
+
+# If you set this to False, Django will make some optimizations so as not
+# to load the internationalization machinery.
+USE_I18N = True
+
+# If you set this to False, Django will not format dates, numbers and
+# calendars according to the current locale.
+USE_L10N = True
+
+# If you set this to False, Django will not use timezone-aware datetimes.
+USE_TZ = True
+
+# Absolute filesystem path to the directory that will hold user-uploaded files.
+# Example: "/home/media/media.lawrence.com/media/"
+MEDIA_ROOT = ''
+
+# URL that handles the media served from MEDIA_ROOT. Make sure to use a
+# trailing slash.
+# Examples: "http://media.lawrence.com/media/", "http://example.com/media/"
+MEDIA_URL = ''
+
+# Absolute path to the directory static files should be collected to.
+# Don't put anything in this directory yourself; store your static files
+# in apps' "static/" subdirectories and in STATICFILES_DIRS.
+# Example: "/home/media/media.lawrence.com/static/"
+STATIC_ROOT = os.path.join(PROJECT_ROOT, '..', 'static')
+
+# URL prefix for static files.
+# Example: "http://media.lawrence.com/static/"
+STATIC_URL = '/static/'
+
+# Additional locations of static files
+STATICFILES_DIRS = (
+ # Put strings here, like "/home/html/static" or "C:/www/django/static".
+ # Always use forward slashes, even on Windows.
+ # Don't forget to use absolute paths, not relative paths.
+ os.path.join(PROJECT_ROOT, 'static'),
+)
+
+# List of finder classes that know how to find static files in
+# various locations.
+STATICFILES_FINDERS = (
+ 'django.contrib.staticfiles.finders.FileSystemFinder',
+ 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
+# 'django.contrib.staticfiles.finders.DefaultStorageFinder',
+)
+
+# Make this unique, and don't share it with anybody.
+SECRET_KEY = 'jgegd*vu)z+g@w*49rj7k&lr9eul+p6a&*+1dvh7t!-eg&2)fo'
+
+# List of callables that know how to import templates from various sources.
+TEMPLATE_LOADERS = (
+ 'fest.loaders.FSLoader',
+ 'fest.loaders.AppLoader',
+)
+
+MIDDLEWARE_CLASSES = (
+ 'django.middleware.common.CommonMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ # Uncomment the next line for simple clickjacking protection:
+ # 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+)
+
+ROOT_URLCONF = 'example.urls'
+
+# Python dotted path to the WSGI application used by Django's runserver.
+WSGI_APPLICATION = 'example.wsgi.application'
+
+TEMPLATE_DIRS = (
+ # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
+ # Always use forward slashes, even on Windows.
+ # Don't forget to use absolute paths, not relative paths.
+)
+
+INSTALLED_APPS = (
+ # 'django.contrib.auth',
+ # 'django.contrib.contenttypes',
+ # 'django.contrib.sessions',
+ # 'django.contrib.sites',
+ # 'django.contrib.messages',
+ # 'django.contrib.staticfiles',
+ # Uncomment the next line to enable the admin:
+ # 'django.contrib.admin',
+ # Uncomment the next line to enable admin documentation:
+ # 'django.contrib.admindocs',
+ 'example.app',
+ 'fest',
+)
+
+# A sample logging configuration. The only tangible logging
+# performed by this configuration is to send an email to
+# the site admins on every HTTP 500 error when DEBUG=False.
+# See http://docs.djangoproject.com/en/dev/topics/logging for
+# more details on how to customize your logging configuration.
+LOGGING = {
+ 'version': 1,
+ 'disable_existing_loggers': False,
+ 'filters': {
+ 'require_debug_false': {
+ '()': 'django.utils.log.RequireDebugFalse'
+ }
+ },
+ 'handlers': {
+ 'mail_admins': {
+ 'level': 'ERROR',
+ 'filters': ['require_debug_false'],
+ 'class': 'django.utils.log.AdminEmailHandler'
+ }
+ },
+ 'loggers': {
+ 'django.request': {
+ 'handlers': ['mail_admins'],
+ 'level': 'ERROR',
+ 'propagate': True,
+ },
+ }
+}
diff --git a/example/example/urls.py b/example/example/urls.py
new file mode 100644
index 0000000..50770f2
--- /dev/null
+++ b/example/example/urls.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+from django.conf.urls import patterns, include, url
+from django.contrib.staticfiles.urls import staticfiles_urlpatterns
+
+# Uncomment the next two lines to enable the admin:
+# from django.contrib import admin
+# admin.autodiscover()
+
+urlpatterns = patterns('',
+ url(r'^$', include('example.app.urls', namespace='app')),
+
+ # Uncomment the admin/doc line below to enable admin documentation:
+ # url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
+
+ # Uncomment the next line to enable the admin:
+ # url(r'^admin/', include(admin.site.urls)),
+)
+
+urlpatterns += staticfiles_urlpatterns()
diff --git a/example/example/wsgi.py b/example/example/wsgi.py
new file mode 100644
index 0000000..9b42e63
--- /dev/null
+++ b/example/example/wsgi.py
@@ -0,0 +1,28 @@
+"""
+WSGI config for example project.
+
+This module contains the WSGI application used by Django's development server
+and any production WSGI deployments. It should expose a module-level variable
+named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
+this application via the ``WSGI_APPLICATION`` setting.
+
+Usually you will have the standard Django WSGI application here, but it also
+might make sense to replace the whole Django WSGI application with a custom one
+that later delegates to the Django one. For example, you could introduce WSGI
+middleware here, or combine a Django application with an application of another
+framework.
+
+"""
+import os
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings")
+
+# This application object is used by any WSGI server configured to use this
+# file. This includes Django's development server, if the WSGI_APPLICATION
+# setting points here.
+from django.core.wsgi import get_wsgi_application
+application = get_wsgi_application()
+
+# Apply WSGI middleware here.
+# from helloworld.wsgi import HelloWorldApplication
+# application = HelloWorldApplication(application)
diff --git a/example/manage.py b/example/manage.py
new file mode 100755
index 0000000..0b3e3d7
--- /dev/null
+++ b/example/manage.py
@@ -0,0 +1,9 @@
+#!/usr/bin/env python
+import os, sys
+
+if __name__ == "__main__":
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings")
+
+ from django.core.management import execute_from_command_line
+
+ execute_from_command_line(sys.argv)
diff --git a/example/static/.keep b/example/static/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/fest/__init__.py b/fest/__init__.py
new file mode 100644
index 0000000..732c3dd
--- /dev/null
+++ b/fest/__init__.py
@@ -0,0 +1,2 @@
+# -*- coding: utf-8 -*-
+__version__ = (0, 1, 0, 'dev', 0)
diff --git a/fest/conf.py b/fest/conf.py
new file mode 100644
index 0000000..c576135
--- /dev/null
+++ b/fest/conf.py
@@ -0,0 +1,11 @@
+# -*- coding: utf-8 -*-
+import os
+
+from django.conf import settings
+
+
+settings.FEST_ROOT = getattr(settings, 'FEST_ROOT',
+ os.path.join(os.path.dirname(__file__), 'static', 'js', 'fest'))
+
+settings.FEST_COMPILED_ROOT = getattr(settings, 'FEST_COMPILED_ROOT',
+ os.path.join(settings.STATIC_ROOT, 'js', 'compiled'))
diff --git a/fest/loaders.py b/fest/loaders.py
new file mode 100644
index 0000000..5c8fcdf
--- /dev/null
+++ b/fest/loaders.py
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+import os
+
+from django.template.base import TemplateDoesNotExist
+from django.template.loaders import app_directories, filesystem
+
+from fest.conf import settings
+from fest.template import Template
+
+
+def load_compiled(filename):
+ filepath = os.path.join(settings.FEST_COMPILED_ROOT, filename)
+ filepath = '%sjs' % filepath.rstrip('xml')
+ try:
+ file = open(filepath)
+ try:
+ return (file.read().decode(settings.FILE_CHARSET), filepath)
+ finally:
+ file.close()
+ except IOError:
+ raise TemplateDoesNotExist('Template %s not found.' % filepath)
+
+
+class AppLoader(app_directories.Loader):
+ is_usable = True
+
+ def load_template(self, template_name, template_dirs=None):
+ source, origin = self.load_template_source(template_name,
+ template_dirs)
+ return Template(source, origin, template_name), origin
+
+ def load_template_source(self, template_name, template_dirs=None):
+ if not settings.DEBUG:
+ return load_compiled(template_name)
+ else:
+ return super(AppLoader, self).load_template_source(template_name,
+ template_dirs)
+
+
+class FSLoader(filesystem.Loader):
+ is_usable = True
+
+ def load_template(self, template_name, template_dirs=None):
+ source, origin = self.load_template_source(template_name,
+ template_dirs)
+ return Template(source, origin, template_name), origin
+
+ def load_template_source(self, template_name, template_dirs=None):
+ if not settings.DEBUG:
+ return load_compiled(template_name)
+ else:
+ return super(FSLoader, self).load_template_source(template_name,
+ template_dirs)
diff --git a/fest/management/__init__.py b/fest/management/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/fest/management/commands/__init__.py b/fest/management/commands/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/fest/management/commands/fest_compile.py b/fest/management/commands/fest_compile.py
new file mode 100644
index 0000000..31dab0a
--- /dev/null
+++ b/fest/management/commands/fest_compile.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+import os
+from fnmatch import fnmatch
+
+from django.conf import settings
+from django.core.management.base import NoArgsCommand
+from django.core.files.base import ContentFile
+from django.core.files.storage import FileSystemStorage
+from django.template import TemplateDoesNotExist
+from django.template.loader import find_template
+from django.template.loaders.cached import Loader as CachedLoader
+
+
+from fest.template import Template
+
+
+class Command(NoArgsCommand):
+
+ def handle_noargs(self, **options):
+ # Force django to calculate template_source_loaders
+ try:
+ find_template('notexists')
+ except TemplateDoesNotExist:
+ pass
+
+ from django.template.loader import template_source_loaders
+
+ loaders = []
+ for loader in template_source_loaders:
+ if isinstance(loader, CachedLoader):
+ loaders.extend(loader.loaders)
+ else:
+ loaders.append(loader)
+
+ paths = set()
+ for loader in loaders:
+ paths.update(list(loader.get_template_sources('')))
+
+ templates = set()
+ for path in paths:
+ for root, dirs, files in os.walk(path):
+ templates.update(os.path.join(root, name)
+ for name in files if not name.startswith('.') and
+ fnmatch(name, '*xml'))
+
+ storage = FileSystemStorage(settings.FEST_COMPILED_ROOT)
+ for template_name in templates:
+ template_file = open(template_name)
+ try:
+ tpl = Template(template_file.read().decode(
+ settings.FILE_CHARSET))
+ template = ContentFile(tpl.compile())
+ finally:
+ template_file.close()
+ name = self.get_dest_filename(template_name)
+ storage.delete(name)
+ storage.save(name, template)
+
+ def get_dest_filename(self, path):
+ path = '%sjs' % path.rstrip('xml')
+ parts = path.split(os.sep)[-2:]
+ return os.path.join(*parts)
diff --git a/fest/models.py b/fest/models.py
new file mode 100644
index 0000000..71a8362
--- /dev/null
+++ b/fest/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/fest/template.py b/fest/template.py
new file mode 100644
index 0000000..33769a7
--- /dev/null
+++ b/fest/template.py
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+import os
+import PyV8
+
+try:
+ import simplejson as json
+except ImportError:
+ import json # noqa
+
+from django.template import TemplateSyntaxError, TemplateEncodingError
+from django.template.loader import get_template
+from django.utils.encoding import smart_unicode
+
+from fest.conf import settings
+
+
+def fest_error(message):
+ raise TemplateSyntaxError(message)
+
+
+class TemplateGlobal(PyV8.JSClass):
+
+ def __init__(self, template):
+ self.__tpl = template
+
+ def __getattr__(self, name):
+ if name == '__fest_error':
+ return fest_error
+
+ name = 'py%s' % name
+ return PyV8.JSClass.__getattribute__(self, name)
+
+ @property
+ def py__dirname(self):
+ return settings.FEST_ROOT
+
+ def py__read_file(self, filename, encoding=None):
+ # parts of compiler
+ if settings.DEBUG and filename.endswith('.js'):
+ return open(filename).read()
+
+ if filename == self.__tpl.name:
+ template = self.__tpl
+ else:
+ filename = os.path.abspath(filename).lstrip('/')
+ template = get_template(filename)
+ return template.template_string
+
+
+class Template(object):
+
+ def __init__(self, template_string, origin=None,
+ name=''):
+ try:
+ template_string = smart_unicode(template_string)
+ except UnicodeDecodeError:
+ raise TemplateEncodingError('Templates can only be constructed'
+ ' from unicode or UTF-8 strings.')
+
+ self.template_string = template_string
+ self.name = name
+
+ @property
+ def context(self):
+ if not hasattr(self, '_context'):
+ self._context = PyV8.JSContext(TemplateGlobal(self))
+ return self._context
+
+ def compile(self):
+ with self.context as cenv:
+ filename = os.path.join(settings.FEST_ROOT, 'compile.js')
+ compiler = cenv.eval('%scompile;' % open(filename).read())
+ return compiler(self.name)
+
+ def render(self, context):
+
+ with self.context as env:
+ if not settings.DEBUG:
+ template = self.template_string
+ else:
+ template = self.compile()
+
+ # maybe i'm just stupid
+ template = """(function(json_string, fest_error) {
+ return %s(JSON.parse(json_string), fest_error);
+ })""" % template
+
+ func = env.eval(template)
+ return func(json.dumps(list(context).pop()), fest_error)
diff --git a/fest/tests/__init__.py b/fest/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/fest/tests/settings.py b/fest/tests/settings.py
new file mode 100644
index 0000000..0723777
--- /dev/null
+++ b/fest/tests/settings.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+import os
+
+PROJECT_ROOT = os.path.dirname(__file__)
+
+DATABASES = {
+ 'default': {
+ 'ENGINE': 'django.db.backends.sqlite3',
+ 'NAME': ':memory:',
+ }
+}
+
+STATIC_ROOT = os.path.join(PROJECT_ROOT, 'static')
+
+TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
+
+INSTALLED_APPS = (
+ 'django_nose',
+)
+
+TEMPLATE_LOADERS = (
+ 'fest.loaders.FSLoader',
+ 'fest.loaders.AppLoader',
+)
+
+TEMPLATE_DIRS = (
+ os.path.join(PROJECT_ROOT, 'templates'),
+)
+
+STATIC_ROOT = os.path.join(PROJECT_ROOT, 'static')
+
+FEST_ROOT = os.path.join(PROJECT_ROOT, '..', 'static', 'js', 'fest')
+FEST_COMPILED_ROOT = os.path.join(STATIC_ROOT, 'js', 'compiled')
diff --git a/fest/tests/static/js/compiled/base.js b/fest/tests/static/js/compiled/base.js
new file mode 100644
index 0000000..f8d653d
--- /dev/null
+++ b/fest/tests/static/js/compiled/base.js
@@ -0,0 +1,76 @@
+function(__fest_context, __fest_error) {
+ "use strict";
+ var __fest_str = "",
+ __fest_result = [],
+ __fest_contexts = [],
+ __fest_if, __fest_foreach, __fest_from, __fest_to, __fest_html = "",
+ __fest_blocks = {},
+ __fest_params, __fest_htmlchars = /[&<>\"]/g,
+ __fest_htmlhash = {
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"'
+ },
+ __fest_jschars = /[\'\"\/\n\t\b\f]/g,
+ __fest_jshash = {
+ "\"": "\\\"",
+ "\\": "\\\\",
+ "/": "\\/",
+ "\n": "\\n",
+ "\t": "\\t",
+ "\b": "\\b",
+ "\f": "\\f",
+ "'": "\\'"
+ };
+ if (typeof __fest_error === "undefined") {
+ __fest_error = function() {
+ return console.error.apply(console, arguments);
+ };
+ }
+ function __fest_replaceHTML(char) {
+ return __fest_htmlhash[char];
+ }
+ function __fest_replaceJS(char) {
+ return __fest_jshash[char];
+ }
+ function __fest_escapeJS(s) {
+ if (typeof s !== "string") s += "";
+ return s.replace(__fest_jschars, __fest_replaceJS);
+ }
+ function __fest_escapeHTML(s) {
+ if (typeof s !== "string") s += "";
+ return s.replace(__fest_htmlchars, __fest_replaceHTML);
+ }
+ if (typeof document === "undefined") {
+ var document = {
+ write: function(string) {
+ __fest_str += string;
+ }
+ };
+ }
+ var json = __fest_context;
+ __fest_str += "Hello, ";
+ try {
+ __fest_str += json.name
+ } catch (e) {
+ __fest_error(e.message);
+ }
+ __fest_str += "
";
+ __fest_result[__fest_result.length] = __fest_str;
+ if (__fest_result.length === 1) {
+ return __fest_result[0]
+ }
+ function setblocks(list) {
+ var __fest_i, __fest_l;
+ for (__fest_i = 0, __fest_l = list.length; __fest_i < __fest_l; __fest_i++) {
+ if (typeof list[__fest_i] === "string") {
+ __fest_html += list[__fest_i];
+ } else {
+ setblocks(list[__fest_i]());
+ }
+ }
+ }
+ setblocks(__fest_result);
+ return __fest_html;
+}
diff --git a/fest/tests/static/js/compiled/doctype.js b/fest/tests/static/js/compiled/doctype.js
new file mode 100644
index 0000000..58e195b
--- /dev/null
+++ b/fest/tests/static/js/compiled/doctype.js
@@ -0,0 +1,70 @@
+function(__fest_context, __fest_error) {
+ "use strict";
+ var __fest_str = "",
+ __fest_result = [],
+ __fest_contexts = [],
+ __fest_if, __fest_foreach, __fest_from, __fest_to, __fest_html = "",
+ __fest_blocks = {},
+ __fest_params, __fest_htmlchars = /[&<>\"]/g,
+ __fest_htmlhash = {
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"'
+ },
+ __fest_jschars = /[\'\"\/\n\t\b\f]/g,
+ __fest_jshash = {
+ "\"": "\\\"",
+ "\\": "\\\\",
+ "/": "\\/",
+ "\n": "\\n",
+ "\t": "\\t",
+ "\b": "\\b",
+ "\f": "\\f",
+ "'": "\\'"
+ };
+ if (typeof __fest_error === "undefined") {
+ __fest_error = function() {
+ return console.error.apply(console, arguments);
+ };
+ }
+ function __fest_replaceHTML(char) {
+ return __fest_htmlhash[char];
+ }
+ function __fest_replaceJS(char) {
+ return __fest_jshash[char];
+ }
+ function __fest_escapeJS(s) {
+ if (typeof s !== "string") s += "";
+ return s.replace(__fest_jschars, __fest_replaceJS);
+ }
+ function __fest_escapeHTML(s) {
+ if (typeof s !== "string") s += "";
+ return s.replace(__fest_htmlchars, __fest_replaceHTML);
+ }
+ if (typeof document === "undefined") {
+ var document = {
+ write: function(string) {
+ __fest_str += string;
+ }
+ };
+ }
+ var json = __fest_context;
+ __fest_str += "";
+ __fest_result[__fest_result.length] = __fest_str;
+ if (__fest_result.length === 1) {
+ return __fest_result[0]
+ }
+ function setblocks(list) {
+ var __fest_i, __fest_l;
+ for (__fest_i = 0, __fest_l = list.length; __fest_i < __fest_l; __fest_i++) {
+ if (typeof list[__fest_i] === "string") {
+ __fest_html += list[__fest_i];
+ } else {
+ setblocks(list[__fest_i]());
+ }
+ }
+ }
+ setblocks(__fest_result);
+ return __fest_html;
+}
diff --git a/fest/tests/static/js/compiled/include.js b/fest/tests/static/js/compiled/include.js
new file mode 100644
index 0000000..6edb8f9
--- /dev/null
+++ b/fest/tests/static/js/compiled/include.js
@@ -0,0 +1,93 @@
+function(__fest_context, __fest_error) {
+ "use strict";
+ var __fest_str = "",
+ __fest_result = [],
+ __fest_contexts = [],
+ __fest_if, __fest_foreach, __fest_from, __fest_to, __fest_html = "",
+ __fest_blocks = {},
+ __fest_params, __fest_htmlchars = /[&<>\"]/g,
+ __fest_htmlhash = {
+ '&': '&',
+ '<': '<',
+ '>': '>',
+ '"': '"'
+ },
+ __fest_jschars = /[\'\"\/\n\t\b\f]/g,
+ __fest_jshash = {
+ "\"": "\\\"",
+ "\\": "\\\\",
+ "/": "\\/",
+ "\n": "\\n",
+ "\t": "\\t",
+ "\b": "\\b",
+ "\f": "\\f",
+ "'": "\\'"
+ };
+ if (typeof __fest_error === "undefined") {
+ __fest_error = function() {
+ return console.error.apply(console, arguments);
+ };
+ }
+ function __fest_replaceHTML(char) {
+ return __fest_htmlhash[char];
+ }
+ function __fest_replaceJS(char) {
+ return __fest_jshash[char];
+ }
+ function __fest_escapeJS(s) {
+ if (typeof s !== "string") s += "";
+ return s.replace(__fest_jschars, __fest_replaceJS);
+ }
+ function __fest_escapeHTML(s) {
+ if (typeof s !== "string") s += "";
+ return s.replace(__fest_htmlchars, __fest_replaceHTML);
+ }
+ if (typeof document === "undefined") {
+ var document = {
+ write: function(string) {
+ __fest_str += string;
+ }
+ };
+ }
+ var json = __fest_context;
+ __fest_contexts[__fest_contexts.length] = json;
+ try {
+ json = json.list
+ } catch (e) {
+ json = {};
+ __fest_error(e.message)
+ };
+ var list = json;
+ var i, __fest_l0, __fest_from0, __fest_to0;
+ try {
+ __fest_foreach = list || [];
+ } catch (e) {
+ __fest_foreach = [];
+ __fest_error(e.message);
+ }
+ __fest_l0 = (typeof __fest_foreach === "number" ? __fest_foreach : typeof __fest_foreach === "string" ? parseInt(__fest_foreach, 10) : __fest_foreach.length || 0);
+ for (i = 0; i < __fest_l0; i++) {
+ try {
+ __fest_str += __fest_escapeHTML(list[i]);
+ } catch (e) {
+ __fest_error(e.message);
+ }
+ }
+ json = __fest_contexts.pop();
+ __fest_result[__fest_result.length] = __fest_str;
+ if (__fest_result.length === 1) {
+ return __fest_result[0]
+ }
+ function setblocks(list) {
+ var __fest_i, __fest_l;
+ for (__fest_i = 0, __fest_l = list.length; __fest_i < __fest_l; __fest_i++) {
+ if (typeof list[__fest_i] === "string") {
+ __fest_html += list[__fest_i];
+ } else {
+ setblocks(list[__fest_i]());
+ }
+ }
+ }
+ setblocks(__fest_result);
+ return __fest_html;
+}
diff --git a/fest/tests/templates/base.xml b/fest/tests/templates/base.xml
new file mode 100644
index 0000000..3ba1c3e
--- /dev/null
+++ b/fest/tests/templates/base.xml
@@ -0,0 +1,10 @@
+
+
+ Hello,json.name
+
+
+
+
diff --git a/fest/tests/templates/doctype.xml b/fest/tests/templates/doctype.xml
new file mode 100644
index 0000000..8163b86
--- /dev/null
+++ b/fest/tests/templates/doctype.xml
@@ -0,0 +1,4 @@
+
+
+ html
+
diff --git a/fest/tests/templates/include.xml b/fest/tests/templates/include.xml
new file mode 100644
index 0000000..93b87de
--- /dev/null
+++ b/fest/tests/templates/include.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/fest/tests/templates/include_foreach.xml b/fest/tests/templates/include_foreach.xml
new file mode 100644
index 0000000..ac036c4
--- /dev/null
+++ b/fest/tests/templates/include_foreach.xml
@@ -0,0 +1,6 @@
+
+
+
+ list[i]
+
+
diff --git a/fest/tests/test_template.py b/fest/tests/test_template.py
new file mode 100644
index 0000000..2249936
--- /dev/null
+++ b/fest/tests/test_template.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+from django.conf import settings
+from django.test import TestCase
+from django.template import Context
+from django.template.loader import get_template
+
+
+class CompiledTemplateTest(TestCase):
+
+ def test_render(self):
+ t = get_template('doctype.xml')
+ self.assertEqual(t.render(Context()), '')
+
+ def test_render_context(self):
+ t = get_template('base.xml')
+ c = Context({'name': 'Jack "The Ripper"'})
+ self.assertEqual(t.render(c), 'Hello, Jack "The Ripper"
')
+
+ def test_render_include(self):
+ t = get_template('include.xml')
+ c = Context({'list': [1, 2]})
+ self.assertEqual(t.render(c), '12')
+
+
+class SourceTemplateTest(CompiledTemplateTest):
+
+ @classmethod
+ def setUpClass(cls):
+ settings.DEBUG = True
+
+ @classmethod
+ def tearDownClass(cls):
+ settings.DEBUG = False
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..6e5cc44
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,3 @@
+git+git://github.com/django/django.git@996f702#egg=django
+
+svn+http://pyv8.googlecode.com/svn/trunk/@429#egg=PyV8
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..d29b9fd
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,16 @@
+# -*- coding: utf-8 -*-
+from setuptools import setup
+
+
+setup(
+ name='django_fest',
+ version='0.1dev',
+ license='MIT',
+ packages=['fest', ],
+ install_requires=[
+ 'PyV8==1.0',
+ ],
+ dependency_links=[
+ 'svn+http://pyv8.googlecode.com/svn/trunk/@429#egg=PyV8-1.0',
+ ]
+)