03 - inja模板引擎

尝试从零实现(抄)一个反射系统,并应用于我酝酿中的游戏引擎项目里!本节将实现反射系统的第三部分:代码生成。首先简要介绍inja代码模板生成引擎,然后尝试利用获取到的Schema信息自动生成上一节文章中写的反射代码。

Inja简介

Inja是为现代C++服务的模板引擎,它可以按模板生成变量、循环、条件等代码,十分强大。例如如下代码将存储在Json中的数据交给模板渲染,并返回渲染结果:

inja::json data;
data["name"] = "world";

// 返回: "Hello world!"
inja::render("Hello {{ name }}!", data);

环境配置

Inja将数据存储在Json中,因此需要依赖Json库 nlohman/json.hpp (>= v3.8.0)。此外Inja也是一个单头文件库,因此在这里下载好,把这两个头文件库放到include文件夹下即可。

接下来介绍它的常用操作。

渲染模板

最基本的模板渲染就是直接将数据渲染到模板,以std::string或流的形式返回:

json data;
data["name"] = "world";

// Returns std::string "Hello world!"
render("Hello {{ name }}!", data);
// Writes "Hello world!" to stream
render_to(std::cout, "Hello {{ name }}!", data);

进阶方法是让环境inja::Environment来进行模板渲染,它就像一个状态机,设置好状态后便能按状态渲染:

Environment env;
 
// 直接渲染字符串模板
std::string result = env.render("Hello {{ name }}!", data); // "Hello world!"
 
// 先文件读取模板, 然后进行渲染
Template temp = env.parse_template("./templates/greeting.txt");
std::string result = env.render(temp, data); // "Hello world!"
data["name"] = "Inja";
std::string result = env.render(temp, data); // "Hello Inja!"
 
// 模板和数据都可先通过文件读取, 然后渲染
result = env.render_file("./templates/greeting.txt", data);
result = env.render_file_with_json_file("./templates/greeting.txt", "./data.json");
 
// 渲染的结果也能存储到本地磁盘中
env.write(temp, data, "./result.txt");
env.write_with_json_file("./templates/greeting.txt", "./data.json", "./result.txt");

环境状态的常用设置方法如下:

// 默认设置
Environment env_default;
 
// 存放模板文件的全局路径, 生成的文件也会在这存储
Environment env_1 {"../path/templates/"};
 
// 作为输入的模板文件路径, 作为输出的生成文件路径
Environment env_2 {"../path/templates/", "../path/results/"};
 
// 设置各种模板参数的起始符和终结符 (这里是默认的)
env.set_expression("{{", "}}"); // 表达式
env.set_comment("{#", "#}"); 	// 注释
env.set_statement("{%", "%}"); 	// 各种语句, 详见下文
env.set_line_statement("##"); 	// 行语句的开头

基本语法

接下来看看Inja模板引擎的基本语法,方便我们进行后续代码渲染操作。

变量

变量在{{ ... }}中被渲染:

json data;
data["neighbour"] = "Peter";
data["guests"] = {"Jeff", "Tom", "Patrick"};
data["time"]["start"] = 16;
data["time"]["end"] = 22;
 
// Indexing in array
render("{{ guests.1 }}", data); // "Tom"
 
// Objects
render("{{ time.start }} to {{ time.end + 1 }}pm", data); // "16 to 23pm"

如果未找到变量,则直接打印有效的 JSON,否则将引发 inja::RenderError异常。

表达式

表达式可以在{% ... %}中被包含,也能写在以##开头的整行中。常用到的表达式有循环,条件和包含。所有表达式均可嵌套。

循环语句

循环的基本写法如下:

