执行任意d代码的模板
原文
我们来搞编译时
运行d代码
.定义
外部模板文件
有时有用.如脚手架/超文本模板
等等.我们用模板
执行d代码
来决定值
.
基本格式
dub init
,并创建views
文件夹.允许模板定义变量
,这样程序
最后来填充它.为了简单,变量只是文本串
.views/template.txt
中内容如下:
DECLARE
$NAME
$AGE
$HOBBIES
START
我叫$NAME,年龄$AGE,爱好为:$HOBBIES.
在source/app.d
中编辑初始结构:
import std;
struct Document
{
string[] declaredVariables;
string templateText;
}//用文档来建模文件.
//现在仅简单变量名及要解析文本.
string resolve(string templateName)(string[string] variables)
{//用该函数来`解析`.
Appender!(char[]) output;
return output.data.assumeUnique;
}//`templateName`模板名为编译时参数.变量为运行时.
Document parseDocument(string contents)
{//解析模板为文本构.
Document doc;
return doc;
}
void main()
{
//使用解析函数.
writeln(resolve!"template.txt"(
[
"$NAME": "Bradley",
"$AGE": "22",
"$HOBBIES": "programming, complaining, 和 long walks at night."
]));
}
用import std;
,可访问整个标准库
,但会降低编译速度
及增加大小.
解析文档
Document parseDocument(string contents)
{
enum Mode
{
none,
declare,
start
}
Document doc;
Mode mode;
foreach(line; contents.lineSplitter())
{
switch(mode) with(Mode)
{
case none:
enforce(line == "DECLARE", "要按'DECLARE'开头");
mode = declare;
break;
case declare:
//声明模式,去左边空格,加入变量
if(line == "START")
{
mode = start;
continue;
}
doc.declaredVariables ~= line.strip(' ');
break;
case start://开始了.
if(doc.templateText.length > 0)
doc.templateText ~= '\n';
doc.templateText ~= line;//合并行.
break;
default: assert(false);
}
}
return doc;
}
用根据当前模式
决定的简单的状态机来解析,D
允许函数有自己的类,结构,枚举
等.
with(Mode)
,用来简化书写.与switch
配对用.
实现解析
string resolve(string templateName)(string[string] variables)
{
// 为了方便用户,显式声明类型,`D`可自动推导
const string TEMPLATE_CONTENTS = import(templateName);
enum Document Doc = parseDocument(TEMPLATE_CONTENTS);
enforce(
Doc.declaredVariables.all!(varName => varName in variables),
"有些变量无值".
);
Appender!(char[]) output;
string text = Doc.templateText;
while(text.length > 0)
{
const nextVarStart = text.indexOf('$');
if(nextVarStart < 0)
{
output.put(text);
break;
}
output.put(text[0..nextVarStart]);
text = text[nextVarStart..$];
const nextSpace = text.indexOfAny(" \r\n");
const varName = (nextSpace < 0) ? text : text[0..nextSpace];
text = (nextSpace < 0) ? null : text[nextSpace..$];
output.put(variables[varName]);
}
return output.data.assumeUnique;
}
这里,
const string TEMPLATE_CONTENTS = import(templateName);
views
目录,为dub
自动告诉编译器
的导入串
目录.以template.txt
为编译时
参数.import(string)
导入文件
为串
变量.
用resolve!"template.txt"
导入该文件
.然后:
enum Document Doc = parseDocument(TEMPLATE_CONTENTS);.
解析文档.枚举值
为清单常量
,exe
中无物理地址,仅在编译时
存在.使用时复制
,类似C++
中的#define
.解析后放在文档
中.这是ctfe
,编译时执行函数.剩下是用变量[值]
中值替换值
.
dub run
运行.
最终格式
DECLARE
$NAME
$AGE
$HOBBIES
COMPUTE
$HOBBIES_LOUD : variables["$HOBBIES"].splitter(',').map!(str => "!!!"~str.strip(' ').toUpper~"!!!").fold!((a,b) => a~", "~b)
//,分割.等等
START
加了个包含D
代码计算
的值的节.现在更新代码:
struct Document
{
string[] declaredVariables;
string templateText;
// 键为名,值为`D代码`
string[string] computedVariables;
}
Document parseDocument(string contents)
{
enum Mode
{
none,
declare,
start,
///
compute
///
}
Document doc;
Mode mode;
foreach(line; contents.lineSplitter())
{
switch(mode) with(Mode)
{
case none:
enforce(line == "DECLARE", "必须按'DECLARE'开头");
mode = declare;
break;
case declare:
if(line == "START")
{
mode = start;
continue;
}
//
else if(line == "COMPUTE")
{
mode = compute;
continue;
}
//
doc.declaredVariables ~= line.strip(' ');
break;
case compute:
if(line == "START")
{
mode = start;
continue;
}
const colon = line.indexOf(':');
const varName = line[0..colon].strip(' ');
const code = line[colon+1..$].strip(' ');
doc.computedVariables[varName] = code;//加代码.
break;
case start:
if(doc.templateText.length > 0)
doc.templateText ~= '\n';
doc.templateText ~= line;
break;
default: assert(false);
}
}
return doc;
}
现在,文档为键/代码的字典
,加个计算
模式,计算
区用首个:
来分割键/代码
.
现在来解析
,你不必自己写词法/语法
解析器.
string resolve(string templateName)(string[string] variables)
{
const string TEMPLATE_CONTENTS = import(templateName);
enum Document Doc = parseDocument(TEMPLATE_CONTENTS);
enforce(
Doc.declaredVariables.all!(varName => varName in variables),
"不是所有变量都有值"
);
static foreach(varName, code; Doc.computedVariables)
variables[varName] = mixin(code);
//插件就完了.
Appender!(char[]) output;
string text = Doc.templateText;
while(text.length > 0)
{
const nextVarStart = text.indexOf('$');
if(nextVarStart < 0)
{
output.put(text);
break;
}
output.put(text[0..nextVarStart]);
text = text[nextVarStart..$];
const nextSpace = text.indexOfAny(" \r\n");
const varName = (nextSpace < 0) ? text : text[0..nextSpace];
text = (nextSpace < 0) ? null : text[nextSpace..$];
output.put(variables[varName]);
}
return output.data.assumeUnique;
}
staticforeach
编译时的每一
.每行插件
就完成求值
了.或者
叫展开
.插件(代码)
是串插件
.可在static foreach和mixin
中直接用枚
中编译时变量.
dub run
.现在,你可用pegged
库来翻译
你的dsl
至d
.你只需要翻译至D
,然后插件
它.
解析其他文件
在views/other.txt
中写入:
DECLARE
$HOWDY
START
HOWDY $HOWDY
views/template.txt
中:
DECLARE
$NAME
$AGE
$HOBBIES
COMPUTE
$HOBBIES_LOUD : variables["$HOBBIES"].splitter(',').map!(str => "!!!"~str.strip(' ').toUpper~"!!!").fold!((a,b) => a~", "~b)
$MYSELF : readText("views/template.txt")
$OTHER : resolve!("other.txt")(["$HOWDY": "Y'ALL"])
START
My name is $NAME I am $AGE years old and my hobbies are: $HOBBIES
爱好大写版: $HOBBIES_LOUD
打印自己!
$MYSELF
其他模板:
$OTHER
剩下,就是自由发挥
了.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现