zz log

zaininnari Blog

openpear/PEGパーサコンビネータを使った簡易CSSパーサ

PEGパーサコンビネータ

を元になんちゃてCSSパーサを作ってみました。
参考元と変わらないところが多いですが、
正規表現を使わずに動くところが楽しいです。

PEGとこのライブラリをしかっり理解できていないため、
ここ変だな、という部分が多々あると思います。

できること

  • cssのコメント「/* xxxx */」の無視
  • 基本的なcss「a{font-size:12px;color:#DDD;} div {text-align:right;}」の解析
  • 解析できなかった文字の表示(中途半端)

できないこと

  • 「@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)
*/