<?php
/**
 * This module inspired by https://www.drupal.org/project/minify
 *
 * I wanted to try a different approach to minifying HTML output
 * instead of having to loop through all the regions I wanted
 * to capture the entire HTML output generated by Drupal
 * and loop it through the HTML and JS minifier instead
 *
 * The JShrink version is the same as the one found one
 * here : https://github.com/tedious/JShrink
 * I just included it here cause I didn't want to mess
 * with the libraries system on page delivery
 *
 * @author René Bakx (rene@renebakx.nl)
 * @author Atul Bhosale (Original MINIFY concept
 * @author Robert Hafner (JShrink)
 *
 */


/**
 * Implements hook_permission()
 */
function minihtml_permission() {
  return array(
    'administer minihtml' => array(
      'title' => t('Administer Minify HTML'),
      'description' => t('Perform administration tasks for minihtml module.'),
    ),
  );
}

/**
 * Implements hook_FORM_ID_alter().
 *
 * @param type $form
 * @param type $form_state
 */
function minihtml_form_system_performance_settings_alter(&$form, &$form_state, $form_id) {
  if (user_access('administer minihtml')) {

    $form['bandwidth_optimization']['minihtml_html'] = array(
      '#type' => 'checkbox',
      '#title' => t('Minify HTML'),
      '#default_value' => intval(variable_get('minihtml_html', 0)),
    );
    $form['bandwidth_optimization']['minihtml_inline_js'] = array(
      '#type' => 'checkbox',
      '#title' => t('Minify HTML processes inline Javascript'),
      '#default_value' => intval(variable_get('minihtml_inline_js', 0)),
      '#states' => array(
        'visible' => array(
          ':input[name="minihtml_html"]' => array('checked' => true),
        ),
      ),
    );
  }
}

/**
 * Implements hook_page_delivery_callback_alter().
 */
function minihtml_page_delivery_callback_alter(&$callback) {
  if (!path_is_admin(current_path())) {
    if (!path_is_admin(current_path()) && $callback == 'drupal_deliver_html_page' && intval(variable_get('minihtml_html', 0))) {
      $callback = 'minihtml_deliver_html_page';
    }
  }

}

/**
 * A complete copy with minimal changes of drupal_deliver_html_page method
 * The change is, the HTML is compressed before send to the browser
 *
 * Oh boy.. If we only had OO code ;)
 *
 * @param $page_callback_result
 * @throws \Exception
 */
function minihtml_deliver_html_page($page_callback_result) {

  // Emit the correct charset HTTP header, but not if the page callback
  // result is NULL, since that likely indicates that it printed something
  // in which case, no further headers may be sent, and not if code running
  // for this page request has already set the content type header.
  if (isset($page_callback_result) && is_null(drupal_get_http_header('Content-Type'))) {
    drupal_add_http_header('Content-Type', 'text/html; charset=utf-8');
  }
  define('REPLACEMENT_HASH', md5('minihtml_' . $_GET['q']));

  // Send appropriate HTTP-Header for browsers and search engines.
  global $language;
  drupal_add_http_header('Content-Language', $language->language);

  // Menu status constants are integers; page content is a string or array.
  if (is_int($page_callback_result)) {
    // @todo: Break these up into separate functions?
    switch ($page_callback_result) {
      case MENU_NOT_FOUND:
        // Print a 404 page.
        drupal_add_http_header('Status', '404 Not Found');

        watchdog('page not found', check_plain($_GET['q']), NULL, WATCHDOG_WARNING);

        // Check for and return a fast 404 page if configured.
        drupal_fast_404();

        // Keep old path for reference, and to allow forms to redirect to it.
        if (!isset($_GET['destination'])) {
          // Make sure that the current path is not interpreted as external URL.
          if (!url_is_external($_GET['q'])) {
            $_GET['destination'] = $_GET['q'];
          }
        }

        $path = drupal_get_normal_path(variable_get('site_404', ''));
        if ($path && $path != $_GET['q']) {
          // Custom 404 handler. Set the active item in case there are tabs to
          // display, or other dependencies on the path.
          menu_set_active_item($path);
          $return = menu_execute_active_handler($path, FALSE);
        }

        if (empty($return) || $return == MENU_NOT_FOUND || $return == MENU_ACCESS_DENIED) {
          // Standard 404 handler.
          drupal_set_title(t('Page not found'));
          $return = t('The requested page "@path" could not be found.', array('@path' => request_uri()));
        }

        drupal_set_page_content($return);
        $page = element_info('page');

        print _minihtml_html(drupal_render_page($page));
        break;

      case MENU_ACCESS_DENIED:
        // Print a 403 page.
        drupal_add_http_header('Status', '403 Forbidden');
        watchdog('access denied', check_plain($_GET['q']), NULL, WATCHDOG_WARNING);

        // Keep old path for reference, and to allow forms to redirect to it.
        if (!isset($_GET['destination'])) {
          // Make sure that the current path is not interpreted as external URL.
          if (!url_is_external($_GET['q'])) {
            $_GET['destination'] = $_GET['q'];
          }
        }

        $path = drupal_get_normal_path(variable_get('site_403', ''));
        if ($path && $path != $_GET['q']) {
          // Custom 403 handler. Set the active item in case there are tabs to
          // display or other dependencies on the path.
          menu_set_active_item($path);
          $return = menu_execute_active_handler($path, FALSE);
        }

        if (empty($return) || $return == MENU_NOT_FOUND || $return == MENU_ACCESS_DENIED) {
          // Standard 403 handler.
          drupal_set_title(t('Access denied'));
          $return = t('You are not authorized to access this page.');
        }

        print _minihtml_html(drupal_render_page($return));
        break;

      case MENU_SITE_OFFLINE:
        // Print a 503 page.
        drupal_maintenance_theme();
        drupal_add_http_header('Status', '503 Service unavailable');
        drupal_set_title(t('Site under maintenance'));
        print theme('maintenance_page', array(
          'content' => filter_xss_admin(variable_get('maintenance_mode_message',
            t('@site is currently under maintenance. We should be back shortly. Thank you for your patience.', array('@site' => variable_get('site_name', 'Drupal')))))
        ));
        break;
    }
  }
  elseif (isset($page_callback_result)) {
    // Print anything besides a menu constant, assuming it's not NULL or
    // undefined.
    print _minihtml_html(drupal_render_page($page_callback_result));
  }

  // Perform end-of-request tasks.
  drupal_page_footer();
}


