You are here

glossify.module in Glossify 6.3

File

glossify.module
View source
<?php

define("GLOSSIFY_USE_FILTER", 0);
define("GLOSSIFY_WITHOUT_FILTER", 1);
define("GLOSSIFY_WITHOUT_FILTER_OMIT_COMMENTS", 2);

/**
 * Implementation of hook_perm().
 */
function glossify_perm() {
  return array(
    'administer glossify',
  );
}

/**
 * Implementation of hook_menu().
 */
function glossify_menu() {
  $items['admin/settings/glossify'] = array(
    'title' => 'Glossify',
    'description' => 'Manipulate glossify behaviour',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'glossify_admin_settings',
    ),
    'access arguments' => array(
      'administer glossify',
    ),
    'file' => 'glossify.admin.inc',
  );
  $weight = 1;
  $items['admin/settings/glossify/global'] = array(
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'title' => t('Global'),
    'weight' => $weight,
  );
  $configurations = variable_get('glossify_configurations', array());
  foreach ($configurations as $configuration => $values) {
    if ($configuration !== 'global') {
      $weight++;
      $items["admin/settings/glossify/{$configuration}"] = array(
        'type' => MENU_LOCAL_TASK,
        'title' => $configuration,
        'page arguments' => array(
          'glossify_admin_settings',
        ),
        'access arguments' => array(
          'administer glossify',
        ),
        'file' => 'glossify.admin.inc',
        'weight' => $weight,
      );
    }
  }
  $items['admin/settings/glossify/add'] = array(
    'type' => MENU_LOCAL_TASK,
    'title' => t('Add a new configuration'),
    'page arguments' => array(
      'glossify_admin_settings',
    ),
    'access arguments' => array(
      'administer glossify',
    ),
    'file' => 'glossify.admin.inc',
    'weight' => 99,
  );
  return $items;
}

/**
 * Implementation of hook_theme().
 */
function glossify_theme() {
  return array(
    'glossify_term' => array(
      'arguments' => array(
        'nid' => NULL,
        'glossify_style' => NULL,
      ),
    ),
    'glossify_reference_section' => array(
      'arguments' => array(
        'term_definition_list' => NULL,
      ),
    ),
  );
}

// endfunction glossify_theme

/**
 * Render a glossary term.
 */
function theme_glossify_term($target, $glossify_style) {

  // outputs proper div so hovertip will work
  if (is_numeric($target)) {
    $term = node_load($target);
  }
  elseif (drupal_lookup_path('source', $target)) {
    $exploded_path = explode('/', drupal_lookup_path('source', $target));
    $term = node_load($exploded_path[count($exploded_path) - 1]);
  }
  switch ($glossify_style) {
    case 'reference':
      $output = '<dt>' . check_plain($term->title) . '</dt>';
      $output .= '<dd>' . check_markup($term->body) . '</dd>';
      break;
    case 'hovertip':
    default:
      $output = '<div id="' . check_plain($term->title) . '" class="hovertip" style="display: none;">';

      // Output a DIV to make hovertip work.
      $output .= '<h1>' . check_plain($term->title) . '</h1>';

      // Process node's body here if not using standard filter approach.
      $glossify_mode = variable_get('glossify_process_mode', GLOSSIFY_USE_FILTER);
      if ($glossify_mode != GLOSSIFY_USE_FILTER) {
        $term->body = glossify_filter('process', 0, $term->format, $term->body);
      }
      $output .= '<p>' . check_markup($term->body) . '</p>';
      $output .= '</div>';
      break;
  }

  // endswitch glossify style
  return $output;
}

/**
 * Render a glossary term reference.
 */
function theme_glossify_reference_section($term_definition_list) {
  $output = '<div id="glossify-reference">';
  $output .= '<h3>Terms referenced:</h3>';
  $output .= '<dl>';
  $output .= $term_definition_list;
  $output .= '</dl>';
  $output .= '</div>';
  return $output;
}

/**
 * Implementation of hook_form_alter().
 */
function glossify_form_alter(&$form, $form_state, $form_id) {
  if (isset($form['type']) && isset($form['#node']) && $form['type']['#value'] . '_node_form' == $form_id) {
    $configurations = variable_get('glossify_configurations', array());
    $use_internal = FALSE;
    foreach ($configurations as $configuration) {
      if ($configuration['methods']['use_internal'] && in_array($form['type']['#value'], $configuration['to'])) {
        $use_internal = TRUE;
      }
    }
    if ($use_internal) {
      $node = $form['#node'];
      $form['glossify'] = array(
        '#type' => 'fieldset',
        '#title' => t('Glossify'),
        '#collapsible' => TRUE,
        '#collapsed' => isset($node->glossify_keywords) && !empty($node->glossify_keywords) || isset($node->glossify_override) && !empty($node->glossify_override) ? FALSE : TRUE,
        '#weight' => 0,
      );
      $form['glossify']['glossify_keywords'] = array(
        '#type' => 'textfield',
        '#title' => t('Glossify Keywords'),
        '#default_value' => isset($node->glossify_keywords) ? $node->glossify_keywords : '',
        '#description' => t('You can add more than one keyword by seperating them with commas.'),
        '#weight' => 0,
      );
      $form['glossify']['glossify_override'] = array(
        '#type' => 'textfield',
        '#title' => t('Glossify Override'),
        '#default_value' => isset($node->glossify_override) ? $node->glossify_override : '',
        '#weight' => 1,
      );
    }
  }
}

/**
 * Implementation of hook_theme_registry_alter().
 */
function glossify_theme_registry_alter(&$theme_registry) {
  if (!empty($theme_registry['comment_view'])) {
    $theme_registry['comment_view']['function'] = 'glossify_theme_comment_view';
  }
}

/**
 * Implementation of hook_nodeapi().
 */
function glossify_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  switch ($op) {
    case 'delete':

      // omerida, 9/30/2010: I think it'd be quicker to
      // remove from the db directly (DELETE FROM {glossify} where nid=%d
      // but I figured it'd be safer to stick with the module's api
      // and use _keyword_table function.
      $configurations = variable_get('glossify_configurations', array());
      foreach ($configurations as $config_name => $configuration) {
        if (isset($configuration['to'][$node->type])) {
          $enabled_styles = array_filter($configuration['style']);
          $methods = $configuration['methods'];
          if (isset($methods['use_title'])) {
            _keyword_table('delete', $node->nid, 'title', $config_name, $node->language);
          }
          if (isset($methods['use_internal'])) {
            _keyword_table('delete', $node->nid, 'internal', $config_name, $node->language);
          }
          if (isset($methods['use_cck'])) {
            _keyword_table('delete', $node->nid, 'cck', $config_name, $node->language);
          }
          if (isset($methods['use_taxonomy'])) {
            _keyword_table('delete', $node->nid, 'taxonomy', $config_name, $node->language);
          }
        }
      }
      break;
    case 'load':
      $glossify_keywords = implode(', ', _fetch_keywords($node->nid, 'internal'));
      $glossify_override = db_result(db_query('SELECT alternate FROM {glossify} WHERE nid = %d', $node->nid));
      return array(
        'glossify_keywords' => $glossify_keywords,
        'glossify_override' => $glossify_override,
      );
      break;
    case 'presave':
      $configurations = variable_get('glossify_configurations', array());
      $clear_filter_cache = array();
      foreach ($configurations as $config_name => $configuration) {
        $clear_filter_cache = in_array($node->type, array_merge($configuration['to'], $configuration['from']));
      }
      if ($clear_filter_cache) {
        cache_clear_all('*', 'cache_filter', TRUE);
      }
      break;
    case 'insert':
      $configurations = variable_get('glossify_configurations', array());
      $keywords = array();
      $affected_content_types = array();
      $html_body = str_get_html($node->body);
      $html_teaser = str_get_html($node->teaser);
      foreach ($configurations as $config_name => $configuration) {
        if (in_array($node->type, $configuration['to'])) {
          if ($configuration['methods']['use_title']) {
            _keyword_table('insert', $node->nid, 'title', $config_name, $node->language, $node->title);
            $keywords[$node->title] = $node->title;
          }
          if ($configuration['methods']['use_internal'] && !empty($node->glossify_keywords)) {
            foreach (array_map('trim', explode(',', $node->glossify_keywords)) as $keyword) {
              _keyword_table('insert', $node->nid, 'internal', $config_name, $node->language, $keyword, $node->glossify_override);
              $keywords[$keyword] = $keyword;
            }
          }
          if ($configuration['methods']['use_cck']) {
            $fieldname = $configuration['methods']['keyword_field'];
            $override = $configuration['methods']['override_field'];
            foreach (array_map('trim', explode(',', $node->{$fieldname}[0]['value'])) as $keyword) {
              _keyword_table('insert', $node->nid, 'cck', $config_name, $node->language, $keyword, $node->{$override}[0]['value']);
              $keywords[$keyword] = $keyword;
            }
          }
          if ($configuration['methods']['use_taxonomy']) {
            foreach (array_map('trim', explode(',', $node->taxonomy['tags'][$configuration['methods']['vocabulary']])) as $keyword) {
              _keyword_table('insert', $node->nid, 'taxonomy', $config_name, $node->language, $keyword);
              $keywords[$keyword] = $keyword;
            }
          }
          $affected_content_types = array_merge($affected_content_types, $configuration['from']);
        }
      }
      break;
    case 'update':
      $configurations = variable_get('glossify_configurations', array());
      $keywords = array();
      $affected_content_types = array();
      $html_body = str_get_html($node->body);
      $html_teaser = str_get_html($node->teaser);
      foreach ($configurations as $config_name => $configuration) {
        if (in_array($node->type, $configuration['to'])) {
          if ($configuration['methods']['use_title']) {
            if (!db_result(db_query("SELECT gtid FROM {glossify} WHERE nid = %d AND method = '%s' AND configuration = '%s'", $node->nid, 'title', $config_name))) {
              _keyword_table('insert', $node->nid, 'title', $config_name, $node->language, $node->title);
              $keywords[$node->title] = $node->title;
            }
            elseif (!db_result(db_query("SELECT gtid FROM {glossify} WHERE nid = %d AND method = '%s' AND term = '%s' AND configuration = '%s'", $node->nid, 'title', $node->title, $config_name))) {
              $keywords = array_merge($keywords, _keyword_table('update', $node->nid, 'title', $config_name, $node->language, $node->title));
              $keywords[$node->title] = $node->title;
            }
          }
          if ($configuration['methods']['use_internal']) {
            $keywords = array_merge($keywords, _fetch_affected_keywords($node, 'internal', $config_name));
          }
          if ($configuration['methods']['use_cck']) {
            $keywords = array_merge($keywords, _fetch_affected_keywords($node, 'cck', $config_name, $configuration['methods']['keyword_field'], $configuration['methods']['override_field']));
          }
          if ($configuration['methods']['use_taxonomy']) {
            $keywords = array_merge($keywords, _fetch_affected_keywords($node, 'taxonomy', $config_name, $configuration['methods']['vocabulary']));
          }
          $affected_content_types[$config_name] = array_merge($affected_content_types, $configuration['from']);
        }
      }
      break;
    case 'view':
      $configurations = variable_get('glossify_configurations', array());
      $term_definition_list = '';
      $referenced_terms = array();
      foreach ($configurations as $config_name => $configuration) {
        if (in_array($node->type, $configuration['from'])) {
          $enabled_styles = array_filter($configuration['style']);
          $possible_keywords = _fetch_possible_keywords($config_name, $configuration, $node->nid);
          if (in_array('reference', $enabled_styles)) {
            foreach ($possible_keywords as $kwd) {
              list($term_title, $path) = $kwd;
              if (preg_match(_glossify_regex($configuration, $term_title), $node->body)) {
                if (!in_array($path, $referenced_terms)) {
                  $term_definition_list .= theme('glossify_term', $path, 'reference');
                  $referenced_terms[] = $path;
                }
              }
            }
          }
          if (in_array('hovertip', $enabled_styles)) {
            foreach ($possible_keywords as $kwd) {
              list($term_title, $path) = $kwd;
              if (preg_match(_glossify_regex($configuration, $term_title), $node->body)) {
                $node->content['glossify_hovertip'][$term_title] = array(
                  '#value' => theme('glossify_term', $path, 'hovertip'),
                  '#weight' => 10,
                );
                $node->content['glossify_hovertip']['#weight'] = 10;
                $node->content['glossify_all_hovertips']['#value'] .= $node->content['glossify_hovertip'][$term_title]['#value'];
              }
            }
          }
        }
      }
      if ($term_definition_list !== '') {
        $node->content['glossify_reference'] = array(
          '#weight' => '10',
          '#value' => theme('glossify_reference_section', $term_definition_list),
        );
      }

      // Process node body if set to 'non-filter' mode. Currently the $node->format is irrelevant.
      $glossify_mode = variable_get('glossify_process_mode', GLOSSIFY_USE_FILTER);
      if ($glossify_mode != GLOSSIFY_USE_FILTER) {
        $node->content['body']['#value'] = glossify_filter('process', 0, $node->format, $node->content['body']['#value']);
      }
      break;
  }
}

