Skip to content

comolongo/Yz-Javascript-Django-Template-Compiler

Repository files navigation

YZ JavaScript Django Template Compiler
Copyright (c) 2010 Weiss I Nicht <KeineAhnung@atliva.com> 
(sha-1: 90f01291285340bf03c2d41952c4b21b3d338907)

This library compiles standard django templates into javascript functions. It allows developers to create templates and markup one time and be able to display it on both the server and client side without any additional work. Unlike many other javascript template systems that parse and compile the templates at run time, ours templates are already compiled and readily executable when they are loaded. Hence on the client browser does not need to do any work, and can generate the HTML extremely quickly.

For a quick demo, please take a look at a simple demo: http://yz-demos.appspot.com/yz_djs_demo

How to use:

yz_js_django_tpl.TemplateJsNode() is the main workhorse through which all the templates are parsed. It can take either a string of the template or a path to the template file in order to generate the javascript expression.

Parsing django template as string
To generate a javascript function via a template string (in the Django shell console via "python manage.py shell" command):

>>> from yz_js_django_tpl import TemplateJsNode()
>>> js_tpl = TemplateJsNode('{% if cond %}true cond string{% else %}false cond string{% endif %}')
>>> js_tpl.render()
u'function(cond){if(cond){return "true cond string"}else{return "false cond string"}}'

Parsing django template with template file path
To generate a javascript function via the template path:

>>> from yz_js_django_tpl import TemplateJsNode()
>>> TemplateJsNode(django_template_path = 'djang_tpl.html', var_list = [var1, var2, var3])
>>> js_tpl.render()
u'function(cond){if(cond){return "true cond string"}else{return "false cond string"}}'

The var_list is optional, the template compiler can figure out what variables you need, however, it is useful if you want to explicitly set the order of the parameters of the generated javascript function

Parsing django templates in batch as a script
For the general use case, you may want to setup a script that auto parses a batch of templates and saves them to an autogenerated javascript file. To do that, take a look at generate_js_tpl.py in the yz_djs_demo_app folder. Basically you define the template files to read from, the location of the output files, and a few other variables. Lets take a look:

>>> from yz_js_django_tpl import generate_js_tpl_file, JsTplSettings
>>> template_generator_configs = {
>>>     'VERSAGER_MODE': False,
>>>     'js_dependencies_location' : 'js_dependencies.js',
>>>     'generated_js_file_location': 'generated_tpl.js',
>>>     'tpls': {
>>>         'test_tpl1.html' : {
>>>             'tpl_func_name': 'yz_djstpl_demo1',
>>>             'var_list': ['server_or_client_side', 'feel_about_demo', 'age', 'addition_to_one', 'comments']
>>>         },
>>>         'test_tpl2.html' : {
>>>             'tpl_func_name': 'yz_djstpl_demo2',
>>>             'var_list': ['comment']
>>>         }
>>>     }
>>> }
>>> JsTplSettings.init_config(template_generator_configs)
>>> generate_js_tpl_file()

The above script reads from two template files: test_tpl1.html and test_tpl2.html and converts them into javascript functions. The tpl_func_name is the name of the corresponding javascript function. The generated_js_file_location is the path of the generated javascript file that contains the javascript templates. The js_dependencies_location is the path of the generated file that contains dependencies. For example, some django template tags and filters require additional helper functions in order to work, so after the compiler parses all of the templates in template_generator_configs['tpls'], it knows which dependencies it needs and generates the file accordingly. This way you won't be including more files than you need. If you don't want the generator to auto generate the dependencies, just set the js_dependencies_location to None
Take a look in the yz_djs_demo_app/generated_js folder for what the generated JS files will look like

Versager Mode:
For older browsers such as IE6, string concatenation is an expensive operation, hence instead of adding strings and closures together (e.g. 'a cat ' + eating_action + 'the bat') to form a rendered string, Versager mode will use array.join() to create a parsed string i.e. ['a cat',eating_action,'the bat'].join(). If your audience enjoys using computers from pre y2k-era, you can set Versager mode to true


Development Status:
Currently only a small subset of all the available Django template tags and filters have been implemented, take a look at defaulttags/ and defaultfilters/ to see what is there. As my needs for more filters and tags grow, I'll be implementing more parsers. Let me know if you think there is one I really should look into porting over, or better yet, create the parsers yourself and contribute to this project!

Nuts and Bolts - Creating tag and filter parsers:
To create a parser for a specific tag or filter, we need to first understand how the parser system works. The parser system works by relying on django.template.Template() to first read and parse the django templates. The parsed Template object has a nodelist property which is a list of the nodes that make up the template. What is a node? The template file can be thought of as a collection of tags, variables, and continuos blocks of strings, all of which are nodes. To get the gist of it, run the following code the the console:

>>> from django.template import Template
>>> django_tpl = Template('some string {{some_var}} more string {% if cond > 5 %}true cond string{% else %}false cond string{% endif %}')
>>> django_tpl.__dict__
{'nodelist': [<Text Node: 'some string '>, <Variable Node: some_var>, <Text Node: ' more string '>, <If node>], 'name': '<Unknown Template>'}

Each tag, text block, and variable is handled by a specific type of Node. Each node has all of the information we need to render its representative section of the template. The yz_js_django_tpl.TemplateJsNode parser will read through this list and use yz_js_django_tpl.JsProcessorRegistry to identify each corresponding JsNode and create an instance. Each node may also have lists of subnodes, so it will need to instantiate those as well. Once all of the nodes and subnodes have been instantiated, they are recursively rendered to create the final javascript expression. 

