SimpleTemplate模板引擎開發


模板引擎相信大家是經常使用的,但是實現原理估計沒多少人知道(你要是說不就是replace嘛,那我也無話說了...)。


先來看看這個SimpleTemplate想實現的是什么功能吧:

  1. 是個C#端的模板引擎
  2. 模板中能放普通變量(i, j, index, username這種直接了當的變量名)
  3. 模板中能放復合變量(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/

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM