CSS風の文法のパーサを作るチュートリアル

CSS風の文法のパーサを作るチュートリアル

このチュートリアルでは、CSS風のデータ言語(CSS Like Data Language、略してCDL)のパーサを作りながら本ライブラリの使い方を解説していく。

CDLの定義

まずCDLについて定義していく。

CDLは、CSS風の文法で連想配列の様なデータ構造を記述することができる。

foobar {
    foo : value;
    bar : value;
}
hogefuga {
    hoge : value;
    fuga : value;
}

CSSとは違って、セレクタはないので以下の文法は不正である。

foo > bar {
    foo : value;
    bar : value;
}

"#"に続く行末までの文字はコメントとして見なされる。

# コメント
foobar {
    # コメント
    foo : bar;
}

連想配列はCSSとは違って入れ子にできる。

foobar {
    hoge : fuga;
    piyo {
        foo : bar;
    }
}

属性名は、CSSと同様にアルファベットだけでなく"-"も受け付ける。

foo-bar {
    line-height : 3;
}

パーサの構築

上筆した文法にしたがってパーサを構築していく。

パーサは主にPEGクラスメソッドを通じて生成する。

コメント

$commentにCDLのコメントのパーサを定義している。

上の行のPHPのコメントには、PEGでの定義を書いた。

<?php
// comment <- "#" (!("\r" / "\n") .)* ("\r\n" / "\n" / "\r")?
$comment = PEG::seq('#', PEG::many(PEG::char("\r\n", true)), PEG::optional(PEG::newLine()));
空白
<?php
// space <- ("\r" / "\n" / "\t" / " ")+
$space = PEG::many1(PEG::char("\r\n\t "));
無視できる要素

空白とコメントは無視出来る要素である。

<?php
// ignore <- (space / comment)*
$ignore = PEG::memo(PEG::many(PEG::choice($space, $comment)));

PEG::memo()メソッドは、引数に与えたパーサの結果をコンテキストとその位置に対してキャッシュするパーサを返す。パーサを線形時間で動作させるのに必要となる。

属性名

"foo : bar;"のfooの部分。

<?php
// symbol <- [-_a-zA-Z]+
$symbol = PEG::memo(PEG::join(PEG::many1(PEG::char('abcdefghijklmnopqrstuvwqyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'))));

$symbol->parse(PEG::context('foo')); // => 'foo'
$symbol->parse(PEG::context('foo-bar')); // => 'foo-bar'
$symbol->parse(PEG::context(' foo-bar')); // => 失敗
属性の値としての文字列

";"以外の値を文字列の一部としてみなす。

"foo : bar;"の": bar;"の部分。

<?php
// str <- ":" ignore (!";")* ";" ignore
$str = PEG::memo(PEG::third(':', $ignore, PEG::join(PEG::many(PEG::char(';', true))), ';'));
属性
<?php
// attr <- symbol ignore ":" ignore (str / hash) ignore
$attr = PEG::memo(PEG::seq($symbol, PEG::drop($ignore), PEG::choice($str, PEG::ref($hash)), $ignore));

// attrs <- attr*
$tohash = function(Array $result) {
    $arr = array();
    foreach ($result as $pair) {
        list($key, $value) = $pair;
        $arr[$key] = $value;
    }
    return $arr;
};
$attrs = PEG::memo(PEG::hook($tohash, PEG::many($attr)));

$hashはまだ定義していないのでPEG::ref()メソッドで包んで渡す。

$tohashは属性名と値の配列を連想配列にする関数である。

連想配列
<?php
// hash <- "{" ignore attrs "}"
$hash = PEG::memo(PEG::third('{', $ignore, $attrs, '}'));
CDLパーサ
<?php
// parser <- ignore attr*
$parser = PEG::second($ignore, $attrs, PEG::eos());

PEG::eos()はコンテキストの終端にヒットするパーサを返す。

コンテキストの全ての文字列を消費出来なかった場合は失敗するためである。

パーサまとめ

以上の定義を一つにまとめると以下のようになる。

<?php
include_once dirname(__FILE__) . '/peg_trunk/code/PEG.php';

/* comment <- "#" (!("\r" / "\n") .)* ("\r\n" / "\n" / "\r")?
 * space   <- ("\r" / "\n" / "\t" / " ")+
 * ignore  <- (space / comment)*
 *
 * symbol  <- [-_a-zA-Z]+
 * str     <- ":" ignore (!";")* ";" ignore
 * attr    <- symbol ignore (str / hash) ignore
 *
 * attrs   <- attr*
 * hash    <- "{" ignore attrs "}"
 *
 * parser  <- ignore attrs
 */

$comment = PEG::seq('#', PEG::many(PEG::char("\r\n", true)), PEG::optional(PEG::newLine()));
$space   = PEG::many1(PEG::char("\r\n\t "));
$ignore  = PEG::memo(PEG::many(PEG::choice($space, $comment)));

$symbol  = PEG::memo(PEG::join(PEG::many1(PEG::char('abcdefghijklmnopqrstuvwqyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_'))));
$str     = PEG::memo(PEG::third(':', $ignore, PEG::join(PEG::many(PEG::char(';', true))), ';'));
$attr    = PEG::memo(PEG::seq($symbol, PEG::drop($ignore), PEG::choice($str, PEG::ref($hash)), PEG::drop($ignore)));

$tohash  = function(Array $result) { 
    $arr = array();
    foreach ($result as $pair) {
        list($key, $value) = $pair;
        $arr[$key] = $value;
    }
    return $arr;
};
$attrs   = PEG::memo(PEG::hook($tohash, PEG::many($attr)));
$hash    = PEG::memo(PEG::third('{', $ignore, $attrs, '}'));

$parser  = PEG::second($ignore, $attrs, PEG::eos());

試してみる

var_export($parser->parse(PEG::context('
foobar {
    # コメント
    foo : bar;
    foo-bar : fugahoge;
    piyo {
        hoge :fuga;
    }
}
')));
/* 結果
array (
  'foobar' => 
  array (
    'foo' => 'bar',
    'foo-bar' => 'fugahoge',
    'piyo' => 
    array (
      'hoge' => 'fuga',
    ),
  ),
)
*/

var_export($parser->parse(PEG::context('
foo : bar;
hoge {
    fuga : piyo;
}
')));
/* 結果
array (
  'foo' => 'bar',
  'hoge' => 
  array (
    'fuga' => 'piyo',
  ),
)
*/

var_export($parser->parse(PEG::context('
foo : bar;
hoge {
    fuga : piyo;
'))); // { }の対応が合っていないので失敗する
 
?
閉じる
C-s
保存
C-a
行頭へ移動
C-e
行末へ移動
C-p
一行上へ
C-n
一行下へ
C-f
右に移動
C-b
左に移動
C-m
改行
C-h
backspace