| # -*- coding: utf-8 -*- | 
 | """ | 
 |     jinja2.ext | 
 |     ~~~~~~~~~~ | 
 |  | 
 |     Jinja extensions allow to add custom tags similar to the way django custom | 
 |     tags work.  By default two example extensions exist: an i18n and a cache | 
 |     extension. | 
 |  | 
 |     :copyright: (c) 2010 by the Jinja Team. | 
 |     :license: BSD. | 
 | """ | 
 | from jinja2 import nodes | 
 | from jinja2.defaults import BLOCK_START_STRING, \ | 
 |      BLOCK_END_STRING, VARIABLE_START_STRING, VARIABLE_END_STRING, \ | 
 |      COMMENT_START_STRING, COMMENT_END_STRING, LINE_STATEMENT_PREFIX, \ | 
 |      LINE_COMMENT_PREFIX, TRIM_BLOCKS, NEWLINE_SEQUENCE, \ | 
 |      KEEP_TRAILING_NEWLINE, LSTRIP_BLOCKS | 
 | from jinja2.environment import Environment | 
 | from jinja2.runtime import concat | 
 | from jinja2.exceptions import TemplateAssertionError, TemplateSyntaxError | 
 | from jinja2.utils import contextfunction, import_string, Markup | 
 | from jinja2._compat import next, with_metaclass, string_types, iteritems | 
 |  | 
 |  | 
 | # the only real useful gettext functions for a Jinja template.  Note | 
 | # that ugettext must be assigned to gettext as Jinja doesn't support | 
 | # non unicode strings. | 
 | GETTEXT_FUNCTIONS = ('_', 'gettext', 'ngettext') | 
 |  | 
 |  | 
 | class ExtensionRegistry(type): | 
 |     """Gives the extension an unique identifier.""" | 
 |  | 
 |     def __new__(cls, name, bases, d): | 
 |         rv = type.__new__(cls, name, bases, d) | 
 |         rv.identifier = rv.__module__ + '.' + rv.__name__ | 
 |         return rv | 
 |  | 
 |  | 
 | class Extension(with_metaclass(ExtensionRegistry, object)): | 
 |     """Extensions can be used to add extra functionality to the Jinja template | 
 |     system at the parser level.  Custom extensions are bound to an environment | 
 |     but may not store environment specific data on `self`.  The reason for | 
 |     this is that an extension can be bound to another environment (for | 
 |     overlays) by creating a copy and reassigning the `environment` attribute. | 
 |  | 
 |     As extensions are created by the environment they cannot accept any | 
 |     arguments for configuration.  One may want to work around that by using | 
 |     a factory function, but that is not possible as extensions are identified | 
 |     by their import name.  The correct way to configure the extension is | 
 |     storing the configuration values on the environment.  Because this way the | 
 |     environment ends up acting as central configuration storage the | 
 |     attributes may clash which is why extensions have to ensure that the names | 
 |     they choose for configuration are not too generic.  ``prefix`` for example | 
 |     is a terrible name, ``fragment_cache_prefix`` on the other hand is a good | 
 |     name as includes the name of the extension (fragment cache). | 
 |     """ | 
 |  | 
 |     #: if this extension parses this is the list of tags it's listening to. | 
 |     tags = set() | 
 |  | 
 |     #: the priority of that extension.  This is especially useful for | 
 |     #: extensions that preprocess values.  A lower value means higher | 
 |     #: priority. | 
 |     #: | 
 |     #: .. versionadded:: 2.4 | 
 |     priority = 100 | 
 |  | 
 |     def __init__(self, environment): | 
 |         self.environment = environment | 
 |  | 
 |     def bind(self, environment): | 
 |         """Create a copy of this extension bound to another environment.""" | 
 |         rv = object.__new__(self.__class__) | 
 |         rv.__dict__.update(self.__dict__) | 
 |         rv.environment = environment | 
 |         return rv | 
 |  | 
 |     def preprocess(self, source, name, filename=None): | 
 |         """This method is called before the actual lexing and can be used to | 
 |         preprocess the source.  The `filename` is optional.  The return value | 
 |         must be the preprocessed source. | 
 |         """ | 
 |         return source | 
 |  | 
 |     def filter_stream(self, stream): | 
 |         """It's passed a :class:`~jinja2.lexer.TokenStream` that can be used | 
 |         to filter tokens returned.  This method has to return an iterable of | 
 |         :class:`~jinja2.lexer.Token`\s, but it doesn't have to return a | 
 |         :class:`~jinja2.lexer.TokenStream`. | 
 |  | 
 |         In the `ext` folder of the Jinja2 source distribution there is a file | 
 |         called `inlinegettext.py` which implements a filter that utilizes this | 
 |         method. | 
 |         """ | 
 |         return stream | 
 |  | 
 |     def parse(self, parser): | 
 |         """If any of the :attr:`tags` matched this method is called with the | 
 |         parser as first argument.  The token the parser stream is pointing at | 
 |         is the name token that matched.  This method has to return one or a | 
 |         list of multiple nodes. | 
 |         """ | 
 |         raise NotImplementedError() | 
 |  | 
 |     def attr(self, name, lineno=None): | 
 |         """Return an attribute node for the current extension.  This is useful | 
 |         to pass constants on extensions to generated template code. | 
 |  | 
 |         :: | 
 |  | 
 |             self.attr('_my_attribute', lineno=lineno) | 
 |         """ | 
 |         return nodes.ExtensionAttribute(self.identifier, name, lineno=lineno) | 
 |  | 
 |     def call_method(self, name, args=None, kwargs=None, dyn_args=None, | 
 |                     dyn_kwargs=None, lineno=None): | 
 |         """Call a method of the extension.  This is a shortcut for | 
 |         :meth:`attr` + :class:`jinja2.nodes.Call`. | 
 |         """ | 
 |         if args is None: | 
 |             args = [] | 
 |         if kwargs is None: | 
 |             kwargs = [] | 
 |         return nodes.Call(self.attr(name, lineno=lineno), args, kwargs, | 
 |                           dyn_args, dyn_kwargs, lineno=lineno) | 
 |  | 
 |  | 
 | @contextfunction | 
 | def _gettext_alias(__context, *args, **kwargs): | 
 |     return __context.call(__context.resolve('gettext'), *args, **kwargs) | 
 |  | 
 |  | 
 | def _make_new_gettext(func): | 
 |     @contextfunction | 
 |     def gettext(__context, __string, **variables): | 
 |         rv = __context.call(func, __string) | 
 |         if __context.eval_ctx.autoescape: | 
 |             rv = Markup(rv) | 
 |         return rv % variables | 
 |     return gettext | 
 |  | 
 |  | 
 | def _make_new_ngettext(func): | 
 |     @contextfunction | 
 |     def ngettext(__context, __singular, __plural, __num, **variables): | 
 |         variables.setdefault('num', __num) | 
 |         rv = __context.call(func, __singular, __plural, __num) | 
 |         if __context.eval_ctx.autoescape: | 
 |             rv = Markup(rv) | 
 |         return rv % variables | 
 |     return ngettext | 
 |  | 
 |  | 
 | class InternationalizationExtension(Extension): | 
 |     """This extension adds gettext support to Jinja2.""" | 
 |     tags = set(['trans']) | 
 |  | 
 |     # TODO: the i18n extension is currently reevaluating values in a few | 
 |     # situations.  Take this example: | 
 |     #   {% trans count=something() %}{{ count }} foo{% pluralize | 
 |     #     %}{{ count }} fooss{% endtrans %} | 
 |     # something is called twice here.  One time for the gettext value and | 
 |     # the other time for the n-parameter of the ngettext function. | 
 |  | 
 |     def __init__(self, environment): | 
 |         Extension.__init__(self, environment) | 
 |         environment.globals['_'] = _gettext_alias | 
 |         environment.extend( | 
 |             install_gettext_translations=self._install, | 
 |             install_null_translations=self._install_null, | 
 |             install_gettext_callables=self._install_callables, | 
 |             uninstall_gettext_translations=self._uninstall, | 
 |             extract_translations=self._extract, | 
 |             newstyle_gettext=False | 
 |         ) | 
 |  | 
 |     def _install(self, translations, newstyle=None): | 
 |         gettext = getattr(translations, 'ugettext', None) | 
 |         if gettext is None: | 
 |             gettext = translations.gettext | 
 |         ngettext = getattr(translations, 'ungettext', None) | 
 |         if ngettext is None: | 
 |             ngettext = translations.ngettext | 
 |         self._install_callables(gettext, ngettext, newstyle) | 
 |  | 
 |     def _install_null(self, newstyle=None): | 
 |         self._install_callables( | 
 |             lambda x: x, | 
 |             lambda s, p, n: (n != 1 and (p,) or (s,))[0], | 
 |             newstyle | 
 |         ) | 
 |  | 
 |     def _install_callables(self, gettext, ngettext, newstyle=None): | 
 |         if newstyle is not None: | 
 |             self.environment.newstyle_gettext = newstyle | 
 |         if self.environment.newstyle_gettext: | 
 |             gettext = _make_new_gettext(gettext) | 
 |             ngettext = _make_new_ngettext(ngettext) | 
 |         self.environment.globals.update( | 
 |             gettext=gettext, | 
 |             ngettext=ngettext | 
 |         ) | 
 |  | 
 |     def _uninstall(self, translations): | 
 |         for key in 'gettext', 'ngettext': | 
 |             self.environment.globals.pop(key, None) | 
 |  | 
 |     def _extract(self, source, gettext_functions=GETTEXT_FUNCTIONS): | 
 |         if isinstance(source, string_types): | 
 |             source = self.environment.parse(source) | 
 |         return extract_from_ast(source, gettext_functions) | 
 |  | 
 |     def parse(self, parser): | 
 |         """Parse a translatable tag.""" | 
 |         lineno = next(parser.stream).lineno | 
 |         num_called_num = False | 
 |  | 
 |         # find all the variables referenced.  Additionally a variable can be | 
 |         # defined in the body of the trans block too, but this is checked at | 
 |         # a later state. | 
 |         plural_expr = None | 
 |         plural_expr_assignment = None | 
 |         variables = {} | 
 |         while parser.stream.current.type != 'block_end': | 
 |             if variables: | 
 |                 parser.stream.expect('comma') | 
 |  | 
 |             # skip colon for python compatibility | 
 |             if parser.stream.skip_if('colon'): | 
 |                 break | 
 |  | 
 |             name = parser.stream.expect('name') | 
 |             if name.value in variables: | 
 |                 parser.fail('translatable variable %r defined twice.' % | 
 |                             name.value, name.lineno, | 
 |                             exc=TemplateAssertionError) | 
 |  | 
 |             # expressions | 
 |             if parser.stream.current.type == 'assign': | 
 |                 next(parser.stream) | 
 |                 variables[name.value] = var = parser.parse_expression() | 
 |             else: | 
 |                 variables[name.value] = var = nodes.Name(name.value, 'load') | 
 |  | 
 |             if plural_expr is None: | 
 |                 if isinstance(var, nodes.Call): | 
 |                     plural_expr = nodes.Name('_trans', 'load') | 
 |                     variables[name.value] = plural_expr | 
 |                     plural_expr_assignment = nodes.Assign( | 
 |                         nodes.Name('_trans', 'store'), var) | 
 |                 else: | 
 |                     plural_expr = var | 
 |                 num_called_num = name.value == 'num' | 
 |  | 
 |         parser.stream.expect('block_end') | 
 |  | 
 |         plural = plural_names = None | 
 |         have_plural = False | 
 |         referenced = set() | 
 |  | 
 |         # now parse until endtrans or pluralize | 
 |         singular_names, singular = self._parse_block(parser, True) | 
 |         if singular_names: | 
 |             referenced.update(singular_names) | 
 |             if plural_expr is None: | 
 |                 plural_expr = nodes.Name(singular_names[0], 'load') | 
 |                 num_called_num = singular_names[0] == 'num' | 
 |  | 
 |         # if we have a pluralize block, we parse that too | 
 |         if parser.stream.current.test('name:pluralize'): | 
 |             have_plural = True | 
 |             next(parser.stream) | 
 |             if parser.stream.current.type != 'block_end': | 
 |                 name = parser.stream.expect('name') | 
 |                 if name.value not in variables: | 
 |                     parser.fail('unknown variable %r for pluralization' % | 
 |                                 name.value, name.lineno, | 
 |                                 exc=TemplateAssertionError) | 
 |                 plural_expr = variables[name.value] | 
 |                 num_called_num = name.value == 'num' | 
 |             parser.stream.expect('block_end') | 
 |             plural_names, plural = self._parse_block(parser, False) | 
 |             next(parser.stream) | 
 |             referenced.update(plural_names) | 
 |         else: | 
 |             next(parser.stream) | 
 |  | 
 |         # register free names as simple name expressions | 
 |         for var in referenced: | 
 |             if var not in variables: | 
 |                 variables[var] = nodes.Name(var, 'load') | 
 |  | 
 |         if not have_plural: | 
 |             plural_expr = None | 
 |         elif plural_expr is None: | 
 |             parser.fail('pluralize without variables', lineno) | 
 |  | 
 |         node = self._make_node(singular, plural, variables, plural_expr, | 
 |                                bool(referenced), | 
 |                                num_called_num and have_plural) | 
 |         node.set_lineno(lineno) | 
 |         if plural_expr_assignment is not None: | 
 |             return [plural_expr_assignment, node] | 
 |         else: | 
 |             return node | 
 |  | 
 |     def _parse_block(self, parser, allow_pluralize): | 
 |         """Parse until the next block tag with a given name.""" | 
 |         referenced = [] | 
 |         buf = [] | 
 |         while 1: | 
 |             if parser.stream.current.type == 'data': | 
 |                 buf.append(parser.stream.current.value.replace('%', '%%')) | 
 |                 next(parser.stream) | 
 |             elif parser.stream.current.type == 'variable_begin': | 
 |                 next(parser.stream) | 
 |                 name = parser.stream.expect('name').value | 
 |                 referenced.append(name) | 
 |                 buf.append('%%(%s)s' % name) | 
 |                 parser.stream.expect('variable_end') | 
 |             elif parser.stream.current.type == 'block_begin': | 
 |                 next(parser.stream) | 
 |                 if parser.stream.current.test('name:endtrans'): | 
 |                     break | 
 |                 elif parser.stream.current.test('name:pluralize'): | 
 |                     if allow_pluralize: | 
 |                         break | 
 |                     parser.fail('a translatable section can have only one ' | 
 |                                 'pluralize section') | 
 |                 parser.fail('control structures in translatable sections are ' | 
 |                             'not allowed') | 
 |             elif parser.stream.eos: | 
 |                 parser.fail('unclosed translation block') | 
 |             else: | 
 |                 assert False, 'internal parser error' | 
 |  | 
 |         return referenced, concat(buf) | 
 |  | 
 |     def _make_node(self, singular, plural, variables, plural_expr, | 
 |                    vars_referenced, num_called_num): | 
 |         """Generates a useful node from the data provided.""" | 
 |         # no variables referenced?  no need to escape for old style | 
 |         # gettext invocations only if there are vars. | 
 |         if not vars_referenced and not self.environment.newstyle_gettext: | 
 |             singular = singular.replace('%%', '%') | 
 |             if plural: | 
 |                 plural = plural.replace('%%', '%') | 
 |  | 
 |         # singular only: | 
 |         if plural_expr is None: | 
 |             gettext = nodes.Name('gettext', 'load') | 
 |             node = nodes.Call(gettext, [nodes.Const(singular)], | 
 |                               [], None, None) | 
 |  | 
 |         # singular and plural | 
 |         else: | 
 |             ngettext = nodes.Name('ngettext', 'load') | 
 |             node = nodes.Call(ngettext, [ | 
 |                 nodes.Const(singular), | 
 |                 nodes.Const(plural), | 
 |                 plural_expr | 
 |             ], [], None, None) | 
 |  | 
 |         # in case newstyle gettext is used, the method is powerful | 
 |         # enough to handle the variable expansion and autoescape | 
 |         # handling itself | 
 |         if self.environment.newstyle_gettext: | 
 |             for key, value in iteritems(variables): | 
 |                 # the function adds that later anyways in case num was | 
 |                 # called num, so just skip it. | 
 |                 if num_called_num and key == 'num': | 
 |                     continue | 
 |                 node.kwargs.append(nodes.Keyword(key, value)) | 
 |  | 
 |         # otherwise do that here | 
 |         else: | 
 |             # mark the return value as safe if we are in an | 
 |             # environment with autoescaping turned on | 
 |             node = nodes.MarkSafeIfAutoescape(node) | 
 |             if variables: | 
 |                 node = nodes.Mod(node, nodes.Dict([ | 
 |                     nodes.Pair(nodes.Const(key), value) | 
 |                     for key, value in variables.items() | 
 |                 ])) | 
 |         return nodes.Output([node]) | 
 |  | 
 |  | 
 | class ExprStmtExtension(Extension): | 
 |     """Adds a `do` tag to Jinja2 that works like the print statement just | 
 |     that it doesn't print the return value. | 
 |     """ | 
 |     tags = set(['do']) | 
 |  | 
 |     def parse(self, parser): | 
 |         node = nodes.ExprStmt(lineno=next(parser.stream).lineno) | 
 |         node.node = parser.parse_tuple() | 
 |         return node | 
 |  | 
 |  | 
 | class LoopControlExtension(Extension): | 
 |     """Adds break and continue to the template engine.""" | 
 |     tags = set(['break', 'continue']) | 
 |  | 
 |     def parse(self, parser): | 
 |         token = next(parser.stream) | 
 |         if token.value == 'break': | 
 |             return nodes.Break(lineno=token.lineno) | 
 |         return nodes.Continue(lineno=token.lineno) | 
 |  | 
 |  | 
 | class WithExtension(Extension): | 
 |     """Adds support for a django-like with block.""" | 
 |     tags = set(['with']) | 
 |  | 
 |     def parse(self, parser): | 
 |         node = nodes.Scope(lineno=next(parser.stream).lineno) | 
 |         assignments = [] | 
 |         while parser.stream.current.type != 'block_end': | 
 |             lineno = parser.stream.current.lineno | 
 |             if assignments: | 
 |                 parser.stream.expect('comma') | 
 |             target = parser.parse_assign_target() | 
 |             parser.stream.expect('assign') | 
 |             expr = parser.parse_expression() | 
 |             assignments.append(nodes.Assign(target, expr, lineno=lineno)) | 
 |         node.body = assignments + \ | 
 |             list(parser.parse_statements(('name:endwith',), | 
 |                                          drop_needle=True)) | 
 |         return node | 
 |  | 
 |  | 
 | class AutoEscapeExtension(Extension): | 
 |     """Changes auto escape rules for a scope.""" | 
 |     tags = set(['autoescape']) | 
 |  | 
 |     def parse(self, parser): | 
 |         node = nodes.ScopedEvalContextModifier(lineno=next(parser.stream).lineno) | 
 |         node.options = [ | 
 |             nodes.Keyword('autoescape', parser.parse_expression()) | 
 |         ] | 
 |         node.body = parser.parse_statements(('name:endautoescape',), | 
 |                                             drop_needle=True) | 
 |         return nodes.Scope([node]) | 
 |  | 
 |  | 
 | def extract_from_ast(node, gettext_functions=GETTEXT_FUNCTIONS, | 
 |                      babel_style=True): | 
 |     """Extract localizable strings from the given template node.  Per | 
 |     default this function returns matches in babel style that means non string | 
 |     parameters as well as keyword arguments are returned as `None`.  This | 
 |     allows Babel to figure out what you really meant if you are using | 
 |     gettext functions that allow keyword arguments for placeholder expansion. | 
 |     If you don't want that behavior set the `babel_style` parameter to `False` | 
 |     which causes only strings to be returned and parameters are always stored | 
 |     in tuples.  As a consequence invalid gettext calls (calls without a single | 
 |     string parameter or string parameters after non-string parameters) are | 
 |     skipped. | 
 |  | 
 |     This example explains the behavior: | 
 |  | 
 |     >>> from jinja2 import Environment | 
 |     >>> env = Environment() | 
 |     >>> node = env.parse('{{ (_("foo"), _(), ngettext("foo", "bar", 42)) }}') | 
 |     >>> list(extract_from_ast(node)) | 
 |     [(1, '_', 'foo'), (1, '_', ()), (1, 'ngettext', ('foo', 'bar', None))] | 
 |     >>> list(extract_from_ast(node, babel_style=False)) | 
 |     [(1, '_', ('foo',)), (1, 'ngettext', ('foo', 'bar'))] | 
 |  | 
 |     For every string found this function yields a ``(lineno, function, | 
 |     message)`` tuple, where: | 
 |  | 
 |     * ``lineno`` is the number of the line on which the string was found, | 
 |     * ``function`` is the name of the ``gettext`` function used (if the | 
 |       string was extracted from embedded Python code), and | 
 |     *  ``message`` is the string itself (a ``unicode`` object, or a tuple | 
 |        of ``unicode`` objects for functions with multiple string arguments). | 
 |  | 
 |     This extraction function operates on the AST and is because of that unable | 
 |     to extract any comments.  For comment support you have to use the babel | 
 |     extraction interface or extract comments yourself. | 
 |     """ | 
 |     for node in node.find_all(nodes.Call): | 
 |         if not isinstance(node.node, nodes.Name) or \ | 
 |            node.node.name not in gettext_functions: | 
 |             continue | 
 |  | 
 |         strings = [] | 
 |         for arg in node.args: | 
 |             if isinstance(arg, nodes.Const) and \ | 
 |                isinstance(arg.value, string_types): | 
 |                 strings.append(arg.value) | 
 |             else: | 
 |                 strings.append(None) | 
 |  | 
 |         for arg in node.kwargs: | 
 |             strings.append(None) | 
 |         if node.dyn_args is not None: | 
 |             strings.append(None) | 
 |         if node.dyn_kwargs is not None: | 
 |             strings.append(None) | 
 |  | 
 |         if not babel_style: | 
 |             strings = tuple(x for x in strings if x is not None) | 
 |             if not strings: | 
 |                 continue | 
 |         else: | 
 |             if len(strings) == 1: | 
 |                 strings = strings[0] | 
 |             else: | 
 |                 strings = tuple(strings) | 
 |         yield node.lineno, node.node.name, strings | 
 |  | 
 |  | 
 | class _CommentFinder(object): | 
 |     """Helper class to find comments in a token stream.  Can only | 
 |     find comments for gettext calls forwards.  Once the comment | 
 |     from line 4 is found, a comment for line 1 will not return a | 
 |     usable value. | 
 |     """ | 
 |  | 
 |     def __init__(self, tokens, comment_tags): | 
 |         self.tokens = tokens | 
 |         self.comment_tags = comment_tags | 
 |         self.offset = 0 | 
 |         self.last_lineno = 0 | 
 |  | 
 |     def find_backwards(self, offset): | 
 |         try: | 
 |             for _, token_type, token_value in \ | 
 |                     reversed(self.tokens[self.offset:offset]): | 
 |                 if token_type in ('comment', 'linecomment'): | 
 |                     try: | 
 |                         prefix, comment = token_value.split(None, 1) | 
 |                     except ValueError: | 
 |                         continue | 
 |                     if prefix in self.comment_tags: | 
 |                         return [comment.rstrip()] | 
 |             return [] | 
 |         finally: | 
 |             self.offset = offset | 
 |  | 
 |     def find_comments(self, lineno): | 
 |         if not self.comment_tags or self.last_lineno > lineno: | 
 |             return [] | 
 |         for idx, (token_lineno, _, _) in enumerate(self.tokens[self.offset:]): | 
 |             if token_lineno > lineno: | 
 |                 return self.find_backwards(self.offset + idx) | 
 |         return self.find_backwards(len(self.tokens)) | 
 |  | 
 |  | 
 | def babel_extract(fileobj, keywords, comment_tags, options): | 
 |     """Babel extraction method for Jinja templates. | 
 |  | 
 |     .. versionchanged:: 2.3 | 
 |        Basic support for translation comments was added.  If `comment_tags` | 
 |        is now set to a list of keywords for extraction, the extractor will | 
 |        try to find the best preceeding comment that begins with one of the | 
 |        keywords.  For best results, make sure to not have more than one | 
 |        gettext call in one line of code and the matching comment in the | 
 |        same line or the line before. | 
 |  | 
 |     .. versionchanged:: 2.5.1 | 
 |        The `newstyle_gettext` flag can be set to `True` to enable newstyle | 
 |        gettext calls. | 
 |  | 
 |     .. versionchanged:: 2.7 | 
 |        A `silent` option can now be provided.  If set to `False` template | 
 |        syntax errors are propagated instead of being ignored. | 
 |  | 
 |     :param fileobj: the file-like object the messages should be extracted from | 
 |     :param keywords: a list of keywords (i.e. function names) that should be | 
 |                      recognized as translation functions | 
 |     :param comment_tags: a list of translator tags to search for and include | 
 |                          in the results. | 
 |     :param options: a dictionary of additional options (optional) | 
 |     :return: an iterator over ``(lineno, funcname, message, comments)`` tuples. | 
 |              (comments will be empty currently) | 
 |     """ | 
 |     extensions = set() | 
 |     for extension in options.get('extensions', '').split(','): | 
 |         extension = extension.strip() | 
 |         if not extension: | 
 |             continue | 
 |         extensions.add(import_string(extension)) | 
 |     if InternationalizationExtension not in extensions: | 
 |         extensions.add(InternationalizationExtension) | 
 |  | 
 |     def getbool(options, key, default=False): | 
 |         return options.get(key, str(default)).lower() in \ | 
 |             ('1', 'on', 'yes', 'true') | 
 |  | 
 |     silent = getbool(options, 'silent', True) | 
 |     environment = Environment( | 
 |         options.get('block_start_string', BLOCK_START_STRING), | 
 |         options.get('block_end_string', BLOCK_END_STRING), | 
 |         options.get('variable_start_string', VARIABLE_START_STRING), | 
 |         options.get('variable_end_string', VARIABLE_END_STRING), | 
 |         options.get('comment_start_string', COMMENT_START_STRING), | 
 |         options.get('comment_end_string', COMMENT_END_STRING), | 
 |         options.get('line_statement_prefix') or LINE_STATEMENT_PREFIX, | 
 |         options.get('line_comment_prefix') or LINE_COMMENT_PREFIX, | 
 |         getbool(options, 'trim_blocks', TRIM_BLOCKS), | 
 |         getbool(options, 'lstrip_blocks', LSTRIP_BLOCKS), | 
 |         NEWLINE_SEQUENCE, | 
 |         getbool(options, 'keep_trailing_newline', KEEP_TRAILING_NEWLINE), | 
 |         frozenset(extensions), | 
 |         cache_size=0, | 
 |         auto_reload=False | 
 |     ) | 
 |  | 
 |     if getbool(options, 'newstyle_gettext'): | 
 |         environment.newstyle_gettext = True | 
 |  | 
 |     source = fileobj.read().decode(options.get('encoding', 'utf-8')) | 
 |     try: | 
 |         node = environment.parse(source) | 
 |         tokens = list(environment.lex(environment.preprocess(source))) | 
 |     except TemplateSyntaxError as e: | 
 |         if not silent: | 
 |             raise | 
 |         # skip templates with syntax errors | 
 |         return | 
 |  | 
 |     finder = _CommentFinder(tokens, comment_tags) | 
 |     for lineno, func, message in extract_from_ast(node, keywords): | 
 |         yield lineno, func, message, finder.find_comments(lineno) | 
 |  | 
 |  | 
 | #: nicer import names | 
 | i18n = InternationalizationExtension | 
 | do = ExprStmtExtension | 
 | loopcontrols = LoopControlExtension | 
 | with_ = WithExtension | 
 | autoescape = AutoEscapeExtension |