/**
 * The actual HTML minifier
 */
function _minihtml_html($buffer) {

  /* Replace <textarea> with placeholder */
  $buffer = preg_replace_callback('/\\s*<textarea(\\b[^>]*?>[\\s\\S]*?<\\/textarea>)\\s*/i', '_minihtml_html_callback', $buffer);

  /* Replace <pre> with placeholder */
  $buffer = preg_replace_callback('/\\s*<pre(\\b[^>]*?>[\\s\\S]*?<\\/pre>)\\s*/i', '_minihtml_html_callback', $buffer);

  /* Replace <iframe> with placeholder */
  $buffer = preg_replace_callback('/\\s*<iframe(\\b[^>]*?>[\\s\\S]*?<\\/iframe>)\\s*/i', '_minihtml_html_iframe_callback', $buffer);

  /* Replace <script> with placeholder */
  $buffer = preg_replace_callback('/\\s*<script(\\b[^>]*?>[\\s\\S]*?<\\/script>)\\s*/i', '_minihtml_html_script_callback', $buffer);

  /* Replace <style> with placeholder */
  $buffer = preg_replace_callback('/\\s*<style(\\b[^>]*?>[\\s\\S]*?<\\/style>)\\s*/i', '_minihtml_html_style_callback', $buffer);

  /* Remove HTML comment */
  $buffer = preg_replace_callback('/<!--([\\s\\S]*?)-->/', '_minihtml_html_html_comment', $buffer);

  $search = array(
    '/\>[^\S ]+/s',
    // remove whitespaces after tags, except space
    '/[^\S ]+\</s',
    // remove whitespaces before tags, except space
    '/(\s)+/s',
    // shorten multiple whitespace sequences
    '/\\s+(<\\/?(?:area|base(?:font)?|blockquote|body'
    . '|caption|center|col(?:group)?|dd|dir|div|dl|dt|fieldset|form'
    . '|frame(?:set)?|h[1-6]|head|hr|html|legend|li|link|map|menu|meta'
    . '|ol|opt(?:group|ion)|p|param|t(?:able|body|head|d|h||r|foot|itle)'
    . '|ul)\\b[^>]*>)/i',
    // remove whitespaces around block/undisplayed elements
    '/^\\s+|\\s+$/m',
    // trim each line
  );

  $replace = array(
    '>',        // remove whitespaces after tags, except space
    '<',        // remove whitespaces before tags, except space
    '\\1',      // shorten multiple whitespace sequences
    '$1',       // remove whitespaces around block/undisplayed elements
    '',         // trim each line
  );

  $buffer = preg_replace($search, $replace, $buffer);

  /* Find and replace <textarea>, <pre>, <iframe>, <script> and <style> place holders values */
  global $placeholders;
  if (!empty($placeholders)) {
    $buffer = str_replace(array_keys($placeholders), array_values($placeholders), $buffer);
  }

  return $buffer;
}

/**
 * Remove HTML comments (not containing IE conditional comments).
 */
function _minihtml_html_html_comment($string) {
  return (0 === strpos($string[1], '[') || FALSE !== strpos($string[1], '<!['))
    ? $string[0]
    : '';
}

/*
 * Helper function to add place holder for <textarea> and <pre> tag
 */
function _minihtml_html_callback($m) {
  return _minihtml_reserve_place($m[0]);
}

/*
 * Helper function to add place holder for <iframe> tag
 */
function _minihtml_html_iframe_callback($m) {
  $iframe = preg_replace('/^\\s+|\\s+$/m', '', $m[0]);
  return _minihtml_reserve_place($iframe);
}

/*
 * Helper function to add place holder for <script> tag
 */
function _minihtml_html_script_callback($m) {
  $search = array(
    '!/\*.*?\*/!s',     // remove multi-line comment
    '/^\\s+|\\s+$/m',   // trim each line
    '/\n(\s*\n)+/',     // remove multiple empty line
  );
  $replace = array('', "\n", "\n");
  $script = preg_replace($search, $replace, $m[0]);
  if ($script && (intval(variable_get('minihtml_inline_js', 0)))) {
    include_once('jshrink.php');
    $script = JShrink\Minifier::minify($script);
  }
  return _minihtml_reserve_place($script);
}

/*
 * Helper function to add place holder for <style> tag
 */
function _minihtml_html_style_callback($m) {
  $search = array(
    '!/\*.*?\*/!s',   // remove multiline comment
    '/^\\s+|\\s+$/m'  // trim each line
  );
  $replace = array('');
  $style = preg_replace($search, $replace, $m[0]);
  return _minihtml_reserve_place($style);
}

/*
 * Helper function to add tag key and value for further replacement
 */
function _minihtml_reserve_place($content) {
  global $placeholders;
  $placeholder = '%' . REPLACEMENT_HASH . count($placeholders) . '%';
  $placeholders[$placeholder] = $content;
  return $placeholder;
}
