メッセージ・ヘッダ風のフロントマター

Markdown の前にブログ生成等のふるまいを記述するフロントマターには、 YAML をはじめとして、 いろんな形式が使われていますが、 メッセージ・ヘッダ風の簡略なものでも良いのではないかとスキャナを書いてみました。 YAML はスラッシュ 3 つで区切りますが、 ヘッダ風ではスラッシュ 2 つで区切ることにします。 MIME マルチパートの区切りがスラッシュ 2 つで始まることと、 一見 YAML のように見えることにひっかけてあります。

--
layout : default
uri    : frontmatter/index.html
--
メッセージ・ヘッダ風のフロントマター
====================================

一見 YAML フロトマターのように見えますが、 YAML ではありません。

スキャンに成功すると、 m_header メンバに名前と値を格納します。 m_size メンバはフロントマターのバイト数で、 この分をスキップすると本体の Markdown を取り出すことができます。

#include <map>
#include <string>

struct frontmatter_type {
    std::map<std::string,std::string> m_header;
    std::size_t m_size;

    bool scan (std::string const& src);
};

フラットなデータ記述なので、 まず、 Perl 互換正規表現で記述します。 通常のメッセージ・ヘッダとは異なり、 区切り記号を使っているので、 空行があっても良いことにしておきます。

qr{
    --\n+
    (?:[A-Za-z0-9][A-Za-z0-9_.~-]*[ ]*:[ ]*[^\n]*\n
       (?:(?:[ ]+[^\n]*)?\n)* )*
    --\n
}msx;

これから遷移表を作ります。 開始状態は S2 です。

S2 : '-' S3
S3 : '-' S4
S4 : "\n" S5
S5 : [A-Za-z0-9] S6 | "\n" S5
S6 : [A-Za-z0-9_.~-] S6 | ' ' S7 | ':' S8
S7 : ' ' S7 | ':' S8
S8 : [\x21-0xff] S9 | "\n" Sa | ' ' S8
S9 : [\x20-0xff] S9 | "\n" Sa
Sa : '-' Sb | [A-Za-z0-9] S6 | "\n" Sa | ' ' S9
Sb : '-' Sc
Sc : "\n" Sd
Sd : MATCH

遷移表から BASE-CHECK 配列 RULE を作成し、 m_headerm_size へ書き込むアクションを追加します。 RULE は 16 進数 3 桁の並びで、 桁は上からアクション番号、 遷移先状態番号、 遷移元状態番号になっています。 遷移元状態番号が state に一致すると状態遷移することができます。

static inline unsigned int
rule_exist (unsigned int const rule[], int const nrule, int const state, int const i)
{
    return 0 <= i && i < nrule && (rule[i] & 0x00f) == state;
}

bool
frontmatter_type::scan (std::string const& src)
{
    enum { NRULE = 31 };
    static const char CODE[] =
      // @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_ !"#$%&'()*+,-./0123456789:;<=>?
        "@@@@@@@@@@D@@@@@@@@@@@@@@@@@@@@@E@@@@@@@@@@@@BG@CCCCCCCCCCF@@@@@"
      // @ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~
        "@CCCCCCCCCCCCCCCCCCCCCCCCCC@@@@G@CCCCCCCCCCCCCCCCCCCCCCCCCC@@@G";
    static const int BASE[] = {
        0-2, 1-2, 2-4, 3-3, 5-2, 11-5, 13-2, 19-2, 25-2, 7-2, 29-4, 30-1
    };
    static const unsigned int RULE[NRULE] = {
        0x032, 0x043, 0x054, 0x165, 0x155, 0x266, 0x266, 0x0cb, 0x276, 0x286,
        0x266, 0x077, 0x087, 0x398, 0x398, 0x3a8, 0x388, 0x398, 0x398, 0x499,
        0x499, 0x4a9, 0x499, 0x499, 0x499, 0x5ba, 0x56a, 0x5aa, 0x59a, 0x0dc,
        0x61d,
    };
    m_header.clear ();
    std::size_t h_first = 0, h_second = 0;
    std::size_t v_first = 0, v_second = 0;
    int state = 2;
    for (std::size_t pos = 0; 1 < state && pos < src.size (); ++pos) {
        int const ch = static_cast<unsigned char> (src[pos]);
        int const code = ((8 == state || 9 == state) && ' ' < ch) ? 3 : ch < 127 ? CODE[ch] - '@' : 0;
        int const jump = BASE[state - 2] + code;
        int const match = BASE[state - 2] + 1;
        unsigned int const rule = rule_exist (RULE, NRULE, state, jump)  ? RULE[jump]
                                : rule_exist (RULE, NRULE, state, match) ? RULE[match]
                                : 0;
        state = (rule & 0x0f0) >> 4;
        switch ((rule & 0xf00) >> 8) {
        case 1:
            h_first = h_second = pos;
            break;
        case 2:
            h_second = pos;
            break;
        case 3:
            v_first = v_second = pos;
            break;
        case 4:
            if (' ' < ch)
                v_second = pos + 1;
            break;
        case 5:
            if (' ' < ch) {
                std::string h = src.substr (h_first, h_second - h_first);
                m_header[h] = src.substr (v_first, v_second - v_first);
                h_first = h_second = pos;
            }
            break;
        case 6:
            m_size = pos;
            break;
        }
    }
    if (1 != state) {
        m_header.clear ();
        m_size = 0;
    }
    return 1 == state;
}