docs: def parse_rule(tokens, variables, neighbors, parents, is_merc):
      """ Parse a rule set, return a list of declarations.
          
          Requires a dictionary of declared variables. Selectors in the neighbors
          list are simply grouped, and are generated from comma-delimited lists
          of selectors in the stylesheet. Selectors in the parents list should
          be combined with those found by this functions, and are generated
          from nested, Less-style rulesets.
          
          A rule set is a combination of selectors and declarations:
          http://www.w3.org/TR/CSS2/syndata.html#rule-sets
          
          Nesting is described in the Less CSS spec:
          http://lesscss.org/#-nested-rules
      
          To handle groups of selectors, use recursion:
          http://www.w3.org/TR/CSS2/selector.html#grouping
      """
      #
      # Local helper function
      #
  
      def validate_selector_elements(elements, line, col):
          if len(elements) > 2:
              raise ParseException('Only two-element selectors are supported for Mapnik styles', line, col)
      
          if len(elements) == 0:
              raise ParseException('At least one element must be present in selectors for Mapnik styles', line, col)
      
          if elements[0].names[0] not in ('Map', 'Layer') and elements[0].names[0][0] not in ('.', '#', '*'):
              raise ParseException('All non-ID, non-class first elements must be "Layer" Mapnik styles', line, col)
          
          if set([name[:1] for name in elements[0].names[1:]]) - set('#.'):
              raise ParseException('All names after the first must be IDs or classes', line, col)
          
          if len(elements) == 2 and elements[1].countTests():
              raise ParseException('Only the first element in a selector may have attributes in Mapnik styles', line, col)
      
          if len(elements) == 2 and elements[1].countIDs():
              raise ParseException('Only the first element in a selector may have an ID in Mapnik styles', line, col)
      
          if len(elements) == 2 and elements[1].countClasses():
              raise ParseException('Only the first element in a selector may have a class in Mapnik styles', line, col)
      
      def parse_variable_definition(tokens):
          """ Look for variable value tokens after an @keyword, return an array.
          """
          while True:
              tname, tvalue, line, col = tokens.next()
              
              if (tname, tvalue) == ('CHAR', ':'):
                  vtokens = []
              
                  while True:
                      tname, tvalue, line, col = tokens.next()
              
                      if (tname, tvalue) in (('CHAR', ';'), ('S', '\n')):
                          return vtokens
                      
                      elif tname not in ('S', 'COMMENT'):
                          vtokens.append((tname, tvalue, line, col))
  
              elif tname not in ('S', 'COMMENT'):
                  raise ParseException('Unexpected token in variable definition: "%s"' % tvalue, line, col)
              
      #
      # The work.
      #
      
      ElementClass = SelectorElement
      element = None
      elements = []
      
      while True:
          tname, tvalue, line, col = tokens.next()
          
          if tname == 'ATKEYWORD':
              #
              # Likely variable definition:
              # http://lesscss.org/#-variables
              #
              variables[tvalue] = parse_variable_definition(tokens)
          
          elif (tname, tvalue) == ('CHAR', '&'):
              #
              # Start of a nested block with a "&" combinator
              # http://lesscss.org/#-nested-rules
              #
              ElementClass = ConcatenatedElement
          
          elif tname == 'S':
              #
              # Definitely no longer in a "&" combinator.
              #
              ElementClass = SelectorElement
          
          elif tname == 'IDENT':
              #
              # Identifier always starts a new element.
              #
              element = ElementClass()
              elements.append(element)
              element.addName(tvalue)
              
          elif tname == 'HASH':
              #
              # Hash is an ID selector:
              # http://www.w3.org/TR/CSS2/selector.html#id-selectors
              #
              if not element:
                  element = ElementClass()
                  elements.append(element)
          
              element.addName(tvalue)
          
          elif (tname, tvalue) == ('CHAR', '.'):
              while True:
                  tname, tvalue, line, col = tokens.next()
                  
                  if tname == 'IDENT':
                      #
                      # Identifier after a period is a class selector:
                      # http://www.w3.org/TR/CSS2/selector.html#class-html
                      #
                      if not element:
                          element = ElementClass()
                          elements.append(element)
                  
                      element.addName('.'+tvalue)
                      break
                  
                  else:
                      raise ParseException('Malformed class selector', line, col)
          
          elif (tname, tvalue) == ('CHAR', '*'):
              #
              # Asterisk character is a universal selector:
              # http://www.w3.org/TR/CSS2/selector.html#universal-selector
              #
              if not element:
                  element = ElementClass()
                  elements.append(element)
          
              element.addName(tvalue)
  
          elif (tname, tvalue) == ('CHAR', '['):
              #
              # Left-bracket is the start of an attribute selector:
              # http://www.w3.org/TR/CSS2/selector.html#attribute-selectors
              #
              if not element:
                  element = ElementClass()
                  elements.append(element)
          
              test = parse_attribute(tokens, is_merc)
              element.addTest(test)
          
          elif (tname, tvalue) == ('CHAR', ','):
              #
              # Comma delineates one of a group of selectors:
              # http://www.w3.org/TR/CSS2/selector.html#grouping
              #
              # Recurse here.
              #
              neighbors.append(Selector(*elements))
              
              return parse_rule(tokens, variables, neighbors, parents, is_merc)
          
          elif (tname, tvalue) == ('CHAR', '{'):
              #
              # Left-brace is the start of a block:
              # http://www.w3.org/TR/CSS2/syndata.html#block
              #
              # Return a full block here.
              #
              class DummySelector:
                  def __init__(self, *elements):
                      self.elements = elements[:]
              
              neighbors.append(DummySelector(*elements))
              
              selectors = []
  
              #
              # Combine lists of parents and neighbors into a single list of
              # selectors, for passing off to parse_block(). There might not
              # be any parents, but there will definitely be neighbors.
              #
              for parent in (parents or [DummySelector()]):
                  for neighbor in neighbors:
                      if len(neighbor.elements) == 0:
                          raise ParseException('At least one element must be present in selectors for Mapnik styles', line, col)
                      
                      elements = chain(parent.elements + neighbor.elements)
                      selector = Selector(deepcopy(elements.next()))
                      
                      for element in elements:
                          if element.__class__ is ConcatenatedElement:
                              for name in element.names: selector.elements[-1].addName(deepcopy(name))
                              for test in element.tests: selector.elements[-1].addTest(deepcopy(test))
                          else:
                              selector.addElement(deepcopy(element))
                      
                      # selector should be fully valid at this point.
                      validate_selector_elements(selector.elements, line, col)
                      selector.convertZoomTests(is_merc)
                      selectors.append(selector)
              
              return parse_block(tokens, variables, selectors, is_merc)
          
          elif tname not in ('S', 'COMMENT'):
              raise ParseException('Unexpected token in selector: "%s"' % tvalue, line, col)