执行任意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库来翻译你的dsld.你只需要翻译至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

剩下,就是自由发挥了.

posted @   zjh6  阅读(30)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示