/* --------------------------------------------------------------- TemplateMaschine - an open source template engine for C# written by Stefan Sarstedt (http://www.stefansarstedt.com/) Released under GNU Lesser General Public License (LGPL), see file 'copying' for details about the license History: - initial release (version 0.5) on Oct 28th, 2004 - minor bugfixes (version 0.6) on March 29th, 2005 - updated to support referencing assemblies that are installed in the GAC (version 0.7) on January 12th, 2007 Thanks to William.Manning@ips-sendero.com - Added support for generic arguments. Ability to pass dictionary of arguments relaxing ordered object[] requirement. Version 0.8 on March 18th, 2007 Thanks to vijay.santhanam@gmail.com --------------------------------------------------------------- */ using System; using System.CodeDom.Compiler; using System.Collections; using System.Collections.Generic; using System.IO; using System.Reflection; using System.Text; using System.Text.RegularExpressions; using Microsoft.CSharp; namespace TemplateMaschine { /// /// Template Compiler Exception /// public class TemplateCompilerException : Exception { /// /// Template Compiler Exception /// /// Error message public TemplateCompilerException(string msg) : base(msg) { } } /// /// Template class, used to load and execute a template /// TODO: default values, precompilation? /// typed arguments i.e. Dictionary?? host assembly would need to reference the same assemblies /// TODO: publically exposed arguments for lookup? Argument from Assembly /// public class Template { private object generatorObject = null; /// /// Load an embedded template from assembly /// /// Name of template /// Assembly to load template from public Template(string embeddedTemplate, Assembly assembly) { this.ReadFileFromResources(embeddedTemplate, assembly); this.ProcessTemplate(embeddedTemplate, null); } /// /// Load an embedded template from assembly /// /// Name of template /// Assembly to load template from /// File to write generated template code to - for debugging purposes public Template(string embeddedTemplate, Assembly assembly, string fileNameForDebugging) { this.ReadFileFromResources(embeddedTemplate, assembly); this.ProcessTemplate(embeddedTemplate, fileNameForDebugging); } /// /// Load a template from file /// /// Template filename public Template(string templateFile) { this.ReadFile(templateFile); this.ProcessTemplate(templateFile, null); } /// /// Load a template from file /// /// Template filename /// File to write generated template code to - for debugging purposes public Template(string templateFile, string fileNameForDebugging) { this.ReadFile(templateFile); this.ProcessTemplate(templateFile, fileNameForDebugging); } private enum Token { LDirective, RTag, LTag, LAssignment, LScript, RScript, String, Backslash, Newline, QuotationMark, Eof } private struct TokenInfo { public Token token; public string s; public TokenInfo(Token token, string s) { this.token = token; this.s = s; } } private int linePos = 1, columnPos = 1; private StringBuilder resultString = new StringBuilder(50000); private StringBuilder resultStringScript = new StringBuilder(50000); private StringBuilder sourceStringOriginal = new StringBuilder(50000); private StringBuilder sourceString; private List assemblies = new List(); private List usings = new List(); private Dictionary arguments = new Dictionary(); // Key=name Value=type /* private struct Argument { public string name; public string type; public Argument(string name, string type) { this.name = name; this.type = type; } } */ private int IndexOfStopToken() { int i = 0; for(; i < sourceString.Length; i++) { if ((sourceString[i] == '<') || (sourceString[i] == '\\') || (sourceString[i] == '%') || (sourceString[i] == '\r') || (sourceString[i] == '"')) break; } //if (i == sourceString.Length) // throw new ApplicationException("IndexOfStopToken"); if (i == 0) return 1; return i; } private int IndexOf(char c) { int i = 0; for(; i < sourceString.Length; i++) { if (sourceString[i] == c) break; } return i; } private TokenInfo NextToken() { Token token = Token.Eof; string tokenVal = null; int pos = 0; if (sourceString.Length == 0) return new TokenInfo(Token.Eof, null); switch(sourceString[0]) { case '<': // Comment? if (sourceString.ToString(0,5).IndexOf("")+3; sourceString.Remove(0,pos); return NextToken(); } // LDirective, LTag or LAssignment? if (sourceString[1] == '%') { // LDirective if (sourceString[2] == '@') { pos += 3; token = Token.LDirective; break; } // LAssignment if (sourceString[2] == '=') { pos += 3; token = Token.LAssignment; break; } // LTag pos += 2; token = Token.LTag; break; } if (sourceString.ToString(0,7) == "')+1; token = Token.LScript; break; } if (sourceString.ToString(0,9) == "") { pos += 9; token = Token.RScript; break; } goto default; case '%': // RTag? if (sourceString[1] == '>') { pos += 2; token = Token.RTag; break; } goto default; case '"': token = Token.QuotationMark; tokenVal = "\""; pos++; break; case '\\': token = Token.Backslash; tokenVal = "\\"; pos++; break; case '\r': // Newline? if (sourceString[1] == '\n') { pos += 2; linePos++; token = Token.Newline; break; } goto default; default: token = Token.String; int stPos = IndexOfStopToken(); tokenVal = sourceString.ToString(pos,stPos); pos += stPos; break; } if (tokenVal == null) tokenVal = sourceString.ToString(0,pos); if (token == Token.Newline) columnPos = 1; else columnPos += pos; sourceString.Remove(0,pos); return new TokenInfo(token, tokenVal); } private void ParseTemplateAssignment() { TokenInfo tokenInfo; while((tokenInfo = NextToken()).token != Token.RTag) { if ((tokenInfo.token != Token.String) && (tokenInfo.token != Token.QuotationMark)) throw new ApplicationException("invalid syntax"); resultString.Append(tokenInfo.s); } } private void ParseTemplateTagBlock() { TokenInfo tokenInfo; while((tokenInfo = NextToken()).token != Token.RTag) { switch(tokenInfo.token) { case Token.LAssignment: resultString.Append("Response.Write("); ParseTemplateAssignment(); resultString.Append(");\r\n"); break; case Token.String: case Token.QuotationMark: case Token.Backslash: resultString.Append(tokenInfo.s); break; case Token.Newline: resultString.Append("\r\n"); break; default: throw new ApplicationException("invalid syntax"); } } } private void ParseTemplate(string templateFile) { bool isBlockOpen = false; TokenInfo tokenInfo, lastTokenInfo = new TokenInfo(Token.Eof,null); while((tokenInfo = NextToken()).token != Token.Eof) { switch(tokenInfo.token) { case Token.LDirective: // ignore directives string s = ""; while((tokenInfo = NextToken()).token != Token.RTag) s += tokenInfo.s; s = s.Trim(new char[] {' '}); if (s.StartsWith("Import")) { Regex directiveRegex = new Regex("[ ]*Import[ ]*NameSpace=\"(?[a-zA-Z0-9._]+)\"", RegexOptions.IgnoreCase); Match match = directiveRegex.Match(s); if (match == null) throw new ApplicationException("invalid syntax in 'Import' expression"); string namspaceToImport = match.Groups["namespace"].Value; usings.Add(namspaceToImport); } else if (s.StartsWith("Assembly")) { Regex directiveRegex = new Regex("[ ]*Assembly[ ]*Name=\"(?[a-zA-Z0-9._]+)\"", RegexOptions.IgnoreCase); Match match = directiveRegex.Match(s); if (match == null) throw new ApplicationException("invalid syntax in 'Assembly' expression"); string assemblyToImport = match.Groups["assemblyName"].Value; assemblies.Add(assemblyToImport); } else if (s.StartsWith("Argument")) { Regex directiveRegex = new Regex("[ ]*Argument[ ]+Name=\"(?[a-zA-Z0-9_]+)\"[ ]+Type=\"(?[\\<>a-zA-Z0-9.\\]\\[]+)\"", RegexOptions.IgnoreCase); Match match = directiveRegex.Match(s); Console.WriteLine(directiveRegex.Match("Argument=\"strings\" Type=\"List\"")); if (match == null) throw new ApplicationException("invalid syntax in 'Argument' expression"); string argumentName = match.Groups["name"].Value; string argumentType = match.Groups["type"].Value; arguments.Add(argumentName, argumentType); } else throw new ApplicationException("invalid syntax in directive"); break; case Token.LAssignment: if (isBlockOpen) { resultString.Append("\"+"); ParseTemplateAssignment(); resultString.Append("+\""); } else { resultString.Append("Response.Write("); ParseTemplateAssignment(); resultString.Append(");\r\n"); } break; case Token.LScript: while((tokenInfo = NextToken()).token != Token.RScript) resultStringScript.Append(tokenInfo.s); break; case Token.LTag: if (isBlockOpen) { int i; for(i=resultString.Length-1; i>=0; i--) { if ((resultString[i] != ' ') && (resultString[i] != '\t')) break; } if ((resultString[i] == '"') && (resultString[i-1] == '(')) resultString.Remove(i-15, resultString.Length-i+15); else resultString.Append("\");\r\n"); isBlockOpen = false; } ParseTemplateTagBlock(); break; case Token.String: if (!isBlockOpen) resultString.Append("Response.Write(\""); isBlockOpen = true; resultString.Append(tokenInfo.s); break; case Token.QuotationMark: if (!isBlockOpen) resultString.Append("Response.Write(\""); isBlockOpen = true; resultString.Append("\\\""); break; case Token.Backslash: resultString.Append("\\\\"); break; case Token.Newline: if (isBlockOpen) resultString.Append("\");Response.WriteLine();\r\n" + "#line " + linePos + " \"" + templateFile + "\"\r\n"); else { if ((lastTokenInfo.token == Token.Newline) || (lastTokenInfo.token == Token.String)) resultString.Append("Response.WriteLine();\r\n" + "#line " + linePos + " \"" + templateFile + "\"\r\n"); } isBlockOpen = false; break; default: break; } lastTokenInfo = tokenInfo; } } private void ReadFile(string templateFile) { if (!File.Exists(templateFile)) throw new FileNotFoundException("Template file '" + templateFile + "' could not be found."); using (StreamReader sr = new StreamReader(templateFile)) { sourceString = new StringBuilder(); // process include directives string s; while((s = sr.ReadLine()) != null) { if (s.IndexOf("Include") != -1) { Regex directiveRegex = new Regex("<%@[ ]+Include[ ]+File=\"(?[a-zA-Z0-9. -\\\\]+)\"[ ]*%>", RegexOptions.IgnoreCase); Match match = directiveRegex.Match(s); if (match == null) throw new ApplicationException("invalid syntax in 'Include' expression"); string fileName = match.Groups["fileName"].Value; using (StreamReader sr_includeFile = new StreamReader(fileName)) { sourceString.Insert(0, sr_includeFile.ReadToEnd()); } } else sourceString.Append(s + "\r\n"); } } sourceStringOriginal.Append(sourceString.ToString()); } private void ReadFileFromResources(string templateFile, Assembly assembly) { bool found = false; foreach (string s in assembly.GetManifestResourceNames()) { if (s.ToLower().EndsWith(templateFile.ToLower())) { templateFile = s; found = true; break; } } if (!found) throw new FileNotFoundException("Template '" + templateFile + "' could not be found as an embedded resource in assembly."); Stream fileStream = assembly.GetManifestResourceStream(templateFile); using (StreamReader sr = new StreamReader(fileStream)) { sourceString = new StringBuilder(); // process include directives string s; while((s = sr.ReadLine()) != null) { if (s.IndexOf("Include") != -1) { Regex directiveRegex = new Regex("<%@[ ]+Include[ ]+File=\"(?[a-zA-Z0-9. -\\\\]+)\"[ ]*%>", RegexOptions.IgnoreCase); Match match = directiveRegex.Match(s); if (match == null) { throw new ApplicationException("invalid syntax in 'Include' expression"); } string fileName = match.Groups["fileName"].Value; string[] resourceNames = assembly.GetManifestResourceNames(); string resourceName = Array.Find(resourceNames, delegate(string fn) { return fn.EndsWith(fileName); }); /*string resourceName = null; foreach (string resName in resourceNames) { if (resName.EndsWith(fileName)) { resourceName = resName; break; } }*/ if (resourceName == null) throw new FileNotFoundException("Included file '" + fileName + "' not found in resource."); Stream includefileStream = assembly.GetManifestResourceStream(resourceName); using (StreamReader sr_includeFile = new StreamReader(includefileStream)) { sourceString.Insert(0, sr_includeFile.ReadToEnd()); } } else sourceString.Append(s + "\r\n"); } } sourceStringOriginal.Append(sourceString.ToString()); } private string GetLine(StringBuilder resultString, int lineNo) { int lineCount = 1; int charPos = 0; string line = ""; while (true) { if (resultString[charPos] == '\n') { lineCount++; if (lineCount == lineNo) { int endPos = charPos; while (endPos < resultString.Length) { endPos++; if (resultString[endPos] == '\n') break; } line = resultString.ToString(charPos, endPos - charPos); } } charPos++; if (charPos >= resultString.Length) break; } return line; } private void ProcessTemplate(string templateFile, string fileNameForDebugging) { ParseTemplate(templateFile); int argNo = 0; foreach(KeyValuePair arg in arguments) { resultString.Insert(0, "this." + arg.Key + " = (" + arg.Value + ")args[" + argNo + "];\r\n"); argNo++; } resultString.Insert(0, "try {\r\n"); resultString.Insert(0, "public StringBuilder Generate(object[] args) {\r\n"); resultString.Insert(0, "public void SetOutput(string outputFile, bool outputToString) { Response.Init(outputFile,outputToString); }\r\n"); resultString.Insert(0, "public TempGeneratorClass() {}\r\n"); foreach(KeyValuePair argument in arguments) { resultString.Insert(0, "private " + argument.Value + " " + argument.Key + ";\r\n"); } resultString.Insert(0, resultStringScript); resultString.Insert(0, "class TempGeneratorClass {\r\n"); resultString.Insert(0, "}\r\n"); resultString.Insert(0, "public static void Init(string outputFile,bool outputToString) { if (outputFile != null) outputStream = new StreamWriter(outputFile); if (outputToString) outputString = new StringBuilder();}\r\n"); resultString.Insert(0, "public static void Cleanup() {if (outputStream != null) outputStream.Close();}\r\n"); resultString.Insert(0, "public static void Write(string s) {if (outputStream != null) outputStream.Write(s); if (outputString != null) outputString.Append(s);} public static void WriteLine(string s) {if (outputStream != null) outputStream.WriteLine(s); if (outputString != null) {outputString.Append(s);outputString.Append(\"\\r\\n\");}} public static void WriteLine() {if (outputStream != null) outputStream.WriteLine(); if (outputString != null) outputString.Append(\"\\r\\n\");}\r\n"); resultString.Insert(0, "public static void Flush() {if (outputStream != null) outputStream.Flush();}\r\n"); resultString.Insert(0, "public static StringBuilder Result {get { return outputString; }}\r\n"); resultString.Insert(0, "private static StreamWriter outputStream = null;\r\n"); resultString.Insert(0, "private static StringBuilder outputString = null;\r\n"); resultString.Insert(0, "internal class Response {\r\n"); resultString.Insert(0, "namespace TempGenerator {\r\n"); foreach(string usingExpr in usings) { resultString.Insert(0, "using " + usingExpr + ";\r\n"); } resultString.Insert(0, "using System.Diagnostics;\r\n"); resultString.Insert(0, "using System.Text;\r\n"); resultString.Insert(0, "using System.IO;\r\n"); resultString.Insert(0, "using System;\r\n"); resultString.Append("} catch(Exception ex) { Response.Flush(); throw ex; };\r\n"); resultString.Append("Response.Cleanup();\r\n"); resultString.Append("return Response.Result;\r\n"); resultString.Append("} } }\r\n"); if (!String.IsNullOrEmpty(fileNameForDebugging)) { StreamWriter debugFile = new StreamWriter(fileNameForDebugging); debugFile.Write(resultString.ToString()); debugFile.Close(); } // compile assembly in memory with a yet unknown name CSharpCodeProvider codeProvider = new CSharpCodeProvider(); System.CodeDom.Compiler.CompilerParameters parameters = new CompilerParameters(); parameters.GenerateExecutable = false; parameters.GenerateInMemory = true; //parameters.OutputAssembly = "Generator.dll"; parameters.ReferencedAssemblies.Add( "System.dll" ); foreach(string assembly in assemblies) { parameters.ReferencedAssemblies.Add(this.ResolveAssemblyPath(assembly)); } CompilerResults results = codeProvider.CompileAssemblyFromSource(parameters, resultString.ToString()); if (results.Errors.Count > 0) { generatorObject = null; string errs = ""; foreach(CompilerError CompErr in results.Errors) { errs += "Template: " + CompErr.FileName + Environment.NewLine + "Line number: " + CompErr.Line + Environment.NewLine + "Error: " + CompErr.ErrorNumber + " '" + CompErr.ErrorText + "'"; string line = GetLine(sourceStringOriginal, CompErr.Line); throw new TemplateCompilerException("Error compiling template: " + Environment.NewLine + errs + Environment.NewLine + "Line: '" + line + "'"); } } else { Assembly generatorAssembly = results.CompiledAssembly; generatorObject = generatorAssembly.CreateInstance("TempGenerator.TempGeneratorClass", false, System.Reflection.BindingFlags.CreateInstance, null, null, null, null); } } private string ResolveAssemblyPath(string name) { name = name.ToLower(); foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) { if (this.IsDynamicAssembly(assembly)) { continue; } if (Path.GetFileNameWithoutExtension(assembly.Location).ToLower().Equals(name)) { return assembly.Location; } } return Path.GetFullPath(name+".dll"); } private bool IsDynamicAssembly(Assembly assembly) { return assembly.ManifestModule.Name.StartsWith("<"); } /// /// Creates an ordered argument array from a named Dictionary collection /// /// Dictionary of Template Arguments keyed by argument name /// Array of objects for arguments(in order as they are defined in template) private object[] CreateOrderedArgumentArray(Dictionary args) { object[] newargs = new object[arguments.Count]; int n=0; foreach (KeyValuePair arg in arguments) { if (args.ContainsKey(arg.Key)) { newargs[n] = args[arg.Key]; } else { throw new ArgumentException("Template Argument " + arg.Key + " was not specified"); } n++; } return newargs; } /// /// Processes a template /// /// Dictionary of Template Arguments keyed by argument name /// Result of processed template public string Generate(Dictionary args) { return Generate(CreateOrderedArgumentArray(args), null, true); } /// /// Processes a template /// /// Template Arguments (in order as they are defined in template) /// File to write results to /// Flag if output should be written to a string /// Result of processed template, if outputToString is set to true public string Generate(object[] args, string outputFile, bool outputToString) { if (generatorObject == null) throw new ApplicationException("please load a template first"); try { generatorObject.GetType().InvokeMember("SetOutput", BindingFlags.Instance | BindingFlags.Public | BindingFlags.InvokeMethod, null, generatorObject, new object[] {outputFile,outputToString}); object str = generatorObject.GetType().InvokeMember("Generate", BindingFlags.Instance | BindingFlags.Public | BindingFlags.InvokeMethod, null, generatorObject, new object[] {args}); if (str == null) return null; return ((StringBuilder)str).ToString(); } catch(Exception ex) { // if (ex is TargetInvocationException) // Console.WriteLine(ex.InnerException.ToString()); // else if (ex is TargetInvocationException) throw ex.InnerException; throw ex; } } /// /// Processes a template /// /// Template Arguments (in order as they are defined in template) /// File to write results to public void Generate(object[] args, string outputFile) { Generate(args, outputFile, false); } /// /// Processes a template /// /// Template Arguments (in order as they are defined in template) /// Result of processed template public string Generate(object[] args) { return Generate(args, null, true); } } }