前回作成したものだと、適当でない文字列のエラーを出す(●●行目のココが怪しいよ風にしたい)のが難しいため、やっぱりPEGに逆戻りしました。
セレクタとブロック部分(div {color:red})は、「{」「:」「;」「}」などで緩くに分割して、中身をあとでチェックする方式にしました。
@規則(@importなど)が意外と面倒です。
例えば@importの場合は、
- はじまり「@import 」は共通
- 次に続くものが、「url()」がついていなくてもOK
- 「url()」がついている場合:「()」の内側に、囲む識別子として「"」「'」「(なし)」の3種類がある
- 「url()」がついていない場合:囲む識別子として「"」「'」の2種類がある
- 次に続くものとして、メディアタイプが続く
- メディアタイプは、「,」で区切って複数記述可能(「screen,projection,tv」)
- 最後に「;」で終わる
- @規則を解析できなかった場合、次の「;」が現れるまでか、「{}」(ブロック)が現れるまでのものを無視する
という具合です。
下記は、「URL関数を使い、メディアタイプなし」で成功を期待するテストとパーサ本体の一部です。
(@規則を解析できなかった場合を除外してあります。)
また、パーサの作成には、HatenaSyntaxを参考にしました。
<?php class CssParserTest extends PHPUnit_Framework_TestCase { /** * @group now * @dataProvider testParseAtMarkImportUrlOnlySimpleData */ public function testParseAtMarkImportUrlOnlySimple($e,$i) { $o = new CssParser_AtMark(PEG::anything()); $r = call_user_func(array($o,'parse'), PEG::context($i)); $expect = array(new CssParser_Node('import',$e,mb_strpos($i,$e))); $this->assertSame(true, $r == $expect); } function testParseAtMarkImportUrlOnlySimpleData() { return array( // ダブルクォーテーション array('style.css', '@import url("style.css");'), array('style.css);', '@import url("style.css);");'), array('style.css\"', '@import url("style.css\"");'), array('style.css\");', '@import url("style.css\");");'), array('style.css', '@import /*");*/url(/*");*/"style.css"/*");*/)/*");*/;/*");*/'), // シングルクォーテーション array('style.css', '@import url(\'style.css\');'), array('style.css);', '@import url(\'style.css);\');'), array('style.css\\\'', '@import url(\'style.css\\\'\');'), array('style.css\\\');', '@import url(\'style.css\\\');\');'), // クォーテーションなし array('style.css', '@import url(style.css);'), array('style.css', '@import url(style.css););'), array('style.css\\\'', '@import url(style.css\\\');'), array('style.css\\\'', '@import url(style.css\\\'););'), array('style.css', '@import /*");*/url(style.css)/*");*/;/*");*/'), // ()の内側のコメントはNG ); } }
<?php class CssParser_AtMark implements PEG_IParser { protected $parser; /** * construct * * @param PEG_IParser $parser PEG_IParser * * @return unknown_type */ function __construct(PEG_IParser $parser) { foreach (array('Double' => '"', 'Single' => '\'', 'No' => null) as $n => $v) { $n = '_importUrlOnly'.$n.'Quotes'; ${$n} = PEG::seq( PEG::drop('@import'.chr(32), $ignore, 'url(', $ignore, ($v !== null) ? $v :''), // @import url( まで new CssParser_NodeCreater( // ノードを作成する 'import', PEG::join( PEG::seq( PEG::many1( PEG::anything(), PEG::drop( // ↓ 「("|'|)」+「);」を先読み、但し「("|')」がエスケープされている場合は除く PEG::not(($v !== null) ? PEG::seq(PEG::char('\\',true), $v, $ignore) : $ignore,')', $ignore, ';') ) ), // ↓ 上記のPEG::many1が失敗(PEG::notが成功した)ときの文字(「("|'|)」+「);」の直前の文字) ($v !== null) ? PEG::seq(PEG::anything(),PEG::anything()) : PEG::anything() ) ) ), PEG::drop(($v !== null) ? $v : '', $ignore, ')', $ignore, ';') // ); まで ); } $this->parser = PEG::choice($_importUrlOnlyDoubleQuotes,$_importUrlOnlySingleQuotes,$_importUrlOnlyNoQuotes); } /** * パースに失敗した場合はPEG_Failureを返すこと。 * 成功した場合はなんらかの値を返すこと。 * * @param PEG_IContext $context PEG_IContext * * @see PEG/PEG_IParser#parse($c) * * @return mixed */ function parse(PEG_IContext $context) { return $this->parser->parse($context); } }