Overview

Packages

  • Jyxo_Beholder
  • Jyxo_Charset
  • Jyxo_Color
  • Jyxo_Css
  • Jyxo_ErrorHandling
  • Jyxo_FirePhp
  • Jyxo_Gettext
    • Parser
  • Jyxo_Html
  • Jyxo_Input
    • Chain
    • Filter
    • Validator
  • Jyxo_Mail
    • Email
    • Parser
    • Sender
  • Jyxo_Rpc
    • Json
    • Xml
  • Jyxo_Shell
  • Jyxo_SpamFilter
  • Jyxo_Spl
  • Jyxo_String
  • Jyxo_Svn
  • Jyxo_Time
  • Jyxo_Timer
  • Jyxo_Webdav
  • Jyxo_XmlReader
  • PHP

Classes

  • Jyxo_Css
  • Overview
  • Package
  • Class
  • Tree
  • Deprecated
  1: <?php
  2: 
  3: /**
  4:  * Jyxo PHP Library
  5:  *
  6:  * LICENSE
  7:  *
  8:  * This source file is subject to the new BSD license that is bundled
  9:  * with this package in the file license.txt.
 10:  * It is also available through the world-wide-web at this URL:
 11:  * https://github.com/jyxo/php/blob/master/license.txt
 12:  */
 13: 
 14: /**
 15:  * Class for working with CSS stylesheets.
 16:  *
 17:  * @category Jyxo
 18:  * @package Jyxo_Css
 19:  * @copyright Copyright (c) 2005-2011 Jyxo, s.r.o.
 20:  * @license https://github.com/jyxo/php/blob/master/license.txt
 21:  * @author Jaroslav HanslĂ­k
 22:  */
 23: class Jyxo_Css
 24: {
 25:     /**
 26:      * Constructor preventing from creating static class instances.
 27:      *
 28:      * @throws LogicException When trying to create an instance
 29:      */
 30:     public final function __construct()
 31:     {
 32:         throw new LogicException(sprintf('Cannot create an instance of a static class %s.', get_class($this)));
 33:     }
 34: 
 35:     /**
 36:      * Cleans up a CSS stylesheet.
 37:      *
 38:      * @param string $css Stylesheet definition
 39:      * @return string
 40:      */
 41:     public static function repair($css)
 42:     {
 43:         // Convert properties to lowercase
 44:         $css = preg_replace_callback('~((?:^|\{|;)\s*)([-a-z]+)(\s*:)~i', function($matches) {
 45:             return $matches[1] . strtolower($matches[2]) . $matches[3];
 46:         }, $css);
 47:         // Convert rgb() and url() to lowercase
 48:         $css = preg_replace_callback('~(rgb|url)(?=\s*\()~i', function($matches) {
 49:             return strtolower($matches[1]);
 50:         }, $css);
 51:         // Remove properties without values
 52:         $css = preg_replace_callback('~\s*[-a-z]+\s*:\s*([;}]|$)~i', function($matches) {
 53:             return '}' === $matches[1] ? '}' : '';
 54:         }, $css);
 55:         // Remove MS Office properties
 56:         $css = preg_replace('~\s*mso-[-a-z]+\s*:[^;}]*;?~i', '', $css);
 57:         // Convert color definitions to lowercase
 58:         $css = preg_replace_callback('~(:[^:]*?)(#[abcdef0-9]{3,6})~i', function($matches) {
 59:             return $matches[1] . strtolower($matches[2]);
 60:         }, $css);
 61:         // Convert colors from RGB to HEX
 62:         $css = preg_replace_callback('~rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)~', function($matches) {
 63:             return sprintf('#%02x%02x%02x', $matches[1], $matches[2], $matches[3]);
 64:         }, $css);
 65: 
 66:         return $css;
 67:     }
 68: 
 69:     /**
 70:      * Filters given properties.
 71:      *
 72:      * @param string $css Stylesheet definition
 73:      * @param array $properties Filtered properties
 74:      * @param boolean $exclude If true, $properties will be removed from the stylesheet; if false, only $properties will be left
 75:      * @return string
 76:      */
 77:     public static function filterProperties($css, array $properties, $exclude = true)
 78:     {
 79:         $properties = array_flip($properties);
 80:         return preg_replace_callback('~\s*([-a-z]+)\s*:[^;}]*;?~i', function($matches) use ($properties, $exclude) {
 81:             if ($exclude) {
 82:                 return isset($properties[$matches[1]]) ? '' : $matches[0];
 83:             } else {
 84:                 return isset($properties[$matches[1]]) ? $matches[0] : '';
 85:             }
 86:         }, $css);
 87:     }
 88: 
 89:     /**
 90:      * Removes unnecessary characters from a CSS stylesheet.
 91:      *
 92:      * It is recommended to use the repair() method on the stylesheet definition first.
 93:      *
 94:      * @param string $css Stylesheet definition
 95:      * @return string
 96:      */
 97:     public static function pack($css)
 98:     {
 99:         // Comments
100:         $packed = preg_replace('~/\*.*\*/~sU', '', $css);
101:         // Whitespace
102:         $packed = preg_replace('~\s*([>+\~,{:;}])\s*~', '\1', $packed);
103:         $packed = preg_replace('~\(\s+~', '(', $packed);
104:         $packed = preg_replace('~\s+\)~', ')', $packed);
105:         $packed = trim($packed);
106:         // Convert colors from #ffffff to #fff
107:         $packed = preg_replace('~(:[^:]*?#)([abcdef0-9]{1})\2([abcdef0-9]{1})\3([abcdef0-9]{1})\4~', '\1\2\3\4', $packed);
108:         // Empty selectors
109:         $packed = preg_replace('~(?<=})[^{]+\{\}~', '', $packed);
110:         // Remove units when 0
111:         $packed = preg_replace('~([\s:]0)(?:px|pt|pc|in|mm|cm|em|ex|%)~', '\1', $packed);
112:         // Unnecessary semicolons
113:         $packed = str_replace(';}', '}', $packed);
114:         $packed = trim($packed, ';');
115: 
116:         return $packed;
117:     }
118: 
119:     /**
120:      * Converts HTML styles inside <style> elements to inline styles.
121:      *
122:      * Supported selectors:
123:      * * a {...}
124:      * * #header {...}
125:      * * .icon {...}
126:      * * h1#header {...}
127:      * * a.icon.small {...}
128:      * * a#remove.icon.small {...}
129:      * * a img {...}
130:      * * h1, h2 {...}
131:      * * a:link {...} - converts to a {...}
132:      *
133:      * @param string $html Processed HTML source
134:      * @return string
135:      */
136:     public static function convertStyleToInline($html)
137:     {
138:         // Extract styles from the source
139:         $cssList = self::parseStyle($html);
140: 
141:         // If no styles were found, return the original HTML source
142:         if (empty($cssList)) {
143:             return $html;
144:         }
145: 
146:         // Parse the HTML source
147:         preg_match_all('~(?:<\w+[^>]*(?: /)?>)|(?:</\w+>)|(?:<![^>]+>)|(?:[^<]+)~', $html, $matches);
148:         $path = array();
149:         $html = '';
150:         $inStyle = false;
151:         foreach ($matches[0] as $htmlPart) {
152:             // Skip <style> elements
153:             if (0 === strpos($htmlPart, '<style')) {
154:                 // <style> opening tag
155:                 $inStyle = true;
156:             } elseif (0 === strpos($htmlPart, '</style')) {
157:                 // <style> closing tag
158:                 $inStyle = false;
159:             } elseif (!$inStyle) {
160:                 // Not inside the <style> element
161: 
162:                 // Closing tag
163:                 if (0 === strpos($htmlPart, '</')) {
164:                     array_pop($path);
165:                 } elseif (('<' === $htmlPart[0]) && (0 !== strpos($htmlPart, '<!'))) {
166:                     // Opening tag or empty element, ignoring comments
167: 
168:                     $htmlPart = trim($htmlPart, '/<> ');
169: 
170:                     $attributeList = array('id' => '', 'class' => '');
171:                     // If there is no space, there are no attributes
172:                     if (false !== strpos($htmlPart, ' ')) {
173:                         list($tag, $attributes) = explode(' ', $htmlPart, 2);
174: 
175:                         // Parse attributes
176:                         foreach (explode('" ', $attributes) as $attribute) {
177:                             list($attributeName, $attributeContent) = explode('="', $attribute);
178:                             $attributeList[$attributeName] = trim($attributeContent, '"');
179:                         }
180:                     } else {
181:                         $tag = $htmlPart;
182:                     }
183:                     $attributeClass = !empty($attributeList['class']) ? explode(' ', $attributeList['class']) : array();
184: 
185:                     // Add element information to the path
186:                     array_push($path, array(
187:                             'tag' => $tag,
188:                             'id' => $attributeList['id'],
189:                             'class' => $attributeClass
190:                         )
191:                     );
192: 
193:                     // Walk through the CSS definition list and add applicable properties
194:                     // Because of inheritance, walk the array in reversed order
195:                     foreach (array_reverse($cssList) as $css) {
196:                         // Selectors have to have equal or less parts than the HTML element nesting level
197:                         if (count($css['selector']) > count($path)) {
198:                             continue;
199:                         }
200: 
201:                         // The last selector part must correspond to the last processed tag
202:                         $lastSelector = end($css['selector']);
203:                         if (((!empty($lastSelector['tag'])) && ($tag !== $lastSelector['tag']))
204:                                 || ((!empty($lastSelector['id'])) && ($attributeList['id'] !== $lastSelector['id']))
205:                                 || (count(array_diff($lastSelector['class'], $attributeClass)) > 0)) {
206:                             continue;
207:                         }
208: 
209:                         $add = true;
210:                         $lastPathKey = count($path);
211:                         foreach (array_reverse($css['selector']) as $selector) {
212:                             // Nothing was found in the previous search, no reason to continue searching
213:                             if (!$add) {
214:                                 break;
215:                             }
216: 
217:                             for ($i = ($lastPathKey - 1); $i >= 0; $i--) {
218:                                 if (((empty($selector['tag'])) || ($path[$i]['tag'] === $selector['tag']))
219:                                         && ((empty($selector['id'])) || ($path[$i]['id'] === $selector['id']))
220:                                         && (0 === count(array_diff($selector['class'], $path[$i]['class'])))) {
221:                                     $lastPathKey = $i;
222:                                     continue 2;
223:                                 }
224:                             }
225: 
226:                             $add = false;
227:                         }
228: 
229:                         if ($add) {
230:                             // Add a semicolon if missing
231:                             if (';' !== substr($css['rules'], -1)) {
232:                                 $css['rules'] .= ';';
233:                             }
234:                             // CSS is processed in the reversed order, co place to the beginning
235:                             $attributeList['style'] = $css['rules'] . (isset($attributeList['style']) ? $attributeList['style'] : '');
236:                         }
237:                     }
238: 
239:                     // Creates a new tag
240:                     $htmlPart = '<' . $tag;
241:                     foreach ($attributeList as $attributeName => $attributeContent) {
242:                         // Not using empty() because it would make attributes with the "0" value ignored
243:                         if ('' !== $attributeContent) {
244:                             $htmlPart .= ' ' . $attributeName . '="' . $attributeContent . '"';
245:                         }
246:                     }
247: 
248:                     // Empty tags
249:                     switch ($tag) {
250:                         case 'br':
251:                         case 'hr':
252:                         case 'img':
253:                         case 'input':
254:                             $htmlPart .= ' />';
255:                             break;
256:                         default:
257:                             $htmlPart .= '>';
258:                             break;
259:                     }
260:                 }
261:             }
262: 
263:             // Append the part to the HTML source
264:             $html .= $htmlPart;
265:         }
266: 
267:         // In case of float: add a cleaner (if there is none present already)
268:         $cleaner = '<div style="clear: both; visibility: hidden; overflow: hidden; height: 1px;">';
269:         if ((preg_match('~<\w+[^>]+style="[^"]*float:[^"]*"~', $html))
270:                 && (!preg_match('~' . preg_quote($cleaner) . '\s*</body>\s*</html>\s*$~', $html))) {
271:             $html = str_replace('</body>', $cleaner . '</body>', $html);
272:         }
273: 
274:         return $html;
275:     }
276: 
277:     /**
278:      * Helper method for searching and parsing <style> definitions inside a HTML source.
279:      *
280:      * @param string $html Processed HTML source
281:      * @return array
282:      * @see Jyxo_Css::convertStyleToInline()
283:      */
284:     private static function parseStyle($html)
285:     {
286:         // Find <style> elements
287:         if (!preg_match_all('~<style\s+(?:[^>]+\s+)*type="text/css"[^>]*>(.*?)</style>~s', $html, $styles)) {
288:             return array();
289:         }
290: 
291:         $cssList = array();
292:         foreach ($styles[1] as $style) {
293:             // Remove CDATA and HTML comments
294:             $style = str_replace(array('<![CDATA[', ']]>', '<!--', '-->'), '', $style);
295: 
296:             // Optimize the parsed definitions
297:             $style = self::pack($style);
298: 
299:             if (empty($style)) {
300:                 continue;
301:             }
302: 
303:             // Replace quotes with apostrophes
304:             $style = str_replace('"', "'", $style);
305: 
306:             // Remove the last empty part
307:             $definitions = explode('}', $style, -1);
308: 
309:             foreach ($definitions as $definition) {
310:                 // Allows only supported selectors with valid rules
311:                 if (!preg_match('~^(?:(?:(?:[-_\w#.:]+)\s?)+,?)+{(?:[-\w]+:[^;]+[;]?)+$~', $definition)) {
312:                     continue;
313:                 }
314: 
315:                 list($selector, $rules) = explode('{', $definition);
316:                 foreach (explode(',', $selector) as $part) {
317:                     // Convert a:link to a
318:                     $part = str_replace(':link', '', $part);
319: 
320:                     $parsedSelector = array();
321:                     foreach (explode(' ', $part) as $selectorPart) {
322:                         // If no tag name was given use a fake one
323:                         if (('.' === $selectorPart[0]) || ('#' === $selectorPart[0])) {
324:                             $selectorPart = ' ' . $selectorPart;
325:                         }
326: 
327:                         if (false !== strpos($selectorPart, '.')) {
328:                             list($selectorPart, $class) = explode('.', $selectorPart, 2);
329:                             // There can be multiple classes
330:                             $class = explode('.', $class);
331:                         } else {
332:                             $class = array();
333:                         }
334:                         if (false !== strpos($selectorPart, '#')) {
335:                             list($selectorPart, $id) = explode('#', $selectorPart, 2);
336:                         } else {
337:                             $id = '';
338:                         }
339:                         $tag = trim($selectorPart);
340: 
341:                         $parsedSelector[] = array(
342:                             'tag' => strtolower($tag),
343:                             'id' => $id,
344:                             'class' => $class
345:                         );
346:                     }
347: 
348:                     $cssList[] = array(
349:                         'selector' => $parsedSelector,
350:                         'rules' => $rules
351:                     );
352:                 }
353:             }
354:         }
355: 
356:         return $cssList;
357:     }
358: }
359: 
Jyxo PHP Library API documentation generated by ApiGen 2.3.0