前回、openpear/PEGパーサコンビネータを使った簡易CSSパーサの続きです。
前回作成したのものを改良して、実践的に使えるものを目指します。
当面の目標は、
Softbank携帯のCSSをパースできるところまで、進めたいと思います。
PEGパーサコンビネータ
CSS関連資料
- 正しい知識を得たい人の爲のCSS2リファレンス
- 日本語で概略を掴めるので助かります。
- Cascading Style Sheets Level 2 Revision 1 (CSS 2.1) Specification
- 最後に頼るのはココ。
- Softbank 技術資料 XHTML編
- au,docomoと比べてこういう詳細な資料は助かります。
できること
できないこと
- 「@import」など「@」から始まる文法の解析
- softbankの技術資料に記載がないため、後回し
- プロパティと値が実在しない書き方でも、パスします。
- 「foo {foobar:bar;}」など。「foo {foobar:ああああ;}」もパスする。
- セレクタのグループ化は未対応
- 「;」の省略は未対応
div {color:#DDD} /* 本来、最後の「;」は省略できるが、パーサは通らない */
なんちゃってcssパーサ
<?php require 'PEG.php'; class CSSParser { protected $error = null; protected $css = null; /** * セレクタの解析結果を記録する */ static $logSelector = null; /** * @var array セレクタ(selector)のチェックリスト */ static $_selectorlist = array( '*' => array( 'null' => true, '*' => false, ' ' => true, 't' => false, '{' => true, '>' => true, '#' => true, ':' => true, ), ' ' => array( 'null' => false, '*' => true, ' ' => true, 't' => true, '{' => true, '>' => true, '#' => true, ':' => true, ), 't' => array( 'null' => true, '*' => false, ' ' => true, 't' => false, '{' => true, '>' => true, '#' => true, ':' => true, ), '>' => array( 'null' => false, '*' => false, ' ' => true, 't' => true, '{' => false, '>' => false, '#' => true, ':' => true, ), '#' => array( 'null' => true, '*' => false, ' ' => true, 't' => false, '{' => true, '>' => true, '#' => true, ':' => true, ), ':' => array( 'null' => true, '*' => false, ' ' => false, 't' => false, '{' => true, '>' => false, '#' => false, ':' => false, ), ); /** * インスタンスを返す * * @return object */ function it() { static $o = null; return $o ? $o : $o = new self; } /** * cssパースを開始する * * @param string $css css * * @return boolean|array */ function parse($css) { $this->css = $css; // PHP5.3未満 //配列を連想配列にする array(array('foo', 'boo')) => array(array('foo' => 'boo')) $tohash = create_function( 'Array $result', '$arr = array(); foreach ($result as $pair) { list($key, $value) = $pair; $arr[$key] = $value; } return $arr;' ); // PHP5.3未満 // 配列を文字列にする array('foo', 'boo') => 'fooboo' $toString = create_function( 'Array $array', ' $result = ""; foreach ($array as $v) $result .= $v; return $result; ' ); // コメント $comment = PEG::seq('/*', PEG::many(PEG::tail(PEG::not('*/'), PEG::anything())), '*/'); // 空白や改行 // スペース(0x20)水平タブ(0x09)行送り:LF(0x0A)復帰:CR(0x0D)書式送り:FF(0x0C) $space = PEG::memo(PEG::many1(PEG::char(chr(13).chr(10).chr(9).chr(32).chr(12)))); // 無視する要素 空白 コメント $ignore = PEG::memo(PEG::many(PEG::choice($space, $comment))); // 全称セレクタ $universalSelector = PEG::choice(PEG::char('*'), PEG::error('universalSelector')); // 子孫セレクタの結合子 $descendant = PEG::choice(PEG::first(PEG::many1(' ')), PEG::error('descendant')); // 子供セレクタの結合子 $child = PEG::choice('>', PEG::error('child')); $selectorChar = PEG::join(PEG::many1(PEG::choice(PEG::alphabet(), PEG::digit(), PEG::char('_-')))); // タイプセレクタ $typeSelector = PEG::choice($selectorChar, PEG::error('typeSelector')); // クラスセレクタ $classSelector = PEG::choice(PEG::join(PEG::seq('.', $selectorChar)), PEG::error('classSelector')); // IDセレクタ $idSelector = PEG::choice(PEG::join(PEG::seq('#', $selectorChar)), PEG::error('idSelector')); // リンク擬似セレクタ/ダイナミック擬似セレクタ $linkSelector = PEG::choice(PEG::join(PEG::seq(':', $selectorChar)), PEG::error('linkSelector')); $selector = PEG::memo( PEG::choice( $child, $descendant, $universalSelector, $classSelector, $idSelector, $linkSelector, $typeSelector, PEG::error('selector') ) ); $selectors = PEG::many1( PEG::choice( PEG::hook(array(__CLASS__, syntaxSelector), $selector), PEG::error('selectors') ) ); $charBody = PEG::choice( PEG::choice( PEG::alphabet(), PEG::digit(), PEG::char('-_'), PEG::error('invalid char') ) ); // 属性 $property = PEG::memo( PEG::choice( PEG::hook('trim', PEG::join(PEG::seq(PEG::alphabet(), PEG::many($charBody)))), PEG::error('invalid char') ) ); // 値 $value = PEG::memo( PEG::choice( PEG::first( PEG::join(PEG::many(PEG::char(';', true))), ';', PEG::drop($ignore) ), PEG::error('value') ) ); // 属性:値; $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( PEG::choice( PEG::hook( $toString, PEG::hook(array(__CLASS__, selector), $selectors) ), PEG::drop(PEG::error('style')) ), 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; } /** * セレクタの最終チェック * * @param array $array セレクタ * * @return array|PEG_Failure */ static function selector(Array $array) { $count = count($array); if ($count === 0) return $array; // 前後の空白を省略してよい結合子 $omissibleCombinators = array('>',chr(32)); // 前後の空白を削除する foreach ($array as $n => $v) { if ($n < $count - 1) { foreach ($omissibleCombinators as $omissibleCombinator) { if ($array[0] === chr(32)) {// 最初の空白を除外 unset($array[0]); } elseif ($array[$count - 1] === chr(32)) {// 最後尾の空白を除外 unset($array[$count - 1]); } elseif ($array[$n] === $omissibleCombinator && $array[$n+1] === chr(32)) { unset($array[$n+1]); } elseif ($array[$n] === chr(32) && $array[$n+1] === $omissibleCombinator) { unset($array[$n]); }//var_dump($count !== count($array)); //変更があれば最初から if ($count !== count($array)) { $count = count($array);// カウンタを振りなおす reset($array);// 内部ポインタを先頭にする reset($omissibleCombinators); } } } } // 添字を振り直す $array = array_merge(array_diff($array, array())); if ($count > 0) { // 初期化。配列の最後尾を登録する self::$logSelector = null; $result = self::syntaxSelector($array[$count - 1]); if($result instanceof PEG_Failure) return PEG::failure(); // チェック。登録した配列の最後尾と「{」の関係をチェックする $result = self::syntaxSelector('{'); if($result instanceof PEG_Failure) return PEG::failure(); } self::$logSelector = null; return $array; } /** * セレクタのチェック。 * セレクタとして適切であれば、引数をそのまま返す。 * セレクタとして適切でなければ、PEG_Failureインスタンスを返す。 * (PEG_Failureインスタンスを返すとパーサは失敗する) * * @param string $string 解析されたセレクタ * * @return PEG_Failure|string */ static function syntaxSelector($string) { // 明らかにセレクタに適当でないものを事前チェックする if($string === '' || $string === false) return PEG::failure(); if(self::$logSelector === null && $string === '{') return PEG::failure(); if(self::$logSelector === null && $string === '>') return PEG::failure(); // セレクタを分類する $test = $string; for (;;) { if ($test === '*') break; if ($test === ' ') break; if ($test === '{') break; if ($test === '>') break; if ($test === null) { $test = 'null'; break; } if (mb_substr($test, 0, 1) === '#' || mb_substr($test, 0, 1) === '.') { $test = '#'; break; } if (mb_substr($test, 0, 1) === ':') { $test = ':'; break; } if (preg_match('/[\w\-]+/', $test)) { $test = 't'; break; } throw new InvalidArgumentException('Invalid argument'); } //初回は結果を記録するだけに済ます if (self::$logSelector === null) { self::$logSelector = $test; return $string; } // 前回と今回の記録を使って、有効かどうかチェックする if (!self::$_selectorlist[self::$logSelector][$test]) { self::$logSelector = null; return PEG::failure(); } // 今回の結果を記録する self::$logSelector = $test; return $string; } }// end of class
使い方
<?php $css1 = ' /* コメント */ div {/* コメント */ /* コメント */font-size:12px;color/* コメント */:/* コメント */#DDD/* コメント */;/* コメント */ } div a { text-align:right; } div * #id.class a:link { color:#DDD; } '; $result1 = CSSParser::it()->parse($css1); var_dump($css1,$result1); /* string ' /* コメント */ div {/* コメント */ /* コメント */font-size:12px;color/* コメント */:/* コメント */#DDD/* コメント */;/* コメント */ } div a { text-align:right; } div * #id.class a:link { color:#DDD; } ' (length=233) array 0 => array 'div' => array 'font-size' => string '12px' (length=4) 'color' => string '#DDD/* コメント */' (length=22) //値の解析は未実装 1 => array 'div a' => array 'text-align' => string 'right' (length=5) 2 => array 'div * #id.class a:link' => array 'color' => string '#DDD' (length=4) */ $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) */ $css3 = 'div* {font-size!:12px;}';//divの直後に全称セレクタ(*)がついている $o3 = CSSParser::it(); $result3 = $o3->parse($css3); var_dump($css3,$result3,$o3->getError()); /* string 'div* {font-size!:12px;}' (length=23) boolean false array 'offset' => int 3 'message' => string 'selectors' (length=9) 'char' => string '*' (length=1) */