Replaces the templates within the inputted text with the given
    options.  Templates are defined as text within matching 
    braces, and can include additional formatting options.  
    Any key that is not found in the options will be replaced 
    as it was found in the inputted text.
    
    :param      text        <str>
    :param      options     <dict> { <str> key: <variant> value, .. }
    :param      processed   <list> [ <str> key, .. ]        used internally
    
    :return     <str> formatted text
    
    :usage      |import projex.text
                |options = { 'key': 10, 'name': 'eric' }
                |template = '[name::lower]_[key]_[date::%m-%d-%y].txt'
                |projex.text.render( template, options )
    
    :built-ins  date    will render the current datetime
    
    :options    The following are a list of formatting options text:
                
                lower         | converts the value to lowercase
                upper         | converts the value to uppercase
                camelHump     | converts the value to camel-humped text
                underscore    | converts the value to underscored text
                pretty        | converts the value to pretty text
                capitalized   | converts the value to capltalized words
                words         | converts the value to space separated words
                upper_first   | capitalizes just the first letter
                lower_first   | lowercases just the first letter
                replace(x, y) | replaces the instances of x with y
                lstrip(x)     | removes the beginning instance of x
                rstrip(x)     | removes the ending instance of x
                slice(x, y)   | same as doing string[x:y]

def render(text, options, processed=None):
    """
    Replaces the templates within the inputted text with the given
    options.  Templates are defined as text within matching 
    braces, and can include additional formatting options.  
    Any key that is not found in the options will be replaced 
    as it was found in the inputted text.
    
    :param      text        <str>
    :param      options     <dict> { <str> key: <variant> value, .. }
    :param      processed   <list> [ <str> key, .. ]        used internally
    
    :return     <str> formatted text
    
    :usage      |import projex.text
                |options = { 'key': 10, 'name': 'eric' }
                |template = '[name::lower]_[key]_[date::%m-%d-%y].txt'
                |projex.text.render( template, options )
    
    :built-ins  date    will render the current datetime
    
    :options    The following are a list of formatting options text:
                
                lower         | converts the value to lowercase
                upper         | converts the value to uppercase
                camelHump     | converts the value to camel-humped text
                underscore    | converts the value to underscored text
                pretty        | converts the value to pretty text
                capitalized   | converts the value to capltalized words
                words         | converts the value to space separated words
                upper_first   | capitalizes just the first letter
                lower_first   | lowercases just the first letter
                replace(x, y) | replaces the instances of x with y
                lstrip(x)     | removes the beginning instance of x
                rstrip(x)     | removes the ending instance of x
                slice(x, y)   | same as doing string[x:y]
    """
    output = unicode_type(text)
    expr = re.compile('(\[+([^\[\]]+)\]\]?)')
    results = expr.findall(output)
    curr_date = datetime.datetime.now()
    options_re = re.compile('(\w+)\(?([^\)]+)?\)?')

    if processed is None:
        processed = []

    for repl, key in results:
        # its possible to get multiple items for processing
        if repl in processed:
            continue

        # record the repl value as being processed
        processed.append(repl)

        # replace template templates
        if repl.startswith('[[') and repl.endswith(']]'):
            output = output.replace(repl, '[%s]' % key)
            continue

        # determine the main key and its options
        splt = key.split('::')
        key = splt[0]
        prefs = splt[1:]
        value = None

        # use the inputted options
        if key in options:
            # extract the value
            value = options[key]

            # format a float
            if type(value) in (float, int):
                if prefs:
                    value = prefs[0] % value
                else:
                    value = nativestring(value)

            # convert date time values
            elif type(value) in (datetime.datetime,
                                 datetime.date,
                                 datetime.time):
                if not prefs:
                    date_format = '%m/%d/%y'
                else:
                    date_format = prefs[0]
                    prefs = prefs[1:]

                value = value.strftime(nativestring(date_format))

            else:
                value = render(options[key], options, processed)

        # look for the built-in options
        elif key == 'date':
            value = curr_date

            if not prefs:
                date_format = '%m/%d/%y'
            else:
                date_format = prefs[0]
                prefs = prefs[1:]

            value = value.strftime(nativestring(date_format))

        # otherwise, continue
        else:
            continue

        # apply the prefs to the value
        if value and prefs:

            for pref in prefs:
                result = options_re.match(pref)
                pref, opts = result.groups()

                if opts:
                    opts = [opt.strip() for opt in opts.split(',')]
                else:
                    opts = []

                if 'lower' == pref:
                    value = value.lower()
                elif 'upper' == pref:
                    value = value.upper()
                elif 'upper_first' == pref:
                    value = value[0].upper() + value[1:]
                elif 'lower_first' == pref:
                    value = value[0].lower() + value[1:]
                elif 'camelHump' == pref:
                    value = camelHump(value)
                elif 'underscore' == pref:
                    value = underscore(value)
                elif 'capitalize' == pref:
                    value = capitalize(value)
                elif pref in ('pluralize', 'plural'):
                    value = pluralize(value)
                elif 'words' == pref:
                    value = ' '.join(words(value))
                elif 'pretty' == pref:
                    value = pretty(value)

                elif 'replace' == pref:
                    if len(opts) == 2:
                        value = value.replace(opts[0], opts[1])
                    else:
                        logger.warning('Invalid options for replace: %s',
                                       ', '.join(opts))

                elif 'slice' == pref:
                    if len(opts) == 2:
                        value = value[int(opts[0]):int(opts[1])]
                    else:
                        logger.warning('Invalid options for slice: %s',
                                       ', '.join(opts))

                elif 'lstrip' == pref:
                    if not opts:
                        value = value.lstrip()
                    else:
                        for k in opts:
                            if value.startswith(k):
                                value = value[len(k):]

                elif 'rstrip' == pref:
                    if not opts:
                        value = value.rstrip()
                    else:
                        for k in opts:
                            if value.endswith(k):
                                value = value[:-len(k)]

        output = output.replace(repl, value)

    return output