zz log

zaininnari Blog

cssparser : @規則(@import)

前回作成したものだと、適当でない文字列のエラーを出す(●●行目のココが怪しいよ風にしたい)のが難しいため、やっぱり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);
  }

}