docs: def parse(self, s):
          """Parse reaction string."""
  
          global_comp = None
          if self._parse_global:
              # Split by colon for global compartment information
              m = re.match(r'^\s*\[(\w+)\]\s*:\s*(.*)', s)
              if m is not None:
                  global_comp = m.group(1)
                  s = m.group(2)
  
          expect_operator = False
          direction = None
          left = []
          right = []
          current_side = left
          saved_token = None
  
          def tokenize():
              for match in re.finditer(self._scanner, s):
                  for i, group in enumerate(match.groups()):
                      if group is not None:
                          token = self._tokens[i][1]
                          span = match.start(), match.end()
                          if token is None:
                              raise ParseError(
                                  'Invalid token in expression string:'
                                  ' {!r}'.format(group), span=span)
                          yield self._tokens[i][1], group, span
                          break
  
          for token, value, span in tokenize():
              # Handle partially parsed compound.
              if saved_token is not None and (
                      token in (_ReactionToken.Plus,
                                _ReactionToken.Arrow,
                                _ReactionToken.End)):
                  compound = parse_compound(saved_token, global_comp)
                  current_side.append((compound, 1))
                  expect_operator = True
                  saved_token = None
  
              if token == _ReactionToken.Plus:
                  # Compound separator. Expect compound name or compound count
                  # next.
                  if not expect_operator:
                      raise ParseError('Unexpected token: {!r}'.format(value),
                                       span=span)
                  expect_operator = False
              elif token == _ReactionToken.Arrow:
                  # Reaction arrow. Expect compound name or compound count next.
                  if direction is not None:
                      raise ParseError(
                          'More than one equation arrow: {!r}'.format(value),
                          span=span)
  
                  if not expect_operator and len(left) > 0:
                      raise ParseError('Unexpected token: {!r}'.format(value),
                                       span=span)
  
                  expect_operator = False
                  direction = self._arrows[value]
                  current_side = right
              elif token == _ReactionToken.Group:
                  # Compound count. Expect compound name next.
                  if expect_operator:
                      raise ParseError('Expected plus or arrow: {!r}'.format(
                          value), span=span)
                  if saved_token is not None:
                      raise ParseError('Expected compound name: {!r}'.format(
                          value), span=span)
                  saved_token = value
              elif token == _ReactionToken.Quoted:
                  # Compound name. Expect operator next.
                  if expect_operator:
                      raise ParseError('Expected plus or arrow: {!r}'.format(
                          value), span=span)
                  if saved_token is not None:
                      try:
                          count = parse_compound_count(saved_token)
                      except ValueError as e:
                          raise_from(ParseError(
                              'Unable to parse compound count: {!r}'.format(
                                  saved_token),
                              span=span), e)
                  else:
                      count = 1
                  compound = parse_compound(value, global_comp)
                  current_side.append((compound, count))
                  expect_operator = True
                  saved_token = None
              elif token == _ReactionToken.Other:
                  # Could be count or compound name. Store and expect other,
                  # quoted, operator or end.
                  if expect_operator:
                      raise ParseError('Expected plus or arrow: {!r}'.format(
                          value), span=span)
                  if saved_token is not None:
                      try:
                          count = parse_compound_count(saved_token)
                      except ValueError as e:
                          raise_from(ParseError(
                              'Unable to parse compound count: {!r}'.format(
                                  saved_token),
                              span=span), e)
                      compound = parse_compound(value, global_comp)
                      current_side.append((compound, count))
                      expect_operator = True
                      saved_token = None
                  else:
                      saved_token = value
  
          if direction is None:
              raise ParseError('Missing equation arrow')
  
          return Reaction(direction, left, right)