<?php
// $Id$

/**
 * @file
 * Customize the "Read More" link shown in teasers.
 */


define('READ_MORE_PLACEMENT_DEFAULT', 'inline');
define('READ_MORE_TEXT_DEFAULT', '[node:read-more:link]');
define('READ_MORE_LINK_TEXT_DEFAULT', '<strong>Read more<span class="element-invisible"> about [node:title]</span></strong>');
define('READ_MORE_TITLE_DEFAULT', '[node:title]');


/**
 * Implements hook_menu().
 */
function read_more_menu() {
  $items['admin/config/content/read_more'] = array(
    'title' => 'Read More link',
    'description' => 'Configures the <strong>Read More</strong> link that appears in node teasers.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('read_more_admin_settings'),
    'access arguments' => array('administer site configuration'),
    'file' => 'read_more.admin.inc',
    'type' => MENU_NORMAL_ITEM,
  );
  $items['admin/config/content/read_more/settings'] = array(
    'title' => 'Settings',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'weight' => -10,
  );
  return $items;
}


/**
 * Implements template_preprocess_node().
 */
function read_more_preprocess_node(&$vars) {
  // Remove the link from the node's $links output if the option is enabled.
  if (variable_get('read_more_remove', TRUE)) {
    unset($vars['content']['links']['node']['#links']['node-readmore']);
  }
}


/**
 * Implements hook_node_view().
 */
function read_more_node_view($node, $view_mode, $langcode) {
  $attachments = variable_get('read_more_' . $node->type . '_view_modes', array());

  if (!empty($attachments[$view_mode])) { /* Teaser */
    $display = variable_get('read_more_placement', READ_MORE_PLACEMENT_DEFAULT);

    // Don't do anything if placing the link is disabled.
    if (($display == 'disable')) {
      return;
    }

    // Is this an RSS feed?
    if ($view_mode == 'rss') {
      // If link replacement in RSS feeds is enabled, prevent core from
      // adding a second link by setting the readmore flag to NULL.
      if (variable_get('read_more_rss', TRUE)) {
        $node->readmore = NULL;
      }
      // Don't do anything if link replacement in RSS feeds is disabled.
      else {
        return ;
      }
    }

    $read_more_link = read_more_link($node, $display, $view_mode);

    // Try to insert the link inline.
    if ($display == 'inline') {
      $elements_array = variable_get('read_more_elements', array('p'));
      $elements = '(?:' . implode('|', $elements_array) . ')';

      // Only add a link if there's a body field.
      if (!variable_get('read_more_require_body_field', FALSE) || isset($node->content['body'])) {
        // Get last position of the last closing marker in teaser, if the body
        // field exists.
        if (isset($node->content['body'])) {
          preg_match('!</?' . $elements . '[^>]*>\s*$!i', $node->content['body'][0]['#markup'], $match, PREG_OFFSET_CAPTURE);
          // Recalculate the position in $teaser. We do this because there may be extra CCK fields appended to the teaser.
          $insert_point = (empty($match)) ? -1 : strpos($view_mode, $node->content['body'][0]['#markup']) + $match[0][1];
          // Insert the link.
          $node->content['body'][0]['#markup'] = substr_replace($node->content['body'][0]['#markup'], drupal_render($read_more_link), $insert_point, 0);
        }

        // Display the link after the rest of the content.
        else {
          $display = 'after';
        }
      }
    }

    // Append the link to the end of the teaser.
    if ($display == 'after') {
      $node->content['read_more'] = read_more_link($node, $display, $view_mode) + array('#weight' => 1);
    }
  }

  if (($view_mode == 'full') && variable_get('read_more_anchor', FALSE)) { /* Full node and anchoring is enabled */
    $body = field_get_items('node', $node, 'body');
    $teaser = field_view_value('node', $node, 'body', $body[0], 'teaser');
    $teaser_rendered = $teaser['#markup'];
    $node->content['body'][0]['#markup'] = substr_replace($node->content['body'][0]['#markup'], $teaser_rendered . '<a name="more"></a>', 0, strlen($teaser_rendered));
  }
}


/**
 * Prepares the link for theming and returns a link.
 *
 * XSS checking and other safety measures are performed here to prevent
 * themers from omitting them.
 */
function read_more_link($node, $display, $view_mode) {
  $uri = entity_uri('node', $node);
  if (empty($uri)) {
    return array();
  }

  // Send prepared data to the theme function.
  return array(
    '#theme' => 'read_more_link',
    '#node' => $node,
    '#display' => $display,
    '#view_mode' => $view_mode,
  );
}

/* Link preprocessing */

/**
 * Add link options as prescribed in the Read More link config page.
 */
