1 # -*- coding: utf-8 -*-
6 Helper module that can convert django templates into Jinja2 templates.
8 This file is not intended to be used as stand alone application but to
9 be used as library. To convert templates you basically create your own
10 writer, add extra conversion logic for your custom template tags,
11 configure your django environment and run the `convert_templates`
14 Here a simple example::
16 # configure django (or use settings.configure)
18 os.environ['DJANGO_SETTINGS_MODULE'] = 'yourapplication.settings'
19 from yourapplication.foo.templatetags.bar import MyNode
21 from django2jinja import Writer, convert_templates
23 def write_my_node(writer, node):
24 writer.start_variable()
25 writer.write('myfunc(')
26 for idx, arg in enumerate(node.args):
34 writer.node_handlers[MyNode] = write_my_node
35 convert_templates('/path/to/output/folder', writer=writer)
37 Here is an example hos to automatically translate your django
41 # List of tuple (Match pattern, Replace pattern, Exclusion pattern)
43 var_re = ((re.compile(r"(u|user)\.is_authenticated"), r"\1.is_authenticated()", None),
44 (re.compile(r"\.non_field_errors"), r".non_field_errors()", None),
45 (re.compile(r"\.label_tag"), r".label_tag()", None),
46 (re.compile(r"\.as_dl"), r".as_dl()", None),
47 (re.compile(r"\.as_table"), r".as_table()", None),
48 (re.compile(r"\.as_widget"), r".as_widget()", None),
49 (re.compile(r"\.as_hidden"), r".as_hidden()", None),
51 (re.compile(r"\.get_([0-9_\w]+)_url"), r".get_\1_url()", None),
52 (re.compile(r"\.url"), r".url()", re.compile(r"(form|calendar).url")),
53 (re.compile(r"\.get_([0-9_\w]+)_display"), r".get_\1_display()", None),
54 (re.compile(r"loop\.counter"), r"loop.index", None),
55 (re.compile(r"loop\.revcounter"), r"loop.revindex", None),
56 (re.compile(r"request\.GET\.([0-9_\w]+)"), r"request.GET.get('\1', '')", None),
57 (re.compile(r"request\.get_host"), r"request.get_host()", None),
59 (re.compile(r"\.all(?!_)"), r".all()", None),
60 (re.compile(r"\.all\.0"), r".all()[0]", None),
61 (re.compile(r"\.([0-9])($|\s+)"), r"[\1]\2", None),
62 (re.compile(r"\.items"), r".items()", None),
64 writer = Writer(var_re=var_re)
66 For details about the writing process have a look at the module code.
68 :copyright: (c) 2009 by the Jinja Team.
74 from jinja2.defaults import *
75 from django.conf import settings
76 from django.template import defaulttags as core_tags, loader, TextNode, \
77 FilterExpression, libraries, Variable, loader_tags, TOKEN_TEXT, \
79 from django.template.debug import DebugVariableNode as VariableNode
80 from django.templatetags import i18n as i18n_tags
81 from StringIO import StringIO
85 _resolved_filters = {}
86 _newline_re = re.compile(r'(?:\r\n|\r|\n)')
89 # Django stores an itertools object on the cycle node. Not only is this
90 # thread unsafe but also a problem for the converter which needs the raw
91 # string values passed to the constructor to create a jinja loop.cycle()
93 _old_cycle_init = core_tags.CycleNode.__init__
94 def _fixed_cycle_init(self, cyclevars, variable_name=None):
95 self.raw_cycle_vars = map(Variable, cyclevars)
96 _old_cycle_init(self, cyclevars, variable_name)
97 core_tags.CycleNode.__init__ = _fixed_cycle_init
102 _node_handlers[cls] = f
107 def convert_templates(output_dir, extensions=('.html', '.txt'), writer=None,
109 """Iterates over all templates in the template dirs configured and
110 translates them and writes the new templates into the output directory.
115 def filter_templates(files):
116 for filename in files:
117 ifilename = filename.lower()
118 for extension in extensions:
119 if ifilename.endswith(extension):
122 def translate(f, loadname):
123 template = loader.get_template(loadname)
124 original = writer.stream
126 writer.body(template.nodelist)
127 writer.stream = original
130 def callback(template):
133 for directory in settings.TEMPLATE_DIRS:
134 for dirname, _, files in os.walk(directory):
135 dirname = dirname[len(directory) + 1:]
136 for filename in filter_templates(files):
137 source = os.path.normpath(os.path.join(dirname, filename))
138 target = os.path.join(output_dir, dirname, filename)
139 basetarget = os.path.dirname(target)
140 if not os.path.exists(basetarget):
141 os.makedirs(basetarget)
143 f = file(target, 'w')
150 class Writer(object):
151 """The core writer class."""
153 def __init__(self, stream=None, error_stream=None,
154 block_start_string=BLOCK_START_STRING,
155 block_end_string=BLOCK_END_STRING,
156 variable_start_string=VARIABLE_START_STRING,
157 variable_end_string=VARIABLE_END_STRING,
158 comment_start_string=COMMENT_START_STRING,
159 comment_end_string=COMMENT_END_STRING,
160 initial_autoescape=True,
161 use_jinja_autoescape=False,
162 custom_node_handlers=None,
167 if error_stream is None:
168 error_stream = sys.stderr
170 self.error_stream = error_stream
171 self.block_start_string = block_start_string
172 self.block_end_string = block_end_string
173 self.variable_start_string = variable_start_string
174 self.variable_end_string = variable_end_string
175 self.comment_start_string = comment_start_string
176 self.comment_end_string = comment_end_string
177 self.autoescape = initial_autoescape
178 self.spaceless = False
179 self.use_jinja_autoescape = use_jinja_autoescape
180 self.node_handlers = dict(_node_handlers,
181 **(custom_node_handlers or {}))
183 self._filters_warned = set()
187 def enter_loop(self):
188 """Increments the loop depth so that write functions know if they
191 self._loop_depth += 1
193 def leave_loop(self):
194 """Reverse of enter_loop."""
195 self._loop_depth -= 1
199 """True if we are in a loop."""
200 return self._loop_depth > 0
203 """Writes stuff to the stream."""
204 self.stream.write(s.encode(settings.FILE_CHARSET))
206 def print_expr(self, expr):
207 """Open a variable tag, write to the string to the stream and close."""
208 self.start_variable()
212 def _post_open(self):
218 def _pre_close(self):
224 def start_variable(self):
225 """Start a variable."""
226 self.write(self.variable_start_string)
229 def end_variable(self, always_safe=False):
230 """End a variable."""
231 if not always_safe and self.autoescape and \
232 not self.use_jinja_autoescape:
235 self.write(self.variable_end_string)
237 def start_block(self):
238 """Starts a block."""
239 self.write(self.block_start_string)
245 self.write(self.block_end_string)
248 """Like `print_expr` just for blocks."""
253 def variable(self, name):
254 """Prints a variable. This performs variable name transformation."""
255 self.write(self.translate_variable_name(name))
257 def literal(self, value):
258 """Writes a value as literal."""
260 if value[:2] in ('u"', "u'"):
264 def filters(self, filters, is_block=False):
265 """Dumps a list of filters."""
266 want_pipe = not is_block
267 for filter, args in filters:
268 name = self.get_filter_name(filter)
270 self.warn('Could not find filter %s' % name)
272 if name not in DEFAULT_FILTERS and \
273 name not in self._filters_warned:
274 self._filters_warned.add(name)
275 self.warn('Filter %s probably doesn\'t exist in Jinja' %
284 for idx, (is_var, value) in enumerate(args):
293 def get_location(self, origin, position):
294 """Returns the location for an origin and position tuple as name
297 if hasattr(origin, 'source'):
298 source = origin.source
299 name = '<unknown source>'
301 source = origin.loader(origin.loadname, origin.dirs)[0]
302 name = origin.loadname
303 lineno = len(_newline_re.findall(source[:position[0]])) + 1
306 def warn(self, message, node=None):
307 """Prints a warning to the error stream."""
308 if node is not None and hasattr(node, 'source'):
309 filename, lineno = self.get_location(*node.source)
310 message = '[%s:%d] %s' % (filename, lineno, message)
311 print >> self.error_stream, message
313 def translate_variable_name(self, var):
314 """Performs variable name translation."""
315 if self.in_loop and var == 'forloop' or var.startswith('forloop.'):
318 for reg, rep, unless in self.var_re:
319 no_unless = unless and unless.search(var) or True
320 if reg.search(var) and no_unless:
321 var = reg.sub(rep, var)
325 def get_filter_name(self, filter):
326 """Returns the filter name for a filter function or `None` if there
329 if filter not in _resolved_filters:
330 for library in libraries.values():
331 for key, value in library.filters.iteritems():
332 _resolved_filters[value] = key
333 return _resolved_filters.get(filter, None)
335 def node(self, node):
336 """Invokes the node handler for a node."""
337 for cls, handler in self.node_handlers.iteritems():
338 if type(node) is cls or type(node).__name__ == cls:
342 self.warn('Untranslatable node %s.%s found' % (
344 node.__class__.__name__
347 def body(self, nodes):
348 """Calls node() for every node in the iterable passed."""
354 def text_node(writer, node):
359 def variable(writer, node):
361 writer.warn('i18n system used, make sure to install translations', node)
363 if node.literal is not None:
364 writer.literal(node.literal)
366 writer.variable(node.var)
372 def variable_node(writer, node):
373 writer.start_variable()
374 if node.filter_expression.var.var == 'block.super' \
375 and not node.filter_expression.filters:
376 writer.write('super()')
378 writer.node(node.filter_expression)
379 writer.end_variable()
382 @node(FilterExpression)
383 def filter_expression(writer, node):
384 writer.node(node.var)
385 writer.filters(node.filters)
388 @node(core_tags.CommentNode)
389 def comment_tag(writer, node):
393 @node(core_tags.DebugNode)
394 def comment_tag(writer, node):
395 writer.warn('Debug tag detected. Make sure to add a global function '
396 'called debug to the namespace.', node=node)
397 writer.print_expr('debug()')
400 @node(core_tags.ForNode)
401 def for_loop(writer, node):
404 for idx, var in enumerate(node.loopvars):
411 writer.node(node.sequence)
413 writer.write(')|reverse')
416 writer.body(node.nodelist_loop)
421 @node(core_tags.IfNode)
422 def if_condition(writer, node):
426 if node.link_type == core_tags.IfNode.LinkTypes.or_:
429 for idx, (ifnot, expr) in enumerate(node.bool_exprs):
431 writer.write(' %s ' % join_with)
436 writer.body(node.nodelist_true)
437 if node.nodelist_false:
439 writer.body(node.nodelist_false)
443 @node(core_tags.IfEqualNode)
444 def if_equal(writer, node):
447 writer.node(node.var1)
452 writer.node(node.var2)
454 writer.body(node.nodelist_true)
455 if node.nodelist_false:
457 writer.body(node.nodelist_false)
461 @node(loader_tags.BlockNode)
462 def block(writer, node):
463 writer.tag('block ' + node.name.replace('-', '_').rstrip('_'))
465 while node.parent is not None:
467 writer.body(node.nodelist)
468 writer.tag('endblock')
471 @node(loader_tags.ExtendsNode)
472 def extends(writer, node):
474 writer.write('extends ')
475 if node.parent_name_expr:
476 writer.node(node.parent_name_expr)
478 writer.literal(node.parent_name)
480 writer.body(node.nodelist)
483 @node(loader_tags.ConstantIncludeNode)
484 @node(loader_tags.IncludeNode)
485 def include(writer, node):
487 writer.write('include ')
488 if hasattr(node, 'template'):
489 writer.literal(node.template.name)
491 writer.node(node.template_name)
495 @node(core_tags.CycleNode)
496 def cycle(writer, node):
497 if not writer.in_loop:
498 writer.warn('Untranslatable free cycle (cycle outside loop)', node=node)
500 if node.variable_name is not None:
502 writer.write('set %s = ' % node.variable_name)
504 writer.start_variable()
505 writer.write('loop.cycle(')
506 for idx, var in enumerate(node.raw_cycle_vars):
511 if node.variable_name is not None:
514 writer.end_variable()
517 @node(core_tags.FilterNode)
518 def filter(writer, node):
520 writer.write('filter ')
521 writer.filters(node.filter_expr.filters, True)
523 writer.body(node.nodelist)
524 writer.tag('endfilter')
527 @node(core_tags.AutoEscapeControlNode)
528 def autoescape_control(writer, node):
529 original = writer.autoescape
530 writer.autoescape = node.setting
531 writer.body(node.nodelist)
532 writer.autoescape = original
535 @node(core_tags.SpacelessNode)
536 def spaceless(writer, node):
537 original = writer.spaceless
538 writer.spaceless = True
539 writer.warn('entering spaceless mode with different semantics', node)
540 # do the initial stripping
541 nodelist = list(node.nodelist)
543 if isinstance(nodelist[0], TextNode):
544 nodelist[0] = TextNode(nodelist[0].s.lstrip())
545 if isinstance(nodelist[-1], TextNode):
546 nodelist[-1] = TextNode(nodelist[-1].s.rstrip())
547 writer.body(nodelist)
548 writer.spaceless = original
551 @node(core_tags.TemplateTagNode)
552 def template_tag(writer, node):
554 'openblock': writer.block_start_string,
555 'closeblock': writer.block_end_string,
556 'openvariable': writer.variable_start_string,
557 'closevariable': writer.variable_end_string,
558 'opencomment': writer.comment_start_string,
559 'closecomment': writer.comment_end_string,
564 writer.start_variable()
566 writer.end_variable()
569 @node(core_tags.URLNode)
570 def url_tag(writer, node):
571 writer.warn('url node used. make sure to provide a proper url() '
575 writer.write('set %s = ' % node.asvar)
577 writer.start_variable()
578 autoescape = writer.autoescape
580 writer.literal(node.view_name)
581 for arg in node.args:
584 for key, arg in node.kwargs.items():
585 writer.write(', %s=' % key)
591 writer.end_variable()
594 @node(core_tags.WidthRatioNode)
595 def width_ratio(writer, node):
596 writer.warn('widthratio expanded into formula. You may want to provide '
597 'a helper function for this calculation', node)
598 writer.start_variable()
600 writer.node(node.val_expr)
602 writer.node(node.max_expr)
604 writer.write(str(int(node.max_width)))
605 writer.write(')|round|int')
606 writer.end_variable(always_safe=True)
609 @node(core_tags.WithNode)
610 def with_block(writer, node):
611 writer.warn('with block expanded into set statement. This could cause '
612 'variables following that block to be overriden.', node)
614 writer.write('set %s = ' % node.name)
615 writer.node(node.var)
617 writer.body(node.nodelist)
620 @node(core_tags.RegroupNode)
621 def regroup(writer, node):
622 if node.expression.var.literal:
623 writer.warn('literal in groupby filter used. Behavior in that '
624 'situation is undefined and translation is skipped.', node)
626 elif node.expression.filters:
627 writer.warn('filters in groupby filter used. Behavior in that '
628 'situation is undefined which is most likely a bug '
629 'in your code. Filters were ignored.', node)
631 writer.write('set %s = ' % node.var_name)
632 writer.node(node.target)
633 writer.write('|groupby(')
634 writer.literal(node.expression.var.var)
639 @node(core_tags.LoadNode)
640 def warn_load(writer, node):
641 writer.warn('load statement used which was ignored on conversion', node)
644 @node(i18n_tags.GetAvailableLanguagesNode)
645 def get_available_languages(writer, node):
646 writer.warn('make sure to provide a get_available_languages function', node)
647 writer.tag('set %s = get_available_languages()' %
648 writer.translate_variable_name(node.variable))
651 @node(i18n_tags.GetCurrentLanguageNode)
652 def get_current_language(writer, node):
653 writer.warn('make sure to provide a get_current_language function', node)
654 writer.tag('set %s = get_current_language()' %
655 writer.translate_variable_name(node.variable))
658 @node(i18n_tags.GetCurrentLanguageBidiNode)
659 def get_current_language_bidi(writer, node):
660 writer.warn('make sure to provide a get_current_language_bidi function', node)
661 writer.tag('set %s = get_current_language_bidi()' %
662 writer.translate_variable_name(node.variable))
665 @node(i18n_tags.TranslateNode)
666 def simple_gettext(writer, node):
667 writer.warn('i18n system used, make sure to install translations', node)
668 writer.start_variable()
670 writer.node(node.value)
672 writer.end_variable()
675 @node(i18n_tags.BlockTranslateNode)
676 def translate_block(writer, node):
683 first_var.append(name)
685 def dump_token_list(tokens):
687 if token.token_type == TOKEN_TEXT:
688 writer.write(token.contents)
689 elif token.token_type == TOKEN_VAR:
690 writer.print_expr(token.contents)
691 touch_var(token.contents)
693 writer.warn('i18n system used, make sure to install translations', node)
695 writer.write('trans')
697 for idx, (key, var) in enumerate(node.extra_context.items()):
700 writer.write(' %s=' % key)
702 writer.node(var.filter_expression)
706 if node.plural and node.countervar and node.counter:
708 plural_var = node.countervar
709 if plural_var not in variables:
712 touch_var(plural_var)
713 writer.write(' %s=' % plural_var)
714 writer.node(node.counter)
717 dump_token_list(node.singular)
718 if node.plural and node.countervar and node.counter:
720 writer.write('pluralize')
721 if node.countervar != first_var[0]:
722 writer.write(' ' + node.countervar)
724 dump_token_list(node.plural)
725 writer.tag('endtrans')
728 def simple_tag(writer, node):
729 """Check if the simple tag exist as a filter in """
732 name not in writer.env.filters and \
733 name not in writer._filters_warned:
734 writer._filters_warned.add(name)
735 writer.warn('Filter %s probably doesn\'t exist in Jinja' %
738 if not node.vars_to_resolve:
739 # No argument, pass the request
740 writer.start_variable()
741 writer.write('request|')
743 writer.end_variable()
746 first_var = node.vars_to_resolve[0]
747 args = node.vars_to_resolve[1:]
748 writer.start_variable()
750 # Copied from Writer.filters()
751 writer.node(first_var)
757 for idx, var in enumerate(args):
763 writer.literal(var.literal)
765 writer.end_variable()
767 # get rid of node now, it shouldn't be used normally