root/trunk/cherrytemplate.py
| Revision 24 (by rdelon, 03/09/06 05:28:59) |
|---|
""" Copyright (c) 2004, CherryPy Team (team@cherrypy.org) All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of the CherryPy Team nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ __version__ = '1.1.1-beta' import sys, StringIO, inspect, os, time import copy default_input_encoding = None default_output_encoding = None default_output_encoding_errors = 'replace' path = ['.'] default_return_generator = False use_caching = False _cache = {} _quote3 = '"""' class RenderError(Exception): pass class ParseError(Exception): pass class InternalError(Exception): pass class FileNotFoundError(Exception): pass def _find_closing_quote(str, quote, startIndex, beforeNewline=1): while 1: i=str.find(quote, startIndex) if i==-1: raise ParseError("No closing '%s' for string '%s ...'" % (quote, str[startIndex:startIndex+20])) elif str[i-1] == '\\': startIndex = i+1 continue if beforeNewline: j=str.find('\n', startIndex) if j!=-1 and j<i: raise ParseError("No closing '%s' for string '%s ...' before the end of the line" % (quote, str[startIndex:j])) return i def _findEndOfTag(str, startIndex, beforeNewline=1): i = _find_closing_quote(str, '"', str.find('"', startIndex)+1, beforeNewline) if str[i+1]=='>': end = i+1 elif str[i+1:i+3]=='/>': end = i+2 elif str[i+1:i+4]==' />': end = i+3 else: raise ParseError("No closing '%s' for string '%s ...'"%('">', str[startIndex:startIndex+20])) if beforeNewline: j=str.find('\n', startIndex) if j!=-1 and j<i: raise ParseError("No closing '%s' for string '%s ...' before the end of the line"%('">', str[startIndex:j])) return (i, end) def _findEndOfPyCode(str, startIndex): i = startIndex while 1: i = _find_closing_quote(str, '>', i+1, 0) if i == -1: raise ParseError("No closing '%s' for string '%s ...'"%('">', str[startIndex:startIndex+20])) elif str[i-1:i+1] == '">': return (i-1, i) elif str[i-2:i+1] == '"/>': return (i-2, i) elif str[i-3:i+1] == '" />': return (i-3, i) def _find_closing_tag(template, openingTag, closingTag, openTagCount, startIndex, text): if openTagCount < 0: raise ParseError("Too many closing tags '%s' for '%s ... %s ...'" % ( closingTag, openingTag, text)) i = template.find(openingTag, startIndex) j = template.find(closingTag, startIndex) if j == -1: raise ParseError("No matching '%s' tag for '%s ... %s ...'"%(closingTag, openingTag, text)) if i == -1 or j < i: # closingTag is first if openTagCount == 0: return j # found it ! return _find_closing_tag(template, openingTag, closingTag, openTagCount-1, j+1, text) else: # openingTag is first return _find_closing_tag(template, openingTag, closingTag, openTagCount+1, i+1, text) def _findClosingDiv(template, startIndex, text): return _find_closing_tag(template, '<div', '</div>', 0, startIndex, text) def _findClosingPyFor(template, startIndex, text): return _find_closing_tag(template, '<py-for', '</py-for>', 0, startIndex, text) def _findClosingPyIf(template, startIndex, text): return _find_closing_tag(template, '<py-if', '</py-if>', 0, startIndex, text) def _findClosingPyElse(template, startIndex, text): return _find_closing_tag(template, '<py-else', '</py-else>', 0, startIndex, text) def _write_in_triple_quotes(f, str, tab): if str: str=str.replace('\\', '\\\\').replace('"""', '\\"""') if str[0]=='"': str='\\'+str if str[-1]=='"': str=str[:-1]+'\\"' if isinstance(str, unicode): unicodePrefix = 'u' else: unicodePrefix = '' f.write( tab + "yield " + unicodePrefix + _quote3 + str + _quote3 + "\n") def _get_template_file(filename, path): if os.path.isabs(filename): return open(filename, 'rb').read() for dir in path: newfilename = os.path.join(dir, filename) if os.path.exists(newfilename): return open(newfilename, 'rb').read() raise FileNotFoundError(filename, path) def _expand_py_include(template, path, loop=0): if loop>100: raise ParseError("Infinite loop in 'py-include'") i=template.find("py-include") if i==-1: return template if template[i+10:i+12]!='="': raise ParseError("Tag 'py-include' should be followed by '=\"'") j,end=_findEndOfTag(template, i) # Read template file templateFilename = template[i+12:j] templateData = _get_template_file(templateFilename, path) # Replace py-include tag with template file if i > 0 and template[i-1] == '<': # CGTL tag (<py-include) i = i-1 j = end else: if i >= 5 and template[i-5:i]=='<div ': # CHTL tag (<div py-include) j = _findClosingDiv(template, i, templateFilename) i = i-5 j = j+5 else: raise ParseError("'py-include' tag can be used either as '<py-include=\"...\">' or '<div py-include=\"...\">...</div>'") template = template[:i] + templateData + template[j+1:] template = _expand_py_include(template, path, loop+1) return template def _firstNonSpace(s): i = 0 while i < len(s) and s[i] == ' ': i += 1 return i def _write_template(f, template, tab): # New cherrytemplate syntax tags = ['<%--', '<%=i', '<%=', '<%'] minI = len(template) minTag = "" for tag in tags: i = template.find(tag) if i == -1: continue if i < minI: minI = i minTag = tag if minTag == '<%': j = _find_closing_tag(template, '<%', '%>', 0, minI + 1, '') execStr = template[minI+2:j] # Try to indent execStr correctly lines=[] minIndent=1000 lastIndent=0 for line in execStr.split('\n'): if line.split(): sLine = line.strip() indentCount = _firstNonSpace(line) lastIndent = indentCount if sLine[-1] == ':': lastIndent += 4 if indentCount < minIndent: minIndent = indentCount lines.append(line) if minIndent==1000: minIndent=0 lastIndent -= minIndent _write_template(f, template[:minI], tab) lastLineIndent = '' isEnd = False if len(lines) == 1: sLine = lines[0].strip() if sLine.startswith('# end') or sLine.startswith('#end'): isEnd = True tab = tab[4:] if not isEnd: for line in lines: f.write(tab + line[minIndent:] + '\n') _write_template(f, template[j+2:], tab + ' ' * lastIndent) return elif minTag == '<%=i': j = _find_closing_tag(template, '<%=i', '%>', 0, minI + 1, '') eval_str = template[minI + 4:j] _write_in_triple_quotes(f, template[:minI], tab) f.write(tab + 'for _line in %s:\n' % eval_str) f.write(tab + ' yield _line\n') _write_template(f, template[j+2:], tab) return elif minTag == '<%=': j = _find_closing_tag(template, '<%=', '%>', 0, minI + 1, '') eval_str = template[minI+3:j] _write_in_triple_quotes(f, template[:minI], tab) f.write(tab+'yield %s\n' % eval_str) _write_template(f, template[j+2:], tab) return elif minTag == '<%--': j = _find_closing_tag(template, '<%--', '--%>', 0, minI + 1, '') _write_in_triple_quotes(f, template[:minI], tab) _write_template(f, template[j+2:], tab) return # Old cherrytemplate syntax tags=['py-eval', 'py-exec', 'py-code', 'py-attr', 'py-if', 'py-for'] minI=len(template) minTag="" for tag in tags: i=template.find(tag+'="') if i==-1: continue if i<minI: minI=i minTag=tag if not minTag: # Check that no tags are left without '=' # This catches common mistake: 'py-if "1==1"' instead of 'py-if="1==1"' if not minTag: minI=-1 for tag in tags: if template[:minI].find(tag) !=-1: raise ParseError("Tag '%s' should be followed by '=\"'"%(tag)) # Check that no "py-else" are left: if not minTag and template.find("py-else")!=-1: raise ParseError("Tag 'py-else' found without corresponding 'py-if'") if not minTag: _write_in_triple_quotes(f, template, tab) if minTag=='py-eval': j,maxI=_findEndOfTag(template, minI) eval_str=template[minI+9:j] if minI>0 and template[minI-1]=='<': # CGTL tag (<py-eval) _write_in_triple_quotes(f, template[:minI-1], tab) f.write(tab+'yield %s\n' % eval_str) _write_template(f, template[maxI+1:], tab) else: # CHTL tag (<div py-eval) j2=template.find('<', j) # Check if we have a special <div, just for the py-eval if minI>=5 and template[minI-5:minI]=='<div ': # Special case for <div py-eval="i+2">Dummy</div>: remove <div and </div in that case j3=_findClosingDiv(template, minI, eval_str) _write_in_triple_quotes(f, template[:minI-5], tab) f.write(tab+'yield %s\n'%eval_str) _write_template(f, template[j3+6:], tab) else: _write_in_triple_quotes(f, template[:minI-1]+'>', tab) f.write(tab+'yield %s\n'%eval_str) _write_template(f, template[j2:], tab) elif minTag=='py-attr': j=_find_closing_quote(template, '"', minI+9) j2a=template.find('="', j) if j2a==-1: j2a=len(template) j2b=template.find("='", j) if j2b==-1: j2b=len(template) if j2a<j2b: j2=j2a j3=template.find('"', j2+2) else: j2=j2b j3=template.find("'", j2+2) eval_str=template[minI+9:j] _write_in_triple_quotes(f, template[:minI-1]+template[j+1:j2+2], tab) f.write(tab+'yield %s\n'%eval_str) _write_template(f, template[j3:], tab) elif minTag=='py-exec': j,maxI=_findEndOfTag(template, minI) execStr=template[minI+9:j] if minI>0 and template[minI-1]=='<': # CGTL tag(<py-exec) _write_in_triple_quotes(f, template[:minI-1], tab) f.write(tab+execStr+'\n') _write_template(f, template[maxI+1:], tab) else: # CHTL tag(<div py-exec) # Check that we have a </div> after the command if template[j+2:j+2+6]!='</div>': raise ParseError("'<div py-exec=%s' is not closed with '</div>'"%execStr) j0=template.rfind('<div', 0, minI) j2=template.find('</div>', j0) _write_in_triple_quotes(f, template[:j0], tab) f.write(tab+execStr+'\n') _write_template(f, template[j2+6:], tab) elif minTag=='py-code': # Has to be used like: # <div py-code=" # i=1 # yield "%s 2"%i # "> if template[minI+9]!='\n': raise ParseError("'py-code=\"' must be followed by a newline") j,maxI=_findEndOfPyCode(template, minI) execStr=template[minI+10:j] # Try to indent execStr correctly lines=[] minIndent=1000 for line in execStr.split('\n'): if line.split(): indentCount = 0 while indentCount < len(line) and line[indentCount:indentCount+4] == ' ': indentCount += 4 if indentCount < minIndent: minIndent=indentCount lines.append(line) if minIndent==1000: minIndent=0 if minI>0 and template[minI-1]=='<': # CGTL tag(<py-code) _write_in_triple_quotes(f, template[:minI-1], tab) for line in lines: # Remove "minIndent" tabs and add "tab" tabs from each line f.write(tab+line[minIndent:]+'\n') _write_template(f, template[maxI+1:], tab) else: # CHTL tag(<div py-code) # Check that we have a </div> after the command if template[j+2:j+2+6]!='</div>': raise ParseError("'<div py-code=%s' is not closed with '</div>'"%execStr) j0=template.rfind('<div', 0, minI) j2=template.find('</div>', j0) _write_in_triple_quotes(f, template[:j0], tab) for line in lines: # Remove "minIndent" tabs and add "tab" tabs from each line f.write(tab+line[minIndent:]+'\n') _write_template(f, template[j2+6:], tab) elif minTag=='py-for': j=_find_closing_quote(template, '">', minI) forStr=template[minI+8:j] if minI>0 and template[minI-1]=='<': # CGTL (<py-for ... </py-for>) j2=_findClosingPyFor(template, j, forStr) text=template[j+2:j2] _write_in_triple_quotes(f, template[:minI-1], tab) try: forStr.split(' in ')[1] except IndexError: raise ParseError("py-for string '%s' is not correct"%forStr) f.write(tab+'for '+forStr+':\n') _write_template(f, text, tab+' ') _write_template(f, template[j2+9:], tab) else: # CHTL (<div py-for ... </div>) j0=template.rfind('<div', 0, minI) # Find matching </div> (warning: could be nested) j2=_findClosingDiv(template, j0+1, forStr) text=template[j+2:j2] _write_in_triple_quotes(f, template[:j0], tab) try: forStr.split(' in ')[1] except IndexError: raise ParseError("py-for string '%s' is not correct"%forStr) f.write(tab+'for '+forStr+':\n') _write_template(f, text, tab+' ') _write_template(f, template[j2+6:], tab) elif minTag=='py-if': j = _find_closing_quote(template, '">', minI) ifStr = template[minI+7:j] if minI > 0 and template[minI - 1] == '<': # CGTL (<py-if ... </py-if> <py-else>...</py-else>) j2 = _findClosingPyIf(template, j, ifStr) ifText = template[j + 2:j2] # Check if there is a <py-else> k = j2 + 8 # k will be the index of the next significant character after </py-if> while k < len(template): if ' \r\n '.find(template[k]) == -1: break k += 1 if k != len(template) and template[k:k + 9] == '<py-else>': j3 = _findClosingPyElse(template, k + 9, ifStr + " else ") elseText = template[k + 9:j3] j4 =j3 + 10 else: elseText = "" j4 = j2 + 8 #print "ifStr:",ifStr _write_in_triple_quotes(f, template[:minI - 1], tab) f.write(tab+'if %s:\n'%ifStr) _write_template(f, ifText, tab+' ') if elseText: f.write(tab+'else:\n') _write_template(f, elseText, tab+' ') _write_template(f, template[j4:], tab) else: # CHTL (<div py-if ... </div> <div py-else>...</div>) j0=template.rfind('<div', 0, minI) # Find matching </div> (warning: could be nested) j2=_findClosingDiv(template, j0+1, ifStr) ifText=template[j+2:j2] # Check if there is a py-else> k=j2+6 # k will be the index of the next significant character after </div> while k<len(template): if ' \r\n '.find(template[k])==-1: break k+=1 if k!=len(template) and template[k:k+13]=='<div py-else>': j3=_findClosingDiv(template, k+13, ifStr+" else ") elseText=template[k+13:j3] else: elseText="" j3=j2 #print "ifStr:",ifStr _write_in_triple_quotes(f, template[:j0], tab) f.write(tab+'if %s:\n'%ifStr) _write_template(f, ifText, tab+' ') if elseText: f.write(tab+'else:\n') _write_template(f, elseText, tab+' ') _write_template(f, template[j3+6:], tab) elif minTag: raise InternalError(minTag) def render(template = '', file = None, input_encoding = None, output_encoding = None, output_encoding_errors = None, return_generator = None, glob = None, loc = None, log_func = None, path = []): path += globals()['path'] # print "* Rendering:", file t0 = time.time() cache_key = (template or (file, tuple(path))) _original_template, _expanded_template, _compiled_template = \ _cache.get(cache_key, (None, None, None)) if not _compiled_template: if file != None: template = _get_template_file(file, path) _original_template = template _expanded_template = template # Expand py-include _expanded_template = _expand_py_include(_expanded_template, path) _expanded_template = _expanded_template.replace('\r\n', '\n') f = StringIO.StringIO() f.write("def _render_template():\n") _write_template(f, _expanded_template, ' ') _expanded_template = f.getvalue() try: _compiled_template = compile(_expanded_template, '<string>', 'exec') except: # In case of an exception, we include the body of the template in # the traceback import sys, traceback tb = "".join(traceback.format_exception(*sys.exc_info())) errors = ["An error occured while trying to render a template."] if file is not None: errors.append("The template file was %s" % repr(file)) errors.append("The traceback was:") errors.append(_indentAndNumberCode(tb, number = False)) errors.append("The template code was:") errors.append(_indentAndNumberCode(_expanded_template)) if file is None: errors.append("The original template was:") errors.append(_indentAndNumberCode(_original_template, number = False)) raise RenderError('\n'.join(errors)) if use_caching: _cache[cache_key] = (_original_template, _expanded_template, _compiled_template) if loc is None: loc = inspect.currentframe(1).f_locals if glob is None: glob = inspect.currentframe(1).f_globals g = glob.copy() # make a copy because we don't want to avoid changing original global scope. g.update(loc) # add local vars to global scope g to allow access from within template. try: exec(_compiled_template, g) except: # In case of an exception, we include the body of the template in # the traceback import sys, traceback tb = "".join(traceback.format_exception(*sys.exc_info())) errors = ["An error occured while trying to render a template."] if file is not None: errors.append("The template file was %s" % repr(file)) errors.append("The traceback was:") errors.append(_indentAndNumberCode(tb, number = False)) errors.append("The template code was:") errors.append(_indentAndNumberCode(_expanded_template)) if file is None: errors.append("The original template was:") errors.append(_indentAndNumberCode(_original_template, number = False)) raise RenderError('\n'.join(errors)) if output_encoding == None: output_encoding = default_output_encoding if output_encoding_errors == None: output_encoding_errors = default_output_encoding_errors input_encoding = input_encoding if input_encoding == None: input_encoding = default_input_encoding if return_generator == None: return_generator = default_return_generator try: result = eval('_render_template()',g) if return_generator: return _resultAsGenerator(result, input_encoding, output_encoding, output_encoding_errors, log_func) else: result = ''.join(list(result)) if not isinstance(result, unicode): if input_encoding: result = unicode(result, input_encoding) if output_encoding: return result.encode(output_encoding, output_encoding_errors) if log_func: log_func("Rendered file %s in %.02fs" % ( file, time.time() - t0)) return result except: # In case of an exception, we include the body of the template in # the traceback import sys, traceback tb = "".join(traceback.format_exception(*sys.exc_info())) errors = ["An error occured while trying to render a template."] if file is not None: errors.append("The template file was %s" % repr(file)) errors.append("The traceback was:") errors.append(_indentAndNumberCode(tb, number = False)) errors.append("The template code was:") errors.append(_indentAndNumberCode(_expanded_template)) if file is None: errors.append("The original template was:") errors.append(_indentAndNumberCode(_original_template, number = False)) raise RenderError('\n'.join(errors)) renderTemplate = render # legacy API def _resultAsGenerator(result, input_encoding, output_encoding, output_encoding_errors, log_func): for line in result: if not isinstance(line, unicode): if input_encoding: line = unicode(line, input_encoding) if output_encoding: yield line.encode(output_encoding, output_encoding_errors) yield line if log_func: log_func("Rendered file %s in %.02fs" % ( file, time.time() - t0)) def _indentAndNumberCode(code, number = True, tab = ' '): resList = [] for i, line in enumerate(code.splitlines()): if number: line = '%05d' % (i+1) + ' ' + line resList.append(tab + line) return '\n'.join(resList)
Note: See TracBrowser for help on using the browser.