// Combining loops and line statements
render(R"(Guest List:
## for guest in guests
    {{ loop.index1 }}: {{ guest }}
## endfor )", data)
 
/* Guest List:
    1: Jeff
    2: Tom
    3: Patrick */

其中,被定义的特殊变量及释义如下:

特殊变量释义值类型
loop.index0开头的循环索引号number
loop.index11开头的循环索引号number
loop.is_first是否是第一个元素boolean
loop.is_last是否是最后一个元素boolean

对于嵌套循环,可以通过形如loop.parent.index的形式去访问外层的循环变量。

除了遍历数组,还能遍历对象:

// Time:  end : 22  start : 16
Time: {% for key, value in time %} {{ key }} : {{ value }} {% endfor %}

条件语句

条件语句则支持传统的if - else if - else结构:

// 和变量的比较
render("{% if time.hour >= 20 %}Serve{% else if time.hour >= 18 %}Make{% endif %} dinner.", data); // Serve dinner.
 
// 列表中的变量
render("{% if neighbour in guests %}Turn up the music!{% endif %}", data); // Turn up the music!
 
// 逻辑运算 and
render("{% if guest_count < (3+2) and all_tired %}Sleepy...{% else %}Keep going...{% endif %}", data); // Sleepy...
 
// 逻辑运算 not
render("{% if not guest_count %}The End{% endif %}", data); // The End

包含语句

可以包含来自内存或文件系统中的其他模板:

// 要想包含来自内存的模板, 得先从环境中转换
inja::Template content_template = env.parse("Hello {{ neighbour }}!");
env.include_template("content", content_template);
env.render("Content: {% include \"content\" %}", data); // "Content: Hello Peter!"
 
// 根据当前文件位置的相对位置包含其他来自文件的模板
render("{% include \"footer.html\" %}", data);

如果文档没有被包含成功(在文件系统中找不到),就会触发包含回调:

// 当前路径和要包含的模板名作为参数
env.set_include_callback([&env](const std::string& path, const std::string& template_name) {
  return env.parse("Hello {{ neighbour }} from " + template_name);
});

赋值语句

在模板中也能通过 set 语句为自定义变量赋值:

render("{% set new_hour=23 %}{{ new_hour }}pm", data); // "23pm"
render("{% set time.start=18 %}{{ time.start }}pm", data); // using json pointers

函数

Inja还实现了一些内置函数:

// 给字符串使用的大小写转换函数
render("Hello {{ upper(neighbour) }}!", data); // "Hello PETER!"
render("Hello {{ lower(neighbour) }}!", data); // "Hello peter!"
 
// 给循环使用的range函数
render("{% for i in range(4) %}{{ loop.index1 }}{% endfor %}", data); // "1234"
render("{% for i in range(3) %}{{ at(guests, i) }} {% endfor %}", data); // "Jeff Tom Patrick "
 
// 获取列表的长度
render("I count {{ length(guests) }} guests.", data); // "I count 3 guests."
 
// 获取列表第一个/最后一个元素
render("{{ first(guests) }} was first.", data); // "Jeff was first."
render("{{ last(guests) }} was last.", data); // "Patir was last."
 
// 排序列表
render("{{ sort([3,2,1]) }}", data); // "[1,2,3]"
render("{{ sort(guests) }}", data); // "[\"Jeff\", \"Patrick\", \"Tom\"]"
 
// 为列表间元素添加分隔符
render("{{ join([1,2,3], \" + \") }}", data); // "1 + 2 + 3"
render("{{ join(guests, \", \") }}", data); // "Jeff, Patrick, Tom"
 
// 给元素按位数四舍五入
render("{{ round(3.1415, 0) }}", data); // 3
render("{{ round(3.1415, 3) }}", data); // 3.142
 
// 检查该元素是奇数/偶数/被x整除
render("{{ odd(42) }}", data); // false
render("{{ even(42) }}", data); // true
render("{{ divisibleBy(42, 7) }}", data); // true
 
// 获取列表最大/最小元素
render("{{ max([1, 2, 3]) }}", data); // 3
render("{{ min([-2.4, -1.2, 4.5]) }}", data); // -2.4
 
// 转换字符串为数值
render("{{ int(\"2\") == 2 }}", data); // true
render("{{ float(\"1.8\") > 2 }}", data); // false
 
// 如果该变量未定义, 返回设定好的默认值
render("Hello {{ default(neighbour, \"my friend\") }}!", data); // "Hello Peter!"
render("Hello {{ default(colleague, \"my friend\") }}!", data); // "Hello my friend!"
 
// 动态访问对象的值
render("{{ at(time, \"start\") }} to {{ time.end }}", data); // "16 to 22"
 
// 检查对象中是否有所给key
render("{{ exists(\"guests\") }}", data); // "true"
render("{{ exists(\"city\") }}", data); // "false"
render("{{ existsIn(time, \"start\") }}", data); // "true"
render("{{ existsIn(time, neighbour) }}", data); // "false"
 
// 检查所给key是否为某种类型
render("{{ isString(neighbour) }}", data); // "true"
render("{{ isArray(guests) }}", data); // "true"
// Implemented type checks: isArray, isBoolean, isFloat, isInteger, isNumber, isObject, isString,

注释

注释可被``包围,被它包围的内容不会输出。

其他特性

没有涉及到的特性如下:

  • 通过自定义回调来实现自定义函数的功能。
  • 模板之间也存在继承关系,可以继承一个模板覆写某些内容。
  • 可以通过设置环境来管理空格。

详见参考资料1。

实践

参考资料

  1. Inja: Main Page (pantor.github.io)