Index: wysiwyg.dialog.inc
===================================================================
RCS file: wysiwyg.dialog.inc
diff -N wysiwyg.dialog.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ wysiwyg.dialog.inc	6 Feb 2009 00:04:55 -0000
@@ -0,0 +1,149 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Wysiwyg dialog page handling functions.
+ */
+
+/**
+ * Menu callback; Output a wysiwyg plugin dialog page.
+ */
+function _wysiwyg_dialog($plugin, $instance) {
+  $plugins = wysiwyg_get_all_plugins();
+  if (!isset($plugins[$plugin])) {
+    return drupal_access_denied();
+  }
+  $callback = $plugin . '_wysiwyg_dialog';
+  if (!function_exists($callback)) {
+    return drupal_not_found();
+  }
+
+  // Suppress admin menu.
+  module_invoke('admin_menu', 'suppress');
+  // Add editor instance id to Drupal.settings.
+  drupal_add_js(array('instance' => $instance), 'setting');
+
+  echo theme('wysiwyg_dialog_page', $callback($instance));
+}
+
+/**
+ * Template preprocess function for theme_wysiwyg_dialog_page().
+ *
+ * @see wysiwyg_dialog()
+ * @see wysiwyg-dialog-page.tpl.php
+ * @see template_preprocess()
+ */
+function template_preprocess_wysiwyg_dialog_page(&$variables) {
+  // Construct page title
+  $head_title = array(strip_tags(drupal_get_title()), variable_get('site_name', 'Drupal'));
+
+  $variables['head_title']        = implode(' | ', $head_title);
+  $variables['base_path']         = base_path();
+  $variables['front_page']        = url();
+  // @todo Would a breadcrumb make sense / possible at all?
+  // $variables['breadcrumb']        = theme('breadcrumb', drupal_get_breadcrumb());
+  $variables['head']              = drupal_get_html_head();
+  $variables['help']              = theme('help');
+  $variables['language']          = $GLOBALS['language'];
+  $variables['language']->dir     = $GLOBALS['language']->direction ? 'rtl' : 'ltr';
+  $variables['messages']          = $variables['show_messages'] ? theme('status_messages') : '';
+  $variables['site_name']         = (theme_get_setting('toggle_name') ? variable_get('site_name', 'Drupal') : '');
+  $variables['css']               = drupal_add_css();
+  $variables['styles']            = drupal_get_css();
+  $variables['scripts']           = drupal_get_js();
+  $variables['tabs']              = theme('menu_local_tasks');
+  $variables['title']             = drupal_get_title();
+  // Closure should be filled last.
+  $variables['closure']           = theme('closure');
+}
+
+/**
+ * @file
+ * Theme template to display a single Wysiwyg (plugin) dialog page.
+ *
+ * Available variables:
+ *
+ * General utility variables:
+ * - $base_path: The base URL path of the Drupal installation. At the very
+ *   least, this will always default to /.
+ * - $css: An array of CSS files for the current page.
+ * - $directory: The directory the theme is located in, e.g. themes/garland or
+ *   themes/garland/minelli.
+ * - $logged_in: TRUE if the user is registered and signed in.
+ * - $is_admin: TRUE if the user has permission to access administration pages.
+ *
+ * Page metadata:
+ * - $language: (object) The language the site is being displayed in.
+ *   $language->language contains its textual representation.
+ *   $language->dir contains the language direction. It will either be 'ltr' or 'rtl'.
+ * - $head_title: A modified version of the page title, for use in the TITLE tag.
+ * - $head: Markup for the HEAD section (including meta tags, keyword tags, and
+ *   so on).
+ * - $styles: Style tags necessary to import all CSS files for the page.
+ * - $scripts: Script tags necessary to load the JavaScript files and settings
+ *   for the page.
+ *
+ * Site identity:
+ * - $site_name: The name of the site, empty when display has been disabled
+ *   in theme settings.
+ *
+ * Page content (in order of occurrance in the default page.tpl.php):
+ * - $breadcrumb: The breadcrumb trail for the current page.
+ * - $title: The page title, for use in the actual HTML content.
+ * - $help: Dynamic help text, mostly for admin pages.
+ * - $messages: HTML for status and error messages. Should be displayed prominently.
+ * - $tabs: Tabs linking to any sub-pages beneath the current page (e.g., the view
+ *   and edit tabs when displaying a node).
+ *
+ * - $content: The main content of the current Drupal page.
+ *
+ * Footer/closing data:
+ * - $footer : The footer region.
+ * - $closure: Final closing markup from any modules that have altered the page.
+ *   This variable should always be output last, after all other dynamic content.
+ *
+ * @see template_preprocess()
+ * @see template_preprocess_wysiwyg_dialog_page()
+ */
+function theme_wysiwyg_dialog_page() {
+$r = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"';
+$r .= '"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">';
+$r .= '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="'. $language->language .'" lang="'. $language->language .'" dir="'. $language->dir .'">';
+$r .= '<head>';
+$r .= '  <title><?php print $head_title; ?></title>';
+$r .= $head . $styles . $scripts;
+$r .= '  <script type="text/javascript"> </script>';
+$r .= '</head>';
+$r .= '<body>';
+$r .= '  <div id="page">';
+$r .= '    <div id="container" class="clear-block">';
+$r .= '      <div id="main" class="column">';
+if (!empty($breadcrumb)) {
+  $r .= '        <div id="breadcrumb">'. $breadcrumb .'</div>';
+}
+$r .= '        <div id="content">';
+if (!empty($title)) {
+  $r .= '        <h1 class="title" id="page-title">'. $title .'</h1>';
+}
+if (!empty($tabs)){
+  $r .= '        <div class="tabs">'. $tabs .'</div>';
+}
+if (!empty($messages)){
+  $r .= $messages; 
+}
+if (!empty($help)) {
+  $r .= $help;
+}
+$r .= '          <div id="content-content" class="clear-block">';
+$r .= $content;
+$r .= '          </div>';
+$r .= '        </div>';
+$r .= '      </div>';
+$r .= '    </div>';
+$r .= '  </div>';
+$r .= $closure;
+$r .= '</body>';
+$r .= '</html>';
+return $r;
+}
\ No newline at end of file
Index: wysiwyg.init.js
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/wysiwyg/wysiwyg.init.js,v
retrieving revision 1.1.2.4
diff -u -p -u -p -r1.1.2.4 wysiwyg.init.js
--- wysiwyg.init.js	30 Nov 2008 17:16:42 -0000	1.1.2.4
+++ wysiwyg.init.js	6 Feb 2009 00:04:55 -0000
@@ -4,7 +4,7 @@ Drupal.behaviors = Drupal.behaviors || {
 
 Drupal.wysiwyg = Drupal.wysiwyg || { 'instances': {} };
 
-Drupal.wysiwyg.editor = Drupal.wysiwyg.editor || { 'init': {}, 'attach': {}, 'detach': {} };
+Drupal.wysiwyg.editor = Drupal.wysiwyg.editor || { 'init': {}, 'attach': {}, 'detach': {}, 'instance': {} };
 
 Drupal.wysiwyg.plugins = Drupal.wysiwyg.plugins || {};
 
Index: wysiwyg.js
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/wysiwyg/wysiwyg.js,v
retrieving revision 1.1.2.10
diff -u -p -u -p -r1.1.2.10 wysiwyg.js
--- wysiwyg.js	4 Feb 2009 02:55:23 -0000	1.1.2.10
+++ wysiwyg.js	6 Feb 2009 00:04:55 -0000
@@ -78,6 +78,14 @@ Drupal.wysiwygAttach = function(context,
     Drupal.wysiwyg.instances[params.field] = {};
     // Provide all input format parameters to editor instance.
     jQuery.extend(Drupal.wysiwyg.instances[params.field], params);
+    // Provide editor callbacks for plugins, if available.
+    if (typeof Drupal.wysiwyg.editor.instance[params.editor] == 'object') {
+      jQuery.extend(Drupal.wysiwyg.instances[params.field], Drupal.wysiwyg.editor.instance[params.editor]);
+    }
+    // Store this field id, so (external) plugins can use it.
+    // @todo Wrong point in time. Probably can only supported by editors which
+    //   support a onFocus() or similar event.
+    Drupal.wysiwyg.activeId = params.field;
     // Attach or update toggle link.
     Drupal.wysiwygAttachToggleLink(context, params);
     // Attach editor, if enabled by default or last state was enabled.
@@ -132,6 +140,7 @@ Drupal.wysiwygAttachToggleLink = functio
       // After disabling the editor, re-attach default behaviors.
       // @todo We HAVE TO invoke Drupal.wysiwygAttach() here.
       Drupal.wysiwyg.editor.attach.none(context, params);
+      Drupal.wysiwyg.instances[params.field] = Drupal.wysiwyg.editor.instance.none;
       Drupal.wysiwyg.instances[params.field].editor = 'none';
       $(this).html(Drupal.settings.wysiwyg.enable).blur();
     }
Index: wysiwyg.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/wysiwyg/wysiwyg.module,v
retrieving revision 1.1.2.22
diff -u -p -u -p -r1.1.2.22 wysiwyg.module
--- wysiwyg.module	1 Feb 2009 10:05:35 -0000	1.1.2.22
+++ wysiwyg.module	6 Feb 2009 00:04:55 -0000
@@ -19,6 +19,12 @@ function wysiwyg_menu($may_cache) {
       'description' => t('Configure client-side editor profiles.'),
       'access' => user_access('administer filters'),
     );
+    $items[] = array(
+      'path' => 'wysiwyg',
+      'callback' => 'wysiwyg_dialog',
+      'access' => user_access('access content'),
+      'type' => MENU_CALLBACK,
+    );
   }
   return $items;
 }
