由浅入深:自己动手开发模板引擎——解释型模板引擎(二)
受到群里兄弟们的竭力邀请,老陈终于决定来分享一下.NET下的模板引擎开发技术。本系列文章将会带您由浅入深的全面认识模板引擎的概念、设计、分析和实战应用,一步一步的带您开发出完全属于自己的模板引擎。关于模板引擎的概念,我去年在百度百科上录入了自己的解释(请参考:模板引擎)。老陈曾经自己开发了一套网鸟Asp.Net模板引擎,虽然我自己并不乐意去推广它,但这已经无法阻挡群友的喜爱了!
概述
本课我们主要讨论“命令解释器”的实现。命令就是指令,指令也是构成更加复杂的模板引擎的基本元素之一。至此我们可以归纳出来,模板引擎在工作的过程中,首先将字符流转换为Token流,然后再将Token流转换为Element集合(也算是流),然后将特定的Element单独拿出来或组合在一起形成指令、语句等。写一个模板引擎,和写一个小型的编译器几乎相当,因此我们需要耐心、细心!
目标
解析并运行如下模板代码结构:
- /_Page_Footer.shtml
- /_Page_Header.shtml
- /_Public_Footer.shtml
- /_Public_Header.shtml
- /Index.shtml
文件"/_Page_Footer.shtml"包含的代码:
<!--#include file="_Public_Footer.shtml" -->
文件"/_Page_Header.shtml"包含的代码:
<!--#include file="_Public_Header.shtml" -->
文件"/_Public_Footer.shtml"包含的代码:
</body>
</html>
文件"/_Public_Header.shtml"包含的代码:
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>{UserName}的博客</title>
</head>
<body>
文件"/Index.shtml"包含的代码:
1 <!--#include file="_Page_Header.shtml" -->
2 <ul>
3 <li>博主姓名:{UserName}</li>
4 <li>创建日期:{CreationTime:yyyy年MM月dd日 HH:mm:ss}</li>
5 <li>粉丝数量:{FunsCount:D4}</li>
6 </ul>
7 <!--#include file="_Page_Footer.shtml" -->
今天的模板内容被切分成了5个部分,嵌套层次达到了3层,解析难度比较大。实际上在编写本文之前,我自己的解释型模板引擎在内部是使用正则表达式的方式来实现嵌套指令解析的。不过,我们今天不会这么做了!
使用正则表达式实现
本节课的目的是说明命令解释器的实现,本着循序渐进的原则,我们首先考虑使用正则表达式来实现“<!--#include file="_Page_Header.shtml" -->”命令,在以后的课程中我们将会学习更加复杂的代码解析方法。
我们需要按照顺序使用正则表达式递归的读取和合并代码文档,具体实现如下:
1 /// <summary> 2 /// 表示 #Include 命令解释器。 3 /// </summary> 4 public static class IncludeCommandParser 5 { 6 private static int _nestedCount; 7 8 /// <summary> 9 /// 处置包含文档。 10 /// </summary> 11 /// <param name="templateString">包含模板代码的字符串。</param> 12 /// <param name="basePath">处置包含命令时要使用的基准路径。</param> 13 /// <returns>返回 <see cref="string"/>。</returns> 14 public static string Parse(string templateString, string basePath) 15 { 16 if (String.IsNullOrWhiteSpace(templateString)) return String.Empty; 17 if (String.IsNullOrWhiteSpace(basePath)) return templateString; 18 19 if (Directory.Exists(basePath) == false) throw new DirectoryNotFoundException(); 20 21 return _ProcessSSIElement(templateString, basePath); 22 } 23 24 private static string _ProcessSSIElement(string templateString, string basePath) 25 { 26 if (_nestedCount > 10) return templateString; 27 28 var matches = Regex.Matches(templateString, @"<!--#include file=""([^""]+)""\s*-->", RegexOptions.IgnoreCase | RegexOptions.Singleline); 29 30 foreach (Match match in matches) 31 { 32 var file = new FileInfo(Path.Combine(basePath, match.Groups[1].Value.Replace('/', '\\'))); 33 34 if (file.Exists == false) continue; 35 36 var subTemplate = File.ReadAllText(file.FullName).Trim(); 37 38 subTemplate = _ProcessSSIElement(subTemplate, Path.GetDirectoryName(file.FullName)); 39 40 templateString = templateString.Replace(match.Groups[0].Value, subTemplate); 41 } 42 43 _nestedCount++; 44 45 return templateString; 46 } 47 }
测试代码:
1 [Test] 2 public void LoadFileTest() 3 { 4 var fileName = Path.Combine(Environment.CurrentDirectory, "Templates\\Index.shtml"); 5 6 Assert.AreEqual(File.Exists(fileName), true); 7 8 this._templateString = File.ReadAllText(fileName); 9 10 Assert.NotNull(this._templateString); 11 12 Trace.WriteLine(this._templateString); 13 14 Assert.Greater(this._templateString.IndexOf("{CreationTime:yyyy年MM月dd日 HH:mm:ss}", StringComparison.Ordinal), 0); 15 } 16 17 [Test] 18 public void ProcessTest() 19 { 20 this.LoadFileTest(); 21 22 Trace.WriteLine("本次输出:"); 23 24 var basePath = Path.Combine(Environment.CurrentDirectory, "Templates"); 25 var templateEngine = TemplateEngine.FromString(this._templateString, basePath); 26 27 templateEngine.SetVariable("url", "http://www.ymind.net/"); 28 templateEngine.SetVariable("UserName", "陈彦铭"); 29 templateEngine.SetVariable("title", "陈彦铭的博客"); 30 templateEngine.SetVariable("FunsCount", 98); 31 templateEngine.SetVariable("CreationTime", new DateTime(2012, 4, 3, 16, 30, 24)); 32 33 var html = templateEngine.Process(); 34 Trace.WriteLine(html); 35 }
运行结果:
1 <!--#include file="_Page_Header.shtml" --> 2 <ul> 3 <li>博主姓名:{UserName}</li> 4 <li>创建日期:{CreationTime:yyyy年MM月dd日 HH:mm:ss}</li> 5 <li>粉丝数量:{FunsCount:D4}个</li> 6 </ul> 7 <!--#include file="_Page_Footer.shtml" --> 8 9 本次输出: 10 <!DOCTYPE HTML> 11 <html> 12 <head> 13 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> 14 <title>陈彦铭的博客</title> 15 </head> 16 <body> 17 <ul> 18 <li>博主姓名:陈彦铭</li> 19 <li>创建日期:2012年04月03日 16:30:24</li> 20 <li>粉丝数量:0098个</li> 21 </ul> 22 </body> 23 </html>
运行结果达到了我们的期望值!
总结和代码下载
本课只是简单的介绍命令解释器的实现思路,实际上还有其他很多办法可以实现。
从下节课开始,我们将会接触到更多代码标记的解析方式,每篇博文篇幅不会太长,但一定会挑重点、击中要害!
本节课的内容较为简单,不提供代码下载。
模板引擎系列教程的规模比原来预想的还要庞大,因为我不想仅仅帖出各种代码就了事,希望能从更多的角度给大家分享。因此,该系列文章以后全部划入周末写作。平时只写文字性内容,或小篇幅技术文章。
希望大家能够谅解!