正規表現で書き直しました。
追記
list*、background*、border* に値が任意に順序を指定できますが、実装が抜けています。
参考
- uupaa-js - Project Hosting on Google Code
- 高速でコンパクト, 未来指向の JavaScript ライブラリ
- こちらのcssパーサを移植して、ソフトバンクcss向けに色々追加しました。
- uu.css.parse.js - uupaa-js - Project Hosting on Google Code
- 高速でコンパクト, 未来指向の JavaScript ライブラリ
- latest log
- uupaa-jsを作った方のブログ
- 軽量化(実効速度&ライブラリサイズ)だけでなく、精度まで追求したエントリーの数々は、見ていて勉強になります。ただし、なるほど、まったくわからん状態のことも多々あります。
- Softbank 技術資料 XHTML編
- cssの詳細データ
できること・できないこと
- cssのコメント「/* xxxx */」の無視
- 基本的なcss「a{font-size:12px;color:#DDD;} div {text-align:right;}」の解析
- 解析できなかった文字は無視される。
- セレクタの解析
- 宣言ブロックのバリデート
- ソフトバンクの仕様書にあるcssをチェック
- 力技でチェックしています。
- 「;」の省略に対応
- ソフトバンクの仕様書にあるcssをチェック
- 「@import」など「@」から始まる文法の解析
- softbankの技術資料に記載がないため、後回し
色々
- softbankの技術資料に記載されているものだけチェックしています。技術資料に載っているのは最低限の部分なので、拡張されている部分には対応いていません。
- 「color:red」などの宣言ブロックのチェックは、力技で解決しています。
- 特にfontなど指定する順序が任意&省略できる項目が多い、ものなってくると、途端に一筋縄でいきません。
<?php /** * 高速でコンパクト, 未来指向の JavaScript ライブラリ * uupaa.js (http://code.google.com/p/uupaa-js) * uupaa.js is licensed under the terms and conditions of the MIT licence. * (http://uupaa-js.googlecode.com/svn/trunk/0.7/doc/LICENSE.htm) * の * cssパーサ部分 * ( http://code.google.com/p/uupaa-js/source/browse/trunk/0.7/uu.css.parse.js ) * をPHPに移殖し、ソフトバンクのcssをチェックできるようにしたものです。 * * @license MIT License */ class CssParser { protected $setError = array(); protected $selectorParseList = array( 'universal' => '/^(\*)/', 'descendant' => '/^( )/', 'id' => '/^(#[a-zA-Z][\w\-]*)/', 'class' => '/^(\.[a-zA-Z][\w\-]*)/', 'child' => '/^(>)/', //Pseudo-classes 'link' => '/^(:(link|visited))/', 'dynamic' => '/^(:(link|visited|hover|active|focus))/', //Pseudo-elements //type 'type' => '/^([a-zA-Z][\w\-]*)/', ); /** * 文字列をパースして返す。 * * @param string $css string * * @return CssParser_Node */ function parse($css) { $rv = array('specs' => array(), 'data' => array()); $escape = $ignore = 0; $css = self::cssClean($css); if(!isset($css)) return $result; $ary = preg_split('/\s*\{|\}\s*/', $css, -1, PREG_SPLIT_NO_EMPTY); if(count($ary) % 1 === 0) throw new InvalidArgumentException; for ($i = 0; $i < count($ary); $i += 2) { // 初期化 $expr = $ary[$i]; // "E>F,G" $decl = trim($ary[$i + 1]); // "color:red;text-aligh:left" $exprs = preg_split('/\s*,\s*/', $expr, -1, PREG_SPLIT_NO_EMPTY); // ["E>F", "G"] $decls = preg_split('/\s*;\s*/', $decl . ';', -1, PREG_SPLIT_NO_EMPTY); // ["color:red", "text-align:left"] $gd1 = $gd2 = $gp1 = $gp2 = array(); $gd1i = $gd2i = $gp1i = $gp2i = -1; // 処理 for ($k = 0, $kz = count($decls); $k < $kz; ++$k) { $ignore = 0; if ($decls[$k]) { $both = preg_split('/\s*:\s*/', $decls[$k], -1, PREG_SPLIT_NO_EMPTY); $prop = array_shift($both); // "color:red" -> "color" $val = implode(':', $both); // "color:red" -> "red" if (mb_strpos(chr(92), $val)) { ++$ignore; } elseif (preg_match('/\!\s*important/i', $val)) { // [!important] rule $val = preg_replace('/\s*!\s*important\s*/i', '', $val); // trim "!important" // TODO 速度優先モードのみ // 常に成功する // uu.config.light is light weight mode //ja 1 で速度優先モードを有効にする // valid = (!uu.config.light && valids[prop]) ? // uu.css.validate[prop](val).valid : 1; $valid = (self::blockValidate($prop, $val) === true) ? 1 : 0; if ($valid) { $gd2[++$gd2i] = $prop . ':' . $val; $gp2[++$gp2i] = array('prop' => $prop, 'val' => $val); } else { ++$ignore; } } else { // [normal] rule $valid = (self::blockValidate($prop, $val) === true) ? 1 : 0; if ($valid) { $gd1[++$gd1i] = $prop . ':' . $val; // "color:red" $gp1[++$gp1i] = array('prop' => $prop, 'val' => $val); //{prop:"color",val:"red"} } else { ++$ignore; } } if($ignore) $this->setError('"' . $prop . ":" . $val + '" ignore decl'); } } // セレクタの前処理 // セレクタを解析できない場合,宣言ブロックごと(グループ化されているものも)無視 foreach ($exprs as $n => $v) { $tmp = $this->selectorValidate( $v, array( 'parseList' => array( 'adjacent' => null, 'adjacent' => null, 'attribute' => null, 'first-child' => null, 'language' => null, 'first-line' => null, 'first-letter' => null, 'before-after' => null, ) ) ); if($tmp['valid'] !== true) { continue 2; } else { $exprs[$n] = $tmp['cleanSelector']; } } // セレクタの処理 for ($j = 0, $jz = count($exprs); $j < $jz; ++$j) { $v = $exprs[$j]; // 重みを計算する $spec = $this->_calcSpec($v); if (count($gd1)) { // normal rule if(!isset($spec, $rv['data'][$spec])) $rv['specs'][] = $spec; $rv['data'][$spec][] = array('expr' => $v, 'decl' => $gd1, 'pair' => $gp1); } if (count($gd2)) { // !important rule $spec += 10000; if(!isset($spec, $rv['data'][$spec])) $rv['specs'][] = $spec; $rv['data'][$spec][] = array('expr' => $v, 'decl' => $gd2, 'pair' => $gp2); } } } // 重みをソート sort($rv['specs']); return $rv; } /** * セレクタの重みの計算 * * @param string $expr string * * @return Number: spec value */ function _calcSpec($expr) { $a = $b = $c = 0; $_specList = array( array('/#[\w\x00C0-\xFFEE\-]+/' , 'a'), // id array('/\.[\w\x00C0-\xFFEE\-]+/' , 'b'), // class array('/\w+/' , 'c'), // E ); foreach ($_specList as $n => $_spec) for ($i = 0, $expr = preg_replace($_spec[0], '', $expr, -1, $count); $i < $count; $i++) { ${$_spec[1]}++; } return $a * 100 + $b * 10 + $c; } /** * チェック * * @param string $prop string * * @return ? */ static function validate($prop) { return $prop; } /** * セレクタのチェックをする。 * * @param string $selector string without comma * @param array $option option * * @return array */ function selectorValidate($selector,Array $option = array()) { $selector = trim($selector); $result = array( 'selector' => $selector, 'cleanSelector' => null, 'parsedSelector' => array(), 'error' => array(), 'valid' => false, ); $parseList = isset($option['parseList']) ? $option['parseList'] : array(); //余分な空白を取り除く $result['cleanSelector'] = $selector = preg_replace( array('/\s+/', '/\s*>\s*/', '/\s*\+\s*/'), array(' ', '>', '+'), $selector ); // 単純セレクタ(simple selector)や結合子(combinators)に分割する $result['parsedSelector'] = self::selectorParse($selector, $parseList); // 単純セレクタ(simple selector)や結合子(combinators)の構文をチェックする $syntax = self::selectorSyntax($result['parsedSelector']); if(!empty($syntax)) $result['error'] = array_merge($result['error'], $syntax); //エラーがなければ成功 if(empty($result['error'])) $result['valid'] = true; return $result; } /** * 単純セレクタ(simple selector)や結合子(combinators)に分割する * * @param string $selector dirty selector ('div * div#id > .class') * @param array $parseList selectorSyntaxに渡すオプション。 * 検索を無効にする場合は、nullを指定。書き換える場合は、正規表現を記述。 * array('child' => null, 'add name' => 'regex') * * @return array */ function selectorParse($selector,Array $parseList = array()) { $res = array(); // 単純セレクタ(simple selector)や結合子(combinators)に分割する $before = 0; while (mb_strlen($selector) !== 0) { foreach (array_merge($this->selectorParseList, $parseList) as $type => $pattern) { if ($pattern !== null && preg_match($pattern, $selector, $matches)) { $res[] = array($type, $matches[1]); $selector = mb_substr($selector, mb_strlen($matches[1])); $seek += mb_strlen($matches[1]); break; } } if ($seek === $before) { $result['error'] = array('parse' => mb_substr($selector, 0, 1)); break; } $before = $seek; } return $res; } /** * 単純セレクタ(simple selector)や結合子(combinators)の構文をチェックする * * @param array $selector array(array('universal', '*'),array('type', 'div')) * * @return array */ function selectorSyntax(Array $selector) { $result = array(); $selectorSyntaxList = array( 'start' => array( 'end' => false, 'combinator' => false, 'universal' => true, 'type' => true, 'id' => true, 'link' => true, ), 'combinator' => array( 'end' => false, 'combinator' => false, 'universal' => true, 'type' => true, 'id' => true, 'link' => true, ), 'universal' => array( 'end' => true, 'combinator' => true, 'universal' => false, 'type' => false, 'id' => false, 'link' => true, ), 'type' => array( 'end' => true, 'combinator' => true, 'universal' => false, 'type' => false, 'id' => true, 'link' => true, ), 'id' => array( 'end' => true, 'combinator' => true, 'universal' => false, 'type' => false, 'id' => true, 'link' => true, ), 'link' => array( 'end' => true, 'combinator' => true, 'universal' => false, 'type' => false, 'id' => true, 'link' => true, ), ); //グルーピングルール $group = array( 'combinator' => array('descendant', 'child'), 'id' => array('id', 'class'), 'link' => array('link', 'dynamic'), ); // 分割したセレクタの構文をチェックする $before = 'start'; $selector[] = array('end' , '{'); // 一番最後にと終了を意味する識別文字を挿入する foreach ($selector as $n => $v) { $test = $v[0]; foreach ($group as $name => $member) { // セレクタのグルーピング if(in_array($test, $member)) $test = $name; } if ($selectorSyntaxList[$before][$test] === false) { //有効かどうかチェックする $result[($n === 0) ? $n : $n - 1] = ($n === 0) ? $selector[$n] : $selector[$n - 1]; break; } $before = $test; // 今回の結果を記録する } return $result; } /** * block(「{」~「}」)の構文チェック * true :成功 * 文字列:失敗したcssのvalue値 * false :失敗するcssのvalue値がない場合に返す * * @param string $prop property。改行を含まないこと。内部の余分な空白は削除しなくてもOK。グループでないこと。 * @param string $val value。改行を含まないこと。内部の余分な空白は削除しなくてもOK。 * * @return boolean|string */ static function blockValidate($prop,$val) { $prop = trim($prop); $val = trim($val); if($prop === '' || $val === '') return false; $length = '(?:[0-9]{1,4}(?:px|em|ex|in|cm|mm|pt|pc)|0)'; $percentage = '(?:[0-9]{1,3}%)'; $color = '(?:black|silver|gray|white|maroon|red|purple|fuchsia|green|lime|olive|yellow|navy|blue|teal|aqua|#[0-9a-fA-F]{3}|#[0-9a-zA-Z]{6})'; $uri = '(?:.*?)'; $absolutesize = '(?:xx-small|x-small|small|medium|large|x-large|xx-large)'; $relativesize = '(?:smaller|larger)'; $border_style = '(?:none|hidden|solid|groove|dotted|dashed|double|ridge|inset|outset)'; $length_percentage_auto = "($length|$percentage|auto)"; $length_percentage_1_4_auto = "(?:($length|$percentage)\s*".str_repeat("($length|$percentage)?\s*", 3)."|auto)"; $left_right_center_justify = '(left|right|center|justify)'; $padding_xxx = "($length|$percentage)"; $length_percentage = "($length|$percentage)"; $thin_medium_thick_length = "(thin|medium|thick|{$length})"; $thin_medium_thick_length_1_4 = "(thin|medium|thick|$length)\s*".str_repeat("(thin|medium|thick|$length)?\s*", 3); $color_transparent = "($color|transparent)"; $color_transparent_1_4 = "($color|transparent)\s*".str_repeat("($color|transparent)?\s*", 3); $none_hidden_solid_groove = "($border_style)"; $none_hidden_solid_groove_1_4 = "($border_style)\s*".str_repeat("($border_style)?\s*", 3); $thin_none_color = "((?:thin|medium|thick)|(?:none|hidden|solid)|$color)"; $normal_length_percentage = "(normal|$length|$percentage)"; $baseline_sub_super_top = "(baseline|sub|super|top|text-top|middle|bottom|text-bottom)"; $disc_circle_square_decimal = "(disc|circle|square|decimal|lower-roman|upper-roman|lower-alpha|upper-alpha|none)"; $uri_none = "(url\([\"\']?{$uri}[\"\']?\)|none)"; $inside_outside = "(inside|outside)"; $disc_uri_inside = "((?:disc|circle|square|decimal|lower-roman|upper-roman|lower-alpha|upper-alpha|none)|(?:url\([\"\']?{$uri}[\"\']?\)|none)|(?:inside|outside))"; $repeat = "(repeat|repeat-x|repeat-y|no-repeat)"; $scroll_fixed = "(scroll|fixed)"; $percentage_length_1_2_top_left = "(?:(?:($percentage|$length)\s*($percentage|$length)?)|(?:(top|center|bottom)\s*(left|center|right)))"; $normal_italic_oblique = "(normal|italic|oblique)"; $normal_smallcaps = "(normal|small-caps)"; $normal_bold_100_900 = "(normal|bold|bolder|lighter|100|200|300|400|500|600|700|800|900)"; $absolutesize_relativesize_length_percentage_inherit = "($absolutesize|$relativesize|$length|$percentage|inherit)"; $normal_length = "(normal|$length)"; $capitalize_uppercase_lowercase_none = '(capitalize|uppercase|lowercase|none)'; $normal_nowrap = '(normal|nowrap)'; $array = array( 'margin-top' => $length_percentage_auto,'margin-right' => $length_percentage_auto, 'margin-bottom' => $length_percentage_auto, 'margin-left' => $length_percentage_auto, 'margin' => $length_percentage_1_4_auto, 'padding-top' =>$length_percentage, 'padding-right' =>$length_percentage, 'padding-bottom' =>$length_percentage, 'padding-left' =>$length_percentage, 'padding' => $length_percentage_1_4_auto, 'border-top-width' => $thin_medium_thick_length,'border-left-width' => $thin_medium_thick_length, 'border-bottom-width' => $thin_medium_thick_length,'border-right-width' => $thin_medium_thick_length, 'border-width' => $thin_medium_thick_length_1_4, 'border-top-color' => $color_transparent,'border-right-color' => $color_transparent, 'border-bottom-color' => $color_transparent,'border-left-color' => $color_transparent, 'border-color' => $color_transparent_1_4, 'border-top-style' => $none_hidden_solid_groove, 'border-right-style' => $none_hidden_solid_groove, 'border-bottom-style' => $none_hidden_solid_groove,'border-left-style' => $none_hidden_solid_groove, 'border-style' => $none_hidden_solid_groove_1_4, 'border-top' => $thin_none_color, 'border-bottom' => $thin_none_color, 'border-right' => $thin_none_color, 'border-left' => $thin_none_color, 'border' => $thin_none_color, 'width' => $length_percentage_auto, 'height' => $length_percentage_auto, 'line-height' => $normal_length_percentage, 'vertical-align' => $baseline_sub_super_top, 'list-style-type' => $disc_circle_square_decimal, 'list-style-image' => $uri_none, 'list-style-position' => $inside_outside, 'list-style' => $disc_uri_inside, 'color' => "($color)", 'background-color' => $color_transparent, 'background-image' => $uri_none, 'background-repeat' => $repeat, 'background-attachment' => $scroll_fixed, 'background-position' => $percentage_length_1_2_top_left, // background // font-family 'font-style' => $normal_italic_oblique,'font-variant' => $normal_smallcaps, 'font-weight' => $normal_bold_100_900, 'font-size' => $absolutesize_relativesize_length_percentage_inherit, // font 'text-indent' => $length_percentage, 'text-align' => $left_right_center_justify, //text-decoration 'letter-spacing' => $normal_length,'word-spacing' => $normal_length, 'text-transform' => $capitalize_uppercase_lowercase_none, 'white-space' => $normal_nowrap, ); // 例外処理をするプロパティのリスト // 対象:値の出現順が任意のもの // 処理:空白でトークンに分け、正規表現で1個ずつチェック // 形式: '<property>' => array('regex' => '<string>', 'splitDelimiter' => '<string>') $exceptionProp = array( //少なくとも4種類の出現順が任意 4P4 'background' => array('regex' => "(?:$color_transparent|$uri_none|$repeat|$scroll_fixed|$percentage_length_1_2_top_left)",'splitDelimiter' => ' '), 'font-family' => array('regex' => "(serif|sans-serif|cursive|fantasy|monospace|\"(?:.*?)\")",'splitDelimiter' => ','), 'text-decoration' => array('regex' => "(?:none|(underline|overline|line-through|blink))",'splitDelimiter' => ' ') ); if ($prop === 'font') { // font-family内の半角空白をエスケープ $val = preg_replace('/([\"\'].*?) (.*?[\"\'])/', '$1 $2', $val, -1, $count); $font_style_variant_weight = array($normal_italic_oblique,$normal_smallcaps,$normal_bold_100_900); $arr = preg_split('/\s* \s*/', $val, -1, PREG_SPLIT_NO_EMPTY); // 半角空白で分割 // 6個以上はありえない if (count($arr) > 5) return $val; if (count($arr) > 2) { $loop = count($arr) - 2; for ($i=0;$i<$loop;$i++) { $before = count($font_style_variant_weight); foreach ($font_style_variant_weight as $n => $v) { if (preg_match("/^(?:$v)$/", $arr[$i], $m)) { unset($font_style_variant_weight[$n]); break; } } if (count($font_style_variant_weight) === $before) return $arr[$i]; } //最後から2番目は、sizeも加える $font_style_variant_weight[] = $absolutesize_relativesize_length_percentage_inherit; if (!preg_match("/^(?:".implode('|', $font_style_variant_weight).")$/", $arr[count($arr)-2], $m)) { return $arr[count($arr)-2]; } } // 最後尾は'font-family' $lastArr = array_pop($arr); if(self::blockValidate('font-family', $lastArr) !== true) return $lastArr; } elseif ($prop === 'text-decoration') { $arr = preg_split('/\s* \s*/', $val, -1, PREG_SPLIT_NO_EMPTY); // 半角空白で分割 $check = array('underline', 'overline', 'line-through', 'blink'); if (count($arr) === 1) { $check[] = 'none'; if (!preg_match("/^(?:".implode('|', $check).")$/", $arr[0], $m)) { return $arr[0]; } } else { for ($i=0;$i<count($arr);$i++) { $before = count($check); foreach ($check as $n => $v) { if (preg_match("/^(?:$v)$/", $arr[$i], $m)) { unset($check[$n]); break; } } if (count($check) === $before) return $arr[$i]; } } } elseif (isset($exceptionProp[$prop])) { // 例外処理 値の順が任意 foreach (preg_split('/\s*'.$exceptionProp[$prop]['splitDelimiter'].'\s*/', $val, -1, PREG_SPLIT_NO_EMPTY) as $n => $v) { $res = preg_match('/^'.$exceptionProp[$prop]['regex'].'$/i', $v, $m); if($res === 0) return $prop;// マッチしない } } elseif (isset($array[$prop])) {// ノーマル処理 値の順が一意 if(!preg_match("/".$array[$prop]."/", $val, $m)) return $prop; } else { return false; } return true; } /** * エラーメッセージをセットする * * @param string $str string * * @return ? */ protected function setError($str) { $this->setError[] = $str; } /** * コメントを削除する * * @param string $css css * * @return string */ static protected function cssClean($css) { // コメントの削除 $css = preg_replace('/\/\*[^*]*\*+([^\/][^*]*\*+)*\//m', '', $css); // 改行の削除 $css = preg_replace('/\s*\n+\s*/m', chr(32), $css); return trim($css); } }
使い方
<?php $input = 'html > * div{color:red;font:italic bold small-caps "MS ゴシック","MS 明朝",fantasy;}.class,#id a:link{color:#DDD;text-decoration:line-through underline overline blink !important;}'; $o = new CssParser(); $o->parse($input); /* array(2) { ["specs"]=> array(5) { [0]=> int(2) [1]=> int(10) [2]=> int(102) [3]=> int(10010) [4]=> int(10102) } ["data"]=> array(5) { [2]=> array(1) { [0]=> array(3) { ["expr"]=> string(10) "html>* div" ["decl"]=> array(2) { [0]=> string(9) "color:red" [1]=> string(65) "font:italic bold small-caps "MS ゴシック","MS 明朝",fantasy" } ["pair"]=> array(2) { [0]=> array(2) { ["prop"]=> string(5) "color" ["val"]=> string(3) "red" } [1]=> array(2) { ["prop"]=> string(4) "font" ["val"]=> string(60) "italic bold small-caps "MS ゴシック","MS 明朝",fantasy" } } } } [10]=> array(1) { [0]=> array(3) { ["expr"]=> string(6) ".class" ["decl"]=> array(1) { [0]=> string(10) "color:#DDD" } ["pair"]=> array(1) { [0]=> array(2) { ["prop"]=> string(5) "color" ["val"]=> string(4) "#DDD" } } } } [10010]=> array(1) { [0]=> array(3) { ["expr"]=> string(6) ".class" ["decl"]=> array(1) { [0]=> string(53) "text-decoration:line-through underline overline blink" } ["pair"]=> array(1) { [0]=> array(2) { ["prop"]=> string(15) "text-decoration" ["val"]=> string(37) "line-through underline overline blink" } } } } [102]=> array(1) { [0]=> array(3) { ["expr"]=> string(10) "#id a:link" ["decl"]=> array(1) { [0]=> string(10) "color:#DDD" } ["pair"]=> array(1) { [0]=> array(2) { ["prop"]=> string(5) "color" ["val"]=> string(4) "#DDD" } } } } [10102]=> array(1) { [0]=> array(3) { ["expr"]=> string(10) "#id a:link" ["decl"]=> array(1) { [0]=> string(53) "text-decoration:line-through underline overline blink" } ["pair"]=> array(1) { [0]=> array(2) { ["prop"]=> string(15) "text-decoration" ["val"]=> string(37) "line-through underline overline blink" } } } } } } */