@@ -37,6 +43,11 @@ function wysiwyg_help($section) {
   }
 }
 
+function wysiwyg_dialog($plugin, $instance) {
+  require_once drupal_get_path('module', 'wysiwyg') .'/wysiwyg.dialog.inc';
+  return _wysiwyg_dialog($plugin, $instance);
+}
+
 /**
  * Callback handler for admin pages; menu callback.
  */
@@ -350,6 +361,30 @@ function wysiwyg_add_plugin_settings($pr
   $plugins = $editor['plugin settings callback']($editor, $profile, $plugins);
 
   drupal_add_js(array('wysiwyg' => array('plugins' => array($profile->editor => $plugins))), 'setting');
+
+  if (!(isset($editor['proxy plugin']) && isset($editor['proxy plugin settings callback']) && function_exists($editor['proxy plugin settings callback']))) {
+    return;
+  }
+  // Collect, load, and add API plugins provided by Drupal modules.
+  $plugins += $editor['proxy plugin'];
+  $proxy = key($editor['proxy plugin']);
+  $proxy_plugins = array();
+  foreach (wysiwyg_get_all_plugins() as $plugin_name => $meta) {
+    if (isset($profile->settings['buttons'][$proxy][$plugin_name])) {
+      $proxy_plugins[$plugin_name] = $meta;
+      // Load the Drupal plugin's JavaScript.
+      drupal_add_js($meta['js path'] .'/'. $meta['js file']);
+      // Add plugin specific settings.
+      if (isset($meta['settings'])) {
+        drupal_add_js(array('wysiwyg' => array('plugins' => array('drupal' => array($plugin_name => $meta['settings'])))), 'setting');
+      }
+    }
+  }
+  // Invoke the editor's proxy plugin settings callback, so it can populate
+  // the settings for Drupal plugins with custom, required values.
+  $proxy_plugins = $editor['proxy plugin settings callback']($editor, $profile, $proxy_plugins);
+
+  drupal_add_js(array('wysiwyg' => array('plugins' => array($profile->editor => array('drupal' => $proxy_plugins)))), 'setting');
 }
 
 /**
@@ -408,11 +443,17 @@ function wysiwyg_get_plugins($editor_nam
     if (isset($editor['plugin callback']) && function_exists($editor['plugin callback'])) {
       $plugins = $editor['plugin callback']($editor);
     }
-    // Load our own plugins.
-    include_once drupal_get_path('module', 'wysiwyg') .'/wysiwyg.plugins.inc';
-  
     // Add editor plugins provided via hook_wysiwyg_plugin().
     $plugins = array_merge($plugins, module_invoke_all('wysiwyg_plugin', $editor['name'], $editor['installed version']));
+    // Add API plugins provided by Drupal modules.
+    // @todo We need to pass the filepath to the plugin icon for Drupal plugins.
+    if (isset($editor['proxy plugin'])) {
+      $plugins += $editor['proxy plugin'];
+      $proxy = key($editor['proxy plugin']);
+      foreach (wysiwyg_get_all_plugins() as $plugin_name => $info) {
+        $plugins[$proxy]['buttons'][$plugin_name] = $info['title'];
+      }
+    }
   }
   return $plugins;
 }
@@ -602,6 +643,46 @@ function wysiwyg_get_all_editors() {
 }
 
 /**
+ * Invoke hook_wysiwyg_plugin() in all modules.
+ */
+function wysiwyg_get_all_plugins() {
+  static $plugins;
+
+  if (isset($plugins)) {
+    return $plugins;
+  }
+
+  $plugins = wysiwyg_load_includes('plugins', 'plugin');
+  foreach ($plugins as $name => $properties) {
+    $plugin = &$plugins[$name];
+    // Fill in required/default properties.
+    $plugin += array(
+      'title' => $plugin['name'],
+      'vendor url' => '',
+      'js path' => $plugin['path'] . '/' . $plugin['name'],
+      'js file' => $plugin['name'] . '.js',
+      'css path' => $plugin['path'] . '/' . $plugin['name'],
+      'css file' => $plugin['name'] . '.css',
+      'icon path' => $plugin['path'] . '/' . $plugin['name'] . '/images',
+      'icon file' => $plugin['name'] . '.png',
+      'dialog path' => $plugin['name'],
+      'dialog settings' => array(),
+      'settings callback' => NULL,
+      'settings form callback' => NULL,
+    );
+    // Fill in default settings.
+    $plugin['settings'] += array(
+      'path' => base_path() . $plugin['path'] . '/' . $plugin['name'],
+    );
+    // Check whether library is present.
+    if (!($plugin['installed'] = file_exists($plugin['js path'] . '/' . $plugin['js file']))) {
+      continue;
+    }
+  }
+  return $plugins;
+}
+
+/**
  * Load include files for wysiwyg implemented by all modules.
  *
  * @param $type
Index: editors/tinymce.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/wysiwyg/editors/tinymce.inc,v
retrieving revision 1.1.2.22
diff -u -p -u -p -r1.1.2.22 tinymce.inc
--- editors/tinymce.inc	1 Feb 2009 05:58:39 -0000	1.1.2.22
+++ editors/tinymce.inc	6 Feb 2009 00:04:55 -0000
@@ -33,6 +33,13 @@ function wysiwyg_tinymce_editor() {
     'settings callback' => 'wysiwyg_tinymce_settings',
     'plugin callback' => 'wysiwyg_tinymce_plugins',
     'plugin settings callback' => 'wysiwyg_tinymce_plugin_settings',
+    'proxy plugin' => array(
+      'drupal' => array(
+        'load' => TRUE,
+        'proxy' => TRUE,
+      ),
+    ),
+    'proxy plugin settings callback' => 'wysiwyg_tinymce_proxy_plugin_settings',
     'versions' => array( // Each version can override global editor properties.
       '2.1' => array(
         // 'include files' => array('tinymce-2.inc'),
@@ -183,8 +190,12 @@ function wysiwyg_tinymce_settings($edito
           if ($type == 'buttons') {
             $init['buttons'][] = $button;
           }
+          // Add external Drupal plugins to the list of extensions.
+          if ($type == 'buttons' && !empty($plugins[$plugin]['proxy'])) {
+            $init['extensions'][_wysiwyg_tinymce_plugin_name('add', $button)] = 1;
+          }
           // Add external plugins to the list of extensions.
-          if ($type == 'buttons' && empty($plugins[$plugin]['internal'])) {
+          else if ($type == 'buttons' && empty($plugins[$plugin]['internal'])) {
             $init['extensions'][_wysiwyg_tinymce_plugin_name('add', $plugin)] = 1;
           }
           // Add internal buttons that also need to be loaded as extension.
@@ -291,10 +302,10 @@ function wysiwyg_tinymce_themes($editor,
 }
 
 /**
- * Build a JS settings array of external plugins that need to be loaded separately.
+ * Build a JS settings array of native external plugins that need to be loaded separately.
  *
  * TinyMCE requires that external plugins (i.e. not residing in the editor's
- * directory) are loaded (once) after the editor has been initialized.
+ * directory) are loaded (once) upon initializing the editor.
  */
 function wysiwyg_tinymce_plugin_settings($editor, $profile, $plugins) {
   $settings = array();
@@ -307,6 +318,24 @@ function wysiwyg_tinymce_plugin_settings
 }
 
 /**
+ * Build a JS settings array for Drupal plugins loaded via the proxy plugin.
+ */
+function wysiwyg_tinymce_proxy_plugin_settings($editor, $profile, $plugins) {
+  $settings = array();
+  foreach ($plugins as $name => $plugin) {
+    // Populate required plugin settings.
+    $settings[$name] = $plugin['dialog settings'] + array(
+      'title' => $plugin['title'],
+      'icon' => base_path() . $plugin['icon path'] .'/'. $plugin['icon file'],
+      'iconTitle' => $plugin['icon title'],
+      // @todo These should only be set if the plugin defined them.
+      'css' => base_path() . $plugin['css path'] .'/'. $plugin['css file'],
+    );
+  }
+  return $settings;
+}
+
+/**
  * Add or remove leading hiven to/of external plugin names.
  *
  * TinyMCE requires that external plugins, which should not be loaded from
Index: editors/js/none.js
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/wysiwyg/editors/js/none.js,v
retrieving revision 1.1.2.4
diff -u -p -u -p -r1.1.2.4 none.js
--- editors/js/none.js	28 Oct 2008 22:46:21 -0000	1.1.2.4
+++ editors/js/none.js	6 Feb 2009 00:04:55 -0000
@@ -43,3 +43,28 @@ Drupal.wysiwyg.editor.detach.none = func
   }
 };
 
+/**
+ * Instance methods for plain text areas.
+ */
+Drupal.wysiwyg.editor.instance.none = {
+  insert: function(content) {
+    var editor = document.getElementById(this.field);
+
+    // IE support.
+    if (document.selection) {
+      editor.focus();
+      sel = document.selection.createRange();
+      sel.text = content;
+    }
+    // Mozilla/Firefox/Netscape 7+ support.
+    else if (editor.selectionStart || editor.selectionStart == '0') {
+      var startPos = editor.selectionStart;
+      var endPos = editor.selectionEnd;
+      editor.value = editor.value.substring(0, startPos) + content + editor.value.substring(endPos, editor.value.length);
+    }
+    // Fallback, just add to the end of the content.
+    else {
+      editor.value += content;
+    }
+  }
+};
Index: editors/js/tinymce-2.js
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/wysiwyg/editors/js/tinymce-2.js,v
retrieving revision 1.1.2.8
diff -u -p -u -p -r1.1.2.8 tinymce-2.js
--- editors/js/tinymce-2.js	1 Dec 2008 14:14:58 -0000	1.1.2.8
+++ editors/js/tinymce-2.js	6 Feb 2009 00:04:55 -0000
@@ -21,8 +21,13 @@ Drupal.wysiwyg.editor.init.tinymce = fun
   for (var format in settings) {
     tinyMCE.init(settings[format]);
   }