/**
 * Implementation of hook_comment().
 */
function glossify_comment(&$a1, $op) {
  if ($op == 'view') {

    // Process comment text if set to 'non-filter' mode and comment processing enabled.
    // Currently the $a1->format is irrelevant.
    $glossify_mode = variable_get('glossify_process_mode', GLOSSIFY_USE_FILTER);
    if ($glossify_mode == GLOSSIFY_WITHOUT_FILTER) {
      $a1->comment = glossify_filter('process', 0, $a1->format, $a1->comment);
    }
  }
}

/**
 * Implementation of hook_filter().
 */
function glossify_filter($op, $delta = 0, $format = -1, $text = '', $cache_id = 0) {
  static $configurations;
  switch ($op) {
    case 'list':
      if (variable_get('glossify_process_mode', GLOSSIFY_USE_FILTER) == GLOSSIFY_USE_FILTER) {
        return array(
          0 => t('Glossify filter'),
        );
      }
      else {
        return array();
      }
    case 'description':
      return t('Glossify the entered content.');
    case 'prepare':
      return $text;
    case 'process':

      // TODO: option should be to only do this on a node page
      // or have a list of paths where this filter will not be invoked
      if (empty($text)) {

        // Nothing to do.
        return $text;
      }
      if (($node = menu_get_object()) == NULL) {
        $q = db_query("SELECT nid FROM {node_revisions} WHERE '%s'=CONCAT(format, ':', MD5(body))", $cache_id);
        $r = db_fetch_object($q);
        if ($r->nid) {
          $node = node_load($r->nid);
        }
        else {

          // have to return something so that we don't lose output
          return $text;
        }
      }
      if (!isset($configurations)) {
        $configurations = variable_get('glossify_configurations', array());
        foreach ($configurations as $key => &$config) {
          if (isset($config['excl_tags']) && !empty($config['excl_tags'])) {
            $html_tags = preg_split("/\r\n|\r|\n|,/", $config['excl_tags']);
            $config['excl_tags'] = array_combine($html_tags, $html_tags);
          }
        }
      }
      if (class_exists('DOMDocument', false)) {
        $newtext = _glossify_replace_terms_phpdom($text, $configurations, $node);
      }
      else {
        $newtext = _glossify_replace_terms_simplehtmldom($text, $configurations, $node);
      }
      return $newtext;
    default:
      return $text;
  }
}
function _glossify_replace_terms_simplehtmldom($text, $configurations, $node) {
  $html_body = str_get_html($text);
  foreach ($configurations as $config_name => $configuration) {
    if (isset($configuration['from'][$node->type])) {
      $enabled_styles = array_filter($configuration['style']);
      $compare_function = $configuration['case_insensitivity'] ? 'stripos' : 'strpos';
      foreach (_fetch_possible_keywords($config_name, $configuration, $node->nid) as $kwd) {
        list($term_title, $target_url) = $kwd;
        $replaced = 0;
        foreach ($enabled_styles as $style => $enabled) {
          if ($enabled && !empty($term_title) && false !== $compare_function($html_body->innertext, $term_title)) {
            $replacement = _fetch_replacement($style, $term_title, $target_url);
            _glossify_replace($configuration, $html_body, $term_title, $replacement, $replaced);

            // Update the state of the DOM to reflect new tags added.
            if (0 < $replaced) {
              $html_body = str_get_html($html_body->innertext);
            }
          }
        }
      }
    }
  }
  return $html_body->innertext;
}

