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

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)