Overview

Namespaces

  • Jyxo
    • Beholder
      • TestCase
    • Gettext
      • Parser
    • Input
      • Chain
      • Filter
      • Validator
    • Mail
      • Email
        • Attachment
      • Parser
      • Sender
    • Rpc
      • Json
      • Xml
    • Shell
    • Spl
    • Svn
    • Time
    • Webdav
  • PHP

Classes

  • Charset
  • Color
  • Css
  • ErrorHandler
  • ErrorMail
  • FirePhp
  • Html
  • HtmlTag
  • SpamFilter
  • String
  • Timer
  • XmlReader

Exceptions

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