65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 中的文本模板转换引擎/代码生成工具

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (7投票s)

2015年1月19日

MIT

10分钟阅读

viewsIcon

37803

downloadIcon

562

一个简单的嵌入式函数,提供类似于 T4 的基于模板的文本生成。

引言

此嵌入式函数提供了一个简单的基于模板的文本生成引擎。它基本上允许代码封装在特殊标签中,这些标签可以操纵或插入文本。如果您使用过经典的 ASP、JSP、PHP 或 T4 模板,您可能对文本模板转换很熟悉。例如,在经典的 ASP 中,代码包装在 <%...%> 中。

<body>Current time:<br /><%Response.Write Now()%></body>

在 T4 中,它被包装成这样:

Current time:<#= DateTime.Now #>

最后,在这个项目中,我们这样做:

Current time: [[=DateTime.Now ]]

Current time: /*=DateTime.Now :*/

基本上,文本转换,也称为动态文本,允许使用编程方法修改文本。常见用途可能包括重复文本部分、填充 ASP 页面上的字段、在电子邮件中显示用户名或帐户代码,或者在网页上写入 1 到 1000。

这个项目类似于微软的 T4,但更简单。它类似是因为它使用封装的 C# 代码来注入文本。Microsoft Visual Studio 的 T4 更强大,这个项目并不打算取代它……至少在 Visual Studio 内部不是!在第三方应用程序中使用 T4 模板时存在一些问题。最重要的是许可。T4 DLL 是不可再分发的。有一些古怪的方法可以解决这个问题,例如安装一些包含 DLL 的 MS 包或安装 Visual Studio Express,但这很麻烦。相比之下,这个项目甚至不是一个 DLL,它是一个简单的嵌入式函数。另一个问题是 T4 与许多语法高亮和代码完成项目不兼容。为了解决这个问题,我在注释中创建了 \*: code-here :*\ 这样的命令。这允许此模板系统直接在 C#/C++ 文件中使用,而不会在设计时错误检查中造成混乱。

