text: "Misc. utility functions/classes for admin documentation generator."
  
  import re
  from email.errors import HeaderParseError
  from email.parser import HeaderParser
  
  from django.urls import reverse
  from django.utils.encoding import force_bytes
  from django.utils.safestring import mark_safe
  
  try:
      import docutils.core
      import docutils.nodes
      import docutils.parsers.rst.roles
  except ImportError:
      docutils_is_available = False
  else:
      docutils_is_available = True
  
  
  def get_view_name(view_func):
      mod_name = view_func.__module__
      view_name = getattr(view_func, '__qualname__', view_func.__class__.__name__)
      return mod_name + '.' + view_name
  
  
  def trim_docstring(docstring):
      """
      Uniformly trim leading/trailing whitespace from docstrings.
  
      Based on https://www.python.org/dev/peps/pep-0257/#handling-docstring-indentation
      """
      if not docstring or not docstring.strip():
          return ''
      # Convert tabs to spaces and split into lines
      lines = docstring.expandtabs().splitlines()
      indent = min(len(line) - len(line.lstrip()) for line in lines if line.lstrip())
      trimmed = [lines[0].lstrip()] + [line[indent:].rstrip() for line in lines[1:]]
      return "\n".join(trimmed).strip()
  
  
  def parse_docstring(docstring):
      """
      Parse out the parts of a docstring.  Return (title, body, metadata).
      """
      docstring = trim_docstring(docstring)
      parts = re.split(r'\n{2,}', docstring)
      title = parts[0]
      if len(parts) == 1:
          body = ''
          metadata = {}
      else:
          parser = HeaderParser()
          try:
              metadata = parser.parsestr(parts[-1])
          except HeaderParseError:
              metadata = {}
              body = "\n\n".join(parts[1:])
          else:
              metadata = dict(metadata.items())
              if metadata:
                  body = "\n\n".join(parts[1:-1])
              else:
                  body = "\n\n".join(parts[1:])
      return title, body, metadata
  
  
  def parse_rst(text, default_reference_context, thing_being_parsed=None):
      """
      Convert the string from reST to an XHTML fragment.
      """
      overrides = {
          'doctitle_xform': True,
          'initial_header_level': 3,
          "default_reference_context": default_reference_context,
          "link_base": reverse('django-admindocs-docroot').rstrip('/'),
          'raw_enabled': False,
          'file_insertion_enabled': False,
      }
      if thing_being_parsed:
          thing_being_parsed = force_bytes("<%s>" % thing_being_parsed)
      # Wrap ``text`` in some reST that sets the default role to ``cmsreference``,
      # then restores it.
      source = """
  .. default-role:: cmsreference
  
  %s
  
  .. default-role::
  """
      parts = docutils.core.publish_parts(
          source % text,
          source_path=thing_being_parsed, destination_path=None,
          writer_name='html', settings_overrides=overrides,
      )
      return mark_safe(parts['fragment'])
  
  
  #
  # reST roles
  #
  ROLES = {
      'model': '%s/models/%s/',
      'view': '%s/views/%s/',
      'template': '%s/templates/%s/',
      'filter': '%s/filters/#%s',
      'tag': '%s/tags/#%s',
  }
  
  
  def create_reference_role(rolename, urlbase):
      def _role(name, rawtext, text, lineno, inliner, options=None, content=None):
          if options is None:
              options = {}
          if content is None:
              content = []
          node = docutils.nodes.reference(
              rawtext,
              text,
              refuri=(urlbase % (
                  inliner.document.settings.link_base,
                  text.lower(),
              )),
              **options
          )
          return [node], []
      docutils.parsers.rst.roles.register_canonical_role(rolename, _role)
  
  
  def default_reference_role(name, rawtext, text, lineno, inliner, options=None, content=None):
      if options is None:
          options = {}
      if content is None:
          content = []
      context = inliner.document.settings.default_reference_context
      node = docutils.nodes.reference(
          rawtext,
          text,
          refuri=(ROLES[context] % (
              inliner.document.settings.link_base,
              text.lower(),
          )),
          **options
      )
      return [node], []
  
  
  if docutils_is_available:
      docutils.parsers.rst.roles.register_canonical_role('cmsreference', default_reference_role)
  
      for name, urlbase in ROLES.items():
          create_reference_role(name, urlbase)
  
  # Match the beginning of a named or unnamed group.
  named_group_matcher = re.compile(r'\(\?P(<\w+>)')
  unnamed_group_matcher = re.compile(r'\(')
  
  
  def replace_named_groups(pattern):
      r"""
      Find named groups in `pattern` and replace them with the group name. E.g.,
      1. ^(?P<a>\w+)/b/(\w+)$ ==> ^<a>/b/(\w+)$
      2. ^(?P<a>\w+)/b/(?P<c>\w+)/$ ==> ^<a>/b/<c>/$
      """
      named_group_indices = [
          (m.start(0), m.end(0), m.group(1))
          for m in named_group_matcher.finditer(pattern)
      ]
      # Tuples of (named capture group pattern, group name).
      group_pattern_and_name = []
      # Loop over the groups and their start and end indices.
      for start, end, group_name in named_group_indices:
          # Handle nested parentheses, e.g. '^(?P<a>(x|y))/b'.
          unmatched_open_brackets, prev_char = 1, None
          for idx, val in enumerate(list(pattern[end:])):
              # If brackets are balanced, the end of the string for the current
              # named capture group pattern has been reached.
              if unmatched_open_brackets == 0:
                  group_pattern_and_name.append((pattern[start:end + idx], group_name))
                  break
  
              # Check for unescaped `(` and `)`. They mark the start and end of a
              # nested group.
              if val == '(' and prev_char != '\\':
                  unmatched_open_brackets += 1
              elif val == ')' and prev_char != '\\':
                  unmatched_open_brackets -= 1
              prev_char = val
  
      # Replace the string for named capture groups with their group names.
      for group_pattern, group_name in group_pattern_and_name:
          pattern = pattern.replace(group_pattern, group_name)
      return pattern
  
  
  def replace_unnamed_groups(pattern):
      r"""
      Find unnamed groups in `pattern` and replace them with '<var>'. E.g.,
      1. ^(?P<a>\w+)/b/(\w+)$ ==> ^(?P<a>\w+)/b/<var>$
      2. ^(?P<a>\w+)/b/((x|y)\w+)$ ==> ^(?P<a>\w+)/b/<var>$
      """
      unnamed_group_indices = [m.start(0) for m in unnamed_group_matcher.finditer(pattern)]
      # Indices of the start of unnamed capture groups.
      group_indices = []
      # Loop over the start indices of the groups.
      for start in unnamed_group_indices:
          # Handle nested parentheses, e.g. '^b/((x|y)\w+)$'.
          unmatched_open_brackets, prev_char = 1, None
          for idx, val in enumerate(list(pattern[start + 1:])):
              if unmatched_open_brackets == 0:
                  group_indices.append((start, start + 1 + idx))
                  break
  
              # Check for unescaped `(` and `)`. They mark the start and end of
              # a nested group.
              if val == '(' and prev_char != '\\':
                  unmatched_open_brackets += 1
              elif val == ')' and prev_char != '\\':
                  unmatched_open_brackets -= 1
              prev_char = val
  
      # Remove unnamed group matches inside other unnamed capture groups.
      group_start_end_indices = []
      prev_end = None
      for start, end in group_indices:
          if prev_end and start > prev_end or not prev_end:
              group_start_end_indices.append((start, end))
          prev_end = end
  
      if group_start_end_indices:
          # Replace unnamed groups with <var>. Handle the fact that replacing the
          # string between indices will change string length and thus indices
          # will point to the wrong substring if not corrected.
          final_pattern, prev_end = [], None
          for start, end in group_start_end_indices:
              if prev_end:
                  final_pattern.append(pattern[prev_end:start])
              final_pattern.append(pattern[:start] + '<var>')
              prev_end = end
          final_pattern.append(pattern[prev_end:])
          return ''.join(final_pattern)
      else:
          return pattern
  