laipower/wp-content/plugins/w3-total-cache/lib/Minify/Minify/HTML.php

386 lines
11 KiB
PHP

<?php
namespace W3TCL\Minify;
/**
* Class Minify_HTML
* @package Minify
*/
/**
* Compress HTML
*
* This is a heavy regex-based removal of whitespace, unnecessary comments and
* tokens. IE conditional comments are preserved. There are also options to have
* STYLE and SCRIPT blocks compressed by callback functions.
*
* A test suite is available.
*
* @package Minify
* @author Stephen Clay <steve@mrclay.org>
*/
class Minify_HTML {
/**
* @var boolean
*/
protected $_jsCleanComments = true;
/**
* "Minify" an HTML page
*
* @param string $html
*
* @param array $options
*
* 'cssMinifier' : (optional) callback function to process content of STYLE
* elements.
*
* 'jsMinifier' : (optional) callback function to process content of SCRIPT
* elements. Note: the type attribute is ignored.
*
* 'xhtml' : (optional boolean) should content be treated as XHTML1.0? If
* unset, minify will sniff for an XHTML doctype.
*
* @return string
*/
public static function minify($html, $options = array()) {
$min = new self($html, $options);
return $min->process();
}
/**
* Create a minifier object
*
* @param string $html
*
* @param array $options
*
* 'cssMinifier' : (optional) callback function to process content of STYLE
* elements.
*
* 'jsMinifier' : (optional) callback function to process content of SCRIPT
* elements. Note: the type attribute is ignored.
*
* 'jsCleanComments' : (optional) whether to remove HTML comments beginning and end of script block
*
* 'xhtml' : (optional boolean) should content be treated as XHTML1.0? If
* unset, minify will sniff for an XHTML doctype.
*
* @return null
*/
public function __construct($html, $options = array())
{
$this->_html = str_replace("\r\n", "\n", trim($html));
if (isset($options['xhtml'])) {
$this->_isXhtml = (bool)$options['xhtml'];
}
if (isset($options['cssMinifier'])) {
$this->_cssMinifier = $options['cssMinifier'];
}
if (isset($options['jsMinifier'])) {
$this->_jsMinifier = $options['jsMinifier'];
}
$this->_stripCrlf = (isset($options['stripCrlf']) ? (boolean) $options['stripCrlf'] : false) ;
$this->_ignoredComments = (isset($options['ignoredComments']) ? (array) $options['ignoredComments'] : array());
}
/**
* Minify the markeup given in the constructor
*
* @return string
*/
public function process()
{
if ($this->_isXhtml === null) {
$this->_isXhtml = (false !== strpos($this->_html, '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML'));
}
$this->_replacementHash = 'MINIFYHTML' . md5( isset( $_SERVER['REQUEST_TIME'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_TIME'] ) ) : '' );
$this->_placeholders = array();
// replace dynamic tags
$this->_html = preg_replace_callback(
'~(<!--\s*m(func|clude)(.*)-->\s*<!--\s*/m(func|clude)\s*-->)~is'
,array($this, '_removeComment')
,$this->_html);
// replace SCRIPTs (and minify) with placeholders
$this->_html = preg_replace_callback(
'/(\\s*)<script(\\b[^>]*?>)([\\s\\S]*?)<\\/script>(\\s*)/i'
,array($this, '_removeScriptCB')
,$this->_html);
// replace STYLEs (and minify) with placeholders
$this->_html = preg_replace_callback(
'/\\s*<style(\\b[^>]*>)([\\s\\S]*?)<\\/style>\\s*/i'
,array($this, '_removeStyleCB')
,$this->_html);
// remove HTML comments (not containing IE conditional comments).
$this->_html = preg_replace_callback(
'/<!--([\\s\\S]*?)-->/'
,array($this, '_commentCB')
,$this->_html);
// replace PREs with placeholders
$this->_html = preg_replace_callback('/\\s*<pre(\\b[^>]*?>[\\s\\S]*?<\\/pre>)\\s*/i'
,array($this, '_removePreCB')
,$this->_html);
// replace TEXTAREAs with placeholders
$this->_html = preg_replace_callback(
'/\\s*<textarea(\\b[^>]*?>[\\s\\S]*?<\\/textarea>)\\s*/i'
,array($this, '_removeTextareaCB')
,$this->_html);
// trim each line.
// @todo take into account attribute values that span multiple lines.
$this->_html = preg_replace('/^\\s+|\\s+$/m', '', $this->_html);
// remove ws around block/undisplayed elements
$this->_html = preg_replace('/\\s+(<\\/?(?:area|article|aside|base(?:font)?|blockquote|body'
.'|canvas|caption|center|col(?:group)?|dd|dir|div|dl|dt|fieldset|figcaption|figure|footer|form'
.'|frame(?:set)?|h[1-6]|head|header|hgroup|hr|html|legend|link|main|map|menu|meta|nav'
.'|ol|opt(?:group|ion)|output|p|param|section|t(?:able|body|head|d|h||r|foot|itle)'
.'|ul|video)\\b[^>]*>)/i', '$1', $this->_html);
// remove whitespaces outside of all elements
$this->_html = preg_replace(
'/>((\\s)(?:\\s*))?([^<]+?)((\\s)(?:\\s*))?</'
,'>$2$3$5<'
,$this->_html);
// remove whitespaces before end of all empty elements
$this->_html = preg_replace(
'/\\s*\\/>/'
,'/>'
,$this->_html);
// remove trailing slash from void elements
$_html = preg_replace(
'~<(area|base|br|col|command|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)(([^\'">]|\"[^\"]*\"|\'[^\']*\'|)*?)\\s*[/]?>~i'
,'<$1$2>'
,$this->_html);
// Avoid PREG_JIT_STACKLIMIT_ERROR. Thanks @ericek111 for https://github.com/BoldGrid/w3-total-cache/issues/190.
if ( preg_last_error() === PREG_NO_ERROR ) {
$this->_html = $_html;
}
unset( $_html );
// use newlines before 1st attribute in open tags (to limit line lengths)
$this->_html = preg_replace('/(<[a-z\\-]+)\\s+([^>]+>)/i', "$1\n$2", $this->_html);
if ($this->_stripCrlf) {
$this->_html = preg_replace("~[\r\n]+~", ' ', $this->_html);
} else {
$this->_html = preg_replace("~[\r\n]+~", "\n", $this->_html);
}
// fill placeholders
$this->_html = str_replace(
array_keys($this->_placeholders)
,array_values($this->_placeholders)
,$this->_html
);
// issue 229: multi-pass to catch scripts that didn't get replaced in textareas
$this->_html = str_replace(
array_keys($this->_placeholders)
,array_values($this->_placeholders)
,$this->_html
);
// in HTML5, type attribute is unnecessary for JavaScript resources
// in HTML5, type attribute for style element is not needed and should be omitted
if (false !== stripos($this->_html, '<!doctype html>')) {
$this->_html = preg_replace(
'/<(script|style)([^>]*)\\stype=[\'"]?(text\\/javascript|text\\/css|application\\/javascript)[\'"]?([^>]*)>/i'
,'<$1$2$4>'
,$this->_html);
}
// unquote attribute values without spaces
$this->_html = preg_replace_callback(
'/(<([a-z\\-]+)\\s)\\s*([^>]+>)/m'
,array($this, '_removeAttributeQuotes')
,$this->_html);
return $this->_html;
}
protected function _commentCB($m)
{
return (0 === strpos($m[1], '[') || false !== strpos($m[1], '<![') || $this->_ignoredComment($m[1]))
? $m[0]
: '';
}
protected function _ignoredComment($comment)
{
foreach ($this->_ignoredComments as $ignoredComment) {
if (!empty($ignoredComment) && stristr($comment, $ignoredComment) !== false) {
return true;
}
}
return false;
}
protected function _reservePlace($content)
{
$placeholder = '%' . $this->_replacementHash . count($this->_placeholders) . '%';
$this->_placeholders[$placeholder] = $content;
return $placeholder;
}
protected $_isXhtml = null;
protected $_replacementHash = null;
protected $_placeholders = array();
protected $_cssMinifier = null;
protected $_jsMinifier = null;
protected $_stripCrlf = null;
protected $_ignoredComments = null;
protected function _removePreCB($m)
{
return $this->_reservePlace("<pre{$m[1]}");
}
protected function _removeTextareaCB($m)
{
return $this->_reservePlace("<textarea{$m[1]}");
}
protected function _removeStyleCB($m)
{
$openStyle = "<style{$m[1]}";
$css = $m[2];
// remove HTML comments
$css = preg_replace('/(?:^\\s*<!--|-->\\s*$)/', '', $css);
// remove CDATA section markers
$css = $this->_removeCdata($css);
// minify
$minifier = $this->_cssMinifier
? $this->_cssMinifier
: 'trim';
$css = call_user_func($minifier, $css);
return $this->_reservePlace($this->_needsCdata($css)
? "{$openStyle}/*<![CDATA[*/{$css}/*]]>*/</style>"
: "{$openStyle}{$css}</style>"
);
}
protected function _removeScriptCB($m)
{
$openScript = "<script{$m[2]}";
$js = $m[3];
$script_tag = "<script{$m[2]}>{$js}</script>";
$type = '';
if (preg_match('#type="([^"]+)"#i', $m[2], $matches)) {
$type = strtolower($matches[1]);
}
// whitespace surrounding? preserve at least one space
$ws1 = ($m[1] === '') ? '' : ' ';
$ws2 = ($m[4] === '') ? '' : ' ';
// remove HTML comments (and ending "//" if present)
if ($this->_jsCleanComments) {
$js = preg_replace('/(?:^\\s*<!--\\s*|\\s*(?:\\/\\/)?\\s*-->\\s*$)/', '', $js);
}
// minify
$minifier = $this->_jsMinifier
? $this->_jsMinifier
: 'trim';
if (in_array($type, array('text/template', 'text/x-handlebars-template'))) {
$minifier = '';
}
$minifier = apply_filters('w3tc_minify_html_script_minifier', $minifier, $type, $script_tag);
if (empty($minifier)) {
$needsCdata = false;
} else {
// remove CDATA section markers
$js_old = $js;
$js = $this->_removeCdata($js);
$needsCdata = ( $js_old != $js );
$js = call_user_func($minifier, $js);
}
return $this->_reservePlace($needsCdata && $this->_needsCdata($js)
? "{$ws1}{$openScript}/*<![CDATA[*/{$js}/*]]>*/</script>{$ws2}"
: "{$ws1}{$openScript}{$js}</script>{$ws2}"
);
}
protected function _removeCdata($str)
{
if (false !== strpos($str, '<![CDATA[')) {
$str = str_replace('//<![CDATA[', '', $str);
$str = preg_replace('~/\*\s*<!\[CDATA\[\s*\*/~', '', $str);
$str = str_replace('<![CDATA[', '', $str);
$str = str_replace('//]]>', '', $str);
$str = preg_replace('~/\*\s*\]\]>\s*\*/~', '', $str);
$str = str_replace(']]>', '', $str);
}
return $str;
}
protected function _removeComment($m)
{
return $this->_reservePlace($m[1]);
}
protected function _needsCdata($str)
{
return ($this->_isXhtml && preg_match('/(?:[<&]|\\-\\-|\\]\\]>)/', $str));
}
protected function _removeAttributeQuotes($m) {
// whatsapp/fb bots dont read meta tags without quotes well
if (strtolower($m[2]) != 'meta') {
$m[3] = preg_replace_callback( '~([a-z0-9\\-])=(?<quote>[\'"])([^"\'\\s=]*)\k<quote>(\\s|>|/>)~i',
array( $this, '_removeAttributeQuotesCallback'), $m[3] );
}
return $m[1] . $m[3];
}
public function _removeAttributeQuotesCallback( $m ) {
// empty tag values like <div data-value=""> to <div data-value
if ( $m[3] === '' ) {
return $m[1] . $m[4];
}
// 1. <a href=bla/>hi</a> is sometimes (XHTML? HTML5 specs doesnt allow that)
// parsed as <a href=bla></a>hi</a> by browsers
// avoid that by turning it to <a href=bla/ >hi</a>
// 2. auto-closing tags without space at the end e.g. <div data-value="aa"/>
// should have space after value <div data-value=aa />
// otherwise some browsers assume data-value="aa/"
if ( /* 1 */ $m[4] == '/>' ||
/* 2 */ ( $m[4] == '>' && substr( $m[3], -1, 1 ) == '/' ) ) {
return $m[1] . '=' . $m[3] . ' ' . $m[4];
}
return $m[1] . '=' . $m[3] . $m[4];
}
}