/**
 * Uses PHP's built in DOM extension if available.
 * Adapted from http://stackoverflow.com/questions/3151064/find-and-replace-keywords-by-hyperlinks-in-an-html-fragment-via-php-dom
 *
 * @param  $text
 * @param  $configurations
 * @param  $node
 * @return text
 */
function _glossify_replace_terms_phpdom($text, $configurations, $node) {
  $original = $text;
  $text = '<meta http-equiv="content-type" content="text/html; charset=utf-8">' . $text;
  $dom = new DOMDocument();
  $dom->formatOutput = true;
  @$dom
    ->loadHTML($text);
  if (!$dom
    ->hasChildNodes()) {
    return $original;
  }
  $xpath = new DOMXpath($dom);
  $xpath_query = "//text()";

  // If there are no text nodes, there is nothing to do.
  $text_nodes = $xpath
    ->query($xpath_query);
  if (0 == $text_nodes->length) {
    return $original;
  }
  $has_replacements = false;
  foreach ($configurations as $config_name => $configuration) {
    if (isset($configuration['from'][$node->type])) {
      $enabled_styles = array_filter($configuration['style']);

      // Check for empty exclusion list.
      $exclude_tags = is_array($configuration['excl_tags']) ? $configuration['excl_tags'] : array();
      $keywords = _fetch_possible_keywords($config_name, $configuration, $node->nid);
      foreach ($keywords as $kwd) {
        list($term_title, $target_url) = $kwd;
        if (empty($term_title)) {
          continue;
        }
        $rx = _glossify_regex($configuration, $term_title);
        if (!preg_match($rx, $original)) {
          continue;
        }
        foreach ($enabled_styles as $style => $enabled) {
          $replacement = _fetch_replacement($style, $term_title, $target_url);
          $child = $dom->firstChild;
          while ($child) {
            if (_glossify_process_domnode($child, $dom, $rx, $replacement, $configuration['excl_mode'], $exclude_tags, $configuration['only_first'])) {
              $has_replacements = true;
              if ($configuration['only_first']) {
                break;
              }
            }
            $child = $child->nextSibling;
          }
        }
      }
    }
  }
  if (false == $has_replacements) {
    return $original;
  }

  // Return just what is in the body tags of our document.
  $body = $dom
    ->GetElementsByTagName('body')
    ->item(0);
  if ($body instanceof DOMNode) {

    // PHP 5.2 compatibility, saveHTML does not accept args.
    $return = new DOMDocument();
    $root = $return
      ->createElement('body');
    $root = $return
      ->appendChild($root);
    $result_node = $return
      ->importNode($body, true);
    $return->documentElement
      ->appendChild($result_node);
    $out = $return
      ->saveHTML();
    $out = str_replace(array(
      '<body>',
      '</body>',
    ), '', $out);
    return $out;
  }
  else {
    return $original;
  }
}

/**
 * Helper function to traverse DOM in predictable order.
 *
 * @param $domnode object
 *  node object.
 * @param $dom object
 *  DOM object.
 * @param $rx string
 *  Regular expression to match.
 * @param $replacement
 *  Text to substitute.
 * @param $excl_mode integer
 *  Exclusion mode value.
 * @param $exclude_tags array
 *  Tags to match for exclude / include functionality.
 * @param $only_first boolean
 *  Flag to indicate whether to match all text occurances.
 * @param $next_node
 *  Can be set to return actual next sibling in event of text placement.
 * @param $level
 *  Tracks DOM hierarchy level for debugging.
 *
 * @return boolean
 *  TRUE indicates text replacement was done.
 */
function _glossify_process_domnode($domnode, $dom, $rx, $replacement, $excl_mode, $exclude_tags, $only_first = FALSE, &$next_node = NULL, $level = 0) {
  $result = false;

  // Flag to indicate text replacement has happened.
  switch ($domnode->nodeType) {
    case XML_TEXT_NODE:
      $exclude_this = false;

      // Find real parent by looking at ancestors and skip anything with class="glossify_term".
      // In practice, if every element in the document had a "glossify_term" class manually added,
      // the upward traversal will still stop at the parent body element, which is created by
      // the loadHTML processing of the text.
      if (!empty($exclude_tags)) {
        $tmpnode = $domnode;
        $real_parent = '';
        while ($tmpnode->parentNode) {
          if ($tmpnode->parentNode
            ->hasAttributes()) {
            $parent_class = $tmpnode->parentNode->attributes
              ->getNamedItem('class');
            if (is_object($parent_class)) {
              if (preg_match('/glossify_term/', $parent_class->nodeValue) !== 0) {
                $tmpnode = $tmpnode->parentNode;
                continue;
              }
            }
          }
          $real_parent = $tmpnode->parentNode->nodeName;
          break;
        }

        // Test for exclusion / inclusion.
        if ($excl_mode == 0 && isset($exclude_tags[$real_parent]) || $excl_mode == 1 && !isset($exclude_tags[$real_parent])) {

          // Include
          $exclude_this = true;
        }
      }

      // Save next sibling before possible replaceChild. DO NOT CLONE the DOMnode!
      $next_sibling = $domnode->nextSibling;
      if (!$exclude_this) {
        $replaced = preg_replace($rx, $replacement, $domnode->wholeText, $only_first ? 1 : -1, $replace_done);
        if ($replace_done) {
          $new_node = $dom
            ->createDocumentFragment();
          $replaced = preg_replace('/&(?!amp;)/', '&amp;', $replaced);
          if (@$new_node
            ->appendXML($replaced)) {
            $domnode->parentNode
              ->replaceChild($new_node, $domnode);
            $result = true;

            // Set return value of variable to indicate next actual sibling.
            $next_node = $next_sibling;
          }
        }
      }
      break;
    default:
      if ($domnode
        ->hasChildNodes()) {
        $child = $domnode->firstChild;
        while ($child) {

          // Exclude these elements (and hence all their children) here.
          if ($child->nodeType != XML_TEXT_NODE && ($child->nodeName == 'iframe' || $child->nodeName == 'object')) {
            continue;
          }
          else {
            $nxtnode = NULL;
            $result = _glossify_process_domnode($child, $dom, $rx, $replacement, $excl_mode, $exclude_tags, $only_first, $nxtnode, $level + 1) || $result;
            if ($result && $only_first) {
              break;
            }
          }
          if (is_object($nxtnode)) {
            $child = $nxtnode;
          }
          else {
            $child = $child->nextSibling;
          }
        }
      }
      break;
  }
  return $result;
}

