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: Copyright 2008 by Armin Ronacher.
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)')
90 _old_cycle_init = core_tags.CycleNode.__init__
91 def _fixed_cycle_init(self, cyclevars, variable_name=None):
92 self.raw_cycle_vars = map(Variable, cyclevars)
93 _old_cycle_init(self, cyclevars, variable_name)
94 core_tags.CycleNode.__init__ = _fixed_cycle_init
99 _node_handlers[cls] = f
104 def convert_templates(output_dir, extensions=('.html', '.txt'), writer=None,
106 """Iterates over all templates in the template dirs configured and
107 translates them and writes the new templates into the output directory.
112 def filter_templates(files):
113 for filename in files:
114 ifilename = filename.lower()
115 for extension in extensions:
116 if ifilename.endswith(extension):
119 def translate(f, loadname):
120 template = loader.get_template(loadname)
121 original = writer.stream
123 writer.body(template.nodelist)
124 writer.stream = original
127 def callback(template):
130 for directory in settings.TEMPLATE_DIRS:
131 for dirname, _, files in os.walk(directory):
132 dirname = dirname[len(directory) + 1:]
133 for filename in filter_templates(files):
134 source = os.path.normpath(os.path.join(dirname, filename))
135 target = os.path.join(output_dir, dirname, filename)
136 basetarget = os.path.dirname(target)
137 if not os.path.exists(basetarget):
138 os.makedirs(basetarget)
140 f = file(target, 'w')
147 class Writer(object):
148 """The core writer class."""
150 def __init__(self, stream=None, error_stream=None,
151 block_start_string=BLOCK_START_STRING,
152 block_end_string=BLOCK_END_STRING,
153 variable_start_string=VARIABLE_START_STRING,
154 variable_end_string=VARIABLE_END_STRING,
155 comment_start_string=COMMENT_START_STRING,
156 comment_end_string=COMMENT_END_STRING,
157 initial_autoescape=True,
158 use_jinja_autoescape=False,
159 custom_node_handlers=None,
164 if error_stream is None:
165 error_stream = sys.stderr
167 self.error_stream = error_stream
168 self.block_start_string = block_start_string
169 self.block_end_string = block_end_string
170 self.variable_start_string = variable_start_string
171 self.variable_end_string = variable_end_string
172 self.comment_start_string = comment_start_string
173 self.comment_end_string = comment_end_string
174 self.autoescape = initial_autoescape
175 self.spaceless = False
176 self.use_jinja_autoescape = use_jinja_autoescape
177 self.node_handlers = dict(_node_handlers,
178 **(custom_node_handlers or {}))
180 self._filters_warned = set()
184 def enter_loop(self):
185 """Increments the loop depth so that write functions know if they
188 self._loop_depth += 1
190 def leave_loop(self):
191 """Reverse of enter_loop."""
192 self._loop_depth -= 1
196 """True if we are in a loop."""
197 return self._loop_depth > 0
200 """Writes stuff to the stream."""
201 self.stream.write(s.encode(settings.FILE_CHARSET))
203 def print_expr(self, expr):
204 """Open a variable tag, write to the string to the stream and close."""
205 self.start_variable()
209 def _post_open(self):
215 def _pre_close(self):
221 def start_variable(self):
222 """Start a variable."""
223 self.write(self.variable_start_string)
226 def end_variable(self, always_safe=False):
227 """End a variable."""
228 if not always_safe and self.autoescape and \
229 not self.use_jinja_autoescape:
232 self.write(self.variable_end_string)
234 def start_block(self):
235 """Starts a block."""
236 self.write(self.block_start_string)
242 self.write(self.block_end_string)
245 """Like `print_expr` just for blocks."""
250 def variable(self, name):
251 """Prints a variable. This performs variable name transformation."""
252 self.write(self.translate_variable_name(name))
254 def literal(self, value):
255 """Writes a value as literal."""
257 if value[:2] in ('u"', "u'"):
261 def filters(self, filters, is_block=False):
262 """Dumps a list of filters."""
263 want_pipe = not is_block
264 for filter, args in filters:
265 name = self.get_filter_name(filter)
267 self.warn('Could not find filter %s' % name)
269 if name not in DEFAULT_FILTERS and \
270 name not in self._filters_warned:
271 self._filters_warned.add(name)
272 self.warn('Filter %s probably doesn\'t exist in Jinja' %
281 for idx, (is_var, value) in enumerate(args):
290 def get_location(self, origin, position):
291 """Returns the location for an origin and position tuple as name
294 if hasattr(origin, 'source'):
295 source = origin.source
296 name = '<unknown source>'
298 source = origin.loader(origin.loadname, origin.dirs)[0]
299 name = origin.loadname
300 lineno = len(_newline_re.findall(source[:position[0]])) + 1
303 def warn(self, message, node=None):
304 """Prints a warning to the error stream."""
305 if node is not None and hasattr(node, 'source'):
306 filename, lineno = self.get_location(*node.source)
307 message = '[%s:%d] %s' % (filename, lineno, message)
308 print >> self.error_stream, message
310 def translate_variable_name(self, var):
311 """Performs variable name translation."""
312 if self.in_loop and var == 'forloop' or var.startswith('forloop.'):
315 for reg, rep, unless in self.var_re:
316 no_unless = unless and unless.search(var) or True
317 if reg.search(var) and no_unless:
318 var = reg.sub(rep, var)
322 def get_filter_name(self, filter):
323 """Returns the filter name for a filter function or `None` if there
326 if filter not in _resolved_filters:
327 for library in libraries.values():
328 for key, value in library.filters.iteritems():
329 _resolved_filters[value] = key
330 return _resolved_filters.get(filter, None)
332 def node(self, node):
333 """Invokes the node handler for a node."""
334 for cls, handler in self.node_handlers.iteritems():
335 if type(node) is cls or type(node).__name__ == cls:
339 self.warn('Untranslatable node %s.%s found' % (
341 node.__class__.__name__
344 def body(self, nodes):
345 """Calls node() for every node in the iterable passed."""
351 def text_node(writer, node):
356 def variable(writer, node):
358 writer.warn('i18n system used, make sure to install translations', node)
360 if node.literal is not None:
361 writer.literal(node.literal)
363 writer.variable(node.var)
369 def variable_node(writer, node):
370 writer.start_variable()
371 if node.filter_expression.var.var == 'block.super' \
372 and not node.filter_expression.filters:
373 writer.write('super()')
375 writer.node(node.filter_expression)
376 writer.end_variable()
379 @node(FilterExpression)
380 def filter_expression(writer, node):
381 writer.node(node.var)
382 writer.filters(node.filters)
385 @node(core_tags.CommentNode)
386 def comment_tag(writer, node):
390 @node(core_tags.DebugNode)
391 def comment_tag(writer, node):
392 writer.warn('Debug tag detected. Make sure to add a global function '
393 'called debug to the namespace.', node=node)
394 writer.print_expr('debug()')
397 @node(core_tags.ForNode)
398 def for_loop(writer, node):
401 for idx, var in enumerate(node.loopvars):
408 writer.node(node.sequence)
410 writer.write(')|reverse')
413 writer.body(node.nodelist_loop)
418 @node(core_tags.IfNode)
419 def if_condition(writer, node):
423 if node.link_type == core_tags.IfNode.LinkTypes.or_:
426 for idx, (ifnot, expr) in enumerate(node.bool_exprs):
428 writer.write(' %s ' % join_with)
433 writer.body(node.nodelist_true)
434 if node.nodelist_false:
436 writer.body(node.nodelist_false)
440 @node(core_tags.IfEqualNode)
441 def if_equal(writer, node):
444 writer.node(node.var1)
449 writer.node(node.var2)
451 writer.body(node.nodelist_true)
452 if node.nodelist_false:
454 writer.body(node.nodelist_false)
458 @node(loader_tags.BlockNode)
459 def block(writer, node):
460 writer.tag('block ' + node.name.replace('-', '_').rstrip('_'))
462 while node.parent is not None:
464 writer.body(node.nodelist)
465 writer.tag('endblock')
468 @node(loader_tags.ExtendsNode)
469 def extends(writer, node):
471 writer.write('extends ')
472 if node.parent_name_expr:
473 writer.node(node.parent_name_expr)
475 writer.literal(node.parent_name)
477 writer.body(node.nodelist)
480 @node(loader_tags.ConstantIncludeNode)
481 @node(loader_tags.IncludeNode)
482 def include(writer, node):
484 writer.write('include ')
485 if hasattr(node, 'template'):
486 writer.literal(node.template.name)
488 writer.node(node.template_name)
492 @node(core_tags.CycleNode)
493 def cycle(writer, node):
494 if not writer.in_loop:
495 writer.warn('Untranslatable free cycle (cycle outside loop)', node=node)
497 if node.variable_name is not None:
499 writer.write('set %s = ' % node.variable_name)
501 writer.start_variable()
502 writer.write('loop.cycle(')
503 for idx, var in enumerate(node.raw_cycle_vars):
508 if node.variable_name is not None:
511 writer.end_variable()
514 @node(core_tags.FilterNode)
515 def filter(writer, node):
517 writer.write('filter ')
518 writer.filters(node.filter_expr.filters, True)
520 writer.body(node.nodelist)
521 writer.tag('endfilter')
524 @node(core_tags.AutoEscapeControlNode)
525 def autoescape_control(writer, node):
526 original = writer.autoescape
527 writer.autoescape = node.setting
528 writer.body(node.nodelist)
529 writer.autoescape = original
532 @node(core_tags.SpacelessNode)
533 def spaceless(writer, node):
534 original = writer.spaceless
535 writer.spaceless = True
536 writer.warn('entering spaceless mode with different semantics', node)
537 # do the initial stripping
538 nodelist = list(node.nodelist)
540 if isinstance(nodelist[0], TextNode):
541 nodelist[0] = TextNode(nodelist[0].s.lstrip())
542 if isinstance(nodelist[-1], TextNode):
543 nodelist[-1] = TextNode(nodelist[-1].s.rstrip())
544 writer.body(nodelist)
545 writer.spaceless = original
548 @node(core_tags.TemplateTagNode)
549 def template_tag(writer, node):
551 'openblock': writer.block_start_string,
552 'closeblock': writer.block_end_string,
553 'openvariable': writer.variable_start_string,
554 'closevariable': writer.variable_end_string,
555 'opencomment': writer.comment_start_string,
556 'closecomment': writer.comment_end_string,
561 writer.start_variable()
563 writer.end_variable()
566 @node(core_tags.URLNode)
567 def url_tag(writer, node):
568 writer.warn('url node used. make sure to provide a proper url() '
572 writer.write('set %s = ' % node.asvar)
574 writer.start_variable()
575 autoescape = writer.autoescape
577 writer.literal(node.view_name)
578 for arg in node.args:
581 for key, arg in node.kwargs.items():
582 writer.write(', %s=' % key)
588 writer.end_variable()
591 @node(core_tags.WidthRatioNode)
592 def width_ratio(writer, node):
593 writer.warn('widthratio expanded into formula. You may want to provide '
594 'a helper function for this calculation', node)
595 writer.start_variable()
597 writer.node(node.val_expr)
599 writer.node(node.max_expr)
601 writer.write(str(int(node.max_width)))
602 writer.write(')|round|int')
603 writer.end_variable(always_safe=True)
606 @node(core_tags.WithNode)
607 def with_block(writer, node):
608 writer.warn('with block expanded into set statement. This could cause '
609 'variables following that block to be overriden.', node)
611 writer.write('set %s = ' % node.name)
612 writer.node(node.var)
614 writer.body(node.nodelist)
617 @node(core_tags.RegroupNode)
618 def regroup(writer, node):
619 if node.expression.var.literal:
620 writer.warn('literal in groupby filter used. Behavior in that '
621 'situation is undefined and translation is skipped.', node)
623 elif node.expression.filters:
624 writer.warn('filters in groupby filter used. Behavior in that '
625 'situation is undefined which is most likely a bug '
626 'in your code. Filters were ignored.', node)
628 writer.write('set %s = ' % node.var_name)
629 writer.node(node.target)
630 writer.write('|groupby(')
631 writer.literal(node.expression.var.var)
636 @node(core_tags.LoadNode)
637 def warn_load(writer, node):
638 writer.warn('load statement used which was ignored on conversion', node)
641 @node(i18n_tags.GetAvailableLanguagesNode)
642 def get_available_languages(writer, node):
643 writer.warn('make sure to provide a get_available_languages function', node)
644 writer.tag('set %s = get_available_languages()' %
645 writer.translate_variable_name(node.variable))
648 @node(i18n_tags.GetCurrentLanguageNode)
649 def get_current_language(writer, node):
650 writer.warn('make sure to provide a get_current_language function', node)
651 writer.tag('set %s = get_current_language()' %
652 writer.translate_variable_name(node.variable))
655 @node(i18n_tags.GetCurrentLanguageBidiNode)
656 def get_current_language_bidi(writer, node):
657 writer.warn('make sure to provide a get_current_language_bidi function', node)
658 writer.tag('set %s = get_current_language_bidi()' %
659 writer.translate_variable_name(node.variable))
662 @node(i18n_tags.TranslateNode)
663 def simple_gettext(writer, node):
664 writer.warn('i18n system used, make sure to install translations', node)
665 writer.start_variable()
667 writer.node(node.value)
669 writer.end_variable()
672 @node(i18n_tags.BlockTranslateNode)
673 def translate_block(writer, node):
680 first_var.append(name)
682 def dump_token_list(tokens):
684 if token.token_type == TOKEN_TEXT:
685 writer.write(token.contents)
686 elif token.token_type == TOKEN_VAR:
687 writer.print_expr(token.contents)
688 touch_var(token.contents)
690 writer.warn('i18n system used, make sure to install translations', node)
692 writer.write('trans')
694 for idx, (key, var) in enumerate(node.extra_context.items()):
697 writer.write(' %s=' % key)
699 writer.node(var.filter_expression)
703 if node.plural and node.countervar and node.counter:
705 plural_var = node.countervar
706 if plural_var not in variables:
709 touch_var(plural_var)
710 writer.write(' %s=' % plural_var)
711 writer.node(node.counter)
714 dump_token_list(node.singular)
715 if node.plural and node.countervar and node.counter:
717 writer.write('pluralize')
718 if node.countervar != first_var[0]:
719 writer.write(' ' + node.countervar)
721 dump_token_list(node.plural)
722 writer.tag('endtrans')
725 def simple_tag(writer, node):
726 """Check if the simple tag exist as a filter in """
729 name not in writer.env.filters and \
730 name not in writer._filters_warned:
731 writer._filters_warned.add(name)
732 writer.warn('Filter %s probably doesn\'t exist in Jinja' %
735 if not node.vars_to_resolve:
736 # No argument, pass the request
737 writer.start_variable()
738 writer.write('request|')
740 writer.end_variable()
743 first_var = node.vars_to_resolve[0]
744 args = node.vars_to_resolve[1:]
745 writer.start_variable()
747 # Copied from Writer.filters()
748 writer.node(first_var)
754 for idx, var in enumerate(args):
760 writer.literal(var.literal)
762 writer.end_variable()
764 # get rid of node now, it shouldn't be used normally