boost::spirit で HTML パーサを書く

spirit を使う時点で「書く」というより構築する,くらいになってしまうんだけど,それくらい boost::spirit は強力で便利で変態的(笑)

さて,今回の仕事は font タグを認識してリッチテキストコントロールに反映させるというもの.何に使うかは前回の日記あたりを見れば想像がつくかと.いえ,現時点ではタダノシュミデス.

ええと,対応すべきタグは font のしかも color 属性のみとします.
ただし,現時点では対応が簡単な b, s タグにも一応対応させてみました.

XMLBNF (http://www.w3.org/TR/REC-xml/) を参考にしていたんですが,最長一致な boost::spirit 君だと色々と修正する必要がありました.このあたりをうまく回避する方法があるなら誰か教えて欲しい.

日本語で書くと長くなるので,汚くて情けなくなるコードだけど晒しちゃいます.

using namespace std;
using namespace boost::spirit;


struct ParserData
{
    vector<wstring> keys;
    vector<wstring> vars;
//略
};

struct my_append {
    ParserData&     values;
    wstring         type_;
    my_append( const wstring& type, ParserData& vec ) : values(vec), type_(type) { }
    my_append( ParserData& vec ) : values(vec), type_(L"") { }

    template<class IteratorT>
    void operator()(IteratorT first, IteratorT last) const { // const が重要
        wstring s( first, last );

        values.keys.push_back( type_ );
        values.vars.push_back( s );

//略
    }
};

// 簡易 html パーザ
struct html_parser : public grammar<html_parser> {
    ParserData& v;

    html_parser( ParserData& vec ) : v(vec) { }

    template<typename ScannerT>
    struct definition {
        rule<ScannerT> document, element,
                       STag, content, ETag,
                       CharData,
                       S, Name, Attribute,
                       CharDataB, CharDataBE,
                       NameE, NameES;

        definition(const html_parser& self)
        {
//[1*]  document    ::= (element | BareElement)*
            document = +( element || Name[my_append(L"bare-element",self.v)] );
            
//[14]  CharData    ::= [^<&]* - ([^<&]* ']]>' [^<&]*) 
            CharData = anychar_p;

//[39*] element     ::= STag content ETag
            element  = STag >> content >> ETag;

//[40]  STag        ::= '<' Name (S Attribute)* S? '>'
//          STag      = '<' >> EName >> *(S >> Attribute) >> !S >> '>';
            STag      = '<'
                        >> NameES[my_append(L"tag-begin",self.v)]
                        >> *(S >> Attribute)
                        >> '>';

//[41]  Attribute   ::= Name Eq AttValue 
            Attribute = (+(CharData-'='-'>'))[my_append(L"key",self.v)]
                        >> '='
                        >> (('"' >> (+(CharDataBE-'"'))[my_append(L"val",self.v)] >> '"') ||
                            NameES[my_append(L"val",self.v)]);

//[42]  ETag        ::= '</' Name S? '>'
            ETag     = str_p("</") >> NameE[my_append(L"tag-end",self.v)] >> !S >> '>';

//[43*] content     ::= CharData? (element CharData?)*
//          content   = !NameE;
            content   = !NameE[my_append(L"content",self.v)]
                        >> *( element
                              >> !NameE[my_append(L"content",self.v)] );

//[3*]  S       ::= (' ' | '\t' | '\n' | '\r')+
            S        = space_p;

//[5]   Name        ::= (Letter | '_' | ':') (NameChar)* 
            CharDataB  = CharData - '<' - '/';  // Char without Begin-bracket
            CharDataBE = CharDataB - '>';   // Char without Begin, End-bracket
            Name     = +CharDataB;
            // Name without End bracket
            NameE    = +CharDataBE;
            // Name without End brackets and Spaces
            NameES   = +(CharDataBE - S);
        }

        const rule<ScannerT>& start() const { return document; }
    };
};

/// パーザ呼び出し用のラッパ関数
template<typename IteratorT>
parse_info<IteratorT>
parse_html(const IteratorT& first, const IteratorT& last, ParserData& v)
{
    html_parser the_parser(v);

    return parse(first, last, the_parser);
}

void CHtml2RichTextDlg::OnBnClickedOk()
{
    ParserData v;

    CString str;
    GetDlgItemText( IDC_EDIT1, str );
    wstring s = str;

    wcout << L"input : " << s << endl;
    parse_info<wstring::iterator> info = parse_html( s.begin(), s.end(), v );
    if (!info.hit) {
        cout << "Error! Point: " << distance(s.begin(), info.stop) << endl;
    }else{
        cout << "Parses OK:" << endl;
    }

    cout << "num of parsed strings : " << v.vars.size() << endl;
    for (vector<wstring>::size_type i = 0; i < v.vars.size(); ++i)
        wprintf( L"%2d: %-15s : %s\n", i, v.keys[i].c_str(), v.vars[i].c_str() );
//略
}

略と書いたところはリッチエディットとのインタフェース(木構造の構築と解析)なので省略.

たったこれだけで EBNF が実現できる!!
…と書きたいんだけど BNF の理解が足りないので結構苦労しました.

それにしても boost::spirit 使うとコンパイル時間がびっくりするくらい長くなりますね.現時点のミドルレンジ PC での最高クラスである Athlon 64 X2 3800+ でもこれだけかかるなら…,まだまだコンパイラの可能性はあるということだ!(コンパイル時間が長すぎで未実装なアイデアが大量にあるという期待)

実行形式:

参考:


追記:
酔いながら書いたんで文章めちゃくちゃです.