/**
 * Helper function that emulates a contributing module's nodeapi load operation
 * @param stdObject
 * @param module
 */
function _glossify_node_load(&$node, $module) {
  $fn = $module . '_nodeapi';
  $result = $fn($node, 'load');
  if ($result) {

    // The content module seems to produce no output with the load operation
    foreach ($result as $key => $value) {
      $node->{$key} = $value;
    }
  }
}

/**
 * Helper function that fetches and returns the styled term depending on the style and term.
 */
function _glossify_replace($configuration, &$html, $term_title, $replacement, &$replaced) {
  $text_nodes = $html
    ->find('text');

  // Add some specific tags to exclusion list.
  $exclude_tags = array_merge(is_array($configuration['excl_tags']) ? $configuration['excl_tags'] : array(), array(
    'iframe' => 'iframe',
    'object' => 'object',
  ));
  foreach ($text_nodes as $text_node) {
    if (!isset($text_node->parent->tag) || $configuration['excl_mode'] == 0 && !isset($exclude_tags[$text_node->parent->tag]) || $configuration['excl_mode'] == 1 && isset($exclude_tags[$text_node->parent->tag])) {

      // Include
      $text_node->innertext = preg_replace(_glossify_regex($configuration, $term_title), $replacement, $text_node->innertext, $configuration['only_first'] ? 1 : -1, $result);
      if ($result) {
        $replaced += $result;
        if ($configuration['only_first']) {
          break;
        }
      }
    }
  }
}

/**
 * Helper function that fetches and returns the styled term depending on the style and term.
 */
function _fetch_replacement($style, $term_title, $target_url) {

  // '$1' is the regex placeholder. This allows the original case of the matching text to be retained in a
  // case-insensitive replacement.
  switch ($style) {
    case 'links':
      return $replacement = l('$1', $target_url, array(
        'attributes' => array(
          'title' => $term_title,
          'class' => 'glossify_term',
        ),
      ));
    case 'hovertip':
      return $replacement = '<span class="glossify_term hovertip_target" hovertip="' . $term_title . '">$1</span>';
    case 'reference':
      return $replacement = '<span class="glossify_term">' . $term_title . '</span>';
  }
}

/**
 * Helper function that fetches and returns an array of all keywords of a node, or just the ones for a specific method.
 */
function _fetch_keywords($nid, $method = FALSE, $configuration = NULL) {
  if (!$configuration) {
    $q = db_query("SELECT term, method FROM {glossify} WHERE nid = %d ORDER BY term", $nid);
  }
  else {
    $q = db_query("SELECT term, method FROM {glossify} WHERE nid = %d AND configuration = '%s' ORDER BY term", $nid, $configuration);
  }
  $glossary = array();
  while ($r = db_fetch_array($q)) {
    if ($method) {
      if ($method == $r['method']) {
        $glossary[] = $r['term'];
      }
    }
    else {
      $glossary[$r['method']][] = $r['term'];
    }
  }
  return $glossary;
}

/**
 * Helper function that performs operations on the Glossify keyword-table and returns the new, old or removed values, depending on the operation.
 */
function _keyword_table($operation, $nid, $method, $configuration = 'global', $language = '', $term = '', $alternate = '') {

  //dpm(array($operation, $nid, $method, $configuration, $language, $term, $alternate));
  switch ($operation) {
    case 'insert':
      $gnid = db_result(db_query("SELECT nid FROM {glossify} WHERE term = '%s' AND language = '%s' AND method = '%s' AND configuration = '%s'", $term, $language, $method, $configuration));
      $new_terms = array();
      if (!$gnid) {
        db_query("INSERT INTO {glossify} (nid, term, language, method, alternate, configuration) VALUES (%d, '%s', '%s', '%s', '%s', '%s')", $nid, $term, $language, $method, $alternate, $configuration);
        $new_terms[$term] = $term;
      }
      else {
        $node = node_load($gnid);
        drupal_set_message(t("The keyword: '%term' for the %method-method under configuration %config was already assigned to point to <a href=\"/node/{$node->nid}\">%title</a>", array(
          '%term' => $term,
          '%title' => $node->title,
          '%method' => $method,
          '%config' => $configuration,
        )), 'warning');
      }
      return $new_terms;
      break;
    case 'update':
      $old_term = db_result(db_query("SELECT term FROM {glossify} WHERE nid = %d AND method = '%s'", $nid, 'title'));
      db_query("UPDATE {glossify} SET term = '%s', language = '%s', method = '%s' WHERE nid = %d AND method = '%s' AND configuration = '%s'", $term, $language, $method, $nid, 'title', $configuration);
      return array(
        $old_term => $old_term,
      );
      break;
    case 'delete':
      $old_terms = array();
      if (!empty($term)) {
        $old_terms[$term] = $term;
        db_query("DELETE FROM {glossify} WHERE nid = %d AND method = '%s' AND language = '%s' AND term = '%s' AND configuration = '%s'", $nid, $method, $language, $term, $configuration);
      }
      else {
        $q = db_query("SELECT term FROM {glossify} WHERE nid = %d AND method = '%s' AND configuration = '%s' ORDER BY term", $nid, $method, $configuration);
        while ($r = db_fetch_array($q)) {
          $old_terms[$r['term']] = $r['term'];
        }
        db_query("DELETE FROM {glossify} WHERE nid = %d AND method = '%s' AND configuration = '%s'", $nid, $method, $configuration);
      }
      return $old_terms;
      break;
  }
  return TRUE;
}

