C ++世界中的Jinja2,第二部分。 渲染图

Jinja2徽标 这是关于将Jinja2模板引擎移植到C ++的故事的第二部分。 您可以在此处阅读第一个模板三阶模板,或者如何将Jinja2移植到C ++ 。 它将着重于渲染模板的过程。 换句话说,就是从头开始编写类似Python的语言的解释器。


这样渲染


解析后,模板变成一棵树,其中包含三种类型的节点: 纯文本计算的表达式控件结构 。 因此,在渲染过程中,应将纯文本放置在输出流中,而无需进行任何更改,应该计算表达式,将其转换为文本,然后将其放置在流中,并且必须执行控制结构。 乍一看,实现渲染过程并不复杂:您只需要遍历树的所有节点,计算所有内容,执行所有内容并生成文本。 一切都很简单。 恰好满足两个条件:a)所有工作仅使用一种类型的字符串(string或wstring)完成; b)仅使用非常简单的表达式和基本表达式。 实际上,正是出于这种限制,实现了inja和Jinja2CppLight。 对于我的Jinja2Cpp,这两个条件均不起作用。 首先,我最初为这两种类型的字符串提供了透明的支持。 其次,整个开发只是为了几乎完全支持Jinja2规范而开始的,从本质上讲,这是一种成熟的脚本语言。 因此,我不得不深入研究渲染而不是解析。


表达评估


如果无法参数化模板,则该模板将不是模板。 原则上,Jinja2允许“本身”选择模板-可以在模板本身内部设置所有必需的变量,然后进行呈现。 但是,在带有从“外部”获得的参数的模板中工作仍然是主要情况。 因此,对表达式求值的结果取决于在计算点上哪些变量(参数)的值可见。 而且要注意的是,在Jinja2中,不仅存在范围(可以嵌套),而且还存在复杂的“透明”规则。 例如,这是一个模板:


{% set param1=10 %} {{ param1 }} 

由于其呈现,将收到文本10
该选项稍微复杂一些:


 {% set param1=10 %} {{ param1 }} {% for param1 in range(10) %}-{{ param1 }}-{% endfor %} {{ param1 }} 

渲染最早于10-0--1--2--3--4--5--6--7--8--9-10
该循环会生成一个新的作用域,您可以在其中定义自己的变量参数,并且这些参数在作用域之外将不可见,就像它们不会在外部变量中研磨相同参数的值一样。 使用扩展/块结构更加棘手,但是最好在Jinja2文档中对此进行阅读。


因此,出现了计算的上下文。 或更确切地说,渲染:


 class RenderContext { public: RenderContext(const InternalValueMap& extValues, IRendererCallback* rendererCallback); InternalValueMap& EnterScope(); void ExitScope(); auto FindValue(const std::string& val, bool& found) const { for (auto p = m_scopes.rbegin(); p != m_scopes.rend(); ++ p) { auto valP = p->find(val); if (valP != p->end()) { found = true; return valP; } } auto valP = m_externalScope->find(val); if (valP != m_externalScope->end()) { found = true; return valP; } found = false; return m_externalScope->end(); } auto& GetCurrentScope() const; auto& GetCurrentScope(); auto& GetGlobalScope(); auto GetRendererCallback(); RenderContext Clone(bool includeCurrentContext) const; private: InternalValueMap* m_currentScope; const InternalValueMap* m_externalScope; std::list<InternalValueMap> m_scopes; IRendererCallback* m_rendererCallback; }; 

从这里


上下文包含指向调用渲染函数时获得的值的集合的指针,作用域列表(堆栈),当前活动作用域以及指向回调接口的指针,以及各种对渲染有用的函数。 但是过一会儿。 参数搜索功能按顺序将上下文列表上移到外部列表,直到找到必要的参数为止。


现在介绍一下参数本身。 从外部接口(及其用户)的角度来看,Jinja2支持以下有效类型的列表:


  • 数字(整数,双精度)
  • 字符串(窄,宽)
  • 布尔
  • 数组(更像无量纲元组)
  • 辞典
  • 反映的C ++结构