If we look at the If node:
>>> if_node = django_tpl.nodelist[3]
<If node>
>>> if_node.__dict__
{'var': (> (literal <django.template.FilterExpression object at 0x102805690>) (literal <django.template.FilterExpression object at 0x102805250>)), 'source': (<django.template.StringOrigin object at 0x102819690>, (37, 54)), 'nodelist_false': [<Text Node: 'false cond string'>], 'nodelist_true': [<Text Node: 'true cond string'>]}

We see that it has the var, nodelist_false, and nodelist_true properties. We can use the var property to create the conditional part of the if statement i.e. if (cond > 5)
The nodelist_true and nodelist_false can be parsed to form the body of the if statement. Take a look at defaulttags/IfJsNode.py for details.

From the top of the class, we have:
expected_node_classname = 'IfNode'
This identifies the corresponding django Node class. So when an IfNode is encountered in the django nodelist, the yz_js_django_tpl.JsProcessorRegistry will know to call the IfJsNode class. The JsNode is registered at the end of the file:
JsProcessorRegistry.register_js_node(IfJsNode)

Writing a tag:
By convention, the JsNode first calls the _init_vars() method. That is where all of the variables should be initiated. In the IfJsNode, this is where the useful properties from IfNode are extracted. If we look at the 'var' property of the IfNode:

>>> if_node.var.__dict__
{'second': (literal <django.template.FilterExpression object at 0x102805250>), 'first': (literal <django.template.FilterExpression object at 0x102805690>)}

We see that it has two sub properties of type FilterExpression, which need to be handled via self._extract_filter_expression in the JsNode. FilterExpression is section in a template tag represented by a template variable and filter e.g. {{ var1|default:"0" }} . The _extract_filter_expression() registers the variable into the JsNodeContext() and uses yz_js_django_tpl.JsProcessorRegistry to figure out which filters to call. The returned result from _extract_filter_expression is a javascript expression representing the variable and any filters that are applied to it, e.g. {{ var1|add:"2"|default:"def" }} renders to default(add(var1,2), "def")

After the properties have been extracted and the template variables have been registered into context, we call the _init_sub_nodes() . In this method, we see if there are any subnodes, and if so, we instantiate them. Generally subnodes are stored in a list, so we can use the self.self.scan_section() method to instantiate them. Upon instantiation of a node, we pass the a reference of the current node and the JsNodeContext to the child node. This way the child node will know which variables come from the parent context. After the subnodes have been instantiated _init_sub_nodes, the child nodes update the parent context on which variables it has created and which variables it has encountered and needs the parent node to provide.

The render method assembles the initiated nodes as well as the extracted properties from the Node to create the actual javascript expression. The self._nodes_to_js_str() renders a list of JsNodes into string. It also checks the settings to make sure the strings are concatenated using the desired method. It also checks for any room for optimization. For example, if the subnode only has a single element and that element is a ForNode, IfNode, or any other node that generally requires an anonymous function closure when being concatenated (i.e. function(inner_if){if(inner_if){return 'inner text'}}()), _nodes_to_js_str() will be smart enough to render the node without the closure as it is unnecessary when there is no concatenation involved. In other words, instead of something like this
function(outer_if, inner_if){if(outer_if){ return function(inner_if){if(inner_if){ return 'inner if string'}}}}, the _nodes_to_js_str will compile this:
function(outer_if, inner_if){if(outer_if){ if(inner_if){ return 'inner if string'}}}

Writing a filter:
As briefly mentioned in the previous section on writing tags, filter tags are located in the FilterExpression object:

>>> from django.template import Template
>>> django_tpl = Template('{{var1|add:"2"}}')
>>> django_tpl.nodelist[0]
<Variable Node: var1|add:"2">
>>> django_tpl.nodelist[0].filter_expression.__dict__
{'var': <Variable: u'var1'>, 'token': u'var1|add:"2"', 'filters': [(<function add at 0x1027f1050>, [(False, u'2')])]}

Compared to nodes, filters are much simpler. They are simply a function that takes two parameters; the variable value and an optional argument. The BaseJsFilter off of which all filters inherit, provides 3 variables: self.expr, self.arg, and self.js_func_params .  self.expr is the name of the template variable i.e. var1 in the previous example. self.arg may or may not be set to a value, and represents the argument i.e. "2" in the previous example. The self.js_func_params is a list of the two aforementioned variables that makes it easy to call self._wrap_expr_in_js_func() to generate the javascript function call to the appropriate function. The js_func_name defines the name of the javascript function to call. Since most filters will simply render a javascript function call to the designated filter function using the format: jsFilterName(var_value, argument), the BaseJsFilter class does this by default. In most cases, only the js_func_name is required when creating a new filter class. The javascript function definition that will be doing most of the work should be located in the same folder and have the same filename as the filter except with a .js extension. If you look at defaultfilters/DefaultJsFilter.py, the corresponding js dependency is called DefaultJsFilter.js


Test the environment:
This library was built on Django 1.2 using Python 2.5.5 on a Mac OSX snow lepoard, and uses Django's templating system to parse the template files. It is not known how well it'll work on earlier builds of django. 

Fortunately, there is a unit test suite that can be run to make sure you won't get any huge surprises:
1. Place the yz_js_django_tpl package into your django
2. From commandline, goto package directory and type:
>>> python unittest.py
3. With any luck the tests will all pass, otherwise let me know at KeineAhnung@atliva.com and I'll try to see if I have an idea of whats going on.

About

Compiles django templates into javascript functions that can generate html on client side

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published