/**
 * Helper function that fetches and returns an array of all new and old keywords of a node, depending on the method.
 * It also provides for an easy way to update the keywords of a given method/configuration combination.
 */
function _fetch_affected_keywords($node, $method, $configuration = 'global', $keywordsource = NULL, $override = '') {
  $keywords = array();
  $existing_keywords = _fetch_keywords($node->nid, $method, $configuration);
  switch ($method) {
    case 'internal':
      $form_keywords = !empty($node->glossify_keywords) ? array_map('trim', explode(',', $node->glossify_keywords)) : array();
      $form_override = $node->glossify_override;
      break;
    case 'cck':
      $keyword = $node->{$keywordsource};
      $override = $node->{$override};
      if (isset($keyword[0]['value'])) {
        $form_keywords = !empty($keyword[0]['value']) ? array_map('trim', explode(',', $keyword[0]['value'])) : array();
      }
      if (isset($override[0]['value'])) {
        $form_override = $override[0]['value'];
      }
      else {
        $form_override = '';
      }
      break;
    case 'taxonomy':
      if (isset($node->taxonomy)) {
        $form_keywords = array();
        foreach ($node->taxonomy['tags'] as $vid => $tag) {
          if (in_array($vid, $keywordsource)) {
            $form_keywords = array_merge($form_keywords, array_map('trim', explode(',', $tag)));
          }
        }
        $form_override = $override;
      }
      break;
  }
  if (count($form_keywords) > 0) {
    if (count($existing_keywords)) {
      $ikwds1 = array_diff($existing_keywords, $form_keywords);
      $ikwds2 = array_diff($form_keywords, $existing_keywords);
      if (count($ikwds1) !== count($ikwds2) || count($ikwds1) !== 0 && count($ikwds2) !== 0) {
        $ikwds3 = array_intersect($ikwds1, $ikwds2);
        if (count($ikwds3) == 0) {
          foreach ($ikwds1 as $keyword) {
            $keywords = array_merge($keywords, _keyword_table('delete', $node->nid, $method, $configuration, $node->language, $keyword));
          }
          foreach ($ikwds2 as $keyword) {
            $keywords = array_merge($keywords, _keyword_table('insert', $node->nid, $method, $configuration, $node->language, $keyword, $form_override));
          }
        }
      }
    }
    else {
      foreach ($form_keywords as $keyword) {
        $keywords = array_merge($keywords, _keyword_table('insert', $node->nid, $method, $configuration, $node->language, $keyword, $form_override));
      }
    }
  }
  else {
    if (count($existing_keywords) > 0) {
      $keywords = array_merge($keywords, _keyword_table('delete', $node->nid, $method, $configuration));
    }
  }
  return $keywords;
}

/**
 * Helper function that fetches and returns an array of all the nodes that contain the keywords.
 */
function _fetch_affected_nodes($keywords, $content_types) {
  $nodes = array();
  foreach ($keywords as $keyword) {
    $q = db_query("SELECT n.nid FROM {node} n INNER JOIN {node_revisions} r ON r.nid = n.nid WHERE n.type IN (" . db_placeholders($content_types, 'varchar') . ") AND r.body LIKE '%%%s%%'", $content_types, $keyword);
    while ($r = db_fetch_array($q)) {
      $nodes[] = $r['nid'];
    }
  }
  return $nodes;
}

/**
 * Helper function that fetches the possible keywords for a given configuration and nid.
 */
