src/tools/template_engine.py
changeset 0 ca70ae20a155
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/tools/template_engine.py	Tue Feb 16 10:07:05 2010 +0530
@@ -0,0 +1,207 @@
+# Copyright (c) 2005 Nokia Corporation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from StringIO import StringIO
+import re
+import os.path
+import traceback
+import sys
+
+TEMPLATEFILE_SUFFIX='.in'
+
+def is_templatefile(filename):
+    return filename.endswith(TEMPLATEFILE_SUFFIX)
+
+def outfilename_from_infilename(infilename):
+    if not is_templatefile(infilename):
+        raise ValueError, "expected file name with suffix "+TEMPLATEFILE_SUFFIX+", got "+infilename
+    return infilename[:-len(TEMPLATEFILE_SUFFIX)]
+
+class MacroEvaluationError(Exception): pass
+
+macro_delimiter_re=re.compile(r'(\${{|}})')
+macro_eval_re=re.compile('\${{(.*?)}}$',re.S)
+macro_exec_re=re.compile('\${{!(\s*\n)?(.*?)\s*}}$',re.S)
+macro_if_re  =re.compile('\${{if (.*)}}$',re.S)
+
+def process_macros(instr, namespace=None):
+    """Return result of doing macro processing on instr, in namespace namespace.
+
+    Basic substitution and evaluation:
+    >>> process_macros('${{foo}}',{'foo':1})
+    '1'
+    >>> process_macros('${{2+2}}')
+    '4'
+    >>> process_macros('${{foo+bar}}',{'foo': 1, 'bar': 2})
+    '3'
+
+    Error cases:
+    >>> process_macros('}}')
+    Traceback (most recent call last):
+      ...
+    MacroEvaluationError: Mismatched parens in macro }}
+    >>> process_macros('${{')
+    Traceback (most recent call last):
+      ...
+    MacroEvaluationError: Mismatched parens in macro ${{
+
+    Corner cases:
+    >>> process_macros('')
+    ''
+    >>> process_macros(u'')
+    u''
+
+    Code execution:
+    >>> process_macros("${{!print 'foo'}}")
+    foo
+    ''
+    >>> process_macros("${{!  \\nfor k in range(4):\\n    write(str(k))}}")
+    '0123'
+
+    >>> process_macros('${{if 1\\nfoo\\n$else\\nbar}}')
+    'foo'
+    >>> process_macros('xxx${{if 1\\n${{foo}}\\n$else\\nbar}}yyy',{'foo':42})
+    'xxx42yyy'
+    >>> process_macros('${{!#}}')
+    ''
+    >>> process_macros('${{"${{foo}}"}}',{'foo':42})
+    '42'
+    >>> process_macros('${{"${{bar}}"}}',{'foo':42})
+    Traceback (most recent call last):
+      ...
+    MacroEvaluationError: Error evaluating expression "${{bar}}": NameError: name 'bar' is not defined
+    <BLANKLINE>
+    """
+    if namespace is None:
+        namespace={}
+
+    def process_text(text):
+        #print "Processing text: "+repr(text)
+        pos=0 # position of the first character of text that is not yet processed
+        outbuf=[]
+        macrobuf=[]
+        while 1:
+            m=macro_delimiter_re.search(text, pos)
+            if m:
+                ##print "found delimiter: "+text[m.start():]
+                outbuf.append(text[pos:m.start()])
+                pos=m.start()
+                paren_level=0
+                for m in macro_delimiter_re.finditer(text,pos): # find a single whole macro expression
+                    delim=m.group(1)
+                    if delim=='${{':
+                        paren_level+=1
+                    elif delim=='}}':
+                        paren_level-=1
+                    else:
+                        assert 0
+                    if paren_level<0:
+                        raise MacroEvaluationError("Mismatched parens in macro %s"%text[pos:m.end()])
+                    if paren_level == 0: # macro expression finishes here, evaluate it.
+                        outbuf.append(process_text(process_macro(text[pos:m.end()])))
+                        pos=m.end()
+                        break
+                if paren_level!=0:
+                    raise MacroEvaluationError("Mismatched parens in macro %s"%text[pos:m.end()])
+            else: # no more macros, just output the rest of the plain text
+                outbuf.append(text[pos:])
+                break
+        result=''.join(outbuf)
+        #print "Text after processing: "+repr(result)
+        return result
+    def process_macro(macro_expression):
+        #print 'Processing macro: '+repr(macro_expression)
+        try:
+            outstr=StringIO()
+            namespace['write']=outstr.write
+            m=macro_exec_re.match(macro_expression) # ${{!code}} -- exec the code
+            if m:
+                exec m.group(2) in namespace
+            else:
+                m=macro_if_re.match(macro_expression) # ${{if -- if expression
+                if m:
+                    outstr.write(handle_if(process_text(m.group(1)),namespace))
+                else:
+                    m=macro_eval_re.match(macro_expression)
+                    if m: # ${{code}}  -- eval the code
+                        outstr.write(str(eval(m.group(1),namespace)))
+                    else:
+                        raise MacroEvaluationError, 'Invalid macro'
+            #print 'Macro result: '+repr(outstr.getvalue())
+            return outstr.getvalue()
+        except:
+            raise MacroEvaluationError, 'Error evaluating expression "%s": %s'%(
+                macro_expression,
+                '\n'.join(traceback.format_exception_only(sys.exc_info()[0],sys.exc_info()[1])))
+    def if_tokenized(code):
+        #print "code: "+repr(code)
+        lines=code.split('\n')
+        yield ('if',lines[0])
+        #print 'lines: '+repr(lines)
+        for line in lines[1:]:
+            m=re.match('\$(elif|else)(.*)',line)
+            if m:
+                yield (m.group(1),m.group(2))
+            else:
+                #print "data line "+repr(line)
+                yield ('data',line)
+        raise StopIteration
+    def handle_if(code,namespace):
+        outbuf=[]
+        true_condition_found=0
+        for token,content in if_tokenized(code):
+            if token=='data' and true_condition_found:
+                outbuf.append(content)
+            else:
+                if true_condition_found: # end of true block reached
+                    break
+                if token=='if' or token=='elif':
+                    true_condition_found=eval(content,namespace)
+                elif token=='else':
+                    true_condition_found=1
+        return '\n'.join(outbuf)
+    return process_text(instr)
+
+def process_file(infilename,namespace):
+    outfilename=outfilename_from_infilename(infilename)
+    infile=open(infilename,'rt')
+    outfile=open(outfilename,'wt')
+    outfile.write(process_macros(infile.read(),namespace))
+    outfile.close()
+    infile.close()
+
+def templatefiles_in_tree(rootdir):
+    files=[]
+    for dirpath, dirnames, filenames in os.walk(rootdir):
+        files+=[os.path.join(dirpath,x) for x in filenames if is_templatefile(x)]
+    return files
+
+def misctest():
+    files=templatefiles_in_tree('.')
+    print "Templates in tree: "+str(files)
+
+    for k in files:
+        print "Processing file: "+k
+        process_file(k,namespace)
+    # import code
+    # code.interact(None,None,locals())
+
+def _test():
+    import doctest
+    doctest.testmod()
+
+if __name__ == "__main__":
+    _test()
+
+