From 6b8553e16fa1320953a4ff4b0cb5f9f2739861c0 Mon Sep 17 00:00:00 2001
From: Derek Wright <git@dwwright.net>
Date: Wed, 16 Mar 2011 17:59:11 -0500
Subject: [PATCH] Issue #1095014 by dww: Remember node context without <dme:context> tags.

---
 dme.inc     |   35 +------
 dme.install |   27 ++++-
 dme.module  |  336 +++++++++++++----------------------------------------------
 3 files changed, 98 insertions(+), 300 deletions(-)

diff --git a/dme.inc b/dme.inc
index 1540310..e9c35a7 100644
--- a/dme.inc
+++ b/dme.inc
@@ -11,8 +11,6 @@
  */
 class dmeEngine {
   var $_tag_list = array();
-  var $_cid = -1;
-  var $_type = '';
 
   function dmeEngine($tag_array = array()) {
     if (count($tag_array) == 0) {
@@ -21,30 +19,6 @@ class dmeEngine {
     $this->_tag_list = $tag_array;
   }
 
-  function set_node($node) {
-    $this->_type = 'node';
-    $this->_cid = $node->nid;
-  }
-
-  /**
-	 * Looks through a block of text to determine if context - a node or other id - can be read from it.
-	 *
-	 * @return bool Is context established?
-	 */
-  function dme_set_context($text = NULL) {
-    if (empty($text)) {
-      return (isset($this->_type) && !empty($this->_type) && isset($this->_cid) && !empty($this->_cid));
-    }
-    else {
-      $context_pattern = '!<dme:context type=(["\'])(.*?)\1 cid=(["\'])(.*?)\3 />!i';
-      if (preg_match($context_pattern, $text, $matches)) {
-        $this->_type = $matches[2];
-        $this->_cid = (int)$matches[4];
-      }
-      return (is_numeric($this->_cid) && in_array($this->_type, array('node', 'comment', 'user')));
-    }
-  }
-
   /**
    * Fetches and returns an array of tags which the DME is handling.
    *
@@ -202,12 +176,9 @@ class dmeEngine {
    *  Text to replace the tags with.
    */
   function _dme_process_callback($matches) {
-    $nid = 0;
-    if (isset($this->_type) && isset($this->_cid)) {
-      if ($this->_type == 'node') {
-        $nid = $this->_cid;
-      }
-    }
+    // Find the node we're currently processing, if any.
+    $current_node = node_content_view();
+    $nid = !empty($current_node) ? $current_node->nid : -1;
 
     // TODO: Change this to work via the array index of the tag list.
     foreach ($this->_tag_list as $tag) {
diff --git a/dme.install b/dme.install
index 48cdd05..5644066 100644
--- a/dme.install
+++ b/dme.install
@@ -2,11 +2,18 @@
 
 /**
  * @file
- * Install file for dme module - sets the module weight to -11.
+ * Install file for dme module.
  */
 
+/**
+ * Set the weight of the dme module higher than CCK, node, etc.
+ *
+ * We need our copy of hook_nodeapi('view') to happen after anything
+ * that inserts things into the node and might be using DME tags.
+ * @see node_content_view() (which lives in dme.module).
+ */
 function dme_install() {
-  db_query("UPDATE {system} SET weight = -11 WHERE name = 'dme' AND type = 'module'");
+  db_query("UPDATE {system} SET weight = 10 WHERE name = 'dme' AND type = 'module'");
 }
 
 /**
@@ -17,4 +24,18 @@ function dme_update_6000() {
 
   $ret[] = update_sql("UPDATE {system} SET weight = -11 WHERE name = 'dme' AND type = 'module'");
   return $ret;
-}
\ No newline at end of file
+}
+
+/**
+ * Set the weight of the dme module higher than CCK, node, etc.
+ *
+ * We need our copy of hook_nodeapi('view') to happen after anything
+ * that inserts things into the node and might be using DME tags.
+ * @see node_content_view() (which lives in dme.module).
+ */
+function dme_update_6001() {
+  $ret = array();
+
+  $ret[] = update_sql("UPDATE {system} SET weight = 10 WHERE name = 'dme' AND type = 'module'");
+  return $ret;
+}
diff --git a/dme.module b/dme.module
index 175b169..08fc862 100644
--- a/dme.module
+++ b/dme.module
@@ -1,36 +1,10 @@
 <?php
+
 /**
  * @file
  * Base module that hooks up the dmeEngine class to nodes and implements it's features as a filter.
  */
 
-define('DME_CONTEXT_REGEXP', '!<dme:context (type|cid)=(["\'])(.*?)\2 (type|cid)=(["\'])(.*?)\5\s*/?>\s*(</dme:context>)?!i');
-
-function dme_menu() {
-  $items = array();
-
-  $items['admin/settings/dme'] = array(
-    'title' => 'Drupal Markup Engine',
-    'description' => 'Regenerate context tags for the DME - only used if upgrading.',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('dme_batch_form'),
-    'access arguments' => array('access administration pages'),
-    'file' => 'dme_batch.inc',
-  );
-  $items['admin/settings/dme/batch'] = array(
-    'title' => 'Drupal Markup Batch',
-    'description' => 'Regenerate context tags for the DME - only used if upgrading.',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('dme_batch_form'),
-    'access arguments' => array('access administration pages'),
-    'file' => 'dme_batch.inc',
-    'type' => MENU_DEFAULT_LOCAL_TASK,
-    'weight' => -10,
-  );
-
-  return $items;
-}
-
 /**
  * Implements hook_filter
  *
@@ -61,13 +35,10 @@ function dme_filter($op, $delta = 0, $format = -1, $text = '') {
     case 'prepare':
       //Find matching approved tags, and change them to square brackets to cheat the html filter!
       $dme = new dmeEngine();
-      $has_context = $dme->dme_set_context($text); //??
       $text = $dme->_dme_prepare_text($text);
       return $text;
     case 'process':
-      if ($dme->dme_set_context()) {
-        $text = $dme->_dme_process_text($text);
-      }
+      $text = $dme->_dme_process_text($text);
       return $text;
     default:
       return $text;
@@ -91,254 +62,89 @@ function dme_filter_tips($delta, $format, $long = FALSE) {
 }
 
 /**
- * Implements hook_nodeapi
+ * Implement hook_view() on behalf of node.module.
  *
- * @param unknown_type $node
- * @param unknown_type $op
- * @param unknown_type $a3
- * @param unknown_type $a4
- */
-function dme_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
-  if ($op == 'presave') {
-    _dme_nodeapi_presave($node);
-  }
-  if ($op == 'prepare') {
-    _dme_nodeapi_prepare($node);
-  }
-  if ($op == 'insert') {
-    _dme_nodeapi_insert($node);
-  }
-}
-
-/**
- * Returns the correct node context tag to place in node bodies, teasers, etc.
+ * This is sort of an evil hack, but it works beautifully to remember the node
+ * we're currently in the middle of processing with check_markup() so that we
+ * can have DME tags that require a node context.
  *
- * @param int $nid Node Id for context.
- * @return string XHTML tag containing context information.
- */
-function _dme_get_node_context_tag($nid) {
-  return '<dme:context type=\'node\' cid=\''. $nid .'\' />';
-}
-
-/**
- * This function gets the list of fields to apply the dme against, per given node type.
- *
- * This is the point where we call the new hook_dme_apply_fields, where each module that has
- * defined a field for the node can indicate if it's a filtered field and it's type.  The intention
- * is that each caller will return an array indexed by the field in the node object, and containing an array
- * with two indexes, 'dme_table_name' for the name of the table holding the data, and 'dme_field_name' for the
- * name of the field holding the data.
+ * node.module's node_build_content() tries to invoke hook_view(). To avoid
+ * namespace conflicts, node_invoke() uses 'node_content' for all of node's
+ * own hooks. However, since node.module itself doesn't implement
+ * node_content_view(), we can do so ourselves. ;) This lets us have our own
+ * static stack of nodes we're in the middle of rendering. That way, deep
+ * inside hook_filter(), we can call this function again with no arguments to
+ * say "tell me the node you're in the middle of building." By storing the
+ * current node in an array we use as a stack, it even works if your DME tags
+ * are inserting other nodes into the content of a parent node. Once we hit
+ * node_build_content() again, we hit this function again with a different
+ * node and push it onto the stack. While we process the child's text areas,
+ * the current context will be this child node.
  *
- * What about multi-value fields?  Hmmm...
+ * When hook_view() is over, node_build_content() calls nodeapi('view').
+ * That's where CCK and friends inject their content into the node. So long as
+ * the system weight for dme.module is heavier than anything injecting itself
+ * into the node, when we hit our own copy of dme_nodeapi(), we can call this
+ * function with FALSE to say that we're done processing the current node and
+ * can pop it off the stack. That way, once we're done with a child node and
+ * continue processing tags for the parent node, we've always got the right
+ * node context.
  *
- * @param string $node_type Type of node to get data for.
- * @return array An array of fields which can have dme applied to.
- */
-function dme_get_fields_to_apply($node_type) {
-  $fields = array();
-
-  $modules = module_implements('dme_apply_fields');
-
-  foreach ($modules as $module) {
-    $module_fields = module_invoke($module, 'dme_apply_fields', $node_type);
-    $fields = array_merge($fields, $module_fields);
-  }
-
-  return $fields;
-}
-
-/**
- * Implements hook_dme_apply_fields(), for the cck module.
+ * @param mixed $node
+ *   There are three possible values of this parameter:
+ *   - A full $node object when called by core's node_invoke(). In this case
+ *     we're starting to process a new node and want to push this onto the top
+ *     of our node context stack.
+ *   - FALSE when called by dme_nodeapi('view'). In this case, we're done
+ *     processing a node and want to pop that node off our node context stack.
+ *   - NULL when called by anyone that wants to know the current node context.
+ * @param boolean $teaser
+ *   Are we building a node teaser or a full node?
+ * @param boolean $page
+ *   Are we building a node as a full page, or in a listing?
  *
- * Since cck won't have information about dme in it's core, we implement this for them.
- * Besides, it's also a nice reference for folks who need to implement this for custom nodes.
+ * @return stdClass
+ *   The current $node object, prepared for viewing.
  */
-function content_dme_apply_fields($node_type) {
-  $result = array();
-  // get fields from cck records.
-  $data = content_types($node_type);
-  // return appropriate fields!
-  $default_table = '';
-  foreach ($data['tables'] as $tablename) {
-    if (stripos($tablename, 'content_type') === 0) {
-      $default_table = $tablename;
-    }
-  }
-
-  foreach ($data['fields'] as $field_name => $field_data) {
-    if ($field_data['type'] == 'text' && array_key_exists('format', $field_data['columns'])) {
-      $table = $default_table;
-      foreach ($data['tables'] as $tablename) {
-        if ('content_'. $field_data['field_name'] == $tablename) {
-          $table = $tablename;
-        }
-      }
-      $result[$field_name] = array(
-        'dme_table_name' => $table,
-        'dme_field_name' => $field_data['field_name'],
-        'multiple' => $field_data['multiple'],
-        'field_node_path' => array($field_name, ($field_data['multiple'] ? '#' : '0'), 'value'),
-        'module' => 'cck',
-                                    );
-    }
-  }
-
-  return $result;
-}
-
-/**
- * Called by dme_nodeapi when $op == 'presave'.
- *
- * The idea is to put a dme:context field into the body and other filtered fields.
- */
-function _dme_nodeapi_presave(&$node) {
-  if (isset($node->nid)) {
-    // Check to see if the teaser is the start of the body or not.
-    $include = !isset($node->teaser) || ($node->teaser == substr($node->body, 0, strlen($node->teaser)));
-    $node_context = _dme_get_node_context_tag($node->nid);
-    // Some of this may no longer be necessary because of the prepare nodeapi call that has been added.
-    if (preg_match(DME_CONTEXT_REGEXP, $node->body, $matches)) {
-      $new_body = preg_replace(DME_CONTEXT_REGEXP, $node_context, $node->body);
-      if (module_exists('wysiwyg')) {
-        // If the wysiwyg module is enabled, then some wysiwyg editors will surround the <dme:context>tag with a paragraph tag - which
-        // can screw up the teaser.
-        $nodetag_in_p_pattern = '!<p>'. $node_context .'</p>!i';
-        $new_body = preg_replace($nodetag_in_p_pattern, $node_context, $new_body);
-      }
-    }
-    else {
-      $new_body = $node_context . $node->body;
-    }
-
-    $node->body = $new_body;
-    if ($include) {
-      $node->teaser = node_teaser($node->body);
-    }
-    else {
-      $node->teaser = $node_context . $node->teaser;
-    }
-
-    // Now, get the fields as needed from the hook and process them with context:
-    $filter_fields = dme_get_fields_to_apply($node->type);
-    foreach ($filter_fields as $field_name => $field_data) {
-      if ($field_data['multiple']) {
-        foreach ($node->{$field_data['field_node_path'][0]} as $field_index => &$field_data_item) {
-          _dme_set_filter_field($field_data_item[$field_data['field_node_path'][2]], $node_context);
-        }
-      }
-      else {
-        switch (count($field_data['field_node_path'])) {
-          case 1:
-            _dme_set_filter_field($node->{$field_data['field_node_path'][0]}, $node_context);
-            break;
-          case 2:
-            _dme_set_filter_field($node->{$field_data['field_node_path'][0]}[$field_data['field_node_path'][1]], $node_context);
-            break;
-          case 3:
-            _dme_set_filter_field($node->{$field_data['field_node_path'][0]}[$field_data['field_node_path'][1]][$field_data['field_node_path'][2]], $node_context);
-            break;
-        }
-      }
-    }
-  }
-}
-
-function _dme_set_filter_field(&$field_value, $node_context) {
-  if (preg_match(DME_CONTEXT_REGEXP, $field_value, $matches)) {
-    $field_value = preg_replace(DME_CONTEXT_REGEXP, $node_context, $field_value);
-  }
-  else {
-    $field_value = $node_context . $field_value;
-  }
-}
-
-/**
- * Called by dme_nodeapi when $op == 'insert'.
- *
- * Since there isn't a nid or vid to use in the presave step, here we go back
- * and put the appropriate node_context in place.
- */
-function _dme_nodeapi_insert(&$node) {
-  $node_context = _dme_get_node_context_tag($node->nid);
-
-  $node->teaser = $node_context . $node->teaser;
-  $node->body = $node_context . $node->body;
-
-  $update_query = "UPDATE {node_revisions} SET body = '%s', teaser = '%s' WHERE vid = %d";
-  db_query($update_query, $node->body, $node->teaser, $node->vid);
-
-  // Now, get the fields as needed from the hook and process them with context:
-  $filter_fields = dme_get_fields_to_apply($node->type);
-  foreach ($filter_fields as $field_name => $field_data) {
-    if ($field_data['multiple']) {
-      foreach ($node->{$field_data['field_node_path'][0]} as $field_index => &$field_data_item) {
-        _dme_insert_filter_field($field_data_item[$field_data['field_node_path'][2]], $node_context);
-      }
+function node_content_view($node = NULL, $teaser = NULL, $page = NULL) {
+  // Stack of nodes we're in the middle of processing for saving node context.
+  static $node_contexts = array();
+
+  // If the caller defined a value, it's either because we're starting to
+  // process a new node (in which case it's a real $node object) or it's
+  // because we've finished processing a node and we got FALSE to end the
+  // current node context.
+  if (isset($node)) {
+    // We're done with the current node, remove it from the node context.
+    if ($node === FALSE) {
+      return array_pop($node_contexts);
     }
+    // We're starting a new node, add it to the saved node context.
     else {
-      switch (count($field_data['field_node_path'])) {
-        case 1:
-          _dme_insert_filter_field($node->{$field_data['field_node_path'][0]}, $node_context);
-          break;
-        case 2:
-          _dme_insert_filter_field($node->{$field_data['field_node_path'][0]}[$field_data['field_node_path'][1]], $node_context);
-          break;
-        case 3:
-          _dme_insert_filter_field($node->{$field_data['field_node_path'][0]}[$field_data['field_node_path'][1]][$field_data['field_node_path'][2]], $node_context);
-          break;
-      }
+      $node_contexts[] = $node;
+      return node_prepare($node, $teaser);
     }
   }
-}
-
-function _dme_insert_filter_field(&$value, $node_context) {
-  $value = $node_context . $value;
+  // Otherwise, we were invoked with a NULL, so return the current context.
+  return end($node_contexts);
 }
 
 /**
- * Called by dme_nodeapi when $op == 'prepare'.
+ * Implements hook_nodeapi().
  *
- * We want to remove the dme tag from the body before it's edited to reduce confusion.
+ * @param unknown_type $node
+ * @param unknown_type $op
+ * @param unknown_type $a3
+ * @param unknown_type $a4
  */
-function _dme_nodeapi_prepare(&$node) {
-  if (preg_match(DME_CONTEXT_REGEXP, $node->body, $matches)) {
-    // Check to see if the teaser is the start of the body or not.
-    $include = !isset($node->teaser) || ($node->teaser == substr($node->body, 0, strlen($node->teaser)));
-    $new_body = preg_replace(DME_CONTEXT_REGEXP, '', $node->body);
-    $node->body = $new_body;
-
-    if ($include) {
-      $node->teaser = node_teaser($node->body);
-    }
-    else {
-      $node->teaser = preg_replace(DME_CONTEXT_REGEXP, '', $node->teaser);
-    }
-  }
-
-  // Now, get the fields as needed from the hook and process them with context:
-  $filter_fields = dme_get_fields_to_apply($node->type);
-  foreach ($filter_fields as $field_name => $field_data) {
-    if ($field_data['multiple'] && count($node->{$field_data['field_node_path'][0]})) {
-      foreach ($node->{$field_data['field_node_path'][0]} as $field_index => &$field_data_item) {
-        _dme_prepare_filter_field($field_data_item[$field_data['field_node_path'][2]]);
-      }
-    }
-    else {
-      switch (count($field_data['field_node_path'])) {
-        case 1:
-          _dme_prepare_filter_field($node->{$field_data['field_node_path'][0]});
-          break;
-        case 2:
-          _dme_prepare_filter_field($node->{$field_data['field_node_path'][0]}[$field_data['field_node_path'][1]]);
-          break;
-        case 3:
-          _dme_prepare_filter_field($node->{$field_data['field_node_path'][0]}[$field_data['field_node_path'][1]][$field_data['field_node_path'][2]]);
-          break;
-      }
-    }
+function dme_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
+  if ($op == 'view') {
+    // Now that we've hit hook_nodeapi('view'), we're done processing the
+    // text and should clear out this node from our saved node context.
+    // This both ensures that we've always got the right node context even
+    // when inserting nodes into each other, and it prevents us from
+    // accidentally re-using a node context when we're no longer rendering
+    // fields on a node.
+    node_content_view(FALSE);
   }
 }
-
-function _dme_prepare_filter_field(&$field) {
-  $field = preg_replace(DME_CONTEXT_REGEXP, '', $field);
-}
\ No newline at end of file
-- 
1.7.3.5