function _fetch_possible_keywords($config_name, $configuration, $nid) {
  $glossify_dict = array();
  static $keyword_lists = array();
  if (isset($keyword_lists[$config_name][$nid])) {
    return $keyword_lists[$config_name][$nid];
  }
  $methods = array();
  if ($configuration['methods']['use_title']) {
    $methods[] = 'title';
  }
  if ($configuration['methods']['use_internal']) {
    $methods[] = 'internal';
  }
  if ($configuration['methods']['use_cck']) {
    $methods[] = 'cck';
  }
  if ($configuration['methods']['use_taxonomy']) {
    $methods[] = 'taxonomy';
  }
  $query = "SELECT g.term, g.nid, g.alternate, g.language, g.method FROM {node} n INNER JOIN {glossify} g ON g.nid = n.nid WHERE n.type IN (" . db_placeholders($configuration['to'], 'varchar') . ") AND g.method IN (" . db_placeholders($methods, 'varchar') . ") AND g.configuration = '%s' AND n.status = 1";
  if ($configuration['language']) {
    $query .= " AND g.language = n.language";
  }
  if (!$configuration['link_self']) {
    $query .= " AND g.nid != %d";
    $q = db_query($query, array_merge($configuration['to'], $methods, (array) $config_name, (array) $nid));
  }
  else {
    $q = db_query($query, array_merge($configuration['to'], $methods, (array) $config_name));
  }
  while ($r = db_fetch_array($q)) {
    if ($configuration['methods']['link_term'] && $r['method'] == 'taxonomy') {
      $term = db_fetch_array(db_query("SELECT tid FROM {term_data} WHERE name = '%s'", $r['term']));
      $glossify_dict[] = array(
        $r['term'],
        drupal_lookup_path('alias', 'taxonomy/term/' . $term['tid'], $r['language']),
      );
    }
    else {
      if (!empty($r['alternate'])) {
        if (is_numeric($r['alternate'])) {
          $glossify_dict[] = array(
            $r['term'],
            drupal_lookup_path('alias', 'node/' . $r['alternate'], $r['language']),
          );
        }
        elseif ($path = drupal_lookup_path('alias', $r['alternate'], $r['language'])) {
          $glossify_dict[] = array(
            $r['term'],
            $path,
          );
        }
        else {
          $glossify_dict[] = array(
            $r['term'],
            $r['alternate'],
          );
        }
      }
      else {
        $glossify_dict[] = array(
          $r['term'],
          drupal_lookup_path('alias', 'node/' . $r['nid'], $r['language']),
        );
      }
    }
  }

  // Add to cache.
  $keyword_lists[$config_name][$nid] = $glossify_dict;
  return $glossify_dict;
}

/**
 * Helper function that updates the keyword-tables according to the new/old methods.
 */
function _update_keywords_for_methods($content_types, $methods, $configuration) {
  $q = db_query("SELECT nid, vid, type, title, language FROM {node} WHERE status=1 AND type IN (" . db_placeholders($content_types, 'varchar') . ")", $content_types);
  while ($node = db_fetch_object($q)) {
    if (isset($methods['title'])) {
      if ($methods['title']) {
        _keyword_table('insert', $node->nid, 'title', $configuration, $node->language, $node->title);
      }
      else {
        _keyword_table('delete', $node->nid, 'title', $configuration, $node->language);
      }
    }
    if (isset($methods['internal'])) {
      _glossify_node_load($node, 'glossify');
      if ($methods['internal']['use_internal']) {
        _fetch_affected_keywords($node, 'internal', $configuration);
      }
      else {
        _keyword_table('delete', $node->nid, 'internal', $configuration, $node->language);
      }
    }
    if (isset($methods['cck'])) {
      _glossify_node_load($node, 'content');
      if ($methods['cck']['use_cck']) {
        _fetch_affected_keywords($node, 'cck', $configuration, $methods['cck']['keyword_field'], $methods['cck']['override_field']);
      }
      else {
        _keyword_table('delete', $node->nid, 'cck', $configuration, $node->language);
      }
    }
    if (isset($methods['taxonomy'])) {
      _glossify_node_load($node, 'taxonomy');
      if ($methods['taxonomy']['use_taxonomy']) {
        _fetch_affected_keywords($node, 'taxonomy', $configuration, $methods['taxonomy']['vocabulary']);
      }
      else {
        _keyword_table('delete', $node->nid, 'taxonomy', $configuration, $node->language);
      }
    }
  }
}

/**
 * Helper function that returns the modifiers of a given configuration
 */
function _fetch_modifiers($configuration) {
  $modifiers = '';
  if ($configuration['unicode']) {
    $modifiers .= 'u';
  }
  if ($configuration['case_insensitivity']) {
    $modifiers .= 'i';
  }
  return $modifiers;
}

/**
 * Helper function that returns the configurations with the same to-content-types
 */
function _check_for_configuration_conflicts($configurations, $content_types, $method) {
  foreach ($configurations as $configuration) {
    if (count(array_intersect($content_types, $configuration['to'])) > 0) {
      if ($configuration['methods'][$method]) {
        return FALSE;
      }
    }
  }
  return TRUE;
}

/**
 * Helper function that cleans up old variables and inserts old cck values into the keyword table.
 */
function _backwards_compatibility() {
  $keyword_field = variable_get('glossify_use_this_cck_field_for_keyword_synonyms', FALSE);
  $override_field = variable_get('glossify_use_this_cck_field_for_target_url_override', '');
  if ($keyword_field) {
    $content_types = variable_get('glossify_glossary_content_type', array());
    if (count($content_types > 0)) {
      $methods = array(
        'title',
        'cck' => array(
          'keyword_field' => $keyword_field,
          'override_field' => $override_field,
        ),
      );
      _update_keywords_for_methods($content_types, $methods);
    }
  }
  variable_del('glossify_glossary_content_type');
  variable_del('glossify_content_types_to_search');
  variable_del('glossify_link_first_only');
  variable_del('glossify_do_we_need_unicode_compatibility');
  variable_del('glossify_teaser');
  variable_del('glossify_style');
  variable_del('glossify_dont_break_words');
  variable_del('glossify_display_parsing_time_for_performance_debugging');
  variable_del('glossify_use_this_cck_field_for_keyword_synonyms');
  variable_del('glossify_use_this_cck_field_for_target_url_override');
}

/**
 * Themes a single comment and related items.
 *
 * @param $comment
 *   The comment object.
 * @param $node
 *   The comment node.
 * @param $links
 *   An associative array containing control links suitable for passing into
 *   theme_links(). These are generated by modules implementing hook_link() with
 *   $type='comment'. Typical examples are links for editing and deleting
 *   comments.
 * @param $visible
 *   Switches between folded/unfolded view. If TRUE the comments are visible, if
 *   FALSE the comments are folded.
 * @ingroup themeable
 */