-  for (var plugin in Drupal.settings.wysiwyg.plugins.tinymce) {
-    tinyMCE.loadPlugin(plugin, Drupal.settings.wysiwyg.plugins.tinymce[plugin]);
+  // Load native external plugins.
+  for (var plugin in Drupal.settings.wysiwyg.plugins.tinymce.native) {
+    tinyMCE.loadPlugin(plugin, Drupal.settings.wysiwyg.plugins.tinymce.native[plugin]);
+  }
+  // Load Drupal plugins.
+  for (var plugin in Drupal.settings.wysiwyg.plugins.tinymce.drupal) {
+    Drupal.wysiwyg.editor.instance.tinymce.addPlugin(plugin, Drupal.settings.wysiwyg.plugins.tinymce.drupal[plugin], Drupal.settings.wysiwyg.plugins.drupal[plugin]);
   }
 };
 
@@ -56,3 +61,135 @@ Drupal.wysiwyg.editor.detach.tinymce = f
 //  }
 };
 
+Drupal.wysiwyg.editor.instance.tinymce = {
+  addPlugin: function(plugin, settings, pluginSettings) {
+    if (typeof Drupal.wysiwyg.plugins[plugin] != 'object') {
+      return;
+    }
+    tinyMCE.addPlugin(plugin, {
+
+      // Register an editor command for this plugin, invoked by the plugin's button.
+      execCommand: function(editor_id, element, command, user_interface, value) {
+        switch (command) {
+          case plugin:
+            if (typeof Drupal.wysiwyg.plugins[plugin].invoke == 'function') {
+              var ed = tinyMCE.getInstanceById(editor_id);
+              var data = { format: 'html', node: ed.getFocusElement(), content: ed.getFocusElement() };
+              Drupal.wysiwyg.plugins[plugin].invoke(data, pluginSettings, ed.formTargetElementId);
+              return true;
+            }
+        }
+        // Pass to next handler in chain.
+        return false;
+      },
+
+      // Register the plugin button.
+      getControlHTML: function(control_name) {
+        switch (control_name) {
+          case plugin:
+            return tinyMCE.getButtonHTML(control_name, settings.iconTitle, settings.icon, plugin);
+        }
+        return '';
+      },
+
+      // Load custom CSS for editor contents on startup.
+      initInstance: function(ed) {
+        if (settings.css) {
+          tinyMCE.importCSS(ed.getDoc(), settings.css);
+        }
+      },
+
+      cleanup: function(type, content) {
+        switch (type) {
+          case 'insert_to_editor':
+            // Attach: Replace plain text with HTML representations.
+            if (typeof Drupal.wysiwyg.plugins[plugin].attach == 'function') {
+              content = Drupal.wysiwyg.plugins[plugin].attach(content, pluginSettings, tinyMCE.selectedInstance.editorId);
+              content = Drupal.wysiwyg.editor.instance.tinymce.prepareContent(content);
+            }
+            break;
+
+          case 'get_from_editor':
+            // Detach: Replace HTML representations with plain text.
+            if (typeof Drupal.wysiwyg.plugins[plugin].detach == 'function') {
+              content = Drupal.wysiwyg.plugins[plugin].detach(content, pluginSettings, tinyMCE.selectedInstance.editorId);
+            }
+            break;
+        }
+        // Pass through to next handler in chain
+        return content;
+      },
+
+      // isNode: Return whether the plugin button should be enabled for the
+      // current selection.
+      handleNodeChange: function(editor_id, node, undo_index, undo_levels, visual_aid, any_selection) {
+        if (node == null) {
+          return;
+        }
+        if (typeof Drupal.wysiwyg.plugins[plugin].isNode == 'function') {
+          if (Drupal.wysiwyg.plugins[plugin].isNode(node)) {
+            tinyMCE.switchClass(editor_id + '_' + plugin, 'mceButtonSelected');
+            return true;
+          }
+        }
+        tinyMCE.switchClass(editor_id + '_' + plugin, 'mceButtonNormal');
+        return true;
+      },
+
+      /**
+       * Return information about the plugin as a name/value array.
+       */
+      getInfo: function() {
+        return {
+          longname: settings.title
+        };
+      }
+    });
+  },
+
+  openDialog: function(dialog, params) {
+    var editor = tinyMCE.getInstanceById(this.field);
+    tinyMCE.openWindow({
+      file: dialog.url + '/' + this.field,
+      width: dialog.width,
+      height: dialog.height,
+      inline: 1
+    }, params);
+  },
+
+  closeDialog: function(dialog) {
+    var editor = tinyMCE.getInstanceById(this.field);
+    tinyMCEPopup.close();
+  },
+
+  prepareContent: function(content) {
+    // Certain content elements need to have additional DOM properties applied
+    // to prevent this editor from highlighting an internal button in addition
+    // to the button of a Drupal plugin.
+    var specialProperties = {
+      img: { name: 'mce_drupal' }
+    };
+    $content = $('<div>' + content + '</div>'); // No .outerHTML() in jQuery :(
+    jQuery.each(specialProperties, function(element, properties) {
+      $content.find(element).each(function() {
+        for (var property in properties) {
+          if (property == 'class') {
+            $(this).addClass(properties[property]);
+          }
+          else {
+            $(this).attr(property, properties[property]);
+          }
+        }
+      });
+    });
+    return $content.html();
+  },
+
+  insert: function(content) {
+    content = this.prepareContent(content);
+    var editor = tinyMCE.getInstanceById(this.field);
+    editor.execCommand('mceInsertContent', false, content);
+    editor.repaint();
+  }
+};
+
Index: editors/js/tinymce-3.js
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/wysiwyg/editors/js/tinymce-3.js,v
retrieving revision 1.1.2.13
diff -u -p -u -p -r1.1.2.13 tinymce-3.js
--- editors/js/tinymce-3.js	4 Feb 2009 02:55:23 -0000	1.1.2.13
+++ editors/js/tinymce-3.js	6 Feb 2009 00:04:55 -0000
@@ -22,8 +22,13 @@ Drupal.wysiwyg.editor.init.tinymce = fun
   for (var format in settings) {
     tinyMCE.init(settings[format]);
   }
