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.index | 0开头的循环索引号 | number |
loop.index1 | 1开头的循环索引号 | 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。
实践
参考资料
- Inja: Main Page (pantor.github.io)