function glossify_theme_comment_view($comment, $node, $links = array(), $visible = TRUE) {
  static $first_new = TRUE;
  $output = '';
  $comment->new = node_mark($comment->nid, $comment->timestamp);
  if ($first_new && $comment->new != MARK_READ) {

    // Assign the anchor only for the first new comment. This avoids duplicate
    // id attributes on a page.
    $first_new = FALSE;
    $output .= "<a id=\"new\"></a>\n";
  }
  $output .= "<a id=\"comment-{$comment->cid}\"></a>\n";
  $glossify_mode = variable_get('glossify_process_mode', GLOSSIFY_USE_FILTER);

  // Switch to folded/unfolded view of the comment
  if ($visible) {

    // check_markup will invoke filter on comment text for links and hovertip styles if in 'filter' mode.
    $comment->comment = check_markup($comment->comment, $comment->format, FALSE);

    // Comment API hook - will process comment text for links and hovertip styles if in 'non-filter' mode.
    comment_invoke_comment($comment, 'view');

    // Now generate hidden hovertip elements for any comment glossary matches.
    // Processing of the hovertip body text takes place in the theme_glossify_term function.
    if ($glossify_mode != GLOSSIFY_WITHOUT_FILTER_OMIT_COMMENTS) {
      _glossify_comment_hovertips($comment, $node);
    }
    $output .= theme('comment', $comment, $node, $links);
  }
  else {
    $output .= theme('comment_folded', $comment);
  }
  return $output;
}

/**
 * Helper function to generate hovertips from comment matches.
 */
function _glossify_comment_hovertips(&$comment, $node) {
  $configurations = variable_get('glossify_configurations', array());
  $referenced_terms = array();
  foreach ($configurations as $config_name => $configuration) {
    if (in_array($node->type, $configuration['from'])) {
      $enabled_styles = array_filter($configuration['style']);
      $possible_keywords = _fetch_possible_keywords($config_name, $configuration, $node->nid);
      if (in_array('hovertip', $configuration['style']) && $configuration['style']['hovertip']) {
        foreach ($possible_keywords as $kwd) {
          list($term_title, $path) = $kwd;
          if (in_array($term_title, $referenced_terms)) {
            continue;
          }
          if (preg_match(_glossify_regex($configuration, $term_title), $comment->comment)) {
            $referenced_terms[] = $term_title;

            // Rough check that node does not already have a hovertip element for this term.
            if (!isset($node->content['glossify_hovertip'][$term_title])) {

              // Theming of comment hovertip will carry out glossify filter on the body
              // if in non-filter mode, to allow hovertips to contain links to other glossary terms.
              $comment->comment .= theme('glossify_term', $path, 'hovertip');
            }
          }
        }
      }
    }
  }
}

/**
 * Helper function to build regular expression.
 */
function _glossify_regex($configuration, $term_title) {
  $modifiers = _fetch_modifiers($configuration);
  if ($configuration['break'] && $configuration['unicode']) {
    $rx = '/(?<![\\pL\\pN_])(' . preg_quote($term_title, '/') . ')(?![\\pL\\pN_])/' . $modifiers;
  }
  else {
    $rx = '/' . ($configuration['break'] ? '\\b(' . preg_quote($term_title, '/') . ')\\b' : '(' . preg_quote($term_title) . ')') . '/' . $modifiers;
  }
  return $rx;
}

Functions

Namesort descending Description
glossify_comment Implementation of hook_comment().
glossify_filter Implementation of hook_filter().
glossify_form_alter Implementation of hook_form_alter().
glossify_menu Implementation of hook_menu().
glossify_nodeapi Implementation of hook_nodeapi().
glossify_perm Implementation of hook_perm().
glossify_theme Implementation of hook_theme().
glossify_theme_comment_view Themes a single comment and related items.
glossify_theme_registry_alter Implementation of hook_theme_registry_alter().
theme_glossify_reference_section Render a glossary term reference.
theme_glossify_term Render a glossary term.
_backwards_compatibility Helper function that cleans up old variables and inserts old cck values into the keyword table.
_check_for_configuration_conflicts Helper function that returns the configurations with the same to-content-types
_fetch_affected_keywords Helper function that fetches and returns an array of all new and old keywords of a node, depending on the method. It also provides for an easy way to update the keywords of a given method/configuration combination.
_fetch_affected_nodes Helper function that fetches and returns an array of all the nodes that contain the keywords.
_fetch_keywords Helper function that fetches and returns an array of all keywords of a node, or just the ones for a specific method.
_fetch_modifiers Helper function that returns the modifiers of a given configuration
_fetch_possible_keywords Helper function that fetches the possible keywords for a given configuration and nid.
_fetch_replacement Helper function that fetches and returns the styled term depending on the style and term.
_glossify_comment_hovertips Helper function to generate hovertips from comment matches.
_glossify_node_load Helper function that emulates a contributing module's nodeapi load operation
_glossify_process_domnode Helper function to traverse DOM in predictable order.
_glossify_regex Helper function to build regular expression.
_glossify_replace Helper function that fetches and returns the styled term depending on the style and term.
_glossify_replace_terms_phpdom Uses PHP's built in DOM extension if available. Adapted from http://stackoverflow.com/questions/3151064/find-and-replace-keywords-by-...
_glossify_replace_terms_simplehtmldom
_keyword_table Helper function that performs operations on the Glossify keyword-table and returns the new, old or removed values, depending on the operation.
_update_keywords_for_methods Helper function that updates the keyword-tables according to the new/old methods.

Constants