以下是一些转换示例。中间步骤只显示为创建最终输出而执行的内容。这些示例使用 ]],[[,[[=,[[! 来封装代码。

Original 中间步骤 最终输出
1[[for(int i=0; i<9; i++){]]0[[}]] Write(“1”); for(int i=0; i<9; i++){ Write(“0”);} 1000000000
1[[~for(int i=0; i<9; i++)]]0 Write(“1”); for(int i=0; i<9; i++) Write(“0”); 1000000000
Printed [[=DateTime.Now]] Write(“Printed ”); Write(DateTime.Now); Printed 1/4/15 2:36PM
[[=i++]]. A [[=i++]]. B Write(i++);Write(“. A”);Write(i++); Write(“. B”); 1. A 2. B

 

运行包含的源文件的示例:(上半部分是函数的输入,下半部分是函数的输出……就这么简单。)

背景

这个函数是由于我正在研究的 AMD GCN 汇编语言项目需要一个简单的文本模板转换引擎而构建的。在汇编语言中,预编译、宏之类的功能非常有用——几乎是必需的。通常,您可能会遇到需要展开循环循环的情况。由于纯汇编语言不支持展开像高级语言那样的“for”或“while”循环,因此通常由程序员来完成这些工作。处理和维护几行难看的模板代码比编写五十次十条汇编语句要好得多。

例如

[[ for(int i = 0; i < 4; i++){ ]]
   Add R[[=i+20], R4, [[=i]]; [[}]]

将被转换为...

   Add R20, R4, 0;
   Add R21, R4, 1;
   Add R22, R4, 2;
   Add R23, R4, 3;

最初,我打算使用微软的 T4,但在调查后,我发现它需要一个不可再分发的 DLL。创建一个文本转换模板引擎似乎很简单也很有趣,所以我着手了。目标是尽可能保持简单,因为我将来可能想将其用于不同的用途,如果有很多垃圾,那么调整起来会很困难。

Using the Code

只需将函数或 static 类放入,然后调用该函数。

  1. 首先,将函数复制到您的应用程序中。根据需要将函数设置为 publicprivateinternal
  2. 通过取消注释标题中的样式来选择要使用的格式。有两种格式
    1. [[CODE]][[=EXPRESSION]][[~FULL_LINE_OF_CODE[[!SKIP_ME]] - 更易读(推荐)
    2. /*:CODE:*//*=EXPRESSION:*///:FULL_LINE_OF_CODE //!SKIP_ME - 更适用于类似 C 语言的代码完成和语法高亮

    3. 或者,创建您自己的
  3. 构建一些需要转换的文本(作为 string)。参考下表
      “[[..]]” 样式 “/*:..:*/” 样式 注释
    代码块 [[ code_here ]] /*: code_here :*/ 正常用法
    代码行 [[~code_here //: code_here 以换行符终止
    表达式 [[=variable]] /*= variable:*/ 将变量包装在 write(...) 中
    注释块 [[! comments ]] /*! comments :*/ 在最终输出中排除
    注释行 (无) //! comments 以换行符结束
    仅限 IDE 代码 (无) /**/ IDE code /**/ 虚拟/填充 IDE 专用代码
  4. 在您的应用程序中调用 Expand(...)。它接受两个 string 参数。第一个 string 参数应包含带有封装的 C# 命令的输入文本。第二个 string 参数将保存结果。最后,如果成功,Expand() 返回 true,如果存在任何编译器错误,则返回 false
    用法:bool success = Expand(myInput, out myOutput);

  5. 调试:编译时错误将作为输出参数返回(而不是结果)。函数将列出每个错误以及行和列信息。在错误之后,将显示中间代码以供参考。如果您想进一步纠正运行时错误,只需获取 program 变量的内容并将其复制粘贴到新的 Visual Studio 控制台项目中。包含了一个 Main(),因此内容可以简单地放入文件并运行。

工作原理

简而言之,Expand() 函数接受一个 string,将该 string 转换为一个程序(步骤 1),编译该程序(步骤 2),最后运行该程序以收集其输出(步骤 3)。

这是完整的代码

public static bool Expand(string input, out string output)
{
    //////////////// Step 1 - Build the generator program ////////////////
    // For [[CODE]] , [[=EXPRESSION]] ,  [[~FULL_LINE_OF_CODE  &  [[!SKIP_ME]]
    // style uncomment the next 5 lines of code
    const string REG = @"(?<txt>.*?)" +      // grab any normal text
        @"(?<type>\[\[[!~=]?)" +             // get type of code block
        @"(?<code>.*?)" +                    // get the code or expression
        @"(\]\]|(?<=\[\[~[^\r\n]*?)\r\n)";   // terminate the code or expression
    const string NORM = @"[[", FULL = @"[[~", EXPR = @"[[=", TAIL = @"]]";

    //// For /*:CODE:*/ , /*=EXPRESSION:*/ , //:FULL_LINE_OF_CODE & //!SKIP_ME
    //// style uncomment the next 5 lines of code
    //const string REG ="(?<txt>.*?)" +          // grab any normal text
    //    @"((/\*\*/.*?/\*\*/)|(?<type>/(/!|\*:|\*=|/:|\*!))" + // get code 
    //    @"(?<code>.*?)" +                      // get the code or expression
    //    @"(:\*/|(?<=//[:|!][^\r\n]*)\r\n))";   // terminate the code or expression
    //const string NORM = @"/*:", FULL = @"//:", EXPR = @"/*=", TAIL = @":*/";               

    System.Text.StringBuilder prog = new System.Text.StringBuilder();
    prog.AppendLine(
@"using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
    class T44Class { 
    static StringBuilder sb = new StringBuilder();
    public string Execute() {");
    foreach (System.Text.RegularExpressions.Match m in
        System.Text.RegularExpressions.Regex.Matches(input + NORM + TAIL, REG,
        System.Text.RegularExpressions.RegexOptions.Singleline))
    {
        prog.Append(" Write(@\"" + m.Groups["txt"].Value.Replace("\"", "\"\"") + "\");");
        string txt = m.Groups["code"].Value;
        switch (m.Groups["type"].Value)
        {
            case NORM: prog.Append(txt); break;  // text to be added
            case FULL: prog.AppendLine(txt); break;
            case EXPR: prog.Append(" sb.Append(" + txt + ");"); break;
        }
            }
            prog.AppendLine(
@"  return sb.ToString();}
static void Write<T>(T val) { sb.Append(val);}
static void Format(string format, params object[] args) { sb.AppendFormat(format,args);}
static void WriteLine(string val) { sb.AppendLine(val);}
static void WriteLine() { sb.AppendLine();} 
static void main() { Console.Write(sb.ToString());} }");
    string program = prog.ToString(); 

    //////////////// Step 2 - Compile the generator program ////////////////
    var res = (new Microsoft.CSharp.CSharpCodeProvider()).CompileAssemblyFromSource(
        new System.CodeDom.Compiler.CompilerParameters()
        {
            GenerateInMemory = true, // note: this is not "in memory"
            ReferencedAssemblies = { "System.dll", "System.Core.dll" } // for linq
        }
        , program);

    res.TempFiles.KeepFiles = false; //clean up files in temp folder

    // Print any errors with the source code and line numbers
    if (res.Errors.HasErrors)
    {
        int cnt = 1;
        output = "There is one or more errors in the template code:\r\n";
        foreach (System.CodeDom.Compiler.CompilerError err in res.Errors)
            output += "[Line " + err.Line + " Col " + err.Column + "] " + 
                        err.ErrorText + "\r\n";
        output += "\r\n================== Source (for debugging) =====================\r\n";
        output += "     0         10        20        30        40        50        60\r\n";
        output += "   1| " + System.Text.RegularExpressions.Regex.Replace(program, "\r\n",
            m => { cnt++; return "\r\n" + cnt.ToString().PadLeft(4) + "| "; });
        return false;
    }

    //////////////// Step 3 - Run the program to collect the output ////////////////
    var type = res.CompiledAssembly.GetType("T44Class");
    var obj = System.Activator.CreateInstance(type);
    output = (string)type.GetMethod("Execute").Invoke(obj, new object[] { });
    return true;
}

步骤 1) 构建生成器程序 - 带有嵌入式 C# 命令的输入文本通过正则表达式解析出不同的部分。输入文本本质上将采用 TEXT-CODE-TEXT-CODE… 的格式,因此我们一次处理一个 TEXT-CODE。这是用于解析每个 TEXT-CODE 的正则表达式

  • (?<txt>.*?) <- 这会捕获将直接用 Write(“text here”) 输出的任何普通文本。
  • (?<type>\[\[!|\[\[\~|\[\[|\[=) <- 这获取开始括号及其类型。它可以是 [[ , [= [[!
  • (?<code>.*?) <- 这捕获代码片段。
  • (\]\]|(?<=\[\[[^\r\n]*?)\r\n) <- 这捕获结束括号。

目标是将源文本转换为程序,以便我们可以执行它。对于每个 <txt>,我们附加一个 sb.Append(txt),其中 sb 是一个 StringBuilder。对于每个 <code>,我们直接写入文本——它不被包装在 sb.Append() 中。开始和结束括号以及任何以“[[!”开头的内容都被删除,不予复制。

在第一步中,还会添加程序头和程序尾。在程序头中,我们添加一些 using 语句、一个类头和一个函数头。在程序尾中,我们添加一些有用的函数,如“Write(...)”和“WriteLine(...)”,最后用“}”完成该类。

需要注意的另一件事是,在运行 RegEx 之前,会在末尾追加一个“[[]]”。(text + NORM + TAIL)。这是因为 RegEx 正在查找 TEXT-CODE 块,这意味着我们必须以 CODE 结尾。在这种情况下,它只是一个空的“[[]]”代码。

步骤 2) 编译生成器程序 - 我们在步骤 1 中构建的程序,然后使用 .NET CSharpCodeProvider 进行编译。GenerateInMemory 不会将文件保存到 RAM 中,而是保存到临时文件夹中。必须设置 TempFiles.KeepFiles = false 以确保这些文件被清理。此外,在此步骤中,我们还会打印出任何错误。

步骤 3) 运行程序以收集输出 - 在最后一步中,我们调用生成的迷你程序并返回其输出。

输入/输出示例

示例输入

第一个例子将三次打印 Hello World

[[~ for(int i=0; i<3; i++){
Hello World [[ Write(i.ToString()+"! "); }]]

[[! 此注释不会添加到输出中。 ]]

Write() 将打印任何 bool, string, char, decimal, double, float, int...
一千兆(Quadrillion)是 1[[ for(int i=0; i<15; i++) Write("0"); ]]

这也将写入 bool, string, char, decimal, double, float, int...
Hello at [[=DateTime.Now]]!

[[ for(int i=1; i<4; i++){ ]]
[[="Hello " + i + " World"+ (i>1?"s!":"!") ]]
How are you? "[[=i]]" [[="\r\n"]]
[[ } ]]

中间生成代码

以下是从样本输入创建的幕后临时生成代码。这将在下一步执行以创建最终输出。下面的块是生成代码,格式不整洁。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
  class T44Class { 
  static StringBuilder sb = new StringBuilder();
  public static string Execute() {
 Write(@"
"); for(int i=0; i<3; i++){
 Write(@"Hello World "); Write(i.ToString()+"! "); } Write(@"

"); Write(@"

Write() will print any bool, string, char, decimal, double, float, int...
    A Quadrillion is 1"); for(int i=0; i<15; i++) Write("0");  Write(@"

This will also write bool, string, char, decimal, double, float, int...
    Hello at "); sb.Append(DateTime.Now); Write(@"!

"); for(int i=1; i<4; i++){  Write(@"
    "); sb.Append("Hello " + i + " World"+ (i>1?"s!":"!") ); Write(@"
    How are you? """); sb.Append(i); Write(@"""  "); sb.Append("\r\n"); Write(@"
"); }  Write(@"
");  return sb.ToString();}
static void Write<T>(T val) { sb.Append(val);}
static void Format(string format, params object[] args) { sb.AppendFormat(format,args);}
static void WriteLine(string val) { sb.AppendLine(val);}
static void WriteLine() { sb.AppendLine();} 
static void Main(string[] args) { Execute(); Console.Write(sb.ToString()); } }

示例输出

第一个例子将三次打印 Hello World

Write() 将打印任何 bool, string, char, decimal, double, float, int...
一千兆是 1000000000000000

这也将写入 bool, string, char, decimal, double, float, int...
您好,现在是 2015 年 1 月 18 日上午 8:48:13!

    Hello 1 World!
    How are you? "1"

    Hello 2 Worlds!
    How are you? "2"

    Hello 3 Worlds!
    How are you? "3"

何时不使用此代码

  • 安全性 – 由于函数编译并运行命令(类似于脚本),因此有可能被滥用。请谨慎对待谁可能调用此函数以及程序正在运行的权限级别。
  • 不能替代 Visual Studio 中的 T4。T4 内置于 Visual Studio 中,因此请使用它。它也功能更丰富,更广为人知,并且在 Visual Studio 的新版本中更容易调试。
  • 如果可能,请避免使用模板。小心不要盲目地使用文本模板。它们可能会让其他人感到困惑,并使代码复杂化。首先确保您确实需要它们。如果文本模板的结构始终相同,那么只需编写代码即可。例如,当 "Current time:" + DateTime.Now.ToString() 就足够时,不要使用模板转换来做 Current time: [[=DateTime.Now ]]。此外,性能也不是很好。

关注点

这个项目最有趣的部分是创建这门语言。主要目标是使其简单易读。我的第一个版本使用“#”进行内联代码,但它不如我想要的那么简洁。经过一些实验,[[...]] 样式胜出。但在玩了一段时间 [[...]] 后,我注意到它对代码完成和语法高亮引擎造成了严重破坏。经过进一步的实验,我有一个想法,使用内置的开/闭注释,但通过一些巧妙的修改,将它们与普通注释分开。最终,一种像 (/*: ... :*///: ...) 这样的样式流行起来。由于注释被代码完成和语法高亮引擎忽略,内联代码也将被忽略。这工作得很好,除了需要为编辑器提供某种填充代码的情况。这是一个例子

int myVar = /*: for(int i=1; i<4; i++) Write(i) :*/; 在设计器中显示为错误,因为 codesense 看到 “int myVar = ;

但这样修改可以解决问题……

int myVar = /*: for(int i=1; i<4; i++) Write(i) :*/ /**/1/**/; 可以工作,因为编辑器会看到 “int myVar = 1;

上面的两种方法在使用模板函数后都可以正常工作。它们都会扩展成“int myVar = 123;”,但是第一种方法在 IDE 中只会显示错误。

兼容性

  • 无需 DLL
  • 不需要 using 语句
  • 在 x64 和 x86 上均可运行
  • 直接在 .NET 3.5、4.0、4.0 Client Profile、4.5 和 4.51 中运行
  • 如果删除“System.Core.dll”和“using System.Linq;”,也可在 .NET 2.0、3.0、3.5 (Client Profile) 中运行。
  • 在 Visual Studio 2010/2012/2013/2015 下测试通过

性能

包含的示例需要 81ms(i7 二代,3.2Ghz,SSD)。发布、调试和无调试器发布都具有相似的性能。

打砖块

  • 生成代码需要 1 毫秒
  • 编译需要 76 毫秒
  • 执行程序需要 3 毫秒

历史

  • 2014 年 12 月:开始
  • 2015 年 1 月 3 日:初始版本
  • 2015 年 1 月 17 日:删除了 linq 代码(更适用于 .NET 3.5 之前的版本)
  • 2015 年 1 月 19 日:添加了缺失的“const
  • 2015 年 1 月 21 日:一些更改
    • 函数签名更改为
      bool success = Expand(string input, out string output)
    • 为了更清晰,也从 [=code]] 改为 [[=code]]
    • 添加了 void main(),以便可以将中间阶段放入 VS 中进行调试
© . All rights reserved.