所有这些都由在boost :: variant基础上创建的特殊数据类型描述:


 using ValueData = boost::variant<EmptyValue, bool, std::string, std::wstring, int64_t, double, boost::recursive_wrapper<ValuesList>, boost::recursive_wrapper<ValuesMap>, GenericList, GenericMap>; class Value { public: Value() = default; template<typename T> Value(T&& val, typename std::enable_if<!std::is_same<std::decay_t<T>, Value>::value>::type* = nullptr) : m_data(std::forward<T>(val)) { } Value(const char* val) : m_data(std::string(val)) { } template<size_t N> Value(char (&val)[N]) : m_data(std::string(val)) { } Value(int val) : m_data(static_cast<int64_t>(val)) { } const ValueData& data() const {return m_data;} ValueData& data() {return m_data;} private: ValueData m_data; }; 

从这里


当然,数组和字典的元素可以是任何列出的类型。 但是问题是,对于内部使用,这组类型太狭窄了。 为了简化实现,需要以下附加类型的支持:


  • 目标格式的字符串。 它可以窄也可以宽,这取决于要渲染的模板类型。
  • 可赎回类型
  • AST树组装
  • 键值对

通过此扩展,可以通过呈现上下文传输服务数据,否则将不得不在公共头文件中“屏蔽”,以及更成功地概括一些适用于数组和字典的算法。


并非偶然选择Boost ::变体。 它的丰富功能用于处理特定类型的参数。 Jinja2CppLight使用多态类出于相同的目的,而inja使用nlohmann json库类型系统。 las,这两种选择都不适合我。 原因:对于boost :: variant(现在是std :: variant)进行n元调度的可能性。 对于变量类型,您可以使静态访问者接受两个特定的存储类型,并将其设置为一对值。 一切都会正常进行! 对于多态类或简单联合,这种便利将不起作用:


 struct StringJoiner : BaseVisitor<> { using BaseVisitor::operator (); InternalValue operator() (EmptyValue, const std::string& str) const { return str; } InternalValue operator() (const std::string& left, const std::string& right) const { return left + right; } }; 

从这里


这样的访客非常简单地称为:


 InternalValue delimiter = m_args["d"]->Evaluate(context); for (const InternalValue& val : values) { if (isFirst) isFirst = false; else result = Apply2<visitors::StringJoiner>(result, delimiter); result = Apply2<visitors::StringJoiner>(result, val); } 

Apply2这里是boost::apply_visitor的包装器,该包装器将template参数指定的类型的访问者应用于一对变量值,并在必要时进行一些转换。 如果访问者的设计师需要参数,则将其传递给访问者所应用的对象之后:


 comparator = [](const KeyValuePair& left, const KeyValuePair& right) { return ConvertToBool(Apply2<visitors::BinaryMathOperation>(left.value, right.value, BinaryExpression::LogicalLt, BinaryExpression::CaseSensitive)); }; 

因此,带有参数的运算逻辑如下:变量->使用访问者解包->对特定类型的特定值执行所需的操作->将结果打包回变量中。 和最少的魔法卧底。 可能会实现js中的所有功能:在任何情况下都执行操作(例如,加法),选择将字符串转换为数字,将数字转换为字符串,将字符串转换为列表等的特定系统。 我选择了一种更简单,更可预测的方式:如果对一个值(或一对值)进行的操作是不可能或不合逻辑的,则返回空结果。 因此,在将数字添加到字符串时,仅当使用串联操作('〜')时,才能获得字符串。 否则,结果将为空值。 操作的优先级由语法确定,因此,在AST处理期间不需要其他检查。


筛选器和测试


