src/tools/template_engine.py
changeset 0 ca70ae20a155
equal deleted inserted replaced
-1:000000000000 0:ca70ae20a155
       
     1 # Copyright (c) 2005 Nokia Corporation
       
     2 #
       
     3 # Licensed under the Apache License, Version 2.0 (the "License");
       
     4 # you may not use this file except in compliance with the License.
       
     5 # You may obtain a copy of the License at
       
     6 #
       
     7 #     http://www.apache.org/licenses/LICENSE-2.0
       
     8 #
       
     9 # Unless required by applicable law or agreed to in writing, software
       
    10 # distributed under the License is distributed on an "AS IS" BASIS,
       
    11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
       
    12 # See the License for the specific language governing permissions and
       
    13 # limitations under the License.
       
    14 
       
    15 from StringIO import StringIO
       
    16 import re
       
    17 import os.path
       
    18 import traceback
       
    19 import sys
       
    20 
       
    21 TEMPLATEFILE_SUFFIX='.in'
       
    22 
       
    23 def is_templatefile(filename):
       
    24     return filename.endswith(TEMPLATEFILE_SUFFIX)
       
    25 
       
    26 def outfilename_from_infilename(infilename):
       
    27     if not is_templatefile(infilename):
       
    28         raise ValueError, "expected file name with suffix "+TEMPLATEFILE_SUFFIX+", got "+infilename
       
    29     return infilename[:-len(TEMPLATEFILE_SUFFIX)]
       
    30 
       
    31 class MacroEvaluationError(Exception): pass
       
    32 
       
    33 macro_delimiter_re=re.compile(r'(\${{|}})')
       
    34 macro_eval_re=re.compile('\${{(.*?)}}$',re.S)
       
    35 macro_exec_re=re.compile('\${{!(\s*\n)?(.*?)\s*}}$',re.S)
       
    36 macro_if_re  =re.compile('\${{if (.*)}}$',re.S)
       
    37 
       
    38 def process_macros(instr, namespace=None):
       
    39     """Return result of doing macro processing on instr, in namespace namespace.
       
    40 
       
    41     Basic substitution and evaluation:
       
    42     >>> process_macros('${{foo}}',{'foo':1})
       
    43     '1'
       
    44     >>> process_macros('${{2+2}}')
       
    45     '4'
       
    46     >>> process_macros('${{foo+bar}}',{'foo': 1, 'bar': 2})
       
    47     '3'
       
    48 
       
    49     Error cases:
       
    50     >>> process_macros('}}')
       
    51     Traceback (most recent call last):
       
    52       ...
       
    53     MacroEvaluationError: Mismatched parens in macro }}
       
    54     >>> process_macros('${{')
       
    55     Traceback (most recent call last):
       
    56       ...
       
    57     MacroEvaluationError: Mismatched parens in macro ${{
       
    58 
       
    59     Corner cases:
       
    60     >>> process_macros('')
       
    61     ''
       
    62     >>> process_macros(u'')
       
    63     u''
       
    64 
       
    65     Code execution:
       
    66     >>> process_macros("${{!print 'foo'}}")
       
    67     foo
       
    68     ''
       
    69     >>> process_macros("${{!  \\nfor k in range(4):\\n    write(str(k))}}")
       
    70     '0123'
       
    71 
       
    72     >>> process_macros('${{if 1\\nfoo\\n$else\\nbar}}')
       
    73     'foo'
       
    74     >>> process_macros('xxx${{if 1\\n${{foo}}\\n$else\\nbar}}yyy',{'foo':42})
       
    75     'xxx42yyy'
       
    76     >>> process_macros('${{!#}}')
       
    77     ''
       
    78     >>> process_macros('${{"${{foo}}"}}',{'foo':42})
       
    79     '42'
       
    80     >>> process_macros('${{"${{bar}}"}}',{'foo':42})
       
    81     Traceback (most recent call last):
       
    82       ...
       
    83     MacroEvaluationError: Error evaluating expression "${{bar}}": NameError: name 'bar' is not defined
       
    84     <BLANKLINE>
       
    85     """
       
    86     if namespace is None:
       
    87         namespace={}
       
    88 
       
    89     def process_text(text):
       
    90         #print "Processing text: "+repr(text)
       
    91         pos=0 # position of the first character of text that is not yet processed
       
    92         outbuf=[]
       
    93         macrobuf=[]
       
    94         while 1:
       
    95             m=macro_delimiter_re.search(text, pos)
       
    96             if m:
       
    97                 ##print "found delimiter: "+text[m.start():]
       
    98                 outbuf.append(text[pos:m.start()])
       
    99                 pos=m.start()
       
   100                 paren_level=0
       
   101                 for m in macro_delimiter_re.finditer(text,pos): # find a single whole macro expression
       
   102                     delim=m.group(1)
       
   103                     if delim=='${{':
       
   104                         paren_level+=1
       
   105                     elif delim=='}}':
       
   106                         paren_level-=1
       
   107                     else:
       
   108                         assert 0
       
   109                     if paren_level<0:
       
   110                         raise MacroEvaluationError("Mismatched parens in macro %s"%text[pos:m.end()])
       
   111                     if paren_level == 0: # macro expression finishes here, evaluate it.
       
   112                         outbuf.append(process_text(process_macro(text[pos:m.end()])))
       
   113                         pos=m.end()
       
   114                         break
       
   115                 if paren_level!=0:
       
   116                     raise MacroEvaluationError("Mismatched parens in macro %s"%text[pos:m.end()])
       
   117             else: # no more macros, just output the rest of the plain text
       
   118                 outbuf.append(text[pos:])
       
   119                 break
       
   120         result=''.join(outbuf)
       
   121         #print "Text after processing: "+repr(result)
       
   122         return result
       
   123     def process_macro(macro_expression):
       
   124         #print 'Processing macro: '+repr(macro_expression)
       
   125         try:
       
   126             outstr=StringIO()
       
   127             namespace['write']=outstr.write
       
   128             m=macro_exec_re.match(macro_expression) # ${{!code}} -- exec the code
       
   129             if m:
       
   130                 exec m.group(2) in namespace
       
   131             else:
       
   132                 m=macro_if_re.match(macro_expression) # ${{if -- if expression
       
   133                 if m:
       
   134                     outstr.write(handle_if(process_text(m.group(1)),namespace))
       
   135                 else:
       
   136                     m=macro_eval_re.match(macro_expression)
       
   137                     if m: # ${{code}}  -- eval the code
       
   138                         outstr.write(str(eval(m.group(1),namespace)))
       
   139                     else:
       
   140                         raise MacroEvaluationError, 'Invalid macro'
       
   141             #print 'Macro result: '+repr(outstr.getvalue())
       
   142             return outstr.getvalue()
       
   143         except:
       
   144             raise MacroEvaluationError, 'Error evaluating expression "%s": %s'%(
       
   145                 macro_expression,
       
   146                 '\n'.join(traceback.format_exception_only(sys.exc_info()[0],sys.exc_info()[1])))
       
   147     def if_tokenized(code):
       
   148         #print "code: "+repr(code)
       
   149         lines=code.split('\n')
       
   150         yield ('if',lines[0])
       
   151         #print 'lines: '+repr(lines)
       
   152         for line in lines[1:]:
       
   153             m=re.match('\$(elif|else)(.*)',line)
       
   154             if m:
       
   155                 yield (m.group(1),m.group(2))
       
   156             else:
       
   157                 #print "data line "+repr(line)
       
   158                 yield ('data',line)
       
   159         raise StopIteration
       
   160     def handle_if(code,namespace):
       
   161         outbuf=[]
       
   162         true_condition_found=0
       
   163         for token,content in if_tokenized(code):
       
   164             if token=='data' and true_condition_found:
       
   165                 outbuf.append(content)
       
   166             else:
       
   167                 if true_condition_found: # end of true block reached
       
   168                     break
       
   169                 if token=='if' or token=='elif':
       
   170                     true_condition_found=eval(content,namespace)
       
   171                 elif token=='else':
       
   172                     true_condition_found=1
       
   173         return '\n'.join(outbuf)
       
   174     return process_text(instr)
       
   175 
       
   176 def process_file(infilename,namespace):
       
   177     outfilename=outfilename_from_infilename(infilename)
       
   178     infile=open(infilename,'rt')
       
   179     outfile=open(outfilename,'wt')
       
   180     outfile.write(process_macros(infile.read(),namespace))
       
   181     outfile.close()
       
   182     infile.close()
       
   183 
       
   184 def templatefiles_in_tree(rootdir):
       
   185     files=[]
       
   186     for dirpath, dirnames, filenames in os.walk(rootdir):
       
   187         files+=[os.path.join(dirpath,x) for x in filenames if is_templatefile(x)]
       
   188     return files
       
   189 
       
   190 def misctest():
       
   191     files=templatefiles_in_tree('.')
       
   192     print "Templates in tree: "+str(files)
       
   193 
       
   194     for k in files:
       
   195         print "Processing file: "+k
       
   196         process_file(k,namespace)
       
   197     # import code
       
   198     # code.interact(None,None,locals())
       
   199 
       
   200 def _test():
       
   201     import doctest
       
   202     doctest.testmod()
       
   203 
       
   204 if __name__ == "__main__":
       
   205     _test()
       
   206 
       
   207