# $Id: Parser.py,v 1.137 2008/03/10 05:25:13 tavis_rudd Exp $
"""Parser classes for Cheetah's Compiler

Classes:
  ParseError( Exception )
  _LowLevelParser( Cheetah.SourceReader.SourceReader ), basically a lexer
  _HighLevelParser( _LowLevelParser )
  Parser === _HighLevelParser (an alias)

Meta-Data
================================================================================
Author: Tavis Rudd <tavis@damnsimple.com>
Version: $Revision: 1.137 $
Start Date: 2001/08/01
Last Revision Date: $Date: 2008/03/10 05:25:13 $
"""
__author__ = "Tavis Rudd <tavis@damnsimple.com>"
__revision__ = "$Revision: 1.137 $"[11:-2]

import os
import sys
import re
from re import DOTALL, MULTILINE
from types import StringType, ListType, TupleType, ClassType, TypeType
import time
from tokenize import pseudoprog
import inspect
import new
import traceback

from Cheetah.SourceReader import SourceReader
from Cheetah import Filters
from Cheetah import ErrorCatchers
from Cheetah.Unspecified import Unspecified
from Cheetah.Macros.I18n import I18n

# re tools
_regexCache = {}
def cachedRegex(pattern):
    if pattern not in _regexCache:
        _regexCache[pattern] = re.compile(pattern)
    return _regexCache[pattern]

def escapeRegexChars(txt,
                     escapeRE=re.compile(r'([\$\^\*\+\.\?\{\}\[\]\(\)\|\\])')):
    
    """Return a txt with all special regular expressions chars escaped."""
    
    return escapeRE.sub(r'\\\1' , txt)

def group(*choices): return '(' + '|'.join(choices) + ')'
def nongroup(*choices): return '(?:' + '|'.join(choices) + ')'
def namedGroup(name, *choices): return '(P:<' + name +'>' + '|'.join(choices) + ')'
def any(*choices): return apply(group, choices) + '*'
def maybe(*choices): return apply(group, choices) + '?'

##################################################
## CONSTANTS & GLOBALS ##

NO_CACHE = 0
STATIC_CACHE = 1
REFRESH_CACHE = 2

SET_LOCAL = 0
SET_GLOBAL = 1
SET_MODULE = 2

##################################################
## Tokens for the parser ##

#generic
identchars = "abcdefghijklmnopqrstuvwxyz" \
            "ABCDEFGHIJKLMNOPQRSTUVWXYZ_"
namechars = identchars + "0123456789"

#operators
powerOp = '**'
unaryArithOps = ('+', '-', '~')
binaryArithOps = ('+', '-', '/', '//','%')
shiftOps = ('>>','<<')
bitwiseOps = ('&','|','^')
assignOp = '='
augAssignOps = ('+=','-=','/=','*=', '**=','^=','%=',
          '>>=','<<=','&=','|=', )
assignmentOps = (assignOp,) + augAssignOps

compOps = ('<','>','==','!=','<=','>=', '<>', 'is', 'in',)
booleanOps = ('and','or','not')
operators = (powerOp,) + unaryArithOps + binaryArithOps \
            + shiftOps + bitwiseOps + assignmentOps \
            + compOps + booleanOps

delimeters = ('(',')','{','}','[',']',
              ',','.',':',';','=','`') + augAssignOps


keywords = ('and',       'del',       'for',       'is',        'raise',
            'assert',    'elif',      'from',      'lambda',    'return',
            'break',     'else',      'global',    'not',       'try',   
            'class',     'except',    'if',        'or',        'while',
            'continue',  'exec',      'import',    'pass',
            'def',       'finally',   'in',        'print',
            )

single3 = "'''"
double3 = '"""'

tripleQuotedStringStarts =  ("'''", '"""', 
                             "r'''", 'r"""', "R'''", 'R"""',
                             "u'''", 'u"""', "U'''", 'U"""',
                             "ur'''", 'ur"""', "Ur'''", 'Ur"""',
                             "uR'''", 'uR"""', "UR'''", 'UR"""')

tripleQuotedStringPairs = {"'''": single3, '"""': double3,
                           "r'''": single3, 'r"""': double3,
                           "u'''": single3, 'u"""': double3,
                           "ur'''": single3, 'ur"""': double3,
                           "R'''": single3, 'R"""': double3,
                           "U'''": single3, 'U"""': double3,
                           "uR'''": single3, 'uR"""': double3,
                           "Ur'''": single3, 'Ur"""': double3,
                           "UR'''": single3, 'UR"""': double3,
                           }

closurePairs= {')':'(',']':'[','}':'{'}
closurePairsRev= {'(':')','[':']','{':'}'}

##################################################
## Regex chunks for the parser ##

tripleQuotedStringREs = {}
def makeTripleQuoteRe(start, end):
    start = escapeRegexChars(start)
    end = escapeRegexChars(end)
    return re.compile(r'(?:' + start + r').*?' + r'(?:' + end + r')', re.DOTALL)

for start, end in tripleQuotedStringPairs.items():
    tripleQuotedStringREs[start] = makeTripleQuoteRe(start, end)

WS = r'[ \f\t]*'  
EOL = r'\r\n|\n|\r'
EOLZ = EOL + r'|\Z'
escCharLookBehind = nongroup(r'(?<=\A)',r'(?<!\\)')
nameCharLookAhead = r'(?=[A-Za-z_])'
identRE=re.compile(r'[a-zA-Z_][a-zA-Z_0-9]*')
EOLre=re.compile(r'(?:\r\n|\r|\n)')

specialVarRE=re.compile(r'([a-zA-z_]+)@') # for matching specialVar comments
# e.g. ##author@ Tavis Rudd

unicodeDirectiveRE = re.compile(
    r'(?:^|\r\n|\r|\n)\s*#\s{0,5}unicode[:\s]*([-\w.]*)\s*(?:\r\n|\r|\n)', re.MULTILINE)
encodingDirectiveRE = re.compile(
    r'(?:^|\r\n|\r|\n)\s*#\s{0,5}encoding[:\s]*([-\w.]*)\s*(?:\r\n|\r|\n)', re.MULTILINE)

escapedNewlineRE = re.compile(r'(?<!\\)\\n')

directiveNamesAndParsers = {
    # importing and inheritance
    'import':None,
    'from':None,
    'extends': 'eatExtends',
    'implements': 'eatImplements',
    'super': 'eatSuper',

    # output, filtering, and caching
    'slurp': 'eatSlurp',
    'raw': 'eatRaw',
    'include': 'eatInclude',
    'cache': 'eatCache',
    'filter': 'eatFilter',
    'echo': None,
    'silent': None,
    'transform' : 'eatTransform',
    
    'call': 'eatCall',
    'arg': 'eatCallArg',
    
    'capture': 'eatCapture',
    
    # declaration, assignment, and deletion
    'attr': 'eatAttr',
    'def': 'eatDef',
    'block': 'eatBlock',
    '@': 'eatDecorator',
    'defmacro': 'eatDefMacro',
    
    'closure': 'eatClosure',
    
    'set': 'eatSet',
    'del': None,
    
    # flow control
    'if': 'eatIf',
    'while': None,
    'for': None,
    'else': None,
    'elif': None,
    'pass': None,
    'break': None,
    'continue': None,
    'stop': None,
    'return': None,
    'yield': None,
    
    # little wrappers
    'repeat': None,
    'unless': None,
    
    # error handling
    'assert': None,
    'raise': None,
    'try': None,
    'except': None,
    'finally': None,
    'errorCatcher': 'eatErrorCatcher',
    
    # intructions to the parser and compiler
    'breakpoint': 'eatBreakPoint',
    'compiler': 'eatCompiler',
    'compiler-settings': 'eatCompilerSettings',
    
    # misc
    'shBang': 'eatShbang',
    'encoding': 'eatEncoding',
    
    'end': 'eatEndDirective',
    }

endDirectiveNamesAndHandlers = {
    'def': 'handleEndDef',      # has short-form
    'block': None,              # has short-form
    'closure': None,            # has short-form
    'cache': None,              # has short-form
    'call': None,               # has short-form
    'capture': None,            # has short-form
    'filter': None,
    'errorCatcher':None,            
    'while': None,              # has short-form
    'for': None,                # has short-form
    'if': None,                 # has short-form
    'try': None,                # has short-form
    'repeat': None,             # has short-form
    'unless': None,             # has short-form
    }

##################################################
## CLASSES ##

# @@TR: SyntaxError doesn't call exception.__str__ for some reason!
#class ParseError(SyntaxError):
class ParseError(ValueError):
    def __init__(self, stream, msg='Invalid Syntax', extMsg='', lineno=None, col=None):
        self.stream = stream
        if stream.pos() >= len(stream):
            stream.setPos(len(stream) -1)
        self.msg = msg
        self.extMsg = extMsg
        self.lineno = lineno
        self.col = col
        
    def __str__(self):
        return self.report()

    def report(self):
        stream = self.stream
        if stream.filename():
            f = " in file %s" % stream.filename()
        else:
            f = ''
        report = ''
        if self.lineno:
            lineno = self.lineno
            row, col, line = (lineno, (self.col or 0),
                              self.stream.splitlines()[lineno-1])
        else:
            row, col, line = self.stream.getRowColLine()

        ## get the surrounding lines
        lines = stream.splitlines()
        prevLines = []                  # (rowNum, content)
        for i in range(1,4):
            if row-1-i <=0:
                break
            prevLines.append( (row-i,lines[row-1-i]) )

        nextLines = []                  # (rowNum, content)
        for i in range(1,4):
            if not row-1+i < len(lines):
                break
            nextLines.append( (row+i,lines[row-1+i]) )
        nextLines.reverse()
        
        ## print the main message
        report += "\n\n%s\n" %self.msg
        report += "Line %i, column %i%s\n\n" % (row, col, f)
        report += 'Line|Cheetah Code\n'
        report += '----|-------------------------------------------------------------\n'
        while prevLines:
            lineInfo = prevLines.pop()
            report += "%(row)-4d|%(line)s\n"% {'row':lineInfo[0], 'line':lineInfo[1]}
        report += "%(row)-4d|%(line)s\n"% {'row':row, 'line':line}
        report += ' '*5 +' '*(col-1) + "^\n"
        
        while nextLines:
            lineInfo = nextLines.pop()
            report += "%(row)-4d|%(line)s\n"% {'row':lineInfo[0], 'line':lineInfo[1]}
        ## add the extra msg
        if self.extMsg:
            report += self.extMsg + '\n'
            
        return report

class ForbiddenSyntax(ParseError): pass
class ForbiddenExpression(ForbiddenSyntax): pass
class ForbiddenDirective(ForbiddenSyntax): pass

class CheetahVariable:
    def __init__(self, nameChunks, useNameMapper=True, cacheToken=None,
                 rawSource=None):
        self.nameChunks = nameChunks
        self.useNameMapper = useNameMapper
        self.cacheToken = cacheToken
        self.rawSource = rawSource
        
class Placeholder(CheetahVariable): pass

class ArgList:
    """Used by _LowLevelParser.getArgList()"""

    def __init__(self):
        self.argNames = []
        self.defVals = []
        self.i = 0

    def addArgName(self, name):
        self.argNames.append( name )
        self.defVals.append( None )

    def next(self):
        self.i += 1

    def addToDefVal(self, token):
        i = self.i
        if self.defVals[i] == None:
            self.defVals[i] = ''
        self.defVals[i] += token
    
    def merge(self):
        defVals = self.defVals
        for i in range(len(defVals)):
            if type(defVals[i]) == StringType:
                defVals[i] = defVals[i].strip()
                
        return map(None, [i.strip() for i in self.argNames], defVals)
    
    def __str__(self):
        return str(self.merge())
    
