<?php

/**
 * @file
 * caption_filter.module
 *
 * Provides a very basic caption filter for input formats
 */

/**
 * Implements hook_init().
 */
function caption_filter_init() {
  $path = drupal_get_path('module', 'caption_filter');
  drupal_add_css($path .'/caption-filter.css', array('every_page' => TRUE));
  drupal_add_js($path .'/js/caption-filter.js', array('every_page' => TRUE));
}

/**
 * Implements hook_filter_info().
 */
function caption_filter_filter_info() {
  $filters = array();
  $filters['caption_filter'] = array(
    'title' => t('Convert [caption] tags and allow image alignment'),
    'process callback' => 'caption_filter_process_filter',
    'tips callback' => 'caption_filter_tips',
    'weight' => 20,
  );
  return $filters;
}

/**
 * Filter tips callback. Display the help for using [caption] syntax.
 */
function caption_filter_tips($filter, $format, $long = FALSE) {
  if ($long) {
    return t('
      <p><strong>Caption Filter</strong></p>
      <p>You may wrap images or embeds with a caption using the code <code>[caption]IMAGE caption[/caption]</code>.</p>
      <p>Examples:</p>
      <ul>
        <li>Single Image:<br /><code>[caption]<img src="" />This is a caption[/caption]</code></li>
        <li>Align the video:<br /><code>[caption align=right]<img src="" />This is another caption[/caption]</code></li>
      </ul>');
  }
  else {
    return check_plain(t('Captions may be specified with [caption]<img src="example.png">Image caption[/caption]. Items can be aligned with [caption align=left].'));
  }
}

/**
 * Filter process callback. Replace the [caption] tags with HTML.
 */
function caption_filter_process_filter($text, $filter) {
  // Prevent useless processing if there are no caption tags at all.
  if (stristr($text, '[caption') !== FALSE) {
    $pattern = "/(\[caption([^\]]*)\])(.*?)(\[\/caption\])/";
    $text = preg_replace_callback($pattern, '_caption_filter_replace', $text);
  }
  return $text;
}

/**
 * Callback for preg_replace_callback() in caption_filter_process_filter().
 *
 * This function actually does the work of replacing each [caption] tag with
 * HTML output.
 */
function _caption_filter_replace($matches) {
  $caption_attributes = _caption_filter_tag_attributes($matches[2]);
  $item = $matches[3];

  $width = _caption_filter_get_width($item);
  $align = isset($caption_attributes['align']) ? $caption_attributes['align'] : 'center';
  $caption = isset($caption_attributes['caption']) ? $caption_attributes['caption'] : '';

  // Remove "align" from the start of the alignment if needed. WordPress
  // commonly uses align="alignright" for example.
  if (strpos($align, 'align') === 0) {
    $align = substr($align, 5);
  }

  return '<div class="caption caption-'. $align .'"><div class="caption-inner" style="width: '. $width .';">'. $item . $caption .'</div></div>';
}

/**
 * Determine the width of the img/object that is being captioned.
 */
function _caption_filter_get_width($item) {
  // Find an img or object tag within the [caption] to determine the width.
  if (preg_match_all('/<(img|object)[^>]+>/i', $item, $internal_result)) {
    foreach($internal_result['0'] as $tag) {
      // If the width is defined, dump that into the $widths array.
      if (preg_match_all('/width="([0-9]*)"/i', $tag, $width_result)) {
        $width = $width_result[1][0];
      }
      // We're going to have to find the image src.
      else {
        preg_match('/src="([^"]*)"/i', $tag, $src_result);
        // If there is a src tag:
        if (!empty($src_result)) {
          list($width) = getimagesize($src_result[1]);
        }
        else {
          // We cannot determine the width so just set it to the default
          // css value.
          $width = 'auto';
        }
      }
    }
  }
  // Free up memory.
  unset($src_result, $width_result, $internal_result, $tag, $item);

  // We need to append the 'px' to any numeric widths.
  if (is_numeric($width)) {
    $width = $width . 'px';
  }

  return $width;
}

/**
 * Retrieve all attributes from a caption "shortcode" tag.
 *
 * This code is based upon the WordPress function shortcode_parse_atts().
 *
 * @see http://core.svn.wordpress.org/branches/3.2/wp-includes/shortcodes.php
 *
 * @param $text
 *   The shortcode tag attributes to be parsed. This should not include the
 *   brackets, the tag itself, or any of the contents within the tag. It should
 *   be a string of attributes, such as:
 *
 *   @code
 *     attr1="value 1" attr2=value2 value3
 *   @endcode
 *
 *   Note that these tags may or may not use quotes, either single or double.
 * @return
 *   An array of attributes and their value.
 */
function _caption_filter_tag_attributes($text) {
  $atts = array();
  $pattern = '/(\w+)\s*=\s*"([^"]*)"(?:\s|$)|(\w+)\s*=\s*\'([^\']*)\'(?:\s|$)|(\w+)\s*=\s*([^\s\'"]+)(?:\s|$)|"([^"]*)"(?:\s|$)|(\S+)(?:\s|$)/';
  $text = preg_replace("/[\x{00a0}\x{200b}]+/u", " ", $text);
  if (preg_match_all($pattern, $text, $match, PREG_SET_ORDER)) {
    foreach ($match as $m) {
      if (!empty($m[1]))
        $atts[strtolower($m[1])] = stripcslashes($m[2]);
      elseif (!empty($m[3]))
        $atts[strtolower($m[3])] = stripcslashes($m[4]);
      elseif (!empty($m[5]))
        $atts[strtolower($m[5])] = stripcslashes($m[6]);
      // These two rules vary slightly from the WordPress source. Single word
      // attributes (e.g "checked") are set to have the value of TRUE.
      elseif (isset($m[7]) and strlen($m[7]))
        $atts[strtolower($m[7])] = TRUE;
      elseif (isset($m[8]))
        $atts[strtolower($m[8])] = TRUE;
    }
  }
  else {
    $atts = ltrim($text);
  }
  return $atts;
}

/**
 * Implements hook_wysiwyg_plugin().
 *
 * This hook returns a list of plugins written directly against certain WYSIWYG
 * editors.
 */
function caption_filter_wysiwyg_plugin($editor, $version) {
  $plugins = array();
  if ($editor == 'tinymce') {
    $plugins['captionfilter'] = array(
      'url' => 'http://drupal.org/project/caption_filter',
      'path' => drupal_get_path('module', 'caption_filter') . '/js',
      'filename' => 'caption-filter-tinymce.js',
      // Caption Filter doesn't actually provide a button, but this code is
      // needed to make it so that WYSIWYG module will load our plugin.
      'buttons' => array(
        'captionfilter' => t('Caption Filter'),
      ),
      'options' => array(
        'captionfilter_css' => base_path() . drupal_get_path('module', 'caption_filter') . '/caption-filter.css',
      ),
      'load' => TRUE,
    );
  }
  return $plugins;
}

/**
 * Implements hook_element_info().
 */
function caption_filter_element_info() {
  // We only enhance widgets if the Insert module is available.
  if (!function_exists('insert_widgets')) {
    return;
  }

  $extra = array('#after_build' => array('caption_filter_element_process'));

  $elements = array();
  foreach (insert_widgets() as $widget_type => $widget) {
    if (isset($widget['fields']['title'])) {
      $element_type = isset($widget['element_type']) ? $widget['element_type'] : $widget_type;
      $elements[$element_type] = $extra;
    }
  }

  return $elements;
}

/**
 * Form API #process function for elements.
 */
function caption_filter_element_process($element) {
  static $js_added;

  // Bail out early if the needed properties aren't available. This happens
  // most frequently when editing a field configuration.
  if (!isset($element['#entity_type'])) {
    return $element;
  }

  $field = field_info_field($element['#field_name']);
  $instance = field_info_instance($element['#entity_type'], $element['#field_name'], $element['#bundle']);

  $widget_settings = $instance['widget']['settings'];
  $widget_type = $instance['widget']['type'];

  if (isset($element['title'])) {
    $element['title']['#title'] = t('Caption');
    $element['title']['#description'] = NULL;

    // Add settings for this widget only once.
    if (!isset($js_added[$widget_type])) {
      $js_added[$widget_type] = TRUE;
      $caption_settings = array(
        'captionFromTitle' => $widget_settings['caption_from_title'],
      );
      drupal_add_js(array('captionFilter' => array('widgets' => array($widget_type => $caption_settings))), 'setting');
    }
  }

  return $element;
}

/**
 * Implements hook_form_alter().
 */
function caption_filter_form_field_ui_field_edit_form_alter(&$form, $form_state) {
  if (isset($form['instance']['settings']['insert']) && isset($form['instance']['settings']['title_field'])) {
    $field = $form['#field'];
    $instance = field_info_instance($form['instance']['entity_type']['#value'], $form['instance']['field_name']['#value'], $form['instance']['bundle']['#value']);
    $form['instance']['settings']['insert'] += caption_filter_field_widget_settings_form($field, $instance);
  }
}

/**
 * Implements hook_field_widget_info_alter().
 *
 * A list of settings needed by Caption Filter module on widgets.
 */
function caption_filter_field_widget_info_alter(&$info) {
  foreach ($info as $widget_type => $widget_info) {
    // If Insert module has added settings to the widget, add our setting to
    // use the title field as the caption.
    if (isset($widget_info['settings']['insert'])) {
      $info[$widget_type]['settings']['caption_from_title'] = 1;
    }
  }
}

/**
 * Configuration form for editing insert settings for a field instance.
 */
function caption_filter_field_widget_settings_form($field, $instance) {
  $widget = $instance['widget'];
  $settings = $widget['settings'];

  $form['caption_from_title'] = array(
    '#type' => 'checkbox',
    '#title' => t('Use the <em>Title</em> field as a caption'),
    '#default_value' => $settings['caption_from_title'],
    '#element_validate' => array('caption_filter_field_widget_caption_validate'),
    '#description' => t('This feature requires that the <em>Title</em> field be enabled above, and that the "Convert [caption] tags" be enabled in the <a href="!formats">text formats configuration</a>.', array('!formats' => url('admin/config/content/formats'))),
    '#weight' => -9,
  );

  return $form;
}

/**
 * An #element_validate function for the "caption_from_title" field.
 */
function caption_filter_field_widget_caption_validate($element, &$form_state) {
  if ($element['#value'] && empty($form_state['values']['instance']['settings']['title_field'])) {
    form_error($element, t('The <em>Title</em> field must be enabled to use it as a caption.'));
  }
}