function _read_more_link_options($uri) {
  // Build link options array.
  $link_options = $uri['options'];
  $link_options['attributes']['title'] = _read_more_link_title($uri['options']['entity']);
  $link_options['html'] = TRUE;

  // Add anchor to link if the option is enabled.
  if (variable_get('read_more_anchor', FALSE)) {
    $link_options['fragment'] = 'more';
  }

  // Add rel="nofollow" to link if the option is enabled.
  if (variable_get('read_more_nofollow', TRUE)) {
    $link_options['attributes']['rel'] = 'nofollow';
  }

  // Add target="blank" to link if the option is enabled.
  if (variable_get('read_more_newwindow', FALSE)) {
    $link_options['attributes']['target'] = '_blank';
  }

  return $link_options;
}

function _read_more_link_uri($node, $options = array()) {
  $uri = entity_uri('node', $node);
  $uri['options'] = _read_more_link_options($uri);
  $uri['options'] = $options + $uri['options'];

  return $uri;
}

/**
 * @return The Read More URL, with any noted options.
 */
function _read_more_link_url($node, $options = array()) {
  $uri = _read_more_link_uri($node, $options);
  return url($uri['path'], $uri['options']);
}


/**
 * Limit the tags in the link text and link wrapper to a specific set.
 */
function _read_more_filter_xss($text) {
  // Allowed tags borrowed largely from filter_xss_admin().
  // See http://api.drupal.org/api/function/filter_xss_admin
  $allowed_tags = array('abbr', 'acronym', 'b', 'big', 'cite', 'code', 'del', 'em', 'i', 'img', 'ins', 'small', 'span', 'strong', 'sub', 'sup');

  return filter_xss($text, $allowed_tags);
}

/* Token handlers */

/**
 * The 'read-more' token will probably include the node:read-more:link token.
 */
function _read_more_wrapper($node, $view_mode) {
  $link_wrapper = variable_get('read_more_text', READ_MORE_TEXT_DEFAULT);

  // Filter link text for cross-site scripting (XSS).
  // We sanitize the link text here because l() will be told to allow HTML.
  $link_wrapper = _read_more_filter_xss($link_wrapper);

  return token_replace($link_wrapper, array('node' => $node), array('read_more_view_mode' => $view_mode));
}

/**
 * The read-more:link token wraps the read-more:link-text in link.
 */
function _read_more_link($node, $view_mode) {
  $link_text = _read_more_link_text($node);
  $link_uri = _read_more_link_uri($node);
  $view_mode_settings = variable_get('read_more_absolute_link_modes', array());
  if (!empty($view_mode_settings[$view_mode])) {
    $link_uri['options']['absolute'] = TRUE;
  }

  return l($link_text, $link_uri['path'], $link_uri['options']);
}

/**
 * The read-more:link-text token contains the text that will be wrapped in a link,
 * but without the link.
 */
function _read_more_link_text($node) {
  $link_text = variable_get('read_more_link_text', READ_MORE_LINK_TEXT_DEFAULT);

  // Filter link text for cross-site scripting (XSS).
  // We sanitize the link text here because l() will be told to allow HTML.
  $link_text = _read_more_filter_xss($link_text);

  // Replace tokens with values if the Token module and the token options are enabled.
  $link_text = token_replace($link_text, array('node' => $node));

  return $link_text;
}

/**
 * The read-more:link-title token will be included in the link as the title attribute.
 */
function _read_more_link_title($node) {
  // We don't need to sanitize the link title attribute because it's passed to l(), which runs strip_tags() for us.
  $link_title = variable_get('read_more_title', READ_MORE_TITLE_DEFAULT);
  $link_title = token_replace($link_title, array('node' => $node));

  return $link_title;
}


/**
 * Implements hook_theme().
 */
function read_more_theme($existing, $type, $theme, $path) {
  return array(
    'read_more_link' => array(
      'variables' => array(
        'node' => NULL,
        'display' => NULL,
        'view_mode' => NULL,
      ),
    ),
  );
}


/**
 * Theme function that wraps the rendered link.
 */
function theme_read_more_link($vars) {
  $display = $vars['display'];
  $node = $vars['node'];
  $view_mode = $vars['view_mode'];

  // Use a <div> (block-level) element for links appended after the teaser.
  if ($display == 'after') {
    $element = 'div';
    $separator = '';
  }
  else {
    // Use a <span> (inline) element for links that appear inside the teaser.
    $element = 'span';
    $separator = ' ';
  }

  return $separator . '<' . $element . ' class="read-more">' . _read_more_wrapper($node, $view_mode) . '</' . $element . '>';
}

function read_more_view_modes_options_list() {
  $entity_info = entity_get_info('node');
  $view_modes = array();

  foreach ($entity_info['view modes'] as $key => $view_mode) {
    $view_modes[$key] = $view_mode['label'];
  }

  return $view_modes;
}
