PEGパーサコンビネータ
を元になんちゃてCSSパーサを作ってみました。
参考元と変わらないところが多いですが、
正規表現を使わずに動くところが楽しいです。
PEGとこのライブラリをしかっり理解できていないため、
ここ変だな、という部分が多々あると思います。
できること
できないこと
- 「@import」など「@」から始まる文法の解析
- id,classや擬似クラスなどのセレクタは指定できず、要素名(単体)のみの解析ができます。
- プロパティと値が実在しない書き方でも、パスします。「foo {foobar:bar;}」
なんちゃってcssパーサ
<?php require 'PEG.php'; class CSSParser { protected $error = null; protected $css = null; /** * インスタンスを返す * * @return object */ function it() { static $o = null; return $o ? $o : $o = new self; } /** * cssパースを開始する * * @param string $css css * * @return boolen|array */ function parse($css) { $this->css = $css; //PHP5.3未満 $tohash = create_function( '$result', 'if($result === null) return null; $arr = array(); foreach ($result as $pair) { list($key, $value) = $pair; $arr[$key] = $value; } return $arr;' ); //コメント $comment = PEG::seq( '/*', PEG::many(PEG::tail(PEG::not('*/'), PEG::anything())), '*/' ); //空白や改行 $space = PEG::many1(PEG::char(chr(13).chr(10).chr(9).chr(32)));//"\r\n\t " //無視する要素 空白 コメント $ignore = PEG::memo(PEG::many(PEG::choice($space, $comment))); //属性や宣言の2文字目以降の文字セット $charBody = PEG::choice( PEG::choice( PEG::alphabet(), PEG::digit(), PEG::char('-_'), PEG::drop(PEG::error('invalid char')) ) ); //属性/宣言 $selector = $property = PEG::memo( PEG::join( PEG::choice( PEG::seq(PEG::alphabet(), PEG::many($charBody)), PEG::drop(PEG::error('invalid char')) ) ) ); //値 $value = PEG::first( PEG::join(PEG::many(PEG::char(';', true))), ';', PEG::drop($ignore) ); //属性:値; $declaration = PEG::memo( PEG::choice( PEG::seq( $property, PEG::drop($ignore), PEG::drop(':'), PEG::drop($ignore), $value ), PEG::drop(PEG::error('declaration')) ) ); //{}で囲まれた部分 declaration block(宣言ブロック) $declarationBlock = PEG::memo(PEG::hook($tohash, PEG::many($declaration))); $style = PEG::memo( PEG::hook( $tohash, PEG::seq( $selector, PEG::drop($ignore), PEG::drop('{'), PEG::drop($ignore), $declarationBlock, PEG::drop($ignore) ), PEG::drop('}'), PEG::drop($ignore) ) ); $styles = PEG::memo(PEG::many($style)); $parser = PEG::second($ignore, $styles, PEG::eos()); $res = $parser->parse($context = PEG::context($css)); if ($res instanceof PEG_Failure) { $this->setError($context->lastError()); return false; } return $res; } /** * エラーメッセージを記録する * * @param array $error PEG_IContext が記録したエラー * * @return ? */ protected function setError(Array $error) { list($offset,$message) = $error; $char = mb_substr($this->css, $offset, 1); $this->_error = array( 'offset' => $offset, 'message' => $message, 'char' => $char, ); } /** * エラー情報を取得する * * @return array */ public function getError() { return $this->_error; } }// end of class
使い方
<?php $css1 = 'a{font-size:12px;color:#DDD;} div {text-align:right;}'; $result1 = CSSParser::it()->parse($css1); var_dump($css1,$result1); /* string 'a{font-size:12px;color:#DDD;} div {text-align:right;}' (length=53) array 0 => array 'a' => array 'font-size' => string '12px' (length=4) 'color' => string '#DDD' (length=4) 1 => array 'div' => array 'text-align' => string 'right' (length=5) */ $css2 = 'a{font-size!:12px;}';//font-sizeの最後に「!」がついている $o2 = CSSParser::it(); $result2 = $o2->parse($css2); var_dump($css2,$result2,$o2->getError()); /* string 'a{font-size!:12px;}' (length=19) boolean false array 'offset' => int 11 'message' => string 'invalid char' (length=12) 'char' => string '!' (length=1) */