模板引擎相信大家是經常使用的,但是實現原理估計沒多少人知道(你要是說不就是replace嘛,那我也無話說了...)。
先來看看這個SimpleTemplate想實現的是什么功能吧:
- 是個C#端的模板引擎
- 模板中能放普通變量(i, j, index, username這種直接了當的變量名)
- 模板中能放復合變量(user.FirstName, user.LastName這種有對象前綴的變量)
最終客戶端代碼通過下面的方式進行調用:
static void Main(string[] args) { string template = @" your name: @{name} your age: @{age} "; Dictionary<string, object> ctx = new Dictionary<string, object>(); ctx["name"] = "McKay"; ctx["age"] = "你猜"; Console.WriteLine(STParser.GenerateStringView(template, ctx)); Console.ReadKey(); }
大家看出來了,重點就在@{xxxx}上
大家也先別噴我,說用正則、replace就搞定了,看后面先,小心噴了后悔。
我選擇用antlr來做這個模板引擎,因為雖然現在看上去這個模板引擎很簡單,但是不代表以后不擴展啊,以后還要加入if/else/for這種通用編程語法的,所以為了擴展性,就用語法解析器了。
知道yacc/flex的也可以去看看,只不過沒有C#插件,而antlr正好有這插件,就用了。
下面我們先來寫文法規則:
parse :expression* ; expression : stringtext | simple_variable | complex_variable ;
parse
規則代表開始規則,這個名稱可以自己起名,只要是小寫就行。
內容只有一行,expression*,代表0個或者無限個expression規則
expression
看清,第一個是冒號“:”,后續的是或者號“|”,最后是封號";"
代表expression可以是三種規則中的一種:stringtext、simple_variable、complex_variable,代表普通的字符串文本、簡單變量、復合變量規則
先來看看普通文本字符串的定義
stringtext : placeholderChar (placeholderChar)* | newlines ; placeholderChar : CHAR | ':' | SPACE | NUMBER | DOT | '\'' | '"' | '<' | '>' | '_' | '+' | '-' | '*' | '/' ; newlines :NEWLINE NEWLINE* ; NEWLINE:'\r'? '\n'; NUMBER: '0'..'9'; CHAR: 'a'..'z'|'A'..'Z'; SPACE:' '; DOT:'.';
stringtext
二選一的規則
第一行代表至少一個占位字符的字符串(后面用了*號,就代表字符數不限)
newlines,看后面的定義也是用了*號,代表一個回車,或者多個回車的規則匹配
占位符,大家看placeholderChar規則,就知道允許的占位符是哪些字符了
注意:大寫的規則其實不是規則,而是token
再來看看簡單變量規則的定義
simple_variable :V_START simple_variable_inner V_END ; simple_variable_inner :identity ; identity :(UNDERLINE|CHAR) (UNDERLINE|CHAR|NUMBER)* ; V_START:'@{'; V_END:'}'; NUMBER: '0'..'9'; CHAR: 'a'..'z'|'A'..'Z'; UNDERLINE: '_';
simple_variable
定義了一個V_START的TOKEN為開頭,也定義了必須以V_END為結尾,字符分別是 @{和},呵呵,中間就是那個變量名了
這個變量名其實就是identity規則的定義,是說第一個字符必須以下划線或英文字母開頭,后續字符可有可無,有的話必須是下划線、英文字母、數字
再看看復合變量的規則
complex_variable :V_START complex_variable_inner V_END ; complex_variable_inner :identity DOT identity ; identity :(UNDERLINE|CHAR) (UNDERLINE|CHAR|NUMBER)* ; DOT:'.';
說說這里的complex_variable_inner規則
由於是要匹配obj.property格式,因此用了個點號DOT,obj和property的規則匹配其實就是identity的規則匹配
我們看看上面規則的效果,antlr解析樹:
還是比較帥的
下面的問題是,怎么運用到C#項目中了
怎么運用到C#項目中
首先,新建一個項目,然后在NuGet中搜索"antlr" ,找到antlr4,然后安裝
然后新建一個任意文件,新建后重命名為g4文件,比如SimpleTemplate.g4,接着還要設置下這個g4文件的生成方式,如下圖
這樣,當我們生成時,antlr就會根據g4文件的規則定義生成對應的C#代碼了。
然后再說說g4文件的內容是怎么拷貝過來(原先的解析樹是在eclipse中才能看的,所以原先的g4定義都在那邊做的)
首先,上方的grammar xxxxx;這里的xxxxx必須要和文件名稱一致。
其次,compileUnit后面的那個規則,必須存在,代表默認規則
再其次,如果編譯時總是報錯(但是eclipse中是正常的),這時要修改下vs環境下的g4文件的編碼,如下:
還得把eclipse中的g4文件內容拷貝到新的g4文件中,別忘了。
接下來就要進入C#編碼層面了,呵呵,是不是有點不耐煩了,`(*∩_∩*)′
掛鈎函數就那么一個,很簡單,基本就是拷貝:
public static class STParser { public static string GenerateStringView(string template, Dictionary<string, object> variables) { Antlr4.Runtime.AntlrInputStream input = new Antlr4.Runtime.AntlrInputStream(template); TemplateLexer lexer = new TemplateLexer(input); Antlr4.Runtime.UnbufferedTokenStream tokens = new Antlr4.Runtime.UnbufferedTokenStream(lexer); TemplateParser parser = new TemplateParser(tokens); var tree = parser.parse(); SimpleTemplateVisitor visitor = new SimpleTemplateVisitor(variables); string result=visitor.Visit(tree); return result; } }
template是傳進來的模板文本
variables是傳進來的變量集合
這段代碼中都是antlr引擎自動生成的類,除了SimpleTemplateVisitor是自定義的(不然咋替換字符串啊)
來看看這個類吧,里面都是VisitXXXX規則的函數重載,需要的自定義邏輯都在里面改寫
class SimpleTemplateVisitor:g4.TemplateBaseVisitor<string> { private Dictionary<string, object> ctx; public SimpleTemplateVisitor(Dictionary<string, object> ctx) { this.ctx = ctx; } public override string VisitParse(g4.TemplateParser.ParseContext context) { StringBuilder sb = new StringBuilder(); foreach(var exp in context.expression()) sb.Append(VisitExpression(exp)); return sb.ToString(); } public override string VisitNewlines(g4.TemplateParser.NewlinesContext context) { return context.GetText(); } public override string VisitStringtext(g4.TemplateParser.StringtextContext context) { return context.GetText(); } public override string VisitSimple_variable(g4.TemplateParser.Simple_variableContext context) { return VisitSimple_variable_inner(context.simple_variable_inner()); } public override string VisitComplex_variable(g4.TemplateParser.Complex_variableContext context) { return VisitComplex_variable_inner(context.complex_variable_inner()); } public override string VisitSimple_variable_inner(g4.TemplateParser.Simple_variable_innerContext context) { string var_name = context.identity().GetText(); if (!ctx.ContainsKey(var_name)) throw new NullReferenceException(var_name); return Convert.ToString(ctx[var_name]); } public override string VisitComplex_variable_inner(g4.TemplateParser.Complex_variable_innerContext context) { string var_name = context.identity()[0].GetText(); if (!ctx.ContainsKey(var_name)) throw new NullReferenceException(var_name); string propertyName = context.identity()[1].GetText(); object obj = ctx[var_name]; Type t = obj.GetType(); PropertyInfo propertyInfo = t.GetProperty(propertyName); var value = propertyInfo.GetValue(obj, null); string string_value = Convert.ToString(value); return string_value; } }
構造函數中傳入的ctx是我們要替換的變量集合
光看這些函數是會暈的,你得結合eclipse中的解析樹層次圖來同時看,要清楚的知道上下關系,然后再套上面這個visit類才能看懂,呵呵,慢慢折騰看吧。
此處等待數周。。。
上面這個只是替換變量的沒意思,我們再做個有循環的,比如:
your name: @{user.name} your age: @{user.age} 1 2 3 @{repeat 5} testing @{end repeat} --------------------- @{repeat count} testing @{end repeat}
看,支持了循環repeat語法
repeat后面可以支持固定的數字,也可以支持簡單變量,也可以支持復合變量,大家應該能在腦子里畫出規則形狀來吧。
有興趣深入的同學可以自己試下實現if/else語法。
代碼已經上傳到github上了,url: https://github.com/daibinhua888/SimpleTemplate/