هذا هو الجزء الثاني من القصة حول نقل محرك قالب Jinja2 إلى C ++. يمكنك قراءة النموذج الأول هنا: قوالب الترتيب الثالث ، أو كيف قمت بنقل Jinja2 إلى C ++ . سيركز على عملية تقديم القوالب. أو ، بعبارة أخرى ، عن الكتابة من الصفر لمترجم بلغة تشبه الثعبان.
التقديم على هذا النحو
بعد التحليل ، يتحول القالب إلى شجرة تحتوي على عقد من ثلاثة أنواع: نص عادي ، وتعبيرات محسوبة ، وهياكل تحكم . وفقًا لذلك ، أثناء عملية العرض ، يجب وضع النص العادي دون أي تغييرات في دفق الإخراج ، ويجب حساب التعبيرات ، وتحويلها إلى نص ، والتي سيتم وضعها في الدفق ، ويجب تنفيذ هياكل التحكم. للوهلة الأولى ، لم يكن هناك شيء معقد في تنفيذ عملية العرض: أنت فقط تحتاج إلى الالتفاف حول جميع عقد الشجرة ، وحساب كل شيء ، وتنفيذ كل شيء وإنشاء نص. كل شيء بسيط. طالما تم استيفاء شرطين: أ) يتم كل عمل بسلسلة من نوع واحد فقط (سلسلة أو سلسلة) ؛ ب) يتم استخدام التعبيرات البسيطة جدًا والتعبيرات الأساسية فقط. في الواقع ، مع هذه القيود يتم تنفيذ 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 ++ المعكوسة
كل هذا تم وصفه بنوع بيانات خاص تم إنشاؤه على أساس التعزيز :: البديل:
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
- زوج القيمة الرئيسية
من خلال هذا التوسيع ، أصبح من الممكن نقل بيانات الخدمة من خلال سياق العرض ، والتي لولا ذلك يجب "تألقها" في العناوين العامة ، وكذلك تعميم بعض الخوارزميات التي تعمل مع المصفوفات والقواميس بنجاح أكبر.
لم يتم اختيار البديل: البديل عن طريق الصدفة. يتم استخدام قدراته الغنية للعمل مع معلمات أنواع معينة. يستخدم Jinja2CppLight فئات متعددة الأشكال لنفس الغرض ، بينما تستخدم inja نظام نوع مكتبة nlohmann json. كلا البدلين ، للأسف ، لم يناسبني. السبب: إمكانية إرسال n-ary لتعزيز: variant (والآن - std :: variant). بالنسبة إلى نوع متغير ، يمكنك إنشاء زائر ثابت يقبل نوعين محددين محددين وتعيينه مقابل زوج من القيم. وسيعمل كل شيء كما ينبغي! في حالة الطبقات متعددة الأشكال أو النقابات البسيطة ، لن تعمل هذه الراحة:
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
، والذي يطبق الزائر من النوع المحدد بواسطة معلمة القالب على زوج من القيم المتغيرة ، مما يؤدي في السابق إلى بعض التحويلات إذا لزم الأمر. إذا كان مصمم الزائر يحتاج إلى معلمات ، يتم تمريرها بعد الكائنات التي ينطبق عليها الزائر:
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(' -> ') }}
هنا ، سيتم تحديد العناصر التي تم تعيين السمة المرئية فيها على true فقط من قائمة menuItems ، ثم يتم أخذ سمة العنوان من هذه العناصر ، وتحويلها إلى أحرف كبيرة ، وسيتم لصق قائمة السطور الناتجة باستخدام الفاصل "->" في سطر واحد. أو على سبيل المثال كمثال من الحياة:
{% 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 %}
يتكرر هذا الماكرو على جميع طرق الفئة المعينة ، ويتجاهل تلك التي تم تعيين السمة isIplicit لها على true ، ويحدد الطرق المتبقية التي تتطابق فيها قيمة سمة accessType مع إحدى السمات المعينة ، وتعرض نماذجها الأولية. واضح نسبيا. وكل ذلك أسهل من الدورات المكونة من ثلاثة طوابق وإذا كان من المفترض أن يتم سياجها. بالمناسبة ، يمكن عمل شيء مشابه في C ++ ضمن مواصفات النطاق v.3 .
في الواقع ، كان الخطأ الرئيسي في الوقت مرتبطًا بتنفيذ حوالي أربعين مرشحًا ، أدرجتها في المجموعة الأساسية. لسبب ما أخذت أنه يمكنني التعامل معها في غضون أسبوع أو أسبوعين. لقد كان متفائلا جدا. وعلى الرغم من أن التنفيذ النموذجي للمرشح بسيط للغاية: خذ قيمة وقم بتطبيق بعض المرحلات عليه ، كان هناك الكثير منها ، وكان علي أن العبث.
كانت مهمة معالجة الحجج مهمة مثيرة للاهتمام منفصلة في عملية التنفيذ. في Jinja2 ، كما هو الحال في python ، يمكن تسمية الحجج التي تم تمريرها إلى المكالمة أو موضعية. ويمكن أن تكون المعلمات في إعلان الفلتر إلزامية أو اختيارية (بقيم افتراضية). علاوة على ذلك ، على عكس C ++ ، يمكن وضع المعلمات الاختيارية في أي مكان في الإعلان. كان من الضروري التوصل إلى خوارزمية لدمج هاتين القائمتين ، مع مراعاة الحالات المختلفة. هنا ، دعنا نقول ، هناك وظيفة range([start, ]stop[, step])
: 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;
من هنا .
تسمى هذه الطريقة (على سبيل المثال ، 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();
يتم استخدام آلية مماثلة عند العمل مع وحدات الماكرو والمختبرين. وعلى الرغم من أنه لا يبدو أن هناك أي شيء معقد في وصف حجج كل مرشح واختبار ، فليس هناك (كيفية تنفيذه) ، ولكن حتى المجموعة "الأساسية" ، التي تتضمن حوالي خمسين من تلك وغيرها ، تبين أنها ضخمة جدًا للتنفيذ. وهذا بشرط ألا يتضمن جميع أنواع الأشياء الصعبة ، مثل سلاسل التنسيق لـ HTML (أو C ++) ، وإخراج القيم بتنسيقات مثل xml أو json ، وما شابه.
في الجزء التالي ، سنركز على تنفيذ العمل مع العديد من القوالب (التصدير ، بما في ذلك ، وحدات الماكرو) ، وكذلك على المغامرات الرائعة مع تنفيذ معالجة الأخطاء والعمل مع سلاسل ذات عروض مختلفة.
تقليديا ، الروابط:
مواصفات Jinja2
تنفيذ Jinja2Cpp