text: """Types, constants, and utility functions used by multiple sub-modules in spec_tools."""
  
  # Copyright (c) 2018-2019 Collabora, Ltd.
  #
  # SPDX-License-Identifier: Apache-2.0
  #
  # Author(s):    Ryan Pavlik <ryan.pavlik@collabora.com>
  
  import platform
  from collections import namedtuple
  from enum import Enum
  from inspect import getframeinfo
  from pathlib import Path
  from sys import stdout
  
  # if we have termcolor and we know our stdout is a TTY,
  # pull it in and use it.
  if hasattr(stdout, 'isatty') and stdout.isatty():
      try:
          from termcolor import colored as colored_impl
          HAVE_COLOR = True
      except ImportError:
          HAVE_COLOR = False
  elif platform.system() == 'Windows':
      try:
          from termcolor import colored as colored_impl
          import colorama
          colorama.init()
          HAVE_COLOR = True
      except ImportError:
          HAVE_COLOR = False
  
  else:
      HAVE_COLOR = False
  
  
  def colored(s, color=None, attrs=None):
      """Call termcolor.colored with same arguments if this is a tty and it is available."""
      if HAVE_COLOR:
          return colored_impl(s, color, attrs=attrs)
      return s
  
  
  ###
  # Constants used in multiple places.
  AUTO_FIX_STRING = 'Note: Auto-fix available.'
  EXTENSION_CATEGORY = 'extension'
  CATEGORIES_WITH_VALIDITY = set(('protos', 'structs'))
  NON_EXISTENT_MACROS = set(('plink', 'ttext', 'dtext'))
  
  ###
  # MessageContext: All the information about where a message relates to.
  MessageContext = namedtuple('MessageContext',
                              ['filename', 'lineNum', 'line',
                               'match', 'group'])
  
  
  def getInterestedRange(message_context):
      """Return a (start, end) pair of character index for the match in a MessageContext."""
      if not message_context.match:
          # whole line
          return (0, len(message_context.line))
      return (message_context.match.start(), message_context.match.end())
  
  
  def getHighlightedRange(message_context):
      """Return a (start, end) pair of character index for the highlighted range in a MessageContext."""
      if message_context.group is not None and message_context.match is not None:
          return (message_context.match.start(message_context.group),
                  message_context.match.end(message_context.group))
      # no group (whole match) or no match (whole line)
      return getInterestedRange(message_context)
  
  
  def toNameAndLine(context, root_path=None):
      """Convert MessageContext into a simple filename:line string."""
      my_fn = Path(context.filename)
      if root_path:
          my_fn = my_fn.relative_to(root_path)
      return '{}:{}'.format(str(my_fn), context.lineNum)
  
  
  def generateInclude(dir_traverse, generated_type, category, entity):
      """Create an include:: directive for generated api or validity from the various pieces."""
      return 'include::{directory_traverse}{generated_type}/{category}/{entity_name}.txt[]'.format(
          directory_traverse=dir_traverse,
          generated_type=generated_type,
          category=category,
          entity_name=entity)
  
  
  # Data stored per entity (function, struct, enumerant type, enumerant, extension, etc.)
  EntityData = namedtuple(
      'EntityData', ['entity', 'macro', 'elem', 'filename', 'category', 'directory'])
  
  
  class MessageType(Enum):
      """Type of a message."""
  
      WARNING = 1
      ERROR = 2
      NOTE = 3
  
      def __str__(self):
          """Format a MessageType as a lowercase string."""
          return str(self.name).lower()
  
      def formattedWithColon(self):
          """Format a MessageType as a colored, lowercase string followed by a colon."""
          if self == MessageType.WARNING:
              return colored(str(self) + ':', 'magenta', attrs=['bold'])
          if self == MessageType.ERROR:
              return colored(str(self) + ':', 'red', attrs=['bold'])
          return str(self) + ':'
  
  
  class MessageId(Enum):
      # Disable bogus pylint warnings in this enum
      # pylint: disable=no-member
      """Enumerates the varieties of messages that can be generated.
  
      Control over enabled messages with -Wbla or -Wno_bla is per-MessageId.
      """
  
      MISSING_TEXT = 1
      LEGACY = 2
      WRONG_MACRO = 3
      MISSING_MACRO = 4
      BAD_ENTITY = 5
      BAD_ENUMERANT = 6
      BAD_MACRO = 7
      UNRECOGNIZED_CONTEXT = 8
      UNKNOWN_MEMBER = 9
      DUPLICATE_INCLUDE = 10
      UNKNOWN_INCLUDE = 11
      API_VALIDITY_ORDER = 12
      UNDOCUMENTED_MEMBER = 13
      MEMBER_PNAME_MISSING = 14
      MISSING_VALIDITY_INCLUDE = 15
      MISSING_API_INCLUDE = 16
      MISUSED_TEXT = 17
      EXTENSION = 18
      REFPAGE_TAG = 19
      REFPAGE_MISSING_DESC = 20
      REFPAGE_XREFS = 21
      REFPAGE_XREFS_COMMA = 22
      REFPAGE_TYPE = 23
      REFPAGE_NAME = 24
      REFPAGE_BLOCK = 25
      REFPAGE_MISSING = 26
      REFPAGE_MISMATCH = 27
      REFPAGE_UNKNOWN_ATTRIB = 28
      REFPAGE_SELF_XREF = 29
      REFPAGE_XREF_DUPE = 30
      REFPAGE_WHITESPACE = 31
      REFPAGE_DUPLICATE = 32
      UNCLOSED_BLOCK = 33
  
      def __str__(self):
          """Format as a lowercase string."""
          return self.name.lower()
  
      def enable_arg(self):
          """Return the corresponding Wbla string to make the 'enable this message' argument."""
          return 'W{}'.format(self.name.lower())
  
      def disable_arg(self):
          """Return the corresponding Wno_bla string to make the 'enable this message' argument."""
          return 'Wno_{}'.format(self.name.lower())
  
      def desc(self):
          """Return a brief description of the MessageId suitable for use in --help."""
          return _MESSAGE_DESCRIPTIONS[self]
  
  
  _MESSAGE_DESCRIPTIONS = {
      MessageId.MISSING_TEXT: "a *text: macro is expected but not found",
      MessageId.LEGACY: "legacy usage of *name: macro when *link: is applicable",
      MessageId.WRONG_MACRO: "wrong macro used for an entity",
      MessageId.MISSING_MACRO: "a macro might be missing",
      MessageId.BAD_ENTITY: "entity not recognized, etc.",
      MessageId.BAD_ENUMERANT: "unrecognized enumerant value used in ename:",
      MessageId.BAD_MACRO: "unrecognized macro used",
      MessageId.UNRECOGNIZED_CONTEXT: "pname used with an unrecognized context",
      MessageId.UNKNOWN_MEMBER: "pname used but member/argument by that name not found",
      MessageId.DUPLICATE_INCLUDE: "duplicated include line",
      MessageId.UNKNOWN_INCLUDE: "include line specified file we wouldn't expect to exists",
      MessageId.API_VALIDITY_ORDER: "saw API include after validity include",
      MessageId.UNDOCUMENTED_MEMBER: "saw an apparent struct/function documentation, but missing a member",
      MessageId.MEMBER_PNAME_MISSING: "pname: missing from a 'scope' operator",
      MessageId.MISSING_VALIDITY_INCLUDE: "missing validity include",
      MessageId.MISSING_API_INCLUDE: "missing API include",
      MessageId.MISUSED_TEXT: "a *text: macro is found but not expected",
      MessageId.EXTENSION: "an extension name is incorrectly marked",
      MessageId.REFPAGE_TAG: "a refpage tag is missing an expected field",
      MessageId.REFPAGE_MISSING_DESC: "a refpage tag has an empty description",
      MessageId.REFPAGE_XREFS: "an unrecognized entity is mentioned in xrefs of a refpage tag",
      MessageId.REFPAGE_XREFS_COMMA: "a comma was founds in xrefs of a refpage tag, which is space-delimited",
      MessageId.REFPAGE_TYPE: "a refpage tag has an incorrect type field",
      MessageId.REFPAGE_NAME: "a refpage tag has an unrecognized entity name in its refpage field",
      MessageId.REFPAGE_BLOCK: "a refpage block is not correctly opened or closed.",
      MessageId.REFPAGE_MISSING: "an API include was found outside of a refpage block.",
      MessageId.REFPAGE_MISMATCH: "an API or validity include was found in a non-matching refpage block.",
      MessageId.REFPAGE_UNKNOWN_ATTRIB: "a refpage tag has an unrecognized attribute",
      MessageId.REFPAGE_SELF_XREF: "a refpage tag has itself in the list of cross-references",
      MessageId.REFPAGE_XREF_DUPE: "a refpage cross-references list has at least one duplicate",
      MessageId.REFPAGE_WHITESPACE: "a refpage cross-references list has non-minimal whitespace",
      MessageId.REFPAGE_DUPLICATE: "a refpage tag has been seen for a single entity for a second time",
      MessageId.UNCLOSED_BLOCK: "one or more blocks remain unclosed at the end of a file"
  }
  
  
  class Message(object):
      """An Error, Warning, or Note with a MessageContext, MessageId, and message text.
  
      May optionally have a replacement, a see_also array, an auto-fix,
      and a stack frame where the message was created.
      """
  
      def __init__(self, message_id, message_type, message, context,
                   replacement=None, see_also=None, fix=None, frame=None):
          """Construct a Message.
  
          Typically called by MacroCheckerFile.diag().
          """
          self.message_id = message_id
  
          self.message_type = message_type
  
          if isinstance(message, str):
              self.message = [message]
          else:
              self.message = message
  
          self.context = context
          if context is not None and context.match is not None and context.group is not None:
              if context.group not in context.match.groupdict():
                  raise RuntimeError(
                      'Group "{}" does not exist in the match'.format(context.group))
  
          self.replacement = replacement
  
          self.fix = fix
  
          if see_also is None:
              self.see_also = None
          elif isinstance(see_also, MessageContext):
              self.see_also = [see_also]
          else:
              self.see_also = see_also
  
          self.script_location = None
          if frame:
              try:
                  frameinfo = getframeinfo(frame)
                  self.script_location = "{}:{}".format(
                      frameinfo.filename, frameinfo.lineno)
              finally:
                  del frame
  