Jinja2中其他语言称为“标准库”的语言称为“过滤器”。 本质上,过滤器是对'|'符号左侧的值进行的一种复杂运算,其结果将是一个新值。 可以通过组织管道将过滤器按链排列:
{{ menuItems | selectattr('visible') | map(attribute='title') | map('upper') | join(' -> ') }}
在这里,只会从menuItems数组中选择visible属性设置为true的那些元素,然后将从这些元素中获取title属性,并将其转换为大写字母,然后将结果行列表与分隔符'->'粘合为一行。 或者说,作为生活中的一个例子:


 {% macro MethodsDecl(class, access) %} {% for method in class.methods | rejectattr('isImplicit') | selectattr('accessType', 'in', access) %} {{ method.fullPrototype }}; {% endfor %} {% endmacro %} 

从这里


替代选择
 {% macro MethodsDecl(class, access) %} {{ for method in class.methods | rejectattr('isImplicit') | selectattr('accessType', 'in', access) | map(attribute='fullPrototype') | join(';\n') }}; {% endmacro %} 

此宏对给定类的所有方法进行迭代,丢弃将isImplicit属性设置为true的那些方法,选择accessType属性值与给定方法之一匹配的其余方法,并显示其原型。 比较清楚。 这比三层楼的建筑要容易得多。 顺便说一下,可以在v.3规范范围内完成C ++中的类似操作。


实际上,主要的时间错失与大约40个过滤器的实现相关,我将其包括在基本集中。 由于某种原因,我认为我可以在一两个星期内处理好它。 太乐观了。 尽管该过滤器的典型实现非常简单:取一个值并为其应用一些函子,但它们太多了,我不得不动手。
在实现过程中,另一个有趣的任务是处理参数的逻辑。 在Jinja2中,就像在python中一样,传递给调用的参数可以命名或定位。 过滤器声明中的参数可以是强制性的,也可以是可选的(具有默认值)。 此外,与C ++不同,可选参数可以位于广告中的任何位置。 考虑到不同的情况,有必要提出一种将这两个列表合并的算法。 假设这里有一个范围函数: range([start, ]stop[, step]) 。 可以通过以下方式调用它:


 range(10) // -> range(start = 0, stop = 10, step = 1) range(1, 10) // -> range(start = 1, stop = 10, step = 1) range(1, 10, 3) // -> range(start = 1, stop = 10, step = 3) range(step=2, 10) // -> range(start = 0, stop = 10, step = 2) range(2, step=2, 10) // -> range(start = 2, stop = 10, step = 2) 

依此类推。 我非常希望在实现过滤器功能的代码中不必考虑所有这些情况。 结果,他决定在过滤器代码,测试器或功能代码中严格按名称获取参数。 通过检查是否以一种或另一种方式给出了所有必需的参数,一个单独的函数将参数的实际列表与期望的参数列表进行比较:


