コンポーネント方式 mustache テンプレート

おもちゃ CGI (Common Gateway Interface) プログラム、 一行掲示板 suzume.cgi、 の改訂を年末から少しずつ進めている最中です。 このプログラムは、 C++11 で小規模の CGI プログラムを手軽に作る実験場で、 最低限の部品を組み込んであります。 当初からテンプレートには mustache 流儀のものを組み込んでおり、 最初はパラメータを JSON 用バリアント・データ構造で受け渡していました。 その後、 いくつか CGI を組んでいくうちに、 このやりかたは C++11 では使い勝手が劣ることが気になってきて、 今回は、 コンポーネント方式へ作り替えました。

GitHub - tociyuki/suzume-cgi-cxx11

コンポーネント方式のテンプレート・エンジンは、 NeXT 社の WebObjects を意識したものになっています。 WebObjectsApple 社に受け継がれた後、 この数年間でフェードアウトして過去のものになりつつあるようですけど、 テンプレートのマークアップインスタンスを結びつけるのに都合の良い方法の一つです。 WebObjects を単純に利用すると、 ウェブ・ページ一つにつき一つのインスタンスが対応し、 テンプレートのマークアップがキー値コーディングでインスタンス変数と結びつきます。 現在の suzume.cgi 用 mustache は、 この単純な場合を参考にして、 テンプレート・エンジンがビュー・クラスのインスタンスから値を取得して利用する形式にしてみました。

ただし、 C++11 では キー値コーディングの代わりに、 メンバ関数に列挙型の値を受け渡すようにしています。 どのマークアップ文字列がどの列挙型の値に対応するかをテンプレートのアセンブル前にエンジンへ登録するようにします。 テンプレートのレンダラは、 ビュー・インスタンスの valueof メンバ関数に登録済みの列挙型の値を渡して、 対応する値を読み取って出力に使います。 繰り返しブロックの初期化にビュー・インスタンスiter メンバ関数を呼び、 次の繰り返し状況へ遷移させるために next メンバ関数を呼びます。 さらに、 ビュー・インスタンスに直接出力を生成させるときに、 expand メンバ関数を呼びます。

class page_base {
public:
    virtual ~page_base () {}
    virtual void valueof (int symbol, std::string& v) { v = ""; }
    virtual void valueof (int symbol, std::string::const_iterator& v1, std::string::const_iterator& v2) {}
    virtual void valueof (int symbol, long& v) { v = 0; }
    virtual void valueof (int symbol, double& v) { v = 0.0; }
    virtual void valueof (int symbol, bool& v) { v = false; }
    virtual void iter (int symbol) {}
    virtual void next (int symbol) {}
    virtual void expand (layout_type const& layout, std::size_t ip, span_type const& op, std::string& output) {}
    static void append_html (int escape_level, std::string::const_iterator first, std::string::const_iterator last, std::string& output);
    static void append_html (int escape_level, double x, std::string& output);
};

例えば、 suzume.cgi のテンプレートは recents ブロックと body スカラの 2 つのマークアップを使っています。

<ul class="entries">
{{#recents}}
<li>{{body}}</li>
{{/recents}}
</ul>

ビュー・オブジェクトはこれらに、 それぞれ RECENTS と BODY の列挙値を登録してテンプレートをアセンブルします。 その際、 recents は FOR ブロック、 body は STRING スカラとしてふるまうよう関連付けます。 この関連付けは WebObjectsバインディング・ファイルの記述を意識していますが、 コード中に埋め込むように、 やりかたを改めています。

struct suzume_view : public mustache::page_base {
    enum { RECENTS, BODY };

    // 途中略

    bool render (std::string& output)
    {
        std::string src;
        if (! slurp (src))
            return false;
        mustache::layout_type layout;
        layout.bind ("recents", RECENTS, mustache::FOR);
        layout.bind ("body",    BODY,    mustache::STRING);
        if (! layout.assemble (src))
            return false;
        layout.expand (*this, output);
        return true;
    }

    // 途中略
};

マークアップのふるまいは、 7 通りを指定可能です。

enum {
    STRING = 1, // valueof で取得した std::string を出力
    STRITER,    // valueof で取得した 2 つの文字列イテレータ間を出力
    INTEGER,    // valueof で取得した long を出力
    DOUBLE,     // valueof で取得した double を出力
    IF,         // valueof で取得した bool が真ならブロックを出力
    FOR,        // iter でループ初期化、 
                // valueof で 取得した bool が真の間ブロックを繰返し
                // next でループの次の繰返しへ移る
    CUSTOM      // expand で直接出力
};

テンプレート・エンジンは、 スカラに bind したマークアップに対して、 valueof メンバ関数で値を読み取って出力します。 FOR ブロックに bind したものに対して、 iter メンバ関数でループの初期化をし、 valueof メンバ関数でループの繰返し中かどうかを調べ、 next メンバでループの次の状況へ移します。 next メンバ関数の利用は必須ではなく、 suzume_view では、 next メンバ関数を使わずに、 valueof で SQLite3 の step 手続きを叩いて次のデータの読み取りをおこなっています。

struct suzume_view : public mustache::page_base {
    // 途中略

    void iter (int symbol)
    {
        if (RECENTS == symbol) data.recents_iter ();
    }

    void valueof (int symbol, bool& v)
    {
        if (RECENTS == symbol) v = data.recents_step ();
    }

    void valueof (int symbol, std::string& v)
    {
        if (BODY == symbol) data.recents_body (v);
    }

    // 途中略
};

このやりかたは、 C++11 のイテレータと相性が良く、 JSON オブジェクトよりも使い勝手が良くなったと感じています。