class _LowLevelParser(SourceReader):
    """This class implements the methods to match or extract ('get*') the basic
    elements of Cheetah's grammar.  It does NOT handle any code generation or
    state management.
    """

    _settingsManager = None

    def setSettingsManager(self, settingsManager):
        self._settingsManager = settingsManager
        
    def setting(self, key, default=Unspecified):
        if default is Unspecified:
            return self._settingsManager.setting(key)
        else:
            return self._settingsManager.setting(key, default=default)
        
    def setSetting(self, key, val):
        self._settingsManager.setSetting(key, val)

    def settings(self):
        return self._settingsManager.settings()
        
    def updateSettings(self, settings):
        self._settingsManager.updateSettings(settings)

    def _initializeSettings(self): 
        self._settingsManager._initializeSettings()
    
    def configureParser(self):
        """Is called by the Compiler instance after the parser has had a
        settingsManager assigned with self.setSettingsManager() 
        """
        self._makeCheetahVarREs()
        self._makeCommentREs()
        self._makeDirectiveREs()
        self._makePspREs()
        self._possibleNonStrConstantChars = (
            self.setting('commentStartToken')[0] +
            self.setting('multiLineCommentStartToken')[0] + 
            self.setting('cheetahVarStartToken')[0] +
            self.setting('directiveStartToken')[0] +
            self.setting('PSPStartToken')[0])
        self._nonStrConstMatchers = [
            self.matchCommentStartToken,
            self.matchMultiLineCommentStartToken,
            self.matchVariablePlaceholderStart,
            self.matchExpressionPlaceholderStart,
            self.matchDirective,
            self.matchPSPStartToken,
            self.matchEOLSlurpToken,
            ]

    ## regex setup ##

    def _makeCheetahVarREs(self):
        
        """Setup the regexs for Cheetah $var parsing."""

        num = r'[0-9\.]+'
        interval =   (r'(?P<interval>' + 
                      num + r's|' +
                      num + r'm|' +
                      num + r'h|' +
                      num + r'd|' +
                      num + r'w|' +
                      num + ')' 
                      )
    
        cacheToken = (r'(?:' +
                      r'(?P<REFRESH_CACHE>\*' + interval + '\*)'+
                      '|' +
                      r'(?P<STATIC_CACHE>\*)' +
                      '|' +                      
                      r'(?P<NO_CACHE>)' +
                      ')')
        self.cacheTokenRE = cachedRegex(cacheToken)

        silentPlaceholderToken = (r'(?:' +
                                  r'(?P<SILENT>' +escapeRegexChars('!')+')'+
                                  '|' +
                                  r'(?P<NOT_SILENT>)' +
                                  ')')
        self.silentPlaceholderTokenRE = cachedRegex(silentPlaceholderToken)
        
        self.cheetahVarStartRE = cachedRegex(
            escCharLookBehind +
            r'(?P<startToken>'+escapeRegexChars(self.setting('cheetahVarStartToken'))+')'+
            r'(?P<silenceToken>'+silentPlaceholderToken+')'+
            r'(?P<cacheToken>'+cacheToken+')'+
            r'(?P<enclosure>|(?:(?:\{|\(|\[)[ \t\f]*))' + # allow WS after enclosure
            r'(?=[A-Za-z_])')
        validCharsLookAhead = r'(?=[A-Za-z_\*!\{\(\[])'
        self.cheetahVarStartToken = self.setting('cheetahVarStartToken')
        self.cheetahVarStartTokenRE = cachedRegex(
            escCharLookBehind +
            escapeRegexChars(self.setting('cheetahVarStartToken'))
            +validCharsLookAhead
            )

        self.cheetahVarInExpressionStartTokenRE = cachedRegex(
            escapeRegexChars(self.setting('cheetahVarStartToken'))
            +r'(?=[A-Za-z_])'
            )

        self.expressionPlaceholderStartRE = cachedRegex(
            escCharLookBehind +
            r'(?P<startToken>' + escapeRegexChars(self.setting('cheetahVarStartToken')) + ')' +
            r'(?P<cacheToken>' + cacheToken + ')' +
            #r'\[[ \t\f]*'
            r'(?:\{|\(|\[)[ \t\f]*'
            + r'(?=[^\)\}\]])'
            )

        if self.setting('EOLSlurpToken'):
            self.EOLSlurpRE = cachedRegex(
                escapeRegexChars(self.setting('EOLSlurpToken'))
                + r'[ \t\f]*'
                + r'(?:'+EOL+')'
                )
        else:
            self.EOLSlurpRE = None


    def _makeCommentREs(self):
        """Construct the regex bits that are used in comment parsing."""
        startTokenEsc = escapeRegexChars(self.setting('commentStartToken'))
        self.commentStartTokenRE = cachedRegex(escCharLookBehind + startTokenEsc)
        del startTokenEsc
        
        startTokenEsc = escapeRegexChars(
            self.setting('multiLineCommentStartToken'))
        endTokenEsc = escapeRegexChars(
            self.setting('multiLineCommentEndToken'))
        self.multiLineCommentTokenStartRE = cachedRegex(escCharLookBehind +
                                                       startTokenEsc)
        self.multiLineCommentEndTokenRE = cachedRegex(escCharLookBehind +
                                                     endTokenEsc)
        
    def _makeDirectiveREs(self):
        """Construct the regexs that are used in directive parsing."""
        startToken = self.setting('directiveStartToken')
        endToken = self.setting('directiveEndToken')
        startTokenEsc = escapeRegexChars(startToken)
        endTokenEsc = escapeRegexChars(endToken)
        validSecondCharsLookAhead = r'(?=[A-Za-z_@])'
        reParts = [escCharLookBehind, startTokenEsc]
        if self.setting('allowWhitespaceAfterDirectiveStartToken'):
            reParts.append('[ \t]*')
        reParts.append(validSecondCharsLookAhead)
        self.directiveStartTokenRE = cachedRegex(''.join(reParts))
        self.directiveEndTokenRE = cachedRegex(escCharLookBehind + endTokenEsc)

    def _makePspREs(self):
        """Setup the regexs for PSP parsing."""
        startToken = self.setting('PSPStartToken')
        startTokenEsc = escapeRegexChars(startToken)
        self.PSPStartTokenRE = cachedRegex(escCharLookBehind + startTokenEsc)
        endToken = self.setting('PSPEndToken')
        endTokenEsc = escapeRegexChars(endToken)
        self.PSPEndTokenRE = cachedRegex(escCharLookBehind + endTokenEsc)


    def isLineClearToStartToken(self, pos=None):
        return self.isLineClearToPos(pos)

    def matchTopLevelToken(self):
        """Returns the first match found from the following methods:
            self.matchCommentStartToken
            self.matchMultiLineCommentStartToken
            self.matchVariablePlaceholderStart
            self.matchExpressionPlaceholderStart
            self.matchDirective
            self.matchPSPStartToken
            self.matchEOLSlurpToken

        Returns None if no match.
        """
        match = None
        if self.peek() in self._possibleNonStrConstantChars:
            for matcher in self._nonStrConstMatchers:
                match = matcher()
                if match:
                    break
        return match

    def matchPyToken(self):
        match = pseudoprog.match(self.src(), self.pos())
        
        if match and match.group() in tripleQuotedStringStarts:
            TQSmatch = tripleQuotedStringREs[match.group()].match(self.src(), self.pos())
            if TQSmatch:
                return TQSmatch
        return match
        
    def getPyToken(self):
        match = self.matchPyToken()
        if match is None:
            raise ParseError(self)
        elif match.group() in tripleQuotedStringStarts:
            raise ParseError(self, msg='Malformed triple-quoted string')
        return self.readTo(match.end())

    def matchEOLSlurpToken(self):
        if self.EOLSlurpRE:
            return self.EOLSlurpRE.match(self.src(), self.pos())

    def getEOLSlurpToken(self):
        match = self.matchEOLSlurpToken()
        if not match:
            raise ParseError(self, msg='Invalid EOL slurp token')
        return self.readTo(match.end())

    def matchCommentStartToken(self):
        return self.commentStartTokenRE.match(self.src(), self.pos())
    
    def getCommentStartToken(self):
        match = self.matchCommentStartToken()
        if not match:
            raise ParseError(self, msg='Invalid single-line comment start token')
        return self.readTo(match.end())

    def matchMultiLineCommentStartToken(self):
        return self.multiLineCommentTokenStartRE.match(self.src(), self.pos())
    
    def getMultiLineCommentStartToken(self):
        match = self.matchMultiLineCommentStartToken()
        if not match:
            raise ParseError(self, msg='Invalid multi-line comment start token')
        return self.readTo(match.end())

    def matchMultiLineCommentEndToken(self):
        return self.multiLineCommentEndTokenRE.match(self.src(), self.pos())
    
    def getMultiLineCommentEndToken(self):
        match = self.matchMultiLineCommentEndToken()
        if not match:
            raise ParseError(self, msg='Invalid multi-line comment end token')
        return self.readTo(match.end())

    def getCommaSeparatedSymbols(self):
        """
            Loosely based on getDottedName to pull out comma separated
            named chunks
        """
        srcLen = len(self)
        pieces = []
        nameChunks = []

        if not self.peek() in identchars:
            raise ParseError(self)
    
        while self.pos() < srcLen:
            c = self.peek()
            if c in namechars:
                nameChunk = self.getIdentifier()
                nameChunks.append(nameChunk)
            elif c == '.':
                if self.pos()+1 <srcLen and self.peek(1) in identchars:
                    nameChunks.append(self.getc())
                else:
                    break
            elif c == ',':
                self.getc()
                pieces.append(''.join(nameChunks))
                nameChunks = []
            elif c in (' ', '\t'):
                self.getc()
            else:
                break

        if nameChunks:
            pieces.append(''.join(nameChunks))

        return pieces
    
    def getDottedName(self):
        srcLen = len(self)
        nameChunks = []
        
        if not self.peek() in identchars:
            raise ParseError(self)
    
        while self.pos() < srcLen:
            c = self.peek()
            if c in namechars:
                nameChunk = self.getIdentifier()
                nameChunks.append(nameChunk)
            elif c == '.':
                if self.pos()+1 <srcLen and self.peek(1) in identchars:
                    nameChunks.append(self.getc())
                else:
                    break
            else:
                break

        return ''.join(nameChunks)

    def matchIdentifier(self):
        return identRE.match(self.src(), self.pos())
    
    def getIdentifier(self):
        match = self.matchIdentifier()
        if not match:
            raise ParseError(self, msg='Invalid identifier')
        return self.readTo(match.end())

    def matchOperator(self):
        match = self.matchPyToken()
        if match and match.group() not in operators:
            match = None
        return match

    def getOperator(self):
        match = self.matchOperator()
        if not match:
            raise ParseError(self, msg='Expected operator')
        return self.readTo( match.end() )

    def matchAssignmentOperator(self):
        match = self.matchPyToken()
        if match and match.group() not in assignmentOps:
            match = None
        return match
        
    def getAssignmentOperator(self):
        match = self.matchAssignmentOperator()
        if not match:
            raise ParseError(self, msg='Expected assignment operator')
        return self.readTo( match.end() )

    def matchDirective(self):
        """Returns False or the name of the directive matched.
        """
        startPos = self.pos()
        if not self.matchDirectiveStartToken():
            return False
        self.getDirectiveStartToken()
        directiveName = self.matchDirectiveName()
        self.setPos(startPos)
        return directiveName

    def matchDirectiveName(self, directiveNameChars=identchars+'0123456789-@'):
        startPos = self.pos()
        possibleMatches = self._directiveNamesAndParsers.keys()
        name = ''
        match = None

        while not self.atEnd():
            c = self.getc()
            if not c in directiveNameChars:
                break
            name += c
            if name == '@':
                if not self.atEnd() and self.peek() in identchars:
                    match = '@'
                break
            possibleMatches = [dn for dn in possibleMatches if dn.startswith(name)]
            if not possibleMatches:
                break
            elif (name in possibleMatches and (self.atEnd() or self.peek() not in directiveNameChars)):
                match = name
                break

        self.setPos(startPos)
        return match
        
    def matchDirectiveStartToken(self):
        return self.directiveStartTokenRE.match(self.src(), self.pos())
    
    def getDirectiveStartToken(self):
        match = self.matchDirectiveStartToken()
        if not match:
            raise ParseError(self, msg='Invalid directive start token')
        return self.readTo(match.end())

    def matchDirectiveEndToken(self):
        return self.directiveEndTokenRE.match(self.src(), self.pos())
    
    def getDirectiveEndToken(self):
        match = self.matchDirectiveEndToken()
        if not match:
            raise ParseError(self, msg='Invalid directive end token')
        return self.readTo(match.end())

        
    def matchColonForSingleLineShortFormDirective(self):
        if not self.atEnd() and self.peek()==':':
            restOfLine = self[self.pos()+1:self.findEOL()]
            restOfLine = restOfLine.strip()
            if not restOfLine:
                return False
            elif self.commentStartTokenRE.match(restOfLine):
                return False
            else: # non-whitespace, non-commment chars found
                return True
        return False        

    def matchPSPStartToken(self):
        return self.PSPStartTokenRE.match(self.src(), self.pos())

    def matchPSPEndToken(self):
        return self.PSPEndTokenRE.match(self.src(), self.pos())

    def getPSPStartToken(self):
        match = self.matchPSPStartToken()
        if not match:
            raise ParseError(self, msg='Invalid psp start token')
        return self.readTo(match.end())

    def getPSPEndToken(self):
        match = self.matchPSPEndToken()
        if not match:
            raise ParseError(self, msg='Invalid psp end token')
        return self.readTo(match.end())

    def matchCheetahVarStart(self):
        """includes the enclosure and cache token"""
        return self.cheetahVarStartRE.match(self.src(), self.pos())

    def matchCheetahVarStartToken(self):
        """includes the enclosure and cache token"""
        return self.cheetahVarStartTokenRE.match(self.src(), self.pos())

    def matchCheetahVarInExpressionStartToken(self):
        """no enclosures or cache tokens allowed"""
        return self.cheetahVarInExpressionStartTokenRE.match(self.src(), self.pos())

    def matchVariablePlaceholderStart(self):
        """includes the enclosure and cache token"""
        return self.cheetahVarStartRE.match(self.src(), self.pos())

    def matchExpressionPlaceholderStart(self):
        """includes the enclosure and cache token"""
        return self.expressionPlaceholderStartRE.match(self.src(), self.pos())        

    def getCheetahVarStartToken(self):
        """just the start token, not the enclosure or cache token"""
        match = self.matchCheetahVarStartToken()
        if not match:
            raise ParseError(self, msg='Expected Cheetah $var start token')            
        return self.readTo( match.end() )


    def getCacheToken(self):
        try:
            token = self.cacheTokenRE.match(self.src(), self.pos())
            self.setPos( token.end() )
            return token.group()
        except:
            raise ParseError(self, msg='Expected cache token')

    def getSilentPlaceholderToken(self):
        try:
            token = self.silentPlaceholderTokenRE.match(self.src(), self.pos())
            self.setPos( token.end() )
            return token.group()
        except:
            raise ParseError(self, msg='Expected silent placeholder token')



    def getTargetVarsList(self):
        varnames = []
        while not self.atEnd():
            if self.peek() in ' \t\f':
                self.getWhiteSpace()
            elif self.peek() in '\r\n':
                break
            elif self.startswith(','):
                self.advance()
            elif self.startswith('in ') or self.startswith('in\t'):
                break
            #elif self.matchCheetahVarStart():
            elif self.matchCheetahVarInExpressionStartToken():
                self.getCheetahVarStartToken()
                self.getSilentPlaceholderToken()
                self.getCacheToken()
                varnames.append( self.getDottedName() )
            elif self.matchIdentifier():
                varnames.append( self.getDottedName() )
            else:
                break
        return varnames
        
    def getCheetahVar(self, plain=False, skipStartToken=False):
        """This is called when parsing inside expressions. Cache tokens are only
        valid in placeholders so this method discards any cache tokens found.
        """
        if not skipStartToken:
            self.getCheetahVarStartToken()
        self.getSilentPlaceholderToken()
        self.getCacheToken()
        return self.getCheetahVarBody(plain=plain)
            
    def getCheetahVarBody(self, plain=False):
        # @@TR: this should be in the compiler
        return self._compiler.genCheetahVar(self.getCheetahVarNameChunks(), plain=plain)
        
    def getCheetahVarNameChunks(self):
        
        """
        nameChunks = list of Cheetah $var subcomponents represented as tuples
          [ (namemapperPart,autoCall,restOfName),
          ]
        where:
          namemapperPart = the dottedName base
          autocall = where NameMapper should use autocalling on namemapperPart
          restOfName = any arglist, index, or slice

        If restOfName contains a call arglist (e.g. '(1234)') then autocall is
        False, otherwise it defaults to True.

        EXAMPLE
        ------------------------------------------------------------------------

        if the raw CheetahVar is
          $a.b.c[1].d().x.y.z
          
        nameChunks is the list
          [ ('a.b.c',True,'[1]'),
            ('d',False,'()'),     
            ('x.y.z',True,''),   
          ]

        """

        chunks = []
        while self.pos() < len(self):
            rest = ''
            autoCall = True
            if not self.peek() in identchars + '.':
                break
            elif self.peek() == '.':
                
                if self.pos()+1 < len(self) and self.peek(1) in identchars:
                    self.advance()  # discard the period as it isn't needed with NameMapper
                else:
                    break
                
            dottedName = self.getDottedName()
            if not self.atEnd() and self.peek() in '([':
                if self.peek() == '(':
                    rest = self.getCallArgString()
                else:
                    rest = self.getExpression(enclosed=True)
                
                period = max(dottedName.rfind('.'), 0)
                if period:
                    chunks.append( (dottedName[:period], autoCall, '') )
                    dottedName = dottedName[period+1:]
                if rest and rest[0]=='(':
                    autoCall = False
            chunks.append( (dottedName, autoCall, rest) )

        return chunks
    

    def getCallArgString(self,
                         enclosures=[],  # list of tuples (char, pos), where char is ({ or [ 
                         useNameMapper=Unspecified):

        """ Get a method/function call argument string. 

        This method understands *arg, and **kw
        """

        # @@TR: this settings mangling should be removed
        if useNameMapper is not Unspecified:
            useNameMapper_orig = self.setting('useNameMapper')
            self.setSetting('useNameMapper', useNameMapper)
        
        if enclosures:
            pass
        else:
            if not self.peek() == '(':
                raise ParseError(self, msg="Expected '('")
            startPos = self.pos()
            self.getc()
            enclosures = [('(', startPos),
                          ]
        
        argStringBits = ['(']
        addBit = argStringBits.append

        while 1:
            if self.atEnd():
                open = enclosures[-1][0]
                close = closurePairsRev[open]
                self.setPos(enclosures[-1][1])
                raise ParseError(
                    self, msg="EOF was reached before a matching '" + close +
                    "' was found for the '" + open + "'")

            c = self.peek()
            if c in ")}]": # get the ending enclosure and break                
                if not enclosures:
                    raise ParseError(self)
                c = self.getc()
                open = closurePairs[c]
                if enclosures[-1][0] == open:
                    enclosures.pop()
                    addBit(')')  
                    break
                else:
                    raise ParseError(self)
            elif c in " \t\f\r\n":
                addBit(self.getc())
            elif self.matchCheetahVarInExpressionStartToken():
                startPos = self.pos()
                codeFor1stToken = self.getCheetahVar()
                WS = self.getWhiteSpace()
                if not self.atEnd() and self.peek() == '=':
                    nextToken = self.getPyToken()
                    if nextToken == '=':
                        endPos = self.pos()
                        self.setPos(startPos)
                        codeFor1stToken = self.getCheetahVar(plain=True)
                        self.setPos(endPos)
                        
                    ## finally
                    addBit( codeFor1stToken + WS + nextToken )
                else:
                    addBit( codeFor1stToken + WS)
            elif self.matchCheetahVarStart():
                # it has syntax that is only valid at the top level
                self._raiseErrorAboutInvalidCheetahVarSyntaxInExpr()
            else:
                beforeTokenPos = self.pos()
                token = self.getPyToken()
                if token in ('{','(','['):
                    self.rev()
                    token = self.getExpression(enclosed=True)
                token = self.transformToken(token, beforeTokenPos)
                addBit(token)

        if useNameMapper is not Unspecified:
            self.setSetting('useNameMapper', useNameMapper_orig) # @@TR: see comment above

        return ''.join(argStringBits)
    
    def getDefArgList(self, exitPos=None, useNameMapper=False):

        """ Get an argument list. Can be used for method/function definition
        argument lists or for #directive argument lists. Returns a list of
        tuples in the form (argName, defVal=None) with one tuple for each arg
        name.

        These defVals are always strings, so (argName, defVal=None) is safe even
        with a case like (arg1, arg2=None, arg3=1234*2), which would be returned as
        [('arg1', None),
         ('arg2', 'None'),
         ('arg3', '1234*2'),         
        ]

        This method understands *arg, and **kw

        """

        if self.peek() == '(':
            self.advance()
        else:
            exitPos = self.findEOL()  # it's a directive so break at the EOL
        argList = ArgList()
        onDefVal = False

        # @@TR: this settings mangling should be removed
        useNameMapper_orig = self.setting('useNameMapper')
        self.setSetting('useNameMapper', useNameMapper)

        while 1:
            if self.atEnd():
                raise ParseError(
                    self, msg="EOF was reached before a matching ')'"+
                    " was found for the '('")

            if self.pos() == exitPos:
                break

            c = self.peek()
            if c == ")" or self.matchDirectiveEndToken():
                break
            elif c == ":":
                break            
            elif c in " \t\f\r\n":
                if onDefVal:
                    argList.addToDefVal(c)
                self.advance()
            elif c == '=':
                onDefVal = True
                self.advance()
            elif c == ",":
                argList.next()
                onDefVal = False
                self.advance()
            elif self.startswith(self.cheetahVarStartToken) and not onDefVal:
                self.advance(len(self.cheetahVarStartToken))
            elif self.matchIdentifier() and not onDefVal:
                argList.addArgName( self.getIdentifier() )
            elif onDefVal:
                if self.matchCheetahVarInExpressionStartToken():
                    token = self.getCheetahVar()
                elif self.matchCheetahVarStart():
                    # it has syntax that is only valid at the top level                    
                    self._raiseErrorAboutInvalidCheetahVarSyntaxInExpr()
                else:
                    beforeTokenPos = self.pos()
                    token = self.getPyToken()
                    if token in ('{','(','['):
                        self.rev()
                        token = self.getExpression(enclosed=True)
                    token = self.transformToken(token, beforeTokenPos)
                argList.addToDefVal(token)
            elif c == '*' and not onDefVal:
                varName = self.getc()
                if self.peek() == '*':
                    varName += self.getc()
                if not self.matchIdentifier():
                    raise ParseError(self)
                varName += self.getIdentifier()
                argList.addArgName(varName)
            else:
                raise ParseError(self)

                
        self.setSetting('useNameMapper', useNameMapper_orig) # @@TR: see comment above
        return argList.merge()
    
    def getExpressionParts(self,
                           enclosed=False, 
                           enclosures=None, # list of tuples (char, pos), where char is ({ or [ 
                           pyTokensToBreakAt=None, # only works if not enclosed
                           useNameMapper=Unspecified,
                           ):

        """ Get a Cheetah expression that includes $CheetahVars and break at
        directive end tokens, the end of an enclosure, or at a specified
        pyToken.
        """

        if useNameMapper is not Unspecified:
            useNameMapper_orig = self.setting('useNameMapper')
            self.setSetting('useNameMapper', useNameMapper)

        if enclosures is None:
            enclosures = []
        
        srcLen = len(self)
        exprBits = []
        while 1:
            if self.atEnd():
                if enclosures:
                    open = enclosures[-1][0]
                    close = closurePairsRev[open]
                    self.setPos(enclosures[-1][1])
                    raise ParseError(
                        self, msg="EOF was reached before a matching '" + close +
                        "' was found for the '" + open + "'")
                else:
                    break

            c = self.peek()
            if c in "{([":
                exprBits.append(c)
                enclosures.append( (c, self.pos()) )
                self.advance()                
            elif enclosed and not enclosures:
                break                
            elif c in "])}":
                if not enclosures:
                    raise ParseError(self)
                open = closurePairs[c]
                if enclosures[-1][0] == open:
                    enclosures.pop()
                    exprBits.append(c)
                else:
                    open = enclosures[-1][0]
                    close = closurePairsRev[open]
                    row, col = self.getRowCol()
                    self.setPos(enclosures[-1][1])
                    raise ParseError(
                        self, msg= "A '" + c + "' was found at line " + str(row) +
                        ", col " + str(col) +
                        " before a matching '" + close +
                        "' was found\nfor the '" + open + "'")
                self.advance()
                                
            elif c in " \f\t":
                exprBits.append(self.getWhiteSpace())            
            elif self.matchDirectiveEndToken() and not enclosures:
                break            
            elif c == "\\" and self.pos()+1 < srcLen:
                eolMatch = EOLre.match(self.src(), self.pos()+1)
                if not eolMatch:
                    self.advance()
                    raise ParseError(self, msg='Line ending expected')
                self.setPos( eolMatch.end() )
            elif c in '\r\n':
                if enclosures:
                    self.advance()                    
                else:
                    break                    
            elif self.matchCheetahVarInExpressionStartToken():
                expr = self.getCheetahVar()
                exprBits.append(expr)
            elif self.matchCheetahVarStart():
                # it has syntax that is only valid at the top level                
                self._raiseErrorAboutInvalidCheetahVarSyntaxInExpr()                    
            else:                
                beforeTokenPos = self.pos()
                token = self.getPyToken()
                if (not enclosures 
                    and pyTokensToBreakAt
                    and token in pyTokensToBreakAt):
                    
                    self.setPos(beforeTokenPos)
                    break

                token = self.transformToken(token, beforeTokenPos)
                        
                exprBits.append(token)                    
                if identRE.match(token):
                    if token == 'for':
                        expr = self.getExpression(useNameMapper=False, pyTokensToBreakAt=['in'])
                        exprBits.append(expr)
                    else:
                        exprBits.append(self.getWhiteSpace())
                        if not self.atEnd() and self.peek() == '(':
                            exprBits.append(self.getCallArgString())                    
        ##
        if useNameMapper is not Unspecified:                            
            self.setSetting('useNameMapper', useNameMapper_orig) # @@TR: see comment above
        return exprBits

    def getExpression(self,
                      enclosed=False, 
                      enclosures=None, # list of tuples (char, pos), where # char is ({ or [
                      pyTokensToBreakAt=None,
                      useNameMapper=Unspecified,
                      ):
        """Returns the output of self.getExpressionParts() as a concatenated
        string rather than as a list.
        """
        return ''.join(self.getExpressionParts(
            enclosed=enclosed, enclosures=enclosures,
            pyTokensToBreakAt=pyTokensToBreakAt,
            useNameMapper=useNameMapper))


    def transformToken(self, token, beforeTokenPos):
        """Takes a token from the expression being parsed and performs and
        special transformations required by Cheetah.

        At the moment only Cheetah's c'$placeholder strings' are transformed.
        """
        if token=='c' and not self.atEnd() and self.peek() in '\'"':
            nextToken = self.getPyToken()
            token = nextToken.upper()
            theStr = eval(token)
            endPos = self.pos()
            if not theStr:
                return
            
            if token.startswith(single3) or token.startswith(double3):
                startPosIdx = 3
            else:
                startPosIdx = 1
            #print 'CHEETAH STRING', nextToken, theStr, startPosIdx
            self.setPos(beforeTokenPos+startPosIdx+1)
            outputExprs = []
            strConst = ''
            while self.pos() < (endPos-startPosIdx):
                if self.matchCheetahVarStart() or self.matchExpressionPlaceholderStart():
                    if strConst:
                        outputExprs.append(repr(strConst))
                        strConst = ''
                    placeholderExpr = self.getPlaceholder()
                    outputExprs.append('str('+placeholderExpr+')')
                else:
                    strConst += self.getc()
            self.setPos(endPos)
            if strConst:
                outputExprs.append(repr(strConst))
            #if not self.atEnd() and self.matches('.join('):
            #    print 'DEBUG***'
            token = "''.join(["+','.join(outputExprs)+"])"
        return token

    def _raiseErrorAboutInvalidCheetahVarSyntaxInExpr(self):
        match = self.matchCheetahVarStart()
        groupdict = match.groupdict()
        if groupdict.get('cacheToken'):
            raise ParseError(
                self,
                msg='Cache tokens are not valid inside expressions. '
                'Use them in top-level $placeholders only.')                    
        elif groupdict.get('enclosure'):                    
            raise ParseError(
                self,
                msg='Long-form placeholders - ${}, $(), $[], etc. are not valid inside expressions. '
                'Use them in top-level $placeholders only.')
        else:
            raise ParseError(
                self,
                msg='This form of $placeholder syntax is not valid here.')
        

    def getPlaceholder(self, allowCacheTokens=False, plain=False, returnEverything=False):
        # filtered 
        for callback in self.setting('preparsePlaceholderHooks'):
            callback(parser=self)

        startPos = self.pos()
        lineCol = self.getRowCol(startPos)
        startToken = self.getCheetahVarStartToken()
        silentPlaceholderToken = self.getSilentPlaceholderToken()
        if silentPlaceholderToken:
            isSilentPlaceholder = True
        else:
            isSilentPlaceholder = False
            
        
        if allowCacheTokens:
            cacheToken = self.getCacheToken()
            cacheTokenParts = self.cacheTokenRE.match(cacheToken).groupdict()        
        else:
            cacheTokenParts = {}

        if self.peek() in '({[':         
            pos = self.pos()
            enclosureOpenChar = self.getc()
            enclosures = [ (enclosureOpenChar, pos) ]
            self.getWhiteSpace()
        else:
            enclosures = []

        filterArgs = None
        if self.matchIdentifier(): 
            nameChunks = self.getCheetahVarNameChunks()
            expr = self._compiler.genCheetahVar(nameChunks[:], plain=plain)
            restOfExpr = None
            if enclosures:
                WS = self.getWhiteSpace()
                expr += WS
                if self.setting('allowPlaceholderFilterArgs') and self.peek()==',':
                    filterArgs = self.getCallArgString(enclosures=enclosures)[1:-1]
                else:
                    if self.peek()==closurePairsRev[enclosureOpenChar]:
                        self.getc()
                    else:
                        restOfExpr = self.getExpression(enclosed=True, enclosures=enclosures)
                        if restOfExpr[-1] == closurePairsRev[enclosureOpenChar]:
                            restOfExpr = restOfExpr[:-1]
                        expr += restOfExpr
            rawPlaceholder = self[startPos: self.pos()]
        else:
            expr = self.getExpression(enclosed=True, enclosures=enclosures)
            if expr[-1] == closurePairsRev[enclosureOpenChar]:
                expr = expr[:-1]
            rawPlaceholder=self[startPos: self.pos()]
            
        expr = self._applyExpressionFilters(expr,'placeholder',
                                            rawExpr=rawPlaceholder,startPos=startPos)
        for callback in self.setting('postparsePlaceholderHooks'):
            callback(parser=self)

        if returnEverything:
            return (expr, rawPlaceholder, lineCol, cacheTokenParts,
                    filterArgs, isSilentPlaceholder)
        else:
            return expr
        