大块代码
 ParsedArguments ParseCallParams(const std::initializer_list<ArgumentInfo>& args, const CallParams& params, bool& isSucceeded) { struct ArgInfo { ArgState state = NotFound; int prevNotFound = -1; int nextNotFound = -1; const ArgumentInfo* info = nullptr; }; boost::container::small_vector<ArgInfo, 8> argsInfo(args.size()); boost::container::small_vector<ParamState, 8> posParamsInfo(params.posParams.size()); isSucceeded = true; ParsedArguments result; int argIdx = 0; int firstMandatoryIdx = -1; int prevNotFound = -1; int foundKwArgs = 0; // Find all provided keyword args for (auto& argInfo : args) { argsInfo[argIdx].info = &argInfo; auto p = params.kwParams.find(argInfo.name); if (p != params.kwParams.end()) { result.args[argInfo.name] = p->second; argsInfo[argIdx].state = Keyword; ++ foundKwArgs; } else { if (argInfo.mandatory) { argsInfo[argIdx].state = NotFoundMandatory; if (firstMandatoryIdx == -1) firstMandatoryIdx = argIdx; } else { argsInfo[argIdx].state = NotFound; } if (prevNotFound != -1) argsInfo[prevNotFound].nextNotFound = argIdx; argsInfo[argIdx].prevNotFound = prevNotFound; prevNotFound = argIdx; } ++ argIdx; } int startPosArg = firstMandatoryIdx == -1 ? 0 : firstMandatoryIdx; int curPosArg = startPosArg; int eatenPosArgs = 0; // Determine the range for positional arguments scanning bool isFirstTime = true; for (; eatenPosArgs < posParamsInfo.size(); ++ eatenPosArgs) { if (isFirstTime) { for (; startPosArg < args.size() && (argsInfo[startPosArg].state == Keyword || argsInfo[startPosArg].state == Positional); ++ startPosArg) ; isFirstTime = false; continue; } int prevNotFound = argsInfo[startPosArg].prevNotFound; if (prevNotFound != -1) { startPosArg = prevNotFound; } else if (curPosArg == args.size()) { break; } else { int nextPosArg = argsInfo[curPosArg].nextNotFound; if (nextPosArg == -1) break; curPosArg = nextPosArg; } } // Map positional params to the desired arguments int curArg = startPosArg; for (int idx = 0; idx < eatenPosArgs && curArg != -1; ++ idx, curArg = argsInfo[curArg].nextNotFound) { result.args[argsInfo[curArg].info->name] = params.posParams[idx]; argsInfo[curArg].state = Positional; } // Fill default arguments (if missing) and check for mandatory for (int idx = 0; idx < argsInfo.size(); ++ idx) { auto& argInfo = argsInfo[idx]; switch (argInfo.state) { case Positional: case Keyword: continue; case NotFound: { if (!IsEmpty(argInfo.info->defaultVal)) result.args[argInfo.info->name] = std::make_shared<ConstantExpression>(argInfo.info->defaultVal); break; } case NotFoundMandatory: isSucceeded = false; break; } } // Fill the extra positional and kw-args for (auto& kw : params.kwParams) { if (result.args.find(kw.first) != result.args.end()) continue; result.extraKwArgs[kw.first] = kw.second; } for (auto idx = eatenPosArgs; idx < params.posParams.size(); ++ idx) result.extraPosArgs.push_back(params.posParams[idx]); return result; } 

从这里


这样称呼(例如range ):


 bool isArgsParsed = true; auto args = helpers::ParseCallParams({{"start"}, {"stop", true}, {"step"}}, m_params, isArgsParsed); if (!isArgsParsed) return InternalValue(); 

并返回以下结构:


 struct ParsedArguments { std::unordered_map<std::string, ExpressionEvaluatorPtr<>> args; std::unordered_map<std::string, ExpressionEvaluatorPtr<>> extraKwArgs; std::vector<ExpressionEvaluatorPtr<>> extraPosArgs; ExpressionEvaluatorPtr<> operator[](std::string name) const { auto p = args.find(name); if (p == args.end()) return ExpressionEvaluatorPtr<>(); return p->second; } }; 

仅通过其名称就可以采用的必要参数:


 auto startExpr = args["start"]; auto stopExpr = args["stop"]; auto stepExpr = args["step"]; InternalValue startVal = startExpr ? startExpr->Evaluate(values) : InternalValue(); InternalValue stopVal = stopExpr ? stopExpr->Evaluate(values) : InternalValue(); InternalValue stepVal = stepExpr ? stepExpr->Evaluate(values) : InternalValue(); 

使用宏和测试器时,使用类似的机制。 尽管描述每个过滤器和测试的参数似乎没有什么复杂的事情,但没有(如何实现),但即使是包括约50个这样的“基本”集合,也证明了实现非常庞大。 而且这提供了它不包括所有棘手的内容,例如为HTML(或C ++)格式化字符串,以xml或json等格式输出值,等等。


在下一部分中,我们将重点介绍使用几个模板(导出,包含,宏)的工作的实现,以及通过错误处理的实现而引人入胜的冒险以及使用不同宽度的字符串的工作。


传统上,链接:


Jinja2规格
Jinja2Cpp实现

Source: https://habr.com/ru/post/zh-CN419011/


All Articles