html模板渲染庫(c++實現,語法與django模版語法一致)

介紹

cJinja 是一個使用cpp編寫的輕量html模版解析庫,依賴 ejson (https://github.com/HuangHongkai/ejson) 來實現模版的數據替換(在jinja中稱爲context,上下文)。模版的語法基本與django jinja一致,功能還算豐富。源碼僅有700行,適合學習,覺得不錯的點個star吧。

(該程序爲 https://github.com/HuangHongkai/tinyserver 中的一個模塊)

編譯

使用cmake來編譯,windows和linux下均可編譯。推薦使用clion作爲IDE。
在這裏插入圖片描述
編譯成功後在build目錄下會有libcjinja.a和cjinja_test.exe這2個文件。libcjinja.a是靜態庫,cjinja_test.exe是一個簡單的測試程序。

運行測試程序後會出現output.html(該文件是tmp.html解析後的結果。)

已經完成的功能

  • 變量,例如 {{ var }}
  • 變量索引訪問,例如 {{ var.var2 }} {{ var[2] }} {{ var[2].key.value[2] }},其中**[]** 表示對數組(類似python的list)進行索引, . 表示對object進行索引(類似與python的dict)
  • 表達式計算(包括字符串拼接) ,例如{{ 1*1+2-3*var }} {{ 1+1*2-3/4 }} {{ “asdfsf”+var }}
  • for-endfor對列表進行迭代, 例如 {% for var in list %} {% endfor %}
  • for-endfor對對象進行迭代,例如 {% for key,value in object %} {% endfor %} 或者 {% for key in object %}{% endfor %} 或者 {% for ,value in object %} {% endfor %}
  • if-else-endif 語句, 其中if的條件支持四則運算,簡單的比較(!= == )等,例如 {% if 1+1==2 %}aaa{% else %}bbb{%endif %}
  • 模版包含,嵌套其他的模版文件{% include ‘other.html’ %}
  • 模版語法錯誤提示

需要注意,表達式之間不能含有空格,例如{{ 1 + 1 }}是非法的,而{{ 1+1 }}是合法的。

使用方法

1. 變量和變量索引

簡單的例子如下,

HtmlTemplate html("username:{{ username }}\n"
                  "parm.list[1][2]: {{parm.list[1][2] }} \n"
                  "parm.key: {{ parm.key }}",
                 1); // 參數1表示傳入的是模版字符串,0表示傳入的是文件名,默認爲0
JSONObject obj = {
    {"username", 1234},
    {"parm", {
        {"key", "cde"},
        {"list", {1, {1,2.3, "abcd"}, "hahaha"}},
    }}
};
html.setValue(obj);
cout << html.render() << endl << endl;
/* 運行後打印如下
username:1234
parm.list[1]: abcd 
parm.key: cde
*/

HtmlTemplate是一個庫的主要類,構造函數爲

explicit HtmlTemplate(const string& str, int flag = 0); // flag=0是str表示文件路徑,不爲0是表示傳入的模版字符串

其中str參數爲字符串,可以表示html模板原始串,也可也表示爲文件的路徑,flag默認爲0。

setValue 方法表示傳入數據給模版對象。

render() 方法表示將模版解析成字符串。

JSONObject來源於 ejson 庫,用來模擬python的dict,構造函數也比較容易看懂。

2. 列表迭代

HtmlTemplate html("{% for x in list %}{{ x }}\n{%endfor%}"
                  "此時x已經是臨時變量了,不可以在打印了 {{x}}\n"
                  , 1);

JSONObject obj = OBJECT(
    KEYVALUE("list", LIST(1,2,3,4,5))
);
cout << html.setValue(obj).render() << endl << endl;
/*運行後輸出如下
1
2
3
4
5
此時x已經是臨時變量了,不可以在打印了 
*/

注意到在迭代過程中x是作爲臨時變量,在外部的話是無法打印出來的。

3. 字典迭代

HtmlTemplate html("{% for key in dict %}迭代1: 字典的key值爲 {{ key }}\n{% endfor %}"
                  "{% for key,value in dict %}迭代2: 字典的key值爲 {{ key }}, value值爲 {{ value}}\n{% endfor %}"
                  "{% for ,value in dict %}迭代3: 字典的value值爲 {{ value }}\n{% endfor %}", 1);
JSONObject obj = OBJECT(
    KEYVALUE("dict", OBJECT(
        KEYVALUE("key1", "value1"),
        KEYVALUE("key2", 1234),
        KEYVALUE("key3", nullptr),
    ))
);
cout << html.setValue(obj).render() << endl << endl;
/*運行後輸出
迭代1: 字典的key值爲 key1
迭代1: 字典的key值爲 key2
迭代1: 字典的key值爲 key3
迭代2: 字典的key值爲 key1, value值爲 value1
迭代2: 字典的key值爲 key2, value值爲 1234
迭代2: 字典的key值爲 key3, value值爲 null
迭代3: 字典的value值爲 value1
迭代3: 字典的value值爲 1234
迭代3: 字典的value值爲 null
*/

4. 字符串拼接與表達式計算

HtmlTemplate html("{{ a+b+c+\"444\" }}\n"
                  "{{x}} * {{y}} + 2 * 3 - 4 / {{x}} = {{ x*y+2*3-4/x }}\n",
                      1);
JSONObject obj = OBJECT(
            KEYVALUE("a", "111"),
            KEYVALUE("b", "222"),
            KEYVALUE("c", "333"),
            KEYVALUE("x", 12),
            KEYVALUE("y", 34)
 );
cout << html.setValue(obj).render() << endl << endl;
/*運行後輸出
111222333444
12 * 34 + 2 * 3 - 4 / 12 = 413.667
*/

5. if-else-endif語句

HtmlTemplate html("{% if 1==1 %} 1==1 成立 {% else %} 1==1不成立 {%endif %}\n"
                  "{% if !x %} x爲空 {% else %} x不爲空 {%endif %}\n"
                  "{% if x==2 %} x==2 成立 {% endif %}\n"
                  "{% if x+1!=2 %} x+1!=2 成立 {% endif %}\n"
                  "{% if x<3 %} x<3 成立 {% endif %}\n"
                  "{% if x>1 %} x>1 成立 {% endif %}\n"
                  "{% if str==\"abcd\" %} str爲abcd {% endif %}\n"
                  "{% if 1 %} 常量表達式1 {% endif %}\n"
                  "{% if 0 %} 常量表達式0,此處不會輸出 {%endif%}", 1);
JSONObject obj = {
    {"x", 2},
    {"str", "abcd"}
};
cout << html.setValue(obj).render() << endl;
/*運行後輸出
 1==1 成立 
 x不爲空 
 x==2 成立 
 x+1!=2 成立 
 x<3 成立 
 x>1 成立 
 str爲abcd 
 常量表達式1 
*/

6.for與if嵌套使用

 HtmlTemplate html("{%for x in list%}"
                       "{%if x %}"
                            "{% for y in list2%}"
                                "{{x}} * {{y}} = {{ x*y }}\n"
                            "{% endfor %}"
                       "{% else %}"
                            "x的值爲空\n"
                       "{%endif%}"
                   "{% endfor%}", 1);
JSONObject obj = OBJECT(
    KEYVALUE("list", LIST(1,2,3,4,5)),
    KEYVALUE("list2", LIST(1,2,3)),
);
cout << html.setValue(obj).render() << endl << endl;
/*運行後輸出
1 * 1 = 1
1 * 2 = 2
1 * 3 = 3
2 * 1 = 2
2 * 2 = 4
2 * 3 = 6
3 * 1 = 3
3 * 2 = 6
3 * 3 = 9
4 * 1 = 4
4 * 2 = 8
4 * 3 = 12
5 * 1 = 5
5 * 2 = 10
5 * 3 = 15
*/

7.模版文件作爲輸出

HtmlTemplate html("tmpl.html");
JSONObject context = OBJECT(
    ...
);
FILE* f = fopen("output.html", "w"); // 寫入到文件中
string&& str = html.setValue(context).render();
fwrite(str.c_str(), 1, str.size(), f);
fclose(f);
/*運行後,代開當前目錄的tmpl.html文件作爲輸入,輸出文件爲output.html*/
/*如果tmpl.html不存在則拋出異常*/

8. 異常處理

HtmlTemplate html("{% if 1 %} xxx ", 1);
// 不傳入context
try {
    cout << html.render() << endl;
} catch(exception& e) {
    cerr << e.what() << endl;
}
cout << endl;

運行後終端上打印如下,

在這裏插入圖片描述

會提示異常的類名,異常文件所在位置,代碼行數,以及一些錯誤的信息。

討論

1. 實現一個簡單的表達式計算器用什麼方法比較好?(例如 {{ 2.3*3+4/5*x }} 這類表達式)

我分享一下我自己的方法,有什麼更好的方法一起討論一下。

  • 第一步,先把數據和符號提取出來放入到數組中,輸入類型全部設爲double。例如上面那個表達式,符號提取出來是{*, /, *}, 數據提取出來是{2.3, 3, 4, 5, x}

    這一步位於__parse_var這個函數,比較簡單不詳細討論。

  • 第二步,先計算乘除法,結果放入棧中,在對棧中元素計算加減法(按照我們平常計算表達式的思路先乘除後加減),這一步實現如下(其中運用到C語言的宏和C++11的匿名函數)

    double cJinja::HtmlTemplate::calculator(vector<any>& number, vector<char>& op) {
        // 例如下表達式會成爲
        // 1 - 2 - 3 + 2 *3 * 4 - 4*5
        // vector<char> op = { '-', '-', '+', '*', '*', '-', '*' };
        // vector<any> number = { 1, 2, 3, 2, 3, 4, 4, 5 };
        if (number.size() != op.size() + 1)
        throwException(TemplateParseException, "運算符號數和操作數不匹配");
    
        /* 定義計算器的內部函數 */
        auto calc = [](any& var1, double var2, char op) -> double{
            // var2 + var1
            // var2 * var1
            // var2 - var1
            // var2 / var1
            // 注意順序
    #define CALC(op2) \
        if(#op2[0] == op) { \
            if (var1.type() == typeid(int)) \
                return var2 op2 static_cast<double>(any_cast<int>(var1)); \
            else if (var1.type() == typeid(float)) \
                return var2 op2 static_cast<double>(any_cast<float>(var1)) ; \
            else if (var1.type() == typeid(double)) \
                return var2 op2 static_cast<double>(any_cast<double>(var1))  ; \
        }
            CALC(+);
            CALC(-);
            CALC(*);
            CALC(/);
            throwException(TemplateParseException, "不允許對空指針進行運算");
    #undef CALC
        };
    
        vector<double> num_stack; // 計算中間結果存儲棧
        num_stack.push_back(calc(number[0], 0, '+')); // 獲取值  number[i+1] + 0 (加法運算零元爲0,乘法運算零元爲1)
        /* 計算 * / 法*/
        for (size_t i = 0; i < op.size(); i++) {
            if (op[i] == '+' || op[i] == '-') {
                num_stack.push_back(calc(number[i + 1], 0, '+')); // number[i+1] + 0
            }
            else if (op[i] == '*' || op[i] == '/') {
                double var1 = num_stack.back(); num_stack.pop_back();
                num_stack.push_back(calc(number[i + 1], var1, op[i])); // var1/number[i+1] 或者是  var1/number[i+1]
            } else
            throwException(TemplateParseException, str_format("非法操作符 %d", op[i]));
        }
        /* 計算 + - 法*/
        double result = num_stack[0];
        size_t i = 1;
        for (auto& ch : op) {
            if (ch == '+') {
                result += num_stack[i++];
            } else if(ch == '-') {
                result -= num_stack[i++];
            }
        }
        return result;
    }
    

2. 拋出異常包含更多的信息

我定義了一個throwException宏,如下

#define throwException(Exception, ...) { \
        std::cerr << "[" << #Exception << "] : FILE: " << string(__FILE__).substr(string(__FILE__).find_last_of('/') + 1) << "   LINE: " << __LINE__ <<  "  FUNCTION: " <<__FUNCTION__ << std::endl; \
        throw Exception(__VA_ARGS__); \
    }

其中__FILE__ 爲文件名,__LINE__ 爲當前代碼行數,這些都是C中的內置宏,__VA_ARGS__是可變參數,對應於宏函數參數中的....

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章