class _HighLevelParser(_LowLevelParser):
    """This class is a StateMachine for parsing Cheetah source and
    sending state dependent code generation commands to
    Cheetah.Compiler.Compiler.
    """
    def __init__(self, src, filename=None, breakPoint=None, compiler=None):
        _LowLevelParser.__init__(self, src, filename=filename, breakPoint=breakPoint)
        self.setSettingsManager(compiler)
        self._compiler = compiler
        self.setupState()
        self.configureParser()

    def setupState(self):
        self._macros = {}        
        self._macroDetails = {}
        self._openDirectivesStack = []

    def cleanup(self):
        """Cleanup to remove any possible reference cycles
        """
        self._macros.clear()
        for macroname, macroDetails in self._macroDetails.items():
            macroDetails.template.shutdown()
            del macroDetails.template
        self._macroDetails.clear()

    def configureParser(self):
        _LowLevelParser.configureParser(self)
        self._initDirectives()
    
    def _initDirectives(self):
        def normalizeParserVal(val):
            if isinstance(val, (str,unicode)):
                handler = getattr(self, val)
            elif type(val) in (ClassType, TypeType):
                handler = val(self)
            elif callable(val):
                handler = val
            elif val is None:
                handler = val
            else:
                raise Exception('Invalid parser/handler value %r for %s'%(val, name))
            return handler
        
        normalizeHandlerVal = normalizeParserVal

        _directiveNamesAndParsers = directiveNamesAndParsers.copy()
        customNamesAndParsers = self.setting('directiveNamesAndParsers',{})
        _directiveNamesAndParsers.update(customNamesAndParsers)

        _endDirectiveNamesAndHandlers = endDirectiveNamesAndHandlers.copy()
        customNamesAndHandlers = self.setting('endDirectiveNamesAndHandlers',{})
        _endDirectiveNamesAndHandlers.update(customNamesAndHandlers)        
        
        self._directiveNamesAndParsers = {}
        for name, val in _directiveNamesAndParsers.items():
            if val in (False, 0):
                continue
            self._directiveNamesAndParsers[name] = normalizeParserVal(val)

        self._endDirectiveNamesAndHandlers = {}        
        for name, val in _endDirectiveNamesAndHandlers.items():
            if val in (False, 0):
                continue
            self._endDirectiveNamesAndHandlers[name] = normalizeHandlerVal(val)
        
        self._closeableDirectives = ['def','block','closure','defmacro',
                                     'call',
                                     'capture',
                                     'cache',
                                     'filter',
                                     'if','unless',
                                     'for','while','repeat',
                                     'try',
                                     ]
        for directiveName in self.setting('closeableDirectives',[]):
            self._closeableDirectives.append(directiveName)



        macroDirectives = self.setting('macroDirectives',{})
        macroDirectives['i18n'] = I18n


        for macroName, callback in macroDirectives.items():
            if type(callback) in (ClassType, TypeType):
                callback = callback(parser=self)
            assert callback                
            self._macros[macroName] = callback
            self._directiveNamesAndParsers[macroName] = self.eatMacroCall
            
    def _applyExpressionFilters(self, expr, exprType, rawExpr=None, startPos=None):
        """Pipes cheetah expressions through a set of optional filter hooks.

        The filters are functions which may modify the expressions or raise
        a ForbiddenExpression exception if the expression is not allowed.  They
        are defined in the compiler setting 'expressionFilterHooks'.

        Some intended use cases:

         - to implement 'restricted execution' safeguards in cases where you
           can't trust the author of the template.

         - to enforce style guidelines  
           
        filter call signature:  (parser, expr, exprType, rawExpr=None, startPos=None)
         - parser is the Cheetah parser  
         - expr is the expression to filter.  In some cases the parser will have
           already modified it from the original source code form.  For example,
           placeholders will have been translated into namemapper calls.  If you
           need to work with the original source, see rawExpr.        
         - exprType is the name of the directive, 'psp', or 'placeholder'. All
           lowercase.  @@TR: These will eventually be replaced with a set of
           constants.
         - rawExpr is the original source string that Cheetah parsed.  This
           might be None in some cases.
         - startPos is the character position in the source string/file
           where the parser started parsing the current expression.

        @@TR: I realize this use of the term 'expression' is a bit wonky as many
         of the 'expressions' are actually statements, but I haven't thought of
         a better name yet.  Suggestions?
        """
        for callback in self.setting('expressionFilterHooks'):
            expr = callback(parser=self, expr=expr,  exprType=exprType,
                            rawExpr=rawExpr, startPos=startPos)
        return expr

    def _filterDisabledDirectives(self, directiveName):
        directiveName = directiveName.lower()
        if (directiveName in self.setting('disabledDirectives')
            or (self.setting('enabledDirectives')
                and directiveName not in self.setting('enabledDirectives'))):
            for callback in self.setting('disabledDirectiveHooks'):
                callback(parser=self, directiveName=directiveName)
            raise ForbiddenDirective(self, msg='This %r directive is disabled'%directiveName)
        
    ## main parse loop

    def parse(self, breakPoint=None, assertEmptyStack=True):
        if breakPoint:
            origBP = self.breakPoint()
            self.setBreakPoint(breakPoint)
            assertEmptyStack = False

        while not self.atEnd():
            if self.matchCommentStartToken():
                self.eatComment()
            elif self.matchMultiLineCommentStartToken():
                self.eatMultiLineComment()
            elif self.matchVariablePlaceholderStart():
                self.eatPlaceholder()
            elif self.matchExpressionPlaceholderStart():
                self.eatPlaceholder()
            elif self.matchDirective():
                self.eatDirective()
            elif self.matchPSPStartToken():
                self.eatPSP()
            elif self.matchEOLSlurpToken():
                self.eatEOLSlurpToken()
            else:
                self.eatPlainText()
        if assertEmptyStack:
            self.assertEmptyOpenDirectivesStack()
        if breakPoint:
            self.setBreakPoint(origBP)
            
    ## non-directive eat methods    
                
    def eatPlainText(self):
        startPos = self.pos()
        match = None
        while not self.atEnd():
            match = self.matchTopLevelToken()
            if match:
                break
            else:
                self.advance()
        strConst = self.readTo(self.pos(), start=startPos)
        self._compiler.addStrConst(strConst)
        return match

    def eatComment(self):
        isLineClearToStartToken = self.isLineClearToStartToken()
        if isLineClearToStartToken:
            self._compiler.handleWSBeforeDirective()
        self.getCommentStartToken()            
        comm = self.readToEOL(gobble=isLineClearToStartToken)
        self._compiler.addComment(comm)

    def eatMultiLineComment(self):
        isLineClearToStartToken = self.isLineClearToStartToken()
        endOfFirstLine = self.findEOL()

        self.getMultiLineCommentStartToken()
        endPos = startPos = self.pos()
        level = 1
        while 1:
            endPos = self.pos()
            if self.atEnd():
                break
            if self.matchMultiLineCommentStartToken():
                self.getMultiLineCommentStartToken()
                level += 1
            elif self.matchMultiLineCommentEndToken():
                self.getMultiLineCommentEndToken()
                level -= 1
            if not level:
                break
            self.advance()
        comm = self.readTo(endPos, start=startPos)

        if not self.atEnd():
            self.getMultiLineCommentEndToken()

        if (not self.atEnd()) and self.setting('gobbleWhitespaceAroundMultiLineComments'):
            restOfLine = self[self.pos():self.findEOL()]
            if not restOfLine.strip(): # WS only to EOL
                self.readToEOL(gobble=isLineClearToStartToken)

            if isLineClearToStartToken and (self.atEnd() or self.pos() > endOfFirstLine):
                self._compiler.handleWSBeforeDirective()
        
        self._compiler.addComment(comm)

    def eatPlaceholder(self):
        (expr, rawPlaceholder,
         lineCol, cacheTokenParts,
         filterArgs, isSilentPlaceholder) = self.getPlaceholder(
            allowCacheTokens=True, returnEverything=True)
        
        self._compiler.addPlaceholder(
            expr,
            filterArgs=filterArgs,
            rawPlaceholder=rawPlaceholder,
            cacheTokenParts=cacheTokenParts,
            lineCol=lineCol,
            silentMode=isSilentPlaceholder)
        return
        
    def eatPSP(self):
        # filtered
        self._filterDisabledDirectives(directiveName='psp')
        self.getPSPStartToken()
        endToken = self.setting('PSPEndToken')
        startPos = self.pos()            
        while not self.atEnd():
            if self.peek() == endToken[0]:
                if self.matchPSPEndToken():
                    break
            self.advance()
        pspString = self.readTo(self.pos(), start=startPos).strip()
        pspString = self._applyExpressionFilters(pspString, 'psp', startPos=startPos)
        self._compiler.addPSP(pspString)
        self.getPSPEndToken()

    ## generic directive eat methods
    _simpleIndentingDirectives = '''
    else elif for while repeat unless try except finally'''.split()
    _simpleExprDirectives = '''
    pass continue stop return yield break
    del assert raise
    silent echo    
    import from'''.split()
    _directiveHandlerNames = {'import':'addImportStatement',
                              'from':'addImportStatement', }
    def eatDirective(self):
        directiveName = self.matchDirective()
        self._filterDisabledDirectives(directiveName)

        for callback in self.setting('preparseDirectiveHooks'):
            callback(parser=self, directiveName=directiveName)

        # subclasses can override the default behaviours here by providing an
        # eater method in self._directiveNamesAndParsers[directiveName]
        directiveParser = self._directiveNamesAndParsers.get(directiveName)
        if directiveParser:
            directiveParser()
        elif directiveName in self._simpleIndentingDirectives:
            handlerName = self._directiveHandlerNames.get(directiveName)
            if not handlerName:
                handlerName = 'add'+directiveName.capitalize()
            handler = getattr(self._compiler, handlerName)
            self.eatSimpleIndentingDirective(directiveName, callback=handler)
        elif directiveName in self._simpleExprDirectives:
            handlerName = self._directiveHandlerNames.get(directiveName)
            if not handlerName:
                handlerName = 'add'+directiveName.capitalize()
            handler = getattr(self._compiler, handlerName)
            if directiveName in ('silent', 'echo'):
                includeDirectiveNameInExpr = False
            else:
                includeDirectiveNameInExpr = True
            expr = self.eatSimpleExprDirective(
                directiveName,
                includeDirectiveNameInExpr=includeDirectiveNameInExpr)
            handler(expr)
        ##    
        for callback in self.setting('postparseDirectiveHooks'):
            callback(parser=self, directiveName=directiveName)

    def _eatRestOfDirectiveTag(self, isLineClearToStartToken, endOfFirstLinePos):
        foundComment = False
        if self.matchCommentStartToken():
            pos = self.pos()
            self.advance()
            if not self.matchDirective():
                self.setPos(pos)
                foundComment = True
                self.eatComment() # this won't gobble the EOL
            else:
                self.setPos(pos)
            
        if not foundComment and self.matchDirectiveEndToken():
                self.getDirectiveEndToken()
        elif isLineClearToStartToken and (not self.atEnd()) and self.peek() in '\r\n':
            # still gobble the EOL if a comment was found. 
            self.readToEOL(gobble=True)
            
        if isLineClearToStartToken and (self.atEnd() or self.pos() > endOfFirstLinePos):
            self._compiler.handleWSBeforeDirective()

    def _eatToThisEndDirective(self, directiveName):
        finalPos = endRawPos = startPos = self.pos()
        directiveChar = self.setting('directiveStartToken')[0]
        isLineClearToStartToken = False
        while not self.atEnd():
            if self.peek() == directiveChar:
                if self.matchDirective() == 'end':
                    endRawPos = self.pos()
                    self.getDirectiveStartToken()
                    self.advance(len('end'))
                    self.getWhiteSpace()
                    if self.startswith(directiveName):
                        if self.isLineClearToStartToken(endRawPos):
                            isLineClearToStartToken = True
                            endRawPos = self.findBOL(endRawPos)
                        self.advance(len(directiveName)) # to end of directiveName
                        self.getWhiteSpace()
                        finalPos = self.pos()
                        break
            self.advance()
            finalPos = endRawPos = self.pos()

        textEaten = self.readTo(endRawPos, start=startPos)
        self.setPos(finalPos)
        
        endOfFirstLinePos = self.findEOL()
        
        if self.matchDirectiveEndToken():
            self.getDirectiveEndToken()
        elif isLineClearToStartToken and (not self.atEnd()) and self.peek() in '\r\n':
            self.readToEOL(gobble=True)
            
        if isLineClearToStartToken and self.pos() > endOfFirstLinePos:
            self._compiler.handleWSBeforeDirective()
        return textEaten


    def eatSimpleExprDirective(self, directiveName, includeDirectiveNameInExpr=True):
        # filtered 
        isLineClearToStartToken = self.isLineClearToStartToken()
        endOfFirstLine = self.findEOL()
        self.getDirectiveStartToken()
        if not includeDirectiveNameInExpr:
            self.advance(len(directiveName))
        startPos = self.pos()
        expr = self.getExpression().strip()
        directiveName = expr.split()[0]
        expr = self._applyExpressionFilters(expr, directiveName, startPos=startPos)
        if directiveName in self._closeableDirectives:
            self.pushToOpenDirectivesStack(directiveName)
        self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLine)
        return expr

    def eatSimpleIndentingDirective(self, directiveName, callback,
                                    includeDirectiveNameInExpr=False):
        # filtered 
        isLineClearToStartToken = self.isLineClearToStartToken()
        endOfFirstLinePos = self.findEOL()
        lineCol = self.getRowCol()
        self.getDirectiveStartToken()
        if directiveName not in 'else elif for while try except finally'.split():
            self.advance(len(directiveName))
        startPos = self.pos()

        self.getWhiteSpace()

        expr = self.getExpression(pyTokensToBreakAt=[':'])
        expr = self._applyExpressionFilters(expr, directiveName, startPos=startPos)
        if self.matchColonForSingleLineShortFormDirective():
            self.advance() # skip over :
            if directiveName in 'else elif except finally'.split():
                callback(expr, dedent=False, lineCol=lineCol)
            else:
                callback(expr, lineCol=lineCol)
                
            self.getWhiteSpace(max=1)
            self.parse(breakPoint=self.findEOL(gobble=True))
            self._compiler.commitStrConst()
            self._compiler.dedent()
        else:
            if self.peek()==':':
                self.advance()
            self.getWhiteSpace()
            self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)
            if directiveName in self._closeableDirectives:
                self.pushToOpenDirectivesStack(directiveName)
            callback(expr, lineCol=lineCol)

    def eatEndDirective(self):
        isLineClearToStartToken = self.isLineClearToStartToken()
        self.getDirectiveStartToken()
        self.advance(3)                 # to end of 'end'
        self.getWhiteSpace()
        pos = self.pos()
        directiveName = False
        for key in self._endDirectiveNamesAndHandlers.keys():
            if self.find(key, pos) == pos:
                directiveName = key
                break
        if not directiveName:
            raise ParseError(self, msg='Invalid end directive')
        
        endOfFirstLinePos = self.findEOL()
        self.getExpression() # eat in any extra comment-like crap
        self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)            
        if directiveName in self._closeableDirectives:
            self.popFromOpenDirectivesStack(directiveName)

        # subclasses can override the default behaviours here by providing an
        # end-directive handler in self._endDirectiveNamesAndHandlers[directiveName]
        if self._endDirectiveNamesAndHandlers.get(directiveName):
            handler = self._endDirectiveNamesAndHandlers[directiveName]
            handler()
        elif directiveName in 'block capture cache call filter errorCatcher'.split():
            if key == 'block':
                self._compiler.closeBlock()
            elif key == 'capture':
                self._compiler.endCaptureRegion()
            elif key == 'cache':
                self._compiler.endCacheRegion()
            elif key == 'call':
                self._compiler.endCallRegion()
            elif key == 'filter':
                self._compiler.closeFilterBlock()
            elif key == 'errorCatcher':
                self._compiler.turnErrorCatcherOff()
        elif directiveName in 'while for if try repeat unless'.split():
            self._compiler.commitStrConst()
            self._compiler.dedent()
        elif directiveName=='closure':
            self._compiler.commitStrConst()
            self._compiler.dedent()
            # @@TR: temporary hack of useSearchList
            self.setSetting('useSearchList', self._useSearchList_orig)            

    ## specific directive eat methods
    
    def eatBreakPoint(self):
        """Tells the parser to stop parsing at this point and completely ignore
        everything else.

        This is a debugging tool.
        """
        self.setBreakPoint(self.pos())

    def eatShbang(self):
        # filtered 
        self.getDirectiveStartToken()
        self.advance(len('shBang'))
        self.getWhiteSpace()
        startPos = self.pos()
        shBang = self.readToEOL()
        shBang = self._applyExpressionFilters(shBang, 'shbang', startPos=startPos)
        self._compiler.setShBang(shBang.strip())

    def eatEncoding(self):
        # filtered 
        self.getDirectiveStartToken()
        self.advance(len('encoding'))
        self.getWhiteSpace()
        startPos = self.pos()
        encoding = self.readToEOL()
        encoding = self._applyExpressionFilters(encoding, 'encoding', startPos=startPos)               
        self._compiler.setModuleEncoding(encoding.strip())
        
    def eatCompiler(self):
        # filtered 
        isLineClearToStartToken = self.isLineClearToStartToken()
        endOfFirstLine = self.findEOL()
        startPos = self.pos()
        self.getDirectiveStartToken()
        self.advance(len('compiler'))   # to end of 'compiler'
        self.getWhiteSpace()

        startPos = self.pos()
        settingName = self.getIdentifier()

        if settingName.lower() == 'reset':
            self.getExpression() # gobble whitespace & junk
            self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLine)
            self._initializeSettings()
            self.configureParser()
            return
        
        self.getWhiteSpace()
        if self.peek() == '=':
            self.advance()
        else:
            raise ParseError(self)
        valueExpr = self.getExpression()
        endPos = self.pos()

        # @@TR: it's unlikely that anyone apply filters would have left this
        # directive enabled:
        # @@TR: fix up filtering, regardless
        self._applyExpressionFilters('%s=%r'%(settingName, valueExpr),
                                     'compiler', startPos=startPos)
        
        self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLine)
        try:
            self._compiler.setCompilerSetting(settingName, valueExpr)
        except:
            out = sys.stderr
            print >> out, 'An error occurred while processing the following #compiler directive.'
            print >> out, '-'*80
            print >> out, self[startPos:endPos]
            print >> out, '-'*80
            print >> out, 'Please check the syntax of these settings.'
            print >> out, 'A full Python exception traceback follows.'
            raise


    def eatCompilerSettings(self):
        # filtered         
        isLineClearToStartToken = self.isLineClearToStartToken()
        endOfFirstLine = self.findEOL()
        self.getDirectiveStartToken()
        self.advance(len('compiler-settings'))   # to end of 'settings'
        
        keywords = self.getTargetVarsList()
        self.getExpression()            # gobble any garbage
            
        self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLine)

        if 'reset' in keywords:
            self._compiler._initializeSettings()
            self.configureParser()
            # @@TR: this implies a single-line #compiler-settings directive, and
            # thus we should parse forward for an end directive.
            # Subject to change in the future
            return 
        startPos = self.pos()
        settingsStr = self._eatToThisEndDirective('compiler-settings')            
        settingsStr = self._applyExpressionFilters(settingsStr, 'compilerSettings', 
                                                   startPos=startPos)
        try:
            self._compiler.setCompilerSettings(keywords=keywords, settingsStr=settingsStr)
        except:
            out = sys.stderr
            print >> out, 'An error occurred while processing the following compiler settings.'
            print >> out, '-'*80
            print >> out, settingsStr.strip()
            print >> out, '-'*80
            print >> out, 'Please check the syntax of these settings.'
            print >> out, 'A full Python exception traceback follows.'
            raise

    def eatAttr(self):
        # filtered         
        isLineClearToStartToken = self.isLineClearToStartToken()
        endOfFirstLinePos = self.findEOL()
        startPos = self.pos()
        self.getDirectiveStartToken()
        self.advance(len('attr'))
        self.getWhiteSpace()
        startPos = self.pos()
        if self.matchCheetahVarStart():
            self.getCheetahVarStartToken()
        attribName = self.getIdentifier()
        self.getWhiteSpace()
        self.getAssignmentOperator()
        expr = self.getExpression()
        expr = self._applyExpressionFilters(expr, 'attr', startPos=startPos)
        self._compiler.addAttribute(attribName, expr)
        self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)

    def eatDecorator(self):
        isLineClearToStartToken = self.isLineClearToStartToken()
        endOfFirstLinePos = self.findEOL()
        startPos = self.pos()
        self.getDirectiveStartToken()
        #self.advance() # eat @
        startPos = self.pos()
        decoratorExpr = self.getExpression()
        decoratorExpr = self._applyExpressionFilters(decoratorExpr, 'decorator', startPos=startPos)
        self._compiler.addDecorator(decoratorExpr)
        self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)
        self.getWhiteSpace()

        directiveName = self.matchDirective()
        if not directiveName or directiveName not in ('def', 'block', 'closure', '@'):
            raise ParseError(
                self, msg='Expected #def, #block, #closure or another @decorator')
        self.eatDirective()
        
    def eatDef(self):
        # filtered         
        self._eatDefOrBlock('def')

    def eatBlock(self):
        # filtered
        startPos = self.pos()
        methodName, rawSignature = self._eatDefOrBlock('block')
        self._compiler._blockMetaData[methodName] = {
            'raw':rawSignature,
            'lineCol':self.getRowCol(startPos),
            }

    def eatClosure(self):
        # filtered         
        self._eatDefOrBlock('closure')
        
    def _eatDefOrBlock(self, directiveName):
        # filtered 
        assert directiveName in ('def','block','closure')
        isLineClearToStartToken = self.isLineClearToStartToken()
        endOfFirstLinePos = self.findEOL()
        startPos = self.pos()
        self.getDirectiveStartToken()
        self.advance(len(directiveName))
        self.getWhiteSpace()
        if self.matchCheetahVarStart():
            self.getCheetahVarStartToken()
        methodName = self.getIdentifier()
        self.getWhiteSpace()
        if self.peek() == '(':
            argsList = self.getDefArgList()
            self.advance()              # past the closing ')'
            if argsList and argsList[0][0] == 'self':
                del argsList[0]
        else:
            argsList=[]

        def includeBlockMarkers():
            if self.setting('includeBlockMarkers'):
                startMarker = self.setting('blockMarkerStart')
                self._compiler.addStrConst(startMarker[0] + methodName + startMarker[1])

        # @@TR: fix up filtering
        self._applyExpressionFilters(self[startPos:self.pos()], 'def', startPos=startPos)

        if self.matchColonForSingleLineShortFormDirective():
            isNestedDef = (self.setting('allowNestedDefScopes')
                           and [name for name in self._openDirectivesStack if name=='def'])
            self.getc()
            rawSignature = self[startPos:endOfFirstLinePos]
            self._eatSingleLineDef(directiveName=directiveName,
                                   methodName=methodName,
                                   argsList=argsList,
                                   startPos=startPos,
                                   endPos=endOfFirstLinePos)
            if directiveName == 'def' and not isNestedDef:
                #@@TR: must come before _eatRestOfDirectiveTag ... for some reason
                self._compiler.closeDef()
            elif directiveName == 'block':
                includeBlockMarkers()
                self._compiler.closeBlock()
            elif directiveName == 'closure' or isNestedDef:
                self._compiler.dedent()
                
            self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)
        else:
            if self.peek()==':':
                self.getc()            
            self.pushToOpenDirectivesStack(directiveName)
            rawSignature = self[startPos:self.pos()]
            self._eatMultiLineDef(directiveName=directiveName,
                                  methodName=methodName,
                                  argsList=argsList,
                                  startPos=startPos,
                                  isLineClearToStartToken=isLineClearToStartToken)
            if directiveName == 'block':
                includeBlockMarkers()

        return methodName, rawSignature

    def _eatMultiLineDef(self, directiveName, methodName, argsList, startPos,
                         isLineClearToStartToken=False):
        # filtered in calling method
        self.getExpression()            # slurp up any garbage left at the end
        signature = self[startPos:self.pos()]
        endOfFirstLinePos = self.findEOL()
        self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)
        signature = ' '.join([line.strip() for line in signature.splitlines()]) 
        parserComment = ('## CHEETAH: generated from ' + signature + 
                         ' at line %s, col %s' % self.getRowCol(startPos)
                         + '.')

        isNestedDef = (self.setting('allowNestedDefScopes')
                       and len([name for name in self._openDirectivesStack if name=='def'])>1)
        if directiveName=='block' or (directiveName=='def' and not isNestedDef):
            self._compiler.startMethodDef(methodName, argsList, parserComment)
        else: #closure
            self._useSearchList_orig = self.setting('useSearchList')
            self.setSetting('useSearchList', False)
            self._compiler.addClosure(methodName, argsList, parserComment)

        return methodName

    def _eatSingleLineDef(self, directiveName, methodName, argsList, startPos, endPos):
        # filtered in calling method        
        fullSignature = self[startPos:endPos]
        parserComment = ('## Generated from ' + fullSignature + 
                         ' at line %s, col %s' % self.getRowCol(startPos)
                         + '.')
        isNestedDef = (self.setting('allowNestedDefScopes')
                       and [name for name in self._openDirectivesStack if name=='def'])
        if directiveName=='block' or (directiveName=='def' and not isNestedDef):
            self._compiler.startMethodDef(methodName, argsList, parserComment)
        else: #closure
            # @@TR: temporary hack of useSearchList
            useSearchList_orig = self.setting('useSearchList')
            self.setSetting('useSearchList', False)            
            self._compiler.addClosure(methodName, argsList, parserComment)            

        self.getWhiteSpace(max=1)
        self.parse(breakPoint=endPos)        
        if directiveName=='closure' or isNestedDef: # @@TR: temporary hack of useSearchList
            self.setSetting('useSearchList', useSearchList_orig)
    
    def eatExtends(self):
        # filtered
        isLineClearToStartToken = self.isLineClearToStartToken()
        endOfFirstLine = self.findEOL()
        self.getDirectiveStartToken()
        self.advance(len('extends'))
        self.getWhiteSpace()
        startPos = self.pos()
        if self.setting('allowExpressionsInExtendsDirective'):
            baseName = self.getExpression()
        else:
            baseName = self.getCommaSeparatedSymbols()
            baseName = ', '.join(baseName)
                       
        baseName = self._applyExpressionFilters(baseName, 'extends', startPos=startPos)
        self._compiler.setBaseClass(baseName) # in compiler
        self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLine)
            
    def eatImplements(self):
        # filtered
        isLineClearToStartToken = self.isLineClearToStartToken()
        endOfFirstLine = self.findEOL()
        self.getDirectiveStartToken()
        self.advance(len('implements'))
        self.getWhiteSpace()       
        startPos = self.pos()
        methodName = self.getIdentifier()
        if not self.atEnd() and self.peek() == '(':
            argsList = self.getDefArgList()
            self.advance()              # past the closing ')'
            if argsList and argsList[0][0] == 'self':
                del argsList[0]
        else:
            argsList=[]

        # @@TR: need to split up filtering of the methodname and the args
        #methodName = self._applyExpressionFilters(methodName, 'implements', startPos=startPos)
        self._applyExpressionFilters(self[startPos:self.pos()], 'implements', startPos=startPos)

        self._compiler.setMainMethodName(methodName)
        self._compiler.setMainMethodArgs(argsList)
            
        self.getExpression()  # throw away and unwanted crap that got added in
        self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLine)

    def eatSuper(self):
        # filtered
        isLineClearToStartToken = self.isLineClearToStartToken()
        endOfFirstLine = self.findEOL()
        self.getDirectiveStartToken()
        self.advance(len('super'))
        self.getWhiteSpace()
        startPos = self.pos()
        if not self.atEnd() and self.peek() == '(':
            argsList = self.getDefArgList()
            self.advance()              # past the closing ')'
            if argsList and argsList[0][0] == 'self':
                del argsList[0]
        else:
            argsList=[]

        self._applyExpressionFilters(self[startPos:self.pos()], 'super', startPos=startPos)

        #parserComment = ('## CHEETAH: generated from ' + signature + 
        #                 ' at line %s, col %s' % self.getRowCol(startPos)
        #                 + '.')

        self.getExpression()  # throw away and unwanted crap that got added in
        self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLine)
        self._compiler.addSuper(argsList)

    def eatSet(self):
        # filtered
        isLineClearToStartToken = self.isLineClearToStartToken()
        endOfFirstLine = self.findEOL()
        self.getDirectiveStartToken()
        self.advance(3)
        self.getWhiteSpace()
        style = SET_LOCAL
        if self.startswith('local'):
            self.getIdentifier()
            self.getWhiteSpace()
        elif self.startswith('global'):
            self.getIdentifier()
            self.getWhiteSpace()
            style = SET_GLOBAL
        elif self.startswith('module'):
            self.getIdentifier()
            self.getWhiteSpace()
            style = SET_MODULE

        startsWithDollar = self.matchCheetahVarStart()
        startPos = self.pos()
        LVALUE = self.getExpression(pyTokensToBreakAt=assignmentOps, useNameMapper=False).strip()
        OP = self.getAssignmentOperator()
        RVALUE = self.getExpression()        
        expr = LVALUE + ' ' + OP + ' ' + RVALUE.strip()
        
        expr = self._applyExpressionFilters(expr, 'set', startPos=startPos)
        self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLine)

        class Components: pass # used for 'set global'
        exprComponents = Components()
        exprComponents.LVALUE = LVALUE
        exprComponents.OP = OP
        exprComponents.RVALUE = RVALUE
        self._compiler.addSet(expr, exprComponents, style)
    
    def eatSlurp(self):
        if self.isLineClearToStartToken():
            self._compiler.handleWSBeforeDirective()
        self._compiler.commitStrConst()
        self.readToEOL(gobble=True)

    def eatEOLSlurpToken(self):
        if self.isLineClearToStartToken():
            self._compiler.handleWSBeforeDirective()
        self._compiler.commitStrConst()
        self.readToEOL(gobble=True)        

    def eatRaw(self):
        isLineClearToStartToken = self.isLineClearToStartToken()
        endOfFirstLinePos = self.findEOL()
        self.getDirectiveStartToken()
        self.advance(len('raw'))
        self.getWhiteSpace()
        if self.matchColonForSingleLineShortFormDirective():
            self.advance() # skip over :
            self.getWhiteSpace(max=1)
            rawBlock = self.readToEOL(gobble=False)
        else:
            if self.peek()==':':
                self.advance()
            self.getWhiteSpace()
            self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)
            rawBlock = self._eatToThisEndDirective('raw')
        self._compiler.addRawText(rawBlock)
    
    def eatInclude(self):
        # filtered
        isLineClearToStartToken = self.isLineClearToStartToken()
        endOfFirstLinePos = self.findEOL()
        self.getDirectiveStartToken()
        self.advance(len('include'))

        self.getWhiteSpace()
        includeFrom = 'file'
        isRaw = False
        if self.startswith('raw'):
            self.advance(3)
            isRaw=True
            
        self.getWhiteSpace()            
        if self.startswith('source'):
            self.advance(len('source'))
            includeFrom = 'str'
            self.getWhiteSpace()
            if not self.peek() == '=':
                raise ParseError(self)
            self.advance()
        startPos = self.pos()
        sourceExpr = self.getExpression()
        sourceExpr = self._applyExpressionFilters(sourceExpr, 'include', startPos=startPos)        
        self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)
        self._compiler.addInclude(sourceExpr, includeFrom, isRaw)

    
    def eatDefMacro(self):
        # @@TR: not filtered yet
        isLineClearToStartToken = self.isLineClearToStartToken()
        endOfFirstLinePos = self.findEOL()
        self.getDirectiveStartToken()
        self.advance(len('defmacro'))

        self.getWhiteSpace()
        if self.matchCheetahVarStart():
            self.getCheetahVarStartToken()
        macroName = self.getIdentifier()
        self.getWhiteSpace()
        if self.peek() == '(':
            argsList = self.getDefArgList(useNameMapper=False)
            self.advance()              # past the closing ')'
            if argsList and argsList[0][0] == 'self':
                del argsList[0]
        else:
            argsList=[]

        assert not self._directiveNamesAndParsers.has_key(macroName)
        argsList.insert(0, ('src',None))
        argsList.append(('parser','None'))
        argsList.append(('macros','None'))
        argsList.append(('compilerSettings','None'))
        argsList.append(('isShortForm','None'))
        argsList.append(('EOLCharsInShortForm','None'))        
        argsList.append(('startPos','None'))
        argsList.append(('endPos','None'))
        
        if self.matchColonForSingleLineShortFormDirective():
            self.advance() # skip over :
            self.getWhiteSpace(max=1)
            macroSrc = self.readToEOL(gobble=False)
            self.readToEOL(gobble=True)
        else:
            if self.peek()==':':
                self.advance()
            self.getWhiteSpace()
            self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)
            macroSrc = self._eatToThisEndDirective('defmacro')

        #print argsList
        normalizedMacroSrc = ''.join(
            ['%def callMacro('+','.join([defv and '%s=%s'%(n,defv) or n
                                         for n,defv in argsList])
             +')\n',
             macroSrc,
             '%end def'])

        
        from Cheetah.Template import Template
        templateAPIClass = self.setting('templateAPIClassForDefMacro', default=Template)
        compilerSettings = self.setting('compilerSettingsForDefMacro', default={})
        searchListForMacros = self.setting('searchListForDefMacro', default=[])
        searchListForMacros = list(searchListForMacros) # copy to avoid mutation bugs
        searchListForMacros.append({'macros':self._macros,
                                    'parser':self,
                                    'compilerSettings':self.settings(),                                    
                                    })
        
        templateAPIClass._updateSettingsWithPreprocessTokens(
            compilerSettings, placeholderToken='@', directiveToken='%')
        macroTemplateClass = templateAPIClass.compile(source=normalizedMacroSrc,
                                                      compilerSettings=compilerSettings)
        #print normalizedMacroSrc
        #t = macroTemplateClass()
        #print t.callMacro('src')
        #print t.generatedClassCode()
        
        class MacroDetails: pass
        macroDetails = MacroDetails()
        macroDetails.macroSrc = macroSrc
        macroDetails.argsList = argsList
        macroDetails.template = macroTemplateClass(searchList=searchListForMacros)

        self._macroDetails[macroName] = macroDetails
        self._macros[macroName] = macroDetails.template.callMacro
        self._directiveNamesAndParsers[macroName] = self.eatMacroCall

    def eatMacroCall(self):
        isLineClearToStartToken = self.isLineClearToStartToken()
        endOfFirstLinePos = self.findEOL()
        startPos = self.pos()
        self.getDirectiveStartToken()
        macroName = self.getIdentifier()
        macro = self._macros[macroName]
        if hasattr(macro, 'parse'):
            return macro.parse(parser=self, startPos=startPos)
        
        if hasattr(macro, 'parseArgs'):
            args = macro.parseArgs(parser=self, startPos=startPos)
        else:
            self.getWhiteSpace()
            args = self.getExpression(useNameMapper=False,
                                      pyTokensToBreakAt=[':']).strip()

        if self.matchColonForSingleLineShortFormDirective():
            isShortForm = True
            self.advance() # skip over :
            self.getWhiteSpace(max=1)
            srcBlock = self.readToEOL(gobble=False)
            EOLCharsInShortForm = self.readToEOL(gobble=True)
            #self.readToEOL(gobble=False)
        else:
            isShortForm = False
            if self.peek()==':':
                self.advance()
            self.getWhiteSpace()
            self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)
            srcBlock = self._eatToThisEndDirective(macroName)


        if hasattr(macro, 'convertArgStrToDict'):
            kwArgs = macro.convertArgStrToDict(args, parser=self, startPos=startPos)
        else:
            def getArgs(*pargs, **kws):
                return pargs, kws
            exec 'positionalArgs, kwArgs = getArgs(%(args)s)'%locals()

        assert not kwArgs.has_key('src')
        kwArgs['src'] = srcBlock

        if type(macro)==new.instancemethod:
            co = macro.im_func.func_code
        elif (hasattr(macro, '__call__')
              and hasattr(macro.__call__, 'im_func')):
            co = macro.__call__.im_func.func_code
        else:
            co = macro.func_code
        availableKwArgs = inspect.getargs(co)[0]
        
        if 'parser' in availableKwArgs:
            kwArgs['parser'] = self
        if 'macros' in availableKwArgs:
            kwArgs['macros'] = self._macros
        if 'compilerSettings' in availableKwArgs:
            kwArgs['compilerSettings'] = self.settings()
        if 'isShortForm' in availableKwArgs:
            kwArgs['isShortForm'] = isShortForm
        if isShortForm and 'EOLCharsInShortForm' in availableKwArgs:
            kwArgs['EOLCharsInShortForm'] = EOLCharsInShortForm

        if 'startPos' in availableKwArgs:
            kwArgs['startPos'] = startPos
        if 'endPos' in availableKwArgs:
            kwArgs['endPos'] = self.pos()

        srcFromMacroOutput = macro(**kwArgs)

        origParseSrc = self._src
        origBreakPoint = self.breakPoint()
        origPos = self.pos()
        # add a comment to the output about the macro src that is being parsed
        # or add a comment prefix to all the comments added by the compiler
        self._src = srcFromMacroOutput
        self.setPos(0)
        self.setBreakPoint(len(srcFromMacroOutput))
        
        self.parse(assertEmptyStack=False)

        self._src = origParseSrc
        self.setBreakPoint(origBreakPoint)
        self.setPos(origPos)                


        #self._compiler.addRawText('end')
        
    def eatCache(self):
        isLineClearToStartToken = self.isLineClearToStartToken()
        endOfFirstLinePos = self.findEOL()
        lineCol = self.getRowCol()
        self.getDirectiveStartToken()
        self.advance(len('cache'))

        startPos = self.pos()
        argList = self.getDefArgList(useNameMapper=True)
        argList = self._applyExpressionFilters(argList, 'cache', startPos=startPos)

        def startCache():
            cacheInfo = self._compiler.genCacheInfoFromArgList(argList)
            self._compiler.startCacheRegion(cacheInfo, lineCol)

        if self.matchColonForSingleLineShortFormDirective():            
            self.advance() # skip over :
            self.getWhiteSpace(max=1)
            startCache()
            self.parse(breakPoint=self.findEOL(gobble=True))
            self._compiler.endCacheRegion()
        else:
            if self.peek()==':':
                self.advance()
            self.getWhiteSpace()            
            self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)
            self.pushToOpenDirectivesStack('cache')
            startCache()        

    def eatCall(self):
        # @@TR: need to enable single line version of this
        isLineClearToStartToken = self.isLineClearToStartToken()
        endOfFirstLinePos = self.findEOL()
        lineCol = self.getRowCol()
        self.getDirectiveStartToken()
        self.advance(len('call'))
        startPos = self.pos()
        
        useAutocallingOrig = self.setting('useAutocalling')
        self.setSetting('useAutocalling', False)
        self.getWhiteSpace()
        if self.matchCheetahVarStart():
            functionName = self.getCheetahVar()
        else:
            functionName = self.getCheetahVar(plain=True, skipStartToken=True)
        self.setSetting('useAutocalling', useAutocallingOrig)
        # @@TR: fix up filtering
        self._applyExpressionFilters(self[startPos:self.pos()], 'call', startPos=startPos)

        self.getWhiteSpace()
        args = self.getExpression(pyTokensToBreakAt=[':']).strip()
        if self.matchColonForSingleLineShortFormDirective():
            self.advance() # skip over :
            self._compiler.startCallRegion(functionName, args, lineCol)
            self.getWhiteSpace(max=1)
            self.parse(breakPoint=self.findEOL(gobble=False))
            self._compiler.endCallRegion()
        else:
            if self.peek()==':':
                self.advance()
            self.getWhiteSpace()
            self.pushToOpenDirectivesStack("call")            
            self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)
            self._compiler.startCallRegion(functionName, args, lineCol)

    def eatCallArg(self):
        isLineClearToStartToken = self.isLineClearToStartToken()
        endOfFirstLinePos = self.findEOL()
        lineCol = self.getRowCol()
        self.getDirectiveStartToken()

        self.advance(len('arg'))
        startPos = self.pos()
        self.getWhiteSpace()
        argName = self.getIdentifier()
        self.getWhiteSpace()
        argName = self._applyExpressionFilters(argName, 'arg', startPos=startPos)
        self._compiler.setCallArg(argName, lineCol)
        if self.peek() == ':':
            self.getc()
        else:        
            self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)

    def eatFilter(self):
        isLineClearToStartToken = self.isLineClearToStartToken()
        endOfFirstLinePos = self.findEOL()

        self.getDirectiveStartToken()
        self.advance(len('filter'))
        self.getWhiteSpace()
        startPos = self.pos()
        if self.matchCheetahVarStart():
            isKlass = True
            theFilter = self.getExpression(pyTokensToBreakAt=[':'])
        else:
            isKlass = False
            theFilter = self.getIdentifier()
            self.getWhiteSpace()
        theFilter = self._applyExpressionFilters(theFilter, 'filter', startPos=startPos)            

        if self.matchColonForSingleLineShortFormDirective():
            self.advance() # skip over :
            self.getWhiteSpace(max=1)            
            self._compiler.setFilter(theFilter, isKlass)
            self.parse(breakPoint=self.findEOL(gobble=False))
            self._compiler.closeFilterBlock()
        else:
            if self.peek()==':':
                self.advance()
            self.getWhiteSpace()
            self.pushToOpenDirectivesStack("filter")
            self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)
            self._compiler.setFilter(theFilter, isKlass)        

    def eatTransform(self):
        isLineClearToStartToken = self.isLineClearToStartToken()
        endOfFirstLinePos = self.findEOL()

        self.getDirectiveStartToken()
        self.advance(len('transform'))
        self.getWhiteSpace()
        startPos = self.pos()
        if self.matchCheetahVarStart():
            isKlass = True
            transformer = self.getExpression(pyTokensToBreakAt=[':'])
        else:
            isKlass = False
            transformer = self.getIdentifier()
            self.getWhiteSpace()
        transformer = self._applyExpressionFilters(transformer, 'transform', startPos=startPos)

        if self.peek()==':':
            self.advance()
        self.getWhiteSpace()
        self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)
        self._compiler.setTransform(transformer, isKlass)

        
    def eatErrorCatcher(self):
        isLineClearToStartToken = self.isLineClearToStartToken()
        endOfFirstLinePos = self.findEOL()
        self.getDirectiveStartToken()
        self.advance(len('errorCatcher'))
        self.getWhiteSpace()
        startPos = self.pos()
        errorCatcherName = self.getIdentifier()
        errorCatcherName = self._applyExpressionFilters(
            errorCatcherName, 'errorcatcher', startPos=startPos)
        self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)        
        self._compiler.setErrorCatcher(errorCatcherName)

    def eatCapture(self):
        # @@TR:  this could be refactored to use the code in eatSimpleIndentingDirective
        # filtered 
        isLineClearToStartToken = self.isLineClearToStartToken()
        endOfFirstLinePos = self.findEOL()
        lineCol = self.getRowCol()

        self.getDirectiveStartToken()
        self.advance(len('capture'))        
        startPos = self.pos()
        self.getWhiteSpace()

        expr = self.getExpression(pyTokensToBreakAt=[':'])
        expr = self._applyExpressionFilters(expr, 'capture', startPos=startPos)
        if self.matchColonForSingleLineShortFormDirective():
            self.advance() # skip over :
            self._compiler.startCaptureRegion(assignTo=expr, lineCol=lineCol)
            self.getWhiteSpace(max=1)
            self.parse(breakPoint=self.findEOL(gobble=False))
            self._compiler.endCaptureRegion()
        else:
            if self.peek()==':':
                self.advance()
            self.getWhiteSpace()
            self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLinePos)
            self.pushToOpenDirectivesStack("capture")
            self._compiler.startCaptureRegion(assignTo=expr, lineCol=lineCol)
        

    def eatIf(self):
        # filtered 
        isLineClearToStartToken = self.isLineClearToStartToken()
        endOfFirstLine = self.findEOL()
        lineCol = self.getRowCol()
        self.getDirectiveStartToken()
        startPos = self.pos()
        
        expressionParts = self.getExpressionParts(pyTokensToBreakAt=[':'])
        expr = ''.join(expressionParts).strip()
        expr = self._applyExpressionFilters(expr, 'if', startPos=startPos)

        isTernaryExpr = ('then' in expressionParts and 'else' in expressionParts)
        if isTernaryExpr:
            conditionExpr = []
            trueExpr = []
            falseExpr = []
            currentExpr = conditionExpr
            for part in expressionParts:
                if part.strip()=='then':
                    currentExpr = trueExpr
                elif part.strip()=='else':
                    currentExpr = falseExpr
                else:
                    currentExpr.append(part)
                    
            conditionExpr = ''.join(conditionExpr)
            trueExpr = ''.join(trueExpr)
            falseExpr = ''.join(falseExpr)
            self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLine)            
            self._compiler.addTernaryExpr(conditionExpr, trueExpr, falseExpr, lineCol=lineCol)
        elif self.matchColonForSingleLineShortFormDirective():
            self.advance() # skip over :
            self._compiler.addIf(expr, lineCol=lineCol)
            self.getWhiteSpace(max=1)
            self.parse(breakPoint=self.findEOL(gobble=True))            
            self._compiler.commitStrConst()            
            self._compiler.dedent()
        else:
            if self.peek()==':':
                self.advance()
            self.getWhiteSpace()                
            self._eatRestOfDirectiveTag(isLineClearToStartToken, endOfFirstLine)            
            self.pushToOpenDirectivesStack('if')
            self._compiler.addIf(expr, lineCol=lineCol)

    ## end directive handlers
    def handleEndDef(self):
        isNestedDef = (self.setting('allowNestedDefScopes')
                       and [name for name in self._openDirectivesStack if name=='def'])
        if not isNestedDef:
            self._compiler.closeDef()
        else:
            # @@TR: temporary hack of useSearchList
            self.setSetting('useSearchList', self._useSearchList_orig)                    
            self._compiler.commitStrConst()
            self._compiler.dedent()
    ###

    def pushToOpenDirectivesStack(self, directiveName):
        assert directiveName in self._closeableDirectives
        self._openDirectivesStack.append(directiveName)

    def popFromOpenDirectivesStack(self, directiveName):
        if not self._openDirectivesStack:
            raise ParseError(self, msg="#end found, but nothing to end")
        
        if self._openDirectivesStack[-1] == directiveName:
            del self._openDirectivesStack[-1]
        else:
            raise ParseError(self, msg="#end %s found, expected #end %s" %(
                directiveName, self._openDirectivesStack[-1]))

    def assertEmptyOpenDirectivesStack(self):
        if self._openDirectivesStack:
            errorMsg = (
                "Some #directives are missing their corresponding #end ___ tag: %s" %(
                ', '.join(self._openDirectivesStack)))
            raise ParseError(self, msg=errorMsg)

##################################################
## Make an alias to export   
Parser = _HighLevelParser