-  for (var plugin in Drupal.settings.wysiwyg.plugins.tinymce) {
-    tinymce.PluginManager.load(plugin, Drupal.settings.wysiwyg.plugins.tinymce[plugin]);
+  // Load native external plugins.
+  for (var plugin in Drupal.settings.wysiwyg.plugins.tinymce.native) {
+    tinymce.PluginManager.load(plugin, Drupal.settings.wysiwyg.plugins.tinymce.native[plugin]);
+  }
+  // Load Drupal plugins.
+  for (var plugin in Drupal.settings.wysiwyg.plugins.tinymce.drupal) {
+    Drupal.wysiwyg.editor.instance.tinymce.addPlugin(plugin, Drupal.settings.wysiwyg.plugins.tinymce.drupal[plugin], Drupal.settings.wysiwyg.plugins.drupal[plugin]);
   }
 };
 
@@ -62,3 +67,122 @@ Drupal.wysiwyg.editor.detach.tinymce = f
   }
 };
 
+Drupal.wysiwyg.editor.instance.tinymce = {
+  addPlugin: function(plugin, settings, pluginSettings) {
+    if (typeof Drupal.wysiwyg.plugins[plugin] != 'object') {
+      return;
+    }
+    tinymce.create('tinymce.plugins.' + plugin, {
+      /**
+       * Initialize the plugin, executed after the plugin has been created.
+       *
+       * @param ed
+       *   The tinymce.Editor instance the plugin is initialized in.
+       * @param url
+       *   The absolute URL of the plugin location.
+       */
+      init: function(ed, url) {
+        // Register an editor command for this plugin, invoked by the plugin's button.
+        ed.addCommand(plugin, function() {
+          if (typeof Drupal.wysiwyg.plugins[plugin].invoke == 'function') {
+            var data = { format: 'html', node: ed.selection.getNode(), content: ed.selection.getContent() };
+            Drupal.wysiwyg.plugins[plugin].invoke(data, pluginSettings, ed.id);
+          }
+        });
+
+        // Register the plugin button.
+        ed.addButton(plugin, {
+          title : settings.iconTitle,
+          cmd : plugin,
+          image : settings.icon
+        });
+
+        // Load custom CSS for editor contents on startup.
+        ed.onInit.add(function() {
+          if (settings.css) {
+            ed.dom.loadCSS(settings.css);
+          }
+        });
+
+        // Attach: Replace plain text with HTML representations.
+        ed.onBeforeSetContent.add(function(ed, data) {
+          if (typeof Drupal.wysiwyg.plugins[plugin].attach == 'function') {
+            data.content = Drupal.wysiwyg.plugins[plugin].attach(data.content, pluginSettings, ed.id);
+            data.content = Drupal.wysiwyg.editor.instance.tinymce.prepareContent(data.content);
+          }
+        });
+
+        // Detach: Replace HTML representations with plain text.
+        ed.onGetContent.add(function(ed, data) {
+          if (typeof Drupal.wysiwyg.plugins[plugin].detach == 'function') {
+            data.content = Drupal.wysiwyg.plugins[plugin].detach(data.content, pluginSettings, ed.id);
+          }
+        });
+
+        // isNode: Return whether the plugin button should be enabled for the
+        // current selection.
+        ed.onNodeChange.add(function(ed, command, node) {
+          if (typeof Drupal.wysiwyg.plugins[plugin].isNode == 'function') {
+            command.setActive(plugin, Drupal.wysiwyg.plugins[plugin].isNode(node));
+          }
+        });
+      },
+
+      /**
+       * Return information about the plugin as a name/value array.
+       */
+      getInfo: function() {
+        return {
+          longname: settings.title
+        };
+      }
+    });
+
+    // Register plugin.
+    tinymce.PluginManager.add(plugin, tinymce.plugins[plugin]);
+  },
+
+  openDialog: function(dialog, params) {
+    var editor = tinyMCE.get(this.field);
+    editor.windowManager.open({
+      file: dialog.url + '/' + this.field,
+      width: dialog.width,
+      height: dialog.height,
+      inline: 1
+    }, params);
+  },
+
+  closeDialog: function(dialog) {
+    var editor = tinyMCE.get(this.field);
+    editor.windowManager.close(dialog);
+  },
+
+  prepareContent: function(content) {
+    // Certain content elements need to have additional DOM properties applied
+    // to prevent this editor from highlighting an internal button in addition
+    // to the button of a Drupal plugin.
+    var specialProperties = {
+      img: { class: 'mceItem' }
+    };
+    $content = $('<div>' + content + '</div>'); // No .outerHTML() in jQuery :(
+    jQuery.each(specialProperties, function(element, properties) {
+      $content.find(element).each(function() {
+        for (var property in properties) {
+          if (property == 'class') {
+            $(this).addClass(properties[property]);
+          }
+          else {
+            $(this).attr(property, properties[property]);
+          }
+        }
+      });
+    });
+    return $content.html();
+  },
+
+  insert: function(content) {
+    content = this.prepareContent(content);
+    tinyMCE.execInstanceCommand(this.field, 'mceInsertContent', false, content);
+  }
+};
+
Index: plugins/break.inc
===================================================================
RCS file: plugins/break.inc
diff -N plugins/break.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ plugins/break.inc	6 Feb 2009 00:04:55 -0000
@@ -0,0 +1,22 @@
+<?php
+// $Id: break.inc,v 1.1.2.7 2008/10/05 04:05:04 sun Exp $
+
+
+/**
+ * Implementation of hook_wysiwyg_plugin().
+ */
+function wysiwyg_break_plugin() {
+  $plugins = array();
+  $plugins['break'] = array(
+    'title' => t('Teaser break'),
+    'vendor url' => 'http://drupal.org/project/wysiwyg',
+    'icon file' => 'break.gif',
+    'icon title' => t('Separate the teaser and body of this content'),
+    'settings' => array(
+      // Path is set by default.
+      // 'path' => wysiwyg_get_path('plugins/break', TRUE),
+    ),
+  );
+  return $plugins;
+}
+
Index: plugins/break/break.js
===================================================================
RCS file: plugins/break/break.js
diff -N plugins/break/break.js
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ plugins/break/break.js	6 Feb 2009 00:04:55 -0000
@@ -0,0 +1,59 @@
+// $Id: editor_plugin.js 201 2008-02-12 15:56:56Z sun $
+
+Drupal.wysiwyg.plugins.break = {
+
+  /**
+   * Return whether the passed node belongs to this plugin.
+   */
+  isNode: function(node) {
+    return ($(node).is('img.wysiwyg-break'));
+  },
+
+  /**
+   * Execute the button.
+   */
+  invoke: function(data, settings, instanceId) {
+    if (data.format == 'html') {
+      // Prevent duplicating a teaser break.
+      if ($(data.node).is('img.wysiwyg-break')) {
+        return;
+      }
+      var content = this._getPlaceholder(settings);
+    }
+    else {
+      // Prevent duplicating a teaser break.
+      // @todo data.content is the selection only; needs access to complete content.
+      if (data.content.match(/<!--break-->/)) {
+        return;
+      }
+      var content = '<!--break-->';
+    }
+    if (typeof content != 'undefined') {
+      Drupal.wysiwyg.instances[instanceId].insert(content);
+    }
+  },
+
+  /**
+   * Replace all <!--break--> tags with images.
+   */
+  attach: function(content, settings, instanceId) {
+    content = content.replace(/<!--break-->/g, this._getPlaceholder(settings));
+    return content;
+  },
+
+  /**
+   * Replace images with <!--break--> tags in content upon detaching editor.
+   */
+  detach: function(content, settings, instanceId) {
+    $content = $('<div>' + content + '</div>'); // No .outerHTML() in jQuery :(
+    $('img.wysiwyg-break', $content).replaceWith('<!--break-->');
+    return $content.html();
+  },
+
+  /**
+   * Helper function to return a HTML placeholder.
+   */
+  _getPlaceholder: function (settings) {
+    return '<img src="' + settings.path + '/images/spacer.gif" alt="&lt;--break-&gt;" title="&lt;--break--&gt;" class="wysiwyg-break" />';
+  }
+};
Index: plugins/break/images/spacer.gif
===================================================================
RCS file: plugins/break/images/spacer.gif
diff -N plugins/break/images/spacer.gif
Binary files /dev/null and spacer.gif differ
