text: import re
  
  from coalib.bears.LocalBear import LocalBear
  from coalib.results.Result import Result, RESULT_SEVERITY
  from coalib.results.Diff import Diff
  
  
  def generate_spacing_diff(file, filename, line, line_number,
                            match_object, required_spacing):
      """
      Generate a diff for incorrectly spaced control or variable tags.
  
      :param match_object:     A Match object containing the groups ``open``,
                               ``close`` containing the opening and closing
                               delimiters of a Jinja2 tag and ``content``
                               containing everything in between.
      :param required_spacing: The number of spaces expected after the ``open``
                               delimiter and before the ``close`` delimiter
      """
      diff = Diff(file)
  
      content_before = line[:match_object.start('open')]
      content_after = line[match_object.end('close'):]
  
      spacing = ' ' * required_spacing
      replacement = (
          '{before}{open}{spacing}{content}{spacing}{close}{after}'.format(
              before=content_before,
              spacing=spacing,
              after=content_after,
              content=match_object.group('content').strip(),
              open=match_object.group('open'),
              close=match_object.group('close')))
      diff.change_line(
          line_number,
          line,
          replacement)
      return {filename: diff}
  
  
  def generate_label_diff(file, filename, line, line_number,
                          match_object, expected_label):
      """
      Generates a diff for a missing or wrong control loop end label.
      Missing labels will be added, wrong ones replaced, content after the
      label will be left untouched.
  
  
      :param match_object:   A Match object containing the groups ``close``
                             containing the closing delimiters of a control
                             end tag and the optional group ``label``
                             containing the end tag label.
      :param expected_label: The expected label for that control block.
  
      """
      diff = Diff(file)
  
      content_before = line[:match_object.end('close')]
  
      suffix_start = match_object.end('close')
      # if a label is there, we cut it out, by starting the kept suffix after it
      if match_object.group('label') is not None:
          suffix_start = match_object.end('label')
  
      content_after = line[suffix_start:]
  
      replacement = '{before}{label}{after}'.format(
          before=content_before,
          after=content_after,
          label=expected_label
      )
      diff.change_line(
          line_number,
          line,
          replacement)
      return {filename: diff}
  
  
  def has_required_spacing(string, required_spacing):
      """
      Checks if a string has the required amount of spaces
      on each side.
  
      :param string:           The string which is to be checked.
      :param required_spacing: The number of spaces expected on both
                               sides of the string.
      :return:                 True if the string has the required
                               number of spaces on both sides, False
                               otherwise.
  
      >>> has_required_spacing("  foo  ", 2)
      True
      >>> has_required_spacing(" foo", 1)
      False
      """
      leading_spaces = len(string) - len(string.lstrip(' '))
      trailing_spaces = len(string) - len(string.rstrip(' '))
  
      return (
          leading_spaces == required_spacing
          and trailing_spaces == required_spacing)
  
  
  class Jinja2Bear(LocalBear):
      LANGUAGES = {'Jinja2'}
      AUTHORS = {'The coala developers'}
      AUTHORS_EMAILS = {'coala-devel@googlegroups.com'}
      LICENSE = 'AGPL-3.0'
      ASCIINEMA_URL = 'https://asciinema.org/a/azi6u1gcxutoxn0l7xpu4pljp'
      CAN_DETECT = {'Syntax'}
      CAN_FIX = {'Formatting', 'Documentation'}
  
      VARIABLE_REGEX = re.compile(
          r'(?P<open>{{)(?P<content>.*?)(?P<close>}})')
      CONTROL_START_REGEX = re.compile(
          r'(?P<open>{%)(?P<content>\s*(for|if).*?)(?P<close>%})')
      CONTROL_END_REGEX = re.compile(
          r'(?P<open>{%)(?P<content>\s*end(for|if)\s*?)'
          r'(?P<close>%})(?P<label>{#.*?#})?')
  
      def handle_control_spacing_issue(self, file, filename, line, line_number,
                                       control_spacing, match_object):
          """
          Handles incorrectly spaced control tags (if and for),
          both start and end tags
  
          :param control_spacing: The number of spaces placed around control tags
                                  as required by the settings.
          :param match_object:    A Match object containing the groups ``open``,
                                  ``close`` containing the opening and closing
                                  delimiters of a Jinja2 tag and ``content``
                                  containing everything in between.
          """
          diff = generate_spacing_diff(
              file, filename, line, line_number, match_object, control_spacing)
          return Result.from_values(
              origin=self,
              message='Control blocks should be spaced with '
                      '`{0}` spaces on each side.'.format(
                          control_spacing),
              file=filename,
              line=line_number,
              column=match_object.start(0) + 1,
              end_line=line_number,
              end_column=match_object.end(0) + 1,
              diffs=diff)
  
      def check_for_variable_spacing_issues(self,
                                            file,
                                            filename,
                                            line,
                                            line_number,
                                            variable_spacing):
          """
          Checks any variable in the given line for spacing issues.
          Yields a Result for each issue found.
  
          :param file:
              The content of the file currently being inspected.
          :param filename:
              The name of the file currently being inspected.
          :param line:
              The content of the line currently being inspected.
          :param line_number:
              The current line number.
          :param variable_spacing:
              The number of spaces required on each side of a variable tag.
          """
          for m in self.VARIABLE_REGEX.finditer(line):
              match = m.group('content')
              if not has_required_spacing(match, variable_spacing):
                  diff = generate_spacing_diff(
                      file, filename, line, line_number, m,
                      variable_spacing)
                  yield Result.from_values(
                      origin=self,
                      message='Variable blocks should be spaced with '
                              '`{0}` spaces on each side.'.format(
                                  variable_spacing),
                      file=filename,
                      line=line_number,
                      column=m.start(0) + 1,
                      end_line=line_number,
                      end_column=m.end(0) + 1,
                      diffs=diff)
  
      def check_control_start_tags(self,
                                   file,
                                   filename,
                                   line,
                                   line_number,
                                   control_spacing):
          """
          Checks any control start tag in the given line for spacing issues
          and puts the expected label on the ``control_stack``
          Yields a Result for each issue found.
  
          :param file:
              The content of the file currently being inspected.
          :param filename:
              The name of the file currently being inspected.
          :param line:
              The content of the line currently being inspected.
          :param line_number:
              The current line number.
          :param control_spacing:
              The number of spaces required on each side of a control tag.
          """
          for m in self.CONTROL_START_REGEX.finditer(line):
              # build the label which is expected at the end of this block
              end_label = '{{#{spacing}{content}{spacing}#}}'.format(
                  content=m.group('content').strip(),
                  spacing=' ' * control_spacing)
              self.control_stack.append((end_label, line_number))
  
              # check for spacing issues with this tag
              if not has_required_spacing(m.group('content'), control_spacing):
                  yield self.handle_control_spacing_issue(
                      file, filename, line, line_number, control_spacing, m)
  
      def check_control_end_tags(self,
                                 file,
                                 filename,
                                 line,
                                 line_number,
                                 control_spacing):
          """
          Checks any control end tag in the given line for spacing issues,
          missing/wrong labels or missing corresponding opening tag.
          Yields a Result for each issue found.
  
          :param file:
              The content of the file currently being inspected.
          :param filename:
              The name of the file currently being inspected.
          :param line:
              The content of the line currently being inspected.
          :param line_number:
              The current line number.
          :param control_spacing:
              The number of spaces required on each side of a control tag.
          """
          for m in self.CONTROL_END_REGEX.finditer(line):
              label = m.group('label')
              try:
                  expected_label, start_in_line = self.control_stack.pop()
              except IndexError:
                  # an end tag without an opening tag
                  yield Result.from_values(
                      origin=self,
                      message='Control end tag has no corresponding start',
                      file=filename,
                      line=line_number,
                      column=m.start(0) + 1,
                      end_line=line_number,
                      end_column=m.end(0) + 1,
                      severity=RESULT_SEVERITY.MAJOR)
                  # Continue to next line as we've detected a syntax error
                  # and label checking can give false results
                  continue
  
              # check for spacing issues with this tag
              if not has_required_spacing(m.group('content'), control_spacing):
                  yield self.handle_control_spacing_issue(
                      file, filename, line, line_number, control_spacing, m)
  
              # yield results for incorrect or missing end labels
              if label is None and line_number != start_in_line:
                  diff = generate_label_diff(
                      file, filename, line, line_number, m, expected_label)
                  yield Result.from_values(
                      origin=self,
                      message='Unlabeled control end tag',
                      file=filename,
                      line=line_number,
                      column=m.start(0) + 1,
                      end_line=line_number,
                      end_column=m.end(0) + 1,
                      diffs=diff)
              elif label != expected_label and line_number != start_in_line:
                  diff = generate_label_diff(
                      file, filename, line, line_number, m, expected_label)
                  yield Result.from_values(
                      origin=self,
                      message='End tag label does not match expected label',
                      file=filename,
                      line=line_number,
                      column=m.start('label') + 1,
                      end_line=line_number,
                      end_column=m.end('label') + 1,
                      diffs=diff)
  
      def check_for_empty_control_stack(self, filename):
          """
          Checks if the control stack is empty and yields a Result for each
          item left on the stack.
  
          :param filename: The name of the file currently being inspected.
          """
          if len(self.control_stack) > 0:
              for (_, line_number) in self.control_stack:
                  yield Result.from_values(
                      origin=self,
                      message='Control start tag has no corresponding end',
                      file=filename,
                      line=line_number,
                      severity=RESULT_SEVERITY.MAJOR)
  
      def run(self,
              filename,
              file,
              variable_spacing: int=1,
              control_spacing: int=1):
          """
          Check `Jinja2 templates <http://jinja.pocoo.org>`_ for syntax,
          formatting and documentation issues.
          The following aspects are being looked at:
  
          * Variable spacing:
              Variable tags should be padded with one space on each side, like
              this: ``{{ var_name }}``. This can be set to any number of spaces
              via the setting ``variable_spacing``.
              Malformatted variable tags are detected and fixes suggested.
          * Control spacing:
              Like variable spacing, but for control blocks, i.e. ``if`` and
              ``for`` constructs. Looks at both start and end block.
          * Control labels:
              It is good practice to label the end of an ``if`` or ``for``
              construct with a comment equal to the content of the start,
              like so::
  
                  {% for x in y %}
                    do something
                  {% endfor %}{# for x in y %}
  
              Mising or differing labels are detected and fixes suggested.
              Constructs with start and end on the same line are being ignored.
          * unbalanced blocks:
              Each opening tag for a ``for`` or ``if`` construct must be closed
              by a corresponding end tag. An unbalanced number of opening and
              closing tags is invalid syntax and will be reported with
              MAJOR severity by the bear.
  
  
          :param variable_spacing:
              The number of spaces a variable block should be spaced with.
              Default is 1.
          :param control_spacing:
              The number of spaces a control block should be spaced with.
              Default is 1.
          """
  
          # Whenever a control construct starts a tuple of the
          # expected end label and line number are put on this stack.
          # Whenever an end tag is encountered the last item added is popped.
          self.control_stack = []
  
          for line_number, line in enumerate(file, start=1):
  
              yield from self.check_for_variable_spacing_issues(
                  file, filename, line, line_number, variable_spacing)
  
              yield from self.check_control_start_tags(
                  file, filename, line, line_number, control_spacing)
  
              yield from self.check_control_end_tags(
                  file, filename, line, line_number, control_spacing)
  
          # We've reached the end of the file.
          # Check if all control blocks have been closed
          yield from self.check_for_empty_control_stack(filename)
  