/** * Parses CSS in a string. The output is saved as an array in $this->css. * * @since 1.0.0 * * @param string $string The CSS code. * @return bool [return value] */ public function parse($string) { // Temporarily set locale to en_US in order to handle floats properly. $old = @setlocale(LC_ALL, 0); @setlocale(LC_ALL, 'C'); $all_properties =& $this->data['csstidy']['all_properties']; $at_rules =& $this->data['csstidy']['at_rules']; $quoted_string_properties =& $this->data['csstidy']['quoted_string_properties']; $this->css = array(); $this->print->input_css = $string; $string = str_replace("\r\n", "\n", $string) . ' '; $cur_comment = ''; for ($i = 0, $size = strlen($string); $i < $size; $i++) { if ("\n" === $string[$i] || "\r" === $string[$i]) { ++$this->line; } switch ($this->status) { /* Case in at-block */ case 'at': if ($this->is_token($string, $i)) { if ('/' === $string[$i] && '*' === @$string[$i + 1]) { $this->status = 'ic'; ++$i; $this->from[] = 'at'; } elseif ('{' === $string[$i]) { $this->status = 'is'; $this->at = $this->css_new_media_section($this->at); $this->_add_token(AT_START, $this->at); } elseif (',' === $string[$i]) { $this->at = trim($this->at) . ','; } elseif ('\\' === $string[$i]) { $this->at .= $this->_unicode($string, $i); } elseif (in_array($string[$i], array('(', ')', ':', '.', '/'), true)) { $this->at .= $string[$i]; } } else { $lastpos = strlen($this->at) - 1; if (!((ctype_space($this->at[$lastpos]) || $this->is_token($this->at, $lastpos) && ',' === $this->at[$lastpos]) && ctype_space($string[$i]))) { $this->at .= $string[$i]; } } break; /* Case in-selector */ /* Case in-selector */ case 'is': if ($this->is_token($string, $i)) { if ('/' === $string[$i] && '*' === @$string[$i + 1] && '' === trim($this->selector)) { $this->status = 'ic'; ++$i; $this->from[] = 'is'; } elseif ('@' === $string[$i] && '' === trim($this->selector)) { // Check for at-rule. $this->invalid_at = true; foreach ($at_rules as $name => $type) { if (!strcasecmp(substr($string, $i + 1, strlen($name)), $name)) { if ('at' === $type) { $this->at = '@' . $name; } else { $this->selector = '@' . $name; } if ('atis' === $type) { $this->next_selector_at = $this->next_selector_at ? $this->next_selector_at : ($this->at ? $this->at : DEFAULT_AT); $this->at = $this->css_new_media_section(' '); $type = 'is'; } $this->status = $type; $i += strlen($name); $this->invalid_at = false; } } if ($this->invalid_at) { $this->selector = '@'; $invalid_at_name = ''; for ($j = $i + 1; $j < $size; ++$j) { if (!ctype_alpha($string[$j])) { break; } $invalid_at_name .= $string[$j]; } $this->log('Invalid @-rule: ' . $invalid_at_name . ' (removed)', 'Warning'); } } elseif ('"' === $string[$i] || "'" === $string[$i]) { $this->cur_string[] = $string[$i]; $this->status = 'instr'; $this->str_char[] = $string[$i]; $this->from[] = 'is'; /* Fixing CSS3 attribute selectors, i.e. a[href$=".mp3" */ $this->quoted_string[] = '=' === $string[$i - 1]; } elseif ($this->invalid_at && ';' === $string[$i]) { $this->invalid_at = false; $this->status = 'is'; if ($this->next_selector_at) { $this->at = $this->css_new_media_section($this->next_selector_at); $this->next_selector_at = ''; } } elseif ('{' === $string[$i]) { $this->status = 'ip'; if ('' === $this->at) { $this->at = $this->css_new_media_section(DEFAULT_AT); } $this->selector = $this->css_new_selector($this->at, $this->selector); $this->_add_token(SEL_START, $this->selector); $this->added = false; } elseif ('}' === $string[$i]) { $this->_add_token(AT_END, $this->at); $this->at = ''; $this->selector = ''; $this->sel_separate = array(); } elseif (',' === $string[$i]) { $this->selector = trim($this->selector) . ','; $this->sel_separate[] = strlen($this->selector); } elseif ('\\' === $string[$i]) { $this->selector .= $this->_unicode($string, $i); } elseif ('*' === $string[$i] && @in_array($string[$i + 1], array('.', '#', '[', ':'), true) && (0 === $i || '/' !== $string[$i - 1])) { // Remove unnecessary universal selector, FS#147, but not comment in selector. } else { $this->selector .= $string[$i]; } } else { $lastpos = strlen($this->selector) - 1; if (-1 === $lastpos || !((ctype_space($this->selector[$lastpos]) || $this->is_token($this->selector, $lastpos) && ',' === $this->selector[$lastpos]) && ctype_space($string[$i]))) { $this->selector .= $string[$i]; } } break; /* Case in-property */ /* Case in-property */ case 'ip': if ($this->is_token($string, $i)) { if ((':' === $string[$i] || '=' === $string[$i]) && '' !== $this->property) { $this->status = 'iv'; if (!$this->get_cfg('discard_invalid_properties') || $this->property_is_valid($this->property)) { $this->property = $this->css_new_property($this->at, $this->selector, $this->property); $this->property = strtolower($this->property); $this->_add_token(PROPERTY, $this->property); } } elseif ('/' === $string[$i] && '*' === @$string[$i + 1] && '' === $this->property) { $this->status = 'ic'; ++$i; $this->from[] = 'ip'; } elseif ('}' === $string[$i]) { $this->explode_selectors(); $this->status = 'is'; $this->invalid_at = false; $this->_add_token(SEL_END, $this->selector); $this->selector = ''; $this->property = ''; if ($this->next_selector_at) { $this->at = $this->css_new_media_section($this->next_selector_at); $this->next_selector_at = ''; } } elseif (';' === $string[$i]) { $this->property = ''; } elseif ('\\' === $string[$i]) { $this->property .= $this->_unicode($string, $i); } elseif ('' === $this->property && !ctype_space($string[$i]) || ('/' === $this->property || '/' === $string[$i])) { $this->property .= $string[$i]; } } elseif (!ctype_space($string[$i])) { $this->property .= $string[$i]; } break; /* Case in-value */ /* Case in-value */ case 'iv': $pn = ("\n" === $string[$i] || "\r" === $string[$i]) && $this->property_is_next($string, $i + 1) || $i === strlen($string) - 1; if (($this->is_token($string, $i) || $pn) && !(',' === $string[$i] && !ctype_space($string[$i + 1]))) { if ('/' === $string[$i] && '*' === @$string[$i + 1]) { $this->status = 'ic'; ++$i; $this->from[] = 'iv'; } elseif ('"' === $string[$i] || "'" === $string[$i] || '(' === $string[$i]) { $this->cur_string[] = $string[$i]; $this->str_char[] = '(' === $string[$i] ? ')' : $string[$i]; $this->status = 'instr'; $this->from[] = 'iv'; $this->quoted_string[] = in_array(strtolower($this->property), $quoted_string_properties, true); } elseif (',' === $string[$i]) { $this->sub_value = trim($this->sub_value) . ','; } elseif ('\\' === $string[$i]) { $this->sub_value .= $this->_unicode($string, $i); } elseif (';' === $string[$i] || $pn) { if ('@' === $this->selector[0] && isset($at_rules[substr($this->selector, 1)]) && 'iv' === $at_rules[substr($this->selector, 1)]) { $this->status = 'is'; switch ($this->selector) { case '@charset': // Add quotes to charset. $this->sub_value_arr[] = '"' . trim($this->sub_value) . '"'; $this->charset = $this->sub_value_arr[0]; break; case '@namespace': // Add quotes to namespace. $this->sub_value_arr[] = '"' . trim($this->sub_value) . '"'; $this->namespace = implode(' ', $this->sub_value_arr); break; case '@import': $this->sub_value = trim($this->sub_value); if (empty($this->sub_value_arr)) { // Quote URLs in imports only if they're not already inside url() and not already quoted. if ('url(' !== substr($this->sub_value, 0, 4)) { if (!($this->sub_value[0] === substr($this->sub_value, -1) && in_array($this->sub_value[0], array("'", '"'), true))) { $this->sub_value = '"' . $this->sub_value . '"'; } } } $this->sub_value_arr[] = $this->sub_value; $this->import[] = implode(' ', $this->sub_value_arr); break; } $this->sub_value_arr = array(); $this->sub_value = ''; $this->selector = ''; $this->sel_separate = array(); } else { $this->status = 'ip'; } } elseif ('}' !== $string[$i]) { $this->sub_value .= $string[$i]; } if (('}' === $string[$i] || ';' === $string[$i] || $pn) && !empty($this->selector)) { if ('' === $this->at) { $this->at = $this->css_new_media_section(DEFAULT_AT); } // Case settings. if ($this->get_cfg('lowercase_s')) { $this->selector = strtolower($this->selector); } $this->property = strtolower($this->property); $this->optimise->subvalue(); if ('' !== $this->sub_value) { $this->sub_value_arr[] = $this->sub_value; $this->sub_value = ''; } $this->value = ''; while (count($this->sub_value_arr)) { $sub = array_shift($this->sub_value_arr); if (strstr($this->selector, 'font-face')) { $sub = $this->quote_font_format($sub); } if ('' !== $sub) { if (strlen($this->value) && (',' !== substr($this->value, -1, 1) || $this->get_cfg('preserve_css'))) { $this->value .= ' '; } $this->value .= $sub; } } $this->optimise->value(); $valid = $this->property_is_valid($this->property); if ((!$this->invalid_at || $this->get_cfg('preserve_css')) && (!$this->get_cfg('discard_invalid_properties') || $valid)) { $this->css_add_property($this->at, $this->selector, $this->property, $this->value); $this->_add_token(VALUE, $this->value); $this->optimise->shorthands(); } if (!$valid) { if ($this->get_cfg('discard_invalid_properties')) { $this->log('Removed invalid property: ' . $this->property, 'Warning'); } else { $this->log('Invalid property in ' . strtoupper($this->get_cfg('css_level')) . ': ' . $this->property, 'Warning'); } } $this->property = ''; $this->sub_value_arr = array(); $this->value = ''; } if ('}' === $string[$i]) { $this->explode_selectors(); $this->_add_token(SEL_END, $this->selector); $this->status = 'is'; $this->invalid_at = false; $this->selector = ''; if ($this->next_selector_at) { $this->at = $this->css_new_media_section($this->next_selector_at); $this->next_selector_at = ''; } } } elseif (!$pn) { $this->sub_value .= $string[$i]; if (ctype_space($string[$i]) || ',' === $string[$i]) { $this->optimise->subvalue(); if ('' !== $this->sub_value) { $this->sub_value_arr[] = $this->sub_value; $this->sub_value = ''; } } } break; /* Case in string */ /* Case in string */ case 'instr': $_str_char = $this->str_char[count($this->str_char) - 1]; $_cur_string = $this->cur_string[count($this->cur_string) - 1]; $_quoted_string = $this->quoted_string[count($this->quoted_string) - 1]; $temp_add = $string[$i]; // Add another string to the stack. Strings can't be nested inside of quotes, only parentheses, // but parentheticals can be nested more than once. if (')' === $_str_char && ('(' === $string[$i] || '"' === $string[$i] || '\\' === $string[$i]) && !$this->escaped($string, $i)) { $this->cur_string[] = $string[$i]; $this->str_char[] = '(' === $string[$i] ? ')' : $string[$i]; $this->from[] = 'instr'; $this->quoted_string[] = ')' === $_str_char && '(' !== $string[$i] && '(' === trim($_cur_string) ? $_quoted_string : '(' !== $string[$i]; continue; } if (')' !== $_str_char && ("\n" === $string[$i] || "\r" === $string[$i]) && !('\\' === $string[$i - 1] && !$this->escaped($string, $i - 1))) { $temp_add = '\\A'; $this->log('Fixed incorrect newline in string', 'Warning'); } $_cur_string .= $temp_add; if ($string[$i] === $_str_char && !$this->escaped($string, $i)) { $this->status = array_pop($this->from); if (!preg_match('|[' . implode('', $this->data['csstidy']['whitespace']) . ']|uis', $_cur_string) && 'content' !== $this->property) { if (!$_quoted_string) { if (')' !== $_str_char) { // Convert properties like // font-family: 'Arial'; // to // font-family: Arial; // or // url("abc") // to // url(abc) $_cur_string = substr($_cur_string, 1, -1); } } else { $_quoted_string = false; } } array_pop($this->cur_string); array_pop($this->quoted_string); array_pop($this->str_char); if (')' === $_str_char) { $_cur_string = "(" . trim(substr($_cur_string, 1, -1)) . ")"; } if ('iv' === $this->status) { if (!$_quoted_string) { if (false !== strpos($_cur_string, ',')) { // We can on only remove space next to ',' $_cur_string = implode(',', array_map('trim', explode(',', $_cur_string))); } // and multiple spaces (too expensive). if (false !== strpos($_cur_string, ' ')) { $_cur_string = preg_replace(',\\s+,', ' ', $_cur_string); } } $this->sub_value .= $_cur_string; } elseif ('is' === $this->status) { $this->selector .= $_cur_string; } elseif ('instr' === $this->status) { $this->cur_string[count($this->cur_string) - 1] .= $_cur_string; } } else { $this->cur_string[count($this->cur_string) - 1] = $_cur_string; } break; /* Case in-comment */ /* Case in-comment */ case 'ic': if ('*' === $string[$i] && '/' === $string[$i + 1]) { $this->status = array_pop($this->from); $i++; $this->_add_token(COMMENT, $cur_comment); $cur_comment = ''; } else { $cur_comment .= $string[$i]; } break; } } $this->optimise->postparse(); $this->print->_reset(); // Set locale back to original setting. @setlocale(LC_ALL, $old); return !(empty($this->css) && empty($this->import) && empty($this->charset) && empty($this->tokens) && empty($this->namespace)); }
/** * [postparse description] * * @since 1.0.0 * * @return [type] [description] */ public function postparse() { if (!empty($this->parser->import)) { $this->parser->import = array(); } if (!empty($this->parser->charset)) { $this->parser->charset = array(); } return parent::postparse(); }