执行任意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
剩下,就是自由发挥了.
浙公网安备 33010602011771号